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.
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:
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 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.
I am including small runnable snippets I can reuse.
flowchart LR
A["User"] --> B["App"]
B --> C["API"]
C --> D["Storage"]
C --> E["Cache"]
npm i -D @mermaid-js/mermaid-cli
I use this for quick reviews because it is fast to read, fast to version-control, and fast to regenerate.
@startuml
actor User
participant API
participant DB
User -> API: Request
API -> DB: Query
DB --> API: Result
API --> User: Response
@enduml
java -jar plantuml.jar -tpng architecture.puml
When a diagram should model lifecycle, protocol, or strict roles, this is my second branch after Mermaid.
## Week 1
### Audit
### Diagram targets
## Week 2
### Implementation
### Regression checks
## Week 3
### Publish preparation
npm i -D markmap-cli
This removes a whole "I have to learn a separate visual DSL" step for internal notes.
digraph G {
rankdir=LR;
"API" -> "Auth";
"API" -> "Search";
"Auth" -> "DB";
"Search" -> "SearchIndex";
}
dot -Tsvg graph.dot -o graph.svg
I use this when relationship direction is the only thing I need to make obvious.
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")
uv add matplotlib
For this kind of visual, AI image generation is the wrong tool.
Data should be generated from data.
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")
uv add Pillow
I use this for simple, repeatable badges and annotations where consistency matters more than illustration style.
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());
npm i d3 jsdom
When relationship density grows, D3 gives me the control that static diagram tools sometimes hide.
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")
blender --background --python render_scene.py
Blender is the last tool I keep for cases where shape and spatial composition are part of the explanation.
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.
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.