English | 简体中文 | Deutsch | Español | Français | Italiano | Português | Русский | Українська | 日本語 | 한국어 | Čeština | Polski | Română | Türkçe | Tiếng Việt | ไทย | Bahasa Indonesia
SVG chart generation for the AI era. One endpoint — /draw?type=... — with the data in the URL, machine-readable <metadata> embedded in every output, and SVG rendered directly by TypeScript. The style system mirrors a designer spec (13 SVGs), with multiple style variants per chart type.
All the data lives in the URL — open it in a browser, drop it into an <img>, paste it into your repo README: the SVG renders right away.
https://x-cmd.com/draw?type=sankey&t=pipeline&labels=feat,fix,docs,chore,merged,closed,rejected,0-1y,1-2y,2y+&links=feat>merged:38;feat>closed:12;feat>rejected:8;fix>merged:33;fix>closed:6;fix>rejected:2;docs>merged:16;docs>closed:4;chore>merged:5;chore>closed:6;chore>rejected:6;merged>0-1y:52;merged>1-2y:28;merged>2y+:12;closed>0-1y:14;closed>1-2y:9;closed>2y+:5;rejected>0-1y:9;rejected>1-2y:5;rejected>2y+:2&title=Pull%20requests%20lifecycle&unit=prs
Hbar — runtime throughput, measured (type=hbar; deno 7238 · node 6553 · bun 4972 ops/s, Apple Silicon)
https://x-cmd.com/draw?type=hbar&labels=deno,node,bun&values=7238,6553,4972&title=Runtime%20throughput&unit=ops%2Fs
https://x-cmd.com/draw?type=radar&t=evil&labels=startup,ecosystem,extensibility,memory,ease&series=vscode:60,95,90,55,80&series=neovim:95,65,85,95,35&series=zed:92,50,55,90,60&title=Editor%20comparison
https://x-cmd.com/draw?type=combo&labels=Mon,Tue,Wed,Thu,Fri,Sat,Sun&series=Downloads:120,180,150,210,240,200,260&line=Star%20growth:3.1,3.4,3.2,3.8,4.1,3.9,4.5&title=Weekly%20npm%20downloads&unit=dl
https://x-cmd.com/draw?type=hbar&t=xcmd-issue&labels=cli,core,docs,ci,ui&series=open:12,8,15,4,9&series=closed:38,52,20,11,26&title=Issues%20by%20area
https://x-cmd.com/draw?type=pie&t=market-mono&title=Languages&unit=%25&labels=TypeScript,Shell,JavaScript,Rust,Other&values=68,14,9,6,3
Every way below hits the same render core with the same parameters — a URL tested one way works every other way.
Zero install: point any web-connected agent at the skill by URL and ask in plain language —
Follow the draw skill at https://raw.githubusercontent.com/x-cmd/draw/main/SKILL.md: chart language stars as a pie — TypeScript 4821, Python 2312, Go 980
The skill carries the whole contract — chart-type selection by data shape, params, styles, the ECharts layer, short links, error recovery — so the agent answers with a URL:
https://x-cmd.com/draw?type=pie&labels=TypeScript,Python,Go&values=4821,2312,980&title=Language%20stars
Agents that load skills natively (Claude Code etc.): drop SKILL.md into your skills folder (~/.claude/skills/draw/SKILL.md) once, and the same prompt works without the URL line.
- Pie — stars by language
- Hbar — open/closed issues by area (butterfly)
- Radar — three-way editor comparison
Or browse the demo page with every type & style: https://x-cmd.com/draw
curl 'https://x-cmd.com/draw?type=bar&labels=Mon,Tue,Wed,Thu,Fri&values=12,18,9,22,15' > barchart.svg
curl 'https://x-cmd.com/draw?type=pie&labels=TS,Py,Go&values=4821,2312,980' -o pie.svg && open pie.svg
curl 'http://localhost:8787/draw?type=line&labels=Q1,Q2,Q3,Q4&values=8,14,23,37' > line.svg # your own service (way 4), same URLsOffline / no network: the same params ship as a binary (npm i -g @x-cmd/draw, or npx): stdout = SVG, stderr = diagnostics, so > and pipes just work.
draw --type pie --labels TS,Py,Go --values 4821,2312,980 > pie.svg
draw "?type=bar&labels=Mon,Tue&values=3,5" # bare query string, same params as the URL
draw echarts '{"series":[{"type":"treemap","data":[{"name":"a","value":3}]}]}' > treemap.svg # ECharts layer (also: file / stdin)git clone https://github.com/x-cmd/draw && cd draw
deno task serve # = deno run --allow-net --allow-env --sloppy-imports src/node/server.ts
# ONLY net+env granted; fs/subprocess/ffi stay denied — the render core needs
# none of them, so Deno's permission sandbox is the security boundary
npm run serve # same server on Node (http://localhost:8787/draw, demo page included)Environment variables: PORT (default 8787) · BASE_PATH · DATA_REPO/DATA_BRANCH/GITHUB_TOKEN (set these to enable POST short links; unset degrades gracefully, exactly like self-deployed workers).
Hand it to your agent (deploy skill in this repo):
Follow the deploy skill at https://raw.githubusercontent.com/x-cmd/draw/main/deploy-on-cloudflare.SKILL.md: deploy x-cmd/draw to my Cloudflare account, serving at charts.example.com/draw
Or run the script yourself:
bash scripts/setup.sh # inside the repo: wrangler login → deploy to workers.dev → smoke test
# or from anywhere:
curl -fsSL https://raw.githubusercontent.com/x-cmd/draw/main/scripts/setup.sh | bashThe script writes a workers.dev config (no routes/vars), deploys, and prints your URL. From then on: npx wrangler deploy to update; custom domain via dashboard → Workers → Domains & Routes (the path prefix auto-adapts — /draw subpath or root both work, no code change). Local workerd dev: npm run dev.
In-process library (npm / JSR) — the core renders synchronously with no network or files; params identical to the URL:
npm install @x-cmd/drawimport { draw, renderEchartsOption } from '@x-cmd/draw';
const { svg } = draw({ type: 'pie', labels: 'A,B', values: '3,2', t: 'evil', scheme: 'dark' });
draw('?type=bar&labels=Mon,Tue&values=3,5'); // query-string form
draw({ type: 'bar', labels: ['Q1, 2024', 'Q2'], values: [1, 2] }); // arrays = repeated params
const e = await renderEchartsOption({ series: [{ type: 'bar', data: [1, 2] }] }, new URLSearchParams());Wire it into any route:
app.get('/chart', (req, res) => res.type('image/svg+xml').send(draw(req.query).svg));Deno: import { draw } from "jsr:@x-cmd/draw";
Or mount the full HTTP layer from this repo (routing, short links, error handling — web-standard Request/Response, runs on Node/Deno/Bun/Workers):
import { handleRequest } from './src/handler.js';
const response = await handleRequest(request, env, {});GET /draw?type=<pie|bar|hbar|line|area|radar|gauge|scatter|combo|sankey|qr> — render a chart
GET /draw (no params) or ?tab=pie — the demo page (11 chart types, all style variants, dual URL forms)
POST /draw?type=... — submit a JSON/CSV body to create a permanent short link
GET /draw/c/<hash> — short-link render (immutable)
Common parameters:
tstyle family (per chart type, see below; falls back to default when not built for that type)schemeauto(default, follows system) /dark/lightvpin a version;w/hsize (200–1600 / 140–1200; qr is 120–1000 / 140–1000)numvalue labels:all/peak(highest only) /none. Defaults follow the style; applies to pie/bar/hbar/line/area/scatter/combo (radar/gauge/sankey/qr ignore it)subtitleeditorial subtitle (only rendered by area's editorial stylet=opendata),sourcedata-source footnote (area only, anyt); other types parse but ignore these
Per chart type, with automatic light/dark palettes:
| type | available families |
|---|---|
pie |
opendata (default, all labels outside), evil (rounded ring + inner values), evil-mono (solid pie + leader lines), market-mono (grayscale + ring total), progress (dotted progress ring), revenue (gradient wedges + dashed inner ring) |
bar |
evil (default, grouped), base (card), grid (table bars) |
hbar |
base (default), opendata (supports ref=/reflabel= reference line), xcmd (pixel bars, %), xcmd-issue (two-series butterfly), rank (two-series solid + hatching) |
line |
shadcn (default), evil, evil-glow (glow), evil-mono, opendata (line-end labels) |
area |
evil (default, two-series dashed areas), star (hand-drawn), opendata (dense gradient + editorial title), pixel, polka |
radar |
base (default), evil (multi-series), xcmd |
sankey |
base (default), fund, pipeline |
gauge |
base (default), gradient, reliability (zoned ring gauge) |
scatter / combo / qr |
base (combo also has evil) |
Flat params (URL stays readable — recommended):
/draw?type=pie&title=Stars by language&unit=stars&labels=TypeScript,Python,Go,Rust&values=4821,2312,980,645
/draw?type=bar&title=Weekly downloads&labels=Mon,Tue&values=1200,1800
When labels contain commas, use repeated params: labels=Q1, 2024&labels=Q2&values=1&values=2
Multi-series comparison (type=bar grouped/stacked, type=line, type=area, ≤6 series):
/draw?type=area&labels=Mon,Tue,Wed&series=Desktop:120,180,150&series=Mobile:80,90,85
JSON form (complex data): ?d= + URL-encoded JSON:
{"title":"Stars by language","unit":"stars","data":[{"label":"TypeScript","value":4821},{"label":"Python","value":2312}]}Multi-series JSON: {"labels":["Mon","Tue"],"series":[{"name":"draw","values":[120,180]},{"name":"qc","values":[80,90]}]}
QR code: /draw?type=qr&d=https://github.com/tzw&fg=%236366f1
Gauge: /draw?type=gauge&title=Build pass rate&value=87&max=100
Scatter: /draw?type=scatter&x=10,20,30&y=5,8,7
Combo: /draw?type=combo&labels=Mon,Tue&series=dl:120,180&line=growth:5,8 (dual y-axes kick in automatically when bars and lines differ by >5×)
POST /draw?type=pie body = chart spec JSON
POST /draw?type=bar&t=evil body = CSV (text/csv, header row)
→ 201 {"hash":"a3f9c2d4e5f60718","url":"https://x-cmd.com/draw/c/a3f9c2d4e5f60718"}
GET /draw/c/<hash> → SVG, cached immutable forever
- The fingerprint is the first 16 hex chars of SHA-256 over canonicalized data (template+theme+spec, keys recursively sorted); identical data → identical short link (dedup, idempotent). On read the fingerprint is re-verified — tampered archives are rejected
- Multi-series is supported for bar/line/area/radar, plus hbar (only
t=xcmd-issue/t=rank, exactly 2 series); other types reject multi-series bodies with 400. POST is rate-limited to 30/min/IP - Server-side config:
DATA_REPO(public repo,owner/name, in wrangler.jsonc) andGITHUB_TOKEN(secret; fine-grained PAT with Contents: write on that repo only — reads go through raw, tokenless). Data files live atcharts/<hash>.json - Visibility: the data repo is public and browsable — do not create short links with sensitive data (private storage is planned, as a paid feature)
- Compared to quickchart: their short links expire (6 months / days); ours never do
- No
v: renders the latest version,Cache-Control: max-age=300(output changes as the service upgrades) ?v=1: pins a version,Cache-Control: max-age=86400(1 day; rendering still evolves with deploys — not frozen forever).vaccepts 1..current; out of range → 400immutableis reserved for short links/c/<hash>(content-addressed, truly unchanging;vis ignored there)
Every SVG embeds <metadata> (escaped JSON): {generator, template, version, spec} — data and params round-trip directly. All text is real <text> elements.
Deploy your own draw to your Cloudflare account (free tier works; you just need a CF account):
curl -fsSL https://raw.githubusercontent.com/x-cmd/draw/main/scripts/setup.sh | bashThe script: clones the repo → browser login for wrangler → deploys → smoke test → prints your *.workers.dev URL. All GET rendering (11 chart types, every style) works out of the box.
- Short links (POST) are not enabled by default — calls return a "not configured" message. To enable: create a public data repo → set
DATA_REPO=owner/namein the local wrangler.jsonc vars →npx wrangler secret put GITHUB_TOKEN(fine-grained PAT, Contents: write on that repo only) → redeploy - Already cloned the repo? Run
npm run setupinside it (skips the clone) - Path prefix auto-adapts: both the workers.dev root and a
/drawsub-route work with zero code changes; or pin it explicitly with theBASE_PATHvar - Custom domain: dashboard → Workers → Settings → Domains & Routes, add a route (any path prefix)
- On Windows, run under WSL or Git Bash
npm install
npm run dev # wrangler dev, local server
npm test # vitest
npm run check # tsc --noEmit × 3 (worker / node / build)
npm run build # emit dist/ (npm library + CLI)
npm run deploy # wrangler deployCode is Apache-2.0. Bundled fonts keep their own licenses: Inter (OFL, full text in src/styles/inter-OFL.txt) and Handlee (OFL).