chart.jschartjs-plugin-zoom

Chartjs: Set minimum value for zoom on drag and proper user feedback


I am using Chartjs 4.0.1 and chartjs-plugin-zoom 2.0.0 and my chart look like this: enter image description here

I have set the drag option to be enabled so the user can draw a rectangle to zoom in. Also I have set the zoom mode to 'x'. So the user can only zoom in on the x axis but not on the y axis. Now I want to limit how far the user can zoom in, to a timespan of one month. I have managed to do that when using the mousewheel to zoom in. But I dont know how to achive the same when using the drag option. I have it configured like this:

drag:{
  enabled: true,
  backgroundColor:'rgba(180,180,180,0.4)',
  threshold: 25,
}

The threshold seems to be my best option to a limit. However that is in pixels and it only says how wide the drawn rectangle has to be for a zoom to occur. I am already using the onZoomStart callback to check how far the chart is zoomed in and based on that decide if the user can zoom in even more. But apparently that callback is only executed when zooming by mousewheel but not when dragging. So I think I would need to be able to set the threshold of the drag object dynamically. Does anyone know how to do that?

Also I was wondering, is it possible to change the border color of the rectangle when dragging to show the user if it is big enough for a scroll to occur?


Solution

  • The standard solution seems to be to set a limits:{x:{minRange:...}} option. It took me a while to realise where that option should be inserted.

    Below is a code snippet with some data resembling yours and a minRange set to 90 days (so I can skip adjusting the tick interval).

    Also, there's a hack that changes the color of the drag rectangle to red if the interval is less than the 90 days. It can easily be adapted to completely reject the zoom for less than the desired interval, instead of the current standard behavior which is to adjust (extend) the interval until it is equal to minRange.

    The same in this fiddle.

        const nPoints = 400,
            t0 = Date.parse("2018-06-02T00:00:00Z"),
            dt = 2.5*365/nPoints*24*3600*1000;
        const data = Array.from(
            {length: nPoints},
            (_, i)=>({
                "timestamp":(t0+dt*i),
                value: 80*Math.sin(i*Math.PI/nPoints)+2*Math.random()
            })
        );
        let mouseMoveHandler = null;
        chart = new Chart(document.getElementById("myChart"), {
            type: 'line',
            data: {
                datasets: [{
                    label: "Count",
                    //pointStyle: false,
                    pointRadius: 2,
                    showLine: true,
                    fill: true,
                    tension: 0,
                    borderColor: '#aa6577',
                    //pointRadius: 4,
                    //pointBorderWidth: 1,
                    //pointBackgroundColor: '#7265ce',
                    data: data
                }]
            },
            options: {
                parsing: {
                    xAxisKey: 'timestamp',
                    yAxisKey: 'value'
                },
                spanGaps: false,
                responsive: false,
                scales: {
                    x: {
                        bounds: 'ticks',
                        type: 'time',
                        time: {
                            unit: 'month',
                        },
                        title: {
                            display: false,
                            text: 'time'
                        },
                        ticks: {
                            display: true,
                            color: '#cecece'
                        }
                    },
                    y: {
                        type: 'linear',
                        display: true,
                        min: -10,
                        max: 140,
                        ticks: {
                            autoSkip: true,
                            color: '#cecece'
                        },
                        grid:{
                            color: ctx => ctx.tick.value === 0 ? '#000' : '#ddd',
                            lineWidth: ctx => ctx.tick.value === 0 ? 3 : 1,
                        },
                        title: {
                            display: false,
                            text: 'Count',
                            align: 'end'
                        },
                    }
                },
                plugins:{
                    legend:{
                        display: false
                    },
                    zoom: {
                        zoom: {
                            drag: {
                                enabled: true,
                                backgroundColor:'rgba(180,180,180,0.4)',
                            },
                            mode: 'x',
                            onZoomStart({chart, event}){
                                const x0 = chart.scales.x.getValueForPixel(event.clientX);
                                if(event.type==="mousedown"){
                                    mouseMoveHandler = function(e){
                                        if(
                                            Math.abs(chart.scales.x.getValueForPixel(e.clientX) - x0) <
                                                chart.options.plugins.zoom.limits.x.minRange
                                        ){
                                            chart.options.plugins.zoom.zoom.drag.backgroundColor = 'rgba(255,180,180,0.4)';
                                        }
                                        else{
                                            chart.options.plugins.zoom.zoom.drag.backgroundColor = 'rgba(180,180,180,0.4)';
                                        }
                                    };
                                    chart.canvas.addEventListener("mousemove", mouseMoveHandler);
                                    chart.canvas.addEventListener("mouseup", function(){
                                        if(mouseMoveHandler){
                                            chart.canvas.removeEventListener("mousemove", mouseMoveHandler);
                                            mouseMoveHandler = null;
                                        }
                                    }, {once: true});
                                }
                            },
                            onZoomComplete({chart}){
                                if(mouseMoveHandler){
                                    chart.canvas.removeEventListener("mousemove", mouseMoveHandler);
                                    mouseMoveHandler = null;
                                }
                                document.querySelector('#zoom').innerText = chart.getZoomLevel().toFixed(1)+'x';
                                document.querySelector('#xSpan').innerText =
                                    Math.round((chart.scales.x.max-chart.scales.x.min)/24/3600/1000)+'days';
                            }
                        },
                        limits:{
                            x: {
                                minRange: 90 * 24* 3600 * 1000
                            }
                        }
                    }
                }
            }
        });
        document.querySelector('#resetZoom').addEventListener('click', function(){chart.resetZoom();});
        document.querySelector('#xSpan').innerText = Math.round((chart.scales.x.max-chart.scales.x.min)/24/3600/1000)+'days';
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.0.1/chart.umd.min.js"
            integrity="sha512-HyprZz2W40JOnIBIXDYHCFlkSscDdYaNe2FYl34g1DOmE9J+zEPoT4HHHZ2b3+milFBtiKVWb4sorDVVp+iuqA=="
            crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-zoom/2.0.0/chartjs-plugin-zoom.min.js"
            integrity="sha512-B6F98QATBNaDHSE7uANGo5h0mU6fhKCUD+SPAY7KZDxE8QgZw9rewDtNiu3mbbutYDWOKT3SPYD8qDBpG2QnEg=="
            crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js">
    </script>
    
    <canvas id="myChart" style="height:500px; width: 90vw"></canvas>
    <button id="resetZoom">Reset zoom</button> <br>
    zoom: <span id="zoom">1x</span><br>
    X axis span: <span id="xSpan"></span>