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

Adds Heatmap and allows arbitrary number of threshold colors #35

Closed
wants to merge 8 commits into from
3 changes: 2 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
dist/*
src/leaflet.js
src/leaflet.js
src/leaflet-heat.js
2 changes: 2 additions & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module.exports = (config) => {
'src/worldmap.js',
'src/data_formatter.js',
'src/leaflet.js',
'src/leaflet-heat.js',
'test/*.js'
],

Expand Down Expand Up @@ -49,6 +50,7 @@ module.exports = (config) => {
'src/worldmap.js',
'src/data_formatter.js',
'src/leaflet.js',
'src/leaflet-heat.js',
'test/**/*.js'
],

Expand Down
22 changes: 17 additions & 5 deletions src/editor.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,28 @@ <h5 class="section-heading">Map Visual Options</h5>
<label class="gf-form-label width-10">Initial Zoom</label>
<input type="text" class="input-small gf-form-input width-10" ng-model="ctrl.panel.initialZoom" ng-change="ctrl.setZoom()" ng-model-onblur />
</div>
<div class="gf-form">
<div class="gf-form">
<label class="gf-form-label width-10">Map Type</label>
<div class="gf-form-select-wrapper max-width-10">
<select class="input-small gf-form-input" ng-model="ctrl.panel.mapType" ng-options="t for t in ['circle', 'heat']" ng-change="ctrl.setNewMapType()"></select>
</div>
</div>
<div class="gf-form" ng-show="ctrl.panel.mapType === 'circle'">
<label class="gf-form-label width-10">Min Circle Size</label>
<input type="text" class="input-small gf-form-input width-10" ng-model="ctrl.panel.circleMinSize" ng-change="ctrl.render()" ng-model-onblur />
</div>
<div class="gf-form">
<div class="gf-form" ng-show="ctrl.panel.mapType === 'circle'">
<label class="gf-form-label width-10">Max Circle Size</label>
<input type="text" class="input-small gf-form-input width-10" ng-model="ctrl.panel.circleMaxSize" ng-change="ctrl.render()" ng-model-onblur />
</div>
<div class="gf-form" ng-show="ctrl.panel.mapType === 'heat'">
<label class="gf-form-label width-10">Heat Radius</label>
<input type="text" class="input-small gf-form-input width-10" ng-model="ctrl.panel.heatSize" ng-change="ctrl.render()" ng-model-onblur />
</div>
<div class="gf-form" ng-show="ctrl.panel.mapType === 'heat'">
<label class="gf-form-label width-10">Heat Blur</label>
<input type="text" class="input-small gf-form-input width-10" ng-model="ctrl.panel.heatBlur" ng-change="ctrl.render()" ng-model-onblur />
</div>
<div class="gf-form">
<label class="gf-form-label width-10">Decimals</label>
<input type="number" class="input-small gf-form-input width-10" ng-model="ctrl.panel.decimals" ng-change="ctrl.refresh()" ng-model-onblur />
Expand Down Expand Up @@ -88,9 +102,7 @@ <h5 class="section-heading">Threshold Options</h5>
</div>
<div class="gf-form">
<label class="gf-form-label width-10">Colors</label>
<spectrum-picker class="gf-form-input width-3" ng-model="ctrl.panel.colors[0]" ng-change="ctrl.changeThresholds()" ></spectrum-picker>
<spectrum-picker class="gf-form-input width-3" ng-model="ctrl.panel.colors[1]" ng-change="ctrl.changeThresholds()" ></spectrum-picker>
<spectrum-picker class="gf-form-input width-3" ng-model="ctrl.panel.colors[2]" ng-change="ctrl.changeThresholds()" ></spectrum-picker>
<spectrum-picker class="gf-form-input width-3" ng-repeat="color in ctrl.panel.colors track by $index" ng-model="ctrl.panel.colors[$index]" ng-change="ctrl.changeThresholds()" ></spectrum-picker>
</div>
</div>
</div>
1 change: 1 addition & 0 deletions src/leaflet-heat.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/map_renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ export default function link(scope, elem, attrs, ctrl) {

if (!ctrl.map.legend && ctrl.panel.showLegend) ctrl.map.createLegend();

ctrl.map.drawCircles();
ctrl.map.drawOverlay();
}
}
89 changes: 78 additions & 11 deletions src/worldmap.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import _ from 'lodash';
/* eslint-disable id-length, no-unused-vars */
import L from './leaflet';
import './leaflet-heat';
/* eslint-disable id-length, no-unused-vars */

