Skip to content

Instantly share code, notes, and snippets.

@jssolichin
Forked from mbostock/.block
Last active January 31, 2023 04:13
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jssolichin/54b4995bd68275691a23 to your computer and use it in GitHub Desktop.
Save jssolichin/54b4995bd68275691a23 to your computer and use it in GitHub Desktop.
Multiple No Collision Brushes in D3js
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.axis text {
font: 11px sans-serif;
}
.axis path {
display: none;
}
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.grid-background {
fill: #ddd;
}
.grid line,
.grid path {
fill: none;
stroke: #fff;
shape-rendering: crispEdges;
}
.grid .minor.tick line {
stroke-opacity: .5;
}
.brush .extent {
stroke: #000;
fill-opacity: .125;
shape-rendering: crispEdges;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.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.time.scale()
.domain([new Date(2013, 2, 1), new Date(2013, 2, 15) - 1])
.range([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("rect")
.attr("class", "grid-background")
.attr("width", width)
.attr("height", height);
svg.append("g")
.attr("class", "x grid")
.attr("transform", "translate(0," + height + ")")
.call(d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(d3.time.hours, 12)
.tickSize(-height)
.tickFormat(""))
.selectAll(".tick")
.classed("minor", function(d) {
return d.getHours();
});
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(d3.time.days)
.tickPadding(0))
.selectAll("text")
.attr("x", 6)
.style("text-anchor", null);
//brushes container
var gBrushes = svg.append('g')
.attr("class", "brushes");
//keep track of existing brushes
var brushes = [];
//return an array that contains the closest brush edge to the left and right
function getBrushesAround(brush, brushes) {
var edge = [];
if(brush.extent.start === undefined)
brush.extent.start = brush.extent();
brushes.forEach(function(otherBrush) {
var otherBrush_extent = otherBrush.extent();
if (otherBrush !== brush) {
if (brush.extent.start !== undefined
&& otherBrush_extent[1].getTime() <= brush.extent.start[0].getTime()) {
if (edge[0] !== undefined && otherBrush_extent[1].getTime() > edge[0].getTime() || edge[0] === undefined)
edge[0] = otherBrush_extent[1];
} else if (brush.extent.start !== undefined
&& otherBrush_extent[0].getTime() > brush.extent.start[0].getTime()) {
if (edge[1] !== undefined && otherBrush_extent[0].getTime() < edge[1].getTime() || edge[1] === undefined)
edge[1] = otherBrush_extent[0];
}
}
});
return edge;
}
//new brush handler
function newBrush() {
var brush = d3.svg.brush()
.x(x)
.on("brush", brushed) //Make sure don't pass surrounding brushes
.on("brushend", brushend); //Keep track of what brushes is surrounding
brushes.push({id: brushes.length, brush: brush});
function brushstart() {
if (d3.event.sourceEvent)
brush.mouseStart = d3.event.sourceEvent.x;
if(brush.extent.start == undefined){
d3.event.sourceEvent.x;
}
};
function brushed() {
var extent0 = brush.extent(),
extent1;
// if dragging, preserve the width of the extent
if (d3.event.mode === "move") {
var d0 = d3.time.day.round(extent0[0]),
d1 = d3.time.day.offset(d0, Math.round((extent0[1] - extent0[0]) / 864e5));
extent1 = [d0, d1];
}
// otherwise, if resizing, round both dates
else {
extent1 = extent0.map(d3.time.day.round);
// if empty when rounded, use floor & ceil instead
if (extent1[0] >= extent1[1]) {
extent1[0] = d3.time.day.floor(extent0[0]);
extent1[1] = d3.time.day.ceil(extent0[1]);
}
}
//Make sure no collision
//find out what surrounds this brush
var edge = getBrushesAround(brush, brushes.map(function(d){return d.brush}));
//if the current block gets brushed beyond the surrounding block, limit it so it does not go past
if (edge[1] !== undefined && extent1[1].getTime() > edge[1].getTime()) {
extent1[1] = edge[1];
//if we are moving, not only do we stop it from going past, but also keep the brush the same size
if (d3.event.mode === "move")
extent1[0] = d3.time.hour.offset(extent1[1], -Math.round((brush.extent.start[1] - brush.extent.start[0]) / 3600000));
} else if (edge[0] !== undefined && extent1[0].getTime() < edge[0].getTime()) {
extent1[0] = edge[0];
if (d3.event.mode === "move")
extent1[1] = d3.time.hour.offset(extent1[0], Math.round((brush.extent.start[1] - brush.extent.start[0]) / 3600000));
}
d3.select(this).call(brush.extent(extent1));
}
function brushend() {
//add a new brush as needed
var lastBrushExtent = brushes[brushes.length - 1].brush.extent();
if (lastBrushExtent[0].getTime() != lastBrushExtent[1].getTime())
newBrush();
//keep track of current loc for comparison later
brush.extent.start = brush.extent();
update();
}
}
function update() {
var gBrush = gBrushes
.selectAll('.brush')
.data(brushes, function (d){return d.id});
gBrush.enter()
.insert("g", '.brush')
.attr('class', 'brush')
.each(function(brushWrapper) {
//call the brush
brushWrapper.brush(d3.select(this));
});
gBrush
.each(function (brushWrapper,i){
d3.select(this)
.attr('class', 'brush brush-'+i)
.selectAll('.background')
.style('pointer-events', function() {
var brush = brushWrapper.brush;
return i === brushes.length-1 &&
brush !== undefined &&
brush.extent()[0].getTime() === brush.extent()[1].getTime()
? 'all' : 'none';
});
})
gBrush.selectAll('rect')
.attr("height", height);
gBrush.exit()
.remove();
}
newBrush();
update();
</script>
@MayaGans
Copy link

Hi - thanks so much for posting this! I'm very new to D3.js and have been trying (unsuccessfully) to build on this so that the user can delete one of the brushed areas. I found this SO post but I'm unsure how to implement when there are multiple brushed areas. Any help appreciated, and thanks again for the code!

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