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/4] 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/4] 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) From 60ac0f134644fb51bee7471bd5d00b3a41dea0b6 Mon Sep 17 00:00:00 2001 From: Robby Prima Suherlan Date: Fri, 21 Aug 2020 14:50:33 +0700 Subject: [PATCH 3/4] feat: show difference between slider value --- example/index.html | 30 ++++++++++++++++ src/slider.js | 86 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/example/index.html b/example/index.html index 745d2a7..570509a 100644 --- a/example/index.html +++ b/example/index.html @@ -63,6 +63,11 @@

Drag to pan & zoom

+

Show difference

+
+

+
+

Examples

New York Times

@@ -483,4 +488,29 @@

Color picker

dragToPanG.call(dragToPanSlider); d3.select('p#value-drag-to-pan').text(dragToPanSlider.value()); + + var showDiffSlider = d3 + .sliderBottom() + .min(0) + .max(50) + .width(300) + .step(1) + .default(5) + .showDiff(10) + .default(25) + .on('onchange', function (val) { + d3.select('p#value-diff').text(val); + }); + + var showDiffWrapper = d3 + .select('#slider-diff') + .append('svg') + .attr('width', 500) + .attr('height', 100) + .append('g') + .attr('transform', 'translate(30,30)'); + + showDiffWrapper.call(showDiffSlider); + + d3.select('p#value-diff').text(showDiffSlider.value()); diff --git a/src/slider.js b/src/slider.js index 7ace9ef..80a79e4 100644 --- a/src/slider.js +++ b/src/slider.js @@ -46,6 +46,7 @@ function slider(orientation, scale) { var lastAnimation = null; var dragModeFn = null; var dragToPan = false; + var diffValue = null; var listeners = dispatch( 'onchange', @@ -93,6 +94,7 @@ function slider(orientation, scale) { var handleSelection = null; var fillSelection = null; var textSelection = null; + var diffSelection = null; if (scale) { domain = [min(scale.domain()), max(scale.domain())]; @@ -335,6 +337,70 @@ function slider(orientation, scale) { } }); + if (diffValue) { + diffSelection = sliderEnter + .selectAll('.diff-overlay') + .data([diffValue]) + + var diffEnter = diffSelection + .enter() + .insert('g', '.parameter-value') + .attr('class', 'diff-overlay') + .attr( + 'text-anchor', + orientation === right + ? 'start' + : orientation === left + ? 'end' + : 'middle' + ); + + diffEnter + .append('line') + .attr('class', 'diff-highlight') + .attr('stroke', 'orange') + .attr('stroke-width', 4) + .attr('stroke-linecap', 'round') + + diffEnter + .append('path') + .attr('transform', 'rotate(' + (orientation + 1) * 90 + ')') + .attr('class', 'diff-handle') + .attr('d', handle) + .attr('fill', 'orange') + .attr('stroke', '#777') + + diffEnter + .append('text') + .attr('class', 'diff-value') + .attr('font-size', 10) // TODO: Remove coupling to font-size in d3-axis + .attr(y, k * 27) + .attr( + 'dy', + orientation === top + ? '0em' + : orientation === bottom + ? '.71em' + : '.32em' + ) + + context + .select('.diff-highlight') + .attr(x + '1', scale(value[0])) + .attr(x + '2', scale(diffValue)) + + context + .select('.diff-handle') + .attr('transform', function() { + return transformAlong(scale(diffValue)) + ' rotate(' + (orientation + 1) * 90 + ')' + }) + + context + .select('.diff-value') + .attr('transform', transformAlong(scale(diffValue))) + .text(tickFormat(diffValue)) + } + if (displayValue) { handleEnter .append('text') @@ -642,7 +708,13 @@ function slider(orientation, scale) { if (selection) { if (displayValue) { var indices = []; - value.forEach(function (val) { + var targetValues = value; + + if (diffValue) { + targetValues = value.concat(diffValue); + } + + targetValues.forEach(function (val) { var distances = []; selection.selectAll('.axis .tick').each(function (d) { @@ -804,6 +876,12 @@ function slider(orientation, scale) { return displayFormat(newValue[idx]); }); } + + if (diffValue) { + selection + .select('.diff-highlight') + .attr(x + '1', scale(value[0])) + } } } @@ -1001,6 +1079,12 @@ function slider(orientation, scale) { return slider; }; + slider.showDiff = function(_) { + if (!arguments.length) return; + diffValue = _; + return slider; + } + return slider; } From a8a7cae32db6706b91e856f78fac193c468f9ae0 Mon Sep 17 00:00:00 2001 From: Chat Wacharamanotham Date: Sat, 22 Aug 2020 15:07:11 +0200 Subject: [PATCH 4/4] Added README for showDiff() --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 06c6342..72f98fd 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,12 @@ The current slider knob—current value—is always kept within the range. By default, panning is disabled. +# slider.showDiff(value]) [<>](https://github.com/johnwalley/d3-simple-slider/blob/master/src/slider.js#L1082 'Source') + +Highlights a difference between the current value and the provided _value_. +To remove the highlight, set _value_ to `null`. +This feature is currently not supported for with a range slider. + ## 🤝 How to Contribute