@thewhitehaven04/chartjs-plugin-zoom 2.2.7 → 2.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1172 @@
1
+ /*!
2
+ * @thewhitehaven04/chartjs-plugin-zoom v2.2.7
3
+ * https://www.chartjs.org/chartjs-plugin-zoom/2.2.7/
4
+ * (c) 2016-2026 chartjs-plugin-zoom Contributors
5
+ * Released under the MIT License
6
+ */
7
+ import Hammer from 'hammerjs';
8
+ import { isNumber, isNullOrUndef, valueOrDefault, almostEquals, sign, getRelativePosition, _isPointInArea } from 'chart.js/helpers';
9
+
10
+ const eventKey = (key)=>`${key}Key`;
11
+ const getModifierKey = (opts)=>opts?.enabled && opts.modifierKey ? opts.modifierKey : undefined;
12
+ const keyPressed = (key, event)=>key && event[eventKey(key)];
13
+ const keyNotPressed = (key, event)=>key && !event[eventKey(key)];
14
+ function directionEnabled(mode, dir, chart) {
15
+ if (mode === undefined) {
16
+ return true;
17
+ } else if (typeof mode === 'string') {
18
+ return mode.indexOf(dir) !== -1;
19
+ } else if (typeof mode === 'function') {
20
+ return mode({
21
+ chart
22
+ }).indexOf(dir) !== -1;
23
+ }
24
+ return false;
25
+ }
26
+ function directionsEnabled(mode, chart) {
27
+ if (typeof mode === 'function') {
28
+ mode = mode({
29
+ chart
30
+ });
31
+ }
32
+ if (typeof mode === 'string') {
33
+ return {
34
+ x: mode.indexOf('x') !== -1,
35
+ y: mode.indexOf('y') !== -1
36
+ };
37
+ }
38
+ return {
39
+ x: false,
40
+ y: false
41
+ };
42
+ }
43
+ function debounce(fn, delay) {
44
+ let timeout;
45
+ return function() {
46
+ clearTimeout(timeout);
47
+ timeout = setTimeout(fn, delay);
48
+ return delay;
49
+ };
50
+ }
51
+ function getScaleUnderPoint({ x, y }, chart) {
52
+ const scales = chart.scales;
53
+ const scaleIds = Object.keys(scales);
54
+ for(let i = 0; i < scaleIds.length; i++){
55
+ const scale = scales[scaleIds[i]];
56
+ if (y >= scale.top && y <= scale.bottom && x >= scale.left && x <= scale.right) {
57
+ return scale;
58
+ }
59
+ }
60
+ return null;
61
+ }
62
+ const convertOverScaleMode = (chart, overScaleMode, scaleEnabled, enabled)=>{
63
+ if (!overScaleMode) {
64
+ return;
65
+ }
66
+ const overScaleEnabled = directionsEnabled(overScaleMode, chart);
67
+ for (const axis of [
68
+ 'x',
69
+ 'y'
70
+ ]){
71
+ if (overScaleEnabled[axis]) {
72
+ scaleEnabled[axis] = enabled[axis];
73
+ enabled[axis] = false;
74
+ }
75
+ }
76
+ };
77
+ const getEnabledScales = (chart, enabled)=>{
78
+ const enabledScales = [];
79
+ for (const scaleItem of Object.values(chart.scales)){
80
+ if (enabled[scaleItem.axis]) {
81
+ enabledScales.push(scaleItem);
82
+ }
83
+ }
84
+ return enabledScales || Object.values(chart.scales);
85
+ };
86
+ function getEnabledScalesByPoint(options, point, chart) {
87
+ const { mode = 'xy', scaleMode, overScaleMode } = options || {};
88
+ const scale = getScaleUnderPoint(point, chart);
89
+ const enabled = directionsEnabled(mode, chart);
90
+ const scaleEnabled = directionsEnabled(scaleMode, chart);
91
+ convertOverScaleMode(chart, overScaleMode, scaleEnabled, enabled);
92
+ if (scale && scaleEnabled[scale.axis]) {
93
+ return [
94
+ scale
95
+ ];
96
+ }
97
+ return getEnabledScales(chart, enabled);
98
+ }
99
+
100
+ const chartStates = new WeakMap();
101
+ function getState(chart) {
102
+ let state = chartStates.get(chart);
103
+ if (!state) {
104
+ state = {
105
+ originalScaleLimits: {},
106
+ updatedScaleLimits: {},
107
+ handlers: {},
108
+ options: {},
109
+ targets: {},
110
+ panDelta: {},
111
+ dragging: false,
112
+ panning: false
113
+ };
114
+ chartStates.set(chart, state);
115
+ }
116
+ return state;
117
+ }
118
+ function removeState(chart) {
119
+ chartStates.delete(chart);
120
+ }
121
+
122
+ const isTimeScale = (scale)=>scale.type === 'time';
123
+ const isNotNumber = (value)=>value === undefined || isNaN(value);
124
+ function zoomDelta(val, min, range, newRange) {
125
+ const minPercent = range && isNumber(val) && isNumber(min) ? Math.max(0, Math.min(1, (val - min) / range)) : 0;
126
+ const maxPercent = 1 - minPercent;
127
+ return {
128
+ min: newRange * minPercent,
129
+ max: newRange * maxPercent
130
+ };
131
+ }
132
+ function getValueAtPoint(scale, point) {
133
+ const pixel = scale.isHorizontal() ? point.x : point.y;
134
+ return scale.getValueForPixel(pixel);
135
+ }
136
+ function linearZoomDelta(scale, zoom, center) {
137
+ const range = scale.max - scale.min;
138
+ const newRange = range * (zoom - 1);
139
+ const centerValue = getValueAtPoint(scale, center);
140
+ return zoomDelta(centerValue, scale.min, range, newRange);
141
+ }
142
+ function logarithmicZoomRange(scale, zoom, center) {
143
+ const centerValue = getValueAtPoint(scale, center);
144
+ if (centerValue === undefined) {
145
+ return {
146
+ min: scale.min,
147
+ max: scale.max
148
+ };
149
+ }
150
+ const logMin = Math.log10(scale.min);
151
+ const logMax = Math.log10(scale.max);
152
+ const logCenter = Math.log10(centerValue);
153
+ const logRange = logMax - logMin;
154
+ const newLogRange = logRange * (zoom - 1);
155
+ const delta = zoomDelta(logCenter, logMin, logRange, newLogRange);
156
+ return {
157
+ min: Math.pow(10, logMin + delta.min),
158
+ max: Math.pow(10, logMax - delta.max)
159
+ };
160
+ }
161
+ function getScaleLimits(scale, limits) {
162
+ return limits?.[scale.id] || limits?.[scale.axis] || {};
163
+ }
164
+ function getLimit(state, scale, scaleLimits, prop, fallback) {
165
+ let limit = scaleLimits[prop];
166
+ if (limit === 'original') {
167
+ const original = state.originalScaleLimits[scale.id][prop];
168
+ if (isNumber(original.options)) {
169
+ return original.options;
170
+ }
171
+ if (!isNullOrUndef(original.options)) {
172
+ const parsed = scale.parse(original.options);
173
+ if (isNumber(parsed)) {
174
+ return parsed;
175
+ }
176
+ }
177
+ limit = original.scale;
178
+ }
179
+ return valueOrDefault(limit, fallback);
180
+ }
181
+ function linearRange(scale, pixel0, pixel1) {
182
+ const v0 = scale.getValueForPixel(pixel0) ?? scale.min;
183
+ const v1 = scale.getValueForPixel(pixel1) ?? scale.max;
184
+ return {
185
+ min: Math.min(v0, v1),
186
+ max: Math.max(v0, v1)
187
+ };
188
+ }
189
+ function fixRange(range, { min, max, minLimit, maxLimit }, state, scale) {
190
+ const offset = (range - max + min) / 2;
191
+ min -= offset;
192
+ max += offset;
193
+ const origLimits = {
194
+ min: 'original',
195
+ max: 'original'
196
+ };
197
+ const origMin = getLimit(state, scale, origLimits, 'min', -Infinity);
198
+ const origMax = getLimit(state, scale, origLimits, 'max', Infinity);
199
+ const epsilon = range / 1e6;
200
+ if (almostEquals(min, origMin, epsilon)) {
201
+ min = origMin;
202
+ }
203
+ if (almostEquals(max, origMax, epsilon)) {
204
+ max = origMax;
205
+ }
206
+ if (min < minLimit) {
207
+ min = minLimit;
208
+ max = Math.min(minLimit + range, maxLimit);
209
+ } else if (max > maxLimit) {
210
+ max = maxLimit;
211
+ min = Math.max(maxLimit - range, minLimit);
212
+ }
213
+ return {
214
+ min,
215
+ max
216
+ };
217
+ }
218
+ function updateRange(scale, { min, max }, limits, zoom = false, pan = false) {
219
+ const state = getState(scale.chart);
220
+ const { options: scaleOpts } = scale;
221
+ const scaleLimits = getScaleLimits(scale, limits);
222
+ const { minRange = 0 } = scaleLimits;
223
+ const minLimit = getLimit(state, scale, scaleLimits, 'min', -Infinity);
224
+ const maxLimit = getLimit(state, scale, scaleLimits, 'max', Infinity);
225
+ if (pan && (min < minLimit || max > maxLimit)) {
226
+ return true;
227
+ }
228
+ const scaleRange = scale.max - scale.min;
229
+ const range = zoom ? Math.max(max - min, minRange) : scaleRange;
230
+ if (zoom && range === minRange && scaleRange <= minRange) {
231
+ return true;
232
+ }
233
+ const newRange = fixRange(range, {
234
+ min,
235
+ max,
236
+ minLimit,
237
+ maxLimit
238
+ }, state, scale);
239
+ scaleOpts.min = newRange.min;
240
+ scaleOpts.max = newRange.max;
241
+ state.updatedScaleLimits[scale.id] = newRange;
242
+ return scale.parse(newRange.min) !== scale.min || scale.parse(newRange.max) !== scale.max;
243
+ }
244
+ function zoomNumericalScale(scale, zoom, center, limits) {
245
+ const delta = linearZoomDelta(scale, zoom, center);
246
+ const newRange = {
247
+ min: scale.min + delta.min,
248
+ max: scale.max - delta.max
249
+ };
250
+ return updateRange(scale, newRange, limits, true);
251
+ }
252
+ function zoomLogarithmicScale(scale, zoom, center, limits) {
253
+ const newRange = logarithmicZoomRange(scale, zoom, center);
254
+ return updateRange(scale, newRange, limits, true);
255
+ }
256
+ function zoomRectNumericalScale(scale, from, to, limits) {
257
+ return updateRange(scale, linearRange(scale, from, to), limits, true);
258
+ }
259
+ const integerChange = (v)=>v === 0 || isNaN(v) ? 0 : v < 0 ? Math.min(Math.round(v), -1) : Math.max(Math.round(v), 1);
260
+ function existCategoryFromMaxZoom(scale) {
261
+ const labels = scale.getLabels();
262
+ const maxIndex = labels.length - 1;
263
+ if (scale.min > 0) {
264
+ scale.min -= 1;
265
+ }
266
+ if (scale.max < maxIndex) {
267
+ scale.max += 1;
268
+ }
269
+ }
270
+ function zoomCategoryScale(scale, zoom, center, limits) {
271
+ const delta = linearZoomDelta(scale, zoom, center);
272
+ if (scale.min === scale.max && zoom < 1) {
273
+ existCategoryFromMaxZoom(scale);
274
+ }
275
+ const newRange = {
276
+ min: scale.min + integerChange(delta.min),
277
+ max: scale.max - integerChange(delta.max)
278
+ };
279
+ return updateRange(scale, newRange, limits, true);
280
+ }
281
+ function scaleLength(scale) {
282
+ return scale.isHorizontal() ? scale.width : scale.height;
283
+ }
284
+ function panCategoryScale(scale, delta, limits) {
285
+ const labels = scale.getLabels();
286
+ const lastLabelIndex = labels.length - 1;
287
+ let { min, max } = scale;
288
+ const range = Math.max(max - min, 1);
289
+ const stepDelta = Math.round(scaleLength(scale) / Math.max(range, 10));
290
+ const stepSize = Math.round(Math.abs(delta / stepDelta));
291
+ let applied;
292
+ if (delta < -stepDelta) {
293
+ max = Math.min(max + stepSize, lastLabelIndex);
294
+ min = range === 1 ? max : max - range;
295
+ applied = max === lastLabelIndex;
296
+ } else if (delta > stepDelta) {
297
+ min = Math.max(0, min - stepSize);
298
+ max = range === 1 ? min : min + range;
299
+ applied = min === 0;
300
+ }
301
+ return updateRange(scale, {
302
+ min,
303
+ max
304
+ }, limits) || Boolean(applied);
305
+ }
306
+ const OFFSETS = {
307
+ millisecond: 0,
308
+ second: 500,
309
+ minute: 30 * 1000,
310
+ hour: 30 * 60 * 1000,
311
+ day: 12 * 60 * 60 * 1000,
312
+ week: 3.5 * 24 * 60 * 60 * 1000,
313
+ month: 15 * 24 * 60 * 60 * 1000,
314
+ quarter: 60 * 24 * 60 * 60 * 1000,
315
+ year: 182 * 24 * 60 * 60 * 1000
316
+ };
317
+ function panNumericalScale(scale, delta, limits, canZoom = false) {
318
+ const { min: prevStart, max: prevEnd } = scale;
319
+ let offset = 0;
320
+ if (isTimeScale(scale)) {
321
+ const round = scale.options.time?.round;
322
+ offset = round ? OFFSETS[round] : 0;
323
+ }
324
+ const newMin = scale.getValueForPixel(scale.getPixelForValue(prevStart + offset) - delta);
325
+ const newMax = scale.getValueForPixel(scale.getPixelForValue(prevEnd + offset) - delta);
326
+ if (isNotNumber(newMin) || isNotNumber(newMax)) {
327
+ return true;
328
+ }
329
+ return updateRange(scale, {
330
+ min: newMin,
331
+ max: newMax
332
+ }, limits, canZoom, true);
333
+ }
334
+ function panNonLinearScale(scale, delta, limits) {
335
+ return panNumericalScale(scale, delta, limits, true);
336
+ }
337
+ const zoomFunctions = {
338
+ category: zoomCategoryScale,
339
+ default: zoomNumericalScale,
340
+ logarithmic: zoomLogarithmicScale
341
+ };
342
+ const zoomRectFunctions = {
343
+ default: zoomRectNumericalScale
344
+ };
345
+ const panFunctions = {
346
+ category: panCategoryScale,
347
+ default: panNumericalScale,
348
+ logarithmic: panNonLinearScale,
349
+ timeseries: panNonLinearScale
350
+ };
351
+
352
+ function shouldUpdateScaleLimits(scale, originalScaleLimits, updatedScaleLimits) {
353
+ const { id, options: { min, max } } = scale;
354
+ if (!originalScaleLimits[id] || !updatedScaleLimits[id]) {
355
+ return true;
356
+ }
357
+ const previous = updatedScaleLimits[id];
358
+ return previous.min !== min || previous.max !== max;
359
+ }
360
+ function removeMissingScales(limits, scales) {
361
+ for (const key of Object.keys(limits)){
362
+ if (!scales[key]) {
363
+ delete limits[key];
364
+ }
365
+ }
366
+ }
367
+ function storeOriginalScaleLimits(chart, state) {
368
+ const { scales } = chart;
369
+ const { originalScaleLimits, updatedScaleLimits } = state;
370
+ for (const scale of Object.values(scales)){
371
+ if (shouldUpdateScaleLimits(scale, originalScaleLimits, updatedScaleLimits)) {
372
+ originalScaleLimits[scale.id] = {
373
+ min: {
374
+ scale: scale.min,
375
+ options: scale.options.min
376
+ },
377
+ max: {
378
+ scale: scale.max,
379
+ options: scale.options.max
380
+ }
381
+ };
382
+ }
383
+ }
384
+ removeMissingScales(originalScaleLimits, scales);
385
+ removeMissingScales(updatedScaleLimits, scales);
386
+ return originalScaleLimits;
387
+ }
388
+ function doZoom(scale, amount, center, limits) {
389
+ const fn = zoomFunctions[scale.type] || zoomFunctions.default;
390
+ fn?.(scale, amount, center, limits);
391
+ }
392
+ function doZoomRect(scale, from, to, limits) {
393
+ const fn = zoomRectFunctions[scale.type] || zoomRectFunctions.default;
394
+ fn?.(scale, from, to, limits);
395
+ }
396
+ function getCenter(chart) {
397
+ const ca = chart.chartArea;
398
+ return {
399
+ x: (ca.left + ca.right) / 2,
400
+ y: (ca.top + ca.bottom) / 2
401
+ };
402
+ }
403
+ function zoom(chart, amount, transition = 'none', trigger = 'api') {
404
+ const { x = 1, y = 1, focalPoint = getCenter(chart) } = typeof amount === 'number' ? {
405
+ x: amount,
406
+ y: amount
407
+ } : amount;
408
+ const state = getState(chart);
409
+ const { options: { limits = {}, zoom: zoomOptions } } = state;
410
+ storeOriginalScaleLimits(chart, state);
411
+ const xEnabled = x !== 1;
412
+ const yEnabled = y !== 1;
413
+ const enabledScales = getEnabledScalesByPoint(zoomOptions, focalPoint, chart);
414
+ for (const scale of enabledScales){
415
+ if (scale.isHorizontal() && xEnabled) {
416
+ doZoom(scale, x, focalPoint, limits);
417
+ } else if (!scale.isHorizontal() && yEnabled) {
418
+ doZoom(scale, y, focalPoint, limits);
419
+ }
420
+ }
421
+ chart.update(transition);
422
+ zoomOptions?.onZoom?.({
423
+ chart,
424
+ trigger,
425
+ amount: {
426
+ x,
427
+ y,
428
+ focalPoint
429
+ }
430
+ });
431
+ }
432
+ function zoomRect(chart, p0, p1, transition = 'none', trigger = 'api') {
433
+ const state = getState(chart);
434
+ const { options: { limits = {}, zoom: zoomOptions = {} } } = state;
435
+ const { mode = 'xy' } = zoomOptions;
436
+ storeOriginalScaleLimits(chart, state);
437
+ const xEnabled = directionEnabled(mode, 'x', chart);
438
+ const yEnabled = directionEnabled(mode, 'y', chart);
439
+ for (const scale of Object.values(chart.scales)){
440
+ if (scale.isHorizontal() && xEnabled) {
441
+ doZoomRect(scale, p0.x, p1.x, limits);
442
+ } else if (!scale.isHorizontal() && yEnabled) {
443
+ doZoomRect(scale, p0.y, p1.y, limits);
444
+ }
445
+ }
446
+ chart.update(transition);
447
+ zoomOptions.onZoom?.({
448
+ chart,
449
+ trigger
450
+ });
451
+ }
452
+ function zoomScale(chart, scaleId, range, transition = 'none', trigger = 'api') {
453
+ const state = getState(chart);
454
+ storeOriginalScaleLimits(chart, state);
455
+ const scale = chart.scales[scaleId];
456
+ updateRange(scale, range, undefined, true);
457
+ chart.update(transition);
458
+ state.options.zoom?.onZoom?.({
459
+ chart,
460
+ trigger
461
+ });
462
+ }
463
+ function resetZoom(chart, transition = 'default') {
464
+ const state = getState(chart);
465
+ const originalScaleLimits = storeOriginalScaleLimits(chart, state);
466
+ for (const scale of Object.values(chart.scales)){
467
+ const scaleOptions = scale.options;
468
+ if (originalScaleLimits[scale.id]) {
469
+ scaleOptions.min = originalScaleLimits[scale.id].min.options;
470
+ scaleOptions.max = originalScaleLimits[scale.id].max.options;
471
+ } else {
472
+ delete scaleOptions.min;
473
+ delete scaleOptions.max;
474
+ }
475
+ delete state.updatedScaleLimits[scale.id];
476
+ }
477
+ chart.update(transition);
478
+ state.options.zoom?.onZoomComplete?.({
479
+ chart
480
+ });
481
+ }
482
+ function getOriginalRange(state, scaleId) {
483
+ const original = state.originalScaleLimits[scaleId];
484
+ if (!original) {
485
+ return undefined;
486
+ }
487
+ const { min, max } = original;
488
+ if (isNumber(max.options) && isNumber(min.options)) {
489
+ return max.options - min.options;
490
+ }
491
+ if (isNumber(max.scale) && isNumber(min.scale)) {
492
+ return max.scale - min.scale;
493
+ }
494
+ return undefined;
495
+ }
496
+ function getZoomLevel(chart) {
497
+ const state = getState(chart);
498
+ let min = 1;
499
+ let max = 1;
500
+ for (const scale of Object.values(chart.scales)){
501
+ const origRange = getOriginalRange(state, scale.id);
502
+ if (origRange) {
503
+ const level = Math.round(origRange / (scale.max - scale.min) * 100) / 100;
504
+ min = Math.min(min, level);
505
+ max = Math.max(max, level);
506
+ }
507
+ }
508
+ return min < 1 ? min : max;
509
+ }
510
+ function panScale(scale, delta, limits, state) {
511
+ const { panDelta } = state;
512
+ const storedDelta = panDelta[scale.id] || 0;
513
+ if (sign(storedDelta) === sign(delta)) {
514
+ delta += storedDelta;
515
+ }
516
+ const fn = panFunctions[scale.type] || panFunctions.default;
517
+ if (fn?.(scale, delta, limits)) {
518
+ panDelta[scale.id] = 0;
519
+ } else {
520
+ panDelta[scale.id] = delta;
521
+ }
522
+ }
523
+ function pan(chart, delta, enabledScales, transition = 'none', trigger = 'other') {
524
+ const { x = 0, y = 0 } = typeof delta === 'number' ? {
525
+ x: delta,
526
+ y: delta
527
+ } : delta;
528
+ const state = getState(chart);
529
+ const { options: { pan: panOptions, limits = {} } } = state;
530
+ const { onPan } = panOptions || {};
531
+ storeOriginalScaleLimits(chart, state);
532
+ const xEnabled = x !== 0;
533
+ const yEnabled = y !== 0;
534
+ const scales = enabledScales || Object.values(chart.scales);
535
+ for (const scale of scales){
536
+ if (scale.isHorizontal() && xEnabled) {
537
+ panScale(scale, x, limits, state);
538
+ } else if (!scale.isHorizontal() && yEnabled) {
539
+ panScale(scale, y, limits, state);
540
+ }
541
+ }
542
+ chart.update(transition);
543
+ onPan?.({
544
+ chart,
545
+ trigger,
546
+ delta: {
547
+ x,
548
+ y
549
+ }
550
+ });
551
+ }
552
+ function getInitialScaleBounds(chart) {
553
+ const state = getState(chart);
554
+ storeOriginalScaleLimits(chart, state);
555
+ const scaleBounds = {};
556
+ for (const scaleId of Object.keys(chart.scales)){
557
+ const { min, max } = state.originalScaleLimits[scaleId] || {
558
+ min: {},
559
+ max: {}
560
+ };
561
+ scaleBounds[scaleId] = {
562
+ min: min.scale,
563
+ max: max.scale
564
+ };
565
+ }
566
+ return scaleBounds;
567
+ }
568
+ function getZoomedScaleBounds(chart) {
569
+ const state = getState(chart);
570
+ const scaleBounds = {};
571
+ for (const scaleId of Object.keys(chart.scales)){
572
+ scaleBounds[scaleId] = state.updatedScaleLimits[scaleId];
573
+ }
574
+ return scaleBounds;
575
+ }
576
+ function isZoomedOrPanned(chart) {
577
+ const scaleBounds = getInitialScaleBounds(chart);
578
+ for (const scaleId of Object.keys(chart.scales)){
579
+ const { min: originalMin, max: originalMax } = scaleBounds[scaleId];
580
+ if (originalMin !== undefined && chart.scales[scaleId].min !== originalMin) {
581
+ return true;
582
+ }
583
+ if (originalMax !== undefined && chart.scales[scaleId].max !== originalMax) {
584
+ return true;
585
+ }
586
+ }
587
+ return false;
588
+ }
589
+ function isZoomingOrPanningState(state) {
590
+ return state.panning || state.dragging;
591
+ }
592
+ function isZoomingOrPanning(chart) {
593
+ const state = getState(chart);
594
+ return !!(isZoomingOrPanningState(state) || state.filterNextClick);
595
+ }
596
+
597
+ const clamp = (x, from, to)=>Math.min(to, Math.max(from, x));
598
+ function removeHandler(chart, type) {
599
+ const { handlers, targets } = getState(chart);
600
+ const handler = handlers[type];
601
+ const target = targets[type];
602
+ if (handler && target) {
603
+ target.removeEventListener(type, handler);
604
+ delete handlers[type];
605
+ }
606
+ }
607
+ function addHandler(chart, target, type, handler) {
608
+ const { handlers, options, targets } = getState(chart);
609
+ const oldHandler = handlers[type];
610
+ if (oldHandler && targets[type] === target) {
611
+ return;
612
+ }
613
+ removeHandler(chart, type);
614
+ const listener = handlers[type] = (event)=>handler(chart, event, options);
615
+ targets[type] = target;
616
+ const passive = type === 'wheel' ? false : undefined;
617
+ target.addEventListener(type, listener, {
618
+ passive
619
+ });
620
+ }
621
+ function mouseMove(chart, event) {
622
+ const state = getState(chart);
623
+ if (state.dragStart) {
624
+ state.dragging = true;
625
+ state.dragEnd = event;
626
+ chart.draw();
627
+ }
628
+ }
629
+ function keyDown(chart, event) {
630
+ const state = getState(chart);
631
+ if (!state.dragStart || event.key !== 'Escape') {
632
+ return;
633
+ }
634
+ removeHandler(chart, 'keydown');
635
+ state.dragging = false;
636
+ state.dragStart = state.dragEnd = undefined;
637
+ chart.draw();
638
+ }
639
+ function getPointPosition(event, chart) {
640
+ if (event.target !== chart.canvas) {
641
+ const canvasArea = chart.canvas.getBoundingClientRect();
642
+ return {
643
+ x: event.clientX - canvasArea.left,
644
+ y: event.clientY - canvasArea.top
645
+ };
646
+ }
647
+ return getRelativePosition(event, chart)
648
+ ;
649
+ }
650
+ function zoomStart(chart, event, zoomOptions) {
651
+ const { onZoomStart, onZoomRejected } = zoomOptions;
652
+ if (onZoomStart) {
653
+ const point = getPointPosition(event, chart);
654
+ if (onZoomStart?.({
655
+ chart,
656
+ event,
657
+ point
658
+ }) === false) {
659
+ onZoomRejected?.({
660
+ chart,
661
+ event
662
+ });
663
+ return false;
664
+ }
665
+ }
666
+ }
667
+ function mouseDown(chart, event) {
668
+ if (chart.legend) {
669
+ const point = getRelativePosition(event, chart)
670
+ ;
671
+ if (_isPointInArea(point, chart.legend)) {
672
+ return;
673
+ }
674
+ }
675
+ const state = getState(chart);
676
+ const { pan: panOptions, zoom: zoomOptions = {} } = state.options;
677
+ if (event.button !== 0 || keyPressed(getModifierKey(panOptions), event) || keyNotPressed(getModifierKey(zoomOptions.drag), event)) {
678
+ return zoomOptions.onZoomRejected?.({
679
+ chart,
680
+ event
681
+ });
682
+ }
683
+ if (zoomStart(chart, event, zoomOptions) === false) {
684
+ return;
685
+ }
686
+ state.dragStart = event;
687
+ addHandler(chart, chart.canvas.ownerDocument, 'mousemove', mouseMove);
688
+ addHandler(chart, window.document, 'keydown', keyDown);
689
+ }
690
+ function applyAspectRatio({ begin, end }, aspectRatio) {
691
+ let width = end.x - begin.x;
692
+ let height = end.y - begin.y;
693
+ const ratio = Math.abs(width / height);
694
+ if (ratio > aspectRatio) {
695
+ width = Math.sign(width) * Math.abs(height * aspectRatio);
696
+ } else if (ratio < aspectRatio) {
697
+ height = Math.sign(height) * Math.abs(width / aspectRatio);
698
+ }
699
+ end.x = begin.x + width;
700
+ end.y = begin.y + height;
701
+ }
702
+ function applyMinMaxProps(rect, chartArea, points, { min, max, prop }) {
703
+ rect[min] = clamp(Math.min(points.begin[prop], points.end[prop]), chartArea[min], chartArea[max]);
704
+ rect[max] = clamp(Math.max(points.begin[prop], points.end[prop]), chartArea[min], chartArea[max]);
705
+ }
706
+ function getRelativePoints(chart, pointEvents, maintainAspectRatio) {
707
+ const points = {
708
+ begin: getPointPosition(pointEvents.dragStart, chart),
709
+ end: getPointPosition(pointEvents.dragEnd, chart)
710
+ };
711
+ if (maintainAspectRatio) {
712
+ const aspectRatio = chart.chartArea.width / chart.chartArea.height;
713
+ applyAspectRatio(points, aspectRatio);
714
+ }
715
+ return points;
716
+ }
717
+ function computeDragRect(chart, mode, pointEvents, maintainAspectRatio) {
718
+ const xEnabled = directionEnabled(mode, 'x', chart);
719
+ const yEnabled = directionEnabled(mode, 'y', chart);
720
+ const { top, left, right, bottom, width: chartWidth, height: chartHeight } = chart.chartArea;
721
+ const rect = {
722
+ top,
723
+ left,
724
+ right,
725
+ bottom
726
+ };
727
+ const points = getRelativePoints(chart, pointEvents, maintainAspectRatio && xEnabled && yEnabled);
728
+ if (xEnabled) {
729
+ applyMinMaxProps(rect, chart.chartArea, points, {
730
+ min: 'left',
731
+ max: 'right',
732
+ prop: 'x'
733
+ });
734
+ }
735
+ if (yEnabled) {
736
+ applyMinMaxProps(rect, chart.chartArea, points, {
737
+ min: 'top',
738
+ max: 'bottom',
739
+ prop: 'y'
740
+ });
741
+ }
742
+ const width = rect.right - rect.left;
743
+ const height = rect.bottom - rect.top;
744
+ return {
745
+ ...rect,
746
+ width,
747
+ height,
748
+ zoomX: xEnabled && width ? 1 + (chartWidth - width) / chartWidth : 1,
749
+ zoomY: yEnabled && height ? 1 + (chartHeight - height) / chartHeight : 1
750
+ };
751
+ }
752
+ function mouseUp(chart, event) {
753
+ const state = getState(chart);
754
+ if (!state.dragStart) {
755
+ return;
756
+ }
757
+ removeHandler(chart, 'mousemove');
758
+ const { mode, onZoomComplete, drag } = state.options.zoom ?? {};
759
+ const { threshold = 0, maintainAspectRatio } = drag ?? {};
760
+ const rect = computeDragRect(chart, mode, {
761
+ dragStart: state.dragStart,
762
+ dragEnd: event
763
+ }, maintainAspectRatio);
764
+ const distanceX = directionEnabled(mode, 'x', chart) ? rect.width : 0;
765
+ const distanceY = directionEnabled(mode, 'y', chart) ? rect.height : 0;
766
+ const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);
767
+ state.dragStart = state.dragEnd = undefined;
768
+ if (distance <= threshold) {
769
+ state.dragging = false;
770
+ chart.draw();
771
+ return;
772
+ }
773
+ zoomRect(chart, {
774
+ x: rect.left,
775
+ y: rect.top
776
+ }, {
777
+ x: rect.right,
778
+ y: rect.bottom
779
+ }, 'zoom', 'drag');
780
+ state.dragging = false;
781
+ state.filterNextClick = true;
782
+ onZoomComplete?.({
783
+ chart
784
+ });
785
+ }
786
+ function wheelPreconditions(chart, event, zoomOptions) {
787
+ if (keyNotPressed(getModifierKey(zoomOptions.wheel), event)) {
788
+ zoomOptions.onZoomRejected?.({
789
+ chart,
790
+ event
791
+ });
792
+ return;
793
+ }
794
+ if (zoomStart(chart, event, zoomOptions) === false) {
795
+ return;
796
+ }
797
+ if (event.cancelable) {
798
+ event.preventDefault();
799
+ }
800
+ if (event.deltaY === undefined) {
801
+ return;
802
+ }
803
+ return true;
804
+ }
805
+ function wheel(chart, event) {
806
+ const { handlers: { onZoomComplete }, options: { zoom: zoomOptions = {} } } = getState(chart);
807
+ if (!wheelPreconditions(chart, event, zoomOptions)) {
808
+ return;
809
+ }
810
+ const rect = event.target?.getBoundingClientRect();
811
+ const speed = zoomOptions?.wheel?.speed ?? 0.1;
812
+ const percentage = event.deltaY >= 0 ? 2 - 1 / (1 - speed) : 1 + speed;
813
+ const amount = {
814
+ x: percentage,
815
+ y: percentage,
816
+ focalPoint: {
817
+ x: event.clientX - rect.left,
818
+ y: event.clientY - rect.top
819
+ }
820
+ };
821
+ zoom(chart, amount, 'zoom', 'wheel');
822
+ onZoomComplete?.(event);
823
+ }
824
+ function addDebouncedHandler(chart, name, handler, delay) {
825
+ if (handler) {
826
+ getState(chart).handlers[name] = debounce(()=>handler?.({
827
+ chart
828
+ }), delay);
829
+ }
830
+ }
831
+ function addListeners(chart, options) {
832
+ const canvas = chart.canvas;
833
+ const { wheel: wheelOptions, drag: dragOptions, onZoomComplete } = options.zoom ?? {};
834
+ if (wheelOptions?.enabled) {
835
+ addHandler(chart, canvas, 'wheel', wheel);
836
+ addDebouncedHandler(chart, 'onZoomComplete', onZoomComplete, 250);
837
+ } else {
838
+ removeHandler(chart, 'wheel');
839
+ }
840
+ if (dragOptions?.enabled) {
841
+ addHandler(chart, canvas, 'mousedown', mouseDown);
842
+ addHandler(chart, canvas.ownerDocument, 'mouseup', mouseUp);
843
+ } else {
844
+ removeHandler(chart, 'mousedown');
845
+ removeHandler(chart, 'mousemove');
846
+ removeHandler(chart, 'mouseup');
847
+ removeHandler(chart, 'keydown');
848
+ }
849
+ }
850
+ function removeListeners(chart) {
851
+ removeHandler(chart, 'mousedown');
852
+ removeHandler(chart, 'mousemove');
853
+ removeHandler(chart, 'mouseup');
854
+ removeHandler(chart, 'wheel');
855
+ removeHandler(chart, 'click');
856
+ removeHandler(chart, 'keydown');
857
+ }
858
+
859
+ function createEnabler(chart, state) {
860
+ return function(_recognizer, event) {
861
+ const { pan: panOptions, zoom: zoomOptions = {} } = state.options;
862
+ if (!panOptions || !panOptions.enabled) {
863
+ return false;
864
+ }
865
+ const srcEvent = event && event.srcEvent;
866
+ if (!srcEvent) {
867
+ return true;
868
+ }
869
+ if (!state.panning && event.pointerType === 'mouse' && (keyNotPressed(getModifierKey(panOptions), srcEvent) || keyPressed(getModifierKey(zoomOptions.drag), srcEvent))) {
870
+ panOptions.onPanRejected?.({
871
+ chart,
872
+ event
873
+ });
874
+ return false;
875
+ }
876
+ return true;
877
+ };
878
+ }
879
+ function pinchAxes(p0, p1) {
880
+ const pinchX = Math.abs(p0.clientX - p1.clientX);
881
+ const pinchY = Math.abs(p0.clientY - p1.clientY);
882
+ const p = pinchX / pinchY;
883
+ let x, y;
884
+ if (p > 0.3 && p < 1.7) {
885
+ x = y = true;
886
+ } else if (pinchX > pinchY) {
887
+ x = true;
888
+ } else {
889
+ y = true;
890
+ }
891
+ return {
892
+ x,
893
+ y
894
+ };
895
+ }
896
+ function handlePinch(chart, state, e) {
897
+ if (state.scale) {
898
+ const { center, pointers } = e;
899
+ const zoomPercent = 1 / state.scale * e.scale;
900
+ const rect = e.target.getBoundingClientRect();
901
+ const pinch = pinchAxes(pointers[0], pointers[1]);
902
+ const mode = state.options.zoom?.mode;
903
+ const amount = {
904
+ x: pinch.x && directionEnabled(mode, 'x', chart) ? zoomPercent : 1,
905
+ y: pinch.y && directionEnabled(mode, 'y', chart) ? zoomPercent : 1,
906
+ focalPoint: {
907
+ x: center.x - rect.left,
908
+ y: center.y - rect.top
909
+ }
910
+ };
911
+ zoom(chart, amount, 'zoom', 'pinch');
912
+ state.scale = e.scale;
913
+ }
914
+ }
915
+ function startPinch(chart, state, e) {
916
+ if (state.options.zoom?.pinch?.enabled) {
917
+ const point = getRelativePosition(e.srcEvent, chart)
918
+ ;
919
+ if (state.options.zoom?.onZoomStart?.({
920
+ chart,
921
+ event: e.srcEvent,
922
+ point
923
+ }) === false) {
924
+ state.scale = null;
925
+ state.options.zoom?.onZoomRejected?.({
926
+ chart,
927
+ event: e.srcEvent
928
+ });
929
+ } else {
930
+ state.scale = 1;
931
+ }
932
+ }
933
+ }
934
+ function endPinch(chart, state, e) {
935
+ if (state.scale) {
936
+ handlePinch(chart, state, e);
937
+ state.scale = null
938
+ ;
939
+ state.options.zoom?.onZoomComplete?.({
940
+ chart
941
+ });
942
+ }
943
+ }
944
+ function handlePan(chart, state, e) {
945
+ const delta = state.delta;
946
+ if (delta) {
947
+ state.panning = true;
948
+ pan(chart, {
949
+ x: e.deltaX - delta.x,
950
+ y: e.deltaY - delta.y
951
+ }, state.panScales && state.panScales.map((i)=>chart.scales[i]).filter(Boolean));
952
+ state.delta = {
953
+ x: e.deltaX,
954
+ y: e.deltaY
955
+ };
956
+ }
957
+ }
958
+ function startPan(chart, state, event) {
959
+ const { enabled, onPanStart, onPanRejected } = state.options.pan ?? {};
960
+ if (!enabled) {
961
+ return;
962
+ }
963
+ const rect = event.target.getBoundingClientRect();
964
+ const point = {
965
+ x: event.center.x - rect.left,
966
+ y: event.center.y - rect.top
967
+ };
968
+ if (onPanStart?.({
969
+ chart,
970
+ event,
971
+ point
972
+ }) === false) {
973
+ return onPanRejected?.({
974
+ chart,
975
+ event
976
+ });
977
+ }
978
+ state.panScales = getEnabledScalesByPoint(state.options.pan, point, chart).map((i)=>i.id);
979
+ state.delta = {
980
+ x: 0,
981
+ y: 0
982
+ };
983
+ handlePan(chart, state, event);
984
+ }
985
+ function endPan(chart, state) {
986
+ state.delta = null;
987
+ if (state.panning) {
988
+ state.panning = false;
989
+ state.filterNextClick = true;
990
+ state.options.pan?.onPanComplete?.({
991
+ chart
992
+ });
993
+ }
994
+ }
995
+ const hammers = new WeakMap();
996
+ function startHammer(chart, options) {
997
+ const state = getState(chart);
998
+ const canvas = chart.canvas;
999
+ const { pan: panOptions, zoom: zoomOptions } = options;
1000
+ const mc = new Hammer.Manager(canvas);
1001
+ if (zoomOptions?.pinch?.enabled) {
1002
+ mc.add(new Hammer.Pinch());
1003
+ mc.on('pinchstart', (e)=>startPinch(chart, state, e));
1004
+ mc.on('pinch', (e)=>handlePinch(chart, state, e));
1005
+ mc.on('pinchend', (e)=>endPinch(chart, state, e));
1006
+ }
1007
+ if (panOptions && panOptions.enabled) {
1008
+ mc.add(new Hammer.Pan({
1009
+ threshold: panOptions.threshold,
1010
+ enable: createEnabler(chart, state)
1011
+ }));
1012
+ mc.on('panstart', (e)=>startPan(chart, state, e));
1013
+ mc.on('panmove', (e)=>handlePan(chart, state, e));
1014
+ mc.on('panend', ()=>endPan(chart, state));
1015
+ }
1016
+ hammers.set(chart, mc);
1017
+ }
1018
+ function stopHammer(chart) {
1019
+ const mc = hammers.get(chart);
1020
+ if (mc) {
1021
+ mc.remove('pinchstart');
1022
+ mc.remove('pinch');
1023
+ mc.remove('pinchend');
1024
+ mc.remove('panstart');
1025
+ mc.remove('pan');
1026
+ mc.remove('panend');
1027
+ mc.destroy();
1028
+ hammers.delete(chart);
1029
+ }
1030
+ }
1031
+ function hammerOptionsChanged(oldOptions, newOptions) {
1032
+ const { pan: oldPan, zoom: oldZoom } = oldOptions;
1033
+ const { pan: newPan, zoom: newZoom } = newOptions;
1034
+ if (oldZoom?.pinch?.enabled !== newZoom?.pinch?.enabled) {
1035
+ return true;
1036
+ }
1037
+ if (oldPan?.enabled !== newPan?.enabled) {
1038
+ return true;
1039
+ }
1040
+ if (oldPan?.threshold !== newPan?.threshold) {
1041
+ return true;
1042
+ }
1043
+ return false;
1044
+ }
1045
+
1046
+ var version = "2.2.7";
1047
+
1048
+ const defaults = {
1049
+ pan: {
1050
+ enabled: false,
1051
+ mode: 'xy',
1052
+ threshold: 10,
1053
+ modifierKey: null
1054
+ },
1055
+ zoom: {
1056
+ wheel: {
1057
+ enabled: false,
1058
+ speed: 0.1,
1059
+ modifierKey: null
1060
+ },
1061
+ drag: {
1062
+ enabled: false,
1063
+ drawTime: 'beforeDatasetsDraw',
1064
+ modifierKey: null
1065
+ },
1066
+ pinch: {
1067
+ enabled: false
1068
+ },
1069
+ mode: 'xy'
1070
+ }
1071
+ };
1072
+
1073
+ function draw(chart, caller, options) {
1074
+ const dragOptions = options.zoom?.drag;
1075
+ const { dragStart, dragEnd } = getState(chart);
1076
+ if (dragOptions?.drawTime !== caller || !dragStart || !dragEnd) {
1077
+ return;
1078
+ }
1079
+ const { left, top, width, height } = computeDragRect(chart, options.zoom?.mode, {
1080
+ dragStart,
1081
+ dragEnd
1082
+ }, dragOptions.maintainAspectRatio);
1083
+ const ctx = chart.ctx;
1084
+ ctx.save();
1085
+ ctx.beginPath();
1086
+ ctx.fillStyle = dragOptions.backgroundColor || 'rgba(225,225,225,0.3)';
1087
+ ctx.fillRect(left, top, width, height);
1088
+ if (dragOptions.borderWidth) {
1089
+ ctx.lineWidth = dragOptions.borderWidth;
1090
+ ctx.strokeStyle = dragOptions.borderColor || 'rgba(225,225,225)';
1091
+ ctx.strokeRect(left, top, width, height);
1092
+ }
1093
+ ctx.restore();
1094
+ }
1095
+ const bindApi = (chart)=>{
1096
+ chart.pan = (delta, panScales, transition)=>pan(chart, delta, panScales, transition, 'api');
1097
+ chart.zoom = (args, transition)=>zoom(chart, args, transition);
1098
+ chart.zoomRect = (p0, p1, transition)=>zoomRect(chart, p0, p1, transition);
1099
+ chart.zoomScale = (id, range, transition)=>zoomScale(chart, id, range, transition);
1100
+ chart.resetZoom = (transition)=>resetZoom(chart, transition);
1101
+ chart.getZoomLevel = ()=>getZoomLevel(chart);
1102
+ chart.getInitialScaleBounds = ()=>getInitialScaleBounds(chart);
1103
+ chart.getZoomedScaleBounds = ()=>getZoomedScaleBounds(chart);
1104
+ chart.isZoomedOrPanned = ()=>isZoomedOrPanned(chart);
1105
+ chart.isZoomingOrPanning = ()=>isZoomingOrPanning(chart);
1106
+ };
1107
+ var plugin = {
1108
+ id: 'zoom',
1109
+ version,
1110
+ defaults,
1111
+ start (chart, _args, options) {
1112
+ const state = getState(chart);
1113
+ state.options = options;
1114
+ if (Object.prototype.hasOwnProperty.call(options.zoom, 'enabled')) {
1115
+ console.warn('The option `zoom.enabled` is no longer supported. Please use `zoom.wheel.enabled`, `zoom.drag.enabled`, or `zoom.pinch.enabled`.');
1116
+ }
1117
+ if (Object.prototype.hasOwnProperty.call(options.zoom, 'overScaleMode') || Object.prototype.hasOwnProperty.call(options.pan, 'overScaleMode')) {
1118
+ console.warn('The option `overScaleMode` is deprecated. Please use `scaleMode` instead (and update `mode` as desired).');
1119
+ }
1120
+ if (Hammer) {
1121
+ startHammer(chart, options);
1122
+ }
1123
+ bindApi(chart);
1124
+ },
1125
+ beforeEvent (chart, { event }) {
1126
+ const state = getState(chart);
1127
+ if (isZoomingOrPanningState(state)) {
1128
+ return false;
1129
+ }
1130
+ if (event.type === 'click' || event.type === 'mouseup') {
1131
+ if (state.filterNextClick) {
1132
+ state.filterNextClick = false;
1133
+ return false;
1134
+ }
1135
+ }
1136
+ },
1137
+ beforeUpdate (chart, _args, options) {
1138
+ const state = getState(chart);
1139
+ const previousOptions = state.options;
1140
+ state.options = options;
1141
+ if (hammerOptionsChanged(previousOptions, options)) {
1142
+ stopHammer(chart);
1143
+ startHammer(chart, options);
1144
+ }
1145
+ addListeners(chart, options);
1146
+ },
1147
+ beforeDatasetsDraw (chart, _args, options) {
1148
+ draw(chart, 'beforeDatasetsDraw', options);
1149
+ },
1150
+ afterDatasetsDraw (chart, _args, options) {
1151
+ draw(chart, 'afterDatasetsDraw', options);
1152
+ },
1153
+ beforeDraw (chart, _args, options) {
1154
+ draw(chart, 'beforeDraw', options);
1155
+ },
1156
+ afterDraw (chart, _args, options) {
1157
+ draw(chart, 'afterDraw', options);
1158
+ },
1159
+ stop (chart) {
1160
+ removeListeners(chart);
1161
+ if (Hammer) {
1162
+ stopHammer(chart);
1163
+ }
1164
+ removeState(chart);
1165
+ },
1166
+ panFunctions,
1167
+ zoomFunctions,
1168
+ zoomRectFunctions
1169
+ };
1170
+
1171
+ export { plugin as default, pan, resetZoom, zoom, zoomRect, zoomScale };
1172
+ //# sourceMappingURL=chartjs-plugin-zoom.esm.js.map