javascriptreactjschart.jsreact-chartjs-2

How to change offset of horizontal bars in react-chartjs-2


I have used following graph config to render graph:

{
    type: 'horizontalBar',
    indexAxis: 'y',
    barThickness: 12,
    scales: {
      x: {
        suggestedMax: 6,
        suggestedMin: 0,
        grid: {
          display: false,
          drawBorder: false,
        },
        ticks: {
          stepSize: 2,
          callback: function (value, index, ticks) {
            return value ? value + 'h' : value;
          },
        }
      },
      y: {
        type: 'category',
        grid: {
          drawBorder: false,
          borderDash: [4, 3],
          color: "#C2C8CD",
          drawTicks: false,
          offset: false,
        },
        ticks: {
        }
      }
    }

What I'm getting using above configuration. I'm getting this graph

what I want:

what I want

Kindly help to render this graph using react-chartjs-2.


Solution

  • If what you want to achieve is to have the grid lines pass just under each bar, that is not a standard feature, since the grid lines pass either through the middle of the bar (with offset: false) or through the middle of the space between bars.

    You can use chartjs-plugin-annotation to draw annotation lines instead of grid lines; short of that I can think of two ways to do that: one using a trick, the other hacky.

    The trick is to add an empty dataset after your actual dataset. That will allocate an empty space on y axis for a bar of this dataset under each of the real bars. You'll also have to make some adjustments: set options.scales.y.ticks.align to 'end' (or set a matching value for options.scales.y.ticks.labelOffset, see the docs) to correctly align the tick labels; disable (filter out) the legend entry for the phantom dataset; adjust the y position of the x scale.

    Here's a set of options doing that - I used comments to remove wrong or superfluous options from your post:

    new Chart('myChart', {
        type: 'bar',
        data: {
            labels: ["Awake", "Deep", "Light", "Rem"],
            datasets: [
                {
                    label: 'data',
                    backgroundColor: ['orange', 'green', 'lightBlue', 'black'],
                    data: [3*6/100, 6*6/100, 76*6/100, 15*6/100]
                },
                {} // empty "phantom" dataset
            ]
        },
        options: {
            //type: 'horizontalBar', // no such thing, also type: not here
            indexAxis: 'y',
            barThickness: 16,
            scales: {
                x: {
                    suggestedMax: 6,
                    suggestedMin: 0,
                    grid: {
                        display: false,
                        //drawBorder: false, // this is border.display
                    },
                    ticks: {
                        stepSize: 2,
                        callback: function (value) {
                            return value ? value + 'h' : value;
                        },
                        maxRotation: 0
                    },
                    position:{ // set the x axis position below the last line
                        y: ({scale: {chart}}) => chart.scales.y.max
                    }
                },
                y: {
                    //type: 'category', // default
                    grid: {
                        //drawBorder: false,  // this is border.display
                        //borderDash: [4, 3], // this is under border.dash
                        color: "#C2C8CD",
                        drawTicks: false,
                        offset: false,
                    },
                    ticks: {
                        align: 'end'
                    },
                    border:{
                        display: true,
                        dash: [4, 3]
                    }
                }
            },
            plugins: {
                legend: {
                    display: false,
                    labels: { // if display enabled, filter out empty dataset
                        filter(item){
                            return !!item.text;
                        }
                    }
                },
                datalabels:{
                    anchor: 'end',
                    align: 'end',
                    formatter: function(value, context) {
                        const sum = context.dataset.data.reduce((s, x) => s + x);
                        return Math.round(value/sum*100).toFixed(1).replace(/\.?0+$/, '') + '%';
                    }
                }
            }
        },
        plugins: [ChartDataLabels],
    });
    <div style="max-width:400px; height: 200px">
        <canvas id="myChart"></canvas>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels"></script>

    The hacky solution would be based on a plugin that moves the bars so they are on top of the grid line; that move can be done in the beforeDatasetDraw hook, but care should be taken such that each bar is moved moved only once, while the beforeDatasetDraw handler might be called several times.

    new Chart('myChart', {
        type: 'bar',
        data: {
            labels: ["Awake", "Deep", "Light", "Rem"],
            datasets: [
                {
                    label: 'data',
                    backgroundColor: ['orange', 'green', 'lightBlue', 'black'],
                    data: [3*6/100, 6*6/100, 76*6/100, 15*6/100]
                }
            ]
        },
        options: {
            indexAxis: 'y',
            barThickness: 16,
    
            clip: false, // allow the for the bar to overflow to the padding
            scales: {
                x: {
                    suggestedMax: 6,
                    suggestedMin: 0,
                    grid: {
                        display: false,
                        drawBorder: false, // this is border.display
                    },
                    ticks: {
                        stepSize: 2,
                        callback: function (value) {
                            return value ? value + 'h' : value;
                        },
                        maxRotation: 0
                    },
                    position:{ // set the x axis position below the last line
                        y: ({scale: {chart}}) => chart.scales.y.max
                    }
                },
                y: {
                    grid: {
                        color: "#C2C8CD",
                        drawTicks: false,
                        offset: false,
                    },
                    ticks: {
                        //align: 'end', OR:
                        labelOffset: function({scale}){
                            const chart = scale.chart;
                            return -chart.getDatasetMeta(0).data[0].height/2 ||
                                (scale.getPixelForValue(0) - scale.getPixelForValue(1/3));
                        },
                    },
                    border:{
                        display: false,
                        dash: [4, 3]
                    },
                }
            },
            plugins: {
                legend: {
                    display: false
                },
                datalabels: {
                    anchor: 'end',
                    align: 'end',
                    formatter: function(value, context) {
                        const sum = context.dataset.data.reduce((s, x) => s + x);
                        return Math.round(value/sum*100).toFixed(1).replace(/\.?0+$/, '') + '%';
                    }
                }
            }
        },
        plugins: [
            ChartDataLabels,
            {
                beforeDatasetDraw(_, {meta}){
                    if(meta.$shifted || meta.type !== 'bar' || !meta.data[0]?.height){ 
                        // shift bars only once, on bars only and after height has been computed
                        // assumes there's no initial animation on barWidth.
                        return;
                    }
                    meta.data.forEach(
                        (elem) => {elem.y -= elem.height/2;}
                    );
                    meta.$shifted = true;
                }
            }
        ],
    });
    <div style="max-width:400px; height: 200px">
    <canvas id="myChart"></canvas>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels"></script>