No Time Dad

A blog about web development written by a busy dad

Simple D3 TypeScript Environment

A lot of the D3 code I’ve seen and worked with has been wild. Often hard to make sense of and untangle. Which might just be the inherent nature of D3 since it’s so math heavy, but it’s frustrating none the less. My hope is that using TypeScript with D3 will help alleviate some of this pain for me. But first I have to build a D3 TypeScript environment.

The basic environment setup files are shown below.

# project structure
| public
    - index.html
| src
    - index.ts
- package.json
- tsconfig.json
- rollup.config.js
// package.json
{
  "name": "ts_rollup",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "npx rollup --config --watch"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@rollup/plugin-node-resolve": "^13.1.3",
    "@rollup/plugin-typescript": "^8.3.1",
    "@types/d3-selection": "^3.0.2",
    "rollup": "^2.70.1",
    "typescript": "^4.6.2"
  },
  "dependencies": {
    "d3-selection": "^3.0.0"
  }
}
// rollup.config.js
import typescript from "@rollup/plugin-typescript"
import { nodeResolve } from "@rollup/plugin-node-resolve"

export default {
  input: "src/index.ts",
  output: {
    dir: "dist",
    format: "iife",
    name: "graph_tools",
  },
  sourceMap: true,
  plugins: [
    typescript({ tsconfig: "./tsconfig.json" }),
    nodeResolve({ modulesOnly: true }),
  ],
}
// tsconfig.json
{
  "compilerOptions": {
    "target": "es2016",
    "module": "esnext",
    "rootDir": "./src",
    "outDir": "./dist",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true
  }
}
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="../dist/index.js"></script>
    <title>Document</title>
  </head>
  <body>
    <div id="myDiv">My Div</div>
  </body>
  <script>
    graph_tools.bg_color()
  </script>
</html>
// src/index.ts
import { selectAll } from "d3-selection"

export function bg_color() {
  const myDiv = selectAll("#myDiv")
  myDiv.style("background-color", "red")
}

Explanation

I had a general idea of how I thought this D3 TypeScript environment would work. But, of course, it didn’t work. My first pass at this was to install TypeScript and D3 via npm, then add my D3 code to the .ts file, transpile it, and point html script source at the transpiled output. It was a good idea in theory, but the problem is that the browser doesn’t know anything about D3 since its source exists in node_modules/ and not in the transpiled JavaScript.

To fix this problem I needed to bundle the D3 node_modules code in with my TypeScript code. But, I haven’t used any kind of bundling libraries like webpack or rollup.js on their own before. I’ve always used them in passing with React or Angular. And I typically don’t modify the default configurations. So, diving into the world of creating bundles was very new to me.

Honestly, it took me about a day or so to figure all of it out. I went back and forth between webpack and rollup.js, but the config for rollup.js ended up being a lot less complicated than webpack’s so that’s what I went with.

Lessons learned

If the .ts doesn’t contain an export then nothing is transpiled to the js bundle from that file. For example, if the ts file just had the below function then it wouldn’t be added. Adding an export (default or not) is what triggers the code to be included. I believe this is part of rollup.js tree-shaking.

// Treated as unused and not included in the bundle
function bg_color() {
  ...
}
// Included in the bundle
export function bg_color() {
  ...
}

Another issue that took me longer than I’d like to admit to figure out was that my bg_color function was always undefined when trying to access it in the index.html file. The reason for this was because I neglected to name my rollup.js output. Which is something that the rollup.js command kept warning me about, but I chose to ignore for some reason. Once I added the name field to the rollup.config.js I could access the bg_color function via graph_tools.bg_color().

// rollup.config.js
...

export default {
  ...
  output: {
    ...
    // Adding the name to provide context
    name: "graph_tools",
  },
  ...
}

The last thing that tripped me up during this process was that the D3 module code I was using was not being included in the bundle. In this example it was D3’s de-selection module. For some reason I thought this would just happen automatically. What I needed to do was add the @rollup/plugin-node-resolve plugin which will include and tree-shake any node_modules I’m referencing in my code.

Conclusion

I’m optimistic that writing D3 code with TypeScript will yield higher quality code. I guess time will tell. Either way, my first foray into creating custom bundles was enlightening. I sort of got a taste for why people say creating bundles isn’t easy. There’s a lot of configurations and options. It made my head hurt trying to figure it all out, and I doubt this is even the most optimal way to do it.