Subversion-Projekte lars-tiefland.cienc

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
5 lars 1
/**
2
 * @license Highmaps JS v1.1.9 (2015-10-07)
3
 * Highmaps as a plugin for Highcharts 4.1.x or Highstock 2.1.x (x being the patch version of this file)
4
 *
5
 * (c) 2011-2014 Torstein Honsi
6
 *
7
 * License: www.highcharts.com/license
8
 */
9
 
10
/*global HighchartsAdapter*/
11
(function (Highcharts) {
12
 
13
 
14
var UNDEFINED,
15
	Axis = Highcharts.Axis,
16
	Chart = Highcharts.Chart,
17
	Color = Highcharts.Color,
18
	Point = Highcharts.Point,
19
	Pointer = Highcharts.Pointer,
20
	Legend = Highcharts.Legend,
21
	LegendSymbolMixin = Highcharts.LegendSymbolMixin,
22
	Renderer = Highcharts.Renderer,
23
	Series = Highcharts.Series,
24
	SVGRenderer = Highcharts.SVGRenderer,
25
	VMLRenderer = Highcharts.VMLRenderer,
26
 
27
	addEvent = Highcharts.addEvent,
28
	each = Highcharts.each,
29
	error = Highcharts.error,
30
	extend = Highcharts.extend,
31
	extendClass = Highcharts.extendClass,
32
	merge = Highcharts.merge,
33
	pick = Highcharts.pick,
34
	defaultOptions = Highcharts.getOptions(),
35
	seriesTypes = Highcharts.seriesTypes,
36
	defaultPlotOptions = defaultOptions.plotOptions,
37
	wrap = Highcharts.wrap,
38
	noop = function () {};
39
 
40
	/**
41
 * Override to use the extreme coordinates from the SVG shape, not the
42
 * data values
43
 */
44
wrap(Axis.prototype, 'getSeriesExtremes', function (proceed) {
45
	var isXAxis = this.isXAxis,
46
		dataMin,
47
		dataMax,
48
		xData = [],
49
		useMapGeometry;
50
 
51
	// Remove the xData array and cache it locally so that the proceed method doesn't use it
52
	if (isXAxis) {
53
		each(this.series, function (series, i) {
54
			if (series.useMapGeometry) {
55
				xData[i] = series.xData;
56
				series.xData = [];
57
			}
58
		});
59
	}
60
 
61
	// Call base to reach normal cartesian series (like mappoint)
62
	proceed.call(this);
63
 
64
	// Run extremes logic for map and mapline
65
	if (isXAxis) {
66
		dataMin = pick(this.dataMin, Number.MAX_VALUE);
67
		dataMax = pick(this.dataMax, -Number.MAX_VALUE);
68
		each(this.series, function (series, i) {
69
			if (series.useMapGeometry) {
70
				dataMin = Math.min(dataMin, pick(series.minX, dataMin));
71
				dataMax = Math.max(dataMax, pick(series.maxX, dataMin));
72
				series.xData = xData[i]; // Reset xData array
73
				useMapGeometry = true;
74
			}
75
		});
76
		if (useMapGeometry) {
77
			this.dataMin = dataMin;
78
			this.dataMax = dataMax;
79
		}
80
	}
81
});
82
 
83
/**
84
 * Override axis translation to make sure the aspect ratio is always kept
85
 */
86
wrap(Axis.prototype, 'setAxisTranslation', function (proceed) {
87
	var chart = this.chart,
88
		mapRatio,
89
		plotRatio = chart.plotWidth / chart.plotHeight,
90
		adjustedAxisLength,
91
		xAxis = chart.xAxis[0],
92
		padAxis,
93
		fixTo,
94
		fixDiff,
95
		preserveAspectRatio;
96
 
97
 
98
	// Run the parent method
99
	proceed.call(this);
100
 
101
	// Check for map-like series
102
	if (this.coll === 'yAxis' && xAxis.transA !== UNDEFINED) {
103
		each(this.series, function (series) {
104
			if (series.preserveAspectRatio) {
105
				preserveAspectRatio = true;
106
			}
107
		});
108
	}
109
 
110
	// On Y axis, handle both
111
	if (preserveAspectRatio) {
112
 
113
		// Use the same translation for both axes
114
		this.transA = xAxis.transA = Math.min(this.transA, xAxis.transA);
115
 
116
		mapRatio = plotRatio / ((xAxis.max - xAxis.min) / (this.max - this.min));
117
 
118
		// What axis to pad to put the map in the middle
119
		padAxis = mapRatio < 1 ? this : xAxis;
120
 
121
		// Pad it
122
		adjustedAxisLength = (padAxis.max - padAxis.min) * padAxis.transA;
123
		padAxis.pixelPadding = padAxis.len - adjustedAxisLength;
124
		padAxis.minPixelPadding = padAxis.pixelPadding / 2;
125
 
126
		fixTo = padAxis.fixTo;
127
		if (fixTo) {
128
			fixDiff = fixTo[1] - padAxis.toValue(fixTo[0], true);
129
			fixDiff *= padAxis.transA;
130
			if (Math.abs(fixDiff) > padAxis.minPixelPadding || (padAxis.min === padAxis.dataMin && padAxis.max === padAxis.dataMax)) { // zooming out again, keep within restricted area
131
				fixDiff = 0;
132
			}
133
			padAxis.minPixelPadding -= fixDiff;
134
		}
135
	}
136
});
137
 
138
/**
139
 * Override Axis.render in order to delete the fixTo prop
140
 */
141
wrap(Axis.prototype, 'render', function (proceed) {
142
	proceed.call(this);
143
	this.fixTo = null;
144
});
145
 
146
 
147
/**
148
 * The ColorAxis object for inclusion in gradient legends
149
 */
150
var ColorAxis = Highcharts.ColorAxis = function () {
151
	this.isColorAxis = true;
152
	this.init.apply(this, arguments);
153
};
154
extend(ColorAxis.prototype, Axis.prototype);
155
extend(ColorAxis.prototype, {
156
	defaultColorAxisOptions: {
157
		lineWidth: 0,
158
		minPadding: 0,
159
		maxPadding: 0,
160
		gridLineWidth: 1,
161
		tickPixelInterval: 72,
162
		startOnTick: true,
163
		endOnTick: true,
164
		offset: 0,
165
		marker: {
166
			animation: {
167
				duration: 50
168
			},
169
			color: 'gray',
170
			width: 0.01
171
		},
172
		labels: {
173
			overflow: 'justify'
174
		},
175
		minColor: '#EFEFFF',
176
		maxColor: '#003875',
177
		tickLength: 5
178
	},
179
	init: function (chart, userOptions) {
180
		var horiz = chart.options.legend.layout !== 'vertical',
181
			options;
182
 
183
		// Build the options
184
		options = merge(this.defaultColorAxisOptions, {
185
			side: horiz ? 2 : 1,
186
			reversed: !horiz
187
		}, userOptions, {
188
			opposite: !horiz,
189
			showEmpty: false,
190
			title: null,
191
			isColor: true
192
		});
193
 
194
		Axis.prototype.init.call(this, chart, options);
195
 
196
		// Base init() pushes it to the xAxis array, now pop it again
197
		//chart[this.isXAxis ? 'xAxis' : 'yAxis'].pop();
198
 
199
		// Prepare data classes
200
		if (userOptions.dataClasses) {
201
			this.initDataClasses(userOptions);
202
		}
203
		this.initStops(userOptions);
204
 
205
		// Override original axis properties
206
		this.horiz = horiz;
207
		this.zoomEnabled = false;
208
	},
209
 
210
	/*
211
	 * Return an intermediate color between two colors, according to pos where 0
212
	 * is the from color and 1 is the to color.
213
	 * NOTE: Changes here should be copied
214
	 * to the same function in drilldown.src.js and solid-gauge-src.js.
215
	 */
216
	tweenColors: function (from, to, pos) {
217
		// Check for has alpha, because rgba colors perform worse due to lack of
218
		// support in WebKit.
219
		var hasAlpha,
220
			ret;
221
 
222
		// Unsupported color, return to-color (#3920)
223
		if (!to.rgba.length || !from.rgba.length) {
224
			ret = to.raw || 'none';
225
 
226
		// Interpolate
227
		} else {
228
			from = from.rgba;
229
			to = to.rgba;
230
			hasAlpha = (to[3] !== 1 || from[3] !== 1);
231
			ret = (hasAlpha ? 'rgba(' : 'rgb(') +
232
				Math.round(to[0] + (from[0] - to[0]) * (1 - pos)) + ',' +
233
				Math.round(to[1] + (from[1] - to[1]) * (1 - pos)) + ',' +
234
				Math.round(to[2] + (from[2] - to[2]) * (1 - pos)) +
235
				(hasAlpha ? (',' + (to[3] + (from[3] - to[3]) * (1 - pos))) : '') + ')';
236
		}
237
		return ret;
238
	},
239
 
240
	initDataClasses: function (userOptions) {
241
		var axis = this,
242
			chart = this.chart,
243
			dataClasses,
244
			colorCounter = 0,
245
			options = this.options,
246
			len = userOptions.dataClasses.length;
247
		this.dataClasses = dataClasses = [];
248
		this.legendItems = [];
249
 
250
		each(userOptions.dataClasses, function (dataClass, i) {
251
			var colors;
252
 
253
			dataClass = merge(dataClass);
254
			dataClasses.push(dataClass);
255
			if (!dataClass.color) {
256
				if (options.dataClassColor === 'category') {
257
					colors = chart.options.colors;
258
					dataClass.color = colors[colorCounter++];
259
					// loop back to zero
260
					if (colorCounter === colors.length) {
261
						colorCounter = 0;
262
					}
263
				} else {
264
					dataClass.color = axis.tweenColors(
265
						Color(options.minColor),
266
						Color(options.maxColor),
267
						len < 2 ? 0.5 : i / (len - 1) // #3219
268
					);
269
				}
270
			}
271
		});
272
	},
273
 
274
	initStops: function (userOptions) {
275
		this.stops = userOptions.stops || [
276
			[0, this.options.minColor],
277
			[1, this.options.maxColor]
278
		];
279
		each(this.stops, function (stop) {
280
			stop.color = Color(stop[1]);
281
		});
282
	},
283
 
284
	/**
285
	 * Extend the setOptions method to process extreme colors and color
286
	 * stops.
287
	 */
288
	setOptions: function (userOptions) {
289
		Axis.prototype.setOptions.call(this, userOptions);
290
 
291
		this.options.crosshair = this.options.marker;
292
		this.coll = 'colorAxis';
293
	},
294
 
295
	setAxisSize: function () {
296
		var symbol = this.legendSymbol,
297
			chart = this.chart,
298
			x,
299
			y,
300
			width,
301
			height;
302
 
303
		if (symbol) {
304
			this.left = x = symbol.attr('x');
305
			this.top = y = symbol.attr('y');
306
			this.width = width = symbol.attr('width');
307
			this.height = height = symbol.attr('height');
308
			this.right = chart.chartWidth - x - width;
309
			this.bottom = chart.chartHeight - y - height;
310
 
311
			this.len = this.horiz ? width : height;
312
			this.pos = this.horiz ? x : y;
313
		}
314
	},
315
 
316
	/**
317
	 * Translate from a value to a color
318
	 */
319
	toColor: function (value, point) {
320
		var pos,
321
			stops = this.stops,
322
			from,
323
			to,
324
			color,
325
			dataClasses = this.dataClasses,
326
			dataClass,
327
			i;
328
 
329
		if (dataClasses) {
330
			i = dataClasses.length;
331
			while (i--) {
332
				dataClass = dataClasses[i];
333
				from = dataClass.from;
334
				to = dataClass.to;
335
				if ((from === UNDEFINED || value >= from) && (to === UNDEFINED || value <= to)) {
336
					color = dataClass.color;
337
					if (point) {
338
						point.dataClass = i;
339
					}
340
					break;
341
				}
342
			}
343
 
344
		} else {
345
 
346
			if (this.isLog) {
347
				value = this.val2lin(value);
348
			}
349
			pos = 1 - ((this.max - value) / ((this.max - this.min) || 1));
350
			i = stops.length;
351
			while (i--) {
352
				if (pos > stops[i][0]) {
353
					break;
354
				}
355
			}
356
			from = stops[i] || stops[i + 1];
357
			to = stops[i + 1] || from;
358
 
359
			// The position within the gradient
360
			pos = 1 - (to[0] - pos) / ((to[0] - from[0]) || 1);
361
 
362
			color = this.tweenColors(
363
				from.color,
364
				to.color,
365
				pos
366
			);
367
		}
368
		return color;
369
	},
370
 
371
	/**
372
	 * Override the getOffset method to add the whole axis groups inside the legend.
373
	 */
374
	getOffset: function () {
375
		var group = this.legendGroup,
376
			sideOffset = this.chart.axisOffset[this.side];
377
 
378
		if (group) {
379
 
380
			// Hook for the getOffset method to add groups to this parent group
381
			this.axisParent = group;
382
 
383
			// Call the base
384
			Axis.prototype.getOffset.call(this);
385
 
386
			// First time only
387
			if (!this.added) {
388
 
389
				this.added = true;
390
 
391
				this.labelLeft = 0;
392
				this.labelRight = this.width;
393
			}
394
			// Reset it to avoid color axis reserving space
395
			this.chart.axisOffset[this.side] = sideOffset;
396
		}
397
	},
398
 
399
	/**
400
	 * Create the color gradient
401
	 */
402
	setLegendColor: function () {
403
		var grad,
404
			horiz = this.horiz,
405
			options = this.options,
406
			reversed = this.reversed;
407
 
408
		grad = horiz ? [+reversed, 0, +!reversed, 0] : [0, +!reversed, 0, +reversed]; // #3190
409
		this.legendColor = {
410
			linearGradient: { x1: grad[0], y1: grad[1], x2: grad[2], y2: grad[3] },
411
			stops: options.stops || [
412
				[0, options.minColor],
413
				[1, options.maxColor]
414
			]
415
		};
416
	},
417
 
418
	/**
419
	 * The color axis appears inside the legend and has its own legend symbol
420
	 */
421
	drawLegendSymbol: function (legend, item) {
422
		var padding = legend.padding,
423
			legendOptions = legend.options,
424
			horiz = this.horiz,
425
			box,
426
			width = pick(legendOptions.symbolWidth, horiz ? 200 : 12),
427
			height = pick(legendOptions.symbolHeight, horiz ? 12 : 200),
428
			labelPadding = pick(legendOptions.labelPadding, horiz ? 16 : 30),
429
			itemDistance = pick(legendOptions.itemDistance, 10);
430
 
431
		this.setLegendColor();
432
 
433
		// Create the gradient
434
		item.legendSymbol = this.chart.renderer.rect(
435
			0,
436
			legend.baseline - 11,
437
			width,
438
			height
439
		).attr({
440
			zIndex: 1
441
		}).add(item.legendGroup);
442
		box = item.legendSymbol.getBBox();
443
 
444
		// Set how much space this legend item takes up
445
		this.legendItemWidth = width + padding + (horiz ? itemDistance : labelPadding);
446
		this.legendItemHeight = height + padding + (horiz ? labelPadding : 0);
447
	},
448
	/**
449
	 * Fool the legend
450
	 */
451
	setState: noop,
452
	visible: true,
453
	setVisible: noop,
454
	getSeriesExtremes: function () {
455
		var series;
456
		if (this.series.length) {
457
			series = this.series[0];
458
			this.dataMin = series.valueMin;
459
			this.dataMax = series.valueMax;
460
		}
461
	},
462
	drawCrosshair: function (e, point) {
463
		var plotX = point && point.plotX,
464
			plotY = point && point.plotY,
465
			crossPos,
466
			axisPos = this.pos,
467
			axisLen = this.len;
468
 
469
		if (point) {
470
			crossPos = this.toPixels(point[point.series.colorKey]);
471
			if (crossPos < axisPos) {
472
				crossPos = axisPos - 2;
473
			} else if (crossPos > axisPos + axisLen) {
474
				crossPos = axisPos + axisLen + 2;
475
			}
476
 
477
			point.plotX = crossPos;
478
			point.plotY = this.len - crossPos;
479
			Axis.prototype.drawCrosshair.call(this, e, point);
480
			point.plotX = plotX;
481
			point.plotY = plotY;
482
 
483
			if (this.cross) {
484
				this.cross
485
					.attr({
486
						fill: this.crosshair.color
487
					})
488
					.add(this.legendGroup);
489
			}
490
		}
491
	},
492
	getPlotLinePath: function (a, b, c, d, pos) {
493
		if (typeof pos === 'number') { // crosshairs only // #3969 pos can be 0 !!
494
			return this.horiz ?
495
				['M', pos - 4, this.top - 6, 'L', pos + 4, this.top - 6, pos, this.top, 'Z'] :
496
				['M', this.left, pos, 'L', this.left - 6, pos + 6, this.left - 6, pos - 6, 'Z'];
497
		} else {
498
			return Axis.prototype.getPlotLinePath.call(this, a, b, c, d);
499
		}
500
	},
501
 
502
	update: function (newOptions, redraw) {
503
		var chart = this.chart,
504
			legend = chart.legend;
505
 
506
		each(this.series, function (series) {
507
			series.isDirtyData = true; // Needed for Axis.update when choropleth colors change
508
		});
509
 
510
		// When updating data classes, destroy old items and make sure new ones are created (#3207)
511
		if (newOptions.dataClasses && legend.allItems) {
512
			each(legend.allItems, function (item) {
513
				if (item.isDataClass) {
514
					item.legendGroup.destroy();
515
				}
516
			});
517
			chart.isDirtyLegend = true;
518
		}
519
 
520
		// Keep the options structure updated for export. Unlike xAxis and yAxis, the colorAxis is
521
		// not an array. (#3207)
522
		chart.options[this.coll] = merge(this.userOptions, newOptions);
523
 
524
		Axis.prototype.update.call(this, newOptions, redraw);
525
		if (this.legendItem) {
526
			this.setLegendColor();
527
			legend.colorizeItem(this, true);
528
		}
529
	},
530
 
531
	/**
532
	 * Get the legend item symbols for data classes
533
	 */
534
	getDataClassLegendSymbols: function () {
535
		var axis = this,
536
			chart = this.chart,
537
			legendItems = this.legendItems,
538
			legendOptions = chart.options.legend,
539
			valueDecimals = legendOptions.valueDecimals,
540
			valueSuffix = legendOptions.valueSuffix || '',
541
			name;
542
 
543
		if (!legendItems.length) {
544
			each(this.dataClasses, function (dataClass, i) {
545
				var vis = true,
546
					from = dataClass.from,
547
					to = dataClass.to;
548
 
549
				// Assemble the default name. This can be overridden by legend.options.labelFormatter
550
				name = '';
551
				if (from === UNDEFINED) {
552
					name = '< ';
553
				} else if (to === UNDEFINED) {
554
					name = '> ';
555
				}
556
				if (from !== UNDEFINED) {
557
					name += Highcharts.numberFormat(from, valueDecimals) + valueSuffix;
558
				}
559
				if (from !== UNDEFINED && to !== UNDEFINED) {
560
					name += ' - ';
561
				}
562
				if (to !== UNDEFINED) {
563
					name += Highcharts.numberFormat(to, valueDecimals) + valueSuffix;
564
				}
565
 
566
				// Add a mock object to the legend items
567
				legendItems.push(extend({
568
					chart: chart,
569
					name: name,
570
					options: {},
571
					drawLegendSymbol: LegendSymbolMixin.drawRectangle,
572
					visible: true,
573
					setState: noop,
574
					isDataClass: true,
575
					setVisible: function () {
576
						vis = this.visible = !vis;
577
						each(axis.series, function (series) {
578
							each(series.points, function (point) {
579
								if (point.dataClass === i) {
580
									point.setVisible(vis);
581
								}
582
							});
583
						});
584
 
585
						chart.legend.colorizeItem(this, vis);
586
					}
587
				}, dataClass));
588
			});
589
		}
590
		return legendItems;
591
	},
592
	name: '' // Prevents 'undefined' in legend in IE8
593
});
594
 
595
/**
596
 * Handle animation of the color attributes directly
597
 */
598
each(['fill', 'stroke'], function (prop) {
599
	HighchartsAdapter.addAnimSetter(prop, function (fx) {
600
		fx.elem.attr(prop, ColorAxis.prototype.tweenColors(Color(fx.start), Color(fx.end), fx.pos));
601
	});
602
});
603
 
604
/**
605
 * Extend the chart getAxes method to also get the color axis
606
 */
607
wrap(Chart.prototype, 'getAxes', function (proceed) {
608
 
609
	var options = this.options,
610
		colorAxisOptions = options.colorAxis;
611
 
612
	proceed.call(this);
613
 
614
	this.colorAxis = [];
615
	if (colorAxisOptions) {
616
		proceed = new ColorAxis(this, colorAxisOptions); // Fake assignment for jsLint
617
	}
618
});
619
 
620
 
621
/**
622
 * Wrap the legend getAllItems method to add the color axis. This also removes the
623
 * axis' own series to prevent them from showing up individually.
624
 */
625
wrap(Legend.prototype, 'getAllItems', function (proceed) {
626
	var allItems = [],
627
		colorAxis = this.chart.colorAxis[0];
628
 
629
	if (colorAxis) {
630
 
631
		// Data classes
632
		if (colorAxis.options.dataClasses) {
633
			allItems = allItems.concat(colorAxis.getDataClassLegendSymbols());
634
		// Gradient legend
635
		} else {
636
			// Add this axis on top
637
			allItems.push(colorAxis);
638
		}
639
 
640
		// Don't add the color axis' series
641
		each(colorAxis.series, function (series) {
642
			series.options.showInLegend = false;
643
		});
644
	}
645
 
646
	return allItems.concat(proceed.call(this));
647
});/**
648
 * Mixin for maps and heatmaps
649
 */
650
var colorPointMixin = {
651
	/**
652
	 * Set the visibility of a single point
653
	 */
654
	setVisible: function (vis) {
655
		var point = this,
656
			method = vis ? 'show' : 'hide';
657
 
658
		// Show and hide associated elements
659
		each(['graphic', 'dataLabel'], function (key) {
660
			if (point[key]) {
661
				point[key][method]();
662
			}
663
		});
664
	}
665
};
666
var colorSeriesMixin = {
667
 
668
	pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
669
		stroke: 'borderColor',
670
		'stroke-width': 'borderWidth',
671
		fill: 'color',
672
		dashstyle: 'dashStyle'
673
	},
674
	pointArrayMap: ['value'],
675
	axisTypes: ['xAxis', 'yAxis', 'colorAxis'],
676
	optionalAxis: 'colorAxis',
677
	trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'],
678
	getSymbol: noop,
679
	parallelArrays: ['x', 'y', 'value'],
680
	colorKey: 'value',
681
 
682
	/**
683
	 * In choropleth maps, the color is a result of the value, so this needs translation too
684
	 */
685
	translateColors: function () {
686
		var series = this,
687
			nullColor = this.options.nullColor,
688
			colorAxis = this.colorAxis,
689
			colorKey = this.colorKey;
690
 
691
		each(this.data, function (point) {
692
			var value = point[colorKey],
693
				color;
694
 
695
			color = point.options.color ||
696
				(value === null ? nullColor : (colorAxis && value !== undefined) ? colorAxis.toColor(value, point) : point.color || series.color);
697
 
698
			if (color) {
699
				point.color = color;
700
			}
701
		});
702
	}
703
};
704
// Add events to the Chart object itself
705
extend(Chart.prototype, {
706
	renderMapNavigation: function () {
707
		var chart = this,
708
			options = this.options.mapNavigation,
709
			buttons = options.buttons,
710
			n,
711
			button,
712
			buttonOptions,
713
			attr,
714
			states,
715
			stopEvent = function (e) {
716
				if (e) {
717
					if (e.preventDefault) {
718
						e.preventDefault();
719
					}
720
					if (e.stopPropagation) {
721
						e.stopPropagation();
722
					}
723
					e.cancelBubble = true;
724
				}
725
			},
726
			outerHandler = function (e) {
727
				this.handler.call(chart, e);
728
				stopEvent(e); // Stop default click event (#4444)
729
			};
730
 
731
		if (pick(options.enableButtons, options.enabled) && !chart.renderer.forExport) {
732
			for (n in buttons) {
733
				if (buttons.hasOwnProperty(n)) {
734
					buttonOptions = merge(options.buttonOptions, buttons[n]);
735
					attr = buttonOptions.theme;
736
					attr.style = merge(buttonOptions.theme.style, buttonOptions.style); // #3203
737
					states = attr.states;
738
					button = chart.renderer.button(
739
							buttonOptions.text,
740
							0,
741
							0,
742
							outerHandler,
743
							attr,
744
							states && states.hover,
745
							states && states.select,
746
							0,
747
							n === 'zoomIn' ? 'topbutton' : 'bottombutton'
748
						)
749
						.attr({
750
							width: buttonOptions.width,
751
							height: buttonOptions.height,
752
							title: chart.options.lang[n],
753
							zIndex: 5
754
						})
755
						.add();
756
					button.handler = buttonOptions.onclick;
757
					button.align(extend(buttonOptions, { width: button.width, height: 2 * button.height }), null, buttonOptions.alignTo);
758
					addEvent(button.element, 'dblclick', stopEvent); // Stop double click event (#4444)
759
				}
760
			}
761
		}
762
	},
763
 
764
	/**
765
	 * Fit an inner box to an outer. If the inner box overflows left or right, align it to the sides of the
766
	 * outer. If it overflows both sides, fit it within the outer. This is a pattern that occurs more places
767
	 * in Highcharts, perhaps it should be elevated to a common utility function.
768
	 */
769
	fitToBox: function (inner, outer) {
770
		each([['x', 'width'], ['y', 'height']], function (dim) {
771
			var pos = dim[0],
772
				size = dim[1];
773
 
774
			if (inner[pos] + inner[size] > outer[pos] + outer[size]) { // right overflow
775
				if (inner[size] > outer[size]) { // the general size is greater, fit fully to outer
776
					inner[size] = outer[size];
777
					inner[pos] = outer[pos];
778
				} else { // align right
779
					inner[pos] = outer[pos] + outer[size] - inner[size];
780
				}
781
			}
782
			if (inner[size] > outer[size]) {
783
				inner[size] = outer[size];
784
			}
785
			if (inner[pos] < outer[pos]) {
786
				inner[pos] = outer[pos];
787
			}
788
		});
789
 
790
 
791
		return inner;
792
	},
793
 
794
	/**
795
	 * Zoom the map in or out by a certain amount. Less than 1 zooms in, greater than 1 zooms out.
796
	 */
797
	mapZoom: function (howMuch, centerXArg, centerYArg, mouseX, mouseY) {
798
		/*if (this.isMapZooming) {
799
			this.mapZoomQueue = arguments;
800
			return;
801
		}*/
802
 
803
		var chart = this,
804
			xAxis = chart.xAxis[0],
805
			xRange = xAxis.max - xAxis.min,
806
			centerX = pick(centerXArg, xAxis.min + xRange / 2),
807
			newXRange = xRange * howMuch,
808
			yAxis = chart.yAxis[0],
809
			yRange = yAxis.max - yAxis.min,
810
			centerY = pick(centerYArg, yAxis.min + yRange / 2),
811
			newYRange = yRange * howMuch,
812
			fixToX = mouseX ? ((mouseX - xAxis.pos) / xAxis.len) : 0.5,
813
			fixToY = mouseY ? ((mouseY - yAxis.pos) / yAxis.len) : 0.5,
814
			newXMin = centerX - newXRange * fixToX,
815
			newYMin = centerY - newYRange * fixToY,
816
			newExt = chart.fitToBox({
817
				x: newXMin,
818
				y: newYMin,
819
				width: newXRange,
820
				height: newYRange
821
			}, {
822
				x: xAxis.dataMin,
823
				y: yAxis.dataMin,
824
				width: xAxis.dataMax - xAxis.dataMin,
825
				height: yAxis.dataMax - yAxis.dataMin
826
			});
827
 
828
		// When mousewheel zooming, fix the point under the mouse
829
		if (mouseX) {
830
			xAxis.fixTo = [mouseX - xAxis.pos, centerXArg];
831
		}
832
		if (mouseY) {
833
			yAxis.fixTo = [mouseY - yAxis.pos, centerYArg];
834
		}
835
 
836
		// Zoom
837
		if (howMuch !== undefined) {
838
			xAxis.setExtremes(newExt.x, newExt.x + newExt.width, false);
839
			yAxis.setExtremes(newExt.y, newExt.y + newExt.height, false);
840
 
841
		// Reset zoom
842
		} else {
843
			xAxis.setExtremes(undefined, undefined, false);
844
			yAxis.setExtremes(undefined, undefined, false);
845
		}
846
 
847
		// Prevent zooming until this one is finished animating
848
		/*chart.holdMapZoom = true;
849
		setTimeout(function () {
850
			chart.holdMapZoom = false;
851
		}, 200);*/
852
		/*delay = animation ? animation.duration || 500 : 0;
853
		if (delay) {
854
			chart.isMapZooming = true;
855
			setTimeout(function () {
856
				chart.isMapZooming = false;
857
				if (chart.mapZoomQueue) {
858
					chart.mapZoom.apply(chart, chart.mapZoomQueue);
859
				}
860
				chart.mapZoomQueue = null;
861
			}, delay);
862
		}*/
863
 
864
		chart.redraw();
865
	}
866
});
867
 
868
/**
869
 * Extend the Chart.render method to add zooming and panning
870
 */
871
wrap(Chart.prototype, 'render', function (proceed) {
872
	var chart = this,
873
		mapNavigation = chart.options.mapNavigation;
874
 
875
	// Render the plus and minus buttons. Doing this before the shapes makes getBBox much quicker, at least in Chrome.
876
	chart.renderMapNavigation();
877
 
878
	proceed.call(chart);
879
 
880
	// Add the double click event
881
	if (pick(mapNavigation.enableDoubleClickZoom, mapNavigation.enabled) || mapNavigation.enableDoubleClickZoomTo) {
882
		addEvent(chart.container, 'dblclick', function (e) {
883
			chart.pointer.onContainerDblClick(e);
884
		});
885
	}
886
 
887
	// Add the mousewheel event
888
	if (pick(mapNavigation.enableMouseWheelZoom, mapNavigation.enabled)) {
889
		addEvent(chart.container, document.onmousewheel === undefined ? 'DOMMouseScroll' : 'mousewheel', function (e) {
890
			chart.pointer.onContainerMouseWheel(e);
891
			return false;
892
		});
893
	}
894
});
895
 
896
// Extend the Pointer
897
extend(Pointer.prototype, {
898
 
899
	/**
900
	 * The event handler for the doubleclick event
901
	 */
902
	onContainerDblClick: function (e) {
903
		var chart = this.chart;
904
 
905
		e = this.normalize(e);
906
 
907
		if (chart.options.mapNavigation.enableDoubleClickZoomTo) {
908
			if (chart.pointer.inClass(e.target, 'highcharts-tracker')) {
909
				chart.hoverPoint.zoomTo();
910
			}
911
		} else if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) {
912
			chart.mapZoom(
913
				0.5,
914
				chart.xAxis[0].toValue(e.chartX),
915
				chart.yAxis[0].toValue(e.chartY),
916
				e.chartX,
917
				e.chartY
918
			);
919
		}
920
	},
921
 
922
	/**
923
	 * The event handler for the mouse scroll event
924
	 */
925
	onContainerMouseWheel: function (e) {
926
		var chart = this.chart,
927
			delta;
928
 
929
		e = this.normalize(e);
930
 
931
		// Firefox uses e.detail, WebKit and IE uses wheelDelta
932
		delta = e.detail || -(e.wheelDelta / 120);
933
		if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) {
934
			chart.mapZoom(
935
				//delta > 0 ? 2 : 0.5,
936
				Math.pow(2, delta),
937
				chart.xAxis[0].toValue(e.chartX),
938
				chart.yAxis[0].toValue(e.chartY),
939
				e.chartX,
940
				e.chartY
941
			);
942
		}
943
	}
944
});
945
 
