diff --git a/README.md b/README.md
index 3f68fa4..72f98fd 100644
--- a/README.md
+++ b/README.md
@@ -202,11 +202,31 @@ 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.
+
+
+# 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
Please read the [contribution guidelines for this project](CONTRIBUTING.md)
diff --git a/example/index.html b/example/index.html
index 0937d9f..570509a 100644
--- a/example/index.html
+++ b/example/index.html
@@ -58,6 +58,16 @@
Transition
+ Drag to pan & zoom
+
+ Show difference
+
Examples
New York Times
@@ -454,4 +464,53 @@
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());
+
+ 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 c145555..80a79e4 100644
--- a/src/slider.js
+++ b/src/slider.js
@@ -41,8 +41,23 @@ 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 diffValue = null;
+
+ var listeners = dispatch(
+ 'onchange',
+ 'start',
+ 'end',
+ 'drag',
+ 'rangeChange',
+ 'rangeStart',
+ 'rangeDrag',
+ 'rangeEnd'
+ );
var selection = null;
var identityClamped = null;
@@ -79,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())];
@@ -108,6 +124,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 +150,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
@@ -271,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')
@@ -429,6 +559,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');
}
@@ -437,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) {
@@ -599,6 +876,12 @@ function slider(orientation, scale) {
return displayFormat(newValue[idx]);
});
}
+
+ if (diffValue) {
+ selection
+ .select('.diff-highlight')
+ .attr(x + '1', scale(value[0]))
+ }
}
}
@@ -790,6 +1073,18 @@ function slider(orientation, scale) {
return value === listeners ? slider : value;
};
+ slider.dragToPan = function (_) {
+ if (!arguments.length) return dragToPan;
+ dragToPan = _;
+ return slider;
+ };
+
+ slider.showDiff = function(_) {
+ if (!arguments.length) return;
+ diffValue = _;
+ return slider;
+ }
+
return slider;
}