Creating D3 components

a journey of pain, joy, frustration and enlightenment

Chris Price / @100pxls

d3
data join
components
our experience

d3

in 2 minutes

DOM abstraction

var selection = d3.select('#id');

selection.select('div')
    .selectAll('.class-name');
  • selection
  • manipulation
  • events
  • XHR
  • transitions
selection.text('inner text')
    .attr('class', 'class-name')
    .style({
        height: 'auto'
    })
    .each(function(data) {
        var element = this;
        // ...
    })
    .append('span');

selection.on('click.foo', function(data) {
    var element = this;
    // ...
});
d3.json('/some/url', function(error, data) {
    // ...
});
selection.style('background', 'blue')
    .transition()
    .delay(100)
    .ease('linear')
    .style('background', 'red');

layout algorithms

var scale = d3.scale.linear()
    .domain([-10, 10])
    .range([0, 500]); 

scale(-5); // 125
scale.invert(125); // -5

scale.ticks(3); // [-10, 0, 10]
  • scales
  • specialised layouts
  • geometric primitives
  • geographic projections
var layout = d3.layout.pie()
    .padAngle(Math.PI/10);

layout([3,1,2]); 
// [ {
//   "data":3,
//   "value":3,
//   "padAngle":0.314...,
//   "startAngle":0,
//   "endAngle":2.984...
// }, ... ]
var polygon = d3.geom.polygon(
    [[0,0],[0,1],[1,1],[1,0]]);

polygon.area(); // 1
polygon.centroid(); // [0.5,0.5]

SVG helpers and misc.

  • SVG helpers
  • time formatting
  • and more...
var format = d3.time.format("%a, %e %B %Y");
format(new Date(2015, 4, 7)); 
// "Thu, 7 May 2015" 

var d = format.parse("Fri, 8 May 2015");
d.toString(); 
// "Fri May 08 2015 00:00:00 GMT+0000 (GMT)"
d3.time.year.ceil(d, 1);
// "Fri Jan 01 2016 00:00:00 GMT+0000 (GMT)"
var line = d3.svg.line();

line([[0,0],[50,50]]);
// "M0,0L50,50"

d3.select('svg')
    .append('path')
    .datum([[0,0],[50,50]])
    .attr('d', line);
// <path d="M0,0L50,50"></path>
var scale = d3.linear.scale()
    .domain([-10, 10])
    .range([0, 100]);
var axis = d3.svg.axis()
    .scale(scale);
d3.select('svg')
    .call(axis);

d3

  • data binding
  • DOM abstraction
  • layout algorithms
  • SVG helpers
  • miscellaneous

data join

contains spoilers

data join

var data = [
  { name: 'Adams', rank: 'Captain' },
  { name: 'Krill', rank: 'Commander' },
  { name: 'Ryback', rank: 'Chief Petty Officer' }
];
<h1>USS Missouri Crew List</h1>

<ul>
  <li></li>
  <li></li>
  <li></li>
</ul>

data join

var data = [
  { name: 'Adams', rank: 'Captain' },
  { name: 'Krill', rank: 'Commander' },
  { name: 'Ryback', rank: 'Chief Petty Officer' }
];

function render() {

  requestAnimationFrame(render);
}

requestAnimationFrame(render);
<h1>USS Missouri Crew List</h1>

<ul>
  <li></li>
  <li></li>
  <li></li>
</ul>
  • render loop

data join

var data = [
  { name: 'Adams', rank: 'Captain' },
  { name: 'Krill', rank: 'Commander' },
  { name: 'Ryback', rank: 'Chief Petty Officer' }
];

function render() {

  var container = d3.select('ul');

  container.selectAll('li');

  requestAnimationFrame(render);
}

requestAnimationFrame(render);
<h1>USS Missouri Crew List</h1>

<ul>
  <li></li>
  <li></li>
  <li></li>
</ul>
  • render loop
  • selection

data join

var data = [
  { name: 'Adams', rank: 'Captain' },
  { name: 'Krill', rank: 'Commander' },
  { name: 'Ryback', rank: 'Chief Petty Officer' }
];

function render() {

  var container = d3.select('ul');

  container.selectAll('li')
    .data(data);

  requestAnimationFrame(render);
}

requestAnimationFrame(render);
<h1>USS Missouri Crew List</h1>

<ul>
  <li></li>
  <li></li>
  <li></li>
</ul>
  • render loop
  • selection
  • data

data join

var data = [
  { name: 'Adams', rank: 'Captain' },
  { name: 'Krill', rank: 'Commander' },
  { name: 'Ryback', rank: 'Chief Petty Officer' }
];

function render() {

  var container = d3.select('ul');

  var updateSelection = container.selectAll('li')
    .data(data);

  updateSelection.text(function(d) {
    return d.name + ' (' + d.rank + ')';
  });

  requestAnimationFrame(render);
}

requestAnimationFrame(render);
<h1>USS Missouri Crew List</h1>

<ul>
  <li>Adams (Captain)</li>
  <li>Krill (Commander)</li>
  <li>Ryback (Chief Petty Officer)</li>
</ul>
  • render loop
  • selection
  • data
  • update selection

data join

var data = [
  { name: 'Adams', rank: 'Captain' },
  { name: 'Krill', rank: 'Commander' },
  { name: 'Ryback', rank: 'Chief Petty Officer' },
  { name: 'Strannix', rank: 'Civilian' }
];

function render() {

  var container = d3.select('ul');

  var updateSelection = container.selectAll('li')
    .data(data);

  updateSelection.text(function(d) {
    return d.name + ' (' + d.rank + ')';
  });

  requestAnimationFrame(render);
}

requestAnimationFrame(render);
<h1>USS Missouri Crew List</h1>

<ul>
  <li>Adams (Captain)</li>
  <li>Krill (Commander)</li>
  <li>Ryback (Chief Petty Officer)</li>
</ul>
  • render loop
  • selection
  • data
  • update selection

data join

var data = [
  { name: 'Adams', rank: 'Captain' },
  { name: 'Krill', rank: 'Commander' },
  { name: 'Ryback', rank: 'Chief Petty Officer' },
  { name: 'Strannix', rank: 'Civilian' }
];

function render() {

  var container = d3.select('ul');

  var updateSelection = container.selectAll('li')
    .data(data);

  updateSelection.enter()
    .append('li');

  updateSelection.text(function(d) {
    return d.name + ' (' + d.rank + ')';
  });

  requestAnimationFrame(render);
}

requestAnimationFrame(render);
<h1>USS Missouri Crew List</h1>

<ul>
  <li>Adams (Captain)</li>
  <li>Krill (Commander)</li>
  <li>Ryback (Chief Petty Officer)</li>
  <li>Strannix (Civilian)</li>
</ul>
  • render loop
  • selection
  • data
  • update selection
  • enter selection

data join

var data = [
  { name: 'Krill', rank: 'Commander' },
  { name: 'Ryback', rank: 'Chief Petty Officer' },
  { name: 'Strannix', rank: 'Civilian' }
];

function render() {

  var container = d3.select('ul');

  var updateSelection = container.selectAll('li')
    .data(data);

  updateSelection.enter()
    .append('li');

  updateSelection.text(function(d) {
    return d.name + ' (' + d.rank + ')';
  });

  requestAnimationFrame(render);
}

requestAnimationFrame(render);
<h1>USS Missouri Crew List</h1>

<ul>
  <li>Krill (Commander)</li>
  <li>Ryback (Chief Petty Officer)</li>
  <li>Strannix (Civilian)</li>
  <li>Strannix (Civilian)</li>
</ul>
  • render loop
  • selection
  • data
  • update selection
  • enter selection

data join

var data = [
  { name: 'Krill', rank: 'Commander' },
  { name: 'Ryback', rank: 'Chief Petty Officer' },
  { name: 'Strannix', rank: 'Civilian' }
];

