From da2ce1029eb0fecec513380770c41b86a778d7cf Mon Sep 17 00:00:00 2001 From: Lennert Claeys Date: Sun, 29 Jul 2018 21:26:32 +0200 Subject: [PATCH] feat(zoom): Add option to zoom by dragging - Adds an option to allow zooming by dragging on the chart instead of using the scrollwheel. - Add reset button when zoomed-in Fix #416 Fix #508 Close #513 skip: add rest button --- demo/demo.js | 68 ++++++++++------- spec/interactions/zoom-spec.js | 100 +++++++++++++++++++++++- src/api/api.zoom.js | 65 ++++++++++++---- src/config/Options.js | 20 ++++- src/config/classes.js | 109 +++++++++++++------------- src/interactions/drag.js | 49 ++++++++---- src/interactions/interaction.js | 3 +- src/interactions/zoom.js | 131 ++++++++++++++++++++++++++++++-- src/internals/ChartInternal.js | 12 ++- src/internals/scale.js | 1 - src/internals/util.js | 5 +- src/scss/billboard.scss | 23 +++++- src/scss/theme/insight.scss | 22 +++++- 13 files changed, 474 insertions(+), 134 deletions(-) diff --git a/demo/demo.js b/demo/demo.js index f8f3a6fec..b2134cee8 100644 --- a/demo/demo.js +++ b/demo/demo.js @@ -1584,6 +1584,20 @@ var demos = { enabled: true } } + }, + DragZoom: { + options: { + data: { + columns: [ + ["sample", 30, 200, 100, 400, 150, 250, 150, 200, 170, 240, 350, 150, 100, 400, 150, 250, 150, 200, 170, 240, 100, 150, 250, 150, 200, 170, 240, 30, 200, 100, 400, 150, 250, 150, 200, 170, 240, 350, 150, 100, 400, 350, 220, 250, 300, 270, 140, 150, 90, 150, 50, 120, 70, 40] + ] + }, + zoom: { + enabled: { + type: "drag" + } + } + } } }, @@ -2595,37 +2609,37 @@ d3.select(".chart_area") done: function() { chart.flow({ columns: [ - ["x", '2013-02-11', '2013-02-12', '2013-02-13', '2013-02-14'], - ["data1", 200, 300, 100, 250], - ["data2", 100, 90, 40, 120], - ["data3", 100, 100, 300, 500] + ["x", '2013-02-11', '2013-02-12', '2013-02-13', '2013-02-14'], + ["data1", 200, 300, 100, 250], + ["data2", 100, 90, 40, 120], + ["data3", 100, 100, 300, 500] ], length: 0, duration: 1500, done: function() { - chart.flow({ - columns: [ - ["x", '2013-03-01', '2013-03-02'], - ["data1", 200, 300], - ["data2", 150, 250], - ["data3", 100, 100] - ], - length: 2, - duration: 1500, - done: function() { - chart.flow({ - columns: [ - ["x", '2013-03-21', '2013-04-01'], - ["data1", 500, 200], - ["data2", 100, 150], - ["data3", 200, 400] - ], - to: '2013-03-01', - duration: 1500 - }); - } - }); - } + chart.flow({ + columns: [ + ["x", '2013-03-01', '2013-03-02'], + ["data1", 200, 300], + ["data2", 150, 250], + ["data3", 100, 100] + ], + length: 2, + duration: 1500, + done: function() { + chart.flow({ + columns: [ + ["x", '2013-03-21', '2013-04-01'], + ["data1", 500, 200], + ["data2", 100, 150], + ["data3", 200, 400] + ], + to: '2013-03-01', + duration: 1500 + }); + } + }); + } }); }, }); diff --git a/spec/interactions/zoom-spec.js b/spec/interactions/zoom-spec.js index 47cf187e3..2fcc864c6 100644 --- a/spec/interactions/zoom-spec.js +++ b/spec/interactions/zoom-spec.js @@ -83,7 +83,9 @@ describe("ZOOM", function() { ] }, zoom: { - enabled: true + enabled: { + type: "wheel" + } } }; }); @@ -129,5 +131,99 @@ describe("ZOOM", function() { // check if chart react on resize expect(+domain.attr("d").match(rx)[1]).to.be.above(pathValue); }); - }); + }); + + describe("zoom type drag", () => { + 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: { + type: "drag" + } + } + }; + }); + + it("check for data zoom", () => { + const main = chart.$.main; + const xValue = +main.select(`.${CLASS.eventRect}-2`).attr("x"); + + // when + chart.zoom([0, 3]); // zoom in + + expect(+main.select(`.${CLASS.eventRect}-2`).attr("x")).to.be.above(xValue); + }); + + it("check for x axis resize after zoom", () => { + const main = chart.$.main; + const rx = /H(\d+)/; + + const domain = main.select(`.${CLASS.axisX} > .domain`); + const pathValue = +domain.attr("d").match(rx)[1]; + + chart.zoom([0, 4]); + chart.resize({ width: 400 }); + + expect(+domain.attr("d").match(rx)[1]).to.be.above(pathValue); + }); + + it("check for x axis resize after zoom in/out", () => { + const main = chart.$.main; + const rx = /H(\d+)/; + + const domain = main.select(`.${CLASS.axisX} > .domain`); + const pathValue = +domain.attr("d").match(rx)[1]; + + chart.zoom([0, 4]); // zoom in + chart.zoom([0, 6]); // zoom out + + expect(+domain.attr("d").match(rx)[1]).to.be.equal(pathValue); + + // resize + chart.resize({ width: 400 }); + + // check if chart react on resize + expect(+domain.attr("d").match(rx)[1]).to.be.above(pathValue); + }); + + it("check for the reset zoom button", () => { + // when + chart.zoom([0, 4]); + + const resetBtn = chart.$.chart.select(`.${CLASS.buttonZoomReset}`); + + expect(resetBtn.empty()).to.be.false; + + // when button is clicked + resetBtn.node().click(); + + expect(resetBtn.style("display")).to.be.equal("none"); + }); + + it("set options zoom.resetButton.text='test", () => { + args.zoom.resetButton = { + text: "test" + }; + }); + + it("check for the custom reset zoom button text", () => { + // when + chart.zoom([0, 4]); + + const resetBtn = chart.$.chart.select(`.${CLASS.buttonZoomReset}`); + + expect(resetBtn.empty()).to.be.false; + expect(resetBtn.text()).to.be.equal("test"); + }); + }); }); diff --git a/src/api/api.zoom.js b/src/api/api.zoom.js index 0ed5ce31f..58da092b0 100644 --- a/src/api/api.zoom.js +++ b/src/api/api.zoom.js @@ -8,7 +8,7 @@ import { } from "d3-array"; import {zoomIdentity as d3ZoomIdentity} from "d3-zoom"; import Chart from "../internals/Chart"; -import {isDefined, isObject, isFunction, extend} from "../internals/util"; +import {isDefined, isObject, isFunction, isString, extend} from "../internals/util"; /** * Zoom by giving x domain. @@ -26,11 +26,12 @@ import {isDefined, isObject, isFunction, extend} from "../internals/util"; */ const zoom = function(domainValue) { const $$ = this.internal; - const isTimeSeries = $$.isTimeSeries(); let domain = domainValue; let resultDomain; if ($$.config.zoom_enabled && domain) { + const isTimeSeries = $$.isTimeSeries(); + if (isTimeSeries) { domain = domain.map(x => $$.parseDate(x)); } @@ -43,8 +44,9 @@ const zoom = function(domainValue) { } else { const orgDomain = $$.x.orgDomain(); const k = (orgDomain[1] - orgDomain[0]) / (domain[1] - domain[0]); + const gap = $$.isCategorized() ? $$.xAxis.tickOffset() : 0; const tx = isTimeSeries ? - (0 - k * $$.x(domain[0].getTime())) : domain[0] - k * ($$.x(domain[0]) - $$.xAxis.tickOffset()); + (0 - k * $$.x(domain[0].getTime())) : domain[0] - k * ($$.x(domain[0]) - gap); $$.zoom.updateTransformScale( d3ZoomIdentity.translate(tx, 0).scale(k) @@ -59,9 +61,13 @@ const zoom = function(domainValue) { withDimension: false }); - isFunction($$.config.zoom_onzoom) && $$.config.zoom_onzoom.call(this, $$.x.orgDomain()); + $$.setZoomResetButton(); + + isFunction($$.config.zoom_onzoom) && + $$.config.zoom_onzoom.call(this, $$.x.orgDomain()); } else { - resultDomain = ($$.zoomScale || $$.x).domain(); + resultDomain = $$.zoomScale ? + $$.zoomScale.domain() : $$.x.orgDomain(); } return resultDomain; @@ -73,18 +79,39 @@ extend(zoom, { * @method zoom․enable * @instance * @memberOf Chart - * @param {Boolean} enabled If enabled is true, the feature of zooming will be enabled. If false is given, it will be disabled.
When set to false, the current zooming status will be reset. + * @param {String|Boolean} enabled Possible string values are "wheel" or "drag". If enabled is true, "wheel" will be used. If false is given, zooming will be disabled.
When set to false, the current zooming status will be reset. * @example - * // Enable zooming + * // Enable zooming using the mouse wheel * chart.zoom.enable(true); + * // Or + * chart.zoom.enable("wheel"); + * + * // Enable zooming by dragging + * chart.zoom.enable("drag"); * * // Disable zooming * chart.zoom.enable(false); */ - enable: function(enabled = false) { + enable: function(enabled = "wheel") { const $$ = this.internal; + const config = $$.config; + let enableType = enabled; + + if (enabled) { + enableType = isString(enabled) && /^(drag|wheel)$/.test(enabled) ? + {type: enabled} : enabled; + } + + config.zoom_enabled = enableType; + + if (!$$.zoom) { + $$.initZoom(); + $$.initZoomBehaviour(); + $$.bindZoomEvent(); + } else if (enabled === false) { + $$.bindZoomEvent(false); + } - $$.config.zoom_enabled = enabled; $$.updateAndRedraw(); }, @@ -177,14 +204,20 @@ extend(Chart.prototype, { */ unzoom() { const $$ = this.internal; + const config = $$.config; - $$.config.subchart_show ? - $$.brush.getSelection().call($$.brush.move, null) : - $$.zoom.updateTransformScale(d3ZoomIdentity); + if ($$.zoomScale) { + config.subchart_show ? + $$.brush.getSelection().call($$.brush.move, null) : + $$.zoom.updateTransformScale(d3ZoomIdentity); - $$.redraw({ - withTransition: true, - withY: $$.config.zoom_rescale - }); + $$.updateZoom(); + $$.zoom.resetBtn && $$.zoom.resetBtn.style("display", "none"); + + $$.redraw({ + withTransition: true, + withY: config.zoom_rescale + }); + } } }); diff --git a/src/config/Options.js b/src/config/Options.js index 769342ea4..91cad423f 100644 --- a/src/config/Options.js +++ b/src/config/Options.js @@ -129,6 +129,7 @@ export default class Options { * @memberOf Options * @type {Object} * @property {Boolean} [zoom.enabled=false] Enable zooming. + * @property {String} [zoom.enabled.type='wheel'] Set zoom interaction type. * @property {Boolean} [zoom.rescale=false] Enable to rescale after zooming.
* If true set, y domain will be updated according to the zoomed region. * @property {Array} [zoom.extent=[1, 10]] Change zoom extent. @@ -140,9 +141,13 @@ export default class Options { * Specified function receives the zoomed domain. * @property {Function} [zoom.onzoomend=undefined] Set callback that is called when zooming ends.
* Specified function receives the zoomed domain. + * @property {Boolean|Object} [zoom.resetButton=true] Set to display zoom reset button for 'drag' type zoom + * @property {String} [zoom.resetButton.text='Reset Zoom'] Text value for zoom reset button. * @example * zoom: { - * enabled: true, + * enabled: { + * type: "drag" + * }, * rescale: true, * extent: [1, 100] // enable more zooming * x: { @@ -151,16 +156,25 @@ export default class Options { * }, * onzoomstart: function(event) { ... }, * onzoom: function(domain) { ... }, - * onzoomend: function(domain) { ... } + * onzoomend: function(domain) { ... }, + * + * // show reset button when is zoomed-in + * resetButton: true, + * + * // customized text value for reset zoom button + * resetButton: { + * text: "Unzoom" + * } * } */ - zoom_enabled: false, + zoom_enabled: undefined, zoom_extent: undefined, zoom_privileged: false, zoom_rescale: false, zoom_onzoom: undefined, zoom_onzoomstart: undefined, zoom_onzoomend: undefined, + zoom_resetButton: true, zoom_x_min: undefined, zoom_x_max: undefined, diff --git a/src/config/classes.js b/src/config/classes.js index 6141298f9..99bd3d87d 100644 --- a/src/config/classes.js +++ b/src/config/classes.js @@ -7,84 +7,87 @@ * @private */ export default { - target: "bb-target", + arc: "bb-arc", + arcs: "bb-arcs", + area: "bb-area", + areas: "bb-areas", + axis: "bb-axis", + axisX: "bb-axis-x", + axisXLabel: "bb-axis-x-label", + axisY: "bb-axis-y", + axisY2: "bb-axis-y2", + axisY2Label: "bb-axis-y2-label", + axisYLabel: "bb-axis-y-label", + bar: "bb-bar", + bars: "bb-bars", + brush: "bb-brush", + button: "bb-button", + buttonZoomReset: "bb-zoom-reset", chart: "bb-chart", - chartLine: "bb-chart-line", - chartLines: "bb-chart-lines", - chartBar: "bb-chart-bar", - chartBars: "bb-chart-bars", - chartText: "bb-chart-text", - chartTexts: "bb-chart-texts", chartArc: "bb-chart-arc", chartArcs: "bb-chart-arcs", - chartArcsTitle: "bb-chart-arcs-title", chartArcsBackground: "bb-chart-arcs-background", - chartArcsGaugeUnit: "bb-chart-arcs-gauge-unit", chartArcsGaugeMax: "bb-chart-arcs-gauge-max", chartArcsGaugeMin: "bb-chart-arcs-gauge-min", + chartArcsGaugeUnit: "bb-chart-arcs-gauge-unit", + chartArcsTitle: "bb-chart-arcs-title", + chartBar: "bb-chart-bar", + chartBars: "bb-chart-bars", + chartLine: "bb-chart-line", + chartLines: "bb-chart-lines", chartRadar: "bb-chart-radar", chartRadars: "bb-chart-radars", + chartText: "bb-chart-text", + chartTexts: "bb-chart-texts", + circle: "bb-circle", + circles: "bb-circles", colorPattern: "bb-color-pattern", - selectedCircle: "bb-selected-circle", - selectedCircles: "bb-selected-circles", + defocused: "bb-defocused", + dragarea: "bb-dragarea", + empty: "bb-empty", eventRect: "bb-event-rect", eventRects: "bb-event-rects", - eventRectsSingle: "bb-event-rects-single", eventRectsMultiple: "bb-event-rects-multiple", - zoomRect: "bb-zoom-rect", - brush: "bb-brush", + eventRectsSingle: "bb-event-rects-single", focused: "bb-focused", - defocused: "bb-defocused", + gaugeValue: "bb-gauge-value", + grid: "bb-grid", + gridLines: "bb-grid-lines", + legendBackground: "bb-legend-background", + legendItem: "bb-legend-item", + legendItemEvent: "bb-legend-item-event", + legendItemFocused: "bb-legend-item-focused", + legendItemHidden: "bb-legend-item-hidden", + legendItemPoint: "bb-legend-item-point", + legendItemTile: "bb-legend-item-tile", + level: "bb-level", + levels: "bb-levels", + line: "bb-line", + lines: "bb-lines", region: "bb-region", regions: "bb-regions", - title: "bb-title", - tooltipContainer: "bb-tooltip-container", - tooltip: "bb-tooltip", - tooltipName: "bb-tooltip-name", + selectedCircle: "bb-selected-circle", + selectedCircles: "bb-selected-circles", shape: "bb-shape", shapes: "bb-shapes", - line: "bb-line", - lines: "bb-lines", - bar: "bb-bar", - bars: "bb-bars", - circle: "bb-circle", - circles: "bb-circles", - arc: "bb-arc", - arcs: "bb-arcs", - area: "bb-area", - areas: "bb-areas", - empty: "bb-empty", + target: "bb-target", text: "bb-text", texts: "bb-texts", - gaugeValue: "bb-gauge-value", - grid: "bb-grid", - gridLines: "bb-grid-lines", + title: "bb-title", + tooltip: "bb-tooltip", + tooltipContainer: "bb-tooltip-container", + tooltipName: "bb-tooltip-name", xgrid: "bb-xgrid", - xgrids: "bb-xgrids", + xgridFocus: "bb-xgrid-focus", xgridLine: "bb-xgrid-line", xgridLines: "bb-xgrid-lines", - xgridFocus: "bb-xgrid-focus", + xgrids: "bb-xgrids", ygrid: "bb-ygrid", - ygrids: "bb-ygrids", ygridLine: "bb-ygrid-line", ygridLines: "bb-ygrid-lines", - axis: "bb-axis", - axisX: "bb-axis-x", - axisXLabel: "bb-axis-x-label", - axisY: "bb-axis-y", - axisYLabel: "bb-axis-y-label", - axisY2: "bb-axis-y2", - axisY2Label: "bb-axis-y2-label", - legendBackground: "bb-legend-background", - legendItem: "bb-legend-item", - legendItemEvent: "bb-legend-item-event", - legendItemTile: "bb-legend-item-tile", - legendItemPoint: "bb-legend-item-point", - legendItemHidden: "bb-legend-item-hidden", - legendItemFocused: "bb-legend-item-focused", - level: "bb-level", - levels: "bb-levels", - dragarea: "bb-dragarea", + ygrids: "bb-ygrids", + zoomBrush: "bb-zoom-brush", + zoomRect: "bb-zoom-rect", EXPANDED: "_expanded_", SELECTED: "_selected_", INCLUDED: "_included_" diff --git a/src/interactions/drag.js b/src/interactions/drag.js index 9fac303ee..ea0dd4bae 100644 --- a/src/interactions/drag.js +++ b/src/interactions/drag.js @@ -19,15 +19,17 @@ extend(ChartInternal.prototype, { const config = $$.config; const main = $$.main; - if ($$.hasArcType()) { return; } - if (!config.data_selection_enabled) { return; } // do nothing if not selectable - if (config.zoom_enabled && !$$.zoom.altDomain) { return; } // skip if zoomable because of conflict drag dehavior - if (!config.data_selection_multiple) { return; } // skip when single selection because drag is used for multiple selection - - const sx = $$.dragStart[0]; - const sy = $$.dragStart[1]; - const mx = mouse[0]; - const my = mouse[1]; + if ($$.hasArcType() || + !config.data_selection_enabled || // do nothing if not selectable + (config.zoom_enabled && !$$.zoom.altDomain) || // skip if zoomable because of conflict drag behavior + !config.data_selection_multiple // skip when single selection because drag is used for multiple selection + ) { + return; + } + + const [sx, sy] = $$.dragStart; + const [mx, my] = mouse; + const minX = Math.min(sx, mx); const maxX = Math.max(sx, mx); const minY = config.data_selection_grouped ? $$.margin.top : Math.min(sy, my); @@ -40,12 +42,14 @@ extend(ChartInternal.prototype, { .attr("height", maxY - minY); // TODO: binary search when multiple xs - main.selectAll(`.${CLASS.shapes}`).selectAll(`.${CLASS.shape}`) + main.selectAll(`.${CLASS.shapes}`) + .selectAll(`.${CLASS.shape}`) .filter(d => config.data_selection_isselectable(d)) .each(function(d, i) { const shape = d3Select(this); const isSelected = shape.classed(CLASS.SELECTED); const isIncluded = shape.classed(CLASS.INCLUDED); + let _x; let _y; let _w; @@ -71,6 +75,7 @@ extend(ChartInternal.prototype, { // line/area selection not supported yet return; } + if (isWithin ^ isIncluded) { shape.classed(CLASS.INCLUDED, !isIncluded); // TODO: included/unincluded callback here @@ -90,14 +95,18 @@ extend(ChartInternal.prototype, { const $$ = this; const config = $$.config; - if ($$.hasArcType()) { return; } - if (!config.data_selection_enabled) { return; } // do nothing if not selectable + if ($$.hasArcType() || !config.data_selection_enabled) { + return; + } + $$.dragStart = mouse; + $$.main.select(`.${CLASS.chart}`) .append("rect") .attr("class", CLASS.dragarea) .style("opacity", "0.1"); - $$.dragging = true; + + $$.setDragStatus(true); }, /** @@ -109,15 +118,23 @@ extend(ChartInternal.prototype, { const $$ = this; const config = $$.config; - if ($$.hasArcType()) { return; } - if (!config.data_selection_enabled) { return; } // do nothing if not selectable + if ($$.hasArcType() || !config.data_selection_enabled) { // do nothing if not selectable + return; + } + $$.main.select(`.${CLASS.dragarea}`) .transition() .duration(100) .style("opacity", "0") .remove(); + $$.main.selectAll(`.${CLASS.shape}`) .classed(CLASS.INCLUDED, false); - $$.dragging = false; + + $$.setDragStatus(false); }, + + setDragStatus(isDragging) { + this.dragging = isDragging; + } }); diff --git a/src/interactions/interaction.js b/src/interactions/interaction.js index b967be7eb..49a127171 100644 --- a/src/interactions/interaction.js +++ b/src/interactions/interaction.js @@ -34,11 +34,12 @@ extend(ChartInternal.prototype, { redrawEventRect() { const $$ = this; const config = $$.config; + const zoomEnabled = config.zoom_enabled; const isMultipleX = $$.isMultipleX(); let eventRectUpdate; const eventRects = $$.main.select(`.${CLASS.eventRects}`) - .style("cursor", config.zoom_enabled ? ( + .style("cursor", zoomEnabled && (zoomEnabled === true || zoomEnabled.type === "wheel") ? ( config.axis_rotate ? "ns-resize" : "ew-resize" ) : null) .classed(CLASS.eventRectsMultiple, isMultipleX) diff --git a/src/interactions/zoom.js b/src/interactions/zoom.js index a66724421..45b57d3dc 100644 --- a/src/interactions/zoom.js +++ b/src/interactions/zoom.js @@ -6,7 +6,11 @@ import { min as d3Min, max as d3Max } from "d3-array"; -import {event as d3Event} from "d3-selection"; +import { + mouse as d3Mouse, + event as d3Event +} from "d3-selection"; +import {drag as d3Drag} from "d3-drag"; import {zoom as d3Zoom} from "d3-zoom"; import ChartInternal from "../internals/ChartInternal"; import CLASS from "../config/classes"; @@ -24,6 +28,32 @@ extend(ChartInternal.prototype, { $$.generateZoom(); }, + /** + * Bind zoom event + * @param {Boolean} bind Weather bind or unbound + * @private + */ + bindZoomEvent(bind = true) { + const $$ = this; + const zoomEnabled = $$.config.zoom_enabled; + + $$.redrawEventRect(); + + if (zoomEnabled && bind) { + if (zoomEnabled === true || zoomEnabled.type === "wheel") { + $$.bindZoomOnEventRect(); + } else if (zoomEnabled.type === "drag") { + $$.bindZoomOnDrag(); + } + } else if (bind === false) { + $$.api.unzoom(); + + $$.main.select(`.${CLASS.eventRects}`) + .on(".zoom", null) + .on(".drag", null); + } + }, + /** * Generate zoom * @private @@ -33,9 +63,9 @@ extend(ChartInternal.prototype, { const config = $$.config; const zoom = d3Zoom().duration(0) - .on("start", $$.onStart.bind($$)) + .on("start", $$.onZoomStart.bind($$)) .on("zoom", $$.onZoom.bind($$)) - .on("end", $$.onEnd.bind($$)); + .on("end", $$.onZoomEnd.bind($$)); // get zoom extent zoom.orgScaleExtent = () => { @@ -61,8 +91,9 @@ extend(ChartInternal.prototype, { zoom.updateTransformScale = transform => { // rescale from the original scale const newScale = transform.rescaleX($$.x.orgScale()); + const domain = $$.trimXDomain(newScale.domain()); - newScale.domain($$.trimXDomain(newScale.domain())); + newScale.domain(domain, $$.orgXDomain); $$.zoomScale = $$.getCustomizedScale(newScale); $$.xAxis.scale($$.zoomScale); @@ -75,7 +106,7 @@ extend(ChartInternal.prototype, { * 'start' event listener * @private */ - onStart() { + onZoomStart() { const $$ = this; const event = d3Event.sourceEvent; const onzoomstart = $$.config.zoom_onzoomstart; @@ -135,9 +166,8 @@ extend(ChartInternal.prototype, { * 'end' event listener * @private */ - onEnd() { + onZoomEnd() { const $$ = this; - const event = d3Event.sourceEvent; const onzoomend = $$.config.zoom_onzoomend; const startEvent = $$.zoom.startEvent; @@ -199,5 +229,92 @@ extend(ChartInternal.prototype, { $$.main.select(`.${CLASS.eventRects}`) .call($$.zoom) .on("dblclick.zoom", null); + }, + + /** + * Initialize the drag behaviour used for zooming. + * @private + */ + initZoomBehaviour() { + const $$ = this; + const config = $$.config; + const isRotated = config.axis_rotated; + let start = 0; + let end = 0; + let zoomRect = null; + + $$.zoomBehaviour = d3Drag() + .on("start", function() { + $$.setDragStatus(true); + + if (!zoomRect) { + zoomRect = $$.main.append("rect") + .attr("clip-path", $$.clipPath) + .attr("class", CLASS.zoomBrush) + .attr("width", isRotated ? $$.width : 0) + .attr("height", isRotated ? 0 : $$.height); + } + + start = d3Mouse(this)[0]; + end = start; + + zoomRect + .attr("x", start) + .attr("width", 0); + }) + .on("drag", function() { + end = d3Mouse(this)[0]; + + zoomRect + .attr("x", Math.min(start, end)) + .attr("width", Math.abs(end - start)); + }) + .on("end", () => { + const scale = $$.zoomScale || $$.x; + + $$.setDragStatus(false); + + zoomRect + .attr("x", 0) + .attr("width", 0); + + if (start > end) { + [start, end] = [end, start]; + } + + if (start !== end) { + $$.api.zoom([start, end].map(v => scale.invert(v))); + } + }); + }, + + /** + * Enable zooming by dragging using the zoombehaviour. + * @private + */ + bindZoomOnDrag() { + const $$ = this; + + $$.main.select(`.${CLASS.eventRects}`) + .call($$.zoomBehaviour); + }, + + setZoomResetButton() { + const $$ = this; + const config = $$.config; + const resetButton = config.zoom_resetButton; + + if (resetButton && config.zoom_enabled.type === "drag") { + if (!$$.zoom.resetBtn) { + $$.zoom.resetBtn = $$.selectChart.append("div") + .classed(CLASS.button, true) + .append("span") + .on("click", $$.api.unzoom.bind($$)) + .classed(CLASS.buttonZoomReset, true) + .text(resetButton.text || "Reset Zoom"); + } else { + $$.zoom.resetBtn.style("display", null); + } + } } }); diff --git a/src/internals/ChartInternal.js b/src/internals/ChartInternal.js index 12c2e4cd3..607a744b0 100644 --- a/src/internals/ChartInternal.js +++ b/src/internals/ChartInternal.js @@ -184,8 +184,12 @@ export default class ChartInternal { $$.axis = new Axis($$); - $$.initBrush && $$.initBrush(); - $$.initZoom && $$.initZoom(); + config.subchart_show && $$.initBrush(); + + if (config.zoom_enabled) { + $$.initZoom(); + $$.initZoomBehaviour(); + } const bindto = { element: config.bindto, @@ -286,7 +290,7 @@ export default class ChartInternal { // Set initialized scales to brush and zoom // if ($$.brush) { $$.brush.scale($$.subX); } - // if (config.zoom_enabled) { $$.zoom.scale($$.x); } + // if (config.zoom_enabled === true || config.zoom_enabled_type) { $$.zoom.scale($$.x); } // Define regions const main = $$.svg.append("g").attr("transform", $$.getTranslate("main")); @@ -765,7 +769,7 @@ export default class ChartInternal { // event rects will redrawn when flow called if (config.interaction_enabled && !options.flow && withEventRect) { $$.redrawEventRect(); - config.zoom_enabled && $$.bindZoomOnEventRect(); + $$.bindZoomEvent(); } // update circleY based on updated parameters diff --git a/src/internals/scale.js b/src/internals/scale.js index ccf061adc..490edfe8a 100644 --- a/src/internals/scale.js +++ b/src/internals/scale.js @@ -53,7 +53,6 @@ extend(ChartInternal.prototype, { getCustomizedScale(scaleValue, offsetValue) { const $$ = this; const offset = offsetValue || (() => $$.xAxis.tickOffset()); - const scale = function(d, raw) { const v = scaleValue(d) + offset(); diff --git a/src/internals/util.js b/src/internals/util.js index 0c307fa0e..75f2df744 100644 --- a/src/internals/util.js +++ b/src/internals/util.js @@ -116,12 +116,13 @@ const getPathBox = path => { const getBrushSelection = function() { let selection = null; const event = d3Event; + const ctx = this.context || this.main; // check from event if (event && event.constructor.name === "BrushEvent") { selection = event.selection; - // check from brush area selection - } else if (this.context && (selection = this.context.select(`.${CLASS.brush}`).node())) { + // check from brush area selection + } else if (ctx && (selection = ctx.select(`.${CLASS.brush}`).node())) { selection = d3BrushSelection(selection); } diff --git a/src/scss/billboard.scss b/src/scss/billboard.scss index d3e5d3ded..7cd37767e 100644 --- a/src/scss/billboard.scss +++ b/src/scss/billboard.scss @@ -10,7 +10,7 @@ stroke: #000; } - text { + text, .bb-button { -webkit-user-select: none; -moz-user-select: none; user-select: none; @@ -119,6 +119,11 @@ fill-opacity: .1; } +/*-- Zoom region --*/ +.bb-zoom-brush { + fill-opacity: .1; +} + /*-- Brush --*/ .bb-brush .extent { fill-opacity: .1; @@ -263,3 +268,19 @@ } } } + +/*-- Button --*/ +.bb-button { + position: absolute; + top: 10px; + right: 10px; + + .bb-zoom-reset { + font-size: 11px; + border: solid 1px #ccc; + background-color: #fff; + padding: 5px; + border-radius: 5px; + cursor: pointer; + } +} diff --git a/src/scss/theme/insight.scss b/src/scss/theme/insight.scss index 0bb96f16d..d00832ac3 100644 --- a/src/scss/theme/insight.scss +++ b/src/scss/theme/insight.scss @@ -18,7 +18,7 @@ fill: none; stroke: #c4c4c4; } - text { + text, .bb-button { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; @@ -136,6 +136,11 @@ } } +/*-- Zoom region --*/ +.bb-zoom-brush { + fill-opacity: .1; +} + /*-- Brush --*/ .bb-brush { .extent { @@ -325,3 +330,18 @@ } } } + +/*-- Button --*/ +.bb-button { + position: absolute; + top: 10px; + right: 10px; + + .bb-zoom-reset { + border: solid 1px #ccc; + background-color: #fff; + padding: 5px; + border-radius: 5px; + cursor: pointer; + } +}