UNPKG

transcend-charts

Version:

Transcend is a charting and graph library for NUVI

1,082 lines (933 loc) 42.8 kB
'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;