KYRIAKOS GEORGIOU

Engineer @Instacart

 

VIS: Force-Directed Graph of Node Pies, with D3

TL;DR & Code: Sneaking pie charts in graph vertices.

The visualization above is based on the force-directed graph layout of the D3 library.

What's different from the D3 example is that pie charts were injected in each vertex, to convey some additional visual information.

The pie charts in the above context, show the percentage of different colored nodes, a given node is connected to. Similar to the visualizations found in the lincomm R package.

Drawing pie charts with SVG & CSS

Approach #1

For a pie chart consisting of 3 equal sized segments, we overlay 3 circle elements. Then, we insert the pie chart on top of the original node element, to form a "node pie", as demonstrated below:

To draw a full circle is straightforward. To draw a segment of a circle that covers a fraction of the whole area, we use the stroke-dasharray attribute. We also halve the circle's r (radius) attribute, but set the stroke-width to the original radius to maintain the desired radius on the final shape.

Since this technique is based on having the smaller segments overlay the larger segments of the pie, the order the circle elements appear in the DOM matters. The larger segments should precede the smaller ones so they stay in the back, without hiding the smaller segments in the front. This effect can be also manipulated by assigning the appropriate z-index values to the circle elements.

HTML

<svg width="590" height="100">
  <circle id="green-segment"/>
  <circle id="pink-segment"/>
  <circle id="blue-segment"/>
</svg>

Javascript

var pieChart = {
  blue:  { color: '#2282c2', percent: 33.33 },
  pink:  { color: '#f13379', percent: 33.33 },
  green: { color: '#2bd07c', percent: 33.34 }
};

var radius = 40;
var halfRadius = radius / 2;
var halfCircumference = 2 * Math.PI * halfRadius;

var percentToDraw = 0;

percentToDraw += pieChart.blue.percent; // 33.33%

d3.select('#blue-segment')
  .attr('cx', '50%').attr('cy', '50%')
  .attr('fill', 'transparent')
  .attr('r', halfRadius)
  .attr('stroke-width', radius)
  .attr('stroke', pieChart.blue.color)
  .attr('stroke-dasharray',
          halfCircumference * percentToDraw / 100
        + ' '
        + halfCircumference);

percentToDraw += pieChart.pink.percent; // 66.66%

d3.select('#pink-segment')
  .attr('cx', '50%').attr('cy', '50%')
  .attr('fill', 'transparent')
  .attr('r', halfRadius)
  .attr('stroke-width', radius)
  .attr('stroke', pieChart.pink.color)
  .attr('stroke-dasharray',
          halfCircumference * percentToDraw / 100
        + ' '
        + halfCircumference);

percentToDraw += pieChart.green.percent; // 100%

d3.select('#green-segment')
  .attr('cx', '50%').attr('cy', '50%')
  .attr('fill', 'transparent')
  .attr('r', halfRadius)
  .attr('stroke-width', radius)
  .attr('stroke', pieChart.green.color)
  .attr('stroke-dasharray',
          halfCircumference * percentToDraw / 100
        + ' '
        + halfCircumference);

Approach #2

A different approach is to paint exactly the desired percentage for each segment, and then rotate the segment by some calculated degrees.

HTML

<svg width="590" height="100">
    <circle id="blue-segment"/>
    <circle id="pink-segment"/>
    <circle id="green-segment"/>
</svg>

Javascript

var pieChart = {
  blue:  { color: '#2282c2', percent: 33.33 },
  pink:  { color: '#f13379', percent: 33.33 },
  green: { color: '#2bd07c', percent: 33.34 }
};

var radius = 40;
var halfRadius = radius / 2;
var halfCircumference = 2 * Math.PI * halfRadius;

// 0deg drawn up to this point
var degreesDrawn = 0;

d3.select('#blue-segment')
  .attr('cx', '50%').attr('cy', '50%')
  .attr('fill', 'transparent')
  .attr('r', halfRadius)
  .attr('stroke-width', radius)
  .attr('stroke', pieChart.blue.color)
  .attr('stroke-dasharray',
          halfCircumference * pieChart.blue.percent / 100
        + ' '
        + halfCircumference)
  .style('transform-origin', '50% 50%')
  .style('transform', 'rotate(' + degreesDrawn + 'deg)');

// 119.988deg drawn up to this point
degreesDrawn += 360 * pieChart.blue.percent / 100;

d3.select('#pink-segment')
  .attr('cx', '50%').attr('cy', '50%')
  .attr('fill', 'transparent')
  .attr('r', halfRadius)
  .attr('stroke-width', radius)
  .attr('stroke', pieChart.pink.color)
  .attr('stroke-dasharray',
          halfCircumference * pieChart.pink.percent / 100
        + ' '
        + halfCircumference)
  .style('transform-origin', '50% 50%')
  .style('transform', 'rotate(' + degreesDrawn + 'deg)');

// 240.012deg drawn up to this point
degreesDrawn += 360 * pieChart.pink.percent / 100;

