[Tech] Adding Light/Dark Theme Support to Graphviz

Background

I’ve been a heavy user of HackMD for a long time. Whether it’s class notes or self-study materials, basically everything is recorded on HackMD.

The reason I love HackMD so much is that it combines many convenient features: for example, wrapping text with $$ lets you write $\LaTeX$ mathematical expressions, and using ```graphviz``` lets you create flowcharts (Graphviz). This design where you only need text to generate the required diagrams/formulas is incredibly useful.

When I decided to start building my personal blog, since it also uses markdown format for writing, I didn’t want to lose these two convenient features. The blog theme I use is based on Fluid’s modified version, which provides built-in light and dark modes, so I discovered a problem that bothered me.

The Problem

Let’s first look at a mathematical expression written with $\LaTeX$ syntax:

$$
\sum\limits_{i=0}^{n}a_i
$$

While $\LaTeX$ can adjust font colors based on the background color (try manually switching between light and dark modes!), Graphviz doesn’t support this. Since native Hexo doesn’t support rendering Graphviz like HackMD does, I used the hexo-filter-viz package. Let’s look at an SVG rendered with the original hexo-filter-viz:

relax A A Updated Want to go to the cloest b b Cloest to the current subgraph (Update first) A->b 1 c c Second close to the current subgraph A->c 5 b->c 2

The first problem I noticed is it’s not centered! And while it looks fine in light mode, switching to dark mode is painful. That awkward background and lines that don’t adapt to the theme really need fixing😢. After a series of adjustments, I finally achieved the following result:


Looks fantastic! Except for the parts that originally had colors, everything else now adapts to the light/dark theme, and the background is gone! In my markdown files, I just need to use Graphviz as usual to generate theme-supporting flowcharts! Let me introduce what I did!

Getting Started

How to Render

First of all, I want to thank Chris’s Tech Notes for this article. Actually, the Fluid theme initially didn’t support generating Graphviz like HackMD does, so I started using his modified package. If you’re interested, definitely check it out!

