|
|
|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<title>Crossfilter</title> |
|
<style> |
|
|
|
@import url(https://fonts.googleapis.com/css?family=Yanone+Kaffeesatz:400,700); |
|
|
|
body { |
|
font-family: "Helvetica Neue"; |
|
margin: 40px auto; |
|
width: 960px; |
|
min-height: 2000px; |
|
} |
|
|
|
#body { |
|
position: relative; |
|
} |
|
|
|
footer { |
|
padding: 2em 0 1em 0; |
|
font-size: 12px; |
|
} |
|
|
|
h1 { |
|
font-size: 96px; |
|
margin-top: .3em; |
|
margin-bottom: 0; |
|
} |
|
|
|
h1 + h2 { |
|
margin-top: 0; |
|
} |
|
|
|
h2 { |
|
font-weight: 400; |
|
font-size: 28px; |
|
} |
|
|
|
h1, h2 { |
|
font-family: "Yanone Kaffeesatz"; |
|
text-rendering: optimizeLegibility; |
|
} |
|
|
|
#body > p { |
|
line-height: 1.5em; |
|
width: 640px; |
|
text-rendering: optimizeLegibility; |
|
} |
|
|
|
#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; |
|
} |
|
|
|
.brush-handle { |
|
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="body"> |
|
|
|
<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="//alexmacy.github.io/crossfilter/crossfilter.v1.min.js"></script> |
|
<script src="//d3js.org/d3.v4.min.js"></script> |
|
<script> |
|
// (It's CSV, but GitHub Pages only gzip's JSON at the moment.) |
|
d3.csv("https://alexmacy.github.io/crossfilter/flights-3m.json", function(error, flights) { |
|
console.log(flights.length) |
|
// Various formatters. |
|
var formatNumber = d3.format(",d"), |
|
formatChange = d3.format("+,d"), |
|
formatDate = d3.timeFormat("%B %d, %Y"), |
|
formatTime = d3.timeFormat("%I:%M %p"); |
|
|
|
// A nest operator, for grouping the flight list. |
|
var nestByDate = d3.nest() |
|
.key(function(d) {return d3.timeDay(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 crossfilter for the relevant dimensions and groups. |
|
var flight = crossfilter(flights), |
|
all = flight.groupAll(), |
|
date = flight.dimension(function(d) {return d.date}), |
|
dates = date.group(d3.timeDay), |
|
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(1999, d.distance)}), |
|
distances = distance.group(function(d) {return Math.floor(d / 50) * 50}); |
|
|
|
var charts = [ |
|
|
|
barChart() |
|
.dimension(hour) |
|
.group(hours) |
|
.x(d3.scaleLinear() |
|
.domain([0, 24]) |
|
.rangeRound([0, 10 * 24])), |
|
|
|
barChart() |
|
.dimension(delay) |
|
.group(delays) |
|
.x(d3.scaleLinear() |
|
.domain([-60, 150]) |
|
.rangeRound([0, 10 * 21])), |
|
|
|
barChart() |
|
.dimension(distance) |
|
.group(distances) |
|
.x(d3.scaleLinear() |
|
.domain([0, 2000]) |
|
.rangeRound([0, 10 * 40])), |
|
|
|
barChart() |
|
.dimension(date) |
|
.group(dates) |
|
.round(d3.timeDay.round) |
|
.x(d3.scaleTime() |
|
.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) |
|
|
|
// 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.timeFormat, 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.exit().remove(); |
|
|
|
date.enter().append("div") |
|
.attr("class", "date") |
|
.append("div") |
|
.attr("class", "day") |
|
.text(function(d) {return formatDate(d.values[0].date)}) |
|
.merge(date); |
|
|
|
|
|
var flight = date.order().selectAll(".flight") |
|
.data(function(d) {return d.values}, function(d) {return d.index}); |
|
|
|
flight.exit().remove(); |
|
|
|
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."}); |
|
|
|
flightEnter.merge(flight); |
|
|
|
flight.order(); |
|
}); |
|
} |
|
|
|
function barChart() { |
|
if (!barChart.id) barChart.id = 0; |
|
|
|
var margin = {top: 10, right: 10, bottom: 20, left: 10}, |
|
x, |
|
y = d3.scaleLinear().range([100, 0]), |
|
id = barChart.id++, |
|
axis = d3.axisBottom(), |
|
brush = d3.brushX(), |
|
brushDirty, |
|
dimension, |
|
group, |
|
round, |
|
gBrush; |
|
|
|
function chart(div) { |
|
var width = x.range()[1], |
|
height = y.range()[0]; |
|
|
|
brush.extent([[0, 0], [width, height]]) |
|
|
|
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. |
|
gBrush = g.append("g") |
|
.attr("class", "brush") |
|
.call(brush); |
|
|
|
gBrush.selectAll(".handle--custom") |
|
.data([{type: "w"}, {type: "e"}]) |
|
.enter().append("path") |
|
.attr("class", "brush-handle") |
|
.attr("cursor", "ew-resize") |
|
.attr("d", resizePath) |
|
.style("display", "none") |
|
} |
|
|
|
// Only redraw the brush if set externally. |
|
if (brushDirty != false) { |
|
var filterVal = brushDirty; |
|
brushDirty = false; |
|
|
|
div.select(".title a").style("display", d3.brushSelection(div) ? null : "none"); |
|
|
|
if (!filterVal) { |
|
g.call(brush) |
|
|
|
g.selectAll("#clip-" + id + " rect") |
|
.attr("x", 0) |
|
.attr("width", width); |
|
|
|
g.selectAll(".brush-handle").style("display", "none") |
|
renderAll(); |
|
|
|
} else { |
|
var range = filterVal.map(x) |
|
brush.move(gBrush, range) |
|
} |
|
} |
|
|
|
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.type == "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("start.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); |
|
var brushRange = d3.event.selection || d3.brushSelection(this); // attempt to read brush range |
|
var xRange = x && x.range(); // attempt to read range from x scale |
|
var activeRange = brushRange || xRange; // default to x range if no brush range available |
|
|
|
var hasRange = activeRange && |
|
activeRange.length === 2 && |
|
!isNaN(activeRange[0]) && |
|
!isNaN(activeRange[1]); |
|
|
|
if (!hasRange) return; // quit early if we don't have a valid range |
|
|
|
// calculate current brush extents using x scale |
|
var extents = activeRange.map(x.invert); |
|
|
|
// if rounding fn supplied, then snap to rounded extents |
|
// and move brush rect to reflect rounded range bounds if it was set by user interaction |
|
if (round) { |
|
extents = extents.map(round); |
|
activeRange = extents.map(x); |
|
|
|
if (d3.event.sourceEvent && |
|
d3.event.sourceEvent.type === "mousemove") { |
|
d3.select(this).call(brush.move, activeRange) |
|
} |
|
} |
|
|
|
// move brush handles to start and end of range |
|
g.selectAll(".brush-handle") |
|
.style("display", null) |
|
.attr("transform", function(d, i) { |
|
return "translate(" + activeRange[i] + ", 0)" |
|
}); |
|
|
|
// resize sliding window to reflect updated range |
|
g.select("#clip-" + id + " rect") |
|
.attr("x", activeRange[0]) |
|
.attr("width", activeRange[1] - activeRange[0]); |
|
|
|
// filter the active dimension to the range extents |
|
dimension.filterRange(extents); |
|
|
|
// re-render the other charts accordingly |
|
renderAll(); |
|
}); |
|
|
|
brush.on("end.chart", function() { |
|
// reset corresponding filter if the brush selection was cleared |
|
// (e.g. user "clicked off" the active range) |
|
if (!d3.brushSelection(this)) { |
|
reset(id); |
|
} |
|
}); |
|
|
|
chart.margin = function(_) { |
|
if (!arguments.length) return margin; |
|
margin = _; |
|
return chart; |
|
}; |
|
|
|
chart.x = function(_) { |
|
if (!arguments.length) return x; |
|
x = _; |
|
axis.scale(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 (!_) dimension.filterAll(); |
|
brushDirty = _; |
|
return chart; |
|
}; |
|
|
|
chart.group = function(_) { |
|
if (!arguments.length) return group; |
|
group = _; |
|
return chart; |
|
}; |
|
|
|
chart.round = function(_) { |
|
if (!arguments.length) return round; |
|
round = _; |
|
return chart; |
|
}; |
|
|
|
chart.gBrush = function() { |
|
return gBrush |
|
} |
|
|
|
return chart; |
|
} |
|
}); |
|
</script> |