Skip to content

Commit c9268ea

Browse files
authored
Combine range options, add minimum scale range (#470)
* Combine range options, add minimum scale range * Review feedback * range to minRange
1 parent 80a398c commit c9268ea

File tree

11 files changed

+231
-224
lines changed

11 files changed

+231
-224
lines changed

docs/guide/options.md

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
The options for chartjs-plugin-zoom should be placed in `options.plugins.zoom` in chart.js configuration.
44

5-
The options are split in two sub-objects, [pan](#pan) and [zoom](#zoom).
5+
The options are split in three sub-objects, [limits](#limits), [pan](#pan) and [zoom](#zoom).
66

77
```js
88
const chart = new Chart('id', {
@@ -14,6 +14,9 @@ const chart = new Chart('id', {
1414
pan: {
1515
// pan options and/or events
1616
},
17+
limits: {
18+
// axis limits
19+
},
1720
zoom: {
1821
// zoom options and/or events
1922
}
@@ -33,8 +36,6 @@ const chart = new Chart('id', {
3336
| `mode` | `'x'`\|`'y'`\|`'xy'` | `'xy'` | Allowed panning directions
3437
| `modifierKey` | `'ctrl'`\|`'alt'`\|`'shift'`\|`'meta'` | `null` | Modifier key required for panning with mouse
3538
| `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
36-
| `rangeMin` | `{x: any, y: any}` | `undefined` | Minimum pan range allowed for the axes. Value type depends on the scale type
37-
| `rangeMax` | `{x: any, y: any}` | `undefined` | Maximum pan range allowed for the axes. Value type depends on the scale type
3839
| `speed` | `number` | `20` | Factor for pan velocity on **category scale**
3940
| `threshold` | `number` | `10` | Mimimal pan distance required before actually applying pan
4041

@@ -56,8 +57,6 @@ const chart = new Chart('id', {
5657
| `drag` | `boolean` | `undefined` | Enable drag-to-zoom behavior (disables zooming by wheel)
5758
| `mode` | `'x'`\|`'y'`\|`'xy'` | `'xy'` | Allowed zoom directions
5859
| `overScaleMode` | `'x'`\|`'y'`\|`'xy'` | `undefined` | Which of the enabled zooming directions should only be available when the mouse cursor is over a scale for that axis
59-
| `rangeMin` | `{x: any, y: any}` | `undefined` | Minimum zoom range allowed for the axes. Value type depends on the scale type
60-
| `rangeMax` | `{x: any, y: any}` | `undefined` | Maximum zoom range allowed for the axes. Value type depends on the scale type
6160
| `speed` | `number` | `0.1` | Factor of zoom speed via mouse wheel.
6261
| `threshold` | `number` | `0` | Mimimal zoom distance required before actually applying zoom
6362
| `wheelModifierKey` | `'ctrl'`\|`'alt'`\|`'shift'`\|`'meta'` | `null` | Modifier key required for zooming with mouse
@@ -69,3 +68,22 @@ const chart = new Chart('id', {
6968
| `onZoom` | `{chart}` | Called while the chart is being zoomed
7069
| `onZoomComplete` | `{chart}` | Called once zooming is completed
7170
| `onZoomRejected` | `{chart,event}` | Called when zoom is rejected due to missing modifier key. `event` is the a [hammer event](https://hammerjs.github.io/api#event-object) that failed
71+
72+
## Limits
73+
74+
Limits options define the limits per axis for pan and zoom.
75+
76+
### Limit options
77+
78+
| Name | Type | Description
79+
| ---- | -----| -----------
80+
| `x` | [`ScaleLimits`](#scale-limits) | Limits for x-axis
81+
| `y` | [`ScaleLimits`](#scale-limits) | Limits for y-axis
82+
83+
#### Scale Limits
84+
85+
| Name | Type | Description
86+
| ---- | -----| -----------
87+
| `min` | `number` | Minimun allowed value for scale.min
88+
| `max` | `number` | Maximum allowed value for scale.max
89+
| `minRange` | `number` | Minimum allowed range (max - min). This defines the max zoom level.

docs/samples/basic.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,20 @@ Object.keys(scales).forEach(scale => Object.assign(scales[scale], scaleOpts));
5252

5353
// <block:zoom:0>
5454
const zoomOptions = {
55-
zoom: {
55+
limits: {
56+
x: {min: -200, max: 200, minRange: 50},
57+
y: {min: -200, max: 200, minRange: 50}
58+
},
59+
pan: {
5660
enabled: true,
5761
mode: 'xy',
5862
},
59-
pan: {
63+
zoom: {
6064
enabled: true,
6165
mode: 'xy',
6266
}
6367
};
64-
// </block>
68+
// </block:zoom>
6569

6670
const panStatus = () => zoomOptions.pan.enabled ? 'enabled' : 'disabled';
6771
const zoomStatus = () => zoomOptions.zoom.enabled ? 'enabled' : 'disabled';

docs/samples/log.md

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -138,29 +138,17 @@ const config = {
138138
scales: scales,
139139
plugins: {
140140
zoom: {
141+
limits: {
142+
x: {min: 0.5, max: 2e3, minRange: 100},
143+
y: {min: -50, max: 10, minRange: 10}
144+
},
141145
pan: {
142146
enabled: true,
143147
mode: 'xy',
144-
rangeMin: {
145-
x: 0.5,
146-
y: -50
147-
},
148-
rangeMax: {
149-
x: 2e3,
150-
y: 10
151-
}
152148
},
153149
zoom: {
154150
enabled: true,
155151
mode: 'xy',
156-
rangeMin: {
157-
x: 0.5,
158-
y: -50
159-
},
160-
rangeMax: {
161-
x: 2e3,
162-
y: 10
163-
}
164152
},
165153
}
166154
},

src/core.js

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ function storeOriginalScaleLimits(chart) {
1818
return originalScaleLimits;
1919
}
2020

21-
function zoomScale(scale, zoom, center, zoomOptions) {
21+
function zoomScale(scale, zoom, center, limits) {
2222
const fn = zoomFunctions[scale.type] || zoomFunctions.default;
23-
call(fn, [scale, zoom, center, zoomOptions]);
23+
call(fn, [scale, zoom, center, limits]);
2424
}
2525

2626
function getCenter(chart) {
@@ -34,12 +34,12 @@ function getCenter(chart) {
3434
/**
3535
* @param chart The chart instance
3636
* @param {number | {x?: number, y?: number, focalPoint?: {x: number, y: number}}} zoom The zoom percentage or percentages and focal point
37-
* @param {object} [options] The zoom options
3837
* @param {boolean} [useTransition] Whether to use `zoom` transition
3938
*/
40-
export function doZoom(chart, zoom, options = {}, useTransition) {
39+
export function doZoom(chart, zoom, useTransition) {
4140
const {x = 1, y = 1, focalPoint = getCenter(chart)} = typeof zoom === 'number' ? {x: zoom, y: zoom} : zoom;
42-
const {mode = 'xy', overScaleMode} = options;
41+
const {options: {limits, zoom: zoomOptions}} = getState(chart);
42+
const {mode = 'xy', overScaleMode} = zoomOptions || {};
4343

4444
storeOriginalScaleLimits(chart);
4545

@@ -49,15 +49,15 @@ export function doZoom(chart, zoom, options = {}, useTransition) {
4949

5050
each(enabledScales || chart.scales, function(scale) {
5151
if (scale.isHorizontal() && xEnabled) {
52-
zoomScale(scale, x, focalPoint, options);
52+
zoomScale(scale, x, focalPoint, limits);
5353
} else if (!scale.isHorizontal() && yEnabled) {
54-
zoomScale(scale, y, focalPoint, options);
54+
zoomScale(scale, y, focalPoint, limits);
5555
}
5656
});
5757

5858
chart.update(useTransition ? 'zoom' : 'none');
5959

60-
call(options.onZoom, [{chart}]);
60+
call(zoomOptions.onZoom, [{chart}]);
6161
}
6262

6363
export function resetZoom(chart) {
@@ -76,14 +76,15 @@ export function resetZoom(chart) {
7676
chart.update();
7777
}
7878

79-
function panScale(scale, delta, panOptions) {
79+
function panScale(scale, delta, panOptions, limits) {
8080
const fn = panFunctions[scale.type] || panFunctions.default;
81-
call(fn, [scale, delta, panOptions]);
81+
call(fn, [scale, delta, panOptions, limits]);
8282
}
8383

84-
export function doPan(chart, pan, options = {}, enabledScales) {
84+
export function doPan(chart, pan, enabledScales) {
8585
const {x = 0, y = 0} = typeof pan === 'number' ? {x: pan, y: pan} : pan;
86-
const {mode = 'xy', onPan} = options;
86+
const {options: {pan: panOptions, limits}} = getState(chart);
87+
const {mode = 'xy', onPan} = panOptions || {};
8788

8889
storeOriginalScaleLimits(chart);
8990

@@ -92,9 +93,9 @@ export function doPan(chart, pan, options = {}, enabledScales) {
9293

9394
each(enabledScales || chart.scales, function(scale) {
9495
if (scale.isHorizontal() && xEnabled) {
95-
panScale(scale, x, options);
96+
panScale(scale, x, panOptions, limits);
9697
} else if (!scale.isHorizontal() && yEnabled) {
97-
panScale(scale, y, options);
98+
panScale(scale, y, panOptions, limits);
9899
}
99100
});
100101

src/hammer.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,7 @@ function handlePinch(chart, state, e) {
4949
const zoomPercent = 1 / state.scale * e.scale;
5050
const rect = e.target.getBoundingClientRect();
5151
const pinch = pinchAxes(pointers[0], pointers[1]);
52-
const options = state.options.zoom;
53-
const mode = options.mode;
52+
const mode = state.options.zoom.mode;
5453
const zoom = {
5554
x: pinch.x && directionEnabled(mode, 'x', chart) ? zoomPercent : 1,
5655
y: pinch.y && directionEnabled(mode, 'y', chart) ? zoomPercent : 1,
@@ -60,7 +59,7 @@ function handlePinch(chart, state, e) {
6059
}
6160
};
6261

63-
doZoom(chart, zoom, options);
62+
doZoom(chart, zoom);
6463

6564
// Keep track of overall scale
6665
state.scale = e.scale;
@@ -86,7 +85,7 @@ function handlePan(chart, state, e) {
8685
const delta = state.delta;
8786
if (delta !== null) {
8887
state.panning = true;
89-
doPan(chart, {x: e.deltaX - delta.x, y: e.deltaY - delta.y}, state.options.pan, state.panScales);
88+
doPan(chart, {x: e.deltaX - delta.x, y: e.deltaY - delta.y}, state.panScales);
9089
state.delta = {x: e.deltaX, y: e.deltaY};
9190
}
9291
}

src/handlers.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export function mouseUp(chart, event) {
9393
y: (rect.top - top) / (1 - dragDistanceY / height) + top
9494
}
9595
};
96-
doZoom(chart, zoom, zoomOptions, true);
96+
doZoom(chart, zoom, true);
9797

9898
call(zoomOptions.onZoomComplete, [chart]);
9999
}
@@ -129,7 +129,7 @@ export function wheel(chart, event) {
129129
}
130130
};
131131

132-
doZoom(chart, zoom, zoomOptions);
132+
doZoom(chart, zoom);
133133

134134
if (onZoomComplete) {
135135
debounce(() => call(onZoomComplete, [{chart}]), 250);

src/plugin.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ export default {
3434
startHammer(chart, options);
3535
}
3636

37-
chart.pan = (pan, panOptions, panScales) => doPan(chart, pan, panOptions, panScales);
38-
chart.zoom = (zoom, zoomOptions, useTransition) => doZoom(chart, zoom, zoomOptions, useTransition);
37+
chart.pan = (pan, panScales) => doPan(chart, pan, panScales);
38+
chart.zoom = (zoom, useTransition) => doZoom(chart, zoom, useTransition);
3939
chart.resetZoom = () => resetZoom(chart);
4040
},
4141

src/scale.types.js

Lines changed: 44 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,55 @@
1-
import {isNullOrUndef} from 'chart.js/helpers';
2-
3-
function rangeMaxLimiter(rangeMax, axis, newMax) {
4-
const limit = rangeMax && rangeMax[axis];
5-
return !isNullOrUndef(limit) && newMax > limit ? limit : newMax;
6-
}
7-
8-
function rangeMinLimiter(rangeMin, axis, newMin) {
9-
const limit = rangeMin && rangeMin[axis];
10-
return !isNullOrUndef(limit) && newMin < limit ? limit : newMin;
11-
}
12-
131
function zoomDelta(scale, zoom, center) {
142
const range = scale.max - scale.min;
15-
const newDiff = range * (zoom - 1);
3+
const newRange = range * (zoom - 1);
164

175
const centerPoint = scale.isHorizontal() ? center.x : center.y;
18-
const minPercent = (scale.getValueForPixel(centerPoint) - scale.min) / range;
6+
const minPercent = (scale.getValueForPixel(centerPoint) - scale.min) / range || 0;
197
const maxPercent = 1 - minPercent;
208

219
return {
22-
min: newDiff * minPercent,
23-
max: newDiff * maxPercent
10+
min: newRange * minPercent,
11+
max: newRange * maxPercent
2412
};
2513
}
2614

27-
function zoomNumericalScale(scale, zoom, center, zoomOptions) {
28-
const delta = zoomDelta(scale, zoom, center);
15+
function updateRange(scale, {min, max}, limits, zoom = false) {
2916
const {axis, options: scaleOpts} = scale;
30-
scaleOpts.min = rangeMinLimiter(zoomOptions.rangeMin, axis, scale.min + delta.min);
31-
scaleOpts.max = rangeMaxLimiter(zoomOptions.rangeMax, axis, scale.max - delta.max);
17+
const {min: minLimit = -Infinity, max: maxLimit = Infinity, minRange = 0} = limits && limits[axis] || {};
18+
const cmin = Math.max(min, minLimit);
19+
const cmax = Math.min(max, maxLimit);
20+
const range = zoom ? Math.max(cmax - cmin, minRange) : scale.max - scale.min;
21+
if (cmax - cmin !== range) {
22+
if (minLimit > cmax - range) {
23+
min = cmin;
24+
max = cmin + range;
25+
} else if (maxLimit < cmin + range) {
26+
max = cmax;
27+
min = cmax - range;
28+
} else if (zoom && range === minRange) {
29+
min = scale.min;
30+
max = scale.max;
31+
} else {
32+
const offset = (range - cmax + cmin) / 2;
33+
min = cmin - offset;
34+
max = cmax + offset;
35+
}
36+
} else {
37+
min = cmin;
38+
max = cmax;
39+
}
40+
scaleOpts.min = min;
41+
scaleOpts.max = max;
42+
}
43+
44+
function zoomNumericalScale(scale, zoom, center, limits) {
45+
const delta = zoomDelta(scale, zoom, center);
46+
const newRange = {min: scale.min + delta.min, max: scale.max - delta.max};
47+
updateRange(scale, newRange, limits, true);
3248
}
3349

3450
const integerChange = (v) => v === 0 || isNaN(v) ? 0 : v < 0 ? Math.min(Math.round(v), -1) : Math.max(Math.round(v), 1);
3551

36-
function zoomCategoryScale(scale, zoom, center, zoomOptions) {
52+
function zoomCategoryScale(scale, zoom, center, limits) {
3753
const labels = scale.getLabels();
3854
const maxIndex = labels.length - 1;
3955
if (scale.min === scale.max && zoom < 1) {
@@ -44,13 +60,12 @@ function zoomCategoryScale(scale, zoom, center, zoomOptions) {
4460
}
4561
}
4662
const delta = zoomDelta(scale, zoom, center);
47-
const {axis, options: scaleOpts} = scale;
48-
scaleOpts.min = Math.max(0, rangeMinLimiter(zoomOptions.rangeMin, axis, scale.min + integerChange(delta.min)));
49-
scaleOpts.max = Math.min(maxIndex, rangeMaxLimiter(zoomOptions.rangeMax, axis, scale.max - integerChange(delta.max)));
63+
const newRange = {min: scale.min + integerChange(delta.min), max: scale.max - integerChange(delta.max)};
64+
updateRange(scale, newRange, limits, true);
5065
}
5166

5267
const categoryDelta = new WeakMap();
53-
function panCategoryScale(scale, delta, panOptions) {
68+
function panCategoryScale(scale, delta, panOptions, limits) {
5469
const labels = scale.getLabels();
5570
const lastLabelIndex = labels.length - 1;
5671
const offsetAmt = Math.max(scale.ticks.length, 1);
@@ -65,31 +80,14 @@ function panCategoryScale(scale, delta, panOptions) {
6580

6681
categoryDelta.set(scale, minIndex !== scaleMin ? 0 : cumDelta);
6782

68-
const {axis, options: scaleOpts} = scale;
69-
scaleOpts.min = rangeMinLimiter(panOptions.rangeMin, axis, minIndex);
70-
scaleOpts.max = rangeMaxLimiter(panOptions.rangeMax, axis, maxIndex);
83+
updateRange(scale, {min: minIndex, max: maxIndex}, limits);
7184
}
7285

73-
function panNumericalScale(scale, delta, panOptions) {
74-
const {axis, min: prevStart, max: prevEnd, options: scaleOpts} = scale;
86+
function panNumericalScale(scale, delta, panOptions, limits) {
87+
const {min: prevStart, max: prevEnd} = scale;
7588
const newMin = scale.getValueForPixel(scale.getPixelForValue(prevStart) - delta);
7689
const newMax = scale.getValueForPixel(scale.getPixelForValue(prevEnd) - delta);
77-
const rangeMin = rangeMinLimiter(panOptions.rangeMin, axis, newMin);
78-
const rangeMax = rangeMaxLimiter(panOptions.rangeMax, axis, newMax);
79-
let diff;
80-
81-
if (newMin >= rangeMin && newMax <= rangeMax) {
82-
scaleOpts.min = newMin;
83-
scaleOpts.max = newMax;
84-
} else if (newMin < rangeMin) {
85-
diff = prevStart - rangeMin;
86-
scaleOpts.min = rangeMin;
87-
scaleOpts.max = prevEnd - diff;
88-
} else if (newMax > rangeMax) {
89-
diff = rangeMax - prevEnd;
90-
scaleOpts.max = rangeMax;
91-
scaleOpts.min = prevStart + diff;
92-
}
90+
updateRange(scale, {min: newMin, max: newMax}, limits);
9391
}
9492

9593
export const zoomFunctions = {

0 commit comments

Comments
 (0)