Skip to content

Instantly share code, notes, and snippets.

@mbostock
Last active April 11, 2024 18:53
Show Gist options
  • Save mbostock/6232537 to your computer and use it in GitHub Desktop.
Save mbostock/6232537 to your computer and use it in GitHub Desktop.
Brush Snapping
license: gpl-3.0
redirect: https://observablehq.com/@d3/brush-snapping-transitions

This brush snaps to day boundaries. On release, the brush fires an end event, allowing a listener to modify the brush selection. Using brush.move to initiate a transition, the brush smoothly interpolates from the original selection to the rounded selection. Compare this approach to using immediate snapping while brushing.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
.axis--grid .domain {
fill: #ddd;
stroke: none;
}
.axis--x .domain,
.axis--grid .tick line {
stroke: #fff;
}
.axis--grid .tick--minor line {
stroke-opacity: .5;
}
</style>
<body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var margin = {top: 200, right: 40, bottom: 200, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scaleTime()
.domain([new Date(2013, 7, 1), new Date(2013, 7, 15) - 1])
.rangeRound([0, width]);
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.append("g")
.attr("class", "axis axis--grid")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x)
.ticks(d3.timeHour, 12)
.tickSize(-height)
.tickFormat(function() { return null; }))
.selectAll(".tick")
.classed("tick--minor", function(d) { return d.getHours(); });
svg.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x)
.ticks(d3.timeDay)
.tickPadding(0))
.attr("text-anchor", null)
.selectAll("text")
.attr("x", 6);
svg.append("g")
.attr("class", "brush")
.call(d3.brushX()
.extent([[0, 0], [width, height]])
.on("end", brushended));
function brushended() {
if (!d3.event.sourceEvent) return; // Only transition after input.
if (!d3.event.selection) return; // Ignore empty selections.
var d0 = d3.event.selection.map(x.invert),
d1 = d0.map(d3.timeDay.round);
// If empty when rounded, use floor & ceil instead.
if (d1[0] >= d1[1]) {
d1[0] = d3.timeDay.floor(d0[0]);
d1[1] = d3.timeDay.offset(d1[0]);
}
d3.select(this).transition().call(d3.event.target.move, d1.map(x));
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment