Creating D3 components

a journey of pain, joy, frustration and enlightenment

Chris Price / @100pxls

d3
data join
components
our experience

d3

the basics

DOM abstraction

selection

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

selection.select('div')
    .selectAll('.class-name');

DOM abstraction

manipulation

selection.text('inner text')
    .attr('class', 'class-name')
    .style({
        height: 'auto'
    })
    .each(function(data) {
        var element = this;
        // ...
    })
    .append('span');

DOM abstraction

events

selection.on('click.foo', 
    function(data) {
        var element = this;
        // ...
    });

DOM abstraction

XHR

d3.json('/some/url', 
    function(error, data) {
        // ...
    });

DOM abstraction

animation

selection.style('background', 'blue')
    .transition()
    .duration(100)
    .ease('linear')
    .style('background', 'red');

layout algorithms

scales

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

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

scale.ticks(3); // [-10, 0, 10]

layout algorithms

specialised layouts

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...
// }, ... ]

layout algorithms

geometric primitives

var polygon = d3.geom.polygon(
    [[0,0],[0,1],[1,1],[1,0]]);

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

layout algorithms

geographic projections

var mercator = d3.geo.projection(
    function(λ, φ) {
        return [
            λ,
            Math.log(Math.tan(π / 4 + φ / 2))
        ];
    });

SVG helpers

axis renderers

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

var axis = d3.svg.axis()
    .scale(scale);

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

SVG helpers

path data generators

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>

miscellaneous

data/time formatting

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

examples

multi-series line chart

collision detection

shiboronoi

d3

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

data join

contains spoilers

data join

the HTML

<h1>USS Missouri Crew List</h1>

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

data join

the data

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

data join

render loop

// ...

function render() {

  requestAnimationFrame(render);
}

requestAnimationFrame(render);

data join

selection

// ...

function render() {

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

  container.selectAll('li');

  requestAnimationFrame(render);
}

// ...

data join

the join

// ...

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

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

requestAnimationFrame(render);

// ...

data join

update selection

// ...

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

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

// ...

data join

update selection

<h1>USS Missouri Crew List</h1>

<ul>
  <li>Adams (Captain)</li>
  <li>Krill (Commander)</li>
  <li>Ryback (Chief Petty Officer)</li>
</ul>

data join

add an item to the data

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

data join

...no change

<h1>USS Missouri Crew List</h1>

<ul>
  <li>Adams (Captain)</li>
  <li>Krill (Commander)</li>
  <li>Ryback (Chief Petty Officer)</li>
</ul>

data join

enter selection

// ...

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

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

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

// ...

data join

a change!

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

data join

remove an item from the data

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

data join

a change...

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

data join

exit selection

// ...

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

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

updateSelection.exit()
  .remove();

// ...

data join

a change!

<h1>USS Missouri Crew List</h1>

<ul>
  <li>Krill (Commander)</li>
  <li>Ryback (Chief Petty Officer)</li>
  <li>Strannix (Civilian)</li>
</ul>

data join

spoilers

<h1>USS Missouri Crew List</h1>

<ul>
  <li>Ryback (Chief Petty Officer)</li>
</ul>
var data = [
  { 
    name: 'Ryback', rank: 'Chief Petty Officer'
  }
];

data join

  • idempotent transformation of data into nodes

demo

components

(functions)

components

just functions

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

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

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

components

invoked with call

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

components

factories

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

var component = caseyRybackFactory();

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

components

configurable

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;
}

components

configurable

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

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

components

  • convention based unit of re-use

demo

our experience

d3fc

d3fc

  • a toolkit for rapidly developing bespoke charts
  • embraces d3
    • small and composable
    • prioritises simplicity
  • a curated and consistent set of examples

our experience

t.d3fc.io

t.d3fc.io

  • tweet-sized d3 code golf
  • ~113 characters of code
    • 140 characters in a tweet
    • - 26 character "shortened" URL
    • -1 whitespace character (optional)

Creating D3 components

Chris Price / @100pxls

 

d3fc.io

 

slides.com/chrisprice/creating-d3-components-1-4

Creating D3 components - BristolJS

By Chris Price

Creating D3 components - BristolJS

The examples that went along with this deck are available on GitHub - https://github.com/chrisprice/creating-d3-components

  • 1,146