Anyway, after diving into his package, the most important file is lib/render.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
var reg = /(\s*)(```) *(graphviz) *\n?([\s\S]+?)\s*(\2)(\n+|$)/g;

function ignore(data) {
var source = data.source;
var ext = source.substring(source.lastIndexOf('.')).toLowerCase();
return ['.js', '.css', '.html', '.htm'].indexOf(ext) > -1;
}

function getId(index) {
return 'graphviz-' + index;
}

exports.render = function(data) {
if (!ignore(data)) {

var graphviz = [];

data.content = data.content
.replace(reg, function(raw, start, startQuote, lang, content, endQuote, end) {
var graphvizId = getId(graphviz.length);
graphviz.push(content);
return start + '<div id="' + graphvizId + '></div>' + end;
});

if (graphviz.length) {
var config = this.config.graphviz;
// resources
data.content += '<script src="' + config.vizjs + '"></script>';
data.content += '<script src="' + config.render + '"></script>';
data.content += graphviz.map(function(code, index) {
var graphvizId = getId(index);
var codeId = graphvizId + '-code';
return '' +
'{% raw %}' +
'<textarea id="' + codeId + '" style="display: none">' + code + '</textarea>' +
'<script>' +
' var viz = new Viz();' +
' var code = document.getElementById("' + codeId + '").value;' +
' viz.renderSVGElement(code)' +
' .then(function(element) {' +
' document.getElementById("' + graphvizId + '").append(element);' +
' });' +
'</script>' +
'{% endraw %}';
}).join('');
}
}
};

I see! So basically it parses the original code block, then uses {% raw %} to inject code, and uses Viz to render the entire code. Finally, it returns the rendered element to the <div id="graphvizID"></div>. Got it, understood the general idea.

Looks like this is where I need to make modifications. What are we waiting for? Let’s fork it and start coding!

Centering

Actually, the first thing that bothered me was the default result isn’t centered, which was painful to look at. So let me manually add the style="text-align:center;" attribute to the <div id="graphvizID"></div> tag!

Removing Background and Modifying Line Colors

Next is the background color. Although I could manually write bgcolor=transparent; when writing markdown, I found that inelegant, so I decided to directly modify the code injected into {% raw %}. First, let’s look at the structure of the rendered SVG:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<svg width="337pt" height="184pt" viewBox="0.00 0.00 337.30 183.74" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 179.7361)">
<title>relax</title>
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-179.7361 333.2956,-179.7361 333.2956,4 -4,4"></polygon>
<!-- A -->
<g id="node1" class="node">
<title>A</title>
<ellipse fill="none" stroke="#b2dfee" cx="151.3691" cy="-18.0079" rx="18.0157" ry="18.0157"></ellipse>
<text text-anchor="middle" x="151.3691" y="-13.8079" font-family="Times,serif" font-size="14.00" fill="#000000">A</text>
<text text-anchor="middle" x="66.6806" y="-57.0157" font-family="Times,serif" font-size="14.00" fill="#000000">Updated</text>
<text text-anchor="middle" x="66.6806" y="-40.2157" font-family="Times,serif" font-size="14.00" fill="#000000">Want to go to the cloest</text>
</g>
<!-- A&#45;&gt;c -->
<g id="edge3" class="edge">
<title>A-&gt;c</title>
<path fill="none" stroke="#a52a2a" d="M167.0241,-27.0463C177.7195,-33.2213 192.1078,-41.5284 204.3,-48.5675"></path>
<polygon fill="#a52a2a" stroke="#a52a2a" points="202.6875,-51.678 213.0978,-53.647 206.1876,-45.6158 202.6875,-51.678"></polygon>
<text text-anchor="middle" x="182.162" y="-42.0069" font-family="Times,serif" font-size="14.00" fill="#000000">5</text>
</g>
</g>
</svg>

Although I’ve only listed a few important components here, from the rendered result we can see: the outermost svg tag adjusts the overall SVG image size, and going to the first level g represents the entire graph, where the first polygon inside is the background, and the following g elements represent graph components like node for shapes and edge for arrows, which contain smaller elements to form the required shapes.

My approach here is to adjust colors by modifying fill and stroke: if I want something invisible, set the color to transparent, and if I want it to adapt to the theme’s text color, change it to var(--text-color). So here I handle the background and other polygons together:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
var viz = new Viz();
var code = document.getElementById(" + codeId + ").value;
viz.renderSVGElement(code)
.then(function(element) {
element.setAttribute("width", "100%"); // Adjust size
polygons = element.getElementsByTagName("polygon");
for (var i = 0; i < polygons.length; i += 1) {
if (polygons[i].parentElement.className.baseVal == "graph") {
polygons[i].setAttribute("stroke", "transparent"); // Background
} else if (polygons[i].getAttribute("stroke") == "#000000") {
polygons[i].setAttribute("stroke", "var(--text-color)");
}
if (polygons[i].parentElement.className.baseVal == "graph") {
polygons[i].setAttribute("fill", "transparent"); // Background
} else if (polygons[i].getAttribute("fill") == "#000000" && polygons[i].parentElement.className.baseVal == "edge") {
polygons[i].setAttribute("fill", "var(--text-color)");
}
}
paths = element.getElementsByTagName("path");
for (var i = 0; i < paths.length; i += 1) {
if (paths[i].getAttribute("fill") == "none" && paths[i].parentElement.className.baseVal == "edge") {
paths[i].setAttribute("fill", "var(--text-color)");
}
if (paths[i].getAttribute("stroke") == "#000000" && paths[i].parentElement.className.baseVal == "edge") {
paths[i].setAttribute("stroke", "var(--text-color)");
}
}
ellipses = element.getElementsByTagName("ellipse");
for (var i = 0; i < ellipses.length; i += 1) {
if (ellipses[i].getAttribute("stroke") == "#000000") {
ellipses[i].setAttribute("stroke", "var(--text-color)");
}
}
texts = element.getElementsByTagName("text");
for (var i = 0; i < texts.length; i += 1) {
if (texts[i].getAttribute("fill") == "#000000") {
texts[i].setAttribute("fill", "var(--text-color)");
}
texts[i].setAttribute("stroke", "none");
}
document.getElementById(" + graphvizId + ").append(element);
});

Here I’ve only adjusted the elements I think are commonly used. I haven’t included others that I think are rarely used.

This just shows the key JavaScript code to be injected into {% raw %}. You can see it simply changes fill and stroke default values to the theme-adapted var(--text-color), and in Polygon, there’s an additional check for background cases.

Adjusting Size

Finally, after making adjustments, I found that if the given diagram is too large, it actually exceeds the note’s layout (overflows to the right), so on line 5 I manually adjusted the width to 100%, so it won’t overflow!

Final Product

Okay! After the above adjustments, everything looks good after a quick review! So I saved the modified result in hexo-filter-viz-customized, so my CI/CD on GitHub can also download this package and properly render Graphviz for me! Awesome!

Conclusion

Actually, I wanted to make this modification around summer 2023, but I was too busy and kept postponing it until winter break, when I fixed this small issue on a whim. Before completing Software Design Lab in my sophomore year, I was still quite intimidated by JavaScript in web pages, but I think through this implementation, I’ve also discovered my ability to read documentation and implement the features I need. I think this is also a kind of growth XD. Although what I made is simple, it still significantly improves the overall aesthetics. Looking forward to continuing to enhance this blog further😎.


[Tech] Adding Light/Dark Theme Support to Graphviz
https://torrid-fish.github.io/tech-graphviz_light_dark_support/
Author
Torrid-Fish
Posted on
February 1, 2024
Licensed under