Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Line fill borderradius in Chart.js

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">
like image 662
Odd Anfinn Flakk Avatar asked Oct 25 '25 14:10

Odd Anfinn Flakk


1 Answers

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>
like image 84
kikon Avatar answered Oct 27 '25 02:10

kikon