Skip to content

Instantly share code, notes, and snippets.

@micahstubbs
Last active February 20, 2017 23:38
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save micahstubbs/6eab2af6785d9e01589f714131640f40 to your computer and use it in GitHub Desktop.
Crossfilter Demo | es2015 d3v4 | original widths
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

this iteration retains the plot width and table width of the original Crossfilter example at http://square.github.io/crossfilter/

see also this later iteration with improved plot and table widths

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: 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>
<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: 10, 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