Is there a way to add a borderradius to the fill background on a line chart in Chart.js?
An example of what I want to achieve: Line fill with borderradius
I've used fill: 'start' on the dataset and the background gets set. The only thing missing is the borderradius.
Edit: Added current progress
const ctx = document.getElementById('LineFill').getContext('2d');
const data = {
labels: ['2020', '2021', '2022', '2023'],
datasets: [
{
label: 'Dataset 1',
data: [1,3,5,7],
backgroundColor: 'transparent',
borderDash: [10, 5],
borderColor: '#1189D0',
},
{
label: 'Dataset 2',
data: [1,2,3],
borderColor: '#001946',
backgroundColor: '#001946',
fill: 'start',
tension: 0.9,
borderRadius: 5,
},
],
};
const config = {
type: 'line',
data: data,
options: {
responsive: true,
plugins: {
legend: {
display: false
},
},
scales: {
x: {
stacked: false,
ticks: {
display: true
},
},
y: {
stacked: false,
beginAtZero: true,
}
}
}
};
new Chart(ctx, config);
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<canvas id="LineFill">
There is no option to set a borderRadius-like property for chart.js's filler. And, in general, unlike with CSS, it is non-trivial to set a border radius for a figure (that is not a rectangle) drawn using the Canvas API.
Since a plugin in chart.js can draw on the canvas and also interrogate the positions of the data points on the plot, one can set up such a plugin that fills a region corresponding to the standard fill of a line dataset with rounded corners.
However, such a figure is not compatible with the rest of the line drawing, since the points should remain at their (corner) positions, and the line will start and end at those points; so the points and line for the dataset that is filled-rounded should be made invisible.
Here's a simplified version of such a plugin:
// from chart.js/chunks/helpers.dataset.js
function _bezierInterpolation(p1, p2, t) {
const _pointInLine = (p1, p2, t) => ({
x: p1.x + t * (p2.x - p1.x),
y: p1.y + t * (p2.y - p1.y)
});
const cp1 = {
x: p1.cp2x,
y: p1.cp2y
};
const cp2 = {
x: p2.cp1x,
y: p2.cp1y
};
const a = _pointInLine(p1, cp1, t);
const b = _pointInLine(cp1, cp2, t);
const c = _pointInLine(cp2, p2, t);
const d = _pointInLine(a, b, t);
const e = _pointInLine(b, c, t);
return _pointInLine(d, e, t);
}
const pluginRoundedCornersFill = {
id: 'pluginRoundedCornersFill',
afterDatasetDraw(chart, {meta}, options) {
let indexFound = false, R, fillColor;
for(const datasetOptions of options?.datasets ?? []){
if(datasetOptions.index === meta.index){
indexFound = true;
R = datasetOptions.radius ?? 5;
fillColor = datasetOptions.fillColor ?? 'rgba(0, 0, 0, 0.1)';
break;
}
}
if(indexFound){
const points = meta.data;
const ctx = meta.controller._ctx;
ctx.save();
ctx.fillStyle = fillColor;
ctx.beginPath();
const nPoints = points.length;
const factFirst = R / Math.hypot(points[0].x-points[1].x, points[0].y-points[1].y);
const factLast = R / Math.hypot(points[nPoints-1].x-points[nPoints-2].x, points[nPoints-1].y-points[nPoints-2].y);
const pFirst = _bezierInterpolation(points[0], points[1], factFirst);
const pLast = _bezierInterpolation(points[nPoints-2], points[nPoints-1], 1-factLast);
const pointsMod = [...points];
pointsMod[0] = {...points[0]};
pointsMod[0].x = pFirst.x;
pointsMod[0].y = pFirst.y;
pointsMod[nPoints-1] = {...points[nPoints-1]};
pointsMod[nPoints-1].x = pLast.x;
pointsMod[nPoints-1].y = pLast.y;
ctx.moveTo(pointsMod[0].x, pointsMod[0].y);
for(let i = 0; i < nPoints - 1; i++){
const previous = pointsMod[i], target = pointsMod[i + 1];
ctx.bezierCurveTo(previous.cp2x, previous.cp2y,
target.cp1x, target.cp1y, target.x, target.y);
}
ctx.arcTo(points[nPoints-1].x, points[nPoints-1].y, points[nPoints-1].x, points[nPoints-1].y + R, R);
const y0 = meta.iScale.top;
ctx.lineTo(points[nPoints-1].x, y0 - R);
ctx.arcTo(points[nPoints-1].x, y0, points[nPoints-1].x - R, y0, R);
ctx.lineTo(points[0].x + R, y0);
ctx.arcTo(points[0].x, y0, points[0].x, y0 - R, R);
ctx.lineTo(points[0].x, points[0].y + R);
ctx.arcTo(points[0].x, points[0].y, pointsMod[0].x, pointsMod[0].y, R);
ctx.lineTo(pointsMod[0].x, pointsMod[0].y);
ctx.fill();
ctx.restore();
}
}
}
const data = {
labels: ['2020', '2021', '2022', '2023'],
datasets: [
{
label: 'Dataset 1',
data: [2, 3, 5, 7],
backgroundColor: 'transparent',
borderDash: [10, 5],
borderColor: '#1189D0',
},
{
label: 'Dataset 2',
data: [1, 2, 3],
borderWidth: 0,
pointRadius: 0,
pointHitRadius: 0,
//backgroundColor: '#00d0f6',
//fill: 'start',
tension: 0.4, // for line-interior angles
},
],
};
const config = {
type: 'line',
data: data,
options: {
responsive: true,
tension: 0.3,
animation: false,
plugins: {
legend: {
display: false
},
pluginRoundedCornersFill: {
datasets: [
{
index: 1,
fillColor: '#1189D077',
radius: 10
}
]
}
},
scales: {
x: {
stacked: false,
ticks: {
display: true
},
},
y: {
stacked: false,
beginAtZero: true,
}
}
},
plugins: [pluginRoundedCornersFill]
};
new Chart('LineFill', config);
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<canvas id="LineFill"></canvas>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With