Commit 667f3d72 authored by Randall Leeds's avatar Randall Leeds

much improved heatmap code

Merging buckets is now done in the initial pass. The entire heatmap
is calculated in two passes. The first creates the buckets while the
second counts the replies.
parent d8cb4768
...@@ -59,7 +59,7 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin ...@@ -59,7 +59,7 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin
below = [] below = []
# Construct control points for the heatmap highlights # Construct control points for the heatmap highlights
points = $.map highlights, (hl, i) => points = highlights.reduce (points, hl, i) =>
x = hl.offset.top - wrapper.offset().top - offset x = hl.offset.top - wrapper.offset().top - offset
h = hl.height h = hl.height
d = hl.data d = hl.data
...@@ -69,81 +69,73 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin ...@@ -69,81 +69,73 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin
else if x + h >= $(window).height() - @BUCKET_SIZE else if x + h >= $(window).height() - @BUCKET_SIZE
if d not in below then below.push d if d not in below then below.push d
else else
return [ points.push [x, 1, d]
[x, 1, d] points.push [x + h, -1, d]
[x + h, -1, d] points
] , []
return []
# Accumulate the overlapping annotations into buckets.
# Accumulate the overlapping annotations into buckets # The algorithm goes like this:
{@buckets, @index} = points.sort(this._collate) # - Collate the points by sorting on position then delta (+1 or -1)
.reduce ({annotations, buckets, index}, [x, d, a], i, points) => # - Reduce over the sorted points
# - For +1 points, add the annotation at this point to an array of
# remove all instances of this annotation from the accumulator # "carried" annotations. If it already exists, increase the
annotations = annotations.reduce (acc, value) -> # corresponding value in an array of counts which maintains the
{values, arrays} = acc # number of points that include this annotation.
if value is a # - For -1 points, decrement the value for the annotation at this point
arrays.push values # in the carried array of counts. If the count is now zero, remove the
acc.values = [] # annotation from the carried array of annotations.
# - If this point is the first, last, sufficiently far from the previous,
# or there are no more carried annotations, add a bucket marker at this
# point.
# - Otherwise, if the last bucket was not isolated (the one before it
# has at least one annotation) then remove it and ensure that its
# annotations and the carried annotations are merged into the previous
# bucket.
{@buckets, @index} = points
.sort(this._collate)
.reduce ({buckets, index, carry}, [x, d, a], i, points) =>
if d > 0 # Add annotation
if (j = carry.annotations.indexOf a) < 0
carry.annotations.unshift a
carry.counts.unshift 1
else else
values.push value carry.counts[j]++
acc else # Remove annotation
, j = carry.annotations.indexOf a # XXX: assert(i >= 0)
values: [] if --carry.counts[j] is 0
arrays: [] carry.annotations.splice j, 1
annotations = d3.merge annotations.arrays carry.counts.splice j, 1
if d > 0 if (
# if this is a +1 control point, (re-)include the current annotation (index.length is 0 or i is points.length - 1) or # First or last?
# by removing and then adding, duplicates are easily avoided carry.annotations.length is 0 or # A zero marker?
annotations.push a x - index[index.length-1] > 180 # A large gap?
buckets.push annotations ) # Mark a new bucket.
buckets.push carry.annotations.slice()
index.push x index.push x
else else
# if this is a -1 control point, exclude the current annotation # Merge the previous bucket, making sure its predecessor contains
buckets.push annotations # all the carried annotations and the annotations in the previous
index.push x # bucket.
if buckets[buckets.length-2]?.length
last = buckets[buckets.length-2]
toMerge = buckets.pop()
index.pop()
else
last = buckets[buckets.length-1]
toMerge = []
last.push a0 for a0 in carry.annotations when a0 not in last
last.push a0 for a0 in toMerge when a0 not in last
{annotations, buckets, index} {buckets, index, carry}
, ,
annotations: []
buckets: [] buckets: []
index: [] index: []
carry:
# Remove redundant points and merge close buckets until done annotations: []
while @buckets.length > 2 counts: []
# Find the two closest points latest: 0
small = 0
threshold = min = 60
for i in [0..@index.length-2]
# Don't merge empty with non-empty buckets
if @buckets[i].length and not @buckets[i+1].length
continue
# Maintain the index of the smallest delta
if (w = @index[i+1] - @index[i]) < min
small = i
min = w
break if min == 0 # short-circuit optimization
# Merge them if they are close enough
if min < threshold
# Prefer merging the successor bucket backward but not if it's last
# since the gradient must always return to 0 at the end
if @buckets[small+2]?
from = small + 1
to = small
for b in @buckets[from]
@buckets[to].push b if b not in @buckets[to]
else
from = small
# Drop the merged bucket and index
@buckets.splice(from, 1)
@index.splice(from, 1)
else
break
# Add the scroll buckets # Add the scroll buckets
@buckets.unshift above, [] @buckets.unshift above, []
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment