├── .gitignore ├── minimal.html ├── d3RangeSlider.css ├── README.md ├── index.html ├── LICENSE └── d3RangeSlider.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /minimal.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /d3RangeSlider.css: -------------------------------------------------------------------------------- 1 | .slider-container { 2 | background-color: #f2f2f9; 3 | } 4 | 5 | .slider { 6 | position: absolute; 7 | border: 1px solid #AAB; 8 | background: #BCE; 9 | height: 100%; 10 | width: 58px; 11 | top: 0px; 12 | cursor: move; 13 | /*margin:-0.5px;*/ 14 | } 15 | 16 | .slider .handle { 17 | position: absolute; 18 | height: 9px; 19 | width: 9px; 20 | border: 1px solid #AAB; 21 | background: #9AC; 22 | 23 | /* Support for bootstrap */ 24 | box-sizing: border-box; 25 | -moz-box-sizing: border-box; 26 | -webkit-box-sizing: border-box; 27 | } 28 | 29 | .slider .EE { 30 | right: -4px; 31 | cursor: e-resize; 32 | } 33 | 34 | .slider .WW { 35 | cursor: w-resize; 36 | left: -4px; 37 | } 38 | 39 | .slider .EE, .slider .WW { 40 | top: 50%; 41 | margin-top: -4px; 42 | } 43 | 44 | .play-container .button{ 45 | fill-opacity: 0.0; 46 | stroke: #AAB; 47 | stroke-width: 1.6; 48 | } 49 | 50 | .play-container .play, .play-container .stop { 51 | fill: #BCE; 52 | stroke: #AAB; 53 | stroke-width: 1; 54 | } 55 | 56 | 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Range slider 2 | 3 | A small widget that allows the user to select a contiguous range of whole numbers 4 | using a slider. Check out [this site](https://rasmusfonseca.github.io/d3RangeSlider/) for a demo. The page 5 | `minimal.html` constitutes a minimal working example: 6 | ```html 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | 34 | 35 | ``` 36 | 37 | This creates a slider that spans the range from 0 - 100 (both inclusive) and adds it to the container-div. If you 38 | want diffent placements of the handles or background colors, the 39 | [supplied CSS](https://github.com/RasmusFonseca/d3RangeSlider/blob/master/d3RangeSlider.css) can easily be adapted. A 40 | couple of functions are defined on the `slider` object: 41 | 42 | `slider.range()` returns the currently selected range as an `{begin: number, end: number}`-object. 43 | 44 | `slider.range(s,b)` sets the range to span the interval from `s` to `b` (both included). If `s>b` the two numbers 45 | are swapped. If `s` or `b` are outside the range limits specified in the call to `createD3Rangeslider` a warning is 46 | printed in the console, and the values are clamped to the valid range limits. 47 | 48 | `slider.range(s)` moves the range without changing its width and so it starts at `s`. If the move causes the range to 49 | go outside the range limits a warning is printed in the console and the range moved back to the limit. 50 | 51 | `slider.onChange(callback)` adds a change-listener to the slider, so any UI modification or call to `range` triggers 52 | a call to `callback` with a single `{begin: number, end: number}`-argument that reflects the newly updated range. 53 | 54 | This example illustrates the use of these functions 55 | ```javascript 56 | // Create slider spanning the range from 0 to 10 57 | var slider = createD3RangeSlider(0, 10, "#slider-container"); 58 | 59 | // Range changes to 3-6 60 | slider.range(3,6); 61 | 62 | // Listener gets added 63 | slider.onChange(function(newRange){ 64 | console.log(newRange); 65 | }); 66 | 67 | // Range changes to 7-10 68 | // Warning is printed that you attempted to set a range (8-11) outside the limits (0-10) 69 | // "{begin: 7, end: 10}" is printed in the console because of the listener 70 | slider.range(8); 71 | 72 | // Access currently set range 73 | var curRange = slider.range(); 74 | 75 | // "7-10" is written to the current position in the document 76 | document.write(curRange.begin + "-" + curRange.end); 77 | ``` 78 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |<div id="slider-container"></div> 89 | <div id="range-label">0 - 10</div> 90 | 91 | <script type="text/javascript"> 92 | var slider = createD3RangeSlider(0, 100, "#slider-container"); 93 | 94 | slider.onChange(function(newRange){ 95 | d3.select("#range-label").text(newRange.begin + " - " + newRange.end); 96 | }); 97 | 98 | slider.range(0,10); 99 | </script> 100 |101 |
position: relative in the styleslider.range(10,20)
9 | *
10 | *
11 | *
12 | *
13 | *
14 | * The appearance can be changed with CSS, but the `position` must be `relative`, and the width of `.slider` should be
15 | * left unaltered.
16 | *
17 | * @param rangeMin Minimum value of the range
18 | * @param rangeMax Maximum value of the range
19 | * @param containerSelector A CSS selection indicating exactly one element in the document
20 | * @returns {{range: function(number, number), onChange: function(function)}}
21 | */
22 | function createD3RangeSlider (rangeMin, rangeMax, containerSelector, playButton) {
23 | "use strict";
24 |
25 | var minWidth = 10;
26 |
27 | var sliderRange = {begin: rangeMin, end: rangeMin};
28 | var changeListeners = [];
29 | var touchEndListeners = [];
30 | var container = d3.select(containerSelector);
31 | var playing = false;
32 | var resumePlaying = false; // Used by drag-events to resume playing on release
33 | var playingRate = 100;
34 | var containerHeight = container.node().offsetHeight;
35 |
36 | // Set up play button if requested
37 | if (playButton) {
38 | // Wrap an additional container inside the main one, and set up a box-layout, see also
39 | // http://stackoverflow.com/questions/14319097/css-auto-resize-div-to-fit-container-width
40 | var box = container.append("div")
41 | .style("display", "box")
42 | .style("display", "-moz-box")
43 | .style("display", "-webkit-box")
44 | .style("box-orient", "horizontal")
45 | .style("-moz-box-orient", "horizontal")
46 | .style("-webkit-box-orient", "horizontal");
47 |
48 | var playBox = box.append("div")
49 | .style("width", containerHeight + "px")
50 | .style("height", containerHeight + "px")
51 | .style("margin-right", "10px")
52 | .style("box-flex", "0")
53 | .style("-moz-box-flex", "0")
54 | .style("-webkit-box-flex", "0")
55 | .classed("play-container", true);
56 |
57 | var sliderBox = box.append("div")
58 | .style("position", "relative")
59 | .style("min-width", (minWidth*2) + "px")
60 | .style("height", containerHeight + "px")
61 | .style("box-flex", "1")
62 | .style("-moz-box-flex", "1")
63 | .style("-webkit-box-flex", "1")
64 | .classed("slider-container", true);
65 |
66 | var playSVG = playBox.append("svg")
67 | .attr("width", containerHeight + "px")
68 | .attr("height", containerHeight + "px")
69 | .style("overflow", "visible");
70 |
71 | var circleSymbol = playSVG.append("circle")
72 | .attr("cx", containerHeight / 2)
73 | .attr("cy", containerHeight / 2)
74 | .attr("r", containerHeight / 2)
75 | .classed("button", true);
76 |
77 | var h = containerHeight;
78 | var stopSymbol = playSVG.append("rect")
79 | .attr("x", 0.3*h)
80 | .attr("y", 0.3*h)
81 | .attr("width", 0.4*h)
82 | .attr("height", 0.4*h)
83 | .style("visibility", "hidden")
84 | .classed("stop", true);
85 |
86 | var playSymbol = playSVG.append("polygon")
87 | .attr("points", (0.37*h) + "," + (0.2*h) + " " + (0.37*h) + "," + (0.8*h) + " " + (0.75*h) + "," + (0.5*h))
88 | .classed("play", true);
89 |
90 | //Circle that captures mouse interactions
91 | playSVG.append("circle")
92 | .attr("cx", containerHeight / 2)
93 | .attr("cy", containerHeight / 2)
94 | .attr("r", containerHeight / 2)
95 | .style("fill-opacity", "0.0")
96 | .style("cursor", "pointer")
97 | .on("click", togglePlayButton)
98 | .on("mouseenter", function(){
99 | circleSymbol
100 | .transition()
101 | .attr("r", 1.2 * containerHeight / 2)
102 | .transition()
103 | .attr("r", containerHeight / 2);
104 | });
105 |
106 |
107 | } else {
108 | var sliderBox = container.append("div")
109 | .style("position", "relative")
110 | .style("height", containerHeight + "px")
111 | .style("min-width", (minWidth*2) + "px")
112 | .classed("slider-container", true);
113 | }
114 |
115 | //Create elements in container
116 | var slider = sliderBox
117 | .append("div")
118 | .attr("class", "slider");
119 | var handleW = slider.append("div").attr("class", "handle WW");
120 | var handleE = slider.append("div").attr("class", "handle EE");
121 |
122 | /** Update the `left` and `width` attributes of `slider` based on `sliderRange` */
123 | function updateUIFromRange () {
124 | var conW = sliderBox.node().clientWidth;
125 | var rangeW = sliderRange.end - sliderRange.begin;
126 | var slope = (conW - minWidth) / (rangeMax - rangeMin);
127 | var uirangeW = minWidth + rangeW * slope;
128 | var ratio = (sliderRange.begin - rangeMin) / (rangeMax - rangeMin - rangeW);
129 | if (isNaN(ratio)) {
130 | ratio = 0;
131 | }
132 | var uirangeL = ratio * (conW - uirangeW);
133 |
134 | slider
135 | .style("left", uirangeL + "px")
136 | .style("width", uirangeW + "px");
137 | }
138 |
139 | /** Update the `sliderRange` based on the `left` and `width` attributes of `slider` */
140 | function updateRangeFromUI () {
141 | var uirangeL = parseFloat(slider.style("left"));
142 | var uirangeW = parseFloat(slider.style("width"));
143 | var conW = sliderBox.node().clientWidth; //parseFloat(container.style("width"));
144 | var slope = (conW - minWidth) / (rangeMax - rangeMin);
145 | var rangeW = (uirangeW - minWidth) / slope;
146 | if (conW == uirangeW) {
147 | var uislope = 0;
148 | } else {
149 | var uislope = (rangeMax - rangeMin - rangeW) / (conW - uirangeW);
150 | }
151 | var rangeL = rangeMin + uislope * uirangeL;
152 | sliderRange.begin = Math.round(rangeL);
153 | sliderRange.end = Math.round(rangeL + rangeW);
154 |
155 | //Fire change listeners
156 | changeListeners.forEach(function (callback) {
157 | callback({begin: sliderRange.begin, end: sliderRange.end});
158 | });
159 | }
160 |
161 | // configure drag behavior for handles and slider
162 | var dragResizeE = d3.drag()
163 | .on("start", function () {
164 | d3.event.sourceEvent.stopPropagation();
165 | resumePlaying = playing;
166 | playing = false;
167 | })
168 | .on("end", function () {
169 | if (resumePlaying) {
170 | startPlaying();
171 | }
172 | touchEndListeners.forEach(function (callback) {
173 | callback({begin: sliderRange.begin, end: sliderRange.end});
174 | });
175 | })
176 | .on("drag", function () {
177 | var dx = d3.event.dx;
178 | if (dx == 0) return;
179 | var conWidth = sliderBox.node().clientWidth; //parseFloat(container.style("width"));
180 | var newLeft = parseInt(slider.style("left"));
181 | var newWidth = parseFloat(slider.style("width")) + dx;
182 | newWidth = Math.max(newWidth, minWidth);
183 | newWidth = Math.min(newWidth, conWidth - newLeft);
184 | slider.style("width", newWidth + "px");
185 | updateRangeFromUI();
186 | });
187 |
188 | var dragResizeW = d3.drag()
189 | .on("start", function () {
190 | this.startX = d3.mouse(this)[0];
191 | d3.event.sourceEvent.stopPropagation();
192 | resumePlaying = playing;
193 | playing = false;
194 | })
195 | .on("end", function () {
196 | if (resumePlaying) {
197 | startPlaying();
198 | }
199 | touchEndListeners.forEach(function (callback) {
200 | callback({begin: sliderRange.begin, end: sliderRange.end});
201 | });
202 | })
203 | .on("drag", function () {
204 | var dx = d3.mouse(this)[0] - this.startX;
205 | if (dx==0) return;
206 | var newLeft = parseFloat(slider.style("left")) + dx;
207 | var newWidth = parseFloat(slider.style("width")) - dx;
208 |
209 | if (newLeft < 0) {
210 | newWidth += newLeft;
211 | newLeft = 0;
212 | }
213 | if (newWidth < minWidth) {
214 | newLeft -= minWidth - newWidth;
215 | newWidth = minWidth;
216 | }
217 |
218 | slider.style("left", newLeft + "px");
219 | slider.style("width", newWidth + "px");
220 |
221 | updateRangeFromUI();
222 | });
223 |
224 | var dragMove = d3.drag()
225 | .on("start", function () {
226 | d3.event.sourceEvent.stopPropagation();
227 | resumePlaying = playing;
228 | playing = false;
229 | })
230 | .on("end", function () {
231 | if (resumePlaying) {
232 | startPlaying();
233 | }
234 | touchEndListeners.forEach(function (callback) {
235 | callback({begin: sliderRange.begin, end: sliderRange.end});
236 | });
237 | })
238 | .on("drag", function () {
239 | var dx = d3.event.dx;
240 | var conWidth = sliderBox.node().clientWidth; //parseInt(container.style("width"));
241 | var newLeft = parseInt(slider.style("left")) + dx;
242 | var newWidth = parseInt(slider.style("width"));
243 |
244 | newLeft = Math.max(newLeft, 0);
245 | newLeft = Math.min(newLeft, conWidth - newWidth);
246 | slider.style("left", newLeft + "px");
247 |
248 | updateRangeFromUI();
249 | });
250 |
251 | handleE.call(dragResizeE);
252 | handleW.call(dragResizeW);
253 | slider.call(dragMove);
254 |
255 | //Click on bar
256 | sliderBox.on("mousedown", function (ev) {
257 | var x = d3.mouse(sliderBox.node())[0];
258 | var props = {};
259 | var sliderWidth = parseFloat(slider.style("width"));
260 | var conWidth = sliderBox.node().clientWidth; //parseFloat(container.style("width"));
261 | props.left = Math.min(conWidth - sliderWidth, Math.max(x - sliderWidth / 2, 0));
262 | props.left = Math.round(props.left);
263 | props.width = Math.round(props.width);
264 | slider.style("left", props.left + "px")
265 | .style("width", props.width + "px");
266 | updateRangeFromUI();
267 | });
268 |
269 | //Reposition slider on window resize
270 | window.addEventListener("resize", function () {
271 | updateUIFromRange();
272 | });
273 |
274 | function onChange(callback){
275 | changeListeners.push(callback);
276 | return this;
277 | }
278 |
279 | function onTouchEnd(callback){
280 | touchEndListeners.push(callback);
281 | return this;
282 | }
283 |
284 | function setRange (b, e) {
285 | sliderRange.begin = b;
286 | sliderRange.end = e;
287 |
288 | updateUIFromRange();
289 |
290 | //Fire change listeners
291 | changeListeners.forEach(function (callback) {
292 | callback({begin: sliderRange.begin, end: sliderRange.end});
293 | });
294 | }
295 |
296 |
297 | /**
298 | * Returns or sets the range depending on arguments.
299 | * If `b` and `e` are both numbers then the range is set to span from `b` to `e`.
300 | * If `b` is a number and `e` is undefined the beginning of the slider is moved to `b`.
301 | * If both `b` and `e` are undefined the currently set range is returned as an object with `begin` and `end`
302 | * attributes.
303 | * If any arguments cause the range to be outside of the `rangeMin` and `rangeMax` specified on slider creation
304 | * then a warning is printed and the range correspondingly clamped.
305 | * @param b beginning of range
306 | * @param e end of range
307 | * @returns {{begin: number, end: number}}
308 | */
309 | function range(b, e) {
310 | var rLower;
311 | var rUpper;
312 |
313 | if (typeof b === "number" && typeof e === "number") {
314 |
315 | rLower = Math.min(b, e);
316 | rUpper = Math.max(b, e);
317 |
318 | //Check that lower and upper range are within their bounds
319 | if (rLower < rangeMin || rUpper > rangeMax) {
320 | console.log("Warning: trying to set range (" + rLower + "," + rUpper + ") which is outside of bounds (" + rangeMin + "," + rangeMax + "). ");
321 | rLower = Math.max(rLower, rangeMin);
322 | rUpper = Math.min(rUpper, rangeMax);
323 | }
324 |
325 | //Set the range
326 | setRange(rLower, rUpper);
327 | } else if (typeof b === "number") {
328 |
329 | rLower = b;
330 | var dif = sliderRange.end - sliderRange.begin;
331 | rUpper = rLower + dif;
332 |
333 | if (rLower < rangeMin) {
334 | console.log("Warning: trying to set range (" + rLower + "," + rUpper + ") which is outside of bounds (" + rangeMin + "," + rangeMax + "). ");
335 | rLower = rangeMin;
336 | }
337 | if(rUpper > rangeMax){
338 | console.log("Warning: trying to set range (" + rLower + "," + rUpper + ") which is outside of bounds (" + rangeMin + "," + rangeMax + "). ");
339 | rLower = rangeMax - dif;
340 | rUpper = rangeMax;
341 | }
342 |
343 | setRange(rLower, rUpper);
344 | }
345 |
346 | return {begin: sliderRange.begin, end: sliderRange.end};
347 | }
348 |
349 | function togglePlayButton () {
350 | if (playing) {
351 | stopPlaying();
352 | } else {
353 | startPlaying();
354 | }
355 | }
356 |
357 | function frameTick() {
358 | if (!playing) {
359 | return;
360 | }
361 |
362 | var limitWidth = rangeMax - rangeMin + 1;
363 | var rangeWidth = sliderRange.end - sliderRange.begin + 1;
364 | var delta = Math.min(Math.ceil(rangeWidth / 10), Math.ceil(limitWidth / 100));
365 |
366 | // Check if playback has reached the end
367 | if (sliderRange.end + delta > rangeMax) {
368 | delta = rangeMax - sliderRange.end;
369 | stopPlaying();
370 | }
371 |
372 | setRange(sliderRange.begin + delta, sliderRange.end + delta);
373 |
374 | setTimeout(frameTick, playingRate);
375 | }
376 |
377 | function startPlaying(rate) {
378 | if (rate !== undefined) {
379 | playingRate = rate;
380 | }
381 |
382 | if (playing) {
383 | return;
384 | }
385 |
386 | playing = true;
387 | if (playButton) {
388 | playSymbol.style("visibility", "hidden");
389 | stopSymbol.style("visibility", "visible");
390 | }
391 | frameTick();
392 | }
393 |
394 | function stopPlaying() {
395 | playing = false;
396 | if (playButton) {
397 | playSymbol.style("visibility", "visible");
398 | stopSymbol.style("visibility", "hidden");
399 | }
400 | }
401 |
402 | setRange(sliderRange.begin, sliderRange.end);
403 |
404 | return {
405 | range: range,
406 | startPlaying: startPlaying,
407 | stopPlaying: stopPlaying,
408 | onChange: onChange,
409 | onTouchEnd: onTouchEnd,
410 | updateUIFromRange: updateUIFromRange
411 | };
412 | }
413 |
--------------------------------------------------------------------------------