spinagram logo

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"])
Web app on AWS, laid out automatically

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 →
The checkout styled with a stylesheet
"""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"])
Python26 linesexamples/tutorial/step5_style.py

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 →
Dark canvas with neon outlines and a glowing Orders service
"""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 d
Python30 linesexamples/looks/_platform.py

Dark 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 →
Anatomy of a Kubernetes workload on a dark canvas
"""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()
Python49 linesexamples/showcase/k8s_platform.py

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.

d.flowchart()d.state_machine()d.activity()a.to(b, "triggers")See how it works →
Architecture, a fraud-check flowchart and the payment state machine
"""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")
Python39 linesexamples/mixed/payments.py

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.

valid.yes(charge)created.on("pay", paid)act.parallel()See how it works →
Checkout flowchart with two decisions and a retry loop
"""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")
Python21 linesexamples/behavior/flowchart.py

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.

for az, tier in tiers.items():direction="down"route: orthoSee how it works →
Three-tier web application on AWS
"""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()
Python51 linesexamples/showcase/aws_three_tier.py

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.

icon="aws:rds"icon="cncf:cilium"icon="ai:claude"icon="logo.svg"See how it works →
Cards in six groups, each showing an icon and its pack:name reference
"""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)
Python26 linesexamples/icons/hybrid.py

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.