Reputation: 583
I'm using chartJS with extension chartjs-gauge, which is basically a customised doughnut used to create a gauge style chart. I've added the chartjs-plugin-datalabels plugin so I can customise the labels.
Labels in pie and doughnut charts are usually used to display the value of that segment, but since this has been customised to appear as a gauge I want to position my data labels at the end of each segment so it displays the value at that point like so:
Whilst it is possible to move the labels around using anchor, align and offset, these allow limited movement around a point and I can't figure out a way to achieve what I need. I can anchor to 'end' to get the label outside of the gauge and then I thought maybe I could combine align and offset to push the label to the end of the segment but unfortunately those commands seem to work based on the axis of the whole gauge and not the arced segment.
Here is the code I'm using to set the options and data. This is within a Salesforce Aura component so chartJsGaugeInstance gets the values from a controller:
let chartJsGaugeInstance = component.get("v.dataInstance");
let type = chartJsGaugeInstance.type;
let target = chartJsGaugeInstance.targetValue;
let firstDivide = target * 0.33;
let secondDivide = target * 0.66;
let data = {
datasets : [{
backgroundColor: ["#0fdc63", "#fd9704", "#ff7143"],
data: [firstDivide, secondDivide, target],
value: chartJsGaugeInstance.dataValues,
datalabels: {
anchor: 'center',
align: 'center',
offset: 0
}
}]
};
let options = {
responsive: true,
title: {
display: true,
text: chartJsGaugeInstance.title
},
layout: {
padding: {
bottom: 30
}
},
needle: {
// Needle circle radius as the percentage of the chart area width
radiusPercentage: 1.5,
// Needle width as the percentage of the chart area width
widthPercentage: 1,
// Needle length as the percentage of the interval between inner radius (0%) and outer radius (100%) of the arc
lengthPercentage: 80,
// The color of the needle
color: 'rgba(0, 0, 0, 1)'
},
valueLabel: {
formatter: Math.round
},
cutoutPercentage : 80,
plugins: {
datalabels: {
display: true,
formatter: function (value, context) {
return context.chart.data.labels[context.dataIndex];
},
color: 'rgba(0, 0, 0, 1.0)',
backgroundColor: null,
font: {
size: 16
}
}
}
}
And this is the resulting chart:
I can move the labels around by changing the datalabels parameters within the datasets object (in this example I've left them in the centre) but as they all move relative to the chart area rather than the direction of the relative segment, I can't get them to all align to the end of their segments. Here is an example which moves the 6k label to about the right place, but the 3k and 10k labels end up way out:
datalabels: {
anchor: 'end',
align: 'right',
offset: 50
}
This is the documentation I'm using for data label positioning. One line under the Align section says:
'end': the label is positioned after the anchor point, following the same direction
I thought this is what I wanted, "following the same direction" to me suggests it should follow the arc of the segment but it doesn't:
datalabels: {
anchor: 'end',
align: 'end',
offset: 20
}
Has anyone else managed to achieve this? There is a similar issue raised in github marked as "enhancement" since 2018 but wondered if there is a way to do this either programatically or by editing the source files since it doesn't look like that'll be delivered any time soon.
Thanks!
Update
I've managed to find a partial solution to this using Scriptable Options. The following updates to the code gives me nicely positioned labels at the end of each sector. I've moved the datalabels code out of datasets and into options
let numSectors = data.datasets[0].data.length;
let sectorDegree = 180 / numSectors;
datalabels: {
anchor: 'end',
align: function(context){
return (sectorDegree * context.dataIndex) - sectorDegree;
},
offset: function(context){
return 70;
},
...
}
I'm happy with this for my current project because I'll always have three evenly split sectors, HOWEVER it isn't a full solution. The angles I've calculated are massively dependent on the offset. Changing the offset means the align calculation no longer gives nice results. The big problem here is that because of offset being so critical, changing the number of sectors means this solution no longer works.
Upvotes: 7
Views: 18135
Reputation: 2643
My idea was to draw an additional transparent chart on top of the original chart that will be in charge of drawing the labels. The labels chart data will contain segments wrapping the end of each segment in the original chart, in a way that label displayed in the middle of the labels chart segment will actually be displayed at the end of the original chart segments.
Here is the code for drawing the transparent labels chart:
const getLabelsChartData = (min, data) => {
if (!data) return [];
const max = data[data.length - 1];
const step = (max - min) / 1000;
const values = data
.slice(0, -1) // remove "max" value
.map((value, index) => {
// remove too close values (because labels collapse)
const prevValue = data[index - 1];
if (!prevValue || value - prevValue > step * 50) {
return value;
}
})
.reduce((arr, value) => {
// create small ranges between each value
if (value) {
return [...arr, value - step, value + step];
}
return arr;
}, []);
return [...values, max];
};
const getLabel = (originalData, labelIndex) => {
if (labelIndex % 2) {
const originalDataIndex = Math.floor(labelIndex / 2);
return originalData[originalDataIndex];
}
return '';
};
const labelsChartConfig = {
type: 'gauge',
plugins: [ChartDataLabels],
data: {
data: getLabelsChartData(minValue, originalData),
minValue: minValue,
fill: false,
backgroundColor: ['transparent'],
borderColor: 'transparent',
}
options: {
plugins: {
datalabels: {
display: true,
formatter: function (value, context) {
return getLabel(originalData, context.dataIndex);
}
anchor: 'end',
align: 'end',
},
},
}
}
new Chart(labelsCanvas, labelsChartConfig);
Upvotes: 1
Reputation: 31
Your code in the align function really helped me get started but I wanted the positioning to be a bit different and I wrote this code to calculate the position based on the segment amounts, the desired font size (for proper offset), and the width of the canvas' parent.
let numSectors = insert length of data array here;
let sectorDegree = 180 / numSectors;
let width = (canvas.parent().width() - parseInt(canvas.css('padding').replace("px", "") * 2));
let fontSize = 16;
let b = (width / 2) - 3;
let c = b - (fontSize * 1.8);
let a = Math.sqrt((Math.pow(b, 2) + Math.pow(c, 2)) - (2 * b * c * Math.cos((sectorDegree / 2) * Math.PI / 180)));
let offset = a - (fontSize * 1.2);
let alignAngle = (Math.asin(Math.sin((sectorDegree / 2) * Math.PI / 180) * c / a) * 180 / Math.PI) - (sectorDegree / 2);
datalabels: {
display: true,
anchor: 'end',
font: {
size: fontSize,
},
offset: offset,
align: function(context) {
return (sectorDegree * context.dataIndex) - alignAngle;
},
}
Upvotes: 3