From f3a1a3f328418b375000320103752fa6c4bdd381 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Mon, 20 Dec 2021 12:03:56 -0500 Subject: [PATCH] feat: Add scaleWithZoom option to heatmaps. --- examples/heatmap/index.pug | 3 +++ examples/heatmap/main.js | 3 ++- src/canvas/heatmapFeature.js | 44 +++++++++++++++++++++++------------- src/heatmapFeature.js | 8 ++++++- tests/cases/heatmap.js | 14 ++++++++++++ 5 files changed, 54 insertions(+), 18 deletions(-) diff --git a/examples/heatmap/index.pug b/examples/heatmap/index.pug index 64e3cdbed3..8ff39d4351 100644 --- a/examples/heatmap/index.pug +++ b/examples/heatmap/index.pug @@ -46,6 +46,9 @@ block append mainContent .form-group(title="Use either a Gaussian distribution or a solid circle with a blurred edge for each point. If a Gaussian 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="If true, scale the point size with zoom. In this case, the radius is specified in pixels a zoom-level 0.") + label(for="scaleWithZoom") Scale With Zoom + input#scaleWithZoom(type="checkbox", placeholder="false") .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 diff --git a/examples/heatmap/main.js b/examples/heatmap/main.js index 27b26101fb..a855f8c995 100644 --- a/examples/heatmap/main.js +++ b/examples/heatmap/main.js @@ -52,6 +52,7 @@ $(function () { ctlvalue = value ? value : 'adderall'; break; case 'gaussian': + case 'scaleWithZoom': ctlvalue = value === 'true'; heatmapOptions.style[key] = value; break; @@ -100,7 +101,6 @@ $(function () { heatmapOptions[key] = ctlvalue = parseInt(value, 10); } break; - // add gaussian and binning when they are added as features } if (ctlvalue !== undefined) { $('#' + ctlkey).val(ctlvalue); @@ -229,6 +229,7 @@ $(function () { fetch_data(); break; case 'gaussian': + case 'scaleWithZoom': heatmapOptions.style[param] = processedValue; heatmap.style(param, processedValue); heatmap.draw(); diff --git a/src/canvas/heatmapFeature.js b/src/canvas/heatmapFeature.js index aacda52737..ecee0c68bc 100644 --- a/src/canvas/heatmapFeature.js +++ b/src/canvas/heatmapFeature.js @@ -73,15 +73,22 @@ var canvas_heatmapFeature = function (arg) { }; /** - * Create circle for each data point. + * Create a circle to render at each data point. * * @returns {this} */ this._createCircle = function () { - var circle, ctx, r, r2, blur, gaussian; + var circle, ctx, r, r2, blur, gaussian, scale; r = m_this.style('radius'); blur = m_this.style('blurRadius'); gaussian = m_this.style('gaussian'); + scale = m_this.style('scaleWithZoom'); + if (scale) { + let zoom = this.layer().map().zoom(); + scale = Math.pow(2, zoom); + r *= scale; + blur *= scale; + } 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'); @@ -307,9 +314,12 @@ var canvas_heatmapFeature = function (arg) { layer = m_this.layer(), mapSize = map.size(); + if (m_this.style('scaleWithZoom')) { + radius *= Math.pow(2, map.zoom()); + } /* Determine if we should bin the data */ if (binned === true || binned === 'auto') { - binned = Math.max(Math.floor(radius / 8), 3); + binned = Math.max(Math.floor(radius / 8), Math.max(1.5, Math.min(3, radius / 2.5))); if (m_this.binned() === 'auto') { var numbins = (Math.ceil((mapSize.width + radius * 2) / binned) * Math.ceil((mapSize.height + radius * 2) / binned)); @@ -327,20 +337,22 @@ var canvas_heatmapFeature = function (arg) { context2d.setTransform(1, 0, 0, 1, 0, 0); context2d.clearRect(0, 0, mapSize.width, mapSize.height); m_heatMapTransform = ''; - map.scheduleAnimationFrame(m_this._setTransform, false); - layer.canvas().css({transform: ''}); - - m_this._createCircle(); - m_this._computeGradient(); - if (!binned) { - m_this._renderPoints(context2d, map, data, radius); - } else { - m_this._renderBinnedData(context2d, map, data, radius, binned); + if (radius > 0.5 && radius < 8192) { + map.scheduleAnimationFrame(m_this._setTransform, false); + layer.canvas().css({transform: ''}); + + m_this._createCircle(); + m_this._computeGradient(); + if (!binned) { + m_this._renderPoints(context2d, map, data, radius); + } else { + m_this._renderBinnedData(context2d, map, data, radius, binned); + } + canvas = layer.canvas()[0]; + pixelArray = context2d.getImageData(0, 0, canvas.width, canvas.height); + m_this._colorize(pixelArray.data, m_this._grad); + context2d.putImageData(pixelArray, 0, 0); } - canvas = layer.canvas()[0]; - pixelArray = context2d.getImageData(0, 0, canvas.width, canvas.height); - m_this._colorize(pixelArray.data, m_this._grad); - context2d.putImageData(pixelArray, 0, 0); m_heatMapPosition = { zoom: map.zoom(), diff --git a/src/heatmapFeature.js b/src/heatmapFeature.js index 49557a11a1..48d5011a3e 100644 --- a/src/heatmapFeature.js +++ b/src/heatmapFeature.js @@ -40,6 +40,11 @@ var transform = require('./transform'); * approximation. 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. + * @property {boolean} [scaleWithZoom=false] If truthy, the value for radius + * and blurRadius scale with zoom. In this case, the values for radius and + * blurRadius are the values at zoom-level zero. If the scaled radius is + * less than 0.5 or more than 8192 screen pixels, the heatmap will not + * render. */ /** @@ -238,7 +243,8 @@ var heatmapFeature = function (arg) { 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}} + 1: {r: 1, g: 0, b: 0, a: 0.8}}, + scaleWithZoom: false }, arg.style === undefined ? {} : arg.style ); diff --git a/tests/cases/heatmap.js b/tests/cases/heatmap.js index ac5bd95c15..d50496f03f 100644 --- a/tests/cases/heatmap.js +++ b/tests/cases/heatmap.js @@ -164,6 +164,20 @@ describe('canvas heatmap', function () { stepAnimationFrame(Date.now()); expect(feature1._binned).toBe(r / 8); }); + + it('scaleWithZoom', function () { + // animation frames are already mocked + var r = 0.80; + feature1.style({radius: r, blurRadius: 0, scaleWithZoom: true}); + map.draw(); + stepAnimationFrame(Date.now()); + expect(feature1._binned).toBe(102); + feature1.style('scaleWithZoom', false); + map.draw(); + stepAnimationFrame(Date.now()); + expect(feature1._binned).toBe(1.5); + }); + it('Remove a feature from a layer', function () { layer.deleteFeature(feature1).draw(); expect(layer.children().length).toBe(0);