summaryrefslogtreecommitdiffstats
path: root/debian/missing-sources/epoch/src/core
diff options
context:
space:
mode:
Diffstat (limited to 'debian/missing-sources/epoch/src/core')
-rw-r--r--debian/missing-sources/epoch/src/core/chart.coffee361
-rw-r--r--debian/missing-sources/epoch/src/core/context.coffee25
-rw-r--r--debian/missing-sources/epoch/src/core/css.coffee131
-rw-r--r--debian/missing-sources/epoch/src/core/d3.coffee25
-rw-r--r--debian/missing-sources/epoch/src/core/format.coffee15
-rw-r--r--debian/missing-sources/epoch/src/core/util.coffee236
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