This visualization is a slightly expanded version of the excellent original Crossfilter demo here
The visualization adds four items to the original example:
Note: added comments and code blocks are marked "BoE".
To see the visualization in action, go here. The project on Github is here, and Github Gist here
forked from mayblue9's block: Adding Day Filter to Crossfilter Example
xxxxxxxxxx
<meta charset="utf-8">
<!--
This visualization is a slightly expanded version of the excellent crossfilter demo at https://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 {
/*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;
}
.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)
// https://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
//https://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: https://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>
https://cdnjs.cloudflare.com/ajax/libs/crossfilter/1.3.12/crossfilter.min.js
https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.13/d3.min.js