Next Level D3

next level d3

By Amit Schechter / @meetamit
Co-founder, TWO-N


Slides available at
www.two-n.com/talks/next-level-d3

Current stack

  • d3
  • React
  • Node.js

[two-n.com](http://www.two-n.com)

[@2nfo](http://www.twitter.com/2nfo)

[@meetamit](http://www.twitter.com/meetamit)

Render **everything** in *update*()

// The initial display.
update(alphabet);

// Grab a random sample of letters
setInterval(function() {
  update(shuffle(alphabet)
      .slice(0, Math.floor(
        Math.random() * 26
      ))
      .sort());
}, 1500);

More understandable code

update()

function update(data) {

  // DATA JOIN
  var text = svg.selectAll("text")
      .data(data);

  // EXIT
  text.exit()
      .attr("class", "exit")
    .transition()
      .attr("y", 60)
      ...
      .remove();

  // UPDATE
  text.attr("class", "update")
    .transition()
      .attr("x", function(d, i) { return i * 32; });

  // ENTER
  text.enter().append("text")
      .attr("class", "enter")
      ...
}

Don't care about what changed —

Only that *something* changed

... less bugs

Integrates well with other frameworks

With Backbone

var ChartView = Backbone.View.extend({
  // a.k.a update()
  render: function() {
    d3.select(this.el).selectAll("text")
      .data(this.model);
    // Update, Enter, Exit
    ...
  }
});

With React

React.createClass({
  render: function() {
    return 
; }, componentDidUpdate: function() { sel = d3.select(this.refs.node).selectAll('.child') .data(this.props.data) // Update, Enter, Exit ... }, componentDidMount: function() { this.forceUpdate(); } });

    Drawbacks of rendering everything?

  • Possibly some unnecessary re-joining and updating
  • ... probably not a problem
  • Don't pre-optimize
  • You can later add checks for actual state changes

Conditional Element Creation

(That's not it)

// Create svg upon initialization
var svg = d3.select("body").append("svg")

function update(data) {
  // DATA JOIN
  var text = svg.selectAll("text")
      .data(data);
  ...
}

We want to move svg creation into update()

(Definitely not it)

function update(data) {
  // WRONG!!! Creates another svg on every update
  var svg = d3.select("body").append("svg")

  // DATA JOIN
  var text = svg.selectAll("text")
      .data(data);
  ...
}

Using if statement

function update(data) {  var svg = d3.select("body").select("svg")  if (svg.empty()) {  // Conditionally create the SVG svg = d3.select("body").append("svg")  }   // DATA JOIN var text = svg.selectAll("text") .data(data); }

Ok, but iffy

The [null] trick

Use D3's enter, update, exit selections

The [null] trick

// Conditionally create the SVG var svg = d3.select("body").selectAll("svg")  .data([null])   svg.enter() // a 1-element selection the first time ONLY .append("svg")  .attr("width", width) .attr("height", height);

The [null] trick

function update(data) {
  // Conditionally create the SVG
  var svg = d3.select("body").selectAll("svg")
        .data([null])
      .enter()
        .append("svg")

  // DATA JOIN
  var text = svg.selectAll("text")
      .data(data);
  ...
}

Example: Responsiveness

Easy when everything is inside update()

Responsiveness

d3.select(window).on("resize", update); function update() {  var width = window.width - margin * 2, height = window.height - margin * 2, enabled = width > 200;// only show chart if there's room   var svg = d3.select("body").selectAll("svg") .data(enabled ? [null] : [])   svg.merge(  svg.enter().append("svg")  ) .attr("width", width) .attr("height", height);   svg.exit() .remove()  }

selection.**each**()

selection.**each**( *function* )

Invokes the specified *function* for each element in the current selection, passing in the current datum `d` and index `i`, with the `this` context of the current DOM element.

svg.selectAll("rect.bar") .each(function(d, i) {  console.log(this); // <rect class="bar"></rect>  })

selection.**each**( *function* )

svg.selectAll("circle")
  .attr("cx",   function(d,i) { return xScale(d.unemployment); })
  .attr("cy",   function(d,i) { return yScale(d.debt);         })
  .attr("r",    function(d,i) { return rScale(d.gdp);          })
  .attr("fill", function(d,i) { return colorScale(i);          });

svg.selectAll("circle") .each(function(d,i) {  d3.select(this)  .attr("cx", xScale(d.unemployment)) .attr("cy", yScale(d.debt) ) .attr("r", rScale(d.gdp) ) .attr("fill", colorScale(i) );  })

Reasons to use .**each**()?

Less re-computation

svg.selectAll("circle") .each(function(d,i) { var centroid = computeCentroid(d.pointCloud);// [43, 21]  d3.select(this) .attr("cx", centroid[0]) .attr("cy", centroid[1]);  })

Flow Control

svg.selectAll(".shape") .each(function(d,i) { var centroid = computeCentroid(d.pointCloud);  if(d.shape == "circle") { d3.select(this) .attr("cx", centroid[0]) .attr("cy", centroid[1]); }  if(d.shape == "rect") { d3.select(this) .attr("x", centroid[0]) .attr("y", centroid[1]); }  })

.**each**() with nested data

.**each**() with nested data

var data = [
  { gdp:123, unemp:9, label:"Greece" },
  { gdp:456, unemp:7, label:"France" },
  { gdp:789, unemp:5, label:"USA"    },
  ...
]
var tr = table.selectAll("tr")
  .data(data);
tr.enter()
  .append("tr");
 
 
tr .each(function(entity, i) {  console.log(this); // <tr></tr>  var td = d3.select(this).selectAll("td") .data([entity.label, entity.gdp, entity.unemp])  // ["Greece", 123, 9]  td.enter() .append("td")  td .text(function(variable, j) { return variable; })  })

Nested Selections

var data = [
  { gdp:123, unemp:9, label:"Greece" },
  { gdp:456, unemp:7, label:"France" },
  { gdp:789, unemp:5, label:"USA"    },
  ...
]
var tr = table.selectAll("tr")
  .data(data);
tr.enter()
  .append("tr");
 
 
var td = tr.selectAll("td") .data(function(entity, i) { // e.g. { gdp:789, unemp:5, label:"USA" }  return [entity.label, entity.gdp, entity.unemp]  })  td.enter() .append("td") td .text(function(variable, j) { return variable; })

.**each**( *function* ) vs .**data**( *function* )

tr.each(function(d, i) { var td = d3.select(this).selectAll("td")  .data([d.label, d.gdp, d.unemp]);   td.enter() .append("td");  td .text(function(variable, j) { return variable; })  color = d.unemp < 7 ? "red" : "black"; td.style("color", color);  });        
var td = tr.selectAll("td")  .data(function(d, i) { return [d.label, d.gdp, d.unemp]; });   td.enter() .append("td");  td .text(function(variable, j) { return variable; })  .style("color", function(variable, j) {  // this <=> <td></td> // this.parentNode <=> <tr></tr>  var d = d3.select(this.parentNode) .datum(); return d.unemp < 7 ? "red" : "black";  })

selection.**call**()

selection.**call**( *function* )

Invokes the specified function once, passing in the current selection along with any optional arguments.

var g = svg.selectAll("g");   function fn(sel) { sel.attr("color", "red") }   fn(g) ⇔ g.call(fn)    g.call(function(sel) { sel.attr("color", "red") })

Use .call() to organize code

Demo

var g = svg.selectAll("g") .data(regions);    var gEnter = g.enter() .append("g") .__ // Create g    gEnter.append("circle") .attr("class", "solid-circle") .__ // Create SOLID circle    gEnter.append("circle") .attr("class", "dashed-circle") .__ // Create DASHED circle      var merged = g.merge(gEnter) .attr("transform", function(d,i) {}) .__ // Update g    merged.select('.solid-circle') .attr("r", function(d,i) { return rScale(d.specific); }) .__ // Update SOLID circle    merged.select('.dashed-circle') .attr("r", function(d,i) { return rScale(d.general); }) .__ // Update DASHED circle
var g = svg.selectAll("g") .data(regions);   var gEnter = g.enter() .append("g") .__ // Create g .call(function(gEnter) {  gEnter.append("circle") .attr("class", "solid-circle") .__ // Create SOLID circle gEnter.append("circle") .attr("class", "dashed-circle") .__ // Create DASHED circle  }); g.merge(gEnter) .__ // Update g .call(function(merged) {  merged.select('.solid-circle') .attr("r", function(d,i) { return rScale(d.specific); }) .__ // Update SOLID circle   merged.select('.dashed-circle') .attr("r", function(d,i) { return rScale(d.general); }) .__ // Update DASHED circle  });

Refactoring

var g = svg.selectAll("g") .data(regions); var gEnter = g.enter() .append("g") .call(createSymbol); g.merge(gEnter) .call(updateSymbol);   function createSymbol(gEnter) { // create circles } function updateSymbol(g) { // update circles }

Can we do better?

(yes)

Single update function

var g = svg.selectAll("g") .data(regions);   g.merge(  g.enter().append("g")  )  .call(symbol); function symbol(sel) { // create & update circles }

**symbol()** is [reusable](http://codepen.io/meetamit/full/NPNaXQ/)

Writing Reusable Code

d3-axis is reusable

axis is a function that has functions

var axis = d3.axisBottom(); var g = svg.append("g"); g.call(axis); // ⇔ axis(g)    axis.scale(d3.scaleLinear());

Functions can have functions

*function* that has *function*

  var color = "gray";  function colorize(sel) {  sel.attr("fill", color);  }    colorize.color = function(arg) {  color = arg;  return colorize;  };   colorize.color("orange"); // has functions  colorize(sel); // is a function    // OR   d3.selectAll("circle") .call(colorize.color("orange"));

*function* that has *function*

  var color = "gray"; function colorize(sel) { sel.attr("fill", color); }   colorize.color = function(arg) { color = arg; return colorize;  };        d3.selectAll("rect") .call(colorize.color("cyan"));    d3.selectAll("circle") .call(colorize.color("orange"));

*function* that has *function*

d3.colorize = function() {  var color = "gray"; function colorize(sel) { sel.attr("fill", color); };   colorize.color = function(arg) {  if (arguments.length == 0) { return color; }  color = arg; return colorize; }  return colorize; }    var colorize = d3.colorize();    d3.selectAll("circle") .call(colorize.color("orange"));

What did we achieve?

Separated function calling from argument passing

Created reusable code

Defined a "class", d3 style

(Moved messy code out of the way)

Now you know how to read the d3 source

d3.axis()

import {slice} from "./array";
import identity from "./identity";

var top = 1,
    right = 2,
    bottom = 3,
    left = 4,
    epsilon = 1e-6;

function translateX(scale0, scale1, d) {
  var x = scale0(d);
  return "translate(" + (isFinite(x) ? x : scale1(d)) + ",0)";
}

function translateY(scale0, scale1, d) {
  var y = scale0(d);
  return "translate(0," + (isFinite(y) ? y : scale1(d)) + ")";
}

function center(scale) {
  var offset = scale.bandwidth() / 2;
  if (scale.round()) offset = Math.round(offset);
  return function(d) {
    return scale(d) + offset;
  };
}

function entering() {
  return !this.__axis;
}

function axis(orient, scale) {
  var tickArguments = [],
      tickValues = null,
      tickFormat = null,
      tickSizeInner = 6,
      tickSizeOuter = 6,
      tickPadding = 3;

  function axis(context) {
    var values = tickValues == null ? (scale.ticks ? scale.ticks.apply(scale, tickArguments) : scale.domain()) : tickValues,
        format = tickFormat == null ? (scale.tickFormat ? scale.tickFormat.apply(scale, tickArguments) : identity) : tickFormat,
        spacing = Math.max(tickSizeInner, 0) + tickPadding,
        transform = orient === top || orient === bottom ? translateX : translateY,
        range = scale.range(),
        range0 = range[0] + 0.5,
        range1 = range[range.length - 1] + 0.5,
        position = (scale.bandwidth ? center : identity)(scale.copy()),
        selection = context.selection ? context.selection() : context,
        path = selection.selectAll(".domain").data([null]),
        tick = selection.selectAll(".tick").data(values, scale).order(),
        tickExit = tick.exit(),
        tickEnter = tick.enter().append("g").attr("class", "tick"),
        line = tick.select("line"),
        text = tick.select("text"),
        k = orient === top || orient === left ? -1 : 1,
        x, y = orient === left || orient === right ? (x = "x", "y") : (x = "y", "x");

    path = path.merge(path.enter().insert("path", ".tick")
        .attr("class", "domain")
        .attr("stroke", "#000"));

    tick = tick.merge(tickEnter);

    line = line.merge(tickEnter.append("line")
        .attr("stroke", "#000")
        .attr(x + "2", k * tickSizeInner)
        .attr(y + "1", 0.5)
        .attr(y + "2", 0.5));

    text = text.merge(tickEnter.append("text")
        .attr("fill", "#000")
        .attr(x, k * spacing)
        .attr(y, 0.5)
        .attr("dy", orient === top ? "0em" : orient === bottom ? "0.71em" : "0.32em"));

    if (context !== selection) {
      path = path.transition(context);
      tick = tick.transition(context);
      line = line.transition(context);
      text = text.transition(context);

      tickExit = tickExit.transition(context)
          .attr("opacity", epsilon)
          .attr("transform", function(d) { return transform(position, this.parentNode.__axis || position, d); });

      tickEnter
          .attr("opacity", epsilon)
          .attr("transform", function(d) { return transform(this.parentNode.__axis || position, position, d); });
    }

    tickExit.remove();

    path
        .attr("d", orient === left || orient == right
            ? "M" + k * tickSizeOuter + "," + range0 + "H0.5V" + range1 + "H" + k * tickSizeOuter
            : "M" + range0 + "," + k * tickSizeOuter + "V0.5H" + range1 + "V" + k * tickSizeOuter);

    tick
        .attr("opacity", 1)
        .attr("transform", function(d) { return transform(position, position, d); });

    line
        .attr(x + "2", k * tickSizeInner);

    text
        .attr(x, k * spacing)
        .text(format);

    selection.filter(entering)
        .attr("fill", "none")
        .attr("font-size", 10)
        .attr("font-family", "sans-serif")
        .attr("text-anchor", orient === right ? "start" : orient === left ? "end" : "middle");

    selection
        .each(function() { this.__axis = position; });
  }

  axis.scale = function(_) {
    return arguments.length ? (scale = _, axis) : scale;
  };

  axis.ticks = function() {
    return tickArguments = slice.call(arguments), axis;
  };

  axis.tickArguments = function(_) {
    return arguments.length ? (tickArguments = _ == null ? [] : slice.call(_), axis) : tickArguments.slice();
  };

  axis.tickValues = function(_) {
    return arguments.length ? (tickValues = _ == null ? null : slice.call(_), axis) : tickValues && tickValues.slice();
  };

  axis.tickFormat = function(_) {
    return arguments.length ? (tickFormat = _, axis) : tickFormat;
  };

  axis.tickSize = function(_) {
    return arguments.length ? (tickSizeInner = tickSizeOuter = +_, axis) : tickSizeInner;
  };

  axis.tickSizeInner = function(_) {
    return arguments.length ? (tickSizeInner = +_, axis) : tickSizeInner;
  };

  axis.tickSizeOuter = function(_) {
    return arguments.length ? (tickSizeOuter = +_, axis) : tickSizeOuter;
  };

  axis.tickPadding = function(_) {
    return arguments.length ? (tickPadding = +_, axis) : tickPadding;
  };

  return axis;
}

export function axisTop(scale) {
  return axis(top, scale);
}

export function axisRight(scale) {
  return axis(right, scale);
}

export function axisBottom(scale) {
  return axis(bottom, scale);
}

export function axisLeft(scale) {
  return axis(left, scale);
}

UI with D3

Data as View Model

var buttons = [ { label: "A", rgb: "#ffdd99" }, { label: "B", rgb: "#aaffdd" }, { label: "C", rgb: "#ffaaee" }, { label: "D", rgb: "#aaff99" } ];
function update() { var btn = d3.select("body").selectAll("button") .data(buttons); btn.enter() .append("button") .attr("class", "btn") .text(function(d) { return d.label; }) }

State selection

var selected = buttons[1]; // { label: "B", rgb: "#aaffdd" } function update() { // ....  btn .classed("selected", function(d, i) { return d == selected; })  .on("click", function(d) { selected = d; update(); });    d3.select("body") .style("background", selected.rgb);  }

d3.behavior.**drag**()

var drag = d3.behavior.drag();    svg.select(".slider-handle") .call(drag);    drag .on("dragstart", function(d,i) { }) .on("drag", function(d, i) {  var mousePosition = d3.mouse(this.parentNode); // [654, 321] d3.select(this) .attr("cx", mousePosition[0])  })

d3 for all the things

var axesG = svg.append("g");  var xAxis = d3.axisBottom() .scale(xScale)  var xAxisG = axesG .append("g") .attr("x axis") .call(xAxis)  var yAxisG = axesG .append("g") .attr("y axis") .____   var circlesG = svg.append("g");  var circles = circlesG.selectAll("circle") .data(scatterData)  var circlesEnter = circles.enter() .append("circle") .attr("cx", function(d,i) {...}) .____   var benchmarksG = svg.append("g"); var benchmarks = benchmarksG.selectAll("line") .data(benchmarksData) .____  var labelsG = svg.append("g"); .____
var layers = ["axes", "circles", "benchmarks", "labels"]  function update() {  var layerG = svg.selectAll("g.layer") .data(layers); layerG.enter() .append("g");  layerG.each(function(d, i) {  switch(d) { case "circles": d3.select(this) .datum(scatterData) .call(scatterPlot); break;  case "axes": var axes = d3.select(this).selectAll("g.axis") .data(["x", "y"]) axes.enter() .append("g") .attr("transform", function(d,i) { if(d == "x") {...} }) ... break;  }  });  }

The End

Thank you for watching

next level d3

By Amit Schechter / @meetamit
Co-founder, TWO-N


Slides available at
www.two-n.com/talks/next-level-d3