TreeGrid.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984
  1. /* *
  2. * (c) 2016 Highsoft AS
  3. * Authors: Jon Arild Nygard
  4. *
  5. * License: www.highcharts.com/license
  6. */
  7. /* eslint no-console: 0 */
  8. 'use strict';
  9. import H from '../parts/Globals.js';
  10. import '../parts/Utilities.js';
  11. import './GridAxis.js';
  12. import Tree from './Tree.js';
  13. import mixinTreeSeries from '../mixins/tree-series.js';
  14. import '../modules/broken-axis.src.js';
  15. var argsToArray = function (args) {
  16. return Array.prototype.slice.call(args, 1);
  17. },
  18. defined = H.defined,
  19. extend = H.extend,
  20. find = H.find,
  21. fireEvent = H.fireEvent,
  22. getLevelOptions = mixinTreeSeries.getLevelOptions,
  23. merge = H.merge,
  24. isBoolean = function (x) {
  25. return typeof x === 'boolean';
  26. },
  27. isNumber = H.isNumber,
  28. isObject = function (x) {
  29. // Always use strict mode.
  30. return H.isObject(x, true);
  31. },
  32. isString = H.isString,
  33. pick = H.pick,
  34. wrap = H.wrap,
  35. GridAxis = H.Axis,
  36. GridAxisTick = H.Tick;
  37. var override = function (obj, methods) {
  38. var method,
  39. func;
  40. for (method in methods) {
  41. if (methods.hasOwnProperty(method)) {
  42. func = methods[method];
  43. wrap(obj, method, func);
  44. }
  45. }
  46. };
  47. var getBreakFromNode = function (node, max) {
  48. var from = node.collapseStart,
  49. to = node.collapseEnd;
  50. // In broken-axis, the axis.max is minimized until it is not within a break.
  51. // Therefore, if break.to is larger than axis.max, the axis.to should not
  52. // add the 0.5 axis.tickMarkOffset, to avoid adding a break larger than
  53. // axis.max
  54. // TODO consider simplifying broken-axis and this might solve itself
  55. if (to >= max) {
  56. from -= 0.5;
  57. }
  58. return {
  59. from: from,
  60. to: to,
  61. showPoints: false
  62. };
  63. };
  64. /**
  65. * Creates a list of positions for the ticks on the axis. Filters out positions
  66. * that are outside min and max, or is inside an axis break.
  67. *
  68. * @private
  69. * @function getTickPositions
  70. *
  71. * @param {Highcharts.Axis} axis
  72. * The Axis to get the tick positions from.
  73. *
  74. * @return {Array<number>}
  75. * List of positions.
  76. */
  77. var getTickPositions = function (axis) {
  78. return Object.keys(axis.mapOfPosToGridNode).reduce(
  79. function (arr, key) {
  80. var pos = +key;
  81. if (
  82. axis.min <= pos &&
  83. axis.max >= pos &&
  84. !axis.isInAnyBreak(pos)
  85. ) {
  86. arr.push(pos);
  87. }
  88. return arr;
  89. },
  90. []
  91. );
  92. };
  93. /**
  94. * Check if a node is collapsed.
  95. *
  96. * @private
  97. * @function isCollapsed
  98. *
  99. * @param {Highcharts.Axis} axis
  100. * The axis to check against.
  101. *
  102. * @param {object} node
  103. * The node to check if is collapsed.
  104. *
  105. * @param {number} pos
  106. * The tick position to collapse.
  107. *
  108. * @return {boolean}
  109. * Returns true if collapsed, false if expanded.
  110. */
  111. var isCollapsed = function (axis, node) {
  112. var breaks = (axis.options.breaks || []),
  113. obj = getBreakFromNode(node, axis.max);
  114. return breaks.some(function (b) {
  115. return b.from === obj.from && b.to === obj.to;
  116. });
  117. };
  118. /**
  119. * Calculates the new axis breaks to collapse a node.
  120. *
  121. * @private
  122. * @function collapse
  123. *
  124. * @param {Highcharts.Axis} axis
  125. * The axis to check against.
  126. *
  127. * @param {object} node
  128. * The node to collapse.
  129. *
  130. * @param {number} pos
  131. * The tick position to collapse.
  132. *
  133. * @return {Array<object>}
  134. * Returns an array of the new breaks for the axis.
  135. */
  136. var collapse = function (axis, node) {
  137. var breaks = (axis.options.breaks || []),
  138. obj = getBreakFromNode(node, axis.max);
  139. breaks.push(obj);
  140. return breaks;
  141. };
  142. /**
  143. * Calculates the new axis breaks to expand a node.
  144. *
  145. * @private
  146. * @function expand
  147. *
  148. * @param {Highcharts.Axis} axis
  149. * The axis to check against.
  150. *
  151. * @param {object} node
  152. * The node to expand.
  153. *
  154. * @param {number} pos
  155. * The tick position to expand.
  156. *
  157. * @returns {Array<object>} Returns an array of the new breaks for the axis.
  158. */
  159. var expand = function (axis, node) {
  160. var breaks = (axis.options.breaks || []),
  161. obj = getBreakFromNode(node, axis.max);
  162. // Remove the break from the axis breaks array.
  163. return breaks.reduce(function (arr, b) {
  164. if (b.to !== obj.to || b.from !== obj.from) {
  165. arr.push(b);
  166. }
  167. return arr;
  168. }, []);
  169. };
  170. /**
  171. * Calculates the new axis breaks after toggling the collapse/expand state of a
  172. * node. If it is collapsed it will be expanded, and if it is exapended it will
  173. * be collapsed.
  174. *
  175. * @private
  176. * @function toggleCollapse
  177. *
  178. * @param {Highcharts.Axis} axis
  179. * The axis to check against.
  180. *
  181. * @param {object} node
  182. * The node to toggle.
  183. *
  184. * @param {number} pos
  185. * The tick position to toggle.
  186. *
  187. * @return {Array<object>}
  188. * Returns an array of the new breaks for the axis.
  189. */
  190. var toggleCollapse = function (axis, node) {
  191. return (
  192. isCollapsed(axis, node) ?
  193. expand(axis, node) :
  194. collapse(axis, node)
  195. );
  196. };
  197. var renderLabelIcon = function (tick, params) {
  198. var icon = tick.labelIcon,
  199. isNew = !icon,
  200. renderer = params.renderer,
  201. labelBox = params.xy,
  202. options = params.options,
  203. width = options.width,
  204. height = options.height,
  205. iconCenter = {
  206. x: labelBox.x - (width / 2) - options.padding,
  207. y: labelBox.y - (height / 2)
  208. },
  209. rotation = params.collapsed ? 90 : 180,
  210. shouldRender = params.show && H.isNumber(iconCenter.y);
  211. if (isNew) {
  212. tick.labelIcon = icon = renderer.path(renderer.symbols[options.type](
  213. options.x,
  214. options.y,
  215. width,
  216. height
  217. ))
  218. .addClass('highcharts-label-icon')
  219. .add(params.group);
  220. }
  221. // Set the new position, and show or hide
  222. if (!shouldRender) {
  223. icon.attr({ y: -9999 }); // #1338
  224. }
  225. // Presentational attributes
  226. if (!renderer.styledMode) {
  227. icon
  228. .attr({
  229. 'stroke-width': 1,
  230. 'fill': pick(params.color, '#666666')
  231. })
  232. .css({
  233. cursor: 'pointer',
  234. stroke: options.lineColor,
  235. strokeWidth: options.lineWidth
  236. });
  237. }
  238. // Update the icon positions
  239. icon[isNew ? 'attr' : 'animate']({
  240. translateX: iconCenter.x,
  241. translateY: iconCenter.y,
  242. rotation: rotation
  243. });
  244. };
  245. var onTickHover = function (label) {
  246. label.addClass('highcharts-treegrid-node-active');
  247. if (!label.renderer.styledMode) {
  248. label.css({
  249. textDecoration: 'underline'
  250. });
  251. }
  252. };
  253. var onTickHoverExit = function (label, options) {
  254. var css = defined(options.style) ? options.style : {};
  255. label.removeClass('highcharts-treegrid-node-active');
  256. if (!label.renderer.styledMode) {
  257. label.css({
  258. textDecoration: css.textDecoration
  259. });
  260. }
  261. };
  262. /**
  263. * Creates a tree structure of the data, and the treegrid. Calculates
  264. * categories, and y-values of points based on the tree.
  265. *
  266. * @private
  267. * @function getTreeGridFromData
  268. *
  269. * @param {Array<*>} data
  270. * All the data points to display in the axis.
  271. *
  272. * @param {boolean} uniqueNames
  273. * Wether or not the data node with the same name should share grid cell.
  274. * If true they do share cell. False by default.
  275. *
  276. * @return {object}
  277. * Returns an object containing categories, mapOfIdToNode,
  278. * mapOfPosToGridNode, and tree.
  279. *
  280. * @todo There should be only one point per line.
  281. * @todo It should be optional to have one category per point, or merge cells
  282. * @todo Add unit-tests.
  283. */
  284. var getTreeGridFromData = function (data, uniqueNames, numberOfSeries) {
  285. var categories = [],
  286. collapsedNodes = [],
  287. mapOfIdToNode = {},
  288. mapOfPosToGridNode = {},
  289. posIterator = -1,
  290. uniqueNamesEnabled = isBoolean(uniqueNames) ? uniqueNames : false,
  291. tree,
  292. treeParams,
  293. updateYValuesAndTickPos;
  294. // Build the tree from the series data.
  295. treeParams = {
  296. // After the children has been created.
  297. after: function (node) {
  298. var gridNode = mapOfPosToGridNode[node.pos],
  299. height = 0,
  300. descendants = 0;
  301. gridNode.children.forEach(function (child) {
  302. descendants += child.descendants + 1;
  303. height = Math.max(child.height + 1, height);
  304. });
  305. gridNode.descendants = descendants;
  306. gridNode.height = height;
  307. if (gridNode.collapsed) {
  308. collapsedNodes.push(gridNode);
  309. }
  310. },
  311. // Before the children has been created.
  312. before: function (node) {
  313. var data = isObject(node.data) ? node.data : {},
  314. name = isString(data.name) ? data.name : '',
  315. parentNode = mapOfIdToNode[node.parent],
  316. parentGridNode = (
  317. isObject(parentNode) ?
  318. mapOfPosToGridNode[parentNode.pos] :
  319. null
  320. ),
  321. hasSameName = function (x) {
  322. return x.name === name;
  323. },
  324. gridNode,
  325. pos;
  326. // If not unique names, look for a sibling node with the same name.
  327. if (
  328. uniqueNamesEnabled &&
  329. isObject(parentGridNode) &&
  330. !!(gridNode = find(parentGridNode.children, hasSameName))
  331. ) {
  332. // If if there is a gridNode with the same name, reuse position.
  333. pos = gridNode.pos;
  334. // Add data node to list of nodes in the grid node.
  335. gridNode.nodes.push(node);
  336. } else {
  337. // If it is a new grid node, increment position.
  338. pos = posIterator++;
  339. }
  340. // Add new grid node to map.
  341. if (!mapOfPosToGridNode[pos]) {
  342. mapOfPosToGridNode[pos] = gridNode = {
  343. depth: parentGridNode ? parentGridNode.depth + 1 : 0,
  344. name: name,
  345. nodes: [node],
  346. children: [],
  347. pos: pos
  348. };
  349. // If not root, then add name to categories.
  350. if (pos !== -1) {
  351. categories.push(name);
  352. }
  353. // Add name to list of children.
  354. if (isObject(parentGridNode)) {
  355. parentGridNode.children.push(gridNode);
  356. }
  357. }
  358. // Add data node to map
  359. if (isString(node.id)) {
  360. mapOfIdToNode[node.id] = node;
  361. }
  362. // If one of the points are collapsed, then start the grid node in
  363. // collapsed state.
  364. if (data.collapsed === true) {
  365. gridNode.collapsed = true;
  366. }
  367. // Assign pos to data node
  368. node.pos = pos;
  369. }
  370. };
  371. updateYValuesAndTickPos = function (map, numberOfSeries) {
  372. var setValues = function (gridNode, start, result) {
  373. var nodes = gridNode.nodes,
  374. end = start + (start === -1 ? 0 : numberOfSeries - 1),
  375. diff = (end - start) / 2,
  376. padding = 0.5,
  377. pos = start + diff;
  378. nodes.forEach(function (node) {
  379. var data = node.data;
  380. if (isObject(data)) {
  381. // Update point
  382. data.y = start + data.seriesIndex;
  383. // Remove the property once used
  384. delete data.seriesIndex;
  385. }
  386. node.pos = pos;
  387. });
  388. result[pos] = gridNode;
  389. gridNode.pos = pos;
  390. gridNode.tickmarkOffset = diff + padding;
  391. gridNode.collapseStart = end + padding;
  392. gridNode.children.forEach(function (child) {
  393. setValues(child, end + 1, result);
  394. end = child.collapseEnd - padding;
  395. });
  396. // Set collapseEnd to the end of the last child node.
  397. gridNode.collapseEnd = end + padding;
  398. return result;
  399. };
  400. return setValues(map['-1'], -1, {});
  401. };
  402. // Create tree from data
  403. tree = Tree.getTree(data, treeParams);
  404. // Update y values of data, and set calculate tick positions.
  405. mapOfPosToGridNode = updateYValuesAndTickPos(
  406. mapOfPosToGridNode,
  407. numberOfSeries
  408. );
  409. // Return the resulting data.
  410. return {
  411. categories: categories,
  412. mapOfIdToNode: mapOfIdToNode,
  413. mapOfPosToGridNode: mapOfPosToGridNode,
  414. collapsedNodes: collapsedNodes,
  415. tree: tree
  416. };
  417. };
  418. H.addEvent(H.Chart, 'beforeRender', function () {
  419. this.axes.forEach(function (axis) {
  420. if (axis.userOptions.type === 'treegrid') {
  421. var labelOptions = axis.options && axis.options.labels,
  422. removeFoundExtremesEvent;
  423. // beforeRender is fired after all the series is initialized,
  424. // which is an ideal time to update the axis.categories.
  425. axis.updateYNames();
  426. // Update yData now that we have calculated the y values
  427. // TODO: it would be better to be able to calculate y values
  428. // before Series.setData
  429. axis.series.forEach(function (series) {
  430. series.yData = series.options.data.map(function (data) {
  431. return data.y;
  432. });
  433. });
  434. // Calculate the label options for each level in the tree.
  435. axis.mapOptionsToLevel = getLevelOptions({
  436. defaults: labelOptions,
  437. from: 1,
  438. levels: labelOptions.levels,
  439. to: axis.tree.height
  440. });
  441. // Collapse all the nodes belonging to a point where collapsed
  442. // equals true.
  443. // Can be called from beforeRender, if getBreakFromNode removes
  444. // its dependency on axis.max.
  445. removeFoundExtremesEvent =
  446. H.addEvent(axis, 'foundExtremes', function () {
  447. axis.collapsedNodes.forEach(function (node) {
  448. var breaks = collapse(axis, node);
  449. axis.setBreaks(breaks, false);
  450. });
  451. removeFoundExtremesEvent();
  452. });
  453. }
  454. });
  455. });
  456. override(GridAxis.prototype, {
  457. init: function (proceed, chart, userOptions) {
  458. var axis = this,
  459. isTreeGrid = userOptions.type === 'treegrid';
  460. // Set default and forced options for TreeGrid
  461. if (isTreeGrid) {
  462. userOptions = merge({
  463. // Default options
  464. grid: {
  465. enabled: true
  466. },
  467. // TODO: add support for align in treegrid.
  468. labels: {
  469. align: 'left',
  470. /**
  471. * Set options on specific levels in a tree grid axis. Takes
  472. * precedence over labels options.
  473. *
  474. * @sample {gantt} gantt/treegrid-axis/labels-levels
  475. * Levels on TreeGrid Labels
  476. *
  477. * @type {Array<*>}
  478. * @product gantt
  479. * @apioption yAxis.labels.levels
  480. */
  481. levels: [{
  482. /**
  483. * Specify the level which the options within this object
  484. * applies to.
  485. *
  486. * @sample {gantt} gantt/treegrid-axis/labels-levels
  487. *
  488. * @type {number}
  489. * @product gantt
  490. * @apioption yAxis.labels.levels.level
  491. */
  492. level: undefined
  493. }, {
  494. level: 1,
  495. /**
  496. * @type {Highcharts.CSSObject}
  497. * @product gantt
  498. * @apioption yAxis.labels.levels.style
  499. */
  500. style: {
  501. /** @ignore-option */
  502. fontWeight: 'bold'
  503. }
  504. }],
  505. /**
  506. * The symbol for the collapse and expand icon in a
  507. * treegrid.
  508. *
  509. * @product gantt
  510. * @optionparent yAxis.labels.symbol
  511. */
  512. symbol: {
  513. /**
  514. * The symbol type. Points to a definition function in
  515. * the `Highcharts.Renderer.symbols` collection.
  516. *
  517. * @validvalue ["arc", "circle", "diamond", "square", "triangle", "triangle-down"]
  518. */
  519. type: 'triangle',
  520. x: -5,
  521. y: -5,
  522. height: 10,
  523. width: 10,
  524. padding: 5
  525. }
  526. },
  527. uniqueNames: false
  528. }, userOptions, { // User options
  529. // Forced options
  530. reversed: true,
  531. // grid.columns is not supported in treegrid
  532. grid: {
  533. columns: undefined
  534. }
  535. });
  536. }
  537. // Now apply the original function with the original arguments,
  538. // which are sliced off this function's arguments
  539. proceed.apply(axis, [chart, userOptions]);
  540. if (isTreeGrid) {
  541. axis.hasNames = true;
  542. axis.options.showLastLabel = true;
  543. }
  544. },
  545. /**
  546. * Override to add indentation to axis.maxLabelDimensions.
  547. *
  548. * @private
  549. * @function Highcharts.GridAxis#getMaxLabelDimensions
  550. *
  551. * @param {Function} proceed
  552. * The original function
  553. */
  554. getMaxLabelDimensions: function (proceed) {
  555. var axis = this,
  556. options = axis.options,
  557. labelOptions = options && options.labels,
  558. indentation = (
  559. labelOptions && isNumber(labelOptions.indentation) ?
  560. options.labels.indentation :
  561. 0
  562. ),
  563. retVal = proceed.apply(axis, argsToArray(arguments)),
  564. isTreeGrid = axis.options.type === 'treegrid',
  565. treeDepth;
  566. if (isTreeGrid && this.mapOfPosToGridNode) {
  567. treeDepth = axis.mapOfPosToGridNode[-1].height;
  568. retVal.width += indentation * (treeDepth - 1);
  569. }
  570. return retVal;
  571. },
  572. /**
  573. * Generates a tick for initial positioning.
  574. *
  575. * @private
  576. * @function Highcharts.GridAxis#generateTick
  577. *
  578. * @param {Function} proceed
  579. * The original generateTick function.
  580. *
  581. * @param {number} pos
  582. * The tick position in axis values.
  583. */
  584. generateTick: function (proceed, pos) {
  585. var axis = this,
  586. mapOptionsToLevel = (
  587. isObject(axis.mapOptionsToLevel) ? axis.mapOptionsToLevel : {}
  588. ),
  589. isTreeGrid = axis.options.type === 'treegrid',
  590. ticks = axis.ticks,
  591. tick = ticks[pos],
  592. levelOptions,
  593. options,
  594. gridNode;
  595. if (isTreeGrid) {
  596. gridNode = axis.mapOfPosToGridNode[pos];
  597. levelOptions = mapOptionsToLevel[gridNode.depth];
  598. if (levelOptions) {
  599. options = {
  600. labels: levelOptions
  601. };
  602. }
  603. if (!tick) {
  604. ticks[pos] = tick =
  605. new GridAxisTick(axis, pos, null, undefined, {
  606. category: gridNode.name,
  607. tickmarkOffset: gridNode.tickmarkOffset,
  608. options: options
  609. });
  610. } else {
  611. // update labels depending on tick interval
  612. tick.parameters.category = gridNode.name;
  613. tick.options = options;
  614. tick.addLabel();
  615. }
  616. } else {
  617. proceed.apply(axis, argsToArray(arguments));
  618. }
  619. },
  620. /**
  621. * Set the tick positions, tickInterval, axis min and max.
  622. *
  623. * @private
  624. * @function Highcharts.GridAxis#setTickInterval
  625. *
  626. * @param {Function} proceed
  627. * The original setTickInterval function.
  628. */
  629. setTickInterval: function (proceed) {
  630. var axis = this,
  631. options = axis.options,
  632. isTreeGrid = options.type === 'treegrid';
  633. if (isTreeGrid && this.mapOfPosToGridNode) {
  634. axis.min = pick(axis.userMin, options.min, axis.dataMin);
  635. axis.max = pick(axis.userMax, options.max, axis.dataMax);
  636. fireEvent(axis, 'foundExtremes');
  637. // setAxisTranslation modifies the min and max according to
  638. // axis breaks.
  639. axis.setAxisTranslation(true);
  640. axis.tickmarkOffset = 0.5;
  641. axis.tickInterval = 1;
  642. axis.tickPositions = getTickPositions(axis);
  643. } else {
  644. proceed.apply(axis, argsToArray(arguments));
  645. }
  646. }
  647. });
  648. override(GridAxisTick.prototype, {
  649. getLabelPosition: function (
  650. proceed,
  651. x,
  652. y,
  653. label,
  654. horiz,
  655. labelOptions,
  656. tickmarkOffset,
  657. index,
  658. step
  659. ) {
  660. var tick = this,
  661. lbOptions = pick(
  662. tick.options && tick.options.labels,
  663. labelOptions
  664. ),
  665. pos = tick.pos,
  666. axis = tick.axis,
  667. options = axis.options,
  668. isTreeGrid = options.type === 'treegrid',
  669. result = proceed.apply(
  670. tick,
  671. [x, y, label, horiz, lbOptions, tickmarkOffset, index, step]
  672. ),
  673. symbolOptions,
  674. indentation,
  675. mapOfPosToGridNode,
  676. node,
  677. level;
  678. if (isTreeGrid) {
  679. symbolOptions = (
  680. lbOptions && isObject(lbOptions.symbol) ?
  681. lbOptions.symbol :
  682. {}
  683. );
  684. indentation = (
  685. lbOptions && isNumber(lbOptions.indentation) ?
  686. lbOptions.indentation :
  687. 0
  688. );
  689. mapOfPosToGridNode = axis.mapOfPosToGridNode;
  690. node = mapOfPosToGridNode && mapOfPosToGridNode[pos];
  691. level = (node && node.depth) || 1;
  692. result.x += (
  693. // Add space for symbols
  694. ((symbolOptions.width) + (symbolOptions.padding * 2)) +
  695. // Apply indentation
  696. ((level - 1) * indentation)
  697. );
  698. }
  699. return result;
  700. },
  701. renderLabel: function (proceed) {
  702. var tick = this,
  703. pos = tick.pos,
  704. axis = tick.axis,
  705. label = tick.label,
  706. mapOfPosToGridNode = axis.mapOfPosToGridNode,
  707. options = axis.options,
  708. labelOptions = pick(
  709. tick.options && tick.options.labels,
  710. options && options.labels
  711. ),
  712. symbolOptions = (
  713. labelOptions && isObject(labelOptions.symbol) ?
  714. labelOptions.symbol :
  715. {}
  716. ),
  717. node = mapOfPosToGridNode && mapOfPosToGridNode[pos],
  718. level = node && node.depth,
  719. isTreeGrid = options.type === 'treegrid',
  720. hasLabel = !!(label && label.element),
  721. shouldRender = axis.tickPositions.indexOf(pos) > -1,
  722. prefixClassName = 'highcharts-treegrid-node-',
  723. collapsed,
  724. addClassName,
  725. removeClassName,
  726. styledMode = axis.chart.styledMode;
  727. if (isTreeGrid && node) {
  728. // Add class name for hierarchical styling.
  729. if (hasLabel) {
  730. label.addClass(prefixClassName + 'level-' + level);
  731. }
  732. }
  733. proceed.apply(tick, argsToArray(arguments));
  734. if (isTreeGrid && node && hasLabel && node.descendants > 0) {
  735. collapsed = isCollapsed(axis, node);
  736. renderLabelIcon(
  737. tick,
  738. {
  739. color: !styledMode && label.styles.color,
  740. collapsed: collapsed,
  741. group: label.parentGroup,
  742. options: symbolOptions,
  743. renderer: label.renderer,
  744. show: shouldRender,
  745. xy: label.xy
  746. }
  747. );
  748. // Add class name for the node.
  749. addClassName = prefixClassName +
  750. (collapsed ? 'collapsed' : 'expanded');
  751. removeClassName = prefixClassName +
  752. (collapsed ? 'expanded' : 'collapsed');
  753. label
  754. .addClass(addClassName)
  755. .removeClass(removeClassName);
  756. if (!styledMode) {
  757. label.css({
  758. cursor: 'pointer'
  759. });
  760. }
  761. // Add events to both label text and icon
  762. [label, tick.labelIcon].forEach(function (object) {
  763. if (!object.attachedTreeGridEvents) {
  764. // On hover
  765. H.addEvent(object.element, 'mouseover', function () {
  766. onTickHover(label);
  767. });
  768. // On hover out
  769. H.addEvent(object.element, 'mouseout', function () {
  770. onTickHoverExit(label, labelOptions);
  771. });
  772. H.addEvent(object.element, 'click', function () {
  773. tick.toggleCollapse();
  774. });
  775. object.attachedTreeGridEvents = true;
  776. }
  777. });
  778. }
  779. }
  780. });
  781. extend(GridAxisTick.prototype, /** @lends Highcharts.Tick.prototype */{
  782. /**
  783. * Collapse the grid cell. Used when axis is of type treegrid.
  784. *
  785. * @see gantt/treegrid-axis/collapsed-dynamically/demo.js
  786. *
  787. * @private
  788. * @function Highcharts.GridAxisTick#collapse
  789. *
  790. * @param {boolean} [redraw=true]
  791. * Whether to redraw the chart or wait for an explicit call to
  792. * {@link Highcharts.Chart#redraw}
  793. */
  794. collapse: function (redraw) {
  795. var tick = this,
  796. axis = tick.axis,
  797. pos = tick.pos,
  798. node = axis.mapOfPosToGridNode[pos],
  799. breaks = collapse(axis, node);
  800. axis.setBreaks(breaks, pick(redraw, true));
  801. },
  802. /**
  803. * Expand the grid cell. Used when axis is of type treegrid.
  804. *
  805. * @see gantt/treegrid-axis/collapsed-dynamically/demo.js
  806. *
  807. * @private
  808. * @function Highcharts.GridAxisTick#expand
  809. *
  810. * @param {boolean} [redraw=true]
  811. * Whether to redraw the chart or wait for an explicit call to
  812. * {@link Highcharts.Chart#redraw}
  813. */
  814. expand: function (redraw) {
  815. var tick = this,
  816. axis = tick.axis,
  817. pos = tick.pos,
  818. node = axis.mapOfPosToGridNode[pos],
  819. breaks = expand(axis, node);
  820. axis.setBreaks(breaks, pick(redraw, true));
  821. },
  822. /**
  823. * Toggle the collapse/expand state of the grid cell. Used when axis is of
  824. * type treegrid.
  825. *
  826. * @see gantt/treegrid-axis/collapsed-dynamically/demo.js
  827. *
  828. * @private
  829. * @function Highcharts.GridAxisTick#toggleCollapse
  830. *
  831. * @param {boolean} [redraw=true]
  832. * Whether to redraw the chart or wait for an explicit call to
  833. * {@link Highcharts.Chart#redraw}
  834. */
  835. toggleCollapse: function (redraw) {
  836. var tick = this,
  837. axis = tick.axis,
  838. pos = tick.pos,
  839. node = axis.mapOfPosToGridNode[pos],
  840. breaks = toggleCollapse(axis, node);
  841. axis.setBreaks(breaks, pick(redraw, true));
  842. }
  843. });
  844. GridAxis.prototype.updateYNames = function () {
  845. var axis = this,
  846. options = axis.options,
  847. isTreeGrid = options.type === 'treegrid',
  848. uniqueNames = options.uniqueNames,
  849. isYAxis = !axis.isXAxis,
  850. series = axis.series,
  851. numberOfSeries = 0,
  852. treeGrid,
  853. data;
  854. if (isTreeGrid && isYAxis) {
  855. // Concatenate data from all series assigned to this axis.
  856. data = series.reduce(function (arr, s) {
  857. if (s.visible) {
  858. // Push all data to array
  859. s.options.data.forEach(function (data) {
  860. if (isObject(data)) {
  861. // Set series index on data. Removed again after use.
  862. data.seriesIndex = numberOfSeries;
  863. arr.push(data);
  864. }
  865. });
  866. // Increment series index
  867. if (uniqueNames === true) {
  868. numberOfSeries++;
  869. }
  870. }
  871. return arr;
  872. }, []);
  873. // Calculate categories and the hierarchy for the grid.
  874. treeGrid = getTreeGridFromData(
  875. data,
  876. uniqueNames,
  877. (uniqueNames === true) ? numberOfSeries : 1
  878. );
  879. // Assign values to the axis.
  880. axis.categories = treeGrid.categories;
  881. axis.mapOfPosToGridNode = treeGrid.mapOfPosToGridNode;
  882. // Used on init to start a node as collapsed
  883. axis.collapsedNodes = treeGrid.collapsedNodes;
  884. axis.hasNames = true;
  885. axis.tree = treeGrid.tree;
  886. }
  887. };
  888. // Make utility functions available for testing.
  889. GridAxis.prototype.utils = {
  890. getNode: Tree.getNode
  891. };