block by milroc 5519642

bar + sum: reusable d3.js

Full Screen

This is a six part series, taking you through stages of designing and creating reusable visualizations with d3.js

All visualizations have the same functionality, showcase the individual points with a bar chart and sum up the selected bars.

Part 2. This is showcasing the power of the reusable chart API. This is showcasing the difference between a reusable bar chart and a prototyped bar chart.

These are examples created for a talk (slides and video).

Cheers,

Miles @milr0c

index.html

<!DOCTYPE html>
  <head>
    <meta charset="utf-8">
    <link rel="stylesheet" type="text/css" href="//littlesparkvt.com/flatstrap/assets/css/bootstrap.css"/>
    <link type="text/css" rel="stylesheet" href="style.css"/>
    <script src="//d3js.org/d3.v3.min.js"></script>
    <script src="src.js"></script>
  </head>
  <body>
    <div class="row">
      <div class="span2"><button class="btn btn-success" onclick="update()">update</button></div>
      <div class="span2" id="sum">TOTAL: 0</div>
    </div>
    <div class="row" id="chart"></div>
    <script type="text/javascript">
var bar = charts.bar()
                .on('brush', makeSum)
                .on('brushend', makeSum),
    data;

update();

function update() {
  data = randomizeData(20, Math.random()*100000);

  d3.select("#chart")
      .datum(data)
      .call(bar);
}

function makeSum() {
  var sumDiv = d3.select('#sum'),
      extent = d3.event.target.extent()
      x = bar.x(),
      sum = 0;
  // inefficient recommend for performance to leverage crossfilter.js
  data.forEach(function(d) {
    if (extent[0] <= x(d.x) && x(d.x) + x.rangeBand() <= extent[1])
      sum += d.y;
  });
  sumDiv.text('TOTAL: ' + sum);
}  

function randomizeData(n, y) {
  if (arguments.length < 2) y = 400;
  if (!arguments.length) n = 20;
  var i = 0;
  return d3.range(~~(Math.random()*n) + 1).map(function(d, i) { return {
            x: ++i,
            y: ~~(Math.random()*y)
          }});
}
    </script>
  </body>
</html>

src.js

charts = {};

