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.

432 lines
14 KiB

2 years ago
  1. /**
  2. * Highcharts Drilldown plugin
  3. *
  4. * Author: Torstein Honsi
  5. * Last revision: 2013-02-18
  6. * License: MIT License
  7. *
  8. * Demo: http://jsfiddle.net/highcharts/Vf3yT/
  9. */
  10. /*global HighchartsAdapter*/
  11. (function (H) {
  12. "use strict";
  13. var noop = function () { },
  14. defaultOptions = H.getOptions(),
  15. each = H.each,
  16. extend = H.extend,
  17. wrap = H.wrap,
  18. Chart = H.Chart,
  19. seriesTypes = H.seriesTypes,
  20. PieSeries = seriesTypes.pie,
  21. ColumnSeries = seriesTypes.column,
  22. fireEvent = HighchartsAdapter.fireEvent;
  23. // Utilities
  24. function tweenColors(startColor, endColor, pos) {
  25. var rgba = [
  26. Math.round(startColor[0] + (endColor[0] - startColor[0]) * pos),
  27. Math.round(startColor[1] + (endColor[1] - startColor[1]) * pos),
  28. Math.round(startColor[2] + (endColor[2] - startColor[2]) * pos),
  29. startColor[3] + (endColor[3] - startColor[3]) * pos
  30. ];
  31. return 'rgba(' + rgba.join(',') + ')';
  32. }
  33. // Add language
  34. extend(defaultOptions.lang, {
  35. drillUpText: '◁ Back to {series.name}'
  36. });
  37. defaultOptions.drilldown = {
  38. activeAxisLabelStyle: {
  39. cursor: 'pointer',
  40. color: '#039',
  41. fontWeight: 'bold',
  42. textDecoration: 'underline'
  43. },
  44. activeDataLabelStyle: {
  45. cursor: 'pointer',
  46. color: '#039',
  47. fontWeight: 'bold',
  48. textDecoration: 'underline'
  49. },
  50. animation: {
  51. duration: 500
  52. },
  53. drillUpButton: {
  54. position: {
  55. align: 'right',
  56. x: -10,
  57. y: 10
  58. }
  59. // relativeTo: 'plotBox'
  60. // theme
  61. }
  62. };
  63. /**
  64. * A general fadeIn method
  65. */
  66. H.SVGRenderer.prototype.Element.prototype.fadeIn = function () {
  67. this
  68. .attr({
  69. opacity: 0.1,
  70. visibility: 'visible'
  71. })
  72. .animate({
  73. opacity: 1
  74. }, {
  75. duration: 250
  76. });
  77. };
  78. // Extend the Chart prototype
  79. Chart.prototype.drilldownLevels = [];
  80. Chart.prototype.addSeriesAsDrilldown = function (point, ddOptions) {
  81. var oldSeries = point.series,
  82. xAxis = oldSeries.xAxis,
  83. yAxis = oldSeries.yAxis,
  84. newSeries,
  85. color = point.color || oldSeries.color,
  86. pointIndex,
  87. level;
  88. ddOptions = extend({
  89. color: color
  90. }, ddOptions);
  91. pointIndex = HighchartsAdapter.inArray(this, oldSeries.points);
  92. level = {
  93. seriesOptions: oldSeries.userOptions,
  94. shapeArgs: point.shapeArgs,
  95. bBox: point.graphic.getBBox(),
  96. color: color,
  97. newSeries: ddOptions,
  98. pointOptions: oldSeries.options.data[pointIndex],
  99. pointIndex: pointIndex,
  100. oldExtremes: {
  101. xMin: xAxis && xAxis.userMin,
  102. xMax: xAxis && xAxis.userMax,
  103. yMin: yAxis && yAxis.userMin,
  104. yMax: yAxis && yAxis.userMax
  105. }
  106. };
  107. this.drilldownLevels.push(level);
  108. newSeries = this.addSeries(ddOptions, false);
  109. if (xAxis) {
  110. xAxis.oldPos = xAxis.pos;
  111. xAxis.userMin = xAxis.userMax = null;
  112. yAxis.userMin = yAxis.userMax = null;
  113. }
  114. // Run fancy cross-animation on supported and equal types
  115. if (oldSeries.type === newSeries.type) {
  116. newSeries.animate = newSeries.animateDrilldown || noop;
  117. newSeries.options.animation = true;
  118. }
  119. oldSeries.remove(false);
  120. this.redraw();
  121. this.showDrillUpButton();
  122. };
  123. Chart.prototype.getDrilldownBackText = function () {
  124. var lastLevel = this.drilldownLevels[this.drilldownLevels.length - 1];
  125. return this.options.lang.drillUpText.replace('{series.name}', lastLevel.seriesOptions.name);
  126. };
  127. Chart.prototype.showDrillUpButton = function () {
  128. var chart = this,
  129. backText = this.getDrilldownBackText(),
  130. buttonOptions = chart.options.drilldown.drillUpButton;
  131. if (!this.drillUpButton) {
  132. this.drillUpButton = this.renderer.button(
  133. backText,
  134. null,
  135. null,
  136. function () {
  137. chart.drillUp();
  138. }
  139. )
  140. .attr(extend({
  141. align: buttonOptions.position.align,
  142. zIndex: 9
  143. }, buttonOptions.theme))
  144. .add()
  145. .align(buttonOptions.position, false, buttonOptions.relativeTo || 'plotBox');
  146. } else {
  147. this.drillUpButton.attr({
  148. text: backText
  149. })
  150. .align();
  151. }
  152. };
  153. Chart.prototype.drillUp = function () {
  154. var chart = this,
  155. level = chart.drilldownLevels.pop(),
  156. oldSeries = chart.series[0],
  157. oldExtremes = level.oldExtremes,
  158. newSeries = chart.addSeries(level.seriesOptions, false);
  159. fireEvent(chart, 'drillup', { seriesOptions: level.seriesOptions });
  160. if (newSeries.type === oldSeries.type) {
  161. newSeries.drilldownLevel = level;
  162. newSeries.animate = newSeries.animateDrillupTo || noop;
  163. newSeries.options.animation = true;
  164. if (oldSeries.animateDrillupFrom) {
  165. oldSeries.animateDrillupFrom(level);
  166. }
  167. }
  168. oldSeries.remove(false);
  169. // Reset the zoom level of the upper series
  170. if (newSeries.xAxis) {
  171. newSeries.xAxis.setExtremes(oldExtremes.xMin, oldExtremes.xMax, false);
  172. newSeries.yAxis.setExtremes(oldExtremes.yMin, oldExtremes.yMax, false);
  173. }
  174. this.redraw();
  175. if (this.drilldownLevels.length === 0) {
  176. this.drillUpButton = this.drillUpButton.destroy();
  177. } else {
  178. this.drillUpButton.attr({
  179. text: this.getDrilldownBackText()
  180. })
  181. .align();
  182. }
  183. };
  184. PieSeries.prototype.animateDrilldown = function (init) {
  185. var level = this.chart.drilldownLevels[this.chart.drilldownLevels.length - 1],
  186. animationOptions = this.chart.options.drilldown.animation,
  187. animateFrom = level.shapeArgs,
  188. start = animateFrom.start,
  189. angle = animateFrom.end - start,
  190. startAngle = angle / this.points.length,
  191. startColor = H.Color(level.color).rgba;
  192. if (!init) {
  193. each(this.points, function (point, i) {
  194. var endColor = H.Color(point.color).rgba;
  195. /*jslint unparam: true*/
  196. point.graphic
  197. .attr(H.merge(animateFrom, {
  198. start: start + i * startAngle,
  199. end: start + (i + 1) * startAngle
  200. }))
  201. .animate(point.shapeArgs, H.merge(animationOptions, {
  202. step: function (val, fx) {
  203. if (fx.prop === 'start') {
  204. this.attr({
  205. fill: tweenColors(startColor, endColor, fx.pos)
  206. });
  207. }
  208. }
  209. }));
  210. /*jslint unparam: false*/
  211. });
  212. }
  213. };
  214. /**
  215. * When drilling up, keep the upper series invisible until the lower series has
  216. * moved into place
  217. */
  218. PieSeries.prototype.animateDrillupTo =
  219. ColumnSeries.prototype.animateDrillupTo = function (init) {
  220. if (!init) {
  221. var newSeries = this,
  222. level = newSeries.drilldownLevel;
  223. each(this.points, function (point) {
  224. point.graphic.hide();
  225. if (point.dataLabel) {
  226. point.dataLabel.hide();
  227. }
  228. if (point.connector) {
  229. point.connector.hide();
  230. }
  231. });
  232. // Do dummy animation on first point to get to complete
  233. setTimeout(function () {
  234. each(newSeries.points, function (point, i) {
  235. // Fade in other points
  236. var verb = i === level.pointIndex ? 'show' : 'fadeIn';
  237. point.graphic[verb]();
  238. if (point.dataLabel) {
  239. point.dataLabel[verb]();
  240. }
  241. if (point.connector) {
  242. point.connector[verb]();
  243. }
  244. });
  245. }, Math.max(this.chart.options.drilldown.animation.duration - 50, 0));
  246. // Reset
  247. this.animate = noop;
  248. }
  249. };
  250. ColumnSeries.prototype.animateDrilldown = function (init) {
  251. var animateFrom = this.chart.drilldownLevels[this.chart.drilldownLevels.length - 1].shapeArgs,
  252. animationOptions = this.chart.options.drilldown.animation;
  253. if (!init) {
  254. animateFrom.x += (this.xAxis.oldPos - this.xAxis.pos);
  255. each(this.points, function (point) {
  256. point.graphic
  257. .attr(animateFrom)
  258. .animate(point.shapeArgs, animationOptions);
  259. });
  260. }
  261. };
  262. /**
  263. * When drilling up, pull out the individual point graphics from the lower series
  264. * and animate them into the origin point in the upper series.
  265. */
  266. ColumnSeries.prototype.animateDrillupFrom =
  267. PieSeries.prototype.animateDrillupFrom =
  268. function (level) {
  269. var animationOptions = this.chart.options.drilldown.animation,
  270. group = this.group;
  271. delete this.group;
  272. each(this.points, function (point) {
  273. var graphic = point.graphic,
  274. startColor = H.Color(point.color).rgba;
  275. delete point.graphic;
  276. /*jslint unparam: true*/
  277. graphic.animate(level.shapeArgs, H.merge(animationOptions, {
  278. step: function (val, fx) {
  279. if (fx.prop === 'start') {
  280. this.attr({
  281. fill: tweenColors(startColor, H.Color(level.color).rgba, fx.pos)
  282. });
  283. }
  284. },
  285. complete: function () {
  286. graphic.destroy();
  287. if (group) {
  288. group = group.destroy();
  289. }
  290. }
  291. }));
  292. /*jslint unparam: false*/
  293. });
  294. };
  295. H.Point.prototype.doDrilldown = function () {
  296. var series = this.series,
  297. chart = series.chart,
  298. drilldown = chart.options.drilldown,
  299. i = drilldown.series.length,
  300. seriesOptions;
  301. while (i-- && !seriesOptions) {
  302. if (drilldown.series[i].id === this.drilldown) {
  303. seriesOptions = drilldown.series[i];
  304. }
  305. }
  306. // Fire the event. If seriesOptions is undefined, the implementer can check for
  307. // seriesOptions, and call addSeriesAsDrilldown async if necessary.
  308. fireEvent(chart, 'drilldown', {
  309. point: this,
  310. seriesOptions: seriesOptions
  311. });
  312. if (seriesOptions) {
  313. chart.addSeriesAsDrilldown(this, seriesOptions);
  314. }
  315. };
  316. wrap(H.Point.prototype, 'init', function (proceed, series, options, x) {
  317. var point = proceed.call(this, series, options, x),
  318. chart = series.chart,
  319. tick = series.xAxis && series.xAxis.ticks[x],
  320. tickLabel = tick && tick.label;
  321. if (point.drilldown) {
  322. // Add the click event to the point label
  323. H.addEvent(point, 'click', function () {
  324. point.doDrilldown();
  325. });
  326. // Make axis labels clickable
  327. if (tickLabel) {
  328. if (!tickLabel._basicStyle) {
  329. tickLabel._basicStyle = tickLabel.element.getAttribute('style');
  330. }
  331. tickLabel
  332. .addClass('highcharts-drilldown-axis-label')
  333. .css(chart.options.drilldown.activeAxisLabelStyle)
  334. .on('click', function () {
  335. if (point.doDrilldown) {
  336. point.doDrilldown();
  337. }
  338. });
  339. }
  340. } else if (tickLabel && tickLabel._basicStyle) {
  341. tickLabel.element.setAttribute('style', tickLabel._basicStyle);
  342. }
  343. return point;
  344. });
  345. wrap(H.Series.prototype, 'drawDataLabels', function (proceed) {
  346. var css = this.chart.options.drilldown.activeDataLabelStyle;
  347. proceed.call(this);
  348. each(this.points, function (point) {
  349. if (point.drilldown && point.dataLabel) {
  350. point.dataLabel
  351. .attr({
  352. 'class': 'highcharts-drilldown-data-label'
  353. })
  354. .css(css)
  355. .on('click', function () {
  356. point.doDrilldown();
  357. });
  358. }
  359. });
  360. });
  361. // Mark the trackers with a pointer
  362. ColumnSeries.prototype.supportsDrilldown = true;
  363. PieSeries.prototype.supportsDrilldown = true;
  364. var type,
  365. drawTrackerWrapper = function (proceed) {
  366. proceed.call(this);
  367. each(this.points, function (point) {
  368. if (point.drilldown && point.graphic) {
  369. point.graphic
  370. .attr({
  371. 'class': 'highcharts-drilldown-point'
  372. })
  373. .css({ cursor: 'pointer' });
  374. }
  375. });
  376. };
  377. for (type in seriesTypes) {
  378. if (seriesTypes[type].prototype.supportsDrilldown) {
  379. wrap(seriesTypes[type].prototype, 'drawTracker', drawTrackerWrapper);
  380. }
  381. }
  382. }(Highcharts));