drilldown.src.js 15 KB

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