Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: panning the slider range by dragging #144

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<a href="#slider_dragToPan" name="slider_dragToPan">#</a> <i>slider</i>.<b>dragToPan</b>(<i>value</i>]) [<>](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)
29 changes: 29 additions & 0 deletions example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ <h2>Transition</h2>
<div class="col-sm-2"><p id="value-transition"></p></div>
<div class="col-sm"><div id="slider-transition"></div></div>
</div>
<h2>Drag to pan & zoom</h2>
<div class="row align-items-center">
<div class="col-sm-2"><p id="value-drag-to-pan"></p></div>
<div class="col-sm"><div id="slider-drag-to-pan"></div></div>
</div>
<h1>Examples</h1>
<h2>New York Times</h2>
<div class="row align-items-center">
Expand Down Expand Up @@ -454,4 +459,28 @@ <h2>Color picker</h2>
});

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());
</script>
215 changes: 213 additions & 2 deletions src/slider.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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())
Expand All @@ -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
Expand Down Expand Up @@ -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');
}
Expand Down Expand Up @@ -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;
}

Expand Down