Skip to content

Instantly share code, notes, and snippets.

@mbostock
Last active August 8, 2019 21:58
Show Gist options
  • Save mbostock/6498000 to your computer and use it in GitHub Desktop.
Save mbostock/6498000 to your computer and use it in GitHub Desktop.
Click-to-Recenter Brush
license: gpl-3.0
redirect: https://observablehq.com/@d3/click-to-recenter-brush

By default, clicking and dragging outside the current brush selection drags a new selection. This brush has been modified such that this interaction instead recenters the brush.

This customization is implemented by changing the type of the brush’s overlay element from overlay to selection so that clicking and dragging on the overlay element behaves identically to clicking and dragging on the brush selection. (The overlay element is the invisible rect element that receives pointer events when clicking outside the brush selection.) When the overlay is clicked and before brushing starts, the brush is recentered around the pointer, such that the brush remains centered around the pointer during brushing.

This technique can be composed with snapping on brushend if desired.

<!DOCTYPE html>
<style>
.selected {
fill: red;
stroke: brown;
}
</style>
<svg width="960" height="500"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var randomX = d3.randomUniform(0, 10),
randomY = d3.randomNormal(0.5, 0.12),
data = d3.range(800).map(function() { return [randomX(), randomY()]; });
var svg = d3.select("svg"),
margin = {top: 194, right: 50, bottom: 214, left: 50},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var x = d3.scaleLinear()
.domain([0, 10])
.range([0, width]);
var y = d3.scaleLinear()
.range([height, 0]);
var brush = d3.brushX()
.extent([[0, 0], [width, height]])
.on("start brush", brushed);
var dot = g.append("g")
.attr("fill-opacity", 0.2)
.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("transform", function(d) { return "translate(" + x(d[0]) + "," + y(d[1]) + ")"; })
.attr("r", 3.5);
g.append("g")
.call(brush)
.call(brush.move, [3, 5].map(x))
.selectAll(".overlay")
.each(function(d) { d.type = "selection"; }) // Treat overlay interaction as move.
.on("mousedown touchstart", brushcentered); // Recenter before brushing.
g.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
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]);
}
function brushed() {
var extent = d3.event.selection.map(x.invert, x);
dot.classed("selected", function(d) { return extent[0] <= d[0] && d[0] <= extent[1]; });
}
</script>
@gamazeps
Copy link

gamazeps commented Dec 6, 2016

Is there a version of this sample for d3 v4 ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment