--- a/labs/busdelay.php +++ b/labs/busdelay.php @@ -1,1 +1,498 @@ - +<!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 extent = brush.extent(); + g.selectAll("#clip-" + id + " rect") + .attr("x", x(extent[0])) + .attr("width", x(extent[1]) - x(extent[0])); + } + } + + g.selectAll(".bar").attr("d", barPath); + }); + + function barPath(groups) { + var path = [], + i = -1, + n = groups.length, + d; + while (++i < n) { + d = groups[i]; + path.push("M", x(d.key), ",", height, "V", y(d.value), "h9V", height); + } + return path.join(""); + } + + function resizePath(d) { + var e = +(d == "e"), + x = e ? 1 : -1, + y = height / 3; + return "M" + (.5 * x) + "," + y + + "A6,6 0 0 " + e + " " + (6.5 * x) + "," + (y + 6) + + "V" + (2 * y - 6) + + "A6,6 0 0 " + e + " " + (.5 * x) + "," + (2 * y) + + "Z" + + "M" + (2.5 * x) + "," + (y + 8) + + "V" + (2 * y - 8) + + "M" + (4.5 * x) + "," + (y + 8) + + "V" + (2 * y - 8); + } + } + + brush.on("brushstart.chart", function() { + var div = d3.select(this.parentNode.parentNode.parentNode); + div.select(".title a").style("display", null); + }); + + brush.on("brush.chart", function() { + var g = d3.select(this.parentNode), + extent = brush.extent(); + if (round) g.select(".brush") + .call(brush.extent(extent = extent.map(round))) + .selectAll(".resize") + .style("display", null); + g.select("#clip-" + id + " rect") + .attr("x", x(extent[0])) + .attr("width", x(extent[1]) - x(extent[0])); + dimension.filterRange(extent); + }); + + brush.on("brushend.chart", function() { + if (brush.empty()) { + var div = d3.select(this.parentNode.parentNode.parentNode); + div.select(".title a").style("display", "none"); + div.select("#clip-" + id + " rect").attr("x", null).attr("width", "100%"); + dimension.filterAll(); + } + }); + + chart.margin = function(_) { + if (!arguments.length) return margin; + margin = _; + return chart; + }; + + chart.x = function(_) { + if (!arguments.length) return x; + x = _; + axis.scale(x); + brush.x(x); + return chart; + }; + + chart.y = function(_) { + if (!arguments.length) return y; + y = _; + return chart; + }; + + chart.dimension = function(_) { + if (!arguments.length) return dimension; + dimension = _; + return chart; + }; + + chart.filter = function(_) { + if (_) { + brush.extent(_); + dimension.filterRange(_); + } else { + brush.clear(); + dimension.filterAll(); + } + brushDirty = true; + return chart; + }; + + chart.group = function(_) { + if (!arguments.length) return group; + group = _; + return chart; + }; + + chart.round = function(_) { + if (!arguments.length) return round; + round = _; + return chart; + }; + + return d3.rebind(chart, brush, "on"); + } +}); + +</script> +