D3 v3/v4 Cohabitation

In June 2016, Mike Bostock released D3 v4 which is not compatible with the previous v3 releases. There are tons of new improvements. This release represents an incremental and substantial evolution of D3.

Personally, I am glad Mike is not completely beholden to backward compatibility; especially in an environment where even the language itself is evolving underneath it.

So folks will have to port their visualizations to D3 v4 if they wish to stay current. If not, their visuals will continue to run.

For visualization frameworks such as c3, nvd3, d3plus and so on, this is a real problem and a significant port.

For dex.js, even more so since I want to use the latest and greatest D3 libraries rather than be stuck at D3 v3 because other frameworks have not caught up yet.

This article will cover how I addressed this issue.

Goals

The General Plan

One thing to note, is that I am using CommonJS (npm) style modules which use the “require” command and return modules as a single variable. I use “browserify” to aggregate my required modules into a browser consumable distribution. This same plan will work in other module schemes, but the mechanics will differ.

Ok, back to the plan:

  1. Include d3 v3 and assign it to a reference called d3v3.
  2. Include d3 v4 and assign it to a reference called d3v4.
  3. In each component with d3 dependencies, ensure the entry points set the relevant version of d3.

Since I am now including d3 v3 and v4 in the dex distribution, users will not have to include d3 in their html pages. It’ll be done automatically for them.

Implementation

The relevant module hierarchy for this discussion is:

dex -> charts -> d3

dex.js:

var dex = {};
dex.version = "0.8.0.8";
// Define the communication bus for publish/subscribe
dex.bus = require("../lib/pubsub");
// Array utilities
dex.array = require('./array/array')(dex);
// Lots more modules included....

// Include charting modules
dex.charts = require("./charts/charts")(dex);

// Set d3 by default to d3 version 3
d3 = dex.charts.d3.d3v3;

module.exports = dex;

charts.js

charts.js remains unchanged:

module.exports = function charts() {
  return {
    'c3'      : require("./c3/c3"),
    'd3'      : require("./d3/d3"),
    'd3plus'  : require("./d3plus/d3plus"),
    'dygraphs': require("./dygraphs/dygraphs"),
    'google'  : require("./google/google"),
    'threejs' : require("./threejs/threejs"),
    'vis'     : require("./vis/vis")
  };
};

d3.js

The magic happens in charts/d3/d3.js:

var d3 = {};

// Store d3 version 4 in a variable called d3v4
d3.d3v4 = require("../../../lib/d3.v4.4.0.min");
// Store d3 version 3 in a variable called d3v3
d3.d3v3 = require("../../../lib/d3.v3.5.17.min");

// Include dex charting components
d3.Chord = require("./Chord");
d3.ClusteredForce = require("./ClusteredForce");
d3.Dendrogram = require("./Dendrogram");
d3.HorizontalLegend = require("./HorizontalLegend");
d3.MotionBarChart = require("./MotionBarChart");
d3.MotionChart = require("./MotionChart");
// And so on and so forth...
// This one is a v3 component, it's treated no differently
d3.TreemapBarChart = require("./TreemapBarChart");

module.exports = d3;

Component Changes

Now component will have access to both d3v3 and d3v4.

In dex.js the entry points into the chart are:

  1. The constructor
  2. render()
  3. update()

So each of these will require setting the relevant d3 version. In the following example, ParallelCoordinates is a v3 component and TreemapBarChart is a v4 component.

Parallel Coordinates Component Changes

var parallelcoordinates = function (userConfig) {
  d3 = dex.charts.d3.d3v3;
  // Same old code as before...

  // And all our entry points...
  chart.render = function render() {
    d3 = dex.charts.d3.d3v3;
    // Same old...
  };
  
  chart.update = function () {
    d3 = dex.charts.d3.d3v3;
    // Same old code...
  };
  // Same old code...
};

TreemapBarChart Component Changes

var treemapbarchart = function (userConfig) {
  d3 = dex.charts.d3.d3v4;
  // Same old code as before...

  // And all our entry points...
  chart.render = function render() {
    d3 = dex.charts.d3.d3v4;
    // Same old...
  };
  
  chart.update = function () {
    d3 = dex.charts.d3.d3v4;
    // Same old code...
  };
  // Same old code...
};

Things to watch out for

Unknown entrypoints into charts like event handlers can be tricky.

Final Result

Now that I can intermingle d3 v3 and v4 components, I can do really cool stuff like having a Parallel Coordinates chart talk to a TreemapBarChart like so:

And the javascript which made that possible:

var csv = // defining data here...

var pc = new dex.charts.d3.ParallelCoordinates({
  'parent'           : '#ParallelCoordinates',
  'csv'              : csv
});
pc.render();

var treemapBarChart = new dex.charts.d3.TreemapBarChart({
  'parent'           : '#TreemapBarChart',
  'categoryLabel.font.size' : 8,
  'csv'              : csv
});
treemapBarChart.render();

treemapBarChart.subscribe(pc, "select", function (msg) {
  treemapBarChart.attr('csv', msg.selected).update();
});

Final Thoughts

So not only can we include multiple versions of d3 together, we can also support interaction between them. Similar mechanisms can be used in plain old browser pages. Here’s a discussion on Stack Overflow which gives a good starting point.

Additionally, I don’t have to port the entire framework to d3 v4 at once. Nor do I have to wait for my other integrated d3-based frameworks to migrate.

All in all, it seems to be a pretty good tradeoff for me.