| | |
| | | /** |
| | | * @license Map plugin v0.1 for Highcharts |
| | | * |
| | | * (c) 2011-2013 Torstein Hønsi |
| | | * |
| | | * License: www.highcharts.com/license |
| | | */ |
| | | |
| | | /* |
| | | * See www.highcharts.com/studies/world-map.htm for use case. |
| | | * |
| | | * To do: |
| | | * - Optimize long variable names and alias adapter methods and Highcharts namespace variables |
| | | * - Zoom and pan GUI |
| | | */ |
| | | (function (Highcharts) { |
| | | var UNDEFINED, |
| | | Axis = Highcharts.Axis, |
| | | Chart = Highcharts.Chart, |
| | | Point = Highcharts.Point, |
| | | Pointer = Highcharts.Pointer, |
| | | each = Highcharts.each, |
| | | extend = Highcharts.extend, |
| | | merge = Highcharts.merge, |
| | | pick = Highcharts.pick, |
| | | numberFormat = Highcharts.numberFormat, |
| | | defaultOptions = Highcharts.getOptions(), |
| | | seriesTypes = Highcharts.seriesTypes, |
| | | plotOptions = defaultOptions.plotOptions, |
| | | wrap = Highcharts.wrap, |
| | | Color = Highcharts.Color, |
| | | noop = function () {}; |
| | | |
| | | |
| | | |
| | | /* |
| | | * Return an intermediate color between two colors, according to pos where 0 |
| | | * is the from color and 1 is the to color |
| | | */ |
| | | function tweenColors(from, to, pos) { |
| | | var i = 4, |
| | | rgba = []; |
| | | |
| | | while (i--) { |
| | | rgba[i] = Math.round( |
| | | to.rgba[i] + (from.rgba[i] - to.rgba[i]) * (1 - pos) |
| | | ); |
| | | } |
| | | return 'rgba(' + rgba.join(',') + ')'; |
| | | } |
| | | |
| | | // Set the default map navigation options |
| | | defaultOptions.mapNavigation = { |
| | | buttonOptions: { |
| | | align: 'right', |
| | | verticalAlign: 'bottom', |
| | | x: 0, |
| | | width: 18, |
| | | height: 18, |
| | | style: { |
| | | fontSize: '15px', |
| | | fontWeight: 'bold', |
| | | textAlign: 'center' |
| | | } |
| | | }, |
| | | buttons: { |
| | | zoomIn: { |
| | | onclick: function () { |
| | | this.mapZoom(0.5); |
| | | }, |
| | | text: '+', |
| | | y: -32 |
| | | }, |
| | | zoomOut: { |
| | | onclick: function () { |
| | | this.mapZoom(2); |
| | | }, |
| | | text: '-', |
| | | y: 0 |
| | | } |
| | | } |
| | | // enableButtons: false, |
| | | // enableTouchZoom: false, |
| | | // zoomOnDoubleClick: false, |
| | | // zoomOnMouseWheel: false |
| | | |
| | | }; |
| | | |
| | | /** |
| | | * Utility for reading SVG paths directly. |
| | | */ |
| | | Highcharts.splitPath = function (path) { |
| | | var i; |
| | | |
| | | // Move letters apart |
| | | path = path.replace(/([A-Za-z])/g, ' $1 '); |
| | | // Trim |
| | | path = path.replace(/^\s*/, "").replace(/\s*$/, ""); |
| | | |
| | | // Split on spaces and commas |
| | | path = path.split(/[ ,]+/); |
| | | |
| | | // Parse numbers |
| | | for (i = 0; i < path.length; i++) { |
| | | if (!/[a-zA-Z]/.test(path[i])) { |
| | | path[i] = parseFloat(path[i]); |
| | | } |
| | | } |
| | | return path; |
| | | }; |
| | | |
| | | // A placeholder for map definitions |
| | | Highcharts.maps = {}; |
| | | |
| | | /** |
| | | * Override to use the extreme coordinates from the SVG shape, not the |
| | | * data values |
| | | */ |
| | | wrap(Axis.prototype, 'getSeriesExtremes', function (proceed) { |
| | | var isXAxis = this.isXAxis, |
| | | dataMin, |
| | | dataMax, |
| | | xData = []; |
| | | |
| | | // Remove the xData array and cache it locally so that the proceed method doesn't use it |
| | | each(this.series, function (series, i) { |
| | | if (series.useMapGeometry) { |
| | | xData[i] = series.xData; |
| | | series.xData = []; |
| | | } |
| | | }); |
| | | |
| | | // Call base to reach normal cartesian series (like mappoint) |
| | | proceed.call(this); |
| | | |
| | | // Run extremes logic for map and mapline |
| | | dataMin = pick(this.dataMin, Number.MAX_VALUE); |
| | | dataMax = pick(this.dataMax, Number.MIN_VALUE); |
| | | each(this.series, function (series, i) { |
| | | if (series.useMapGeometry) { |
| | | dataMin = Math.min(dataMin, series[isXAxis ? 'minX' : 'minY']); |
| | | dataMax = Math.max(dataMax, series[isXAxis ? 'maxX' : 'maxY']); |
| | | series.xData = xData[i]; // Reset xData array |
| | | } |
| | | }); |
| | | |
| | | this.dataMin = dataMin; |
| | | this.dataMax = dataMax; |
| | | }); |
| | | |
| | | /** |
| | | * Override axis translation to make sure the aspect ratio is always kept |
| | | */ |
| | | wrap(Axis.prototype, 'setAxisTranslation', function (proceed) { |
| | | var chart = this.chart, |
| | | mapRatio, |
| | | plotRatio = chart.plotWidth / chart.plotHeight, |
| | | isXAxis = this.isXAxis, |
| | | adjustedAxisLength, |
| | | xAxis = chart.xAxis[0], |
| | | padAxis; |
| | | |
| | | // Run the parent method |
| | | proceed.call(this); |
| | | |
| | | // On Y axis, handle both |
| | | if (chart.options.chart.type === 'map' && !isXAxis && xAxis.transA !== UNDEFINED) { |
| | | |
| | | // Use the same translation for both axes |
| | | this.transA = xAxis.transA = Math.min(this.transA, xAxis.transA); |
| | | |
| | | mapRatio = (xAxis.max - xAxis.min) / (this.max - this.min); |
| | | |
| | | // What axis to pad to put the map in the middle |
| | | padAxis = mapRatio > plotRatio ? this : xAxis; |
| | | |
| | | // Pad it |
| | | adjustedAxisLength = (padAxis.max - padAxis.min) * padAxis.transA; |
| | | padAxis.minPixelPadding = (padAxis.len - adjustedAxisLength) / 2; |
| | | } |
| | | }); |
| | | |
| | | |
| | | //--- Start zooming and panning features |
| | | |
| | | wrap(Chart.prototype, 'render', function (proceed) { |
| | | var chart = this, |
| | | mapNavigation = chart.options.mapNavigation; |
| | | |
| | | proceed.call(chart); |
| | | |
| | | // Render the plus and minus buttons |
| | | chart.renderMapNavigation(); |
| | | |
| | | // Add the double click event |
| | | if (mapNavigation.zoomOnDoubleClick) { |
| | | Highcharts.addEvent(chart.container, 'dblclick', function (e) { |
| | | chart.pointer.onContainerDblClick(e); |
| | | }); |
| | | } |
| | | |
| | | // Add the mousewheel event |
| | | if (mapNavigation.zoomOnMouseWheel) { |
| | | Highcharts.addEvent(chart.container, document.onmousewheel === undefined ? 'DOMMouseScroll' : 'mousewheel', function (e) { |
| | | chart.pointer.onContainerMouseWheel(e); |
| | | }); |
| | | } |
| | | }); |
| | | |
| | | // Extend the Pointer |
| | | extend(Pointer.prototype, { |
| | | |
| | | /** |
| | | * The event handler for the doubleclick event |
| | | */ |
| | | onContainerDblClick: function (e) { |
| | | var chart = this.chart; |
| | | |
| | | e = this.normalize(e); |
| | | |
| | | if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) { |
| | | chart.mapZoom( |
| | | 0.5, |
| | | chart.xAxis[0].toValue(e.chartX), |
| | | chart.yAxis[0].toValue(e.chartY) |
| | | ); |
| | | } |
| | | }, |
| | | |
| | | /** |
| | | * The event handler for the mouse scroll event |
| | | */ |
| | | onContainerMouseWheel: function (e) { |
| | | var chart = this.chart, |
| | | delta; |
| | | |
| | | e = this.normalize(e); |
| | | |
| | | // Firefox uses e.detail, WebKit and IE uses wheelDelta |
| | | delta = e.detail || -(e.wheelDelta / 120); |
| | | if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) { |
| | | chart.mapZoom( |
| | | delta > 0 ? 2 : 0.5, |
| | | chart.xAxis[0].toValue(e.chartX), |
| | | chart.yAxis[0].toValue(e.chartY) |
| | | ); |
| | | } |
| | | } |
| | | }); |
| | | // Implement the pinchType option |
| | | wrap(Pointer.prototype, 'init', function (proceed, chart, options) { |
| | | |
| | | proceed.call(this, chart, options); |
| | | |
| | | // Pinch status |
| | | if (options.mapNavigation.enableTouchZoom) { |
| | | this.pinchX = this.pinchHor = |
| | | this.pinchY = this.pinchVert = true; |
| | | } |
| | | }); |
| | | |
| | | // Add events to the Chart object itself |
| | | extend(Chart.prototype, { |
| | | renderMapNavigation: function () { |
| | | var chart = this, |
| | | options = this.options.mapNavigation, |
| | | buttons = options.buttons, |
| | | n, |
| | | button, |
| | | buttonOptions, |
| | | outerHandler = function () { |
| | | this.handler.call(chart); |
| | | }; |
| | | |
| | | if (options.enableButtons) { |
| | | for (n in buttons) { |
| | | if (buttons.hasOwnProperty(n)) { |
| | | buttonOptions = merge(options.buttonOptions, buttons[n]); |
| | | |
| | | button = chart.renderer.button(buttonOptions.text, 0, 0, outerHandler) |
| | | .attr({ |
| | | width: buttonOptions.width, |
| | | height: buttonOptions.height |
| | | }) |
| | | .css(buttonOptions.style) |
| | | .add(); |
| | | button.handler = buttonOptions.onclick; |
| | | button.align(extend(buttonOptions, { width: button.width, height: button.height }), null, 'spacingBox'); |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | |
| | | /** |
| | | * Fit an inner box to an outer. If the inner box overflows left or right, align it to the sides of the |
| | | * outer. If it overflows both sides, fit it within the outer. This is a pattern that occurs more places |
| | | * in Highcharts, perhaps it should be elevated to a common utility function. |
| | | */ |
| | | fitToBox: function (inner, outer) { |
| | | each([['x', 'width'], ['y', 'height']], function (dim) { |
| | | var pos = dim[0], |
| | | size = dim[1]; |
| | | if (inner[pos] + inner[size] > outer[pos] + outer[size]) { // right overflow |
| | | if (inner[size] > outer[size]) { // the general size is greater, fit fully to outer |
| | | inner[size] = outer[size]; |
| | | inner[pos] = outer[pos]; |
| | | } else { // align right |
| | | inner[pos] = outer[pos] + outer[size] - inner[size]; |
| | | } |
| | | } |
| | | if (inner[size] > outer[size]) { |
| | | inner[size] = outer[size]; |
| | | } |
| | | if (inner[pos] < outer[pos]) { |
| | | inner[pos] = outer[pos]; |
| | | } |
| | | |
| | | }); |
| | | |
| | | return inner; |
| | | }, |
| | | |
| | | /** |
| | | * Zoom the map in or out by a certain amount. Less than 1 zooms in, greater than 1 zooms out. |
| | | */ |
| | | mapZoom: function (howMuch, centerXArg, centerYArg) { |
| | | |
| | | if (this.isMapZooming) { |
| | | return; |
| | | } |
| | | |
| | | var chart = this, |
| | | xAxis = chart.xAxis[0], |
| | | xRange = xAxis.max - xAxis.min, |
| | | centerX = pick(centerXArg, xAxis.min + xRange / 2), |
| | | newXRange = xRange * howMuch, |
| | | yAxis = chart.yAxis[0], |
| | | yRange = yAxis.max - yAxis.min, |
| | | centerY = pick(centerYArg, yAxis.min + yRange / 2), |
| | | newYRange = yRange * howMuch, |
| | | newXMin = centerX - newXRange / 2, |
| | | newYMin = centerY - newYRange / 2, |
| | | animation = pick(chart.options.chart.animation, true), |
| | | delay, |
| | | newExt = chart.fitToBox({ |
| | | x: newXMin, |
| | | y: newYMin, |
| | | width: newXRange, |
| | | height: newYRange |
| | | }, { |
| | | x: xAxis.dataMin, |
| | | y: yAxis.dataMin, |
| | | width: xAxis.dataMax - xAxis.dataMin, |
| | | height: yAxis.dataMax - yAxis.dataMin |
| | | }); |
| | | |
| | | xAxis.setExtremes(newExt.x, newExt.x + newExt.width, false); |
| | | yAxis.setExtremes(newExt.y, newExt.y + newExt.height, false); |
| | | |
| | | // Prevent zooming until this one is finished animating |
| | | delay = animation ? animation.duration || 500 : 0; |
| | | if (delay) { |
| | | chart.isMapZooming = true; |
| | | setTimeout(function () { |
| | | chart.isMapZooming = false; |
| | | }, delay); |
| | | } |
| | | |
| | | chart.redraw(); |
| | | } |
| | | }); |
| | | |
| | | /** |
| | | * Extend the default options with map options |
| | | */ |
| | | plotOptions.map = merge(plotOptions.scatter, { |
| | | animation: false, // makes the complex shapes slow |
| | | nullColor: '#F8F8F8', |
| | | borderColor: 'silver', |
| | | borderWidth: 1, |
| | | marker: null, |
| | | stickyTracking: false, |
| | | dataLabels: { |
| | | verticalAlign: 'middle' |
| | | }, |
| | | turboThreshold: 0, |
| | | tooltip: { |
| | | followPointer: true, |
| | | pointFormat: '{point.name}: {point.y}<br/>' |
| | | }, |
| | | states: { |
| | | normal: { |
| | | animation: true |
| | | } |
| | | } |
| | | }); |
| | | |
| | | var MapAreaPoint = Highcharts.extendClass(Point, { |
| | | /** |
| | | * Extend the Point object to split paths |
| | | */ |
| | | applyOptions: function (options, x) { |
| | | |
| | | var point = Point.prototype.applyOptions.call(this, options, x); |
| | | |
| | | if (point.path && typeof point.path === 'string') { |
| | | point.path = point.options.path = Highcharts.splitPath(point.path); |
| | | } |
| | | |
| | | return point; |
| | | }, |
| | | /** |
| | | * Stop the fade-out |
| | | */ |
| | | onMouseOver: function () { |
| | | clearTimeout(this.colorInterval); |
| | | Point.prototype.onMouseOver.call(this); |
| | | }, |
| | | /** |
| | | * Custom animation for tweening out the colors. Animation reduces blinking when hovering |
| | | * over islands and coast lines. We run a custom implementation of animation becuase we |
| | | * need to be able to run this independently from other animations like zoom redraw. Also, |
| | | * adding color animation to the adapters would introduce almost the same amount of code. |
| | | */ |
| | | onMouseOut: function () { |
| | | var point = this, |
| | | start = +new Date(), |
| | | normalColor = Color(point.options.color), |
| | | hoverColor = Color(point.pointAttr.hover.fill), |
| | | animation = point.series.options.states.normal.animation, |
| | | duration = animation && (animation.duration || 500); |
| | | |
| | | if (duration && normalColor.rgba.length === 4 && hoverColor.rgba.length === 4) { |
| | | delete point.pointAttr[''].fill; // avoid resetting it in Point.setState |
| | | |
| | | clearTimeout(point.colorInterval); |
| | | point.colorInterval = setInterval(function () { |
| | | var pos = (new Date() - start) / duration, |
| | | graphic = point.graphic; |
| | | if (pos > 1) { |
| | | pos = 1; |
| | | } |
| | | if (graphic) { |
| | | graphic.attr('fill', tweenColors(hoverColor, normalColor, pos)); |
| | | } |
| | | if (pos >= 1) { |
| | | clearTimeout(point.colorInterval); |
| | | } |
| | | }, 13); |
| | | } |
| | | Point.prototype.onMouseOut.call(point); |
| | | } |
| | | }); |
| | | |
| | | /** |
| | | * Add the series type |
| | | */ |
| | | seriesTypes.map = Highcharts.extendClass(seriesTypes.scatter, { |
| | | type: 'map', |
| | | pointAttrToOptions: { // mapping between SVG attributes and the corresponding options |
| | | stroke: 'borderColor', |
| | | 'stroke-width': 'borderWidth', |
| | | fill: 'color' |
| | | }, |
| | | colorKey: 'y', |
| | | pointClass: MapAreaPoint, |
| | | trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'], |
| | | getSymbol: noop, |
| | | supportsDrilldown: true, |
| | | getExtremesFromAll: true, |
| | | useMapGeometry: true, // get axis extremes from paths, not values |
| | | init: function (chart) { |
| | | var series = this, |
| | | valueDecimals = chart.options.legend.valueDecimals, |
| | | legendItems = [], |
| | | name, |
| | | from, |
| | | to, |
| | | fromLabel, |
| | | toLabel, |
| | | colorRange, |
| | | valueRanges, |
| | | gradientColor, |
| | | grad, |
| | | tmpLabel, |
| | | horizontal = chart.options.legend.layout === 'horizontal'; |
| | | |
| | | |
| | | Highcharts.Series.prototype.init.apply(this, arguments); |
| | | colorRange = series.options.colorRange; |
| | | valueRanges = series.options.valueRanges; |
| | | |
| | | if (valueRanges) { |
| | | each(valueRanges, function (range) { |
| | | from = range.from; |
| | | to = range.to; |
| | | |
| | | // Assemble the default name. This can be overridden by legend.options.labelFormatter |
| | | name = ''; |
| | | if (from === UNDEFINED) { |
| | | name = '< '; |
| | | } else if (to === UNDEFINED) { |
| | | name = '> '; |
| | | } |
| | | if (from !== UNDEFINED) { |
| | | name += numberFormat(from, valueDecimals); |
| | | } |
| | | if (from !== UNDEFINED && to !== UNDEFINED) { |
| | | name += ' - '; |
| | | } |
| | | if (to !== UNDEFINED) { |
| | | name += numberFormat(to, valueDecimals); |
| | | } |
| | | |
| | | // Add a mock object to the legend items |
| | | legendItems.push(Highcharts.extend({ |
| | | chart: series.chart, |
| | | name: name, |
| | | options: {}, |
| | | drawLegendSymbol: seriesTypes.area.prototype.drawLegendSymbol, |
| | | visible: true, |
| | | setState: function () {}, |
| | | setVisible: function () {} |
| | | }, range)); |
| | | }); |
| | | series.legendItems = legendItems; |
| | | |
| | | } else if (colorRange) { |
| | | |
| | | from = colorRange.from; |
| | | to = colorRange.to; |
| | | fromLabel = colorRange.fromLabel; |
| | | toLabel = colorRange.toLabel; |
| | | |
| | | // Flips linearGradient variables and label text. |
| | | grad = horizontal ? [0, 0, 1, 0] : [0, 1, 0, 0]; |
| | | if (!horizontal) { |
| | | tmpLabel = fromLabel; |
| | | fromLabel = toLabel; |
| | | toLabel = tmpLabel; |
| | | } |
| | | |
| | | // Creates color gradient. |
| | | gradientColor = { |
| | | linearGradient: { x1: grad[0], y1: grad[1], x2: grad[2], y2: grad[3] }, |
| | | stops: |
| | | [ |
| | | [0, from], |
| | | [1, to] |
| | | ] |
| | | }; |
| | | |
| | | // Add a mock object to the legend items. |
| | | legendItems = [{ |
| | | chart: series.chart, |
| | | options: {}, |
| | | fromLabel: fromLabel, |
| | | toLabel: toLabel, |
| | | color: gradientColor, |
| | | drawLegendSymbol: this.drawLegendSymbolGradient, |
| | | visible: true, |
| | | setState: function () {}, |
| | | setVisible: function () {} |
| | | }]; |
| | | |
| | | series.legendItems = legendItems; |
| | | } |
| | | }, |
| | | |
| | | /** |
| | | * If neither valueRanges nor colorRanges are defined, use basic area symbol. |
| | | */ |
| | | drawLegendSymbol: seriesTypes.area.prototype.drawLegendSymbol, |
| | | |
| | | /** |
| | | * Gets the series' symbol in the legend and extended legend with more information. |
| | | * |
| | | * @param {Object} legend The legend object |
| | | * @param {Object} item The series (this) or point |
| | | */ |
| | | drawLegendSymbolGradient: function (legend, item) { |
| | | var spacing = legend.options.symbolPadding, |
| | | padding = pick(legend.options.padding, 8), |
| | | positionY, |
| | | positionX, |
| | | gradientSize = this.chart.renderer.fontMetrics(legend.options.itemStyle.fontSize).h, |
| | | horizontal = legend.options.layout === 'horizontal', |
| | | box1, |
| | | box2, |
| | | box3, |
| | | rectangleLength = pick(legend.options.rectangleLength, 200); |
| | | |
| | | // Set local variables based on option. |
| | | if (horizontal) { |
| | | positionY = -(spacing / 2); |
| | | positionX = 0; |
| | | } else { |
| | | positionY = -rectangleLength + legend.baseline - (spacing / 2); |
| | | positionX = padding + gradientSize; |
| | | } |
| | | |
| | | // Creates the from text. |
| | | item.fromText = this.chart.renderer.text( |
| | | item.fromLabel, // Text. |
| | | positionX, // Lower left x. |
| | | positionY // Lower left y. |
| | | ).attr({ |
| | | zIndex: 2 |
| | | }).add(item.legendGroup); |
| | | box1 = item.fromText.getBBox(); |
| | | |
| | | // Creates legend symbol. |
| | | // Ternary changes variables based on option. |
| | | item.legendSymbol = this.chart.renderer.rect( |
| | | horizontal ? box1.x + box1.width + spacing : box1.x - gradientSize - spacing, // Upper left x. |
| | | box1.y, // Upper left y. |
| | | horizontal ? rectangleLength : gradientSize, // Width. |
| | | horizontal ? gradientSize : rectangleLength, // Height. |
| | | 2 // Corner radius. |
| | | ).attr({ |
| | | zIndex: 1 |
| | | }).add(item.legendGroup); |
| | | box2 = item.legendSymbol.getBBox(); |
| | | |
| | | // Creates the to text. |
| | | // Vertical coordinate changed based on option. |
| | | item.toText = this.chart.renderer.text( |
| | | item.toLabel, |
| | | box2.x + box2.width + spacing, |
| | | horizontal ? positionY : box2.y + box2.height - spacing |
| | | ).attr({ |
| | | zIndex: 2 |
| | | }).add(item.legendGroup); |
| | | box3 = item.toText.getBBox(); |
| | | |
| | | // Changes legend box settings based on option. |
| | | if (horizontal) { |
| | | legend.offsetWidth = box1.width + box2.width + box3.width + (spacing * 2) + padding; |
| | | legend.itemY = gradientSize + padding; |
| | | } else { |
| | | legend.offsetWidth = Math.max(box1.width, box3.width) + (spacing) + box2.width + padding; |
| | | legend.itemY = box2.height + padding; |
| | | legend.itemX = spacing; |
| | | } |
| | | }, |
| | | |
| | | /** |
| | | * Get the bounding box of all paths in the map combined. |
| | | */ |
| | | getBox: function (paths) { |
| | | var maxX = Number.MIN_VALUE, |
| | | minX = Number.MAX_VALUE, |
| | | maxY = Number.MIN_VALUE, |
| | | minY = Number.MAX_VALUE; |
| | | |
| | | |
| | | // Find the bounding box |
| | | each(paths || this.options.data, function (point) { |
| | | var path = point.path, |
| | | i = path.length, |
| | | even = false, // while loop reads from the end |
| | | pointMaxX = Number.MIN_VALUE, |
| | | pointMinX = Number.MAX_VALUE, |
| | | pointMaxY = Number.MIN_VALUE, |
| | | pointMinY = Number.MAX_VALUE; |
| | | |
| | | while (i--) { |
| | | if (typeof path[i] === 'number' && !isNaN(path[i])) { |
| | | if (even) { // even = x |
| | | pointMaxX = Math.max(pointMaxX, path[i]); |
| | | pointMinX = Math.min(pointMinX, path[i]); |
| | | } else { // odd = Y |
| | | pointMaxY = Math.max(pointMaxY, path[i]); |
| | | pointMinY = Math.min(pointMinY, path[i]); |
| | | } |
| | | even = !even; |
| | | } |
| | | } |
| | | // Cache point bounding box for use to position data labels |
| | | point._maxX = pointMaxX; |
| | | point._minX = pointMinX; |
| | | point._maxY = pointMaxY; |
| | | point._minY = pointMinY; |
| | | |
| | | maxX = Math.max(maxX, pointMaxX); |
| | | minX = Math.min(minX, pointMinX); |
| | | maxY = Math.max(maxY, pointMaxY); |
| | | minY = Math.min(minY, pointMinY); |
| | | }); |
| | | this.minY = minY; |
| | | this.maxY = maxY; |
| | | this.minX = minX; |
| | | this.maxX = maxX; |
| | | |
| | | }, |
| | | |
| | | |
| | | |
| | | /** |
| | | * Translate the path so that it automatically fits into the plot area box |
| | | * @param {Object} path |
| | | */ |
| | | translatePath: function (path) { |
| | | |
| | | var series = this, |
| | | even = false, // while loop reads from the end |
| | | xAxis = series.xAxis, |
| | | yAxis = series.yAxis, |
| | | i; |
| | | |
| | | // Preserve the original |
| | | path = [].concat(path); |
| | | |
| | | // Do the translation |
| | | i = path.length; |
| | | while (i--) { |
| | | if (typeof path[i] === 'number') { |
| | | if (even) { // even = x |
| | | path[i] = Math.round(xAxis.translate(path[i])); |
| | | } else { // odd = Y |
| | | path[i] = Math.round(yAxis.len - yAxis.translate(path[i])); |
| | | } |
| | | even = !even; |
| | | } |
| | | } |
| | | return path; |
| | | }, |
| | | |
| | | setData: function () { |
| | | Highcharts.Series.prototype.setData.apply(this, arguments); |
| | | this.getBox(); |
| | | }, |
| | | |
| | | /** |
| | | * Add the path option for data points. Find the max value for color calculation. |
| | | */ |
| | | translate: function () { |
| | | var series = this, |
| | | dataMin = Number.MAX_VALUE, |
| | | dataMax = Number.MIN_VALUE; |
| | | |
| | | series.generatePoints(); |
| | | |
| | | each(series.data, function (point) { |
| | | |
| | | point.shapeType = 'path'; |
| | | point.shapeArgs = { |
| | | d: series.translatePath(point.path) |
| | | }; |
| | | |
| | | // TODO: do point colors in drawPoints instead of point.init |
| | | if (typeof point.y === 'number') { |
| | | if (point.y > dataMax) { |
| | | dataMax = point.y; |
| | | } else if (point.y < dataMin) { |
| | | dataMin = point.y; |
| | | } |
| | | } |
| | | }); |
| | | |
| | | series.translateColors(dataMin, dataMax); |
| | | }, |
| | | |
| | | /** |
| | | * In choropleth maps, the color is a result of the value, so this needs translation too |
| | | */ |
| | | translateColors: function (dataMin, dataMax) { |
| | | |
| | | var seriesOptions = this.options, |
| | | valueRanges = seriesOptions.valueRanges, |
| | | colorRange = seriesOptions.colorRange, |
| | | colorKey = this.colorKey, |
| | | from, |
| | | to; |
| | | |
| | | if (colorRange) { |
| | | from = Color(colorRange.from); |
| | | to = Color(colorRange.to); |
| | | } |
| | | |
| | | each(this.data, function (point) { |
| | | var value = point[colorKey], |
| | | range, |
| | | color, |
| | | i, |
| | | pos; |
| | | |
| | | if (valueRanges) { |
| | | i = valueRanges.length; |
| | | while (i--) { |
| | | range = valueRanges[i]; |
| | | from = range.from; |
| | | to = range.to; |
| | | if ((from === UNDEFINED || value >= from) && (to === UNDEFINED || value <= to)) { |
| | | color = range.color; |
| | | break; |
| | | } |
| | | |
| | | } |
| | | } else if (colorRange && value !== undefined) { |
| | | |
| | | pos = 1 - ((dataMax - value) / (dataMax - dataMin)); |
| | | color = value === null ? seriesOptions.nullColor : tweenColors(from, to, pos); |
| | | } |
| | | |
| | | if (color) { |
| | | point.color = null; // reset from previous drilldowns, use of the same data options |
| | | point.options.color = color; |
| | | } |
| | | }); |
| | | }, |
| | | |
| | | drawGraph: noop, |
| | | |
| | | /** |
| | | * We need the points' bounding boxes in order to draw the data labels, so |
| | | * we skip it now and call if from drawPoints instead. |
| | | */ |
| | | drawDataLabels: noop, |
| | | |
| | | /** |
| | | * Use the drawPoints method of column, that is able to handle simple shapeArgs. |
| | | * Extend it by assigning the tooltip position. |
| | | */ |
| | | drawPoints: function () { |
| | | var series = this, |
| | | xAxis = series.xAxis, |
| | | yAxis = series.yAxis, |
| | | colorKey = series.colorKey; |
| | | |
| | | // Make points pass test in drawing |
| | | each(series.data, function (point) { |
| | | point.plotY = 1; // pass null test in column.drawPoints |
| | | if (point[colorKey] === null) { |
| | | point[colorKey] = 0; |
| | | point.isNull = true; |
| | | } |
| | | }); |
| | | |
| | | // Draw them |
| | | seriesTypes.column.prototype.drawPoints.apply(series); |
| | | |
| | | each(series.data, function (point) { |
| | | |
| | | var dataLabels = point.dataLabels, |
| | | minX = xAxis.toPixels(point._minX, true), |
| | | maxX = xAxis.toPixels(point._maxX, true), |
| | | minY = yAxis.toPixels(point._minY, true), |
| | | maxY = yAxis.toPixels(point._maxY, true); |
| | | |
| | | point.plotX = Math.round(minX + (maxX - minX) * pick(dataLabels && dataLabels.anchorX, 0.5)); |
| | | point.plotY = Math.round(minY + (maxY - minY) * pick(dataLabels && dataLabels.anchorY, 0.5)); |
| | | |
| | | |
| | | // Reset escaped null points |
| | | if (point.isNull) { |
| | | point[colorKey] = null; |
| | | } |
| | | }); |
| | | |
| | | // Now draw the data labels |
| | | Highcharts.Series.prototype.drawDataLabels.call(series); |
| | | |
| | | }, |
| | | |
| | | /** |
| | | * Animate in the new series from the clicked point in the old series. |
| | | * Depends on the drilldown.js module |
| | | */ |
| | | animateDrilldown: function (init) { |
| | | var toBox = this.chart.plotBox, |
| | | level = this.chart.drilldownLevels[this.chart.drilldownLevels.length - 1], |
| | | fromBox = level.bBox, |
| | | animationOptions = this.chart.options.drilldown.animation, |
| | | scale; |
| | | |
| | | if (!init) { |
| | | |
| | | scale = Math.min(fromBox.width / toBox.width, fromBox.height / toBox.height); |
| | | level.shapeArgs = { |
| | | scaleX: scale, |
| | | scaleY: scale, |
| | | translateX: fromBox.x, |
| | | translateY: fromBox.y |
| | | }; |
| | | |
| | | // TODO: Animate this.group instead |
| | | each(this.points, function (point) { |
| | | |
| | | point.graphic |
| | | .attr(level.shapeArgs) |
| | | .animate({ |
| | | scaleX: 1, |
| | | scaleY: 1, |
| | | translateX: 0, |
| | | translateY: 0 |
| | | }, animationOptions); |
| | | |
| | | }); |
| | | |
| | | delete this.animate; |
| | | } |
| | | |
| | | }, |
| | | |
| | | /** |
| | | * When drilling up, pull out the individual point graphics from the lower series |
| | | * and animate them into the origin point in the upper series. |
| | | */ |
| | | animateDrillupFrom: function (level) { |
| | | seriesTypes.column.prototype.animateDrillupFrom.call(this, level); |
| | | }, |
| | | |
| | | |
| | | /** |
| | | * When drilling up, keep the upper series invisible until the lower series has |
| | | * moved into place |
| | | */ |
| | | animateDrillupTo: function (init) { |
| | | seriesTypes.column.prototype.animateDrillupTo.call(this, init); |
| | | } |
| | | }); |
| | | |
| | | |
| | | // The mapline series type |
| | | plotOptions.mapline = merge(plotOptions.map, { |
| | | lineWidth: 1, |
| | | backgroundColor: 'none' |
| | | }); |
| | | seriesTypes.mapline = Highcharts.extendClass(seriesTypes.map, { |
| | | type: 'mapline', |
| | | pointAttrToOptions: { // mapping between SVG attributes and the corresponding options |
| | | stroke: 'color', |
| | | 'stroke-width': 'lineWidth', |
| | | fill: 'backgroundColor' |
| | | }, |
| | | drawLegendSymbol: seriesTypes.line.prototype.drawLegendSymbol |
| | | }); |
| | | |
| | | // The mappoint series type |
| | | plotOptions.mappoint = merge(plotOptions.scatter, { |
| | | dataLabels: { |
| | | enabled: true, |
| | | format: '{point.name}', |
| | | color: 'black', |
| | | style: { |
| | | textShadow: '0 0 5px white' |
| | | } |
| | | } |
| | | }); |
| | | seriesTypes.mappoint = Highcharts.extendClass(seriesTypes.scatter, { |
| | | type: 'mappoint' |
| | | }); |
| | | |
| | | |
| | | |
| | | /** |
| | | * A wrapper for Chart with all the default values for a Map |
| | | */ |
| | | Highcharts.Map = function (options, callback) { |
| | | |
| | | var hiddenAxis = { |
| | | endOnTick: false, |
| | | gridLineWidth: 0, |
| | | labels: { |
| | | enabled: false |
| | | }, |
| | | lineWidth: 0, |
| | | minPadding: 0, |
| | | maxPadding: 0, |
| | | startOnTick: false, |
| | | tickWidth: 0, |
| | | title: null |
| | | }, |
| | | seriesOptions; |
| | | |
| | | // Don't merge the data |
| | | seriesOptions = options.series; |
| | | options.series = null; |
| | | |
| | | options = merge({ |
| | | chart: { |
| | | type: 'map', |
| | | panning: 'xy' |
| | | }, |
| | | xAxis: hiddenAxis, |
| | | yAxis: merge(hiddenAxis, { reversed: true }) |
| | | }, |
| | | options, // user's options |
| | | |
| | | { // forced options |
| | | chart: { |
| | | inverted: false |
| | | } |
| | | }); |
| | | |
| | | options.series = seriesOptions; |
| | | |
| | | |
| | | return new Highcharts.Chart(options, callback); |
| | | }; |
| | | }(Highcharts)); |
| | | /**
|
| | | * @license Map plugin v0.1 for Highcharts
|
| | | *
|
| | | * (c) 2011-2013 Torstein Hønsi
|
| | | *
|
| | | * License: www.highcharts.com/license
|
| | | */
|
| | |
|
| | | /* |
| | | * See www.highcharts.com/studies/world-map.htm for use case.
|
| | | *
|
| | | * To do:
|
| | | * - Optimize long variable names and alias adapter methods and Highcharts namespace variables
|
| | | * - Zoom and pan GUI
|
| | | */
|
| | | (function (Highcharts) {
|
| | | var UNDEFINED,
|
| | | Axis = Highcharts.Axis,
|
| | | Chart = Highcharts.Chart,
|
| | | Point = Highcharts.Point,
|
| | | Pointer = Highcharts.Pointer,
|
| | | each = Highcharts.each,
|
| | | extend = Highcharts.extend,
|
| | | merge = Highcharts.merge,
|
| | | pick = Highcharts.pick,
|
| | | numberFormat = Highcharts.numberFormat,
|
| | | defaultOptions = Highcharts.getOptions(),
|
| | | seriesTypes = Highcharts.seriesTypes,
|
| | | plotOptions = defaultOptions.plotOptions,
|
| | | wrap = Highcharts.wrap,
|
| | | Color = Highcharts.Color,
|
| | | noop = function () {};
|
| | |
|
| | | |
| | |
|
| | | /*
|
| | | * Return an intermediate color between two colors, according to pos where 0
|
| | | * is the from color and 1 is the to color
|
| | | */
|
| | | function tweenColors(from, to, pos) {
|
| | | var i = 4,
|
| | | rgba = [];
|
| | |
|
| | | while (i--) {
|
| | | rgba[i] = Math.round(
|
| | | to.rgba[i] + (from.rgba[i] - to.rgba[i]) * (1 - pos)
|
| | | );
|
| | | }
|
| | | return 'rgba(' + rgba.join(',') + ')';
|
| | | }
|
| | |
|
| | | // Set the default map navigation options
|
| | | defaultOptions.mapNavigation = {
|
| | | buttonOptions: {
|
| | | align: 'right',
|
| | | verticalAlign: 'bottom',
|
| | | x: 0,
|
| | | width: 18,
|
| | | height: 18,
|
| | | style: {
|
| | | fontSize: '15px',
|
| | | fontWeight: 'bold',
|
| | | textAlign: 'center'
|
| | | }
|
| | | },
|
| | | buttons: {
|
| | | zoomIn: {
|
| | | onclick: function () {
|
| | | this.mapZoom(0.5);
|
| | | },
|
| | | text: '+',
|
| | | y: -32
|
| | | },
|
| | | zoomOut: {
|
| | | onclick: function () {
|
| | | this.mapZoom(2);
|
| | | },
|
| | | text: '-',
|
| | | y: 0
|
| | | }
|
| | | }
|
| | | // enableButtons: false,
|
| | | // enableTouchZoom: false,
|
| | | // zoomOnDoubleClick: false,
|
| | | // zoomOnMouseWheel: false
|
| | |
|
| | | };
|
| | | |
| | | /**
|
| | | * Utility for reading SVG paths directly.
|
| | | */
|
| | | Highcharts.splitPath = function (path) {
|
| | | var i;
|
| | |
|
| | | // Move letters apart
|
| | | path = path.replace(/([A-Za-z])/g, ' $1 ');
|
| | | // Trim
|
| | | path = path.replace(/^\s*/, "").replace(/\s*$/, "");
|
| | | |
| | | // Split on spaces and commas
|
| | | path = path.split(/[ ,]+/);
|
| | | |
| | | // Parse numbers
|
| | | for (i = 0; i < path.length; i++) {
|
| | | if (!/[a-zA-Z]/.test(path[i])) {
|
| | | path[i] = parseFloat(path[i]);
|
| | | }
|
| | | }
|
| | | return path;
|
| | | };
|
| | |
|
| | | // A placeholder for map definitions
|
| | | Highcharts.maps = {};
|
| | | |
| | | /**
|
| | | * Override to use the extreme coordinates from the SVG shape, not the
|
| | | * data values
|
| | | */
|
| | | wrap(Axis.prototype, 'getSeriesExtremes', function (proceed) {
|
| | | var isXAxis = this.isXAxis,
|
| | | dataMin,
|
| | | dataMax,
|
| | | xData = [];
|
| | |
|
| | | // Remove the xData array and cache it locally so that the proceed method doesn't use it
|
| | | each(this.series, function (series, i) {
|
| | | if (series.useMapGeometry) {
|
| | | xData[i] = series.xData;
|
| | | series.xData = [];
|
| | | }
|
| | | });
|
| | |
|
| | | // Call base to reach normal cartesian series (like mappoint)
|
| | | proceed.call(this);
|
| | |
|
| | | // Run extremes logic for map and mapline
|
| | | dataMin = pick(this.dataMin, Number.MAX_VALUE);
|
| | | dataMax = pick(this.dataMax, Number.MIN_VALUE);
|
| | | each(this.series, function (series, i) {
|
| | | if (series.useMapGeometry) {
|
| | | dataMin = Math.min(dataMin, series[isXAxis ? 'minX' : 'minY']);
|
| | | dataMax = Math.max(dataMax, series[isXAxis ? 'maxX' : 'maxY']);
|
| | | series.xData = xData[i]; // Reset xData array
|
| | | }
|
| | | });
|
| | | |
| | | this.dataMin = dataMin;
|
| | | this.dataMax = dataMax;
|
| | | });
|
| | | |
| | | /**
|
| | | * Override axis translation to make sure the aspect ratio is always kept
|
| | | */
|
| | | wrap(Axis.prototype, 'setAxisTranslation', function (proceed) {
|
| | | var chart = this.chart,
|
| | | mapRatio,
|
| | | plotRatio = chart.plotWidth / chart.plotHeight,
|
| | | isXAxis = this.isXAxis,
|
| | | adjustedAxisLength,
|
| | | xAxis = chart.xAxis[0],
|
| | | padAxis;
|
| | | |
| | | // Run the parent method
|
| | | proceed.call(this);
|
| | | |
| | | // On Y axis, handle both
|
| | | if (chart.options.chart.type === 'map' && !isXAxis && xAxis.transA !== UNDEFINED) {
|
| | | |
| | | // Use the same translation for both axes
|
| | | this.transA = xAxis.transA = Math.min(this.transA, xAxis.transA);
|
| | | |
| | | mapRatio = (xAxis.max - xAxis.min) / (this.max - this.min);
|
| | | |
| | | // What axis to pad to put the map in the middle
|
| | | padAxis = mapRatio > plotRatio ? this : xAxis;
|
| | | |
| | | // Pad it
|
| | | adjustedAxisLength = (padAxis.max - padAxis.min) * padAxis.transA;
|
| | | padAxis.minPixelPadding = (padAxis.len - adjustedAxisLength) / 2;
|
| | | }
|
| | | });
|
| | |
|
| | |
|
| | | //--- Start zooming and panning features
|
| | |
|
| | | wrap(Chart.prototype, 'render', function (proceed) {
|
| | | var chart = this,
|
| | | mapNavigation = chart.options.mapNavigation;
|
| | |
|
| | | proceed.call(chart);
|
| | |
|
| | | // Render the plus and minus buttons
|
| | | chart.renderMapNavigation();
|
| | |
|
| | | // Add the double click event
|
| | | if (mapNavigation.zoomOnDoubleClick) {
|
| | | Highcharts.addEvent(chart.container, 'dblclick', function (e) {
|
| | | chart.pointer.onContainerDblClick(e);
|
| | | });
|
| | | }
|
| | |
|
| | | // Add the mousewheel event
|
| | | if (mapNavigation.zoomOnMouseWheel) {
|
| | | Highcharts.addEvent(chart.container, document.onmousewheel === undefined ? 'DOMMouseScroll' : 'mousewheel', function (e) {
|
| | | chart.pointer.onContainerMouseWheel(e);
|
| | | });
|
| | | }
|
| | | });
|
| | |
|
| | | // Extend the Pointer
|
| | | extend(Pointer.prototype, {
|
| | |
|
| | | /**
|
| | | * The event handler for the doubleclick event
|
| | | */
|
| | | onContainerDblClick: function (e) {
|
| | | var chart = this.chart;
|
| | |
|
| | | e = this.normalize(e);
|
| | |
|
| | | if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) {
|
| | | chart.mapZoom(
|
| | | 0.5,
|
| | | chart.xAxis[0].toValue(e.chartX),
|
| | | chart.yAxis[0].toValue(e.chartY)
|
| | | );
|
| | | }
|
| | | },
|
| | |
|
| | | /**
|
| | | * The event handler for the mouse scroll event
|
| | | */
|
| | | onContainerMouseWheel: function (e) {
|
| | | var chart = this.chart,
|
| | | delta;
|
| | |
|
| | | e = this.normalize(e);
|
| | |
|
| | | // Firefox uses e.detail, WebKit and IE uses wheelDelta
|
| | | delta = e.detail || -(e.wheelDelta / 120);
|
| | | if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) {
|
| | | chart.mapZoom(
|
| | | delta > 0 ? 2 : 0.5,
|
| | | chart.xAxis[0].toValue(e.chartX),
|
| | | chart.yAxis[0].toValue(e.chartY)
|
| | | );
|
| | | }
|
| | | }
|
| | | });
|
| | | // Implement the pinchType option
|
| | | wrap(Pointer.prototype, 'init', function (proceed, chart, options) {
|
| | |
|
| | | proceed.call(this, chart, options);
|
| | |
|
| | | // Pinch status
|
| | | if (options.mapNavigation.enableTouchZoom) {
|
| | | this.pinchX = this.pinchHor = |
| | | this.pinchY = this.pinchVert = true;
|
| | | }
|
| | | });
|
| | |
|
| | | // Add events to the Chart object itself
|
| | | extend(Chart.prototype, {
|
| | | renderMapNavigation: function () {
|
| | | var chart = this,
|
| | | options = this.options.mapNavigation,
|
| | | buttons = options.buttons,
|
| | | n,
|
| | | button,
|
| | | buttonOptions,
|
| | | outerHandler = function () { |
| | | this.handler.call(chart); |
| | | };
|
| | |
|
| | | if (options.enableButtons) {
|
| | | for (n in buttons) {
|
| | | if (buttons.hasOwnProperty(n)) {
|
| | | buttonOptions = merge(options.buttonOptions, buttons[n]);
|
| | |
|
| | | button = chart.renderer.button(buttonOptions.text, 0, 0, outerHandler)
|
| | | .attr({
|
| | | width: buttonOptions.width,
|
| | | height: buttonOptions.height
|
| | | })
|
| | | .css(buttonOptions.style)
|
| | | .add();
|
| | | button.handler = buttonOptions.onclick;
|
| | | button.align(extend(buttonOptions, { width: button.width, height: button.height }), null, 'spacingBox');
|
| | | }
|
| | | }
|
| | | }
|
| | | },
|
| | |
|
| | | /**
|
| | | * Fit an inner box to an outer. If the inner box overflows left or right, align it to the sides of the
|
| | | * outer. If it overflows both sides, fit it within the outer. This is a pattern that occurs more places
|
| | | * in Highcharts, perhaps it should be elevated to a common utility function.
|
| | | */
|
| | | fitToBox: function (inner, outer) {
|
| | | each([['x', 'width'], ['y', 'height']], function (dim) {
|
| | | var pos = dim[0],
|
| | | size = dim[1];
|
| | | if (inner[pos] + inner[size] > outer[pos] + outer[size]) { // right overflow
|
| | | if (inner[size] > outer[size]) { // the general size is greater, fit fully to outer
|
| | | inner[size] = outer[size];
|
| | | inner[pos] = outer[pos];
|
| | | } else { // align right
|
| | | inner[pos] = outer[pos] + outer[size] - inner[size];
|
| | | }
|
| | | }
|
| | | if (inner[size] > outer[size]) {
|
| | | inner[size] = outer[size];
|
| | | }
|
| | | if (inner[pos] < outer[pos]) {
|
| | | inner[pos] = outer[pos];
|
| | | }
|
| | | |
| | | });
|
| | |
|
| | | return inner;
|
| | | },
|
| | |
|
| | | /**
|
| | | * Zoom the map in or out by a certain amount. Less than 1 zooms in, greater than 1 zooms out.
|
| | | */
|
| | | mapZoom: function (howMuch, centerXArg, centerYArg) {
|
| | |
|
| | | if (this.isMapZooming) {
|
| | | return;
|
| | | }
|
| | |
|
| | | var chart = this,
|
| | | xAxis = chart.xAxis[0],
|
| | | xRange = xAxis.max - xAxis.min,
|
| | | centerX = pick(centerXArg, xAxis.min + xRange / 2),
|
| | | newXRange = xRange * howMuch,
|
| | | yAxis = chart.yAxis[0],
|
| | | yRange = yAxis.max - yAxis.min,
|
| | | centerY = pick(centerYArg, yAxis.min + yRange / 2),
|
| | | newYRange = yRange * howMuch,
|
| | | newXMin = centerX - newXRange / 2,
|
| | | newYMin = centerY - newYRange / 2,
|
| | | animation = pick(chart.options.chart.animation, true),
|
| | | delay,
|
| | | newExt = chart.fitToBox({
|
| | | x: newXMin,
|
| | | y: newYMin,
|
| | | width: newXRange,
|
| | | height: newYRange
|
| | | }, {
|
| | | x: xAxis.dataMin,
|
| | | y: yAxis.dataMin,
|
| | | width: xAxis.dataMax - xAxis.dataMin,
|
| | | height: yAxis.dataMax - yAxis.dataMin
|
| | | });
|
| | |
|
| | | xAxis.setExtremes(newExt.x, newExt.x + newExt.width, false);
|
| | | yAxis.setExtremes(newExt.y, newExt.y + newExt.height, false);
|
| | |
|
| | | // Prevent zooming until this one is finished animating
|
| | | delay = animation ? animation.duration || 500 : 0;
|
| | | if (delay) {
|
| | | chart.isMapZooming = true;
|
| | | setTimeout(function () {
|
| | | chart.isMapZooming = false;
|
| | | }, delay);
|
| | | }
|
| | |
|
| | | chart.redraw();
|
| | | }
|
| | | });
|
| | | |
| | | /**
|
| | | * Extend the default options with map options
|
| | | */
|
| | | plotOptions.map = merge(plotOptions.scatter, {
|
| | | animation: false, // makes the complex shapes slow
|
| | | nullColor: '#F8F8F8',
|
| | | borderColor: 'silver',
|
| | | borderWidth: 1,
|
| | | marker: null,
|
| | | stickyTracking: false,
|
| | | dataLabels: {
|
| | | verticalAlign: 'middle'
|
| | | },
|
| | | turboThreshold: 0,
|
| | | tooltip: {
|
| | | followPointer: true,
|
| | | pointFormat: '{point.name}: {point.y}<br/>'
|
| | | },
|
| | | states: {
|
| | | normal: {
|
| | | animation: true
|
| | | }
|
| | | }
|
| | | });
|
| | |
|
| | | var MapAreaPoint = Highcharts.extendClass(Point, {
|
| | | /**
|
| | | * Extend the Point object to split paths
|
| | | */
|
| | | applyOptions: function (options, x) {
|
| | |
|
| | | var point = Point.prototype.applyOptions.call(this, options, x);
|
| | |
|
| | | if (point.path && typeof point.path === 'string') {
|
| | | point.path = point.options.path = Highcharts.splitPath(point.path);
|
| | | }
|
| | |
|
| | | return point;
|
| | | },
|
| | | /**
|
| | | * Stop the fade-out |
| | | */
|
| | | onMouseOver: function () {
|
| | | clearTimeout(this.colorInterval);
|
| | | Point.prototype.onMouseOver.call(this);
|
| | | },
|
| | | /**
|
| | | * Custom animation for tweening out the colors. Animation reduces blinking when hovering
|
| | | * over islands and coast lines. We run a custom implementation of animation becuase we
|
| | | * need to be able to run this independently from other animations like zoom redraw. Also,
|
| | | * adding color animation to the adapters would introduce almost the same amount of code.
|
| | | */
|
| | | onMouseOut: function () {
|
| | | var point = this,
|
| | | start = +new Date(),
|
| | | normalColor = Color(point.options.color),
|
| | | hoverColor = Color(point.pointAttr.hover.fill),
|
| | | animation = point.series.options.states.normal.animation,
|
| | | duration = animation && (animation.duration || 500);
|
| | |
|
| | | if (duration && normalColor.rgba.length === 4 && hoverColor.rgba.length === 4) {
|
| | | delete point.pointAttr[''].fill; // avoid resetting it in Point.setState
|
| | |
|
| | | clearTimeout(point.colorInterval);
|
| | | point.colorInterval = setInterval(function () {
|
| | | var pos = (new Date() - start) / duration,
|
| | | graphic = point.graphic;
|
| | | if (pos > 1) {
|
| | | pos = 1;
|
| | | }
|
| | | if (graphic) {
|
| | | graphic.attr('fill', tweenColors(hoverColor, normalColor, pos));
|
| | | }
|
| | | if (pos >= 1) {
|
| | | clearTimeout(point.colorInterval);
|
| | | }
|
| | | }, 13);
|
| | | }
|
| | | Point.prototype.onMouseOut.call(point);
|
| | | }
|
| | | });
|
| | |
|
| | | /**
|
| | | * Add the series type
|
| | | */
|
| | | seriesTypes.map = Highcharts.extendClass(seriesTypes.scatter, {
|
| | | type: 'map',
|
| | | pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
|
| | | stroke: 'borderColor',
|
| | | 'stroke-width': 'borderWidth',
|
| | | fill: 'color'
|
| | | },
|
| | | colorKey: 'y',
|
| | | pointClass: MapAreaPoint,
|
| | | trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'],
|
| | | getSymbol: noop,
|
| | | supportsDrilldown: true,
|
| | | getExtremesFromAll: true,
|
| | | useMapGeometry: true, // get axis extremes from paths, not values
|
| | | init: function (chart) {
|
| | | var series = this,
|
| | | valueDecimals = chart.options.legend.valueDecimals,
|
| | | legendItems = [],
|
| | | name,
|
| | | from,
|
| | | to,
|
| | | fromLabel,
|
| | | toLabel,
|
| | | colorRange,
|
| | | valueRanges,
|
| | | gradientColor,
|
| | | grad,
|
| | | tmpLabel,
|
| | | horizontal = chart.options.legend.layout === 'horizontal';
|
| | |
|
| | | |
| | | Highcharts.Series.prototype.init.apply(this, arguments);
|
| | | colorRange = series.options.colorRange;
|
| | | valueRanges = series.options.valueRanges;
|
| | |
|
| | | if (valueRanges) {
|
| | | each(valueRanges, function (range) {
|
| | | from = range.from;
|
| | | to = range.to;
|
| | | |
| | | // Assemble the default name. This can be overridden by legend.options.labelFormatter
|
| | | name = '';
|
| | | if (from === UNDEFINED) {
|
| | | name = '< ';
|
| | | } else if (to === UNDEFINED) {
|
| | | name = '> ';
|
| | | }
|
| | | if (from !== UNDEFINED) {
|
| | | name += numberFormat(from, valueDecimals);
|
| | | }
|
| | | if (from !== UNDEFINED && to !== UNDEFINED) {
|
| | | name += ' - ';
|
| | | }
|
| | | if (to !== UNDEFINED) {
|
| | | name += numberFormat(to, valueDecimals);
|
| | | }
|
| | | |
| | | // Add a mock object to the legend items
|
| | | legendItems.push(Highcharts.extend({
|
| | | chart: series.chart,
|
| | | name: name,
|
| | | options: {},
|
| | | drawLegendSymbol: seriesTypes.area.prototype.drawLegendSymbol,
|
| | | visible: true,
|
| | | setState: function () {},
|
| | | setVisible: function () {}
|
| | | }, range));
|
| | | });
|
| | | series.legendItems = legendItems;
|
| | |
|
| | | } else if (colorRange) {
|
| | |
|
| | | from = colorRange.from;
|
| | | to = colorRange.to;
|
| | | fromLabel = colorRange.fromLabel;
|
| | | toLabel = colorRange.toLabel;
|
| | |
|
| | | // Flips linearGradient variables and label text.
|
| | | grad = horizontal ? [0, 0, 1, 0] : [0, 1, 0, 0]; |
| | | if (!horizontal) {
|
| | | tmpLabel = fromLabel;
|
| | | fromLabel = toLabel;
|
| | | toLabel = tmpLabel;
|
| | | } |
| | |
|
| | | // Creates color gradient.
|
| | | gradientColor = {
|
| | | linearGradient: { x1: grad[0], y1: grad[1], x2: grad[2], y2: grad[3] },
|
| | | stops: |
| | | [
|
| | | [0, from],
|
| | | [1, to]
|
| | | ]
|
| | | };
|
| | |
|
| | | // Add a mock object to the legend items.
|
| | | legendItems = [{
|
| | | chart: series.chart,
|
| | | options: {},
|
| | | fromLabel: fromLabel,
|
| | | toLabel: toLabel,
|
| | | color: gradientColor,
|
| | | drawLegendSymbol: this.drawLegendSymbolGradient,
|
| | | visible: true,
|
| | | setState: function () {},
|
| | | setVisible: function () {}
|
| | | }];
|
| | |
|
| | | series.legendItems = legendItems;
|
| | | }
|
| | | },
|
| | |
|
| | | /**
|
| | | * If neither valueRanges nor colorRanges are defined, use basic area symbol.
|
| | | */
|
| | | drawLegendSymbol: seriesTypes.area.prototype.drawLegendSymbol,
|
| | |
|
| | | /**
|
| | | * Gets the series' symbol in the legend and extended legend with more information.
|
| | | * |
| | | * @param {Object} legend The legend object
|
| | | * @param {Object} item The series (this) or point
|
| | | */
|
| | | drawLegendSymbolGradient: function (legend, item) {
|
| | | var spacing = legend.options.symbolPadding,
|
| | | padding = pick(legend.options.padding, 8),
|
| | | positionY,
|
| | | positionX,
|
| | | gradientSize = this.chart.renderer.fontMetrics(legend.options.itemStyle.fontSize).h,
|
| | | horizontal = legend.options.layout === 'horizontal',
|
| | | box1,
|
| | | box2,
|
| | | box3,
|
| | | rectangleLength = pick(legend.options.rectangleLength, 200);
|
| | |
|
| | | // Set local variables based on option.
|
| | | if (horizontal) {
|
| | | positionY = -(spacing / 2);
|
| | | positionX = 0;
|
| | | } else {
|
| | | positionY = -rectangleLength + legend.baseline - (spacing / 2);
|
| | | positionX = padding + gradientSize;
|
| | | }
|
| | |
|
| | | // Creates the from text.
|
| | | item.fromText = this.chart.renderer.text(
|
| | | item.fromLabel, // Text.
|
| | | positionX, // Lower left x.
|
| | | positionY // Lower left y.
|
| | | ).attr({
|
| | | zIndex: 2
|
| | | }).add(item.legendGroup);
|
| | | box1 = item.fromText.getBBox();
|
| | |
|
| | | // Creates legend symbol.
|
| | | // Ternary changes variables based on option.
|
| | | item.legendSymbol = this.chart.renderer.rect(
|
| | | horizontal ? box1.x + box1.width + spacing : box1.x - gradientSize - spacing, // Upper left x.
|
| | | box1.y, // Upper left y.
|
| | | horizontal ? rectangleLength : gradientSize, // Width.
|
| | | horizontal ? gradientSize : rectangleLength, // Height.
|
| | | 2 // Corner radius.
|
| | | ).attr({
|
| | | zIndex: 1
|
| | | }).add(item.legendGroup);
|
| | | box2 = item.legendSymbol.getBBox();
|
| | |
|
| | | // Creates the to text.
|
| | | // Vertical coordinate changed based on option.
|
| | | item.toText = this.chart.renderer.text(
|
| | | item.toLabel,
|
| | | box2.x + box2.width + spacing,
|
| | | horizontal ? positionY : box2.y + box2.height - spacing
|
| | | ).attr({
|
| | | zIndex: 2
|
| | | }).add(item.legendGroup);
|
| | | box3 = item.toText.getBBox();
|
| | |
|
| | | // Changes legend box settings based on option.
|
| | | if (horizontal) {
|
| | | legend.offsetWidth = box1.width + box2.width + box3.width + (spacing * 2) + padding;
|
| | | legend.itemY = gradientSize + padding;
|
| | | } else {
|
| | | legend.offsetWidth = Math.max(box1.width, box3.width) + (spacing) + box2.width + padding;
|
| | | legend.itemY = box2.height + padding;
|
| | | legend.itemX = spacing;
|
| | | }
|
| | | },
|
| | |
|
| | | /**
|
| | | * Get the bounding box of all paths in the map combined.
|
| | | */
|
| | | getBox: function (paths) {
|
| | | var maxX = Number.MIN_VALUE, |
| | | minX = Number.MAX_VALUE, |
| | | maxY = Number.MIN_VALUE, |
| | | minY = Number.MAX_VALUE;
|
| | | |
| | | |
| | | // Find the bounding box
|
| | | each(paths || this.options.data, function (point) {
|
| | | var path = point.path,
|
| | | i = path.length,
|
| | | even = false, // while loop reads from the end
|
| | | pointMaxX = Number.MIN_VALUE, |
| | | pointMinX = Number.MAX_VALUE, |
| | | pointMaxY = Number.MIN_VALUE, |
| | | pointMinY = Number.MAX_VALUE;
|
| | | |
| | | while (i--) {
|
| | | if (typeof path[i] === 'number' && !isNaN(path[i])) {
|
| | | if (even) { // even = x
|
| | | pointMaxX = Math.max(pointMaxX, path[i]);
|
| | | pointMinX = Math.min(pointMinX, path[i]);
|
| | | } else { // odd = Y
|
| | | pointMaxY = Math.max(pointMaxY, path[i]);
|
| | | pointMinY = Math.min(pointMinY, path[i]);
|
| | | }
|
| | | even = !even;
|
| | | }
|
| | | }
|
| | | // Cache point bounding box for use to position data labels
|
| | | point._maxX = pointMaxX;
|
| | | point._minX = pointMinX;
|
| | | point._maxY = pointMaxY;
|
| | | point._minY = pointMinY;
|
| | |
|
| | | maxX = Math.max(maxX, pointMaxX);
|
| | | minX = Math.min(minX, pointMinX);
|
| | | maxY = Math.max(maxY, pointMaxY);
|
| | | minY = Math.min(minY, pointMinY);
|
| | | });
|
| | | this.minY = minY;
|
| | | this.maxY = maxY;
|
| | | this.minX = minX;
|
| | | this.maxX = maxX;
|
| | | |
| | | },
|
| | | |
| | | |
| | | |
| | | /**
|
| | | * Translate the path so that it automatically fits into the plot area box
|
| | | * @param {Object} path
|
| | | */
|
| | | translatePath: function (path) {
|
| | | |
| | | var series = this,
|
| | | even = false, // while loop reads from the end
|
| | | xAxis = series.xAxis,
|
| | | yAxis = series.yAxis,
|
| | | i;
|
| | | |
| | | // Preserve the original
|
| | | path = [].concat(path);
|
| | | |
| | | // Do the translation
|
| | | i = path.length;
|
| | | while (i--) {
|
| | | if (typeof path[i] === 'number') {
|
| | | if (even) { // even = x
|
| | | path[i] = Math.round(xAxis.translate(path[i]));
|
| | | } else { // odd = Y
|
| | | path[i] = Math.round(yAxis.len - yAxis.translate(path[i]));
|
| | | }
|
| | | even = !even;
|
| | | }
|
| | | }
|
| | | return path;
|
| | | },
|
| | | |
| | | setData: function () {
|
| | | Highcharts.Series.prototype.setData.apply(this, arguments);
|
| | | this.getBox();
|
| | | },
|
| | | |
| | | /**
|
| | | * Add the path option for data points. Find the max value for color calculation.
|
| | | */
|
| | | translate: function () {
|
| | | var series = this,
|
| | | dataMin = Number.MAX_VALUE,
|
| | | dataMax = Number.MIN_VALUE;
|
| | | |
| | | series.generatePoints();
|
| | | |
| | | each(series.data, function (point) {
|
| | | |
| | | point.shapeType = 'path';
|
| | | point.shapeArgs = {
|
| | | d: series.translatePath(point.path)
|
| | | };
|
| | | |
| | | // TODO: do point colors in drawPoints instead of point.init
|
| | | if (typeof point.y === 'number') {
|
| | | if (point.y > dataMax) {
|
| | | dataMax = point.y;
|
| | | } else if (point.y < dataMin) {
|
| | | dataMin = point.y;
|
| | | }
|
| | | }
|
| | | });
|
| | | |
| | | series.translateColors(dataMin, dataMax);
|
| | | },
|
| | | |
| | | /**
|
| | | * In choropleth maps, the color is a result of the value, so this needs translation too
|
| | | */
|
| | | translateColors: function (dataMin, dataMax) {
|
| | | |
| | | var seriesOptions = this.options,
|
| | | valueRanges = seriesOptions.valueRanges,
|
| | | colorRange = seriesOptions.colorRange,
|
| | | colorKey = this.colorKey,
|
| | | from,
|
| | | to;
|
| | |
|
| | | if (colorRange) {
|
| | | from = Color(colorRange.from);
|
| | | to = Color(colorRange.to);
|
| | | }
|
| | | |
| | | each(this.data, function (point) {
|
| | | var value = point[colorKey],
|
| | | range,
|
| | | color,
|
| | | i,
|
| | | pos;
|
| | |
|
| | | if (valueRanges) {
|
| | | i = valueRanges.length;
|
| | | while (i--) {
|
| | | range = valueRanges[i];
|
| | | from = range.from;
|
| | | to = range.to;
|
| | | if ((from === UNDEFINED || value >= from) && (to === UNDEFINED || value <= to)) {
|
| | | color = range.color;
|
| | | break;
|
| | | }
|
| | | |
| | | }
|
| | | } else if (colorRange && value !== undefined) {
|
| | |
|
| | | pos = 1 - ((dataMax - value) / (dataMax - dataMin));
|
| | | color = value === null ? seriesOptions.nullColor : tweenColors(from, to, pos);
|
| | | }
|
| | |
|
| | | if (color) {
|
| | | point.color = null; // reset from previous drilldowns, use of the same data options
|
| | | point.options.color = color;
|
| | | }
|
| | | });
|
| | | },
|
| | | |
| | | drawGraph: noop,
|
| | | |
| | | /**
|
| | | * We need the points' bounding boxes in order to draw the data labels, so |
| | | * we skip it now and call if from drawPoints instead.
|
| | | */
|
| | | drawDataLabels: noop,
|
| | | |
| | | /** |
| | | * Use the drawPoints method of column, that is able to handle simple shapeArgs.
|
| | | * Extend it by assigning the tooltip position.
|
| | | */
|
| | | drawPoints: function () {
|
| | | var series = this,
|
| | | xAxis = series.xAxis,
|
| | | yAxis = series.yAxis,
|
| | | colorKey = series.colorKey;
|
| | | |
| | | // Make points pass test in drawing
|
| | | each(series.data, function (point) {
|
| | | point.plotY = 1; // pass null test in column.drawPoints
|
| | | if (point[colorKey] === null) {
|
| | | point[colorKey] = 0;
|
| | | point.isNull = true;
|
| | | }
|
| | | });
|
| | | |
| | | // Draw them
|
| | | seriesTypes.column.prototype.drawPoints.apply(series);
|
| | | |
| | | each(series.data, function (point) {
|
| | |
|
| | | var dataLabels = point.dataLabels,
|
| | | minX = xAxis.toPixels(point._minX, true),
|
| | | maxX = xAxis.toPixels(point._maxX, true),
|
| | | minY = yAxis.toPixels(point._minY, true),
|
| | | maxY = yAxis.toPixels(point._maxY, true);
|
| | |
|
| | | point.plotX = Math.round(minX + (maxX - minX) * pick(dataLabels && dataLabels.anchorX, 0.5));
|
| | | point.plotY = Math.round(minY + (maxY - minY) * pick(dataLabels && dataLabels.anchorY, 0.5)); |
| | | |
| | | |
| | | // Reset escaped null points
|
| | | if (point.isNull) {
|
| | | point[colorKey] = null;
|
| | | }
|
| | | });
|
| | |
|
| | | // Now draw the data labels
|
| | | Highcharts.Series.prototype.drawDataLabels.call(series);
|
| | | |
| | | },
|
| | |
|
| | | /**
|
| | | * Animate in the new series from the clicked point in the old series.
|
| | | * Depends on the drilldown.js module
|
| | | */
|
| | | animateDrilldown: function (init) {
|
| | | var toBox = this.chart.plotBox,
|
| | | level = this.chart.drilldownLevels[this.chart.drilldownLevels.length - 1],
|
| | | fromBox = level.bBox,
|
| | | animationOptions = this.chart.options.drilldown.animation,
|
| | | scale;
|
| | | |
| | | if (!init) {
|
| | |
|
| | | scale = Math.min(fromBox.width / toBox.width, fromBox.height / toBox.height);
|
| | | level.shapeArgs = {
|
| | | scaleX: scale,
|
| | | scaleY: scale,
|
| | | translateX: fromBox.x,
|
| | | translateY: fromBox.y
|
| | | };
|
| | | |
| | | // TODO: Animate this.group instead
|
| | | each(this.points, function (point) {
|
| | |
|
| | | point.graphic
|
| | | .attr(level.shapeArgs)
|
| | | .animate({
|
| | | scaleX: 1,
|
| | | scaleY: 1,
|
| | | translateX: 0,
|
| | | translateY: 0
|
| | | }, animationOptions);
|
| | |
|
| | | });
|
| | |
|
| | | delete this.animate;
|
| | | }
|
| | | |
| | | },
|
| | |
|
| | | /**
|
| | | * When drilling up, pull out the individual point graphics from the lower series
|
| | | * and animate them into the origin point in the upper series.
|
| | | */
|
| | | animateDrillupFrom: function (level) {
|
| | | seriesTypes.column.prototype.animateDrillupFrom.call(this, level);
|
| | | },
|
| | |
|
| | |
|
| | | /**
|
| | | * When drilling up, keep the upper series invisible until the lower series has
|
| | | * moved into place
|
| | | */
|
| | | animateDrillupTo: function (init) {
|
| | | seriesTypes.column.prototype.animateDrillupTo.call(this, init);
|
| | | }
|
| | | });
|
| | |
|
| | |
|
| | | // The mapline series type
|
| | | plotOptions.mapline = merge(plotOptions.map, {
|
| | | lineWidth: 1,
|
| | | backgroundColor: 'none'
|
| | | });
|
| | | seriesTypes.mapline = Highcharts.extendClass(seriesTypes.map, {
|
| | | type: 'mapline',
|
| | | pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
|
| | | stroke: 'color',
|
| | | 'stroke-width': 'lineWidth',
|
| | | fill: 'backgroundColor'
|
| | | },
|
| | | drawLegendSymbol: seriesTypes.line.prototype.drawLegendSymbol
|
| | | });
|
| | |
|
| | | // The mappoint series type
|
| | | plotOptions.mappoint = merge(plotOptions.scatter, {
|
| | | dataLabels: {
|
| | | enabled: true,
|
| | | format: '{point.name}',
|
| | | color: 'black',
|
| | | style: {
|
| | | textShadow: '0 0 5px white'
|
| | | }
|
| | | }
|
| | | });
|
| | | seriesTypes.mappoint = Highcharts.extendClass(seriesTypes.scatter, {
|
| | | type: 'mappoint'
|
| | | });
|
| | | |
| | |
|
| | | |
| | | /**
|
| | | * A wrapper for Chart with all the default values for a Map
|
| | | */
|
| | | Highcharts.Map = function (options, callback) {
|
| | | |
| | | var hiddenAxis = {
|
| | | endOnTick: false,
|
| | | gridLineWidth: 0,
|
| | | labels: {
|
| | | enabled: false
|
| | | },
|
| | | lineWidth: 0,
|
| | | minPadding: 0,
|
| | | maxPadding: 0,
|
| | | startOnTick: false,
|
| | | tickWidth: 0,
|
| | | title: null
|
| | | },
|
| | | seriesOptions;
|
| | | |
| | | // Don't merge the data
|
| | | seriesOptions = options.series;
|
| | | options.series = null;
|
| | | |
| | | options = merge({
|
| | | chart: {
|
| | | type: 'map',
|
| | | panning: 'xy'
|
| | | },
|
| | | xAxis: hiddenAxis,
|
| | | yAxis: merge(hiddenAxis, { reversed: true }) |
| | | },
|
| | | options, // user's options
|
| | | |
| | | { // forced options
|
| | | chart: {
|
| | | inverted: false
|
| | | }
|
| | | });
|
| | | |
| | | options.series = seriesOptions;
|
| | | |
| | | |
| | | return new Highcharts.Chart(options, callback);
|
| | | };
|
| | | }(Highcharts));
|