| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018 | /** * (c) 2009-2019 Torstein Honsi * * License: www.highcharts.com/license *//** * Containing the position of a box that should be avoided by labels. * * @interface Highcharts.LabelIntersectBoxObject *//** * @name Highcharts.LabelIntersectBoxObject#bottom * @type {number} *//** * @name Highcharts.LabelIntersectBoxObject#left * @type {number} *//** * @name Highcharts.LabelIntersectBoxObject#right * @type {number} *//** * @name Highcharts.LabelIntersectBoxObject#top * @type {number} *//* * Highcharts module to place labels next to a series in a natural position. * * TODO: * - add column support (box collision detection, boxesToAvoid logic) * - avoid data labels, when data labels above, show series label below. * - add more options (connector, format, formatter) * * https://jsfiddle.net/highcharts/L2u9rpwr/ * https://jsfiddle.net/highcharts/y5A37/ * https://jsfiddle.net/highcharts/264Nm/ * https://jsfiddle.net/highcharts/y5A37/ */'use strict';import H from '../parts/Globals.js';import '../parts/Utilities.js';import '../parts/Chart.js';import '../parts/Series.js';var labelDistance = 3,    addEvent = H.addEvent,    extend = H.extend,    isNumber = H.isNumber,    pick = H.pick,    Series = H.Series,    SVGRenderer = H.SVGRenderer,    Chart = H.Chart;H.setOptions({    /**     * @optionparent plotOptions     */    plotOptions: {        series: {            /**             * Series labels are placed as close to the series as possible in a             * natural way, seeking to avoid other series. The goal of this             * feature is to make the chart more easily readable, like if a             * human designer placed the labels in the optimal position.             *             * The series labels currently work with series types having a             * `graph` or an `area`.             *             * Requires the `series-label.js` module.             *             * @sample highcharts/series-label/line-chart             *         Line chart             * @sample highcharts/demo/streamgraph             *         Stream graph             * @sample highcharts/series-label/stock-chart             *         Stock chart             *             * @since   6.0.0             * @product highcharts highstock gantt             */            label: {                /**                 * Enable the series label per series.                 */                enabled: true,                /**                 * Allow labels to be placed distant to the graph if necessary,                 * and draw a connector line to the graph. Setting this option                 * to true may decrease the performance significantly, since the                 * algorithm with systematically search for open spaces in the                 * whole plot area. Visually, it may also result in a more                 * cluttered chart, though more of the series will be labeled.                 */                connectorAllowed: false,                /**                 * If the label is closer than this to a neighbour graph, draw a                 * connector.                 */                connectorNeighbourDistance: 24,                /**                 * For area-like series, allow the font size to vary so that                 * small areas get a smaller font size. The default applies this                 * effect to area-like series but not line-like series.                 *                 * @type {number|null}                 */                minFontSize: null,                /**                 * For area-like series, allow the font size to vary so that                 * small areas get a smaller font size. The default applies this                 * effect to area-like series but not line-like series.                 *                 * @type {number|null}                 */                maxFontSize: null,                /**                 * Draw the label on the area of an area series. By default it                 * is drawn on the area. Set it to `false` to draw it next to                 * the graph instead.                 *                 * @type {boolean|null}                 */                onArea: null,                /**                 * Styles for the series label. The color defaults to the series                 * color, or a contrast color if `onArea`.                 *                 * @type    {Highcharts.CSSObject}                 * @default {"font-weight": "bold"}                 */                style: {                    /**                     * @ignore                     */                    fontWeight: 'bold'                },                /**                 * An array of boxes to avoid when laying out the labels. Each                 * item has a `left`, `right`, `top` and `bottom` property.                 *                 * @type {Array<Highcharts.LabelIntersectBoxObject>}                 */                boxesToAvoid: []            }        }    }});/** * Counter-clockwise, part of the fast line intersection logic. * * @private * @function ccw * * @param {number} x1 * * @param {number} y1 * * @param {number} x2 * * @param {number} y2 * * @param {number} x3 * * @param {number} y3 * * @return {boolean} */function ccw(x1, y1, x2, y2, x3, y3) {    var cw = ((y3 - y1) * (x2 - x1)) - ((y2 - y1) * (x3 - x1));    return cw > 0 ? true : !(cw < 0);}/** * Detect if two lines intersect. * * @private * @function ccw * * @param {number} x1 * * @param {number} y1 * * @param {number} x2 * * @param {number} y2 * * @param {number} x3 * * @param {number} y3 * * @param {number} x4 * * @param {number} y4 * * @return {boolean} */function intersectLine(x1, y1, x2, y2, x3, y3, x4, y4) {    return ccw(x1, y1, x3, y3, x4, y4) !== ccw(x2, y2, x3, y3, x4, y4) &&        ccw(x1, y1, x2, y2, x3, y3) !== ccw(x1, y1, x2, y2, x4, y4);}/** * Detect if a box intersects with a line. * * @private * @function boxIntersectLine * * @param {number} x * * @param {number} y * * @param {number} w * * @param {number} h * * @param {number} x1 * * @param {number} y1 * * @param {number} x2 * * @param {number} y2 * * @return {boolean} */function boxIntersectLine(x, y, w, h, x1, y1, x2, y2) {    return (        intersectLine(x, y, x + w, y, x1, y1, x2, y2) || // top of label        intersectLine(x + w, y, x + w, y + h, x1, y1, x2, y2) || // right        intersectLine(x, y + h, x + w, y + h, x1, y1, x2, y2) || // bottom        intersectLine(x, y, x, y + h, x1, y1, x2, y2) // left of label    );}/** * General symbol definition for labels with connector. * * @private * @function Highcharts.SVGRenderer#symbols.connector * * @param {number} x * * @param {number} y * * @param {number} w * * @param {number} h * * @param {Highcharts.SymbolOptionsObject} options * * @return {Highcharts.SVGPathArray} */SVGRenderer.prototype.symbols.connector = function (x, y, w, h, options) {    var anchorX = options && options.anchorX,        anchorY = options && options.anchorY,        path,        yOffset,        lateral = w / 2;    if (isNumber(anchorX) && isNumber(anchorY)) {        path = ['M', anchorX, anchorY];        // Prefer 45 deg connectors        yOffset = y - anchorY;        if (yOffset < 0) {            yOffset = -h - yOffset;        }        if (yOffset < w) {            lateral = anchorX < x + (w / 2) ? yOffset : w - yOffset;        }        // Anchor below label        if (anchorY > y + h) {            path.push('L', x + lateral, y + h);        // Anchor above label        } else if (anchorY < y) {            path.push('L', x + lateral, y);        // Anchor left of label        } else if (anchorX < x) {            path.push('L', x, y + h / 2);        // Anchor right of label        } else if (anchorX > x + w) {            path.push('L', x + w, y + h / 2);        }    }    return path || [];};/** * Points to avoid. In addition to actual data points, the label should avoid * interpolated positions. * * @private * @function Highcharts.Series#getPointsOnGraph * * @return {Array<Highcharts.Point>} */Series.prototype.getPointsOnGraph = function () {    if (!this.xAxis && !this.yAxis) {        return;    }    var distance = 16,        points = this.points,        point,        last,        interpolated = [],        i,        deltaX,        deltaY,        delta,        len,        n,        j,        d,        graph = this.graph || this.area,        node = graph.element,        inverted = this.chart.inverted,        xAxis = this.xAxis,        yAxis = this.yAxis,        paneLeft = inverted ? yAxis.pos : xAxis.pos,        paneTop = inverted ? xAxis.pos : yAxis.pos,        onArea = pick(this.options.label.onArea, !!this.area),        translatedThreshold = yAxis.getThreshold(this.options.threshold),        grid = {};    // Push the point to the interpolated points, but only if that position in    // the grid has not been occupied. As a performance optimization, we divide    // the plot area into a grid and only add one point per series (#9815).    function pushDiscrete(point) {        var cellSize = 8,            key = Math.round(point.plotX / cellSize) + ',' +            Math.round(point.plotY / cellSize);        if (!grid[key]) {            grid[key] = 1;            interpolated.push(point);        }    }    // For splines, get the point at length (possible caveat: peaks are not    // correctly detected)    if (        this.getPointSpline &&        node.getPointAtLength &&        !onArea &&        // Not performing well on complex series, node.getPointAtLength is too        // heavy (#9815)        points.length < this.chart.plotSizeX / distance    ) {        // If it is animating towards a path definition, use that briefly, and        // reset        if (graph.toD) {            d = graph.attr('d');            graph.attr({ d: graph.toD });        }        len = node.getTotalLength();        for (i = 0; i < len; i += distance) {            point = node.getPointAtLength(i);            pushDiscrete({                chartX: paneLeft + point.x,                chartY: paneTop + point.y,                plotX: point.x,                plotY: point.y            });        }        if (d) {            graph.attr({ d: d });        }        // Last point        point = points[points.length - 1];        point.chartX = paneLeft + point.plotX;        point.chartY = paneTop + point.plotY;        pushDiscrete(point);    // Interpolate    } else {        len = points.length;        for (i = 0; i < len; i += 1) {            point = points[i];            last = points[i - 1];            // Absolute coordinates so we can compare different panes            point.chartX = paneLeft + point.plotX;            point.chartY = paneTop + point.plotY;            if (onArea) {                // Vertically centered inside area                point.chartCenterY = paneTop + (                    point.plotY +                    pick(point.yBottom, translatedThreshold)                ) / 2;            }            // Add interpolated points            if (i > 0) {                deltaX = Math.abs(point.chartX - last.chartX);                deltaY = Math.abs(point.chartY - last.chartY);                delta = Math.max(deltaX, deltaY);                if (delta > distance) {                    n = Math.ceil(delta / distance);                    for (j = 1; j < n; j += 1) {                        pushDiscrete({                            chartX: last.chartX +                                (point.chartX - last.chartX) * (j / n),                            chartY: last.chartY +                                (point.chartY - last.chartY) * (j / n),                            chartCenterY: last.chartCenterY +                                (point.chartCenterY - last.chartCenterY) *                                (j / n),                            plotX: last.plotX +                                (point.plotX - last.plotX) * (j / n),                            plotY: last.plotY +                                (point.plotY - last.plotY) * (j / n)                        });                    }                }            }            // Add the real point in order to find positive and negative peaks            if (isNumber(point.plotY)) {                pushDiscrete(point);            }        }    }    // Get the bounding box so we can do a quick check first if the bounding    // boxes overlap.    /*    interpolated.bBox = node.getBBox();    interpolated.bBox.x += paneLeft;    interpolated.bBox.y += paneTop;    */    return interpolated;};/** * Overridable function to return series-specific font sizes for the labels. By * default it returns bigger font sizes for series with the greater sum of y * values. * * @private * @function Highcharts.Series#labelFontSize * * @param {number} minFontSize * * @param {number} maxFontSize * * @return {string} */Series.prototype.labelFontSize = function (minFontSize, maxFontSize) {    return minFontSize + (        (this.sum / this.chart.labelSeriesMaxSum) *        (maxFontSize - minFontSize)    ) + 'px';};/** * Check whether a proposed label position is clear of other elements. * * @private * @function Highcharts.Series#checkClearPoint * * @param {number} x * * @param {number} y * * @param {Highcharts.BBoxObject} * * @param {boolean} [checkDistance] * * @return {false|*} */Series.prototype.checkClearPoint = function (x, y, bBox, checkDistance) {    var distToOthersSquared = Number.MAX_VALUE, // distance to other graphs        distToPointSquared = Number.MAX_VALUE,        dist,        connectorPoint,        connectorEnabled = this.options.label.connectorAllowed,        onArea = pick(this.options.label.onArea, !!this.area),        chart = this.chart,        series,        points,        leastDistance = 16,        withinRange,        xDist,        yDist,        i,        j;    function intersectRect(r1, r2) {        return !(r2.left > r1.right ||            r2.right < r1.left ||            r2.top > r1.bottom ||            r2.bottom < r1.top);    }    /**     * Get the weight in order to determine the ideal position. Larger distance     * to other series gives more weight. Smaller distance to the actual point     * (connector points only) gives more weight.     */    function getWeight(distToOthersSquared, distToPointSquared) {        return distToOthersSquared - distToPointSquared;    }    // First check for collision with existing labels    for (i = 0; i < chart.boxesToAvoid.length; i += 1) {        if (intersectRect(chart.boxesToAvoid[i], {            left: x,            right: x + bBox.width,            top: y,            bottom: y + bBox.height        })) {            return false;        }    }    // For each position, check if the lines around the label intersect with any    // of the graphs.    for (i = 0; i < chart.series.length; i += 1) {        series = chart.series[i];        points = series.interpolatedPoints;        if (series.visible && points) {            for (j = 1; j < points.length; j += 1) {                if (                    // To avoid processing, only check intersection if the X                    // values are close to the box.                    points[j].chartX >= x - leastDistance &&                    points[j - 1].chartX <= x + bBox.width + leastDistance                ) {                    // If any of the box sides intersect with the line, return.                    if (boxIntersectLine(                        x,                        y,                        bBox.width,                        bBox.height,                        points[j - 1].chartX,                        points[j - 1].chartY,                        points[j].chartX,                        points[j].chartY                    )) {                        return false;                    }                    // But if it is too far away (a padded box doesn't                    // intersect), also return.                    if (this === series && !withinRange && checkDistance) {                        withinRange = boxIntersectLine(                            x - leastDistance,                            y - leastDistance,                            bBox.width + 2 * leastDistance,                            bBox.height + 2 * leastDistance,                            points[j - 1].chartX,                            points[j - 1].chartY,                            points[j].chartX,                            points[j].chartY                        );                    }                }                // Find the squared distance from the center of the label. On                // area series, avoid its own graph.                if (                    (connectorEnabled || withinRange) &&                    (this !== series || onArea)                ) {                    xDist = x + bBox.width / 2 - points[j].chartX;                    yDist = y + bBox.height / 2 - points[j].chartY;                    distToOthersSquared = Math.min(                        distToOthersSquared,                        xDist * xDist + yDist * yDist                    );                }            }            // Do we need a connector?            if (                !onArea &&                connectorEnabled &&                this === series &&                (                    (checkDistance && !withinRange) ||                    distToOthersSquared < Math.pow(                        this.options.label.connectorNeighbourDistance,                        2                    )                )            ) {                for (j = 1; j < points.length; j += 1) {                    dist = Math.min(                        (                            Math.pow(x + bBox.width / 2 - points[j].chartX, 2) +                            Math.pow(y + bBox.height / 2 - points[j].chartY, 2)                        ),                        (                            Math.pow(x - points[j].chartX, 2) +                            Math.pow(y - points[j].chartY, 2)                        ),                        (                            Math.pow(x + bBox.width - points[j].chartX, 2) +                            Math.pow(y - points[j].chartY, 2)                        ),                        (                            Math.pow(x + bBox.width - points[j].chartX, 2) +                            Math.pow(y + bBox.height - points[j].chartY, 2)                        ),                        (                            Math.pow(x - points[j].chartX, 2) +                            Math.pow(y + bBox.height - points[j].chartY, 2)                        )                    );                    if (dist < distToPointSquared) {                        distToPointSquared = dist;                        connectorPoint = points[j];                    }                }                withinRange = true;            }        }    }    return !checkDistance || withinRange ? {        x: x,        y: y,        weight: getWeight(            distToOthersSquared,            connectorPoint ? distToPointSquared : 0        ),        connectorPoint: connectorPoint    } : false;};/** * The main initiator method that runs on chart level after initiation and * redraw. It runs in  a timeout to prevent locking, and loops over all series, * taking all series and labels into account when placing the labels. * * @private * @function Highcharts.Chart#drawSeriesLabels */Chart.prototype.drawSeriesLabels = function () {    // console.time('drawSeriesLabels');    var chart = this,        labelSeries = this.labelSeries;    chart.boxesToAvoid = [];    // Build the interpolated points    labelSeries.forEach(function (series) {        series.interpolatedPoints = series.getPointsOnGraph();        (series.options.label.boxesToAvoid || []).forEach(function (box) {            chart.boxesToAvoid.push(box);        });    });    chart.series.forEach(function (series) {        if (!series.xAxis && !series.yAxis) {            return;        }        var bBox,            x,            y,            results = [],            clearPoint,            i,            best,            labelOptions = series.options.label,            inverted = chart.inverted,            paneLeft = inverted ? series.yAxis.pos : series.xAxis.pos,            paneTop = inverted ? series.xAxis.pos : series.yAxis.pos,            paneWidth = chart.inverted ? series.yAxis.len : series.xAxis.len,            paneHeight = chart.inverted ? series.xAxis.len : series.yAxis.len,            points = series.interpolatedPoints,            onArea = pick(labelOptions.onArea, !!series.area),            label = series.labelBySeries,            minFontSize = labelOptions.minFontSize,            maxFontSize = labelOptions.maxFontSize;        function insidePane(x, y, bBox) {            return x > paneLeft && x <= paneLeft + paneWidth - bBox.width &&                y >= paneTop && y <= paneTop + paneHeight - bBox.height;        }        function destroyLabel() {            if (label) {                series.labelBySeries = label.destroy();            }        }        if (series.visible && !series.isSeriesBoosting && points) {            if (!label) {                series.labelBySeries = label = chart.renderer                    .label(series.name, 0, -9999, 'connector')                    .addClass(                        'highcharts-series-label ' +                        'highcharts-series-label-' + series.index + ' ' +                        (series.options.className || '')                    )                    .css(extend({                        color: onArea ?                            chart.renderer.getContrast(series.color) :                            series.color                    }, series.options.label.style));                // Adapt label sizes to the sum of the data                if (minFontSize && maxFontSize) {                    label.css({                        fontSize: series.labelFontSize(minFontSize, maxFontSize)                    });                }                label                    .attr({                        padding: 0,                        opacity: chart.renderer.forExport ? 1 : 0,                        stroke: series.color,                        'stroke-width': 1,                        zIndex: 3                    })                    .add()                    .animate({ opacity: 1 }, { duration: 200 });            }            bBox = label.getBBox();            bBox.width = Math.round(bBox.width);            // Ideal positions are centered above or below a point on right side            // of chart            for (i = points.length - 1; i > 0; i -= 1) {                if (onArea) {                    // Centered                    x = points[i].chartX - bBox.width / 2;                    y = points[i].chartCenterY - bBox.height / 2;                    if (insidePane(x, y, bBox)) {                        best = series.checkClearPoint(                            x,                            y,                            bBox                        );                    }                    if (best) {                        results.push(best);                    }                } else {                    // Right - up                    x = points[i].chartX + labelDistance;                    y = points[i].chartY - bBox.height - labelDistance;                    if (insidePane(x, y, bBox)) {                        best = series.checkClearPoint(                            x,                            y,                            bBox,                            true                        );                    }                    if (best) {                        results.push(best);                    }                    // Right - down                    x = points[i].chartX + labelDistance;                    y = points[i].chartY + labelDistance;                    if (insidePane(x, y, bBox)) {                        best = series.checkClearPoint(                            x,                            y,                            bBox,                            true                        );                    }                    if (best) {                        results.push(best);                    }                    // Left - down                    x = points[i].chartX - bBox.width - labelDistance;                    y = points[i].chartY + labelDistance;                    if (insidePane(x, y, bBox)) {                        best = series.checkClearPoint(                            x,                            y,                            bBox,                            true                        );                    }                    if (best) {                        results.push(best);                    }                    // Left - up                    x = points[i].chartX - bBox.width - labelDistance;                    y = points[i].chartY - bBox.height - labelDistance;                    if (insidePane(x, y, bBox)) {                        best = series.checkClearPoint(                            x,                            y,                            bBox,                            true                        );                    }                    if (best) {                        results.push(best);                    }                }            }            // Brute force, try all positions on the chart in a 16x16 grid            if (labelOptions.connectorAllowed && !results.length && !onArea) {                for (                    x = paneLeft + paneWidth - bBox.width;                    x >= paneLeft;                    x -= 16                ) {                    for (                        y = paneTop;                        y < paneTop + paneHeight - bBox.height;                        y += 16                    ) {                        clearPoint = series.checkClearPoint(x, y, bBox, true);                        if (clearPoint) {                            results.push(clearPoint);                        }                    }                }            }            if (results.length) {                results.sort(function (a, b) {                    return b.weight - a.weight;                });                best = results[0];                chart.boxesToAvoid.push({                    left: best.x,                    right: best.x + bBox.width,                    top: best.y,                    bottom: best.y + bBox.height                });                // Move it if needed                var dist = Math.sqrt(                    Math.pow(Math.abs(best.x - label.x), 2),                    Math.pow(Math.abs(best.y - label.y), 2)                );                if (dist) {                    // Move fast and fade in - pure animation movement is                    // distractive...                    var attr = {                            opacity: chart.renderer.forExport ? 1 : 0,                            x: best.x,                            y: best.y                        },                        anim = {                            opacity: 1                        };                    // ... unless we're just moving a short distance                    if (dist <= 10) {                        anim = {                            x: attr.x,                            y: attr.y                        };                        attr = {};                    }                    series.labelBySeries                        .attr(extend(attr, {                            anchorX: best.connectorPoint &&                                best.connectorPoint.plotX + paneLeft,                            anchorY: best.connectorPoint &&                                best.connectorPoint.plotY + paneTop                        }))                        .animate(anim);                    // Record closest point to stick to for sync redraw                    series.options.kdNow = true;                    series.buildKDTree();                    var closest = series.searchPoint({                        chartX: best.x,                        chartY: best.y                    }, true);                    label.closest = [                        closest,                        best.x - closest.plotX,                        best.y - closest.plotY                    ];                }            } else {                destroyLabel();            }        } else {            destroyLabel();        }    });    // console.timeEnd('drawSeriesLabels');};/** * Prepare drawing series labels. * * @private * @function drawLabels */function drawLabels(e) {    var chart = this,        delay = Math.max(            H.animObject(chart.renderer.globalAnimation).duration,            250        );    chart.labelSeries = [];    chart.labelSeriesMaxSum = 0;    H.clearTimeout(chart.seriesLabelTimer);    // Which series should have labels    chart.series.forEach(function (series) {        var options = series.options.label,            label = series.labelBySeries,            closest = label && label.closest;        if (            options.enabled &&            series.visible &&            (series.graph || series.area) &&            !series.isSeriesBoosting        ) {            chart.labelSeries.push(series);            if (options.minFontSize && options.maxFontSize) {                series.sum = series.yData.reduce(function (pv, cv) {                    return (pv || 0) + (cv || 0);                }, 0);                chart.labelSeriesMaxSum = Math.max(                    chart.labelSeriesMaxSum,                    series.sum                );            }            // The labels are processing heavy, wait until the animation is done            if (e.type === 'load') {                delay = Math.max(                    delay,                    H.animObject(series.options.animation).duration                );            }            // Keep the position updated to the axis while redrawing            if (closest) {                if (closest[0].plotX !== undefined) {                    label.animate({                        x: closest[0].plotX + closest[1],                        y: closest[0].plotY + closest[2]                    });                } else {                    label.attr({ opacity: 0 });                }            }        }    });    chart.seriesLabelTimer = H.syncTimeout(function () {        if (chart.series && chart.labelSeries) { // #7931, chart destroyed            chart.drawSeriesLabels();        }    }, chart.renderer.forExport ? 0 : delay);}// Leave both events, we handle animation differently (#9815)addEvent(Chart, 'load', drawLabels);addEvent(Chart, 'redraw', drawLabels);
 |