Skip to content

Commit

Permalink
fix: wheel zoom on logarighmic scale
Browse files Browse the repository at this point in the history
  • Loading branch information
kurkle committed Nov 17, 2024
1 parent 348bc9f commit 113847a
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 14 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
61 changes: 61 additions & 0 deletions test/specs/zoom.wheel.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,67 @@ describe('zoom with wheel', function () {
});
});

describe('with logarithmic scale', function() {
it('should zoom correctly when mouse in center of chart', function () {
const config = {
type: 'line',
data: {
datasets: [
{data: [1, 10, 100, 1000, 10000]}
],
},
options: {
scales: {
y: {
type: 'logarithmic'
}
},
plugins: {
zoom: {
zoom: {
mode: 'y',
wheel: {
enabled: true,
},
}
}
}
}
};
const chart = window.acquireChart(config);
const scaleY = chart.scales.y;

const wheelEv = {
x: Math.round(scaleY.left + (scaleY.right - scaleY.left) / 2),
y: Math.round(scaleY.top + (scaleY.bottom - scaleY.top) / 2),
deltaY: -1
};

expect(scaleY.min).toBe(1);
expect(scaleY.max).toBe(10000);

jasmine.triggerWheelEvent(chart, wheelEv);

expect(scaleY.min).toBeCloseTo(1.6, 1);
expect(scaleY.max).toBeCloseTo(6310, -1);

jasmine.triggerWheelEvent(chart, wheelEv);

expect(scaleY.min).toBeCloseTo(2.4, 1);
expect(scaleY.max).toBeCloseTo(4170, -1);

chart.resetZoom();

expect(scaleY.min).toBe(1);
expect(scaleY.max).toBe(10000);

jasmine.triggerWheelEvent(chart, {...wheelEv, deltaY: 1});

expect(scaleY.min).toBe(0.6);
expect(scaleY.max).toBeCloseTo(15800, -2);
});
});

describe('events', function () {
it('should call onZoomStart', function () {
const startSpy = jasmine.createSpy('started');
Expand Down

0 comments on commit 113847a

Please sign in to comment.