Skip to content

Commit eb302da

Browse files
authored
Fix "original" scale limits with nonlinear pan (#774)
* Add some JSDoc types These currently raise spurious errors, due to limitations in Chart.js types. * Fix handling of "original" with panning nonlinear scales Nonlinear scales failed to resolve "original" within panNumericalScale. To fix this: * Resolving "original" within panNumericalScale seemed redundant, when updateRange was already doing it, so I instead moved the logic into updateRange and control it with a new special value for the `zoom` parameter. (Hopefully the use of a special string like this is acceptable as long as JSDoc calls it out - if I should use another approach, please let me know.) * This change put updateRange over the configured ESLint complexity limit, so I extracted a new getScaleLimits function to reduce complexity. Add unit tests, both for this bug and for the preexisting but untested "original" feature. * Upgrade Chart.js to fix type errors
1 parent c1985b1 commit eb302da

File tree

4 files changed

+126
-16
lines changed

4 files changed

+126
-16
lines changed

package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"@typescript-eslint/eslint-plugin": "^5.4.0",
4141
"@typescript-eslint/parser": "^5.4.0",
4242
"babel-loader": "^8.3.0",
43-
"chart.js": "^4.2.1",
43+
"chart.js": "^4.3.2",
4444
"chartjs-adapter-date-fns": "^2.0.1",
4545
"chartjs-test-utils": "^0.3.0",
4646
"concurrently": "^6.0.2",

src/scale.types.js

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
import {valueOrDefault} from 'chart.js/helpers';
22
import {getState} from './state';
33

4+
/**
5+
* @typedef {import('chart.js').Point} Point
6+
* @typedef {import('chart.js').Scale} Scale
7+
* @typedef {import('../types/options').LimitOptions} LimitOptions
8+
* @typedef {{min: number, max: number}} ScaleRange
9+
* @typedef {import('../types/options').ScaleLimits} ScaleLimits
10+
*/
11+
12+
/**
13+
* @param {Scale} scale
14+
* @param {number} zoom
15+
* @param {Point} center
16+
* @returns {ScaleRange}
17+
*/
418
function zoomDelta(scale, zoom, center) {
519
const range = scale.max - scale.min;
620
const newRange = range * (zoom - 1);
@@ -20,6 +34,15 @@ function zoomDelta(scale, zoom, center) {
2034
};
2135
}
2236

37+
/**
38+
* @param {Scale} scale
39+
* @param {LimitOptions|undefined} limits
40+
* @returns {ScaleLimits}
41+
*/
42+
function getScaleLimits(scale, limits) {
43+
return limits && (limits[scale.id] || limits[scale.axis]) || {};
44+
}
45+
2346
function getLimit(state, scale, scaleLimits, prop, fallback) {
2447
let limit = scaleLimits[prop];
2548
if (limit === 'original') {
@@ -29,6 +52,12 @@ function getLimit(state, scale, scaleLimits, prop, fallback) {
2952
return valueOrDefault(limit, fallback);
3053
}
3154

55+
/**
56+
* @param {Scale} scale
57+
* @param {number} pixel0
58+
* @param {number} pixel1
59+
* @returns {ScaleRange}
60+
*/
3261
function getRange(scale, pixel0, pixel1) {
3362
const v0 = scale.getValueForPixel(pixel0);
3463
const v1 = scale.getValueForPixel(pixel1);
@@ -38,15 +67,27 @@ function getRange(scale, pixel0, pixel1) {
3867
};
3968
}
4069

70+
/**
71+
* @param {Scale} scale
72+
* @param {ScaleRange} minMax
73+
* @param {LimitOptions} [limits]
74+
* @param {boolean|'pan'} [zoom]
75+
* @returns {boolean}
76+
*/
4177
export function updateRange(scale, {min, max}, limits, zoom = false) {
4278
const state = getState(scale.chart);
43-
const {id, axis, options: scaleOpts} = scale;
79+
const {options: scaleOpts} = scale;
4480

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

86+
if (zoom === 'pan' && (min < minLimit || max > maxLimit)) {
87+
// At limit: No change but return true to indicate no need to store the delta.
88+
return true;
89+
}
90+
5091
const range = zoom ? Math.max(max - min, minRange) : scale.max - scale.min;
5192
const offset = (range - max + min) / 2;
5293
min -= offset;
@@ -139,20 +180,18 @@ const OFFSETS = {
139180
year: 182 * 24 * 60 * 60 * 1000 // 182 d
140181
};
141182

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

158197
function panNonLinearScale(scale, delta, limits) {

test/specs/pan.spec.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,77 @@ describe('pan', function() {
124124
expect(scale.options.min).toBe(2);
125125
expect(scale.options.max).toBe(2);
126126
});
127+
128+
it('should respect original limits', function() {
129+
const chart = window.acquireChart({
130+
type: 'line',
131+
data,
132+
options: {
133+
plugins: {
134+
zoom: {
135+
pan: {
136+
enabled: true,
137+
mode: 'x',
138+
},
139+
limits: {
140+
x: {
141+
min: 'original',
142+
max: 'original',
143+
}
144+
},
145+
}
146+
},
147+
scales: {
148+
x: {
149+
min: 1,
150+
max: 2
151+
}
152+
}
153+
}
154+
});
155+
const scale = chart.scales.x;
156+
expect(scale.min).toBe(1);
157+
expect(scale.max).toBe(2);
158+
chart.pan(100);
159+
expect(scale.min).toBe(1);
160+
expect(scale.max).toBe(2);
161+
});
162+
163+
it('should respect original limits for nonlinear scales', function() {
164+
const chart = window.acquireChart({
165+
type: 'line',
166+
data,
167+
options: {
168+
plugins: {
169+
zoom: {
170+
pan: {
171+
enabled: true,
172+
mode: 'x',
173+
},
174+
limits: {
175+
x: {
176+
min: 'original',
177+
max: 'original',
178+
}
179+
},
180+
}
181+
},
182+
scales: {
183+
x: {
184+
type: 'logarithmic',
185+
min: 1,
186+
max: 10
187+
}
188+
}
189+
}
190+
});
191+
const scale = chart.scales.x;
192+
expect(scale.min).toBe(1);
193+
expect(scale.max).toBe(10);
194+
chart.pan(100);
195+
expect(scale.min).toBe(1);
196+
expect(scale.max).toBe(10);
197+
});
127198
});
128199

129200
describe('events', function() {

0 commit comments

Comments
 (0)