946
// Implement the pinchType option
947
wrap(Pointer.prototype, 'init', function (proceed, chart, options) {
948
 
949
	proceed.call(this, chart, options);
950
 
951
	// Pinch status
952
	if (pick(options.mapNavigation.enableTouchZoom, options.mapNavigation.enabled)) {
953
		this.pinchX = this.pinchHor = this.pinchY = this.pinchVert = this.hasZoom = true;
954
	}
955
});
956
 
957
// Extend the pinchTranslate method to preserve fixed ratio when zooming
958
wrap(Pointer.prototype, 'pinchTranslate', function (proceed, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) {
959
	var xBigger;
960
	proceed.call(this, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
961
 
962
	// Keep ratio
963
	if (this.chart.options.chart.type === 'map' && this.hasZoom) {
964
		xBigger = transform.scaleX > transform.scaleY;
965
		this.pinchTranslateDirection(
966
			!xBigger,
967
			pinchDown,
968
			touches,
969
			transform,
970
			selectionMarker,
971
			clip,
972
			lastValidTouch,
973
			xBigger ? transform.scaleX : transform.scaleY
974
		);
975
	}
976
});
977
 
978
 
979
// The vector-effect attribute is not supported in IE <= 11 (at least), so we need
980
// diffent logic (#3218)
981
var supportsVectorEffect = document.documentElement.style.vectorEffect !== undefined;
982
 
983
/**
984
 * Extend the default options with map options
985
 */
986
defaultPlotOptions.map = merge(defaultPlotOptions.scatter, {
987
	allAreas: true,
988
 
989
	animation: false, // makes the complex shapes slow
990
	nullColor: '#F8F8F8',
991
	borderColor: 'silver',
992
	borderWidth: 1,
993
	marker: null,
994
	stickyTracking: false,
995
	dataLabels: {
996
		formatter: function () { // #2945
997
			return this.point.value;
998
		},
999
		inside: true, // for the color
1000
		verticalAlign: 'middle',
1001
		crop: false,
1002
		overflow: false,
1003
		padding: 0
1004
	},
1005
	turboThreshold: 0,
1006
	tooltip: {
1007
		followPointer: true,
1008
		pointFormat: '{point.name}: {point.value}<br/>'
1009
	},
1010
	states: {
1011
		normal: {
1012
			animation: true
1013
		},
1014
		hover: {
1015
			brightness: 0.2,
1016
			halo: null
1017
		}
1018
	}
1019
});
1020
 
1021
/**
1022
 * The MapAreaPoint object
1023
 */
1024
var MapAreaPoint = extendClass(Point, extend({
1025
	/**
1026
	 * Extend the Point object to split paths
1027
	 */
1028
	applyOptions: function (options, x) {
1029
 
1030
		var point = Point.prototype.applyOptions.call(this, options, x),
1031
			series = this.series,
1032
			joinBy = series.joinBy,
1033
			mapPoint;
1034
 
1035
		if (series.mapData) {
1036
			mapPoint = point[joinBy[1]] !== undefined && series.mapMap[point[joinBy[1]]];
1037
			if (mapPoint) {
1038
				// This applies only to bubbles
1039
				if (series.xyFromShape) {
1040
					point.x = mapPoint._midX;
1041
					point.y = mapPoint._midY;
1042
				}
1043
				extend(point, mapPoint); // copy over properties
1044
			} else {
1045
				point.value = point.value || null;
1046
			}
1047
		}
1048
 
1049
		return point;
1050
	},
1051
 
1052
	/**
1053
	 * Stop the fade-out
1054
	 */
1055
	onMouseOver: function (e) {
1056
		clearTimeout(this.colorInterval);
1057
		if (this.value !== null) {
1058
			Point.prototype.onMouseOver.call(this, e);
1059
		} else { //#3401 Tooltip doesn't hide when hovering over null points
1060
			this.series.onMouseOut(e);
1061
		}
1062
	},
1063
	/**
1064
	 * Custom animation for tweening out the colors. Animation reduces blinking when hovering
1065
	 * over islands and coast lines. We run a custom implementation of animation becuase we
1066
	 * need to be able to run this independently from other animations like zoom redraw. Also,
1067
	 * adding color animation to the adapters would introduce almost the same amount of code.
1068
	 */
1069
	onMouseOut: function () {
1070
		var point = this,
1071
			start = +new Date(),
1072
			normalColor = Color(point.color),
1073
			hoverColor = Color(point.pointAttr.hover.fill),
1074
			animation = point.series.options.states.normal.animation,
1075
			duration = animation && (animation.duration || 500),
1076
			fill;
1077
 
1078
		if (duration && normalColor.rgba.length === 4 && hoverColor.rgba.length === 4 && point.state !== 'select') {
1079
			fill = point.pointAttr[''].fill;
1080
			delete point.pointAttr[''].fill; // avoid resetting it in Point.setState
1081
 
1082
			clearTimeout(point.colorInterval);
1083
			point.colorInterval = setInterval(function () {
1084
				var pos = (new Date() - start) / duration,
1085
					graphic = point.graphic;
1086
				if (pos > 1) {
1087
					pos = 1;
1088
				}
1089
				if (graphic) {
1090
					graphic.attr('fill', ColorAxis.prototype.tweenColors.call(0, hoverColor, normalColor, pos));
1091
				}
1092
				if (pos >= 1) {
1093
					clearTimeout(point.colorInterval);
1094
				}
1095
			}, 13);
1096
		}
1097
		Point.prototype.onMouseOut.call(point);
1098
 
1099
		if (fill) {
1100
			point.pointAttr[''].fill = fill;
1101
		}
1102
	},
1103
 
1104
	/**
1105
	 * Zoom the chart to view a specific area point
1106
	 */
1107
	zoomTo: function () {
1108
		var point = this,
1109
			series = point.series;
1110
 
1111
		series.xAxis.setExtremes(
1112
			point._minX,
1113
			point._maxX,
1114
			false
1115
		);
1116
		series.yAxis.setExtremes(
1117
			point._minY,
1118
			point._maxY,
1119
			false
1120
		);
1121
		series.chart.redraw();
1122
	}
1123
}, colorPointMixin)
1124
);
1125
 
1126
/**
1127
 * Add the series type
1128
 */
1129
seriesTypes.map = extendClass(seriesTypes.scatter, merge(colorSeriesMixin, {
1130
	type: 'map',
1131
	pointClass: MapAreaPoint,
1132
	supportsDrilldown: true,
1133
	getExtremesFromAll: true,
1134
	useMapGeometry: true, // get axis extremes from paths, not values
1135
	forceDL: true,
1136
	searchPoint: noop,
1137
	directTouch: true, // When tooltip is not shared, this series (and derivatives) requires direct touch/hover. KD-tree does not apply.
1138
	preserveAspectRatio: true, // X axis and Y axis must have same translation slope
1139
	/**
1140
	 * Get the bounding box of all paths in the map combined.
1141
	 */
1142
	getBox: function (paths) {
1143
		var MAX_VALUE = Number.MAX_VALUE,
1144
			maxX = -MAX_VALUE,
1145
			minX =  MAX_VALUE,
1146
			maxY = -MAX_VALUE,
1147
			minY =  MAX_VALUE,
1148
			minRange = MAX_VALUE,
1149
			xAxis = this.xAxis,
1150
			yAxis = this.yAxis,
1151
			hasBox;
1152
 
1153
		// Find the bounding box
1154
		each(paths || [], function (point) {
1155
 
1156
			if (point.path) {
1157
				if (typeof point.path === 'string') {
1158
					point.path = Highcharts.splitPath(point.path);
1159
				}
1160
 
1161
				var path = point.path || [],
1162
					i = path.length,
1163
					even = false, // while loop reads from the end
1164
					pointMaxX = -MAX_VALUE,
1165
					pointMinX =  MAX_VALUE,
1166
					pointMaxY = -MAX_VALUE,
1167
					pointMinY =  MAX_VALUE,
1168
					properties = point.properties;
1169
 
1170
				// The first time a map point is used, analyze its box
1171
				if (!point._foundBox) {
1172
					while (i--) {
1173
						if (typeof path[i] === 'number' && !isNaN(path[i])) {
1174
							if (even) { // even = x
1175
								pointMaxX = Math.max(pointMaxX, path[i]);
1176
								pointMinX = Math.min(pointMinX, path[i]);
1177
							} else { // odd = Y
1178
								pointMaxY = Math.max(pointMaxY, path[i]);
1179
								pointMinY = Math.min(pointMinY, path[i]);
1180
							}
1181
							even = !even;
1182
						}
1183
					}
1184
					// Cache point bounding box for use to position data labels, bubbles etc
1185
					point._midX = pointMinX + (pointMaxX - pointMinX) *
1186
						(point.middleX || (properties && properties['hc-middle-x']) || 0.5); // pick is slower and very marginally needed
1187
					point._midY = pointMinY + (pointMaxY - pointMinY) *
1188
						(point.middleY || (properties && properties['hc-middle-y']) || 0.5);
1189
					point._maxX = pointMaxX;
1190
					point._minX = pointMinX;
1191
					point._maxY = pointMaxY;
1192
					point._minY = pointMinY;
1193
					point.labelrank = pick(point.labelrank, (pointMaxX - pointMinX) * (pointMaxY - pointMinY));
1194
					point._foundBox = true;
1195
				}
1196
 
1197
				maxX = Math.max(maxX, point._maxX);
1198
				minX = Math.min(minX, point._minX);
1199
				maxY = Math.max(maxY, point._maxY);
1200
				minY = Math.min(minY, point._minY);
1201
				minRange = Math.min(point._maxX - point._minX, point._maxY - point._minY, minRange);
1202
				hasBox = true;
1203
			}
1204
		});
1205
 
1206
		// Set the box for the whole series
1207
		if (hasBox) {
1208
			this.minY = Math.min(minY, pick(this.minY, MAX_VALUE));
1209
			this.maxY = Math.max(maxY, pick(this.maxY, -MAX_VALUE));
1210
			this.minX = Math.min(minX, pick(this.minX, MAX_VALUE));
1211
			this.maxX = Math.max(maxX, pick(this.maxX, -MAX_VALUE));
1212
 
1213
			// If no minRange option is set, set the default minimum zooming range to 5 times the
1214
			// size of the smallest element
1215
			if (xAxis && xAxis.options.minRange === undefined) {
1216
				xAxis.minRange = Math.min(5 * minRange, (this.maxX - this.minX) / 5, xAxis.minRange || MAX_VALUE);
1217
			}
1218
			if (yAxis && yAxis.options.minRange === undefined) {
1219
				yAxis.minRange = Math.min(5 * minRange, (this.maxY - this.minY) / 5, yAxis.minRange || MAX_VALUE);
1220
			}
1221
		}
1222
	},
1223
 
1224
	getExtremes: function () {
1225
		// Get the actual value extremes for colors
1226
		Series.prototype.getExtremes.call(this, this.valueData);
1227
 
1228
		// Recalculate box on updated data
1229
		if (this.chart.hasRendered && this.isDirtyData) {
1230
			this.getBox(this.options.data);
1231
		}
1232
 
1233
		this.valueMin = this.dataMin;
1234
		this.valueMax = this.dataMax;
1235
 
1236
		// Extremes for the mock Y axis
1237
		this.dataMin = this.minY;
1238
		this.dataMax = this.maxY;
1239
	},
1240
 
1241
	/**
1242
	 * Translate the path so that it automatically fits into the plot area box
1243
	 * @param {Object} path
1244
	 */
1245
	translatePath: function (path) {
1246
 
1247
		var series = this,
1248
			even = false, // while loop reads from the end
1249
			xAxis = series.xAxis,
1250
			yAxis = series.yAxis,
1251
			xMin = xAxis.min,
1252
			xTransA = xAxis.transA,
1253
			xMinPixelPadding = xAxis.minPixelPadding,
1254
			yMin = yAxis.min,
1255
			yTransA = yAxis.transA,
1256
			yMinPixelPadding = yAxis.minPixelPadding,
1257
			i,
1258
			ret = []; // Preserve the original
1259
 
1260
		// Do the translation
1261
		if (path) {
1262
			i = path.length;
1263
			while (i--) {
1264
				if (typeof path[i] === 'number') {
1265
					ret[i] = even ?
1266
						(path[i] - xMin) * xTransA + xMinPixelPadding :
1267
						(path[i] - yMin) * yTransA + yMinPixelPadding;
1268
					even = !even;
1269
				} else {
1270
					ret[i] = path[i];
1271
				}
1272
			}
1273
		}
1274
 
1275
		return ret;
1276
	},
1277
 
1278
	/**
1279
	 * Extend setData to join in mapData. If the allAreas option is true, all areas
1280
	 * from the mapData are used, and those that don't correspond to a data value
1281
	 * are given null values.
1282
	 */
1283
	setData: function (data, redraw) {
1284
		var options = this.options,
1285
			mapData = options.mapData,
1286
			joinBy = options.joinBy,
1287
			joinByNull = joinBy === null,
1288
			dataUsed = [],
1289
			mapPoint,
1290
			transform,
1291
			mapTransforms,
1292
			props,
1293
			i;
1294
 
1295
		if (joinByNull) {
1296
			joinBy = '_i';
1297
		}
1298
		joinBy = this.joinBy = Highcharts.splat(joinBy);
1299
		if (!joinBy[1]) {
1300
			joinBy[1] = joinBy[0];
1301
		}
1302
 
1303
		// Pick up numeric values, add index
1304
		if (data) {
1305
			each(data, function (val, i) {
1306
				if (typeof val === 'number') {
1307
					data[i] = {
1308
						value: val
1309
					};
1310
				}
1311
				if (joinByNull) {
1312
					data[i]._i = i;
1313
				}
1314
			});
1315
		}
1316
 
1317
		this.getBox(data);
1318
		if (mapData) {
1319
			if (mapData.type === 'FeatureCollection') {
1320
				if (mapData['hc-transform']) {
1321
					this.chart.mapTransforms = mapTransforms = mapData['hc-transform'];
1322
					// Cache cos/sin of transform rotation angle
1323
					for (transform in mapTransforms) {
1324
						if (mapTransforms.hasOwnProperty(transform) && transform.rotation) {
1325
							transform.cosAngle = Math.cos(transform.rotation);
1326
							transform.sinAngle = Math.sin(transform.rotation);
1327
						}
1328
					}
1329
				}
1330
				mapData = Highcharts.geojson(mapData, this.type, this);
1331
			}
1332
 
1333
			this.getBox(mapData);
1334
			this.mapData = mapData;
1335
			this.mapMap = {};
1336
 
1337
			for (i = 0; i < mapData.length; i++) {
1338
				mapPoint = mapData[i];
1339
				props = mapPoint.properties;
1340
 
1341
				mapPoint._i = i;
1342
				// Copy the property over to root for faster access
1343
				if (joinBy[0] && props && props[joinBy[0]]) {
1344
					mapPoint[joinBy[0]] = props[joinBy[0]];
1345
				}
1346
				this.mapMap[mapPoint[joinBy[0]]] = mapPoint;
1347
			}
1348
 
1349
			if (options.allAreas) {
1350
 
1351
				data = data || [];
1352
 
1353
				// Registered the point codes that actually hold data
1354
				if (joinBy[1]) {
1355
					each(data, function (point) {
1356
						dataUsed.push(point[joinBy[1]]);
1357
					});
1358
				}
1359
 
1360
				// Add those map points that don't correspond to data, which will be drawn as null points
1361
				dataUsed = '|' + dataUsed.join('|') + '|'; // String search is faster than array.indexOf
1362
 
1363
				each(mapData, function (mapPoint) {
1364
					if (!joinBy[0] || dataUsed.indexOf('|' + mapPoint[joinBy[0]] + '|') === -1) {
1365
						data.push(merge(mapPoint, { value: null }));
1366
					}
1367
				});
1368
			}
1369
		}
1370
		Series.prototype.setData.call(this, data, redraw);
1371
	},
1372
 
1373
 
1374
	/**
1375
	 * No graph for the map series
1376
	 */
1377
	drawGraph: noop,
1378
 
1379
	/**
1380
	 * We need the points' bounding boxes in order to draw the data labels, so
1381
	 * we skip it now and call it from drawPoints instead.
1382
	 */
1383
	drawDataLabels: noop,
1384
 
1385
	/**
1386
	 * Allow a quick redraw by just translating the area group. Used for zooming and panning
1387
	 * in capable browsers.
1388
	 */
1389
	doFullTranslate: function () {
1390
		return this.isDirtyData || this.chart.isResizing || this.chart.renderer.isVML || !this.baseTrans;
1391
	},
1392
 
1393
	/**
1394
	 * Add the path option for data points. Find the max value for color calculation.
1395
	 */
1396
	translate: function () {
1397
		var series = this,
1398
			xAxis = series.xAxis,
1399
			yAxis = series.yAxis,
1400
			doFullTranslate = series.doFullTranslate();
1401
 
1402
		series.generatePoints();
1403
 
1404
		each(series.data, function (point) {
1405
 
1406
			// Record the middle point (loosely based on centroid), determined
1407
			// by the middleX and middleY options.
1408
			point.plotX = xAxis.toPixels(point._midX, true);
1409
			point.plotY = yAxis.toPixels(point._midY, true);
1410
 
1411
			if (doFullTranslate) {
1412
 
1413
				point.shapeType = 'path';
1414
				point.shapeArgs = {
1415
					d: series.translatePath(point.path)
1416
				};
1417
				if (supportsVectorEffect) {
1418
					point.shapeArgs['vector-effect'] = 'non-scaling-stroke';
1419
				}
1420
			}
1421
		});
1422
 
1423
		series.translateColors();
1424
	},
1425
 
1426
	/**
1427
	 * Use the drawPoints method of column, that is able to handle simple shapeArgs.
1428
	 * Extend it by assigning the tooltip position.
1429
	 */
1430
	drawPoints: function () {
1431
		var series = this,
1432
			xAxis = series.xAxis,
1433
			yAxis = series.yAxis,
1434
			group = series.group,
1435
			chart = series.chart,
1436
			renderer = chart.renderer,
1437
			scaleX,
1438
			scaleY,
1439
			translateX,
1440
			translateY,
1441
			baseTrans = this.baseTrans;
1442
 
1443
		// Set a group that handles transform during zooming and panning in order to preserve clipping
1444
		// on series.group
1445
		if (!series.transformGroup) {
1446
			series.transformGroup = renderer.g()
1447
				.attr({
1448
					scaleX: 1,
1449
					scaleY: 1
1450
				})
1451
				.add(group);
1452
			series.transformGroup.survive = true;
1453
		}
1454
 
1455
		// Draw the shapes again
1456
		if (series.doFullTranslate()) {
1457
 
1458
			// Individual point actions
1459
			if (chart.hasRendered && series.pointAttrToOptions.fill === 'color') {
1460
				each(series.points, function (point) {
1461
 
1462
					// Reset color on update/redraw
1463
					if (point.shapeArgs) {
1464
						point.shapeArgs.fill = point.pointAttr[pick(point.state, '')].fill; // #3529
1465
					}
1466
				});
1467
			}
1468
 
1469
			// If vector-effect is not supported, we set the stroke-width on the group element
1470
			// and let all point graphics inherit. That way we don't have to iterate over all
1471
			// points to update the stroke-width on zooming.
1472
			if (!supportsVectorEffect) {
1473
				each(series.points, function (point) {
1474
					var attr = point.pointAttr[''];
1475
					if (attr['stroke-width'] === series.pointAttr['']['stroke-width']) {
1476
						attr['stroke-width'] = 'inherit';
1477
					}
1478
				});
1479
			}
1480
 
1481
			// Draw them in transformGroup
1482
			series.group = series.transformGroup;
1483
			seriesTypes.column.prototype.drawPoints.apply(series);
1484
			series.group = group; // Reset
1485
 
1486
			// Add class names
1487
			each(series.points, function (point) {
1488
				if (point.graphic) {
1489
					if (point.name) {
1490
						point.graphic.addClass('highcharts-name-' + point.name.replace(' ', '-').toLowerCase());
1491
					}
1492
					if (point.properties && point.properties['hc-key']) {
1493
						point.graphic.addClass('highcharts-key-' + point.properties['hc-key'].toLowerCase());
1494
					}
1495
 
1496
					if (!supportsVectorEffect) {
1497
						point.graphic['stroke-widthSetter'] = noop;
1498
					}
1499
				}
1500
			});
1501
 
1502
			// Set the base for later scale-zooming. The originX and originY properties are the
1503
			// axis values in the plot area's upper left corner.
1504
			this.baseTrans = {
1505
				originX: xAxis.min - xAxis.minPixelPadding / xAxis.transA,
1506
				originY: yAxis.min - yAxis.minPixelPadding / yAxis.transA + (yAxis.reversed ? 0 : yAxis.len / yAxis.transA),
1507
				transAX: xAxis.transA,
1508
				transAY: yAxis.transA
1509
			};
1510
 
1511
			// Reset transformation in case we're doing a full translate (#3789)
1512
			this.transformGroup.animate({
1513
				translateX: 0,
1514
				translateY: 0,
1515
				scaleX: 1,
1516
				scaleY: 1
1517
			});
1518
 
1519
		// Just update the scale and transform for better performance
1520
		} else {
1521
			scaleX = xAxis.transA / baseTrans.transAX;
1522
			scaleY = yAxis.transA / baseTrans.transAY;
1523
			translateX = xAxis.toPixels(baseTrans.originX, true);
1524
			translateY = yAxis.toPixels(baseTrans.originY, true);
1525
 
1526
			// Handle rounding errors in normal view (#3789)
1527
			if (scaleX > 0.99 && scaleX < 1.01 && scaleY > 0.99 && scaleY < 1.01) {
1528
				scaleX = 1;
1529
				scaleY = 1;
1530
				translateX = Math.round(translateX);
1531
				translateY = Math.round(translateY);
1532
			}
1533
 
1534
			this.transformGroup.animate({
1535
				translateX: translateX,
1536
				translateY: translateY,
1537
				scaleX: scaleX,
1538
				scaleY: scaleY
1539
			});
1540
 
1541
		}
1542
 
1543
		// Set the stroke-width directly on the group element so the children inherit it. We need to use
1544
		// setAttribute directly, because the stroke-widthSetter method expects a stroke color also to be
1545
		// set.
1546
		if (!supportsVectorEffect) {
1547
			series.group.element.setAttribute('stroke-width', series.options.borderWidth / (scaleX || 1));
1548
		}
1549
 
1550
		this.drawMapDataLabels();
1551
 
1552
 
1553
	},
1554
 
1555
	/**
1556
	 * Draw the data labels. Special for maps is the time that the data labels are drawn (after points),
1557
	 * and the clipping of the dataLabelsGroup.
1558
	 */
1559
	drawMapDataLabels: function () {
1560
 
1561
		Series.prototype.drawDataLabels.call(this);
1562
		if (this.dataLabelsGroup) {
1563
			this.dataLabelsGroup.clip(this.chart.clipRect);
1564
		}
1565
	},
1566
 
1567
	/**
1568
	 * Override render to throw in an async call in IE8. Otherwise it chokes on the US counties demo.
1569
	 */
1570
	render: function () {
1571
		var series = this,
1572
			render = Series.prototype.render;
1573
 
1574
		// Give IE8 some time to breathe.
1575
		if (series.chart.renderer.isVML && series.data.length > 3000) {
1576
			setTimeout(function () {
1577
				render.call(series);
1578
			});
1579
		} else {
1580
			render.call(series);
1581
		}
1582
	},
1583
 
1584
	/**
1585
	 * The initial animation for the map series. By default, animation is disabled.
1586
	 * Animation of map shapes is not at all supported in VML browsers.
1587
	 */
1588
	animate: function (init) {
1589
		var chart = this.chart,
1590
			animation = this.options.animation,
1591
			group = this.group,
1592
			xAxis = this.xAxis,
1593
			yAxis = this.yAxis,
1594
			left = xAxis.pos,
1595
			top = yAxis.pos;
1596
 
1597
		if (chart.renderer.isSVG) {
1598
 
1599
			if (animation === true) {
1600
				animation = {
1601
					duration: 1000
1602
				};
1603
			}
1604
 
1605
			// Initialize the animation
1606
			if (init) {
1607
 
1608
				// Scale down the group and place it in the center
1609
				group.attr({
1610
					translateX: left + xAxis.len / 2,
1611
					translateY: top + yAxis.len / 2,
1612
					scaleX: 0.001, // #1499
1613
					scaleY: 0.001
1614
				});
1615
 
1616
			// Run the animation
1617
			} else {
1618
				group.animate({
1619
					translateX: left,
1620
					translateY: top,
1621
					scaleX: 1,
1622
					scaleY: 1
1623
				}, animation);
1624
 
1625
				// Delete this function to allow it only once
1626
				this.animate = null;
1627
			}
1628
		}
1629
	},
1630
 
1631
	/**
1632
	 * Animate in the new series from the clicked point in the old series.
1633
	 * Depends on the drilldown.js module
1634
	 */
1635
	animateDrilldown: function (init) {
1636
		var toBox = this.chart.plotBox,
1637
			level = this.chart.drilldownLevels[this.chart.drilldownLevels.length - 1],
1638
			fromBox = level.bBox,
1639
			animationOptions = this.chart.options.drilldown.animation,
1640
			scale;
1641
 
1642
		if (!init) {
1643
 
1644
			scale = Math.min(fromBox.width / toBox.width, fromBox.height / toBox.height);
1645
			level.shapeArgs = {
1646
				scaleX: scale,
1647
				scaleY: scale,
1648
				translateX: fromBox.x,
1649
				translateY: fromBox.y
1650
			};
1651
 
1652
			// TODO: Animate this.group instead
1653
			each(this.points, function (point) {
1654
				if (point.graphic) {
1655
					point.graphic
1656
						.attr(level.shapeArgs)
1657
						.animate({
1658
							scaleX: 1,
1659
							scaleY: 1,
1660
							translateX: 0,
1661
							translateY: 0
1662
						}, animationOptions);
1663
				}
1664
			});
1665
 
1666
			this.animate = null;
1667
		}
1668
 
1669
	},
1670
 
1671
	drawLegendSymbol: LegendSymbolMixin.drawRectangle,
1672
 
1673
	/**
1674
	 * When drilling up, pull out the individual point graphics from the lower series
1675
	 * and animate them into the origin point in the upper series.
1676
	 */
1677
	animateDrillupFrom: function (level) {
1678
		seriesTypes.column.prototype.animateDrillupFrom.call(this, level);
1679
	},
1680
 
1681
 
1682
	/**
1683
	 * When drilling up, keep the upper series invisible until the lower series has
1684
	 * moved into place
1685
	 */
1686
	animateDrillupTo: function (init) {
1687
		seriesTypes.column.prototype.animateDrillupTo.call(this, init);
1688
	}
1689
}));
1690
 
1691
 
1692
// The mapline series type
1693
defaultPlotOptions.mapline = merge(defaultPlotOptions.map, {
1694
	lineWidth: 1,
1695
	fillColor: 'none'
1696
});
1697
seriesTypes.mapline = extendClass(seriesTypes.map, {
1698
	type: 'mapline',
1699
	pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
1700
		stroke: 'color',
1701
		'stroke-width': 'lineWidth',
1702
		fill: 'fillColor',
1703
		dashstyle: 'dashStyle'
1704
	},
1705
	drawLegendSymbol: seriesTypes.line.prototype.drawLegendSymbol
1706
});
1707
 
