Search code examples
chartschart.jsscrollbar

How can I implement a scroll bar that adapts to zoom on a Chart.js chart?


I'm trying to implement a scroll bar, which adapts to zoom, on a chart made with chart.js. I've found several posts that allow me to implement the scroll bar. So I made a code pen to try it out.

Here's the result.

However, I realized that a problem persists. Whenever I zoom in, the scroll bar doesn't adapt to keep covering the whole chart. In other words, after i zoom in, I can no longer scroll over the entire chart. I managed to achieve the desired result with FusionCharts, but not with Chart.js. The "problem" is that way i made this is intrinsic to FusionCharts and there is no code to take inspiration from to implement it with chart.js.

FusionCharts.ready(function() {
var myChart = new FusionCharts({
    type: "zoomline",
    renderAt: "chart-container",
    width: "100%",
    height: "100%",
    dataFormat: "json",
    dataSource
  }).render();
});

Here's an example of what I'm trying to achieve.

So I was wondering if anyone has managed to implement such a scroll bar, and if so, how did you proceed?


Solution

  • I completely forgot about this question...

    In the end, I managed to implement the scrollbar with a custom inline plugin.

    var canvas = document.getElementById('myCanvas'),
      ctx = canvas.getContext('2d'),
      isDragging,
      rectPosition = null;
    
     function generateData(number){
        let data = [];
        for (let i = 0; i < number ; i++){
          data.push(Math.random()*10);
        }
        return data;
      }
    
    function generateLabels(number){
      let labels = [];
      for (let i = 0; i < number ; i++){
        labels.push(i);
      }
      return labels;
    }
    
    function generateDatasets(nbrDatasets, nbrData) {
      var datasets = [];
      for(let i = 0 ; i < nbrDatasets ; i++ ){
        datasets.push({data: generateData(nbrData), pointRadius: 0, pointHoverRadius: 7, label: `Dataset ${i}`});
      }
      return datasets;
    }
    
    var cfg = {
      type: 'line',
      data:{
        labels: generateLabels(100),
        datasets: generateDatasets(2, 100),
      },
      options:{
        animations: false,
        responsive: true,
            maintainAspectRatio: false,
        normalized: true,
        layout: {
                    padding: {
                    bottom: 40,
                },
            },
        interaction:{
                mode: 'nearest',
              intersect: false,
              axis: 'x'
            },
        plugins:{
          zoom:{
            zoom:{
              drag:{
                modifierKey: 'shift',
                enabled:true,
              },
              mode: 'x',
            }
          }
        }
      },
      plugins:[{
                    id: "zoomRangeSlider",
                    afterDatasetsDraw(chart, args, plugins) {
                        var chartMin = chart.data.labels[0],
                            chartMax = chart.data.labels[chart.data.labels.length - 1],
                            currentZoom = chart.options.scales.x.max - chart.options.scales.x.min,
                            minMaxDiff = chartMax - chartMin,
                            posX;
                        const {
                            ctx,
                            chartArea: {
                                left,
                                top,
                                bottom,
                                right,
                                width
                            },
                        } = chart;
                        var rectWidth = (currentZoom / minMaxDiff) * width;
    
                        if(rectWidth < 7) {
                            rectWidth = 7;
                        }
    
                        posX = (chart.options.scales.x.min - chartMin) / minMaxDiff;
                        rectPosition = (posX * (width)) + (rectWidth / 2) + left;
    
                        if (rectPosition < left + rectWidth / 2) {
                            rectPosition = left + rectWidth / 2
                        }
    
                        // Scrollbar background
                        ctx.beginPath();
                        ctx.fillStyle = "#f0f0f0";
                        ctx.rect(left, bottom + 40, width, 15);
                        ctx.fill();
    
                        // Scrollbar
                        ctx.beginPath();
                        ctx.fillStyle = "#cdcdcd";
                        ctx.rect(rectPosition - (rectWidth / 2), bottom + 40, rectWidth, 13);
                        ctx.fill();
                    },
    
                    afterEvent(chart, args, plugins) {
                        var chartMin = chart.data.labels[0],
                            chartMax = chart.data.labels[chart.data.labels.length - 1],
                            currentZoom = chart.options.scales.x.max - chart.options.scales.x.min,
                            step = chart.data.labels[1] - chart.data.labels[0],
                            minMaxDiff = chartMax - chartMin;
                        const {
                            ctx,
                            canvas,
                            chartArea: {
                                left,
                                top,
                                bottom,
                                right,
                                width
                            },
                        } = chart;
    
                        if (args.event.type === "mousemove" && isDragging === true && args.event.y > 1.05 * bottom) {
    
                            // Check where the mouse is
                            var xPosition = parseFloat(((args.event.x-left) / (right-left)).toFixed(2));
                            xPosition = xPosition > 1 ? 1 : xPosition < 0 ? 0 : xPosition;
                            var min = chartMin + (xPosition * minMaxDiff) - (0.5 * currentZoom) - (chartMin + (xPosition * minMaxDiff) - (0.5 * currentZoom)) % step,
                                max = min + currentZoom;
    
                            if (max >= chartMax) {
                                max = chartMax;
                                min = max - currentZoom;
                            } else if (min < chartMin) {
                                min = chartMin;
                                max = min + currentZoom;
                            }
    
                            chart.options.scales.x.min = min;
                            chart.options.scales.x.max = max;
                            args.changed = true;
    
                            // Synchronize the grid and the chart
                            chart.update('none');
                        }
                    },
                }]
    };
    
    const myChart = new Chart(ctx, cfg);
    
    canvas.onmousedown = function(){
        isDragging = true;
    };
    
    canvas.onmouseup = function(){
        isDragging = false;
    };
    
    function zoomout(){
      myChart.options.scales.x.min = undefined;
      myChart.options.scales.x.max = undefined;
      myChart.update('none');
    }
    .container {
      height: 400px;
    }
    <script src="https://cdn.jsdelivr.net/npm/chart.js@4.3.0/dist/chart.umd.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/hammerjs@2.0.8"></script>
    <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@2.0.1/dist/chartjs-plugin-zoom.min.js "></script>
    
    <div class="container">
      <button onclick="zoomout()">Zoom out</button>
      <canvas id='myCanvas'></canvas>
    </div>