/* *
 *
 *  (c) 2010-2024 Torstein Honsi
 *
 *  License: www.highcharts.com/license
 *
 *  !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
 *
 * */
'use strict';
import A from '../Animation/AnimationUtilities.js';
const { getDeferredAnimation } = A;
import F from '../Templating.js';
const { format } = F;
import H from '../Globals.js';
const { composed } = H;
import U from '../Utilities.js';
const { defined, extend, fireEvent, isArray, isString, merge, objectEach, pick, pInt, pushUnique, splat } = U;
/* *
 *
 *  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.
     * @private
     */
    function hasDataLabels() {
        return mergedDataLabelOptions(this)
            .some((o) => o?.enabled);
    }
    /**
     * Align each individual data label.
     * @private
     */
    function alignDataLabel(point, dataLabel, options, alignTo, isNew) {
        const series = this, chart = this.chart, inverted = this.isCartesian && chart.inverted, enabledDataSorting = this.enabledDataSorting, plotX = point.plotX, plotY = point.plotY, rotation = options.rotation, align = options.align, isInsidePlot = defined(plotX) &&
            defined(plotY) &&
            chart.isInsidePlot(plotX, Math.round(plotY), {
                inverted,
                paneCoordinates: true,
                series
            }), setStartPos = (alignOptions) => {
            if (enabledDataSorting && series.xAxis && !justify) {
                series.setDataLabelStartPos(point, dataLabel, isNew, isInsidePlot, alignOptions);
            }
        };
        let baseline, rotCorr, // rotation correction
        // Math.round for rounding errors (#2683), alignTo to allow column
        // labels (#2700)
        alignAttr, // the final position;
        justify = pick(options.overflow, (enabledDataSorting ? 'none' : 'justify')) === 'justify', visible = this.visible &&
            point.visible !== false &&
            defined(plotX) &&
            (point.series.forceDL ||
                (enabledDataSorting && !justify) ||
                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
                    })));
        const pos = point.pos();
        if (visible && pos) {
            if (rotation) {
                dataLabel.attr({ align });
            }
            let bBox = dataLabel.getBBox(true), bBoxCorrection = [0, 0];
            baseline = chart.renderer.fontMetrics(dataLabel).b;
            // The alignment box is a singular point
            alignTo = extend({
                x: pos[0],
                y: Math.round(pos[1]),
                width: 0,
                height: 0
            }, alignTo);
            // Add the text size for alignment calculation
            extend(options, {
                width: bBox.width,
                height: bBox.height
            });
            // Allow a hook for changing alignment in the last moment, then do
            // the alignment
            if (rotation) {
                justify = false; // Not supported for rotated text
                rotCorr = chart.renderer.rotCorr(baseline, rotation); // #3723
                alignAttr = {
                    x: (alignTo.x +
                        (options.x || 0) +
                        alignTo.width / 2 +
                        rotCorr.x),
                    y: (alignTo.y +
                        (options.y || 0) +
                        { top: 0, middle: 0.5, bottom: 1 }[options.verticalAlign] *
                            alignTo.height)
                };
                bBoxCorrection = [
                    bBox.x - Number(dataLabel.attr('x')),
                    bBox.y - Number(dataLabel.attr('y'))
                ];
                setStartPos(alignAttr); // data sorting
                dataLabel[isNew ? 'attr' : 'animate'](alignAttr);
            }
            else {
                setStartPos(alignTo); // data sorting
                dataLabel.align(options, void 0, alignTo);
                alignAttr = dataLabel.alignAttr;
            }
            // Handle justify or crop
            if (justify && alignTo.height >= 0) { // #8830
                this.justifyDataLabel(dataLabel, options, alignAttr, bBox, alignTo, isNew);
                // Now check that the data label is within the plot area
            }
            else if (pick(options.crop, true)) {
                let { x, y } = alignAttr;
                x += bBoxCorrection[0];
                y += bBoxCorrection[1];
                // Uncomment this block to visualize the bounding boxes used for
                // determining visibility
                /*
                chart.renderer.rect(
                    chart.plotLeft + alignAttr.x + bBox.x,
                    chart.plotTop + alignAttr.y + bBox.y + 9999,
                    bBox.width,
                    bBox.height
                ).attr({
                    stroke: 'rgba(0, 0, 0, 0.3)',
                    'stroke-width': 0.5
                }).add();
                chart.renderer.circle(
                    chart.plotLeft + alignAttr.x,
                    chart.plotTop + alignAttr.y,
                    2
                ).attr({
                    fill: 'red',
                    zIndex: 20
                }).add();
                // */
                visible =
                    chart.isInsidePlot(x, y, {
                        paneCoordinates: true,
                        series
                    }) &&
                        chart.isInsidePlot(x + bBox.width, y + bBox.height, {
                            paneCoordinates: true,
                            series
                        });
            }
            // When we're using a shape, make it possible with a connector or an
            // arrow pointing to thie point
            if (options.shape && !rotation) {
                dataLabel[isNew ? 'attr' : 'animate']({
                    anchorX: pos[0],
                    anchorY: pos[1]
                });
            }
        }
        // To use alignAttr property in hideOverlappingLabels
        if (isNew && enabledDataSorting) {
            dataLabel.placed = false;
        }
        // Show or hide based on the final aligned position
        if (!visible && (!enabledDataSorting || justify)) {
            dataLabel.hide();
            dataLabel.placed = false; // don't animate back in
        }
        else {
            dataLabel.show();
        }
    }
    /**
     * Handle the dataLabels.filter option.
     * @private
     */
    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;
    }
    /**
     * @private
     */
    function compose(SeriesClass) {
        if (pushUnique(composed, compose)) {
            const seriesProto = SeriesClass.prototype;
            seriesProto.initDataLabelsGroup = initDataLabelsGroup;
            seriesProto.initDataLabels = initDataLabels;
            seriesProto.alignDataLabel = alignDataLabel;
            seriesProto.drawDataLabels = drawDataLabels;
            seriesProto.justifyDataLabel = justifyDataLabel;
            seriesProto.setDataLabelStartPos = setDataLabelStartPos;
            seriesProto.hasDataLabels = hasDataLabels;
        }
    }
    DataLabel.compose = compose;
    /**
     * Create the SVGElement group for dataLabels
     * @private
     */
    function initDataLabelsGroup() {
        return this.plotGroup('dataLabelsGroup', 'data-labels', this.hasRendered ? 'inherit' : 'hidden', // #5133, #10220
        this.options.dataLabels.zIndex || 6);
    }
    /**
     * Init the data labels with the correct animation
     * @private
     */
    function initDataLabels(animationConfig) {
        const series = this, hasRendered = series.hasRendered || 0;
        // Create a separate group for the data labels to avoid rotation
        const dataLabelsGroup = this.initDataLabelsGroup()
            .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
     * @private
     */
    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) ||
            "#000000" /* Palette.neutralColor100 */), seriesDlOptions = mergedDataLabelOptions(series);
        let pointOptions, dataLabelsGroup;
        // Resolve the animation
        const { animation, defer } = seriesDlOptions[0], animationConfig = defer ?
            getDeferredAnimation(chart, animation, series) :
            { defer: 0, duration: 0 };
        fireEvent(this, 'drawDataLabels');
        if (series.hasDataLabels?.()) {
            dataLabelsGroup = this.initDataLabels(animationConfig);
            // Make the labels for each point
            points.forEach((point) => {
                const dataLabels = point.dataLabels || [];
                // 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 treemaps
                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 &&
                        // #2282, #4641, #7112, #10049
                        (!point.isNull || point.dataLabelOnNull) &&
                        applyFilter(point, labelOptions)), { backgroundColor, borderColor, distance, style = {} } = labelOptions;
                    let labelConfig, 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);
                        labelConfig = point.getLabelConfig();
                        labelText = defined(formatString) ?
                            format(formatString, labelConfig, chart) :
                            (labelOptions[point.formatPrefix + 'Formatter'] ||
                                labelOptions.formatter).call(labelConfig, labelOptions);
                        rotation = labelOptions.rotation;
                        if (!chart.styledMode) {
                            // Determine the color
                            style.color = pick(labelOptions.color, style.color, isString(series.color) ? series.color : void 0, "#000000" /* Palette.neutralColor100 */);
                            // Get automated contrast color
                            if (style.color === 'contrast') {
                                if (backgroundColor !== 'none') {
                                    labelBgColor = backgroundColor;
                                }
                                point.contrastColor = renderer.getContrast(labelBgColor !== 'auto' && labelBgColor ||
                                    (point.color || series.color));
                                style.color = (labelBgColor || // #20007
                                    (!defined(distance) &&
                                        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 || 0,
                            rotation,
                            padding: labelOptions.padding,
                            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) ||
                        !!dataLabel.div !== !!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)) {
                        if (!dataLabel) {
                            // Create new label element
                            dataLabel = rotation ?
                                // Labels don't rotate, use text element
                                renderer.text(labelText, 0, 0, labelOptions.useHTML)
                                    .addClass('highcharts-data-label') :
                                // We can use label
                                renderer.label(labelText, 0, 0, labelOptions.shape, void 0, void 0, labelOptions.useHTML, void 0, 'data-label');
                            if (dataLabel) {
                                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);
                            }
                            const textPathOptions = labelOptions[point.formatPrefix + 'TextPath'] || labelOptions.textPath;
                            if (textPathOptions && !labelOptions.useHTML) {
                                dataLabel.setTextPath(point.getDataLabelPath?.(dataLabel) ||
                                    point.graphic, textPathOptions);
                                if (point.dataLabelPath &&
                                    !textPathOptions.enabled) {
                                    // clean the DOM
                                    point.dataLabelPath = (point.dataLabelPath.destroy());
                                }
                            }
                            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] || !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.
     * @private
     */
    function justifyDataLabel(dataLabel, options, alignAttr, bBox, alignTo, isNew) {
        const chart = this.chart, align = options.align, verticalAlign = options.verticalAlign, padding = dataLabel.box ? 0 : (dataLabel.padding || 0);
        let { x = 0, y = 0 } = options, off, justified;
        // Off left
        off = (alignAttr.x || 0) + padding;
        if (off < 0) {
            if (align === 'right' && x >= 0) {
                options.align = 'left';
                options.inside = true;
            }
            else {
                x -= off;
            }
            justified = true;
        }
        // Off right
        off = (alignAttr.x || 0) + bBox.width - padding;
        if (off > chart.plotWidth) {
            if (align === 'left' && x <= 0) {
                options.align = 'right';
                options.inside = true;
            }
            else {
                x += chart.plotWidth - off;
            }
            justified = true;
        }
        // Off top
        off = alignAttr.y + padding;
        if (off < 0) {
            if (verticalAlign === 'bottom' && y >= 0) {
                options.verticalAlign = 'top';
                options.inside = true;
            }
            else {
                y -= off;
            }
            justified = true;
        }
        // Off bottom
        off = (alignAttr.y || 0) + bBox.height - padding;
        if (off > chart.plotHeight) {
            if (verticalAlign === 'top' && y <= 0) {
                options.verticalAlign = 'bottom';
                options.inside = true;
            }
            else {
                y += chart.plotHeight - off;
            }
            justified = true;
        }
        if (justified) {
            options.x = x;
            options.y = y;
            dataLabel.placed = !isNew;
            dataLabel.align(options, void 0, alignTo);
        }
        return justified;
    }
    /**
     * 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.
     * @private
     */
    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.
     * @private
     */
    function mergedDataLabelOptions(series) {
        const plotOptions = series.chart.options.plotOptions;
        return splat(mergeArrays(mergeArrays(plotOptions?.series?.dataLabels, plotOptions?.[series.type]?.dataLabels), series.options.dataLabels));
    }
    /**
     * Set starting position for data label sorting animation.
     * @private
     */
    function setDataLabelStartPos(point, dataLabel, isNew, isInside, alignOptions) {
        const chart = this.chart, inverted = chart.inverted, xAxis = this.xAxis, reversed = xAxis.reversed, labelCenter = ((inverted ? dataLabel.height : dataLabel.width) || 0) / 2, pointWidth = point.pointWidth, halfWidth = pointWidth ? pointWidth / 2 : 0;
        dataLabel.startXPos = inverted ?
            alignOptions.x :
            (reversed ?
                -labelCenter - halfWidth :
                xAxis.width - labelCenter + halfWidth);
        dataLabel.startYPos = inverted ?
            (reversed ?
                this.yAxis.height - labelCenter + halfWidth :
                -labelCenter - halfWidth) : alignOptions.y;
        // We need to handle visibility in case of sorting point outside plot
        // area
        if (!isInside) {
            dataLabel
                .attr({ opacity: 1 })
                .animate({ opacity: 0 }, void 0, dataLabel.hide);
        }
        else if (dataLabel.visibility === 'hidden') {
            dataLabel.show();
            dataLabel
                .attr({ opacity: 0 })
                .animate({ opacity: 1 });
        }
        // Save start position on first render, but do not change position
        if (!chart.hasRendered) {
            return;
        }
        // Set start position
        if (isNew) {
            dataLabel.attr({ x: dataLabel.startXPos, y: dataLabel.startYPos });
        }
        dataLabel.placed = true;
    }
})(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.PointLabelObject} 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