1708
// The mappoint series type
1709
defaultPlotOptions.mappoint = merge(defaultPlotOptions.scatter, {
1710
	dataLabels: {
1711
		enabled: true,
1712
		formatter: function () { // #2945
1713
			return this.point.name;
1714
		},
1715
		crop: false,
1716
		defer: false,
1717
		overflow: false,
1718
		style: {
1719
			color: '#000000'
1720
		}
1721
	}
1722
});
1723
seriesTypes.mappoint = extendClass(seriesTypes.scatter, {
1724
	type: 'mappoint',
1725
	forceDL: true,
1726
	pointClass: extendClass(Point, {
1727
		applyOptions: function (options, x) {
1728
			var point = Point.prototype.applyOptions.call(this, options, x);
1729
			if (options.lat !== undefined && options.lon !== undefined) {
1730
				point = extend(point, this.series.chart.fromLatLonToPoint(point));
1731
			}
1732
			return point;
1733
		}
1734
	})
1735
});
1736
 
1737
// The mapbubble series type
1738
if (seriesTypes.bubble) {
1739
 
1740
	defaultPlotOptions.mapbubble = merge(defaultPlotOptions.bubble, {
1741
		animationLimit: 500,
1742
		tooltip: {
1743
			pointFormat: '{point.name}: {point.z}'
1744
		}
1745
	});
1746
	seriesTypes.mapbubble = extendClass(seriesTypes.bubble, {
1747
		pointClass: extendClass(Point, {
1748
			applyOptions: function (options, x) {
1749
				var point;
1750
				if (options && options.lat !== undefined && options.lon !== undefined) {
1751
					point = Point.prototype.applyOptions.call(this, options, x);
1752
					point = extend(point, this.series.chart.fromLatLonToPoint(point));
1753
				} else {
1754
					point = MapAreaPoint.prototype.applyOptions.call(this, options, x);
1755
				}
1756
				return point;
1757
			},
1758
			ttBelow: false
1759
		}),
1760
		xyFromShape: true,
1761
		type: 'mapbubble',
1762
		pointArrayMap: ['z'], // If one single value is passed, it is interpreted as z
1763
		/**
1764
		 * Return the map area identified by the dataJoinBy option
1765
		 */
1766
		getMapData: seriesTypes.map.prototype.getMapData,
1767
		getBox: seriesTypes.map.prototype.getBox,
1768
		setData: seriesTypes.map.prototype.setData
1769
	});
1770
}
1771
 
