block by renecnielsen b4a505a4e83c32b849871e6197d5f8c1

Crossfilter Demo | es2015 d3v4

Full Screen

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).

forked from micahstubbs‘s block: Crossfilter Demo | es2015 d3v4

index.html


<!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>


vis.js

/* 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;
  }
});