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.
else # - If this point is the first, last, sufficiently far from the previous,
values.push value # or there are no more carried annotations, add a bucket marker at this
acc # point.
, # - Otherwise, if the last bucket was not isolated (the one before it
values: [] # has at least one annotation) then remove it and ensure that its
arrays: [] # annotations and the carried annotations are merged into the previous
annotations = d3.merge annotations.arrays # bucket.
{@buckets, @index} = points
if d > 0 .sort(this._collate)
# if this is a +1 control point, (re-)include the current annotation .reduce ({buckets, index, carry}, [x, d, a], i, points) =>
# by removing and then adding, duplicates are easily avoided if d > 0 # Add annotation
annotations.push a if (j = carry.annotations.indexOf a) < 0
buckets.push annotations carry.annotations.unshift a
index.push x carry.counts.unshift 1
else else
# if this is a -1 control point, exclude the current annotation carry.counts[j]++
buckets.push annotations else # Remove annotation
index.push x j = carry.annotations.indexOf a # XXX: assert(i >= 0)
if --carry.counts[j] is 0
carry.annotations.splice j, 1
carry.counts.splice j, 1
if (
(index.length is 0 or i is points.length - 1) or # First or last?
carry.annotations.length is 0 or # A zero marker?
x - index[index.length-1] > 180 # A large gap?
) # Mark a new bucket.
buckets.push carry.annotations.slice()
index.push x
else
# Merge the previous bucket, making sure its predecessor contains
# all the carried annotations and the annotations in the previous
# 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