Chartjs-Chart Js Line positioning

0👍

Edit: A simpler solution occurred to me, that has the same effect as the solution 2 below but doesn’t require remapping of data. It simply declares a second category axis and lets chart.js reassign the space. Because of its simplicity I pushed it as the favoured solution, but the discussion on the reallocation of intervals below should not be ignored.

const ageArray = Array.from({length: 12}, (_, i)=>20+(i+1)/100), // + (i+1)/100 to make sure which point gets in which month
    arrayOfUsers = Array.from({length: 12}, ()=>Math.ceil(7+8*Math.random())),
    moreUsers = Array.from({length: 12}, ()=>Math.ceil(5+10*Math.random()));

new Chart('chart1', {
    grouped: true,
    data: {
        labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
        datasets: [
            {
                label: 'age',
                data: ageArray,
                xAxisID: 'x1',
                backgroundColor: 'rgba(33, 221, 33, 0.3)',

                borderWidth: 2,
                tension: 0,
                fill: true,
                pointRadius: 4,
                type: "line",
                order: 2,
            },
            {
                label: 'users',
                data: arrayOfUsers,
                backgroundColor: "#fc9a00",
                order: 0,
                type: "bar",
            },
            {
                label: 'more users',
                data: moreUsers,
                backgroundColor: "#4fa1ee",
                order: 1,
                type: "bar",
            },
        ],
    },

    options: {
        clip: false,
        responsive: true,
        interaction: {
            intersect: false,
            mode: "index",
        },
        maintainAspectRatio: false,
        scales: {
            x: {
                grid: {
                    display: false,
                    drawTicks: false,
                },
                border: {
                    display: false,
                    dash: [2, 4],
                },
                ticks: {
                    padding: 1,
                    beginAtZero: true,
                    min: 0,
                },
                offset: true,
            },
            x1: {
                display: false
            },
            y: {
                offset: false,

                border: {
                    display: false,
                    dash: [2, 4],
                },
                grid: {
                    drawTicks: false,
                    color: "#eaecef",
                    lineWidth: 2,
                },
                ticks: {
                    padding: 1,
                    stepSize: 250,
                    beginAtZero: true,
                    min: 0,
                },
            },
        },
        plugins: {
            legend: {
                position: "bottom",
                labels: {
                    usePointStyle: true,
                    padding: 24,
                },
            },
        },
    },
});
<div style="height:500px">
    <canvas id="chart1"></canvas>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.3.0/chart.umd.js"
        integrity="sha512-CMF3tQtjOoOJoOKlsS7/2loJlkyctwzSoDK/S40iAB+MqWSaf50uObGQSk5Ny/gfRhRCjNLvoxuCvdnERU4WGg=="
        crossOrigin="anonymous" referrerpolicy="no-referrer"></script>

Original post

The points are well placed as they are in the line graph, since the number of points for the line graph is equal to the number of intervals for the bar, so each point should be placed in the centre of the interval, which means the first and last one are half-interval off.

If you want to extend the line graph to cover the whole x-space, there are simple technical solutions. For instance, add a secondary x axis for the line.

However, there remains the fundamental problem: if there are N months of data, you have N bar pairs – that is N intervals while the the array contains N points (which means N-1 intervals). So you have to chose from (at least) two imperfect solutions to cover the missing interval:

  1. move only the first and the last point, each by half interval to cover the missing interval or
  2. move all points proportionally, which means no point is very far from its correct position, but also most of them are slightly off-centric.

In the implementations below there are the two cases, with points highlighted so one can see what happens in each case. The difference is only in the line that generates the data array for the line.

First variant:

const ageArray = Array.from({length: 12}, (_, i)=>20+(i+1)/100), // + (i+1)/100 to make sure which point gets in which month
    arrayOfUsers = Array.from({length: 12}, ()=>Math.ceil(7+8*Math.random())),
    moreUsers = Array.from({length: 12}, ()=>Math.ceil(5+10*Math.random()));

