diagrams-and-charts
Create blog-quality diagrams and data charts (architecture flows, data structure cards, grouped bar charts) as plain HTML, then export them to PNG with a playwriter-based CLI. Ships two styles: claude
SKILL.mdskills/diagrams-and-charts/SKILL.md
GitHubdiagrams-and-charts
Figures are plain HTML files: boxes laid out with a css grid, arrows and charts drawn from
data by small TypeScript helpers. render.ts exports them through the user's Chrome.
The image is only the diagram or chart on its light background, with padding and square corners. No title, no page frame, no outer card: the blog post or doc supplies those. Put the title in the surrounding text, not in the image.
fig.html ──▶ render.ts: Bun.serve + playwriter tab ──▶ wait network, fonts, helpers ──▶ png
│ links /skill/<style>/style.css, imports /skill/diagram.ts, /skill/charts.ts
Styles
A style is only fonts and colors: <style>/style.css sets tokens and imports the shared
base.css. Layout rules live in claude-style/DESIGN.md and apply to every style.
| folder | look |
|---|---|
claude-style/ |
Anthropic engineering blog: light, warm gray, clay accent |
nextjs-style/ |
Next.js docs: dark, gray cards, blue, pink, purple, green, amber |
Switch style by changing one line in the html:
<link rel="stylesheet" href="/skill/nextjs-style/style.css" />
Start from the closest example in examples/ (they link claude-style):
| example | use for |
|---|---|
architecture-flow.html |
services, clients, data flow arrows |
changeset-shape.html |
struct / record card, storage tiers, derived data |
grouped-bar-chart.html |
benchmark comparisons, before vs after |
Workflow
Read
claude-style/DESIGN.md(and the chosen style's DESIGN.md). Copy the closest example into a new file with a unique name, next to where the user wants the image or in./tmp/<figure-name>/. Never edit files in this skill folder: other agents may render from it at the same time.Replace the content with the user's data. Keep
<div class="canvas" id="diagram">as root; it is exactly what gets exported. Use.section+.section-labelto split one figure into regions (e.g. metadata vs storage).Render, then read the png back and fix what looks wrong:
bun ~/.config/opencode/skills/diagrams-and-charts/render.ts render ./fig.html -o ./fig.png
Run render.ts render --help for options (--id, --scale, --width, --dark). Output type
comes from the extension: .png / .jpg (default scale 2: 2120px wide) or .svg.
SVG for websites and READMEs. -o fig.svg --dark nextjs-style writes one self-contained
svg (css and latin fonts embedded, ~80 to 160 KB) whose colors follow the viewer's light or dark
mode: the page's style in light mode, the colors of --dark <style> in dark mode (fonts stay).
Embed it as a plain markdown image : no <picture>, works on GitHub
and in Holocron. Write a full alt text that describes the flow, agents read it instead of
the image.
Writing figures
No coordinates. Place boxes in a
.flowgrid by cell (grid-area: row / col). Give boxes that arrows touch anid.Arrows from ids, after the markup:
<script type="module"> import { connect } from '/skill/diagram.ts' connect('dev', 'cache', { color: 'accent', label: 'Trees and file contents' }) connect('cache', 'server', { from: 'right', to: 'bottom', color: 'accent' }) connect('server', 'storage', { color: 'purple' }) </script>Sides are picked from the layout. Force them with
from/towhen the route crosses a box.Use the whole palette. Every style has
blueplus four category hues:pink,purple,green,amber. Give each category of boxes one hue (class="box green", also on.groupand.dashed) and color the wires of that category the same (color: 'green'; the label follows). Typical mapping: clients/users green, your system blue, cloud/third party purple, devices or targets pink, storage amber. Keepaccentfor the one highlighted path. Bar chart series takecolors: ['bar', 'green', ...].Charts from data:
barChart('#diagram', spec)fromcharts.ts, on an empty canvas. Format value labels yourself; values in one chart share one unit./skill/maps to this folder, other paths to the html folder..tsfiles are transpiled on the fly, so page scripts can be TypeScript too (<script type="module" src="./data.ts">).
Gotchas
- Concurrent renders are safe: each run uses its own port, tab and playwriter session. Only the html and output paths must be unique per figure.
- Needs Chrome with the playwriter extension connected. If connecting fails, ask the user to
click the extension icon. The CLI opens its own tab in a
diagramstab group and closes it. - Page errors (bad id in
connect, js exceptions) fail the render with the message. Read it. - The canvas is 1060px. Keep to about 5 columns and short sub lines; the CLI prints
warning: clippedwith the element names when something sticks out. Fix every warning. - A wire label is centered on its longest segment, so the gap between two boxes must be wider
than the label. Shorten the label or put it in
sub(second line) when a gap is narrow. - Two arrows leaving the same side of one box overlap. Spread them with
fromAt: 0.3andfromAt: 0.7, or start one from the enclosing group. - The svg is the rendered html inside a
<foreignObject>. Browsers render it (also in<img>); design tools (Figma, Inkscape) and some image converters do not. Use png there. - Code that draws asynchronously must register its promise with
track()fromdiagram.ts, otherwise the screenshot can happen before it finishes. - Use
.box.dashed(svg outline), not cssborder-style: dashed. Importdiagram.tson pages with dashed boxes even when there are no arrows. - Deps come from
~/.config/opencode/node_modules(goke, playwriter, zod, string-dedent). If imports fail, runbun add goke playwriter zod string-dedentin~/.config/opencode.
Adding a style
Create <name>-style/ with style.css (fonts @import, then @import url('../base.css'), then
the :root tokens; copy the token list from an existing style), a short DESIGN.md with the
palette table, and references/. Never hardcode colors in base.css, the ts helpers or html:
use tokens, so every figure works in every style.