Skip to content

Instantly share code, notes, and snippets.

@nickbenes
Forked from mbostock/.block
Last active January 7, 2016 19:58
Show Gist options
  • Save nickbenes/fc8da4ec0b39c727ef28 to your computer and use it in GitHub Desktop.
Save nickbenes/fc8da4ec0b39c727ef28 to your computer and use it in GitHub Desktop.
Brush Snapping with varying timespans

Tweaks to http://bl.ocks.org/mbostock/6232537 that adds data-driven timespans to the widget. This version allows timespans of varying sizes. It also modifies the brushend listener to log information based on the timespans where the brush's endpoints are located.

This brush snaps to day boundaries. When the user releases the brush, the brush fires a brushend event, allowing a listener to adjust the brush extent. Using the new brush transitions released in D3 3.3, the brush smoothly interpolates from the original extent to the rounded extent. Compare this approach to using immediate snapping while brushing.

<!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;
}
.timespans rect {
fill: steelblue;
stroke: #444;
}
</style>
<body>
<!--script src="d3.js"></script-->
<script src="//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, 7, 1), new Date(2013, 7, 15) - 1])
.domain([new Date(2015, 9, 1), new Date(2016, 0, 15) - 1])
.range([0, width]);
var brush = d3.svg.brush()
.x(x)
// .extent([new Date(2013, 7, 4), new Date(2013, 7, 7)])
.extent([new Date(2016, 0, 1), new Date(2016, 0, 7)])
.on("brushend", brushended);
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)
.ticks(d3.time.weeks)
.tickPadding(0))
.selectAll("text")
.attr("x", 6)
.style("text-anchor", null);
var gBrush = svg.append("g")
.attr("class", "brush")
.call(brush) // Adds brush SVG elements to DOM under the gBrush group
// This last brush.event call is not needed
// .call(brush.event);
gBrush.selectAll("rect")
.attr("height", height);
/**
* Data-driven stuff
*/
var timeObjects = buildTimeObjects2();
// Insert timespans group before the brush so the brush is still on top
var gTimespans = svg.insert('g','.brush')
.attr('class', 'timespans');
// Set up timespan rectangles
gTimespans.selectAll('rect')
// Bind rectangles to timeObjects data, using startDate as the unique identifier
.data(timeObjects, function(d) { return d.startDate; })
// Now tell d3 what to do with all these new bits of data, starting by appending a rectangle for each
.enter().append('rect')
// Position the left side of the rectangles based on their startDate
.attr('x', function(d) { return x(d.startDate); })
// And figure out the width by taking endDate - startDate in x-coordinates
.attr('width', function(d) { return x(d.endDate) - x(d.startDate); })
// Leave all the heights the same for now
.attr('height', height);
/**
* Functions
*/
function brushended() {
if (!d3.event.sourceEvent) return; // only transition after input
var extent0 = brush.extent(),
extent1 = [];
// Get timespans for each endpoint
var leftSpan = getIntersection(extent0[0], timeObjects)[0],
rightSpan = getIntersection(extent0[1], timeObjects)[0],
centerSpan = getIntersection(new Date((+extent0[0] +(+extent0[1]))/2), timeObjects)[0];
// Round each endpoint to the closer end of its timespan and save in extent1
if (extent0[0] - leftSpan.startDate <= leftSpan.endDate - extent0[0]) {
extent1[0] = leftSpan.startDate;
} else {
extent1[0] = leftSpan.endDate;
}
if (extent0[1] - rightSpan.startDate <= rightSpan.endDate - extent0[1]) {
extent1[1] = rightSpan.startDate;
} else {
extent1[1] = rightSpan.endDate;
}
// if empty when rounded, check where the original center was and use start & end of that timespan instead
if (extent1[0] >= extent1[1]) {
extent1[0] = centerSpan.startDate;
extent1[1] = centerSpan.endDate;
}
d3.select(this).transition()
.call(brush.extent(extent1))
.call(brush.event);
// New bit: Log the objects where the brush endpoints originally landed
console.log("Brush moved")
console.log("Left timespan: " + JSON.stringify(
getIntersection( extent0[0], timeObjects )
));
console.log("Right timespan: " + JSON.stringify(
getIntersection( extent0[1], timeObjects )
))
}
function buildTimeObjects() {
var arr = [];
var startYear = 2013,
startMonth = 7, // Aug
numLg = 2, // number of large batches
sizeLg = 3, // size of large batches (in days)
numMd = 3, // number of medium batches
sizeMd = 2, // size of medium batches (in days)
numSm = 2, // number of small batches
sizeSm = 1; // size of small batches (in days)
var i=0;
for (i = 0; i < numLg; i++) {
arr.push({
startDate: new Date(startYear, startMonth, 1 + sizeLg*i),
endDate: new Date(startYear, startMonth, 1 + sizeLg*(i+1)),
span: sizeLg
});
}
for (i = 0; i < numMd; i++) {
arr.push({
startDate: new Date(startYear, startMonth, 1 + sizeLg*numLg + sizeMd*i),
endDate: new Date(startYear, startMonth, 1 + sizeLg*numLg + sizeMd*(i+1)),
span: sizeMd
});
}
for (i = 0; i < numSm; i++) {
arr.push({
startDate: new Date(startYear, startMonth, 1 + sizeLg*numLg + sizeMd*numMd + sizeSm*i),
endDate: new Date(startYear, startMonth, 1 + sizeLg*numLg + sizeMd*numMd + sizeSm*(i+1)),
span: sizeSm
});
}
return arr;
}
function buildTimeObjects2() {
var arr = [];
var startYear = 2015,
startMonth = 9, // October
numMonths = 2,
numWeeks = 4,
numDays = 30;
var i=0;
for (i = 0; i < numMonths; i++) {
arr.push({
startDate: new Date(startYear, startMonth + i, 1),
endDate: new Date(startYear, startMonth + i + 1, 1),
span: 'month'
});
}
for (i = 0; i < numWeeks; i++) {
arr.push({
startDate: new Date(startYear, startMonth + numMonths, 1 + 7*i),
endDate: new Date(startYear, startMonth + numMonths, 1 + 7*(i+1)),
span: 'week'
});
}
for (i = 0; i < numDays; i++) {
arr.push({
startDate: new Date(startYear, startMonth + numMonths, 1 + 7*numWeeks + i),
endDate: new Date(startYear, startMonth + numMonths, 1 + 7*numWeeks + i + 1),
span: 'day'
});
}
return arr;
}
function getIntersection(time, timespanArray) {
return timespanArray.filter(function(d) {
return d.startDate <= time && time < d.endDate;
});
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment