|
<!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> |