|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<!-- |
|
This visualization is a slightly expanded version of the excellent crossfilter demo at http://square.github.io/crossfilter/ |
|
The viz adds four items to the original example: |
|
- ability to filter on days and day types |
|
- a dataset view, that allows all of the 230K records to be viewed in a canvas element |
|
- a timer function/object that can be used to measure crossfilter performance |
|
- expanded and more detailed comments |
|
|
|
Please see the README.md for additional information |
|
|
|
Note: added comments and code blocks are marked "BoE" |
|
Author of added features: Bo Ericsson, bo@boe.net |
|
--> |
|
<title>Crossfilter with Day Filter</title> |
|
<style> |
|
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 { |
|
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; |
|
} |
|
.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; |
|
} |
|
fieldset { |
|
margin-bottom: 20px; |
|
border: 1px solid lightgray; |
|
} |
|
fieldset > label, |
|
span > label { |
|
margin-right: 10px; |
|
font-size: 12px; |
|
} |
|
label.selected { |
|
color: black; |
|
} |
|
label.notSelected { |
|
color: lightgray; |
|
} |
|
</style> |
|
<body> |
|
<div id="body"> |
|
<div> |
|
<a href="javascript:resetAll()" class="reset" style="margin-left: -10px; margin-bottom: 20px">Reset all</a> |
|
</div> |
|
<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> |
|
<div id="daySelectionDiv"></div> |
|
<div id="canvasDiv"></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="https://cdnjs.cloudflare.com/ajax/libs/crossfilter/1.3.12/crossfilter.min.js"></script> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.13/d3.min.js"></script> |
|
<script> |
|
"use strict"; |
|
|
|
// ensure at least 790px height if running in an iframe (bl.ocks) |
|
// http://bl.ocks.org/mbostock/1093025 |
|
d3.select(self.frameElement).transition().duration(500).style("height", "790px"); |
|
|
|
// BoE: add time logger function |
|
function timeLogger() { |
|
if (timeLogger.id == undefined) timeLogger.id = 0; |
|
else ++timeLogger.id; |
|
|
|
var events = []; |
|
var initTs = new Date().getTime(); |
|
var currTs = initTs; |
|
|
|
return { |
|
log: function (event) { |
|
var ts = new Date().getTime(); |
|
var duration = ts - currTs; |
|
events.push({ event: event, duration: duration }) |
|
currTs = ts; |
|
}, |
|
reset: function() { |
|
currTs = new Date().getTime(); |
|
}, |
|
results: function () { |
|
var results = events.concat([{ event: "total elapsed", duration: new Date().getTime() - initTs }]); |
|
results.forEach(function(d) { console.log(d.event, " --> ", d.duration)}) |
|
return results; |
|
} |
|
}; |
|
}; |
|
|
|
// BoE: start two time loggers |
|
var tl = timeLogger(); |
|
var tl2 = timeLogger(); |
|
|
|
// BoE: get the data |
|
d3.csv("xFlights-3m.csv", function(error, flights) { |
|
|
|
// BoE: add usage example of time logger |
|
tl2.log("data load"); |
|
tl2.results(); |
|
|
|
// BoE: add array that holds the currently selected "in-filter" selected records |
|
var selected = []; |
|
|
|
// Various formatters. |
|
var formatNumber = d3.format(",d"), |
|
formatChange = d3.format("+,d"), |
|
formatDate = d3.time.format("%B %d, %Y"), |
|
formatDateWithDay = d3.time.format("%a %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; |
|
d.selected = false; // BoE add |
|
}); |
|
|
|
// Create the crossfilter for the relevant dimensions and groups. |
|
// BoE: the "tl" stuff below are adds to the original example, to compute time consumed at various steps |
|
tl.reset(); |
|
var flight = crossfilter(flights); |
|
tl.log("crossfilter constructor"); |
|
var all = flight.groupAll(); |
|
tl.log("flight.groupAll") |
|
|
|
// date dimension |
|
tl.reset(); |
|
var date = flight.dimension(function(d) { return d.date; }); // date dim |
|
tl.log("date dimension"); |
|
var dates = date.group(d3.time.day); // date group |
|
tl.log("date.group") |
|
dates.groupId = "dates"; |
|
|
|
// hour dimension |
|
tl.reset(); |
|
var hour = flight.dimension(function(d) { return d.date.getHours() + d.date.getMinutes() / 60; }); // hour dim |
|
tl.log("hour dimension") |
|
var hours = hour.group(Math.floor); // hour group |
|
tl.log("hour.group()"); |
|
hours.groupId = "hours"; |
|
|
|
// delay dimension |
|
tl.reset(); |
|
var delay = flight.dimension(function(d) { return Math.max(-60, Math.min(149, d.delay)); }); // delay dim |
|
tl.log("delay dimension"); |
|
var delays = delay.group(function(d) { return Math.floor(d / 10) * 10; }); // delay group |
|
tl.log("delay.group") |
|
delays.groupId = "delays"; |
|
|
|
// distances dimension |
|
tl.reset() |
|
var distance = flight.dimension(function(d) { return Math.min(1999, d.distance); }); // distance dim |
|
tl.log("distance dimension") |
|
var distances = distance.group(function(d) { return Math.floor(d / 50) * 50; }); // distance group |
|
tl.log("distance.group") |
|
distances.groupId = "distances"; |
|
|
|
// BoE: add new day dimension |
|
tl.reset(); |
|
var dayNumber = flight.dimension(function(d) { return d.date.getDay(); }); |
|
tl.log("dayNumber dimension"); |
|
var dayNumbers = dayNumber.group(function(d) { return d; }); |
|
tl.log("dayNumber.group") |
|
console.log("--", tl.results()) |
|
|
|
// BoE: add day selection variables |
|
var days = { |
|
mon: { state: true, name: "days", text: "Monday", type: "workDay", dayNumber: 1, order: 0 }, |
|
tue: { state: true, name: "days", text: "Tuesday", type: "workDay", dayNumber: 2, order: 1 }, |
|
wed: { state: true, name: "days", text: "Wednesday", type: "workDay", dayNumber: 3, order: 2 }, |
|
thu: { state: true, name: "days", text: "Thursday", type: "workDay", dayNumber: 4, order: 3 }, |
|
fri: { state: true, name: "days", text: "Friday", type: "workDay", dayNumber: 5, order: 4 }, |
|
sat: { state: true, name: "days", text: "Saturday", type: "weekendDay", dayNumber: 6, order: 5 }, |
|
sun: { state: true, name: "days", text: "Sunday", type: "weekendDay", dayNumber: 0, order: 6 } |
|
}, |
|
workDays = Object.keys(days).filter(function(d) { if (days[d].type == "workDay") return true; }).map(function(d) { return d; }), |
|
weekendDays = Object.keys(days).filter(function(d) { if (days[d].type == "weekendDay") return true; }).map(function(d) { return d; }), |
|
dayNumbers = (function() { var obj = {}; Object.keys(days).forEach(function(d) { var key = days[d].dayNumber; var value = d; obj[key] = value }); return obj; })(), |
|
dayTypes = { |
|
weekendDays: { state: false, name: "dayType", text: "Weekend Days", order: 2 }, |
|
allDays: { state: true, name: "dayType", text: "All Days", order: 0 }, |
|
workDays: { state: false, name: "dayType", text: "Work Days", order: 1 }, |
|
someDays: { state: false, name: "dayType", text: "Some Days", order: 3, last: true } |
|
}; |
|
|
|
/* |
|
// BoE: uncomment this to see the day related variables |
|
console.log("workDays", workDays) |
|
console.log("weekendDays", weekendDays) |
|
console.log("dayNumbers", dayNumbers) |
|
*/ |
|
|
|
// BoE: prep add radio buttons and checkbox data |
|
var radioData = Object.keys(dayTypes).map(function(d) { dayTypes[d].value = d; return dayTypes[d]; }).sort(function(a, b) { return (a.order > b.order ? 1 : (a.order < b.order) ? -1 : 0) }) |
|
var checkboxData = Object.keys(days).map(function(d) { days[d].value = d; return days[d]; }).sort(function(a, b) { return (a.order > b.order ? 1 : (a.order < b.order) ? -1 : 0) }) |
|
|
|
// BoE: create radio buttons |
|
//http://stackoverflow.com/questions/19302318/radio-buttons-in-d3-how-to-align-text-correctly-and-select-a-default |
|
var fieldset = d3.select("#daySelectionDiv").append("fieldset") |
|
fieldset.append("legend").text("Day of Week"); |
|
|
|
// BoE: add spans to hold radio buttons |
|
var radioSpan = fieldset.selectAll(".radio") |
|
.data(radioData) |
|
.enter().append("span") |
|
.attr("class", "radio") |
|
.style("margin-right", function(d) { return d.last == true ? "30px" : "0px" }); |
|
|
|
// BoE: add radio button to each span |
|
radioSpan.append("input") |
|
.attr({ |
|
type: "radio", |
|
name: function(d) { return d.name }, |
|
}) |
|
.property({ |
|
checked: function(d) { return d.state }, |
|
value: function(d) { return d.value } |
|
}); |
|
|
|
// BoE: add radio button label |
|
radioSpan.append("label") |
|
.text(function(d) { return d.text }); |
|
|
|
// BoE: add spans to hold checkboxes |
|
var checkboxSpan = fieldset.selectAll(".checkbox") |
|
.data(checkboxData) |
|
.enter().append("span") |
|
.attr("class", "checkbox") |
|
//.style("margin-right", "10px") |
|
|
|
// BoE: add checkbox to each span |
|
checkboxSpan |
|
.append("input") |
|
//.attr("type", "checkbox") |
|
//.attr("name", function(d) { return d.name }) |
|
.attr({ |
|
type: "checkbox", |
|
name: function(d) { return d.name } |
|
}) |
|
//.property("value", function(d) { return d.value }) |
|
//.property("checked", function(d) { return d.state }) |
|
.property({ |
|
value: function(d) { return d.value }, |
|
checked: function(d) { return d.state } |
|
}) |
|
|
|
// BoE: add checkbox label |
|
checkboxSpan |
|
.append("label") |
|
.text(function(d) { return d.text }) |
|
|
|
|
|
// BoE: add radio button event handler |
|
d3.selectAll("input[type=radio][name=dayType]") |
|
.on("change", function() { |
|
var elem = d3.select(this); |
|
var dayType = elem.property("value"); |
|
switch (dayType) { |
|
case "allDays": |
|
case "someDays": |
|
workDays.forEach(function(day) { days[day].state = true; }) |
|
weekendDays.forEach(function(day) { days[day].state = true; }) |
|
break; |
|
case "workDays": |
|
workDays.forEach(function(day) { days[day].state = true; }) |
|
weekendDays.forEach(function(day) { days[day].state = false; }) |
|
break; |
|
case "weekendDays": |
|
workDays.forEach(function(day) { days[day].state = false; }) |
|
weekendDays.forEach(function(day) { days[day].state = true; }) |
|
break; |
|
} |
|
|
|
updateDaySelection(); |
|
renderAll(); |
|
}); |
|
|
|
// BoE: init checkboxes and add event handler |
|
d3.selectAll("input[type=checkbox][name=days]") |
|
.property("checked", function(d, i, a) { |
|
var elem = d3.select(this); |
|
var day = elem.property("value"); |
|
//console.log("elem", elem, "day", day, days[day]) |
|
return days[day].state; |
|
}) |
|
.on("change", function() { |
|
var elem = d3.select(this); |
|
var checked = elem.property("checked"); |
|
var day = elem.property("value"); |
|
days[day].state = checked; |
|
|
|
updateDaySelection(); |
|
renderAll(); |
|
}) |
|
|
|
|
|
// BoE: update the state of the day selection radio buttons and checkboxes (called after "change" events from those elements) |
|
function updateDaySelection() { |
|
// BoE: update checkboxes |
|
d3.selectAll("input[type=checkbox][name=days]") |
|
.property("checked", function(d, i, a) { |
|
var elem = d3.select(this); |
|
var day = elem.property("value"); |
|
return days[day].state; |
|
}) |
|
|
|
// BoE: process selected days |
|
var workDayCount = workDays.reduce(function(p, c) { return (days[c].state) ? p + 1 : p }, 0); |
|
var weekendDayCount = weekendDays.reduce(function(p, c) { return (days[c].state) ? p + 1 : p }, 0); |
|
|
|
// BoE: determine day type |
|
var dayType = "someDays"; |
|
if ((workDayCount + weekendDayCount) == 7) dayType = "allDays" |
|
else if (workDayCount == 5 && weekendDayCount == 0) dayType = "workDays" |
|
else if (workDayCount == 0 && weekendDayCount == 2) dayType = "weekendDays" |
|
|
|
// BoE: set the selected radio button |
|
d3.selectAll("input[type=radio][value=" + dayType + "]").property("checked", true); |
|
|
|
// BoE: create/update day number filter |
|
dayNumber.filter(function(d) { return days[dayNumbers[d]].state; }) |
|
} |
|
|
|
// BoE: create canvas element that holds one record per canvas pixel |
|
var canvasWidth = d3.select("body").property("clientWidth") - 10, |
|
canvasHeight = Math.ceil(flight.size() / canvasWidth), |
|
canvas, |
|
ctx, |
|
canvasDiv, |
|
currentLabel; |
|
|
|
selected = date.top(Infinity); |
|
|
|
// BoE: get reference to canvas div |
|
canvasDiv = d3.select("#canvasDiv") |
|
.style("padding", "2px") |
|
.style("width", "100%") |
|
.style("margin-bottom", "20px") |
|
|
|
// BoE: add div to hold description of canvas content |
|
canvasDiv |
|
.append("div") |
|
.attr("class", "title") |
|
.style("margin-bottom", "20px") |
|
.style("margin-bottom", "10px") |
|
.html("<span>Dataset View – Each of the " + formatNumber(flight.size()) + " pixels represents a data record (White = Selected, Black = Not Selected, Red = Out of Bounds)</span><br><span style='font-style: italic; font-size: 14px; color: red;'>Move the mouse over the canvas below to see individual data records</span>"); |
|
|
|
// BoE: add canvas element and mouse handler |
|
canvas = canvasDiv |
|
.append("canvas") |
|
.attr("width", canvasWidth) |
|
.attr("height", canvasHeight) |
|
.style("width", canvasWidth + "px") |
|
.style("height", canvasHeight + "px") |
|
.style("border", "1px solid lightgray") |
|
.style("display", "block") |
|
.style("cursor", "crosshair") |
|
.on("mousemove", function() { |
|
var mouse = d3.mouse(canvas.node()), |
|
x = Math.round(mouse[0]), |
|
y = Math.round(mouse[1]), |
|
index = y * canvasWidth + x; |
|
|
|
// BoE: event handler delivers mouse mousemove events outside the canvas sometimes (don't know why); |
|
// therefore, ensure that only valid x and y values are processed |
|
if (x < 0 || x > canvasWidth - 1 || y < 0 || y > canvasHeight -1) { |
|
currentLabel.html(" "); |
|
return; |
|
} |
|
// then check if the index is out of bounds (the right part of the last row is NOT part of the dataset) |
|
if (index > flight.size() - 1) { |
|
currentLabel.html(" "); |
|
return; |
|
} |
|
|
|
var item = flights[index], |
|
dateText = formatDateWithDay(item.date), |
|
timeText = formatTime(item.date); |
|
|
|
var labelText = labelText = "Selected: " + item.selected + ", Date: " + dateText + " " + timeText + ", Delay: "; |
|
labelText += item.delay + ", Distance: " + item.distance + ", Route: " + item.origin + "-->" + item.destination + " (idx: " + index + ")"; |
|
currentLabel |
|
.attr("class", function(d) { return item.selected ? "selected" : "notSelected" }) |
|
.text(labelText); |
|
}) |
|
.on("mouseleave", function() { |
|
currentLabel.html(" "); |
|
}) |
|
|
|
// BoE: create label for mousemove |
|
currentLabel = canvasDiv.append("label").html(" "); |
|
|
|
// BoE: get context to canvas elem |
|
ctx = canvas.node().getContext('2d'); |
|
|
|
// BoE: this code defines the four charts in an array |
|
var charts = [ |
|
|
|
barChart() |
|
.dimension(hour) |
|
.group(hours) |
|
.x(d3.scale.linear() |
|
.domain([0, 24]) |
|
.rangeRound([0, 10 * 24])) // 10 pixels per bar, 240 pixels total |
|
.filter([8, 18]), // added by BoE |
|
|
|
barChart() |
|
.dimension(delay) |
|
.group(delays) |
|
.x(d3.scale.linear() |
|
.domain([-60, 150]) |
|
.rangeRound([0, 10 * 21])), // 21 delay groups, 210 pixels total |
|
|
|
barChart() |
|
.dimension(distance) |
|
.group(distances) |
|
.x(d3.scale.linear() |
|
.domain([0, 2000]) |
|
.rangeRound([0, 10 * 40])), // 40 distance groups |
|
|
|
barChart() |
|
.dimension(date) |
|
.group(dates) |
|
.round(d3.time.day.round) // ensures whole days |
|
.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") |
|
// BoE: the charts array defined above are provided as data to the d3 selection of ".charts" |
|
.data(charts) |
|
.each(function(chart) { |
|
chart |
|
.on("brush", function() { |
|
renderAll(); |
|
}) |
|
.on("brushend", function() { |
|
renderAll(); |
|
}); |
|
}); |
|
|
|
// Render the initial lists. |
|
var list = d3.selectAll(".list") |
|
.data([flightList]); |
|
|
|
// Render the total. |
|
d3.selectAll("#total") |
|
.text(formatNumber(flight.size())); |
|
|
|
// BoE: initial render |
|
renderAll(); |
|
|
|
// BoE debug... |
|
var sum = dates.all().reduce(function(p, c, i, a) { |
|
return { value: p.value + c.value }; |
|
}) |
|
//console.log("sum", sum.value); |
|
|
|
// BoE debug... |
|
var groups = [dates, hours, delays, distances] |
|
groups.forEach(function(group) { |
|
var grp = group.group; |
|
var sum = group.all().reduce(function(p, c, i, a) { return { value: p.value + c.value } }) |
|
//console.log("sum: ", sum, "grp", grp) |
|
}) |
|
|
|
// Renders the specified chart or list. |
|
function render(method) { |
|
// BoE: "method" is the "d" value of data binding to chart above, which happens to be the chart function from barChart |
|
d3.select(this).call(method); |
|
} |
|
|
|
// Whenever the brush moves, re-rendering everything. |
|
function renderAll() { |
|
// BoE: uncomment the next lines to see the what's being rendered |
|
//console.log("renderAll", chart[0].length, list[0].length, all.value()) |
|
//chart.each(function(d, i) { console.log("this", this, "d", d, "i", i) }) |
|
chart.each(render); // render is called with this set to div and d set to the chart function from barChart |
|
list.each(render); |
|
d3.select("#active").text(formatNumber(all.value())); |
|
|
|
var tl3 = timeLogger(); |
|
// BoE: update the "selected" array, which holds the currently selected (in-filter) items |
|
selected = date.top(Infinity); |
|
tl3.log("date.top(Infinity)") |
|
|
|
// BoE: set the selected status in "flights" ("flights" is the data source) |
|
flights.forEach(function(d) { d.selected = false; }); // first clear all |
|
selected.forEach(function(d) { flights[d.index].selected = true; }) // then set some |
|
|
|
// BoE: clear canvas |
|
tl3.reset(); |
|
ctx.fillStyle = "rgb(0,0,0)"; |
|
ctx.fillRect (0, 0, canvasWidth, canvasHeight); |
|
tl3.log("filling canvas background") |
|
|
|
// BoE: add red out of bounds pixels |
|
tl3.reset(); |
|
var xSpan = (canvasWidth * canvasHeight) % flight.size(); |
|
var x = canvasWidth - xSpan; |
|
var y = canvasHeight - 1; |
|
ctx.fillStyle = "rgb(255, 0, 0)"; |
|
ctx.fillRect(x, y, xSpan, 1); |
|
tl3.log("setting red pixels") |
|
|
|
// BoE add: draw white pixel for each active element |
|
tl3.reset(); |
|
ctx.fillStyle = "rgb(255,255,255)"; |
|
selected.forEach(function(d) { |
|
var x = d.index % canvasWidth; |
|
var y = Math.floor(d.index / canvasWidth) |
|
ctx.fillRect(x, y, 1, 1 ); |
|
}) |
|
tl3.log("setting canvas pixels"); |
|
tl3.results(); |
|
} |
|
|
|
// 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)); |
|
} |
|
|
|
// BoE: this (window.resetAll) isn't used, therefore |
|
// repurposed this to reset all filters by javascript triggered by a new "a" tag in the DOM |
|
window.resetAll = function() { |
|
var filters = [null, null, null, null]; |
|
filters.forEach(function(d, i) { charts[i].filter(d); }); |
|
Object.keys(days).forEach(function(d) { days[d].state = true }); |
|
updateDaySelection(); |
|
renderAll(); |
|
}; |
|
|
|
// BoE: resets the filter for a particular dimension |
|
window.reset = function(i) { |
|
charts[i].filter(null); |
|
renderAll(); |
|
}; |
|
|
|
// BoE: this looks really wasteful – every cell is a div, why not just use a html5 table? |
|
function flightList(div) { |
|
|
|
var flightsByDate = nestByDate.entries(date.top(10)); |
|
|
|
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() { |
|
// BoE: barChart.id is shared by all instances of barChart's chart, effectively an instance counter |
|
if (!barChart.id) barChart.id = 0; |
|
|
|
var margin = {top: 10, right: 10, bottom: 20, left: 10}, |
|
x, |
|
y = d3.scale.linear().range([100, 0]), // 100 pixels height |
|
id = barChart.id++, |
|
axis = d3.svg.axis().orient("bottom"), |
|
brush = d3.svg.brush(), |
|
brushDirty, |
|
dimension, |
|
group, |
|
round, |
|
yMax; |
|
|
|
function chart(div) { |
|
/* |
|
// BoE: uncomment this to see how the domain and ranges change as the filters are modified |
|
console.log("margin", margin); |
|
console.log("x.range()", x.range()) |
|
console.log("x.domain()", x.domain()) |
|
console.log("y.range()", y.range()) |
|
console.log("y.domain()", y.domain()) |
|
*/ |
|
|
|
var width = x.range()[1], |
|
height = y.range()[0]; |
|
|
|
yMax = 0; |
|
y.domain([0, yMax == 0 ? group.top(1)[0].value : yMax]); // set y domain to max value in this group |
|
|
|
// BoE: why is this done using "each"? There is only one div per chart; therefore the construct "div.each" will |
|
// only be executed once |
|
div.each(function() { |
|
var div = d3.select(this), |
|
g = div.select("g"); |
|
|
|
// Create the skeletal chart. |
|
// BoE: this is only executed once, at init, when there is nothing in the group |
|
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 + ")"); |
|
|
|
// BoE: reset the clip path to full width |
|
g.append("clipPath") |
|
.attr("id", "clip-" + id) |
|
.append("rect") |
|
.attr("width", width) |
|
.attr("height", height); |
|
|
|
// BoE: generate two paths, one background, one foreground |
|
g.selectAll(".bar") |
|
.data(["background", "foreground"]) |
|
.enter().append("path") |
|
.attr("class", function(d) { return d + " bar"; }) |
|
// assign all the data in the group to the path |
|
.datum(group.all()); |
|
|
|
// BoE: assign the clip path to the foreground bars |
|
g.selectAll(".foreground.bar") |
|
.attr("clip-path", "url(#clip-" + id + ")"); |
|
|
|
// BoE: add the x-axis to the svg group |
|
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. |
|
// BoE: at init, the **Date** chart has an externally set brush; |
|
// in this extended example, the **Time of Day** chart also has an externally set brush |
|
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])); |
|
} |
|
} |
|
|
|
// BoE: this sets the **d** attribute on the path |
|
g.selectAll(".bar").attr("d", barPath); |
|
}); |
|
|
|
// BoE: the barPath function is called as usual with the d, i, a arguments |
|
// (d being called **groups** here, the other args not used) |
|
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); |
|
} |
|
// BoE: uncomment the next line to see the neat path array that has been generated |
|
//console.log("path", path) |
|
return path.join(""); |
|
} |
|
|
|
// BoE: the resizePath function defines the look of the brush "handles" on the left and right side of the brush |
|
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); |
|
} |
|
} |
|
|
|
// BoE: the .chart below threw me for a loop at first; turns out the app is registering multiple brush handlers; |
|
// see above for the first instance of brush handlers; to allow multiple handlers to be registered on the same event, |
|
// d3 requires a "namespace identifier" on the subsequent event handlers; here "chart" is used as a namespace indicator |
|
// alse see: http://stackoverflow.com/questions/32459420/what-does-d3-brush-onbrush-chat-means |
|
brush.on("brushstart.chart", function() { |
|
var div = d3.select(this.parentNode.parentNode.parentNode); |
|
div.select(".title a").style("display", null); |
|
}); |
|
|
|
// BoE: uncomment the next like to see another brush handler in action using a dummy namespace |
|
//brush.on("brush.whatever", function() { console.log("brush.whatever") }) |
|
|
|
// BoE: this "brush.chart" event handler fires before the "brush" handler fires above; |
|
// this particular handler just sets the filter; |
|
// the other handler updates all charts |
|
brush.on("brush.chart", function() { |
|
var g = d3.select(this.parentNode), |
|
extent = brush.extent(); // extent contains the domain (x.invert) of the brush |
|
|
|
// BoE: handle rounding of extent (allow only integers) |
|
if (round) g.select(".brush") |
|
.call(brush.extent(extent = extent.map(round))) |
|
.selectAll(".resize") |
|
.style("display", null); // remove the resize a element |
|
g.select("#clip-" + id + " rect") |
|
.attr("x", x(extent[0])) // set clip rect x pos |
|
.attr("width", x(extent[1]) - x(extent[0])); // set clip rect width |
|
|
|
// BoE: set a new filter range |
|
dimension.filterRange(extent); |
|
}); |
|
|
|
brush.on("brushend.chart", function() { |
|
if (brush.empty()) { |
|
// BoE: if brush is empty, invalidate the clip rect (show all foreground bars), and remove the dimension filter |
|
var div = d3.select(this.parentNode.parentNode.parentNode); |
|
|
|
// BoE: remove the reset "a" element |
|
div.select(".title a").style("display", "none"); |
|
|
|
// BoE: invalidate the clip rect (thereby show all foreground blue bars) |
|
div.select("#clip-" + id + " rect") |
|
.attr("x", null) // remove the x attribute which will render the clipRect invalid |
|
.attr("width", "100%"); |
|
|
|
// BoE: remove the dimension's filter |
|
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(_) { |
|
//console.log("chart.dimension..." + _) |
|
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 = _; |
|
// added by BoE |
|
yMax = group.top(1)[0].value; |
|
return chart; |
|
}; |
|
|
|
chart.round = function(_) { |
|
if (!arguments.length) return round; |
|
round = _; |
|
return chart; |
|
}; |
|
|
|
// BoE: the d3 rebind function moves the "on" method from the "brush" object/function to the "chart" object/function; |
|
// it then returns the "chart" function/object |
|
return d3.rebind(chart, brush, "on"); |
|
} |
|
}); |
|
</script> |
|
</body> |
|
</html> |