Skip to content

Commit

Permalink
fix(zoom): fix inconsistent zoom on .zoom() -> wheel
Browse files Browse the repository at this point in the history
Refactor zoom transform and its releated codes.

Fix #2194
  • Loading branch information
netil committed Jul 27, 2021
1 parent 083f5c7 commit 4eee2d8
Show file tree
Hide file tree
Showing 10 changed files with 269 additions and 167 deletions.
77 changes: 39 additions & 38 deletions src/Chart/api/zoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@
* billboard.js project is licensed under the MIT license
*/
import {zoomIdentity as d3ZoomIdentity, zoomTransform as d3ZoomTransform} from "d3-zoom";
import CLASS from "../../config/classes";
import {callFn, extend, getMinMax, isDefined, isObject, parseDate} from "../../module/util";

/**
* Check if the given domain is within zoom range
* @param {Array} domain domain value
* @param {Array} range zoom range value
* @param {Array} domain Target domain value
* @param {Array} current Current zoom domain value
* @param {Array} range Zoom range value
* @returns {boolean}
* @private
*/
function withinRange(domain: (number | string)[], range: number[]): boolean {
function withinRange(domain: (number|Date)[], current, range: number[]): boolean {
const [min, max] = range;

return domain.every((v, i) => (
i === 0 ? (v >= min) : (v <= max)
));
)) && !(domain[0] === current[0] && domain[1] === current[1]);
}

