diff options
Diffstat (limited to 'debian/missing-sources/epoch/src/core')
-rw-r--r-- | debian/missing-sources/epoch/src/core/chart.coffee | 361 | ||||
-rw-r--r-- | debian/missing-sources/epoch/src/core/context.coffee | 25 | ||||
-rw-r--r-- | debian/missing-sources/epoch/src/core/css.coffee | 131 | ||||
-rw-r--r-- | debian/missing-sources/epoch/src/core/d3.coffee | 25 | ||||
-rw-r--r-- | debian/missing-sources/epoch/src/core/format.coffee | 15 | ||||
-rw-r--r-- | debian/missing-sources/epoch/src/core/util.coffee | 236 |
6 files changed, 793 insertions, 0 deletions
diff --git a/debian/missing-sources/epoch/src/core/chart.coffee b/debian/missing-sources/epoch/src/core/chart.coffee new file mode 100644 index 0000000..068335a --- /dev/null +++ b/debian/missing-sources/epoch/src/core/chart.coffee @@ -0,0 +1,361 @@ +# The base class for all charts in Epoch. Defines chart dimensions, keeps a reference +# of the chart's containing elements. And defines core method for handling data and +# drawing. +class Epoch.Chart.Base extends Epoch.Events + defaults = + width: 320 + height: 240 + dataFormat: null + + optionListeners = + 'option:width': 'dimensionsChanged' + 'option:height': 'dimensionsChanged' + 'layer:shown': 'layerChanged' + 'layer:hidden': 'layerChanged' + + # Creates a new base chart. + # @param [Object] options Options to set for this chart. + # @option options [Integer] width Sets an explicit width for the visualization. + # @option options [Integer] height Sets an explicit height for the visualization. + # @option options [Object, String] dataFormat Specific data format for the chart. + # @option options [Object] model Data model for the chart. + constructor: (@options={}) -> + super() + + if @options.model + if @options.model.hasData()? + @setData(@options.model.getData(@options.type, @options.dataFormat)) + else + @setData(@options.data or []) + @options.model.on 'data:updated', => @setDataFromModel() + else + @setData(@options.data or []) + + if @options.el? + @el = d3.select(@options.el) + + @width = @options.width + @height = @options.height + + if @el? + @width = @el.width() unless @width? + @height = @el.height() unless @height? + else + @width = defaults.width unless @width? + @height = defaults.height unless @height? + @el = d3.select(document.createElement('DIV')) + .attr('width', @width) + .attr('height', @height) + + @onAll optionListeners + + # @return [Object] A copy of this charts options. + _getAllOptions: -> + Epoch.Util.defaults({}, @options) + + # Chart option accessor. + # @param key Name of the option to fetch. Can be hierarchical, e.g. 'margins.left' + # @return The requested option if found, undefined otherwise. + _getOption: (key) -> + parts = key.split('.') + scope = @options + while parts.length and scope? + subkey = parts.shift() + scope = scope[subkey] + scope + + # Chart option mutator. + # @param key Name of the option to fetch. Can be hierarchical, e.g. 'margins.top' + # @param value Value to set for the option. + # @event option:`key` Triggers an option event with the given key being set. + _setOption: (key, value) -> + parts = key.split('.') + scope = @options + while parts.length + subkey = parts.shift() + if parts.length == 0 + scope[subkey] = arguments[1] + @trigger "option:#{arguments[0]}" + return + unless scope[subkey]? + scope[subkey] = {} + scope = scope[subkey] + + # Sets all options given an object of mixed hierarchical keys and nested objects. + # @param [Object] options Options to set. + # @event option:* Triggers an option event for each key that was set + _setManyOptions: (options, prefix='') -> + for own key, value of options + if Epoch.isObject(value) + @_setManyOptions value, "#{prefix + key}." + else + @_setOption prefix + key, value + + # General accessor / mutator for chart options. + # + # @overload option() + # Fetches chart options. + # @return a copy of this chart's options. + # + # @overload option(name) + # Fetches the value the option with the given name. + # @param [String] name Name of the option to fetch. Can be hierarchical, e.g. <code>'margins.left'</code> + # @return The requested option if found, <code>undefined</code> otherwise. + # + # @overload option(name, value) + # Sets an option and triggers the associated event. + # @param [String] name Name of the option to fetch. Can be hierarchical, e.g. 'margins.top' + # @param value Value to set for the option. + # @event option:`name` Triggers an option event with the given key being set. + # + # @overload option(options) + # Sets multiple options at once. + # @param [Object] options Options to set for the chart. + # @event option:* Triggers an option event for each key that was set. + option: -> + if arguments.length == 0 + @_getAllOptions() + else if arguments.length == 1 and Epoch.isString(arguments[0]) + @_getOption arguments[0] + else if arguments.length == 2 and Epoch.isString(arguments[0]) + @_setOption arguments[0], arguments[1] + else if arguments.length == 1 and Epoch.isObject(arguments[0]) + @_setManyOptions arguments[0] + + # Retrieves and sets data from the chart's model + setDataFromModel: -> + prepared = @_prepareData @options.model.getData(@options.type, @options.dataFormat) + @data = @_annotateLayers(prepared) + @draw() + + # Set the initial data for the chart. + # @param data Data to initially set for the given chart. The data format can vary + # from chart to chart. The base class assumes that the data provided will be an + # array of layers. + setData: (data, options={}) -> + prepared = @_prepareData (@rawData = @_formatData(data)) + @data = @_annotateLayers(prepared) + + # Performs post formatted data preparation. + # @param data Data to prepare before setting. + # @return The prepared data. + _prepareData: (data) -> data + + # Performs data formatting before setting the charts data + # @param data Data to be formatted. + # @return The chart specific formatted data. + _formatData: (data) -> + Epoch.Data.formatData(data, @options.type, @options.dataFormat) + + # Annotates data to add class names, categories, and initial visibility states + _annotateLayers: (data) -> + category = 1 + for layer in data + classes = ['layer'] + classes.push "category#{category}" + layer.category = category + layer.visible = true + classes.push(Epoch.Util.dasherize layer.label) if layer.label? + layer.className = classes.join(' ') + category++ + return data + + # Finds a layer in the chart's current data that has the given label or index. + # @param [String, Number] labelOrIndex The label or index of the layer to find. + _findLayer: (labelOrIndex) -> + layer = null + if Epoch.isString(labelOrIndex) + for l in @data + if l.label == labelOrIndex + layer = l + break + else if Epoch.isNumber(labelOrIndex) + index = parseInt(labelOrIndex) + layer = @data[index] unless index < 0 or index >= @data.length + return layer + + # Instructs the chart that a data layer should be displayed. + # @param [String, Number] labelOrIndex The label or index of the layer to show. + # @event 'layer:shown' If a layer that was previously hidden now became visible. + showLayer: (labelOrIndex) -> + return unless (layer = @_findLayer labelOrIndex) + return if layer.visible + layer.visible = true + @trigger 'layer:shown' + + # Instructs the chart that a data layer should not be displayed. + # @param [String, Number] labelOrIndex The label or index of the layer to hide. + # @event 'layer:hidden' If a layer that was visible was made hidden. + hideLayer: (labelOrIndex) -> + return unless (layer = @_findLayer labelOrIndex) + return unless layer.visible + layer.visible = false + @trigger 'layer:hidden' + + # Instructs the chart that a data layer's visibility should be toggled. + # @param [String, Number] labelOrIndex The label or index of the layer to toggle. + # @event 'layer:shown' If the layer was made visible + # @event 'layer:hidden' If the layer was made invisible + toggleLayer: (labelOrIndex) -> + return unless (layer = @_findLayer labelOrIndex) + layer.visible = !layer.visible + if layer.visible + @trigger 'layer:shown' + else + @trigger 'layer:hidden' + + # Determines whether or not a data layer is visible. + # @param [String, Number] labelOrIndex The label or index of the layer to toggle. + # @return <code>true</code> if the layer is visible, <code>false</code> otherwise. + isLayerVisible: (labelOrIndex) -> + return null unless (layer = @_findLayer labelOrIndex) + layer.visible + + # Calculates an array of layers in the charts data that are flagged as visible. + # @return [Array] The chart's visible layers. + getVisibleLayers: -> + return @data.filter((layer) -> layer.visible) + + # Updates the chart with new data. + # @param data Data to replace the current data for the chart. + # @param [Boolean] draw Whether or not to redraw the chart after the data has been set. + # Default: true. + update: (data, draw=true) -> + @setData data + @draw() if draw + + # Draws the chart. Triggers the 'draw' event, subclasses should call super() after drawing to + # ensure that the event is triggered. + # @abstract Must be overriden in child classes to perform chart specific drawing. + draw: -> @trigger 'draw' + + # Determines a resulting scale domain for the y axis given a domain + # @param [Mixed] givenDomain A set domain, a label associated with mutliple + # layers, or null. + # @return The domain for the y scale + _getScaleDomain: (givenDomain) -> + # Explicitly set given domain + if Array.isArray(givenDomain) + return givenDomain + + # Check for "labeled" layer ranges + if Epoch.isString(givenDomain) + layers = @getVisibleLayers() + .filter((l) -> l.range == givenDomain) + .map((l) -> l.values) + if layers? && layers.length + values = Epoch.Util.flatten(layers).map((d) -> d.y) + minFn = (memo, curr) -> if curr < memo then curr else memo + maxFn = (memo, curr) -> if curr > memo then curr else memo + return [values.reduce(minFn, values[0]), values.reduce(maxFn, values[0])] + + # Find the domain based on chart options + if Array.isArray(@options.range) + @options.range + else if @options.range && Array.isArray(@options.range.left) + @options.range.left + else if @options.range && Array.isArray(@options.range.right) + @options.range.right + else + @extent((d) -> d.y) + + # Calculates an extent throughout the layers based on the given comparator. + # @param [Function] cmp Comparator to use for performing the min and max for the extent + # calculation. + # @return [Array] an extent array with the first element as the minimum value in the + # chart's data set and the second element as the maximum. + extent: (cmp) -> + [ + d3.min(@getVisibleLayers(), (layer) -> d3.min(layer.values, cmp)), + d3.max(@getVisibleLayers(), (layer) -> d3.max(layer.values, cmp)) + ] + + # Updates the width and height members and container dimensions in response to an + # 'option:width' or 'option:height' event. + dimensionsChanged: -> + @width = @option('width') or @width + @height = @option('height') or @height + @el.width(@width) + @el.height(@height) + + # Updates the chart in response to a layer being shown or hidden + layerChanged: -> + @draw() + +# Base class for all SVG charts (via d3). +class Epoch.Chart.SVG extends Epoch.Chart.Base + # Initializes the chart and places the rendering SVG in the specified HTML + # containing element. + # @param [Object] options Options for the SVG chart. + # @option options [HTMLElement] el Container element for the chart. + # @option options [Array] data Layered data used to render the chart. + constructor: (@options={}) -> + super(@options) + if @el? + @svg = @el.append('svg') + else + @svg = d3.select(document.createElement('svg')) + @svg.attr + xmlns: 'http://www.w3.org/2000/svg', + width: @width, + height: @height + + # Resizes the svg element in response to a 'option:width' or 'option:height' event. + dimensionsChanged: -> + super() + @svg.attr('width', @width).attr('height', @height) + +# Base Class for all Canvas based charts. +class Epoch.Chart.Canvas extends Epoch.Chart.Base + # Initializes the chart and places the rendering canvas in the specified + # HTML container element. + # @param [Object] options Options for the SVG chart. + # @option options [HTMLElement] el Container element for the chart. + # @option options [Array] data Layered data used to render the chart. + constructor: (@options={}) -> + super(@options) + + if @options.pixelRatio? + @pixelRatio = @options.pixelRatio + else if window.devicePixelRatio? + @pixelRatio = window.devicePixelRatio + else + @pixelRatio = 1 + + @canvas = d3.select( document.createElement('CANVAS') ) + @canvas.style + 'width': "#{@width}px" + 'height': "#{@height}px" + + @canvas.attr + width: @getWidth() + height: @getHeight() + + @el.node().appendChild @canvas.node() if @el? + @ctx = Epoch.Util.getContext @canvas.node() + + # @return [Number] width of the canvas with respect to the pixel ratio of the display + getWidth: -> @width * @pixelRatio + + # @return [Number] height of the canvas with respect to the pixel ratio of the display + getHeight: -> @height * @pixelRatio + + # Clears the render canvas. + clear: -> + @ctx.clearRect(0, 0, @getWidth(), @getHeight()) + + # @return [Object] computed styles for the given selector in the context of this chart. + # @param [String] selector The selector used to compute the styles. + getStyles: (selector) -> + Epoch.QueryCSS.getStyles(selector, @el) + + # Resizes the canvas element when the dimensions of the container change + dimensionsChanged: -> + super() + @canvas.style {'width': "#{@width}px", 'height': "#{@height}px"} + @canvas.attr { width: @getWidth(), height: @getHeight() } + + # Purges QueryCSS cache and redraws the Canvas based chart. + redraw: -> + Epoch.QueryCSS.purge() + @draw() diff --git a/debian/missing-sources/epoch/src/core/context.coffee b/debian/missing-sources/epoch/src/core/context.coffee new file mode 100644 index 0000000..8566552 --- /dev/null +++ b/debian/missing-sources/epoch/src/core/context.coffee @@ -0,0 +1,25 @@ +# Rendering context used for unit testing. +class Epoch.TestContext + VOID_METHODS = [ + 'arc', 'arcTo', 'beginPath', 'bezierCurveTo', 'clearRect', + 'clip', 'closePath', 'drawImage', 'fill', 'fillRect', 'fillText', + 'moveTo', 'quadraticCurveTo', 'rect', 'restore', 'rotate', 'save', + 'scale', 'scrollPathIntoView', 'setLineDash', 'setTransform', + 'stroke', 'strokeRect', 'strokeText', 'transform', 'translate', 'lineTo' + ] + + # Creates a new test rendering context. + constructor: -> + @_log = [] + @_makeFauxMethod(method) for method in VOID_METHODS + + # Creates a fake method with the given name that logs the method called + # and arguments passed when executed. + # @param name Name of the fake method to create. + _makeFauxMethod: (name) -> + @[name] = -> @_log.push "#{name}(#{(arg.toString() for arg in arguments).join(',')})" + + # Faux method that emulates the "getImageData" method + getImageData: -> + @_log.push "getImageData(#{(arg.toString() for arg in arguments).join(',')})" + return { width: 0, height: 0, resolution: 1.0, data: [] } diff --git a/debian/missing-sources/epoch/src/core/css.coffee b/debian/missing-sources/epoch/src/core/css.coffee new file mode 100644 index 0000000..03a7310 --- /dev/null +++ b/debian/missing-sources/epoch/src/core/css.coffee @@ -0,0 +1,131 @@ +# Singelton class used to query CSS styles by way of reference elements. +# This allows canvas based visualizations to use the same styles as their +# SVG counterparts. +class QueryCSS + # Reference container id + REFERENCE_CONTAINER_ID = '_canvas_css_reference' + + # Container Hash Attribute + CONTAINER_HASH_ATTR = 'data-epoch-container-id' + + # Handles automatic container id generation + containerCount = 0 + nextContainerId = -> "epoch-container-#{containerCount++}" + + # Expression used to derive tag name, id, and class names from + # selectors given the the put method. + PUT_EXPR = /^([^#. ]+)?(#[^. ]+)?(\.[^# ]+)?$/ + + # Whether or not to log full selector lists + logging = false + + # Converts selectors into actual dom elements (replaces put.js) + # Limited the functionality to what Epoch actually needs to + # operate correctly. We detect class names, ids, and element + # tag names. + put = (selector) -> + match = selector.match(PUT_EXPR) + return Epoch.error('Query CSS cannot match given selector: ' + selector) unless match? + [whole, tag, id, classNames] = match + tag = (tag ? 'div').toUpperCase() + + element = document.createElement(tag) + element.id = id.substr(1) if id? + if classNames? + element.className = classNames.substr(1).replace(/\./g, ' ') + + return element + + # Lets the user set whether or not to log selector lists and resulting DOM trees. + # Useful for debugging QueryCSS itself. + @log: (b) -> + logging = b + + # Key-Value cache for computed styles that we found using this class. + @cache = {} + + # List of styles to pull from the full list of computed styles + @styleList = ['fill', 'stroke', 'stroke-width'] + + # The svg reference container + @container = null + + # Purges the selector to style cache + @purge: -> + QueryCSS.cache = {} + + # Gets the reference element container. + @getContainer: -> + return QueryCSS.container if QueryCSS.container? + container = document.createElement('DIV') + container.id = REFERENCE_CONTAINER_ID + document.body.appendChild(container) + QueryCSS.container = d3.select(container) + + # @return [String] A unique identifier for the given container and selector. + # @param [String] selector Selector from which to derive the styles + # @param container The containing element for a chart. + @hash: (selector, container) -> + containerId = container.attr(CONTAINER_HASH_ATTR) + unless containerId? + containerId = nextContainerId() + container.attr(CONTAINER_HASH_ATTR, containerId) + return "#{containerId}__#{selector}" + + # @return The computed styles for the given selector in the given container element. + # @param [String] selector Selector from which to derive the styles. + # @param container HTML containing element in which to place the reference SVG. + @getStyles: (selector, container) -> + # 0) Check for cached styles + cacheKey = QueryCSS.hash(selector, container) + cache = QueryCSS.cache[cacheKey] + return cache if cache? + + # 1) Build a full reference tree (parents, container, and selector elements) + parents = [] + parentNode = container.node().parentNode + + while parentNode? and parentNode.nodeName.toLowerCase() != 'body' + parents.unshift parentNode + parentNode = parentNode.parentNode + parents.push container.node() + + selectorList = [] + for element in parents + sel = element.nodeName.toLowerCase() + if element.id? and element.id.length > 0 + sel += '#' + element.id + if element.className? and element.className.length > 0 + sel += '.' + Epoch.Util.trim(element.className).replace(/\s+/g, '.') + selectorList.push sel + + selectorList.push('svg') + + for subSelector in Epoch.Util.trim(selector).split(/\s+/) + selectorList.push(subSelector) + + console.log(selectorList) if logging + + parent = root = put(selectorList.shift()) + while selectorList.length + el = put(selectorList.shift()) + parent.appendChild el + parent = el + + console.log(root) if logging + + # 2) Place the reference tree and fetch styles given the selector + QueryCSS.getContainer().node().appendChild(root) + + ref = d3.select('#' + REFERENCE_CONTAINER_ID + ' ' + selector) + styles = {} + for name in QueryCSS.styleList + styles[name] = ref.style(name) + QueryCSS.cache[cacheKey] = styles + + # 3) Cleanup and return the styles + QueryCSS.getContainer().html('') + return styles + + +Epoch.QueryCSS = QueryCSS
\ No newline at end of file diff --git a/debian/missing-sources/epoch/src/core/d3.coffee b/debian/missing-sources/epoch/src/core/d3.coffee new file mode 100644 index 0000000..a33d717 --- /dev/null +++ b/debian/missing-sources/epoch/src/core/d3.coffee @@ -0,0 +1,25 @@ +# Gets the width of the first node, or sets the width of all the nodes +# in a d3 selection. +# @param value [Number, String] (optional) Width to set for all the nodes in the selection. +# @return The selection if setting the width of the nodes, or the width +# in pixels of the first node in the selection. +d3.selection::width = (value) -> + if value? and Epoch.isString(value) + @style('width', value) + else if value? and Epoch.isNumber(value) + @style('width', "#{value}px") + else + +Epoch.Util.getComputedStyle(@node(), null).width.replace('px', '') + +# Gets the height of the first node, or sets the height of all the nodes +# in a d3 selection. +# @param value (optional) Height to set for all the nodes in the selection. +# @return The selection if setting the height of the nodes, or the height +# in pixels of the first node in the selection. +d3.selection::height = (value) -> + if value? and Epoch.isString(value) + @style('height', value) + else if value? and Epoch.isNumber(value) + @style('height', "#{value}px") + else + +Epoch.Util.getComputedStyle(@node(), null).height.replace('px', '')
\ No newline at end of file diff --git a/debian/missing-sources/epoch/src/core/format.coffee b/debian/missing-sources/epoch/src/core/format.coffee new file mode 100644 index 0000000..d65932f --- /dev/null +++ b/debian/missing-sources/epoch/src/core/format.coffee @@ -0,0 +1,15 @@ +# Tick formatter identity. +Epoch.Formats.regular = (d) -> d + +# Tick formatter that formats the numbers using standard SI postfixes. +Epoch.Formats.si = (d) -> Epoch.Util.formatSI(d) + +# Tick formatter for percentages. +Epoch.Formats.percent = (d) -> (d*100).toFixed(1) + "%" + +# Tick formatter for seconds from timestamp data. +Epoch.Formats.seconds = (t) -> d3Seconds(new Date(t*1000)) +d3Seconds = d3.time.format('%I:%M:%S %p') + +# Tick formatter for bytes +Epoch.Formats.bytes = (d) -> Epoch.Util.formatBytes(d) diff --git a/debian/missing-sources/epoch/src/core/util.coffee b/debian/missing-sources/epoch/src/core/util.coffee new file mode 100644 index 0000000..30995bf --- /dev/null +++ b/debian/missing-sources/epoch/src/core/util.coffee @@ -0,0 +1,236 @@ +typeFunction = (objectName) -> (v) -> + Object::toString.call(v) == "[object #{objectName}]" + +# @return [Boolean] <code>true</code> if the given value is an array, <code>false</code> otherwise. +# @param v Value to test. +Epoch.isArray = Array.isArray ? typeFunction('Array') + +# @return [Boolean] <code>true</code> if the given value is an object, <code>false</code> otherwise. +# @param v Value to test. +Epoch.isObject = typeFunction('Object') + +# @return [Boolean] <code>true</code> if the given value is a string, <code>false</code> otherwise. +# @param v Value to test. +Epoch.isString = typeFunction('String') + +# @return [Boolean] <code>true</code> if the given value is a function, <code>false</code> otherwise. +# @param v Value to test. +Epoch.isFunction = typeFunction('Function') + +# @return [Boolean] <code>true</code> if the given value is a number, <code>false</code> otherwise. +# @param v Value to test. +Epoch.isNumber = typeFunction('Number') + +# Attempts to determine if a given value represents a DOM element. The result is always correct if the +# browser implements DOM Level 2, but one can fool it on certain versions of IE. Adapted from: +# <a href="http://goo.gl/yaD9hV">Stack Overflow #384286</a>. +# @return [Boolean] <code>true</code> if the given value is a DOM element, <code>false</code> otherwise. +# @param v Value to test. +Epoch.isElement = (v) -> + if HTMLElement? + v instanceof HTMLElement + else + v? and Epoch.isObject(v) and v.nodeType == 1 and Epoch.isString(v.nodeName) + +# Determines if a given value is a non-empty array. +# @param v Value to test. +# @return [Boolean] <code>true</code> if the given value is an array with at least one element. +Epoch.isNonEmptyArray = (v) -> + Epoch.isArray(v) and v.length > 0 + +# Generates shallow copy of an object. +# @return A shallow copy of the given object. +# @param [Object] original Object for which to make the shallow copy. +Epoch.Util.copy = (original) -> + return null unless original? + copy = {} + copy[k] = v for own k, v of original + return copy + +# Creates a deep copy of the given options filling in missing defaults. +# @param [Object] options Options to copy. +# @param [Object] defaults Default values for the options. +Epoch.Util.defaults = (options, defaults) -> + result = Epoch.Util.copy(options) + for own k, v of defaults + opt = options[k] + def = defaults[k] + bothAreObjects = Epoch.isObject(opt) and Epoch.isObject(def) + + if opt? and def? + if bothAreObjects and not Epoch.isArray(opt) + result[k] = Epoch.Util.defaults(opt, def) + else + result[k] = opt + else if opt? + result[k] = opt + else + result[k] = def + + return result + +# Formats numbers with standard postfixes (e.g. K, M, G) +# @param [Number] v Value to format. +# @param [Integer] fixed Number of floating point digits to fix after conversion. +# @param [Boolean] fixIntegers Whether or not to add floating point digits to non-floating point results. +# @example Formatting a very large number +# Epoch.Util.formatSI(1120000) == "1.1 M" +Epoch.Util.formatSI = (v, fixed=1, fixIntegers=false) -> + if v < 1000 + q = v + q = q.toFixed(fixed) unless (q|0) == q and !fixIntegers + return q + + for own i, label of ['K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'] + base = Math.pow(10, ((i|0)+1)*3) + if v >= base and v < Math.pow(10, ((i|0)+2)*3) + q = v/base + q = q.toFixed(fixed) unless (q % 1) == 0 and !fixIntegers + return "#{q} #{label}" + +# Formats large bandwidth and disk space usage numbers with byte postfixes (e.g. KB, MB, GB, etc.) +# @param [Number] v Value to format. +# @param [Integer] fixed Number of floating point digits to fix after conversion. +# @param [Boolean] fixIntegers Whether or not to add floating point digits to non-floating point results. +# @example Formatting a large number of bytes +# Epoch.Util.formatBytes(5.21 * Math.pow(2, 20)) == "5.2 MB" +Epoch.Util.formatBytes = (v, fixed=1, fix_integers=false) -> + if v < 1024 + q = v + q = q.toFixed(fixed) unless (q % 1) == 0 and !fix_integers + return "#{q} B" + + for own i, label of ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + base = Math.pow(1024, (i|0)+1) + if v >= base and v < Math.pow(1024, (i|0)+2) + q = v/base + q = q.toFixed(fixed) unless (q % 1) == 0 and !fix_integers + return "#{q} #{label}" + +# @return a "dasherized" css class names from a given string +# @example Using dasherize +# Epoch.Util.dasherize('My Awesome Name') == 'my-awesome-name' +Epoch.Util.dasherize = (str) -> + Epoch.Util.trim(str).replace("\n", '').replace(/\s+/g, '-').toLowerCase() + +# @return the full domain of a given variable from an array of layers +# @param [Array] layers Layered plot data. +# @param [String] key The key name of the value at on each entry in the layers. +Epoch.Util.domain = (layers, key='x') -> + set = {} + domain = [] + for layer in layers + for entry in layer.values + continue if set[entry[key]]? + domain.push(entry[key]) + set[entry[key]] = true + return domain + +# Strips whitespace from the beginning and end of a string. +# @param [String] string String to trim. +# @return [String] The string without leading or trailing whitespace. +# Returns null if the given parameter was not a string. +Epoch.Util.trim = (string) -> + return null unless Epoch.isString(string) + string.replace(/^\s+/g, '').replace(/\s+$/g, '') + +# Returns the computed styles of an element in the document +# @param [HTMLElement] Element for which to fetch the styles. +# @param [String] pseudoElement Pseudo selectors on which to search for the element. +# @return [Object] The styles for the given element. +Epoch.Util.getComputedStyle = (element, pseudoElement) -> + if Epoch.isFunction(window.getComputedStyle) + window.getComputedStyle(element, pseudoElement) + else if element.currentStyle? + element.currentStyle + +# Converts a CSS color string into an RGBA string with the given opacity +# @param [String] color Color string to convert into an rgba +# @param [Number] opacity Opacity to use for the resulting color. +# @return the resulting rgba color string. +Epoch.Util.toRGBA = (color, opacity) -> + if (parts = color.match /^rgba\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*[0-9\.]+\)/) + [all, r, g, b] = parts + result = "rgba(#{r},#{g},#{b},#{opacity})" + else if (v = d3.rgb color) + result = "rgba(#{v.r},#{v.g},#{v.b},#{opacity})" + return result + +# Obtains a graphics context for the given canvas node. Nice to have +# this abstracted out in case we want to support WebGL in the future. +# Also allows us to setup a special context when unit testing, as +# jsdom doesn't have canvas support, and node-canvas is a pain in the +# butt to install properly across different platforms. +Epoch.Util.getContext = (node, type='2d') -> + node.getContext(type) + +# Basic eventing base class for all Epoch classes. +class Epoch.Events + constructor: -> + @_events = {} + + # Registers a callback to a given event. + # @param [String] name Name of the event. + # @param [Function, String] callback Either a closure to call when the event fires + # or a string that denotes a method name to call on this object. + on: (name, callback) -> + return unless callback? + @_events[name] ?= [] + @_events[name].push callback + + # Registers a map of event names to given callbacks. This method calls <code>.on</code> + # directly for each of the events given. + # @param [Object] map A map of event names to callbacks. + onAll: (map) -> + return unless Epoch.isObject(map) + @on(name, callback) for own name, callback of map + + # Removes a specific callback listener or all listeners for a given event. + # @param [String] name Name of the event. + # @param [Function, String] callback (Optional) Callback to remove from the listener list. + # If this parameter is not provided then all listeners will be removed for the event. + off: (name, callback) -> + return unless Epoch.isArray(@_events[name]) + return delete(@_events[name]) unless callback? + while (i = @_events[name].indexOf(callback)) >= 0 + @_events[name].splice(i, 1) + + # Removes a set of callback listeners for all events given in the map or array of strings. + # This method calls <code>.off</code> directly for each event and callback to remove. + # @param [Object, Array] mapOrList Either a map that associates event names to specific callbacks + # or an array of event names for which to completely remove listeners. + offAll: (mapOrList) -> + if Epoch.isArray(mapOrList) + @off(name) for name in mapOrList + else if Epoch.isObject(mapOrList) + @off(name, callback) for own name, callback of mapOrList + + # Triggers an event causing all active listeners to be executed. + # @param [String] name Name of the event to fire. + trigger: (name) -> + return unless @_events[name]? + args = (arguments[i] for i in [1...arguments.length]) + for callback in @_events[name] + fn = null + if Epoch.isString(callback) + fn = @[callback] + else if Epoch.isFunction(callback) + fn = callback + unless fn? + Epoch.exception "Callback for event '#{name}' is not a function or reference to a method." + fn.apply @, args + +# Performs a single pass flatten on a multi-array +# @param [Array] multiarray A deep multi-array to flatten +# @returns [Array] A single pass flatten of the multi-array +Epoch.Util.flatten = (multiarray) -> + if !Array.isArray(multiarray) + throw new Error('Epoch.Util.flatten only accepts arrays') + result = [] + for array in multiarray + if Array.isArray(array) + for item in array + result.push item + else + result.push array + result |