new Chart('chart1', {
    grouped: true,
    data: {
        labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
        datasets: [
            {
                label: 'age',
                data: ageArray.map((age, i) => ({x: i===0 ? i : i === ageArray.length-1 ? i+1 : i+0.5, y: age})),
                xAxisID: 'x1',
                backgroundColor: 'rgba(33, 221, 33, 0.3)',

                borderWidth: 2,
                tension: 0,
                fill: true,
                pointRadius: 4,
                type: "line",
                order: 2,
            },
            {
                label: 'users',
                data: arrayOfUsers,
                backgroundColor: "#fc9a00",
                order: 0,
                type: "bar",
            },
            {
                label: 'more users',
                data: moreUsers,
                backgroundColor: "#4fa1ee",
                order: 1,
                type: "bar",
            },
        ],
    },

    options: {
        clip: false,
        responsive: true,
        interaction: {
            intersect: false,
            mode: "index",
        },
        maintainAspectRatio: false,
        scales: {
            x: {
                grid: {
                    display: false,
                    drawTicks: false,
                },
                border: {
                    display: false,
                    dash: [2, 4],
                },
                ticks: {
                    padding: 1,
                    beginAtZero: true,
                    min: 0,
                },
                offset: true,
            },
            x1: {
                type: 'linear',
                display: false
            },
            y: {
                offset: false,

                border: {
                    display: false,
                    dash: [2, 4],
                },
                grid: {
                    drawTicks: false,
                    color: "#eaecef",
                    lineWidth: 2,
                },
                ticks: {
                    padding: 1,
                    stepSize: 250,
                    beginAtZero: true,
                    min: 0,
                },
            },
        },
        plugins: {
            legend: {
                position: "bottom",
                labels: {
                    usePointStyle: true,
                    padding: 24,
                },
            },
        },
    },
});
<div style="height:500px">
    <canvas id="chart1"></canvas>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.3.0/chart.umd.js" integrity="sha512-CMF3tQtjOoOJoOKlsS7/2loJlkyctwzSoDK/S40iAB+MqWSaf50uObGQSk5Ny/gfRhRCjNLvoxuCvdnERU4WGg=="  crossOrigin="anonymous" referrerpolicy="no-referrer"></script>

and the second one:

const ageArray = Array.from({length: 12}, (_, i)=>20+(i+1)/100), // + (i+1)/100 to make sure which point gets in which month
    arrayOfUsers = Array.from({length: 12}, ()=>Math.ceil(7+8*Math.random())),
    moreUsers = Array.from({length: 12}, ()=>Math.ceil(5+10*Math.random()));

new Chart('chart1', {
    grouped: true,
    data: {
        labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
        datasets: [
            {
                label: 'age',
                data: ageArray.map((age, i) => ({x: i * ageArray.length/(ageArray.length-1) , y: age})),
                xAxisID: 'x1',
                backgroundColor: 'rgba(33, 221, 33, 0.3)',

                borderWidth: 2,
                tension: 0,
                fill: true,
                pointRadius: 4,
                type: "line",
                order: 2,
            },
            {
                label: 'users',
                data: arrayOfUsers,
                backgroundColor: "#fc9a00",
                order: 0,
                type: "bar",
            },
            {
                label: 'more users',
                data: moreUsers,
                backgroundColor: "#4fa1ee",
                order: 1,
                type: "bar",
            },
        ],
    },

    options: {
        clip: false,
        responsive: true,
        interaction: {
            intersect: false,
            mode: "index",
        },
        maintainAspectRatio: false,
        scales: {
            x: {
                grid: {
                    display: false,
                    drawTicks: false,
                },
                border: {
                    display: false,
                    dash: [2, 4],
                },
                ticks: {
                    padding: 1,
                    beginAtZero: true,
                    min: 0,
                },
                offset: true,
            },
            x1: {
                type: 'linear',
                display: false
            },
            y: {
                offset: false,

                border: {
                    display: false,
                    dash: [2, 4],
                },
                grid: {
                    drawTicks: false,
                    color: "#eaecef",
                    lineWidth: 2,
                },
                ticks: {
                    padding: 1,
                    stepSize: 250,
                    beginAtZero: true,
                    min: 0,
                },
            },
        },
        plugins: {
            legend: {
                position: "bottom",
                labels: {
                    usePointStyle: true,
                    padding: 24,
                },
            },
        },
    },
});
<div style="height:500px">
    <canvas id="chart1"></canvas>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.3.0/chart.umd.js"
        integrity="sha512-CMF3tQtjOoOJoOKlsS7/2loJlkyctwzSoDK/S40iAB+MqWSaf50uObGQSk5Ny/gfRhRCjNLvoxuCvdnERU4WGg=="
        crossOrigin="anonymous" referrerpolicy="no-referrer"></script>

Leave a comment