function render() {

  var container = d3.select('ul');

  var updateSelection = container.selectAll('li')
    .data(data);

  updateSelection.enter()
    .append('li');

  updateSelection.text(function(d) {
    return d.name + ' (' + d.rank + ')';
  });

  updateSelection.exit()
     .remove();

  requestAnimationFrame(render);
}

requestAnimationFrame(render);
<h1>USS Missouri Crew List</h1>

<ul>
  <li>Krill (Commander)</li>
  <li>Ryback (Chief Petty Officer)</li>
  <li>Strannix (Civilian)</li>
</ul>
  • render loop
  • selection
  • data
  • update selection
  • enter selection
  • exit selection

data join

var data = [
  { name: 'Ryback', rank: 'Chief Petty Officer' }
];

function render() {

  var container = d3.select('ul');

  var updateSelection = container.selectAll('li')
    .data(data);

  updateSelection.enter()
    .append('li');

  updateSelection.text(function(d) {
    return d.name + ' (' + d.rank + ')';
  });

  updateSelection.exit()
     .remove();

  requestAnimationFrame(render);
}

requestAnimationFrame(render);
<h1>USS Missouri Crew List</h1>

<ul>
  <li>Ryback (Chief Petty Officer)</li>
</ul>
  • render loop
  • selection
  • data
  • update selection
  • enter selection
  • exit selection

data join

  • idempotent transformation of data into nodes
  • join is configurable
    • index-based (default)
    • identity-based
    • custom
  • values stored directly on DOM node
    • accessible using selection.datum()

components

are easy

components

function caseyRyback(selection) {
  selection.text('Another cold day in Hell.');
}

function dramaticEffect(selection) {
  selection.style('fontWeight', 'bold');
}

var selection = d3.select('span');

caseyRyback(selection);

caseyRyback(dramaticEffect(selection));
  • just a function
<span style="font-weight: bold">
    Another cold day in Hell.
</span>

components

function caseyRyback(selection) {
  selection.text('Another cold day in Hell.');
}

function dramaticEffect(selection) {
  selection.style('fontWeight', 'bold');
}

var selection = d3.select('span')
    .call(caseyRyback)
    .call(dramaticEffect);
  • just a function
  • invoked with call
<span style="font-weight: bold">
    Another cold day in Hell.
</span>

components

function caseyRybackFactory() {

  function caseyRyback(selection) {
    selection.text('Another cold day in Hell.');
  }

  return caseyRyback;
}

var component = caseyRybackFactory();

d3.select('span')
  .call(component);
  • just a function
  • invoked with call
  • factory
<span>
    Another cold day in Hell.
</span>

components

function caseyRybackFactory() {

  var quote = 'Another cold day in Hell.';

  function caseyRyback(selection) {
    selection.text(quote);
  }

  caseyRyback.quote = function(value) {
    if (!arguments.length) {
      return quote;
    }
    quote = value;
    return caseyRyback;
  };

  return caseyRyback;
}

var component = caseyRybackFactory()
  .quote('Keep the faith, Strannix.');

d3.select('span')
  .call(component);
  • just a function
  • invoked with call
  • factory
  • configurable
<span>
    Keep the faith, Strannix.
</span>

components

  • convention based unit of re-use
  • invocation is idempotent
  • applicable to multi-node selections

our experience

8 rules

#1

data join in components

  • use each for reusability
  • consider if enter or update is most appropriate
function componentFactory() {

  function component(selection) {

    selection.each(function(data) {

      var container = d3.select(this);

      var circles = container.selectAll('circle')
        .data(data);

      circles.enter()
        .append('circle')
        .attr('stroke', 'white');

      circles.attr('r', function(d) { 
        return d; 
      });

      circles.exit()
        .remove();

    });
  }

  return component;
}

var component = componentFactory();
d3.select('svg')
  .datum([50,30,20,5])
  .call(component);
<svg>
  <circle stroke="white" r="50"></circle>
  <circle stroke="white" r="30"></circle>
  <circle stroke="white" r="20"></circle>
  <circle stroke="white" r="5"></circle>
</svg>

#2

singleton elements

function componentFactory() {

  function component(selection) {

    selection.each(function(data) {

      var container = d3.select(this);

      var circle = container.selectAll('circle')
        .data([data]);

      circle.enter()
        .append('circle')
        .attr('r', 50);

    });
  }

  return component;
}

var component = componentFactory();
d3.select('svg')
  .call(component);
  • default join behaviour is index based - enter selection will run once
  • update and exit selections most likely redundant
  • why use data as the dummy element?

#3

avoid g soup

function componentFactory() {

  function component(selection) {

    selection.each(function(data) {

      var container = d3.select(this);

      var g = container.selectAll('g')
        .data([data]);
      g.enter()
        .append('g');

      var circles = g.selectAll('circle')
        .data(data);
      // ...

    });
  }

  return component;
}

var component = componentFactory();
d3.select('svg')
  .datum([50,30,20,5])
  .call(component);
  • don't create container elements
  • callers pass in the container
  • container won't be re-used
  • components are free to manipulate their container's attributes and events

#4

no state in components

  • prevents reusability
function componentFactory() {
  var container = null;

  function component(selection) {
    selection.each(function(data) {

      container = d3.select(this);
      // ...

    });
  }

  component.container = function() {
    return container;
  };

  return component;
}

var component = componentFactory();

d3.select('svg')
  .call(component);

#5

separate concerns

function componentFactory() {

  var oddColor = 'black';
  var evenColor = 'white';
   
  function component(selection) {
    selection.attr('fill', function(d, i) { 
      return i % 2 === 0 ? evenColor : oddColor; 
    });
  }

  component.oddColor = function(value) {
    // ...
  };

  component.evenColor = function(value) {
    // ...
  };

  return component;
}

var component = componentFactory();
d3.select('rect')
  .call(component);
  • extract algorithmic functionality into a separate component

#6

use rebind

  • selectively expose configuration of nested components
function componentFactory() {

  function component(selection) {
    // ...
  }

  d3.rebind(component, strober, 
    'oddColor', 'evenColor');

  return component;
}

var component = componentFactory();
d3.select('rect')
  .oddColor('pink')
  .evenColor('white')
  .call(component);

#7

namespace your events

function componentFactory() {
  var colors = d3.scale.category10();

  function component(selection) {
    selection.each(function(data) {

      d3.select(this)
        .attr('fill', colors(data.index))
        .on('click.component', function(data) {
          data.index++;

          d3.select(this)
            .call(component);
        });
    });
  }

  return component;
}

var component = componentFactory();
d3.select('svg')
  .append('circle')
  .attr('r', 100)
  .datum({index: 0})
  .call(component);
  • only one handler per type
  • reduces conflicts with other components
  • use event context 

#8

expose your own events

function componentFactory() {

  var event = d3.dispatch("customevent");

  function component(selection) {

    selection.each(function(data) {

      d3.select(this)
        .on('click.component', click);
    });
  }

  function click(data) {
    event.customevent.apply(this, arguments);
  }

  d3.rebind(component, event, 'on');

  return component;
}

var component = componentFactory()
  .on('customevent', function() { 
    console.log(this, arguments); 
  });
d3.select('svg')
  .call(component);
  • allow interactions to propagate outside of your component
  • increase the flexibility of your component

our experience

  • keep it small and simple
  • refer back to how d3 itself works
  • check you're not re-creating d3/SVG functionality

our library

"the one"

d3-financial-components

  • a toolkit for rapidly developing bespoke charts
  • one charting library to rule them all

but...

d3-financial-components

  • embraces d3: prioritises simplicity
  • a curated and consistent set of examples

Creating D3 components

a journey of pain, joy, frustration and enlightenment

(creating d3-financial-components)

Chris Price / @100pxls

Creating D3 components - ScotlandJS

By Chris Price

Creating D3 components - ScotlandJS

The video of this talk is now available on YouTube - https://www.youtube.com/watch?v=aVps_7Llo-M

  • 1,582