const tileServers = {
Expand All @@ -13,7 +14,8 @@ export default class WorldMap {
this.ctrl = ctrl;
this.mapContainer = mapContainer;
this.createMap();
this.circles = [];
this.circles = {};
this.heatData = [];
}

createMap() {
Expand Down Expand Up @@ -56,22 +58,46 @@ export default class WorldMap {
this.legend.addTo(this.map);
}


drawOverlay() {
switch (this.ctrl.data.mapType) {
case "circle":
this.clearHeat();
this.drawCircles();
break;
case "heat":
this.clearCircles();
this.drawHeat();
break;
default:
break;
}
}

needToRedrawCircles() {
if (this.circles.length === 0 && this.ctrl.data.length > 0) return true;
if (this.circles.length !== this.ctrl.data.length) return true;
const locations = _.map(_.map(this.circles, 'options'), 'location').sort();
const dataPoints = _.map(this.ctrl.data, 'key').sort();
let nCirc = _.size(this.circles);
if (nCirc === 0 && this.ctrl.data.length > 0) return true;
if (nCirc !== this.ctrl.data.length) return true;
const locations = new Set(_.map(_.map(this.circles, 'options'), 'location'));
const dataPoints = new Set(_.map(this.ctrl.data, 'key'));
return !_.isEqual(locations, dataPoints);
}

clearCircles() {
if (this.circlesLayer) {
this.circlesLayer.clearLayers();
this.removeCircles(this.circlesLayer);
this.circles = [];
this.circles = {};
}
}

clearHeat() {
if (this.heatLayer) {
this.map.removeLayer(this.heatLayer);
}
this.heatData = [];
}

drawCircles() {
if (this.needToRedrawCircles()) {
this.clearCircles();
Expand All @@ -82,20 +108,20 @@ export default class WorldMap {
}

createCircles() {
const circles = [];
const circles = {};
this.ctrl.data.forEach(dataPoint => {
if (!dataPoint.locationName) return;
circles.push(this.createCircle(dataPoint));
circles[dataPoint.key] = this.createCircle(dataPoint);
});
this.circlesLayer = this.addCircles(circles);
this.circlesLayer = this.addCircles(_.values(circles));
this.circles = circles;
}

updateCircles() {
this.ctrl.data.forEach(dataPoint => {
if (!dataPoint.locationName) return;

const circle = _.find(this.circles, cir => { return cir.options.location === dataPoint.key; });
const circle = this.circles[dataPoint.key];

if (circle) {
circle.setRadius(this.calcCircleSize(dataPoint.value || 0));
Expand Down Expand Up @@ -162,6 +188,47 @@ export default class WorldMap {
return _.first(this.ctrl.panel.colors);
}

rgb2hex(rgb){
rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
return (rgb && rgb.length === 4) ? "#" +
("0" + parseInt(rgb[1],10).toString(16)).slice(-2) +
("0" + parseInt(rgb[2],10).toString(16)).slice(-2) +
("0" + parseInt(rgb[3],10).toString(16)).slice(-2) : '';
}

drawHeat() {
this.clearHeat();
// build up the array to feed the heat layer
const heatData = [];
let minValue = _.min(_.map(this.ctrl.data, 'value'));
let maxValue = _.max(_.map(this.ctrl.data, 'value'));

this.ctrl.data.forEach(dataPoint => {
if (!dataPoint.locationName) return;
let normalizedValue = (dataPoint.value - minValue) / (maxValue - minValue);
// let heatPoint = [dataPoint.locationLatitude, dataPoint.locationLongitude, normalizedValue];
let heatPoint = [dataPoint.locationLatitude, dataPoint.locationLongitude, dataPoint.value];
heatData.push(heatPoint);
});

let maxGrad = _.max(this.ctrl.data.thresholds);
let minGrad = 0;//_.min(this.ctrl.data.thresholds);
let mapMaxVal = maxGrad;//_.max([maxGrad,maxValue]);
// gradient - color gradient config, e.g. {0.4: 'blue', 0.65: 'lime', 1: 'red'}
var gradientData = {};
gradientData[0] = this.rgb2hex(this.ctrl.panel.colors[0]);
for (let index = 0; index < this.ctrl.data.thresholds.length; index++) {
let thresh = (this.ctrl.data.thresholds[index] - minGrad) / (maxGrad - minGrad);
if (thresh > 1) thresh = 1;
else if (thresh < 0) thresh = 0;
gradientData[thresh] = this.rgb2hex(this.ctrl.panel.colors[index+1]);
}
let radius = parseInt(this.ctrl.panel.heatSize), blur = parseInt(this.ctrl.panel.heatBlur);
let heatOpts = {radius:radius,blur:blur,max:mapMaxVal,gradient:gradientData};
this.heatLayer = window.L.heatLayer(heatData,heatOpts).addTo(this.map);
this.heatData = heatData;
}

resize() {
this.map.invalidateSize();
}
Expand Down Expand Up @@ -189,7 +256,7 @@ export default class WorldMap {
}

remove() {
this.circles = [];
this.circles = {};
if (this.circlesLayer) this.removeCircles();
if (this.legend) this.removeLegend();
this.map.remove();
Expand Down
20 changes: 19 additions & 1 deletion src/worldmap_ctrl.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ const panelDefaults = {
mapCenterLongitude: 0,
initialZoom: 1,
valueName: 'total',
mapType: 'circle', // or 'heat'
circleMinSize: 2,
circleMaxSize: 30,
heatSize: 25,
heatBlur: 15,
locationData: 'countries',
thresholds: '0,10',
colors: ['rgba(245, 54, 54, 0.9)', 'rgba(237, 129, 40, 0.89)', 'rgba(50, 172, 45, 0.97)'],
Expand Down Expand Up @@ -135,7 +138,8 @@ export class WorldmapCtrl extends MetricsPanelCtrl {
this.data = data;

this.updateThresholdData();

this.data.mapType = this.panel.mapType;

this.render();
}

Expand Down Expand Up @@ -260,6 +264,11 @@ export class WorldmapCtrl extends MetricsPanelCtrl {
this.render();
}

setNewMapType() {
this.data.mapType = this.panel.mapType;
this.render();
}

setZoom() {
this.map.setZoom(this.panel.initialZoom);
}
Expand All @@ -281,6 +290,15 @@ export class WorldmapCtrl extends MetricsPanelCtrl {
this.data.thresholds = this.panel.thresholds.split(',').map(strValue => {
return Number(strValue.trim());
});
while (_.size(this.panel.colors) > _.size(this.data.thresholds) + 1) {
// too many colors. remove the last one.
this.panel.colors.pop();
}
while (_.size(this.panel.colors) < _.size(this.data.thresholds) + 1) {
// not enough colors. add one.
let newColor = 'rgba(50, 172, 45, 0.97)';
this.panel.colors.push(newColor);
}
}

changeLocationData() {
Expand Down