DEV Community

Sho Naka
Sho Naka

Posted on

I Replaced Image AI for Technical Diagrams with an 8-Tool Code-First Matrix

I needed faster edits for technical diagrams, and a lower recurring overhead for recurring visuals.
I stopped asking for new images for everything.
That change started the moment I replaced "generate now, tweak later" with a fixed 8-tool matrix.

TL;DR:
I moved recurring illustration work into seven scriptable stacks + one 3D stack and kept image-generation AI only as a fallback.

Why I rewrote this workflow

When I edited an article recently, I was spending too much time redoing the same visual shape in slightly different versions.
The same chart logic should not need prompt guessing each time.

I asked myself:

  • Can this be represented as text or code?
  • Can I regenerate it exactly when requirements change?
  • Do I need raw design freedom, or do I need deterministic structure?

If the answer was mostly "text/code + deterministic output," I did not open an image-generation model first.

I also kept one practical boundary: this was not an academic tool roundup.
This is a log of what I actually used and in what context.

The number that changed my mind: an 8-tool decision matrix

The number I now defend is exactly 8.
Instead of inventing synthetic savings, I evaluate every new illustration request against this matrix.

Tool Best fit Why I pick it
Mermaid flow, sequence, architecture notes fastest in markdown-native writing
PlantUML UML-heavy docs strict structure when Mermaid gets too loose
Markmap map-style summaries converts headings directly
Graphviz dependency and direction graphs compact graph semantics
matplotlib numeric visualizations source-of-truth from data tables
Pillow labels, badges, annotations deterministic pixel edits in Python
D3.js node/link or hierarchy interactions data-driven relationship rendering
Blender 3D explanatory graphics stronger structural clarity for complex scenes

This is the exact set I now reach for before any image-generation request.

What happened first: practical snippets

I am including small runnable snippets I can reuse.

1. Mermaid for deterministic flow maps

flowchart LR
  A["User"] --> B["App"]
  B --> C["API"]
  C --> D["Storage"]
  C --> E["Cache"]
Enter fullscreen mode Exit fullscreen mode
npm i -D @mermaid-js/mermaid-cli
Enter fullscreen mode Exit fullscreen mode

I use this for quick reviews because it is fast to read, fast to version-control, and fast to regenerate.

2. PlantUML for strict structure

@startuml
actor User
participant API
participant DB
User -> API: Request
API -> DB: Query
DB --> API: Result
API --> User: Response
@enduml
Enter fullscreen mode Exit fullscreen mode
java -jar plantuml.jar -tpng architecture.puml
Enter fullscreen mode Exit fullscreen mode

When a diagram should model lifecycle, protocol, or strict roles, this is my second branch after Mermaid.

3. Markmap from markdown headings

# Release Plan
## Week 1
### Audit
### Diagram targets
## Week 2
### Implementation
### Regression checks
## Week 3
### Publish preparation
Enter fullscreen mode Exit fullscreen mode
npm i -D markmap-cli
Enter fullscreen mode Exit fullscreen mode

This removes a whole "I have to learn a separate visual DSL" step for internal notes.

4. Graphviz for dependency graphs

digraph G {
  rankdir=LR;
  "API" -> "Auth";
  "API" -> "Search";
  "Auth" -> "DB";
  "Search" -> "SearchIndex";
}
Enter fullscreen mode Exit fullscreen mode
dot -Tsvg graph.dot -o graph.svg
Enter fullscreen mode Exit fullscreen mode

I use this when relationship direction is the only thing I need to make obvious.

5. matplotlib for reproducible data visuals

import matplotlib.pyplot as plt

stages = ["Flow", "Auth", "Search", "Storage", "Cache"]
latency = [1.2, 0.7, 2.1, 0.9, 0.4]

plt.figure(figsize=(7, 3.5))
plt.plot(stages, latency, marker="o")
plt.title("Pipeline Latency by Stage")
plt.ylabel("Seconds")
plt.tight_layout()
plt.savefig("pipeline-latency.svg")
Enter fullscreen mode Exit fullscreen mode
uv add matplotlib
Enter fullscreen mode Exit fullscreen mode

