map.src.js 33 KB

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