I am creating an interactive scatter chart with eCharts in Angular, that will potentially have a very large data set. I have two groupings of points that will be displayed on the same chart.
I want to separate the labels, line one group of labels up at the top and the other beneath the data points. They have a lot of overlap, so I am using labellayout: { moveOverlap: 'shiftX' }.
However, the 'shiftX' option only looks for x Axis overlap, so if one label is at the top of the chart and another is at the bottom of the chart, it will still separate their x positions. It looks very strange and is not an efficient way to display the data. I can't figure out how to use 'shiftX' separately on each half of the labels - I've tried putting them into separate grids/axes/series, etc.
Any ideas?
Here's my code - I modified one of the eCharts examples to illustrate my dilemma. I can turn on hideOverlap if the points are actually dense, but for this example, if you take off the 'shiftX' on either series, the other looks perfect. And the series without the 'shiftX' is a mess.
When both have 'shiftX', more organized mess, but way more overlap than is necessary for the data set size, because it is trying to minimize overlap horizontally among ALL labels, not just the top row or the bottom row.
const data = [
[[28604,77,17096869,'Australia',1990],[31163,77.4,27662440,'Canada',1990],[1516,68,1154605773,'China',1990],[13670,74.7,10582082,'Cuba',1990],[28599,75,4986705,'Finland',1990],[29476,77.1,56943299,'France',1990],[31476,75.4,78958237,'Germany',1990],[28666,78.1,254830,'Iceland',1990],[1777,57.7,870601776,'India',1990],[29550,79.1,122249285,'Japan',1990],[2076,67.9,20194354,'North Korea',1990],[12087,72,42972254,'South Korea',1990],[24021,75.4,3397534,'New Zealand',1990],[43296,76.8,4240375,'Norway',1990],[10088,70.8,38195258,'Poland',1990],[19349,69.6,147568552,'Russia',1990],[10670,67.3,53994605,'Turkey',1990],[26424,75.7,57110117,'United Kingdom',1990],[37062,75.4,252847810,'United States',1990]],
[[44056,81.8,23968973,'Australia',2015],[43294,81.7,35939927,'Canada',2015],[13334,76.9,1376048943,'China',2015],[21291,78.5,11389562,'Cuba',2015],[38923,80.8,5503457,'Finland',2015],[37599,81.9,64395345,'France',2015],[44053,81.1,80688545,'Germany',2015],[42182,82.8,329425,'Iceland',2015],[5903,66.8,1311050527,'India',2015],[36162,83.5,126573481,'Japan',2015],[1390,71.4,25155317,'North Korea',2015],[34644,80.7,50293439,'South Korea',2015],[34186,80.6,4528526,'New Zealand',2015],[64304,81.6,5210967,'Norway',2015],[24787,77.3,38611794,'Poland',2015],[23038,73.13,143456918,'Russia',2015],[19360,76.5,78665830,'Turkey',2015],[38225,81.4,64715810,'United Kingdom',2015],[53354,79.1,321773631,'United States',2015]]
];
option = {
xAxis: [{
splitLine: { show: false }
},
{
splitLine: { show: false }
}],
yAxis: [{
splitLine: { show: false },
scale: true,
},
{
splitLine: { show: false },
scale: true,
}],
grid: {
left: 40,
right: 130
},
series: [
{
name: '1990',
data: data[0],
xAxisIndex: 0,
yAxisIndex: 0,
type: 'scatter',
symbolSize: function (data) {
return Math.sqrt(data[2]) / 5e2;
},
emphasis: {
focus: 'self'
},
labelLayout: function () {
return {
y: myChart.getHeight() - 750,
moveOverlap: 'shiftX'
};
},
labelLine: {
show: true,
lineStyle: {
color: '#bbb'
}
},
label: {
show: true,
formatter: function (param: any) {
return param.data[3];
},
position: 'right',
minMargin: 2
}
},
{
name: '1990',
data: data[1],
xAxisIndex: 1,
yAxisIndex: 1,
type: 'scatter',
symbolSize: function (data) {
return Math.sqrt(data[2]) / 5e2;
},
emphasis: {
focus: 'self'
},
labelLayout: function () {
return {
y: myChart.getHeight() - 30,
moveOverlap: 'shiftX'
};
},
labelLine: {
show: true,
lineStyle: {
color: '#bbb'
}
},
label: {
show: true,
formatter: function (param: any) {
return param.data[3];
},
position: 'right',
minMargin: 2
}
}
]
};
It is true that the LabelManager
will get all the labels
of the chart and distribute all of those that have moveOverlap: 'shiftX'
regardless of the series they belong to.
One tricky/hacky way to solve this conundrum is to set moveOverlap: 'shiftX'
in different stages for each of the series. More precisely, you may set initially shiftX
for the first series and not for the other(s). After the chart finished
, collect the actual x positions of the labels of the first series and replace shiftX
by explicit x
values, while adding moveOverlap: 'shiftX'
for the second series and update the chart through .setOption
. If there are n
series, this can be repeated
n-1
times.
Here's your code with this trick applied:
const myChart = echarts.init(document.getElementById('chart'));
const data = [
[[28604,77,17096869,'Australia',1990],[31163,77.4,27662440,'Canada',1990],[1516,68,1154605773,'China',1990],[13670,74.7,10582082,'Cuba',1990],[28599,75,4986705,'Finland',1990],[29476,77.1,56943299,'France',1990],[31476,75.4,78958237,'Germany',1990],[28666,78.1,254830,'Iceland',1990],[1777,57.7,870601776,'India',1990],[29550,79.1,122249285,'Japan',1990],[2076,67.9,20194354,'North Korea',1990],[12087,72,42972254,'South Korea',1990],[24021,75.4,3397534,'New Zealand',1990],[43296,76.8,4240375,'Norway',1990],[10088,70.8,38195258,'Poland',1990],[19349,69.6,147568552,'Russia',1990],[10670,67.3,53994605,'Turkey',1990],[26424,75.7,57110117,'United Kingdom',1990],[37062,75.4,252847810,'United States',1990]],
[[44056,81.8,23968973,'Australia',2015],[43294,81.7,35939927,'Canada',2015],[13334,76.9,1376048943,'China',2015],[21291,78.5,11389562,'Cuba',2015],[38923,80.8,5503457,'Finland',2015],[37599,81.9,64395345,'France',2015],[44053,81.1,80688545,'Germany',2015],[42182,82.8,329425,'Iceland',2015],[5903,66.8,1311050527,'India',2015],[36162,83.5,126573481,'Japan',2015],[1390,71.4,25155317,'North Korea',2015],[34644,80.7,50293439,'South Korea',2015],[34186,80.6,4528526,'New Zealand',2015],[64304,81.6,5210967,'Norway',2015],[24787,77.3,38611794,'Poland',2015],[23038,73.13,143456918,'Russia',2015],[19360,76.5,78665830,'Turkey',2015],[38225,81.4,64715810,'United Kingdom',2015],[53354,79.1,321773631,'United States',2015]]
];
let labelsXBySeries = Array(data.length-1).fill(null); // may be attached to the chart instance if global variables are an issue
const option = {
xAxis: [{
splitLine: { show: false }
},
{
splitLine: { show: false }
}],
yAxis: [{
splitLine: { show: false },
scale: true,
},
{
splitLine: { show: false },
scale: true,
}],
grid: {
left: 40,
right: 130
},
series: [
{
name: '1990',
data: data[0],
xAxisIndex: 0,
yAxisIndex: 0,
type: 'scatter',
symbolSize: function (data) {
return Math.sqrt(data[2]) / 5e2;
},
emphasis: {
focus: 'self'
},
labelLayout: function (params) {
const layout = {y: 0}; // use the y value for this series
if(labelsXBySeries[params.seriesIndex]){ // if x positions were computed
layout.x = labelsXBySeries[params.seriesIndex][params.dataIndex];
}
else{ // if not, use `shiftX` to set the positions
layout.moveOverlap = 'shiftX';
}
return layout;
},
labelLine: {
show: true,
lineStyle: {
color: '#bbb'
}
},
label: {
show: true,
formatter: function (param) {
return param.data[3];
},
position: 'top',
minMargin: 2
}
},
{
name: '2015',
data: data[1],
xAxisIndex: 1,
yAxisIndex: 1,
type: 'scatter',
symbolSize: function (data) {
return Math.sqrt(data[2]) / 5e2;
},
emphasis: {
focus: 'self'
},
labelLayout: {
y: myChart.getHeight() - 30
},
labelLine: {
show: true,
lineStyle: {
color: '#bbb'
}
},
label: {
show: true,
formatter: function (param) {
return param.data[3];
},
position: 'bottom',
minMargin: 2
}
}
]
};
function collectSeriesLabelsX(chart, seriesIndex){
const labelManager = Object.entries(chart._api)
.filter(([prop])=>prop.match(/^__ec_/))
.find(([_, o])=>o.hasOwnProperty('labelManager'))[1].labelManager;
const labelsX = [];
labelManager._labelList.forEach(label=>{
if(label.seriesModel.seriesIndex === seriesIndex){
labelsX[label.dataIndex] = label.label.x;
}
});
return labelsX;
}
myChart.on('finished', () => {
const firstNull = labelsXBySeries.findIndex(labels => labels === null);
if(firstNull < 0){
labelsXBySeries.forEach((_, i, a) => {a[i] = null;});
return;
}
labelsXBySeries[firstNull] = collectSeriesLabelsX(myChart, firstNull);
if(firstNull === labelsXBySeries.length - 1){
// the last series
option.series[firstNull + 1].labelLayout.moveOverlap = 'shiftX';
}
//option.animation = false; // to speed up the process
myChart.setOption(option);
});
myChart.setOption(option);
<div id='chart' style='height: 250px; min-width: 1000px'></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.5.0/echarts.min.js"></script>