Zoom and Pan in react-chartjs-2

Viewed 10264

I have recently implemented chart display using react-chartjs-2 (https://github.com/jerairrest/react-chartjs-2)

I want to enable zooming and panning feature so that it will be more user-friendly in touch based screens. To implement this features, I installed react-hammerjs and chartjs-plugin-zoom.

import {Chart, Line} from 'react-chartjs-2';
import Hammer from 'react-hammerjs';
import zoom from 'chartjs-plugin-zoom'

And I registered the plugin

componentWillMount(){
    Chart.plugins.register(zoom)
}

And the render method goes as follows:

render(){
    return <Line data={data} options={options} />
}

Pan and Zoom options:

pan:{
    enabled=true,
    mode:'x'
},
zoom:{
    enabled:true,
    drag:true,
    mode:'xy'
}

I guess this is the correct method to implement. Unfortunately, the above implementation did not work. I will be really grateful if some of you guys already implemented Zooming and Panning using react-chartjs-2 plugin, please share if how you achieved these functionalities. Or you could point out the problem in my code above.

5 Answers

In order to add Zoom and Pan capabilities to your chart components based on react-chartjs-2, you can follow the steps as shown below:

Step 1: you need to install chartjs-plugin-zoom

$ npm install chartjs-plugin-zoom

Step 2: Import chartjs-plugin-zoom in your chart component

import 'chartjs-plugin-zoom';

Step 3: Enable zoom and pan in the ChartJS component options

        zoom: {
          enabled: true,
          mode: 'x',
        },
        pan: {
          enabled: true,
          mode: 'x',
        },

That's it. So now your chart component should look like this:

import React from 'react';
import { Line } from 'react-chartjs-2';
import 'chartjs-plugin-zoom';

export default function TimelineChart({ dailyDataSets }) {
  const lineChart = dailyDataSets[0] ? (
    <Line
      data={{
        labels: dailyDataSets.map(({ date }) => date),
        datasets: [
          {
            data: dailyDataSets.map((data) => data.attr1),
            label: 'First data set',
            borderColor: 'red',
            fill: true,
          },
          {
            data: dailyDataSets.map((data) => data.attr2),
            label: 'Second data set',
            borderColor: 'green',
            fill: true,
          },
        ],
      }}
      options={{
        title: { display: true, text: 'My Chart' },
        zoom: {
          enabled: true,
          mode: 'x',
        },
        pan: {
          enabled: true,
          mode: 'x',
        },
      }}
    />
  ) : null;

  return <div>{lineChart}</div>;
}


Notes:

  1. You don't have to install hammerjs explicitly, as it will be automatically included by installing chartjs-plugin-zoom as its dependency, see below:
$ npm ls
...
├─┬ chartjs-plugin-zoom@0.7.7
│ └── hammerjs@2.0.8
...
  1. One way to zoom as an example (at least for Mac), you can move your mouse pointer into the chart area, and then scroll your mouse down or up. Once zoomed in, you can keep your mouse clicked while dragging left or right.

There's a syntax error under pan object for enabled attribute.

You've mistakenly put = instead of :

Replace this:

pan:{
  enabled=true,
  ...
},

With:

pan:{
  enabled:true,
  ...
},

And also as @Jun Bin suggested:

Install hammerjs as:

npm install hammerjs --save

And in your component, import it as:

import Hammer from "hammerjs";

you imported the wrong hammer it should be from "hammerjs";

You need to add import 'chartjs-plugin-zoom'; and then add zoom options into options.plugins.zoom, like:

const options = {
  plugins: {
    zoom: {
      pan: {
        enabled: true,
        mode: 'x',
      },
      zoom: {
        enabled: true,
        drag: true,
        mode: 'xy'
      }
    }
  }
};

I am trying to do this in a NextJS Project. But to no success so far. I am using a timeseries plot with date-fns/locale for German and English and keep getting this error:

Cannot convert a Symbol value to a string
TypeError: Cannot convert a Symbol value to a string at TypedRegistry.register (webpack-internal:///./node_modules/chart.js/dist/chart.esm.js:4802:50) at Registry._exec (webpack-internal:///./node_modules/chart.js/dist/chart.esm.js:4927:21) at eval (webpack-internal:///./node_modules/chart.js/dist/chart.esm.js:4919:16) at each (webpack-internal:///./node_modules/chart.js/dist/chunks/helpers.segment.js:233:10) at eval (webpack-internal:///./node_modules/chart.js/dist/chart.esm.js:4917:70) at Array.forEach (<anonymous>) at Registry._each (webpack-internal:///./node_modules/chart.js/dist/chart.esm.js:4912:15) at Registry.add (webpack-internal:///./node_modules/chart.js/dist/chart.esm.js:4870:10) at Function.value [as register] (webpack-internal:///./node_modules/chart.js/dist/chart.esm.js:6192:16) at eval (webpack-internal:///./components/Charts/PortfolioPriceLineDual.jsx:39:45) at Module../components/Charts/PortfolioPriceLineDual.jsx (https://dev.domain.de/_next/static/chunks/components_Charts_PortfolioPriceLineDual_jsx.js:7758:1) at Module.options.factory (https://dev.domain.de/_next/static/chunks/webpack.js?ts=1653499440538:655:31) at __webpack_require__ (https://dev.domain.de/_next/static/chunks/webpack.js?ts=1653499440538:37:33) at Function.fn (https://dev.domain.de/_next/static/chunks/webpack.js?ts=1653499440538:310:21)

My Component:


import { Line } from 'react-chartjs-2'

import 'chartjs-adapter-date-fns'
import { de, enGB, ja } from 'date-fns/locale'

import dynamic from 'next/dynamic'
import 'chart.js/auto'
import { useRouter } from 'next/router'
import { Chart } from 'chart.js'

// import zoomPlugin from 'chartjs-plugin-zoom';
const zoomPlugin = dynamic(() => import('chartjs-plugin-zoom'), {
  ssr: false,
})
Chart.register(zoomPlugin);

const PortfolioPriceLineDual = ({
  title,
  data,
  unit,
  axesOptions,
  showLegend = true,
}) => {
  const totalDuration = 5000
  const delayBetweenPoints = totalDuration / data.datasets[0].data.length
  // const animation =
  const { locale } = useRouter()
  let format
  switch (locale) {
    case 'de-DE':
      format = de
      break
    case 'en-US':
      format = enGB

      break
    case 'ja-JP':
      format = ja

      break
    default:
      break
  }

  return (
    <Line
      data={data}
      options={{
        responsive: true,
        // maintainAspectRatio: true,
        // aspectRatio: 16 / 9,
        resizeDelay: 5,
        animation: {
          x: {
            type: 'number',
            easing: 'linear',
            duration: delayBetweenPoints,
            from: NaN, // the point is initially skipped
            delay: (ctx) => {
              if (ctx.type !== 'data' || ctx.xStarted) {
                return 0
              }
              ctx.xStarted = true
              return ctx.index * delayBetweenPoints
            },
          },
          y: {
            type: 'number',
            easing: 'linear',
            duration: delayBetweenPoints,
            from: (ctx) => {
              return ctx.index === 0
                ? ctx.chart.scales.y.getPixelForValue(100)
                : ctx.chart
                    .getDatasetMeta(ctx.datasetIndex)
                    .data[ctx.index - 1].getProps(['y'], true).y
            },
            delay: (ctx) => {
              if (ctx.type !== 'data' || ctx.yStarted) {
                return 0
              }
              ctx.yStarted = true
              return ctx.index * delayBetweenPoints
            },
          },
          y1: {
            type: 'number',
            easing: 'linear',
            duration: delayBetweenPoints,
            from: (ctx) => {
              return ctx.index === 0
                ? ctx.chart.scales.y.getPixelForValue(100)
                : ctx.chart
                    .getDatasetMeta(ctx.datasetIndex)
                    .data[ctx.index - 1].getProps(['y'], true).y
            },
            delay: (ctx) => {
              if (ctx.type !== 'data' || ctx.yStarted) {
                return 0
              }
              ctx.yStarted = true
              return ctx.index * delayBetweenPoints
            },
          },
        },
        interaction: {
          mode: 'index',
          intersect: false,
        },
        scales: {
          x: {
            type: 'time',

            time: {
              unit: 'year',
              displayFormats: {
                quarter: 'yyyy',
              },
              tooltipFormat: 'MMMM yyyy',
            },
            adapters: {
              date: {
                locale: format,
              },
            },
            ticks: {
              align: 'start',
              color: '#122a42',
              font: {
                size: 14,
                weight: 'bold',
              },
            },
            grid: {
              display: true,
              drawBorder: false,
              drawOnChartArea: true,
              drawTicks: true,
            },
          },
          y: {
            type: 'logarithmic',

            grid: {
              display: true,
              drawBorder: false,
              drawOnChartArea: true,
              drawTicks: true,
            },
            ticks: {
              color: '#122a42',
              align: 'end',
              font: {
                size: 10,
                weight: 'normal',
              },
              // Include a dollar sign in the ticks
              // stepSize: 1000,
              callback: function (value) {
                // callback: function (value, index, ticks) {
                return `${new Intl.NumberFormat(locale, axesOptions).format(
                  value
                )}`
              },
            },
          },
          y1: {
            type: 'linear',
            display: true,
            position: 'right',

            // grid line settings
            grid: {
              drawOnChartArea: false, // only want the grid lines for one axis to show up
            },
            ticks: {
              color: '#122a42',
              align: 'end',
              font: {
                size: 10,
                weight: 'normal',
              },
              // Include a dollar sign in the ticks
              // stepSize: 1000,
              callback: function (value) {
                // callback: function (value, index, ticks) {
                return `${new Intl.NumberFormat(locale, axesOptions).format(
                  value
                )}`
              },
            },
          },
        },
        zoom: {
          enabled: true,
          mode: 'x',
        },
        pan: {
          enabled: true,
          mode: 'x',
        },
        plugins: {
          zoom: {
            enabled: true,
            mode: 'x',
          },
          pan: {
            enabled: true,
            mode: 'x',
          },
          // zoom: {
          //   zoom: {
          //     wheel: {
          //       enabled: true,
          //     },
          //     pinch: {
          //       enabled: true,
          //     },
          //     mode: 'x',
          //   },
          // },
          title: {
            display: true,
            color: '#151C30',
            font: {
              size: 26,
              weight: 'bold',
              style: 'normal',
            },
            padding: {
              bottom: 10,
            },
            text: `${title}`,
          },
          tooltip: {
            enabled: true,
            backgroundColor: '#122a42',
            itemSort: function (a, b) {
              return b.raw - a.raw
            },
            callbacks: {
              label: function (context) {
                let label = context.dataset.label || ''

                if (label) {
                  label += ': '
                }
                if (context.parsed.y !== null) {
                  label += `${new Intl.NumberFormat(locale, axesOptions).format(
                    context.parsed.y
                  )} ${unit}`
                }
                return label
              },
            },
          },
          legend: {
            position: 'bottom',
            labels: {
              // This more specific font property overrides the global property
              color: '#151C30',
              font: {
                size: 12,
                weight: 'light',
              },
            },
          },
        },
      }}
    />
  )
}

export default PortfolioPriceLineDual

Related