| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017 | /* * * *  This module implements sunburst charts in Highcharts. * *  (c) 2016-2019 Highsoft AS * *  Authors: Jon Arild Nygard * *  License: www.highcharts.com/license * * */'use strict';import H from '../parts/Globals.js';import '../mixins/centered-series.js';import drawPoint from '../mixins/draw-point.js';import mixinTreeSeries from '../mixins/tree-series.js';import '../parts/Series.js';import './treemap.src.js';var CenteredSeriesMixin = H.CenteredSeriesMixin,    Series = H.Series,    extend = H.extend,    getCenter = CenteredSeriesMixin.getCenter,    getColor = mixinTreeSeries.getColor,    getLevelOptions = mixinTreeSeries.getLevelOptions,    getStartAndEndRadians = CenteredSeriesMixin.getStartAndEndRadians,    isBoolean = function (x) {        return typeof x === 'boolean';    },    isNumber = H.isNumber,    isObject = H.isObject,    isString = H.isString,    merge = H.merge,    noop = H.noop,    rad2deg = 180 / Math.PI,    seriesType = H.seriesType,    seriesTypes = H.seriesTypes,    setTreeValues = mixinTreeSeries.setTreeValues,    updateRootId = mixinTreeSeries.updateRootId;// TODO introduce step, which should default to 1.var range = function range(from, to) {    var result = [],        i;    if (isNumber(from) && isNumber(to) && from <= to) {        for (i = from; i <= to; i++) {            result.push(i);        }    }    return result;};/** * @private * @function calculateLevelSizes * * @param {object} levelOptions *        Map of level to its options. * * @param {object} params *        Object containing number parameters `innerRadius` and `outerRadius`. */var calculateLevelSizes = function calculateLevelSizes(levelOptions, params) {    var result,        p = isObject(params) ? params : {},        totalWeight = 0,        diffRadius,        levels,        levelsNotIncluded,        remainingSize,        from,        to;    if (isObject(levelOptions)) {        result = merge({}, levelOptions); // Copy levelOptions        from = isNumber(p.from) ? p.from : 0;        to = isNumber(p.to) ? p.to : 0;        levels = range(from, to);        levelsNotIncluded = Object.keys(result).filter(function (k) {            return levels.indexOf(+k) === -1;        });        diffRadius = remainingSize = isNumber(p.diffRadius) ? p.diffRadius : 0;        // Convert percentage to pixels.        // Calculate the remaining size to divide between "weight" levels.        // Calculate total weight to use in convertion from weight to pixels.        levels.forEach(function (level) {            var options = result[level],                unit = options.levelSize.unit,                value = options.levelSize.value;            if (unit === 'weight') {                totalWeight += value;            } else if (unit === 'percentage') {                options.levelSize = {                    unit: 'pixels',                    value: (value / 100) * diffRadius                };                remainingSize -= options.levelSize.value;            } else if (unit === 'pixels') {                remainingSize -= value;            }        });        // Convert weight to pixels.        levels.forEach(function (level) {            var options = result[level],                weight;            if (options.levelSize.unit === 'weight') {                weight = options.levelSize.value;                result[level].levelSize = {                    unit: 'pixels',                    value: (weight / totalWeight) * remainingSize                };            }        });        // Set all levels not included in interval [from,to] to have 0 pixels.        levelsNotIncluded.forEach(function (level) {            result[level].levelSize = {                value: 0,                unit: 'pixels'            };        });    }    return result;};/** * Find a set of coordinates given a start coordinates, an angle, and a * distance. * * @private * @function getEndPoint * * @param {number} x *        Start coordinate x * * @param {number} y *        Start coordinate y * * @param {number} angle *        Angle in radians * * @param {number} distance *        Distance from start to end coordinates * * @return {Highcharts.SVGAttributes} *         Returns the end coordinates, x and y. */var getEndPoint = function getEndPoint(x, y, angle, distance) {    return {        x: x + (Math.cos(angle) * distance),        y: y + (Math.sin(angle) * distance)    };};var layoutAlgorithm = function layoutAlgorithm(parent, children, options) {    var startAngle = parent.start,        range = parent.end - startAngle,        total = parent.val,        x = parent.x,        y = parent.y,        radius = (            (                options &&                isObject(options.levelSize) &&                isNumber(options.levelSize.value)            ) ?                options.levelSize.value :                0        ),        innerRadius = parent.r,        outerRadius = innerRadius + radius,        slicedOffset = options && isNumber(options.slicedOffset) ?            options.slicedOffset :            0;    return (children || []).reduce(function (arr, child) {        var percentage = (1 / total) * child.val,            radians = percentage * range,            radiansCenter = startAngle + (radians / 2),            offsetPosition = getEndPoint(x, y, radiansCenter, slicedOffset),            values = {                x: child.sliced ? offsetPosition.x : x,                y: child.sliced ? offsetPosition.y : y,                innerR: innerRadius,                r: outerRadius,                radius: radius,                start: startAngle,                end: startAngle + radians            };        arr.push(values);        startAngle = values.end;        return arr;    }, []);};var getDlOptions = function getDlOptions(params) {    // Set options to new object to avoid problems with scope    var point = params.point,        shape = isObject(params.shapeArgs) ? params.shapeArgs : {},        optionsPoint = (            isObject(params.optionsPoint) ?                params.optionsPoint.dataLabels :                {}        ),        optionsLevel = (            isObject(params.level) ?                params.level.dataLabels :                {}        ),        options = merge({            style: {}        }, optionsLevel, optionsPoint),        rotationRad,        rotation,        rotationMode = options.rotationMode;    if (!isNumber(options.rotation)) {        if (rotationMode === 'auto') {            if (                point.innerArcLength < 1 &&                point.outerArcLength > shape.radius            ) {                rotationRad = 0;            } else if (                point.innerArcLength > 1 &&                point.outerArcLength > 1.5 * shape.radius            ) {                rotationMode = 'parallel';            } else {                rotationMode = 'perpendicular';            }        }        if (rotationMode !== 'auto') {            rotationRad = (shape.end - (shape.end - shape.start) / 2);        }        if (rotationMode === 'parallel') {            options.style.width = Math.min(                shape.radius * 2.5,                (point.outerArcLength + point.innerArcLength) / 2            );        } else {            options.style.width = shape.radius;        }        if (            rotationMode === 'perpendicular' &&            point.series.chart.renderer.fontMetrics(options.style.fontSize).h >            point.outerArcLength        ) {            options.style.width = 1;        }        // Apply padding (#8515)        options.style.width = Math.max(            options.style.width - 2 * (options.padding || 0),            1        );        rotation = (rotationRad * rad2deg) % 180;        if (rotationMode === 'parallel') {            rotation -= 90;        }        // Prevent text from rotating upside down        if (rotation > 90) {            rotation -= 180;        } else if (rotation < -90) {            rotation += 180;        }        options.rotation = rotation;    }    // NOTE: alignDataLabel positions the data label differntly when rotation is    // 0. Avoiding this by setting rotation to a small number.    if (options.rotation === 0) {        options.rotation = 0.001;    }    return options;};var getAnimation = function getAnimation(shape, params) {    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 = {            end: shape.end,            start: shape.start,            innerR: shape.innerR,            r: shape.r,            x: shape.x,            y: shape.y        };    if (visible) {        // Animate points in        if (!point.graphic && shapePreviousRoot) {            if (idRoot === point.id) {                from = {                    start: radians.start,                    end: radians.end                };            } else {                from = (shapePreviousRoot.end <= shape.start) ? {                    start: radians.end,                    end: radians.end                } : {                    start: radians.start,                    end: radians.start                };            }            // Animate from center and outwards.            from.innerR = from.r = innerR;        }    } else {        // Animate points out        if (point.graphic) {            if (idPreviousRoot === point.id) {                to = {                    innerR: innerR,                    r: innerR                };            } else if (shapeRoot) {                to = (shapeRoot.end <= shapeExisting.start) ?                    {                        innerR: innerR,                        r: innerR,                        start: radians.end,                        end: radians.end                    } : {                        innerR: innerR,                        r: innerR,                        start: radians.start,                        end: radians.start                    };            }        }    }    return {        from: from,        to: to    };};var getDrillId = function getDrillId(point, idRoot, mapIdToNode) {    var drillId,        node = point.node,        nodeRoot;    if (!node.isLeaf) {        // When it is the root node, the drillId should be set to parent.        if (idRoot === point.id) {            nodeRoot = mapIdToNode[idRoot];            drillId = nodeRoot.parent;        } else {            drillId = point.id;        }    }    return drillId;};var cbSetTreeValuesBefore = function before(node, options) {    var mapIdToNode = options.mapIdToNode,        nodeParent = mapIdToNode[node.parent],        series = options.series,        chart = series.chart,        points = series.points,        point = points[node.i],        colorInfo = getColor(node, {            colors: chart && chart.options && chart.options.colors,            colorIndex: series.colorIndex,            index: options.index,            mapOptionsToLevel: options.mapOptionsToLevel,            parentColor: nodeParent && nodeParent.color,            parentColorIndex: nodeParent && nodeParent.colorIndex,            series: options.series,            siblings: options.siblings        });    node.color = colorInfo.color;    node.colorIndex = colorInfo.colorIndex;    if (point) {        point.color = node.color;        point.colorIndex = node.colorIndex;        // Set slicing on node, but avoid slicing the top node.        node.sliced = (node.id !== options.idRoot) ? point.sliced : false;    }    return node;};/** * A Sunburst displays hierarchical data, where a level in the hierarchy is * represented by a circle. The center represents the root node of the tree. * The visualization bears a resemblance to both treemap and pie charts. * * @sample highcharts/demo/sunburst *         Sunburst chart * * @extends      plotOptions.pie * @excluding    allAreas, clip, colorAxis, compare, compareBase, dataGrouping, *               depth, endAngle, gapSize, gapUnit, ignoreHiddenPoint, *               innerSize, joinBy, legendType, linecap, minSize, *               navigatorOptions, pointRange * @product      highcharts * @optionparent plotOptions.sunburst */var sunburstOptions = {    /**     * Set options on specific levels. Takes precedence over series options,     * but not point options.     *     * @sample highcharts/demo/sunburst     *         Sunburst chart     *     * @type      {Array<*>}     * @apioption plotOptions.sunburst.levels     */    /**     * Can set a `borderColor` on all points which lies on the same level.     *     * @type      {Highcharts.ColorString}     * @apioption plotOptions.sunburst.levels.borderColor     */    /**     * Can set a `borderWidth` on all points which lies on the same level.     *     * @type      {number}     * @apioption plotOptions.sunburst.levels.borderWidth     */    /**     * Can set a `borderDashStyle` on all points which lies on the same level.     *     * @type      {string}     * @apioption plotOptions.sunburst.levels.borderDashStyle     */    /**     * Can set a `color` on all points which lies on the same level.     *     * @type      {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}     * @apioption plotOptions.sunburst.levels.color     */    /**     * Can set a `colorVariation` on all points which lies on the same level.     *     * @apioption plotOptions.sunburst.levels.colorVariation     */    /**     * The key of a color variation. Currently supports `brightness` only.     *     * @type      {string}     * @apioption plotOptions.sunburst.levels.colorVariation.key     */    /**     * The ending value of a color variation. The last sibling will receive this     * value.     *     * @type      {number}     * @apioption plotOptions.sunburst.levels.colorVariation.to     */    /**     * Can set `dataLabels` on all points which lies on the same level.     *     * @type      {object}     * @apioption plotOptions.sunburst.levels.dataLabels     */    /**     * Can set a `levelSize` on all points which lies on the same level.     *     * @type      {object}     * @apioption plotOptions.sunburst.levels.levelSize     */    /**     * Can set a `rotation` on all points which lies on the same level.     *     * @type      {number}     * @apioption plotOptions.sunburst.levels.rotation     */    /**     * Can set a `rotationMode` on all points which lies on the same level.     *     * @type      {string}     * @apioption plotOptions.sunburst.levels.rotationMode     */    /**     * When enabled the user can click on a point which is a parent and     * zoom in on its children.     *     * @sample highcharts/demo/sunburst     *         Allow drill to node     *     * @type      {boolean}     * @default   false     * @apioption plotOptions.sunburst.allowDrillToNode     */    /**     * The center of the sunburst chart relative to the plot area. Can be     * percentages or pixel values.     *     * @sample {highcharts} highcharts/plotoptions/pie-center/     *         Centered at 100, 100     *     * @type    {Array<number|string>}     * @default ["50%", "50%"]     * @product highcharts     */    center: ['50%', '50%'],    colorByPoint: false,    /**     * @extends   plotOptions.series.dataLabels     * @excluding align, allowOverlap, distance, staggerLines, step     */    dataLabels: {        allowOverlap: true,        defer: true,        style: {            textOverflow: 'ellipsis'        },        /**         * Decides how the data label will be rotated relative to the perimeter         * of the sunburst. Valid values are `auto`, `parallel` and         * `perpendicular`. When `auto`, the best fit will be computed for the         * point.         *         * The `series.rotation` option takes precedence over `rotationMode`.         *         * @since      6.0.0         * @validvalue ["auto", "perpendicular", "parallel"]         */        rotationMode: 'auto'    },    /**     * Which point to use as a root in the visualization.     *     * @type {string}     */    rootId: undefined,    /**     * Used together with the levels and `allowDrillToNode` options. When     * set to false the first level visible when drilling is considered     * to be level one. Otherwise the level will be the same as the tree     * structure.     */    levelIsConstant: true,    /**     * Determines the width of the ring per level.     *     * @sample {highcharts} highcharts/plotoptions/sunburst-levelsize/     *         Sunburst with various sizes per level     *     * @since 6.0.5     */    levelSize: {        /**         * The value used for calculating the width of the ring. Its' affect is         * determined by `levelSize.unit`.         *         * @sample {highcharts} highcharts/plotoptions/sunburst-levelsize/         *         Sunburst with various sizes per level         */        value: 1,        /**         * How to interpret `levelSize.value`.         *         * - `percentage` gives a width relative to result of outer radius minus         *   inner radius.         *         * - `pixels` gives the ring a fixed width in pixels.         *         * - `weight` takes the remaining width after percentage and pixels, and         *   distributes it accross all "weighted" levels. The value relative to         *   the sum of all weights determines the width.         *         * @sample {highcharts} highcharts/plotoptions/sunburst-levelsize/         *         Sunburst with various sizes per level         *         * @validvalue ["percentage", "pixels", "weight"]         */        unit: 'weight'    },    /**     * If a point is sliced, moved out from the center, how many pixels     * should it be moved?.     *     * @sample highcharts/plotoptions/sunburst-sliced     *         Sliced sunburst     *     * @since 6.0.4     */    slicedOffset: 10};// Properties of the Sunburst series.var sunburstSeries = {    drawDataLabels: noop, // drawDataLabels is called in drawPoints    drawPoints: function drawPoints() {        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) ?                    optionsChart.animation :                    true            ),            positions = series.center,            center = {                x: positions[0],                y: positions[1]            },            innerR = positions[3] / 2,            renderer = series.chart.renderer,            animateLabels,            animateLabelsCalled = false,            addedHack = false,            hackDataLabelAnimation = !!(                animation &&                hasRendered &&                idRoot !== idPreviousRoot &&                series.dataLabelsGroup            );        if (hackDataLabelAnimation) {            series.dataLabelsGroup.attr({ opacity: 0 });            animateLabels = function () {                var s = series;                animateLabelsCalled = true;                if (s.dataLabelsGroup) {                    s.dataLabelsGroup.animate({                        opacity: 1,                        visibility: 'visible'                    });                }            };        }        points.forEach(function (point) {            var node = point.node,                level = mapOptionsToLevel[node.level],                shapeExisting = point.shapeExisting || {},                shape = node.shapeArgs || {},                animationInfo,                onComplete,                visible = !!(node.visible && node.shapeArgs);            if (hasRendered && animation) {                animationInfo = getAnimation(shape, {                    center: center,                    point: point,                    radians: radians,                    innerR: innerR,                    idRoot: idRoot,                    idPreviousRoot: idPreviousRoot,                    shapeExisting: shapeExisting,                    shapeRoot: shapeRoot,                    shapePreviousRoot: shapePreviousRoot,                    visible: visible                });            } else {                // When animation is disabled, attr is called from animation.                animationInfo = {                    to: shape,                    from: {}                };            }            extend(point, {                shapeExisting: shape, // Store for use in animation                tooltipPos: [shape.plotX, shape.plotY],                drillId: getDrillId(point, idRoot, nodeMap),                name: '' + (point.name || point.id || point.index),                plotX: shape.plotX, // used for data label position                plotY: shape.plotY, // used for data label position                value: node.val,                isNull: !visible // used for dataLabels & point.draw            });            point.dlOptions = getDlOptions({                point: point,                level: level,                optionsPoint: point.options,                shapeArgs: shape            });            if (!addedHack && visible) {                addedHack = true;                onComplete = animateLabels;            }            point.draw({                animatableAttribs: animationInfo.to,                attribs: extend(                    animationInfo.from,                    !chart.styledMode && series.pointAttribs(                        point,                        point.selected && 'select'                    )                ),                onComplete: onComplete,                group: group,                renderer: renderer,                shapeType: 'arc',                shapeArgs: shape            });        });        // Draw data labels after points        // TODO draw labels one by one to avoid addtional looping        if (hackDataLabelAnimation && addedHack) {            series.hasRendered = false;            series.options.dataLabels.defer = true;            Series.prototype.drawDataLabels.call(series);            series.hasRendered = true;            // If animateLabels is called before labels were hidden, then call            // it again.            if (animateLabelsCalled) {                animateLabels();            }        } else {            Series.prototype.drawDataLabels.call(series);        }    },    pointAttribs: seriesTypes.column.prototype.pointAttribs,    // The layout algorithm for the levels    layoutAlgorithm: layoutAlgorithm,    // Set the shape arguments on the nodes. Recursive from root down.    setShapeArgs: function (parent, parentValues, mapOptionsToLevel) {        var childrenValues = [],            level = parent.level + 1,            options = mapOptionsToLevel[level],            // Collect all children which should be included            children = parent.children.filter(function (n) {                return n.visible;            }),            twoPi = 6.28; // Two times Pi.        childrenValues = this.layoutAlgorithm(parentValues, children, options);        children.forEach(function (child, index) {            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 ?                        { x: values.x, y: values.y } :                        getEndPoint(values.x, values.y, angle, radius)                ),                val = (                    child.val ?                        (                            child.childrenTotal > child.val ?                                child.childrenTotal :                                child.val                        ) :                        child.childrenTotal                );            // The inner arc length is a convenience for data label filters.            if (this.points[child.i]) {                this.points[child.i].innerArcLength = radians * values.innerR;                this.points[child.i].outerArcLength = radians * values.r;            }            child.shapeArgs = merge(values, {                plotX: center.x,                plotY: center.y + 4 * Math.abs(Math.cos(angle))            });            child.values = merge(values, {                val: val            });            // If node has children, then call method recursively            if (child.children.length) {                this.setShapeArgs(child, child.values, mapOptionsToLevel);            }        }, this);    },    translate: function translate() {        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,            // NOTE: updateRootId modifies series.            rootId = updateRootId(series),            mapIdToNode = series.nodeMap,            mapOptionsToLevel,            idTop,            nodeRoot = mapIdToNode && mapIdToNode[rootId],            nodeTop,            tree,            values;        series.shapeRoot = nodeRoot && nodeRoot.shapeArgs;        // Call prototype function        Series.prototype.translate.call(series);        // @todo Only if series.isDirtyData is true        tree = series.tree = series.getTree();        mapIdToNode = series.nodeMap;        nodeRoot = mapIdToNode[rootId];        idTop = isString(nodeRoot.parent) ? nodeRoot.parent : '';        nodeTop = mapIdToNode[idTop];        mapOptionsToLevel = getLevelOptions({            from: nodeRoot.level > 0 ? nodeRoot.level : 1,            levels: series.options.levels,            to: tree.height,            defaults: {                colorByPoint: options.colorByPoint,                dataLabels: options.dataLabels,                levelIsConstant: options.levelIsConstant,                levelSize: options.levelSize,                slicedOffset: options.slicedOffset            }        });        // NOTE consider doing calculateLevelSizes in a callback to        // getLevelOptions        mapOptionsToLevel = calculateLevelSizes(mapOptionsToLevel, {            diffRadius: diffRadius,            from: nodeRoot.level > 0 ? nodeRoot.level : 1,            to: tree.height        });        // TODO Try to combine setTreeValues & setColorRecursive to avoid        //  unnecessary looping.        setTreeValues(tree, {            before: cbSetTreeValuesBefore,            idRoot: rootId,            levelIsConstant: options.levelIsConstant,            mapOptionsToLevel: mapOptionsToLevel,            mapIdToNode: mapIdToNode,            points: series.points,            series: series        });        values = mapIdToNode[''].shapeArgs = {            end: radians.end,            r: innerRadius,            start: radians.start,            val: nodeRoot.val,            x: positions[0],            y: positions[1]        };        this.setShapeArgs(nodeTop, values, mapOptionsToLevel);        // Set mapOptionsToLevel on series for use in drawPoints.        series.mapOptionsToLevel = mapOptionsToLevel;    },    // Animate the slices in. Similar to the animation of polar charts.    animate: function (init) {        var chart = this.chart,            center = [                chart.plotWidth / 2,                chart.plotHeight / 2            ],            plotLeft = chart.plotLeft,            plotTop = chart.plotTop,            attribs,            group = this.group;        // Initialize the animation        if (init) {            // Scale down the group and place it in the center            attribs = {                translateX: center[0] + plotLeft,                translateY: center[1] + plotTop,                scaleX: 0.001, // #1499                scaleY: 0.001,                rotation: 10,                opacity: 0.01            };            group.attr(attribs);        // Run the animation        } else {            attribs = {                translateX: plotLeft,                translateY: plotTop,                scaleX: 1,                scaleY: 1,                rotation: 0,                opacity: 1            };            group.animate(attribs, this.options.animation);            // Delete this function to allow it only once            this.animate = null;        }    },    utils: {        calculateLevelSizes: calculateLevelSizes,        range: range    }};// Properties of the Sunburst series.var sunburstPoint = {    draw: drawPoint,    shouldDraw: function shouldDraw() {        var point = this;        return !point.isNull;    }};/** * A `sunburst` series. If the [type](#series.sunburst.type) option is * not specified, it is inherited from [chart.type](#chart.type). * * @extends   series,plotOptions.sunburst * @excluding dataParser, dataURL, stack * @product   highcharts * @apioption series.sunburst *//** * @type      {Array<number|*>} * @extends   series.treemap.data * @excluding x, y * @product   highcharts * @apioption series.sunburst.data *//** * The value of the point, resulting in a relative area of the point * in the sunburst. * * @type      {number} * @since     6.0.0 * @product   highcharts * @apioption series.sunburst.data.value *//** * Use this option to build a tree structure. The value should be the id of the * point which is the parent. If no points has a matching id, or this option is * undefined, then the parent will be set to the root. * * @type      {string} * @since     6.0.0 * @product   highcharts * @apioption series.treemap.data.parent *//**  * Whether to display a slice offset from the center. When a sunburst point is  * sliced, its children are also offset.  *  * @sample highcharts/plotoptions/sunburst-sliced  *         Sliced sunburst  *  * @type      {boolean}  * @default   false  * @since     6.0.4  * @product   highcharts  * @apioption series.sunburst.data.sliced  *//** * @private * @class * @name Highcharts.seriesTypes.sunburst * * @augments Highcharts.Series */seriesType(    'sunburst',    'treemap',    sunburstOptions,    sunburstSeries,    sunburstPoint);
 |