sunburst.src.js 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955
  1. /* *
  2. *
  3. * This module implements sunburst charts in Highcharts.
  4. *
  5. * (c) 2016-2020 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, error = U.error, extend = U.extend, isNumber = U.isNumber, isObject = U.isObject, isString = U.isString, merge = U.merge, seriesType = U.seriesType, 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. }, noop = H.noop, rad2deg = 180 / Math.PI, 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`, `circular`, `parallel` and
  512. * `perpendicular`. When `auto`, the best fit will be
  513. * computed for the point. The `circular` option works similiar
  514. * to `auto`, but uses the `textPath` feature - labels are curved,
  515. * resulting in a better layout, however multiple lines and
  516. * `textOutline` are not supported.
  517. *
  518. * The `series.rotation` option takes precedence over `rotationMode`.
  519. *
  520. * @type {string}
  521. * @sample {highcharts} highcharts/plotoptions/sunburst-datalabels-rotationmode-circular/
  522. * Circular rotation mode
  523. * @validvalue ["auto", "perpendicular", "parallel", "circular"]
  524. * @since 6.0.0
  525. */
  526. rotationMode: 'auto',
  527. style: {
  528. /** @internal */
  529. textOverflow: 'ellipsis'
  530. }
  531. },
  532. /**
  533. * Which point to use as a root in the visualization.
  534. *
  535. * @type {string}
  536. */
  537. rootId: void 0,
  538. /**
  539. * Used together with the levels and `allowDrillToNode` options. When
  540. * set to false the first level visible when drilling is considered
  541. * to be level one. Otherwise the level will be the same as the tree
  542. * structure.
  543. */
  544. levelIsConstant: true,
  545. /**
  546. * Determines the width of the ring per level.
  547. *
  548. * @sample {highcharts} highcharts/plotoptions/sunburst-levelsize/
  549. * Sunburst with various sizes per level
  550. *
  551. * @since 6.0.5
  552. */
  553. levelSize: {
  554. /**
  555. * The value used for calculating the width of the ring. Its' affect is
  556. * determined by `levelSize.unit`.
  557. *
  558. * @sample {highcharts} highcharts/plotoptions/sunburst-levelsize/
  559. * Sunburst with various sizes per level
  560. */
  561. value: 1,
  562. /**
  563. * How to interpret `levelSize.value`.
  564. *
  565. * - `percentage` gives a width relative to result of outer radius minus
  566. * inner radius.
  567. *
  568. * - `pixels` gives the ring a fixed width in pixels.
  569. *
  570. * - `weight` takes the remaining width after percentage and pixels, and
  571. * distributes it accross all "weighted" levels. The value relative to
  572. * the sum of all weights determines the width.
  573. *
  574. * @sample {highcharts} highcharts/plotoptions/sunburst-levelsize/
  575. * Sunburst with various sizes per level
  576. *
  577. * @validvalue ["percentage", "pixels", "weight"]
  578. */
  579. unit: 'weight'
  580. },
  581. /**
  582. * Options for the button appearing when traversing down in a treemap.
  583. *
  584. * @extends plotOptions.treemap.traverseUpButton
  585. * @since 6.0.0
  586. * @apioption plotOptions.sunburst.traverseUpButton
  587. */
  588. /**
  589. * If a point is sliced, moved out from the center, how many pixels
  590. * should it be moved?.
  591. *
  592. * @sample highcharts/plotoptions/sunburst-sliced
  593. * Sliced sunburst
  594. *
  595. * @since 6.0.4
  596. */
  597. slicedOffset: 10
  598. };
  599. // Properties of the Sunburst series.
  600. var sunburstSeries = {
  601. drawDataLabels: noop,
  602. drawPoints: function drawPoints() {
  603. 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) ?
  604. optionsChart.animation :
  605. true), positions = series.center, center = {
  606. x: positions[0],
  607. y: positions[1]
  608. }, innerR = positions[3] / 2, renderer = series.chart.renderer, animateLabels, animateLabelsCalled = false, addedHack = false, hackDataLabelAnimation = !!(animation &&
  609. hasRendered &&
  610. idRoot !== idPreviousRoot &&
  611. series.dataLabelsGroup);
  612. if (hackDataLabelAnimation) {
  613. series.dataLabelsGroup.attr({ opacity: 0 });
  614. animateLabels = function () {
  615. var s = series;
  616. animateLabelsCalled = true;
  617. if (s.dataLabelsGroup) {
  618. s.dataLabelsGroup.animate({
  619. opacity: 1,
  620. visibility: 'visible'
  621. });
  622. }
  623. };
  624. }
  625. points.forEach(function (point) {
  626. var node = point.node, level = mapOptionsToLevel[node.level], shapeExisting = point.shapeExisting || {}, shape = node.shapeArgs || {}, animationInfo, onComplete, visible = !!(node.visible && node.shapeArgs);
  627. if (hasRendered && animation) {
  628. animationInfo = getAnimation(shape, {
  629. center: center,
  630. point: point,
  631. radians: radians,
  632. innerR: innerR,
  633. idRoot: idRoot,
  634. idPreviousRoot: idPreviousRoot,
  635. shapeExisting: shapeExisting,
  636. shapeRoot: shapeRoot,
  637. shapePreviousRoot: shapePreviousRoot,
  638. visible: visible
  639. });
  640. }
  641. else {
  642. // When animation is disabled, attr is called from animation.
  643. animationInfo = {
  644. to: shape,
  645. from: {}
  646. };
  647. }
  648. extend(point, {
  649. shapeExisting: shape,
  650. tooltipPos: [shape.plotX, shape.plotY],
  651. drillId: getDrillId(point, idRoot, nodeMap),
  652. name: '' + (point.name || point.id || point.index),
  653. plotX: shape.plotX,
  654. plotY: shape.plotY,
  655. value: node.val,
  656. isNull: !visible // used for dataLabels & point.draw
  657. });
  658. point.dlOptions = getDlOptions({
  659. point: point,
  660. level: level,
  661. optionsPoint: point.options,
  662. shapeArgs: shape
  663. });
  664. if (!addedHack && visible) {
  665. addedHack = true;
  666. onComplete = animateLabels;
  667. }
  668. point.draw({
  669. animatableAttribs: animationInfo.to,
  670. attribs: extend(animationInfo.from, (!chart.styledMode && series.pointAttribs(point, (point.selected && 'select')))),
  671. onComplete: onComplete,
  672. group: group,
  673. renderer: renderer,
  674. shapeType: 'arc',
  675. shapeArgs: shape
  676. });
  677. });
  678. // Draw data labels after points
  679. // TODO draw labels one by one to avoid addtional looping
  680. if (hackDataLabelAnimation && addedHack) {
  681. series.hasRendered = false;
  682. series.options.dataLabels.defer = true;
  683. Series.prototype.drawDataLabels.call(series);
  684. series.hasRendered = true;
  685. // If animateLabels is called before labels were hidden, then call
  686. // it again.
  687. if (animateLabelsCalled) {
  688. animateLabels();
  689. }
  690. }
  691. else {
  692. Series.prototype.drawDataLabels.call(series);
  693. }
  694. },
  695. pointAttribs: seriesTypes.column.prototype.pointAttribs,
  696. // The layout algorithm for the levels
  697. layoutAlgorithm: layoutAlgorithm,
  698. // Set the shape arguments on the nodes. Recursive from root down.
  699. setShapeArgs: function (parent, parentValues, mapOptionsToLevel) {
  700. var childrenValues = [], level = parent.level + 1, options = mapOptionsToLevel[level],
  701. // Collect all children which should be included
  702. children = parent.children.filter(function (n) {
  703. return n.visible;
  704. }), twoPi = 6.28; // Two times Pi.
  705. childrenValues = this.layoutAlgorithm(parentValues, children, options);
  706. children.forEach(function (child, index) {
  707. 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 ?
  708. { x: values.x, y: values.y } :
  709. getEndPoint(values.x, values.y, angle, radius)), val = (child.val ?
  710. (child.childrenTotal > child.val ?
  711. child.childrenTotal :
  712. child.val) :
  713. child.childrenTotal);
  714. // The inner arc length is a convenience for data label filters.
  715. if (this.points[child.i]) {
  716. this.points[child.i].innerArcLength = radians * values.innerR;
  717. this.points[child.i].outerArcLength = radians * values.r;
  718. }
  719. child.shapeArgs = merge(values, {
  720. plotX: center.x,
  721. plotY: center.y + 4 * Math.abs(Math.cos(angle))
  722. });
  723. child.values = merge(values, {
  724. val: val
  725. });
  726. // If node has children, then call method recursively
  727. if (child.children.length) {
  728. this.setShapeArgs(child, child.values, mapOptionsToLevel);
  729. }
  730. }, this);
  731. },
  732. translate: function translate() {
  733. 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,
  734. // NOTE: updateRootId modifies series.
  735. rootId = updateRootId(series), mapIdToNode = series.nodeMap, mapOptionsToLevel, idTop, nodeRoot = mapIdToNode && mapIdToNode[rootId], nodeTop, tree, values, nodeIds = {};
  736. series.shapeRoot = nodeRoot && nodeRoot.shapeArgs;
  737. // Call prototype function
  738. Series.prototype.translate.call(series);
  739. // @todo Only if series.isDirtyData is true
  740. tree = series.tree = series.getTree();
  741. // Render traverseUpButton, after series.nodeMap i calculated.
  742. series.renderTraverseUpButton(rootId);
  743. mapIdToNode = series.nodeMap;
  744. nodeRoot = mapIdToNode[rootId];
  745. idTop = isString(nodeRoot.parent) ? nodeRoot.parent : '';
  746. nodeTop = mapIdToNode[idTop];
  747. var _a = getLevelFromAndTo(nodeRoot), from = _a.from, to = _a.to;
  748. mapOptionsToLevel = getLevelOptions({
  749. from: from,
  750. levels: series.options.levels,
  751. to: to,
  752. defaults: {
  753. colorByPoint: options.colorByPoint,
  754. dataLabels: options.dataLabels,
  755. levelIsConstant: options.levelIsConstant,
  756. levelSize: options.levelSize,
  757. slicedOffset: options.slicedOffset
  758. }
  759. });
  760. // NOTE consider doing calculateLevelSizes in a callback to
  761. // getLevelOptions
  762. mapOptionsToLevel = calculateLevelSizes(mapOptionsToLevel, {
  763. diffRadius: diffRadius,
  764. from: from,
  765. to: to
  766. });
  767. // TODO Try to combine setTreeValues & setColorRecursive to avoid
  768. // unnecessary looping.
  769. setTreeValues(tree, {
  770. before: cbSetTreeValuesBefore,
  771. idRoot: rootId,
  772. levelIsConstant: options.levelIsConstant,
  773. mapOptionsToLevel: mapOptionsToLevel,
  774. mapIdToNode: mapIdToNode,
  775. points: series.points,
  776. series: series
  777. });
  778. values = mapIdToNode[''].shapeArgs = {
  779. end: radians.end,
  780. r: innerRadius,
  781. start: radians.start,
  782. val: nodeRoot.val,
  783. x: positions[0],
  784. y: positions[1]
  785. };
  786. this.setShapeArgs(nodeTop, values, mapOptionsToLevel);
  787. // Set mapOptionsToLevel on series for use in drawPoints.
  788. series.mapOptionsToLevel = mapOptionsToLevel;
  789. // #10669 - verify if all nodes have unique ids
  790. series.data.forEach(function (child) {
  791. if (nodeIds[child.id]) {
  792. error(31, false, series.chart);
  793. }
  794. // map
  795. nodeIds[child.id] = true;
  796. });
  797. // reset object
  798. nodeIds = {};
  799. },
  800. alignDataLabel: function (point, dataLabel, labelOptions) {
  801. if (labelOptions.textPath && labelOptions.textPath.enabled) {
  802. return;
  803. }
  804. return seriesTypes.treemap.prototype.alignDataLabel
  805. .apply(this, arguments);
  806. },
  807. // Animate the slices in. Similar to the animation of polar charts.
  808. animate: function (init) {
  809. var chart = this.chart, center = [
  810. chart.plotWidth / 2,
  811. chart.plotHeight / 2
  812. ], plotLeft = chart.plotLeft, plotTop = chart.plotTop, attribs, group = this.group;
  813. // Initialize the animation
  814. if (init) {
  815. // Scale down the group and place it in the center
  816. attribs = {
  817. translateX: center[0] + plotLeft,
  818. translateY: center[1] + plotTop,
  819. scaleX: 0.001,
  820. scaleY: 0.001,
  821. rotation: 10,
  822. opacity: 0.01
  823. };
  824. group.attr(attribs);
  825. // Run the animation
  826. }
  827. else {
  828. attribs = {
  829. translateX: plotLeft,
  830. translateY: plotTop,
  831. scaleX: 1,
  832. scaleY: 1,
  833. rotation: 0,
  834. opacity: 1
  835. };
  836. group.animate(attribs, this.options.animation);
  837. }
  838. },
  839. utils: {
  840. calculateLevelSizes: calculateLevelSizes,
  841. getLevelFromAndTo: getLevelFromAndTo,
  842. range: range
  843. }
  844. };
  845. // Properties of the Sunburst series.
  846. var sunburstPoint = {
  847. draw: drawPoint,
  848. shouldDraw: function shouldDraw() {
  849. return !this.isNull;
  850. },
  851. isValid: function isValid() {
  852. return true;
  853. },
  854. getDataLabelPath: function (label) {
  855. var renderer = this.series.chart.renderer, shapeArgs = this.shapeExisting, start = shapeArgs.start, end = shapeArgs.end, angle = start + (end - start) / 2, // arc middle value
  856. upperHalf = angle < 0 &&
  857. angle > -Math.PI ||
  858. angle > Math.PI, r = (shapeArgs.r + (label.options.distance || 0)), moreThanHalf;
  859. // Check if point is a full circle
  860. if (start === -Math.PI / 2 &&
  861. correctFloat(end) === correctFloat(Math.PI * 1.5)) {
  862. start = -Math.PI + Math.PI / 360;
  863. end = -Math.PI / 360;
  864. upperHalf = true;
  865. }
  866. // Check if dataLabels should be render in the
  867. // upper half of the circle
  868. if (end - start > Math.PI) {
  869. upperHalf = false;
  870. moreThanHalf = true;
  871. }
  872. if (this.dataLabelPath) {
  873. this.dataLabelPath = this.dataLabelPath.destroy();
  874. }
  875. this.dataLabelPath = renderer
  876. .arc({
  877. open: true,
  878. longArc: moreThanHalf ? 1 : 0
  879. })
  880. // Add it inside the data label group so it gets destroyed
  881. // with the label
  882. .add(label);
  883. this.dataLabelPath.attr({
  884. start: (upperHalf ? start : end),
  885. end: (upperHalf ? end : start),
  886. clockwise: +upperHalf,
  887. x: shapeArgs.x,
  888. y: shapeArgs.y,
  889. r: (r + shapeArgs.innerR) / 2
  890. });
  891. return this.dataLabelPath;
  892. }
  893. };
  894. /**
  895. * A `sunburst` series. If the [type](#series.sunburst.type) option is
  896. * not specified, it is inherited from [chart.type](#chart.type).
  897. *
  898. * @extends series,plotOptions.sunburst
  899. * @excluding dataParser, dataURL, stack, dataSorting
  900. * @product highcharts
  901. * @requires modules/sunburst.js
  902. * @apioption series.sunburst
  903. */
  904. /**
  905. * @type {Array<number|null|*>}
  906. * @extends series.treemap.data
  907. * @excluding x, y
  908. * @product highcharts
  909. * @apioption series.sunburst.data
  910. */
  911. /**
  912. * @type {Highcharts.SeriesSunburstDataLabelsOptionsObject|Array<Highcharts.SeriesSunburstDataLabelsOptionsObject>}
  913. * @product highcharts
  914. * @apioption series.sunburst.data.dataLabels
  915. */
  916. /**
  917. * The value of the point, resulting in a relative area of the point
  918. * in the sunburst.
  919. *
  920. * @type {number|null}
  921. * @since 6.0.0
  922. * @product highcharts
  923. * @apioption series.sunburst.data.value
  924. */
  925. /**
  926. * Use this option to build a tree structure. The value should be the id of the
  927. * point which is the parent. If no points has a matching id, or this option is
  928. * undefined, then the parent will be set to the root.
  929. *
  930. * @type {string}
  931. * @since 6.0.0
  932. * @product highcharts
  933. * @apioption series.sunburst.data.parent
  934. */
  935. /**
  936. * Whether to display a slice offset from the center. When a sunburst point is
  937. * sliced, its children are also offset.
  938. *
  939. * @sample highcharts/plotoptions/sunburst-sliced
  940. * Sliced sunburst
  941. *
  942. * @type {boolean}
  943. * @default false
  944. * @since 6.0.4
  945. * @product highcharts
  946. * @apioption series.sunburst.data.sliced
  947. */
  948. /**
  949. * @private
  950. * @class
  951. * @name Highcharts.seriesTypes.sunburst
  952. *
  953. * @augments Highcharts.Series
  954. */
  955. seriesType('sunburst', 'treemap', sunburstOptions, sunburstSeries, sunburstPoint);