Skip to content

Instantly share code, notes, and snippets.

@henryjameslau
Last active April 3, 2019 06:42
Show Gist options
  • Save henryjameslau/b5bdcd52cd51694903ef9768b236b3d9 to your computer and use it in GitHub Desktop.
Save henryjameslau/b5bdcd52cd51694903ef9768b236b3d9 to your computer and use it in GitHub Desktop.
d3-simple-slider with blue background box
license: bsd-3-clause
scrolling: yes
border: no

Renders a simple interactive slider with added accessibility features.

  • Handle is focusable, has tabindex and aria-label
  • Up/right arrows increase amount by a small bit
  • Down/left arrows decrease amount by a small bit
  • PageUp/PageDown changes amount by a larger bit
  • Home/End move handle to start/end of range.

Inspired by The New York Times Is It Better to Rent or Buy?

Source code and documentation available on Github.

forked from johnwalley's block: d3-simple-slider

forked from henryjameslau's block: d3-simple-slider with added accessibility features

// https://github.com/johnwalley/d3-simple-slider Version 1.4.1. Copyright 2018 John Walley.
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-array'), require('d3-axis'), require('d3-dispatch'), require('d3-drag'), require('d3-ease'), require('d3-scale'), require('d3-selection'), require('d3-transition')) :
typeof define === 'function' && define.amd ? define(['exports', 'd3-array', 'd3-axis', 'd3-dispatch', 'd3-drag', 'd3-ease', 'd3-scale', 'd3-selection', 'd3-transition'], factory) :
(global = global || self, factory(global.d3 = global.d3 || {}, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3));
}(this, function (exports, d3Array, d3Axis, d3Dispatch, d3Drag, d3Ease, d3Scale, d3Selection) { 'use strict';
var UPDATE_DURATION = 200;
var SLIDER_END_PADDING = 8;
var top = 1;
var right = 2;
var bottom = 3;
var left = 4;
function translateX(x) {
return 'translate(' + x + ',0)';
}
function translateY(y) {
return 'translate(0,' + y + ')';
}
function slider(orientation, scale) {
scale = typeof scale !== 'undefined' ? scale : null;
var value = [0];
var defaultValue = [0];
var domain = [0, 10];
var width = 100;
var height = 100;
var displayValue = true;
var handle = 'M-5.5,-5.5v10l6,5.5l6,-5.5v-10z';
var step = null;
var tickValues = null;
var marks = null;
var tickFormat = null;
var ticks = null;
var displayFormat = null;
var fill = null;
var listeners = d3Dispatch.dispatch('onchange', 'start', 'end', 'drag');
var selection = null;
var identityClamped = null;
var handleIndex = null;
var k = orientation === top || orientation === left ? -1 : 1;
var x = orientation === left || orientation === right ? 'y' : 'x';
var y = orientation === left || orientation === right ? 'x' : 'y';
var transformAlong =
orientation === top || orientation === bottom ? translateX : translateY;
var transformAcross =
orientation === top || orientation === bottom ? translateY : translateX;
var axisFunction = null;
switch (orientation) {
case top:
axisFunction = d3Axis.axisTop;
break;
case right:
axisFunction = d3Axis.axisRight;
break;
case bottom:
axisFunction = d3Axis.axisBottom;
break;
case left:
axisFunction = d3Axis.axisLeft;
break;
}
var handleSelection = null;
var fillSelection = null;
var textSelection = null;
if (scale) {
domain = [d3Array.min(scale.domain()), d3Array.max(scale.domain())];
if (orientation === top || orientation === bottom) {
width = d3Array.max(scale.range()) - d3Array.min(scale.range());
} else {
height = d3Array.max(scale.range()) - d3Array.min(scale.range());
}
scale = scale.clamp(true);
}
function slider(context) {
selection = context.selection ? context.selection() : context;
if (scale) {
scale = scale.range([
d3Array.min(scale.range()),
d3Array.min(scale.range()) +
(orientation === top || orientation === bottom ? width : height),
]);
} else {
scale = domain[0] instanceof Date ? d3Scale.scaleTime() : d3Scale.scaleLinear();
scale = scale
.domain(domain)
.range([
0,
orientation === top || orientation === bottom ? width : height,
])
.clamp(true);
}
identityClamped = d3Scale.scaleLinear()
.range(scale.range())
.domain(scale.range())
.clamp(true);
// Ensure value is valid
value = value.map(function(d) {
return d3Scale.scaleLinear()
.range(domain)
.domain(domain)
.clamp(true)(d);
});
tickFormat = tickFormat || scale.tickFormat();
displayFormat = displayFormat || tickFormat || scale.tickFormat();
var axis = selection.selectAll('.axis').data([null]);
axis
.enter()
.append('g')
.attr('transform', transformAcross(k * 7))
.attr('class', 'axis');
var slider = selection.selectAll('.slider').data([null]);
var sliderEnter = slider
.enter()
.append('g')
.attr('class', 'slider')
.attr(
'cursor',
orientation === top || orientation === bottom
? 'ew-resize'
: 'ns-resize'
)
.call(
d3Drag.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended)
);
// sliderEnter
// .append('line')
// .attr('class', 'track')
// .attr(x + '1', scale.range()[0] - SLIDER_END_PADDING)
// .attr('stroke', '#bbb')
// .attr('stroke-width', 12)
// .attr('stroke-linecap', 'round');
sliderEnter
.append('line')
.attr('class', 'track-inset')
.attr(x + '1', scale.range()[0] - SLIDER_END_PADDING)
.attr('stroke', '#eee')
.attr('stroke-width', 10)
.attr('stroke-linecap', 'round');
if (fill) {
sliderEnter
.append('line')
.attr('class', 'track-fill')
.attr(
x + '1',
value.length === 1
? scale.range()[0] - SLIDER_END_PADDING
: scale(value[0])
)
.attr('stroke', fill)
.attr('stroke-width', 10)
.attr('stroke-linecap', 'round');
}
sliderEnter
.append('line')
.attr('class', 'track-overlay')
.attr(x + '1', scale.range()[0] - SLIDER_END_PADDING)
.attr('stroke', 'transparent')
.attr('stroke-width', 40)
.attr('stroke-linecap', 'round')
.merge(slider.select('.track-overlay'));
handleSelection = sliderEnter.selectAll('.parameter-value').data(value);
var handleEnter = handleSelection
.enter()
.append('g')
.attr('class', 'parameter-value')
.attr('transform', function(d) {
return transformAlong(scale(d));
})
.attr('font-family', 'sans-serif')
.attr(
'text-anchor',
orientation === right
? 'start'
: orientation === left
? 'end'
: 'middle'
);
handleEnter
.append('path')
.attr('transform', 'rotate(' + (orientation + 1) * 90 + ')')
.attr('d', handle)
.attr('focusable','true')
.attr('tabindex',0)
.attr('id','handle')
.attr('aria-label','handle')
.attr('fill', 'white')
.attr('stroke', '#777');
handleEnter
.append('path')
.attr('transform', 'translate(0 24)')
.attr('d', d3.symbol()
.type(d3.symbolTriangle)
.size(150))
.style('fill', '#206095')
.style('stroke',"none")
if (displayValue && value.length === 1) {
handleEnter
.append('text')
.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'
)
.text(tickFormat(value[0]));
var text=handleEnter.select('text')
var bbox = text.node().getBBox();
handleEnter.select('text').remove()
var padding = 5;
var rect = handleEnter.append("rect")
.attr("x", bbox.x - padding)
.attr("y", bbox.y - padding)
.attr("ry",5)
.attr("width", bbox.width + (padding*2))
.attr("height", bbox.height + (padding*2))
.style("fill", "#206095");
handleEnter
.append('text')
.text(tickFormat(value[0]))
.style('fill','white')
.attr('font-weight',700)
.attr('y',text.attr('y'))
.attr('dy',text.attr('dy'))
}
context
.select('.track')
.attr(x + '2', scale.range()[1] + SLIDER_END_PADDING);
context
.select('.track-inset')
.attr(x + '2', scale.range()[1] + SLIDER_END_PADDING);
if (fill) {
context
.select('.track-fill')
.attr(x + '2', value.length === 1 ? scale(value[0]) : scale(value[1]));
}
context
.select('.track-overlay')
.attr(x + '2', scale.range()[1] + SLIDER_END_PADDING);
context.select('.axis').call(
axisFunction(scale)
.tickFormat(tickFormat)
.ticks(ticks)
.tickValues(tickValues)
);
// https://bl.ocks.org/mbostock/4323929
selection
.select('.axis')
.select('.domain')
.remove();
context.select('.axis').attr('transform', transformAcross(k * 7));
context
.selectAll('.axis 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'
);
context.selectAll('.axis line').attr('stroke', '#aaa');
context.selectAll('.parameter-value').attr('transform', function(d) {
return transformAlong(scale(d));
});
fadeTickText();
function dragstarted() {
d3Selection.select(this).classed('active', true);
var pos = identityClamped(
orientation === bottom || orientation === top ? d3Selection.event.x : d3Selection.event.y
);
handleIndex = d3Array.scan(
value.map(function(d) {
return Math.abs(d - alignedValue(scale.invert(pos)));
})
);
var newValue = value.map(function(d, i) {
return i === handleIndex ? alignedValue(scale.invert(pos)) : d;
});
updateHandle(newValue);
listeners.call(
'start',
slider,
newValue.length === 1 ? newValue[0] : newValue
);
updateValue(newValue, true);
}
function dragged() {
var pos = identityClamped(
orientation === bottom || orientation === top ? d3Selection.event.x : d3Selection.event.y
);
var adjustedValue = alignedValue(scale.invert(pos));
var newValue = value.map(function(d, i) {
if (value.length === 2) {
return i === handleIndex
? handleIndex === 0
? Math.min(adjustedValue, alignedValue(value[1]))
: Math.max(adjustedValue, alignedValue(value[0]))
: d;
} else {
return i === handleIndex ? adjustedValue : d;
}
});
updateHandle(newValue);
listeners.call(
'drag',
slider,
newValue.length === 1 ? newValue[0] : newValue
);
updateValue(newValue, true);
}
function dragended() {
d3Selection.select(this).classed('active', false);
var pos = identityClamped(
orientation === bottom || orientation === top ? d3Selection.event.x : d3Selection.event.y
);
var newValue = value.map(function(d, i) {
return i === handleIndex ? alignedValue(scale.invert(pos)) : d;
});
updateHandle(newValue);
listeners.call(
'end',
slider,
newValue.length === 1 ? newValue[0] : newValue
);
updateValue(newValue, true);
handleIndex = null;
}
textSelection = selection.select('.parameter-value text');
fillSelection = selection.select('.track-fill');
}
function fadeTickText() {
if (displayValue && value.length === 1) {
var distances = [];
selection.selectAll('.axis .tick').each(function(d) {
distances.push(Math.abs(d - value[0]));
});
var index = d3Array.scan(distances);
selection.selectAll('.axis .tick text').attr('opacity', function(d, i) {
return i === index ? 0 : 1;
});
}
}
function alignedValue(newValue) {
if (step) {
var valueModStep = (newValue - domain[0]) % step;
var alignValue = newValue - valueModStep;
if (valueModStep * 2 > step) {
alignValue += step;
}
return newValue instanceof Date ? new Date(alignValue) : alignValue;
}
if (marks) {
var index = d3Array.scan(
marks.map(function(d) {
return Math.abs(newValue - d);
})
);
return marks[index];
}
return newValue;
}
function updateValue(newValue, notifyListener) {
if (value !== newValue) {
value = newValue;
if (notifyListener) {
listeners.call(
'onchange',
slider,
newValue.length === 1 ? newValue[0] : newValue
);
}
fadeTickText();
}
}
function updateHandle(newValue, animate) {
animate = typeof animate !== 'undefined' ? animate : false;
if (animate) {
selection
.selectAll('.parameter-value')
.data(newValue)
.transition()
.ease(d3Ease.easeQuadOut)
.duration(UPDATE_DURATION)
.attr('transform', function(d) {
return transformAlong(scale(d));
});
if (fill) {
fillSelection
.transition()
.ease(d3Ease.easeQuadOut)
.duration(UPDATE_DURATION)
.attr(
x + '1',
value.length === 1
? scale.range()[0] - SLIDER_END_PADDING
: scale(newValue[0])
)
.attr(
x + '2',
value.length === 1 ? scale(newValue[0]) : scale(newValue[1])
);
}
} else {
selection
.selectAll('.parameter-value')
.data(newValue)
.attr('transform', function(d) {
return transformAlong(scale(d));
});
if (fill) {
fillSelection
.attr(
x + '1',
value.length === 1
? scale.range()[0] - SLIDER_END_PADDING
: scale(newValue[0])
)
.attr(
x + '2',
value.length === 1 ? scale(newValue[0]) : scale(newValue[1])
);
}
}
if (displayValue) {
textSelection.text(displayFormat(newValue[0]));
}
}
slider.min = function(_) {
if (!arguments.length) return domain[0];
domain[0] = _;
return slider;
};
slider.max = function(_) {
if (!arguments.length) return domain[1];
domain[1] = _;
return slider;
};
slider.domain = function(_) {
if (!arguments.length) return domain;
domain = _;
return slider;
};
slider.width = function(_) {
if (!arguments.length) return width;
width = _;
return slider;
};
slider.height = function(_) {
if (!arguments.length) return height;
height = _;
return slider;
};
slider.tickFormat = function(_) {
if (!arguments.length) return tickFormat;
tickFormat = _;
return slider;
};
slider.displayFormat = function(_) {
if (!arguments.length) return displayFormat;
displayFormat = _;
return slider;
};
slider.ticks = function(_) {
if (!arguments.length) return ticks;
ticks = _;
return slider;
};
slider.value = function(_) {
if (!arguments.length) {
if (value.length === 1) {
return value[0];
}
return value;
}
var toArray = Array.isArray(_) ? _ : [_];
var pos = toArray.map(scale).map(identityClamped);
var newValue = pos.map(scale.invert).map(alignedValue);
updateHandle(newValue, true);
updateValue(newValue, true);
return slider;
};
slider.silentValue = function(_) {
if (!arguments.length) {
if (value.length === 1) {
return value[0];
}
return value;
}
var toArray = Array.isArray(_) ? _ : [_];
var pos = toArray.map(scale).map(identityClamped);
var newValue = pos.map(scale.invert).map(alignedValue);
updateHandle(newValue, false);
updateValue(newValue, false);
return slider;
};
slider.default = function(_) {
if (!arguments.length) {
if (defaultValue.length === 1) {
return defaultValue[0];
}
return defaultValue;
}
var toArray = Array.isArray(_) ? _ : [_];
defaultValue = toArray;
value = toArray;
return slider;
};
slider.step = function(_) {
if (!arguments.length) return step;
step = _;
return slider;
};
slider.tickValues = function(_) {
if (!arguments.length) return tickValues;
tickValues = _;
return slider;
};
slider.marks = function(_) {
if (!arguments.length) return marks;
marks = _;
return slider;
};
slider.handle = function(_) {
if (!arguments.length) return handle;
handle = _;
return slider;
};
slider.displayValue = function(_) {
if (!arguments.length) return displayValue;
displayValue = _;
return slider;
};
slider.fill = function(_) {
if (!arguments.length) return fill;
fill = _;
return slider;
};
slider.on = function() {
var value = listeners.on.apply(listeners, arguments);
return value === listeners ? slider : value;
};
return slider;
}
function sliderHorizontal(scale) {
return slider(bottom, scale);
}
function sliderVertical(scale) {
return slider(left, scale);
}
function sliderTop(scale) {
return slider(top, scale);
}
function sliderRight(scale) {
return slider(right, scale);
}
function sliderBottom(scale) {
return slider(bottom, scale);
}
function sliderLeft(scale) {
return slider(left, scale);
}
exports.sliderHorizontal = sliderHorizontal;
exports.sliderVertical = sliderVertical;
exports.sliderTop = sliderTop;
exports.sliderRight = sliderRight;
exports.sliderBottom = sliderBottom;
exports.sliderLeft = sliderLeft;
Object.defineProperty(exports, '__esModule', { value: true });
}));
<!DOCTYPE html>
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<title>d3-simple-slider</title>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="d3-simple-slider.js"></script>
<link href='https://fonts.googleapis.com/css?family=Open+Sans:400,700' rel='stylesheet' type='text/css'>
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
crossorigin="anonymous"
/>
<style>
body{
max-width:700px;
}
.slider text{
font-size: 18px;
}
#slider-simple text{
font-size: 18px;
}
.parameter-value path{
fill:#0F8243;
stroke:white;
stroke-width:1.5px;
}
.parameter-value path:focus{
fill:#0F8243;
stroke:orange;
stroke-width:3px;
}
input{
border: none;
border-radius: 0;
border-bottom: solid 1px #ddd;
font-size: 18px;
line-height: 32px;
margin:16px 0;
padding: 6px 0 0 0;
}
input[type="number"]::-webkit-outer-spin-button, input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
-moz-appearance: textfield;
}
p{
line-height: 32px;
font-size: 18px;
margin:16px 0;
padding: 6px 0 10px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>Blue callout box</h1>
<div class="row align-items-center">
<div class="col-sm-6"><p>Label for number</p></div>
<div class="col-sm-6"><input id="value-simple" type="number" value="175000" oninput="sliderchange()" min="0" max="400000"></div>
<div class="col-sm-12"><div id="slider-simple"></div></div>
</div>
</div>
<script>
// Simple
var sliderSimple = d3
.sliderBottom()
.min(0)
.max(400000)
.width(parseInt(d3.select('body').style("width"))-80)
.tickFormat(d3.format(',.0f'))
.ticks(5)
.default(175000)
.handle(
d3.symbol()
.type(d3.symbolCircle)
.size(500)
)
.fill("#206595")
.on('onchange', val => {
document.getElementById("value-simple").value=d3.format('.0f')(val)
});
var gSimple = d3
.select('div#slider-simple')
.append('svg')
.attr('width', parseInt(d3.select('body').style("width")))
.attr('height', 100)
.append('g')
.attr('transform', 'translate(40,30)');
gSimple.call(sliderSimple);
document.getElementById("value-simple").value=d3.format('.0f')(sliderSimple.value());
//
// var text = d3.select('g.parameter-value text')
// var bbox = text.node().getBBox();
// var padding = 5;
// var rect = d3.select('g.parameter-value').append("rect")
// .attr("x", bbox.x - padding)
// .attr("y", bbox.y - padding)
// .attr("ry",5)
// .attr("width", bbox.width + (padding*2))
// .attr("height", bbox.height + (padding*2))
// .style("fill", "#206095");
// console.log(text)
// d3.select('g.parameter-value')
// .append('text')
// .text(text._groups[0][0].innerHTML)
// .style('fill','white')
// .attr('y',text.attr('y'))
// .attr('dy',text.attr('dy'))
function sliderchange(){
sliderSimple.silentValue(document.getElementById('value-simple').value)
}
d3.select('body').on('keydown',function(){
if(document.getElementById("handle")===document.activeElement){//if handle is focussed
var max = document.getElementById('value-simple').max
var min = document.getElementById('value-simple').min
if (d3.event.key=='ArrowLeft') {
if(+document.getElementById('value-simple').value-100<min){
sliderSimple.silentValue(min)
document.getElementById("value-simple").value=min
}else{
sliderSimple.silentValue(+document.getElementById('value-simple').value-100)
document.getElementById("value-simple").value=+document.getElementById("value-simple").value-100
}
}
if (d3.event.key=='ArrowUp') {
d3.event.preventDefault();
if(+document.getElementById('value-simple').value+100>max){
sliderSimple.silentValue(max)
document.getElementById("value-simple").value=max
}else{
sliderSimple.silentValue(+document.getElementById('value-simple').value+100)
document.getElementById("value-simple").value=+document.getElementById("value-simple").value+100
}
}
if (d3.event.key=='ArrowRight') {
if(+document.getElementById('value-simple').value+100>max){
sliderSimple.silentValue(max)
document.getElementById("value-simple").value=max
}else{
sliderSimple.silentValue(+document.getElementById('value-simple').value+100)
document.getElementById("value-simple").value=+document.getElementById("value-simple").value+100
} }
if (d3.event.key=='ArrowDown') {
d3.event.preventDefault();
if(+document.getElementById('value-simple').value-100<min){
sliderSimple.silentValue(min)
document.getElementById("value-simple").value=min
}else{
sliderSimple.silentValue(+document.getElementById('value-simple').value-100)
document.getElementById("value-simple").value=+document.getElementById("value-simple").value-100
}
}
if (d3.event.key=='PageDown') {
d3.event.preventDefault();
if(+document.getElementById('value-simple').value-1000<min){
sliderSimple.silentValue(min)
document.getElementById("value-simple").value=min
}else{
sliderSimple.silentValue(+document.getElementById('value-simple').value-1000)
document.getElementById("value-simple").value=+document.getElementById("value-simple").value-1000
}
}
if (d3.event.key=='PageUp') {
d3.event.preventDefault();
if(+document.getElementById('value-simple').value+1000>max){
sliderSimple.silentValue(max)
document.getElementById("value-simple").value=max
}else{
sliderSimple.silentValue(+document.getElementById('value-simple').value+1000)
document.getElementById("value-simple").value=+document.getElementById("value-simple").value+1000
} }
if (d3.event.key=='Home') {
d3.event.preventDefault();
sliderSimple.silentValue(min)
document.getElementById("value-simple").value=min
}
if (d3.event.key=='End') {
d3.event.preventDefault();
sliderSimple.silentValue(max)
document.getElementById("value-simple").value=max
}
}
})
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment