Using chart.js to create bar chart where one record can have multiple colors

47 Views Asked by At

I am creating bar chart using chart.js which represents what medicine patient was taking and if it was working. X-axis is represented as timestamp.

In the image below you can see how it is supposed to look: enter image description here

Green represents that medicine is working, red represents that it is not. As you can one medicine can be working for a while and then stop (see Metotrexat in the image).

Do you have any idea how this can be done using chart.js?

As of now, I have implemented it as separate records but I face these issues:

  1. How to put that label over all parts that belongs under one medicine when it has multiple records?
  2. How to achieve that border-radius as it can be multiple separate records which just act as one?

Thank you for your opinions.

1

There are 1 best solutions below

0
On

We should've started from your attempts, the way you structured your data, etc. Because of that, this is only valid as an example, rather than a full solution.

One can set multiple colors of a bar using for the backgroundColor property, a LinearGradient color, described in the documentation.

If we set the color stops a linear gradient like this:

gradient.addColorStop(0, 'green');
gradient.addColorStop(0.3, 'green');
gradient.addColorStop(0.3, 'red');
gradient.addColorStop(1, 'red');

the surface will be up to the 30% mark green and then abruptly change color for the rest of 70% to red.

Here's the code example. inspired by the image you pasted, with all the nitty-gritty implementation details

Chart.defaults.elements.bar.fractions = []; // define new option for bars
Chart.defaults.elements.bar.fractionColors = []; // define new option for bars
function gradientBar(dataContext, options){
    const chart = dataContext.chart,
        data = dataContext.dataset.data[dataContext.dataIndex].x,
        fraction = options.fractions ?? 0.5,
        fractionColors = options.fractionColors;
    const vScale = chart.getDatasetMeta(dataContext.datasetIndex).vScale,
        [x0, x1] = data.map(s=>vScale.parse(s)).map(vScale.getPixelForValue, vScale);
    if(x0 && x1){
        const gradient = chart.ctx.createLinearGradient(x0, 0, x1, 0);
        if(fraction > 0){
            gradient.addColorStop(0, fractionColors[0]);
            gradient.addColorStop(fraction, fractionColors[0]);
        }
        if(fraction < 1){
            gradient.addColorStop(fraction, fractionColors[1]);
            gradient.addColorStop(1, fractionColors[1]);
        }
        return gradient;
    }
}

const medicines = {
    // each type has the same vertical position and time intervals
    // for drugs in the same type are not supposed to overlap
    "type A": {
        Azathioprine: {
            from: '2024-02-02T13:32:00.000Z',
            to: '2024-02-18T16:42:00.000Z',
            workedUntil: true // always worked
        },
        Methotrexate:{
            from: '2024-02-21T12:10:00.000Z',
            to: '2024-03-05T12:10:00.000Z',
            workedUntil: '2024-02-25T15:03:00.000Z'
        },
    },
    "type B": {
        Infliximab:{
            from: '2024-02-12T12:10:00.000Z',
            to: '2024-02-21T10:12:00.000Z',
            workedUntil: false // never worked
        }
    }
}

medicinesToChartjsDatasets = function(medicines){
    const data = [], fractions = [];
    for(const type in medicines){
        for(const name in medicines[type]){
            const item = medicines[type][name];
            data.push({y: type, x: [item.from, item.to], name, raw: item});
            if(item.workedUntil === true){
                fractions.push(1);
            }
            else if(item.workedUntil === false){
                fractions.push(0);
            }
            else{
                const [a, b, x] = [item.from, item.to, item.workedUntil].map(Date.parse);
                fractions.push((x-a)/(b-a))
            }
        }
    }

    return [{ // just one dataset
        data,
        fractions,
        fractionColors: [['rgba(0, 255, 0, 0.75)', 'rgba(255, 0, 0, 0.75)']],
        borderRadius: 10,
        borderSkipped: false,
        datalabels:{
            formatter(dataContext){
                return dataContext.name
            },
            anchor: dataContext => { // anchor the label to the largest segment - if red, to the right
                const f = dataContext.dataset.fractions[dataContext.dataIndex];
                return f > 0 && f < 0.5 ? 'end' : 'start'
            },
            align: dataContext => {
                const f = dataContext.dataset.fractions[dataContext.dataIndex];
                return f > 0 && f < 0.5 ? 'start' : 'end'
            },
            color: '#000'
        },
        backgroundColor: gradientBar
    }]

    // return data.map((drug, index) => // each drug as a dataset
    //     ({
    //         label: drug.name,
    //         data: [drug],
    //         fractions: [fractions[index]],
    //         fractionColors: [['rgba(0, 255, 0, 0.75)', 'rgba(255, 0, 0, 0.75)']],
    //         borderRadius: 10,
    //         borderSkipped: false,
    //         backgroundColor: gradientBar
    //
    //     }))
}

const config = {
    type: 'bar',
    data: {
        datasets: medicinesToChartjsDatasets(medicines)
    },
    options: {
        responsive: true,
        maintainAspectRatio: false,
        indexAxis: 'y',
        scales: {
            x: {
                type: 'time',
                min: '2024-02-02T13:32:00.000Z',
                time:{
                    unit: 'day'
                }
            },
            y:{
                stacked: true
            }
        },
        plugins: {
            legend: {
                display: false
            },
            title: {
                display: true,
                text: 'Chart.js Floating Bar Chart'
            },
            tooltip:{
                callbacks: {
                    title([ctx]){
                        return ctx.dataset.data[ctx.dataIndex].name;
                    },
                    label(ctx){
                        const {from, to, workedUntil} = ctx.dataset.data[ctx.dataIndex].raw;
                        return [`from ${from}`,`to ${to}`,
                            workedUntil === true ? 'always worked' : workedUntil === false ? 'never worked' :
                            `worked until ${workedUntil}`];
                    }
                }
            }
        }
    },
    plugins: [ChartDataLabels]
};

new Chart('myChart', config);
<div style="height:150px; min-width: 600px; width: 100%">
    <canvas id="myChart"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
<script src="https://cdn.jsdelivr.net/npm/luxon@^2"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon@^1"></script>