/**
Expand All @@ -38,49 +38,59 @@ function withinRange(domain: (number | string)[], range: number[]): boolean {
* // Get the current zoomed domain range
* chart.zoom();
*/
const zoom = function(domainValue?: (number | string)[]): (Date | number)[] {
const zoom = function(domainValue?: (number|Date)[]): (number|Date)[]|undefined {
const $$ = this.internal;
const {config, scale} = $$;
const {$el, config, org, scale} = $$;
const isRotated = config.axis_rotated;
const isCategorized = $$.axis.isCategorized();
let domain = domainValue;
let resultDomain;

if (config.zoom_enabled && domain) {
if ($$.axis.isTimeSeries()) {
domain = domain.map(x => parseDate.bind($$)(x));
}

if (withinRange(domain, $$.getZoomDomain())) {
if (withinRange(domain, $$.getZoomDomain(true), $$.getZoomDomain())) {
if (isCategorized) {
domain = domain.map((v, i) => Number(v) + (i === 0 ? 0 : 1));
}

// hide any possible tooltip show before the zoom
$$.api.tooltip.hide();

if (config.subchart_show) {
const xScale = scale.zoom || scale.x;
const x = scale.zoom || scale.x;

$$.brush.getSelection().call($$.brush.move, [xScale(domain[0]), xScale(domain[1])]);
resultDomain = domain;
$$.brush.getSelection().call($$.brush.move, [x(domain[0]), x(domain[1])]);
// resultDomain = domain;
} else {
scale.x.domain(domain);
scale.zoom = scale.x;
$$.axis.x.scale(scale.zoom);

resultDomain = scale.zoom.orgDomain();
// in case of 'config.zoom_rescale=true', use org.xScale
const x = isCategorized ? scale.x.orgScale() : (org.xScale || scale.x);

// Get transform from given domain value
// https://github.com/d3/d3-zoom/issues/57#issuecomment-246434951
const translate = [-x(domain[0]), 0];
const transform = d3ZoomIdentity
.scale(x.range()[1] / (
x(domain[1]) - x(domain[0])
))
.translate(
...(isRotated ? translate.reverse() : translate) as [number, number]
);

$$.$T($el.eventRect)
.call($$.zoom.transform, transform);
}

$$.redraw({
withTransition: true,
withY: config.zoom_rescale,
withDimension: false
});

$$.setZoomResetButton();
callFn(config.zoom_onzoom, $$.api, resultDomain);
callFn(config.zoom_onzoom, $$.api, domain);
}
} else {
resultDomain = scale.zoom ?
domain = scale.zoom ?
scale.zoom.domain() : scale.x.orgDomain();
}

return resultDomain;
return domain;
};

extend(zoom, {
Expand Down Expand Up @@ -213,7 +223,7 @@ export default {
*/
unzoom(): void {
const $$ = this.internal;
const {config} = $$;
const {config, $el: {eventRect}} = $$;

if ($$.scale.zoom) {
config.subchart_show ?
Expand All @@ -224,18 +234,9 @@ export default {
$$.zoom.resetBtn && $$.zoom.resetBtn.style("display", "none");

// reset transform
const eventRects = $$.$el.main.select(`.${CLASS.eventRects}`);

if (d3ZoomTransform(eventRects.node()) !== d3ZoomIdentity) {
$$.zoom.transform(eventRects, d3ZoomIdentity);
if (d3ZoomTransform(eventRect.node()) !== d3ZoomIdentity) {
$$.zoom.transform($$.$T(eventRect), d3ZoomIdentity);
}

$$.redraw({
withTransition: true,
withUpdateXDomain: true,
withUpdateOrgXDomain: true,
withY: config.zoom_rescale
});
}
}
};
18 changes: 18 additions & 0 deletions src/ChartInternal/ChartInternal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
utcFormat as d3UtcFormat
} from "d3-time-format";
import {select as d3Select} from "d3-selection";
import {d3Selection} from "../../types/types";
import CLASS from "../config/classes";
import Store from "../config/Store/Store";
import Options from "../config/Options/Options";
Expand Down Expand Up @@ -472,6 +473,23 @@ export default class ChartInternal {
notEmpty($$.config.data_labels) && !$$.hasArcType(null, ["radar"]) && $$.initText();
}

/**
* Get selection based on transition config
* @param {d3Selection} selection Target selection
* @param {string} name Transition name
* @returns {d3Selection}
* @private
*/
$T(selection: d3Selection, name: string): d3Selection {
const duration = this.config.transition_duration;

return (
duration ?
selection.transition(name).duration(duration) :
selection
) as d3Selection;
}

setChartElements(): void {
const $$ = this;
const {$el: {
Expand Down
78 changes: 36 additions & 42 deletions src/ChartInternal/interactions/zoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
*/
import {drag as d3Drag} from "d3-drag";
import {zoom as d3Zoom} from "d3-zoom";
import {d3Selection} from "../../../types/types";
import CLASS from "../../config/classes";
import {callFn, diffDomain, getMinMax, getPointer, isDefined, isFunction} from "../../module/util";
import {callFn, diffDomain, getPointer, isFunction} from "../../module/util";

export default {
/**
Expand All @@ -28,18 +29,17 @@ export default {
*/
bindZoomEvent(bind = true): void {
const $$ = this;
const {config, $el: {main}} = $$;
const {config, $el: {eventRect}} = $$;
const zoomEnabled = config.zoom_enabled;
const eventRects = main.select(`.${CLASS.eventRects}`);

if (zoomEnabled && bind) {
// Do not bind zoom event when subchart is shown
!config.subchart_show &&
$$.bindZoomOnEventRect(eventRects, config.zoom_type);
$$.bindZoomOnEventRect(eventRect, config.zoom_type);
} else if (bind === false) {
$$.api.unzoom();

eventRects
eventRect
.on(".zoom", null)
.on(".drag", null);
}
Expand Down Expand Up @@ -96,6 +96,10 @@ export default {

newScale.domain(domain, org.xDomain);

if (!$$.state.xTickOffset) {
$$.state.xTickOffset = $$.axis.x.tickOffset();
}

scale.zoom = $$.getCustomizedScale(newScale);
$$.axis.x.scale(scale.zoom);

Expand All @@ -106,6 +110,22 @@ export default {
}
};

/**
* Get zoom domain
* @returns {Array} zoom domain
* @private
*/
// @ts-ignore
zoom.getDomain = (): number|Date[] => {
const domain = scale[scale.zoom ? "zoom" : "subX"].domain();
const isCategorized = $$.axis.isCategorized();

if (isCategorized) {
domain[1] -= 2;
}

return domain;
};
$$.zoom = zoom;
},

Expand Down Expand Up @@ -139,15 +159,14 @@ export default {

if (
!config.zoom_enabled ||
!event.sourceEvent ||
$$.filterTargetsToShow($$.data.targets).length === 0 ||
(!scale.zoom && sourceEvent.type.indexOf("touch") > -1 && sourceEvent.touches.length === 1)
(!scale.zoom && sourceEvent?.type.indexOf("touch") > -1 && sourceEvent?.touches.length === 1)
) {
return;
}

const isMousemove = sourceEvent.type === "mousemove";
const isZoomOut = sourceEvent.wheelDelta < 0;
const isMousemove = sourceEvent?.type === "mousemove";
const isZoomOut = sourceEvent?.wheelDelta < 0;
const {transform} = event;

if (!isMousemove && isZoomOut && scale.x.domain().every((v, i) => v !== org.xDomain[i])) {
Expand All @@ -156,9 +175,6 @@ export default {

$$.zoom.updateTransformScale(transform);

if ($$.axis.isCategorized() && scale.x.orgDomain()[0] === org.xDomain[0]) {
scale.x.domain([org.xDomain[0] - 1e-10, scale.x.orgDomain()[1]]);
}

$$.redraw({
withTransition: false,
Expand All @@ -169,7 +185,7 @@ export default {
});

$$.state.cancelClick = isMousemove;
callFn(config.zoom_onzoom, $$.api, scale.zoom.domain());
callFn(config.zoom_onzoom, $$.api, $$.zoom.getDomain());
},

/**
Expand All @@ -179,9 +195,9 @@ export default {
*/
onZoomEnd(event): void {
const $$ = this;
const {config, scale} = $$;
const {config} = $$;
let {startEvent} = $$.zoom;
let e = event && event.sourceEvent;
let e = event?.sourceEvent;

if ((startEvent && startEvent.type.indexOf("touch") > -1)) {
startEvent = startEvent.changedTouches[0];
Expand All @@ -190,8 +206,7 @@ export default {

// if click, do nothing. otherwise, click interaction will be canceled.
if (config.zoom_type === "drag" && (
!startEvent ||
(e && startEvent.clientX === e.clientX && startEvent.clientY === e.clientY)
e && startEvent.clientX === e.clientX && startEvent.clientY === e.clientY
)) {
return;
}
Expand All @@ -200,28 +215,7 @@ export default {
$$.updateZoom();

$$.state.zooming = false;
callFn(config.zoom_onzoomend, $$.api, scale[scale.zoom ? "zoom" : "subX"].domain());
},

/**
* Get zoom domain
* @returns {Array} zoom domain
* @private
*/
getZoomDomain(): [number, number] {
const $$ = this;
const {config, org} = $$;
let [min, max] = org.xDomain;

if (isDefined(config.zoom_x_min)) {
min = getMinMax("min", [min, config.zoom_x_min]);
}

if (isDefined(config.zoom_x_max)) {
max = getMinMax("max", [max, config.zoom_x_max]);
}

return [min, max];
callFn(config.zoom_onzoomend, $$.api, $$.zoom.getDomain());
},

/**
Expand Down Expand Up @@ -252,19 +246,19 @@ export default {

/**
* Attach zoom event on <rect>
* @param {d3.selection} eventRects evemt <rect> element
* @param {d3.selection} eventRect evemt <rect> element
* @param {string} type zoom type
* @private
*/
bindZoomOnEventRect(eventRects, type: "drag" | "wheel"): void {
bindZoomOnEventRect(eventRect: d3Selection, type: "drag" | "wheel"): void {
const $$ = this;
const behaviour = type === "drag" ? $$.zoomBehaviour : $$.zoom;

// Since Chrome 89, wheel zoom not works properly
// Applying the workaround: https://github.com/d3/d3-zoom/issues/231#issuecomment-802305692
$$.$el.svg.on("wheel", () => {});

eventRects
eventRect
.call(behaviour)
.on("dblclick.zoom", null);
},
Expand Down
2 changes: 1 addition & 1 deletion src/ChartInternal/internals/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ export default {
* @returns {Array} zoom domain
* @private
*/
getZoomDomain(): [number, number] {
getZoomDomain(): [number|Date, number|Date] {
const $$ = this;
const {config, org} = $$;
let [min, max] = org.xDomain;
Expand Down
2 changes: 1 addition & 1 deletion src/ChartInternal/shape/line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export default {
const regions = config.data_regions[d.id];

if (regions) {
path = $$.lineWithRegions(values, x, y, regions);
path = $$.lineWithRegions(values, scale.zoom || x, y, regions);
} else {
if ($$.isStepType(d)) {
values = $$.convertValuesToStep(values);
Expand Down
Loading

0 comments on commit 4eee2d8

Please sign in to comment.