summaryrefslogtreecommitdiffstats
path: root/debian/missing-sources/epoch/src/time/heatmap.coffee
diff options
context:
space:
mode:
Diffstat (limited to 'debian/missing-sources/epoch/src/time/heatmap.coffee')
-rw-r--r--debian/missing-sources/epoch/src/time/heatmap.coffee261
1 files changed, 261 insertions, 0 deletions
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