sunburst.src.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017
  1. /* *
  2. *
  3. * This module implements sunburst charts in Highcharts.
  4. *
  5. * (c) 2016-2019 Highsoft AS
  6. *
  7. * Authors: Jon Arild Nygard
  8. *
  9. * License: www.highcharts.com/license
  10. *
  11. * */
  12. 'use strict';
  13. import H from '../parts/Globals.js';
  14. import '../mixins/centered-series.js';
  15. import drawPoint from '../mixins/draw-point.js';
  16. import mixinTreeSeries from '../mixins/tree-series.js';
  17. import '../parts/Series.js';
  18. import './treemap.src.js';
  19. var CenteredSeriesMixin = H.CenteredSeriesMixin,
  20. Series = H.Series,
  21. extend = H.extend,
  22. getCenter = CenteredSeriesMixin.getCenter,
  23. getColor = mixinTreeSeries.getColor,
  24. getLevelOptions = mixinTreeSeries.getLevelOptions,
  25. getStartAndEndRadians = CenteredSeriesMixin.getStartAndEndRadians,
  26. isBoolean = function (x) {
  27. return typeof x === 'boolean';
  28. },
  29. isNumber = H.isNumber,
  30. isObject = H.isObject,
  31. isString = H.isString,
  32. merge = H.merge,
  33. noop = H.noop,
  34. rad2deg = 180 / Math.PI,
  35. seriesType = H.seriesType,
  36. seriesTypes = H.seriesTypes,
  37. setTreeValues = mixinTreeSeries.setTreeValues,
  38. updateRootId = mixinTreeSeries.updateRootId;
  39. // TODO introduce step, which should default to 1.
  40. var range = function range(from, to) {
  41. var result = [],
  42. i;
  43. if (isNumber(from) && isNumber(to) && from <= to) {
  44. for (i = from; i <= to; i++) {
  45. result.push(i);
  46. }
  47. }
  48. return result;
  49. };
  50. /**
  51. * @private
  52. * @function calculateLevelSizes
  53. *
  54. * @param {object} levelOptions
  55. * Map of level to its options.
  56. *
  57. * @param {object} params
  58. * Object containing number parameters `innerRadius` and `outerRadius`.
  59. */
  60. var calculateLevelSizes = function calculateLevelSizes(levelOptions, params) {
  61. var result,
  62. p = isObject(params) ? params : {},
  63. totalWeight = 0,
  64. diffRadius,
  65. levels,
  66. levelsNotIncluded,
  67. remainingSize,
  68. from,
  69. to;
  70. if (isObject(levelOptions)) {
  71. result = merge({}, levelOptions); // Copy levelOptions
  72. from = isNumber(p.from) ? p.from : 0;
  73. to = isNumber(p.to) ? p.to : 0;
  74. levels = range(from, to);
  75. levelsNotIncluded = Object.keys(result).filter(function (k) {
  76. return levels.indexOf(+k) === -1;
  77. });
  78. diffRadius = remainingSize = isNumber(p.diffRadius) ? p.diffRadius : 0;
  79. // Convert percentage to pixels.
  80. // Calculate the remaining size to divide between "weight" levels.
  81. // Calculate total weight to use in convertion from weight to pixels.
  82. levels.forEach(function (level) {
  83. var options = result[level],
  84. unit = options.levelSize.unit,
  85. value = options.levelSize.value;
  86. if (unit === 'weight') {
  87. totalWeight += value;
  88. } else if (unit === 'percentage') {
  89. options.levelSize = {
  90. unit: 'pixels',
  91. value: (value / 100) * diffRadius
  92. };
  93. remainingSize -= options.levelSize.value;
  94. } else if (unit === 'pixels') {
  95. remainingSize -= value;
  96. }
  97. });
  98. // Convert weight to pixels.
  99. levels.forEach(function (level) {
  100. var options = result[level],
  101. weight;
  102. if (options.levelSize.unit === 'weight') {
  103. weight = options.levelSize.value;
  104. result[level].levelSize = {
  105. unit: 'pixels',
  106. value: (weight / totalWeight) * remainingSize
  107. };
  108. }
  109. });
  110. // Set all levels not included in interval [from,to] to have 0 pixels.
  111. levelsNotIncluded.forEach(function (level) {
  112. result[level].levelSize = {
  113. value: 0,
  114. unit: 'pixels'
  115. };
  116. });
  117. }
  118. return result;
  119. };
  120. /**
  121. * Find a set of coordinates given a start coordinates, an angle, and a
  122. * distance.
  123. *
  124. * @private
  125. * @function getEndPoint
  126. *
  127. * @param {number} x
  128. * Start coordinate x
  129. *
  130. * @param {number} y
  131. * Start coordinate y
  132. *
  133. * @param {number} angle
  134. * Angle in radians
  135. *
  136. * @param {number} distance
  137. * Distance from start to end coordinates
  138. *
  139. * @return {Highcharts.SVGAttributes}
  140. * Returns the end coordinates, x and y.
  141. */
  142. var getEndPoint = function getEndPoint(x, y, angle, distance) {
  143. return {
  144. x: x + (Math.cos(angle) * distance),
  145. y: y + (Math.sin(angle) * distance)
  146. };
  147. };
  148. var layoutAlgorithm = function layoutAlgorithm(parent, children, options) {
  149. var startAngle = parent.start,
  150. range = parent.end - startAngle,
  151. total = parent.val,
  152. x = parent.x,
  153. y = parent.y,
  154. radius = (
  155. (
  156. options &&
  157. isObject(options.levelSize) &&
  158. isNumber(options.levelSize.value)
  159. ) ?
  160. options.levelSize.value :
  161. 0
  162. ),
  163. innerRadius = parent.r,
  164. outerRadius = innerRadius + radius,
  165. slicedOffset = options && isNumber(options.slicedOffset) ?
  166. options.slicedOffset :
  167. 0;
  168. return (children || []).reduce(function (arr, child) {
  169. var percentage = (1 / total) * child.val,
  170. radians = percentage * range,
  171. radiansCenter = startAngle + (radians / 2),
  172. offsetPosition = getEndPoint(x, y, radiansCenter, slicedOffset),
  173. values = {
  174. x: child.sliced ? offsetPosition.x : x,
  175. y: child.sliced ? offsetPosition.y : y,
  176. innerR: innerRadius,
  177. r: outerRadius,
  178. radius: radius,
  179. start: startAngle,
  180. end: startAngle + radians
  181. };
  182. arr.push(values);
  183. startAngle = values.end;
  184. return arr;
  185. }, []);
  186. };
  187. var getDlOptions = function getDlOptions(params) {
  188. // Set options to new object to avoid problems with scope
  189. var point = params.point,
  190. shape = isObject(params.shapeArgs) ? params.shapeArgs : {},
  191. optionsPoint = (
  192. isObject(params.optionsPoint) ?
  193. params.optionsPoint.dataLabels :
  194. {}
  195. ),
  196. optionsLevel = (
  197. isObject(params.level) ?
  198. params.level.dataLabels :
  199. {}
  200. ),
  201. options = merge({
  202. style: {}
  203. }, optionsLevel, optionsPoint),
  204. rotationRad,
  205. rotation,
  206. rotationMode = options.rotationMode;
  207. if (!isNumber(options.rotation)) {
  208. if (rotationMode === 'auto') {
  209. if (
  210. point.innerArcLength < 1 &&
  211. point.outerArcLength > shape.radius
  212. ) {
  213. rotationRad = 0;
  214. } else if (
  215. point.innerArcLength > 1 &&
  216. point.outerArcLength > 1.5 * shape.radius
  217. ) {
  218. rotationMode = 'parallel';
  219. } else {
  220. rotationMode = 'perpendicular';
  221. }
  222. }
  223. if (rotationMode !== 'auto') {
  224. rotationRad = (shape.end - (shape.end - shape.start) / 2);
  225. }
  226. if (rotationMode === 'parallel') {
  227. options.style.width = Math.min(
  228. shape.radius * 2.5,
  229. (point.outerArcLength + point.innerArcLength) / 2
  230. );
  231. } else {
  232. options.style.width = shape.radius;
  233. }
  234. if (
  235. rotationMode === 'perpendicular' &&
  236. point.series.chart.renderer.fontMetrics(options.style.fontSize).h >
  237. point.outerArcLength
  238. ) {
  239. options.style.width = 1;
  240. }
  241. // Apply padding (#8515)
  242. options.style.width = Math.max(
  243. options.style.width - 2 * (options.padding || 0),
  244. 1
  245. );
  246. rotation = (rotationRad * rad2deg) % 180;
  247. if (rotationMode === 'parallel') {
  248. rotation -= 90;
  249. }
  250. // Prevent text from rotating upside down
  251. if (rotation > 90) {
  252. rotation -= 180;
  253. } else if (rotation < -90) {
  254. rotation += 180;
  255. }
  256. options.rotation = rotation;
  257. }
  258. // NOTE: alignDataLabel positions the data label differntly when rotation is
  259. // 0. Avoiding this by setting rotation to a small number.
  260. if (options.rotation === 0) {
  261. options.rotation = 0.001;
  262. }
  263. return options;
  264. };
  265. var getAnimation = function getAnimation(shape, params) {
  266. var point = params.point,
  267. radians = params.radians,
  268. innerR = params.innerR,
  269. idRoot = params.idRoot,
  270. idPreviousRoot = params.idPreviousRoot,
  271. shapeExisting = params.shapeExisting,
  272. shapeRoot = params.shapeRoot,
  273. shapePreviousRoot = params.shapePreviousRoot,
  274. visible = params.visible,
  275. from = {},
  276. to = {
  277. end: shape.end,
  278. start: shape.start,
  279. innerR: shape.innerR,
  280. r: shape.r,
  281. x: shape.x,
  282. y: shape.y
  283. };
  284. if (visible) {
  285. // Animate points in
  286. if (!point.graphic && shapePreviousRoot) {
  287. if (idRoot === point.id) {
  288. from = {
  289. start: radians.start,
  290. end: radians.end
  291. };
  292. } else {
  293. from = (shapePreviousRoot.end <= shape.start) ? {
  294. start: radians.end,
  295. end: radians.end
  296. } : {
  297. start: radians.start,
  298. end: radians.start
  299. };
  300. }
  301. // Animate from center and outwards.
  302. from.innerR = from.r = innerR;
  303. }
  304. } else {
  305. // Animate points out
  306. if (point.graphic) {
  307. if (idPreviousRoot === point.id) {
  308. to = {
  309. innerR: innerR,
  310. r: innerR
  311. };
  312. } else if (shapeRoot) {
  313. to = (shapeRoot.end <= shapeExisting.start) ?
  314. {
  315. innerR: innerR,
  316. r: innerR,
  317. start: radians.end,
  318. end: radians.end
  319. } : {
  320. innerR: innerR,
  321. r: innerR,
  322. start: radians.start,
  323. end: radians.start
  324. };
  325. }
  326. }
  327. }
  328. return {
  329. from: from,
  330. to: to
  331. };
  332. };
  333. var getDrillId = function getDrillId(point, idRoot, mapIdToNode) {
  334. var drillId,
  335. node = point.node,
  336. nodeRoot;
  337. if (!node.isLeaf) {
  338. // When it is the root node, the drillId should be set to parent.
  339. if (idRoot === point.id) {
  340. nodeRoot = mapIdToNode[idRoot];
  341. drillId = nodeRoot.parent;
  342. } else {
  343. drillId = point.id;
  344. }
  345. }
  346. return drillId;
  347. };
  348. var cbSetTreeValuesBefore = function before(node, options) {
  349. var mapIdToNode = options.mapIdToNode,
  350. nodeParent = mapIdToNode[node.parent],
  351. series = options.series,
  352. chart = series.chart,
  353. points = series.points,
  354. point = points[node.i],
  355. colorInfo = getColor(node, {
  356. colors: chart && chart.options && chart.options.colors,
  357. colorIndex: series.colorIndex,
  358. index: options.index,
  359. mapOptionsToLevel: options.mapOptionsToLevel,
  360. parentColor: nodeParent && nodeParent.color,
  361. parentColorIndex: nodeParent && nodeParent.colorIndex,
  362. series: options.series,
  363. siblings: options.siblings
  364. });
  365. node.color = colorInfo.color;
  366. node.colorIndex = colorInfo.colorIndex;
  367. if (point) {
  368. point.color = node.color;
  369. point.colorIndex = node.colorIndex;
  370. // Set slicing on node, but avoid slicing the top node.
  371. node.sliced = (node.id !== options.idRoot) ? point.sliced : false;
  372. }
  373. return node;
  374. };
  375. /**
  376. * A Sunburst displays hierarchical data, where a level in the hierarchy is
  377. * represented by a circle. The center represents the root node of the tree.
  378. * The visualization bears a resemblance to both treemap and pie charts.
  379. *
  380. * @sample highcharts/demo/sunburst
  381. * Sunburst chart
  382. *
  383. * @extends plotOptions.pie
  384. * @excluding allAreas, clip, colorAxis, compare, compareBase, dataGrouping,
  385. * depth, endAngle, gapSize, gapUnit, ignoreHiddenPoint,
  386. * innerSize, joinBy, legendType, linecap, minSize,
  387. * navigatorOptions, pointRange
  388. * @product highcharts
  389. * @optionparent plotOptions.sunburst
  390. */
  391. var sunburstOptions = {
  392. /**
  393. * Set options on specific levels. Takes precedence over series options,
  394. * but not point options.
  395. *
  396. * @sample highcharts/demo/sunburst
  397. * Sunburst chart
  398. *
  399. * @type {Array<*>}
  400. * @apioption plotOptions.sunburst.levels
  401. */
  402. /**
  403. * Can set a `borderColor` on all points which lies on the same level.
  404. *
  405. * @type {Highcharts.ColorString}
  406. * @apioption plotOptions.sunburst.levels.borderColor
  407. */
  408. /**
  409. * Can set a `borderWidth` on all points which lies on the same level.
  410. *
  411. * @type {number}
  412. * @apioption plotOptions.sunburst.levels.borderWidth
  413. */
  414. /**
  415. * Can set a `borderDashStyle` on all points which lies on the same level.
  416. *
  417. * @type {string}
  418. * @apioption plotOptions.sunburst.levels.borderDashStyle
  419. */
  420. /**
  421. * Can set a `color` on all points which lies on the same level.
  422. *
  423. * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
  424. * @apioption plotOptions.sunburst.levels.color
  425. */
  426. /**
  427. * Can set a `colorVariation` on all points which lies on the same level.
  428. *
  429. * @apioption plotOptions.sunburst.levels.colorVariation
  430. */
  431. /**
  432. * The key of a color variation. Currently supports `brightness` only.
  433. *
  434. * @type {string}
  435. * @apioption plotOptions.sunburst.levels.colorVariation.key
  436. */
  437. /**
  438. * The ending value of a color variation. The last sibling will receive this
  439. * value.
  440. *
  441. * @type {number}
  442. * @apioption plotOptions.sunburst.levels.colorVariation.to
  443. */
  444. /**
  445. * Can set `dataLabels` on all points which lies on the same level.
  446. *
  447. * @type {object}
  448. * @apioption plotOptions.sunburst.levels.dataLabels
  449. */
  450. /**
  451. * Can set a `levelSize` on all points which lies on the same level.
  452. *
  453. * @type {object}
  454. * @apioption plotOptions.sunburst.levels.levelSize
  455. */
  456. /**
  457. * Can set a `rotation` on all points which lies on the same level.
  458. *
  459. * @type {number}
  460. * @apioption plotOptions.sunburst.levels.rotation
  461. */
  462. /**
  463. * Can set a `rotationMode` on all points which lies on the same level.
  464. *
  465. * @type {string}
  466. * @apioption plotOptions.sunburst.levels.rotationMode
  467. */
  468. /**
  469. * When enabled the user can click on a point which is a parent and
  470. * zoom in on its children.
  471. *
  472. * @sample highcharts/demo/sunburst
  473. * Allow drill to node
  474. *
  475. * @type {boolean}
  476. * @default false
  477. * @apioption plotOptions.sunburst.allowDrillToNode
  478. */
  479. /**
  480. * The center of the sunburst chart relative to the plot area. Can be
  481. * percentages or pixel values.
  482. *
  483. * @sample {highcharts} highcharts/plotoptions/pie-center/
  484. * Centered at 100, 100
  485. *
  486. * @type {Array<number|string>}
  487. * @default ["50%", "50%"]
  488. * @product highcharts
  489. */
  490. center: ['50%', '50%'],
  491. colorByPoint: false,
  492. /**
  493. * @extends plotOptions.series.dataLabels
  494. * @excluding align, allowOverlap, distance, staggerLines, step
  495. */
  496. dataLabels: {
  497. allowOverlap: true,
  498. defer: true,
  499. style: {
  500. textOverflow: 'ellipsis'
  501. },
  502. /**
  503. * Decides how the data label will be rotated relative to the perimeter
  504. * of the sunburst. Valid values are `auto`, `parallel` and
  505. * `perpendicular`. When `auto`, the best fit will be computed for the
  506. * point.
  507. *
  508. * The `series.rotation` option takes precedence over `rotationMode`.
  509. *
  510. * @since 6.0.0
  511. * @validvalue ["auto", "perpendicular", "parallel"]
  512. */
  513. rotationMode: 'auto'
  514. },
  515. /**
  516. * Which point to use as a root in the visualization.
  517. *
  518. * @type {string}
  519. */
  520. rootId: undefined,
  521. /**
  522. * Used together with the levels and `allowDrillToNode` options. When
  523. * set to false the first level visible when drilling is considered
  524. * to be level one. Otherwise the level will be the same as the tree
  525. * structure.
  526. */
  527. levelIsConstant: true,
  528. /**
  529. * Determines the width of the ring per level.
  530. *
  531. * @sample {highcharts} highcharts/plotoptions/sunburst-levelsize/
  532. * Sunburst with various sizes per level
  533. *
  534. * @since 6.0.5
  535. */
  536. levelSize: {
  537. /**
  538. * The value used for calculating the width of the ring. Its' affect is
  539. * determined by `levelSize.unit`.
  540. *
  541. * @sample {highcharts} highcharts/plotoptions/sunburst-levelsize/
  542. * Sunburst with various sizes per level
  543. */
  544. value: 1,
  545. /**
  546. * How to interpret `levelSize.value`.
  547. *
  548. * - `percentage` gives a width relative to result of outer radius minus
  549. * inner radius.
  550. *
  551. * - `pixels` gives the ring a fixed width in pixels.
  552. *
  553. * - `weight` takes the remaining width after percentage and pixels, and
  554. * distributes it accross all "weighted" levels. The value relative to
  555. * the sum of all weights determines the width.
  556. *
  557. * @sample {highcharts} highcharts/plotoptions/sunburst-levelsize/
  558. * Sunburst with various sizes per level
  559. *
  560. * @validvalue ["percentage", "pixels", "weight"]
  561. */
  562. unit: 'weight'
  563. },
  564. /**
  565. * If a point is sliced, moved out from the center, how many pixels
  566. * should it be moved?.
  567. *
  568. * @sample highcharts/plotoptions/sunburst-sliced
  569. * Sliced sunburst
  570. *
  571. * @since 6.0.4
  572. */
  573. slicedOffset: 10
  574. };
  575. // Properties of the Sunburst series.
  576. var sunburstSeries = {
  577. drawDataLabels: noop, // drawDataLabels is called in drawPoints
  578. drawPoints: function drawPoints() {
  579. var series = this,
  580. mapOptionsToLevel = series.mapOptionsToLevel,
  581. shapeRoot = series.shapeRoot,
  582. group = series.group,
  583. hasRendered = series.hasRendered,
  584. idRoot = series.rootNode,
  585. idPreviousRoot = series.idPreviousRoot,
  586. nodeMap = series.nodeMap,
  587. nodePreviousRoot = nodeMap[idPreviousRoot],
  588. shapePreviousRoot = nodePreviousRoot && nodePreviousRoot.shapeArgs,
  589. points = series.points,
  590. radians = series.startAndEndRadians,
  591. chart = series.chart,
  592. optionsChart = chart && chart.options && chart.options.chart || {},
  593. animation = (
  594. isBoolean(optionsChart.animation) ?
  595. optionsChart.animation :
  596. true
  597. ),
  598. positions = series.center,
  599. center = {
  600. x: positions[0],
  601. y: positions[1]
  602. },
  603. innerR = positions[3] / 2,
  604. renderer = series.chart.renderer,
  605. animateLabels,
  606. animateLabelsCalled = false,
  607. addedHack = false,
  608. hackDataLabelAnimation = !!(
  609. animation &&
  610. hasRendered &&
  611. idRoot !== idPreviousRoot &&
  612. series.dataLabelsGroup
  613. );
  614. if (hackDataLabelAnimation) {
  615. series.dataLabelsGroup.attr({ opacity: 0 });
  616. animateLabels = function () {
  617. var s = series;
  618. animateLabelsCalled = true;
  619. if (s.dataLabelsGroup) {
  620. s.dataLabelsGroup.animate({
  621. opacity: 1,
  622. visibility: 'visible'
  623. });
  624. }
  625. };
  626. }
  627. points.forEach(function (point) {
  628. var node = point.node,
  629. level = mapOptionsToLevel[node.level],
  630. shapeExisting = point.shapeExisting || {},
  631. shape = node.shapeArgs || {},
  632. animationInfo,
  633. onComplete,
  634. visible = !!(node.visible && node.shapeArgs);
  635. if (hasRendered && animation) {
  636. animationInfo = getAnimation(shape, {
  637. center: center,
  638. point: point,
  639. radians: radians,
  640. innerR: innerR,
  641. idRoot: idRoot,
  642. idPreviousRoot: idPreviousRoot,
  643. shapeExisting: shapeExisting,
  644. shapeRoot: shapeRoot,
  645. shapePreviousRoot: shapePreviousRoot,
  646. visible: visible
  647. });
  648. } else {
  649. // When animation is disabled, attr is called from animation.
  650. animationInfo = {
  651. to: shape,
  652. from: {}
  653. };
  654. }
  655. extend(point, {
  656. shapeExisting: shape, // Store for use in animation
  657. tooltipPos: [shape.plotX, shape.plotY],
  658. drillId: getDrillId(point, idRoot, nodeMap),
  659. name: '' + (point.name || point.id || point.index),
  660. plotX: shape.plotX, // used for data label position
  661. plotY: shape.plotY, // used for data label position
  662. value: node.val,
  663. isNull: !visible // used for dataLabels & point.draw
  664. });
  665. point.dlOptions = getDlOptions({
  666. point: point,
  667. level: level,
  668. optionsPoint: point.options,
  669. shapeArgs: shape
  670. });
  671. if (!addedHack && visible) {
  672. addedHack = true;
  673. onComplete = animateLabels;
  674. }
  675. point.draw({
  676. animatableAttribs: animationInfo.to,
  677. attribs: extend(
  678. animationInfo.from,
  679. !chart.styledMode && series.pointAttribs(
  680. point,
  681. point.selected && 'select'
  682. )
  683. ),
  684. onComplete: onComplete,
  685. group: group,
  686. renderer: renderer,
  687. shapeType: 'arc',
  688. shapeArgs: shape
  689. });
  690. });
  691. // Draw data labels after points
  692. // TODO draw labels one by one to avoid addtional looping
  693. if (hackDataLabelAnimation && addedHack) {
  694. series.hasRendered = false;
  695. series.options.dataLabels.defer = true;
  696. Series.prototype.drawDataLabels.call(series);
  697. series.hasRendered = true;
  698. // If animateLabels is called before labels were hidden, then call
  699. // it again.
  700. if (animateLabelsCalled) {
  701. animateLabels();
  702. }
  703. } else {
  704. Series.prototype.drawDataLabels.call(series);
  705. }
  706. },
  707. pointAttribs: seriesTypes.column.prototype.pointAttribs,
  708. // The layout algorithm for the levels
  709. layoutAlgorithm: layoutAlgorithm,
  710. // Set the shape arguments on the nodes. Recursive from root down.
  711. setShapeArgs: function (parent, parentValues, mapOptionsToLevel) {
  712. var childrenValues = [],
  713. level = parent.level + 1,
  714. options = mapOptionsToLevel[level],
  715. // Collect all children which should be included
  716. children = parent.children.filter(function (n) {
  717. return n.visible;
  718. }),
  719. twoPi = 6.28; // Two times Pi.
  720. childrenValues = this.layoutAlgorithm(parentValues, children, options);
  721. children.forEach(function (child, index) {
  722. var values = childrenValues[index],
  723. angle = values.start + ((values.end - values.start) / 2),
  724. radius = values.innerR + ((values.r - values.innerR) / 2),
  725. radians = (values.end - values.start),
  726. isCircle = (values.innerR === 0 && radians > twoPi),
  727. center = (
  728. isCircle ?
  729. { x: values.x, y: values.y } :
  730. getEndPoint(values.x, values.y, angle, radius)
  731. ),
  732. val = (
  733. child.val ?
  734. (
  735. child.childrenTotal > child.val ?
  736. child.childrenTotal :
  737. child.val
  738. ) :
  739. child.childrenTotal
  740. );
  741. // The inner arc length is a convenience for data label filters.
  742. if (this.points[child.i]) {
  743. this.points[child.i].innerArcLength = radians * values.innerR;
  744. this.points[child.i].outerArcLength = radians * values.r;
  745. }
  746. child.shapeArgs = merge(values, {
  747. plotX: center.x,
  748. plotY: center.y + 4 * Math.abs(Math.cos(angle))
  749. });
  750. child.values = merge(values, {
  751. val: val
  752. });
  753. // If node has children, then call method recursively
  754. if (child.children.length) {
  755. this.setShapeArgs(child, child.values, mapOptionsToLevel);
  756. }
  757. }, this);
  758. },
  759. translate: function translate() {
  760. var series = this,
  761. options = series.options,
  762. positions = series.center = getCenter.call(series),
  763. radians = series.startAndEndRadians = getStartAndEndRadians(
  764. options.startAngle,
  765. options.endAngle
  766. ),
  767. innerRadius = positions[3] / 2,
  768. outerRadius = positions[2] / 2,
  769. diffRadius = outerRadius - innerRadius,
  770. // NOTE: updateRootId modifies series.
  771. rootId = updateRootId(series),
  772. mapIdToNode = series.nodeMap,
  773. mapOptionsToLevel,
  774. idTop,
  775. nodeRoot = mapIdToNode && mapIdToNode[rootId],
  776. nodeTop,
  777. tree,
  778. values;
  779. series.shapeRoot = nodeRoot && nodeRoot.shapeArgs;
  780. // Call prototype function
  781. Series.prototype.translate.call(series);
  782. // @todo Only if series.isDirtyData is true
  783. tree = series.tree = series.getTree();
  784. mapIdToNode = series.nodeMap;
  785. nodeRoot = mapIdToNode[rootId];
  786. idTop = isString(nodeRoot.parent) ? nodeRoot.parent : '';
  787. nodeTop = mapIdToNode[idTop];
  788. mapOptionsToLevel = getLevelOptions({
  789. from: nodeRoot.level > 0 ? nodeRoot.level : 1,
  790. levels: series.options.levels,
  791. to: tree.height,
  792. defaults: {
  793. colorByPoint: options.colorByPoint,
  794. dataLabels: options.dataLabels,
  795. levelIsConstant: options.levelIsConstant,
  796. levelSize: options.levelSize,
  797. slicedOffset: options.slicedOffset
  798. }
  799. });
  800. // NOTE consider doing calculateLevelSizes in a callback to
  801. // getLevelOptions
  802. mapOptionsToLevel = calculateLevelSizes(mapOptionsToLevel, {
  803. diffRadius: diffRadius,
  804. from: nodeRoot.level > 0 ? nodeRoot.level : 1,
  805. to: tree.height
  806. });
  807. // TODO Try to combine setTreeValues & setColorRecursive to avoid
  808. // unnecessary looping.
  809. setTreeValues(tree, {
  810. before: cbSetTreeValuesBefore,
  811. idRoot: rootId,
  812. levelIsConstant: options.levelIsConstant,
  813. mapOptionsToLevel: mapOptionsToLevel,
  814. mapIdToNode: mapIdToNode,
  815. points: series.points,
  816. series: series
  817. });
  818. values = mapIdToNode[''].shapeArgs = {
  819. end: radians.end,
  820. r: innerRadius,
  821. start: radians.start,
  822. val: nodeRoot.val,
  823. x: positions[0],
  824. y: positions[1]
  825. };
  826. this.setShapeArgs(nodeTop, values, mapOptionsToLevel);
  827. // Set mapOptionsToLevel on series for use in drawPoints.
  828. series.mapOptionsToLevel = mapOptionsToLevel;
  829. },
  830. // Animate the slices in. Similar to the animation of polar charts.
  831. animate: function (init) {
  832. var chart = this.chart,
  833. center = [
  834. chart.plotWidth / 2,
  835. chart.plotHeight / 2
  836. ],
  837. plotLeft = chart.plotLeft,
  838. plotTop = chart.plotTop,
  839. attribs,
  840. group = this.group;
  841. // Initialize the animation
  842. if (init) {
  843. // Scale down the group and place it in the center
  844. attribs = {
  845. translateX: center[0] + plotLeft,
  846. translateY: center[1] + plotTop,
  847. scaleX: 0.001, // #1499
  848. scaleY: 0.001,
  849. rotation: 10,
  850. opacity: 0.01
  851. };
  852. group.attr(attribs);
  853. // Run the animation
  854. } else {
  855. attribs = {
  856. translateX: plotLeft,
  857. translateY: plotTop,
  858. scaleX: 1,
  859. scaleY: 1,
  860. rotation: 0,
  861. opacity: 1
  862. };
  863. group.animate(attribs, this.options.animation);
  864. // Delete this function to allow it only once
  865. this.animate = null;
  866. }
  867. },
  868. utils: {
  869. calculateLevelSizes: calculateLevelSizes,
  870. range: range
  871. }
  872. };
  873. // Properties of the Sunburst series.
  874. var sunburstPoint = {
  875. draw: drawPoint,
  876. shouldDraw: function shouldDraw() {
  877. var point = this;
  878. return !point.isNull;
  879. }
  880. };
  881. /**
  882. * A `sunburst` series. If the [type](#series.sunburst.type) option is
  883. * not specified, it is inherited from [chart.type](#chart.type).
  884. *
  885. * @extends series,plotOptions.sunburst
  886. * @excluding dataParser, dataURL, stack
  887. * @product highcharts
  888. * @apioption series.sunburst
  889. */
  890. /**
  891. * @type {Array<number|*>}
  892. * @extends series.treemap.data
  893. * @excluding x, y
  894. * @product highcharts
  895. * @apioption series.sunburst.data
  896. */
  897. /**
  898. * The value of the point, resulting in a relative area of the point
  899. * in the sunburst.
  900. *
  901. * @type {number}
  902. * @since 6.0.0
  903. * @product highcharts
  904. * @apioption series.sunburst.data.value
  905. */
  906. /**
  907. * Use this option to build a tree structure. The value should be the id of the
  908. * point which is the parent. If no points has a matching id, or this option is
  909. * undefined, then the parent will be set to the root.
  910. *
  911. * @type {string}
  912. * @since 6.0.0
  913. * @product highcharts
  914. * @apioption series.treemap.data.parent
  915. */
  916. /**
  917. * Whether to display a slice offset from the center. When a sunburst point is
  918. * sliced, its children are also offset.
  919. *
  920. * @sample highcharts/plotoptions/sunburst-sliced
  921. * Sliced sunburst
  922. *
  923. * @type {boolean}
  924. * @default false
  925. * @since 6.0.4
  926. * @product highcharts
  927. * @apioption series.sunburst.data.sliced
  928. */
  929. /**
  930. * @private
  931. * @class
  932. * @name Highcharts.seriesTypes.sunburst
  933. *
  934. * @augments Highcharts.Series
  935. */
  936. seriesType(
  937. 'sunburst',
  938. 'treemap',
  939. sunburstOptions,
  940. sunburstSeries,
  941. sunburstPoint
  942. );