Skip to content

Instantly share code, notes, and snippets.

@ludwigschubert
Last active December 15, 2023 09:04
Show Gist options
  • Save ludwigschubert/0236fa8594c4b02711b2606a8f95f605 to your computer and use it in GitHub Desktop.
Save ludwigschubert/0236fa8594c4b02711b2606a8f95f605 to your computer and use it in GitHub Desktop.
d3-multiple-brushes
license: mit

Click-and-Drag to create selections.

d3-brush Multiple Brushes

This is an implementation of multiple brushes in D3js version 4. While brushes give a good out-of-the-box behavior for single brushes, it is not immediately clear how to implement multiple selections/brushes. This is far from a good solution, but it gets you started in understanding what's necessary.

Reasoning

Here's an explanation of what's going on that I wrote for students.

About

Created 2016-10-30 by Ludwig Schubert for Stanford's Fall 2016 offering of CS 448b—Data Visualization.

<!DOCTYPE html>
<meta charset="utf-8">
<meta title="Minimal D3 v4 multiple brushes example--Ludwig Schubert for Stanford CS448b">
<style>
.grid-background {
fill: #eee;
}
.grid line,
.grid path {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.brush .selection {
stroke: #000;
fill: red;
}
</style>
<body>
<script src="http://d3js.org/d3.v4.min.js"></script>
<script>
var margin = {
top: 10,
right: 10,
bottom: 10,
left: 10
},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
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("rect")
.attr("class", "grid-background")
.attr("width", width)
.attr("height", height);
// We initially generate a SVG group to keep our brushes' DOM elements in:
var gBrushes = svg.append('g')
.attr("class", "brushes");
// We also keep the actual d3-brush functions and their IDs in a list:
var brushes = [];
/* CREATE NEW BRUSH
*
* This creates a new brush. A brush is both a function (in our array) and a set of predefined DOM elements
* Brushes also have selections. While the selection are empty (i.e. a suer hasn't yet dragged)
* the brushes are invisible. We will add an initial brush when this viz starts. (see end of file)
* Now imagine the user clicked, moved the mouse, and let go. They just gave a selection to the initial brush.
* We now want to create a new brush.
* However, imagine the user had simply dragged an existing brush--in that case we would not want to create a new one.
* We will use the selection of a brush in brushend() to differentiate these cases.
*/
function newBrush() {
var brush = d3.brush()
.on("start", brushstart)
.on("brush", brushed)
.on("end", brushend);
brushes.push({id: brushes.length, brush: brush});
function brushstart() {
// your stuff here
};
function brushed() {
// your stuff here
}
function brushend() {
// Figure out if our latest brush has a selection
var lastBrushID = brushes[brushes.length - 1].id;
var lastBrush = document.getElementById('brush-' + lastBrushID);
var selection = d3.brushSelection(lastBrush);
// If it does, that means we need another one
if (selection && selection[0] !== selection[1]) {
newBrush();
}
// Always draw brushes
drawBrushes();
}
}
function drawBrushes() {
var brushSelection = gBrushes
.selectAll('.brush')
.data(brushes, function (d){return d.id});
// Set up new brushes
brushSelection.enter()
.insert("g", '.brush')
.attr('class', 'brush')
.attr('id', function(brush){ return "brush-" + brush.id; })
.each(function(brushObject) {
//call the brush
brushObject.brush(d3.select(this));
});
/* REMOVE POINTER EVENTS ON BRUSH OVERLAYS
*
* This part is abbit tricky and requires knowledge of how brushes are implemented.
* They register pointer events on a .overlay rectangle within them.
* For existing brushes, make sure we disable their pointer events on their overlay.
* This frees the overlay for the most current (as of yet with an empty selection) brush to listen for click and drag events
* The moving and resizing is done with other parts of the brush, so that will still work.
*/
brushSelection
.each(function (brushObject){
d3.select(this)
.attr('class', 'brush')
.selectAll('.overlay')
.style('pointer-events', function() {
var brush = brushObject.brush;
if (brushObject.id === brushes.length-1 && brush !== undefined) {
return 'all';
} else {
return 'none';
}
});
})
brushSelection.exit()
.remove();
}
newBrush();
drawBrushes();
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment