Skip to content

Commit

Permalink
feat: add trigger to onZoom context (#901)
Browse files Browse the repository at this point in the history
* feat: add trigger to onZoom context

* fix: try to fix lint

* fix: lint
  • Loading branch information
kurkle authored Nov 26, 2024
1 parent 5322427 commit edcf386
Show file tree
Hide file tree
Showing 9 changed files with 114 additions and 56 deletions.
65 changes: 56 additions & 9 deletions src/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,12 @@ function getCenter(chart) {
}

/**
* @param chart The chart instance
* @param {number | {x?: number, y?: number, focalPoint?: {x: number, y: number}}} amount The zoom percentage or percentages and focal point
* @param {string} [transition] Which transition mode to use. Defaults to 'none'
* @param {import('chart.js').Chart} chart The Chart instance
* @param {import('../types').ZoomAmount} amount The zoom percentage or percentages and focal point
* @param {import('chart.js').UpdateMode} [transition] Which transition mode to use. Defaults to 'none'
* @param {import('../types/options').ZoomTrigger} [trigger] What triggered the zoom. Defaults to 'api'
*/
export function zoom(chart, amount, transition = 'none') {
export function zoom(chart, amount, transition = 'none', trigger = 'api') {
const {x = 1, y = 1, focalPoint = getCenter(chart)} = typeof amount === 'number' ? {x: amount, y: amount} : amount;
const state = getState(chart);
const {options: {limits, zoom: zoomOptions}} = state;
Expand All @@ -72,6 +73,7 @@ export function zoom(chart, amount, transition = 'none') {
const yEnabled = y !== 1;
const enabledScales = getEnabledScalesByPoint(zoomOptions, focalPoint, chart);

// @ts-expect-error No overload matches this call
each(enabledScales || chart.scales, function(scale) {
if (scale.isHorizontal() && xEnabled) {
doZoom(scale, x, focalPoint, limits);
Expand All @@ -82,10 +84,18 @@ export function zoom(chart, amount, transition = 'none') {

chart.update(transition);

call(zoomOptions.onZoom, [{chart}]);
// @ts-expect-error args not assignable to unknown[]
call(zoomOptions.onZoom, [{chart, trigger}]);
}

export function zoomRect(chart, p0, p1, transition = 'none') {
/**
* @param {import('chart.js').Chart} chart The Chart instance
* @param {import('chart.js').Point} p0 First corner of the rect
* @param {import('chart.js').Point} p1 Opposite corner of the rect
* @param {import('chart.js').UpdateMode} [transition]
* @param {import('../types/options').ZoomTrigger} [trigger] What triggered the zoom. Defaults to 'api'
*/
export function zoomRect(chart, p0, p1, transition = 'none', trigger = 'api') {
const state = getState(chart);
const {options: {limits, zoom: zoomOptions}} = state;
const {mode = 'xy'} = zoomOptions;
Expand All @@ -104,19 +114,32 @@ export function zoomRect(chart, p0, p1, transition = 'none') {

chart.update(transition);

call(zoomOptions.onZoom, [{chart}]);
// @ts-expect-error args not assignable to unknown[]
call(zoomOptions.onZoom, [{chart, trigger}]);
}

export function zoomScale(chart, scaleId, range, transition = 'none') {
/**
* @param {import('chart.js').Chart} chart The Chart instance
* @param {string} scaleId
* @param {import('../types').ScaleRange} range
* @param {import('chart.js').UpdateMode} [transition]
* @param {import('../types/options').ZoomTrigger} [trigger] What triggered the zoom. Defaults to 'api'
*/
export function zoomScale(chart, scaleId, range, transition = 'none', trigger = 'api') {
const state = getState(chart);
storeOriginalScaleLimits(chart, state);
const scale = chart.scales[scaleId];
updateRange(scale, range, undefined, true);
chart.update(transition);

call(state.options.zoom?.onZoom, [{chart}]);
// @ts-expect-error args not assignable to unknown[]
call(state.options.zoom?.onZoom, [{chart, trigger}]);
}

/**
* @param {import('chart.js').Chart} chart The Chart instance
* @param {import('chart.js').UpdateMode} transition
*/
export function resetZoom(chart, transition = 'default') {
const state = getState(chart);
const originalScaleLimits = storeOriginalScaleLimits(chart, state);
Expand All @@ -134,6 +157,7 @@ export function resetZoom(chart, transition = 'default') {
});
chart.update(transition);

// @ts-expect-error args not assignable to unknown[]
call(state.options.zoom.onZoomComplete, [{chart}]);
}

Expand All @@ -146,6 +170,9 @@ function getOriginalRange(state, scaleId) {
return valueOrDefault(max.options, max.scale) - valueOrDefault(min.options, min.scale);
}

/**
* @param {import('chart.js').Chart} chart The Chart instance
*/
export function getZoomLevel(chart) {
const state = getState(chart);
let min = 1;
Expand Down Expand Up @@ -178,6 +205,12 @@ function panScale(scale, delta, limits, state) {
}
}

/**
* @param {import('chart.js').Chart} chart The Chart instance
* @param {import('../types').PanAmount} delta
* @param {import('chart.js').Scale[]} [enabledScales]
* @param {import('chart.js').UpdateMode} [transition]
*/
export function pan(chart, delta, enabledScales, transition = 'none') {
const {x = 0, y = 0} = typeof delta === 'number' ? {x: delta, y: delta} : delta;
const state = getState(chart);
Expand All @@ -189,6 +222,7 @@ export function pan(chart, delta, enabledScales, transition = 'none') {
const xEnabled = x !== 0;
const yEnabled = y !== 0;

// @ts-expect-error No overload matches this call
each(enabledScales || chart.scales, function(scale) {
if (scale.isHorizontal() && xEnabled) {
panScale(scale, x, limits, state);
Expand All @@ -199,9 +233,13 @@ export function pan(chart, delta, enabledScales, transition = 'none') {

chart.update(transition);

// @ts-expect-error args not assignable to unknown[]
call(onPan, [{chart}]);
}

/**
* @param {import('chart.js').Chart} chart The Chart instance
*/
export function getInitialScaleBounds(chart) {
const state = getState(chart);
storeOriginalScaleLimits(chart, state);
Expand All @@ -214,6 +252,9 @@ export function getInitialScaleBounds(chart) {
return scaleBounds;
}

/**
* @param {import('chart.js').Chart} chart The Chart instance
*/
export function getZoomedScaleBounds(chart) {
const state = getState(chart);
const scaleBounds = {};
Expand All @@ -224,6 +265,9 @@ export function getZoomedScaleBounds(chart) {
return scaleBounds;
}

/**
* @param {import('chart.js').Chart} chart The Chart instance
*/
export function isZoomedOrPanned(chart) {
const scaleBounds = getInitialScaleBounds(chart);
for (const scaleId of Object.keys(chart.scales)) {
Expand All @@ -241,6 +285,9 @@ export function isZoomedOrPanned(chart) {
return false;
}

/**
* @param {import('chart.js').Chart} chart The Chart instance
*/
export function isZoomingOrPanning(chart) {
const state = getState(chart);
return state.panning || state.dragging;
Expand Down
2 changes: 1 addition & 1 deletion src/hammer.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ function handlePinch(chart, state, e) {
}
};

zoom(chart, amount);
zoom(chart, amount, 'zoom', 'pinch');

// Keep track of overall scale
state.scale = e.scale;
Expand Down
19 changes: 17 additions & 2 deletions src/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,18 @@ function getPointPosition(event, chart) {
return getRelativePosition(event, chart);
}

/**
* @param {import('chart.js').Chart} chart
* @param {*} event
* @param {import('../types/options').ZoomOptions} zoomOptions
*/
function zoomStart(chart, event, zoomOptions) {
const {onZoomStart, onZoomRejected} = zoomOptions;
if (onZoomStart) {
const point = getPointPosition(event, chart);
// @ts-expect-error args not assignable to unknown[]
if (call(onZoomStart, [{chart, event, point}]) === false) {
// @ts-expect-error args not assignable to unknown[]
call(onZoomRejected, [{chart, event}]);
return false;
}
Expand All @@ -93,6 +100,7 @@ export function mouseDown(chart, event) {
keyPressed(getModifierKey(panOptions), event) ||
keyNotPressed(getModifierKey(zoomOptions.drag), event)
) {
// @ts-expect-error args not assignable to unknown[]
return call(zoomOptions.onZoomRejected, [{chart, event}]);
}

Expand Down Expand Up @@ -189,16 +197,23 @@ export function mouseUp(chart, event) {
return;
}

zoomRect(chart, {x: rect.left, y: rect.top}, {x: rect.right, y: rect.bottom}, 'zoom');
zoomRect(chart, {x: rect.left, y: rect.top}, {x: rect.right, y: rect.bottom}, 'zoom', 'drag');

state.dragging = false;
state.filterNextClick = true;
// @ts-expect-error args not assignable to unknown[]
call(onZoomComplete, [{chart}]);
}

/**
* @param {import('chart.js').Chart} chart
* @param {*} event
* @param {import('../types/options').ZoomOptions} zoomOptions
*/
function wheelPreconditions(chart, event, zoomOptions) {
// Before preventDefault, check if the modifier key required and pressed
if (keyNotPressed(getModifierKey(zoomOptions.wheel), event)) {
// @ts-expect-error args not assignable to unknown[]
call(zoomOptions.onZoomRejected, [{chart, event}]);
return;
}
Expand Down Expand Up @@ -239,7 +254,7 @@ export function wheel(chart, event) {
}
};

zoom(chart, amount);
zoom(chart, amount, 'zoom', 'wheel');

call(onZoomComplete, [{chart}]);
}
Expand Down
12 changes: 12 additions & 0 deletions src/state.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
/**
* @typedef {import("chart.js").Chart} Chart
* @typedef {{originalScaleLimits: any; updatedScaleLimits: any; handlers: any; panDelta: any; dragging: boolean; panning: boolean; options?: import("../types/options").ZoomPluginOptions, dragStart?: any, dragEnd?: any, filterNextClick?: boolean}} ZoomPluginState
*/

/**
* @type WeakMap<Chart, ZoomPluginState>
*/
const chartStates = new WeakMap();

/**
* @param {import("chart.js").Chart} chart
* @returns {ZoomPluginState}
*/
export function getState(chart) {
let state = chartStates.get(chart);
if (!state) {
Expand Down
2 changes: 1 addition & 1 deletion test/specs/api.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ describe('api', function() {

chart.zoomScale('x', {min: 2, max: 10}, 'default');

expect(zoomSpy).toHaveBeenCalledWith({chart});
expect(zoomSpy).toHaveBeenCalledWith({chart, trigger: 'api'});
});
});

Expand Down
2 changes: 1 addition & 1 deletion test/specs/zoom.drag.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@ describe('zoom with drag', function() {
// expect(chart.isZoomingOrPanning()).toBe(false);

expect(startSpy).toHaveBeenCalled();
expect(zoomSpy).toHaveBeenCalled();
expect(zoomSpy).toHaveBeenCalledWith({chart, trigger: 'drag'});
});

it('should call onZoomRejected when onZoomStart returns false', function() {
Expand Down
41 changes: 9 additions & 32 deletions test/specs/zoom.wheel.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -373,8 +373,9 @@ describe('zoom with wheel', function() {
});

describe('events', function() {
it('should call onZoomStart', function() {
const startSpy = jasmine.createSpy('started');
it('should call onZoomStart, onZoom and onZoomComplete', function(done) {
const startSpy = jasmine.createSpy('start');
const zoomSpy = jasmine.createSpy('zoom');
const chart = window.acquireChart({
type: 'scatter',
data,
Expand All @@ -386,7 +387,9 @@ describe('zoom with wheel', function() {
enabled: true,
},
mode: 'xy',
onZoomStart: startSpy
onZoomStart: startSpy,
onZoom: zoomSpy,
onZoomComplete: () => done()
}
}
}
Expand All @@ -397,8 +400,11 @@ describe('zoom with wheel', function() {
y: chart.scales.y.getPixelForValue(1.1),
deltaY: 1
};

jasmine.triggerWheelEvent(chart, wheelEv);

expect(startSpy).toHaveBeenCalled();
expect(zoomSpy).toHaveBeenCalledWith({chart, trigger: 'wheel'});
expect(chart.scales.x.min).not.toBe(1);
});

Expand Down Expand Up @@ -467,34 +473,5 @@ describe('zoom with wheel', function() {
expect(rejectSpy).toHaveBeenCalled();
expect(chart.scales.x.min).toBe(1);
});

it('should call onZoomComplete', function(done) {
const chart = window.acquireChart({
type: 'scatter',
data,
options: {
plugins: {
zoom: {
zoom: {
wheel: {
enabled: true,
},
mode: 'xy',
onZoomComplete(ctx) {
expect(ctx.chart.scales.x.min).not.toBe(1);
done();
}
}
}
}
}
});
const wheelEv = {
x: chart.scales.x.getPixelForValue(1.5),
y: chart.scales.y.getPixelForValue(1.1),
deltaY: 1
};
jasmine.triggerWheelEvent(chart, wheelEv);
});
});
});
7 changes: 4 additions & 3 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { Plugin, ChartType, Chart, Scale, UpdateMode, ScaleTypeRegistry, ChartTy
import { LimitOptions, ZoomPluginOptions } from './options';

type Point = { x: number, y: number };
type ZoomAmount = number | Partial<Point> & { focalPoint?: Point };
type PanAmount = number | Partial<Point>;
type ScaleRange = { min: number, max: number };
type DistributiveArray<T> = [T] extends [unknown] ? Array<T> : never

export type PanAmount = number | Partial<Point>;
export type ScaleRange = { min: number, max: number };
export type ZoomAmount = number | Partial<Point> & { focalPoint?: Point };

declare module 'chart.js' {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface PluginOptionsByType<TType extends ChartType> {
Expand Down
Loading

0 comments on commit edcf386

Please sign in to comment.