[Chartjs]-Minor grid lines in log chart with chartjs

2👍

  1. Chart.js will, by default, generate minor grid lines for logarithm axes. OP’s code doesn’t show any minor grid lines because the ticks.callback function returns null for non-conforming ticks; this results in the removal of that tick and of the grid line.

    A possible solution is to replace return null with return '' in ticks.callback; this keeps the tick (and gridline) but shows no label. One drawback is that even if the label has size zero, the system allocates a space for it, which may impact the labeled ticks; one may want to disable ticks.autoSkip: false.

  2. After that, the true challenge is to differentiate visually minor grid lines from major ones. The solution comes from the fact that most grid line settings are scriptable options, that can be assigned functions, useful to adapt the result (the option, e.g., the grid line color) to runtime circumstances – in this case to whether the corresponding tick is minor or major.

    I identified in the code below all relevant options that may be used to visually differentiate minor grid lines and ticks from major ones. You may keep only some of them or just none.

  3. I also added a possible filter for the ticks in the function options.scales.y.afterBuildTicks, more a demonstration of that feature. One can also filter grid lines in ticks.callback, by actually returning null for some values.

Options, extracted:

const config = {
    type: 'line',
    data: {
        // ............
    },
    options: {
        // ..... other options 
        scales: {
            x: // .....
            y: {
                display: true,
                type: 'logarithmic',

                grid: {
                    tickColor: function(data){ // color of the tick line
                        return data.tick.major ? 'rgb(196, 196, 196)' :
                            'rgba(196, 196, 196, 0)' // minor ticks not visible
                    },
                    lineWidth: function(data){ // the width of grid line
                        return data.tick.major ? 2 : 1
                    },
                    color:function(data){ // the color of the grid line
                        return data.tick.major ? 'rgb(196, 196, 196)' :
                            'rgba(196, 196, 196, 0.5)'
                    }
                },
                border:{
                    dash:function(data){
                        // dash pattern for grid lines
                        // (unexpected position for this option here)
                        return data.tick.major ? null : [5, 1]
                    }
                },
                min: 1,
                afterBuildTicks: function(ax){
                    ax.ticks = ax.ticks.filter(({value})=>{
                        const r = value/ Math.pow(10, Math.floor(Math.log10(value)+1e-5));
                        return Math.abs(r - Math.round(r)) < 1e-5
                    })
                    // this eliminates tick values like 15 or 150 and only keeps
                    // those of the form n*10^m with n, m one digit integers
                    // this might not be necessary
                },
                ticks: {
                    callback: function (value, index, ticks) {
                        if (value === 1000000) return "1M";
                        if (value === 100000) return "100K";
                        if (value === 10000) return "10K";
                        if (value === 1000) return "1K";
                        if (value === 100) return "100";
                        if (value === 10) return "10";
                        if (value === 1) return "1";
                        return '';
                    }
                }
            }
        },
        // ......
    }
}
   

Full running example, forked from OP’s, height increased to make log minor grid lines visible.

const termFreqWithChartjs = (ctx, series, term, termFreq) => {
    const config = {
        type: 'line',
        data: {
            labels: termFreq.map(e => e.journalYear),
            datasets: [
                {
                    label: series.y1,
                    data: termFreq.map(e => e.total),
                    borderColor: 'red',
                    borderWidth: 1,
                    backgroundColor: 'rgba(255, 0, 0, 0.1)',
                    pointStyle: 'circle',
                    pointRadius: 3,
                    pointBorderColor: 'rgb(0, 0, 0)'
                },
                {
                    label: series.y2,
                    data: termFreq.map(e => e.withImages),
                    borderColor: 'blue',
                    borderWidth: 1,
                    backgroundColor: 'rgba(0, 0, 255, 0.1)',
                    pointStyle: 'circle',
                    pointRadius: 3,
                    pointBorderColor: 'rgb(0, 0, 0)'
                }
            ]
        },
        options: {
            interaction: {
                intersect: false,
                mode: 'x',
            },
            animation: false,
            responsive: true,
            scales: {
                x: {
                    display: true,
                },
                y: {
                    display: true,
                    type: 'logarithmic',

                    grid: {
                        tickColor: function(data){
                            return data.tick.major ? 'rgb(196, 196, 196)' :
                                'rgba(196, 196, 196, 0)' // minor ticks not visible
                        },
                        lineWidth: function(data){
                            return data.tick.major ? 2 : 1
                        },
                        color:function(data){
                            return data.tick.major ? 'rgb(196, 196, 196)' :
                                'rgba(196, 196, 196, 0.5)'
                        }
                    },
                    border:{
                        dash:function(data){
                            // dash line for grid lines
                            // (unexpected position for this option here)
                            return data.tick.major ? null : [5, 1]
                        }
                    },
                    min: 1,
                    afterBuildTicks: function(ax){
                        ax.ticks = ax.ticks.filter(({value})=>{
                            const r = value/ Math.pow(10, Math.floor(Math.log10(value)+1e-5));
                            return Math.abs(r - Math.round(r)) < 1e-5
                        })
                        // this eliminates tick values like 15 or 150 and only keeps
                        // those of the form n*10^m with n, m one digit integers
                        // this might not be necessary
                    },
                    ticks: {
                        autoSkip: false,
                        callback: function (value, index, ticks) {
                            if (value === 1000000) return "1M";
                            if (value === 100000) return "100K";
                            if (value === 10000) return "10K";
                            if (value === 1000) return "1K";
                            if (value === 100) return "100";
                            if (value === 10) return "10";
                            if (value === 1) return "1";
                            return '';
                        }
                    }
                }
            },
            plugins: {
                title: {
                    display: true,
                    text: `occurrence of '${term}' in text by year`,
                },
                legend: {
                    display: true,
                    position: 'chartArea',
                    labels: {
                        usePointStyle: true,
                    }
                },
                tooltip: {
                    enabled: true
                }
            }
        }
    };

    let canvas = document.getElementById('termFreq');

    if (canvas) {
        termFreqChart.destroy();
        termFreqChart = new Chart(canvas, config);
    }
    else {
        canvas = document.createElement('canvas');
        canvas.id = "termFreq";
        canvas.width = 960;
        canvas.height = 600;
        ctx.appendChild(canvas);
        termFreqChart = new Chart(canvas, config);
    }
}
const termFreq = [
    {
        "journalYear": 1841,
        "total": 3,
        "withImages": 0
    },
    {
        "journalYear": 1846,
        "total": 2,
        "withImages": 0
    },
    {
        "journalYear": 1850,
        "total": 2,
        "withImages": 0
    },
    {
        "journalYear": 1851,
        "total": 26,
        "withImages": 0
    },
    {
        "journalYear": 1853,
        "total": 5,
        "withImages": 0
    },
    {
        "journalYear": 1855,
        "total": 7,
        "withImages": 0
    },
    {
        "journalYear": 1857,
        "total": 27,
        "withImages": 0
    },
    {
        "journalYear": 1859,
        "total": 30,
        "withImages": 0
    },
    {
        "journalYear": 1860,
        "total": 4,
        "withImages": 0
    },
    {
        "journalYear": 1861,
        "total": 9,
        "withImages": 0
    },
    {
        "journalYear": 1862,
        "total": 18,
        "withImages": 0
    },
    {
        "journalYear": 1863,
        "total": 4,
        "withImages": 0
    },
    {
        "journalYear": 1866,
        "total": 12,
        "withImages": 0
    },
    {
        "journalYear": 1877,
        "total": 1,
        "withImages": 0
    },
    {
        "journalYear": 1884,
        "total": 2,
        "withImages": 0
    },
    {
        "journalYear": 1886,
        "total": 12,
        "withImages": 0
    },
    {
        "journalYear": 1887,
        "total": 2,
        "withImages": 0
    },
    {
        "journalYear": 1890,
        "total": 5,
        "withImages": 0
    },
    {
        "journalYear": 1893,
        "total": 4,
        "withImages": 0
    },
    {
        "journalYear": 1894,
        "total": 9,
        "withImages": 0
    },
    {
        "journalYear": 1895,
        "total": 3,
        "withImages": 0
    },
    {
        "journalYear": 1896,
        "total": 1,
        "withImages": 0
    },
    {
        "journalYear": 1902,
        "total": 3,
        "withImages": 0
    },
    {
        "journalYear": 1904,
        "total": 14,
        "withImages": 0
    },
    {
        "journalYear": 1905,
        "total": 10,
        "withImages": 0
    },
    {
        "journalYear": 1910,
        "total": 1,
        "withImages": 0
    },
    {
        "journalYear": 1912,
        "total": 1,
        "withImages": 0
    },
    {
        "journalYear": 1913,
        "total": 3,
        "withImages": 0
    },
    {
        "journalYear": 1914,
        "total": 7,
        "withImages": 0
    },
    {
        "journalYear": 1915,
        "total": 5,
        "withImages": 0
    },
    {
        "journalYear": 1920,
        "total": 1,
        "withImages": 0
    },
    {
        "journalYear": 1922,
        "total": 8,
        "withImages": 0
    },
    {
        "journalYear": 1924,
        "total": 1,
        "withImages": 0
    },
    {
        "journalYear": 1926,
        "total": 5,
        "withImages": 0
    },
    {
        "journalYear": 1928,
        "total": 5,
        "withImages": 0
    },
    {
        "journalYear": 1932,
        "total": 2,
        "withImages": 0
    },
    {
        "journalYear": 1949,
        "total": 3,
        "withImages": 0
    },
    {
        "journalYear": 1950,
        "total": 1,
        "withImages": 0
    },
    {
        "journalYear": 1953,
        "total": 2,
        "withImages": 0
    },
    {
        "journalYear": 1955,
        "total": 3,
        "withImages": 0
    },
    {
        "journalYear": 1956,
        "total": 1,
        "withImages": 0
    },
    {
        "journalYear": 1958,
        "total": 2,
        "withImages": 0
    },
    {
        "journalYear": 1959,
        "total": 1,
        "withImages": 0
    },
    {
        "journalYear": 1960,
        "total": 2,
        "withImages": 0
    },
    {
        "journalYear": 1975,
        "total": 2,
        "withImages": 0
    },
    {
        "journalYear": 1979,
        "total": 136,
        "withImages": 0
    },
    {
        "journalYear": 1990,
        "total": 27,
        "withImages": 5
    },
    {
        "journalYear": 1992,
        "total": 3,
        "withImages": 0
    },
    {
        "journalYear": 1993,
        "total": 1,
        "withImages": 0
    },
    {
        "journalYear": 1997,
        "total": 1,
        "withImages": 1
    },
    {
        "journalYear": 2000,
        "total": 251,
        "withImages": 225
    },
    {
        "journalYear": 2001,
        "total": 14,
        "withImages": 0
    },
    {
        "journalYear": 2003,
        "total": 141,
        "withImages": 139
    },
    {
        "journalYear": 2004,
        "total": 2,
        "withImages": 0
    },
    {
        "journalYear": 2005,
        "total": 62,
        "withImages": 0
    },
    {
        "journalYear": 2006,
        "total": 16,
        "withImages": 12
    },
    {
        "journalYear": 2007,
        "total": 79,
        "withImages": 17
    },
    {
        "journalYear": 2008,
        "total": 42,
        "withImages": 16
    },
    {
        "journalYear": 2009,
        "total": 141,
        "withImages": 60
    },
    {
        "journalYear": 2010,
        "total": 111,
        "withImages": 5
    },
    {
        "journalYear": 2011,
        "total": 62,
        "withImages": 14
    },
    {
        "journalYear": 2012,
        "total": 100,
        "withImages": 25
    },
    {
        "journalYear": 2013,
        "total": 137,
        "withImages": 129
    },
    {
        "journalYear": 2014,
        "total": 54,
        "withImages": 23
    },
    {
        "journalYear": 2015,
        "total": 139,
        "withImages": 73
    },
    {
        "journalYear": 2016,
        "total": 166,
        "withImages": 36
    },
    {
        "journalYear": 2017,
        "total": 87,
        "withImages": 74
    },
    {
        "journalYear": 2018,
        "total": 79,
        "withImages": 49
    },
    {
        "journalYear": 2019,
        "total": 142,
        "withImages": 96
    },
    {
        "journalYear": 2020,
        "total": 84,
        "withImages": 27
    },
    {
        "journalYear": 2021,
        "total": 907,
        "withImages": 386
    },
    {
        "journalYear": 2022,
        "total": 79,
        "withImages": 50
    },
    {
        "journalYear": 2023,
        "total": 3,
        "withImages": 0
    }
];

const series = {
    x: "journal year",
    y1: "total",
    y2: "with images"
}

const ctx = document.getElementById('graphdiv');
termFreqWithChartjs(ctx, series, 'formica', termFreq);
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<div id="graphdiv"></div>

Leave a comment