javascriptreactjschart.jsreact-chartjschartjs-plugin-zoom

ChartJS zoom/pan onPan event not firing in React useEffect hook


I've run into an issue with ChartJS's Pan/Zoom plugin event onPan not firing when the Chart configuration is assigned from inside of a useEffect hook. I am listening to the pan & zoom event and firing an event bus accordingly to synchronize multiple charts' zoom/pan bounds with each other. The problem arises with only onPan as onZoom works perfectly fine.

Here's the general important code (this is all in a functional component):

useEffect(() => {
    if(applicableEffectCondition.isMet) {
      return; // to prevent infinite loop
    }

    // do code magic ...

    function dispatchInteractEvent(reference: MutableRefObject < any | undefined > ) {
        dispatch({
            type: "@@charts/INTERACT",
            payload: {
                sender: reference.current.id,
                x: {
                    min: reference.current.scales.x.min,
                    max: reference.current.scales.x.max,
                },
            }
        });
    }

    const options: ChartOptions = {
        // ...

        plugins: {

            zoom: {
                limits: {
                    // minimum X value & maximum X value
                    x: {
                        min: Math.min(...labelsNumber),
                        max: Math.max(...labelsNumber)
                    },
                },
                zoom: {
                    wheel: {
                        enabled: true,
                        speed: 0.1,
                    },
                    pinch: {
                        enabled: true
                    },
                    mode: 'x',
                    onZoom: () => {
                        alert("I am zooming!");
                        dispatchInteractEvent(chartRef);
                    }
                },
                pan: {
                    enabled: true,
                    mode: 'x',

                    rangeMin: {
                        y: allDataMin
                    },
                    rangeMax: {
                        y: allDataMax
                    },

                    onPan: () => {
                        alert("I am panning!");
                        dispatchInteractEvent(chartRef);
                    }
                }
                as any
            }
        },
    };

    setChartOptions(options); // use state

    const data: ChartData = {
        labels: labelsString,
        datasets: datasetState.getDatasets(),
    };

    setChartData(data); // use state
}, [applicableChannels]);

useBus(
    '@@charts/INTERACT',
    (data) => {
        const payload = data.payload;

        if (payload.sender === chartRef.current.id) {
            return;
        }

        // update chart scale/zoom/pan to be in sync with all others
        chartRef.current.options.scales.x.min = payload.x.min;
        chartRef.current.options.scales.x.max = payload.x.max;

        chartRef.current.update();
    },
    [],
);

return (

      <Chart type={"line"} ref={chartRef} options={chartOptions} data={chartData} />
);

As you can see in the configuration, I put two alerts to notify me when the events are fired. "I am zooming!" is fired perfectly fine, but "I am panning!" is never fired. Nor is any other pan-specific event.

This is a very odd issue. Any help would be appreciated! Thanks!


Solution

  • The issue has been resolved.

    When a chart is initialized, HammerJS is initialized and adds listeners for panning/zooming to the canvas. The issue happens when the chart is initialized with a configuration before the configuration is created (setting the configuration to an empty state), and when the real configuration is updated (with the zoom/pan plugin), the state of enabled in the pan object is not updated correctly, so panning becomes disabled.

    The solution is to wait to display the chart React Element until the configuration is available to populate it.

    This issue was really happening because the configuration was being set in a useEffect hook, making it available in a later event loop tick than the actual JSX element was being created in.