纽威
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.

558 lines
14 KiB

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