Load external SVGs with d3 (v5)

 

If you search the web for examples of d3 data visualizations, you can expect to find a host of charts, graphs, plots, and maps. But data visualizations don’t have to be limited to these examples. They can also include photos, drawings, and animated graphics. I will focus on using SVGs so we can manipulate each shape inside our graphic, and rescale them without losing image quality. D3 has the power to load, print, and manipulate SVG data, giving us limitless ways to create visualizations.

This graphic from The South China Morning Post uses illustration
to visualize how much money rich people spend on clothes.

In a previous related post I described steps to create web-friendly graphics using Procreate and Adobe Illustrator. In this post, I will explain how to load those illustrations into an application. This seems like it should be straightforward; however, there are a few gotchas to be aware of such as when loading an SVG more than one time, when working with multiple SVGs and when positioning the SVG with data.

Table of contents

  1. Load one SVG
  2. Load multiple SVGs
  3. Summary: Using imported SVGs in the real world

Tools


Set up files

Create a folder containing your design files, an empty js file, and an HTML document with this template:

<html>

<head>
  <style></style>
</head>

<body>
  <script src="index.js"></script>
</body>

</html>

Load the d3.js library

Download the latest version of d3 to your folder or link directly from d3js.org. Paste the path to d3.min.js or the direct link into a script tag before your index.js script.

<html>

<head>
  <style></style>
</head>

<body>
  <script src="https://d3js.org/d3.v5.min.js" defer></script>
  <script src="index.js" defer></script>
</body>

</html>

Get the XML data

Since SVG is an XML language, d3.xml() can be used to access your image document. The request for the XML data will go in your index.js file.

Load one illustration, one time

Create a new div in the body of your html template. This will be the container for your SVG.

In your javascript file, select the new div with d3.select("#svg-container") and attach the XML data to it using .node().append(data.documentElement).

I’ve added a max height and width to the style tag in the html document to make sure the SVG doesn’t run off the edge of the window.

index.html

<html>

<head>
  <style>
    svg {
      max-height: calc(100vh - 10px);
      max-width: calc(100vw - 10px);
    }
  </style>
</head>

<body>
  <div id="svg-container"></div>

  <script src="https://d3js.org/d3.v5.min.js" defer></script>
  <script src="index.js" defer></script>
</body>

</html>

index.js

d3.xml("maple_illustration.svg")
  .then(data => {
    d3.select("#svg-container").node().append(data.documentElement)
  });

Result

See the full page result

Multiple instances of a single illustration

Add two or more divs to the body.

When we made the ajax request for the SVG, d3 parsed the file and created an object representation of the contents, in this case an xml dom node. We cannot simply change
d3.select("#svg-container") to d3.selectAll("div") and expect the illustration to print out in both divs. Instead, we must copy the object into each div using .cloneNode().

On the next line, .nodes().forEach() executes an anonymous function, which accepts “n” as the first parameter, which in this case is a dom node.

index.html

<html>

<head>
  <style>
    svg {
      max-height: calc(100vh - 10px);
      max-width: calc(100vw - 10px);
    }
  </style>
</head>

<body>
  <div></div>
  <div></div>

  <script src="https://d3js.org/d3.v5.min.js" defer></script>
  <script src="index.js" defer></script>
</body>

</html>

index.js

d3.xml("maple_illustration.svg")
  .then(data => {
    d3.selectAll("div").nodes().forEach(n => {
      n.append(data.documentElement.cloneNode(true))
  })
});

Result

See the full page result

Multiple files, printed one time each

Create two divs with unique ids in the body.

Illustrator assigns classes like cls-1 to paths upon exporting. The paths in your SVGs will need to have unique class names or else their styles will overwrite each other. Unfortunately this renaming needs to be done in the text editor, although Atom has a convenient find and replace all tool (cmd/ctrl + F), which can be used to replace all of the cls-1s/2s/etc with unique names.

To access more than one document, use Promise.all. Promise.all is used to run a batch of asynchronous tasks in parallel; in this case, we are wrapping multiple ajax requests into one promise.

The rest of the process is very similar to the setup for loading a single illustration, with some minor changes to make sure we are selecting unique xml objects and appending to unique divs. Even though we are printing multiple illustrations, we do not need to use .cloneNode() here because the illustrations are coming from different files and only get printed once.

index.html

<html>

<head>
  <style>
    svg {
      max-height: calc(100vh - 10px);
      max-width: calc(100vw - 10px);
    }
  </style>
</head>

<body>
  <div id="maple-container"></div>
  <div id="oak-container"></div>

  <script src="https://d3js.org/d3.v5.min.js" defer></script>
  <script src="index.js" defer></script>
</body>

</html>

index.js

Promise.all([
  d3.xml("maple_illustration.svg"),
  d3.xml("oak_illustration.svg")
])
.then(([mapleIllustration, oakIllustration]) => {
  d3.select("#maple-container").node().append(mapleIllustration.documentElement);
  d3.select("#oak-container").node().append(oakIllustration.documentElement);
});
</html>

Result

See the full page result

Multiple instances of multiple files

Create four or more divs in the body. Give some of them one class name and the rest a different class name.

This process is basically a combination of the last two examples. The steps include:

  • renaming the classes in your SVG file so they are unique
  • using .cloneNode() to copy the dom nodes
  • using Promise.all to send multiple ajax requests
  • using d3.selectAll and .forEach to append the objects to their respective divs

index.html

<html>

<head>
  <style>
    svg {
      max-height: calc(100vh - 10px);
      max-width: calc(100vw - 10px);
    }
    </style>
</head>

<body>
  <div class="maple-container"></div>
  <div class="oak-container"></div>
  <div class="maple-container"></div>
  <div class="oak-container"></div>

  <script src="https://d3js.org/d3.v5.min.js" defer></script>
  <script src="index.js" defer></script>
</body>

</html>

index.js

Promise.all([
  d3.xml("maple_illustration.svg"),
  d3.xml("oak_illustration.svg")
])
.then(([mapleIllustration, oakIllustration]) => {
  d3.selectAll(".maple-container").nodes().forEach(n => {
    n.append(mapleIllustration.documentElement.cloneNode(true))
  });
  d3.selectAll(".oak-container").nodes().forEach(n => {
    n.append(oakIllustration.documentElement.cloneNode(true))
  });
});

Result

See the full page result

Summary: Using imported SVGs in the real world

Everything I’ve shown has just been illustrations appended to fullscreen divs. Most projects will require more sophistication than that, and the possibilities of layouts are greater than I can cover here.

Take a look at this scatterplot that uses imported SVGs as symbols. Since SVGs have their own coordinate system, positioning them correctly on an axis can be tricky. The SVGs in this plot have been appended to a pattern that is used as a fill for d3.symbolSquare, which is much easier to set coordinates for in this case. I’m sure that there are many more ways that this could have been solved that I’m not even considering.

I’ve opened the floor for suggestions on Twitter about ways to improve this plot (which Mike Bostock has contributed to generously). If you have another solution, or if you would like to discuss problems/solutions you have faced when working with imported SVGs, I would love to hear from you in a comment or tweet.

 

Leave a Reply

Your email address will not be published. Required fields are marked *