diff --git a/src/scale.types.js b/src/scale.types.js index a4699d06..391c28fc 100644 --- a/src/scale.types.js +++ b/src/scale.types.js @@ -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), }; } @@ -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 { @@ -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); @@ -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); } @@ -201,6 +257,7 @@ function panNonLinearScale(scale, delta, limits) { export const zoomFunctions = { category: zoomCategoryScale, default: zoomNumericalScale, + logarithmic: zoomLogarithmicScale, }; export const zoomRectFunctions = { diff --git a/test/specs/api.spec.js b/test/specs/api.spec.js index 5f21ef76..19d70de3 100644 --- a/test/specs/api.spec.js +++ b/test/specs/api.spec.js @@ -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() { @@ -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); + }); + }); }); diff --git a/test/specs/fixtures.spec.js b/test/specs/fixtures.spec.js new file mode 100644 index 00000000..7161c43e --- /dev/null +++ b/test/specs/fixtures.spec.js @@ -0,0 +1,4 @@ +describe('fixtures', function() { + describe('zoom', jasmine.fixture.specs('zoom')); + describe('pan', jasmine.fixture.specs('pan')); +}); diff --git a/test/specs/pan.spec.js b/test/specs/pan.spec.js index 072e757b..ca5d42b6 100644 --- a/test/specs/pan.spec.js +++ b/test/specs/pan.spec.js @@ -1,6 +1,4 @@ describe('pan', function() { - describe('auto', jasmine.fixture.specs('pan')); - const data = { labels: ['a', 'b', 'c', 'd', 'e'], datasets: [{ diff --git a/test/specs/zoom.drag.spec.js b/test/specs/zoom.drag.spec.js new file mode 100644 index 00000000..d9ff2602 --- /dev/null +++ b/test/specs/zoom.drag.spec.js @@ -0,0 +1,493 @@ +describe('zoom with drag', function() { + const data = { + datasets: [{ + data: [{ + x: 1, + y: 3 + }, { + x: 2, + y: 2 + }, { + x: 3, + y: 1 + }] + }] + }; + + describe('on linear scale', function() { + let chart, scaleX, scaleY; + + it('should be applied on X scale when mode = x', function() { + chart = window.acquireChart({ + type: 'line', + data, + options: { + scales: { + xScale0: { + id: 'xScale0', + type: 'linear' + }, + yScale0: { + id: 'yScale0', + type: 'linear' + } + }, + plugins: { + zoom: { + zoom: { + drag: { + enabled: true + }, + mode: 'x' + } + } + } + } + }); + + scaleX = chart.scales.xScale0; + scaleY = chart.scales.yScale0; + + jasmine.triggerMouseEvent(chart, 'mousedown', { + x: scaleX.getPixelForValue(1.5), + y: scaleY.getPixelForValue(1.1) + }); + jasmine.triggerMouseEvent(chart, 'mouseup', { + x: scaleX.getPixelForValue(2.8), + y: scaleY.getPixelForValue(1.7) + }); + + expect(scaleX.options.min).toBeCloseTo(1.5); + expect(scaleX.options.max).toBeCloseTo(2.8); + expect(scaleY.options.min).toBeUndefined(); + expect(scaleY.options.max).toBeUndefined(); + }); + + it('should be applied on X scale when mode = f() => x', function() { + chart = window.acquireChart({ + type: 'line', + data, + options: { + scales: { + xScale0: { + id: 'xScale0', + type: 'linear' + }, + yScale0: { + id: 'yScale0', + type: 'linear' + } + }, + plugins: { + zoom: { + zoom: { + drag: { + enabled: true + }, + mode: function() { + return 'x'; + } + } + } + } + } + }); + + scaleX = chart.scales.xScale0; + scaleY = chart.scales.yScale0; + + jasmine.triggerMouseEvent(chart, 'mousedown', { + x: scaleX.getPixelForValue(1.5), + y: scaleY.getPixelForValue(1.1) + }); + jasmine.triggerMouseEvent(chart, 'mouseup', { + x: scaleX.getPixelForValue(2.8), + y: scaleY.getPixelForValue(1.7) + }); + + expect(scaleX.options.min).toBeCloseTo(1.5); + expect(scaleX.options.max).toBeCloseTo(2.8); + expect(scaleY.options.min).toBeUndefined(); + expect(scaleY.options.max).toBeUndefined(); + }); + + it('should be applied on Y scale when mode = y', function() { + chart = window.acquireChart({ + type: 'line', + data, + options: { + scales: { + xScale0: { + id: 'xScale0', + type: 'linear' + }, + yScale0: { + id: 'yScale0', + type: 'linear' + } + }, + plugins: { + zoom: { + zoom: { + drag: { + enabled: true + }, + mode: 'y' + } + } + } + } + }); + + scaleX = chart.scales.xScale0; + scaleY = chart.scales.yScale0; + + jasmine.triggerMouseEvent(chart, 'mousedown', { + x: scaleX.getPixelForValue(1.5), + y: scaleY.getPixelForValue(1.1) + }); + jasmine.triggerMouseEvent(chart, 'mouseup', { + x: scaleX.getPixelForValue(2.8), + y: scaleY.getPixelForValue(1.7) + }); + + expect(scaleX.options.min).toBeUndefined(); + expect(scaleX.options.max).toBeUndefined(); + expect(scaleY.options.min).toBeCloseTo(1.1); + expect(scaleY.options.max).toBeCloseTo(1.7); + }); + + it('should be applied on Y scale when mode = f() => y', function() { + chart = window.acquireChart({ + type: 'line', + data, + options: { + scales: { + xScale0: { + id: 'xScale0', + type: 'linear' + }, + yScale0: { + id: 'yScale0', + type: 'linear' + } + }, + plugins: { + zoom: { + zoom: { + drag: { + enabled: true + }, + mode: function() { + return 'y'; + } + } + } + } + } + }); + + scaleX = chart.scales.xScale0; + scaleY = chart.scales.yScale0; + + jasmine.triggerMouseEvent(chart, 'mousedown', { + x: scaleX.getPixelForValue(1.5), + y: scaleY.getPixelForValue(1.1) + }); + jasmine.triggerMouseEvent(chart, 'mouseup', { + x: scaleX.getPixelForValue(2.8), + y: scaleY.getPixelForValue(1.7) + }); + + expect(scaleX.options.min).toBeUndefined(); + expect(scaleX.options.max).toBeUndefined(); + expect(scaleY.options.min).toBeCloseTo(1.1); + expect(scaleY.options.max).toBeCloseTo(1.7); + }); + + it('should be applied on X and Y scales when mode = xy', function() { + chart = window.acquireChart({ + type: 'line', + data, + options: { + scales: { + xScale0: { + id: 'xScale0', + type: 'linear' + }, + yScale0: { + id: 'yScale0', + type: 'linear' + } + }, + plugins: { + zoom: { + zoom: { + drag: { + enabled: true + }, + mode: 'xy' + } + } + } + } + }); + + scaleX = chart.scales.xScale0; + scaleY = chart.scales.yScale0; + + jasmine.triggerMouseEvent(chart, 'mousedown', { + x: scaleX.getPixelForValue(1.5), + y: scaleY.getPixelForValue(1.1) + }); + jasmine.triggerMouseEvent(chart, 'mouseup', { + x: scaleX.getPixelForValue(2.8), + y: scaleY.getPixelForValue(1.7) + }); + + expect(scaleX.options.min).toBeCloseTo(1.5); + expect(scaleX.options.max).toBeCloseTo(2.8); + expect(scaleY.options.min).toBeCloseTo(1.1); + expect(scaleY.options.max).toBeCloseTo(1.7); + }); + }); + + describe('with modifierKey', function() { + for (const key of ['ctrl', 'alt', 'shift', 'meta']) { + for (const pressed of [true, false]) { + let chart, scaleX, scaleY; + it(`should ${pressed ? '' : 'not '}change ${pressed ? 'with' : 'without'} key ${key}`, async function() { + const rejectedSpy = jasmine.createSpy('wheelFailed'); + chart = window.acquireChart({ + type: 'line', + data, + options: { + scales: { + x: { + type: 'linear', + min: 0, + max: 10 + }, + y: { + type: 'linear' + } + }, + plugins: { + zoom: { + zoom: { + drag: { + enabled: true, + modifierKey: key, + }, + mode: 'x', + onZoomRejected: rejectedSpy + } + } + } + } + }); + + scaleX = chart.scales.x; + scaleY = chart.scales.y; + + const oldMinX = scaleX.options.min; + const oldMaxX = scaleX.options.max; + + const pt = { + x: scaleX.getPixelForValue(1.5), + y: scaleY.getPixelForValue(1.1), + }; + const pt2 = {x: pt.x + 20, y: pt.y + 20}; + const init = {}; + if (pressed) { + init[key + 'Key'] = true; + } + + jasmine.dispatchEvent(chart, 'mousedown', pt, init); + jasmine.dispatchEvent(chart, 'mousemove', pt2, init); + jasmine.dispatchEvent(chart, 'mouseup', pt2, init); + + if (pressed) { + expect(scaleX.options.min).not.toEqual(oldMinX); + expect(scaleX.options.max).not.toEqual(oldMaxX); + expect(rejectedSpy).not.toHaveBeenCalled(); + } else { + expect(scaleX.options.min).toEqual(oldMinX); + expect(scaleX.options.max).toEqual(oldMaxX); + expect(rejectedSpy).toHaveBeenCalled(); + } + }); + } + } + }); + + describe('drag with pan.modifierKey', function() { + for (const key of ['ctrl', 'alt', 'shift', 'meta']) { + for (const pressed of [true, false]) { + let chart, scaleX, scaleY; + it(`should ${pressed ? 'not ' : ''}change ${pressed ? 'without' : 'with'} key ${key}`, async function() { + const rejectedSpy = jasmine.createSpy('zoomRejected'); + const clickSpy = jasmine.createSpy('clicked'); + chart = window.acquireChart({ + type: 'line', + data, + options: { + scales: { + x: { + type: 'linear', + min: 0, + max: 10 + }, + y: { + type: 'linear' + } + }, + plugins: { + zoom: { + pan: { + enabled: true, + modifierKey: key, + }, + zoom: { + drag: { + enabled: true, + }, + mode: 'x', + onZoomRejected: rejectedSpy + } + } + }, + onClick: clickSpy + } + }); + + scaleX = chart.scales.x; + scaleY = chart.scales.y; + + const oldMinX = scaleX.options.min; + const oldMaxX = scaleX.options.max; + + const pt = { + x: scaleX.getPixelForValue(1.5), + y: scaleY.getPixelForValue(1.1), + }; + const pt2 = {x: pt.x + 20, y: pt.y + 20}; + const init = {}; + if (pressed) { + init[key + 'Key'] = true; + } + + jasmine.dispatchEvent(chart, 'mousedown', pt, init); + jasmine.dispatchEvent(chart, 'mousemove', pt2, init); + jasmine.dispatchEvent(chart, 'mouseup', pt2, init); + + if (pressed) { + expect(scaleX.options.min).toEqual(oldMinX); + expect(scaleX.options.max).toEqual(oldMaxX); + expect(rejectedSpy).toHaveBeenCalled(); + } else { + expect(scaleX.options.min).not.toEqual(oldMinX); + expect(scaleX.options.max).not.toEqual(oldMaxX); + expect(rejectedSpy).not.toHaveBeenCalled(); + } + expect(clickSpy).not.toHaveBeenCalled(); + }); + } + } + }); + + describe('events', function() { + it('should call onZoomStart, onZoom and onZoomComplete', function(done) { + const startSpy = jasmine.createSpy('start'); + const zoomSpy = jasmine.createSpy('zoom'); + const chart = window.acquireChart({ + type: 'scatter', + data, + options: { + plugins: { + zoom: { + zoom: { + drag: { + enabled: true, + }, + mode: 'xy', + onZoomStart: startSpy, + onZoom: zoomSpy, + onZoomComplete: () => done() + } + } + } + } + }); + + const pt = { + x: chart.scales.x.getPixelForValue(1.5), + y: chart.scales.y.getPixelForValue(1.1), + }; + const pt2 = {x: pt.x + 20, y: pt.y + 20}; + + expect(chart.isZoomingOrPanning()).toBe(false); + + jasmine.dispatchEvent(chart, 'mousedown', pt); + jasmine.dispatchEvent(chart, 'mousemove', pt2); + + expect(chart.isZoomingOrPanning()).toBe(true); + + jasmine.dispatchEvent(chart, 'mouseup', pt2); + + // Drag state isn't cleared until a timeout fires (later), so we can't + // easily test this here. + // expect(chart.isZoomingOrPanning()).toBe(false); + + expect(startSpy).toHaveBeenCalled(); + expect(zoomSpy).toHaveBeenCalled(); + }); + + it('should call onZoomRejected when onZoomStart returns false', function() { + const zoomSpy = jasmine.createSpy('zoom'); + const rejectSpy = jasmine.createSpy('reject'); + const doneSpy = jasmine.createSpy('done'); + const chart = window.acquireChart({ + type: 'scatter', + data, + options: { + plugins: { + zoom: { + zoom: { + drag: { + enabled: true, + }, + mode: 'xy', + onZoomStart: () => false, + onZoom: zoomSpy, + onZoomComplete: doneSpy, + onZoomRejected: rejectSpy + } + } + } + } + }); + + const pt = { + x: chart.scales.x.getPixelForValue(1.5), + y: chart.scales.y.getPixelForValue(1.1), + }; + const pt2 = {x: pt.x + 20, y: pt.y + 20}; + + expect(chart.isZoomingOrPanning()).toBe(false); + + jasmine.dispatchEvent(chart, 'mousedown', pt); + + expect(chart.isZoomingOrPanning()).toBe(false); + + jasmine.dispatchEvent(chart, 'mousemove', pt2); + jasmine.dispatchEvent(chart, 'mouseup', pt2); + + expect(chart.isZoomingOrPanning()).toBe(false); + + expect(rejectSpy).toHaveBeenCalled(); + expect(zoomSpy).not.toHaveBeenCalled(); + expect(doneSpy).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/test/specs/zoom.spec.js b/test/specs/zoom.spec.js deleted file mode 100644 index 615bc6a4..00000000 --- a/test/specs/zoom.spec.js +++ /dev/null @@ -1,965 +0,0 @@ -describe('zoom', function() { - describe('auto', jasmine.fixture.specs('zoom')); - - const data = { - datasets: [{ - data: [{ - x: 1, - y: 3 - }, { - x: 2, - y: 2 - }, { - x: 3, - y: 1 - }] - }] - }; - - describe('with drag', function() { - describe('on linear scale', function() { - let chart, scaleX, scaleY; - - it('should be applied on X scale when mode = x', function() { - chart = window.acquireChart({ - type: 'line', - data, - options: { - scales: { - xScale0: { - id: 'xScale0', - type: 'linear' - }, - yScale0: { - id: 'yScale0', - type: 'linear' - } - }, - plugins: { - zoom: { - zoom: { - drag: { - enabled: true - }, - mode: 'x' - } - } - } - } - }); - - scaleX = chart.scales.xScale0; - scaleY = chart.scales.yScale0; - - jasmine.triggerMouseEvent(chart, 'mousedown', { - x: scaleX.getPixelForValue(1.5), - y: scaleY.getPixelForValue(1.1) - }); - jasmine.triggerMouseEvent(chart, 'mouseup', { - x: scaleX.getPixelForValue(2.8), - y: scaleY.getPixelForValue(1.7) - }); - - expect(scaleX.options.min).toBeCloseTo(1.5); - expect(scaleX.options.max).toBeCloseTo(2.8); - expect(scaleY.options.min).toBeUndefined(); - expect(scaleY.options.max).toBeUndefined(); - }); - - it('should be applied on X scale when mode = f() => x', function() { - chart = window.acquireChart({ - type: 'line', - data, - options: { - scales: { - xScale0: { - id: 'xScale0', - type: 'linear' - }, - yScale0: { - id: 'yScale0', - type: 'linear' - } - }, - plugins: { - zoom: { - zoom: { - drag: { - enabled: true - }, - mode: function() { - return 'x'; - } - } - } - } - } - }); - - scaleX = chart.scales.xScale0; - scaleY = chart.scales.yScale0; - - jasmine.triggerMouseEvent(chart, 'mousedown', { - x: scaleX.getPixelForValue(1.5), - y: scaleY.getPixelForValue(1.1) - }); - jasmine.triggerMouseEvent(chart, 'mouseup', { - x: scaleX.getPixelForValue(2.8), - y: scaleY.getPixelForValue(1.7) - }); - - expect(scaleX.options.min).toBeCloseTo(1.5); - expect(scaleX.options.max).toBeCloseTo(2.8); - expect(scaleY.options.min).toBeUndefined(); - expect(scaleY.options.max).toBeUndefined(); - }); - - it('should be applied on Y scale when mode = y', function() { - chart = window.acquireChart({ - type: 'line', - data, - options: { - scales: { - xScale0: { - id: 'xScale0', - type: 'linear' - }, - yScale0: { - id: 'yScale0', - type: 'linear' - } - }, - plugins: { - zoom: { - zoom: { - drag: { - enabled: true - }, - mode: 'y' - } - } - } - } - }); - - scaleX = chart.scales.xScale0; - scaleY = chart.scales.yScale0; - - jasmine.triggerMouseEvent(chart, 'mousedown', { - x: scaleX.getPixelForValue(1.5), - y: scaleY.getPixelForValue(1.1) - }); - jasmine.triggerMouseEvent(chart, 'mouseup', { - x: scaleX.getPixelForValue(2.8), - y: scaleY.getPixelForValue(1.7) - }); - - expect(scaleX.options.min).toBeUndefined(); - expect(scaleX.options.max).toBeUndefined(); - expect(scaleY.options.min).toBeCloseTo(1.1); - expect(scaleY.options.max).toBeCloseTo(1.7); - }); - - it('should be applied on Y scale when mode = f() => y', function() { - chart = window.acquireChart({ - type: 'line', - data, - options: { - scales: { - xScale0: { - id: 'xScale0', - type: 'linear' - }, - yScale0: { - id: 'yScale0', - type: 'linear' - } - }, - plugins: { - zoom: { - zoom: { - drag: { - enabled: true - }, - mode: function() { - return 'y'; - } - } - } - } - } - }); - - scaleX = chart.scales.xScale0; - scaleY = chart.scales.yScale0; - - jasmine.triggerMouseEvent(chart, 'mousedown', { - x: scaleX.getPixelForValue(1.5), - y: scaleY.getPixelForValue(1.1) - }); - jasmine.triggerMouseEvent(chart, 'mouseup', { - x: scaleX.getPixelForValue(2.8), - y: scaleY.getPixelForValue(1.7) - }); - - expect(scaleX.options.min).toBeUndefined(); - expect(scaleX.options.max).toBeUndefined(); - expect(scaleY.options.min).toBeCloseTo(1.1); - expect(scaleY.options.max).toBeCloseTo(1.7); - }); - - it('should be applied on X and Y scales when mode = xy', function() { - chart = window.acquireChart({ - type: 'line', - data, - options: { - scales: { - xScale0: { - id: 'xScale0', - type: 'linear' - }, - yScale0: { - id: 'yScale0', - type: 'linear' - } - }, - plugins: { - zoom: { - zoom: { - drag: { - enabled: true - }, - mode: 'xy' - } - } - } - } - }); - - scaleX = chart.scales.xScale0; - scaleY = chart.scales.yScale0; - - jasmine.triggerMouseEvent(chart, 'mousedown', { - x: scaleX.getPixelForValue(1.5), - y: scaleY.getPixelForValue(1.1) - }); - jasmine.triggerMouseEvent(chart, 'mouseup', { - x: scaleX.getPixelForValue(2.8), - y: scaleY.getPixelForValue(1.7) - }); - - expect(scaleX.options.min).toBeCloseTo(1.5); - expect(scaleX.options.max).toBeCloseTo(2.8); - expect(scaleY.options.min).toBeCloseTo(1.1); - expect(scaleY.options.max).toBeCloseTo(1.7); - }); - }); - }); - - describe('with modifierKey', function() { - for (const key of ['ctrl', 'alt', 'shift', 'meta']) { - for (const pressed of [true, false]) { - let chart, scaleX, scaleY; - it(`should ${pressed ? '' : 'not '}change ${pressed ? 'with' : 'without'} key ${key}`, async function() { - const rejectedSpy = jasmine.createSpy('wheelFailed'); - chart = window.acquireChart({ - type: 'line', - data, - options: { - scales: { - x: { - type: 'linear', - min: 0, - max: 10 - }, - y: { - type: 'linear' - } - }, - plugins: { - zoom: { - zoom: { - wheel: { - enabled: true, - modifierKey: key, - }, - mode: 'x', - onZoomRejected: rejectedSpy - } - } - } - } - }); - - scaleX = chart.scales.x; - scaleY = chart.scales.y; - - const oldMinX = scaleX.options.min; - const oldMaxX = scaleX.options.max; - - const wheelEv = { - x: scaleX.getPixelForValue(1.5), - y: scaleY.getPixelForValue(1.1), - deltaY: 1 - }; - if (pressed) { - wheelEv[key + 'Key'] = true; - } - - jasmine.triggerWheelEvent(chart, wheelEv); - - if (pressed) { - expect(scaleX.options.min).not.toEqual(oldMinX); - expect(scaleX.options.max).not.toEqual(oldMaxX); - expect(rejectedSpy).not.toHaveBeenCalled(); - } else { - expect(scaleX.options.min).toEqual(oldMinX); - expect(scaleX.options.max).toEqual(oldMaxX); - expect(rejectedSpy).toHaveBeenCalled(); - } - }); - } - } - }); - - describe('drag with pan.modifierKey', function() { - for (const key of ['ctrl', 'alt', 'shift', 'meta']) { - for (const pressed of [true, false]) { - let chart, scaleX, scaleY; - it(`should ${pressed ? 'not ' : ''}change ${pressed ? 'without' : 'with'} key ${key}`, async function() { - const rejectedSpy = jasmine.createSpy('zoomRejected'); - const clickSpy = jasmine.createSpy('clicked'); - chart = window.acquireChart({ - type: 'line', - data, - options: { - scales: { - x: { - type: 'linear', - min: 0, - max: 10 - }, - y: { - type: 'linear' - } - }, - plugins: { - zoom: { - pan: { - enabled: true, - modifierKey: key, - }, - zoom: { - drag: { - enabled: true, - }, - mode: 'x', - onZoomRejected: rejectedSpy - } - } - }, - onClick: clickSpy - } - }); - - scaleX = chart.scales.x; - scaleY = chart.scales.y; - - const oldMinX = scaleX.options.min; - const oldMaxX = scaleX.options.max; - - const pt = { - x: scaleX.getPixelForValue(1.5), - y: scaleY.getPixelForValue(1.1), - }; - const pt2 = {x: pt.x + 20, y: pt.y + 20}; - const init = {}; - if (pressed) { - init[key + 'Key'] = true; - } - - jasmine.dispatchEvent(chart, 'mousedown', pt, init); - jasmine.dispatchEvent(chart, 'mousemove', pt2, init); - jasmine.dispatchEvent(chart, 'mouseup', pt2, init); - - if (pressed) { - expect(scaleX.options.min).toEqual(oldMinX); - expect(scaleX.options.max).toEqual(oldMaxX); - expect(rejectedSpy).toHaveBeenCalled(); - } else { - expect(scaleX.options.min).not.toEqual(oldMinX); - expect(scaleX.options.max).not.toEqual(oldMaxX); - expect(rejectedSpy).not.toHaveBeenCalled(); - } - expect(clickSpy).not.toHaveBeenCalled(); - }); - } - } - }); - - describe('drag with modifierKey', function() { - for (const key of ['ctrl', 'alt', 'shift', 'meta']) { - for (const pressed of [true, false]) { - let chart, scaleX, scaleY; - it(`should ${pressed ? '' : 'not '}change ${pressed ? 'with' : 'without'} key ${key}`, async function() { - const rejectedSpy = jasmine.createSpy('wheelFailed'); - chart = window.acquireChart({ - type: 'line', - data, - options: { - scales: { - x: { - type: 'linear', - min: 0, - max: 10 - }, - y: { - type: 'linear' - } - }, - plugins: { - zoom: { - zoom: { - drag: { - enabled: true, - modifierKey: key, - }, - mode: 'x', - onZoomRejected: rejectedSpy - } - } - } - } - }); - - scaleX = chart.scales.x; - scaleY = chart.scales.y; - - const oldMinX = scaleX.options.min; - const oldMaxX = scaleX.options.max; - - const pt = { - x: scaleX.getPixelForValue(1.5), - y: scaleY.getPixelForValue(1.1), - }; - const pt2 = {x: pt.x + 20, y: pt.y + 20}; - const init = {}; - if (pressed) { - init[key + 'Key'] = true; - } - - jasmine.dispatchEvent(chart, 'mousedown', pt, init); - jasmine.dispatchEvent(chart, 'mousemove', pt2, init); - jasmine.dispatchEvent(chart, 'mouseup', pt2, init); - - if (pressed) { - expect(scaleX.options.min).not.toEqual(oldMinX); - expect(scaleX.options.max).not.toEqual(oldMaxX); - expect(rejectedSpy).not.toHaveBeenCalled(); - } else { - expect(scaleX.options.min).toEqual(oldMinX); - expect(scaleX.options.max).toEqual(oldMaxX); - expect(rejectedSpy).toHaveBeenCalled(); - } - }); - } - } - }); - - describe('with overScaleMode = y and mode = xy', function() { - const config = { - type: 'line', - data, - options: { - scales: { - x: { - type: 'linear', - min: 1, - max: 10 - }, - y: { - type: 'linear' - } - }, - plugins: { - zoom: { - zoom: { - wheel: { - enabled: true, - }, - mode: 'xy', - overScaleMode: 'y' - } - } - } - } - }; - - describe('Wheel under Y scale', function() { - it('should be applied on Y, but not on X scales.', async function() { - const chart = window.acquireChart(config); - - const scaleX = chart.scales.x; - const scaleY = chart.scales.y; - - const oldMinX = scaleX.options.min; - const oldMaxX = scaleX.options.max; - const oldMinY = scaleY.options.min; - const oldMaxY = scaleY.options.max; - - const wheelEv = { - x: scaleY.left + (scaleY.right - scaleY.left) / 2, - y: scaleY.top + (scaleY.bottom - scaleY.top) / 2, - deltaY: 1 - }; - - await jasmine.triggerWheelEvent(chart, wheelEv); - - expect(scaleX.options.min).toEqual(oldMinX); - expect(scaleX.options.max).toEqual(oldMaxX); - expect(scaleY.options.min).not.toEqual(oldMinY); - expect(scaleY.options.max).not.toEqual(oldMaxY); - }); - }); - - describe('Wheel not under Y scale', function() { - it('should be applied on X, but not on Y scales.', async function() { - const chart = window.acquireChart(config); - - const scaleX = chart.scales.x; - const scaleY = chart.scales.y; - - const oldMinX = scaleX.options.min; - const oldMaxX = scaleX.options.max; - const oldMinY = scaleY.options.min; - const oldMaxY = scaleY.options.max; - - const wheelEv = { - x: scaleX.getPixelForValue(1.5), - y: scaleY.getPixelForValue(1.1), - deltaY: 1 - }; - - await jasmine.triggerWheelEvent(chart, wheelEv); - - expect(scaleX.options.min).not.toEqual(oldMinX); - expect(scaleX.options.max).not.toEqual(oldMaxX); - expect(scaleY.options.min).toEqual(oldMinY); - expect(scaleY.options.max).toEqual(oldMaxY); - }); - }); - }); - - describe('with scaleMode = y and mode = xy', function() { - const config = { - type: 'line', - data, - options: { - scales: { - x: { - type: 'linear', - min: 1, - max: 10 - }, - y: { - type: 'linear' - } - }, - plugins: { - zoom: { - zoom: { - wheel: { - enabled: true, - }, - mode: 'xy', - scaleMode: 'y' - } - } - } - } - }; - - describe('Wheel under Y scale', function() { - it('should be applied on Y, but not on X scales.', async function() { - const chart = window.acquireChart(config); - - const scaleX = chart.scales.x; - const scaleY = chart.scales.y; - - const oldMinX = scaleX.options.min; - const oldMaxX = scaleX.options.max; - const oldMinY = scaleY.options.min; - const oldMaxY = scaleY.options.max; - - const wheelEv = { - x: scaleY.left + (scaleY.right - scaleY.left) / 2, - y: scaleY.top + (scaleY.bottom - scaleY.top) / 2, - deltaY: 1 - }; - - await jasmine.triggerWheelEvent(chart, wheelEv); - - expect(scaleX.options.min).toEqual(oldMinX); - expect(scaleX.options.max).toEqual(oldMaxX); - expect(scaleY.options.min).not.toEqual(oldMinY); - expect(scaleY.options.max).not.toEqual(oldMaxY); - }); - }); - - describe('Wheel not under Y scale', function() { - it('should be applied on X and Y scales.', async function() { - const chart = window.acquireChart(config); - - const scaleX = chart.scales.x; - const scaleY = chart.scales.y; - - const oldMinX = scaleX.options.min; - const oldMaxX = scaleX.options.max; - const oldMinY = scaleY.options.min; - const oldMaxY = scaleY.options.max; - - const wheelEv = { - x: scaleX.getPixelForValue(1.5), - y: scaleY.getPixelForValue(1.1), - deltaY: 1 - }; - - await jasmine.triggerWheelEvent(chart, wheelEv); - - expect(scaleX.options.min).not.toEqual(oldMinX); - expect(scaleX.options.max).not.toEqual(oldMaxX); - expect(scaleY.options.min).not.toEqual(oldMinY); - expect(scaleY.options.max).not.toEqual(oldMaxY); - }); - }); - }); - - describe('events', function() { - describe('wheel', function() { - it('should call onZoomStart', function() { - const startSpy = jasmine.createSpy('started'); - const chart = window.acquireChart({ - type: 'scatter', - data, - options: { - plugins: { - zoom: { - zoom: { - wheel: { - enabled: true, - }, - mode: 'xy', - onZoomStart: startSpy - } - } - } - } - }); - const wheelEv = { - x: chart.scales.x.getPixelForValue(1.5), - y: chart.scales.y.getPixelForValue(1.1), - deltaY: 1 - }; - jasmine.triggerWheelEvent(chart, wheelEv); - expect(startSpy).toHaveBeenCalled(); - expect(chart.scales.x.min).not.toBe(1); - }); - - it('should detect configuration change', function() { - const startSpy = jasmine.createSpy('started'); - const chart = window.acquireChart({ - type: 'scatter', - data, - options: { - plugins: { - zoom: { - zoom: { - wheel: { - enabled: false, - }, - mode: 'xy', - onZoomStart: startSpy - } - } - } - } - }); - const wheelEv = { - x: chart.scales.x.getPixelForValue(1.5), - y: chart.scales.y.getPixelForValue(1.1), - deltaY: 1 - }; - jasmine.triggerWheelEvent(chart, wheelEv); - expect(startSpy).not.toHaveBeenCalled(); - expect(chart.scales.x.min).toBe(1); - - chart.options.plugins.zoom.zoom.wheel.enabled = true; - chart.update(); - - jasmine.triggerWheelEvent(chart, wheelEv); - expect(startSpy).toHaveBeenCalled(); - expect(chart.scales.x.min).not.toBe(1); - }); - - it('should call onZoomRejected when onZoomStart returns false', function() { - const rejectSpy = jasmine.createSpy('rejected'); - const chart = window.acquireChart({ - type: 'scatter', - data, - options: { - plugins: { - zoom: { - zoom: { - wheel: { - enabled: true, - }, - mode: 'xy', - onZoomStart: () => false, - onZoomRejected: rejectSpy - } - } - } - } - }); - const wheelEv = { - x: chart.scales.x.getPixelForValue(1.5), - y: chart.scales.y.getPixelForValue(1.1), - deltaY: 1 - }; - jasmine.triggerWheelEvent(chart, wheelEv); - expect(rejectSpy).toHaveBeenCalled(); - expect(chart.scales.x.min).toBe(1); - }); - - it('should call onZoomComplete', function(done) { - const chart = window.acquireChart({ - type: 'scatter', - data, - options: { - plugins: { - zoom: { - zoom: { - wheel: { - enabled: true, - }, - mode: 'xy', - onZoomComplete(ctx) { - expect(ctx.chart.scales.x.min).not.toBe(1); - done(); - } - } - } - } - } - }); - const wheelEv = { - x: chart.scales.x.getPixelForValue(1.5), - y: chart.scales.y.getPixelForValue(1.1), - deltaY: 1 - }; - jasmine.triggerWheelEvent(chart, wheelEv); - }); - }); - - describe('drag', function() { - it('should call onZoomStart, onZoom and onZoomComplete', function(done) { - const startSpy = jasmine.createSpy('start'); - const zoomSpy = jasmine.createSpy('zoom'); - const chart = window.acquireChart({ - type: 'scatter', - data, - options: { - plugins: { - zoom: { - zoom: { - drag: { - enabled: true, - }, - mode: 'xy', - onZoomStart: startSpy, - onZoom: zoomSpy, - onZoomComplete: () => done() - } - } - } - } - }); - - const pt = { - x: chart.scales.x.getPixelForValue(1.5), - y: chart.scales.y.getPixelForValue(1.1), - }; - const pt2 = {x: pt.x + 20, y: pt.y + 20}; - - expect(chart.isZoomingOrPanning()).toBe(false); - - jasmine.dispatchEvent(chart, 'mousedown', pt); - jasmine.dispatchEvent(chart, 'mousemove', pt2); - - expect(chart.isZoomingOrPanning()).toBe(true); - - jasmine.dispatchEvent(chart, 'mouseup', pt2); - - // Drag state isn't cleared until a timeout fires (later), so we can't - // easily test this here. - // expect(chart.isZoomingOrPanning()).toBe(false); - - expect(startSpy).toHaveBeenCalled(); - expect(zoomSpy).toHaveBeenCalled(); - }); - - it('should call onZoomRejected when onZoomStart returns false', function() { - const zoomSpy = jasmine.createSpy('zoom'); - const rejectSpy = jasmine.createSpy('reject'); - const doneSpy = jasmine.createSpy('done'); - const chart = window.acquireChart({ - type: 'scatter', - data, - options: { - plugins: { - zoom: { - zoom: { - drag: { - enabled: true, - }, - mode: 'xy', - onZoomStart: () => false, - onZoom: zoomSpy, - onZoomComplete: doneSpy, - onZoomRejected: rejectSpy - } - } - } - } - }); - - const pt = { - x: chart.scales.x.getPixelForValue(1.5), - y: chart.scales.y.getPixelForValue(1.1), - }; - const pt2 = {x: pt.x + 20, y: pt.y + 20}; - - expect(chart.isZoomingOrPanning()).toBe(false); - - jasmine.dispatchEvent(chart, 'mousedown', pt); - - expect(chart.isZoomingOrPanning()).toBe(false); - - jasmine.dispatchEvent(chart, 'mousemove', pt2); - jasmine.dispatchEvent(chart, 'mouseup', pt2); - - expect(chart.isZoomingOrPanning()).toBe(false); - - expect(rejectSpy).toHaveBeenCalled(); - expect(zoomSpy).not.toHaveBeenCalled(); - expect(doneSpy).not.toHaveBeenCalled(); - }); - }); - }); - - describe('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' - } - }, - plugins: { - zoom: { - zoom: { - wheel: { - enabled: true, - }, - } - } - } - } - }); - 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); - }); - }); -}); diff --git a/test/specs/zoom.wheel.spec.js b/test/specs/zoom.wheel.spec.js new file mode 100644 index 00000000..d0be2745 --- /dev/null +++ b/test/specs/zoom.wheel.spec.js @@ -0,0 +1,437 @@ +describe('zoom with wheel', function() { + const data = { + datasets: [{ + data: [{ + x: 1, + y: 3 + }, { + x: 2, + y: 2 + }, { + x: 3, + y: 1 + }] + }] + }; + + describe('with modifierKey', function() { + for (const key of ['ctrl', 'alt', 'shift', 'meta']) { + for (const pressed of [true, false]) { + let chart, scaleX, scaleY; + it(`should ${pressed ? '' : 'not '}change ${pressed ? 'with' : 'without'} key ${key}`, async function() { + const rejectedSpy = jasmine.createSpy('wheelFailed'); + chart = window.acquireChart({ + type: 'line', + data, + options: { + scales: { + x: { + type: 'linear', + min: 0, + max: 10 + }, + y: { + type: 'linear' + } + }, + plugins: { + zoom: { + zoom: { + wheel: { + enabled: true, + modifierKey: key, + }, + mode: 'x', + onZoomRejected: rejectedSpy + } + } + } + } + }); + + scaleX = chart.scales.x; + scaleY = chart.scales.y; + + const oldMinX = scaleX.options.min; + const oldMaxX = scaleX.options.max; + + const wheelEv = { + x: scaleX.getPixelForValue(1.5), + y: scaleY.getPixelForValue(1.1), + deltaY: 1 + }; + if (pressed) { + wheelEv[key + 'Key'] = true; + } + + jasmine.triggerWheelEvent(chart, wheelEv); + + if (pressed) { + expect(scaleX.options.min).not.toEqual(oldMinX); + expect(scaleX.options.max).not.toEqual(oldMaxX); + expect(rejectedSpy).not.toHaveBeenCalled(); + } else { + expect(scaleX.options.min).toEqual(oldMinX); + expect(scaleX.options.max).toEqual(oldMaxX); + expect(rejectedSpy).toHaveBeenCalled(); + } + }); + } + } + }); + + describe('with overScaleMode = y and mode = xy', function() { + const config = { + type: 'line', + data, + options: { + scales: { + x: { + type: 'linear', + min: 1, + max: 10 + }, + y: { + type: 'linear' + } + }, + plugins: { + zoom: { + zoom: { + wheel: { + enabled: true, + }, + mode: 'xy', + overScaleMode: 'y' + } + } + } + } + }; + + describe('Wheel under Y scale', function() { + it('should be applied on Y, but not on X scales.', function() { + const chart = window.acquireChart(config); + + const scaleX = chart.scales.x; + const scaleY = chart.scales.y; + + const oldMinX = scaleX.options.min; + const oldMaxX = scaleX.options.max; + const oldMinY = scaleY.options.min; + const oldMaxY = scaleY.options.max; + + const wheelEv = { + x: scaleY.left + (scaleY.right - scaleY.left) / 2, + y: scaleY.top + (scaleY.bottom - scaleY.top) / 2, + deltaY: 1 + }; + + jasmine.triggerWheelEvent(chart, wheelEv); + + expect(scaleX.options.min).toEqual(oldMinX); + expect(scaleX.options.max).toEqual(oldMaxX); + expect(scaleY.options.min).not.toEqual(oldMinY); + expect(scaleY.options.max).not.toEqual(oldMaxY); + }); + }); + + describe('Wheel not under Y scale', function() { + it('should be applied on X, but not on Y scales.', function() { + const chart = window.acquireChart(config); + + const scaleX = chart.scales.x; + const scaleY = chart.scales.y; + + const oldMinX = scaleX.options.min; + const oldMaxX = scaleX.options.max; + const oldMinY = scaleY.options.min; + const oldMaxY = scaleY.options.max; + + const wheelEv = { + x: scaleX.getPixelForValue(1.5), + y: scaleY.getPixelForValue(1.1), + deltaY: 1 + }; + + jasmine.triggerWheelEvent(chart, wheelEv); + + expect(scaleX.options.min).not.toEqual(oldMinX); + expect(scaleX.options.max).not.toEqual(oldMaxX); + expect(scaleY.options.min).toEqual(oldMinY); + expect(scaleY.options.max).toEqual(oldMaxY); + }); + }); + }); + + describe('with scaleMode = y and mode = xy', function() { + const config = { + type: 'line', + data, + options: { + scales: { + x: { + type: 'linear', + min: 1, + max: 10 + }, + y: { + type: 'linear' + } + }, + plugins: { + zoom: { + zoom: { + wheel: { + enabled: true, + }, + mode: 'xy', + scaleMode: 'y' + } + } + } + } + }; + + describe('Wheel under Y scale', function() { + it('should be applied on Y, but not on X scales.', function() { + const chart = window.acquireChart(config); + + const scaleX = chart.scales.x; + const scaleY = chart.scales.y; + + const oldMinX = scaleX.options.min; + const oldMaxX = scaleX.options.max; + const oldMinY = scaleY.options.min; + const oldMaxY = scaleY.options.max; + + const wheelEv = { + x: scaleY.left + (scaleY.right - scaleY.left) / 2, + y: scaleY.top + (scaleY.bottom - scaleY.top) / 2, + deltaY: 1 + }; + + jasmine.triggerWheelEvent(chart, wheelEv); + + expect(scaleX.options.min).toEqual(oldMinX); + expect(scaleX.options.max).toEqual(oldMaxX); + expect(scaleY.options.min).not.toEqual(oldMinY); + expect(scaleY.options.max).not.toEqual(oldMaxY); + }); + }); + + describe('Wheel not under Y scale', function() { + it('should be applied on X and Y scales.', function() { + const chart = window.acquireChart(config); + + const scaleX = chart.scales.x; + const scaleY = chart.scales.y; + + const oldMinX = scaleX.options.min; + const oldMaxX = scaleX.options.max; + const oldMinY = scaleY.options.min; + const oldMaxY = scaleY.options.max; + + const wheelEv = { + x: scaleX.getPixelForValue(1.5), + y: scaleY.getPixelForValue(1.1), + deltaY: 1 + }; + + jasmine.triggerWheelEvent(chart, wheelEv); + + expect(scaleX.options.min).not.toEqual(oldMinX); + expect(scaleX.options.max).not.toEqual(oldMaxX); + expect(scaleY.options.min).not.toEqual(oldMinY); + expect(scaleY.options.max).not.toEqual(oldMaxY); + }); + }); + }); + + 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'); + const chart = window.acquireChart({ + type: 'scatter', + data, + options: { + plugins: { + zoom: { + zoom: { + wheel: { + enabled: true, + }, + mode: 'xy', + onZoomStart: startSpy + } + } + } + } + }); + const wheelEv = { + x: chart.scales.x.getPixelForValue(1.5), + y: chart.scales.y.getPixelForValue(1.1), + deltaY: 1 + }; + jasmine.triggerWheelEvent(chart, wheelEv); + expect(startSpy).toHaveBeenCalled(); + expect(chart.scales.x.min).not.toBe(1); + }); + + it('should detect configuration change', function() { + const startSpy = jasmine.createSpy('started'); + const chart = window.acquireChart({ + type: 'scatter', + data, + options: { + plugins: { + zoom: { + zoom: { + wheel: { + enabled: false, + }, + mode: 'xy', + onZoomStart: startSpy + } + } + } + } + }); + const wheelEv = { + x: chart.scales.x.getPixelForValue(1.5), + y: chart.scales.y.getPixelForValue(1.1), + deltaY: 1 + }; + jasmine.triggerWheelEvent(chart, wheelEv); + expect(startSpy).not.toHaveBeenCalled(); + expect(chart.scales.x.min).toBe(1); + + chart.options.plugins.zoom.zoom.wheel.enabled = true; + chart.update(); + + jasmine.triggerWheelEvent(chart, wheelEv); + expect(startSpy).toHaveBeenCalled(); + expect(chart.scales.x.min).not.toBe(1); + }); + + it('should call onZoomRejected when onZoomStart returns false', function() { + const rejectSpy = jasmine.createSpy('rejected'); + const chart = window.acquireChart({ + type: 'scatter', + data, + options: { + plugins: { + zoom: { + zoom: { + wheel: { + enabled: true, + }, + mode: 'xy', + onZoomStart: () => false, + onZoomRejected: rejectSpy + } + } + } + } + }); + const wheelEv = { + x: chart.scales.x.getPixelForValue(1.5), + y: chart.scales.y.getPixelForValue(1.1), + deltaY: 1 + }; + jasmine.triggerWheelEvent(chart, wheelEv); + expect(rejectSpy).toHaveBeenCalled(); + expect(chart.scales.x.min).toBe(1); + }); + + it('should call onZoomComplete', function(done) { + const chart = window.acquireChart({ + type: 'scatter', + data, + options: { + plugins: { + zoom: { + zoom: { + wheel: { + enabled: true, + }, + mode: 'xy', + onZoomComplete(ctx) { + expect(ctx.chart.scales.x.min).not.toBe(1); + done(); + } + } + } + } + } + }); + const wheelEv = { + x: chart.scales.x.getPixelForValue(1.5), + y: chart.scales.y.getPixelForValue(1.1), + deltaY: 1 + }; + jasmine.triggerWheelEvent(chart, wheelEv); + }); + }); +});