You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Highcharts中正确显示重叠的数据标签?

Highcharts 同坐标数据标签重叠问题解决(无需旋转)

我基于Highcharts实现了图表展示,核心配置函数如下:

const getBenchmarkChartSettings = (model, isEpd) => {

    let isStrScore = model.isStrScore;
    let metricSeries = reorderMetricSeries(model.series.metricSeries);
    let generatedSeries = model.series.generatedSeries;
    let benchmarkSeries = model.series.benchmarkSeries;
    let pitSeries = getPitSeries(model);

    _.forEach(metricSeries, s => { s.color = s.prop ? propColor : metricColor; });
    _.forEach(generatedSeries, s => { s.color = genColor; });
    _.forEach(benchmarkSeries, (s, i) => {
        s.color = colors[i];
        s.type = 'spline';
        if (s.benchmarkLegend && s.benchmarkType) {
            s.name = s.benchmarkLegend;
        }
        _.forEach(s.data, d => {
            d.x = new Date(d.dataDate).getTime();
            d.y = isStrScore ? defaultscores.indexOf(d.value) : parseFloat(d.value);
        });
    });

    _.forEach(metricSeries, e => { e.minPointLength = 6; e.type = 'column'; });
    _.forEach(generatedSeries, e => { e.minPointLength = 6; e.type = 'column'; });

    let seriesRaw = _.union(metricSeries, generatedSeries, benchmarkSeries, pitSeries);
    let series = _.filter(seriesRaw, { 'errorMessage': null });

    let shiftedSeries = _.map(series, ser => _.assign({}, ser, {
        data: _.map(ser.data, d => _.assign({}, d, { x: d.x }))
    }));

    let allDefaultScore = isStrScore && _(shiftedSeries).flatMap('data').flatMap('value').value()
            .every(val => val === 'd');

    _.forEach(shiftedSeries, e => {
        // allow for series longer than 1000 elements
        e.turboThreshold = 0;
        _.forEach(e.data, d => {
            if (!d.isPit) {
                d.x = new Date(d.dataDate).getTime();
                d.y = isStrScore ? 
                    (allDefaultScore ? defaultCompanyScores.indexOf(d.value) : defaultscores.indexOf(d.value)) 
                    : parseFloat(d.value);
            }
        });
    });

    const getShiftedSeries = (shiftedSeries, date) => {
        let maxYear = new Date(date).getFullYear();
        let updatedShiftedSeries = _.map(shiftedSeries, o => {
            if (!o.isPit) {
                let sdata = o.data.filter(itm => new Date(itm.x).getFullYear() >= maxYear);
                o.data = sdata;
                if (sdata.length > 0) { return o; }
                return null;
            }
            return o;
        });
        updatedShiftedSeries = _.without(updatedShiftedSeries, undefined || null);
        return updatedShiftedSeries;
    };

    let maxDate = _.chain(shiftedSeries)
        .map(o => _.maxBy(o.data, 'x'))
        .filter(e => e !== undefined)
        .map('x')
        .max()
        .value();

    if (pitSeries) {
        let pitAdjSeries = _.filter(shiftedSeries, { 'isPit': true });
        pitAdjSeries[0].data[0].x = maxDate;
    }

    let minDate = _.chain(shiftedSeries)
        .map(o => _.minBy(o.data, 'x'))
        .filter(e => e !== undefined)
        .map('x')
        .min()
        .value();

    let dateBuffer = ((maxDate - minDate) * 0.1);
    shiftedSeries = getShiftedSeries(shiftedSeries, minDate);

    let hasOnlyFewStatements = _.filter(shiftedSeries, e => 
                                    (e.name.indexOf('CIQ Data') > -1 || e.name.indexOf('Prop Data') > -1))
                                .reduce((dataPoints, { data }) => dataPoints.concat(data), [])
                                .length < 3;

    let chartSettings = {
        chart: {
            width: 220,
            height: 135,
            spacingLeft: 0,
            marginLeft: !isStrScore ? 25 : 12,
            marginRight: 1,
            backgroundColor: isEpd ? '#f2f2f2' : 'transparent',
            plotBorderColor: '#9A9A9A',
            plotBorderWidth: 1
        },
        title: {
            text: null
        },
        credits: {
            enabled: false
        },
        legend: {
            align: 'left',
            layout: 'horizontal',
            verticalAlign: 'top',
            alignColumns: false,
            itemDistance: 0,
            symbolHeight: 10,
            symbolWidth: 6,
            padding: 0,
            width: 200,
            symbolRadius: 0,
            margin: 7,
            itemMarginTop: 2,
            itemStyle: {
                fontSize: '4px'
            },
            y: -5,
            x: !isStrScore ? 25 : 10
        },
        xAxis: [
            {
                type: 'datetime',
                dateTimeLabelFormats: {
                    millisecond: '%b-%d-%Y',
                    second: '%b-%d-%Y',
                    minute: '%b-%d-%Y',
                    hour: '%b-%d-%Y',
                    day: '%b-%Y',
                    week: '%b-%Y',
                    month: '%b-%d-%Y',
                    year: '%Y'
                },
                labels: {
                    rotation: 0,
                    style: {
                        fontSize: '4px',
                        color: '#000'
                    },
                    y: 7,
                    x: isEpd ? -5 : !isStrScore ? 0 : (hasOnlyFewStatements ? 0 : -10)
                },
                min: minDate - dateBuffer,
                max: maxDate + dateBuffer,
                marginRight: 10,
                gridLineColor: 'white',
                gridLineWidth: 1,
                lineWidth: 0.5,
                minorGridLineWidth: 0,
                lineColor: '#808080',
                minorTickLength: 0,
                tickLength: 0,                
                tickInterval: isEpd ? (364 * 24 * 60 * 60000) : (maxDate - minDate) > 32000000000 ? undefined : 
                        (hasOnlyFewStatements ? undefined : 8000000000),
                align: 'left'
            }
        ],
        yAxis: [
            {
                gridLineWidth: 0,
                lineWidth: 0.5,
                lineColor: '#808080',
                minRange: 0,
                min: 0,
                ceiling: 100,
                tickAmount: isStrScore ? undefined : 5,
                labels: {
                    style: {
                        fontSize: '4px',
                        color: 'black'
                    },
                    x: -2,
                    y: isStrScore ? 7 : 3
                },
                title: {
                    text: !isStrScore ? '(PD%)' : '',
                    align: 'middle',
                    margin: 0,
                    x: -2,
                    rotation: -90,
                    ceiling: 100,
                    style: {
                        fontSize: '5px',
                        color: 'black'
                    },
                    labels: {
                        style: {
                            fontWeight: 'bold',
                            color: 'black',
                            fontSize: '2px'
                        },
                        x: -3
                    }
                }
            }
        ],
        plotOptions: {
            column: {
                maxPointWidth: 10
            },
            series: {
                states: {
                    hover: {
                        enabled: false
                    },
                    inactive: {
                        opacity: 1
                    }
                },
                enableMouseTracking: false,
                borderWidth: 1,
                borderColor: '#FFFFFF',
                threshold: 0,
                lineWidth: 1.5,
                marker: {
                    enabled: false
                },
                minPointLength: 10,
                dataLabels: {
                    enabled: true,
                    style: {
                        fontSize: '4px',
                        color: 'black',
                        textOutline: false
                    },
                    y: isLabelAdjustmentRequired(metricSeries, benchmarkSeries) ? -2 : undefined
                    
                }
            }
        },
        series: shiftedSeries?.map(item => ({
            ...item,
            data: item?.data.sort((a, b) => new Date(a.dataDate || null) - new Date(b.dataDate || null))
        })) || []
    };

    if (isStrScore) {
        chartSettings.yAxis[0].tickInterval = 4;
    }

    _.forEach(chartSettings.series, e => {
        if (e.type === 'column' || e.isPit) {
            e.dataLabels = { 
                padding: 1, 
                allowOverlap:true, 
                format: '&lt;span style=\'font-size:4px;\'&gt;{point.formattedValue}&lt;/span&gt;' };
        }
        else if (e.type === 'spline') {
            e.dataLabels = { enabled: false };
        }
    });

    return chartSettings;
}

