Diagrams as code, editable everywhere
Write a few lines of Python. Get SVG, PNG, PowerPoint with native shapes, and Figma nodes you can keep editing. spinagram does the layout.
pip install "spinagram[all]"from spinagram import Diagramd = Diagram("Q01", "Web app on AWS", subtitle="No coordinates, laid out automatically")users = d.box("Users", icon="aws:users")with d.group("us-east-1", style="aws-region", direction="right"): with d.group("VPC", style="aws-vpc", direction="right"): lb = d.box("Load balancer", icon="aws:alb") api = d.box("API", text="FastAPI on ECS", icon="terrastruct:python") with d.group(): # no style: a light "soft" zone db = d.box("Orders DB", text="PostgreSQL", icon="terrastruct:postgres") cache = d.box("Cache", text="Redis", icon="terrastruct:redis") s3 = d.box("Uploads", text="S3 bucket", icon="aws:s3")users.to(lb, "HTTPS") >> apiapi.to(db, "SQL")api >> [cache, s3]d.save("out/", formats=["svg", "png", "pptx"])After the quickstart
What you can do next
Every picture below is rendered by spinagram from a file in examples/. Switch between scenarios, then open the Python and CSS tabs to see the code behind each one.
Five steps
From boxes to a finished slide
Start with boxes and arrows. Add icons, then groups, then numbered steps and a legend. One stylesheet at the end adds your brand. Every step is a few more lines in the same file.
a >> bwith d.group(...)step="Order is stored"d.add_legend()d.stylesheet("brand.css")See how it works →"""Tutorial step 5: classes and a stylesheet give the diagram your brand."""
from spinagram import Diagram
d = Diagram("T05", "Checkout", subtitle="Step 5: style it with CSS")
d.stylesheet("checkout.css")
shoppers = d.box("Shoppers", icon="aws:users", class_="external")
with d.group("AWS eu-west-1", style="aws-region", direction="right"):
store = d.box("Storefront", text="CloudFront", icon="aws:cloudfront")
with d.group("VPC", style="aws-vpc", direction="right"):
orders = d.box("Orders API", text="FastAPI on ECS", icon="terrastruct:python", class_="critical")
with d.group("Data", gap=24):
db = d.box("Orders DB", text="PostgreSQL", icon="terrastruct:postgres")
cache = d.box("Cache", text="Redis", icon="terrastruct:redis")
queue = d.box("Events", text="SQS queue", icon="aws:sqs")
shoppers.to(store, "HTTPS") >> orders
orders.to(db, "SQL")
orders >> cache
orders.to(queue, "order.placed", class_="async")
d.add_legend({".external": "Outside our account", ".critical": "Revenue critical",
"edge.async": "Async event"}, position="bottom")
if __name__ == "__main__":
d.save("out/", formats=["svg", "png", "pptx"])Real CSS
One diagram, four looks
Classes name a role, never a colour. The stylesheet decides how each role looks, and the legend restyles itself. Swap one file to rebrand a whole deck. The Python stays the same.
box-shadow: 0 0 32px:root { --night: #070B16 }stroke-dasharraymarker-end: open-arrowSee how it works →"""The diagram every look in this folder restyles: the code only says what each element *is*."""
from spinagram import Diagram
def platform(id: str, title: str, sheet: str) -> Diagram:
d = Diagram(id, title, subtitle=f"The same diagram, styled by {sheet}",
layout={"engine": "layered", "direction": "right", "rank_gap": 64, "route": "ortho"})
d.stylesheet(sheet)
with d.group("Event-driven shop", id="canvas", class_="canvas", direction="right"):
with d.group("Clients", id="clients", class_="tier"):
web = d.box("Web app", text="React", icon="terrastruct:react", class_="client")
mobile = d.box("Mobile", text="iOS, Android", icon="selfhosted:android", class_="client")
gateway = d.box("API gateway", text="auth, rate limits", icon="aws:api-gateway", class_="edge")
with d.group("Services", id="services", class_="tier"):
users = d.box("Accounts", text="Go", icon="terrastruct:go", class_="service")
orders = d.box("Orders", text="Python", icon="terrastruct:python", class_="service hero")
search = d.box("Search", text="Elasticsearch", icon="selfhosted:elasticsearch", class_="service")
with d.group("Data", id="data", class_="tier"):
db = d.box("Orders DB", text="PostgreSQL", icon="terrastruct:postgres", class_="store")
bus = d.box("Event bus", text="Kafka on MSK", icon="aws:amazon-managed-streamingfor-apache-kafka", class_="stream")
web >> gateway
mobile >> gateway
gateway >> [orders, users, search]
orders.to(db, "SQL")
orders.to(bus, "order.placed", class_="async")
bus.to(search, "reindex", class_="async")
d.add_legend({".client": "Client", ".service": "Service", ".store": "Data store", ".stream": "Event stream",
".hero": "Revenue critical",
"edge.async": "Async event"}, position="bottom")
return dDark canvases
Architecture slides that look designed
Glass cards, glowing heroes and project colours are all CSS. One shared stylesheet styles the whole Kubernetes platform: Cilium yellow, Gateway API teal, add-ons violet.
group.cluster { fill: var(--night) }group.cluster node::titlebox-shadow: 0 8px 18pxSee how it works →"""A Kubernetes application platform: control plane, a namespaced web app with autoscaling and a
stateful database on persistent volumes."""
from spinagram import Diagram
WIDE = {"w": 230, "h": 84}
POD = {"w": 150, "h": 60}
d = Diagram("K8S01", "Anatomy of a Kubernetes workload", node_size=(210, 84),
subtitle="Ingress to pods, autoscaling, config and stateful storage in one namespace",
layout={"engine": "flex", "direction": "right", "gap": 32, "route": "ortho",
"area": [56, 176, 1808, 830]})
d.stylesheet("showcase.css")
d.stylesheet("k8s.css")
with d.group("Cluster · prod-eu-1", id="cluster", class_="cluster", direction="down", gap=28, padding=16):
with d.group("Control plane", id="cp", class_="cp", direction="right", gap=64):
api = d.box("API server", text="REST, admission", icon="k8s:api-labeled", class_="control-plane", **WIDE)
etcd = d.box("etcd", text="3 members, Raft", icon="k8s:etcd", class_="control-plane")
sched = d.box("Scheduler", text="binds pods", icon="k8s:sched-labeled", class_="control-plane")
cm = d.box("Controllers", text="reconcile", icon="k8s:c-m-labeled", class_="control-plane", **WIDE)
with d.group(id="workloads", class_="bare", direction="right", gap=16, align="center"):
with d.group("namespace: shop", id="ns", class_="ns", direction="right", gap=16, align="center"):
with d.group(id="entry", class_="bare", direction="down", gap=40):
ing = d.box("Ingress", text="from clients, TLS", icon="k8s:ing", class_="networking")
svc = d.box("Service", text="ClusterIP :80", icon="k8s:svc", class_="networking")
with d.group(id="control", class_="bare", direction="down", gap=40):
hpa = d.box("HPA", text="2–10 replicas", icon="k8s:hpa", class_="autoscaling")
dep = d.box("Deployment", text="rolling update", icon="k8s:deploy", class_="workload", **WIDE)
cfg = d.box("ConfigMap", text="+ Secret", icon="k8s:cm", class_="configuration")
with d.group("ReplicaSet", id="rs", class_="pods", direction="down", gap=12):
pods = [d.box(f"web-{s}", text="ready", icon="k8s:pod", class_="pod", id=f"pod-{s}", **POD)
for s in "abc"]
with d.group(id="data", class_="bare", direction="down", gap=64):
sts = d.box("StatefulSet", text="postgres ×3", icon="k8s:sts", class_="workload", **WIDE)
pvc = d.box("PVC", text="50 Gi, RWO", icon="k8s:pvc", class_="storage")
pv = d.box("PV", text="gp3 StorageClass", icon="k8s:pv", class_="storage")
ing >> svc
svc.to(dep, "selects")
for p in pods:
d.edge(dep, p)
hpa.to(dep, "scales", class_="control-loop")
cfg.to(dep, "env", class_="injects-config")
pods[0].to(sts, "SQL")
sts.to(pvc, "claims")
pvc.to(pv, "bound", class_="binds-volume")
api.to(etcd, "watch", arrow="both")
d.callout(pv, "Snapshots every 6 h to object storage", title="Backups", kind="tip")
d.add_legend()Typed regions
Several diagram types on one canvas
Put a flowchart, a state machine or swimlanes next to your architecture. Each region is laid out by its own engine. Ordinary arrows connect them.
"""Three diagram types on one slide: the payments architecture, the fraud-check flowchart its API runs
and the lifecycle of a payment. Each region has its own layout; arrows connect them like any boxes."""
from spinagram import Diagram
d = Diagram("M01", "Payments, end to end", subtitle="Architecture, flowchart and state machine on one slide",
layout={"engine": "flex", "direction": "right", "gap": 56})
d.stylesheet("mixed.css")
with d.flowchart("Fraud check", id="fraud", class_="flow") as fc:
start = fc.start()
score = fc.process("Score risk")
risky = fc.decision("Score > 80?")
review = fc.process("Manual review")
ok = fc.end("Approve")
start >> score >> risky
risky.yes(review) >> ok
risky.no(ok)
with d.group("Architecture", id="arch", class_="arch", direction="down"):
shop = d.box("Checkout", text="web and mobile", icon="terrastruct:react")
api = d.box("Payments API", text="Python", icon="terrastruct:python", class_="hero")
psp = d.box("Card network", text="Stripe", icon="selfhosted:stripe")
shop.to(api, "pay")
api.to(psp, "charge")
with d.state_machine("Payment", id="payment", class_="states") as sm:
pending = sm.state("Pending")
captured = sm.state("Captured")
refunded = sm.state("Refunded")
failed = sm.state("Failed")
sm.initial >> pending
pending.on("capture", captured)
pending.on("decline", failed)
captured.on("refund", refunded)
refunded >> sm.final
failed >> sm.final
api.to(score, "runs", class_="link")
ok.to(pending, "creates", class_="link")Behavior
Flowcharts, state machines and swimlanes
Builders that speak each diagram’s language: decisions with .yes() and .no(), transitions written the UML way, lanes with fork and join bars.
"""Flowchart: `spinagram render examples/behavior/flowchart.py -f svg,pptx -o out/`."""
from spinagram import Flowchart
fc = Flowchart("B01", "Checkout", subtitle="Flowchart builder, laid out by the layered engine",
layout={"engine": "layered", "direction": "right", "rank_gap": 48},
css="node::title { font-size: 21 }") # a touch smaller so all eight steps fit one slide
start = fc.start()
card = fc.io("Read card")
valid = fc.decision("Card valid?")
charge = fc.process("Charge card")
paid = fc.decision("Charged?")
receipt = fc.document("Send receipt")
error = fc.io("Show error")
start >> card >> valid
valid.yes(charge) >> paid
valid.no(error)
paid.yes(receipt) >> fc.end()
paid.no(error)
error.to(card, "retry")Automatic layout
Clouds and networks that nest themselves
Regions, VPCs, zones, VLANs and racks are with-blocks. Plain Python loops build the repeated parts. You never write a coordinate.
"""AWS three-tier web application, after the AWS reference architecture: an automatic legend from the CSS
classes and numbered steps along the request path."""
from spinagram import Diagram
N = {"w": 196, "h": 80} # compact cards: diagram, legend and steps share one slide
d = Diagram("AWS01", "Three-tier web application on AWS",
subtitle="Edge, web, app and data tiers across two Availability Zones",
layout={"engine": "flex", "direction": "right", "gap": 32, "route": "ortho",
"area": [56, 176, 1808, 830]})
d.stylesheet("showcase.css")
d.stylesheet("aws.css")
with d.group("Internet edge", id="edge", class_="edge", direction="down", gap=22, padding=16):
users = d.box("Users", text="web, mobile", icon="aws:users", class_="external", **N)
dns = d.box("Route 53", text="latency DNS", icon="aws:amazon-route53", class_="edge-service", **N)
cdn = d.box("CloudFront", text="CDN, TLS", icon="aws:cloudfront", class_="edge-service", w=216, h=80)
waf = d.box("AWS WAF", text="managed rules", icon="aws:awswaf", class_="security", **N)
s3 = d.box("S3", text="static origin", icon="aws:s3", class_="storage", **N)
with d.group("us-east-1", style="aws-region", padding=16):
with d.group("VPC 10.0.0.0/16", style="aws-vpc", direction="right", align="center", gap=12, padding=16):
alb = d.box("ALB", text="HTTPS :443", icon="aws:alb", class_="load-balancer", **N)
with d.group(id="azs", class_="bare", direction="down", gap=20, padding=0):
tiers = {}
for az, n in (("a", 0), ("b", 1)):
with d.group(f"Availability Zone {az.upper()}", style="aws-az", direction="right", gap=12, padding=16):
with d.group(f"Public 10.0.{n}.0/24", style="aws-subnet", class_="public", padding=14):
web = d.box("Web", text="Nginx · ASG", icon="aws:amazon-ec2", class_="web-tier",
id=f"web-{az}", **N)
with d.group(f"Private 10.0.1{n}.0/24", style="aws-subnet", class_="private", padding=14):
app = d.box("App", text="ECS Fargate", icon="aws:amazon-elastic-container-service",
class_="app-tier", id=f"app-{az}", **N)
with d.group(f"Data 10.0.2{n}.0/24", style="aws-subnet", class_="data", padding=14):
db = d.box("Aurora " + ("writer" if az == "a" else "reader"), text="PostgreSQL",
icon="aws:amazon-aurora", class_="database", id=f"db-{az}", **N)
tiers[az] = (web, app, db)
users.to(dns, "resolve", step="Users resolve the domain")
dns.to(cdn, "alias", step="Route 53 aliases to CloudFront")
cdn.to(waf, step="AWS WAF filters each request")
waf.to(alb, step="The ALB balances across zones")
for az, (web, app, db) in tiers.items():
alb >> web
if az == "a":
web.to(app, step="Web tier calls the app tier")
app.to(db, "SQL", step="App tier writes to Aurora")
else:
web >> app
app.to(db, "SQL")
d.edge(tiers["a"][2], tiers["b"][2], "replication", class_="replication")
d.add_legend()Icons
Thousands of icons, one argument
AWS, Google Cloud, Azure, Kubernetes, CNCF, AI, self-hosted, network gear and your own SVG files. Packs download on first use and then work offline.
"""Icons from a dozen packs and a local file. An icon is just `icon="pack:name"` or a path next to the spec;
packs download on first use. Find names with `spinagram icons search <word>`."""
from spinagram import Diagram
d = Diagram("I01", "One argument, thousands of icons", subtitle='icon="pack:name" or icon="file.svg"',
layout={"engine": "flex", "direction": "down", "gap": 24})
d.stylesheet("icons.css")
GROUPS = {
"Cloud": [("EKS", "aws:eks"), ("BigQuery", "gcp:bigquery"), ("AKS", "azure:aks"), ("Compute", "oci:vm")],
"Cloud native": [("Pod", "k8s:pod"), ("Cilium", "cncf:cilium"), ("Argo", "cncf:argo"),
("OpenTelemetry", "cncf:otel")],
"Languages and data": [("Rust", "devicon:rust"), ("Python", "terrastruct:python"),
("PostgreSQL", "devicon:postgresql"), ("Kafka", "devicon:kafka")],
"Products and AI": [("Terraform", "logos:terraform"), ("GitHub Actions", "logos:github-actions"),
("Datadog", "logos:datadog"), ("Claude", "ai:claude")],
"Self-hosted": [("Proxmox", "selfhosted:proxmox"), ("Grafana", "selfhosted:grafana"),
("Home Assistant", "selfhosted:hass"), ("OpenStack", "openstack:nova")],
"Network and yours": [("Firewall", "network:firewall"), ("L3 switch", "network:l3-switch"),
("Access point", "network:ap"), ("Your logo", "acme.svg")],
}
with d.group(id="grid", class_="bare", columns=3, gap=20, padding=0, node_size=(262, 78)):
for name, items in GROUPS.items():
with d.group(name, class_=name.lower().replace(" ", "-"), columns=2, gap=12, padding=14):
for title, icon in items:
d.box(title, text=icon, icon=icon)Editable everywhere
PowerPoint slides with native shapes and bound connectors, Figma and FigJam nodes, SVG and PNG.
Python first
users >> lb >> api. Groups are with-blocks, lists fan out, loops build the rest.
Automatic layout
No coordinates. Boxes fit their text, groups nest, four engines to choose from.
Thousands of icons
AWS, GCP, Azure, Kubernetes, CNCF projects, product logos, languages and more: twelve packs, downloaded on first use.
Your brand
One theme directory sets colours, fonts, styles and chrome for every output.
Built for review
Overflow checks, a live preview, and numbers that carry their sources.
Find the one closest to yours
Copy an example, run spinagram serve, and edit it with a live preview.