如何在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: '<span style=\'font-size:4px;\'>{point.formattedValue}</span>' }; } 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: '<span style=\'font-size:4px;\'>{point.formattedValue}</span>' }; } 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: '<span style=\'font-size:4px;\'>{point.formattedValue}</span>', 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: '<span style=\'font-size:4px;\'>{point.formattedValue}</span>', 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: '<span style=\'font-size:4px;\'>{point.formattedValue}</span>', align: seriesIndex % 2 === 0 ? 'left' : 'right', x: seriesIndex % 2 === 0 ? 5 : -5 // 配合对齐添加水平偏移 }; } else if (e.type === 'spline') { e.dataLabels = { enabled: false }; } });
以上方法均无需旋转标签,可根据实际数据量和设计需求选择适配方案。
内容的提问来源于stack exchange,提问作者Xtremcool
相关产品推荐
相关产品推荐

