Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add trigger to onZoom context #901

Merged
merged 3 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading