Skip to content

Commit

Permalink
Merge branch 'chartjs:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
gopal-panigrahi authored Nov 15, 2024
2 parents d9fd526 + 7704540 commit 990188d
Show file tree
Hide file tree
Showing 15 changed files with 8,069 additions and 20,103 deletions.
4 changes: 4 additions & 0 deletions docs/guide/developers.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ Returns the initial scale bounds of each scale before any zooming or panning too

Returns whether the chart has been zoomed or panned - i.e. whether the initial scale of any axis is different to the one used currently.

### `chart.isZoomingOrPanning(): boolean`

Returns whether the user is currently in the middle of a drag operation or pan operation.

## Custom Scales

You can extend chartjs-plugin-zoom with support for [custom scales](https://www.chartjs.org/docs/latest/developers/axes.html) by using the zoom plugin's `zoomFunctions`, `zoomRectFunctions`, and `panFunctions` members. These objects are indexed by scale types (scales' `id` members) and give optional handlers for zoom and pan functionality.
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const chart = new Chart('id', {
| `backgroundColor` | `Color` | `'rgba(225,225,225,0.3)'` | Fill color
| `borderColor` | `Color` | `'rgba(225,225,225)'` | Stroke color
| `borderWidth` | `number` | `0` | Stroke width
| [`drawTime`](#draw-time) | `string` | `beforeDatasetsDraw` | When the dragging box is dran on the chart
| [`drawTime`](#draw-time) | `string` | `beforeDatasetsDraw` | When the dragging box is drawn on the chart
| `threshold` | `number` | `0` | Minimal zoom distance required before actually applying zoom
| `modifierKey` | `'ctrl'`\|`'alt'`\|`'shift'`\|`'meta'` | `null` | Modifier key required for drag-to-zoom

Expand Down
27,968 changes: 7,894 additions & 20,074 deletions package-lock.json

Large diffs are not rendered by default.

20 changes: 11 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@
"@babel/preset-env": "^7.20.2",
"@rollup/plugin-json": "^4.1.0",
"@simonbrunel/vuepress-plugin-versions": "^0.2.0",
"@types/linkify-it": "^3.0.5",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"babel-loader": "^8.3.0",
"chart.js": "^4.2.1",
"chart.js": "^4.3.2",
"chartjs-adapter-date-fns": "^2.0.1",
"chartjs-test-utils": "^0.3.0",
"concurrently": "^6.0.2",
Expand All @@ -54,11 +55,11 @@
"eslint-plugin-markdown": "^2.0.1",
"hammer-simulator": "^0.0.1",
"jasmine": "^3.7.0",
"karma": "^6.3.2",
"karma-chrome-launcher": "^3.1.0",
"karma-coverage": "^2.0.3",
"karma-firefox-launcher": "^2.1.0",
"karma-jasmine": "^4.0.1",
"karma": "^6.4.4",
"karma-chrome-launcher": "^3.2.0",
"karma-coverage": "^2.2.1",
"karma-firefox-launcher": "^2.1.3",
"karma-jasmine": "^4.0.2",
"karma-jasmine-html-reporter": "^1.5.4",
"karma-rollup-preprocessor": "^7.0.7",
"karma-spec-reporter": "0.0.32",
Expand All @@ -69,9 +70,9 @@
"rollup-plugin-istanbul": "^3.0.0",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-terser": "^7.0.2",
"typedoc": "^0.22.9",
"typedoc-plugin-markdown": "^3.11.6",
"typescript": "^4.2.4",
"typedoc": "^0.26.11",
"typedoc-plugin-markdown": "^4.2.10",
"typescript": "^5.6.3",
"vuepress": "^1.8.2",
"vuepress-plugin-flexsearch": "^0.3.0",
"vuepress-plugin-redirect": "^1.2.5",
Expand All @@ -82,6 +83,7 @@
"chart.js": ">=3.2.0"
},
"dependencies": {
"@types/hammerjs": "^2.0.45",
"hammerjs": "^2.0.8"
}
}
5 changes: 5 additions & 0 deletions src/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,3 +225,8 @@ export function isZoomedOrPanned(chart) {

return false;
}

export function isZoomingOrPanning(chart) {
const state = getState(chart);
return state.panning || state.dragging;
}
1 change: 1 addition & 0 deletions src/hammer.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ function handlePinch(chart, state, e) {

function startPinch(chart, state) {
if (state.options.zoom.pinch.enabled) {
call(state.options.zoom.onZoomStart, [{chart}]);
state.scale = 1;
}
}
Expand Down
9 changes: 5 additions & 4 deletions src/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ function addHandler(chart, target, type, handler) {
removeHandler(chart, type);
handlers[type] = (event) => handler(chart, event, options);
handlers[type].target = target;
target.addEventListener(type, handlers[type]);

// `passive: false` for wheel events, to prevent chrome warnings. Use default value for other events.
const passive = type === 'wheel' ? false : undefined;
target.addEventListener(type, handlers[type], {passive});
}

export function mouseMove(chart, event) {
Expand Down Expand Up @@ -191,9 +194,7 @@ export function wheel(chart, event) {

zoom(chart, amount);

if (onZoomComplete) {
onZoomComplete();
}
call(onZoomComplete, [{chart}]);
}

function addDebouncedHandler(chart, name, handler, delay) {
Expand Down
6 changes: 3 additions & 3 deletions src/plugin.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Hammer from 'hammerjs';
import {addListeners, computeDragRect, removeListeners} from './handlers';
import {startHammer, stopHammer} from './hammer';
import {pan, zoom, resetZoom, zoomScale, getZoomLevel, getInitialScaleBounds, isZoomedOrPanned, zoomRect} from './core';
import {pan, zoom, resetZoom, zoomScale, getZoomLevel, getInitialScaleBounds, isZoomedOrPanned, isZoomingOrPanning, zoomRect} from './core';
import {panFunctions, zoomFunctions, zoomRectFunctions} from './scale.types';
import {getState, removeState} from './state';
import {version} from '../package.json';
Expand Down Expand Up @@ -83,11 +83,11 @@ export default {
chart.getZoomLevel = () => getZoomLevel(chart);
chart.getInitialScaleBounds = () => getInitialScaleBounds(chart);
chart.isZoomedOrPanned = () => isZoomedOrPanned(chart);
chart.isZoomingOrPanning = () => isZoomingOrPanning(chart);
},

beforeEvent(chart) {
const state = getState(chart);
if (state.panning || state.dragging) {
if (isZoomingOrPanning(chart)) {
// cancel any event handling while panning or dragging
return false;
}
Expand Down
53 changes: 46 additions & 7 deletions src/scale.types.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
import {valueOrDefault} from 'chart.js/helpers';
import {getState} from './state';

/**
* @typedef {import('chart.js').Point} Point
* @typedef {import('chart.js').Scale} Scale
* @typedef {import('../types/options').LimitOptions} LimitOptions
* @typedef {{min: number, max: number}} ScaleRange
* @typedef {import('../types/options').ScaleLimits} ScaleLimits
*/

/**
* @param {Scale} scale
* @param {number} zoom
* @param {Point} center
* @returns {ScaleRange}
*/
function zoomDelta(scale, zoom, center) {
const range = scale.max - scale.min;
const newRange = range * (zoom - 1);
Expand All @@ -20,6 +34,15 @@ function zoomDelta(scale, zoom, center) {
};
}

/**
* @param {Scale} scale
* @param {LimitOptions|undefined} limits
* @returns {ScaleLimits}
*/
function getScaleLimits(scale, limits) {
return limits && (limits[scale.id] || limits[scale.axis]) || {};
}

function getLimit(state, scale, scaleLimits, prop, fallback) {
let limit = scaleLimits[prop];
if (limit === 'original') {
Expand All @@ -29,6 +52,12 @@ function getLimit(state, scale, scaleLimits, prop, fallback) {
return valueOrDefault(limit, fallback);
}

/**
* @param {Scale} scale
* @param {number} pixel0
* @param {number} pixel1
* @returns {ScaleRange}
*/
function getRange(scale, pixel0, pixel1) {
const v0 = scale.getValueForPixel(pixel0);
const v1 = scale.getValueForPixel(pixel1);
Expand All @@ -38,15 +67,27 @@ function getRange(scale, pixel0, pixel1) {
};
}

/**
* @param {Scale} scale
* @param {ScaleRange} minMax
* @param {LimitOptions} [limits]
* @param {boolean|'pan'} [zoom]
* @returns {boolean}
*/
export function updateRange(scale, {min, max}, limits, zoom = false) {
const state = getState(scale.chart);
const {id, axis, options: scaleOpts} = scale;
const {options: scaleOpts} = scale;

const scaleLimits = limits && (limits[id] || limits[axis]) || {};
const scaleLimits = getScaleLimits(scale, limits);
const {minRange = 0} = scaleLimits;
const minLimit = getLimit(state, scale, scaleLimits, 'min', -Infinity);
const maxLimit = getLimit(state, scale, scaleLimits, 'max', Infinity);

if (zoom === 'pan' && (min < minLimit || max > maxLimit)) {
// At limit: No change but return true to indicate no need to store the delta.
return true;
}

const range = zoom ? Math.max(max - min, minRange) : scale.max - scale.min;
const offset = (range - max + min) / 2;
min -= offset;
Expand Down Expand Up @@ -139,20 +180,18 @@ const OFFSETS = {
year: 182 * 24 * 60 * 60 * 1000 // 182 d
};

function panNumericalScale(scale, delta, limits, canZoom = false) {
function panNumericalScale(scale, delta, limits, pan = false) {
const {min: prevStart, max: prevEnd, options} = scale;
const round = options.time && options.time.round;
const offset = OFFSETS[round] || 0;
const newMin = scale.getValueForPixel(scale.getPixelForValue(prevStart + offset) - delta);
const newMax = scale.getValueForPixel(scale.getPixelForValue(prevEnd + offset) - delta);
const {min: minLimit = -Infinity, max: maxLimit = Infinity} = canZoom && limits && limits[scale.axis] || {};
if (isNaN(newMin) || isNaN(newMax) || newMin < minLimit || newMax > maxLimit) {
// At limit: No change but return true to indicate no need to store the delta.
if (isNaN(newMin) || isNaN(newMax)) {
// NaN can happen for 0-dimension scales (either because they were configured
// with min === max or because the chart has 0 plottable area).
return true;
}
return updateRange(scale, {min: newMin, max: newMax}, limits, canZoom);
return updateRange(scale, {min: newMin, max: newMax}, limits, pan ? 'pan' : false);
}

function panNonLinearScale(scale, delta, limits) {
Expand Down
4 changes: 3 additions & 1 deletion src/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ export function getState(chart) {
originalScaleLimits: {},
updatedScaleLimits: {},
handlers: {},
panDelta: {}
panDelta: {},
dragging: false,
panning: false
};
chartStates.set(chart, state);
}
Expand Down
71 changes: 71 additions & 0 deletions test/specs/pan.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,77 @@ describe('pan', function() {
expect(scale.options.min).toBe(2);
expect(scale.options.max).toBe(2);
});

it('should respect original limits', function() {
const chart = window.acquireChart({
type: 'line',
data,
options: {
plugins: {
zoom: {
pan: {
enabled: true,
mode: 'x',
},
limits: {
x: {
min: 'original',
max: 'original',
}
},
}
},
scales: {
x: {
min: 1,
max: 2
}
}
}
});
const scale = chart.scales.x;
expect(scale.min).toBe(1);
expect(scale.max).toBe(2);
chart.pan(100);
expect(scale.min).toBe(1);
expect(scale.max).toBe(2);
});

it('should respect original limits for nonlinear scales', function() {
const chart = window.acquireChart({
type: 'line',
data,
options: {
plugins: {
zoom: {
pan: {
enabled: true,
mode: 'x',
},
limits: {
x: {
min: 'original',
max: 'original',
}
},
}
},
scales: {
x: {
type: 'logarithmic',
min: 1,
max: 10
}
}
}
});
const scale = chart.scales.x;
expect(scale.min).toBe(1);
expect(scale.max).toBe(10);
chart.pan(100);
expect(scale.min).toBe(1);
expect(scale.max).toBe(10);
});
});

describe('events', function() {
Expand Down
16 changes: 16 additions & 0 deletions test/specs/zoom.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -791,10 +791,19 @@ describe('zoom', function() {
};
const pt2 = {x: pt.x + 20, y: pt.y + 20};

expect(chart.isZoomingOrPanning()).toBe(false);

jasmine.dispatchEvent(chart, 'mousedown', pt);
jasmine.dispatchEvent(chart, 'mousemove', pt2);

expect(chart.isZoomingOrPanning()).toBe(true);

jasmine.dispatchEvent(chart, 'mouseup', pt2);

// Drag state isn't cleared until a timeout fires (later), so we can't
// easily test this here.
// expect(chart.isZoomingOrPanning()).toBe(false);

expect(startSpy).toHaveBeenCalled();
expect(zoomSpy).toHaveBeenCalled();
});
Expand Down Expand Up @@ -830,10 +839,17 @@ describe('zoom', function() {
};
const pt2 = {x: pt.x + 20, y: pt.y + 20};

expect(chart.isZoomingOrPanning()).toBe(false);

jasmine.dispatchEvent(chart, 'mousedown', pt);

expect(chart.isZoomingOrPanning()).toBe(false);

jasmine.dispatchEvent(chart, 'mousemove', pt2);
jasmine.dispatchEvent(chart, 'mouseup', pt2);

expect(chart.isZoomingOrPanning()).toBe(false);

expect(rejectSpy).toHaveBeenCalled();
expect(zoomSpy).not.toHaveBeenCalled();
expect(doneSpy).not.toHaveBeenCalled();
Expand Down
2 changes: 2 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ declare module 'chart.js' {
getZoomLevel(): number;
getInitialScaleBounds(): Record<string, {min: number, max: number}>;
isZoomedOrPanned(): boolean;
isZoomingOrPanning(): boolean;
}
}

Expand Down Expand Up @@ -56,3 +57,4 @@ export function resetZoom(chart: Chart, mode?: UpdateMode): void;
export function getZoomLevel(chart: Chart): number;
export function getInitialScaleBounds(chart: Chart): Record<string, {min: number, max: number}>;
export function isZoomedOrPanned(chart: Chart): boolean;
export function isZoomingOrPanning(chart: Chart): boolean;
6 changes: 3 additions & 3 deletions types/options.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Chart, Color, Point } from 'chart.js';

import { Input as HammerInput } from 'hammerjs';

type Mode = 'x' | 'y' | 'xy';
type Key = 'ctrl' | 'alt' | 'shift' | 'meta';
Expand Down Expand Up @@ -167,9 +167,9 @@ export interface PanOptions {
* Function called when pan fails because modifier key was not detected.
* event is the a hammer event that failed - see https://hammerjs.github.io/api#event-object
*/
onPanRejected?: (context: { chart: Chart, event: Event }) => void;
onPanRejected?: (context: { chart: Chart, event: HammerInput }) => void;

onPanStart?: (context: { chart: Chart, event: Event, point: Point }) => boolean | undefined;
onPanStart?: (context: { chart: Chart, event: HammerInput, point: Point }) => boolean | undefined;
}

export interface ScaleLimits {
Expand Down
Loading

0 comments on commit 990188d

Please sign in to comment.