Chartjs-How to do a round tick marks in ChartJS?

0👍

Custom ticks can be drawn by extending the scale class used by the chart, as described in the the documentation. In particular, to draw circular ticks on a linear axis one may override the drawGrid method of LinearScale:

class LinearScaleWithRoundTicks extends Chart.LinearScale{
    static id = "linear-roundTicks";
    drawGrid(chartArea) {
        const optionsGrid = this.options.grid,
            optionsTicks = this.options.ticks;
        let drawTicks = false;
        if(optionsGrid.display && optionsGrid.drawTicks){
            optionsGrid.drawTicks = false;
            drawTicks = true;
        }
        super.drawGrid(chartArea);
        if(drawTicks){
            optionsGrid.drawTicks = true;
            const ctx = this.ctx;
            const items = this._gridLineItems;

            for(const item of items){
                const tickR = optionsTicks.radius ?? 4,
                    fillColor = optionsTicks.backgroundColor ?? 'black';
                ctx.save();
                ctx.beginPath();
                ctx.arc(item.tx1, item.ty1, tickR, 0, 2*Math.PI);
                ctx.fillStyle = fillColor;
                ctx.fill();
                ctx.restore();
            }
        }
    }
}
Chart.register(LinearScaleWithRoundTicks);

used as in the example below:

class LinearScaleWithRoundTicks extends Chart.LinearScale{
    static id = "linear-roundTicks";
    drawGrid(chartArea) {
        const optionsGrid = this.options.grid,
            optionsTicks = this.options.ticks;
        let drawTicks = false;
        if(optionsGrid.display && optionsGrid.drawTicks){
            optionsGrid.drawTicks = false;
            drawTicks = true;
        }
        super.drawGrid(chartArea);
        if(drawTicks){
            optionsGrid.drawTicks = true;
            const ctx = this.ctx;
            const items = this._gridLineItems;

            for(const item of items){
                const tickR = optionsTicks.radius ?? 4,
                    fillColor = optionsTicks.backgroundColor ?? 'black';
                ctx.save();
                ctx.beginPath();
                ctx.arc(item.tx1, item.ty1, tickR, 0, 2*Math.PI);
                ctx.fillStyle = fillColor;
                ctx.fill();
                ctx.restore();
            }
        }
    }
}
Chart.register(LinearScaleWithRoundTicks);

const nPoints = 15;
const datasets = Array.from({length: 2},
    (_, dataSet)=>({
        data: Array.from(
            {length: nPoints},
            (_, i)=>({
                x: i+1,
                y: 2+Math.cos((-i*4*(dataSet+1))/nPoints)+Math.random()/5
            })
        ),
        tension: 0.3,
        label: 'Set '+(dataSet+1),
        borderWidth: 1
    })
);
const config = {
    type: 'line',
    data: {datasets},
    options: {
        scales:{
            x: {
                type: 'linear-roundTicks',
                ticks:{
                    radius: 4,
                    backgroundColor: 'rgba(0,0,0,0.4)'
                },
            }
        },
        animation: false
    },
};
new Chart(document.querySelector('#myChart'), config);
<div style="min-height:400px">
<canvas id="myChart"></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>

There is here a "mixin vs inheritance" problem: the solution above only works for linear scales; if a chart has multiple types of axes, e.g., category, time, linear, one would have to extend all the classes of used scales. Extending the base case Scale would not work since the derived classes (e.g., CategoryScale) use the original class, not the extended one. That’s why a mixin is a solution that works for any type of axis, although seems much hackier.

In the example below, the drawGrid is changed (only once per axis) in its beforeDataLimits callback. Thus different drawing functions, drawXTick and drawYTick are given for each of the axes, one linear and the other categorical.

function mixIn_tickDraw(scale, drawTick){
    if(!scale.customTickDraw){
        scale.customTickDraw = true;
        const super_drawGrid = scale.drawGrid;
        scale.drawGrid = function(chartArea){
            const optionsGrid = this.options.grid,
                optionsTicks = this.options.ticks;
            let drawTicks = false;
            if(optionsGrid.display && optionsGrid.drawTicks){
                optionsGrid.drawTicks = false;
                drawTicks = true;
            }
            super_drawGrid.call(this, chartArea);
            if(drawTicks){
                optionsGrid.drawTicks = true;
                const ctx = this.ctx;
                const items = this._gridLineItems;

                for(const item of items){
                    drawTick(ctx, item, optionsTicks);
                }
            }
        }
    }
}

function drawXTick(ctx, tick, optionsTicks){
    const tickR = optionsTicks.radius ?? 4,
        fillColor = optionsTicks.backgroundColor ?? 'black';
    ctx.save();
    ctx.beginPath();
    ctx.arc(tick.tx1, tick.ty1, tickR, 0, 2 * Math.PI);
    ctx.fillStyle = fillColor;
    ctx.fill();
    ctx.restore();
}

function drawYTick(ctx, tick, optionsTicks){
    const tickR = optionsTicks.radius ?? 4,
        fillColor = optionsTicks.backgroundColor ?? 'gray';
    ctx.save();
    const nSpikes = 5;
    //draw star, from https://stackoverflow.com/a/58043598/16466946
    ctx.beginPath();
    for(let i = 0; i < nSpikes*2; i++){
        let rotation = Math.PI/2;
        let angle = (i/(nSpikes*2))*Math.PI*2+rotation;
        let dist = tickR * (1+ i%2);
        let x = tick.tx1 + Math.cos(angle)*dist;
        let y = tick.ty1 +Math.sin(angle)*dist;
        if(i === 0) {
            ctx.moveTo(x, y);
            continue; //skip
        }
        ctx.lineTo(x, y);
    }
    ctx.closePath();
    ctx.fillStyle = fillColor;
    ctx.fill();
    ctx.restore();
}

const nPoints = 15;
const datasets = Array.from({length: 2},
    (_, dataSet)=>({
        data: Array.from(
            {length: nPoints},
            (_, i)=> 2+Math.cos((-i*4*(dataSet+1))/nPoints)+Math.random()/5
        ),
        tension: 0.3,
        label: 'Set '+(dataSet+1),
        borderWidth: 1
    })
);
const config = {
    type: 'line',
    data: {
        labels: Array.from({length: nPoints}, (_, i)=>'L'+(i+1)),
        datasets
    },
    options: {
        scales:{
            x: {
                ticks:{
                    radius: 4,
                    backgroundColor: 'rgba(16,92,78,0.7)'
                },
                beforeDataLimits(scale){
                    mixIn_tickDraw(scale, drawXTick);
                }
            },
            y: {
                ticks:{
                    radius: 4,
                    backgroundColor: 'rgba(0,0,255,0.9)'
                },
                beforeDataLimits(scale){
                    mixIn_tickDraw(scale, drawYTick);
                }
            }
        },
        //animation: false
    },
};

new Chart(document.querySelector('#myChart'), config);
<div style="min-height:400px">
<canvas id="myChart"></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