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

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

8 rules

#1

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);
function componentFactory() {
  var event = d3.dispatch("customevent");

  function component(selection) {
    selection.each(function(data) {
      d3.select(this)
        .on('click.component', function() {

            event.customevent.apply(
                this, arguments);
        });
    });
  }
  d3.rebind(component, event, 'on');

  return component;
}

react ripped off d3

idempotent DOM operations

const root = <ul className="my-list">
               <li>Text Content</li>
             </ul>;

ReactDOM.render(root, 
  document.getElementById('example'));
const root = function(container) {
  const ul = container.selectAll('ul')
    .data([null])
    .enter()
    .append('ul')
    .classed('my-list')
    .append('li')
    .text('Text Content');
};

d3.select('#element')
  .call(root);

component trees of props/data

class Avatar extends React.Component {
  render() {
    return (
      <div>
        <Pic username={this.props.user} />
        {this.props.user}
      </div>
    );
  }
}

ReactDOM.render(
  <Avatar user="pwh" />,
  document.getElementById('example')
);
const avatar = function(selection) {
  const div = selection.selectAll('div')
    .data((data) => [data]);

  div.enter()
    .append('div');

  div.datum((d) => { user: d.user })
    .call(pic)
    .text((d) => d.user);
};

d3.select('#element')
  .datum({ user: 'pwh' })
  .call(avatar);

...

flux

https://facebook.github.io/flux/docs/overview.html

immutable.js + d3 = ?

react didn't rip off d3

(probably)

Creating D3 components

Chris Price / @100pxls

 

d3fc.io

 

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

Creating D3 components - Bristech

By Chris Price

Creating D3 components - Bristech

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

  • 1,267