d3.select('#green-segment')
  .attr('cx', '50%').attr('cy', '50%')
  .attr('fill', 'transparent')
  .attr('r', halfRadius)
  .attr('stroke-width', radius)
  .attr('stroke', pieChart.green.color)
  .attr('stroke-dasharray',
          halfCircumference * pieChart.green.percent / 100
        + ' '
        + halfCircumference)
  .style('transform-origin', '50% 50%')
  .style('transform', 'rotate(' + degreesDrawn + 'deg)');

// 360deg, i.e. a full circle, drawn
degreesDrawn += 360 * pieChart.green.percent / 100;

Notes

Burdening the DOM with multiple HTML elements to draw a single pie chart doesn't feel right. We could use a conic gradient instead, with the help of this polyfill by Lea Verou. Hopefully, browsers will build support for conic-gradient soon.

Lea has also authored an article that nicely explains the CSS & SVG techniques that were used to draw the pie charts above.

For more CSS magic, it's worth checking out her book CSS Secrets: Better Solutions to Everyday Web Design Problems.

Force-Directed Layout & Node Pies

Now we've figured out how to dynamically draw pie charts using CSS & SVG, it's time to inject them in our D3 force-directed layout.

The force-directed layout example in the D3 gallery is very clearly written by Mike Bostock, the author of D3. All we have to do now is to get a reference on each graph node element and draw each respective pie chart inside. For this example, the input data was tweaked as well. The D3 example uses data based from Victor Hugo's "Les Misérables", however for this example a dummy JSON file was used, to represent a graph of 8 vertices and 12 edges.

The Data

{
  "nodes": [
    {
      "id": "a", "group": 1,
      "pieChart": [
        { "color": 1, "percent": 40 },
        { "color": 2, "percent": 20 },
        { "color": 3, "percent": 20 },
        { "color": 4, "percent": 20 }
      ]
    },
    {
      "id": "b", "group": 2,
      "pieChart": [
        { "color": 1, "percent": 33.33 },
        { "color": 2, "percent": 33.34 },
        { "color": 3, "percent": 33.33 }
      ]
    },
    {
      "id": "c", "group": 3,
      "pieChart": [
        { "color": 1, "percent": 25 },
        { "color": 2, "percent": 25 },
        { "color": 3, "percent": 25 },
        { "color": 4, "percent": 25 }
      ]
    },
    {
      "id": "d", "group": 4,
      "pieChart": [
        { "color": 1, "percent": 33.33 },
        { "color": 3, "percent": 33.33 },
        { "color": 4, "percent": 33.34 }
      ]
    },
    {
      "id": "e", "group": 1,
      "pieChart": [
        { "color": 1, "percent": 100 }
      ]
    },
    {
      "id": "f", "group": 1,
      "pieChart": [
        { "color": 1, "percent": 100 }
      ]
    },
    {
      "id": "g", "group": 2,
      "pieChart": [
        { "color": 2, "percent": 50 },
        { "color": 3, "percent": 50 }
      ]
    },
    {
      "id": "h", "group": 3,
      "pieChart": [
        { "color": 2, "percent": 50 },
        { "color": 3, "percent": 50 }
      ]
    },
    {
      "id": "i", "group": 4,
      "pieChart": [
        { "color": 4, "percent": 100 }
      ]
    }
  ],
  "links": [
    { "source": "a", "target": "b", "value": 5 },
    { "source": "a", "target": "c", "value": 5 },
    { "source": "a", "target": "d", "value": 5 },
    { "source": "a", "target": "e", "value": 5 },
    { "source": "a", "target": "f", "value": 5 },
    { "source": "b", "target": "c", "value": 5 },
    { "source": "b", "target": "g", "value": 5 },
    { "source": "c", "target": "d", "value": 5 },
    { "source": "c", "target": "h", "value": 5 },
    { "source": "d", "target": "i", "value": 5 },
    { "source": "e", "target": "f", "value": 5 },
    { "source": "h", "target": "g", "value": 5 }
  ]
}

Notice that the details of the pie charts to be drawn on each node, are explicitly defined in the input data.

{
  "pieChart": [
    { "color": 1, "percent": 33.33 },
    { "color": 2, "percent": 33.34 },
    { "color": 3, "percent": 33.33 }
  ]
}

The color property could be an RGB value or a CSS color name, depending how we're planning to parse that attribute in our code. However, for this example we use d3.schemeCategory10 to generate colors from integers.

A different approach would be to omit the pie chart details from the input data and compute them dynamically during runtime.

Iterate, Select & Draw, with D3 operators

All the graph node elements are created by D3, according to the input data and are stored in the node variable. Now we can iterate each datum with .each(), and by using d3.select(this) we can select the current node element in the DOM and apply D3 operators, like .attr() and .style(), to modify it.

node.each(function (d) {
  var nodeElement = d3.select(this);
  /* Draw node pie for each node element */
});

The pie chart drawing logic is located in a separated file, node-pie.js.

You can see the full source code and how it looks here.