transcend-charts
Version:
Transcend is a charting and graph library for NUVI
1,082 lines (933 loc) • 42.8 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _Charts = require('../helpers/Charts');
var _Charts2 = _interopRequireDefault(_Charts);
var _Numbers = require('../helpers/Numbers');
var _Numbers2 = _interopRequireDefault(_Numbers);
var _Color = require('./Color');
var _Color2 = _interopRequireDefault(_Color);
var _Geometry = require('./Geometry');
var _Geometry2 = _interopRequireDefault(_Geometry);
var _ChartDictionary = require('./ChartDictionary');
var _ChartDictionary2 = _interopRequireDefault(_ChartDictionary);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var ChoroplethMap = function ChoroplethMap(htmlContainer, mapData, mapOptions, mapBoundaries) {
var self = this;
var htmlCanvas = null;
var needsRender = false;
var options = mapOptions;
if (!options.backgroundColor) {
options.backgroundColor = new _Color2.default.Color('transparent');
} else {
options.backgroundColor = new _Color2.default.Color(options.backgroundColor);
}
if (!options.borderColor) {
options.borderColor = new _Color2.default.Color('#00ffff');
} else {
options.borderColor = new _Color2.default.Color(options.borderColor);
}
if (!options.fillColor) {
options.fillColor = new _Color2.default.Color('#00ffff');
} else {
options.fillColor = new _Color2.default.Color(options.fillColor);
}
if (!options.zeroFillColor) {
if (!options.backgroundColor.isTransparent) {
options.zeroFillColor = _Color2.default.makeColorBetween(options.backgroundColor, options.fillColor, 0.5);
} else {
options.zeroFillColor = new _Color2.default.Color(options.fillColor);
}
} else {
options.zeroFillColor = new _Color2.default.Color(options.zeroFillColor);
}
// legend
if (options.showLegend === undefined || options.showLegend === null || options.showLegend === true) {
options.showLegend = true;
} else {
options.showLegend = false;
}
if (!options.legendFontColor) {
options.legendFontColor = new _Color2.default.Color('#ffffff');
} else {
options.legendFontColor = new _Color2.default.Color(options.legendFontColor);
}
if (!options.legendFontSize) {
options.legendFontSize = 11;
} else {
options.legendFontSize = parseFloat(options.legendFontSize);
}
if (!options.legendFontFamily) {
options.legendFontFamily = 'sans-serif';
}
if (!options.legendBackgroundColor) {
options.legendBackgroundColor = new _Color2.default.Color('rgba(0,0,0,0.05)');
} else {
options.legendBackgroundColor = new _Color2.default.Color(options.legendBackgroundColor);
}
if (!options.legendOutlineColor) {
options.legendOutlineColor = new _Color2.default.Color('transparent');
} else {
options.legendOutlineColor = new _Color2.default.Color(options.legendOutlineColor);
}
// mouse controls
if (options.allowZoomAndPan === undefined || options.allowZoomAndPan === null || options.allowZoomAndPan === true) {
options.allowZoomAndPan = true;
} else {
options.allowZoomAndPan = false;
}
var data = null;
var legendOutlineWidth = 1; // the width of the stroke around the legend
var legendOutlineCornerSize = 8; // the width of the little corner notches around the legend
var tooltipPadding = 8;
var pxRatio = window.devicePixelRatio || 1;
var DEBUG = false;
var fullScreenChangeListener;
var webkitFullScreenChangeListener;
var mozFullScreenChangeListener;
var msFullScreenChangeListener;
var resizeListener;
var isDestroyed = false;
var resizeTimer = null;
var countryOutlines = mapBoundaries['110m'];
if (!countryOutlines) countryOutlines = mapBoundaries['50m'];
var minViewport = {
minLng: -180,
maxLng: 180,
minLat: -70,
maxLat: 90
};
if (mapBoundaries.minViewport) {
minViewport = {
minLng: mapBoundaries.minViewport.minLng,
maxLng: mapBoundaries.minViewport.maxLng,
minLat: mapBoundaries.minViewport.minLat,
maxLat: mapBoundaries.minViewport.maxLat
};
}
var viewport = {
minLng: minViewport.minLng,
maxLng: minViewport.maxLng,
minLat: minViewport.minLat,
maxLat: minViewport.maxLat
};
var mapRatio = 2.25;
if (mapBoundaries.mapRatio) mapRatio = mapBoundaries.mapRatio;
var maxScale = 0.1;
this.render = function (exportAsImage, scale) {
//console.log('map start: ' + new Date().getTime());
// If there's no HTML canvas made something went horribly wrong. Abort!
if (!htmlCanvas) return;
var canvas = htmlCanvas;
var ctx = canvas.getContext('2d');
if (!scale) {
scale = 1;
}
var backingStoreRatio = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1;
if (exportAsImage) {
canvas = document.createElement('CANVAS');
canvas.width = htmlCanvas.width * scale;
canvas.height = htmlCanvas.height * scale;
ctx = canvas.getContext('2d');
}
// upscale this thang if the device pixel ratio is higher than 1
ctx.save();
if (pxRatio > 1 || scale !== 1) {
ctx.scale(pxRatio / backingStoreRatio * scale, pxRatio / backingStoreRatio * scale);
}
var canvasWidth = canvas.width / (pxRatio / backingStoreRatio * scale);
var canvasHeight = canvas.height / (pxRatio / backingStoreRatio * scale);
// clear the background, ready to re-render
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
if (options.backgroundColor.toString() !== 'transparent') {
ctx.fillStyle = options.backgroundColor.toString();
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
}
// calculate the ratio between lat/lng and px
var lngperpx = (viewport.maxLng - viewport.minLng) / canvasWidth;
var latperpx = (viewport.maxLat - viewport.minLat) / canvasHeight;
// calculate which fill amount is the max for this choropleth map so we can scale fill colors off of it
var maxFill = undefined;
for (var a = 0; a < data.length; a++) {
if (maxFill === undefined || data[a].value > maxFill) {
maxFill = data[a].value;
}
}
// iterate over features and draw the outlines of all of them
ctx.lineWidth = 1;
ctx.strokeStyle = options.borderColor.toString();
ctx.fillStyle = options.zeroFillColor.toString();
for (var i = 0; i < countryOutlines.features.length; i++) {
if (countryOutlines.features[i].bbox[0] > viewport.maxLng || countryOutlines.features[i].bbox[2] < viewport.minLng || countryOutlines.features[i].bbox[1] > viewport.maxLat || countryOutlines.features[i].bbox[3] < viewport.minLat) {
continue;
}
if ((countryOutlines.features[i].bbox[2] - countryOutlines.features[i].bbox[0]) / lngperpx < 4 && (countryOutlines.features[i].bbox[3] - countryOutlines.features[i].bbox[1]) / latperpx < 4) {
continue;
}
// if multipolygon
if (countryOutlines.features[i].geometry.type === 'MultiPolygon') {
for (var j = 0; j < countryOutlines.features[i].geometry.coordinates.length; j++) {
ctx.beginPath();
for (var k = 0; k < countryOutlines.features[i].geometry.coordinates[j].length; k++) {
var lastPt = undefined;
for (var l = 0; l < countryOutlines.features[i].geometry.coordinates[j][k].length; l++) {
var lng = countryOutlines.features[i].geometry.coordinates[j][k][l][0];
var lat = countryOutlines.features[i].geometry.coordinates[j][k][l][1];
var x = (lng - viewport.minLng) / lngperpx;
var y = canvasHeight - (lat - viewport.minLat) / latperpx; // invert this sucker
if (lastPt !== undefined && lastPt.x === Math.round(x) && lastPt.y === Math.round(y)) continue;
if (l === 0) ctx.moveTo(x, y);else ctx.lineTo(x, y);
lastPt = { x: Math.round(x), y: Math.round(y) };
}
ctx.closePath();
}
ctx.fill();
ctx.stroke();
}
}
// if polygon
else if (countryOutlines.features[i].geometry.type === 'Polygon') {
for (var j = 0; j < countryOutlines.features[i].geometry.coordinates.length; j++) {
ctx.beginPath();
var lastPt = undefined;
for (var k = 0; k < countryOutlines.features[i].geometry.coordinates[j].length; k++) {
var lng = countryOutlines.features[i].geometry.coordinates[j][k][0];
var lat = countryOutlines.features[i].geometry.coordinates[j][k][1];
var x = (lng - viewport.minLng) / lngperpx;
var y = canvasHeight - (lat - viewport.minLat) / latperpx; // invert this sucker
if (lastPt !== undefined && lastPt.x === Math.round(x) && lastPt.y === Math.round(y)) continue;
if (k === 0) ctx.moveTo(x, y);else ctx.lineTo(x, y);
lastPt = { x: Math.round(x), y: Math.round(y) };
}
ctx.closePath();
ctx.fill();
ctx.stroke();
}
}
}
// iterate over features and fill any of them that need it
for (var i = 0; i < countryOutlines.features.length; i++) {
var shouldFill = false;
for (var a = 0; a < data.length; a++) {
if (data[a].name === countryOutlines.features[i].properties.iso_a2 || data[a].name === countryOutlines.features[i].properties.postal) {
var fillPct = data[a].value / maxFill;
var color = _Color2.default.makeColorBetween(_Color2.default.makeColorBetween(options.zeroFillColor, options.fillColor, 0.5), options.fillColor, fillPct);
ctx.fillStyle = color.toString();
shouldFill = true;
}
}
if (shouldFill) {
if (countryOutlines.features[i].bbox[0] > viewport.maxLng || countryOutlines.features[i].bbox[2] < viewport.minLng || countryOutlines.features[i].bbox[1] > viewport.maxLat || countryOutlines.features[i].bbox[3] < viewport.minLat) {
continue;
}
if ((countryOutlines.features[i].bbox[2] - countryOutlines.features[i].bbox[0]) / lngperpx < 4 && (countryOutlines.features[i].bbox[3] - countryOutlines.features[i].bbox[1]) / latperpx < 4) {
continue;
}
// if multipolygon
if (countryOutlines.features[i].geometry.type === 'MultiPolygon') {
for (var j = 0; j < countryOutlines.features[i].geometry.coordinates.length; j++) {
ctx.beginPath();
for (var k = 0; k < countryOutlines.features[i].geometry.coordinates[j].length; k++) {
var lastPt = undefined;
for (var l = 0; l < countryOutlines.features[i].geometry.coordinates[j][k].length; l++) {
var lng = countryOutlines.features[i].geometry.coordinates[j][k][l][0];
var lat = countryOutlines.features[i].geometry.coordinates[j][k][l][1];
//var x = (canvasWidth-mapSize.width)/2 + ((lng - viewport.minLng) / lngperpx);
//var y = canvasHeight - (canvasHeight-mapSize.height)/2 - ((lat - viewport.minLat) / latperpx); // invert this sucker
var x = (lng - viewport.minLng) / lngperpx;
var y = canvasHeight - (lat - viewport.minLat) / latperpx; // invert this sucker
if (lastPt !== undefined && lastPt.x === Math.round(x) && lastPt.y === Math.round(y)) continue;
if (l === 0) ctx.moveTo(x, y);else ctx.lineTo(x, y);
lastPt = { x: Math.round(x), y: Math.round(y) };
}
}
ctx.closePath();
ctx.fill();
}
}
// if polygon
else if (countryOutlines.features[i].geometry.type === 'Polygon') {
for (var j = 0; j < countryOutlines.features[i].geometry.coordinates.length; j++) {
ctx.beginPath();
var lastPt = undefined;
for (var k = 0; k < countryOutlines.features[i].geometry.coordinates[j].length; k++) {
var lng = countryOutlines.features[i].geometry.coordinates[j][k][0];
var lat = countryOutlines.features[i].geometry.coordinates[j][k][1];
//var x = (canvasWidth-mapSize.width)/2 + ((lng - viewport.minLng) / lngperpx);
//var y = canvasHeight - (canvasHeight-mapSize.height)/2 - ((lat - viewport.minLat) / latperpx); // invert this sucker
var x = (lng - viewport.minLng) / lngperpx;
var y = canvasHeight - (lat - viewport.minLat) / latperpx; // invert this sucker
if (lastPt !== undefined && lastPt.x === Math.round(x) && lastPt.y === Math.round(y)) continue;
if (k === 0) ctx.moveTo(x, y);else ctx.lineTo(x, y);
lastPt = { x: Math.round(x), y: Math.round(y) };
}
ctx.closePath();
ctx.fill();
}
}
}
}
// find the highlighted country and highlight it and show the label/legend or whatever
for (var i = 0; i < countryOutlines.features.length; i++) {
var highlightedBoundary = data.find(function (datum) {
return (datum.name === countryOutlines.features[i].properties.iso_a2 || datum.name === countryOutlines.features[i].properties.postal) && datum.isHighlighted;
});
if (!highlightedBoundary) continue;
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
ctx.lineWidth = 3;
ctx.lineJoin = 'round';
// if multipolygon
if (countryOutlines.features[i].geometry.type === 'MultiPolygon') {
for (var j = 0; j < countryOutlines.features[i].geometry.coordinates.length; j++) {
ctx.beginPath();
for (var k = 0; k < countryOutlines.features[i].geometry.coordinates[j].length; k++) {
var lastPt = undefined;
for (var l = 0; l < countryOutlines.features[i].geometry.coordinates[j][k].length; l++) {
var lng = countryOutlines.features[i].geometry.coordinates[j][k][l][0];
var lat = countryOutlines.features[i].geometry.coordinates[j][k][l][1];
var x = (lng - viewport.minLng) / lngperpx;
var y = canvasHeight - (lat - viewport.minLat) / latperpx; // invert this sucker
if (lastPt !== undefined && lastPt.x === Math.round(x) && lastPt.y === Math.round(y)) continue;
if (l === 0) ctx.moveTo(x, y);else ctx.lineTo(x, y);
lastPt = { x: Math.round(x), y: Math.round(y) };
}
}
ctx.closePath();
ctx.stroke();
}
}
// if polygon
else if (countryOutlines.features[i].geometry.type === 'Polygon') {
for (var j = 0; j < countryOutlines.features[i].geometry.coordinates.length; j++) {
ctx.beginPath();
var lastPt = undefined;
for (var k = 0; k < countryOutlines.features[i].geometry.coordinates[j].length; k++) {
var lng = countryOutlines.features[i].geometry.coordinates[j][k][0];
var lat = countryOutlines.features[i].geometry.coordinates[j][k][1];
//var x = (canvasWidth-mapSize.width)/2 + ((lng - viewport.minLng) / lngperpx);
//var y = canvasHeight - (canvasHeight-mapSize.height)/2 - ((lat - viewport.minLat) / latperpx); // invert this sucker
var x = (lng - viewport.minLng) / lngperpx;
var y = canvasHeight - (lat - viewport.minLat) / latperpx; // invert this sucker
if (lastPt !== undefined && lastPt.x === Math.round(x) && lastPt.y === Math.round(y)) continue;
if (k === 0) ctx.moveTo(x, y);else ctx.lineTo(x, y);
lastPt = { x: Math.round(x), y: Math.round(y) };
}
ctx.closePath();
ctx.stroke();
}
}
// get the corresponding country object and its relevant data to display
var valueObj = undefined;
for (var a = 0; a < data.length; a++) {
if (data[a].name === countryOutlines.features[i].properties.iso_a2 || data[a].name === countryOutlines.features[i].properties.postal) {
valueObj = data[a];
break;
}
}
// show the legend box
ctx.font = options.legendFontSize + 'px ' + options.legendFontFamily;
var labelstr = countryOutlines.features[i].properties.name + ': ';
var displayValue = '' + (valueObj.prefix || '') + (valueObj.value || '0') + (valueObj.suffix || '');
if (valueObj.displayValue) {
displayValue = valueObj.displayValue;
}
var legendWidth = tooltipPadding + ctx.measureText(labelstr).width + ctx.measureText(displayValue).width + tooltipPadding;
var legendHeight = options.legendFontSize * 1.25 + tooltipPadding * 2;
var legendPos = {
x: mouseProperties.lastPt.x - legendWidth / 2,
y: mouseProperties.lastPt.y - legendHeight - tooltipPadding
};
// guarantee the legend doesn't get rendered off screen
if (legendPos.x < 0) legendPos.x = 0;
if (legendPos.y < 0) legendPos.y = 0;
if (legendPos.x + legendWidth > canvasWidth) legendPos.x = canvasWidth - legendWidth;
// legend background box
ctx.fillStyle = options.legendBackgroundColor;
ctx.fillRect(legendPos.x, legendPos.y, legendWidth, legendHeight);
// draw four corners
ctx.strokeStyle = options.legendOutlineColor.toString();
ctx.lineWidth = legendOutlineWidth;
ctx.beginPath();
ctx.moveTo(legendPos.x + legendOutlineWidth, legendPos.y + legendOutlineCornerSize);
ctx.lineTo(legendPos.x + legendOutlineWidth, legendPos.y + legendOutlineWidth);
ctx.lineTo(legendPos.x + legendOutlineCornerSize, legendPos.y + legendOutlineWidth);
ctx.moveTo(legendPos.x + legendWidth - legendOutlineCornerSize, legendPos.y + legendOutlineWidth);
ctx.lineTo(legendPos.x + legendWidth - legendOutlineWidth, legendPos.y + legendOutlineWidth);
ctx.lineTo(legendPos.x + legendWidth - legendOutlineWidth, legendPos.y + legendOutlineCornerSize);
ctx.moveTo(legendPos.x + legendWidth - legendOutlineWidth, legendPos.y + legendHeight - legendOutlineCornerSize);
ctx.lineTo(legendPos.x + legendWidth - legendOutlineWidth, legendPos.y + legendHeight - legendOutlineWidth);
ctx.lineTo(legendPos.x + legendWidth - legendOutlineCornerSize, legendPos.y + legendHeight - legendOutlineWidth);
ctx.moveTo(legendPos.x + legendOutlineCornerSize, legendPos.y + legendHeight - legendOutlineWidth);
ctx.lineTo(legendPos.x + legendOutlineWidth, legendPos.y + legendHeight - legendOutlineWidth);
ctx.lineTo(legendPos.x + legendOutlineWidth, legendPos.y + legendHeight - legendOutlineCornerSize);
ctx.stroke();
// show the legend text
ctx.textBaseline = 'baseline';
ctx.textAlign = 'left';
// label
ctx.fillStyle = options.legendFontColor.toString();
ctx.fillText(labelstr, legendPos.x + tooltipPadding, legendPos.y + tooltipPadding + options.legendFontSize);
// value
ctx.fillStyle = options.fillColor.toString();
ctx.fillText(displayValue, legendPos.x + tooltipPadding + ctx.measureText(labelstr).width, legendPos.y + tooltipPadding + options.legendFontSize);
}
// draw fps
if (DEBUG) {
ctx.fillStyle = '#666666';
ctx.fillRect(canvasWidth - 40, canvasHeight - 15, 40, 15);
ctx.textAlign = 'right';
ctx.textBaseline = 'bottom';
ctx.fillStyle = '#000000';
ctx.font = '11px sans-serif';
ctx.fillText(String(Math.round(fps)) + ' fps', canvasWidth - 5, canvasHeight - 1);
}
ctx.restore();
if (exportAsImage) {
return canvas.toDataURL('image/png');
}
};
function containViewport() {
// if we're too zoomed out to make the map fill the canvas, let's just fill the canvas
if (viewport.maxLng - viewport.minLng > minViewport.maxLng - minViewport.minLng || viewport.maxLat - viewport.minLat > minViewport.maxLat - minViewport.minLat) {
// determine whether the lng or lat will fill the mapSize
var canvasRatio = htmlCanvas.width / htmlCanvas.height;
var lngspan = 0;
var latspan = 0;
if (canvasRatio > mapRatio) {
latspan = minViewport.maxLat - minViewport.minLat;
lngspan = latspan * canvasRatio;
} else {
lngspan = minViewport.maxLng - minViewport.minLng;
latspan = lngspan / canvasRatio;
}
viewport = {
minLat: (minViewport.maxLat + minViewport.minLat) / 2 - latspan / 2,
maxLat: (minViewport.maxLat + minViewport.minLat) / 2 + latspan / 2,
minLng: (minViewport.maxLng + minViewport.minLng) / 2 - lngspan / 2,
maxLng: (minViewport.maxLng + minViewport.minLng) / 2 + lngspan / 2
};
} else {
// prevent zooming out or in to space on the outside of the map's minViewport
if (viewport.minLng < minViewport.minLng) {
var diff = minViewport.minLng - viewport.minLng;
viewport.minLng += diff;
viewport.maxLng += diff;
}
if (viewport.maxLng > minViewport.maxLng) {
var diff = viewport.maxLng - minViewport.maxLng;
viewport.minLng -= diff;
viewport.maxLng -= diff;
}
if (viewport.minLat < minViewport.minLat) {
var diff = minViewport.minLat - viewport.minLat;
viewport.minLat += diff;
viewport.maxLat += diff;
}
if (viewport.maxLat > minViewport.maxLat) {
var diff = viewport.maxLat - minViewport.maxLat;
viewport.minLat -= diff;
viewport.maxLat -= diff;
}
}
}
/***
* Handles mouse scrolling on the map's canvas
***/
function handleScroll(e) {
var delta = e.wheelDelta ? e.wheelDelta : e.detail;
var ctx = htmlCanvas.getContext('2d');
var backingStoreRatio = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1;
var canvasWidth = htmlCanvas.width / (pxRatio / backingStoreRatio);
var canvasHeight = htmlCanvas.height / (pxRatio / backingStoreRatio);
var rect = htmlCanvas.getBoundingClientRect();
var pt = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
var canvasRatio = htmlCanvas.width / htmlCanvas.height;
// get lng span in viewport
var lngspan = viewport.maxLng - viewport.minLng;
var latspan = viewport.maxLat - viewport.minLat;
// get lat/lng of mouse pt
var mouseLatLng = {
lng: viewport.minLng + pt.x / canvasWidth * lngspan,
lat: viewport.maxLat - pt.y / canvasHeight * latspan // upside-down y axis
};
// weight new span toward mouse x,y
var relativePt = {
x: pt.x / canvasWidth,
y: pt.y / canvasHeight
};
var newlngspan = lngspan;
var newlatspan = latspan;
// zoom in
if (delta > 0) {
newlngspan = lngspan * 0.92; // why do we zoom in 92%? because it feels right
newlatspan = newlngspan / canvasRatio;
}
// zoom out
else if (delta < 0) {
newlngspan = lngspan * 1.07; // why do we zoom out 107%? because it's the recipericol of 92%.
newlatspan = newlngspan / canvasRatio;
}
// if the new viewport is going to be more zoomed out than minViewport, let's just set the map to fill the canvas
if (newlngspan >= minViewport.maxLng - minViewport.minLng && newlatspan >= minViewport.maxLat - minViewport.minLat) {
if (canvasRatio > mapRatio) {
newlatspan = minViewport.maxLat - minViewport.minLat;
newlngspan = newlatspan * canvasRatio;
} else {
newlngspan = minViewport.maxLng - minViewport.minLng;
newlatspan = newlngspan / canvasRatio;
}
}
// if we haven't zoomed in past the maxScale
if (newlngspan > maxScale * (minViewport.maxLng - minViewport.minLng)) {
viewport = {
minLng: mouseLatLng.lng - relativePt.x * newlngspan,
maxLng: mouseLatLng.lng + (1 - relativePt.x) * newlngspan,
minLat: mouseLatLng.lat - (1 - relativePt.y) * newlatspan, // upside down y axis crap
maxLat: mouseLatLng.lat + relativePt.y * newlatspan
};
// prevent zooming out to space on the outside of the map's minViewport
if (newlngspan > lngspan) {
// if zooming out
if (viewport.minLng < minViewport.minLng) {
var diff = minViewport.minLng - viewport.minLng;
viewport.minLng += diff;
viewport.maxLng += diff;
}
if (viewport.maxLng > minViewport.maxLng) {
var diff = viewport.maxLng - minViewport.maxLng;
viewport.minLng -= diff;
viewport.maxLng -= diff;
}
if (viewport.minLat < minViewport.minLat) {
var diff = minViewport.minLat - viewport.minLat;
viewport.minLat += diff;
viewport.maxLat += diff;
}
if (viewport.maxLat > minViewport.maxLat) {
var diff = viewport.maxLat - minViewport.maxLat;
viewport.minLat -= diff;
viewport.maxLat -= diff;
}
}
}
if (viewport.maxLng - viewport.minLng < 90 || viewport.maxLat - viewport.minLat < 40) countryOutlines = mapBoundaries['50m'];else {
countryOutlines = mapBoundaries['110m'];
if (!countryOutlines) countryOutlines = mapBoundaries['50m'];
}
e.stopPropagation();
e.preventDefault();
needsRender = true;
}
var mouseProperties = {
isDown: false,
isDragging: false,
lastPt: { x: 0, y: 0 }
};
/***
* Handles mouse down events on the map canvas
***/
function handleMouseDown(e) {
// get the x,y offset of the mouse click relative to the map canvas and save it
var rect = htmlCanvas.getBoundingClientRect();
var pt = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
// set some kind of flag indicating that the mouse is down (so we can track dragging)
mouseProperties.isDown = true;
mouseProperties.isDragging = false;
// update the mouseProperties.lastPt
mouseProperties.lastPt = { x: pt.x, y: pt.y };
e.stopPropagation();
e.preventDefault();
needsRender = true;
}
/***
* Handles mouse move events on the map canvas
***/
function handleMouseMove(e) {
if (!options.showLegend) return;
var ctx = htmlCanvas.getContext('2d');
var backingStoreRatio = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1;
var canvasWidth = htmlCanvas.width / (pxRatio / backingStoreRatio);
var canvasHeight = htmlCanvas.height / (pxRatio / backingStoreRatio);
// get the x,y offset of the mouse move point relative to the map canvas
var rect = htmlCanvas.getBoundingClientRect();
var pt = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
// if the mouse down flag is set, we should set a flag indicating that we're dragging now
if (mouseProperties.isDown) mouseProperties.isDragging = true;
// if we're dragging, adjust the viewport
if (mouseProperties.isDragging) {
// convert the px distance to lat/lng
var lngperpx = (viewport.maxLng - viewport.minLng) / canvasWidth;
var latperpx = (viewport.maxLat - viewport.minLat) / canvasHeight;
// add the differences to the viewport
viewport.minLng -= (pt.x - mouseProperties.lastPt.x) * lngperpx;
viewport.maxLng -= (pt.x - mouseProperties.lastPt.x) * lngperpx;
viewport.minLat += (pt.y - mouseProperties.lastPt.y) * latperpx;
viewport.maxLat += (pt.y - mouseProperties.lastPt.y) * latperpx;
// check for out of bounds
containViewport();
} else {
// check for hover
unhighlightAllBoundaries();
var hoveredBoundary = hitTestBoundaries(pt, null, null);
if (hoveredBoundary) {
hoveredBoundary.isHighlighted = true;
}
}
// update the mouseProperties.lastPt
mouseProperties.lastPt = { x: pt.x, y: pt.y };
e.stopPropagation();
e.preventDefault();
needsRender = true;
}
function unhighlightAllBoundaries() {
data.forEach(function (datum) {
datum.isHighlighted = false;
});
}
/***
* Handles mouse up events on the map canvas
***/
function handleMouseUp(e) {
if (!mouseProperties.isDragging) {
// get the x,y offset of the mouse move point relative to the map canvas
var rect = htmlCanvas.getBoundingClientRect();
var pt = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
var hoveredBoundary = hitTestBoundaries(pt, null, null);
if (hoveredBoundary) {
if (hoveredBoundary.onClick) {
hoveredBoundary.onClick(e);
}
}
}
// set the mousedown and dragging flags to false
mouseProperties.isDown = false;
mouseProperties.isDragging = false;
e.stopPropagation();
e.preventDefault();
needsRender = true;
}
/***
* Handles the mouse out event (so we can stop dragging if you were.)
* Thanks, Blake for finding this issue
***/
function handleMouseOut(e) {
mouseProperties.isDragging = false;
mouseProperties.isDown = false;
e.stopPropagation();
e.preventDefault();
needsRender = true;
}
function hitTestBoundaries(pt, onHit, onMiss) {
var hoveredBoundary = null;
var ctx = htmlCanvas.getContext('2d');
var backingStoreRatio = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1;
var canvasWidth = htmlCanvas.width / (pxRatio / backingStoreRatio);
var canvasHeight = htmlCanvas.height / (pxRatio / backingStoreRatio);
var lngperpx = (viewport.maxLng - viewport.minLng) / canvasWidth;
var latperpx = (viewport.maxLat - viewport.minLat) / canvasHeight;
// get lng span in viewport
var lngspan = viewport.maxLng - viewport.minLng;
var latspan = viewport.maxLat - viewport.minLat;
// mouse coordinates in lat/lng
var mouseLatLng = {
lng: viewport.minLng + pt.x / canvasWidth * lngspan,
lat: viewport.minLat + (1 - pt.y / canvasHeight) * latspan // freakin frackin inverted y axis
};
// a control line to measure everything against
var controlLine = {
x1: 10000, // necessarily outside of the canvas
y1: -10000, // necessarily outside of the canvas
x2: pt.x,
y2: pt.y
};
for (var i = 0; i < countryOutlines.features.length; i++) {
countryOutlines.features[i].isHighlighted = false; // start out setting everything to false
// don't hit test countries that don't have any data anyway
// var canHighlight = false;
var currentBoundary = data.find(function (datum) {
return datum.name === countryOutlines.features[i].properties.iso_a2 || datum.name === countryOutlines.features[i].properties.postal;
});
if (!currentBoundary) {
continue;
}
// don't hit test countries whose bounding boxes don't contain the mouse
if (countryOutlines.features[i].bbox[0] > mouseLatLng.lng || countryOutlines.features[i].bbox[2] < mouseLatLng.lng || countryOutlines.features[i].bbox[1] > mouseLatLng.lat || countryOutlines.features[i].bbox[3] < mouseLatLng.lat) {
continue;
}
// if multipolygon
if (countryOutlines.features[i].geometry.type === 'MultiPolygon') {
for (var j = 0; j < countryOutlines.features[i].geometry.coordinates.length; j++) {
var numCrosses = 0;
for (var k = 0; k < countryOutlines.features[i].geometry.coordinates[j].length; k++) {
var prevPt = undefined;
var firstPt = undefined;
for (var l = 0; l < countryOutlines.features[i].geometry.coordinates[j][k].length; l++) {
var lng = countryOutlines.features[i].geometry.coordinates[j][k][l][0];
var lat = countryOutlines.features[i].geometry.coordinates[j][k][l][1];
var x = (lng - viewport.minLng) / lngperpx;
var y = canvasHeight - (lat - viewport.minLat) / latperpx; // invert this sucker
if (!firstPt) firstPt = { x: x, y: y };
if (prevPt) {
var line = {
x1: prevPt.x,
y1: prevPt.y,
x2: x,
y2: y
};
if (_Geometry2.default.doLinesCross(controlLine, line)) numCrosses++;
}
// test last point wrapped around to first point
if (l === countryOutlines.features[i].geometry.coordinates[j][k].length - 1) {
var line = {
x1: x,
y1: y,
x2: firstPt.x,
y2: firstPt.y
};
if (line.x1 !== line.x2 || line.y1 !== line.y2 && _Geometry2.default.doLinesCross(controlLine, line)) numCrosses++;
}
prevPt = { x: x, y: y };
}
}
if (numCrosses % 2 === 1) {
// contained!
if (onHit) {
onHit(currentBoundary);
}
hoveredBoundary = currentBoundary;
} else {
if (onMiss) {
onMiss(currentBoundary);
}
}
}
}
// if polygon
else if (countryOutlines.features[i].geometry.type === 'Polygon') {
for (var j = 0; j < countryOutlines.features[i].geometry.coordinates.length; j++) {
var numCrosses = 0;
var prevPt = undefined;
var firstPt = undefined;
for (var k = 0; k < countryOutlines.features[i].geometry.coordinates[j].length; k++) {
var lng = countryOutlines.features[i].geometry.coordinates[j][k][0];
var lat = countryOutlines.features[i].geometry.coordinates[j][k][1];
var x = (lng - viewport.minLng) / lngperpx;
var y = canvasHeight - (lat - viewport.minLat) / latperpx; // invert this sucker
if (!firstPt) firstPt = { x: x, y: y };
if (prevPt) {
var line = {
x1: prevPt.x,
y1: prevPt.y,
x2: x,
y2: y
};
if (_Geometry2.default.doLinesCross(controlLine, line)) numCrosses++;
}
// test last point wrapped around to first point
if (k === countryOutlines.features[i].geometry.coordinates[j].length - 1) {
var line = {
x1: x,
y1: y,
x2: firstPt.x,
y2: firstPt.y
};
if (line.x1 !== line.x2 || line.y1 !== line.y2 && _Geometry2.default.doLinesCross(controlLine, line)) numCrosses++;
}
prevPt = { x: x, y: y };
}
if (numCrosses % 2 === 1) {
// contained!
if (onHit) {
onHit(currentBoundary);
}
hoveredBoundary = currentBoundary;
} else {
if (onMiss) {
onMiss(currentBoundary);
}
}
}
}
}
return hoveredBoundary;
}
/***
* The animateFrame method is called many times per second to calculate any
* movement needed in the graph. It then calls render() to update the display.
* Even if there is no animation in the graph this method ensures the view is updated frequently in case
* mouse events change the view
***/
function animateFrame() {
var thisFrame = new Date().getTime();
var elapsed = thisFrame - lastFrame; // elapsed time since last render
fps = 1000 / elapsed;
// animation effects
// none for now. we still need this method, though, because it ensures the graph will re-render quickly whenever mouse events update the view
if (needsRender) {
self.render();
needsRender = false;
}
lastFrame = thisFrame;
if (!isDestroyed) {
if (window.requestAnimationFrame) {
window.requestAnimationFrame(animateFrame);
} else if (window.webkitRequestAnimationFrame) {
window.webkitRequestAnimationFrame(animateFrame);
} else if (window.mozRequestAnimationFrame) {
window.mozRequestAnimationFrame(animateFrame);
} else if (window.oRequestAnimationFrame) {
window.oRequestAnimationFrame(animateFrame);
}
}
}
var lastFrame = new Date().getTime(); // the timestamp of the last time the frame was last rendered
var fps = 0;
/***
* Handles the window.resize event so the canvas can be made to fill the parent automatically
***/
this.fillParent = function () {
if (htmlCanvas && htmlCanvas.parentNode) {
var style = window.getComputedStyle(htmlCanvas.parentNode);
var width = htmlCanvas.parentNode.offsetWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight);
var height = htmlCanvas.parentNode.offsetHeight - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom);
// upscale this thang if the device pixel ratio is higher than 1
var pxRatio = window.devicePixelRatio || 1;
htmlCanvas.width = width * pxRatio;
htmlCanvas.height = height * pxRatio;
htmlCanvas.style.width = width + 'px';
htmlCanvas.style.height = height + 'px';
// determine whether the lng or lat will fill the mapSize
var canvasRatio = width / height;
var lngspan = 0;
var latspan = 0;
if (canvasRatio > mapRatio) {
latspan = minViewport.maxLat - minViewport.minLat;
lngspan = latspan * canvasRatio;
} else {
lngspan = minViewport.maxLng - minViewport.minLng;
latspan = lngspan / canvasRatio;
}
viewport = {
minLat: (minViewport.maxLat + minViewport.minLat) / 2 - latspan / 2,
maxLat: (minViewport.maxLat + minViewport.minLat) / 2 + latspan / 2,
minLng: (minViewport.maxLng + minViewport.minLng) / 2 - lngspan / 2,
maxLng: (minViewport.maxLng + minViewport.minLng) / 2 + lngspan / 2
};
}
needsRender = true;
}.bind(this);
this.getBase64Image = function (scale) {
// call render with a special flag
return this.render(true, scale);
};
/***
* Initialize the canvas on the DOM and add event listening
***/
function _init() {
var _this = this;
htmlCanvas = document.createElement('CANVAS');
htmlContainer.innerHTML = '';
htmlContainer.appendChild(htmlCanvas);
this.setData(mapData);
needsRender = true;
this.fillParent();
var guid = _Charts2.default.createGuid();
_ChartDictionary2.default.push({ id: guid, canvasElement: htmlCanvas, chart: this });
htmlCanvas.setAttribute('data-chart-id', guid);
// Set up some event handling, please
if (options.allowZoomAndPan) {
htmlCanvas.addEventListener('mousewheel', handleScroll, false); // IE9, Chrome, Safari, Opera
htmlCanvas.addEventListener('DOMMouseScroll', handleScroll, false); // Firefox
htmlCanvas.addEventListener('mousedown', handleMouseDown, false);
htmlCanvas.addEventListener('mousemove', handleMouseMove, false);
htmlCanvas.addEventListener('mouseup', handleMouseUp, false);
htmlCanvas.addEventListener('mouseout', handleMouseOut, false);
}
resizeListener = window.addEventListener('resize', function () {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function () {
_this.fillParent.call(_this);
}, 50);
});
fullScreenChangeListener = window.addEventListener('webkitfullscreenchange', fullscreenChange);
webkitFullScreenChangeListener = window.addEventListener('fullscreenchange', fullscreenChange);
mozFullScreenChangeListener = window.addEventListener('mozfullscreenchange', fullscreenChange);
msFullScreenChangeListener = window.addEventListener('msfullscreenchange', fullscreenChange);
function fullscreenChange(event) {
var fullscreenElement = document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement;
if (!fullscreenElement) {
// exiting full screen
var targetElement = event.target;
// if this element contains a canvas, make it tiny so we don't mess up the size of its parent
// don't worry, the window.resize event that fires next will make it fill its parent just fine
var canvases = targetElement.getElementsByTagName('CANVAS');
for (var c = 0; c < canvases.length; c++) {
if (canvases[c] === htmlCanvas) {
canvases[c].width = 1;
canvases[c].height = 1;
canvases[c].style.width = '1px';
canvases[c].style.height = '1px';
setTimeout(fillParent.bind(self), 10);
break;
}
}
}
}
if (window.requestAnimationFrame) {
window.requestAnimationFrame(animateFrame);
} else if (window.webkitRequestAnimationFrame) {
window.webkitRequestAnimationFrame(animateFrame);
} else if (window.mozRequestAnimationFrame) {
window.mozRequestAnimationFrame(animateFrame);
} else if (window.oRequestAnimationFrame) {
window.oRequestAnimationFrame(animateFrame);
}
}
this.setData = function (mapData) {
data = mapData || [];
for (var i = 0; i < data.length; i++) {
var parts = _Numbers2.default.separateNumberUnits(data[i].value);
if (data[i].prefix === undefined) data[i].prefix = parts.prefix;
if (data[i].suffix === undefined) data[i].suffix = parts.suffix;
data[i].value = parts.value;
}
needsRender = true;
};
this.destroy = function () {
if (fullScreenChangeListener) {
window.removeEventListener('fullscreenchange', fullScreenChangeListener);
}
if (webkitFullScreenChangeListener) {
window.removeEventListener('webkitfullscreenchange', webkitFullScreenChangeListener);
}
if (mozFullScreenChangeListener) {
window.removeEventListener('mozfullscreenchange', mozFullScreenChangeListener);
}
if (msFullScreenChangeListener) {
window.removeEventListener('msfullscreenchange', msFullScreenChangeListener);
}
if (resizeListener) {
window.removeEventListener('resize', resizeListener);
}
if (htmlCanvas && htmlCanvas.parentNode) {
htmlCanvas.parentNode.removeChild(htmlCanvas);
}
isDestroyed = true;
};
// Initialize
_init.call(this);
};
exports.default = ChoroplethMap;