You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

2265 line
58 KiB

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