charts.bar = function() {
  // basic data
  var margin = {top: 0, bottom: 20, left: 0, right: 0},
      width = 400,
      height = 400,
      // accessors
      xValue = function(d) { return d.x; },
      yValue = function(d) { return d.y; },
      // chart underpinnings
      brush = d3.svg.brush(),
      xAxis = d3.svg.axis().orient('bottom'),
      yAxis = d3.svg.axis().orient('left'),
      x = d3.scale.ordinal(),
      y = d3.scale.linear(),
      // chart enhancements
      elastic = {
        margin: true,
        x: true,
        y: true
      },
      convertData = true,
      duration = 500,
      formatNumber = d3.format(',d');

  function render(selection) {
    selection.each(function(data) {
      // setup the basics
      if (elastic.margin) margin.left = formatNumber(d3.max(data, function(d) { return d.y; })).length * 14;
      var w = width - margin.left - margin.right,
          h = height - margin.top - margin.bottom;

      // if needed convert the data
      if (convertData) {
        data = data.map(function(d, i) {
          return {
            x: xValue.call(data, d, i),
            y: yValue.call(data, d, i)
          };
        });
      }

      // set scales
      if (elastic.x) x.domain(data.map(function(d) { return d.x; }));
      if (elastic.y) y.domain([0, d3.max(data, function(d) { return d.y; })]);
      x.rangeRoundBands([0, w], .1);
      y.range([h, 0]);


      // reset axes and brush
      xAxis.scale(x);
      yAxis.scale(y);
      brush.x(x)
          .on('brushstart.chart', brushstart)
          .on('brush.chart', brushmove)
          .on('brushend.chart', brushend);
      brush.clear();

      var svg = selection.selectAll('svg').data([data]),
          chartEnter = svg.enter().append('svg')
                                  .append('g')
                                    .attr('width', w)
                                    .attr('height', h)
                                    .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
                                    .classed('chart', true),
          chart = svg.select('.chart');

      chartEnter.append('g')
                .classed('x axis', true)
                .attr('transform', 'translate(' + 0 + ',' + h + ')');
      chartEnter.append('g')
                .classed('y axis', true)
      chartEnter.append('g').classed('barGroup', true);
      
      chart.selectAll('.brush').remove();
      chart.selectAll('.selected').classed('selected', false);
      
      chart.append('g')
                .classed('brush', true)
                .call(brush)
              .selectAll('rect')
                .attr('height', h);

      bars = chart.select('.barGroup').selectAll('.bar').data(data);

      bars.enter()
            .append('rect')
              .classed('bar', true)
              .attr('x', w) // start here for object constancy
              .attr('width', x.rangeBand())
              .attr('y', function(d, i) { return y(d.y); })
              .attr('height', function(d, i) { return h - y(d.y); });

      bars.transition()
            .duration(duration)
              .attr('width', x.rangeBand())
              .attr('x', function(d, i) { return x(d.x); })
              .attr('y', function(d, i) { return y(d.y); })
              .attr('height', function(d, i) { return h - y(d.y); });

      bars.exit()
            .transition()
                .duration(duration)
                    .style('opacity', 0)
                    .remove();

      chart.select('.x.axis')
            .transition()
                .duration(duration)
                  .call(xAxis);
      chart.select('.y.axis')
            .transition()
                .duration(duration)
                  .call(yAxis);  

      function brushstart() {
        chart.classed("selecting", true);
      }

      function brushmove() {
        var extent = d3.event.target.extent();
        bars.classed("selected", function(d) { return extent[0] <= x(d.x) && x(d.x) + x.rangeBand() <= extent[1]; });
      }

      function brushend() {
        chart.classed("selecting", !d3.event.target.empty());
      } 
    });
  }

  // basic data
  render.margin = function(_) {
    if (!arguments.length) return margin;
    margin = _;
    return render;
  };
  render.width = function(_) {
    if (!arguments.length) return width;
    width = _;
    return render;
  };
  render.height = function(_) {
    if (!arguments.length) return height;
    height = _;
    return render;
  };

  // accessors
  render.xValue = function(_) {
    if (!arguments.length) return xValue;
    xValue = _;
    return render;
  };
  render.yValue = function(_) {
    if (!arguments.length) return yValue;
    yValue = _;
    return render;
  };

  // chart underpinnings
  render.brush = function(_) {
    if (!arguments.length) return brush;
    brush = _;
    return render;
  };
  render.xAxis = function(_) {
    if (!arguments.length) return xAxis;
    xAxis = _;
    return render;
  };
  render.yAxis = function(_) {
    if (!arguments.length) return yAxis;
    yAxis = _;
    return render;
  };
  render.x = function(_) {
    if (!arguments.length) return x;
    x = _;
    return render;
  };
  render.y = function(_) {
    if (!arguments.length) return y;
    y = _;
    return render;
  };
  
  // chart enhancements
  render.elastic = function(_) {
    if (!arguments.length) return elastic;
    elastic = _;
    return render;
  };
  render.convertData = function(_) {
    if (!arguments.length) return convertData;
    convertData = _;
    return render;
  };
  render.duration = function(_) {
    if (!arguments.length) return duration;
    duration = _;
    return render;
  };
  render.formatNumber = function(_) {
    if (!arguments.length) return formatNumber;
    formatNumber = _;
    return render;
  };

  return d3.rebind(render, brush, 'on');
};

style.css

body {
  font: 14px helvetica;
  color: #f0f0f0;
  background-color: #3E4651;
}

.row {
  padding: 5px;
}

.axis path,
.axis line {
  fill: none;
  stroke: #f0f0f0;
  shape-rendering: crispEdges;
}

.axis text {
  fill: #f0f0f0;
}

.brush .extent {
  stroke: #f0f0f0;
  fill-opacity: .125;
  shape-rendering: crispEdges;
}

.bar {
  fill: #5EB4E3;
}
.selected {
  fill: #78C656;
}