app/assets/javascripts/highcharts/modules/map.js in highcharts-rails-3.0.7 vs app/assets/javascripts/highcharts/modules/map.js in highcharts-rails-3.0.8

- old
+ new

@@ -1,42 +1,37 @@ /** - * @license Map plugin v0.1 for Highcharts + * @license Map plugin v0.2 for Highcharts * - * (c) 2011-2013 Torstein Hønsi + * (c) 2011-2014 Torstein Honsi * * License: www.highcharts.com/license */ -/* - * See www.H.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 - */ /*global HighchartsAdapter*/ (function (H) { var UNDEFINED, Axis = H.Axis, Chart = H.Chart, + Color = H.Color, Point = H.Point, Pointer = H.Pointer, + Legend = H.Legend, + Series = H.Series, SVGRenderer = H.SVGRenderer, VMLRenderer = H.VMLRenderer, + symbols = SVGRenderer.prototype.symbols, each = H.each, extend = H.extend, extendClass = H.extendClass, merge = H.merge, pick = H.pick, numberFormat = H.numberFormat, defaultOptions = H.getOptions(), seriesTypes = H.seriesTypes, - inArray = HighchartsAdapter.inArray, plotOptions = defaultOptions.plotOptions, wrap = H.wrap, - Color = H.Color, noop = function () {}; // Add language extend(defaultOptions.lang, { zoomIn: 'Zoom in', @@ -164,67 +159,63 @@ 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; + padAxis, + fixTo, + fixDiff; + // Run the parent method proceed.call(this); // On Y axis, handle both - if (chart.options.chart.type === 'map' && !isXAxis && xAxis.transA !== UNDEFINED) { + if (chart.options.chart.preserveAspectRatio && this.coll === 'yAxis' && 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); + mapRatio = chart.mapRatio = plotRatio / ((xAxis.max - xAxis.min) / (this.max - this.min)); // What axis to pad to put the map in the middle - padAxis = mapRatio > plotRatio ? this : xAxis; - + padAxis = mapRatio < 1 ? this : xAxis; + // Pad it adjustedAxisLength = (padAxis.max - padAxis.min) * padAxis.transA; - padAxis.minPixelPadding = (padAxis.len - adjustedAxisLength) / 2; + padAxis.pixelPadding = padAxis.len - adjustedAxisLength; + padAxis.minPixelPadding = padAxis.pixelPadding / 2; + + fixTo = padAxis.fixTo; + if (fixTo) { + fixDiff = fixTo[1] - padAxis.toValue(fixTo[0], true); + fixDiff *= padAxis.transA; + if (Math.abs(fixDiff) > padAxis.minPixelPadding) { // zooming out again, keep within restricted area + fixDiff = 0; + } + padAxis.minPixelPadding -= fixDiff; + + } } }); - - //--- 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 (pick(mapNavigation.enableDoubleClickZoom, mapNavigation.enabled) || mapNavigation.enableDoubleClickZoomTo) { - H.addEvent(chart.container, 'dblclick', function (e) { - chart.pointer.onContainerDblClick(e); - }); - } - - // Add the mousewheel event - if (pick(mapNavigation.enableMouseWheelZoom, mapNavigation.enabled)) { - H.addEvent(chart.container, document.onmousewheel === undefined ? 'DOMMouseScroll' : 'mousewheel', function (e) { - chart.pointer.onContainerMouseWheel(e); - }); - } + /** + * Override Axis.render in order to delete the fixTo prop + */ + wrap(Axis.prototype, 'render', function (proceed) { + proceed.call(this); + this.fixTo = null; }); // Extend the Pointer extend(Pointer.prototype, { @@ -236,17 +227,19 @@ e = this.normalize(e); if (chart.options.mapNavigation.enableDoubleClickZoomTo) { if (chart.pointer.inClass(e.target, 'highcharts-tracker')) { - chart.zoomToShape(chart.hoverPoint); + chart.hoverPoint.zoomTo(); } } else 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) + chart.yAxis[0].toValue(e.chartY), + e.chartX, + e.chartY ); } }, /** @@ -260,13 +253,15 @@ // 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, + delta > 0 ? 2 : 1 / 2, chart.xAxis[0].toValue(e.chartX), - chart.yAxis[0].toValue(e.chartY) + chart.yAxis[0].toValue(e.chartY), + delta > 0 ? undefined : e.chartX, + delta > 0 ? undefined : e.chartY ); } } }); @@ -302,10 +297,406 @@ xBigger ? transform.scaleX : transform.scaleY ); } }); + + + + /** + * The ColorAxis object for inclusion in gradient legends + */ + var ColorAxis = H.ColorAxis = function () { + this.init.apply(this, arguments); + }; + extend(ColorAxis.prototype, Axis.prototype); + extend(ColorAxis.prototype, { + defaultColorAxisOptions: { + lineWidth: 0, + gridLineWidth: 1, + tickPixelInterval: 72, + startOnTick: true, + endOnTick: true, + offset: 0, + marker: { // docs: use another name? + animation: { + duration: 50 + }, + color: 'gray', + width: 0.01 + }, + labels: { + overflow: 'justify' + }, + minColor: '#EFEFFF', + maxColor: '#102d4c' + }, + init: function (chart, userOptions) { + var horiz = chart.options.legend.layout !== 'vertical', + options; + + // Build the options + options = merge(this.defaultColorAxisOptions, { + side: horiz ? 2 : 1, + reversed: !horiz + }, userOptions, { + isX: horiz, + opposite: !horiz, + showEmpty: false, + title: null + }); + + Axis.prototype.init.call(this, chart, options); + + // Base init() pushes it to the xAxis array, now pop it again + //chart[this.isXAxis ? 'xAxis' : 'yAxis'].pop(); + + // Prepare data classes + if (userOptions.dataClasses) { + this.initDataClasses(userOptions); + } + + // Override original axis properties + this.isXAxis = true; + this.horiz = horiz; + }, + + initDataClasses: function (userOptions) { + var chart = this.chart, + dataClasses, + colorCounter = 0, + options = this.options; + this.dataClasses = dataClasses = []; + + each(userOptions.dataClasses, function (dataClass, i) { + var colors; + + dataClass = merge(dataClass); + dataClasses.push(dataClass); + if (!dataClass.color) { + if (options.dataClassColor === 'category') { + colors = chart.options.colors; + dataClass.color = colors[colorCounter++]; + // loop back to zero + if (colorCounter === colors.length) { + colorCounter = 0; + } + } else { + dataClass.color = tweenColors(Color(options.minColor), Color(options.maxColor), i / (userOptions.dataClasses.length - 1)); + } + } + }); + }, + + /** + * Extend the setOptions method to process extreme colors and color + * stops. + */ + setOptions: function (userOptions) { + Axis.prototype.setOptions.call(this, userOptions); + + this.options.crosshair = this.options.marker; + + this.stops = userOptions.stops || [ + [0, this.options.minColor], + [1, this.options.maxColor] + ]; + each(this.stops, function (stop) { + stop.color = Color(stop[1]); + }); + this.coll = 'colorAxis'; + }, + + setAxisSize: function () { + var symbol = this.legendSymbol, + chart = this.chart; + + if (symbol) { + this.left = symbol.x; + this.top = symbol.y; + this.width = symbol.width; + this.height = symbol.height; + this.right = chart.chartWidth - this.left - this.width; + this.bottom = chart.chartHeight - this.top - this.height; + + this.len = this.horiz ? this.width : this.height; + this.pos = this.horiz ? this.left : this.top; + } + }, + + /** + * Translate from a value to a color + */ + toColor: function (value, point) { + var pos, + stops = this.stops, + from, + to, + color, + dataClasses = this.dataClasses, + dataClass, + i; + + if (dataClasses) { + i = dataClasses.length; + while (i--) { + dataClass = dataClasses[i]; + from = dataClass.from; + to = dataClass.to; + if ((from === UNDEFINED || value >= from) && (to === UNDEFINED || value <= to)) { + color = dataClass.color; + if (point) { + point.dataClass = i; + } + break; + } + } + + } else { + + if (this.isLog) { + value = this.val2lin(value); + } + pos = 1 - ((this.max - value) / (this.max - this.min)); + i = stops.length; + while (i--) { + if (pos > stops[i][0]) { + break; + } + } + from = stops[i] || stops[i + 1]; + to = stops[i + 1] || from; + + // The position within the gradient + pos = 1 - (to[0] - pos) / ((to[0] - from[0]) || 1); + + color = tweenColors( + from.color, + to.color, + pos + ); + } + return color; + }, + + getOffset: function () { + var group = this.legendGroup; + if (group) { + + Axis.prototype.getOffset.call(this); + + if (!this.axisGroup.parentGroup) { + + // Move the axis elements inside the legend group + this.axisGroup.add(group); + this.gridGroup.add(group); + this.labelGroup.add(group); + + this.added = true; + } + } + }, + + /** + * Create the color gradient + */ + setLegendColor: function () { + var grad, + horiz = this.horiz, + options = this.options; + + grad = horiz ? [0, 0, 1, 0] : [0, 0, 0, 1]; + this.legendColor = { + linearGradient: { x1: grad[0], y1: grad[1], x2: grad[2], y2: grad[3] }, + stops: options.stops || [ + [0, options.minColor], + [1, options.maxColor] + ] + }; + }, + + /** + * The color axis appears inside the legend and has its own legend symbol + */ + drawLegendSymbol: function (legend, item) { + var padding = legend.padding, + legendOptions = legend.options, + horiz = this.horiz, + box, + width = pick(legendOptions.symbolWidth, horiz ? 200 : 12), + height = pick(legendOptions.symbolHeight, horiz ? 12 : 200), + labelPadding = pick(legendOptions.labelPadding, horiz ? 10 : 30); + + this.setLegendColor(); + + // Create the gradient + item.legendSymbol = this.chart.renderer.rect( + 0, + legend.baseline - 11, + width, + height + ).attr({ + zIndex: 1 + }).add(item.legendGroup); + box = item.legendSymbol.getBBox(); + + // Set how much space this legend item takes up + this.legendItemWidth = width + padding + (horiz ? 0 : labelPadding); + this.legendItemHeight = height + padding + (horiz ? labelPadding : 0); + }, + /** + * Fool the legend + */ + setState: noop, + visible: true, + setVisible: noop, + getSeriesExtremes: function () { + var series; + if (this.series.length) { + series = this.series[0]; + this.dataMin = series.valueMin; + this.dataMax = series.valueMax; + } + }, + drawCrosshair: function (e, point) { + var newCross = !this.cross, + plotX = point && point.plotX, + plotY = point && point.plotY, + crossPos, + axisPos = this.pos, + axisLen = this.len; + + if (point) { + crossPos = this.toPixels(point.value); + if (crossPos < axisPos) { + crossPos = axisPos - 2; + } else if (crossPos > axisPos + axisLen) { + crossPos = axisPos + axisLen + 2; + } + + point.plotX = crossPos; + point.plotY = this.len - crossPos; + Axis.prototype.drawCrosshair.call(this, e, point); + point.plotX = plotX; + point.plotY = plotY; + + if (!newCross && this.cross) { + this.cross + .attr({ + fill: this.crosshair.color + }) + .add(this.labelGroup); + } + } + }, + getPlotLinePath: function (a, b, c, d, pos) { + if (pos) { // crosshairs only + return this.horiz ? + ['M', pos - 4, this.top - 6, 'L', pos + 4, this.top - 6, pos, this.top, 'Z'] : + ['M', this.left, pos, 'L', this.left - 6, pos + 6, this.left - 6, pos - 6, 'Z']; + } else { + return Axis.prototype.getPlotLinePath.call(this, a, b, c, d); + } + }, + + update: function (newOptions, redraw) { + Axis.prototype.update.call(this, newOptions, redraw); + if (this.legendItem) { + this.setLegendColor(); + this.chart.legend.colorizeItem(this, true); + } + }, + + /** + * Get the legend item symbols for data classes + */ + getDataClassLegendSymbols: function () { + var axis = this, + chart = this.chart, + legendItems = [], + legendOptions = chart.options.legend, + valueDecimals = legendOptions.valueDecimals, + valueSuffix = legendOptions.valueSuffix || '', + name; + + each(this.dataClasses, function (dataClass, i) { + var vis = true, + from = dataClass.from, + to = dataClass.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) + valueSuffix; + } + if (from !== UNDEFINED && to !== UNDEFINED) { + name += ' - '; + } + if (to !== UNDEFINED) { + name += numberFormat(to, valueDecimals) + valueSuffix; + } + + // Add a mock object to the legend items + legendItems.push(H.extend({ + chart: chart, + name: name, + options: {}, + drawLegendSymbol: H.LegendSymbolMixin.drawRectangle, + visible: true, + setState: noop, + setVisible: function () { + vis = this.visible = !vis; + each(axis.series, function (series) { + each(series.points, function (point) { + if (point.dataClass === i) { + point.setVisible(vis); + } + }); + }); + + chart.legend.colorizeItem(this, vis); + } + }, dataClass)); + }); + return legendItems; + } + }); + + /** + * Wrap the legend getAllItems method to add the color axis. This also removes the + * axis' own series to prevent them from showing up individually. + */ + wrap(Legend.prototype, 'getAllItems', function (proceed) { + var allItems = [], + colorAxis = this.chart.colorAxis[0]; + + if (colorAxis) { + + // Data classes + if (colorAxis.options.dataClasses) { + allItems = allItems.concat(colorAxis.getDataClassLegendSymbols()); + // Gradient legend + } else { + // Add this axis on top + allItems.push(colorAxis); + } + + // Don't add the color axis' series + each(colorAxis.series, function (series) { + series.options.showInLegend = false; + }); + } + + return allItems.concat(proceed.call(this)); + }); + + // Add events to the Chart object itself extend(Chart.prototype, { renderMapNavigation: function () { var chart = this, options = this.options.mapNavigation, @@ -317,11 +708,11 @@ states, outerHandler = function () { this.handler.call(chart); }; - if (pick(options.enableButtons, options.enabled)) { + if (pick(options.enableButtons, options.enabled) && !chart.renderer.forExport) { for (n in buttons) { if (buttons.hasOwnProperty(n)) { buttonOptions = merge(options.buttonOptions, buttons[n]); attr = buttonOptions.theme; states = attr.states; @@ -382,29 +773,29 @@ }, /** * 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) { + mapZoom: function (howMuch, centerXArg, centerYArg, mouseX, mouseY) { + /*if (this.isMapZooming) { + this.mapZoomQueue = arguments; 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, + fixToX = mouseX ? ((mouseX - xAxis.pos) / xAxis.len) : 0.5, + fixToY = mouseY ? ((mouseY - yAxis.pos) / yAxis.len) : 0.5, + newXMin = centerX - newXRange * fixToX, + newYMin = centerY - newYRange * fixToY, newExt = chart.fitToBox({ x: newXMin, y: newYMin, width: newXRange, height: newYRange @@ -413,96 +804,151 @@ 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); + // When mousewheel zooming, fix the point under the mouse + if (mouseX) { + xAxis.fixTo = [mouseX - xAxis.pos, centerXArg]; + } + if (mouseY) { + yAxis.fixTo = [mouseY - yAxis.pos, centerYArg]; + } + // Zoom + if (howMuch !== undefined) { + xAxis.setExtremes(newExt.x, newExt.x + newExt.width, false); + yAxis.setExtremes(newExt.y, newExt.y + newExt.height, false); + + // Reset zoom + } else { + xAxis.setExtremes(undefined, undefined, false); + yAxis.setExtremes(undefined, undefined, false); + } + // Prevent zooming until this one is finished animating - delay = animation ? animation.duration || 500 : 0; + /*delay = animation ? animation.duration || 500 : 0; if (delay) { chart.isMapZooming = true; setTimeout(function () { chart.isMapZooming = false; + if (chart.mapZoomQueue) { + chart.mapZoom.apply(chart, chart.mapZoomQueue); + } + chart.mapZoomQueue = null; }, delay); - } - + }*/ chart.redraw(); - }, + } + }); - /** - * Zoom the chart to view a specific area point - */ - zoomToShape: function (point) { - var series = point.series, - chart = series.chart; + /** + * Extend the chart getAxes method to also get the color axis + */ + wrap(Chart.prototype, 'getAxes', function (proceed) { - series.xAxis.setExtremes( - point._minX, - point._maxX, - false - ); - series.yAxis.setExtremes( - point._minY, - point._maxY, - false - ); - chart.redraw(); + var options = this.options, + colorAxisOptions = options.colorAxis; + + proceed.call(this); + + this.colorAxis = []; + if (colorAxisOptions) { + proceed = new ColorAxis(this, colorAxisOptions); // Fake assignment for jsLint } }); + + /** + * Extend the Chart.render method to add zooming and panning + */ + 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 (pick(mapNavigation.enableDoubleClickZoom, mapNavigation.enabled) || mapNavigation.enableDoubleClickZoomTo) { + H.addEvent(chart.container, 'dblclick', function (e) { + chart.pointer.onContainerDblClick(e); + }); + } + + // Add the mousewheel event + if (pick(mapNavigation.enableMouseWheelZoom, mapNavigation.enabled)) { + H.addEvent(chart.container, document.onmousewheel === undefined ? 'DOMMouseScroll' : 'mousewheel', function (e) { + chart.pointer.onContainerMouseWheel(e); + return false; + }); + } + }); + + /** * Extend the default options with map options */ plotOptions.map = merge(plotOptions.scatter, { + allAreas: true, animation: false, // makes the complex shapes slow nullColor: '#F8F8F8', borderColor: 'silver', borderWidth: 1, marker: null, stickyTracking: false, dataLabels: { + format: '{point.value}', verticalAlign: 'middle' }, turboThreshold: 0, tooltip: { followPointer: true, - pointFormat: '{point.name}: {point.y}<br/>' + pointFormat: '{point.name}: {point.value}<br/>' }, states: { normal: { animation: true + }, + hover: { + brightness: 0.2 } } }); + /** + * The MapAreaPoint object + */ var MapAreaPoint = extendClass(Point, { /** * Extend the Point object to split paths */ applyOptions: function (options, x) { var point = Point.prototype.applyOptions.call(this, options, x), series = this.series, seriesOptions = series.options, - joinBy = seriesOptions.dataJoinBy, + joinBy = seriesOptions.joinBy, mapPoint; - if (joinBy && seriesOptions.mapData) { - mapPoint = series.getMapData(joinBy, point[joinBy]); + if (seriesOptions.mapData) { + mapPoint = joinBy ? + series.getMapData(joinBy, point[joinBy]) : // Join by a string + seriesOptions.mapData[point.x]; // Use array position (faster) if (mapPoint) { // This applies only to bubbles if (series.xyFromShape) { point.x = mapPoint._midX; point.y = mapPoint._midY; } extend(point, mapPoint); // copy over properties } else { - point.y = point.y || null; + point.value = point.value || null; } } return point; }, @@ -541,11 +987,11 @@ 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) { + if (duration && normalColor.rgba.length === 4 && hoverColor.rgba.length === 4 && point.state !== 'select') { delete point.pointAttr[''].fill; // avoid resetting it in Point.setState clearTimeout(point.colorInterval); point.colorInterval = setInterval(function () { var pos = (new Date() - start) / duration, @@ -560,10 +1006,30 @@ clearTimeout(point.colorInterval); } }, 13); } Point.prototype.onMouseOut.call(point); + }, + + /** + * Zoom the chart to view a specific area point + */ + zoomTo: function () { + var point = this, + series = point.series; + + series.xAxis.setExtremes( + point._minX, + point._maxX, + false + ); + series.yAxis.setExtremes( + point._minY, + point._maxY, + false + ); + series.chart.redraw(); } }); /** * Add the series type @@ -571,207 +1037,25 @@ seriesTypes.map = extendClass(seriesTypes.scatter, { type: 'map', pointAttrToOptions: { // mapping between SVG attributes and the corresponding options stroke: 'borderColor', 'stroke-width': 'borderWidth', - fill: 'color' + fill: 'color', + dashstyle: 'dashStyle' }, - colorKey: 'y', pointClass: MapAreaPoint, + pointArrayMap: ['value'], + axisTypes: ['xAxis', 'yAxis', 'colorAxis'], + optionalAxis: 'colorAxis', 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, - legendOptions = chart.options.legend, - valueDecimals = legendOptions.valueDecimals, - valueSuffix = legendOptions.valueSuffix || '', - legendItems = [], - name, - from, - to, - fromLabel, - toLabel, - colorRange, - valueRanges, - gradientColor, - grad, - tmpLabel, - horizontal = chart.options.legend.layout === 'horizontal'; + parallelArrays: ['x', 'y', 'value'], - - H.Series.prototype.init.apply(this, arguments); - colorRange = series.options.colorRange; - valueRanges = series.options.valueRanges; - - if (valueRanges) { - each(valueRanges, function (range, i) { - var vis = true; - 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) + valueSuffix; - } - if (from !== UNDEFINED && to !== UNDEFINED) { - name += ' - '; - } - if (to !== UNDEFINED) { - name += numberFormat(to, valueDecimals) + valueSuffix; - } - - // Add a mock object to the legend items - legendItems.push(H.extend({ - chart: series.chart, - name: name, - options: {}, - drawLegendSymbol: seriesTypes.area.prototype.drawLegendSymbol, - visible: true, - setState: noop, - setVisible: function () { - vis = this.visible = !vis; - each(series.points, function (point) { - if (point.valueRange === i) { - point.setVisible(vis); - } - }); - - chart.legend.colorizeItem(this, vis); - } - }, 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: noop, - setVisible: noop - }]; - - 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, @@ -808,12 +1092,12 @@ } even = !even; } } // Cache point bounding box for use to position data labels, bubbles etc - point._midX = pointMinX + (pointMaxX - pointMinX) * pick(point.middleX, 0.5); - point._midY = pointMinY + (pointMaxY - pointMinY) * pick(point.middleY, 0.5); + point._midX = pointMinX + (pointMaxX - pointMinX) * (point.middleX || 0.5); // pick is slower and very marginally needed + point._midY = pointMinY + (pointMaxY - pointMinY) * (point.middleY || 0.5); point._maxX = pointMaxX; point._minX = pointMinX; point._maxY = pointMaxY; point._minY = pointMinY; point._foundBox = true; @@ -836,10 +1120,22 @@ this.maxX = Math.max(maxX, pick(this.maxX, Number.MIN_VALUE)); } }, getExtremes: function () { + // Get the actual value extremes for colors + Series.prototype.getExtremes.call(this, this.valueData); + + // Recalculate box on updated data + if (this.chart.hasRendered && this.isDirtyData) { + this.getBox(this.options.data); + } + + this.valueMin = this.dataMin; + this.valueMax = this.dataMax; + + // Extremes for the mock Y axis this.dataMin = this.minY; this.dataMax = this.maxY; }, /** @@ -850,41 +1146,46 @@ var series = this, even = false, // while loop reads from the end xAxis = series.xAxis, yAxis = series.yAxis, - i; + xMin = xAxis.min, + xTransA = xAxis.transA, + xMinPixelPadding = xAxis.minPixelPadding, + yMin = yAxis.min, + yTransA = yAxis.transA, + yMinPixelPadding = yAxis.minPixelPadding, + i, + ret = []; // Preserve the original - // 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] = xAxis.translate(path[i]); - } else { // odd = Y - path[i] = yAxis.len - yAxis.translate(path[i]); + if (path) { + i = path.length; + while (i--) { + if (typeof path[i] === 'number') { + ret[i] = even ? + (path[i] - xMin) * xTransA + xMinPixelPadding : + (path[i] - yMin) * yTransA + yMinPixelPadding; + even = !even; + } else { + ret[i] = path[i]; } - even = !even; } } - - return path; + return ret; }, /** * Extend setData to join in mapData. If the allAreas option is true, all areas * from the mapData are used, and those that don't correspond to a data value * are given null values. */ setData: function (data, redraw) { var options = this.options, mapData = options.mapData, - joinBy = options.dataJoinBy, + joinBy = options.joinBy, dataUsed = []; this.getBox(data); this.getBox(mapData); @@ -898,17 +1199,18 @@ dataUsed.push(point[joinBy]); }); } // Add those map points that don't correspond to data, which will be drawn as null points + dataUsed = '|' + dataUsed.join('|') + '|'; // String search is faster than array.indexOf each(mapData, function (mapPoint) { - if (!joinBy || inArray(mapPoint[joinBy], dataUsed) === -1) { - data.push(merge(mapPoint, { y: null })); + if (!joinBy || dataUsed.indexOf('|' + mapPoint[joinBy] + '|') === -1) { + data.push(merge(mapPoint, { value: null })); } }); } - H.Series.prototype.setData.call(this, data, redraw); + Series.prototype.setData.call(this, data, redraw); }, /** * For each point, get the corresponding map data */ @@ -918,11 +1220,11 @@ mapMap = this.mapMap, i = mapData.length; // Create a cache for quicker lookup second time if (!mapMap) { - mapMap = this.mapMap = []; + mapMap = this.mapMap = {}; } if (mapMap[value] !== undefined) { return mapData[mapMap[value]]; } else if (value !== undefined) { @@ -934,144 +1236,221 @@ } } }, /** - * 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, - nullColor = seriesOptions.nullColor, - from, - to; + translateColors: function () { + var series = this, + nullColor = this.options.nullColor, + colorAxis = this.colorAxis; - if (colorRange) { - from = Color(colorRange.from); - to = Color(colorRange.to); - } each(this.data, function (point) { - var value = point[colorKey], - isNull = value === null, - range, - color, - i, - pos; + var value = point.value, + color; - if (valueRanges) { - i = valueRanges.length; - if (isNull) { - color = nullColor; - } else { - while (i--) { - range = valueRanges[i]; - from = range.from; - to = range.to; - if ((from === UNDEFINED || value >= from) && (to === UNDEFINED || value <= to)) { - color = range.color; - break; - } - } - point.valueRange = i; - } - } else if (colorRange && !isNull) { + color = value === null ? nullColor : colorAxis ? colorAxis.toColor(value, point) : (point.color) || series.color; - pos = 1 - ((dataMax - value) / (dataMax - dataMin)); - color = tweenColors(from, to, pos); - } else if (isNull) { - color = nullColor; - } - if (color) { - point.color = null; // reset from previous drilldowns, use of the same data options - point.options.color = color; + point.color = point.options.color = color; } }); }, + /** + * No graph for the map series + */ drawGraph: noop, /** * We need the points' bounding boxes in order to draw the data labels, so * we skip it now and call it 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. + /** + * Add the path option for data points. Find the max value for color calculation. */ - drawPoints: function () { + translate: function () { var series = this, xAxis = series.xAxis, - yAxis = series.yAxis, - colorKey = series.colorKey; + yAxis = series.yAxis; + + series.generatePoints(); - // 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) { - // Record the middle point (loosely based on centroid), determined // by the middleX and middleY options. point.plotX = xAxis.toPixels(point._midX, true); point.plotY = yAxis.toPixels(point._midY, true); - - // Reset escaped null points - if (point.isNull) { - point[colorKey] = null; + + if (series.isDirtyData || series.chart.renderer.isVML) { + + point.shapeType = 'path'; + point.shapeArgs = { + //d: display ? series.translatePath(point.path) : '' + d: series.translatePath(point.path), + 'vector-effect': 'non-scaling-stroke' + }; } }); + + series.translateColors(); + }, + + /** + * 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, + scale, + translateX, + group = series.group, + chart = series.chart, + renderer = chart.renderer, + translateY, + getTranslate = function (axis, mapRatio) { + var dataMin = axis.dataMin, + dataMax = axis.dataMax, + fullDataMin = dataMin - ((dataMax - dataMin) * (mapRatio - 1) / 2), + fullMin = axis.min - axis.minPixelPadding / axis.transA, + minOffset = fullMin - fullDataMin, + centerOffset = (dataMax - dataMin - axis.max + axis.min) * mapRatio, + center = minOffset / centerOffset; + return (axis.len * (1 - scale)) * center; + }; + // Set a group that handles transform during zooming and panning in order to preserve clipping + // on series.group + if (!series.transformGroup) { + series.transformGroup = renderer.g() + .attr({ + scaleX: 1, + scaleY: 1 + }) + .add(group); + } + + // Draw the shapes again + if (series.isDirtyData || renderer.isVML) { + + // Draw them in transformGroup + series.group = series.transformGroup; + seriesTypes.column.prototype.drawPoints.apply(series); + series.group = group; // Reset + + // Individual point actions + each(series.points, function (point) { + + // Reset color on update/redraw + if (chart.hasRendered && point.graphic) { + point.graphic.attr('fill', point.options.color); + } + + }); + + // Set the base for later scale-zooming + this.transA = xAxis.transA; + + // Just update the scale and transform for better performance + } else { + scale = xAxis.transA / this.transA; + if (scale > 0.99 && scale < 1.01) { // rounding errors + translateX = 0; + translateY = 0; + scale = 1; + + } else { + translateX = getTranslate(xAxis, Math.max(1, series.chart.mapRatio)); + translateY = getTranslate(yAxis, 1 / Math.min(1, series.chart.mapRatio)); + } + + this.transformGroup.animate({ + translateX: translateX, + translateY: translateY, + scaleX: scale, + scaleY: scale + }); + + } + + // Now draw the data labels - H.Series.prototype.drawDataLabels.call(series); + Series.prototype.drawDataLabels.call(series); }, /** + * Override render to throw in an async call in IE8. Otherwise it chokes on the US counties demo. + */ + render: function () { + var series = this, + render = Series.prototype.render; + + // Give IE8 some time to breathe. + if (series.chart.renderer.isVML && series.data.length > 3000) { + setTimeout(function () { + render.call(series); + }); + } else { + render.call(series); + } + }, + + /** + * The initial animation for the map series. By default, animation is disabled. + * Animation of map shapes is not at all supported in VML browsers. + */ + animate: function (init) { + var chart = this.chart, + animation = this.options.animation, + group = this.group, + xAxis = this.xAxis, + yAxis = this.yAxis, + left = xAxis.pos, + top = yAxis.pos; + + if (chart.renderer.isSVG) { + + if (animation === true) { + animation = { + duration: 1000 + }; + } + + // Initialize the animation + if (init) { + + // Scale down the group and place it in the center + group.attr({ + translateX: left + xAxis.len / 2, + translateY: top + yAxis.len / 2, + scaleX: 0.001, // #1499 + scaleY: 0.001 + }); + + // Run the animation + } else { + group.animate({ + translateX: left, + translateY: top, + scaleX: 1, + scaleY: 1 + }, animation); + + // Delete this function to allow it only once + this.animate = null; + } + } + }, + + /** * 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, @@ -1102,15 +1481,17 @@ translateY: 0 }, animationOptions); }); - delete this.animate; + this.animate = null; } }, + drawLegendSymbol: H.LegendSymbolMixin.drawRectangle, + /** * 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) { @@ -1129,18 +1510,18 @@ // The mapline series type plotOptions.mapline = merge(plotOptions.map, { lineWidth: 1, - backgroundColor: 'none' + fillColor: 'none' }); seriesTypes.mapline = extendClass(seriesTypes.map, { type: 'mapline', pointAttrToOptions: { // mapping between SVG attributes and the corresponding options stroke: 'color', 'stroke-width': 'lineWidth', - fill: 'backgroundColor' + fill: 'fillColor' }, drawLegendSymbol: seriesTypes.line.prototype.drawLegendSymbol }); // The mappoint series type @@ -1231,43 +1612,45 @@ H.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 + title: null, + tickPositions: [] + //tickInterval: 500, + //gridZIndex: 10 }, seriesOptions; // Don't merge the data seriesOptions = options.series; options.series = null; options = merge({ chart: { - panning: 'xy' + panning: 'xy', + type: 'map' }, xAxis: hiddenAxis, yAxis: merge(hiddenAxis, { reversed: true }) }, options, // user's options { // forced options chart: { - type: 'map', - inverted: false + inverted: false, + alignTicks: false, + preserveAspectRatio: true } }); options.series = seriesOptions; return new Chart(options, callback); }; }(Highcharts)); +