sunburst.src.js 35 KB

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