Skip to content

Commit

Permalink
Add an option to have approximate Gaussian points instead of solid po…
Browse files Browse the repository at this point in the history
…ints with a blur.
  • Loading branch information
manthey committed May 16, 2016
1 parent f24c8f9 commit f8f6496
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 28 deletions.
3 changes: 3 additions & 0 deletions examples/heatmap/index.jade
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ block append mainContent
.form-group(title="Radius of blur around points in pixels.")
label(for="blurRadius") Blur Radius
input#blurRadius(type="number" placeholder="15")
.form-group(title="Use either a Gaussian distribution or a solid circle with a blurred edge for each point. If a Guassian is used, the total radius is the sume of the radius and blur radius values.")
label(for="gaussian") Gaussian Points
input#gaussian(type="checkbox", placeholder="true", checked="checked")
.form-group(title="Color Gradient. Entries with intensities of 0 and 1 are needed to form a valid color gradient.")
label Color Gradient
table.gradient
Expand Down
19 changes: 14 additions & 5 deletions examples/heatmap/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ $(function () {
case 'dataset':
ctlvalue = value ? value : 'adderall';
break;
case 'gaussian':
ctlvalue = value === 'true';
heatmapOptions.style[key] = value;
break;
case 'gradient':
var parts = value.split(',').map(parseFloat);
if (parts.length >= 5) {
Expand Down Expand Up @@ -82,11 +86,6 @@ $(function () {
points = ctlvalue = parseInt(value, 10);
}
break;
case 'updateDelay':
if (value.length) {
heatmapOptions[key] = ctlvalue = parseInt(value, 10);
}
break;
case 'radius': case 'blurRadius':
if (value.length) {
value = parseFloat(value);
Expand All @@ -95,6 +94,11 @@ $(function () {
}
}
break;
case 'updateDelay':
if (value.length) {
heatmapOptions[key] = ctlvalue = parseInt(value, 10);
}
break;
// add gaussian and binning when they are added as features
}
if (ctlvalue !== undefined) {
Expand Down Expand Up @@ -216,6 +220,11 @@ $(function () {
case 'dataset':
fetch_data();
break;
case 'gaussian':
heatmapOptions.style[param] = processedValue;
heatmap.style(param, processedValue);
map.draw();
break;
case 'gradient':
var gradient = {};
for (var idx = 1; idx <= 6; idx += 1) {
Expand Down
57 changes: 44 additions & 13 deletions src/canvas/heatmapFeature.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,27 +96,58 @@ var canvas_heatmapFeature = function (arg) {
*/
////////////////////////////////////////////////////////////////////////////
this._createCircle = function () {
var circle, ctx, r, r2, blur;
var circle, ctx, r, r2, blur, gaussian;
r = m_this.style('radius');
blur = m_this.style('blurRadius');
if (!m_this._circle || m_this._circle.radius !== r ||
m_this._circle.blurRadius !== blur) {
gaussian = m_this.style('gaussian');
if (!m_this._circle || m_this._circle.gaussian !== gaussian ||
m_this._circle.radius !== r || m_this._circle.blurRadius !== blur) {
circle = m_this._circle = document.createElement('canvas');
ctx = circle.getContext('2d');

r2 = blur + r;

circle.width = circle.height = r2 * 2;
ctx.shadowOffsetX = ctx.shadowOffsetY = r2 * 2;
ctx.shadowBlur = blur;
ctx.shadowColor = 'black';

ctx.beginPath();
ctx.arc(-r2, -r2, r, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
if (!gaussian) {
ctx.shadowOffsetX = ctx.shadowOffsetY = r2 * 2;
ctx.shadowBlur = blur;
ctx.shadowColor = 'black';
ctx.beginPath();
ctx.arc(-r2, -r2, r, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
} else {
/* This approximates a gaussian distribution by using a 10-step
* piecewise linear radial gradient. Strictly, it should not stop at
* the radius, but should be attenuated further. The scale has been
* selected such that the values at the radius are around 1/256th of
* the maximum, and therefore would not be visible using an 8-bit alpha
* channel for the summation. The values for opacity were generated by
* the python expression:
* from scipy.stats import norm
* for r in [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]:
* opacity = norm.pdf(r, scale=0.3) / norm.pdf(0, scale=0.3)
* Usng a 10-interval approximation is accurate to within 0.5% of the
* actual Gaussian magnitude. Switching to a 20-interval approximation
* would get within 0.1%, at which point there is more error from using
* a Gaussian truncated at the radius than from the approximation.
*/
var grad = ctx.createRadialGradient(r2, r2, 0, r2, r2, r2);
grad.addColorStop(0.0, 'rgba(255,255,255,1)');
grad.addColorStop(0.1, 'rgba(255,255,255,0.946)');
grad.addColorStop(0.2, 'rgba(255,255,255,0.801)');
grad.addColorStop(0.3, 'rgba(255,255,255,0.607)');
grad.addColorStop(0.4, 'rgba(255,255,255,0.411)');
grad.addColorStop(0.5, 'rgba(255,255,255,0.249)');
grad.addColorStop(0.6, 'rgba(255,255,255,0.135)');
grad.addColorStop(0.7, 'rgba(255,255,255,0.066)');
grad.addColorStop(0.8, 'rgba(255,255,255,0.029)');
grad.addColorStop(0.9, 'rgba(255,255,255,0.011)');
grad.addColorStop(1.0, 'rgba(255,255,255,0)');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, r2 * 2, r2 * 2);
}
circle.radius = r;
circle.blurRadius = blur;
circle.gaussian = gaussian;
m_this._circle = circle;
}
return m_this;
Expand Down
20 changes: 13 additions & 7 deletions src/heatmapFeature.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,6 @@ var transform = require('./transform');
* @class
* @param {Object} arg Options object
* @extends geo.feature
* @param {Object|string|Function} [color] Color transfer function that.
* will be used to evaluate color of each pixel using normalized intensity
* as the look up value.
* @param {Object|Function} [radius=10] Radius of a point in terms of number
* of pixels.
* @param {Object|Function} [blurRadius=10] Gaussian blur radius for each
* point in terms of number of pixels.
* @param {Object|Function} [position] Position of the data. Default is
* (data). The position is an Object which specifies the location of the
* data in geo-spatial context.
Expand All @@ -33,6 +26,18 @@ var transform = require('./transform');
* be computed.
* @param {number} [updateDelay=1000] Delay in milliseconds after a zoom,
* rotate, or pan event before recomputing the heatmap.
* @param {Object|string|Function} [style.color] Color transfer function that.
* will be used to evaluate color of each pixel using normalized intensity
* as the look up value.
* @param {Object|Function} [style.radius=10] Radius of a point in terms of
* number of pixels.
* @param {Object|Function} [style.blurRadius=10] Blur radius for each point in
* terms of number of pixels.
* @param {boolean} [style.gaussian=true] If true, appoximate a gaussian
* distribution for each point using a multi-segment linear radial
* appoximation. The total weight of the gaussian area is approximately the
* 9/16 r^2. The sum of radius + blurRadius is used as the radius for the
* gaussian distribution.
* @returns {geo.heatmapFeature}
*/
//////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -179,6 +184,7 @@ var heatmapFeature = function (arg) {
{
radius: 10,
blurRadius: 10,
gaussian: true,
color: {0: {r: 0, g: 0, b: 0.0, a: 0.0},
0.25: {r: 0, g: 0, b: 1, a: 0.5},
0.5: {r: 0, g: 1, b: 1, a: 0.6},
Expand Down
31 changes: 28 additions & 3 deletions tests/cases/heatmap.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('canvas heatmap feature', function () {
map.resize(0, 0, width, height);
});

it('Add features to a layer', function () {
it('Add feature to a layer', function () {
feature1 = layer.createFeature('heatmap')
.data(testData)
.intensity(function (d) {
Expand All @@ -55,7 +55,7 @@ describe('canvas heatmap feature', function () {
map.draw();
stepAnimationFrame(new Date().getTime());
expect(layer.children().length).toBe(1);
unmockAnimationFrame();
// leave animation frames mocked for later tests.
});

it('Validate selection API option', function () {
Expand Down Expand Up @@ -103,11 +103,36 @@ describe('canvas heatmap feature', function () {
clock.tick(2000);
expect(feature1.buildTime().getMTime()).toBe(buildTime);
});
it('radius, blurRadius, and gaussian', function () {
// animation frames are already mocked
expect(feature1._circle.radius).toBe(5);
expect(feature1._circle.blurRadius).toBe(15);
expect(feature1._circle.gaussian).toBe(true);
expect(feature1._circle.width).toBe(40);
expect(feature1._circle.height).toBe(40);
feature1.style('gaussian', false);
map.draw();
stepAnimationFrame(new Date().getTime());
expect(feature1._circle.gaussian).toBe(false);
feature1.style('radius', 10);
expect(feature1._circle.radius).toBe(5);
map.draw();
stepAnimationFrame(new Date().getTime());
expect(feature1._circle.radius).toBe(10);
expect(feature1._circle.width).toBe(50);
expect(feature1._circle.height).toBe(50);
feature1.style('blurRadius', 0);
map.draw();
stepAnimationFrame(new Date().getTime());
expect(feature1._circle.blurRadius).toBe(0);
expect(feature1._circle.width).toBe(20);
expect(feature1._circle.height).toBe(20);
unmockAnimationFrame();
});
it('Remove a feature from a layer', function () {
layer.deleteFeature(feature1).draw();
expect(layer.children().length).toBe(0);
});

});

describe('core.heatmapFeature', function () {
Expand Down

0 comments on commit f8f6496

Please sign in to comment.