1772
/**
1773
 * Extend the default options with map options
1774
 */
1775
defaultOptions.plotOptions.heatmap = merge(defaultOptions.plotOptions.scatter, {
1776
	animation: false,
1777
	borderWidth: 0,
1778
	nullColor: '#F8F8F8',
1779
	dataLabels: {
1780
		formatter: function () { // #2945
1781
			return this.point.value;
1782
		},
1783
		inside: true,
1784
		verticalAlign: 'middle',
1785
		crop: false,
1786
		overflow: false,
1787
		padding: 0 // #3837
1788
	},
1789
	marker: null,
1790
	pointRange: null, // dynamically set to colsize by default
1791
	tooltip: {
1792
		pointFormat: '{point.x}, {point.y}: {point.value}<br/>'
1793
	},
1794
	states: {
1795
		normal: {
1796
			animation: true
1797
		},
1798
		hover: {
1799
			halo: false,  // #3406, halo is not required on heatmaps
1800
			brightness: 0.2
1801
		}
1802
	}
1803
});
1804
 
1805
// The Heatmap series type
1806
seriesTypes.heatmap = extendClass(seriesTypes.scatter, merge(colorSeriesMixin, {
1807
	type: 'heatmap',
1808
	pointArrayMap: ['y', 'value'],
1809
	hasPointSpecificOptions: true,
1810
	pointClass: extendClass(Point, colorPointMixin),
1811
	supportsDrilldown: true,
1812
	getExtremesFromAll: true,
1813
	directTouch: true,
1814
 
1815
	/**
1816
	 * Override the init method to add point ranges on both axes.
1817
	 */
1818
	init: function () {
1819
		var options;
1820
		seriesTypes.scatter.prototype.init.apply(this, arguments);
1821
 
1822
		options = this.options;
1823
		this.pointRange = options.pointRange = pick(options.pointRange, options.colsize || 1); // #3758, prevent resetting in setData
1824
		this.yAxis.axisPointRange = options.rowsize || 1; // general point range
1825
	},
1826
	translate: function () {
1827
		var series = this,
1828
			options = series.options,
1829
			xAxis = series.xAxis,
1830
			yAxis = series.yAxis,
1831
			between = function (x, a, b) {
1832
				return Math.min(Math.max(a, x), b);
1833
			};
1834
 
1835
		series.generatePoints();
1836
 
1837
		each(series.points, function (point) {
1838
			var xPad = (options.colsize || 1) / 2,
1839
				yPad = (options.rowsize || 1) / 2,
1840
				x1 = between(Math.round(xAxis.len - xAxis.translate(point.x - xPad, 0, 1, 0, 1)), 0, xAxis.len),
1841
				x2 = between(Math.round(xAxis.len - xAxis.translate(point.x + xPad, 0, 1, 0, 1)), 0, xAxis.len),
1842
				y1 = between(Math.round(yAxis.translate(point.y - yPad, 0, 1, 0, 1)), 0, yAxis.len),
1843
				y2 = between(Math.round(yAxis.translate(point.y + yPad, 0, 1, 0, 1)), 0, yAxis.len);
1844
 
1845
			// Set plotX and plotY for use in K-D-Tree and more
1846
			point.plotX = point.clientX = (x1 + x2) / 2;
1847
			point.plotY = (y1 + y2) / 2;
1848
 
1849
			point.shapeType = 'rect';
1850
			point.shapeArgs = {
1851
				x: Math.min(x1, x2),
1852
				y: Math.min(y1, y2),
1853
				width: Math.abs(x2 - x1),
1854
				height: Math.abs(y2 - y1)
1855
			};
1856
		});
1857
 
1858
		series.translateColors();
1859
 
1860
		// Make sure colors are updated on colorAxis update (#2893)
1861
		if (this.chart.hasRendered) {
1862
			each(series.points, function (point) {
1863
				point.shapeArgs.fill = point.options.color || point.color; // #3311
1864
			});
1865
		}
1866
	},
1867
	drawPoints: seriesTypes.column.prototype.drawPoints,
1868
	animate: noop,
1869
	getBox: noop,
1870
	drawLegendSymbol: LegendSymbolMixin.drawRectangle,
1871
 
1872
	getExtremes: function () {
1873
		// Get the extremes from the value data
1874
		Series.prototype.getExtremes.call(this, this.valueData);
1875
		this.valueMin = this.dataMin;
1876
		this.valueMax = this.dataMax;
1877
 
1878
		// Get the extremes from the y data
1879
		Series.prototype.getExtremes.call(this);
1880
	}
1881
 
1882
}));
1883
 
