懒羊羊
2023-08-30 1ac2bc1590406d9babec036e154d8d08f34a6aa1
提交 | 用户 | 时间
1ac2bc 1 /**
2  * @license Map plugin v0.1 for Highcharts
3  *
4  * (c) 2011-2013 Torstein Hønsi
5  *
6  * License: www.highcharts.com/license
7  */
8
9 /* 
10  * See www.highcharts.com/studies/world-map.htm for use case.
11  *
12  * To do:
13  * - Optimize long variable names and alias adapter methods and Highcharts namespace variables
14  * - Zoom and pan GUI
15  */
16 (function (Highcharts) {
17     var UNDEFINED,
18         Axis = Highcharts.Axis,
19         Chart = Highcharts.Chart,
20         Point = Highcharts.Point,
21         Pointer = Highcharts.Pointer,
22         each = Highcharts.each,
23         extend = Highcharts.extend,
24         merge = Highcharts.merge,
25         pick = Highcharts.pick,
26         numberFormat = Highcharts.numberFormat,
27         defaultOptions = Highcharts.getOptions(),
28         seriesTypes = Highcharts.seriesTypes,
29         plotOptions = defaultOptions.plotOptions,
30         wrap = Highcharts.wrap,
31         Color = Highcharts.Color,
32         noop = function () {};
33
34     
35
36     /*
37      * Return an intermediate color between two colors, according to pos where 0
38      * is the from color and 1 is the to color
39      */
40     function tweenColors(from, to, pos) {
41         var i = 4,
42             rgba = [];
43
44         while (i--) {
45             rgba[i] = Math.round(
46                 to.rgba[i] + (from.rgba[i] - to.rgba[i]) * (1 - pos)
47             );
48         }
49         return 'rgba(' + rgba.join(',') + ')';
50     }
51
52     // Set the default map navigation options
53     defaultOptions.mapNavigation = {
54         buttonOptions: {
55             align: 'right',
56             verticalAlign: 'bottom',
57             x: 0,
58             width: 18,
59             height: 18,
60             style: {
61                 fontSize: '15px',
62                 fontWeight: 'bold',
63                 textAlign: 'center'
64             }
65         },
66         buttons: {
67             zoomIn: {
68                 onclick: function () {
69                     this.mapZoom(0.5);
70                 },
71                 text: '+',
72                 y: -32
73             },
74             zoomOut: {
75                 onclick: function () {
76                     this.mapZoom(2);
77                 },
78                 text: '-',
79                 y: 0
80             }
81         }
82         // enableButtons: false,
83         // enableTouchZoom: false,
84         // zoomOnDoubleClick: false,
85         // zoomOnMouseWheel: false
86
87     };
88     
89     /**
90      * Utility for reading SVG paths directly.
91      */
92     Highcharts.splitPath = function (path) {
93         var i;
94
95         // Move letters apart
96         path = path.replace(/([A-Za-z])/g, ' $1 ');
97         // Trim
98         path = path.replace(/^\s*/, "").replace(/\s*$/, "");
99         
100         // Split on spaces and commas
101         path = path.split(/[ ,]+/);
102         
103         // Parse numbers
104         for (i = 0; i < path.length; i++) {
105             if (!/[a-zA-Z]/.test(path[i])) {
106                 path[i] = parseFloat(path[i]);
107             }
108         }
109         return path;
110     };
111
112     // A placeholder for map definitions
113     Highcharts.maps = {};
114     
115     /**
116      * Override to use the extreme coordinates from the SVG shape, not the
117      * data values
118      */
119     wrap(Axis.prototype, 'getSeriesExtremes', function (proceed) {
120         var isXAxis = this.isXAxis,
121             dataMin,
122             dataMax,
123             xData = [];
124
125         // Remove the xData array and cache it locally so that the proceed method doesn't use it
126         each(this.series, function (series, i) {
127             if (series.useMapGeometry) {
128                 xData[i] = series.xData;
129                 series.xData = [];
130             }
131         });
132
133         // Call base to reach normal cartesian series (like mappoint)
134         proceed.call(this);
135
136         // Run extremes logic for map and mapline
137         dataMin = pick(this.dataMin, Number.MAX_VALUE);
138         dataMax = pick(this.dataMax, Number.MIN_VALUE);
139         each(this.series, function (series, i) {
140             if (series.useMapGeometry) {
141                 dataMin = Math.min(dataMin, series[isXAxis ? 'minX' : 'minY']);
142                 dataMax = Math.max(dataMax, series[isXAxis ? 'maxX' : 'maxY']);
143                 series.xData = xData[i]; // Reset xData array
144             }
145         });
146         
147         this.dataMin = dataMin;
148         this.dataMax = dataMax;
149     });
150     
151     /**
152      * Override axis translation to make sure the aspect ratio is always kept
153      */
154     wrap(Axis.prototype, 'setAxisTranslation', function (proceed) {
155         var chart = this.chart,
156             mapRatio,
157             plotRatio = chart.plotWidth / chart.plotHeight,
158             isXAxis = this.isXAxis,
159             adjustedAxisLength,
160             xAxis = chart.xAxis[0],
161             padAxis;
162         
163         // Run the parent method
164         proceed.call(this);
165         
166         // On Y axis, handle both
167         if (chart.options.chart.type === 'map' && !isXAxis && xAxis.transA !== UNDEFINED) {
168             
169             // Use the same translation for both axes
170             this.transA = xAxis.transA = Math.min(this.transA, xAxis.transA);
171             
172             mapRatio = (xAxis.max - xAxis.min) / (this.max - this.min);
173             
174             // What axis to pad to put the map in the middle
175             padAxis = mapRatio > plotRatio ? this : xAxis;
176             
177             // Pad it
178             adjustedAxisLength = (padAxis.max - padAxis.min) * padAxis.transA;
179             padAxis.minPixelPadding = (padAxis.len - adjustedAxisLength) / 2;
180         }
181     });
182
183
184     //--- Start zooming and panning features
185
186     wrap(Chart.prototype, 'render', function (proceed) {
187         var chart = this,
188             mapNavigation = chart.options.mapNavigation;
189
190         proceed.call(chart);
191
192         // Render the plus and minus buttons
193         chart.renderMapNavigation();
194
195         // Add the double click event
196         if (mapNavigation.zoomOnDoubleClick) {
197             Highcharts.addEvent(chart.container, 'dblclick', function (e) {
198                 chart.pointer.onContainerDblClick(e);
199             });
200         }
201
202         // Add the mousewheel event
203         if (mapNavigation.zoomOnMouseWheel) {
204             Highcharts.addEvent(chart.container, document.onmousewheel === undefined ? 'DOMMouseScroll' : 'mousewheel', function (e) {
205                 chart.pointer.onContainerMouseWheel(e);
206             });
207         }
208     });
209
210     // Extend the Pointer
211     extend(Pointer.prototype, {
212
213         /**
214          * The event handler for the doubleclick event
215          */
216         onContainerDblClick: function (e) {
217             var chart = this.chart;
218
219             e = this.normalize(e);
220
221             if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) {
222                 chart.mapZoom(
223                     0.5,
224                     chart.xAxis[0].toValue(e.chartX),
225                     chart.yAxis[0].toValue(e.chartY)
226                 );
227             }
228         },
229
230         /**
231          * The event handler for the mouse scroll event
232          */
233         onContainerMouseWheel: function (e) {
234             var chart = this.chart,
235                 delta;
236
237             e = this.normalize(e);
238
239             // Firefox uses e.detail, WebKit and IE uses wheelDelta
240             delta = e.detail || -(e.wheelDelta / 120);
241             if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) {
242                 chart.mapZoom(
243                     delta > 0 ? 2 : 0.5,
244                     chart.xAxis[0].toValue(e.chartX),
245                     chart.yAxis[0].toValue(e.chartY)
246                 );
247             }
248         }
249     });
250     // Implement the pinchType option
251     wrap(Pointer.prototype, 'init', function (proceed, chart, options) {
252
253         proceed.call(this, chart, options);
254
255         // Pinch status
256         if (options.mapNavigation.enableTouchZoom) {
257             this.pinchX = this.pinchHor = 
258                 this.pinchY = this.pinchVert = true;
259         }
260     });
261
262     // Add events to the Chart object itself
263     extend(Chart.prototype, {
264         renderMapNavigation: function () {
265             var chart = this,
266                 options = this.options.mapNavigation,
267                 buttons = options.buttons,
268                 n,
269                 button,
270                 buttonOptions,
271                 outerHandler = function () { 
272                     this.handler.call(chart); 
273                 };
274
275             if (options.enableButtons) {
276                 for (n in buttons) {
277                     if (buttons.hasOwnProperty(n)) {
278                         buttonOptions = merge(options.buttonOptions, buttons[n]);
279
280                         button = chart.renderer.button(buttonOptions.text, 0, 0, outerHandler)
281                             .attr({
282                                 width: buttonOptions.width,
283                                 height: buttonOptions.height
284                             })
285                             .css(buttonOptions.style)
286                             .add();
287                         button.handler = buttonOptions.onclick;
288                         button.align(extend(buttonOptions, { width: button.width, height: button.height }), null, 'spacingBox');
289                     }
290                 }
291             }
292         },
293
294         /**
295          * Fit an inner box to an outer. If the inner box overflows left or right, align it to the sides of the
296          * outer. If it overflows both sides, fit it within the outer. This is a pattern that occurs more places
297          * in Highcharts, perhaps it should be elevated to a common utility function.
298          */
299         fitToBox: function (inner, outer) {
300             each([['x', 'width'], ['y', 'height']], function (dim) {
301                 var pos = dim[0],
302                     size = dim[1];
303                 if (inner[pos] + inner[size] > outer[pos] + outer[size]) { // right overflow
304                     if (inner[size] > outer[size]) { // the general size is greater, fit fully to outer
305                         inner[size] = outer[size];
306                         inner[pos] = outer[pos];
307                     } else { // align right
308                         inner[pos] = outer[pos] + outer[size] - inner[size];
309                     }
310                 }
311                 if (inner[size] > outer[size]) {
312                     inner[size] = outer[size];
313                 }
314                 if (inner[pos] < outer[pos]) {
315                     inner[pos] = outer[pos];
316                 }
317                 
318             });
319
320             return inner;
321         },
322
323         /**
324          * Zoom the map in or out by a certain amount. Less than 1 zooms in, greater than 1 zooms out.
325          */
326         mapZoom: function (howMuch, centerXArg, centerYArg) {
327
328             if (this.isMapZooming) {
329                 return;
330             }
331
332             var chart = this,
333                 xAxis = chart.xAxis[0],
334                 xRange = xAxis.max - xAxis.min,
335                 centerX = pick(centerXArg, xAxis.min + xRange / 2),
336                 newXRange = xRange * howMuch,
337                 yAxis = chart.yAxis[0],
338                 yRange = yAxis.max - yAxis.min,
339                 centerY = pick(centerYArg, yAxis.min + yRange / 2),
340                 newYRange = yRange * howMuch,
341                 newXMin = centerX - newXRange / 2,
342                 newYMin = centerY - newYRange / 2,
343                 animation = pick(chart.options.chart.animation, true),
344                 delay,
345                 newExt = chart.fitToBox({
346                     x: newXMin,
347                     y: newYMin,
348                     width: newXRange,
349                     height: newYRange
350                 }, {
351                     x: xAxis.dataMin,
352                     y: yAxis.dataMin,
353                     width: xAxis.dataMax - xAxis.dataMin,
354                     height: yAxis.dataMax - yAxis.dataMin
355                 });
356
357             xAxis.setExtremes(newExt.x, newExt.x + newExt.width, false);
358             yAxis.setExtremes(newExt.y, newExt.y + newExt.height, false);
359
360             // Prevent zooming until this one is finished animating
361             delay = animation ? animation.duration || 500 : 0;
362             if (delay) {
363                 chart.isMapZooming = true;
364                 setTimeout(function () {
365                     chart.isMapZooming = false;
366                 }, delay);
367             }
368
369             chart.redraw();
370         }
371     });
372     
373     /**
374      * Extend the default options with map options
375      */
376     plotOptions.map = merge(plotOptions.scatter, {
377         animation: false, // makes the complex shapes slow
378         nullColor: '#F8F8F8',
379         borderColor: 'silver',
380         borderWidth: 1,
381         marker: null,
382         stickyTracking: false,
383         dataLabels: {
384             verticalAlign: 'middle'
385         },
386         turboThreshold: 0,
387         tooltip: {
388             followPointer: true,
389             pointFormat: '{point.name}: {point.y}<br/>'
390         },
391         states: {
392             normal: {
393                 animation: true
394             }
395         }
396     });
397
398     var MapAreaPoint = Highcharts.extendClass(Point, {
399         /**
400          * Extend the Point object to split paths
401          */
402         applyOptions: function (options, x) {
403
404             var point = Point.prototype.applyOptions.call(this, options, x);
405
406             if (point.path && typeof point.path === 'string') {
407                 point.path = point.options.path = Highcharts.splitPath(point.path);
408             }
409
410             return point;
411         },
412         /**
413          * Stop the fade-out 
414          */
415         onMouseOver: function () {
416             clearTimeout(this.colorInterval);
417             Point.prototype.onMouseOver.call(this);
418         },
419         /**
420          * Custom animation for tweening out the colors. Animation reduces blinking when hovering
421          * over islands and coast lines. We run a custom implementation of animation becuase we
422          * need to be able to run this independently from other animations like zoom redraw. Also,
423          * adding color animation to the adapters would introduce almost the same amount of code.
424          */
425         onMouseOut: function () {
426             var point = this,
427                 start = +new Date(),
428                 normalColor = Color(point.options.color),
429                 hoverColor = Color(point.pointAttr.hover.fill),
430                 animation = point.series.options.states.normal.animation,
431                 duration = animation && (animation.duration || 500);
432
433             if (duration && normalColor.rgba.length === 4 && hoverColor.rgba.length === 4) {
434                 delete point.pointAttr[''].fill; // avoid resetting it in Point.setState
435
436                 clearTimeout(point.colorInterval);
437                 point.colorInterval = setInterval(function () {
438                     var pos = (new Date() - start) / duration,
439                         graphic = point.graphic;
440                     if (pos > 1) {
441                         pos = 1;
442                     }
443                     if (graphic) {
444                         graphic.attr('fill', tweenColors(hoverColor, normalColor, pos));
445                     }
446                     if (pos >= 1) {
447                         clearTimeout(point.colorInterval);
448                     }
449                 }, 13);
450             }
451             Point.prototype.onMouseOut.call(point);
452         }
453     });
454
455     /**
456      * Add the series type
457      */
458     seriesTypes.map = Highcharts.extendClass(seriesTypes.scatter, {
459         type: 'map',
460         pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
461             stroke: 'borderColor',
462             'stroke-width': 'borderWidth',
463             fill: 'color'
464         },
465         colorKey: 'y',
466         pointClass: MapAreaPoint,
467         trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'],
468         getSymbol: noop,
469         supportsDrilldown: true,
470         getExtremesFromAll: true,
471         useMapGeometry: true, // get axis extremes from paths, not values
472         init: function (chart) {
473             var series = this,
474                 valueDecimals = chart.options.legend.valueDecimals,
475                 legendItems = [],
476                 name,
477                 from,
478                 to,
479                 fromLabel,
480                 toLabel,
481                 colorRange,
482                 valueRanges,
483                 gradientColor,
484                 grad,
485                 tmpLabel,
486                 horizontal = chart.options.legend.layout === 'horizontal';
487
488             
489             Highcharts.Series.prototype.init.apply(this, arguments);
490             colorRange = series.options.colorRange;
491             valueRanges = series.options.valueRanges;
492
493             if (valueRanges) {
494                 each(valueRanges, function (range) {
495                     from = range.from;
496                     to = range.to;
497                     
498                     // Assemble the default name. This can be overridden by legend.options.labelFormatter
499                     name = '';
500                     if (from === UNDEFINED) {
501                         name = '< ';
502                     } else if (to === UNDEFINED) {
503                         name = '> ';
504                     }
505                     if (from !== UNDEFINED) {
506                         name += numberFormat(from, valueDecimals);
507                     }
508                     if (from !== UNDEFINED && to !== UNDEFINED) {
509                         name += ' - ';
510                     }
511                     if (to !== UNDEFINED) {
512                         name += numberFormat(to, valueDecimals);
513                     }
514                     
515                     // Add a mock object to the legend items
516                     legendItems.push(Highcharts.extend({
517                         chart: series.chart,
518                         name: name,
519                         options: {},
520                         drawLegendSymbol: seriesTypes.area.prototype.drawLegendSymbol,
521                         visible: true,
522                         setState: function () {},
523                         setVisible: function () {}
524                     }, range));
525                 });
526                 series.legendItems = legendItems;
527
528             } else if (colorRange) {
529
530                 from = colorRange.from;
531                 to = colorRange.to;
532                 fromLabel = colorRange.fromLabel;
533                 toLabel = colorRange.toLabel;
534
535                 // Flips linearGradient variables and label text.
536                 grad = horizontal ? [0, 0, 1, 0] : [0, 1, 0, 0]; 
537                 if (!horizontal) {
538                     tmpLabel = fromLabel;
539                     fromLabel = toLabel;
540                     toLabel = tmpLabel;
541                 } 
542
543                 // Creates color gradient.
544                 gradientColor = {
545                     linearGradient: { x1: grad[0], y1: grad[1], x2: grad[2], y2: grad[3] },
546                     stops: 
547                     [
548                         [0, from],
549                         [1, to]
550                     ]
551                 };
552
553                 // Add a mock object to the legend items.
554                 legendItems = [{
555                     chart: series.chart,
556                     options: {},
557                     fromLabel: fromLabel,
558                     toLabel: toLabel,
559                     color: gradientColor,
560                     drawLegendSymbol: this.drawLegendSymbolGradient,
561                     visible: true,
562                     setState: function () {},
563                     setVisible: function () {}
564                 }];
565
566                 series.legendItems = legendItems;
567             }
568         },
569
570         /**
571          * If neither valueRanges nor colorRanges are defined, use basic area symbol.
572          */
573         drawLegendSymbol: seriesTypes.area.prototype.drawLegendSymbol,
574
575         /**
576          * Gets the series' symbol in the legend and extended legend with more information.
577          * 
578          * @param {Object} legend The legend object
579          * @param {Object} item The series (this) or point
580          */
581         drawLegendSymbolGradient: function (legend, item) {
582             var spacing = legend.options.symbolPadding,
583                 padding = pick(legend.options.padding, 8),
584                 positionY,
585                 positionX,
586                 gradientSize = this.chart.renderer.fontMetrics(legend.options.itemStyle.fontSize).h,
587                 horizontal = legend.options.layout === 'horizontal',
588                 box1,
589                 box2,
590                 box3,
591                 rectangleLength = pick(legend.options.rectangleLength, 200);
592
593             // Set local variables based on option.
594             if (horizontal) {
595                 positionY = -(spacing / 2);
596                 positionX = 0;
597             } else {
598                 positionY = -rectangleLength + legend.baseline - (spacing / 2);
599                 positionX = padding + gradientSize;
600             }
601
602             // Creates the from text.
603             item.fromText = this.chart.renderer.text(
604                     item.fromLabel,    // Text.
605                     positionX,        // Lower left x.
606                     positionY        // Lower left y.
607                 ).attr({
608                     zIndex: 2
609                 }).add(item.legendGroup);
610             box1 = item.fromText.getBBox();
611
612             // Creates legend symbol.
613             // Ternary changes variables based on option.
614             item.legendSymbol = this.chart.renderer.rect(
615                 horizontal ? box1.x + box1.width + spacing : box1.x - gradientSize - spacing,        // Upper left x.
616                 box1.y,                                                                                // Upper left y.
617                 horizontal ? rectangleLength : gradientSize,                                            // Width.
618                 horizontal ? gradientSize : rectangleLength,                                        // Height.
619                 2                                                                                    // Corner radius.
620             ).attr({
621                 zIndex: 1
622             }).add(item.legendGroup);
623             box2 = item.legendSymbol.getBBox();
624
625             // Creates the to text.
626             // Vertical coordinate changed based on option.
627             item.toText = this.chart.renderer.text(
628                     item.toLabel,
629                     box2.x + box2.width + spacing,
630                     horizontal ? positionY : box2.y + box2.height - spacing
631                 ).attr({
632                     zIndex: 2
633                 }).add(item.legendGroup);
634             box3 = item.toText.getBBox();
635
636             // Changes legend box settings based on option.
637             if (horizontal) {
638                 legend.offsetWidth = box1.width + box2.width + box3.width + (spacing * 2) + padding;
639                 legend.itemY = gradientSize + padding;
640             } else {
641                 legend.offsetWidth = Math.max(box1.width, box3.width) + (spacing) + box2.width + padding;
642                 legend.itemY = box2.height + padding;
643                 legend.itemX = spacing;
644             }
645         },
646
647         /**
648          * Get the bounding box of all paths in the map combined.
649          */
650         getBox: function (paths) {
651             var maxX = Number.MIN_VALUE, 
652                 minX =  Number.MAX_VALUE, 
653                 maxY = Number.MIN_VALUE, 
654                 minY =  Number.MAX_VALUE;
655             
656             
657             // Find the bounding box
658             each(paths || this.options.data, function (point) {
659                 var path = point.path,
660                     i = path.length,
661                     even = false, // while loop reads from the end
662                     pointMaxX = Number.MIN_VALUE, 
663                     pointMinX =  Number.MAX_VALUE, 
664                     pointMaxY = Number.MIN_VALUE, 
665                     pointMinY =  Number.MAX_VALUE;
666                     
667                 while (i--) {
668                     if (typeof path[i] === 'number' && !isNaN(path[i])) {
669                         if (even) { // even = x
670                             pointMaxX = Math.max(pointMaxX, path[i]);
671                             pointMinX = Math.min(pointMinX, path[i]);
672                         } else { // odd = Y
673                             pointMaxY = Math.max(pointMaxY, path[i]);
674                             pointMinY = Math.min(pointMinY, path[i]);
675                         }
676                         even = !even;
677                     }
678                 }
679                 // Cache point bounding box for use to position data labels
680                 point._maxX = pointMaxX;
681                 point._minX = pointMinX;
682                 point._maxY = pointMaxY;
683                 point._minY = pointMinY;
684
685                 maxX = Math.max(maxX, pointMaxX);
686                 minX = Math.min(minX, pointMinX);
687                 maxY = Math.max(maxY, pointMaxY);
688                 minY = Math.min(minY, pointMinY);
689             });
690             this.minY = minY;
691             this.maxY = maxY;
692             this.minX = minX;
693             this.maxX = maxX;
694             
695         },
696         
697         
698         
699         /**
700          * Translate the path so that it automatically fits into the plot area box
701          * @param {Object} path
702          */
703         translatePath: function (path) {
704             
705             var series = this,
706                 even = false, // while loop reads from the end
707                 xAxis = series.xAxis,
708                 yAxis = series.yAxis,
709                 i;
710                 
711             // Preserve the original
712             path = [].concat(path);
713                 
714             // Do the translation
715             i = path.length;
716             while (i--) {
717                 if (typeof path[i] === 'number') {
718                     if (even) { // even = x
719                         path[i] = Math.round(xAxis.translate(path[i]));
720                     } else { // odd = Y
721                         path[i] = Math.round(yAxis.len - yAxis.translate(path[i]));
722                     }
723                     even = !even;
724                 }
725             }
726             return path;
727         },
728         
729         setData: function () {
730             Highcharts.Series.prototype.setData.apply(this, arguments);
731             this.getBox();
732         },
733         
734         /**
735          * Add the path option for data points. Find the max value for color calculation.
736          */
737         translate: function () {
738             var series = this,
739                 dataMin = Number.MAX_VALUE,
740                 dataMax = Number.MIN_VALUE;
741     
742             series.generatePoints();
743     
744             each(series.data, function (point) {
745                 
746                 point.shapeType = 'path';
747                 point.shapeArgs = {
748                     d: series.translatePath(point.path)
749                 };
750                 
751                 // TODO: do point colors in drawPoints instead of point.init
752                 if (typeof point.y === 'number') {
753                     if (point.y > dataMax) {
754                         dataMax = point.y;
755                     } else if (point.y < dataMin) {
756                         dataMin = point.y;
757                     }
758                 }
759             });
760             
761             series.translateColors(dataMin, dataMax);
762         },
763         
764         /**
765          * In choropleth maps, the color is a result of the value, so this needs translation too
766          */
767         translateColors: function (dataMin, dataMax) {
768             
769             var seriesOptions = this.options,
770                 valueRanges = seriesOptions.valueRanges,
771                 colorRange = seriesOptions.colorRange,
772                 colorKey = this.colorKey,
773                 from,
774                 to;
775
776             if (colorRange) {
777                 from = Color(colorRange.from);
778                 to = Color(colorRange.to);
779             }
780             
781             each(this.data, function (point) {
782                 var value = point[colorKey],
783                     range,
784                     color,
785                     i,
786                     pos;
787
788                 if (valueRanges) {
789                     i = valueRanges.length;
790                     while (i--) {
791                         range = valueRanges[i];
792                         from = range.from;
793                         to = range.to;
794                         if ((from === UNDEFINED || value >= from) && (to === UNDEFINED || value <= to)) {
795                             color = range.color;
796                             break;
797                         }
798                             
799                     }
800                 } else if (colorRange && value !== undefined) {
801
802                     pos = 1 - ((dataMax - value) / (dataMax - dataMin));
803                     color = value === null ? seriesOptions.nullColor : tweenColors(from, to, pos);
804                 }
805
806                 if (color) {
807                     point.color = null; // reset from previous drilldowns, use of the same data options
808                     point.options.color = color;
809                 }
810             });
811         },
812         
813         drawGraph: noop,
814         
815         /**
816          * We need the points' bounding boxes in order to draw the data labels, so 
817          * we skip it now and call if from drawPoints instead.
818          */
819         drawDataLabels: noop,
820         
821         /** 
822          * Use the drawPoints method of column, that is able to handle simple shapeArgs.
823          * Extend it by assigning the tooltip position.
824          */
825         drawPoints: function () {
826             var series = this,
827                 xAxis = series.xAxis,
828                 yAxis = series.yAxis,
829                 colorKey = series.colorKey;
830             
831             // Make points pass test in drawing
832             each(series.data, function (point) {
833                 point.plotY = 1; // pass null test in column.drawPoints
834                 if (point[colorKey] === null) {
835                     point[colorKey] = 0;
836                     point.isNull = true;
837                 }
838             });
839             
840             // Draw them
841             seriesTypes.column.prototype.drawPoints.apply(series);
842             
843             each(series.data, function (point) {
844
845                 var dataLabels = point.dataLabels,
846                     minX = xAxis.toPixels(point._minX, true),
847                     maxX = xAxis.toPixels(point._maxX, true),
848                     minY = yAxis.toPixels(point._minY, true),
849                     maxY = yAxis.toPixels(point._maxY, true);
850
851                 point.plotX = Math.round(minX + (maxX - minX) * pick(dataLabels && dataLabels.anchorX, 0.5));
852                 point.plotY = Math.round(minY + (maxY - minY) * pick(dataLabels && dataLabels.anchorY, 0.5)); 
853                 
854                 
855                 // Reset escaped null points
856                 if (point.isNull) {
857                     point[colorKey] = null;
858                 }
859             });
860
861             // Now draw the data labels
862             Highcharts.Series.prototype.drawDataLabels.call(series);
863             
864         },
865
866         /**
867          * Animate in the new series from the clicked point in the old series.
868          * Depends on the drilldown.js module
869          */
870         animateDrilldown: function (init) {
871             var toBox = this.chart.plotBox,
872                 level = this.chart.drilldownLevels[this.chart.drilldownLevels.length - 1],
873                 fromBox = level.bBox,
874                 animationOptions = this.chart.options.drilldown.animation,
875                 scale;
876                 
877             if (!init) {
878
879                 scale = Math.min(fromBox.width / toBox.width, fromBox.height / toBox.height);
880                 level.shapeArgs = {
881                     scaleX: scale,
882                     scaleY: scale,
883                     translateX: fromBox.x,
884                     translateY: fromBox.y
885                 };
886                 
887                 // TODO: Animate this.group instead
888                 each(this.points, function (point) {
889
890                     point.graphic
891                         .attr(level.shapeArgs)
892                         .animate({
893                             scaleX: 1,
894                             scaleY: 1,
895                             translateX: 0,
896                             translateY: 0
897                         }, animationOptions);
898
899                 });
900
901                 delete this.animate;
902             }
903             
904         },
905
906         /**
907          * When drilling up, pull out the individual point graphics from the lower series
908          * and animate them into the origin point in the upper series.
909          */
910         animateDrillupFrom: function (level) {
911             seriesTypes.column.prototype.animateDrillupFrom.call(this, level);
912         },
913
914
915         /**
916          * When drilling up, keep the upper series invisible until the lower series has
917          * moved into place
918          */
919         animateDrillupTo: function (init) {
920             seriesTypes.column.prototype.animateDrillupTo.call(this, init);
921         }
922     });
923
924
925     // The mapline series type
926     plotOptions.mapline = merge(plotOptions.map, {
927         lineWidth: 1,
928         backgroundColor: 'none'
929     });
930     seriesTypes.mapline = Highcharts.extendClass(seriesTypes.map, {
931         type: 'mapline',
932         pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
933             stroke: 'color',
934             'stroke-width': 'lineWidth',
935             fill: 'backgroundColor'
936         },
937         drawLegendSymbol: seriesTypes.line.prototype.drawLegendSymbol
938     });
939
940     // The mappoint series type
941     plotOptions.mappoint = merge(plotOptions.scatter, {
942         dataLabels: {
943             enabled: true,
944             format: '{point.name}',
945             color: 'black',
946             style: {
947                 textShadow: '0 0 5px white'
948             }
949         }
950     });
951     seriesTypes.mappoint = Highcharts.extendClass(seriesTypes.scatter, {
952         type: 'mappoint'
953     });
954     
955
956     
957     /**
958      * A wrapper for Chart with all the default values for a Map
959      */
960     Highcharts.Map = function (options, callback) {
961         
962         var hiddenAxis = {
963                 endOnTick: false,
964                 gridLineWidth: 0,
965                 labels: {
966                     enabled: false
967                 },
968                 lineWidth: 0,
969                 minPadding: 0,
970                 maxPadding: 0,
971                 startOnTick: false,
972                 tickWidth: 0,
973                 title: null
974             },
975             seriesOptions;
976         
977         // Don't merge the data
978         seriesOptions = options.series;
979         options.series = null;
980         
981         options = merge({
982             chart: {
983                 type: 'map',
984                 panning: 'xy'
985             },
986             xAxis: hiddenAxis,
987             yAxis: merge(hiddenAxis, { reversed: true })    
988         },
989         options, // user's options
990     
991         { // forced options
992             chart: {
993                 inverted: false
994             }
995         });
996     
997         options.series = seriesOptions;
998     
999     
1000         return new Highcharts.Chart(options, callback);
1001     };
1002 }(Highcharts));