当前遇到的问题是同x、y轴坐标的数据点标签重叠,相关的dataLabels配置代码如下:

_.forEach(chartSettings.series, e => {
    if (e.type === 'column' || e.isPit) {
        e.dataLabels = { 
            padding: 1, 
            allowOverlap:true, 
            format: '&lt;span style=\'font-size:4px;\'&gt;{point.formattedValue}&lt;/span&gt;'    };
    }
    else if (e.type === 'spline') {
        e.dataLabels = { enabled: false };
    }
});

我尝试过以下方法,但都不符合需求:

  • 设置allowOverlap: false:会直接隐藏其中一个标签,无法全部展示
  • 旋转标签(rotation: -45):不符合设计要求
  • 调整标签相对位置(top属性):未生效

导致重叠的示例数据如下:

0 = {
  data: (2)[{… }, {… }]
  0: {
    formattedValue: '9.36%'
    value: '9.36062867480189400'
    x: 1640889000000
    y: 9.360628674801895
  }
  
  1: {
    formattedValue: '9.23%'
    value: '9.23670432063350600'
    x: 1672425000000
    y: 9.236704320633507
  }
}

1 = {
  data: (1)[{… }]
  0: {
    formattedValue: '9.23%'
    value: '9.23670432063350600'
    x: 1672425000000
    y: 9.236704320633507
  }
}