1884
 
1885
/**
1886
 * Test for point in polygon. Polygon defined as array of [x,y] points.
1887
 */
1888
function pointInPolygon(point, polygon) {
1889
	var i, j, rel1, rel2, c = false,
1890
		x = point.x,
1891
		y = point.y;
1892
 
1893
	for (i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
1894
		rel1 = polygon[i][1] > y;
1895
		rel2 = polygon[j][1] > y;
1896
		if (rel1 !== rel2 && (x < (polygon[j][0] - polygon[i][0]) * (y - polygon[i][1]) / (polygon[j][1] - polygon[i][1]) + polygon[i][0])) {
1897
			c = !c;
1898
		}
1899
	}
1900
 
1901
	return c;
1902
}
1903
 
1904
/**
1905
 * Get point from latLon using specified transform definition
1906
 */
1907
Chart.prototype.transformFromLatLon = function (latLon, transform) {
1908
	if (window.proj4 === undefined) {
1909
		error(21);
1910
		return {
1911
			x: 0,
1912
			y: null
1913
		};
1914
	}
1915
 
1916
	var projected = window.proj4(transform.crs, [latLon.lon, latLon.lat]),
1917
		cosAngle = transform.cosAngle || (transform.rotation && Math.cos(transform.rotation)),
1918
		sinAngle = transform.sinAngle || (transform.rotation && Math.sin(transform.rotation)),
1919
		rotated = transform.rotation ? [projected[0] * cosAngle + projected[1] * sinAngle, -projected[0] * sinAngle + projected[1] * cosAngle] : projected;
1920
 
1921
	return {
1922
		x: ((rotated[0] - (transform.xoffset || 0)) * (transform.scale || 1) + (transform.xpan || 0)) * (transform.jsonres || 1) + (transform.jsonmarginX || 0),
1923
		y: (((transform.yoffset || 0) - rotated[1]) * (transform.scale || 1) + (transform.ypan || 0)) * (transform.jsonres || 1) - (transform.jsonmarginY || 0)
1924
	};
1925
};
1926
 
