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