解决方案推荐

方法1:自定义dataLabels的positioner函数,动态调整重叠标签的y偏移

利用Highcharts的positioner回调函数,针对同x坐标的点,给每个标签设置不同的垂直偏移量:

// 先预处理数据,统计每个x对应的点数量
const xPointCount = {};
_.forEach(chartSettings.series, series => {
    if (series.type === 'column' || series.isPit) {
        _.forEach(series.data, point => {
            xPointCount[point.x] = (xPointCount[point.x] || 0) + 1;
        });
    }
});

// 给每个系列的dataLabels添加positioner
_.forEach(chartSettings.series, e => {
    if (e.type === 'column' || e.isPit) {
        let pointIndex = 0;
        e.dataLabels = { 
            padding: 1, 
            allowOverlap: true, 
            format: '&lt;span style=\'font-size:4px;\'&gt;{point.formattedValue}&lt;/span&gt;',
            positioner: function() {
                const count = xPointCount[this.point.x];
                // 每个标签垂直偏移6px(根据字体大小调整),让重叠标签上下分布
                const offset = (pointIndex * 6) - ((count - 1) * 3); // 居中对齐偏移
                pointIndex = (pointIndex + 1) % count;
                return { x: this.plotX, y: this.plotY - 10 - offset };
            }
        };
    }
    else if (e.type === 'spline') {
        e.dataLabels = { enabled: false };
    }
});

方法2:给重叠点添加自定义偏移属性,在dataLabels中引用

预处理数据时给同x的点添加labelOffset属性,再在dataLabels中动态应用:

// 预处理数据,给同x的点分配偏移量
const xPointMap = {};
_.forEach(shiftedSeries, series => {
    if (series.type === 'column' || series.isPit) {
        _.forEach(series.data, point => {
            if (!xPointMap[point.x]) {
                xPointMap[point.x] = [];
            }
            xPointMap[point.x].push(point);
        });
    }
});

// 为每个点设置垂直偏移
_.forEach(xPointMap, points => {
    points.forEach((point, index) => {
        point.labelOffset = index * 5; // 每个点偏移5px
    });
});

// 配置dataLabels时使用自定义偏移
_.forEach(chartSettings.series, e => {
    if (e.type === 'column' || e.isPit) {
        e.dataLabels = { 
            padding: 1, 
            allowOverlap: true, 
            format: '&lt;span style=\'font-size:4px;\'&gt;{point.formattedValue}&lt;/span&gt;',
            y: function() {
                const baseOffset = isLabelAdjustmentRequired(metricSeries, benchmarkSeries) ? -2 : 0;
                return baseOffset - this.point.labelOffset;
            }
        };
    }
    else if (e.type === 'spline') {
        e.dataLabels = { enabled: false };
    }
});

方法3:使用对齐方式区分重叠标签

如果重叠点数量不多,给不同系列的标签设置不同的水平对齐方向,配合偏移避免重叠:

_.forEach(chartSettings.series, (e, seriesIndex) => {
    if (e.type === 'column' || e.isPit) {
        e.dataLabels = { 
            padding: 1, 
            allowOverlap: true, 
            format: '&lt;span style=\'font-size:4px;\'&gt;{point.formattedValue}&lt;/span&gt;',
            align: seriesIndex % 2 === 0 ? 'left' : 'right',
            x: seriesIndex % 2 === 0 ? 5 : -5 // 配合对齐添加水平偏移
        };
    }
    else if (e.type === 'spline') {
        e.dataLabels = { enabled: false };
    }
});

以上方法均无需旋转标签,可根据实际数据量和设计需求选择适配方案。

内容的提问来源于stack exchange,提问作者Xtremcool

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 11:14:52