map.src.js 25 KB

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