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