1927
/**
1928
 * Get latLon from point using specified transform definition
1929
 */
1930
Chart.prototype.transformToLatLon = function (point, transform) {
1931
	if (window.proj4 === undefined) {
1932
		error(21);
1933
		return;
1934
	}
1935
 
1936
	var normalized = {
1937
			x: ((point.x - (transform.jsonmarginX || 0)) / (transform.jsonres || 1) - (transform.xpan || 0)) / (transform.scale || 1) + (transform.xoffset || 0),
1938
			y: ((-point.y - (transform.jsonmarginY || 0)) / (transform.jsonres || 1) + (transform.ypan || 0)) / (transform.scale || 1) + (transform.yoffset || 0)
1939
		},
1940
		cosAngle = transform.cosAngle || (transform.rotation && Math.cos(transform.rotation)),
1941
		sinAngle = transform.sinAngle || (transform.rotation && Math.sin(transform.rotation)),
1942
		// Note: Inverted sinAngle to reverse rotation direction
1943
		projected = window.proj4(transform.crs, 'WGS84', transform.rotation ? {
1944
			x: normalized.x * cosAngle + normalized.y * -sinAngle,
1945
			y: normalized.x * sinAngle + normalized.y * cosAngle
1946
		} : normalized);
1947
 
1948
	return {lat: projected.y, lon: projected.x};
1949
};
1950
 
