chartjs / chartjs/chartjs-plugin-zoom

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

Open
#945 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
623
Forks
331
Avg merge
7h 5m
Merged PRs (30d)
7

Description

## 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:

```html





1. Hold Ctrl + drag to pan.

2. Release Ctrl and immediately drag (no Ctrl) to zoom.

Bug: step 2 pans instead of showing the zoom selection box.

If you wait 1–2 seconds before step 2, it works correctly.




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%' }
}
}
});

```

## 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.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.