Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@micahstubbs
Last active September 15, 2023 07:01
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save micahstubbs/66db7c01723983ff028584b6f304a54a to your computer and use it in GitHub Desktop.
Save micahstubbs/66db7c01723983ff028584b6f304a54a to your computer and use it in GitHub Desktop.
Crossfilter Demo | es2015 d3v4
license: MIT
border: no
height: 1150

this iteration converts the code to ES2015 in something like the airbnb style

forked from @alexmacy's block: Updated Crossfilter.js demo

see also an earlier iteration that retains the plot width and table width of the original Crossfilter example at http://square.github.io/crossfilter/

commit history


This is an updated version of this demo of the crossfilter library. Crossfilter has been one of my favorite - and what I think to be on of the most underrated - JavaScript libraries. It hasn't seen much of any updates in quite a while, so I wanted to find out how it would work with version 4 of d3.js.

There were some issues that came up with how d3-brush has been updated for v4. Big thanks goes to Alastair Dant (@ajdant) for helping to figure out a couple of those issues!

Also worth reading, is this discussion started by Robert Monfera (@monfera).

<!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: 430px;
}
#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: 925px;
margin-bottom: 1px;
}
#flight-list .time {
color: #999;
}
#flight-list .flight div {
display: inline-block;
}
#flight-list div.time {
width: 100px;
text-align: left;
}
#flight-list div.origin {
width: 50px;
text-align: right;
padding-right: 15px;
}
#flight-list div.destination {
width: 100px;
text-align: left;
padding-left: 15px;
}
#flight-list div.distance {
width: 100px;
text-align: left;
}
#flight-list div.delay {
width: 120px;
padding-right: 0px;
text-align: right;
}
#flight-list .early {
color: green;
}
aside {
position: absolute;
left: 740px;
font-size: smaller;
width: 220px;
}
</style>
<head>
<script src='//alexmacy.github.io/crossfilter/crossfilter.v1.min.js' defer></script>
<script src='//d3js.org/d3.v4.min.js' defer></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.23.1/babel.min.js' defer></script>
<script src='vis.js' defer></script>
</head>
<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>
</body>
/* global d3 crossfilter reset */
// (It's CSV, but GitHub Pages only gzip's JSON at the moment.)
d3.csv('https://alexmacy.github.io/crossfilter/flights-3m.json', (error, flights) => {
console.log(flights.length);
// Various formatters.
const formatNumber = d3.format(',d');
const formatChange = d3.format('+,d');
const formatDate = d3.timeFormat('%B %d, %Y');
const formatTime = d3.timeFormat('%I:%M %p');
// A nest operator, for grouping the flight list.
const nestByDate = d3.nest()
.key(d => d3.timeDay(d.date));
// A little coercion, since the CSV is untyped.
flights.forEach((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.
const flight = crossfilter(flights);
const all = flight.groupAll();
const date = flight.dimension(d => d.date);
const dates = date.group(d3.timeDay);
const hour = flight.dimension(d => d.date.getHours() + d.date.getMinutes() / 60);
const hours = hour.group(Math.floor);
const delay = flight.dimension(d => Math.max(-60, Math.min(149, d.delay)));
const delays = delay.group(d => Math.floor(d / 10) * 10);
const distance = flight.dimension(d => Math.min(1999, d.distance));
const distances = distance.group(d => Math.floor(d / 50) * 50);
const 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.
const chart = d3.selectAll('.chart')
.data(charts);
// Render the initial lists.
const 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 = filters => {
filters.forEach((d, i) => { charts[i].filter(d); });
renderAll();
};
window.reset = i => {
charts[i].filter(null);
renderAll();
};
function flightList(div) {
const flightsByDate = nestByDate.entries(date.top(40));
div.each(function () {
const date = d3.select(this).selectAll('.date')
.data(flightsByDate, d => d.key);
date.exit().remove();
date.enter().append('div')
.attr('class', 'date')
.append('div')
.attr('class', 'day')
.text(d => formatDate(d.values[0].date))
.merge(date);
const flight = date.order().selectAll('.flight')
.data(d => d.values, d => d.index);
flight.exit().remove();
const flightEnter = flight.enter().append('div')
.attr('class', 'flight');
flightEnter.append('div')
.attr('class', 'time')
.text(d => formatTime(d.date));
flightEnter.append('div')
.attr('class', 'origin')
.text(d => d.origin);
flightEnter.append('div')
.attr('class', 'destination')
.text(d => d.destination);
flightEnter.append('div')
.attr('class', 'distance')
.text(d => `${formatNumber(d.distance)} mi.`);
flightEnter.append('div')
.attr('class', 'delay')
.classed('early', d => d.delay < 0)
.text(d => `${formatChange(d.delay)} min.`);
flightEnter.merge(flight);
flight.order();
});
}
function barChart() {
if (!barChart.id) barChart.id = 0;
let margin = { top: 10, right: 13, bottom: 20, left: 10 };
let x;
let y = d3.scaleLinear().range([100, 0]);
const id = barChart.id++;
const axis = d3.axisBottom();
const brush = d3.brushX();
let brushDirty;
let dimension;
let group;
let round;
let gBrush;
function chart(div) {
const width = x.range()[1];
const height = y.range()[0];
brush.extent([[0, 0], [width, height]]);
y.domain([0, group.top(1)[0].value]);
div.each(function () {
const div = d3.select(this);
let 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', d => `${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) {
const 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 {
const range = filterVal.map(x);
brush.move(gBrush, range);
}
}
g.selectAll('.bar').attr('d', barPath);
});
function barPath(groups) {
const path = [];
let i = -1;
const n = groups.length;
let 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) {
const e = +(d.type === 'e');
const x = e ? 1 : -1;
const y = height / 3;
return `M${0.5 * x},${y}A6,6 0 0 ${e} ${6.5 * x},${y + 6}V${2 * y - 6}A6,6 0 0 ${e} ${0.5 * x},${2 * y}ZM${2.5 * x},${y + 8}V${2 * y - 8}M${4.5 * x},${y + 8}V${2 * y - 8}`;
}
}
brush.on('start.chart', function () {
const div = d3.select(this.parentNode.parentNode.parentNode);
div.select('.title a').style('display', null);
});
brush.on('brush.chart', function () {
const g = d3.select(this.parentNode);
const brushRange = d3.event.selection || d3.brushSelection(this); // attempt to read brush range
const xRange = x && x.range(); // attempt to read range from x scale
let activeRange = brushRange || xRange; // default to x range if no brush range available
const 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
let 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', (d, i) => `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 = _ => {
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 = () => gBrush;
return chart;
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment