Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve heatmap documentation. #859

Merged
merged 2 commits into from
Jul 12, 2018
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 43 additions & 58 deletions src/canvas/heatmapFeature.js
Original file line number Diff line number Diff line change
@@ -2,15 +2,16 @@ var inherit = require('../inherit');
var registerFeature = require('../registry').registerFeature;
var heatmapFeature = require('../heatmapFeature');
var timestamp = require('../timestamp');
var util = require('../util');

/**
* Create a new instance of class heatmapFeature
* Create a new instance of class canvas.heatmapFeature.
* Inspired from
* https://github.com/mourner/simpleheat/blob/gh-pages/simpleheat.js
* https://github.com/mourner/simpleheat/blob/gh-pages/simpleheat.js .
*
* @class
* @alias geo.canvas.heatmapFeature
* @param {Object} arg Options object
* @param {geo.heatmapFeature.spec} arg
* @extends geo.heatmapFeature
* @returns {canvas_heatmapFeature}
*/
@@ -36,30 +37,14 @@ var canvas_heatmapFeature = function (arg) {
m_typedBufferData,
m_heatMapPosition,
m_heatMapTransform,
s_exit = this._exit,
s_init = this._init,
s_update = this._update,
m_renderTime = timestamp();

/**
* Meta functions for converting from geojs styles to canvas.
* @private
*/
this._convertColor = function (c) {
var color;
if (c.hasOwnProperty('r') &&
c.hasOwnProperty('g') &&
c.hasOwnProperty('b') &&
c.hasOwnProperty('a')) {
color = 'rgba(' + 255 * c.r + ',' + 255 * c.g + ','
+ 255 * c.b + ',' + c.a + ')';
}
return color;
};

/**
* Compute gradient (color lookup table)
* @protected
* Compute gradient. This creates a color lookup table.
*
* @returns {this}
*/
this._computeGradient = function () {
var canvas, stop, context2d, gradient, colors;
@@ -74,7 +59,7 @@ var canvas_heatmapFeature = function (arg) {
canvas.height = 256;

for (stop in colors) {
gradient.addColorStop(stop, m_this._convertColor(colors[stop]));
gradient.addColorStop(stop, util.convertColorToRGBA(colors[stop]));
}

context2d.fillStyle = gradient;
@@ -87,8 +72,9 @@ var canvas_heatmapFeature = function (arg) {
};

/**
* Create circle for each data point
* @protected
* Create circle for each data point.
*
* @returns {this}
*/
this._createCircle = function () {
var circle, ctx, r, r2, blur, gaussian;
@@ -149,7 +135,11 @@ var canvas_heatmapFeature = function (arg) {
};

/**
* Compute color for each pixel on the screen
* Compute color for each pixel on the screen.
*
* @param {Uint8ClampedArray} pixels A 2D canvas `getImageData` buffer.
* @param {Uint8ClampedArray} gradient A 2D canvas with 256 pixels that
* contain a color gradient.
* @protected
*/
this._colorize = function (pixels, gradient) {
@@ -174,11 +164,11 @@ var canvas_heatmapFeature = function (arg) {

/**
* Render individual data points on the canvas.
* @protected
* @param {object} context2d the canvas context to draw in.
* @param {object} map the parent map object.
* @param {Array} data the main data array.
* @param {number} radius the sum of radius and blurRadius.
*
* @param {RenderingContext} context2d The canvas context to draw in.
* @param {geo.map} map The parent map object.
* @param {array} data The main data array.
* @param {number} radius The sum of `radius` and `blurRadius`.
*/
this._renderPoints = function (context2d, map, data, radius) {
var position = m_this.gcsPosition(),
@@ -189,7 +179,7 @@ var canvas_heatmapFeature = function (arg) {

for (idx = data.length - 1; idx >= 0; idx -= 1) {
pos = map.worldToDisplay(position[idx]);
intensity = (intensityFunc(data[idx]) - minIntensity) / rangeIntensity;
intensity = (intensityFunc(data[idx], idx) - minIntensity) / rangeIntensity;
if (intensity <= 0) {
continue;
}
@@ -202,12 +192,12 @@ var canvas_heatmapFeature = function (arg) {

/**
* Render data points on the canvas by binning.
* @protected
* @param {object} context2d the canvas context to draw in.
* @param {object} map the parent map object.
* @param {Array} data the main data array.
* @param {number} radius the sum of radius and blurRadius.
* @param {number} binSize size of the bins in pixels.
*
* @param {RenderingContext} context2d The canvas context to draw in.
* @param {geo.map} map The parent map object.
* @param {array} data The main data array.
* @param {number} radius The sum of `radius` and `blurRadius`.
* @param {number} binSize Size of the bins in pixels.
*/
this._renderBinnedData = function (context2d, map, data, radius, binSize) {
var position = m_this.gcsPosition(),
@@ -249,7 +239,7 @@ var canvas_heatmapFeature = function (arg) {
if (y < 0 || y >= maxy) {
continue;
}
intensity = (intensityFunc(data[idx]) - minIntensity) / rangeIntensity;
intensity = (intensityFunc(data[idx], idx) - minIntensity) / rangeIntensity;
if (intensity <= 0) {
continue;
}
@@ -300,9 +290,10 @@ var canvas_heatmapFeature = function (arg) {

/**
* Render the data on the canvas, then colorize the resulting opacity map.
* @protected
* @param {object} context2d the canvas context to draw in.
* @param {object} map the parent map object.
*
* @param {RenderingContext} context2d The canvas context to draw in.
* @param {geo.map} map The parent map object.
* @returns {this}
*/
this._renderOnCanvas = function (context2d, map) {

@@ -365,8 +356,9 @@ var canvas_heatmapFeature = function (arg) {
};

/**
* Initialize
* @protected
* Initialize.
*
* @returns {this}
*/
this._init = function () {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we do not take spec for heatMap? Also, looks like we do not have arg passed here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The args are passed when we create the feature. They aren't repassed to _init. It works out the same, but is a different code format from other features.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, so the arg to s_init would be meaningless (not looking at the code right now)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be the arg that was passed when creating the feature, so it is used. It just has a different scope.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the feature is instantiated, we call m_this._init(arg);, where arg doesn't get used as passed, since the canvas _init doesn't take it. The canvas's _init then calls s_init with the feature-scoped arg (rather than the function scoped arg).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that this is inconsistent with other features, but this feature, in general, has some inconsistencies (styles have to be values rather than values or functions, for instance). I was mostly trying to update the documentation in this PR. Other improvements should be separate PRs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, yes the arg is passed to the feature will be used. Thanks, I understand the original intent. Should we create an issue to make this feature consistent just so that we do not forget it? I am fine with fixing it in some other PR as that work is orthogonal to this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See issue #865.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks!

s_init.call(m_this, arg);
@@ -377,8 +369,9 @@ var canvas_heatmapFeature = function (arg) {
};

/**
* Update
* @protected
* Update the feature.
*
* @returns {this}
*/
this._update = function () {
s_update.call(m_this);
@@ -392,7 +385,8 @@ var canvas_heatmapFeature = function (arg) {

/**
* Update the css transform for the layer as part of an animation frame.
* @protected
* This allows an existing rendered version of the heatmap to appear with a
* transform until a new version can be computed.
*/
this._setTransform = function () {
if (m_this.layer() && m_this.layer().canvas() && m_this.layer().canvas()[0]) {
@@ -401,10 +395,9 @@ var canvas_heatmapFeature = function (arg) {
};

/**
* Animate pan (and zoom)
* @protected
* Animate pan and zoom.
*/
this._animatePan = function (e) {
this._animatePan = function () {

if (!m_heatMapPosition) {
return;
@@ -455,14 +448,6 @@ var canvas_heatmapFeature = function (arg) {
}
};

/**
* Destroy
* @protected
*/
this._exit = function () {
s_exit.call(m_this);
};

m_this._init(arg);
return this;
};
143 changes: 85 additions & 58 deletions src/heatmapFeature.js
Original file line number Diff line number Diff line change
@@ -4,47 +4,48 @@ var feature = require('./feature');
var transform = require('./transform');

/**
* Create a new instance of class heatmapFeature
* Heatmap feature specification.
*
* @class
* @alias geo.heatmapFeature
* @param {Object} arg Options object
* @extends geo.feature
* @param {Object|Function} [position] Position of the data. Default is
* (data).
* @param {Object|Function} [intensity] Scalar value of each data point. Scalar
* value must be a positive real number and will be used to compute
* the weight for each data point.
* @param {number} [maxIntensity=null] Maximum intensity of the data. Maximum
* intensity must be a positive real number and will be used to normalize all
* intensities with a dataset. If no value is given, then a it will
* be computed.
* @param {number} [minIntensity=null] Minimum intensity of the data. Minimum
* intensity must be a positive real number will be used to normalize all
* intensities with a dataset. If no value is given, then a it will
* be computed.
* @typedef {geo.feature.spec} geo.heatmapFeature.spec
* @property {geo.geoPosition|function} [position] Position of the data.
* Default is (data).
* @param {function} [intensity] Scalar value of each data point. The scalar
* value must be a positive real number and is used to compute the weight
* for each data point.
* @param {number} [maxIntensity=null] Maximum intensity of the data. Maximum
* intensity must be a positive real number and is used to normalize all
* intensities within a dataset. If `null`, it is computed.
* @param {number} [minIntensity=null] Minimum intensity of the data. Minimum
* intensity must be a positive real number and is used to normalize all
* intensities within a dataset. If `null`, it is computed.
* @param {number} [updateDelay=1000] Delay in milliseconds after a zoom,
* rotate, or pan event before recomputing the heatmap.
* @param {boolean|number|'auto'} [binned='auto'] If true or a number,
* spatially bin data as part of producing the heatpmap. If false, each
* datapoint stands on its own. If 'auto', bin data if there are more data
* points than there would be bins. Using true or auto uses bins that are
* max(Math.floor((radius + blurRadius) / 8), 3).
* @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
* @param {boolean|number|'auto'} [binned='auto'] If `true` or a number,
* spatially bin data as part of producing the heatmap. If falsy, each
* datapoint stands on its own. If `'auto'`, bin data if there are more data
* points than there would be bins. Using `true` or `auto` uses bins that
* are `max(Math.floor((radius + blurRadius) / 8), 3)`.
* @param {object} [style.color] An object where the keys are numbers from
* [0-1] and the values are {@link geo.geoColor}. This is used to transform
* normalized intensity.
* @param {number} [style.radius=10] Radius of a point in pixels.
* @param {number} [style.blurRadius=10] Blur radius for each point in pixels.
* @param {boolean} [style.gaussian=true] If truthy, 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}
* `9/16 r^2`. The sum of `radius + blurRadius` is used as the radius for
* the gaussian distribution.
*/

/**
* Create a new instance of class heatmapFeature.
*
* @class
* @alias geo.heatmapFeature
* @param {geo.heatmapFeature.spec} arg
* @extends geo.feature
* @returns {geo.heatmapFeature}
*/
var heatmapFeature = function (arg) {
'use strict';
if (!(this instanceof heatmapFeature)) {
@@ -74,9 +75,12 @@ var heatmapFeature = function (arg) {
m_updateDelay = arg.updateDelay ? parseInt(arg.updateDelay, 10) : 1000;

/**
* Get/Set maxIntensity
* Get/Set maxIntensity.
*
* @returns {geo.heatmap}
* @param {number|null} [val] If not specified, return the current value.
* If a number, use this as the maximum intensity. If `null`, compute
* the maximum intensity.
* @returns {number|null|this}
*/
this.maxIntensity = function (val) {
if (val === undefined) {
@@ -90,9 +94,12 @@ var heatmapFeature = function (arg) {
};

/**
* Get/Set maxIntensity
* Get/Set minIntensity.
*
* @returns {geo.heatmap}
* @param {number|null} [val] If not specified, return the current value.
* If a number, use this as the minimum intensity. If `null`, compute
* the minimum intensity.
* @returns {number|null|this}
*/
this.minIntensity = function (val) {
if (val === undefined) {
@@ -106,9 +113,12 @@ var heatmapFeature = function (arg) {
};

/**
* Get/Set updateDelay
* Get/Set updateDelay.
*
* @returns {geo.heatmap}
* @param {number} [val] If not specified, return the current update delay.
* If specified, this is the delay in milliseconds after a zoom, rotate,
* or pan event before recomputing the heatmap.
* @returns {number|this}
*/
this.updateDelay = function (val) {
if (val === undefined) {
@@ -120,9 +130,15 @@ var heatmapFeature = function (arg) {
};

/**
* Get/Set binned
* Get/Set binned value.
*
* @returns {geo.heatmap}
* @param {boolean|number|'auto'} [val] If not specified, return the current
* binned value. If `true` or a number, spatially bin data as part of
* producing the heatmap. If falsy, each datapoint stands on its own.
* If `'auto'`, bin data if there are more data points than there would be
* bins. Using `true` or `auto` uses bins that are
* `max(Math.floor((radius + blurRadius) / 8), 3)`.
* @returns {boolean|number|'auto'|this}
*/
this.binned = function (val) {
if (val === undefined) {
@@ -146,9 +162,14 @@ var heatmapFeature = function (arg) {
};

/**
* Get/Set position accessor
* Get/Set position accessor.
*
* @returns {geo.heatmap}
* @param {geo.geoPosition|function} [val] If not specified, return the
* current position accessor. If specified, use this for the position
* accessor and return `this`. If a function is given, this is called
* with `(dataElement, dataIndex)`.
* @returns {geo.geoPosition|function|this} The current position or this
* feature.
*/
this.position = function (val) {
if (val === undefined) {
@@ -162,7 +183,7 @@ var heatmapFeature = function (arg) {
};

/**
* Get pre-computed gcs position accessor
* Get pre-computed gcs position accessor.
*
* @returns {geo.heatmap}
*/
@@ -172,9 +193,11 @@ var heatmapFeature = function (arg) {
};

/**
* Get/Set intensity
* Get/Set intensity.
*
* @returns {geo.heatmap}
* @param {function} [val] If not specified, the current intensity accessor.
* Otherwise, a function that returns the intensity of each data point.
* @returns {function|this}
*/
this.intensity = function (val) {
if (val === undefined) {
@@ -188,7 +211,9 @@ var heatmapFeature = function (arg) {
};

/**
* Initialize
* Initialize.
*
* @param {geo.heatmapFeature.spec} arg
*/
this._init = function (arg) {
s_init.call(m_this, arg);
@@ -199,11 +224,12 @@ 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},
0.75: {r: 1, g: 1, b: 0, a: 0.7},
1: {r: 1, g: 0, b: 0, a: 0.8}}
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},
0.75: {r: 1, g: 1, b: 0, a: 0.7},
1: {r: 1, g: 0, b: 0, a: 0.8}}
},
arg.style === undefined ? {} : arg.style
);
@@ -216,8 +242,9 @@ var heatmapFeature = function (arg) {
};

/**
* Build
* @override
* Build the fetaure.
*
* @returns {this}
*/
this._build = function () {
var data = m_this.data(),
@@ -226,10 +253,10 @@ var heatmapFeature = function (arg) {
setMax = (m_maxIntensity === null || m_maxIntensity === undefined),
setMin = (m_minIntensity === null || m_minIntensity === undefined);

data.forEach(function (d) {
position.push(m_this.position()(d));
data.forEach(function (d, i) {
position.push(m_this.position()(d, i));
if (setMax || setMin) {
intensity = m_this.intensity()(d);
intensity = m_this.intensity()(d, i);
if (m_maxIntensity === null || m_maxIntensity === undefined) {
m_maxIntensity = intensity;
}