Bill Mill web site logo

Bundle d3 with esbuild

Over the last few blog posts, I've built some small javascript applications. Each time, I've used a regular old <script> tag to download the libraries I need, rather than deal with bundling javascript.

Using <script> tags works for quick stuff, but eventually you're likely going to want to build a Javascript bundle to minimize the amount of javascript your client has to download.

There are several tools for building javascript bundles, but my favorite is esbuild.

To build a javascript bundle, I start by installing all my dependencies and esbuild:

npm install d3-array d3-fetch d3-geo d3-queue \
    d3-scale d3-scale-chromatic d3-selection \
    esbuild topojson-client

In the previous blog posts, I downloaded a large d3 bundle and worked with it for convenience; but d3 is built as a series of packages and the best way to work with it is to select only the ones I need. This lets me ship a smaller bundle to my clients and makes my application faster to load.

The next step is to modify the javascript I'm using to use the npm modules I just installed instead of relying on global d3 and topojson variables. I changed map.js to import its dependencies explicitly:

import { extent } from "d3-array";
import { json } from "d3-fetch";
import { geoPath } from "d3-geo";
import { scaleLog } from "d3-scale";
import { interpolateGreys } from "d3-scale-chromatic";
import { select } from "d3-selection";
import { feature } from "topojson-client";

function map(mapData, populationData) {
  const width = 975,
    height = 610,
    scale = scaleLog().domain(extent(Object.values(populationData))),
    colorScale = (d) => interpolateGreys(scale(d));

  const svg = select("#map")
    .append("svg")
    .attr("width", width)
    .attr("height", height)
    .attr("viewBox", [0, 0, 975, 610])
    .attr("style", "width: 100%; height: auto; height: intrinsic;");

  const usa = svg
    .append("g")
    .append("path")
    .datum(feature(mapData, mapData.objects.nation))
    .attr("d", geoPath());

  const state = svg
    .append("g")
    .attr("stroke", "#444")
    .selectAll("path")
    .data(feature(mapData, mapData.objects.states).features)
    .join("path")
    .attr("fill", (d) => colorScale(populationData[d.properties.name]))
    .attr("vector-effect", "non-scaling-stroke")
    .attr("d", geoPath());
}

window.addEventListener("DOMContentLoaded", async (event) => {
  map(
    ...(await Promise.all([
      json(`https://cdn.jsdelivr.net/npm/us-atlas@3/states-albers-10m.json`),
      json(`https://cdn.billmill.org/static/blog/us_choro/population.json`),
    ]))
  );
});

To compile our updated code, I use esbuild like this:

node_modules/.bin/esbuild map.js --bundle --outfile=bundle.js --sourcemap

I'm using three command line flags for esbuild:

Check out the man page for all the command line options, and you can dig into the API to do more complicated things.

At this point, I have a bundle file called bundle.js which contains my code and all its dependencies, and bundle.js.map which contains debugging information that will help the browser show me useful errors and let me debug my application.

The last step is to create a web page that loads my json and displays the map:

<html>
  <head>
    <script src="bundle.js"></script>
  </head>
  <body>
    <div id="map">
    </div>
  </body>
</html>

And finally, I have a single javascript file to display the same map I made before: