Creating a Static Interactive Graph Diagram for Technical Documentation
Technical documentation for graph libraries needs to show graph structure
visually without relying on JavaScript frameworks that may not be available
in static site environments. This tutorial covers building node-edge diagrams
using SVG, CSS, and HTML details/summary, all of which work in GitHub
README files, Hugo/Jekyll static sites, and documentation platforms like
Read the Docs.
Design Goals
A good static graph diagram for documentation:
1. Renders correctly in all major browsers without JS
2. Is accessible (screen reader compatible)
3. Is copy-paste reusable (self-contained SVG or HTML)
4. Shows the information density appropriate for the section
Avoid: D3.js, Cytoscape.js, or any library that requires a JavaScript bundle.
These are appropriate for interactive applications, not documentation.
Approach 1: SVG Node-Edge Diagram
SVG is natively supported in all modern browsers and GitHub Markdown. A simple
5-node graph:
<svg width="400" height="280" xmlns="http://www.w3.org/2000/svg"
role="img"
aria-label="A 5-node directed graph showing message passing flow">
<title>Message Passing Graph Example</title>
<desc>Five nodes connected by directed edges illustrating
neighborhood aggregation in a GNN</desc>
<!-- Edges (draw before nodes so nodes appear on top) -->
<!-- n0 → n1 -->
<line x1="80" y1="80" x2="190" y2="80"
stroke="#888" stroke-width="2" marker-end="url(#arrow)"/>
<!-- n1 → n2 -->
<line x1="230" y1="80" x2="310" y2="150"
stroke="#888" stroke-width="2" marker-end="url(#arrow)"/>
<!-- n0 → n3 -->
<line x1="80" y1="100" x2="80" y2="180"
stroke="#888" stroke-width="2" marker-end="url(#arrow)"/>
<!-- n3 → n2 -->
<line x1="100" y1="200" x2="310" y2="165"
stroke="#888" stroke-width="2" marker-end="url(#arrow)"/>
<!-- n2 → n4 -->
<line x1="330" y1="170" x2="330" y2="230"
stroke="#888" stroke-width="2" marker-end="url(#arrow)"/>
<!-- Arrow marker definition -->
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5"
markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#888"/>
</marker>
</defs>
<!-- Nodes -->
<circle cx="80" cy="80" r="25" fill="#d0e8ff" stroke="#4a90e2" stroke-width="2"/>
<text x="80" y="85" text-anchor="middle" font-size="13" font-family="monospace">n₀</text>
<circle cx="210" cy="80" r="25" fill="#d0e8ff" stroke="#4a90e2" stroke-width="2"/>
<text x="210" y="85" text-anchor="middle" font-size="13" font-family="monospace">n₁</text>
<circle cx="330" cy="160" r="25" fill="#ffe0b2" stroke="#e65100" stroke-width="2"/>
<text x="330" y="165" text-anchor="middle" font-size="13" font-family="monospace">n₂</text>
<circle cx="80" cy="200" r="25" fill="#d0e8ff" stroke="#4a90e2" stroke-width="2"/>
<text x="80" y="205" text-anchor="middle" font-size="13" font-family="monospace">n₃</text>
<circle cx="330" cy="248" r="25" fill="#c8e6c9" stroke="#2e7d32" stroke-width="2"/>
<text x="330" y="253" text-anchor="middle" font-size="13" font-family="monospace">n₄</text>
<!-- Legend -->
<rect x="10" y="250" width="14" height="14" fill="#d0e8ff" stroke="#4a90e2"/>
<text x="30" y="262" font-size="11" fill="#555">source nodes</text>
<rect x="110" y="250" width="14" height="14" fill="#ffe0b2" stroke="#e65100"/>
<text x="130" y="262" font-size="11" fill="#555">target node (n₂)</text>
<rect x="240" y="250" width="14" height="14" fill="#c8e6c9" stroke="#2e7d32"/>
<text x="260" y="262" font-size="11" fill="#555">downstream</text>
</svg>
Approach 2: CSS-Styled Node Grid
For tensor shape diagrams (not graph topology), a CSS grid works well:
<style>
.tensor-grid {
display: flex;
gap: 6px;
align-items: flex-end;
font-family: monospace;
font-size: 12px;
margin: 12px 0;
}
.tensor-node {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.node-box {
width: 52px;
height: 52px;
border: 2px solid #4a90e2;
border-radius: 6px;
background: #d0e8ff;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
color: #333;
}
.node-label {
font-size: 11px;
color: #555;
}
</style>
<div class="tensor-grid" role="img"
aria-label="node_features tensor: N nodes each holding a C-H-W tensor">
<div class="tensor-node">
<div class="node-box">n₀<br>[C,H,W]</div>
<div class="node-label">node 0</div>
</div>
<div class="tensor-node">
<div class="node-box">n₁<br>[C,H,W]</div>
<div class="node-label">node 1</div>
</div>
<div class="tensor-node">
<div class="node-box">n₂<br>[C,H,W]</div>
<div class="node-label">node 2</div>
</div>
<span style="font-size:20px;color:#999;padding-bottom:14px">···</span>
<div class="tensor-node">
<div class="node-box">nₙ₋₁<br>[C,H,W]</div>
<div class="node-label">node N-1</div>
</div>
</div>
<p style="font-size:12px;color:#666">
<code>node_features</code>: shape [N, C, H, W]
</p>
Approach 3: details/summary for Expandable Diagrams
Long diagrams interrupt reading. Wrap complex ones in details/summary:
<details>
<summary>
<strong>GraphBatch batching diagram</strong>
— click to expand
</summary>
<svg width="480" height="200" xmlns="http://www.w3.org/2000/svg"
aria-label="GraphBatch combines three graphs into one batched graph">
<!-- Graph 1 box -->
<rect x="10" y="20" width="100" height="80" rx="6"
fill="#e8f4fd" stroke="#4a90e2" stroke-width="1.5"/>
<text x="60" y="42" text-anchor="middle" font-size="11">Graph g₁</text>
<text x="60" y="58" text-anchor="middle" font-size="10" fill="#555">N₁=4 nodes</text>
<text x="60" y="72" text-anchor="middle" font-size="10" fill="#555">E₁=5 edges</text>
<!-- Graph 2 box -->
<rect x="120" y="20" width="100" height="80" rx="6"
fill="#e8f4fd" stroke="#4a90e2" stroke-width="1.5"/>
<text x="170" y="42" text-anchor="middle" font-size="11">Graph g₂</text>
<text x="170" y="58" text-anchor="middle" font-size="10" fill="#555">N₂=3 nodes</text>
<text x="170" y="72" text-anchor="middle" font-size="10" fill="#555">E₂=4 edges</text>
<!-- Graph 3 box -->
<rect x="230" y="20" width="100" height="80" rx="6"
fill="#e8f4fd" stroke="#4a90e2" stroke-width="1.5"/>
<text x="280" y="42" text-anchor="middle" font-size="11">Graph g₃</text>
<text x="280" y="58" text-anchor="middle" font-size="10" fill="#555">N₃=5 nodes</text>
<text x="280" y="72" text-anchor="middle" font-size="10" fill="#555">E₃=6 edges</text>
<!-- Arrow -->
<text x="348" y="65" font-size="20" fill="#888">→</text>
<!-- Batched graph box -->
<rect x="370" y="10" width="100" height="100" rx="6"
fill="#fff8e1" stroke="#f57f17" stroke-width="1.5"/>
<text x="420" y="32" text-anchor="middle" font-size="11">GraphBatch</text>
<text x="420" y="50" text-anchor="middle" font-size="10" fill="#555">N=12 nodes</text>
<text x="420" y="65" text-anchor="middle" font-size="10" fill="#555">E=15 edges</text>
<text x="420" y="80" text-anchor="middle" font-size="10" fill="#555">offsets applied</text>
<!-- Caption -->
<text x="240" y="175" text-anchor="middle" font-size="11" fill="#555">
GraphBatch.from_graphs([g1, g2, g3])
</text>
</svg>
</details>
Accessibility Checklist
Before publishing any diagram:
- [ ] SVG has
role="img"andaria-labelattribute - [ ] SVG has
<title>element inside it - [ ] Text in SVG uses
<text>elements (not embedded images of text) - [ ] Color is not the only way to convey information (use labels too)
- [ ]
detailssummary text describes content clearly
When to Use Each Approach
| Situation | Approach |
|---|---|
| GitHub README (limited HTML) | ASCII with fenced code block |
| GitHub README (SVG allowed) | Inline SVG (renders in GitHub) |
| Static site with HTML support | SVG + details/summary |
| Tensor shape documentation | CSS grid or SVG boxes |
| Complex graph topology | SVG with labeled nodes/edges |
| Print/PDF documentation | SVG (PDF renders SVG correctly) |
| Jupyter notebook | SVG or matplotlib (inline) |
Static, dependency-free diagrams age well. A diagram built from SVG and HTML
will render correctly in five years without any build step or CDN dependency.
That is worth the extra verbosity compared to a one-line chart library call.