diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-06 00:55:53 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-06 00:55:53 +0000 |
commit | 129b974b59c74140570847bb4a2774d41d1e5fae (patch) | |
tree | f2dc68e7186b8157e33aebbc2526e016912ded82 /debian/missing-sources/epoch/src | |
parent | Adding upstream version 3.2.1. (diff) | |
download | knot-resolver-debian/3.2.1-3.tar.xz knot-resolver-debian/3.2.1-3.zip |
Adding debian version 3.2.1-3.debian/3.2.1-3
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'debian/missing-sources/epoch/src')
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() |