1951
Chart.prototype.fromPointToLatLon = function (point) {
1952
	var transforms = this.mapTransforms,
1953
		transform;
1954
 
1955
	if (!transforms) {
1956
		error(22);
1957
		return;
1958
	}
1959
 
1960
	for (transform in transforms) {
1961
		if (transforms.hasOwnProperty(transform) && transforms[transform].hitZone && pointInPolygon({x: point.x, y: -point.y}, transforms[transform].hitZone.coordinates[0])) {
1962
			return this.transformToLatLon(point, transforms[transform]);
1963
		}
1964
	}
1965
 
1966
	return this.transformToLatLon(point, transforms['default']);
1967
};
1968
 
1969
Chart.prototype.fromLatLonToPoint = function (latLon) {
1970
	var transforms = this.mapTransforms,
1971
		transform,
1972
		coords;
1973
 
1974
	if (!transforms) {
1975
		error(22);
1976
		return {
1977
			x: 0,
1978
			y: null
1979
		};
1980
	}
1981
 
1982
	for (transform in transforms) {
1983
		if (transforms.hasOwnProperty(transform) && transforms[transform].hitZone) {
1984
			coords = this.transformFromLatLon(latLon, transforms[transform]);
1985
			if (pointInPolygon({x: coords.x, y: -coords.y}, transforms[transform].hitZone.coordinates[0])) {
1986
				return coords;
1987
			}
1988
		}
1989
	}
1990
 
1991
	return this.transformFromLatLon(latLon, transforms['default']);
1992
};
1993
 
