Victor Björklund

Building awesome diagrams with D2

Published: Jun 20 2023

There is a better way than manually creating diagrams in Figma. Enter the world of declarative diagramming where you generate diagrams from code. There are several frameworks to choose from where perhaps the most famous is mermaid.js. However, I recently spotted D2lang in a thread in Hacker News and even if it might not have the same big community the diagrams that it generates can both be very complex and they just look really good.

Just look at the really cool types of graphs you can create with D2:

Why declarative diagramming?

The first question is of course why we even need to bother with this in the first place. Why not just manually create the diagram in a software like Figma or Illustrator?

Ease of use: Instead of painstakingly arranging and aligning shapes manually, users can simply describe the desired structure and connections using a concise and structured language. The diagramming tool then interprets this input and generates the diagram accordingly. This automation not only saves significant time and effort but also minimizes the chances of errors that can occur during manual diagram creation.

Easy of modification: One of the most compelling aspects of declarative diagramming is its inherent adaptability and ease of modification. Traditional diagramming methods often make it challenging to revise or update diagrams, especially when the changes involve rearranging or reconnecting elements. Declarative diagramming mitigates these challenges by separating the diagram’s structure and relationships from its visual presentation. This separation allows for quick updates and modifications without the need for extensive manual adjustments, enabling users to iterate and refine their diagrams rapidly. You could change dozens of diagrams on a site with just a few adjustments to the code.

Large scale production: Declarative diagramming also enables you to use diagrams when the amount of diagrams are not realistically possible to create manually. For example if you have several thousands similar diagrams that you need to create or if they all change several times per day. Or if you want diagrams based on user generated content, perhaps you want to generate a diagram showing the relations between users in a social media app.

Installing and setting up D2

D2 is written in golang which enables it to

curl -fsSL https://d2lang.com/install.sh | sh -s --

Create your first diagrams

Using files

The standard way to generate a D2 diagram is to store the code in a file so lets create a file called input.d2 with a simple diagram by writing this in the terminal:

echo 'x -> y' > input.d2

And then we use the D2 cli to generate the diagram:

d2 -w input.d2 out.svg

And the result is a simple diagram:

Notice my diagrams might look at bit different to yours and it is because I use different styling than the default. More on styling later.

Without files

This took me a while to find on the website and could probably be better documented. If you want to use D2 without first creating a .d2 file you can write ”-” in the cli to get it to use stdinput instead. That is useful if you don’t need to store the d2 files or you store the code inside something else (for example this article is written in markdown that contains D2 code).

This command will use the stdinput and send the result to the stdoutput:

echo "x -> y" | d2 - -

You can of course mix with files and without files. For example you can use the stdinput but save to a file like this:

echo "x -> y" | d2 - output.svg

The building blocks

Shapes

You can declare shapes simply by writing out a name as a key for them like this:

simple shape
simple_shape
sIMPle_sHAPE
simple shape
simple_shape
sIMPle_sHAPE

By default the label and the key is the same thing. But you can also specifically give it another label like this:

pg: PostgreSQL

By default all shapes are in the form rectangles but you can choose another shape by setting the shape attribute like this:

Cloud: my cloud
Cloud.shape: cloud

This is the current list of possible shapes (see the documentation for an up-to-date list:

  • rectangle
  • square
  • page
  • parallelogram
  • document
  • cylinder
  • queue
  • package
  • step
  • callout
  • stored_data
  • person
  • diamond
  • oval
  • circle
  • hexagon
  • cloud

Connections

Just creating shapes on their own might be cool but not so useful. The real benefit comes when we in our diagram can show relations between different shapes. In D2 there are 4 different types of connections between shapes:

  • --
  • ->
  • <-
  • <->

Here you can see what they result in:

James -- Linda: --

John -> Anna: ->

Mike <- Emma: <-

Gordon <-> Mia: ->

You can add more than one connection per shape and even more than one connection between the same shapes:

John -> Anna
John -> Anna
John -> Anna

Labels

You can create labels on the connections to make it more clear what type of connection you are showing. You create a label by simply writing colon and the text you want in your label.

John <-> Anna: Married

Styling

Themes

There are several ways to style your diagrams to achieve that exact look you are after. Let’s start with the simplest way and that is to choose which “theme” you want to apply. The current styles build into D2 are the following:

And you select the theme by setting the -t flag in your command

d2 -t 101 input.d2

Or by setting an environmental variable:

export D2_THEME=101

And related to themes is the “hand-drawn”-mode which you can activate which makes all your diagrams look hand-drawn which is just awesome. You activate it by setting the flag –sketch when you call D2:

d2 --sketch input.d2

Styles

You can customize the style of the diagram or individual shapes by setting attributes.

Styling the diagram:

You can style the “root” of the diagram which means for example the background. For example by default your diagrams will have a white background which can be annoying if you embed your diagrams on a website with some other background color. Therefore I usually specify the background color to be transparent:

Anna <-> John
style: {
	fill: transparent
}

The different types of styles you can apply to the root of your document are the following:

  • fill: diagram background color
  • fill-pattern: background fill pattern
  • stroke: frame around the diagram
  • stroke-width
  • stroke-dash
  • double-border: two frames, which is a popular framing method

One thing related to styling the diagram is the amount of padding used. By default the padding has a default value of 100 but I personally prefer to set the padding using css in the website rather than in the image so I always change it to zero. You can set the padding by setting the flag –pad=50 for a padding of 50.

d2 --pad=50 input.d2
Styling shapes and connections

You set the styling by accessing the attribute of the shape or connections in this way:

Anna <-> John
Anna.style.fill: "#f4a261"
John.style.fill: honeydew

There are plenty of different style attributes that you can set on your shapes. To see a complete list go to the documentation. But hopefully you now know the principles by styling the shapes.

Call D2 from Javascript

If you are like me you like to build automated solutions instead of manually going in generating the diagrams and save them to the right place (after all that is part of the reason we use something like D2 in the first place).

If you aren’t building your website/app using Golang you might wonder how we can use D2 in our project. I will assume you are using some kind of Javascript (for better or worse that is the state of the world right now). How can we generate D2 diagrams for our website? There are of course multiple ways to achieve this result. One way would be to simply build a microservice in Golang that generates the diagram and call that service from the backend or frontend.

But I wanted a more simple solution and came up with an easy way by using Exec in Javascript that allows us to call to the terminal. One thing to note is that I only use D2 on static sites so far so I’m not so concerned with the speed or performance since the generation is just done once at build time. I might not use this method if the site wasn’t static. At least I would implement some kind of caching.

Choose where you want to run your generation. It depends on your framework/setup of course but I have gone with a simplified setup where I just place the code in a nodejs script that is run before the normal build process.

import child_process from "child_process";

// We import util and uses promisfy on exec to make it possible 
// to use async/await later. Don't wanna deal with callback hell

import util from 'util';
const exec = util.promisify(child_process.exec);

async function generate_image(d2input, filename) {
	await exec(`echo "${d2input}" | d2 ${filename}`)
	return
}

This is of course a very simplified example. In production you probably want to handle any errors etc but it is a good start for you to work on.

Conclusion

In this article, we explored the world of declarative diagramming with D2. We learned about the advantages of declarative diagramming, how to get started with D2, and the various techniques for creating diagrams, styling shapes, and adding themes and dimensions.

D2 proves to be a valuable tool, especially when automating diagrams that may evolve over time. Its simple syntax and extensive customization options make it popular among developers for documentation and entity-relationship diagrams.

Frequently asked questions