diff --git a/src/Chart/api/subchart.ts b/src/Chart/api/subchart.ts index 98da2998d..ac4a1a1eb 100644 --- a/src/Chart/api/subchart.ts +++ b/src/Chart/api/subchart.ts @@ -8,6 +8,8 @@ import type {TDomain} from "../../ChartInternal/data/IData"; /** * Select subchart by giving x domain range. + * - **ℹ️ NOTE:** + * - Due to the limitations of floating point precision, domain value may not be exact returning approximately values. * @function subchart * @instance * @memberof Chart @@ -18,31 +20,40 @@ import type {TDomain} from "../../ChartInternal/data/IData"; * chart.subchart([1, 2]); * * // Get the current subchart selection domain range + * // Domain value may not be exact returning approximately values. * chart.subchart(); */ // NOTE: declared funciton assigning to variable to prevent duplicated method generation in JSDoc. const subchart = function(domainValue?: T): T | undefined { const $$ = this.internal; - const {axis, brush, config, scale: {x, subX}} = $$; - let domain: any = domainValue; + const {axis, brush, config, scale: {x, subX}, state} = $$; + let domain; - if (config.subchart_show && Array.isArray(domain)) { - if (axis.isTimeSeries()) { - domain = domain.map(x => parseDate.bind($$)(x)); - } + if (config.subchart_show) { + domain = domainValue; + + if (Array.isArray(domain)) { + if (axis.isTimeSeries()) { + domain = domain.map(x => parseDate.bind($$)(x)); + } + + const isWithinRange = $$.withinRange( + domain, + $$.getZoomDomain("subX", true), + $$.getZoomDomain("subX") + ); - const isWithinRange = $$.withinRange( - domain, - $$.getZoomDomain("subX", true), - $$.getZoomDomain("subX") - ); - - isWithinRange && brush.move( - brush.getSelection(), - domain.map(subX) - ); - } else { - domain = x.orgDomain(); + if (isWithinRange) { + state.domain = domain; + + brush.move( + brush.getSelection(), + domain.map(subX) + ); + } + } else { + domain = state.domain ?? x.orgDomain(); + } } return domain as T; diff --git a/src/Chart/api/zoom.ts b/src/Chart/api/zoom.ts index 4654e6bb5..2fc7e7bc7 100644 --- a/src/Chart/api/zoom.ts +++ b/src/Chart/api/zoom.ts @@ -8,9 +8,11 @@ import type {TDomainRange} from "../../ChartInternal/data/IData"; /** * Zoom by giving x domain range. - * - **NOTE:** + * - **ℹ️ NOTE:** * - For `wheel` type zoom, the minimum zoom range will be set as the given domain range. To get the initial state, [.unzoom()](#unzoom) should be called. * - To be used [zoom.enabled](Options.html#.zoom) option should be set as `truthy`. + * - When x axis type is `category`, domain range should be specified as index numbers. + * - Due to the limitations of floating point precision, domain value may not be exact returning approximately values. * @function zoom * @instance * @memberof Chart @@ -20,70 +22,80 @@ import type {TDomainRange} from "../../ChartInternal/data/IData"; * // Zoom to specified domain range * chart.zoom([10, 20]); * - * // For timeseries, the domain value can be string, but the format should match with the 'data.xFormat' option. + * // For timeseries x axis, the domain value can be string, but the format should match with the 'data.xFormat' option. * chart.zoom(["2021-02-03", "2021-02-08"]); * + * // For category x axis, the domain value should be index number. + * chart.zoom([0, 3]); + * * // Get the current zoomed domain range + * // Domain value may not be exact returning approximately values. * chart.zoom(); */ // NOTE: declared funciton assigning to variable to prevent duplicated method generation in JSDoc. const zoom = function(domainValue?: T): T | undefined { const $$ = this.internal; - const {$el, axis, config, org, scale} = $$; + const {$el, axis, config, org, scale, state} = $$; const isRotated = config.axis_rotated; const isCategorized = axis.isCategorized(); - let domain: any = domainValue; - - if (config.zoom_enabled && Array.isArray(domain)) { - if (axis.isTimeSeries()) { - domain = domain.map(x => parseDate.bind($$)(x)); - } + let domain; - const isWithinRange = $$.withinRange( - domain, - $$.getZoomDomain("zoom", true), - $$.getZoomDomain("zoom") - ); + if (config.zoom_enabled) { + domain = domainValue; - if (isWithinRange) { - if (isCategorized) { - domain = domain.map((v, i) => Number(v) + (i === 0 ? 0 : 1)) as T; + if (Array.isArray(domain)) { + if (axis.isTimeSeries()) { + domain = domain.map(x => parseDate.bind($$)(x)); } - // hide any possible tooltip show before the zoom - $$.api.tooltip.hide(); - - if (config.subchart_show) { - const x = scale.zoom || scale.x; - - $$.brush.getSelection().call($$.brush.move, domain.map(x)); - // resultDomain = domain; - } else { - // 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] - ); - - $el.eventRect - .call($$.zoom.transform, transform); + const isWithinRange = $$.withinRange( + domain, + $$.getZoomDomain("zoom", true), + $$.getZoomDomain("zoom") + ); + + if (isWithinRange) { + state.domain = domain; + + 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 x = scale.zoom || scale.x; + + $$.brush.getSelection().call($$.brush.move, domain.map(x)); + // resultDomain = domain; + } else { + // 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] + ); + + $el.eventRect + .call($$.zoom.transform, transform); + } + + $$.setZoomResetButton(); } - - $$.setZoomResetButton(); + } else { + domain = $$.zoom.getDomain(); } - } else { - domain = scale.zoom?.domain() ?? scale.x.orgDomain(); } - return domain; + return state.domain ?? domain; }; extend(zoom, { @@ -217,7 +229,7 @@ export default { */ unzoom(): void { const $$ = this.internal; - const {config, $el: {eventRect, zoomResetBtn}} = $$; + const {config, $el: {eventRect, zoomResetBtn}, state} = $$; if ($$.scale.zoom) { config.subchart_show ? @@ -231,6 +243,8 @@ export default { if (d3ZoomTransform(eventRect.node()) !== d3ZoomIdentity) { $$.zoom.transform(eventRect, d3ZoomIdentity); } + + state.domain = undefined; } } }; diff --git a/src/ChartInternal/interactions/subchart.ts b/src/ChartInternal/interactions/subchart.ts index 0fdea283f..3f6dc6e16 100644 --- a/src/ChartInternal/interactions/subchart.ts +++ b/src/ChartInternal/interactions/subchart.ts @@ -18,9 +18,10 @@ export default { */ initBrush(): void { const $$ = this; - const {config, scale, $el: {subchart}} = $$; + const {config, scale, $el: {subchart}, state} = $$; const isRotated = config.axis_rotated; let lastDomain; + let lastSelection; let timeout; // set the brush @@ -42,14 +43,25 @@ export default { // bind brush event $$.brush.on("start brush end", event => { - const {selection, target, type} = event; + const {selection, sourceEvent, target, type} = event; if (type === "start") { $$.state.inputType === "touch" && $$.hideTooltip(); + lastSelection = sourceEvent ? selection : null; + // sourceEvent && (state.domain = null); } + // if (type === "brush") { if (/(start|brush)/.test(type)) { - $$.redrawForBrush(); + // when brush selection updates happens on one edge, update only chainging edge and + // is only for adjustment of given domain range to be used to return current domain range. + type === "brush" && sourceEvent && state.domain && lastSelection?.forEach((v, i) => { + if (v !== selection[i]) { + state.domain[i] = scale.x.orgDomain()[i]; + } + }); + + $$.redrawForBrush(type !== "start"); } if (type === "end") { @@ -320,23 +332,28 @@ export default { $$.redrawCircle(cx, cy, withTransition, undefined, true); } - !state.rendered && initRange && $$.brush.move( - $$.brush.getSelection(), - initRange.map($$.scale.x) - ); + if (!state.rendered && initRange) { + state.domain = initRange; + + $$.brush.move( + $$.brush.getSelection(), + initRange.map($$.scale.x) + ); + } } } }, /** * Redraw the brush. + * @param {boolean} [callCallbck=true] Call 'onbrush' callback or not. * @private */ - redrawForBrush() { + redrawForBrush(callCallbck = true): void { const $$ = this; const {config: { subchart_onbrush: onBrush, zoom_rescale: withY - }, scale} = $$; + }, scale, state} = $$; $$.redraw({ withTransition: false, @@ -346,7 +363,8 @@ export default { withDimension: false }); - onBrush.bind($$.api)(scale.x.orgDomain()); + callCallbck && state.rendered && + onBrush.bind($$.api)(state.domain ?? scale.x.orgDomain()); }, /** diff --git a/src/ChartInternal/interactions/zoom.ts b/src/ChartInternal/interactions/zoom.ts index f38ed6990..04de57b17 100644 --- a/src/ChartInternal/interactions/zoom.ts +++ b/src/ChartInternal/interactions/zoom.ts @@ -125,7 +125,7 @@ export default { * @private */ // @ts-ignore - zoom.getDomain = (): number|Date[] => { + zoom.getDomain = (): (number | Date)[] => { const domain = scale[scale.zoom ? "zoom" : "subX"].domain(); const isCategorized = $$.axis.isCategorized(); @@ -135,6 +135,7 @@ export default { return domain; }; + $$.zoom = zoom; }, @@ -175,6 +176,7 @@ export default { if (event.sourceEvent) { state.zooming = true; + state.domain = undefined; } const isMousemove = sourceEvent?.type === "mousemove"; @@ -206,7 +208,11 @@ export default { $$.state.cancelClick = isMousemove; // do not call event cb when is .unzoom() is called - !isUnZoom && callFn(config.zoom_onzoom, $$.api, $$.zoom.getDomain()); + !isUnZoom && callFn( + config.zoom_onzoom, + $$.api, + $$.state.domain ?? $$.zoom.getDomain() + ); }, /** @@ -239,7 +245,11 @@ export default { state.zooming = false; // do not call event cb when is .unzoom() is called - !isUnZoom && (e || state.dragging) && callFn(config.zoom_onzoomend, $$.api, $$.zoom.getDomain()); + !isUnZoom && (e || state.dragging) && callFn( + config.zoom_onzoomend, + $$.api, + $$.state.domain ?? $$.zoom.getDomain() + ); }, /** diff --git a/src/config/Store/State.ts b/src/config/Store/State.ts index c59739eb6..93d5a4978 100644 --- a/src/config/Store/State.ts +++ b/src/config/Store/State.ts @@ -46,6 +46,9 @@ export default class State { cssRule: {}, current: { + // current domain value. Assigned when is zoom is called + domain: undefined, + // chart whole dimension width: 0, height: 0, diff --git a/test/api/subchart-spec.ts b/test/api/subchart-spec.ts index 060ec2293..2396c1f15 100644 --- a/test/api/subchart-spec.ts +++ b/test/api/subchart-spec.ts @@ -179,6 +179,8 @@ describe("API subchart", () => { }); describe(".subchart() / .subchart.reset()", () => { + const spy = sinon.spy(); + before(() => { args = { data: { @@ -187,6 +189,11 @@ describe("API subchart", () => { ], type: "line" }, + axis: { + y: { + show: false + } + }, subchart: { show: true, showHandle: true @@ -201,6 +208,8 @@ describe("API subchart", () => { // when chart.subchart(range); + expect(chart.subchart()).to.deep.equal(range); + const selection = brush.getSelection().select(".selection"); const posX = +selection.attr("x"); const width = +selection.attr("width"); @@ -218,10 +227,12 @@ describe("API subchart", () => { it("when trying to give out of bounds data.", () => { const {brush, scale: {x, subX}} = chart.internal; const selection = brush.getSelection().select(".selection"); + const range = [100, 200]; // when - chart.subchart([100, 200]); + chart.subchart(range); + expect(chart.subchart()).to.deep.equal(x.orgDomain()); expect(selection.attr("width")).to.be.null; expect(x.domain()).to.be.deep.equal(subX.domain()); @@ -238,6 +249,23 @@ describe("API subchart", () => { expect(x.domain()).to.be.deep.equal(subX.domain()); }); + it("set options: subchart.onbrush", () => { + args.subchart.onbrush = spy; + }); + + it("onbrush callback should be called once.", () => { + const range = [1, 2.5]; + + expect(spy.callCount).to.be.equal(0); + + // when + chart.subchart(range); + + expect(spy.callCount).to.be.equal(1); + expect(spy.args[0][0]).to.be.deep.equal(range); + + }); + it("set options: axis.x.type='timeseries'", () => { args = { data: { diff --git a/test/api/zoom-spec.ts b/test/api/zoom-spec.ts index 3ceee4c87..cb4096652 100644 --- a/test/api/zoom-spec.ts +++ b/test/api/zoom-spec.ts @@ -39,6 +39,8 @@ describe("API zoom", function() { chart.zoom(target); + expect(chart.zoom()).to.deep.equal(target); + setTimeout(() => { const domain = chart.internal.scale.zoom.domain().map(Math.round); @@ -112,6 +114,8 @@ describe("API zoom", function() { chart.zoom(target); + expect(chart.zoom()).to.deep.equal(target); + setTimeout(() => { const domain = chart.internal.scale.zoom.domain(); @@ -175,6 +179,8 @@ describe("API zoom", function() { chart.zoom(target); + expect(chart.zoom()).to.deep.equal(target); + setTimeout(() => { const {internal} = chart; const {state, zoom} = internal; @@ -314,9 +320,7 @@ describe("API zoom", function() { }); // check the returned domain value - chart.zoom(domain).map(Math.round).forEach((v, i) => { - expect(v).to.not.equal(domain[i]); - }); + expect(chart.zoom(domain)).to.be.undefined; expect(coords[1].x).to.be.equal(xValue); diff --git a/test/assets/helper.ts b/test/assets/helper.ts index e4ea9312e..e82716868 100644 --- a/test/assets/helper.ts +++ b/test/assets/helper.ts @@ -72,7 +72,7 @@ const hoverChart = (hoverChart, eventName = "mousemove", pos = {clientX: 100, cl const doDrag = (el, from = {clientX: 100, clientY: 100}, to = {clientX: 200, clientY: 200}, chart?) => { fireEvent(el, "mousedown", from, chart); fireEvent(el, "mousemove", to, chart); - fireEvent(el, "mouseup", from, chart); + fireEvent(el, "mouseup", to, chart); }; /** diff --git a/test/interactions/zoom-spec.ts b/test/interactions/zoom-spec.ts index 271b099ce..8050d2202 100644 --- a/test/interactions/zoom-spec.ts +++ b/test/interactions/zoom-spec.ts @@ -77,7 +77,7 @@ describe("ZOOM", function() { const spyOnZoom = sinon.spy(domain => (zoomDomain = domain)); const spyOnZoomEnd = sinon.spy(domain => (zoomDomain = domain)); let zoomDomain; - let eventOrder = []; + let eventOrder: string[] = []; before(() => { args = { @@ -1197,7 +1197,7 @@ describe("ZOOM", function() { it("check bar's width during wheel zoom in/out", () => { const {$: {bar}, internal: {$el: {eventRect}}} = chart; - const len = []; + const len: number[] = []; bar.bars.each(function() { len.push(this.getBoundingClientRect().width); @@ -1249,7 +1249,7 @@ describe("ZOOM", function() { it("bar width should scales as zoom scales", done => { const {bars} = chart.$.bar; - const width = []; + const width: number[] = []; bars.each(function() { width.push(this.getBoundingClientRect().width); diff --git a/test/internals/text-spec.ts b/test/internals/text-spec.ts index 843c08131..41475a753 100644 --- a/test/internals/text-spec.ts +++ b/test/internals/text-spec.ts @@ -1452,7 +1452,7 @@ describe("TEXT", () => { }); describe("labels.colors callback", () => { - let ctx = []; + let ctx: any = []; before(() => { args = { @@ -1593,7 +1593,7 @@ describe("TEXT", () => { ]; chart.$.text.texts.each(function() { - const expected = expectedPos.shift(); + const expected = expectedPos.shift() as number[]; expect(+this.getAttribute("x")).to.be.closeTo(expected[0], 1); expect(+this.getAttribute("y")).to.be.closeTo(expected[1], 2); diff --git a/test/shape/arc-needle-spec.ts b/test/shape/arc-needle-spec.ts index dae95b866..2f928adaf 100644 --- a/test/shape/arc-needle-spec.ts +++ b/test/shape/arc-needle-spec.ts @@ -80,6 +80,7 @@ describe("SHAPE ARC: NEEDLE option", () => { it(".updateHelper()", done => { const {$el: {arcs, needle}} = chart.internal; + const getRotate = transform => +transform.replace(/rotate\((\d+\.?\d+?)deg\).*/, "$1"); new Promise((resolve) => { needle.updateHelper(70); @@ -89,7 +90,7 @@ describe("SHAPE ARC: NEEDLE option", () => { expect(+arcs.select(`.${$ARC.chartArcsTitle}`).text()).to.be.equal(70); // needle has been updated? - expect(getDegree(needle.style("transform"))).to.be.equal(252); + expect(getRotate(needle.style("transform"))).to.be.equal(252); resolve(); }, 500); @@ -101,7 +102,8 @@ describe("SHAPE ARC: NEEDLE option", () => { setTimeout(() => { // title text value should be equal as the initial value expect(+arcs.select(`.${$ARC.chartArcsTitle}`).text()).to.be.equal(args.arc.needle.value); - expect(getDegree(needle.style("transform"))).to.be.equal(158.4); + + expect(getRotate(needle.style("transform"))).to.be.equal(158.4); // show 'data1' chart.toggle("data1");