Skip to content

Commit

Permalink
fix: wheel zoom on logarighmic scale (#864)
Browse files Browse the repository at this point in the history
* chore: re-arrange tests

* test: add missing api method expectations

* chore(tests): remove unneeded async/awaits

* fix: wheel zoom on logarighmic scale

* fix: lint
  • Loading branch information
kurkle authored Nov 17, 2024
1 parent 11305aa commit ec1c125
Show file tree
Hide file tree
Showing 7 changed files with 1,141 additions and 981 deletions.
85 changes: 71 additions & 14 deletions src/scale.types.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,73 @@ import {getState} from './state';
* @typedef {import('../types/options').ScaleLimits} ScaleLimits
*/

/**
*
* @param {number} val
* @param {number} min
* @param {number} range
* @param {number} newRange
* @returns {ScaleRange}
*/
function zoomDelta(val, min, range, newRange) {
const minPercent = Math.max(0, Math.min(1, (val - min) / range || 0));
const maxPercent = 1 - minPercent;

return {
min: newRange * minPercent,
max: newRange * maxPercent
};
}

/**
* @param {Scale} scale
* @param {Point} point
* @returns number | undefined
*/
function getValueAtPoint(scale, point) {
const pixel = scale.isHorizontal() ? point.x : point.y;

return scale.getValueForPixel(pixel);
}

/**
* @param {Scale} scale
* @param {number} zoom
* @param {Point} center
* @returns {ScaleRange}
*/
function zoomDelta(scale, zoom, center) {
function linearZoomDelta(scale, zoom, center) {
const range = scale.max - scale.min;
const newRange = range * (zoom - 1);
const centerValue = getValueAtPoint(scale, center);

const centerPoint = scale.isHorizontal() ? center.x : center.y;
// `scale.getValueForPixel()` can return a value less than the `scale.min` or
// greater than `scale.max` when `centerPoint` is outside chartArea.
const minPercent = Math.max(0, Math.min(1,
(scale.getValueForPixel(centerPoint) - scale.min) / range || 0
));
return zoomDelta(centerValue, scale.min, range, newRange);
}

const maxPercent = 1 - minPercent;
/**
* @param {Scale} scale
* @param {number} zoom
* @param {Point} center
* @returns {ScaleRange}
*/
function logarithmicZoomRange(scale, zoom, center) {
const centerValue = getValueAtPoint(scale, center);

// Return the original range, if value could not be determined.
if (centerValue === undefined) {
return {min: scale.min, max: scale.max};
}

const logMin = Math.log10(scale.min);
const logMax = Math.log10(scale.max);
const logCenter = Math.log10(centerValue);
const logRange = logMax - logMin;
const newLogRange = logRange * (zoom - 1);
const delta = zoomDelta(logCenter, logMin, logRange, newLogRange);

return {
min: newRange * minPercent,
max: newRange * maxPercent
min: Math.pow(10, logMin + delta.min),
max: Math.pow(10, logMax - delta.max),
};
}

Expand Down Expand Up @@ -58,7 +103,7 @@ function getLimit(state, scale, scaleLimits, prop, fallback) {
* @param {number} pixel1
* @returns {ScaleRange}
*/
function getRange(scale, pixel0, pixel1) {
function linearRange(scale, pixel0, pixel1) {
const v0 = scale.getValueForPixel(pixel0);
const v1 = scale.getValueForPixel(pixel1);
return {
Expand Down Expand Up @@ -110,13 +155,24 @@ export function updateRange(scale, {min, max}, limits, zoom = false) {
}

function zoomNumericalScale(scale, zoom, center, limits) {
const delta = zoomDelta(scale, zoom, center);
const delta = linearZoomDelta(scale, zoom, center);
const newRange = {min: scale.min + delta.min, max: scale.max - delta.max};
return updateRange(scale, newRange, limits, true);
}

function zoomLogarithmicScale(scale, zoom, center, limits) {
const newRange = logarithmicZoomRange(scale, zoom, center);
return updateRange(scale, newRange, limits, true);
}

/**
* @param {Scale} scale
* @param {number} from
* @param {number} to
* @param {LimitOptions} [limits]
*/
function zoomRectNumericalScale(scale, from, to, limits) {
updateRange(scale, getRange(scale, from, to), limits, true);
updateRange(scale, linearRange(scale, from, to), limits, true);
}

const integerChange = (v) => v === 0 || isNaN(v) ? 0 : v < 0 ? Math.min(Math.round(v), -1) : Math.max(Math.round(v), 1);
Expand All @@ -134,7 +190,7 @@ function existCategoryFromMaxZoom(scale) {
}

function zoomCategoryScale(scale, zoom, center, limits) {
const delta = zoomDelta(scale, zoom, center);
const delta = linearZoomDelta(scale, zoom, center);
if (scale.min === scale.max && zoom < 1) {
existCategoryFromMaxZoom(scale);
}
Expand Down Expand Up @@ -201,6 +257,7 @@ function panNonLinearScale(scale, delta, limits) {
export const zoomFunctions = {
category: zoomCategoryScale,
default: zoomNumericalScale,
logarithmic: zoomLogarithmicScale,
};

export const zoomRectFunctions = {
Expand Down
136 changes: 136 additions & 0 deletions test/specs/api.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ describe('api', function() {
expect(typeof chart.resetZoom).toBe('function');
expect(typeof chart.getZoomLevel).toBe('function');
expect(typeof chart.getInitialScaleBounds).toBe('function');
expect(typeof chart.getZoomedScaleBounds).toBe('function');
expect(typeof chart.isZoomedOrPanned).toBe('function');
expect(typeof chart.isZoomingOrPanning).toBe('function');

});

describe('zoom and resetZoom', function() {
Expand Down Expand Up @@ -369,4 +373,136 @@ describe('api', function() {
expect(chart.getZoomedScaleBounds().x).toBeUndefined();
});
});

describe('with category scale', function() {
it('should zoom up to and out from single category', function() {
const chart = window.acquireChart({
type: 'bar',
data: {
labels: ['a', 'b', 'c', 'd', 'e'],
datasets: [{
data: [1, 2, 3, 2, 1]
}]
},
options: {
scales: {
x: {
min: 'b',
max: 'd'
}
},
}
});
expect(chart.scales.x.min).toBe(1);
expect(chart.scales.x.max).toBe(3);
chart.zoom(1.1);
expect(chart.scales.x.min).toBe(2);
expect(chart.scales.x.max).toBe(2);
chart.zoom(0.9);
expect(chart.scales.x.min).toBe(1);
expect(chart.scales.x.max).toBe(3);
chart.zoom(0.9);
expect(chart.scales.x.min).toBe(0);
expect(chart.scales.x.max).toBe(4);
chart.resetZoom();
expect(chart.scales.x.min).toBe(1);
expect(chart.scales.x.max).toBe(3);
});

it('should not exceed limits', function() {
const chart = window.acquireChart({
type: 'bar',
data: {
labels: ['0', '1', '2', '3', '4', '5', '6'],
datasets: [{
data: [1, 2, 3, 2, 1, 0, 1]
}]
},
options: {
indexAxis: 'y',
scales: {
y: {
min: 2,
max: 4
}
},
plugins: {
zoom: {
limits: {
y: {
min: 1,
max: 5,
minRange: 1
}
},
zoom: {
wheel: {
enabled: true,
},
mode: 'y'
}
}
}
}
});
expect(chart.scales.y.min).toBe(2);
expect(chart.scales.y.max).toBe(4);
chart.zoom(1.1);
expect(chart.scales.y.min).toBe(3);
expect(chart.scales.y.max).toBe(4);
chart.pan(-100);
expect(chart.scales.y.min).toBe(4);
expect(chart.scales.y.max).toBe(5);
chart.zoom(0.9);
expect(chart.scales.y.min).toBe(3);
expect(chart.scales.y.max).toBe(5);
chart.zoom(0.9);
expect(chart.scales.y.min).toBe(1);
expect(chart.scales.y.max).toBe(5);
chart.zoom(0.9);
expect(chart.scales.y.min).toBe(1);
expect(chart.scales.y.max).toBe(5);
chart.pan(-100);
expect(chart.scales.y.min).toBe(1);
expect(chart.scales.y.max).toBe(5);
chart.pan(100);
expect(chart.scales.y.min).toBe(1);
expect(chart.scales.y.max).toBe(5);
});
});

describe('with logarithmic scale', function() {
it('should zoom in and out', function() {
const chart = window.acquireChart({
type: 'bar',
data: {
labels: ['a', 'b', 'c', 'd', 'e'],
datasets: [{
data: [1, 22, 3333, 22, 1],
}]
},
options: {
scales: {
y: {
type: 'logarithmic',
}
},
}
});
expect(chart.scales.y.min).toBe(0.1);
expect(chart.scales.y.max).toBe(4000);
chart.zoom(1.1);
expect(chart.scales.y.min).toBeCloseTo(0.17, 2);
expect(chart.scales.y.max).toBeCloseTo(2355, 0);
chart.zoom(0.9);
expect(chart.scales.y.min).toBeCloseTo(0.105, 3);
expect(chart.scales.y.max).toBeCloseTo(3794, 0);
chart.zoom(0.9);
expect(chart.scales.y.min).toBeCloseTo(0.06, 2);
expect(chart.scales.y.max).toBeCloseTo(6410, 0);
chart.resetZoom();
expect(chart.scales.y.min).toBe(0.1);
expect(chart.scales.y.max).toBe(4000);
});
});
});
4 changes: 4 additions & 0 deletions test/specs/fixtures.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
describe('fixtures', function() {
describe('zoom', jasmine.fixture.specs('zoom'));
describe('pan', jasmine.fixture.specs('pan'));
});
2 changes: 0 additions & 2 deletions test/specs/pan.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
describe('pan', function() {
describe('auto', jasmine.fixture.specs('pan'));

const data = {
labels: ['a', 'b', 'c', 'd', 'e'],
datasets: [{
Expand Down
Loading

0 comments on commit ec1c125

Please sign in to comment.