summaryrefslogtreecommitdiffstats
path: root/debian/missing-sources/epoch/src
diff options
context:
space:
mode:
Diffstat (limited to 'debian/missing-sources/epoch/src')
-rw-r--r--debian/missing-sources/epoch/src/adapters.coffee13
-rw-r--r--debian/missing-sources/epoch/src/adapters/MooTools.coffee19
-rw-r--r--debian/missing-sources/epoch/src/adapters/jQuery.coffee18
-rw-r--r--debian/missing-sources/epoch/src/adapters/zepto.coffee27
-rw-r--r--debian/missing-sources/epoch/src/basic.coffee243
-rw-r--r--debian/missing-sources/epoch/src/basic/area.coffee51
-rw-r--r--debian/missing-sources/epoch/src/basic/bar.coffee273
-rw-r--r--debian/missing-sources/epoch/src/basic/histogram.coffee61
-rw-r--r--debian/missing-sources/epoch/src/basic/line.coffee46
-rw-r--r--debian/missing-sources/epoch/src/basic/pie.coffee59
-rw-r--r--debian/missing-sources/epoch/src/basic/scatter.coffee59
-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
-rw-r--r--debian/missing-sources/epoch/src/data.coffee311
-rw-r--r--debian/missing-sources/epoch/src/epoch.coffee17
-rw-r--r--debian/missing-sources/epoch/src/model.coffee55
-rw-r--r--debian/missing-sources/epoch/src/time.coffee615
-rw-r--r--debian/missing-sources/epoch/src/time/area.coffee80
-rw-r--r--debian/missing-sources/epoch/src/time/bar.coffee48
-rw-r--r--debian/missing-sources/epoch/src/time/gauge.coffee209
-rw-r--r--debian/missing-sources/epoch/src/time/heatmap.coffee261
-rw-r--r--debian/missing-sources/epoch/src/time/line.coffee39
26 files changed, 3297 insertions, 0 deletions
diff --git a/debian/missing-sources/epoch/src/adapters.coffee b/debian/missing-sources/epoch/src/adapters.coffee
new file mode 100644
index 0000000..e378af5
--- /dev/null
+++ b/debian/missing-sources/epoch/src/adapters.coffee
@@ -0,0 +1,13 @@
+# Maps short string names to classes for library adapters.
+Epoch._typeMap =
+ 'area': Epoch.Chart.Area
+ 'bar': Epoch.Chart.Bar
+ 'line': Epoch.Chart.Line
+ 'pie': Epoch.Chart.Pie
+ 'scatter': Epoch.Chart.Scatter
+ 'histogram': Epoch.Chart.Histogram
+ 'time.area': Epoch.Time.Area
+ 'time.bar': Epoch.Time.Bar
+ 'time.line': Epoch.Time.Line
+ 'time.gauge': Epoch.Time.Gauge
+ 'time.heatmap': Epoch.Time.Heatmap
diff --git a/debian/missing-sources/epoch/src/adapters/MooTools.coffee b/debian/missing-sources/epoch/src/adapters/MooTools.coffee
new file mode 100644
index 0000000..8b3d544
--- /dev/null
+++ b/debian/missing-sources/epoch/src/adapters/MooTools.coffee
@@ -0,0 +1,19 @@
+MooToolsModule = ->
+ # Data key to use for storing a reference to the chart instance on an element.
+ DATA_NAME = 'epoch-chart'
+
+ # Adds an Epoch chart of the given type to the referenced element.
+ # @param [Object] options Options for the chart.
+ # @option options [String] type The type of chart to append to the referenced element.
+ # @return [Object] The chart instance that was associated with the containing element.
+ Element.implement 'epoch', (options) ->
+ self = $$(this)
+ unless (chart = self.retrieve(DATA_NAME)[0])?
+ options.el = this
+ klass = Epoch._typeMap[options.type]
+ unless klass?
+ Epoch.exception "Unknown chart type '#{options.type}'"
+ self.store DATA_NAME, (chart = new klass options)
+ return chart
+
+MooToolsModule() if window.MooTools?
diff --git a/debian/missing-sources/epoch/src/adapters/jQuery.coffee b/debian/missing-sources/epoch/src/adapters/jQuery.coffee
new file mode 100644
index 0000000..e115bad
--- /dev/null
+++ b/debian/missing-sources/epoch/src/adapters/jQuery.coffee
@@ -0,0 +1,18 @@
+jQueryModule = ($) ->
+ # Data key to use for storing a reference to the chart instance on an element.
+ DATA_NAME = 'epoch-chart'
+
+ # Adds an Epoch chart of the given type to the referenced element.
+ # @param [Object] options Options for the chart.
+ # @option options [String] type The type of chart to append to the referenced element.
+ # @return [Object] The chart instance that was associated with the containing element.
+ $.fn.epoch = (options) ->
+ options.el = @get(0)
+ unless (chart = @data(DATA_NAME))?
+ klass = Epoch._typeMap[options.type]
+ unless klass?
+ Epoch.exception "Unknown chart type '#{options.type}'"
+ @data DATA_NAME, (chart = new klass options)
+ return chart
+
+jQueryModule(jQuery) if window.jQuery?
diff --git a/debian/missing-sources/epoch/src/adapters/zepto.coffee b/debian/missing-sources/epoch/src/adapters/zepto.coffee
new file mode 100644
index 0000000..81153dc
--- /dev/null
+++ b/debian/missing-sources/epoch/src/adapters/zepto.coffee
@@ -0,0 +1,27 @@
+zeptoModule = ($) ->
+ # For mapping charts to selected elements
+ DATA_NAME = 'epoch-chart'
+ chartMap = {}
+ chartId = 0
+ next_cid = -> "#{DATA_NAME}-#{++chartId}"
+
+ # Adds an Epoch chart of the given type to the referenced element.
+ # @param [Object] options Options for the chart.
+ # @option options [String] type The type of chart to append to the referenced element.
+ # @return [Object] The chart instance that was associated with the containing element.
+ $.extend $.fn,
+ epoch: (options) ->
+ return chartMap[cid] if (cid = @data(DATA_NAME))?
+ options.el = @get(0)
+
+ klass = Epoch._typeMap[options.type]
+ unless klass?
+ Epoch.exception "Unknown chart type '#{options.type}'"
+
+ @data DATA_NAME, (cid = next_cid())
+ chart = new klass options
+ chartMap[cid] = chart
+
+ return chart
+
+zeptoModule(Zepto) if window.Zepto?
diff --git a/debian/missing-sources/epoch/src/basic.coffee b/debian/missing-sources/epoch/src/basic.coffee
new file mode 100644
index 0000000..8af3465
--- /dev/null
+++ b/debian/missing-sources/epoch/src/basic.coffee
@@ -0,0 +1,243 @@
+# Base class for all two-dimensional basic d3 charts. This class handles axes and
+# margins so that subclasses can focus on the construction of particular chart
+# types.
+class Epoch.Chart.Plot extends Epoch.Chart.SVG
+ defaults =
+ domain: null,
+ range: null,
+ axes: ['left', 'bottom']
+ ticks:
+ top: 14
+ bottom: 14
+ left: 5
+ right: 5
+ tickFormats:
+ top: Epoch.Formats.regular
+ bottom: Epoch.Formats.regular
+ left: Epoch.Formats.si
+ right: Epoch.Formats.si
+
+ defaultAxisMargins =
+ top: 25
+ right: 50
+ bottom: 25
+ left: 50
+
+ optionListeners =
+ 'option:margins.top': 'marginsChanged'
+ 'option:margins.right': 'marginsChanged'
+ 'option:margins.bottom': 'marginsChanged'
+ 'option:margins.left': 'marginsChanged'
+ 'option:axes': 'axesChanged'
+ 'option:ticks.top': 'ticksChanged'
+ 'option:ticks.right': 'ticksChanged'
+ 'option:ticks.bottom': 'ticksChanged'
+ 'option:ticks.left': 'ticksChanged'
+ 'option:tickFormats.top': 'tickFormatsChanged'
+ 'option:tickFormats.right': 'tickFormatsChanged'
+ 'option:tickFormats.bottom': 'tickFormatsChanged'
+ 'option:tickFormats.left': 'tickFormatsChanged'
+ 'option:domain': 'domainChanged'
+ 'option:range': 'rangeChanged'
+
+ # Creates a new plot chart.
+ # @param [Object] options Options to use when constructing the plot.
+ # @option options [Object] margins For setting explicit values for the top,
+ # right, bottom, and left margins in the visualization. Normally these can
+ # be omitted and the class will set appropriately sized margins given which
+ # axes are specified.
+ # @option options [Array] axes A list of axes to display (top, left, bottom, right).
+ # @option options [Object] ticks Number of ticks to place on the top, left bottom
+ # and right axes.
+ # @option options [Object] tickFormats What tick formatting functions to use for
+ # the top, bottom, left, and right axes.
+ constructor: (@options={}) ->
+ givenMargins = Epoch.Util.copy(@options.margins) or {}
+ super(@options = Epoch.Util.defaults(@options, defaults))
+
+ # Margins are used in a special way and only for making room for axes.
+ # However, a user may explicitly set margins in the options, so we need
+ # to determine if they did so, and zero out the ones they didn't if no
+ # axis is present.
+ @margins = {}
+ for pos in ['top', 'right', 'bottom', 'left']
+ @margins[pos] = if @options.margins? and @options.margins[pos]?
+ @options.margins[pos]
+ else if @hasAxis(pos)
+ defaultAxisMargins[pos]
+ else
+ 6
+
+ # Add a translation for the top and left margins
+ @g = @svg.append("g")
+ .attr("transform", "translate(#{@margins.left}, #{@margins.top})")
+
+ # Register option change events
+ @onAll optionListeners
+
+ # Sets the tick formatting function to use on the given axis.
+ # @param [String] axis Name of the axis.
+ # @param [Function] fn Formatting function to use.
+ setTickFormat: (axis, fn) ->
+ @options.tickFormats[axis] = fn
+
+ # @return [Boolean] <code>true</code> if the chart has an axis with a given name, <code>false</code> otherwise.
+ # @param [String] axis Name of axis to check.
+ hasAxis: (axis) ->
+ @options.axes.indexOf(axis) > -1
+
+ # @return [Number] Width of the visualization portion of the chart (width - margins).
+ innerWidth: ->
+ @width - (@margins.left + @margins.right)
+
+ # @return [Number] Height of the visualization portion of the chart (height - margins).
+ innerHeight: ->
+ @height - (@margins.top + @margins.bottom)
+
+ # @return [Function] The x scale for the visualization.
+ x: ->
+ domain = @options.domain ? @extent((d) -> d.x)
+ d3.scale.linear()
+ .domain(domain)
+ .range([0, @innerWidth()])
+
+ # @return [Function] The y scale for the visualization.
+ y: (givenDomain) ->
+ d3.scale.linear()
+ .domain(@_getScaleDomain(givenDomain))
+ .range([@innerHeight(), 0])
+
+ # @return [Function] d3 axis to use for the bottom of the visualization.
+ bottomAxis: ->
+ d3.svg.axis().scale(@x()).orient('bottom')
+ .ticks(@options.ticks.bottom)
+ .tickFormat(@options.tickFormats.bottom)
+
+ # @return [Function] d3 axis to use for the top of the visualization.
+ topAxis: ->
+ d3.svg.axis().scale(@x()).orient('top')
+ .ticks(@options.ticks.top)
+ .tickFormat(@options.tickFormats.top)
+
+ # @return [Function] d3 axis to use on the left of the visualization.
+ leftAxis: ->
+ range = if @options.range then @options.range.left else null
+ d3.svg.axis().scale(@y(range)).orient('left')
+ .ticks(@options.ticks.left)
+ .tickFormat(@options.tickFormats.left)
+
+ # @return [Function] d3 axis to use on the right of the visualization.
+ rightAxis: ->
+ range = if @options.range then @options.range.right else null
+ d3.svg.axis().scale(@y(range)).orient('right')
+ .ticks(@options.ticks.right)
+ .tickFormat(@options.tickFormats.right)
+
+ # Renders the axes for the visualization (subclasses must implement specific
+ # drawing routines).
+ draw: ->
+ if @_axesDrawn
+ @_redrawAxes()
+ else
+ @_drawAxes()
+ super()
+
+ # Redraws the axes for the visualization.
+ _redrawAxes: ->
+ if @hasAxis('bottom')
+ @g.selectAll('.x.axis.bottom').transition()
+ .duration(500)
+ .ease('linear')
+ .call(@bottomAxis())
+ if @hasAxis('top')
+ @g.selectAll('.x.axis.top').transition()
+ .duration(500)
+ .ease('linear')
+ .call(@topAxis())
+ if @hasAxis('left')
+ @g.selectAll('.y.axis.left').transition()
+ .duration(500)
+ .ease('linear')
+ .call(@leftAxis())
+ if @hasAxis('right')
+ @g.selectAll('.y.axis.right').transition()
+ .duration(500)
+ .ease('linear')
+ .call(@rightAxis())
+
+ # Draws the initial axes for the visualization.
+ _drawAxes: ->
+ if @hasAxis('bottom')
+ @g.append("g")
+ .attr("class", "x axis bottom")
+ .attr("transform", "translate(0, #{@innerHeight()})")
+ .call(@bottomAxis())
+ if @hasAxis('top')
+ @g.append("g")
+ .attr('class', 'x axis top')
+ .call(@topAxis())
+ if @hasAxis('left')
+ @g.append("g")
+ .attr("class", "y axis left")
+ .call(@leftAxis())
+ if @hasAxis('right')
+ @g.append('g')
+ .attr('class', 'y axis right')
+ .attr('transform', "translate(#{@innerWidth()}, 0)")
+ .call(@rightAxis())
+ @_axesDrawn = true
+
+ dimensionsChanged: ->
+ super()
+ @g.selectAll('.axis').remove()
+ @_axesDrawn = false
+ @draw()
+
+ # Updates margins in response to a <code>option:margin.*</code> event.
+ marginsChanged: ->
+ return unless @options.margins?
+ for own pos, size of @options.margins
+ unless size?
+ @margins[pos] = 6
+ else
+ @margins[pos] = size
+
+ @g.transition()
+ .duration(750)
+ .attr("transform", "translate(#{@margins.left}, #{@margins.top})")
+
+ @draw()
+
+ # Updates axes in response to a <code>option:axes</code> event.
+ axesChanged: ->
+ # Remove default axis margins
+ for pos in ['top', 'right', 'bottom', 'left']
+ continue if @options.margins? and @options.margins[pos]?
+ if @hasAxis(pos)
+ @margins[pos] = defaultAxisMargins[pos]
+ else
+ @margins[pos] = 6
+
+ # Update the margin offset
+ @g.transition()
+ .duration(750)
+ .attr("transform", "translate(#{@margins.left}, #{@margins.top})")
+
+ # Remove the axes and redraw
+ @g.selectAll('.axis').remove()
+ @_axesDrawn = false
+ @draw()
+
+ # Updates ticks in response to a <code>option:ticks.*</code> event.
+ ticksChanged: -> @draw()
+
+ # Updates tick formats in response to a <code>option:tickFormats.*</code> event.
+ tickFormatsChanged: -> @draw()
+
+ # Updates chart in response to a <code>option:domain</code> event.
+ domainChanged: -> @draw()
+
+ # Updates chart in response to a <code>option:range</code> event.
+ rangeChanged: -> @draw()
+
+# "They will see us waving from such great heights, come down now..." - The Postal Service
diff --git a/debian/missing-sources/epoch/src/basic/area.coffee b/debian/missing-sources/epoch/src/basic/area.coffee
new file mode 100644
index 0000000..e44b480
--- /dev/null
+++ b/debian/missing-sources/epoch/src/basic/area.coffee
@@ -0,0 +1,51 @@
+
+# Static stacked area chart implementation using d3.
+class Epoch.Chart.Area extends Epoch.Chart.Plot
+ constructor: (@options={}) ->
+ @options.type ?= 'area'
+ super(@options)
+ @draw()
+
+ # Generates a scale needed to appropriately render the stacked visualization.
+ # @return [Function] The y scale for the visualization.
+ y: ->
+ a = []
+ for layer in @getVisibleLayers()
+ for own k, v of layer.values
+ a[k] += v.y if a[k]?
+ a[k] = v.y unless a[k]?
+ d3.scale.linear()
+ .domain(@options.range ? [0, d3.max(a)])
+ .range([@height - @margins.top - @margins.bottom, 0])
+
+ # Renders the SVG elements needed to display the stacked area chart.
+ draw: ->
+ [x, y, layers] = [@x(), @y(), @getVisibleLayers()]
+
+ @g.selectAll('.layer').remove()
+ return if layers.length == 0
+
+ area = d3.svg.area()
+ .x((d) -> x(d.x))
+ .y0((d) -> y(d.y0))
+ .y1((d) -> y(d.y0 + d.y))
+
+ stack = d3.layout.stack()
+ .values((d) -> d.values)
+
+ data = stack layers
+
+ layer = @g.selectAll('.layer')
+ .data(layers, (d) -> d.category)
+
+ layer.select('.area')
+ .attr('d', (d) -> area(d.values))
+
+ layer.enter().append('g')
+ .attr('class', (d) -> d.className)
+
+ layer.append('path')
+ .attr('class', 'area')
+ .attr('d', (d) -> area(d.values))
+
+ super()
diff --git a/debian/missing-sources/epoch/src/basic/bar.coffee b/debian/missing-sources/epoch/src/basic/bar.coffee
new file mode 100644
index 0000000..8fbc427
--- /dev/null
+++ b/debian/missing-sources/epoch/src/basic/bar.coffee
@@ -0,0 +1,273 @@
+# Static bar chart implementation (using d3).
+class Epoch.Chart.Bar extends Epoch.Chart.Plot
+ defaults =
+ type: 'bar'
+ style: 'grouped'
+ orientation: 'vertical'
+ padding:
+ bar: 0.08
+ group: 0.1
+ outerPadding:
+ bar: 0.08
+ group: 0.1
+
+ horizontal_specific =
+ tickFormats:
+ top: Epoch.Formats.si
+ bottom: Epoch.Formats.si
+ left: Epoch.Formats.regular
+ right: Epoch.Formats.regular
+
+ horizontal_defaults = Epoch.Util.defaults(horizontal_specific, defaults)
+
+ optionListeners =
+ 'option:orientation': 'orientationChanged'
+ 'option:padding': 'paddingChanged'
+ 'option:outerPadding': 'paddingChanged'
+ 'option:padding:bar': 'paddingChanged'
+ 'option:padding:group': 'paddingChanged'
+ 'option:outerPadding:bar': 'paddingChanged'
+ 'option:outerPadding:group': 'paddingChanged'
+
+ constructor: (@options={}) ->
+ if @_isHorizontal()
+ @options = Epoch.Util.defaults(@options, horizontal_defaults)
+ else
+ @options = Epoch.Util.defaults(@options, defaults)
+ super(@options)
+ @onAll optionListeners
+ @draw()
+
+ # @return [Boolean] True if the chart is vertical, false otherwise
+ _isVertical: ->
+ @options.orientation == 'vertical'
+
+ # @return [Boolean] True if the chart is horizontal, false otherwise
+ _isHorizontal: ->
+ @options.orientation == 'horizontal'
+
+ # @return [Function] The scale used to generate the chart's x scale.
+ x: ->
+ if @_isVertical()
+ d3.scale.ordinal()
+ .domain(Epoch.Util.domain(@getVisibleLayers()))
+ .rangeRoundBands([0, @innerWidth()], @options.padding.group, @options.outerPadding.group)
+ else
+ extent = @extent((d) -> d.y)
+ extent[0] = Math.min(0, extent[0])
+ d3.scale.linear()
+ .domain(extent)
+ .range([0, @width - @margins.left - @margins.right])
+
+ # @return [Function] The x scale used to render the horizontal bar chart.
+ x1: (x0) ->
+ d3.scale.ordinal()
+ .domain((layer.category for layer in @getVisibleLayers()))
+ .rangeRoundBands([0, x0.rangeBand()], @options.padding.bar, @options.outerPadding.bar)
+
+ # @return [Function] The y scale used to render the bar chart.
+ y: ->
+ if @_isVertical()
+ extent = @extent((d) -> d.y)
+ extent[0] = Math.min(0, extent[0])
+ d3.scale.linear()
+ .domain(extent)
+ .range([@height - @margins.top - @margins.bottom, 0])
+ else
+ d3.scale.ordinal()
+ .domain(Epoch.Util.domain(@getVisibleLayers()))
+ .rangeRoundBands([0, @innerHeight()], @options.padding.group, @options.outerPadding.group)
+
+ # @return [Function] The x scale used to render the vertical bar chart.
+ y1: (y0) ->
+ d3.scale.ordinal()
+ .domain((layer.category for layer in @getVisibleLayers()))
+ .rangeRoundBands([0, y0.rangeBand()], @options.padding.bar, @options.outerPadding.bar)
+
+ # Remaps the bar chart data into a form that is easier to display.
+ # @return [Array] The reorganized data.
+ _remapData: ->
+ map = {}
+ for layer in @getVisibleLayers()
+ className = 'bar ' + layer.className.replace(/\s*layer\s*/, '')
+ for entry in layer.values
+ map[entry.x] ?= []
+ map[entry.x].push { label: layer.category, y: entry.y, className: className }
+ ({group: k, values: v} for own k, v of map)
+
+ # Draws the bar char.
+ draw: ->
+ if @_isVertical()
+ @_drawVertical()
+ else
+ @_drawHorizontal()
+ super()
+
+ # Draws the bar chart with a vertical orientation
+ _drawVertical: ->
+ [x0, y] = [@x(), @y()]
+ x1 = @x1(x0)
+ height = @height - @margins.top - @margins.bottom
+ data = @_remapData()
+
+ # 1) Join
+ layer = @g.selectAll(".layer")
+ .data(data, (d) -> d.group)
+
+ # 2) Update
+ layer.transition().duration(750)
+ .attr("transform", (d) -> "translate(#{x0(d.group)}, 0)")
+
+ # 3) Enter / Create
+ layer.enter().append("g")
+ .attr('class', 'layer')
+ .attr("transform", (d) -> "translate(#{x0(d.group)}, 0)")
+
+ rects = layer.selectAll('rect')
+ .data((group) -> group.values)
+
+ rects.attr('class', (d) -> d.className)
+
+ rects.transition().duration(600)
+ .attr('x', (d) -> x1(d.label))
+ .attr('y', (d) -> y(d.y))
+ .attr('width', x1.rangeBand())
+ .attr('height', (d) -> height - y(d.y))
+
+ rects.enter().append('rect')
+ .attr('class', (d) -> d.className)
+ .attr('x', (d) -> x1(d.label))
+ .attr('y', (d) -> y(d.y))
+ .attr('width', x1.rangeBand())
+ .attr('height', (d) -> height - y(d.y))
+
+ rects.exit().transition()
+ .duration(150)
+ .style('opacity', '0')
+ .remove()
+
+ # 4) Update new and existing
+
+ # 5) Exit / Remove
+ layer.exit()
+ .transition()
+ .duration(750)
+ .style('opacity', '0')
+ .remove()
+
+ # Draws the bar chart with a horizontal orientation
+ _drawHorizontal: ->
+ [x, y0] = [@x(), @y()]
+ y1 = @y1(y0)
+ width = @width - @margins.left - @margins.right
+ data = @_remapData()
+
+ # 1) Join
+ layer = @g.selectAll(".layer")
+ .data(data, (d) -> d.group)
+
+ # 2) Update
+ layer.transition().duration(750)
+ .attr("transform", (d) -> "translate(0, #{y0(d.group)})")
+
+ # 3) Enter / Create
+ layer.enter().append("g")
+ .attr('class', 'layer')
+ .attr("transform", (d) -> "translate(0, #{y0(d.group)})")
+
+ rects = layer.selectAll('rect')
+ .data((group) -> group.values)
+
+ rects.attr('class', (d) -> d.className)
+
+ rects.transition().duration(600)
+ .attr('x', (d) -> 0)
+ .attr('y', (d) -> y1(d.label))
+ .attr('height', y1.rangeBand())
+ .attr('width', (d) -> x(d.y))
+
+ rects.enter().append('rect')
+ .attr('class', (d) -> d.className)
+ .attr('x', (d) -> 0)
+ .attr('y', (d) -> y1(d.label))
+ .attr('height', y1.rangeBand())
+ .attr('width', (d) -> x(d.y))
+
+ rects.exit().transition()
+ .duration(150)
+ .style('opacity', '0')
+ .remove()
+
+ # 4) Update new and existing
+
+ # 5) Exit / Remove
+ layer.exit()
+ .transition()
+ .duration(750)
+ .style('opacity', '0')
+ .remove()
+
+ # Generates specific tick marks to emulate d3's linear scale axis ticks
+ # for ordinal scales. Note: this should only be called if the user has
+ # defined a set number of ticks for a given axis.
+ # @param [Number] numTicks Number of ticks to generate
+ # @param [String] dataKey Property name of a datum to use for the tick value
+ # @return [Array] The ticks for the given axis
+ _getTickValues: (numTicks, dataKey='x') ->
+ return [] unless @data[0]?
+ total = @data[0].values.length
+ step = Math.ceil(total / numTicks)|0
+ tickValues = (@data[0].values[i].x for i in [0...total] by step)
+
+ # @return [Function] d3 axis to use for the bottom of the visualization.
+ bottomAxis: ->
+ axis = d3.svg.axis().scale(@x()).orient('bottom')
+ .ticks(@options.ticks.bottom)
+ .tickFormat(@options.tickFormats.bottom)
+ if @_isVertical() and @options.ticks.bottom?
+ axis.tickValues @_getTickValues(@options.ticks.bottom)
+ axis
+
+ # @return [Function] d3 axis to use for the top of the visualization.
+ topAxis: ->
+ axis = d3.svg.axis().scale(@x()).orient('top')
+ .ticks(@options.ticks.top)
+ .tickFormat(@options.tickFormats.top)
+ if @_isVertical() and @options.ticks.top?
+ axis.tickValues @_getTickValues(@options.ticks.top)
+ axis
+
+ # @return [Function] d3 axis to use on the left of the visualization.
+ leftAxis: ->
+ axis = d3.svg.axis().scale(@y()).orient('left')
+ .ticks(@options.ticks.left)
+ .tickFormat(@options.tickFormats.left)
+ if @_isHorizontal() and @options.ticks.left?
+ axis.tickValues @_getTickValues(@options.ticks.left)
+ axis
+
+ # @return [Function] d3 axis to use on the right of the visualization.
+ rightAxis: ->
+ axis = d3.svg.axis().scale(@y()).orient('right')
+ .ticks(@options.ticks.right)
+ .tickFormat(@options.tickFormats.right)
+ if @_isHorizontal() and @options.ticks.right?
+ axis.tickValues @_getTickValues(@options.ticks.right)
+ axis
+
+ # Updates orientation in response <code>option:orientation</code>.
+ orientationChanged: ->
+ top = @options.tickFormats.top
+ bottom = @options.tickFormats.bottom
+ left = @options.tickFormats.left
+ right = @options.tickFormats.right
+
+ @options.tickFormats.left = top
+ @options.tickFormats.right = bottom
+ @options.tickFormats.top = left
+ @options.tickFormats.bottom = right
+
+ @draw()
+
+ # Updates padding in response to <code>option:padding:*</code> and <code>option:outerPadding:*</code>.
+ paddingChanged: -> @draw()
diff --git a/debian/missing-sources/epoch/src/basic/histogram.coffee b/debian/missing-sources/epoch/src/basic/histogram.coffee
new file mode 100644
index 0000000..4548e67
--- /dev/null
+++ b/debian/missing-sources/epoch/src/basic/histogram.coffee
@@ -0,0 +1,61 @@
+class Epoch.Chart.Histogram extends Epoch.Chart.Bar
+ defaults =
+ type: 'histogram'
+ domain: [0, 100]
+ bucketRange: [0, 100]
+ buckets: 10
+ cutOutliers: false
+
+ optionListeners =
+ 'option:bucketRange': 'bucketRangeChanged'
+ 'option:buckets': 'bucketsChanged'
+ 'option:cutOutliers': 'cutOutliersChanged'
+
+ constructor: (@options={}) ->
+ super(@options = Epoch.Util.defaults(@options, defaults))
+ @onAll optionListeners
+ @draw()
+
+ # Prepares data by sorting it into histogram buckets as instructed by the chart options.
+ # @param [Array] data Data to prepare for rendering.
+ # @return [Array] The data prepared to be displayed as a histogram.
+ _prepareData: (data) ->
+ bucketSize = (@options.bucketRange[1] - @options.bucketRange[0]) / @options.buckets
+
+ prepared = []
+ for layer in data
+ buckets = (0 for i in [0...@options.buckets])
+ for point in layer.values
+ index = parseInt((point.x - @options.bucketRange[0]) / bucketSize)
+
+ if @options.cutOutliers and ((index < 0) or (index >= @options.buckets))
+ continue
+ if index < 0
+ index = 0
+ else if index >= @options.buckets
+ index = @options.buckets - 1
+
+ buckets[index] += parseInt point.y
+
+ preparedLayer = { values: (buckets.map (d, i) -> {x: parseInt(i) * bucketSize, y: d}) }
+ for own k, v of layer
+ preparedLayer[k] = v unless k == 'values'
+
+ prepared.push preparedLayer
+
+ return prepared
+
+ # Called when options change, this prepares the raw data for the chart according to the new
+ # options, sets it, and renders the chart.
+ resetData: ->
+ @setData @rawData
+ @draw()
+
+ # Updates the chart in response to an <code>option:bucketRange</code> event.
+ bucketRangeChanged: -> @resetData()
+
+ # Updates the chart in response to an <code>option:buckets</code> event.
+ bucketsChanged: -> @resetData()
+
+ # Updates the chart in response to an <code>option:cutOutliers</code> event.
+ cutOutliersChanged: -> @resetData()
diff --git a/debian/missing-sources/epoch/src/basic/line.coffee b/debian/missing-sources/epoch/src/basic/line.coffee
new file mode 100644
index 0000000..de56780
--- /dev/null
+++ b/debian/missing-sources/epoch/src/basic/line.coffee
@@ -0,0 +1,46 @@
+# Static line chart implementation (using d3).
+class Epoch.Chart.Line extends Epoch.Chart.Plot
+ constructor: (@options={}) ->
+ @options.type ?= 'line'
+ super(@options)
+ @draw()
+
+ # @return [Function] The line generator used to construct the plot.
+ line: (layer) ->
+ [x, y] = [@x(), @y(layer.range)]
+ d3.svg.line()
+ .x((d) -> x(d.x))
+ .y((d) -> y(d.y))
+
+ # Draws the line chart.
+ draw: ->
+ [x, y, layers] = [@x(), @y(), @getVisibleLayers()]
+
+ # Zero visible layers, just drop all and get out
+ if layers.length == 0
+ return @g.selectAll('.layer').remove()
+
+ # 1) Join
+ layer = @g.selectAll('.layer')
+ .data(layers, (d) -> d.category)
+
+ # 2) Update (only existing)
+ layer.select('.line').transition().duration(500)
+ .attr('d', (l) => @line(l)(l.values))
+
+ # 3) Enter (Create)
+ layer.enter().append('g')
+ .attr('class', (l) -> l.className)
+ .append('path')
+ .attr('class', 'line')
+ .attr('d', (l) => @line(l)(l.values))
+
+ # 4) Update (existing & new)
+ # Nuuupp
+
+ # 5) Exit (Remove)
+ layer.exit().transition().duration(750)
+ .style('opacity', '0')
+ .remove()
+
+ super()
diff --git a/debian/missing-sources/epoch/src/basic/pie.coffee b/debian/missing-sources/epoch/src/basic/pie.coffee
new file mode 100644
index 0000000..e794a2f
--- /dev/null
+++ b/debian/missing-sources/epoch/src/basic/pie.coffee
@@ -0,0 +1,59 @@
+
+# Static Pie Chart implementation (using d3).
+class Epoch.Chart.Pie extends Epoch.Chart.SVG
+ defaults =
+ type: 'pie'
+ margin: 10
+ inner: 0
+
+ # Creates a new pie chart.
+ # @param [Object] options Options for the pie chart.
+ # @option options [Number] margin Margins to add around the pie chart (default: 10).
+ # @option options [Number] inner The inner radius for the chart (default: 0).
+ constructor: (@options={}) ->
+ super(@options = Epoch.Util.defaults(@options, defaults))
+ @pie = d3.layout.pie().sort(null)
+ .value (d) -> d.value
+ @arc = d3.svg.arc()
+ .outerRadius(=> (Math.max(@width, @height) / 2) - @options.margin)
+ .innerRadius(=> @options.inner)
+ @g = @svg.append('g')
+ .attr("transform", "translate(#{@width/2}, #{@height/2})")
+ @on 'option:margin', 'marginChanged'
+ @on 'option:inner', 'innerChanged'
+ @draw()
+
+ # Draws the pie chart
+ draw: ->
+ @g.selectAll('.arc').remove()
+
+ arcs = @g.selectAll(".arc")
+ .data(@pie(@getVisibleLayers()), (d) -> d.data.category)
+
+ arcs.enter().append('g')
+ .attr('class', (d) -> "arc pie " + d.data.className)
+
+ arcs.select('path')
+ .attr('d', @arc)
+
+ arcs.select('text')
+ .attr("transform", (d) => "translate(#{@arc.centroid(d)})")
+ .text((d) -> d.data.label or d.data.category)
+
+ path = arcs.append("path")
+ .attr("d", @arc)
+ .each((d) -> @._current = d)
+
+ text = arcs.append("text")
+ .attr("transform", (d) => "translate(#{@arc.centroid(d)})")
+ .attr("dy", ".35em")
+ .style("text-anchor", "middle")
+ .text((d) -> d.data.label or d.data.category)
+
+ super()
+
+ # Updates margins in response to an <code>option:margin</code> event.
+ marginChanged: -> @draw()
+
+ # Updates inner margin in response to an <code>option:inner</code> event.
+ innerChanged: -> @draw()
diff --git a/debian/missing-sources/epoch/src/basic/scatter.coffee b/debian/missing-sources/epoch/src/basic/scatter.coffee
new file mode 100644
index 0000000..b8563ec
--- /dev/null
+++ b/debian/missing-sources/epoch/src/basic/scatter.coffee
@@ -0,0 +1,59 @@
+
+# Static scatter plot implementation (using d3).
+class Epoch.Chart.Scatter extends Epoch.Chart.Plot
+ defaults =
+ type: 'scatter'
+ radius: 3.5
+ axes: ['top', 'bottom', 'left', 'right']
+
+ # Creates a new scatter plot.
+ # @param [Object] options Options for the plot.
+ # @option options [Number] radius The default radius to use for the points in
+ # the plot (default 3.5). This can be overrwitten by individual points.
+ constructor: (@options={}) ->
+ super(@options = Epoch.Util.defaults(@options, defaults))
+ @on 'option:radius', 'radiusChanged'
+ @draw()
+
+ # Draws the scatter plot.
+ draw: ->
+ [x, y, layers] = [@x(), @y(), @getVisibleLayers()]
+ radius = @options.radius
+
+ if layers.length == 0
+ return @g.selectAll('.layer').remove()
+
+ layer = @g.selectAll('.layer')
+ .data(layers, (d) -> d.category)
+
+ layer.enter().append('g')
+ .attr('class', (d) -> d.className)
+
+ dots = layer.selectAll('.dot')
+ .data((l) -> l.values)
+
+ dots.transition().duration(500)
+ .attr("r", (d) -> d.r ? radius)
+ .attr("cx", (d) -> x(d.x))
+ .attr("cy", (d) -> y(d.y))
+
+ dots.enter().append('circle')
+ .attr('class', 'dot')
+ .attr("r", (d) -> d.r ? radius)
+ .attr("cx", (d) -> x(d.x))
+ .attr("cy", (d) -> y(d.y))
+
+ dots.exit().transition()
+ .duration(750)
+ .style('opacity', 0)
+ .remove()
+
+ layer.exit().transition()
+ .duration(750)
+ .style('opacity', 0)
+ .remove()
+
+ super()
+
+ # Updates radius in response to an <code>option:radius</code> event.
+ radiusChanged: -> @draw()
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
diff --git a/debian/missing-sources/epoch/src/data.coffee b/debian/missing-sources/epoch/src/data.coffee
new file mode 100644
index 0000000..7627fd4
--- /dev/null
+++ b/debian/missing-sources/epoch/src/data.coffee
@@ -0,0 +1,311 @@
+Epoch.Data ?= {}
+Epoch.Data.Format ?= {}
+
+# Private Helper Function for data formats below
+applyLayerLabel = (layer, options, i, keys=[]) ->
+ [labels, autoLabels, keyLabels] = [options.labels, options.autoLabels, options.keyLabels]
+ if labels? and Epoch.isArray(labels) and labels.length > i
+ layer.label = labels[i]
+ else if keyLabels and keys.length > i
+ layer.label = keys[i]
+ else if autoLabels
+ label = []
+ while i >= 0
+ label.push String.fromCharCode(65+(i%26))
+ i -= 26
+ layer.label = label.join('')
+ return layer
+
+# Formats a given input array for the chart of the specified type. Notes:
+#
+# * Basic pie charts require a flat array of numbers
+# * Real-time histogram charts require sparse histogram objects
+#
+# @param data Data array to format (can be multidimensional to allow for multiple layers).
+# @option options [String] type Type of chart for which to format the data.
+# @option options [Function] x(d, i) Maps the data to x values given a data point and the index of the point.
+# @option options [Function] y(d, i) Maps the data to y values given a data point and the index of the point.
+# @option options [Function] time(d, i, startTime) Maps the data to time values for real-time plots given the point and index.
+# @option options [Array] labels Labels to apply to each data layer.
+# @option options [Boolean] autoLabels Apply labels of ascending capital letters to each layer if true.
+# @option options [Number] startTime Unix timestamp used as the starting point for auto acsending times in
+# real-time data formatting.
+Epoch.Data.Format.array = (->
+ defaultOptions =
+ x: (d, i) -> i
+ y: (d, i) -> d
+ time: (d, i, startTime) -> parseInt(startTime) + parseInt(i)
+ type: 'area'
+ autoLabels: false
+ labels: []
+ startTime: parseInt(new Date().getTime() / 1000)
+
+ buildLayers = (data, options, mapFn) ->
+ result = []
+ if Epoch.isArray(data[0])
+ for own i, series of data
+ result.push applyLayerLabel({values: series.map(mapFn)}, options, parseInt(i))
+ else
+ result.push applyLayerLabel({values: data.map(mapFn)}, options, 0)
+ return result
+
+ formatBasicPlot = (data, options) ->
+ buildLayers data, options, (d, i) ->
+ { x: options.x(d, i), y: options.y(d, i) }
+
+ formatTimePlot = (data, options) ->
+ buildLayers data, options, (d, i) ->
+ { time: options.time(d, i, options.startTime), y: options.y(d, i) }
+
+ formatHeatmap = (data, options) ->
+ buildLayers data, options, (d, i) ->
+ { time: options.time(d, i, options.startTime), histogram: d }
+
+ formatPie = (data, options) ->
+ result = []
+ for own i, v of data
+ return [] unless Epoch.isNumber(data[0])
+ result.push applyLayerLabel({ value: v }, options, i)
+ return result
+
+ format = (data=[], options={}) ->
+ return [] unless Epoch.isNonEmptyArray(data)
+ opt = Epoch.Util.defaults options, defaultOptions
+
+ if opt.type == 'time.heatmap'
+ formatHeatmap data, opt
+ else if opt.type.match /^time\./
+ formatTimePlot data, opt
+ else if opt.type == 'pie'
+ formatPie data, opt
+ else
+ formatBasicPlot data, opt
+
+ format.entry = (datum, options={}) ->
+ if options.type == 'time.gauge'
+ return 0 unless datum?
+ opt = Epoch.Util.defaults options, defaultOptions
+ d = if Epoch.isArray(datum) then datum[0] else datum
+ return opt.y(d, 0)
+
+ return [] unless datum?
+ unless options.startTime?
+ options.startTime = parseInt(new Date().getTime() / 1000)
+
+ if Epoch.isArray(datum)
+ data = datum.map (d) -> [d]
+ else
+ data = [datum]
+
+ (layer.values[0] for layer in format(data, options))
+
+ return format
+)()
+
+# Formats an input array of tuples such that the first element of the tuple is set
+# as the x-coordinate and the second element as the y-coordinate. Supports layers
+# of tupled series. For real-time plots the first element of a tuple is set as the
+# time component of the value.
+#
+# This formatter will return an empty array if the chart <code>type</code> option is
+# set as 'time.heatmap', 'time.gauge', or 'pie'.
+#
+# @param data Data array to format (can be multidimensional to allow for multiple layers).
+# @option options [String] type Type of chart for which to format the data.
+# @option options [Function] x(d, i) Maps the data to x values given a data point and the index of the point.
+# @option options [Function] y(d, i) Maps the data to y values given a data point and the index of the point.
+# @option options [Function] time(d, i, startTime) Maps the data to time values for real-time plots given the point and index.
+# @option options [Array] labels Labels to apply to each data layer.
+# @option options [Boolean] autoLabels Apply labels of ascending capital letters to each layer if true.
+Epoch.Data.Format.tuple = (->
+ defaultOptions =
+ x: (d, i) -> d
+ y: (d, i) -> d
+ time: (d, i) -> d
+ type: 'area'
+ autoLabels: false
+ labels: []
+
+ buildLayers = (data, options, mapFn) ->
+ return [] unless Epoch.isArray(data[0])
+ result = []
+ if Epoch.isArray(data[0][0])
+ for own i, series of data
+ result.push applyLayerLabel({values: series.map(mapFn)}, options, parseInt(i))
+ else
+ result.push applyLayerLabel({values: data.map(mapFn)}, options, 0)
+ return result
+
+ format = (data=[], options={}) ->
+ return [] unless Epoch.isNonEmptyArray(data)
+ opt = Epoch.Util.defaults options, defaultOptions
+
+ if opt.type == 'pie' or opt.type == 'time.heatmap' or opt.type == 'time.gauge'
+ return []
+ else if opt.type.match /^time\./
+ buildLayers data, opt, (d, i) ->
+ {time: opt.time(d[0], parseInt(i)), y: opt.y(d[1], parseInt(i))}
+ else
+ buildLayers data, opt, (d, i) ->
+ {x: opt.x(d[0], parseInt(i)), y: opt.y(d[1], parseInt(i))}
+
+ format.entry = (datum, options={}) ->
+ return [] unless datum?
+ unless options.startTime?
+ options.startTime = parseInt(new Date().getTime() / 1000)
+
+ if Epoch.isArray(datum) and Epoch.isArray(datum[0])
+ data = datum.map (d) -> [d]
+ else
+ data = [datum]
+
+ (layer.values[0] for layer in format(data, options))
+
+ return format
+)()
+
+# This formatter expects to be passed a flat array of objects and a list of keys.
+# It then extracts the value for each key across each of the objects in the array
+# to produce multi-layer plot data of the given chart type. Note that this formatter
+# also can be passed an <code>x</code> or <code>time</code> option as a string that
+# allows the programmer specify a key to use for the value of the first component
+# (x or time) of each resulting layer value.
+#
+# Note that this format does not work with basic pie charts nor real-time gauge charts.
+#
+# @param [Array] data Flat array of objects to format.
+# @param [Array] keys List of keys used to extract data from each of the objects.
+# @option options [String] type Type of chart for which to format the data.
+# @option options [Function, String] x Either the key to use for the x-componet of
+# the resulting values or a function of the data at that point and index of the data.
+# @option options [Function, String] time Either an object key or function to use for the
+# time-component of resulting real-time plot values.
+# @option options [Function] y(d, i) Maps the data to y values given a data point and the index of the point.
+# @option options [Array] labels Labels to apply to each data layer.
+# @option options [Boolean] autoLabels Apply labels of ascending capital letters to each layer if true.
+# @option options [Boolean] keyLabels Apply labels using the keys passed to the formatter (defaults to true).
+# @option options [Number] startTime Unix timestamp used as the starting point for auto acsending times in
+# real-time data formatting.
+Epoch.Data.Format.keyvalue = (->
+ defaultOptions =
+ type: 'area',
+ x: (d, i) -> parseInt(i)
+ y: (d, i) -> d
+ time: (d, i, startTime) -> parseInt(startTime) + parseInt(i)
+ labels: []
+ autoLabels: false
+ keyLabels: true
+ startTime: parseInt(new Date().getTime() / 1000)
+
+ buildLayers = (data, keys, options, mapFn) ->
+ result = []
+ for own j, key of keys
+ values = []
+ for own i, d of data
+ values.push mapFn(d, key, parseInt(i))
+ result.push applyLayerLabel({ values: values }, options, parseInt(j), keys)
+ return result
+
+ formatBasicPlot = (data, keys, options) ->
+ buildLayers data, keys, options, (d, key, i) ->
+ if Epoch.isString(options.x)
+ x = d[options.x]
+ else
+ x = options.x(d, parseInt(i))
+ { x: x, y: options.y(d[key], parseInt(i)) }
+
+ formatTimePlot = (data, keys, options, rangeName='y') ->
+ buildLayers data, keys, options, (d, key, i) ->
+ if Epoch.isString(options.time)
+ value = { time: d[options.time] }
+ else
+ value = { time: options.time(d, parseInt(i), options.startTime) }
+ value[rangeName] = options.y(d[key], parseInt(i))
+ value
+
+ format = (data=[], keys=[], options={}) ->
+ return [] unless Epoch.isNonEmptyArray(data) and Epoch.isNonEmptyArray(keys)
+ opt = Epoch.Util.defaults options, defaultOptions
+
+ if opt.type == 'pie' or opt.type == 'time.gauge'
+ return []
+ else if opt.type == 'time.heatmap'
+ formatTimePlot data, keys, opt, 'histogram'
+ else if opt.type.match /^time\./
+ formatTimePlot data, keys, opt
+ else
+ formatBasicPlot data, keys, opt
+
+ format.entry = (datum, keys=[], options={}) ->
+ return [] unless datum? and Epoch.isNonEmptyArray(keys)
+ unless options.startTime?
+ options.startTime = parseInt(new Date().getTime() / 1000)
+ (layer.values[0] for layer in format([datum], keys, options))
+
+ return format
+)()
+
+# Convenience data formatting method for easily accessing the various formatters.
+# @param [String] formatter Name of the formatter to use.
+# @param [Array] data Data to format.
+# @param [Object] options Options to pass to the formatter (if any).
+Epoch.data = (formatter, args...) ->
+ return [] unless (formatFn = Epoch.Data.Format[formatter])?
+ formatFn.apply formatFn, args
+
+
+# Method used by charts and models for handling option based data formatting.
+# Abstracted here because we'd like to allow models and indivisual charts to
+# perform this action depending on the context.
+Epoch.Data.formatData = (data=[], type, dataFormat) ->
+ return data unless Epoch.isNonEmptyArray(data)
+
+ if Epoch.isString(dataFormat)
+ opts = { type: type }
+ return Epoch.data(dataFormat, data, opts)
+
+ return data unless Epoch.isObject(dataFormat)
+ return data unless dataFormat.name? and Epoch.isString(dataFormat.name)
+ return data unless Epoch.Data.Format[dataFormat.name]?
+
+ args = [dataFormat.name, data]
+ if dataFormat.arguments? and Epoch.isArray(dataFormat.arguments)
+ args.push(a) for a in dataFormat.arguments
+
+ if dataFormat.options?
+ opts = dataFormat.options
+ if type?
+ opts.type ?= type
+ args.push opts
+ else if type?
+ args.push {type: type}
+
+ Epoch.data.apply(Epoch.data, args)
+
+# Method used to format incoming entries for real-time charts.
+Epoch.Data.formatEntry = (datum, type, format) ->
+ return datum unless format?
+
+ if Epoch.isString(format)
+ opts = { type: type }
+ return Epoch.Data.Format[format].entry datum, opts
+
+ return datum unless Epoch.isObject(format)
+ return datum unless format.name? and Epoch.isString(format.name)
+ return datum unless Epoch.Data.Format[format.name]?
+
+ dataFormat = Epoch.Util.defaults format, {}
+
+ args = [datum]
+ if dataFormat.arguments? and Epoch.isArray(dataFormat.arguments)
+ args.push(a) for a in dataFormat.arguments
+
+ if dataFormat.options?
+ opts = dataFormat.options
+ opts.type = type
+ args.push opts
+ else if type?
+ args.push {type: type}
+
+ entry = Epoch.Data.Format[dataFormat.name].entry
+ entry.apply entry, args
diff --git a/debian/missing-sources/epoch/src/epoch.coffee b/debian/missing-sources/epoch/src/epoch.coffee
new file mode 100644
index 0000000..b2d06ca
--- /dev/null
+++ b/debian/missing-sources/epoch/src/epoch.coffee
@@ -0,0 +1,17 @@
+window.Epoch ?= {}
+window.Epoch.Chart ?= {}
+window.Epoch.Time ?= {}
+window.Epoch.Util ?= {}
+window.Epoch.Formats ?= {}
+
+# Sends a warning to the developer console with the given message.
+# @param [String] msg Message for the warning.
+Epoch.warn = (msg) ->
+ (console.warn or console.log)("Epoch Warning: #{msg}")
+
+# Raises an exception with the given message (with the 'Epoch Error:' preamble).
+# @param [String] msg Specific message for the exception.
+Epoch.exception = (msg) ->
+ throw "Epoch Error: #{msg}"
+
+# "I think, baby, I was born just a little late!" -- Middle Class Rut
diff --git a/debian/missing-sources/epoch/src/model.coffee b/debian/missing-sources/epoch/src/model.coffee
new file mode 100644
index 0000000..76f6acd
--- /dev/null
+++ b/debian/missing-sources/epoch/src/model.coffee
@@ -0,0 +1,55 @@
+# Data model for epoch charts. By instantiating a model and passing it to each
+# of the charts on a page the application programmer can update data once and
+# have each of the charts respond accordingly.
+#
+# In addition to setting basic / historical data via the setData method, the
+# model also supports the push method, which when used will cause real-time
+# plots to automatically update and animate.
+class Epoch.Model extends Epoch.Events
+ defaults =
+ dataFormat: null
+
+ # Creates a new Model.
+ # @option options dataFormat The default data fromat for the model.
+ # @option data Initial data for the model.
+ constructor: (options={}) ->
+ super()
+ options = Epoch.Util.defaults options, defaults
+ @dataFormat = options.dataFormat
+ @data = options.data
+ @loading = false
+
+ # Sets the model's data.
+ # @param data Data to set for the model.
+ # @event data:updated Instructs listening charts that new data is available.
+ setData: (data) ->
+ @data = data
+ @trigger 'data:updated'
+
+ # Pushes a new entry into the model.
+ # @param entry Entry to push.
+ # @event data:push Instructs listening charts that a new data entry is available.
+ push: (entry) ->
+ @entry = entry
+ @trigger 'data:push'
+
+ # Determines if the model has data.
+ # @return true if the model has data, false otherwise.
+ hasData: ->
+ @data?
+
+ # Retrieves and formats adata for the specific chart type and data format.
+ # @param [String] type Type of the chart for which to fetch the data.
+ # @param [String, Object] dataFormat (optional) Used to override the model's default data format.
+ # @return The model's data formatted based the parameters.
+ getData: (type, dataFormat) ->
+ dataFormat ?= @dataFormat
+ Epoch.Data.formatData @data, type, dataFormat
+
+ # Retrieves the latest data entry that was pushed into the model.
+ # @param [String] type Type of the chart for which to fetch the data.
+ # @param [String, Object] dataFormat (optional) Used to override the model's default data format.
+ # @return The model's next data entry formatted based the parameters.
+ getNext: (type, dataFormat) ->
+ dataFormat ?= @dataFormat
+ Epoch.Data.formatEntry @entry, type, dataFormat
diff --git a/debian/missing-sources/epoch/src/time.coffee b/debian/missing-sources/epoch/src/time.coffee
new file mode 100644
index 0000000..402cc50
--- /dev/null
+++ b/debian/missing-sources/epoch/src/time.coffee
@@ -0,0 +1,615 @@
+# Real-time Plot Base Class. Uses an html5 canvas to recreate the basic d3 drawing routines
+# while simultaneously reducing the load on the viewer's cpu (and, you know, not leaking
+# memory which ultimately leads to a crashed browser).
+#
+# The class also handles the creation of axes and margins common to all time-series plots.
+# Furthermore it layers the canvas below an SVG element to keep visual consistency when
+# rendering text, glyphs, etc.
+class Epoch.Time.Plot extends Epoch.Chart.Canvas
+ defaults =
+ range: null
+ fps: 24
+ historySize: 120
+ windowSize: 40
+ queueSize: 10
+ axes: ['bottom']
+ ticks:
+ time: 15
+ left: 5
+ right: 5
+ tickFormats:
+ top: Epoch.Formats.seconds
+ bottom: Epoch.Formats.seconds
+ left: Epoch.Formats.si
+ right: Epoch.Formats.si
+
+ defaultAxisMargins =
+ top: 25
+ right: 50
+ bottom: 25
+ left: 50
+
+ optionListeners =
+ 'option:margins': 'marginsChanged'
+ 'option:margins.top': 'marginsChanged'
+ 'option:margins.right': 'marginsChanged'
+ 'option:margins.bottom': 'marginsChanged'
+ 'option:margins.left': 'marginsChanged'
+ 'option:axes': 'axesChanged'
+ 'option:ticks': 'ticksChanged'
+ 'option:ticks.top': 'ticksChanged'
+ 'option:ticks.right': 'ticksChanged'
+ 'option:ticks.bottom': 'ticksChanged'
+ 'option:ticks.left': 'ticksChanged'
+ 'option:tickFormats': 'tickFormatsChanged'
+ 'option:tickFormats.top': 'tickFormatsChanged'
+ 'option:tickFormats.right': 'tickFormatsChanged'
+ 'option:tickFormats.bottom': 'tickFormatsChanged'
+ 'option:tickFormats.left': 'tickFormatsChanged'
+
+ # Creates a new real-time plot.
+ #
+ # @param [Object] options Options for the plot.
+ # @option options [Integer] fps Number of frames per second to use when animating
+ # the plot.
+ # @option options [Integer] historySize Maximum number of elements to keep in history
+ # for the plot.
+ # @option options [Integer] windowSize Number of entries to simultaneously display
+ # when rendering the visualization.
+ # @option options [Integer] queueSize Number of elements to queue while not animating
+ # but still recieving elements. In some browsers, intervals will not fire if the
+ # page containing them is not the active tab. By setting a maximum limit to the
+ # number of unprocessed data points we can ensure that the memory footprint of the
+ # page does not get out of hand.
+ # @option options [Object] margins Explicit margins to use for the visualization. Note
+ # that these are optional and will be automatically generated based on which axes are
+ # used for the visualization. Margins are keyed by their position (top, left, bottom
+ # and/or right) and should map to [Integer] values.
+ # @option options [Array] axes Which axes to display when rendering the visualization
+ # (top, left, bottom, and/or right).
+ # @option options [Object] ticks Number of ticks to display on each axis available axes
+ # ares: time, left, and right. The number provided for the left and right axes are in
+ # absolute terms (i.e. there will be exactly that number of ticks). The time ticks
+ # denote how often a tick should be generated (e.g. if 5 is provided then a tick will
+ # be added every fifth time you push a new data entry into the visualization).
+ # @option options [Object] tickFormats Formatting functions for ticks on the given axes.
+ # The avaiable axes are: top, bottom, left, and right.
+ constructor: (@options) ->
+ givenMargins = Epoch.Util.copy(@options.margins) or {}
+ super(@options = Epoch.Util.defaults(@options, defaults))
+
+ if @options.model
+ @options.model.on 'data:push', => @pushFromModel()
+
+ # Queue entering data to get around memory bloat and "non-active" tab issues
+ @_queue = []
+
+ # Margins
+ @margins = {}
+ for pos in ['top', 'right', 'bottom', 'left']
+ @margins[pos] = if @options.margins? and @options.margins[pos]?
+ @options.margins[pos]
+ else if @hasAxis(pos)
+ defaultAxisMargins[pos]
+ else
+ 6
+
+ # SVG Overlay
+ @svg = @el.insert('svg', ':first-child')
+ .attr('width', @width)
+ .attr('height', @height)
+ .style('z-index', '1000')
+
+ # Position the canvas "under" the SVG element
+ if @el.style('position') != 'absolute' and @el.style('position') != 'relative'
+ @el.style('position', 'relative')
+
+ @canvas.style { position: 'absolute', 'z-index': '999' }
+ @_sizeCanvas()
+
+ # Animation / Transitions
+ @animation =
+ interval: null
+ active: false
+ delta: => -(@w() / @options.fps),
+ tickDelta: => -( (@w() / @pixelRatio) / @options.fps )
+ frame: 0,
+ duration: @options.fps
+
+ # Add SVG Axes
+ @_buildAxes()
+
+ # Callback used for animation
+ @animationCallback = => @_animate()
+
+ # Listen for specific option changes
+ @onAll optionListeners
+
+ # Positions and sizes the canvas based on margins and axes.
+ _sizeCanvas: ->
+ @canvas.attr
+ width: @innerWidth()
+ height: @innerHeight()
+
+ @canvas.style
+ width: "#{@innerWidth() / @pixelRatio}px"
+ height: "#{@innerHeight() / @pixelRatio}px"
+ top: "#{@margins.top}px"
+ left: "#{@margins.left}px"
+
+ # Removes any axes found in the SVG and adds both the time and range axes to the plot.
+ _buildAxes: ->
+ @svg.selectAll('.axis').remove()
+ @_prepareTimeAxes()
+ @_prepareRangeAxes()
+
+ # Works exactly as in Epoch.Chart.Base with the addition of truncating value arrays
+ # to that of the historySize defined in the chart's options.
+ _annotateLayers: (prepared) ->
+ data = []
+ for own i, layer of prepared
+ copy = Epoch.Util.copy(layer)
+ start = Math.max(0, layer.values.length - @options.historySize)
+ copy.values = layer.values.slice(start)
+ classes = ['layer']
+ classes.push "category#{(i|0)+1}"
+ classes.push(Epoch.Util.dasherize layer.label) if layer.label?
+ copy.className = classes.join(' ')
+ copy.visible = true
+ data.push copy
+ return data
+
+ # This method is called to provide a small offset for placement of horizontal ticks.
+ # The value returned will be added to the x value of each tick as they are being
+ # rendered.
+ #
+ # @return [Number] The horizontal offset for the top and bottom axes ticks.
+ _offsetX: -> 0
+
+ # Builds time axes (bottom and top)
+ _prepareTimeAxes: ->
+ if @hasAxis('bottom')
+ axis = @bottomAxis = @svg.append('g')
+ .attr('class', "x axis bottom canvas")
+ .attr('transform', "translate(#{@margins.left-1}, #{@innerHeight()/@pixelRatio+@margins.top})")
+ axis.append('path')
+ .attr('class', 'domain')
+ .attr('d', "M0,0H#{@innerWidth()/@pixelRatio+1}")
+
+ if @hasAxis('top')
+ axis = @topAxis = @svg.append('g')
+ .attr('class', "x axis top canvas")
+ .attr('transform', "translate(#{@margins.left-1}, #{@margins.top})")
+ axis.append('path')
+ .attr('class', 'domain')
+ .attr('d', "M0,0H#{@innerWidth()/@pixelRatio+1}")
+
+ @_resetInitialTimeTicks()
+
+ # Resets the initial ticks for the time axes.
+ _resetInitialTimeTicks: ->
+ tickInterval = @options.ticks.time
+ @_ticks = []
+ @_tickTimer = tickInterval
+
+ @bottomAxis.selectAll('.tick').remove() if @bottomAxis?
+ @topAxis.selectAll('.tick').remove() if @topAxis?
+
+ for layer in @data
+ continue unless Epoch.isNonEmptyArray(layer.values)
+ [i, k] = [@options.windowSize-1, layer.values.length-1]
+ while i >= 0 and k >= 0
+ @_pushTick i, layer.values[k].time, false, true
+ i -= tickInterval
+ k -= tickInterval
+ break
+
+ # Builds the range axes (left and right)
+ _prepareRangeAxes: ->
+ if @hasAxis('left')
+ @svg.append("g")
+ .attr("class", "y axis left")
+ .attr('transform', "translate(#{@margins.left-1}, #{@margins.top})")
+ .call(@leftAxis())
+
+ if @hasAxis('right')
+ @svg.append('g')
+ .attr('class', 'y axis right')
+ .attr('transform', "translate(#{@width - @margins.right}, #{@margins.top})")
+ .call(@rightAxis())
+
+ # @return [Object] The d3 left axis.
+ leftAxis: ->
+ ticks = @options.ticks.left
+ axis = d3.svg.axis().scale(@ySvgLeft()).orient('left')
+ .tickFormat(@options.tickFormats.left)
+ if ticks == 2
+ axis.tickValues @extent((d) -> d.y)
+ else
+ axis.ticks(ticks)
+
+ # @return [Object] The d3 right axis.
+ rightAxis: ->
+ extent = @extent((d) -> d.y)
+ ticks = @options.ticks.right
+ axis = d3.svg.axis().scale(@ySvgRight()).orient('right')
+ .tickFormat(@options.tickFormats.right)
+ if ticks == 2
+ axis.tickValues @extent((d) -> d.y)
+ else
+ axis.ticks(ticks)
+
+ # Determines if the visualization is displaying the axis with the given name.
+ # @param [String] name Name of the axis
+ # @return [Boolean] <code>true</code> if the axis was set in the options, <code>false</code> otherwise.
+ hasAxis: (name) ->
+ @options.axes.indexOf(name) > -1
+
+ # @return [Number] the width of the visualization area of the plot (full width - margins)
+ innerWidth: ->
+ (@width - (@margins.left + @margins.right)) * @pixelRatio
+
+ # @return [Number] the height of the visualization area of the plot (full height - margins)
+ innerHeight: ->
+ (@height - (@margins.top + @margins.bottom)) * @pixelRatio
+
+ # Abstract method for performing any preprocessing before queuing new entries
+ # @param entry [Object] The entry to prepare.
+ # @return [Object] The prepared entry.
+ _prepareEntry: (entry) -> entry
+
+ # Abstract method for preparing a group of layered entries entering the visualization
+ # @param [Array] layers The layered entries to prepare.
+ # @return [Array] The prepared layers.
+ _prepareLayers: (layers) -> layers
+
+ # This method will remove the first incoming entry from the visualization's queue
+ # and shift it into the working set (aka window). It then starts the animating the
+ # transition of the element into the visualization.
+ # @event transition:start in the case that animation is actually started.
+ _startTransition: ->
+ return if @animation.active == true or @_queue.length == 0
+ @trigger 'transition:start'
+ @_shift()
+ @animation.active = true
+ @animation.interval = setInterval(@animationCallback, 1000/@options.fps)
+
+ # Stops animating and clears the animation interval given there is no more
+ # incoming data to process. Also finalizes tick entering and exiting.
+ # @event transition:end After the transition has completed.
+ _stopTransition: ->
+ return unless @inTransition()
+
+ # Shift data off the end
+ for layer in @data
+ continue unless layer.values.length > @options.windowSize + 1
+ layer.values.shift()
+
+ # Finalize tick transitions
+ [firstTick, lastTick] = [@_ticks[0], @_ticks[@_ticks.length-1]]
+
+ if lastTick? and lastTick.enter
+ lastTick.enter = false
+ lastTick.opacity = 1
+
+ if firstTick? and firstTick.exit
+ @_shiftTick()
+
+ # Reset the animation frame modulus
+ @animation.frame = 0
+
+ # Trigger that we are done transitioning
+ @trigger 'transition:end'
+
+ # Clear the transition interval unless another entry is already queued
+ if @_queue.length > 0
+ @_shift()
+ else
+ @animation.active = false
+ clearInterval @animation.interval
+
+ # Determines if the plot is currently animating a transition.
+ # @return [Boolean] <code>true</code> if the plot is animating, <code>false</code> otherwise.
+ inTransition: ->
+ @animation.active
+
+ # This method is used by the application programmer to introduce new data into
+ # the timeseries plot. The method queues the incoming data, ensures a fixed size
+ # for the data queue, and finally calls <code>_startTransition</code> method to
+ # begin animating the plot.
+ # @param [Array] layers Layered incoming visualization data.
+ # @event push Triggered after the new data has been pushed into the queue.
+ push: (layers) ->
+ layers = @_prepareLayers(layers)
+
+ # Handle entry queue maximum size
+ if @_queue.length > @options.queueSize
+ @_queue.splice @options.queueSize, (@_queue.length - @options.queueSize)
+ return false if @_queue.length == @options.queueSize
+
+ # Push the entry into the queue
+ @_queue.push layers.map((entry) => @_prepareEntry(entry))
+
+ @trigger 'push'
+
+ # Begin the transition unless we are already doing so
+ @_startTransition() unless @inTransition()
+
+ # Fetches new entry data from the model in response to a 'data:push' event.
+ pushFromModel: ->
+ @push @options.model.getNext(@options.type, @options.dataFormat)
+
+ # Shift elements off the incoming data queue (see the implementation of
+ # push above).
+ #
+ # If there's data to be shoved into the visualization it will pull it
+ # off the queue and put it into the working dataset. It also calls through
+ # to @_updateTicks to handle horizontal (or "time") axes tick transitions
+ # since we're implementing independent of d3 as well.
+ #
+ # @event before:shift Before an element has been shifted off the queue.
+ # @event after:shift After the element has been shifted off the queue.
+ _shift: ->
+ @trigger 'before:shift'
+ entry = @_queue.shift()
+ layer.values.push(entry[i]) for own i, layer of @data
+ @_updateTicks(entry[0].time)
+ @_transitionRangeAxes()
+ @trigger 'after:shift'
+
+ # Transitions the left and right axes when the range of the plot has changed.
+ _transitionRangeAxes: ->
+ if @hasAxis('left')
+ @svg.selectAll('.y.axis.left').transition()
+ .duration(500)
+ .ease('linear')
+ .call(@leftAxis())
+
+ if @hasAxis('right')
+ @svg.selectAll('.y.axis.right').transition()
+ .duration(500)
+ .ease('linear')
+ .call(@rightAxis())
+
+ # Performs the animation for transitioning elements in the visualization.
+ _animate: ->
+ return unless @inTransition()
+ @_stopTransition() if ++@animation.frame == @animation.duration
+ @draw(@animation.frame * @animation.delta())
+ @_updateTimeAxes()
+
+ # @param [Array] givenDomain A given domain for the scale
+ # @return [Function] The y scale for the plot
+ y: (givenDomain) ->
+ d3.scale.linear()
+ .domain(@_getScaleDomain(givenDomain))
+ .range([@innerHeight(), 0])
+
+ # @param [Array] givenDomain Optional domain to override default
+ # @return [Function] The y scale for the svg portions of the plot
+ ySvg: (givenDomain) ->
+ d3.scale.linear()
+ .domain(@_getScaleDomain(givenDomain))
+ .range([@innerHeight() / @pixelRatio, 0])
+
+ # @return [Function] The y scale for the svg portion of the plot for the left axis
+ ySvgLeft: ->
+ if @options.range?
+ @ySvg @options.range.left
+ else
+ @ySvg()
+
+ # @return [Function] The y scale for the svg portion of the plot for the right axis
+ ySvgRight: ->
+ if @options.range?
+ @ySvg @options.range.right
+ else
+ @ySvg()
+
+ # @return [Number] The width of a single section of the graph pertaining to a data point
+ w: ->
+ @innerWidth() / @options.windowSize
+
+ # This is called every time we introduce new data (as a result of _shift)
+ # it checks to see if we also need to update the working tick set and
+ # makes the approriate changes for handling tick animation (enter, exit,
+ # and update in the d3 model).
+ #
+ # @param [Integer] newTime Current newest timestamp in the data
+ _updateTicks: (newTime) ->
+ return unless @hasAxis('top') or @hasAxis('bottom')
+
+ # Incoming ticks
+ unless (++@_tickTimer) % @options.ticks.time
+ @_pushTick(@options.windowSize, newTime, true)
+
+ # Outgoing ticks
+ return unless @_ticks.length > 0
+ unless @_ticks[0].x - (@w()/@pixelRatio) >= 0
+ @_ticks[0].exit = true
+
+ # Makes and pushes a new tick into the visualization.
+ #
+ # @param bucket Index in the data window where the tick should initially be position
+ # @param time The unix timestamp associated with the tick
+ # @param enter Whether or not the tick should be considered as "newly entering"
+ # Used primarily for performing the tick opacity tween.
+ _pushTick: (bucket, time, enter=false, reverse=false) ->
+ return unless @hasAxis('top') or @hasAxis('bottom')
+ tick =
+ time: time
+ x: bucket*(@w()/@pixelRatio) + @_offsetX()
+ opacity: if enter then 0 else 1
+ enter: if enter then true else false
+ exit: false
+
+ if @hasAxis('bottom')
+ g = @bottomAxis.append('g')
+ .attr('class', 'tick major')
+ .attr('transform', "translate(#{tick.x+1},0)")
+ .style('opacity', tick.opacity)
+
+ g.append('line')
+ .attr('y2', 6)
+
+ g.append('text')
+ .attr('text-anchor', 'middle')
+ .attr('dy', 19)
+ .text(@options.tickFormats.bottom(tick.time))
+
+ tick.bottomEl = g
+
+ if @hasAxis('top')
+ g = @topAxis.append('g')
+ .attr('class', 'tick major')
+ .attr('transform', "translate(#{tick.x+1},0)")
+ .style('opacity', tick.opacity)
+
+ g.append('line')
+ .attr('y2', -6)
+
+ g.append('text')
+ .attr('text-anchor', 'middle')
+ .attr('dy', -10)
+ .text(@options.tickFormats.top(tick.time))
+
+ tick.topEl = g
+
+ if reverse
+ @_ticks.unshift tick
+ else
+ @_ticks.push tick
+ return tick
+
+ # Shifts a tick that is no longer needed out of the visualization.
+ _shiftTick: ->
+ return unless @_ticks.length > 0
+ tick = @_ticks.shift()
+ tick.topEl.remove() if tick.topEl?
+ tick.bottomEl.remove() if tick.bottomEl?
+
+ # This performs animations for the time axes (top and bottom).
+ _updateTimeAxes: ->
+ return unless @hasAxis('top') or @hasAxis('bottom')
+ [dx, dop] = [@animation.tickDelta(), 1 / @options.fps]
+
+ for tick in @_ticks
+ tick.x += dx
+ if @hasAxis('bottom')
+ tick.bottomEl.attr('transform', "translate(#{tick.x+1},0)")
+ if @hasAxis('top')
+ tick.topEl.attr('transform', "translate(#{tick.x+1},0)")
+
+ if tick.enter
+ tick.opacity += dop
+ else if tick.exit
+ tick.opacity -= dop
+
+ if tick.enter or tick.exit
+ tick.bottomEl.style('opacity', tick.opacity) if @hasAxis('bottom')
+ tick.topEl.style('opacity', tick.opacity) if @hasAxis('top')
+
+ # Draws the visualization in the plot's canvas.
+ # @param delta The current x offset to apply to all elements when rendering. This number
+ # will be 0 when the plot is not animating and negative when it is.
+ # @abstract It does nothing on its own but is provided so that subclasses can
+ # define a custom rendering routine.
+ draw: (delta=0) -> super()
+
+ dimensionsChanged: ->
+ super()
+ @svg.attr('width', @width).attr('height', @height)
+ @_sizeCanvas()
+ @_buildAxes()
+ @draw(@animation.frame * @animation.delta())
+
+ # Updates axes in response to an <code>option:axes</code> event.
+ axesChanged: ->
+ for pos in ['top', 'right', 'bottom', 'left']
+ continue if @options.margins? and @options.margins[pos]?
+ if @hasAxis(pos)
+ @margins[pos] = defaultAxisMargins[pos]
+ else
+ @margins[pos] = 6
+ @_sizeCanvas()
+ @_buildAxes()
+ @draw(@animation.frame * @animation.delta())
+
+ # Updates ticks in response to an <code>option.ticks.*</code> event.
+ ticksChanged: ->
+ @_resetInitialTimeTicks()
+ @_transitionRangeAxes()
+ @draw(@animation.frame * @animation.delta())
+
+ # Updates tick formats in response to an <code>option.tickFormats.*</code> event.
+ tickFormatsChanged: ->
+ @_resetInitialTimeTicks()
+ @_transitionRangeAxes()
+ @draw(@animation.frame * @animation.delta())
+
+ # Updates margins in response to an <code>option.margins.*</code> event.
+ marginsChanged: ->
+ return unless @options.margins?
+ for own pos, size of @options.margins
+ unless size?
+ @margins[pos] = 6
+ else
+ @margins[pos] = size
+
+ @_sizeCanvas()
+ @draw(@animation.frame * @animation.delta())
+
+ layerChanged: ->
+ @_transitionRangeAxes()
+ super()
+
+
+# Base class for all "stacked" plot types (e.g. bar charts, area charts, etc.)
+# @abstract It does not perform rendering but instead formats the data
+# so as to ease the process of rendering stacked plots.
+class Epoch.Time.Stack extends Epoch.Time.Plot
+ # Sets stacking information (y0) for each of the points in each layer
+ _stackLayers: ->
+ return unless (layers = @getVisibleLayers()).length > 0
+ for i in [0...layers[0].values.length]
+ y0 = 0
+ for layer in layers
+ layer.values[i].y0 = y0
+ y0 += layer.values[i].y
+
+ # Adds stacking information for layers entering the visualization.
+ # @param [Array] layers Layers to stack.
+ _prepareLayers: (layers) ->
+ y0 = 0
+ for own i, d of layers
+ continue unless @data[i].visible
+ d.y0 = y0
+ y0 += d.y
+ return layers
+
+ # Ensures that elements are stacked when setting the initial data.
+ # @param [Array] data Layered data to set for the visualization.
+ setData: (data) ->
+ super(data)
+ @_stackLayers()
+
+ # Finds the correct extent to use for range axes (left and right).
+ # @return [Array] An extent array with the first element equal to 0
+ # and the second element equal to the maximum value amongst the
+ # stacked entries.
+ extent: ->
+ [max, layers] = [0, @getVisibleLayers()]
+ return [0, 0] unless layers.length
+
+ for i in [0...layers[0].values.length]
+ sum = 0
+ for j in [0...layers.length]
+ sum += layers[j].values[i].y
+ max = sum if sum > max
+
+ [0, max]
+
+ layerChanged: ->
+ @_stackLayers()
+ @_prepareLayers(layers) for layers in @_queue
+ super()
diff --git a/debian/missing-sources/epoch/src/time/area.coffee b/debian/missing-sources/epoch/src/time/area.coffee
new file mode 100644
index 0000000..22bf9db
--- /dev/null
+++ b/debian/missing-sources/epoch/src/time/area.coffee
@@ -0,0 +1,80 @@
+
+# Real-time stacked area chart implementation.
+class Epoch.Time.Area extends Epoch.Time.Stack
+ constructor: (@options={}) ->
+ @options.type ?= 'time.area'
+ super(@options)
+ @draw()
+
+ # Sets the appropriate styles to the graphics context given a particular layer.
+ # @param [Object] layer Layer for which to set the styles.
+ setStyles: (layer) ->
+ if layer? && layer.className?
+ styles = @getStyles "g.#{layer.className.replace(/\s/g,'.')} path.area"
+ else
+ styles = @getStyles "g path.area"
+ @ctx.fillStyle = styles.fill
+ if styles.stroke?
+ @ctx.strokeStyle = styles.stroke
+ if styles['stroke-width']?
+ @ctx.lineWidth = styles['stroke-width'].replace('px', '')
+
+ # Draws areas for the chart
+ _drawAreas: (delta=0) ->
+ [y, w, layers] = [@y(), @w(), @getVisibleLayers()]
+
+ for i in [layers.length-1..0]
+ continue unless (layer = layers[i])
+
+ @setStyles layer
+ @ctx.beginPath()
+
+ [j, k, trans] = [@options.windowSize, layer.values.length, @inTransition()]
+ firstX = null
+ while (--j >= -2) and (--k >= 0)
+ entry = layer.values[k]
+ args = [(j+1)*w+delta, y(entry.y + entry.y0)]
+ args[0] += w if trans
+ if i == @options.windowSize - 1
+ @ctx.moveTo.apply @ctx, args
+ else
+ @ctx.lineTo.apply @ctx, args
+
+ if trans
+ borderX = (j+3)*w+delta
+ else
+ borderX = (j+2)*w+delta
+
+ @ctx.lineTo(borderX, @innerHeight())
+ @ctx.lineTo(@width*@pixelRatio+w+delta, @innerHeight())
+ @ctx.closePath()
+ @ctx.fill()
+
+ # Draws strokes for the chart
+ _drawStrokes: (delta=0) ->
+ [y, w, layers] = [@y(), @w(), @getVisibleLayers()]
+
+ for i in [layers.length-1..0]
+ continue unless (layer = layers[i])
+ @setStyles layer
+ @ctx.beginPath()
+
+ [i, k, trans] = [@options.windowSize, layer.values.length, @inTransition()]
+ firstX = null
+ while (--i >= -2) and (--k >= 0)
+ entry = layer.values[k]
+ args = [(i+1)*w+delta, y(entry.y + entry.y0)]
+ args[0] += w if trans
+ if i == @options.windowSize - 1
+ @ctx.moveTo.apply @ctx, args
+ else
+ @ctx.lineTo.apply @ctx, args
+
+ @ctx.stroke()
+
+ # Draws the area chart.
+ draw: (delta=0) ->
+ @clear()
+ @_drawAreas(delta)
+ @_drawStrokes(delta)
+ super()
diff --git a/debian/missing-sources/epoch/src/time/bar.coffee b/debian/missing-sources/epoch/src/time/bar.coffee
new file mode 100644
index 0000000..7bcb7be
--- /dev/null
+++ b/debian/missing-sources/epoch/src/time/bar.coffee
@@ -0,0 +1,48 @@
+
+# Real-time Bar Chart implementation.
+class Epoch.Time.Bar extends Epoch.Time.Stack
+ constructor: (@options={}) ->
+ @options.type ?= 'time.bar'
+ super(@options)
+ @draw()
+
+ # @return [Number] An offset used to align the ticks to the center of the rendered bars.
+ _offsetX: ->
+ 0.5 * @w() / @pixelRatio
+
+ # Sets the styles for the graphics context given a layer class name.
+ # @param [String] className The class name to use when deriving the styles.
+ setStyles: (className) ->
+ styles = @getStyles "rect.bar.#{className.replace(/\s/g,'.')}"
+ @ctx.fillStyle = styles.fill
+
+ if !styles.stroke? or styles.stroke == 'none'
+ @ctx.strokeStyle = 'transparent'
+ else
+ @ctx.strokeStyle = styles.stroke
+
+ if styles['stroke-width']?
+ @ctx.lineWidth = styles['stroke-width'].replace('px', '')
+
+ # Draws the stacked bar chart.
+ draw: (delta=0) ->
+ @clear()
+ [y, w] = [@y(), @w()]
+
+ for layer in @getVisibleLayers()
+ continue unless Epoch.isNonEmptyArray(layer.values)
+ @setStyles(layer.className)
+
+ [i, k, trans] = [@options.windowSize, layer.values.length, @inTransition()]
+ iBoundry = if trans then -1 else 0
+
+ while (--i >= iBoundry) and (--k >= 0)
+ entry = layer.values[k]
+ [ex, ey, ey0] = [i*w+delta, entry.y, entry.y0]
+ ex += w if trans
+ args = [ex+1, y(ey+ey0), w-2, @innerHeight()-y(ey)+0.5*@pixelRatio]
+
+ @ctx.fillRect.apply(@ctx, args)
+ @ctx.strokeRect.apply(@ctx, args)
+
+ super()
diff --git a/debian/missing-sources/epoch/src/time/gauge.coffee b/debian/missing-sources/epoch/src/time/gauge.coffee
new file mode 100644
index 0000000..8efadb2
--- /dev/null
+++ b/debian/missing-sources/epoch/src/time/gauge.coffee
@@ -0,0 +1,209 @@
+
+# Real-time Gauge Visualization. Note: Looks best with a 4:3 aspect ratio (w:h)
+class Epoch.Time.Gauge extends Epoch.Chart.Canvas
+ defaults =
+ type: 'time.gauge'
+ domain: [0, 1]
+ ticks: 10
+ tickSize: 5
+ tickOffset: 5
+ fps: 34
+ format: Epoch.Formats.percent
+
+ optionListeners =
+ 'option:domain': 'domainChanged'
+ 'option:ticks': 'ticksChanged'
+ 'option:tickSize': 'tickSizeChanged'
+ 'option:tickOffset': 'tickOffsetChanged'
+ 'option:format': 'formatChanged'
+
+ # Creates the new gauge chart.
+ # @param [Object] options Options for the gauge chart.
+ # @option options [Array] domain The domain to use when rendering values (default: [0, 1]).
+ # @option options [Integer] ticks Number of ticks to render (default: 10).
+ # @option options [Integer] tickSize The length (in pixels) for each tick (default: 5).
+ # @option options [Integer] tickOffset The number of pixels by which to offset ticks from the outer arc (default: 5).
+ # @option options [Integer] fps The number of animation frames to render per second (default: 34).
+ # @option options [Function] format The formatting function to use when rendering the gauge label
+ # (default: Epoch.Formats.percent).
+ constructor: (@options={}) ->
+ super(@options = Epoch.Util.defaults(@options, defaults))
+ @value = @options.value or 0
+
+ if @options.model
+ @options.model.on 'data:push', => @pushFromModel()
+
+ # SVG Labels Overlay
+ if @el.style('position') != 'absolute' and @el.style('position') != 'relative'
+ @el.style('position', 'relative')
+
+ @svg = @el.insert('svg', ':first-child')
+ .attr('width', @width)
+ .attr('height', @height)
+ .attr('class', 'gauge-labels')
+
+ @svg.style
+ 'position': 'absolute'
+ 'z-index': '1'
+
+ @svg.append('g')
+ .attr('transform', "translate(#{@textX()}, #{@textY()})")
+ .append('text')
+ .attr('class', 'value')
+ .text(@options.format(@value))
+
+ # Animations
+ @animation =
+ interval: null
+ active: false
+ delta: 0
+ target: 0
+
+ @_animate = =>
+ if Math.abs(@animation.target - @value) < Math.abs(@animation.delta)
+ @value = @animation.target
+ clearInterval @animation.interval
+ @animation.active = false
+ else
+ @value += @animation.delta
+
+ @svg.select('text.value').text(@options.format(@value))
+ @draw()
+
+ @onAll optionListeners
+ @draw()
+
+ # Sets the value for the gauge to display and begins animating the guage.
+ # @param [Number] value Value to set for the gauge.
+ update: (value) ->
+ @animation.target = value
+ @animation.delta = (value - @value) / @options.fps
+ unless @animation.active
+ @animation.interval = setInterval @_animate, (1000/@options.fps)
+ @animation.active = true
+
+ # Alias for the <code>update()</code> method.
+ # @param [Number] value Value to set for the gauge.
+ push: (value) ->
+ @update value
+
+ # Responds to a model's 'data:push' event.
+ pushFromModel: ->
+ next = @options.model.getNext(@options.type, @options.dataFormat)
+ @update next
+
+ # @return [Number] The radius for the gauge.
+ radius: -> @getHeight() / 1.58
+
+ # @return [Number] The center position x-coordinate for the gauge.
+ centerX: -> @getWidth() / 2
+
+ # @return [Number] The center position y-coordinate for the gauge.
+ centerY: -> 0.68 * @getHeight()
+
+ # @return [Number] The x-coordinate for the gauge text display.
+ textX: -> @width / 2
+
+ # @return [Number] The y-coordinate for the gauge text display.
+ textY: -> 0.48 * @height
+
+ # @return [Number] The angle to set for the needle given a value within the domain.
+ # @param [Number] value Value to translate into a needle angle.
+ getAngle: (value) ->
+ [a, b] = @options.domain
+ ((value - a) / (b - a)) * (Math.PI + 2*Math.PI/8) - Math.PI/2 - Math.PI/8
+
+ # Sets context styles given a particular selector.
+ # @param [String] selector The selector to use when setting the styles.
+ setStyles: (selector) ->
+ styles = @getStyles selector
+ @ctx.fillStyle = styles.fill
+ @ctx.strokeStyle = styles.stroke
+ @ctx.lineWidth = styles['stroke-width'].replace('px', '') if styles['stroke-width']?
+
+ # Draws the gauge.
+ draw: ->
+ [cx, cy, r] = [@centerX(), @centerY(), @radius()]
+ [tickOffset, tickSize] = [@options.tickOffset, @options.tickSize]
+
+ @clear()
+
+ # Draw Ticks
+ t = d3.scale.linear()
+ .domain([0, @options.ticks])
+ .range([ -(9/8)*Math.PI, Math.PI/8 ])
+
+ @setStyles '.epoch .gauge .tick'
+ @ctx.beginPath()
+ for i in [0..@options.ticks]
+ a = t(i)
+ [c, s] = [Math.cos(a), Math.sin(a)]
+
+ x1 = c * (r-tickOffset) + cx
+ y1 = s * (r-tickOffset) + cy
+ x2 = c * (r-tickOffset-tickSize) + cx
+ y2 = s * (r-tickOffset-tickSize) + cy
+
+ @ctx.moveTo x1, y1
+ @ctx.lineTo x2, y2
+
+ @ctx.stroke()
+
+ # Outer arc
+ @setStyles '.epoch .gauge .arc.outer'
+ @ctx.beginPath()
+ @ctx.arc cx, cy, r, -(9/8)*Math.PI, (1/8)*Math.PI, false
+ @ctx.stroke()
+
+ # Inner arc
+ @setStyles '.epoch .gauge .arc.inner'
+ @ctx.beginPath()
+ @ctx.arc cx, cy, r-10, -(9/8)*Math.PI, (1/8)*Math.PI, false
+ @ctx.stroke()
+
+ @drawNeedle()
+
+ super()
+
+ # Draws the needle.
+ drawNeedle: ->
+ [cx, cy, r] = [@centerX(), @centerY(), @radius()]
+ ratio = @value / @options.domain[1]
+
+ @setStyles '.epoch .gauge .needle'
+ @ctx.beginPath()
+ @ctx.save()
+ @ctx.translate cx, cy
+ @ctx.rotate @getAngle(@value)
+
+ @ctx.moveTo 4 * @pixelRatio, 0
+ @ctx.lineTo -4 * @pixelRatio, 0
+ @ctx.lineTo -1 * @pixelRatio, 19-r
+ @ctx.lineTo 1, 19-r
+ @ctx.fill()
+
+ @setStyles '.epoch .gauge .needle-base'
+ @ctx.beginPath()
+ @ctx.arc 0, 0, (@getWidth() / 25), 0, 2*Math.PI
+ @ctx.fill()
+
+ @ctx.restore()
+
+ # Correctly responds to an <code>option:</code>
+ domainChanged: -> @draw()
+
+ # Correctly responds to an <code>option:</code>
+ ticksChanged: -> @draw()
+
+ # Correctly responds to an <code>option:</code>
+ tickSizeChanged: -> @draw()
+
+ # Correctly responds to an <code>option:</code>
+ tickOffsetChanged: -> @draw()
+
+ # Correctly responds to an <code>option:</code>
+ formatChanged: -> @svg.select('text.value').text(@options.format(@value))
+
+
+
+# "The mother of a million sons... CIVILIZATION!" -- Justice
diff --git a/debian/missing-sources/epoch/src/time/heatmap.coffee b/debian/missing-sources/epoch/src/time/heatmap.coffee
new file mode 100644
index 0000000..d7f2c50
--- /dev/null
+++ b/debian/missing-sources/epoch/src/time/heatmap.coffee
@@ -0,0 +1,261 @@
+
+# Real-time Heatmap Implementation.
+class Epoch.Time.Heatmap extends Epoch.Time.Plot
+ defaults =
+ type: 'time.heatmap'
+ buckets: 10
+ bucketRange: [0, 100]
+ opacity: 'linear'
+ bucketPadding: 2
+ paintZeroValues: false
+ cutOutliers: false
+
+ # Easy to use "named" color functions
+ colorFunctions =
+ root: (value, max) -> Math.pow(value/max, 0.5)
+ linear: (value, max) -> value / max
+ quadratic: (value, max) -> Math.pow(value/max, 2)
+ cubic: (value, max) -> Math.pow(value/max, 3)
+ quartic: (value, max) -> Math.pow(value/max, 4)
+ quintic: (value, max) -> Math.pow(value/max, 5)
+
+ optionListeners =
+ 'option:buckets': 'bucketsChanged'
+ 'option:bucketRange': 'bucketRangeChanged'
+ 'option:opacity': 'opacityChanged'
+ 'option:bucketPadding': 'bucketPaddingChanged'
+ 'option:paintZeroValues': 'paintZeroValuesChanged'
+ 'option:cutOutliers': 'cutOutliersChanged'
+
+ # Creates a new heatmap.
+ # @param [Object] options Options for the heatmap.
+ # @option options [Integer] buckets Number of vertical buckets to use when normalizing the
+ # incoming histogram data for visualization in the heatmap (default: 10).
+ # @option options [Array] bucketRange A range of acceptable values to be bucketed (default: [0, 100]).
+ # @option options [String, Function] opacity The opacity coloring function to use when rendering buckets
+ # in a column. The built-in functions (referenced by string) are: 'root', 'linear', 'quadratic', 'cubic',
+ # 'quartic', and 'quintic'. A custom function can be supplied given it accepts two parameters (value, max)
+ # and returns a numeric value from 0 to 1. Default: linear.
+ # @option options [Number] bucketPadding Amount of padding to apply around buckets (default: 2).
+ constructor: (@options={}) ->
+ super(@options = Epoch.Util.defaults(@options, defaults))
+ @_setOpacityFunction()
+ @_setupPaintCanvas()
+ @onAll optionListeners
+ @draw()
+
+ _setOpacityFunction: ->
+ if Epoch.isString(@options.opacity)
+ @_opacityFn = colorFunctions[@options.opacity]
+ Epoch.exception "Unknown coloring function provided '#{@options.opacity}'" unless @_opacityFn?
+ else if Epoch.isFunction(@options.opacity)
+ @_opacityFn = @options.opacity
+ else
+ Epoch.exception "Unknown type for provided coloring function."
+
+ # Prepares initially set data for rendering.
+ # @param [Array] data Layered histogram data for the visualization.
+ setData: (data) ->
+ super(data)
+ for layer in @data
+ layer.values = layer.values.map((entry) => @_prepareEntry(entry))
+
+ # Distributes the full histogram in the entry into the defined buckets
+ # for the visualization.
+ # @param [Object] entry Entry to prepare for visualization.
+ _getBuckets: (entry) ->
+ prepared =
+ time: entry.time
+ max: 0
+ buckets: (0 for i in [0...@options.buckets])
+
+ # Bucket size = (Range[1] - Range[0]) / number of buckets
+ bucketSize = (@options.bucketRange[1] - @options.bucketRange[0]) / @options.buckets
+
+ for own value, count of entry.histogram
+ index = parseInt((value - @options.bucketRange[0]) / bucketSize)
+
+ # Remove outliers from the preprared buckets if instructed to do so
+ if @options.cutOutliers and ((index < 0) or (index >= @options.buckets))
+ continue
+
+ # Bound the histogram to the range (aka, handle out of bounds values)
+ if index < 0
+ index = 0
+ else if index >= @options.buckets
+ index = @options.buckets - 1
+
+ prepared.buckets[index] += parseInt count
+
+ for i in [0...prepared.buckets.length]
+ prepared.max = Math.max(prepared.max, prepared.buckets[i])
+
+ return prepared
+
+ # @return [Function] The y scale for the heatmap.
+ y: ->
+ d3.scale.linear()
+ .domain(@options.bucketRange)
+ .range([@innerHeight(), 0])
+
+ # @return [Function] The y scale for the svg portions of the heatmap.
+ ySvg: ->
+ d3.scale.linear()
+ .domain(@options.bucketRange)
+ .range([@innerHeight() / @pixelRatio, 0])
+
+ # @return [Number] The height to render each bucket in a column (disregards padding).
+ h: ->
+ @innerHeight() / @options.buckets
+
+ # @return [Number] The offset needed to center ticks at the middle of each column.
+ _offsetX: ->
+ 0.5 * @w() / @pixelRatio
+
+ # Creates the painting canvas which is used to perform all the actual drawing. The contents
+ # of the canvas are then copied into the actual display canvas and through some image copy
+ # trickery at the end of a transition the illusion of motion over time is preserved.
+ #
+ # Using two canvases in this way allows us to render an incredible number of buckets in the
+ # visualization and animate them at high frame rates without smashing the cpu.
+ _setupPaintCanvas: ->
+ # Size the paint canvas to have a couple extra columns so we can perform smooth transitions
+ @paintWidth = (@options.windowSize + 1) * @w()
+ @paintHeight = @height * @pixelRatio
+
+ # Create the "memory only" canvas and nab the drawing context
+ @paint = document.createElement('CANVAS')
+ @paint.width = @paintWidth
+ @paint.height = @paintHeight
+ @p = Epoch.Util.getContext @paint
+
+ # Paint the initial data (rendering backwards from just before the fixed paint position)
+ @redraw()
+
+ # Hook into the events to paint the next row after it's been shifted into the data
+ @on 'after:shift', '_paintEntry'
+
+ # At the end of a transition we must reset the paint canvas by shifting the viewable
+ # buckets to the left (this allows for a fixed cut point and single renders below in @draw)
+ @on 'transition:end', '_shiftPaintCanvas'
+ @on 'transition:end', => @draw(@animation.frame * @animation.delta())
+
+ # Redraws the entire heatmap for the current data.
+ redraw: ->
+ return unless Epoch.isNonEmptyArray(@data) and Epoch.isNonEmptyArray(@data[0].values)
+ entryIndex = @data[0].values.length
+ drawColumn = @options.windowSize
+
+ # This addresses a strange off-by-one issue when the chart is transitioning
+ drawColumn++ if @inTransition()
+
+ while (--entryIndex >= 0) and (--drawColumn >= 0)
+ @_paintEntry(entryIndex, drawColumn)
+ @draw(@animation.frame * @animation.delta())
+
+ # Computes the correct color for a given bucket.
+ # @param [Integer] value Normalized value at the bucket.
+ # @param [Integer] max Normalized maximum for the column.
+ # @param [String] color Computed base color for the bucket.
+ _computeColor: (value, max, color) ->
+ Epoch.Util.toRGBA(color, @_opacityFn(value, max))
+
+ # Paints a single entry column on the paint canvas at the given column.
+ # @param [Integer] entryIndex Index of the entry to paint.
+ # @param [Integer] drawColumn Column on the paint canvas to place the visualized entry.
+ _paintEntry: (entryIndex=null, drawColumn=null) ->
+ [w, h] = [@w(), @h()]
+
+ entryIndex ?= @data[0].values.length - 1
+ drawColumn ?= @options.windowSize
+
+ entries = []
+ bucketTotals = (0 for i in [0...@options.buckets])
+ maxTotal = 0
+
+ for layer in @getVisibleLayers()
+ entry = @_getBuckets( layer.values[entryIndex] )
+ for own bucket, count of entry.buckets
+ bucketTotals[bucket] += count
+ maxTotal += entry.max
+ styles = @getStyles ".#{layer.className.split(' ').join('.')} rect.bucket"
+ entry.color = styles.fill
+ entries.push entry
+
+ xPos = drawColumn * w
+
+ @p.clearRect xPos, 0, w, @paintHeight
+
+ j = @options.buckets
+
+ for own bucket, sum of bucketTotals
+ color = @_avgLab(entries, bucket)
+ max = 0
+ for entry in entries
+ max += (entry.buckets[bucket] / sum) * maxTotal
+ if sum > 0 or @options.paintZeroValues
+ @p.fillStyle = @_computeColor(sum, max, color)
+ @p.fillRect xPos, (j-1) * h, w-@options.bucketPadding, h-@options.bucketPadding
+ j--
+
+ # This shifts the image contents of the paint canvas to the left by 1 column width.
+ # It is called after a transition has ended (yay, slight of hand).
+ _shiftPaintCanvas: ->
+ data = @p.getImageData @w(), 0, @paintWidth-@w(), @paintHeight
+ @p.putImageData data, 0, 0
+
+ # Performs an averaging of the colors for muli-layer heatmaps using the lab color space.
+ # @param [Array] entries The layers for which the colors are to be averaged.
+ # @param [Number] bucket The bucket in the entries that must be averaged.
+ # @return [String] The css color code for the average of all the layer colors.
+ _avgLab: (entries, bucket) ->
+ [l, a, b, total] = [0, 0, 0, 0]
+ for entry in entries
+ continue unless entry.buckets[bucket]?
+ total += entry.buckets[bucket]
+
+ for own i, entry of entries
+ if entry.buckets[bucket]?
+ value = entry.buckets[bucket]|0
+ else
+ value = 0
+ ratio = value / total
+ color = d3.lab(entry.color)
+ l += ratio * color.l
+ a += ratio * color.a
+ b += ratio * color.b
+
+ d3.lab(l, a, b).toString()
+
+ # Copies the paint canvas onto the display canvas, thus rendering the heatmap.
+ draw: (delta=0) ->
+ @clear()
+ @ctx.drawImage @paint, delta, 0
+ super()
+
+ # Changes the number of buckets in response to an <code>option:buckets</code> event.
+ bucketsChanged: -> @redraw()
+
+ # Changes the range of the buckets in response to an <code>option:bucketRange</code> event.
+ bucketRangeChanged: ->
+ @_transitionRangeAxes()
+ @redraw()
+
+ # Changes the opacity function in response to an <code>option:opacity</code> event.
+ opacityChanged: ->
+ @_setOpacityFunction()
+ @redraw()
+
+ # Changes the bucket padding in response to an <code>option:bucketPadding</code> event.
+ bucketPaddingChanged: -> @redraw()
+
+ # Changes whether or not to paint zeros in response to an <code>option:paintZeroValues</code> event.
+ paintZeroValuesChanged: -> @redraw()
+
+ # Changes whether or not to cut outliers when bucketing in response to an
+ # <code>option:cutOutliers</code> event.
+ cutOutliersChanged: -> @redraw()
+
+ layerChanged: -> @redraw()
+
+# "Audio... Audio... Audio... Video Disco..." - Justice
diff --git a/debian/missing-sources/epoch/src/time/line.coffee b/debian/missing-sources/epoch/src/time/line.coffee
new file mode 100644
index 0000000..8a4271b
--- /dev/null
+++ b/debian/missing-sources/epoch/src/time/line.coffee
@@ -0,0 +1,39 @@
+
+# Real-time line chart implementation
+class Epoch.Time.Line extends Epoch.Time.Plot
+ constructor: (@options={}) ->
+ @options.type ?= 'time.line'
+ super(@options)
+ @draw()
+
+ # Sets the graphics context styles based ont he given layer class name.
+ # @param [String] className The class name of the layer for which to set the styles.
+ setStyles: (className) ->
+ styles = @getStyles "g.#{className.replace(/\s/g,'.')} path.line"
+ @ctx.fillStyle = styles.fill
+ @ctx.strokeStyle = styles.stroke
+ @ctx.lineWidth = @pixelRatio * styles['stroke-width'].replace('px', '')
+
+ # Draws the line chart.
+ draw: (delta=0) ->
+ @clear()
+ w = @w()
+ for layer in @getVisibleLayers()
+ continue unless Epoch.isNonEmptyArray(layer.values)
+ @setStyles(layer.className)
+ @ctx.beginPath()
+ y = @y(layer.range)
+ [i, k, trans] = [@options.windowSize, layer.values.length, @inTransition()]
+
+ while (--i >= -2) and (--k >= 0)
+ entry = layer.values[k]
+ args = [(i+1)*w+delta, y(entry.y)]
+ args[0] += w if trans
+ if i == @options.windowSize - 1
+ @ctx.moveTo.apply @ctx, args
+ else
+ @ctx.lineTo.apply @ctx, args
+
+ @ctx.stroke()
+
+ super()