Skip to content

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

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions