block by alexmacy ebe599703421757852d36bcf71174dfc

Updated Crossfilter.js demo

Full Screen

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

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

<div id="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>

</div>

	<script src="//alexmacy.github.io/crossfilter/crossfilter.v1.min.js"></script>
  <script src="//d3js.org/d3.v4.min.js"></script>
<script>
// (It's CSV, but GitHub Pages only gzip's JSON at the moment.)
d3.csv("https://alexmacy.github.io/crossfilter/flights-3m.json", function(error, flights) {
console.log(flights.length)
  // Various formatters.
  var formatNumber = d3.format(",d"),
      formatChange = d3.format("+,d"),
      formatDate = d3.timeFormat("%B %d, %Y"),
      formatTime = d3.timeFormat("%I:%M %p");

  // A nest operator, for grouping the flight list.
  var nestByDate = d3.nest()
      .key(function(d) {return d3.timeDay(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;
  });

  // Create the crossfilter for the relevant dimensions and groups.
  var flight = crossfilter(flights),
      all = flight.groupAll(),
      date = flight.dimension(function(d) {return d.date}),
      dates = date.group(d3.timeDay),
      hour = flight.dimension(function(d) {return d.date.getHours() + d.date.getMinutes() / 60}),
      hours = hour.group(Math.floor),
      delay = flight.dimension(function(d) {return Math.max(-60, Math.min(149, d.delay))}),
      delays = delay.group(function(d) {return Math.floor(d / 10) * 10}),
      distance = flight.dimension(function(d) {return Math.min(1999, d.distance)}),
      distances = distance.group(function(d) {return Math.floor(d / 50) * 50});

  var 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.
  var chart = d3.selectAll(".chart")
      .data(charts)

  // Render the initial lists.
  var 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 = function(filters) {
    filters.forEach(function(d, i) {charts[i].filter(d)});
    renderAll();
  };

  window.reset = function(i) {
    charts[i].filter(null);
    renderAll();
  };

  function flightList(div) {
    var flightsByDate = nestByDate.entries(date.top(40));

    div.each(function() {
      var date = d3.select(this).selectAll(".date")
          .data(flightsByDate, function(d) {return d.key});

      date.exit().remove();

      date.enter().append("div")
          .attr("class", "date")
        .append("div")
          .attr("class", "day")
          .text(function(d) {return formatDate(d.values[0].date)})
        .merge(date);


      var flight = date.order().selectAll(".flight")
          .data(function(d) {return d.values}, function(d) {return d.index});

      flight.exit().remove();

      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."});

      flightEnter.merge(flight);

      flight.order();
    });
  }

  function barChart() {
    if (!barChart.id) barChart.id = 0;

    var margin = {top: 10, right: 10, bottom: 20, left: 10},
        x,
        y = d3.scaleLinear().range([100, 0]),
        id = barChart.id++,
        axis = d3.axisBottom(),
        brush = d3.brushX(),
        brushDirty,
        dimension,
        group,
        round,
        gBrush;

    function chart(div) {
      var width = x.range()[1],
          height = y.range()[0];

      brush.extent([[0, 0], [width, height]])

      y.domain([0, group.top(1)[0].value]);

      div.each(function() {
        var div = d3.select(this),
            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", function(d) {return 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) {
          var 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 {
            var range = filterVal.map(x)
            brush.move(gBrush, range)
          }
        }

        g.selectAll(".bar").attr("d", barPath);
      });

      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);
        }
        return path.join("");
      }

      function resizePath(d) {
        var e = +(d.type == "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);
      }
    }

    brush.on("start.chart", function() {
      var div = d3.select(this.parentNode.parentNode.parentNode);
      div.select(".title a").style("display", null);
    });

    brush.on("brush.chart", function() {
      var g = d3.select(this.parentNode);
      var brushRange = d3.event.selection || d3.brushSelection(this); // attempt to read brush range
      var xRange = x && x.range(); // attempt to read range from x scale
      var activeRange = brushRange || xRange; // default to x range if no brush range available

      var 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
      var 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", function(d, i) {
            return "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 = function(_) {
      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 = function() {
      return gBrush
    }

    return chart;
  }
});
</script>