Skip to content

Commit bb7b8fc

Browse files
authored
Multi-axis zoom and original zoom limits (#524)
* Minor formatting, code cleanup, spelling fixes * Allow configuring limits for individual axes Fixes #522. * Allow using original axis as zoom limits Fixes #523. * Handle autoscaled min/max * Fix resetZoom This should fix test failures. * Retrieve chart state within updateRange This lets us keep the `zoomFunctions` and `panFunctions` signatures unchanged, since they're part of the public API. * Revise state management As discussed in code review, it seems cleaner to consistently get state at the start of a function then pass it to `storeOriginalScaleLimits`.
1 parent 327775b commit bb7b8fc

5 files changed

Lines changed: 95 additions & 39 deletions

File tree

docs/guide/options.md

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const chart = new Chart('id', {
3636
| `mode` | `'x'`\|`'y'`\|`'xy'` | `'xy'` | Allowed panning directions
3737
| `modifierKey` | `'ctrl'`\|`'alt'`\|`'shift'`\|`'meta'` | `null` | Modifier key required for panning with mouse
3838
| `overScaleMode` | `'x'`\|`'y'`\|`'xy'` | `undefined` | Which of the enabled panning directions should only be available when the mouse cursor is over a scale for that axis
39-
| `threshold` | `number` | `10` | Mimimal pan distance required before actually applying pan
39+
| `threshold` | `number` | `10` | Minimal pan distance required before actually applying pan
4040

4141
### Pan Events
4242

@@ -103,10 +103,42 @@ Limits options define the limits per axis for pan and zoom.
103103
| `x` | [`ScaleLimits`](#scale-limits) | Limits for x-axis
104104
| `y` | [`ScaleLimits`](#scale-limits) | Limits for y-axis
105105

106+
If you're using multiple or custom axes (scales), you can define limits for those, too.
107+
108+
```js
109+
const chart = new Chart('id', {
110+
type: 'line',
111+
data: {},
112+
options: {
113+
scales: {
114+
y: {
115+
min: 20,
116+
max: 80,
117+
},
118+
y2: {
119+
position: 'right',
120+
min: -5,
121+
max: 5
122+
}
123+
},
124+
plugins: {
125+
zoom: {
126+
limits: {
127+
y: {min: 0, max: 100},
128+
y2: {min: -5, max: 5}
129+
},
130+
}
131+
}
132+
}
133+
});
134+
```
135+
106136
#### Scale Limits
107137

108138
| Name | Type | Description
109139
| ---- | -----| -----------
110-
| `min` | `number` | Minimun allowed value for scale.min
111-
| `max` | `number` | Maximum allowed value for scale.max
140+
| `min` | `number | 'original'` | Minimum allowed value for scale.min
141+
| `max` | `number | 'original'` | Maximum allowed value for scale.max
112142
| `minRange` | `number` | Minimum allowed range (max - min). This defines the max zoom level.
143+
144+
You may use the keyword `'original'` in place of a numeric limit to instruct chartjs-plugin-zoom to use whatever limits the scale had when the chart was first displayed.

src/core.js

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ import {panFunctions, updateRange, zoomFunctions} from './scale.types';
33
import {getState} from './state';
44
import {directionEnabled, getEnabledScalesByPoint} from './utils';
55

6-
function storeOriginalScaleLimits(chart) {
7-
const {originalScaleLimits} = getState(chart);
6+
function storeOriginalScaleLimits(chart, state) {
7+
const {originalScaleLimits} = state;
88
each(chart.scales, function(scale) {
99
if (!originalScaleLimits[scale.id]) {
10-
originalScaleLimits[scale.id] = {min: scale.options.min, max: scale.options.max};
10+
originalScaleLimits[scale.id] = {
11+
min: {scale: scale.min, options: scale.options.min},
12+
max: {scale: scale.max, options: scale.options.max},
13+
};
1114
}
1215
});
1316
each(originalScaleLimits, function(opt, key) {
@@ -34,14 +37,15 @@ function getCenter(chart) {
3437
/**
3538
* @param chart The chart instance
3639
* @param {number | {x?: number, y?: number, focalPoint?: {x: number, y: number}}} amount The zoom percentage or percentages and focal point
37-
* @param {string} [transition] Which transiton mode to use. Defaults to 'none'
40+
* @param {string} [transition] Which transition mode to use. Defaults to 'none'
3841
*/
3942
export function zoom(chart, amount, transition = 'none') {
4043
const {x = 1, y = 1, focalPoint = getCenter(chart)} = typeof amount === 'number' ? {x: amount, y: amount} : amount;
41-
const {options: {limits, zoom: zoomOptions}} = getState(chart);
44+
const state = getState(chart);
45+
const {options: {limits, zoom: zoomOptions}} = state;
4246
const {mode = 'xy', overScaleMode} = zoomOptions || {};
4347

44-
storeOriginalScaleLimits(chart);
48+
storeOriginalScaleLimits(chart, state);
4549

4650
const xEnabled = x !== 1 && directionEnabled(mode, 'x', chart);
4751
const yEnabled = y !== 1 && directionEnabled(mode, 'y', chart);
@@ -70,10 +74,11 @@ function getRange(scale, pixel0, pixel1) {
7074
}
7175

7276
export function zoomRect(chart, p0, p1, transition = 'none') {
73-
const {options: {limits, zoom: zoomOptions}} = getState(chart);
77+
const state = getState(chart);
78+
const {options: {limits, zoom: zoomOptions}} = state;
7479
const {mode = 'xy'} = zoomOptions;
7580

76-
storeOriginalScaleLimits(chart);
81+
storeOriginalScaleLimits(chart, state);
7782
const xEnabled = directionEnabled(mode, 'x', chart);
7883
const yEnabled = directionEnabled(mode, 'y', chart);
7984

@@ -91,21 +96,21 @@ export function zoomRect(chart, p0, p1, transition = 'none') {
9196
}
9297

9398
export function zoomScale(chart, scaleId, range, transition = 'none') {
94-
storeOriginalScaleLimits(chart);
99+
storeOriginalScaleLimits(chart, getState(chart));
95100
const scale = chart.scales[scaleId];
96101
updateRange(scale, range, undefined, true);
97102
chart.update(transition);
98103
}
99104

100105

101106
export function resetZoom(chart, transition = 'default') {
102-
const originalScaleLimits = storeOriginalScaleLimits(chart);
107+
const originalScaleLimits = storeOriginalScaleLimits(chart, getState(chart));
103108

104109
each(chart.scales, function(scale) {
105110
const scaleOptions = scale.options;
106111
if (originalScaleLimits[scale.id]) {
107-
scaleOptions.min = originalScaleLimits[scale.id].min;
108-
scaleOptions.max = originalScaleLimits[scale.id].max;
112+
scaleOptions.min = originalScaleLimits[scale.id].min.options;
113+
scaleOptions.max = originalScaleLimits[scale.id].max.options;
109114
} else {
110115
delete scaleOptions.min;
111116
delete scaleOptions.max;
@@ -114,8 +119,8 @@ export function resetZoom(chart, transition = 'default') {
114119
chart.update(transition);
115120
}
116121

117-
function panScale(scale, delta, limits) {
118-
const {panDelta} = getState(scale.chart);
122+
function panScale(scale, delta, limits, state) {
123+
const {panDelta} = state;
119124
// Add possible cumulative delta from previous pan attempts where scale did not change
120125
const storedDelta = panDelta[scale.id] || 0;
121126
if (sign(storedDelta) === sign(delta)) {
@@ -133,19 +138,20 @@ function panScale(scale, delta, limits) {
133138

134139
export function pan(chart, delta, enabledScales, transition = 'none') {
135140
const {x = 0, y = 0} = typeof delta === 'number' ? {x: delta, y: delta} : delta;
136-
const {options: {pan: panOptions, limits}} = getState(chart);
141+
const state = getState(chart);
142+
const {options: {pan: panOptions, limits}} = state;
137143
const {mode = 'xy', onPan} = panOptions || {};
138144

139-
storeOriginalScaleLimits(chart);
145+
storeOriginalScaleLimits(chart, state);
140146

141147
const xEnabled = x !== 0 && directionEnabled(mode, 'x', chart);
142148
const yEnabled = y !== 0 && directionEnabled(mode, 'y', chart);
143149

144150
each(enabledScales || chart.scales, function(scale) {
145151
if (scale.isHorizontal() && xEnabled) {
146-
panScale(scale, x, limits);
152+
panScale(scale, x, limits, state);
147153
} else if (!scale.isHorizontal() && yEnabled) {
148-
panScale(scale, y, limits);
154+
panScale(scale, y, limits, state);
149155
}
150156
});
151157

src/hammer.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import {pan, zoom} from './core';
44
import {getState} from './state';
55
import {directionEnabled, getEnabledScalesByPoint} from './utils';
66

7-
function createEnabler(chart) {
8-
const state = getState(chart);
7+
function createEnabler(chart, state) {
98
return function(recognizer, event) {
109
const panOptions = state.options.pan;
1110
if (!panOptions || !panOptions.enabled) {
@@ -135,7 +134,7 @@ export function startHammer(chart, options) {
135134
if (panOptions && panOptions.enabled) {
136135
mc.add(new Hammer.Pan({
137136
threshold: panOptions.threshold,
138-
enable: createEnabler(chart)
137+
enable: createEnabler(chart, state)
139138
}));
140139
mc.on('panstart', (e) => startPan(chart, state, e));
141140
mc.on('panmove', (e) => handlePan(chart, state, e));

src/scale.types.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {getState} from './state';
2+
13
function zoomDelta(scale, zoom, center) {
24
const range = scale.max - scale.min;
35
const newRange = range * (zoom - 1);
@@ -12,9 +14,27 @@ function zoomDelta(scale, zoom, center) {
1214
};
1315
}
1416

17+
function getLimit(chartState, scale, scaleLimits, prop, fallback) {
18+
let limit = scaleLimits[prop];
19+
if (limit === 'original') {
20+
const original = chartState.originalScaleLimits[scale.id][prop];
21+
limit = original.options !== null && original.options !== undefined ? original.options : original.scale;
22+
}
23+
if (limit === null || limit === undefined) {
24+
limit = fallback;
25+
}
26+
return limit;
27+
}
28+
1529
export function updateRange(scale, {min, max}, limits, zoom = false) {
16-
const {axis, options: scaleOpts} = scale;
17-
const {min: minLimit = -Infinity, max: maxLimit = Infinity, minRange = 0} = limits && limits[axis] || {};
30+
const chartState = getState(scale.chart);
31+
const {id, axis, options: scaleOpts} = scale;
32+
33+
const scaleLimits = limits && (limits[id] || limits[axis]) || {};
34+
const {minRange = 0} = scaleLimits;
35+
const minLimit = getLimit(chartState, scale, scaleLimits, 'min', -Infinity);
36+
const maxLimit = getLimit(chartState, scale, scaleLimits, 'max', Infinity);
37+
1838
const cmin = Math.max(min, minLimit);
1939
const cmax = Math.min(max, maxLimit);
2040
const range = zoom ? Math.max(cmax - cmin, minRange) : scale.max - scale.min;
@@ -58,7 +78,6 @@ function existCategoryFromMaxZoom(scale) {
5878
if (scale.max < maxIndex) {
5979
scale.max += 1;
6080
}
61-
6281
}
6382

6483
function zoomCategoryScale(scale, zoom, center, limits) {

types/options.d.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export interface PinchOptions {
5757
}
5858

5959
/**
60-
* Container for zoop options
60+
* Container for zoom options
6161
*/
6262
export interface ZoomOptions {
6363
/**
@@ -115,7 +115,6 @@ export interface PanOptions {
115115
*/
116116
enabled?: boolean;
117117

118-
119118
/**
120119
* Panning directions. Remove the appropriate direction to disable
121120
* Eg. 'y' would only allow panning in the y direction
@@ -158,17 +157,18 @@ export interface PanOptions {
158157
onPanStart?: (context: { chart: Chart, event: Event, point: Point }) => boolean | undefined;
159158
}
160159

160+
export interface ScaleLimits {
161+
min?: number | 'original';
162+
max?: number | 'original';
163+
minRange?: number;
164+
}
165+
161166
export interface LimitOptions {
162-
x?: {
163-
min?: number;
164-
max?: number;
165-
minRange?: number;
166-
},
167-
y?: {
168-
min?: number;
169-
max?: number;
170-
minRange?: number;
171-
}
167+
// Default horizontal and vertical scale limits
168+
x?: ScaleLimits;
169+
y?: ScaleLimits;
170+
// Optional additional scale limits, indexed by the scale's ID (key)
171+
[axisId: string]: ScaleLimits;
172172
}
173173

174174
export interface ZoomPluginOptions {

0 commit comments

Comments
 (0)