Let’s Make a (D3) Plugin

This tutorial teaches you to create a plugin for D3 using D3’s new 4.0 module pattern. Although the word plugin suggests extending core functionality, this pattern is used internally by D3 to organize the code into modules. Thus, you can use this pattern to replace default behavior or to pick a subset of features for a custom build. Another way of thinking about it is that everything in D3 is a plugin, including core features like colors, scales, and selections.

By adopting this convention, it’ll be easier for people to discover and use your code across the various environments that D3 supports (CommonJS, AMD, ES6, etc.). And if you want help, the module pattern invites others in the D3 community to collaborate with you, on work you initiate.

Note: This article discusses modules as implemented in version 4.0, which is not yet released. Numerous D3 modules are available now, but this tutorial is primarily intended for contributors and those planning ahead for the new release.

# Create a Repository

First, pick a name for your plugin. Start with the prefix “d3-”, and then pick a short word or two that concisely describe your new plugin’s function. See the other D3 modules for inspiration. For this tutorial, we’ll use the (inspired) name “d3-foo”.

Download d3-plugin.zip. This archive is a starter template with all the files you’ll need to create a new D3 plugin. Unzip its contents to create a new folder:

unzip ~/Downloads/d3-plugin.zip

The resulting folder should look like this:

d3-foo
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── index.js
├── package.json
├─┬ src
│ └── foo.js
└─┬ test
  └── foo-test.js

(Note that the two dot files might be invisible unless you ls -a. Also, if you double-clicked on the zip archive rather than using unzip on the command line, then you might have a nested d3-plugin folder within your new d3-foo folder. So watch out for that.)

Initialize the git repository:

git init

The output of git status should look something like this:

On branch master

Initial commit

Untracked files:
  (use "git add ..." to include in what will be committed)

  .gitignore
  .npmignore
  LICENSE
  README.md
  index.js
  package.json
  src/
  test/

nothing added to commit but untracked files present (use "git add" to track)

Great! Now before we add and commit anything, let’s edit the starter files to make them specific to you and your new plugin.

# Edit the Metadata

The package.json file contains metadata describing your plugin: what it’s called, what it does, who authored it, how to build and test it, etc. In your preferred text editor, find and replace all instances of “foo” in the package.json file with your new plugin name.

Edit the description and keyword fields to describe what your new plugin does. If you are so inclined, add author or contributors fields, too. See the package.json documentation for more information on the supported fields and their meaning.

Edit the LICENSE and add the current year and your name. If you want to use a different open-source license than the three-clause BSD license used by D3, replace the LICENSE file with whatever you want, and edit the license field in the package.json file accordingly.

Edit the README, again replacing instances of “foo” with your plugin name, and adding appropriate description and documentation for your plugin. If you’re not sure yet what your plugin will do, use an ellipsis (…) as a placeholder. Effective documentation is a topic for another day, but try to convey at least one compelling, common use case so that readers can quickly grasp your plugin’s raison d’être. Adopting the standard format for the API reference will also help people learn how to use your plugin more quickly.

If your plugin depends on other D3 modules, specify a dependencies hash in the package.json, using the name of the module as the key and the acceptable versions as the value. For example:

"dependencies": {
  "d3-path": "1"
}

Alternatively, use npm install --save to install a module and add it to the package.json automatically. This is nice because it will always pick the latest published version. As the last step of specifying dependencies, find the rollup command in the pretest script in the package.json, and add a -g argument to enumerate the names of each dependant module. The -g step is necessary because Rollup by default generates globals of the form d3Path, and all D3 plugins should share the global d3 instead. For example:

rollup -g d3-path:d3

Now let’s get to the code!

# Edit the Code

The top-level index.js file specifies the symbols that your module exports: the public API. In the starter file, that’s a single symbol named foo:

export {default as foo} from "./src/foo";

Everything you export from this top-level file should be documented in your API reference in the README. See d3-timer and d3-random for more examples. You can put arbitrary code in your index file, too (as in d3-time and d3-format), but keep things simple if you can.

The src folder contains the module implementation. Typically there’s one source file for each symbol you export, but just as often a source file exports multiple symbols or contains code that’s private to the module. That’s all up to you. For example, here’s src/foo.js from the starter template:

export default function() {
  return 42;
};

Note that these import and export statements are the only new langauge feature adopted by D3 modules. While you can use other language features and a transpiler (say, Babel), I recommend limiting yourself. D3’s module pattern is intended to maximize compatibility with other D3 modules, and so tends to be conservative. The imports and exports are processed by Rollup to produce the bundle, but Rollup does not transpile any other language features.

As you implement your plugin, please update the tests for the public API accordingly. By convention, D3 modules use Substack’s Tape as a testing harness. (And we use JSDOM for DOM testing.) You’re free to use something else, but it’s nice if modules are consistent.

To verify that everything is setup correctly, install all dependencies, run the tests, and build generated files like so:

npm install

You should see the tests pass in green, and a few messages about generating d3-foo.js and d3-foo.min.js. Hooray! (Hooray?)

# Test the Code

While automated testing is invoked by npm test, you’ll also want to test your code in a real browser. It’s a good idea to publish a practical example—say on bl.ocks.org—so that people can see a live demonstration of your plugin.

The npm run prepublish command will build your code. Then, copy a generated bundle (d3-foo.js or d3-foo.min.js) out of the repository’s build folder and into your new example folder. The simplest way to load the generated bundle is with a script tag:

<script src="d3-foo.js"></script>

This will define a d3 global which contains everything exported from the top-level index.js. Thus:

d3.foo(); // 42

If your module has dependencies, you’ll need to load those, too. And the same is true if your example wants to use other modules (even if your new plugin doesn’t depend on them). For example, if you want d3-shape and d3-timer, you need three script tags because d3-shape depends on d3-path:

<script src="d3-timer.js"></script>
<script src="d3-path.js"></script>
<script src="d3-shape.js"></script>

Make sure that dependees are listed before dependants! (See the D3 repository for an example.) In the future I expect we’ll have a nice web-based UI for generating custom builds, although most examples should favor the convenience of the default build hosted on d3js.org.

Make sure that dependees are listed before dependants!

Lastly, you can generate an optimized minimal bundle using Rollup, but I’ll cover that another time. (See the d3-selection repository for an example.) In the future I expect we’ll have a nice web-based UI for generating custom builds, although most examples should favor the convenience of the default build hosted on d3js.org.

# Publish to GitHub

If you’re ready to share your code with the world, make your first commit:

git add .
git commit

Then create a new repository on GitHub. Add the git remote, and push:

git remote add origin [email protected]:{USERNAME}/d3-foo.git
git push -u origin master

Replace {USERNAME} with your GitHub user name and d3-foo with your plugin name, obvs.

Next, create a git tag that corresponds to the version in your package.json file. (If this is not your first release, you’ll also want to bump the version to 0.0.2, 0.1.0, or 1.0.0 as appropriate.) You can push commits as frequently as you like, but periodically you should bundle these commits together into a release. Tags are simply a mechanism for identifying releases in the repository.

git tag -a v0.0.1

(Alternatively, use npm version, which also edits the package.json file for you.)

Push the tag to GitHub:

git push --tags

Publish to NPM:

npm publish

As a side-effect of publishing, NPM will create a d3-foo.zip archive of your release in the build folder. Add this as a custom download to your GitHub releases (for example, see d3-shape) so that people can download your code without needing to use NPM. Edit your release notes to tell people what’s changed.

After publishing, feel free to add your plugin to the wiki and announce it on the Google group, Twitter and Slack!