Skip to content

Commit

Permalink
fix(zoom): fix wheel zoom feeling "stuck" when panning past the edge
Browse files Browse the repository at this point in the history
Adjust transfrom property '__zoom' to mitigate panning stuck
during wheel drag move

Fix #2588
Close #2648
  • Loading branch information
ebencollins authored and netil committed Apr 28, 2022
1 parent 6a73754 commit 50ed640
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 4 deletions.
21 changes: 17 additions & 4 deletions src/ChartInternal/interactions/zoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* billboard.js project is licensed under the MIT license
*/
import {drag as d3Drag} from "d3-drag";
import {zoomIdentity as d3ZoomIdentity, zoom as d3Zoom} from "d3-zoom";
import {zoomIdentity as d3ZoomIdentity, zoom as d3Zoom, ZoomTransform as d3ZoomTransform} from "d3-zoom";
import {$COMMON, $ZOOM} from "../../config/classes";
import {callFn, diffDomain, getPointer, isFunction} from "../../module/util";

Expand Down Expand Up @@ -75,23 +75,36 @@ export default {
/**
* Update scale according zoom transform value
* @param {object} transform transform object
* @param {boolean} correctTransform if the d3 transform should be updated after rescaling
* @private
*/
// @ts-ignore
zoom.updateTransformScale = (transform: object): void => {
zoom.updateTransformScale = (transform: d3ZoomTransform, correctTransform: boolean): void => {
const isRotated = config.axis_rotated;

// in case of resize, update range of orgXScale
org.xScale?.range(scale.x.range());

// rescale from the original scale
const newScale = transform[
config.axis_rotated ? "rescaleY" : "rescaleX"
isRotated ? "rescaleY" : "rescaleX"
](org.xScale || scale.x);

const domain = $$.trimXDomain(newScale.domain());
const rescale = config.zoom_rescale;

newScale.domain(domain, org.xDomain);

// prevent chart from panning off the edge and feeling "stuck"
// https://github.com/naver/billboard.js/issues/2588
if (correctTransform) {
const t = newScale(scale.x.domain()[0]);
const tX = isRotated ? transform.x : t;
const tY = isRotated ? t : transform.y;

$$.$el.eventRect.property("__zoom", d3ZoomIdentity.translate(tX, tY).scale(transform.k));
}

if (!$$.state.xTickOffset) {
$$.state.xTickOffset = $$.axis.x.tickOffset();
}
Expand Down Expand Up @@ -172,7 +185,7 @@ export default {
scale.x.domain(org.xDomain);
}

$$.zoom.updateTransformScale(transform);
$$.zoom.updateTransformScale(transform, config.zoom_type === "wheel" && sourceEvent);

// do zoom transiton when:
// - zoom type 'drag'
Expand Down
98 changes: 98 additions & 0 deletions test/interactions/zoom-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
/* eslint-disable */
/* global describe, beforeEach, it, expect */
import {expect} from "chai";
import {zoomTransform as d3ZoomTransform} from "d3-zoom";
import sinon from "sinon";
import {$AXIS, $EVENT, $GRID, $REGION, $ZOOM} from "../../src/config/classes";
import util from "../assets/util";
Expand Down Expand Up @@ -117,6 +118,8 @@ describe("ZOOM", function() {
const {$el: {eventRect}} = chart.internal;
const rect = eventRect.node();

// must set initial zoom level or the following pans will fail
chart.zoom([0, 3]);
new Promise((resolve, reject) => {
util.fireEvent(rect, "mousedown", {
clientX: 100,
Expand Down Expand Up @@ -420,6 +423,101 @@ describe("ZOOM", function() {
})
});

describe("wheel zoom doesn't stick", () => {
before(() => {
args = {
size: {
width: 300,
height: 250
},
data: {
columns: [
["data1", 30, 200, 100, 400, 3150, 250],
["data2", 50, 20, 10, 40, 15, 6025]
],
},
zoom: {
enabled: true
}
};
});

function drag(down, move, up) {
const eventRect = chart.internal.$el.eventRect.node();
return new Promise((resolve) => {
util.fireEvent(eventRect, "mousedown", {
clientX: down.x,
clientY: down.y
}, chart);
resolve(true);
}).then(() => {
return new Promise((resolve) => {
setTimeout(() => {
util.fireEvent(eventRect, "mousemove", {
clientX: move.x,
clientY: move.y
}, chart);

resolve(true);
}, 300);
})
}).then(() => {
return new Promise((resolve) => {
setTimeout(() => {
util.fireEvent(eventRect, "mouseup", {
clientX: up.x,
clientY: up.y
}, chart);
resolve(true);
}, 300);
});
})
}

it("check doesn't stick left", (done) => {
const {internal: {$el}} = chart;
const eventRect = $el.eventRect.node();

chart.zoom([0, 2]);
drag({x: 150, y: 150}, {x: 2000, y: 120}, {x: 2000, y: 120}).then(() => {
expect(d3ZoomTransform(eventRect).x).to.approximately(0, 0.01);
expect(chart.zoom()[0]).to.approximately(0, 0.1);
drag({x: 150, y: 150}, {x: 0, y: 130}, {x: 0, y: 130}).then(() => {
expect(d3ZoomTransform(eventRect).x).to.approximately(-150, 0.01);
expect(chart.zoom()[0]).to.greaterThan(0);
done();
});
});
});

it("check doesn't stick right", (done) => {
chart.zoom([4, 5]);
drag({x: 150, y: 150}, {x: -2000, y: 120}, {x: -2000, y: 120}).then(() => {
expect(chart.zoom()[1]).to.greaterThan(5);
drag({x: 150, y: 150}, {x: 300, y: 130}, {x: 300, y: 130}).then(() => {
expect(chart.zoom()[1]).to.lessThan(5);
done();
});
});
});

it("set rotated", () => {
args.axis = {rotated: true};
});

it("check doesn't stick rotated", (done) => {
chart.zoom([0, 3]);
drag({x: 150, y: 150}, {x: 150, y: 2000}, {x: 150, y: 2000}).then(() => {
expect(chart.zoom()[0]).to.approximately(0, 0.1);
drag({x: 150, y: 150}, {x: 150, y: 0}, {x: 150, y: 0}).then(() => {
expect(chart.zoom()[0]).to.greaterThan(0);
done();
});
});
})
});


describe("zoom type drag", () => {
const spy = sinon.spy();
let clickedData;
Expand Down

0 comments on commit 50ed640

Please sign in to comment.