Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(zoom): fix inconsistent zoom on .zoom() -> wheel #2217

Merged
merged 1 commit into from
Jul 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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