1994
/**
1995
 * Convert a geojson object to map data of a given Highcharts type (map, mappoint or mapline).
1996
 */
1997
Highcharts.geojson = function (geojson, hType, series) {
1998
	var mapData = [],
1999
		path = [],
2000
		polygonToPath = function (polygon) {
2001
			var i = 0,
2002
				len = polygon.length;
2003
			path.push('M');
2004
			for (; i < len; i++) {
2005
				if (i === 1) {
2006
					path.push('L');
2007
				}
2008
				path.push(polygon[i][0], -polygon[i][1]);
2009
			}
2010
		};
2011
 
2012
	hType = hType || 'map';
2013
 
2014
	each(geojson.features, function (feature) {
2015
 
2016
		var geometry = feature.geometry,
2017
			type = geometry.type,
2018
			coordinates = geometry.coordinates,
2019
			properties = feature.properties,
2020
			point;
2021
 
2022
		path = [];
2023
 
2024
		if (hType === 'map' || hType === 'mapbubble') {
2025
			if (type === 'Polygon') {
2026
				each(coordinates, polygonToPath);
2027
				path.push('Z');
2028
 
2029
			} else if (type === 'MultiPolygon') {
2030
				each(coordinates, function (items) {
2031
					each(items, polygonToPath);
2032
				});
2033
				path.push('Z');
2034
			}
2035
 
2036
			if (path.length) {
2037
				point = { path: path };
2038
			}
2039
 
2040
		} else if (hType === 'mapline') {
2041
			if (type === 'LineString') {
2042
				polygonToPath(coordinates);
2043
			} else if (type === 'MultiLineString') {
2044
				each(coordinates, polygonToPath);
2045
			}
2046
 
2047
			if (path.length) {
2048
				point = { path: path };
2049
			}
2050
 
2051
		} else if (hType === 'mappoint') {
2052
			if (type === 'Point') {
2053
				point = {
2054
					x: coordinates[0],
2055
					y: -coordinates[1]
2056
				};
2057
			}
2058
		}
2059
		if (point) {
2060
			mapData.push(extend(point, {
2061
				name: properties.name || properties.NAME,
2062
				properties: properties
2063
			}));
2064
		}
2065
 
2066
	});
2067
 
2068
	// Create a credits text that includes map source, to be picked up in Chart.showCredits
2069
	if (series && geojson.copyrightShort) {
2070
		series.chart.mapCredits = '<a href="http://www.highcharts.com">Highcharts</a> \u00A9 ' +
2071
			'<a href="' + geojson.copyrightUrl + '">' + geojson.copyrightShort + '</a>';
2072
		series.chart.mapCreditsFull = geojson.copyright;
2073
	}
2074
 
2075
	return mapData;
2076
};
2077
 
2078
/**
2079
 * Override showCredits to include map source by default
2080
 */
2081
wrap(Chart.prototype, 'showCredits', function (proceed, credits) {
2082
 
2083
	if (defaultOptions.credits.text === this.options.credits.text && this.mapCredits) { // default text and mapCredits is set
2084
		credits.text = this.mapCredits;
2085
		credits.href = null;
2086
	}
2087
 
2088
	proceed.call(this, credits);
2089
 
2090
	if (this.credits) {
2091
		this.credits.attr({
2092
			title: this.mapCreditsFull
2093
		});
2094
	}
2095
});
2096
 
2097
// Add language
2098
extend(defaultOptions.lang, {
2099
	zoomIn: 'Zoom in',
2100
	zoomOut: 'Zoom out'
2101
});
2102
 
2103
 
2104
// Set the default map navigation options
2105
defaultOptions.mapNavigation = {
2106
	buttonOptions: {
2107
		alignTo: 'plotBox',
2108
		align: 'left',
2109
		verticalAlign: 'top',
2110
		x: 0,
2111
		width: 18,
2112
		height: 18,
2113
		style: {
2114
			fontSize: '15px',
2115
			fontWeight: 'bold',
2116
			textAlign: 'center'
2117
		},
2118
		theme: {
2119
			'stroke-width': 1
2120
		}
2121
	},
2122
	buttons: {
2123
		zoomIn: {
2124
			onclick: function () {
2125
				this.mapZoom(0.5);
2126
			},
2127
			text: '+',
2128
			y: 0
2129
		},
2130
		zoomOut: {
2131
			onclick: function () {
2132
				this.mapZoom(2);
2133
			},
2134
			text: '-',
2135
			y: 28
2136
		}
2137
	}
2138
	// enabled: false,
2139
	// enableButtons: null, // inherit from enabled
2140
	// enableTouchZoom: null, // inherit from enabled
2141
	// enableDoubleClickZoom: null, // inherit from enabled
2142
	// enableDoubleClickZoomTo: false
2143
	// enableMouseWheelZoom: null, // inherit from enabled
2144
};
2145
 
2146
/**
2147
 * Utility for reading SVG paths directly.
2148
 */
2149
Highcharts.splitPath = function (path) {
2150
	var i;
2151
 
2152
	// Move letters apart
2153
	path = path.replace(/([A-Za-z])/g, ' $1 ');
2154
	// Trim
2155
	path = path.replace(/^\s*/, "").replace(/\s*$/, "");
2156
 
2157
	// Split on spaces and commas
2158
	path = path.split(/[ ,]+/);
2159
 
2160
	// Parse numbers
2161
	for (i = 0; i < path.length; i++) {
2162
		if (!/[a-zA-Z]/.test(path[i])) {
2163
			path[i] = parseFloat(path[i]);
2164
		}
2165
	}
2166
	return path;
2167
};
2168
 
2169
// A placeholder for map definitions
2170
Highcharts.maps = {};
2171
 
2172
 
2173
 
2174
 
2175
 
2176
// Create symbols for the zoom buttons
2177
function selectiveRoundedRect(x, y, w, h, rTopLeft, rTopRight, rBottomRight, rBottomLeft) {
2178
	return ['M', x + rTopLeft, y,
2179
        // top side
2180
        'L', x + w - rTopRight, y,
2181
        // top right corner
2182
        'C', x + w - rTopRight / 2, y, x + w, y + rTopRight / 2, x + w, y + rTopRight,
2183
        // right side
2184
        'L', x + w, y + h - rBottomRight,
2185
        // bottom right corner
2186
        'C', x + w, y + h - rBottomRight / 2, x + w - rBottomRight / 2, y + h, x + w - rBottomRight, y + h,
2187
        // bottom side
2188
        'L', x + rBottomLeft, y + h,
2189
        // bottom left corner
2190
        'C', x + rBottomLeft / 2, y + h, x, y + h - rBottomLeft / 2, x, y + h - rBottomLeft,
2191
        // left side
2192
        'L', x, y + rTopLeft,
2193
        // top left corner
2194
        'C', x, y + rTopLeft / 2, x + rTopLeft / 2, y, x + rTopLeft, y,
2195
        'Z'
2196
    ];
2197
}
2198
SVGRenderer.prototype.symbols.topbutton = function (x, y, w, h, attr) {
2199
	return selectiveRoundedRect(x - 1, y - 1, w, h, attr.r, attr.r, 0, 0);
2200
};
2201
SVGRenderer.prototype.symbols.bottombutton = function (x, y, w, h, attr) {
2202
	return selectiveRoundedRect(x - 1, y - 1, w, h, 0, 0, attr.r, attr.r);
2203
};
2204
// The symbol callbacks are generated on the SVGRenderer object in all browsers. Even
2205
// VML browsers need this in order to generate shapes in export. Now share
2206
// them with the VMLRenderer.
2207
if (Renderer === VMLRenderer) {
2208
	each(['topbutton', 'bottombutton'], function (shape) {
2209
		VMLRenderer.prototype.symbols[shape] = SVGRenderer.prototype.symbols[shape];
2210
	});
2211
}
2212
 
2213
 
2214
/**
2215
 * A wrapper for Chart with all the default values for a Map
2216
 */
2217
Highcharts.Map = function (options, callback) {
2218
 
2219
	var hiddenAxis = {
2220
			endOnTick: false,
2221
			gridLineWidth: 0,
2222
			lineWidth: 0,
2223
			minPadding: 0,
2224
			maxPadding: 0,
2225
			startOnTick: false,
2226
			title: null,
2227
			tickPositions: []
2228
		},
2229
		seriesOptions;
2230
 
2231
	/* For visual testing
2232
	hiddenAxis.gridLineWidth = 1;
2233
	hiddenAxis.gridZIndex = 10;
2234
	hiddenAxis.tickPositions = undefined;
2235
	// */
2236
 
2237
	// Don't merge the data
2238
	seriesOptions = options.series;
2239
	options.series = null;
2240
 
2241
	options = merge({
2242
		chart: {
2243
			panning: 'xy',
2244
			type: 'map'
2245
		},
2246
		xAxis: hiddenAxis,
2247
		yAxis: merge(hiddenAxis, { reversed: true })
2248
	},
2249
	options, // user's options
2250
 
2251
	{ // forced options
2252
		chart: {
2253
			inverted: false,
2254
			alignTicks: false
2255
		}
2256
	});
2257
 
2258
	options.series = seriesOptions;
2259
 
2260
 
2261
	return new Chart(options, callback);
2262
};
2263
 
2264
}(Highcharts));