SunburstSeries.js 35 KB

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