Skip to content

Race condition: drag-zoom triggers a pan when switching quickly from pan (ctrl+drag) to drag-zoom #945

@leopowershield

Description

@leopowershield

Description

When pan.modifierKey: 'ctrl' and zoom.drag.enabled: true are both configured, there is a race condition when switching quickly between the two interactions.

If the user:

  1. Pans the chart by holding Ctrl + drag
  2. Releases Ctrl
  3. Immediately starts a new drag (without Ctrl) to trigger drag-zoom

...the new drag pans the chart, but it is still showing the drag-zoom selection box. The plugin appears to still be in pan mode for a short period after Ctrl is released. If the user waits 1–2 seconds between releasing Ctrl and starting the next drag, the drag-zoom works correctly.

This only happens when the two interactions are performed in quick succession. It is a timing/state issue — the plugin does not fully exit pan mode before the next mousedown event is processed.

Steps to reproduce

  1. Open the minimal example below.
  2. Hold Ctrl and drag to pan the chart.
  3. Immediately release Ctrl and drag again (without Ctrl) to zoom.
  4. Observe: the chart pans again instead of showing the drag-zoom selection area.

Expected: releasing Ctrl and dragging should immediately trigger drag-zoom with the blue selection box.
Actual: the chart pans again. The blue selection box only appears if you wait ~1–2 seconds after releasing Ctrl.

Environment

  • chartjs-plugin-zoom: 2.0.1
  • chart.js: 4.4.0
  • chartjs-adapter-date-fns: 3.0.0
  • hammerjs: 2.0.8
  • Browser: Chrome / Firefox (reproducible in both)

Minimal reproducible example

Paste into a single HTML file and open in browser:

<!DOCTYPE html>
<html>
<head>
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/hammerjs@2.0.8/hammer.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@2.0.1/dist/chartjs-plugin-zoom.min.js"></script>
</head>
<body style="font-family: sans-serif; padding: 1rem;">
  <p>
    1. Hold <b>Ctrl + drag</b> to pan.<br>
    2. Release Ctrl and <b>immediately drag</b> (no Ctrl) to zoom.<br>
    Bug: step 2 pans instead of showing the zoom selection box.<br>
    If you <b>wait 1–2 seconds</b> before step 2, it works correctly.
  </p>
  <canvas id="chart" style="max-height: 400px;"></canvas>
  <script>
    const now = Date.now();
    new Chart(document.getElementById('chart'), {
      type: 'line',
      data: {
        labels: Array.from({ length: 12 }, (_, i) => now + i * 1000),
        datasets: [
          {
            label: 'Series A',
            data: [1, 3, 2, 5, 4, 7, 6, 8, 5, 9, 4, 6],
            borderColor: 'steelblue',
            tension: 0.3,
            pointRadius: 4
          },
          {
            label: 'Series B',
            data: [4, 2, 6, 3, 7, 2, 8, 3, 7, 4, 8, 3],
            borderColor: 'tomato',
            tension: 0.3,
            pointRadius: 4
          }
        ]
      },
      options: {
        plugins: {
          zoom: {
            zoom: {
              wheel: { enabled: true },
              drag: {
                enabled: true,
                borderColor: 'rgb(54, 162, 235)',
                borderWidth: 1,
                backgroundColor: 'rgba(54, 162, 235, 0.3)',
                threshold: 5
              },
              mode: 'xy',
              scaleMode: 'xy'
            },
            pan: {
              enabled: true,
              mode: 'xy',
              modifierKey: 'ctrl'
            }
          }
        },
        scales: {
          x: { type: 'time' },
          y: { grace: '25%' }
        }
      }
    });
  </script>
</body>
</html>

Notes

The root cause appears to be that the plugin's internal pan state (driven by Hammer.js) is not reset synchronously when the Ctrl key is released. The modifierKey check happens at the start of a gesture, but Hammer.js gesture recognizers have their own lifecycle and may still be in a "possible" or "began" state from the previous pan gesture when the next mousedown fires.

A workaround — dynamically toggling zoom.drag.enabled and pan.enabled on mousedown (capture phase) depending on e.ctrlKey — resolves the issue, but requires significant boilerplate. A proper fix in the plugin would be to ensure pan state and Hammer recognizer state are fully reset as soon as modifierKey is no longer held.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions