Public
Edited
Dec 20, 2022
2 stars
Insert cell
Insert cell
Insert cell
viewof myslider = slider(0, 25)
Insert cell
myslider
Insert cell
Insert cell
viewof myslider2 = slider(0, 25, 10, 20)
Insert cell
viewof myslider3 = slider(0, 25, undefined, 15)
Insert cell
Insert cell
viewof mysnapslider = slider_snap(1990, 2015)
Insert cell
mysnapslider
Insert cell
Insert cell
viewof mysnapslider2 = slider_snap(1990, 2015, 1995, 2000)
Insert cell
viewof mysnapslider3 = slider_snap(1990, 2015, undefined, 1997)
Insert cell
viewof mysnapslider4 = slider_snap(1990, 2015, 1995, 1997, v => `x${v}`)
Insert cell
Insert cell
Insert cell
slider = function(min, max, starting_min=min, starting_max=max) {

var range = [min, max]
var starting_range = [starting_min, starting_max]

// set width and height of svg
var w = layout.width
var h = layout.height
var margin = layout.margin

// dimensions of slider bar
var width = w - margin.left - margin.right;
var height = h - margin.top - margin.bottom;

// create x scale
var x = d3.scaleLinear()
.domain(range) // data space
.range([0, width]); // display space
// create svg and translated g
var svg = d3.select(DOM.svg(w,h))
const g = svg.append('g').attr('transform', `translate(${margin.left}, ${margin.top})`)
// labels
var labelL = g.append('text')
.attr('id', 'labelleft')
.attr('x', 0)
.attr('y', height + 5)

var labelR = g.append('text')
.attr('id', 'labelright')
.attr('x', 0)
.attr('y', height + 5)

// define brush
var brush = d3.brushX()
.extent([[0,0], [width, height]])
.on('brush', function() {
var s = d3.event.selection;
// update and move labels
labelL.attr('x', s[0])
.text((x.invert(s[0]).toFixed(2)))
labelR.attr('x', s[1])
.text((x.invert(s[1]).toFixed(2)))
// move brush handles
handle.attr("display", null).attr("transform", function(d, i) { return "translate(" + [ s[i], - height / 4] + ")"; });
// update view
// if the view should only be updated after brushing is over,
// move these two lines into the on('end') part below
svg.node().value = s.map(function(d) {var temp = x.invert(d); return +temp.toFixed(2)});
svg.node().dispatchEvent(new CustomEvent("input"));
})

// append brush to g
var gBrush = g.append("g")
.attr("class", "brush")
.call(brush)

// add brush handles (from https://bl.ocks.org/Fil/2d43867ba1f36a05459c7113c7f6f98a)
var brushResizePath = function(d) {
var e = +(d.type == "e"),
x = e ? 1 : -1,
y = height / 2;
return "M" + (.5 * x) + "," + y + "A6,6 0 0 " + e + " " + (6.5 * x) + "," + (y + 6) + "V" + (2 * y - 6) +
"A6,6 0 0 " + e + " " + (.5 * x) + "," + (2 * y) + "Z" + "M" + (2.5 * x) + "," + (y + 8) + "V" + (2 * y - 8) +
"M" + (4.5 * x) + "," + (y + 8) + "V" + (2 * y - 8);
}

var handle = gBrush.selectAll(".handle--custom")
.data([{type: "w"}, {type: "e"}])
.enter().append("path")
.attr("class", "handle--custom")
.attr("stroke", "#000")
.attr("fill", '#eee')
.attr("cursor", "ew-resize")
.attr("d", brushResizePath);
// override default behaviour - clicking outside of the selected area
// will select a small piece there rather than deselecting everything
// https://bl.ocks.org/mbostock/6498000
gBrush.selectAll(".overlay")
.each(function(d) { d.type = "selection"; })
.on("mousedown touchstart", brushcentered)
function brushcentered() {
var dx = x(1) - x(0), // Use a fixed width when recentering.
cx = d3.mouse(this)[0],
x0 = cx - dx / 2,
x1 = cx + dx / 2;
d3.select(this.parentNode).call(brush.move, x1 > width ? [width - dx, width] : x0 < 0 ? [0, dx] : [x0, x1]);
}
// select entire range
gBrush.call(brush.move, starting_range.map(x))
return svg.node()
}
Insert cell
slider_snap = function(min, max, starting_min=min, starting_max=max, formatFunc = v => v) {

var range = [min, max + 1]
var starting_range = [starting_min, starting_max + 1]

// set width and height of svg
var w = layout.width
var h = layout.height
var margin = layout.margin

// dimensions of slider bar
var width = w - margin.left - margin.right;
var height = h - margin.top - margin.bottom;

// create x scale
var x = d3.scaleLinear()
.domain(range) // data space
.range([0, width]); // display space
// create svg and translated g
var svg = d3.select(DOM.svg(w,h))
const g = svg.append('g').attr('transform', `translate(${margin.left}, ${margin.top})`)
// draw background lines
g.append('g').selectAll('line')
.data(d3.range(range[0], range[1]+1))
.enter()
.append('line')
.attr('x1', d => x(d)).attr('x2', d => x(d))
.attr('y1', 0).attr('y2', height)
.style('stroke', '#ccc')
// labels
var labelL = g.append('text')
.attr('id', 'labelleft')
.attr('x', 0)
.attr('y', height + 5)
.text(range[0])

var labelR = g.append('text')
.attr('id', 'labelright')
.attr('x', 0)
.attr('y', height + 5)
.text(range[1])

// define brush
var brush = d3.brushX()
.extent([[0,0], [width, height]])
.on('brush', function() {
var s = d3.event.selection;
// update and move labels
labelL.attr('x', s[0])
.text(formatFunc(Math.round(x.invert(s[0]))))
labelR.attr('x', s[1])
.text(formatFunc(Math.round(x.invert(s[1])) - 1))
// move brush handles
handle.attr("display", null).attr("transform", function(d, i) { return "translate(" + [ s[i], - height / 4] + ")"; });
// update view
// if the view should only be updated after brushing is over,
// move these two lines into the on('end') part below
svg.node().value = s.map(d => Math.round(x.invert(d)));
svg.node().dispatchEvent(new CustomEvent("input"));
})
.on('end', function() {
if (!d3.event.sourceEvent) return;
var d0 = d3.event.selection.map(x.invert);
var d1 = d0.map(Math.round)
d3.select(this).transition().call(d3.event.target.move, d1.map(x))
})

// append brush to g
var gBrush = g.append("g")
.attr("class", "brush")
.call(brush)

// add brush handles (from https://bl.ocks.org/Fil/2d43867ba1f36a05459c7113c7f6f98a)
var brushResizePath = function(d) {
var e = +(d.type == "e"),
x = e ? 1 : -1,
y = height / 2;
return "M" + (.5 * x) + "," + y + "A6,6 0 0 " + e + " " + (6.5 * x) + "," + (y + 6) + "V" + (2 * y - 6) +
"A6,6 0 0 " + e + " " + (.5 * x) + "," + (2 * y) + "Z" + "M" + (2.5 * x) + "," + (y + 8) + "V" + (2 * y - 8) +
"M" + (4.5 * x) + "," + (y + 8) + "V" + (2 * y - 8);
}

var handle = gBrush.selectAll(".handle--custom")
.data([{type: "w"}, {type: "e"}])
.enter().append("path")
.attr("class", "handle--custom")
.attr("stroke", "#000")
.attr("fill", '#eee')
.attr("cursor", "ew-resize")
.attr("d", brushResizePath);
// override default behaviour - clicking outside of the selected area
// will select a small piece there rather than deselecting everything
// https://bl.ocks.org/mbostock/6498000
gBrush.selectAll(".overlay")
.each(function(d) { d.type = "selection"; })
.on("mousedown touchstart", brushcentered)
function brushcentered() {
var dx = x(1) - x(0), // Use a fixed width when recentering.
cx = d3.mouse(this)[0],
x0 = cx - dx / 2,
x1 = cx + dx / 2;
d3.select(this.parentNode).call(brush.move, x1 > width ? [width - dx, width] : x0 < 0 ? [0, dx] : [x0, x1]);
}
// select entire starting range
gBrush.call(brush.move, starting_range.map(x))
return svg.node()
}
Insert cell
layout = ({
width: 600,
height: 60,
margin: {
top: 10,
bottom: 15,
left: 80,
right: 80
}
})
Insert cell
Insert cell
Insert cell
html`${sliderCss}`
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more