|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<title>Tesseract</title> |
|
<style> |
|
|
|
|
|
#charts { |
|
padding: 10px 0; |
|
} |
|
|
|
.chart { |
|
display: inline-block; |
|
height: 151px; |
|
margin-bottom: 20px; |
|
} |
|
|
|
.reset { |
|
padding-left: 1em; |
|
font-size: smaller; |
|
color: #ccc; |
|
} |
|
|
|
.background.bar { |
|
fill: #ccc; |
|
} |
|
|
|
.foreground.bar { |
|
fill: steelblue; |
|
} |
|
|
|
.axis path, .axis line { |
|
fill: none; |
|
stroke: #000; |
|
shape-rendering: crispEdges; |
|
} |
|
|
|
.axis text { |
|
font: 10px sans-serif; |
|
} |
|
|
|
.brush rect.extent { |
|
fill: steelblue; |
|
fill-opacity: .125; |
|
} |
|
|
|
.brush .resize path { |
|
fill: #eee; |
|
stroke: #666; |
|
} |
|
|
|
#hour-chart { |
|
width: 260px; |
|
} |
|
|
|
#delay-chart { |
|
width: 230px; |
|
} |
|
|
|
#distance-chart { |
|
width: 420px; |
|
} |
|
|
|
#date-chart { |
|
width: 920px; |
|
} |
|
|
|
#flight-list { |
|
min-height: 1024px; |
|
} |
|
|
|
#flight-list .date, |
|
#flight-list .day { |
|
margin-bottom: .4em; |
|
} |
|
|
|
#flight-list .flight { |
|
line-height: 1.5em; |
|
background: #eee; |
|
width: 640px; |
|
margin-bottom: 1px; |
|
} |
|
|
|
#flight-list .time { |
|
color: #999; |
|
} |
|
|
|
#flight-list .flight div { |
|
display: inline-block; |
|
width: 100px; |
|
} |
|
|
|
#flight-list div.distance, |
|
#flight-list div.delay { |
|
width: 160px; |
|
padding-right: 10px; |
|
text-align: right; |
|
} |
|
|
|
#flight-list .early { |
|
color: green; |
|
} |
|
|
|
aside { |
|
position: absolute; |
|
left: 740px; |
|
font-size: smaller; |
|
width: 220px; |
|
} |
|
|
|
</style> |
|
|
|
|
|
<div id="charts"> |
|
<div id="hour-chart" class="chart"> |
|
<div class="title">Time of Day</div> |
|
</div> |
|
<div id="delay-chart" class="chart"> |
|
<div class="title">Arrival Delay (min.)</div> |
|
</div> |
|
<div id="distance-chart" class="chart"> |
|
<div class="title">Distance (mi.)</div> |
|
</div> |
|
<div id="date-chart" class="chart"> |
|
<div class="title">Date</div> |
|
</div> |
|
</div> |
|
|
|
<aside id="totals"><span id="active">-</span> of <span id="total">-</span> flights selected.</aside> |
|
|
|
<div id="lists"> |
|
<div id="flight-list" class="list"></div> |
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
<script src="../js/tesseract/tesseract.min.js"></script> |
|
<script src="../js/d3/d3.v2.min.js"></script> |
|
<script> |
|
|
|
d3.csv("busdelay.csv.php", function(flights) { |
|
|
|
// Various formatters. |
|
var formatNumber = d3.format(",d"), |
|
formatChange = d3.format("+,d"), |
|
formatDate = d3.time.format("%B %d, %Y"), |
|
formatTime = d3.time.format("%I:%M %p"); |
|
|
|
// A nest operator, for grouping the flight list. |
|
var nestByDate = d3.nest() |
|
.key(function(d) { return d3.time.day(d.date); }); |
|
|
|
// A little coercion, since the CSV is untyped. |
|
flights.forEach(function(d, i) { |
|
d.index = i; |
|
d.date = parseDate(d.date); |
|
d.delay = +d.delay; |
|
d.distance = +d.distance; |
|
}); |
|
|
|
// Create the tesseract and relevant dimensions and groups. |
|
flight = tesseract(flights), |
|
all = flight.groupAll(), |
|
date = flight.dimension(function(d) { return d3.time.day(d.date); }), |
|
dates = date.group(), |
|
hour = flight.dimension(function(d) { return d.date.getHours() + d.date.getMinutes() / 60; }), |
|
hours = hour.group(Math.floor), |
|
delay = flight.dimension(function(d) { return Math.max(-60, Math.min(149, d.delay)); }), |
|
delays = delay.group(function(d) { return Math.floor(d / 10) * 10; }), |
|
distance = flight.dimension(function(d) { return Math.min(90, d.distance); }), |
|
distances = distance.group(function(d) { return Math.floor(d / 50) * 50; }); |
|
|
|
var charts = [ |
|
|
|
barChart() |
|
.dimension(hour) |
|
.group(hours) |
|
.x(d3.scale.linear() |
|
.domain([0, 24]) |
|
.rangeRound([0, 10 * 24])), |
|
|
|
barChart() |
|
.dimension(delay) |
|
.group(delays) |
|
.x(d3.scale.linear() |
|
.domain([-60, 150]) |
|
.rangeRound([0, 10 * 21])), |
|
|
|
barChart() |
|
.dimension(distance) |
|
.group(distances) |
|
.x(d3.scale.linear() |
|
.domain([0, 90]) |
|
.rangeRound([0, 10 * 40])), |
|
|
|
barChart() |
|
.dimension(date) |
|
.group(dates) |
|
.round(d3.time.day.round) |
|
.x(d3.time.scale() |
|
.domain([new Date(2001, 0, 1), new Date(2001, 3, 1)]) |
|
.rangeRound([0, 10 * 90])) |
|
.filter([new Date(2001, 1, 1), new Date(2001, 2, 1)]) |
|
|
|
]; |
|
|
|
// Given our array of charts, which we assume are in the same order as the |
|
// .chart elements in the DOM, bind the charts to the DOM and render them. |
|
// We also listen to the chart's brush events to update the display. |
|
var chart = d3.selectAll(".chart") |
|
.data(charts) |
|
.each(function(chart) { chart.on("brush", renderAll).on("brushend", renderAll); }); |
|
|
|
// Render the initial lists. |
|
var list = d3.selectAll(".list") |
|
.data([flightList]); |
|
|
|
// Render the total. |
|
d3.selectAll("#total") |
|
.text(formatNumber(flight.size())); |
|
|
|
renderAll(); |
|
|
|
// Renders the specified chart or list. |
|
function render(method) { |
|
d3.select(this).call(method); |
|
} |
|
|
|
// Whenever the brush moves, re-rendering everything. |
|
function renderAll() { |
|
chart.each(render); |
|
list.each(render); |
|
d3.select("#active").text(formatNumber(all.value())); |
|
} |
|
|
|
// Like d3.time.format, but faster. |
|
function parseDate(d) { |
|
return new Date(2001, |
|
d.substring(0, 2) - 1, |
|
d.substring(2, 4), |
|
d.substring(4, 6), |
|
d.substring(6, 8)); |
|
} |
|
|
|
window.filter = function(filters) { |
|
filters.forEach(function(d, i) { charts[i].filter(d); }); |
|
renderAll(); |
|
}; |
|
|
|
window.reset = function(i) { |
|
charts[i].filter(null); |
|
renderAll(); |
|
}; |
|
|
|
function flightList(div) { |
|
var flightsByDate = nestByDate.entries(date.top(40)); |
|
|
|
div.each(function() { |
|
var date = d3.select(this).selectAll(".date") |
|
.data(flightsByDate, function(d) { return d.key; }); |
|
|
|
date.enter().append("div") |
|
.attr("class", "date") |
|
.append("div") |
|
.attr("class", "day") |
|
.text(function(d) { return formatDate(d.values[0].date); }); |
|
|
|
date.exit().remove(); |
|
|
|
var flight = date.order().selectAll(".flight") |
|
.data(function(d) { return d.values; }, function(d) { return d.index; }); |
|
|
|
var flightEnter = flight.enter().append("div") |
|
.attr("class", "flight"); |
|
|
|
flightEnter.append("div") |
|
.attr("class", "time") |
|
.text(function(d) { return formatTime(d.date); }); |
|
|
|
flightEnter.append("div") |
|
.attr("class", "origin") |
|
.text(function(d) { return d.origin; }); |
|
|
|
flightEnter.append("div") |
|
.attr("class", "destination") |
|
.text(function(d) { return d.destination; }); |
|
|
|
flightEnter.append("div") |
|
.attr("class", "distance") |
|
.text(function(d) { return formatNumber(d.distance) + " mi."; }); |
|
|
|
flightEnter.append("div") |
|
.attr("class", "delay") |
|
.classed("early", function(d) { return d.delay < 0; }) |
|
.text(function(d) { return formatChange(d.delay) + " min."; }); |
|
|
|
flight.exit().remove(); |
|
|
|
flight.order(); |
|
}); |
|
} |
|
|
|
function barChart() { |
|
if (!barChart.id) barChart.id = 0; |
|
|
|
var margin = {top: 10, right: 10, bottom: 20, left: 10}, |
|
x, |
|
y = d3.scale.linear().range([100, 0]), |
|
id = barChart.id++, |
|
axis = d3.svg.axis().orient("bottom"), |
|
brush = d3.svg.brush(), |
|
brushDirty, |
|
dimension, |
|
group, |
|
round; |
|
|
|
function chart(div) { |
|
var width = x.range()[1], |
|
height = y.range()[0]; |
|
|
|
y.domain([0, group.top(1)[0].value]); |
|
|
|
div.each(function() { |
|
var div = d3.select(this), |
|
g = div.select("g"); |
|
|
|
// Create the skeletal chart. |
|
if (g.empty()) { |
|
div.select(".title").append("a") |
|
.attr("href", "javascript:reset(" + id + ")") |
|
.attr("class", "reset") |
|
.text("reset") |
|
.style("display", "none"); |
|
|
|
g = div.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 + ")"); |
|
|
|
g.append("clipPath") |
|
.attr("id", "clip-" + id) |
|
.append("rect") |
|
.attr("width", width) |
|
.attr("height", height); |
|
|
|
g.selectAll(".bar") |
|
.data(["background", "foreground"]) |
|
.enter().append("path") |
|
.attr("class", function(d) { return d + " bar"; }) |
|
.datum(group.all()); |
|
|
|
g.selectAll(".foreground.bar") |
|
.attr("clip-path", "url(#clip-" + id + ")"); |
|
|
|
g.append("g") |
|
.attr("class", "axis") |
|
.attr("transform", "translate(0," + height + ")") |
|
.call(axis); |
|
|
|
// Initialize the brush component with pretty resize handles. |
|
var gBrush = g.append("g").attr("class", "brush").call(brush); |
|
gBrush.selectAll("rect").attr("height", height); |
|
gBrush.selectAll(".resize").append("path").attr("d", resizePath); |
|
} |
|
|
|
// Only redraw the brush if set externally. |
|
if (brushDirty) { |
|
brushDirty = false; |
|
g.selectAll(".brush").call(brush); |
|
div.select(".title a").style("display", brush.empty() ? "none" : null); |
|
if (brush.empty()) { |
|
g.selectAll("#clip-" + id + " rect") |
|
.attr("x", 0) |
|
.attr("width", width); |
|
} else { |
|
var e |