Published 2021‑11‑07 • Last Modified 2025‑01‑01
One of the things I like about having my own homemade website is that I can do whatever I want with it. I suppose that's rather banal, but hey, it's my blog and I can say what I want on it.
Sometimes, what I want to say (not that I say many things on here...) is a diagram. In graduate school I made custom graphics using TikZ, which is amazing for making arbitrary production-quality graphics, but is hardly a casual tool. Another tool, more appropriate for casual usage, is Graphviz and the DOT language for laying out graphs. Since I mostly think about software these days, and many things in software are graphs, it would be nice to be able to have a clean, simple way to get a DOT graph into a blog post.
Update: I've switched this website to use React! I've discarded the workflow described below in favor of a pure-Javascript solution; see the update at the end of the post. The diagrams in this section don't work anymore!
So how do I do that?
I store these posts as Markdown
and render them using
@ts-stack/markdown
.
I could store the DOT source somewhere,
generate the images as static files as part of my build pipeline,
then link to the images using the standard Markdown link syntax.
But these diagrams would only be used in the particular post they appear in.
Why require a sub-request for a static asset that would only appear on one page?
A neat feature of @ts-stack/markdown
is that it lets you
provide custom implementations of Renderer
methods,
one for each type of Markdown "cell".
Even better, graphviz
can output SVG,
and SVG can be injected directly into HTML for display.
So I can add a custom code
method which takes a Markdown code block containing DOT source,
renders the DOT source into SVG,
and adds that SVG directly into the HTML document being rendered.
Here's (most of) the code needed for that:
import { execSync } from "child_process" import { Renderer } from "@ts-stack/markdown" // Render DOT source using the dot command line tool, part of graphviz. // You need graphviz installed for this to work! https://graphviz.org/download/ function dotToBuffer(input: string, format: "png" | "jpeg" | "svg"): Buffer { return execSync(`dot -T${format}`, { input: input, }) } class BlahRenderer extends Renderer { code(code: string, lang?: string, escaped?: boolean): string { if (lang === "dot-rendered") { const svg = dotToBuffer(code, "svg").toString() // render the code in the block as SVG in a string // Inject the raw SVG from the Buffer directly into the rendered HTML // (along with some Tailwind styles). return `<p class="mx-auto overflow-auto">${svg}</p>` } // Handle normal code blocks - not quite a super() call, but roughly equivalent return this.highlightCode(code, lang, escaped) } }
There's some other code to hook up the rendering to the actual web request,
but there's nothing special there, just standard Express code.
The end result is that a block with the custom language code dot-rendered
gets rendered as inline SVG in the HTML output sent back to the browser.
Here's an example:
digraph { bgcolor="transparent" subgraph cluster_0 { label="Subgraph 1" A -> B A -> C C -> D[color=red, penwidth=3] } subgraph cluster_1 { label="Subgraph B" 1 -> 2 -> 3 -> 4[color=blue] { rank="same"; 2, 3 } } }
Since that worked, I did the same trick with PlantUML, which provides a domain-specific language for making various kinds of UML diagrams that it translates to DOT under the hood. Here's what that pipeline looks like:
@startuml Express -> "@ts-stack/markdown" "@ts-stack/markdown" -> PlantUML PlantUML -> DOT DOT -> PlantUML PlantUML -> "@ts-stack/markdown" "@ts-stack/markdown" -> Express @enduml
As part of migrating this website to React, I wanted to make sure that the whole website could be rendered client-side. Though there have been some attempts to make Graphviz available via WASM, there doesn't seem to be anything like for PlantUML (unsurprisingly, since it's a Java library).
As a replacement, I went with Mermaid, which is a pure-Javascript library with some similar functionality to both Graphviz and PlantUML.
This is a simple React hook-based function component that defers rendering to Mermaid:
import React, { PropsWithChildren, useEffect, useState } from "react" import mermaidAPI from "mermaid" export function Mermaid({ children }: PropsWithChildren) { const [html, sethtml] = useState("") useEffect(() => { mermaidAPI.render("mermaid", String(children), sethtml) }, [children]) return ( <div dangerouslySetInnerHTML={{ __html: html, }} /> ) }
sequenceDiagram participant Alice participant Bob Alice->>Bob: Hi Bob Bob->>Alice: Hi Alice
Update: there's a nice package that handles this for you now, react-mermaid-diagram!
This Website Uses React | 2024‑06‑16 |
Adding Support for Inline DOT to Blahs | 2021‑11‑07 |
Adventures in GitHub Project Automation | 2020‑09‑05 |
Fluent Interfaces in Python and Ruby | 2020‑05‑25 |
My Favorite Software Materials | 2020‑03‑07 |