From 71db6c295038eec7c56eb65b9b02c25263ad1a97 Mon Sep 17 00:00:00 2001 From: Robby Prima Suherlan Date: Sat, 1 Aug 2020 01:46:52 +0700 Subject: [PATCH 1/2] feat: range panning & zooming --- example/index.html | 29 ++++++ src/slider.js | 215 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 242 insertions(+), 2 deletions(-) diff --git a/example/index.html b/example/index.html index 0937d9f..745d2a7 100644 --- a/example/index.html +++ b/example/index.html @@ -58,6 +58,11 @@

Transition

+

Drag to pan & zoom

+
+

+
+

Examples

New York Times

@@ -454,4 +459,28 @@

Color picker

}); d3.select('p#value-color-picker').text(`#${num2hex(rgb)}`); + + var dragToPanSlider = d3 + .sliderBottom() + .min(2) + .max(15) + .width(300) + .step(1) + .default(5) + .dragToPan(true) + .on('onchange', function (val) { + d3.select('p#value-drag-to-pan').text(val); + }); + + var dragToPanG = d3 + .select('#slider-drag-to-pan') + .append('svg') + .attr('width', 500) + .attr('height', 100) + .append('g') + .attr('transform', 'translate(30,30)'); + + dragToPanG.call(dragToPanSlider); + + d3.select('p#value-drag-to-pan').text(dragToPanSlider.value()); diff --git a/src/slider.js b/src/slider.js index c145555..7ace9ef 100644 --- a/src/slider.js +++ b/src/slider.js @@ -41,8 +41,22 @@ function slider(orientation, scale) { var ticks = null; var displayFormat = null; var fill = null; - - var listeners = dispatch('onchange', 'start', 'end', 'drag'); + var minTravelDrag = 5; + var dragStartPos = null; + var lastAnimation = null; + var dragModeFn = null; + var dragToPan = false; + + var listeners = dispatch( + 'onchange', + 'start', + 'end', + 'drag', + 'rangeChange', + 'rangeStart', + 'rangeDrag', + 'rangeEnd' + ); var selection = null; var identityClamped = null; @@ -108,6 +122,11 @@ function slider(orientation, scale) { .clamp(true); } + if (!step) { + step = + (domain[1] - domain[0]) / (ticks ? ticks : scale.ticks().length - 1); + } + identityClamped = scaleLinear() .range(scale.range()) .domain(scale.range()) @@ -129,6 +148,51 @@ function slider(orientation, scale) { .attr('transform', transformAcross(k * 7)) .attr('class', 'axis'); + var rangePanning = selection + .selectAll('.range-panning') + .data(dragToPan ? [null] : []); + + var rangePanningEnter = rangePanning + .enter() + .append('g') + .attr('class', 'range-panning') + .call( + dragToPan + ? drag() + .on('start', panZoomStart) + .on('drag', panZoomDrag) + .on('end', panZoomEnd) + : function () {} + ); + + rangePanningEnter + .append('rect') + .attr('x', function () { + return -SLIDER_END_PADDING; + }) + .attr('width', function () { + if ([top, bottom].indexOf(orientation) !== -1) { + return scale.range()[1] + 2 * SLIDER_END_PADDING; + } + + return 27; + }) + .attr('height', function () { + if ([top, bottom].indexOf(orientation) === -1) { + return scale.range()[0] + 2 * SLIDER_END_PADDING; + } + + return 27; + }) + .attr('y', k * 27 * 0.5) + .attr('fill', 'transparent') + .attr( + 'cursor', + orientation === top || orientation === bottom + ? 'ew-resize' + : 'ns-resize' + ) + var sliderSelection = selection.selectAll('.slider').data([null]); var sliderEnter = sliderSelection @@ -429,6 +493,147 @@ function slider(orientation, scale) { handleIndex = null; } + function inc(val) { + return val instanceof Date + ? new Date(val.getTime() + getStep()) + : val + getStep(); + } + + function subtract(val) { + return val instanceof Date + ? new Date(val.getTime() - getStep()) + : val - getStep(); + } + + // allow zoom in and out domain based on drag direction relative to drag start position + function rangeZoom(dragStartPos) { + /** + * drag start position = right + */ + var nextDomain; + if (dragStartPos.x > width * 0.5) { + nextDomain = dragStartPos.x < event.x ? inc(domain[1]) : subtract(domain[1]) + domain = [ + domain[0], + nextDomain < max(value) ? domain[1] : nextDomain, + ]; + } else if (dragStartPos.x <= width * 0.5) { + /** + * drag start position = left + */ + nextDomain = dragStartPos.x <= event.x ? inc(domain[0]) : subtract(domain[0]) + + domain = [ + nextDomain > min(value) ? domain[0] : nextDomain, + domain[1], + ]; + } + } + + function rangePan(dragStartPos) { + // drag direction = left + if (dragStartPos.x > event.x) { + domain = [ + domain[0] >= min(value) ? domain[0] : inc(domain[0]), + inc(domain[1]), + ]; + } else { + domain = [ + subtract(domain[0]), + domain[1] <= max(value) ? domain[1] : subtract(domain[1]), + ]; + } + + return domain; + } + + function getStep() { + // Need to validate further about step calc if no step given + return step; + } + + function panZoomStart() { + var zooming = event.x / width <= 0.33 || event.x / width >= 0.66; + + dragModeFn = zooming ? rangeZoom : rangePan; + dragStartPos = event; + + listeners.call('rangeStart', slider); + } + + function panZoomDrag() { + if (Math.abs(dragStartPos.x - event.x) < minTravelDrag) return; + + // prevent animation overlap + if (lastAnimation === null) { + lastAnimation = new Date(); + } + + listeners.call('rangeDrag', slider, event); + + // only after previous animation completed + if (new Date() - lastAnimation >= UPDATE_DURATION) { + dragModeFn.call(this, dragStartPos); + + scale.domain(domain); + + listeners.call('rangeChange', slider, domain); + + refreshAxis(); + + lastAnimation = null; + } + } + + function panZoomEnd() { + dragStartPos = null; + + listeners.call('rangeEnd', slider); + } + + function refreshAxis() { + selection + .select('.axis') + .transition() + .ease(easeQuadOut) + .duration(UPDATE_DURATION) + .call(function (sel) { + sel.call( + axisFunction(scale) + .ticks(ticks) + .tickFormat(tickFormat) + .tickValues(tickValues) + ); + + sel + .selectAll('text') + .attr('fill', '#aaa') + .attr(y, k * 20) + .attr( + 'dy', + orientation === top + ? '0em' + : orientation === bottom + ? '.71em' + : '.32em' + ) + .attr( + 'text-anchor', + orientation === right + ? 'start' + : orientation === left + ? 'end' + : 'middle' + ); + + updateHandle(value, true); + }) + .on('start', function () { + selection.selectAll('.axis line').attr('stroke', '#aaa'); + selection.select('.axis .domain').remove(); + }); + } + textSelection = selection.selectAll('.parameter-value text'); fillSelection = selection.select('.track-fill'); } @@ -790,6 +995,12 @@ function slider(orientation, scale) { return value === listeners ? slider : value; }; + slider.dragToPan = function (_) { + if (!arguments.length) return dragToPan; + dragToPan = _; + return slider; + }; + return slider; } From 571e5a182cb92d7310ca2814ec17c3a410a3990a Mon Sep 17 00:00:00 2001 From: Chat Wacharamanotham Date: Sat, 22 Aug 2020 15:01:22 +0200 Subject: [PATCH 2/2] Added README for dragToPan --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 3f68fa4..06c6342 100644 --- a/README.md +++ b/README.md @@ -202,11 +202,25 @@ The _typenames_ is a string containing one or more _typename_ separated by white - `start` - after a new pointer becomes active (on mousedown or touchstart). - `drag` - after an active pointer moves (on mousemove or touchmove). - `end` - after an active pointer becomes inactive (on mouseup, touchend or touchcancel). +- `rangeChange` - after the range of the slider is changed +- `rangeStart` - after mousedown on the ticks area +- `rangeDrag` - after mousemove within the ticks area +- `rangeEnd` - after the range adjustment is inactive (mouseup) You might consider throttling `onchange` and `drag` events. For example using [`lodash.throttle`](https://lodash.com/docs/4.17.4#throttle). See [_dispatch_.on](https://github.com/d3/d3-dispatch#dispatch_on) for more. +# slider.dragToPan(value]) [<>](https://github.com/johnwalley/d3-simple-slider/blob/master/src/slider.js#L998 'Source') + +If _value_ is true, enables dragging on the ticks of the slider to change its range. +Dragging around the middle area for panning (moving both the minimum and the maximum). +Dragging near the left edge allows adjusting only the minimum, and dragging around near the right edge allows adjusting only the maximum. +The current slider knob—current value—is always kept within the range. +By default, panning is disabled. + + + ## 🤝 How to Contribute Please read the [contribution guidelines for this project](CONTRIBUTING.md)