/* *
 *
 *  (c) 2010-2026 Highsoft AS
 *  Author: Torstein Hønsi
 *
 *  Integration of this software requires a license.
 *  - For commercial use, see www.highcharts.com/license
 *  - For non-commercial, see www.highcharts.com/license-eula
 *
 *
 * */
'use strict';
import A from '../Animation/AnimationUtilities.js';
const { getDeferredAnimation } = A;
import F from '../Templating.js';
const { format } = F;
import { defined, extend, fireEvent, getAlignFactor, isArray, isNumber, isString, merge, objectEach, pick, pInt, splat } from '../../Shared/Utilities.js';
/* *
 *
 *  Composition
 *
 * */
var DataLabel;
(function (DataLabel) {
    /* *
     *
     *  Declarations
     *
     * */
    /* *
     *
     *  Functions
     *
     * */
    /**
     * Check if this series has data labels, either a series-level setting, or
     * individual. In case of individual point labels, this method is overridden
     * to always return true.
     * @internal
     */
    function hasDataLabels() {
        return mergedDataLabelOptions(this)
            .some((o) => o?.enabled);
    }
    /**
     * Align each individual data label.
     * @internal
     */
    function alignDataLabel(point, dataLabel, options, alignTo, isNew) {
        const series = this, { chart } = this, inverted = this.isCartesian && chart.inverted, { condemned, origin, plotX, plotY } = point, { crop = true, distance, overflow = 'justify', rotation = 0 } = options, alignFactor = getAlignFactor(options.align), verticalAlignFactor = getAlignFactor(options.verticalAlign), justify = rotation === 0 && !condemned && overflow === 'justify', pos = point.pos(), isInsidePlot = defined(plotX) &&
            defined(plotY) &&
            chart.isInsidePlot(plotX, Math.round(plotY), {
                inverted,
                paneCoordinates: true,
                series
            });
        // Math.round for rounding errors (#2683), alignTo to allow column
        // labels (#2700)
        let visible = this.visible &&
            point.visible &&
            defined(plotX) &&
            (series.forceDL ||
                condemned || // Allow it to fade out
                isInsidePlot ||
                (
                // If the data label is inside the align box, it is enough
                // that parts of the align box is inside the plot area
                // (#12370). When stacking, it is always inside regardless
                // of the option (#15148).
                pick(options.inside, !!this.options.stacking) &&
                    alignTo &&
                    chart.isInsidePlot(plotX, inverted ?
                        alignTo.x + 1 :
                        alignTo.y + alignTo.height - 1, {
                        inverted,
                        paneCoordinates: true,
                        series
                    })));
        if (pos) {
            const bBox = dataLabel.getBBox(), unrotatedbBox = dataLabel.getBBox(void 0, 0);
            // The alignment box is a singular point
            alignTo = extend({
                x: pos[0],
                y: Math.round(pos[1]),
                width: 0,
                height: 0
            }, alignTo || {});
            // Align to plot edges
            if (options.alignTo === 'plotEdges' && series.isCartesian) {
                alignTo[inverted ? 'x' : 'y'] = 0;
                alignTo[inverted ? 'width' : 'height'] = this.yAxis?.len || 0;
            }
            // Add the text size for alignment calculation
            extend(options, {
                width: bBox.width,
                height: bBox.height
            });
            // Apply the distance
            let distX = 0, distY = 0;
            if (isNumber(distance) && !options.inside) {
                distX = distance * (1 - 2 * alignFactor);
                distY = distance * (1 - 2 * verticalAlignFactor);
            }
            // Align the label to the adjusted box with for unrotated bBox due
            // to rotationOrigin, which is based on unrotated label
            dataLabel.align(merge(options, {
                x: (options.x || 0) + distX,
                y: (options.y || 0) + distY,
                width: unrotatedbBox.width,
                height: unrotatedbBox.height
            }), false, alignTo, false);
            // Record for later use in justifyDataLabel
            dataLabel.distX = distX;
            dataLabel.distY = distY;
            // Modify for rotation
            dataLabel.alignAttr.x += alignFactor *
                (unrotatedbBox.width - bBox.width);
            dataLabel.alignAttr.y += verticalAlignFactor *
                (unrotatedbBox.height - bBox.height);
            // The placement attributes before justifyDataLabel correction
            const x = dataLabel.alignAttr.x +
                (bBox.width - unrotatedbBox.width) / 2, y = dataLabel.alignAttr.y +
                (bBox.height - unrotatedbBox.height) / 2;
            // Handle the data label position for entrance animation. Simply
            // offset the computed position by the difference between current
            // position and the origin.
            if (origin) {
                const originPos = point.pos(false, origin.x, origin.y);
                if (originPos) {
                    const offset = [
                        originPos[0] - pos[0],
                        originPos[1] - pos[1]
                    ];
                    if (series.is('column') || point.plotHigh) {
                        offset[inverted ? 0 : 1] = 0;
                    }
                    dataLabel.attr({
                        x: x + offset[0],
                        y: y + offset[1],
                        // Start at non-zero to avoid overlapping logic treating
                        // it as hidden
                        opacity: 0.01
                    });
                    dataLabel.placed = true;
                    isNew = false;
                }
            }
            // Set the position before potential justification
            const placeAttribs = {
                'text-align': dataLabel.alignAttr['text-align'] || 'center',
                x,
                y,
                rotationOriginX: (dataLabel.width || 0) / 2,
                rotationOriginY: (dataLabel.height || 0) / 2
            };
            if (condemned || !visible) {
                placeAttribs.opacity = 0;
            }
            dataLabel[dataLabel.placed ? 'animate' : 'attr'](placeAttribs);
            // Uncomment this block to visualize the bounding boxes used for
            // determining visibility
            // chart.renderer.rect(
            //     (dataLabel.alignAttr.x || 0) + chart.plotLeft,
            //     (dataLabel.alignAttr.y || 0) + chart.plotTop,
            //     bBox.width,
            //     bBox.height
            // ).attr({
            //     stroke: 'rgba(0, 0, 0, 0.3)',
            //     'stroke-width': 1,
            //     zIndex: 20
            // }).add();
            // chart.renderer.circle(
            //     chart.plotLeft + pick(dataLabel.alignAttr.x, 0),
            //     chart.plotTop + pick(dataLabel.alignAttr.y, 0),
            //     2
            // ).attr({
            //     fill: 'red',
            //     zIndex: 20
            // }).add();
            if (justify && visible && alignTo.height >= 0) { // #8830
                this.justifyDataLabel(dataLabel, options, dataLabel.alignAttr, bBox, alignTo, isNew);
            }
            else if (crop && !condemned) {
                const { x, y } = dataLabel.alignAttr, correction = 1;
                // Check if the dataLabel should be visible.
                visible =
                    chart.isInsidePlot(x, y, {
                        paneCoordinates: true,
                        series
                    }) &&
                        chart.isInsidePlot(x + bBox.width - correction, y + bBox.height - correction, {
                            paneCoordinates: true,
                            series
                        });
            }
            // When we're using a shape, make it possible with a connector or an
            // arrow pointing to this point
            if (options.shape && !rotation) {
                dataLabel[isNew ? 'attr' : 'animate']({
                    anchorX: pos[0],
                    anchorY: pos[1]
                });
            }
        }
        // Show or hide based on the final aligned position
        dataLabel[isNew ? 'attr' : 'animate']({
            visibility: visible ? 'inherit' : 'hidden'
        });
        dataLabel.placed = visible;
    }
    /**
     * Handle the dataLabels.filter option.
     * @internal
     */
    function applyFilter(point, options) {
        const filter = options.filter;
        if (filter) {
            const op = filter.operator, prop = point[filter.property], val = filter.value;
            if ((op === '>' && prop > val) ||
                (op === '<' && prop < val) ||
                (op === '>=' && prop >= val) ||
                (op === '<=' && prop <= val) ||
                (op === '==' && prop == val) || // eslint-disable-line eqeqeq
                (op === '===' && prop === val) ||
                (op === '!=' && prop != val) || // eslint-disable-line eqeqeq
                (op === '!==' && prop !== val)) {
                return true;
            }
            return false;
        }
        return true;
    }
    /**
     * Compose the data label composition onto a series class.
     *
     * @internal
     * @function compose
     *
     * @param {Highcharts.Series} SeriesClass
     * The series class to compose onto.
     *
     * @return {void}
     */
    function compose(SeriesClass) {
        const seriesProto = SeriesClass.prototype;
        if (!seriesProto.initDataLabels) {
            seriesProto.initDataLabels = initDataLabels;
            seriesProto.initDataLabelsGroup = initDataLabelsGroup;
            seriesProto.alignDataLabel = alignDataLabel;
            seriesProto.drawDataLabels = drawDataLabels;
            seriesProto.justifyDataLabel = justifyDataLabel;
            seriesProto.mergeArrays = mergeArrays;
            seriesProto.hasDataLabels = hasDataLabels;
        }
    }
    DataLabel.compose = compose;
    /**
     * Create the SVGElement group for dataLabels.
     *
     * @internal
     * @function initDataLabelsGroup
     *
     * @param {number} index
     * The index of the data labels group.
     * @param {Highcharts.DataLabelOptions} [dataLabelsOptions]
     * Data label options for the group.
     *
     * @return {Highcharts.SVGElement}
     * The SVGElement group.
     */
    function initDataLabelsGroup(index, dataLabelsOptions) {
        fireEvent(this, 'initDataLabelsGroup', {
            index,
            zIndex: dataLabelsOptions?.zIndex
        });
        // Existing group or first time
        this.dataLabelsGroup = this.dataLabelsGroups?.[index];
        const group = this.plotGroup('dataLabelsGroup', 'data-labels', this.hasRendered ? 'inherit' : 'hidden', // #5133, #10220
        dataLabelsOptions?.zIndex ?? 6, this.dataLabelsParentGroups?.[index]);
        this.dataLabelsGroups || (this.dataLabelsGroups = []);
        this.dataLabelsGroups[index] = group;
        // Keep reference to the 1st group
        this.dataLabelsGroup = this.dataLabelsGroups[0];
        return group;
    }
    /**
     * Init the data labels with the correct animation.
     *
     * @internal
     * @function initDataLabels
     *
     * @param {number} index
     * The index of the data labels group.
     * @param {Highcharts.AnimationOptions} animationConfig
     * The animation options.
     * @param {Highcharts.DataLabelOptions} [dataLabelsOptions]
     * Data label options for the group.
     *
     * @return {Highcharts.SVGElement}
     * The SVGElement group.
     */
    function initDataLabels(index, animationConfig, dataLabelsOptions) {
        const series = this, hasRendered = !!series.hasRendered;
        // Create a separate group for the data labels to avoid rotation
        const dataLabelsGroup = this.initDataLabelsGroup(index, dataLabelsOptions)
            .attr({ opacity: +hasRendered }); // #3300
        if (!hasRendered && dataLabelsGroup) {
            if (series.visible) { // #2597, #3023, #3024
                dataLabelsGroup.show();
            }
            if (series.options.animation) {
                dataLabelsGroup.animate({ opacity: 1 }, animationConfig);
            }
            else {
                dataLabelsGroup.attr({ opacity: 1 });
            }
        }
        return dataLabelsGroup;
    }
    /**
     * Draw the data labels.
     * @internal
     */
    function drawDataLabels(points) {
        points = points || this.points;
        const series = this, chart = series.chart, seriesOptions = series.options, renderer = chart.renderer, { backgroundColor, plotBackgroundColor } = chart.options.chart, contrastColor = renderer.getContrast((isString(plotBackgroundColor) && plotBackgroundColor) ||
            (isString(backgroundColor) && backgroundColor) ||
            'var(--highcharts-background-color)'), groupByIndex = [], seriesDlOptions = mergedDataLabelOptions(series);
        // Resolve the animation
        const { animation, defer } = seriesDlOptions[0], animationConfig = defer ?
            getDeferredAnimation(chart, animation, series) :
            { defer: 0, duration: 0 };
        fireEvent(this, 'drawDataLabels');
        if (series.hasDataLabels?.()) {
            // Make the labels for each point
            points.concat(series.condemnedPoints).forEach((point) => {
                const dataLabels = point.dataLabels || [], pointColor = point.color || series.color, 
                // Merge in series options for the point.
                // @note
                // dataLabelAttribs (like pointAttribs) would eradicate the
                // need for dlOptions, and simplify the section below.
                pointOptions = splat(mergeArrays(seriesDlOptions, 
                // The dlOptions prop is used in treemap
                point.dlOptions || point.options?.dataLabels));
                // Handle each individual data label for this point
                pointOptions.forEach((labelOptions, i) => {
                    // Options for one dataLabel
                    const labelEnabled = (labelOptions.enabled &&
                        (point.visible || point.dataLabelOnHidden) &&
                        // #2282, #4641, #7112, #10049
                        (!point.isNull || point.dataLabelOnNull) &&
                        applyFilter(point, labelOptions)), { backgroundColor, borderColor, distance, style = {} } = labelOptions, padding = splat(labelOptions.padding || 0);
                    let formatString, labelText, rotation, attr = {}, dataLabel = dataLabels[i], isNew = !dataLabel, labelBgColor;
                    if (labelEnabled) {
                        // Create individual options structure that can be
                        // extended without affecting others
                        formatString = pick(labelOptions[point.formatPrefix + 'Format'], labelOptions.format);
                        labelText = defined(formatString) ?
                            format(formatString, point, chart) :
                            (labelOptions[point.formatPrefix + 'Formatter'] ||
                                labelOptions.formatter).call(point, labelOptions, point);
                        rotation = labelOptions.rotation;
                        if (!chart.styledMode) {
                            // Determine the color
                            style.color = pick(labelOptions.color, style.color, isString(series.color) ? series.color : void 0, 'var(--highcharts-neutral-color-100)');
                            // Get automated contrast color
                            if (style.color === 'contrast') {
                                if (backgroundColor !== 'none') {
                                    labelBgColor = backgroundColor;
                                }
                                point.contrastColor = renderer.getContrast((labelBgColor !== 'auto' &&
                                    labelBgColor !== 'contrast' &&
                                    isString(labelBgColor) &&
                                    labelBgColor) ||
                                    (isString(pointColor) ? pointColor : ''));
                                style.color = ((labelBgColor &&
                                    labelBgColor !== 'contrast') || // #20007
                                    labelOptions.inside ||
                                    pInt(distance || 0) < 0 ||
                                    seriesOptions.stacking) ?
                                    point.contrastColor :
                                    contrastColor;
                            }
                            else {
                                delete point.contrastColor;
                            }
                            if (seriesOptions.cursor) {
                                style.cursor = seriesOptions.cursor;
                            }
                        }
                        attr = {
                            r: labelOptions.borderRadius ?? 3,
                            rotation,
                            padding: padding[0],
                            paddingLeft: padding[3 % padding.length],
                            paddingRight: padding[1 % padding.length],
                            zIndex: 1
                        };
                        if (!chart.styledMode) {
                            attr.fill = backgroundColor === 'auto' ?
                                point.color :
                                backgroundColor;
                            attr.stroke = borderColor === 'auto' ?
                                point.color :
                                borderColor;
                            attr['stroke-width'] = labelOptions.borderWidth;
                        }
                        // Remove unused attributes (#947)
                        objectEach(attr, (val, name) => {
                            if (typeof val === 'undefined') {
                                delete attr[name];
                            }
                        });
                    }
                    // If the point is outside the plot area, or the label
                    // changes properties that we cannot change, destroy it and
                    // build a new one below. #678, #820.
                    if (dataLabel && (!labelEnabled ||
                        !defined(labelText) ||
                        // Changed useHTML value
                        !!(dataLabel.div ||
                            dataLabel.text?.foreignObject) !== !!labelOptions.useHTML ||
                        (
                        // Change from no rotation to rotation and
                        // vice versa. Don't use defined() because
                        // rotation = 0 means also rotation = undefined
                        (!dataLabel.rotation ||
                            !labelOptions.rotation) &&
                            dataLabel.rotation !== labelOptions.rotation))) {
                        dataLabel = void 0;
                        isNew = true;
                    }
                    // Individual labels are disabled if the are explicitly
                    // disabled in the point options, or if they fall outside
                    // the plot area.
                    if (labelEnabled &&
                        defined(labelText) &&
                        labelText !== '') {
                        if (!dataLabel) {
                            // Create new label element
                            dataLabel = renderer.label(labelText, 0, 0, labelOptions.shape, void 0, void 0, labelOptions.useHTML, void 0, 'data-label');
                            dataLabel.addClass(' highcharts-data-label-color-' +
                                point.colorIndex +
                                ' ' + (labelOptions.className || '') +
                                ( // #3398
                                labelOptions.useHTML ?
                                    ' highcharts-tracker' :
                                    ''));
                        }
                        else {
                            // Use old element and just update text
                            attr.text = labelText;
                        }
                        // Store data label options for later access
                        if (dataLabel) {
                            dataLabel.options = labelOptions;
                            dataLabel.attr(attr);
                            if (!chart.styledMode) {
                                // Styles must be applied before add in order to
                                // read text bounding box
                                dataLabel.css(style).shadow(labelOptions.shadow);
                            }
                            else if (style.width) {
                                // In styled mode with a width property set,
                                // the width should be applied to the
                                // dataLabel. (#20499). These properties affect
                                // layout and must be applied also in styled
                                // mode.
                                dataLabel.css({
                                    width: style.width,
                                    textOverflow: style.textOverflow,
                                    whiteSpace: style.whiteSpace
                                });
                            }
                            fireEvent(dataLabel, 'beforeAddingDataLabel', { labelOptions, point });
                            // On the first occurrence, create a dataLabelsGroup
                            // for each data labels config (#24626)
                            const dataLabelsGroup = groupByIndex[i] = (groupByIndex[i] || this.initDataLabels(i, animationConfig, labelOptions));
                            if (!dataLabel.added) {
                                dataLabel.add(dataLabelsGroup);
                            }
                            // Now the data label is created and placed at 0,0,
                            // so we need to align it
                            series.alignDataLabel(point, dataLabel, labelOptions, void 0, isNew);
                            dataLabel.isActive = true;
                            if (dataLabels[i] && dataLabels[i] !== dataLabel) {
                                dataLabels[i].destroy();
                            }
                            dataLabels[i] = dataLabel;
                        }
                    }
                });
                // Destroy and remove the inactive ones
                let j = dataLabels.length;
                while (j--) {
                    // The item can be undefined if a disabled data label is
                    // succeeded by an enabled one (#19457)
                    if (!dataLabels[j]?.isActive) {
                        dataLabels[j]?.destroy();
                        dataLabels.splice(j, 1);
                    }
                    else {
                        dataLabels[j].isActive = false;
                    }
                }
                // Write back
                point.dataLabel = dataLabels[0];
                point.dataLabels = dataLabels;
            });
        }
        fireEvent(this, 'afterDrawDataLabels');
    }
    /**
     * If data labels fall partly outside the plot area, align them back in, in
     * a way that doesn't hide the point.
     * @internal
     */
    function justifyDataLabel(dataLabel, options, alignAttr, bBox, alignTo, isNew) {
        const chart = this.chart, { align, verticalAlign } = options, { distX = 0, distY = 0 } = dataLabel, padding = dataLabel.box ? 0 : (dataLabel.padding || 0), horizontalAxis = chart.inverted ? this.yAxis : this.xAxis, horizontalAxisShift = horizontalAxis ?
            horizontalAxis.left - chart.plotLeft : 0, verticalAxis = chart.inverted ? this.xAxis : this.yAxis, verticalAxisShift = verticalAxis ?
            verticalAxis.top - chart.plotTop : 0;
        let { x = 0, y = 0 } = options, off, justifiedX, justifiedY;
        // Off left
        off = (alignAttr.x || 0) - distX + padding + horizontalAxisShift;
        if (off < 0) {
            if (align === 'right' && x >= 0) {
                options.align = 'left';
                options.inside = true;
                x -= distX;
            }
            else {
                x -= off;
            }
            justifiedX = true;
        }
        // Off right
        off = (alignAttr.x || 0) + bBox.width - distX - padding +
            horizontalAxisShift;
        if (off > chart.plotWidth) {
            if (align === 'left' && x <= 0) {
                options.align = 'right';
                options.inside = true;
                x -= distX;
            }
            else {
                x += chart.plotWidth - off;
            }
            justifiedX = true;
        }
        // Off top
        off = (alignAttr.y || 0) - distY + padding + verticalAxisShift;
        if (off < 0) {
            if (verticalAlign === 'bottom' && y >= 0) {
                options.verticalAlign = 'top';
                options.inside = true;
                y -= distY;
            }
            else {
                y -= off;
            }
            justifiedY = true;
        }
        // Off bottom
        off = (alignAttr.y || 0) + bBox.height - distY - padding +
            verticalAxisShift;
        if (off > chart.plotHeight) {
            if (verticalAlign === 'top' && y <= 0) {
                options.verticalAlign = 'bottom';
                options.inside = true;
                y -= distY;
            }
            else {
                y += chart.plotHeight - off;
            }
            justifiedY = true;
        }
        if (justifiedX || justifiedY) {
            options.x = justifiedX ? x : x + distX;
            options.y = justifiedY ? y : y + distY;
            dataLabel.placed = !isNew;
            dataLabel.align(options, void 0, alignTo);
        }
        return justifiedX || justifiedY;
    }
    /**
     * Merge two objects that can be arrays. If one of them is an array, the
     * other is merged into each element. If both are arrays, each element is
     * merged by index. If neither are arrays, we use normal merge.
     * @internal
     */
    function mergeArrays(one, two) {
        let res = [], i;
        if (isArray(one) && !isArray(two)) {
            res = one.map(function (el) {
                return merge(el, two);
            });
        }
        else if (isArray(two) && !isArray(one)) {
            res = two.map(function (el) {
                return merge(one, el);
            });
        }
        else if (!isArray(one) && !isArray(two)) {
            res = merge(one, two);
        }
        else if (isArray(one) && isArray(two)) {
            i = Math.max(one.length, two.length);
            while (i--) {
                res[i] = merge(one[i], two[i]);
            }
        }
        return res;
    }
    /**
     * Merge plotOptions and series options for dataLabels.
     * @internal
     */
    function mergedDataLabelOptions(series) {
        const plotOptions = series.chart.options.plotOptions;
        return splat(mergeArrays(mergeArrays(plotOptions?.series?.dataLabels, plotOptions?.[series.type]?.dataLabels), series.options.dataLabels));
    }
})(DataLabel || (DataLabel = {}));
/* *
 *
 *  Default Export
 *
 * */
export default DataLabel;
/* *
 *
 *  API Declarations
 *
 * */
/**
 * Callback JavaScript function to format the data label as a string. Note that
 * if a `format` is defined, the format takes precedence and the formatter is
 * ignored.
 *
 * @callback Highcharts.DataLabelsFormatterCallbackFunction
 *
 * @param {Highcharts.Point} this
 * Data label context to format
 *
 * @param {Highcharts.DataLabelsOptions} options
 * [API options](/highcharts/plotOptions.series.dataLabels) of the data label
 *
 * @return {number|string|null|undefined}
 * Formatted data label text
 */
/**
 * Values for handling data labels that flow outside the plot area.
 *
 * @typedef {"allow"|"justify"} Highcharts.DataLabelsOverflowValue
 */
''; // Keeps doclets above in JS file