For this kind of visual, AI image generation is the wrong tool.
Data should be generated from data.

6. Pillow for labels and annotations

from PIL import Image, ImageDraw, ImageFont

canvas = Image.new("RGB", (640, 200), "#1f2d3d")
draw = ImageDraw.Draw(canvas)
draw.rectangle((20, 40, 620, 160), outline="#f4d03f", width=3)
draw.text((40, 80), "Deployment Checklist", fill="#ffffff")
canvas.save("badge-note.png")
Enter fullscreen mode Exit fullscreen mode
uv add Pillow
Enter fullscreen mode Exit fullscreen mode

I use this for simple, repeatable badges and annotations where consistency matters more than illustration style.

7. D3.js for flexible network diagrams

import { JSDOM } from "jsdom";
import * as d3 from "d3";
import fs from "node:fs";

const width = 540;
const height = 360;
const nodes = [{id: "A"}, {id: "B"}, {id: "C"}];
const links = [{source: "A", target: "B"}, {source: "B", target: "C"}];

const dom = new JSDOM("<!doctype html><body></body>");
const body = d3.select(dom.window.document.body);
const svg = body.append("svg").attr("viewBox", `0 0 ${width} ${height}`);

const simulation = d3.forceSimulation(nodes)
  .force("link", d3.forceLink(links).id(d => d.id).distance(110))
  .force("charge", d3.forceManyBody().strength(-220))
  .force("center", d3.forceCenter(width / 2, height / 2));

simulation.tick(80);

svg.selectAll("line")
  .data(links)
  .join("line")
  .attr("x1", d => d.source.x)
  .attr("y1", d => d.source.y)
  .attr("x2", d => d.target.x)
  .attr("y2", d => d.target.y);

svg.selectAll("circle")
  .data(nodes)
  .join("circle")
  .attr("cx", d => d.x)
  .attr("cy", d => d.y)
  .attr("r", 18);

fs.writeFileSync("network.svg", body.html());
Enter fullscreen mode Exit fullscreen mode
npm i d3 jsdom
Enter fullscreen mode Exit fullscreen mode

When relationship density grows, D3 gives me the control that static diagram tools sometimes hide.

8. Blender for 3D explanatory scenes

import bpy

bpy.ops.wm.read_factory_settings(use_empty=True)
camera = bpy.data.objects["Camera"]
camera.location = (4, -6, 3)
camera.data.lens = 40
cube = bpy.ops.mesh.primitive_cube_add(size=2, location=(0, 0, 1))
sphere = bpy.ops.mesh.primitive_uv_sphere_add(radius=0.6, location=(2, 0, 0.6))

bpy.ops.render.render(write_still=True, filepath="infra-overview.png")
Enter fullscreen mode Exit fullscreen mode
blender --background --python render_scene.py
Enter fullscreen mode Exit fullscreen mode

Blender is the last tool I keep for cases where shape and spatial composition are part of the explanation.

The rule I extracted

I call it the "draw from intent, not from prompt" rule:

If the figure has structure, use text/code and regenerate it from source. Use image-generation AI only for final polish or style-first deliverables.

This removed most of the recurring "I know the idea but cannot get the same output again" pain.

Try it yourself

  1. Choose one article section that currently uses an image-style AI file.
  2. Convert it to one of the 8 matrix buckets.
  3. Replace one graphic with the smallest corresponding snippet above.
  4. Regenerate after one parameter/text change and compare the maintenance speed.
  • Mermaid for control-flow logic
  • PlantUML for strict protocol or class views
  • Markmap for knowledge maps
  • Graphviz for dependency direction
  • matplotlib for numbers
  • Pillow for labels and badges
  • D3.js for link-heavy visuals
  • Blender for 3D structure-only cases

If I had to do one thing now, I would first swap one recurring illustration with Mermaid or matplotlib and leave the rest unchanged until the matrix habit becomes automatic.

I can ship fast when visuals are generated like code.

Top comments (0)