spinagram 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spinagram/__init__.py +11 -0
- spinagram/agent_skills/spinagram/SKILL.md +244 -0
- spinagram/agent_skills/spinagram-css/SKILL.md +155 -0
- spinagram/agent_skills/spinagram-css/examples/aws.css +46 -0
- spinagram/agent_skills/spinagram-css/examples/aws_network_hub.py +51 -0
- spinagram/agent_skills/spinagram-css/examples/aws_three_tier.py +53 -0
- spinagram/agent_skills/spinagram-css/examples/campus.css +19 -0
- spinagram/agent_skills/spinagram-css/examples/campus_network.py +45 -0
- spinagram/agent_skills/spinagram-css/examples/evpn_fabric.py +68 -0
- spinagram/agent_skills/spinagram-css/examples/fabric.css +27 -0
- spinagram/agent_skills/spinagram-css/examples/gcp.css +34 -0
- spinagram/agent_skills/spinagram-css/examples/gcp_global_app.py +49 -0
- spinagram/agent_skills/spinagram-css/examples/homelab.css +26 -0
- spinagram/agent_skills/spinagram-css/examples/homelab.py +49 -0
- spinagram/agent_skills/spinagram-css/examples/k8s.css +48 -0
- spinagram/agent_skills/spinagram-css/examples/k8s_networking.py +50 -0
- spinagram/agent_skills/spinagram-css/examples/k8s_platform.py +52 -0
- spinagram/agent_skills/spinagram-css/examples/notes.css +9 -0
- spinagram/agent_skills/spinagram-css/examples/notes.py +23 -0
- spinagram/agent_skills/spinagram-css/examples/showcase.css +17 -0
- spinagram/agent_skills/spinagram-css/references/recipes.css +72 -0
- spinagram/annotate.py +539 -0
- spinagram/api.py +81 -0
- spinagram/autolayout/__init__.py +442 -0
- spinagram/autolayout/engines/__init__.py +54 -0
- spinagram/autolayout/engines/d2.py +296 -0
- spinagram/autolayout/engines/dot.py +179 -0
- spinagram/autolayout/engines/elk.py +170 -0
- spinagram/autolayout/engines/elk_runner.mjs +35 -0
- spinagram/autolayout/engines/flex.py +97 -0
- spinagram/autolayout/engines/layered.py +679 -0
- spinagram/autolayout/graph.py +60 -0
- spinagram/autolayout/route.py +146 -0
- spinagram/autolayout/sizing.py +77 -0
- spinagram/backends/__init__.py +48 -0
- spinagram/backends/figma.py +589 -0
- spinagram/backends/png.py +40 -0
- spinagram/backends/pptx.py +555 -0
- spinagram/backends/svg.py +257 -0
- spinagram/behavior.py +311 -0
- spinagram/cli.py +344 -0
- spinagram/css.py +706 -0
- spinagram/deck.py +241 -0
- spinagram/facts.py +195 -0
- spinagram/icon_packs/aws.yaml +43 -0
- spinagram/icon_packs/azure.yaml +12 -0
- spinagram/icon_packs/gcp.yaml +11 -0
- spinagram/icon_packs/k8s.yaml +9 -0
- spinagram/icon_packs/network.yaml +30 -0
- spinagram/icon_packs/oci.yaml +9 -0
- spinagram/icon_packs/openstack.yaml +8 -0
- spinagram/icon_packs/selfhosted.yaml +28 -0
- spinagram/icon_packs/terrastruct.yaml +84 -0
- spinagram/icons.py +438 -0
- spinagram/inspect_fmt.py +193 -0
- spinagram/layout.py +501 -0
- spinagram/metadata.py +66 -0
- spinagram/model.py +604 -0
- spinagram/qa.py +73 -0
- spinagram/scene.py +239 -0
- spinagram/shapes.py +248 -0
- spinagram/skills.py +99 -0
- spinagram/text.py +83 -0
- spinagram/theme.py +291 -0
- spinagram/themes/default/theme.yaml +114 -0
- spinagram/ui/__init__.py +5 -0
- spinagram/ui/catalog.py +118 -0
- spinagram/ui/events.py +48 -0
- spinagram/ui/server.py +271 -0
- spinagram/ui/static/assets/index-BDoYKVM5.css +2 -0
- spinagram/ui/static/assets/index-DAg_fACF.js +11 -0
- spinagram/ui/static/index.html +13 -0
- spinagram/ui/themes.py +152 -0
- spinagram/ui/workspace.py +340 -0
- spinagram/vectorize.py +158 -0
- spinagram/yaml_io.py +322 -0
- spinagram-0.1.0.dist-info/METADATA +225 -0
- spinagram-0.1.0.dist-info/RECORD +81 -0
- spinagram-0.1.0.dist-info/WHEEL +4 -0
- spinagram-0.1.0.dist-info/entry_points.txt +9 -0
- spinagram-0.1.0.dist-info/licenses/LICENSE +21 -0
spinagram/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""spinagram — diagrams as code, rendered editable to SVG, PNG, PowerPoint and Figma."""
|
|
2
|
+
from .api import render, scenes
|
|
3
|
+
from .behavior import Activity, Flowchart, StateMachine
|
|
4
|
+
from .layout import build_scene
|
|
5
|
+
from .model import Diagram, Edge, Legend, Node, Table, Zone
|
|
6
|
+
from .theme import Theme
|
|
7
|
+
from .yaml_io import load
|
|
8
|
+
|
|
9
|
+
__all__ = ["Activity", "Diagram", "Flowchart", "StateMachine", "Edge", "Legend", "Node", "Table", "Zone", "Theme", "build_scene", "load", "render",
|
|
10
|
+
"scenes"]
|
|
11
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: spinagram
|
|
3
|
+
description: Author professional diagrams as code with spinagram (YAML or Python) and deliver them as SVG, PNG, editable PowerPoint and editable Figma. Use when asked to create, update or export flowcharts, decision trees, process/approval flows, swimlanes, today-vs-target comparisons, org/structure charts, matrices or tables-as-diagrams, especially for slide decks, studies or Figma. Covers layout grid, text fitting, edge routing, themes, icons, QA, and the Figma hand-off (through a Figma MCP server when the agent has one). For CSS styling use the spinagram-css skill. Not for data charts (bar/line) or auto-laid-out graphs of hundreds of nodes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# spinagram: diagrams as code with editable outputs
|
|
7
|
+
|
|
8
|
+
You write a **spec** (YAML or Python). A **theme** directory supplies the brand. spinagram resolves both into one scene and renders it through backends: `svg`, `png`, `pptx`, `figma`.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
spec (YAML/.py) + theme dir ──► layout (fonts, wrap, anchors) ──► svg · png · pptx · figma
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Workflow
|
|
15
|
+
|
|
16
|
+
1. **Message first.** State the single question the diagram answers. That becomes the `subtitle`, a full sentence with the conclusion. The `title` names the topic in 2–6 words.
|
|
17
|
+
2. **Pick a pattern** (table below) and sketch the grid on paper: columns, rows, gaps.
|
|
18
|
+
3. **Write the spec.** Prefer Python: `a >> b` arrows, `with d.group(...)` zones, loops for repetition, and automatic layout when no node has coordinates. Use YAML when another tool generates the diagram.
|
|
19
|
+
4. **`spinagram check`** must report 0 issues (text overflow).
|
|
20
|
+
5. **Render PNG and look at it.** Check overlaps, arrows crossing text, label pills on borders, and empty zones. Fix the spec, never the output.
|
|
21
|
+
6. **Deliver.** Use `-f svg,png,pptx` for decks. Add `figma` and follow the Figma hand-off below.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
spinagram render specs/ -t <theme-dir> -f svg,png,pptx,figma -o out/
|
|
25
|
+
spinagram check specs/ -t <theme-dir>
|
|
26
|
+
spinagram render specs/ --only D04 -f png # iterate on one
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Patterns
|
|
30
|
+
|
|
31
|
+
| Need | Pattern | Building blocks |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| Linear process | Row of 4–6 boxes and `chain` | `row()`, `chain()` |
|
|
34
|
+
| Decision | Diamonds with `yes`/`no` labels; branches leave from different sides | `kind: diamond`, side anchors |
|
|
35
|
+
| Today vs target | Two zones, paired cards, one arrow per pair | `zones`, mirrored `x` |
|
|
36
|
+
| Process + per-context detail | Row on top, table below | `tables` with an `icons` column |
|
|
37
|
+
| Lifecycle / loop | Clockwise ring; the dashed return arrow carries the loop condition | `dashed: true`, `via` |
|
|
38
|
+
| Org / governance | Tree top-down: global body on top, one row per level | `column()` per branch, `"a.bottom -> b.top"` |
|
|
39
|
+
| Approval scenario | Request → engine → policy box → parallel approvers → outcomes | `style` = semantic state |
|
|
40
|
+
|
|
41
|
+
## Diagram or UI mockup?
|
|
42
|
+
|
|
43
|
+
spinagram draws **diagrams**. For **product UI** such as an e-mail, a chat, a card or a screen, a diagram looks fake. Use an HTML page instead:
|
|
44
|
+
|
|
45
|
+
1. Build it with Tailwind at the slide size (1920×1080). Use the same header and footer as the diagram slides, and the theme's tokens, fonts and icons.
|
|
46
|
+
2. Screenshot it at 2x with Playwright.
|
|
47
|
+
3. Add the screenshot to the deck as a full-bleed picture slide.
|
|
48
|
+
|
|
49
|
+
Keep the real, implementable template (an e-mail HTML with inline CSS, or an Adaptive Card) next to it, so the mockup is never the only artifact.
|
|
50
|
+
|
|
51
|
+
## Layout rules
|
|
52
|
+
|
|
53
|
+
- **Canvas** is 1920×1080 with an 80 px side margin. Content goes in `y` 170–1000, between the header and the footer rule.
|
|
54
|
+
- **Use a grid.** Gaps between boxes are 28–40 in dense rows, 60–100 between steps and 40 between rows. Equal sizes inside a row.
|
|
55
|
+
- **Size boxes for text.** Allow at most 3 body lines. If `check` reports `overflow-y`, enlarge the box or cut text. Do not shrink the font below the theme default.
|
|
56
|
+
- **Icons** scale with box height (theme `icon_scale`, `icon_max`). Use `icon_size` only to make one emphasis node larger. Every box in a row has an icon, or none does.
|
|
57
|
+
- **One reading direction:** left→right for flows, top→bottom for hierarchies. Return arrows go dashed.
|
|
58
|
+
- **Layered landscape:** put the layer name in a left column (free `label`) and shift the boxes right. A zone label at the top collides with vertical arrows between layers.
|
|
59
|
+
- **Short vertical gaps:** put the edge label beside the line (`label_at` offset in x), not on it. Otherwise the pill hides the arrowhead.
|
|
60
|
+
- **Don't let tall boxes float text:** a 590 px box holding 8 lines reads as empty. Split the content into several boxes (one per source or route), or shrink the box to fit its text.
|
|
61
|
+
- **Leave white space.** If a zone is half empty, shrink it. Do not stretch cards to fill it.
|
|
62
|
+
|
|
63
|
+
## Text rules
|
|
64
|
+
|
|
65
|
+
- **One idea per box.** Titles have 1–4 words. The body is a short phrase, not a sentence list.
|
|
66
|
+
- **Numbers** in a diagram must come from the study/report sources. Put the source in `source:` (footer).
|
|
67
|
+
- **Same vocabulary across all diagrams in a set:** one term per concept.
|
|
68
|
+
- Edge labels are 1–2 words (`yes`, `no`, `affects`, `rejected`).
|
|
69
|
+
|
|
70
|
+
## Edges
|
|
71
|
+
|
|
72
|
+
- Prefer `"a -> b"` (sides auto-picked) or `"a.right -> b.left"`. Use `@0.3` to spread several edges leaving one side.
|
|
73
|
+
- Side-centre anchors become **bound connectors in PowerPoint**: moving a box drags the arrow. Off-centre (`@frac`), `via` and `curve` edges are drawn exactly but stay unbound.
|
|
74
|
+
- Route around boxes with `via: [[x, y], ...]`. Do not cross text. Place labels with `label_at` when the midpoint lands on a box.
|
|
75
|
+
- Colours are semantic: default = normal flow, alert/orange = risk path, gray = alternative or neutral.
|
|
76
|
+
|
|
77
|
+
## Styles (semantic, theme-mapped)
|
|
78
|
+
|
|
79
|
+
| Style | Use |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `default` | Normal step |
|
|
82
|
+
| `dark` | Engine / central decision maker (one or two per diagram) |
|
|
83
|
+
| `soft` | Background grouping, decision diamonds |
|
|
84
|
+
| `warning` | Needs review / attention |
|
|
85
|
+
| `alert` | Risk, problem, blocking body (review board, incident) |
|
|
86
|
+
| `neutral` | Outcome or side note |
|
|
87
|
+
|
|
88
|
+
## Numbers are facts
|
|
89
|
+
|
|
90
|
+
Never type a number from data into a spec. Write `{{fact_id}}` and declare `facts:` (plus `locale:`) in `_defaults.yaml`. Each fact must carry its `query` or `formula` and its `computed_at`.
|
|
91
|
+
|
|
92
|
+
- Forecasts are derived facts whose formula states the assumption, e.g. `0.3*critical_changes`.
|
|
93
|
+
- `spinagram facts` lists every fact used; the generated `fontes.md` (sources) is the reviewer's document.
|
|
94
|
+
- Use `|raw` to get a plain number for CSS widths in HTML bar charts.
|
|
95
|
+
|
|
96
|
+
## Story decks
|
|
97
|
+
|
|
98
|
+
**HTML deck** (`spinagram deck deck.yaml -t <theme> [-o out.html]`): one self-contained page — cover
|
|
99
|
+
(title, subtitle, date, authors with photo and role), a full-colour divider per section, then one slide
|
|
100
|
+
per diagram (title band + one-line message + the diagram at full width). Fonts, logos, photos and
|
|
101
|
+
diagrams are embedded; printing gives one slide per page. `deck.yaml` lists `specs:` (dirs, relative to
|
|
102
|
+
the file), `authors:` and `sections: [{title, subtitle, slides: [{diagram: <id>, title, message}]}]`;
|
|
103
|
+
`image: svg` (vector, default) or `png` (smaller with many fonts). Colours and logos come from the
|
|
104
|
+
theme's `deck:` block (`accent`, `strong`, `deep`, `page`, `logo`, `logo_negative`).
|
|
105
|
+
|
|
106
|
+
For PowerPoint decks:
|
|
107
|
+
|
|
108
|
+
When the diagrams form a presentation:
|
|
109
|
+
|
|
110
|
+
- Keep a `deck.yaml` with sections, slide order and a `talk` line per slide; it becomes the speaker notes.
|
|
111
|
+
- Open with a cover and a roadmap. Close with a case exercise, the benefits and the asks.
|
|
112
|
+
- Slides not listed go to an appendix.
|
|
113
|
+
|
|
114
|
+
## YAML pitfalls
|
|
115
|
+
|
|
116
|
+
- **Quote values in flow mappings** (`{...}`) that contain `,`, `:`, `?` or `#`. For example, `text: "a, b, c"` or `title: ["Any", "trigger?"]`. Unquoted commas silently split the value; the loader now rejects the resulting unknown keys.
|
|
117
|
+
- `yes`, `no`, `on` and `off` are booleans in YAML; never use them unquoted as ids (the loader rejects them).
|
|
118
|
+
- `at: [x, y, w, h]` is shorthand for the four keys.
|
|
119
|
+
- A `_defaults.yaml` in the specs folder supplies keys every diagram shares (`kicker`, `source`).
|
|
120
|
+
|
|
121
|
+
## Live preview
|
|
122
|
+
|
|
123
|
+
While iterating, run `spinagram serve <specs> --theme <theme>` (extra `ui`): the browser re-renders on every
|
|
124
|
+
save, shows load/render errors inline, the theme token sheet (colour token names, styles) and the icon packs
|
|
125
|
+
(search names, fetch missing packs). Use it to pick tokens and icon names instead of guessing.
|
|
126
|
+
|
|
127
|
+
## Icon packs
|
|
128
|
+
|
|
129
|
+
Use `icon: "<pack>:<name>"` for vendor icons: `aws`, `gcp`, `azure`, `k8s`, `oci`, `openstack`, `terrastruct`,
|
|
130
|
+
`selfhosted` (Proxmox, OPNsense, VMware ESXi, TrueNAS, UniFi… for homelabs and private clouds) and `network`
|
|
131
|
+
(router, switch, firewall… as `network:router`, `-green`, `-circle`, `-plain`) are built in
|
|
132
|
+
and download on first use (`spinagram icons fetch`). Find names with `spinagram icons search <words>`;
|
|
133
|
+
short aliases exist for common services (`aws:s3`, `aws:alb`, `aws:kms`, `gcp:gke`, `azure:aks`, `k8s:pod`).
|
|
134
|
+
For general software architecture (languages, databases, caches, queues) use `terrastruct`:
|
|
135
|
+
`terrastruct:java|python|go|nodejs|react`, `terrastruct:postgres|mysql|mongo|redis|oracle`,
|
|
136
|
+
`terrastruct:kafka|sqs|sns|service-bus|pubsub`, `terrastruct:tech-server`, `terrastruct:db`. It has no
|
|
137
|
+
native Kafka/RabbitMQ logo: `kafka` maps to the AWS MSK icon.
|
|
138
|
+
Register a brand or extra set with `spinagram icons add <name> --dir|--zip|--npm|--github …`
|
|
139
|
+
(`--project` writes `./spinagram-icons.yaml`, shareable with the repo). Prefer SVG packs; PNG packs
|
|
140
|
+
(oci, openstack) carry a dark variant used automatically on dark fills.
|
|
141
|
+
|
|
142
|
+
**PNG → SVG:** with the `vector` extra, PNG icons (theme, spec paths, packs) become traced SVG when a
|
|
143
|
+
fidelity gate passes — crisper output and editable vectors in Figma (no PNG uploads). Check a set with
|
|
144
|
+
`spinagram vectorize <dir>`; disable with `SPINAGRAM_VECTORIZE=0` or `icons: {vectorize: false}`.
|
|
145
|
+
|
|
146
|
+
## Automatic layout (no coordinates)
|
|
147
|
+
|
|
148
|
+
For architecture views (Region › VPC › AZ) and flows, prefer `layout:` and `groups:` over hand-placed
|
|
149
|
+
coordinates. spinagram sizes and places everything, and each group is drawn as a zone.
|
|
150
|
+
|
|
151
|
+
- **Engine:**
|
|
152
|
+
- `dot` for ranked flows and AWS views; it gives clean orthogonal routes.
|
|
153
|
+
- `elk` for many edges crossing group borders.
|
|
154
|
+
- `flex` (no dependencies) for grids of cards; add `route: ortho` for elbowed edges.
|
|
155
|
+
- `d2` when you need D2's layouts.
|
|
156
|
+
- **Compare engines** before choosing: `spinagram render spec.yaml --layout dot|elk|flex|d2`.
|
|
157
|
+
- **Group hints for flex:** `direction` (row|column), `columns`, `gap`, `padding`, `align`. Sibling groups stretch to equal size.
|
|
158
|
+
- **Leave edges unanchored** (`a -> b`) so the engine routes them. An anchor (`a.right`) or `via` opts that edge out.
|
|
159
|
+
- **Mixed slides:** a legend or note with explicit `at:` stays where you put it; the auto content fills `layout.area`.
|
|
160
|
+
- **Hand-tuning:** `spinagram layout spec.yaml -o frozen.yaml` freezes the result; edit the frozen spec.
|
|
161
|
+
- `check` reports `layout: overflow` when the content does not fit. Change the direction, split the diagram or shorten titles.
|
|
162
|
+
|
|
163
|
+
## Groups with outlines (AWS-style)
|
|
164
|
+
|
|
165
|
+
Zone styles may declare `stroke`, `dashed`, `icon`, `radius`, `upper` and `fill: none`, which draws
|
|
166
|
+
cloud-provider groups the way AWS Architecture Icons do: an outline, no fill, the group icon flush in
|
|
167
|
+
the top-left corner and the label beside it (`d.zone("AZ 1a", ..., color="aws-az")`). Nest them
|
|
168
|
+
Account › Region › VPC › AZ; regional services (S3, KMS) sit inside the Region but outside the VPC.
|
|
169
|
+
Keep AZ labels short (`AZ 1a`) so arrows entering the boxes below do not cross them.
|
|
170
|
+
|
|
171
|
+
## Python DSL
|
|
172
|
+
|
|
173
|
+
Automatic layout (no coordinates anywhere: the diagram is laid out with `flex`):
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from spinagram import Diagram
|
|
177
|
+
d = Diagram("Q01", "Web app on AWS")
|
|
178
|
+
users = d.box("Users", icon="aws:users")
|
|
179
|
+
with d.group("VPC", style="aws-vpc", direction="right"):
|
|
180
|
+
api = d.box("API", text="FastAPI", icon="terrastruct:python")
|
|
181
|
+
db, cache = d.box("Orders DB", icon="terrastruct:postgres"), d.box("Cache", icon="terrastruct:redis")
|
|
182
|
+
users.to(api, "HTTPS") >> [db, cache] # a >> b, a << b, a - b (no head); lists fan out/in
|
|
183
|
+
diagram = d
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Fixed coordinates:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from spinagram import Diagram
|
|
190
|
+
d = Diagram("D01", "Release flow", subtitle="Evidence decides the path")
|
|
191
|
+
a, b, c = d.row(["Build", "Stress & chaos", "Risk gate"], x=80, y=220, w=440, h=220, gap=100,
|
|
192
|
+
styles=[None, None, "dark"])
|
|
193
|
+
d.chain(a, b, c)
|
|
194
|
+
d.edge(c.bottom, d.box("Deploy", x=700, y=560, w=500, h=150).top, label="green")
|
|
195
|
+
diagram = d # the CLI picks up `diagram`, `diagrams` or `build()`
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Annotate with `d.add_legend()`, `a.to(b, step="…")`, `d.callout(node, "…", kind=...)` and
|
|
199
|
+
`d.note(..., variant="card"|"quote")`; see the spinagram-css skill.
|
|
200
|
+
|
|
201
|
+
Give diagrams a `category=` and `tags=` (YAML `category:`, `tags:`) so `spinagram serve` groups them and
|
|
202
|
+
`--tag`/`--category` filter them; vendor icons already add `aws`, `gcp`, `kubernetes`… tags.
|
|
203
|
+
|
|
204
|
+
Helpers next to the spec are importable (`from _layout import spread`); files starting with `_`
|
|
205
|
+
are not loaded as specs.
|
|
206
|
+
|
|
207
|
+
## Outputs
|
|
208
|
+
|
|
209
|
+
- **PowerPoint:** one slide per diagram, with native shapes and the text inside each shape. Tables are native, and the diagram `source` goes into the speaker notes. Viewers need the theme fonts installed.
|
|
210
|
+
- **Figma:**
|
|
211
|
+
- **Scripts:** `out/figma/<slug>-NN.js` (run in order) and `<slug>.assets.json`. They run through the
|
|
212
|
+
Figma MCP server's `use_figma` tool. Without that server, hand the scripts to the user.
|
|
213
|
+
1. If the agent has Figma's `figma-use` skill, load it.
|
|
214
|
+
2. Run each script with `use_figma` (`fileKey` from the URL).
|
|
215
|
+
- **Container:** the diagram becomes a frame (Design file), a slide (Slides file) or a section (FigJam board, `figma.com/board/...`) named `spinagram:<id>`. Re-running it rebuilds the content in place.
|
|
216
|
+
- **FigJam:** edges whose both ends sit on box sides become native connectors attached to the boxes (they follow when a box moves); free edges stay vectors. Load the `figma-use-figjam` skill too, when available.
|
|
217
|
+
- **SVG icons** are rebuilt as editable vectors inside the script (`createNodeFromSvg`) — no upload. Only raster icons (PNG) need `upload_assets`.
|
|
218
|
+
- **FigJam in one step:** `-f figjam` writes library-mode scripts, `figma/icons/` (every icon used),
|
|
219
|
+
`icon-library.js` (finds the uploaded layers by file name) and a `README.md` with the order of calls.
|
|
220
|
+
- **Icon library (recommended for many diagrams):** upload every distinct icon once with `upload_assets` (multipart, the file name becomes the layer name), run `icon_library_js({file: nodeId})` to rename them `dk-icon:<file>` inside a section `spinagram:icons`, then generate scripts with `FigmaBackend(library=True)` / `to_figma_js(scene, library=True)`: icons are cloned from the library — exact bytes, much smaller scripts (≈30 KB instead of 45 KB+ with inline SVG).
|
|
221
|
+
- **FigJam gotchas:** connector endpoints use `magnet` (TOP/BOTTOM/LEFT/RIGHT) for side centres; `position` is in **pixels** from the node's top-left, not 0–1. Elbow routes are auto-computed — if one crosses a box, re-anchor that end to another side. `setPluginData` is not available in `use_figma` (placeholders carry ` · asset:<file>` in the layer name instead).
|
|
222
|
+
- **Images:**
|
|
223
|
+
- The script returns `imageTargets`.
|
|
224
|
+
- Call `upload_assets(fileKey, count=n, nodeIds=[...])`.
|
|
225
|
+
- POST each file listed in `assets.json` to its URL.
|
|
226
|
+
- **Edges (Design/Slides):** drawn as vectors, because the connector node is FigJam-only.
|
|
227
|
+
- **SVG** is self-contained (fonts and images embedded). **PNG** is rendered at 2x (3840×2160) for slides.
|
|
228
|
+
|
|
229
|
+
## CSS styling
|
|
230
|
+
|
|
231
|
+
Stylesheets restyle any diagram without touching its code: `d.stylesheet("brand.css")`, `class_="critical"`,
|
|
232
|
+
`edge.async { stroke-dasharray: 6 4 }`. For anything beyond a rule or two, load the **spinagram-css**
|
|
233
|
+
skill: selectors, properties, recipes, layout hints for dense architecture slides and the debugging loop.
|
|
234
|
+
|
|
235
|
+
## Themes
|
|
236
|
+
|
|
237
|
+
Use `extends: default` and override `colors` (tokens may alias tokens), `fonts` (with files, for metrics and embedding), `icons` (`dir` + `pattern` `{name}-{variant}.png`, variants per background tone), `styles.*` and `chrome` (bar, title, kicker template, footer logo). Keep brand themes **outside** the engine repository.
|
|
238
|
+
|
|
239
|
+
## Done when
|
|
240
|
+
|
|
241
|
+
- `check` reports 0 issues.
|
|
242
|
+
- The PNG has been reviewed: no overlap, no clipped text, and arrows do not cross labels.
|
|
243
|
+
- Requested formats are written. PPTX opens.
|
|
244
|
+
- The Figma scripts ran, or the hand-off steps were given to the user.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: spinagram-css
|
|
3
|
+
description: Style spinagram diagrams with CSS (selectors, cascade, custom properties, shadows, dashes, markers) and lay out dense cloud architecture slides (AWS, Google Cloud, Azure, Kubernetes) that fit one 1920×1080 canvas. Use when asked to make spinagram diagrams look polished or on-brand, write or debug a stylesheet, build a dark or vendor-coloured architecture diagram, or explain why an element looks the way it does. Includes recipes, worked examples and layout hints. Requires the spinagram CLI.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Style spinagram diagrams with CSS
|
|
7
|
+
|
|
8
|
+
A stylesheet restyles a diagram without touching its code. The same rules render in SVG, PNG,
|
|
9
|
+
PowerPoint and Figma. This skill covers the CSS subset, the recipes that look good, and the layout
|
|
10
|
+
habits that make dense architecture diagrams fit one slide. For general spinagram authoring, see
|
|
11
|
+
the `spinagram` skill.
|
|
12
|
+
|
|
13
|
+
Files next to this one:
|
|
14
|
+
|
|
15
|
+
- `references/recipes.css`: tested rules to copy (base cards, dark canvas, tiers, edge semantics,
|
|
16
|
+
vendor accents).
|
|
17
|
+
- `examples/`: six complete diagrams with their stylesheets: AWS three-tier and hub-and-spoke networking
|
|
18
|
+
(legends, numbered steps), Kubernetes workload and service networking and a Google Cloud global app
|
|
19
|
+
(legends, callouts), and an architecture-decisions slide of designed notes.
|
|
20
|
+
Render them with `spinagram render examples/ -f png -o out/`.
|
|
21
|
+
|
|
22
|
+
## Workflow
|
|
23
|
+
|
|
24
|
+
1. **Structure first, style second.** Write the diagram with groups and edges; give elements
|
|
25
|
+
semantic classes (`class_="security"`, `class_="async"`), never colours.
|
|
26
|
+
2. **One base sheet plus one accent sheet.** The base sets cards, type and edges for every diagram
|
|
27
|
+
in the set. The accent sheet adds the vendor or brand palette. Attach both in cascade order:
|
|
28
|
+
`d.stylesheet("base.css")` then `d.stylesheet("aws.css")`.
|
|
29
|
+
3. **`spinagram check <spec>`** must report 0 issues. `info: unused selector` lines are hints,
|
|
30
|
+
not failures.
|
|
31
|
+
4. **Render a PNG and look at it** if you can view images; otherwise ask the user to. Check that
|
|
32
|
+
edges don't cross boxes, labels don't cover text and nothing is clipped.
|
|
33
|
+
5. **Explain a value** with `spinagram check --styles <spec> --select "#api"`, or press **Inspect**
|
|
34
|
+
in `spinagram serve <specs>`, which reloads on every save of a spec or stylesheet.
|
|
35
|
+
|
|
36
|
+
## Attaching styles
|
|
37
|
+
|
|
38
|
+
| Where | Python | YAML |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| Diagram | `d.stylesheet("brand.css")`, `Diagram(..., css="...")` | `stylesheets: [brand.css]`, `css:` |
|
|
41
|
+
| Element | `class_="critical"`, `css="stroke-width: 6"` | `class:`, `css:` |
|
|
42
|
+
| Folder | — | `stylesheets:` in `_defaults.yaml` |
|
|
43
|
+
| Theme | `theme.css` next to `theme.yaml` | same |
|
|
44
|
+
|
|
45
|
+
Paths are relative to the spec. Later sheets win at equal specificity; inline `css=` beats every
|
|
46
|
+
sheet rule and `!important` beats inline. Classes work on nodes, edges and groups
|
|
47
|
+
(`d.group("VPC", style="aws-vpc", class_="private")` keeps the AWS outline and icon and adds the class).
|
|
48
|
+
|
|
49
|
+
## Selectors
|
|
50
|
+
|
|
51
|
+
| Selector | Matches |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `node`, `edge`, `group`, `region`, `lane`, `diagram` | elements by type |
|
|
54
|
+
| `box`, `data`, `state`, `decision`, … | nodes of that kind |
|
|
55
|
+
| `.critical`, `#api`, `*` | class, id, anything |
|
|
56
|
+
| `[icon]`, `[kind=data]`, `edge[from=api]`, `edge[to=db]`, `[label="yes"]` | attributes |
|
|
57
|
+
| `group.vpc node`, `region > state` | descendant, child |
|
|
58
|
+
| `:first-child`, `:last-child`, `:not(.x)`, `:root` | pseudo-classes |
|
|
59
|
+
| `node::title`, `node::body`, `edge::label`, `group::label` | text parts |
|
|
60
|
+
|
|
61
|
+
Node ids come from titles (`"Orders DB"` → `#orders-db`) unless you pass `id=`.
|
|
62
|
+
|
|
63
|
+
## Properties
|
|
64
|
+
|
|
65
|
+
| Applies to | Properties |
|
|
66
|
+
|---|---|
|
|
67
|
+
| Nodes, groups | `fill`, `fill-opacity`, `stroke`, `stroke-width`, `stroke-dasharray`, `border-radius`, `opacity`, `box-shadow` (one: `x y blur colour`) |
|
|
68
|
+
| Text (`::title`, `::body`, `::label`) | `color`, `font-size`, `font-weight`, `font-style`; inherited from groups |
|
|
69
|
+
| Edges | `stroke`, `stroke-width`, `stroke-dasharray`, `opacity`, `marker-start`, `marker-end` (`none`, `arrow`, `open-arrow`, `diamond`, `open-diamond`, `circle`, `bar`) |
|
|
70
|
+
|
|
71
|
+
Colours: hex, `rgb()`/`rgba()`, CSS names, theme tokens (`fill: primary-soft`) and `var(--x)`, which
|
|
72
|
+
falls back to the theme token `x`. Lengths are canvas pixels (`4` or `4px`).
|
|
73
|
+
|
|
74
|
+
## Recipes
|
|
75
|
+
|
|
76
|
+
All are in `references/recipes.css`.
|
|
77
|
+
|
|
78
|
+
- **Layout-only group.** `fill: none` is rejected. A group that only arranges its children uses
|
|
79
|
+
`group.bare { fill-opacity: 0; stroke-width: 0; box-shadow: none; }`.
|
|
80
|
+
- **Dark canvas.** Zones are translucent by default, so a dark group looks grey until it sets
|
|
81
|
+
`opacity: 1; fill-opacity: 1`. Restyle what sits inside with descendant selectors:
|
|
82
|
+
`group.night node::title { color: #F8FAFC }`.
|
|
83
|
+
- **Hero element.** One glow per diagram: `box-shadow: 0 0 22px rgba(140, 79, 255, 0.45)` with a
|
|
84
|
+
thicker stroke. More than one and nothing stands out.
|
|
85
|
+
- **External actors.** Dashed outline and no shadow (`.external`), so they read as outside the system.
|
|
86
|
+
- **Edge semantics.** Solid for requests, dashed for async or control (`edge.async`, `edge.control`),
|
|
87
|
+
a colour for one special path (`edge.hot`), `marker-start` for two-way flows (`edge.replication`).
|
|
88
|
+
- **Tiers.** Tint subnets or zones by tier (`group.public`, `group.private`, `group.data`) and colour
|
|
89
|
+
their labels to match.
|
|
90
|
+
- **Vendor palettes.** AWS colours by service category (compute orange, network purple, database
|
|
91
|
+
magenta, storage green); Google Cloud uses its four brand colours; Kubernetes uses `#326CE5`.
|
|
92
|
+
|
|
93
|
+
## Annotations
|
|
94
|
+
|
|
95
|
+
Legends, numbered steps, callouts and notes make a reference architecture explain itself; all are styled
|
|
96
|
+
by CSS and native in every output.
|
|
97
|
+
|
|
98
|
+
- **Legend**: `d.add_legend()` lists the classes and styles in use, labelled from their names, so name
|
|
99
|
+
classes for meaning (`.web-tier` → "Web tier", `edge.backup-path` → "Backup path"). Pass
|
|
100
|
+
`{".class": "Label", "edge.class": ..., "style:dark": ..., "aws:s3": ...}` to choose entries.
|
|
101
|
+
- **Steps**: `a.to(b, "HTTPS", step="Users call the API")`, `d.step("…", on=node)`. Badges sit beside edge
|
|
102
|
+
labels or on node corners; the Steps panel lists them in order. Keep each step to one line (~38 chars).
|
|
103
|
+
- **Callouts**: `d.callout(node, "text", title=..., kind="info|tip|warning|risk")`. Auto placement needs
|
|
104
|
+
~360 px of clear space beside the node; if `check` reports `callout: overlap`, target a node with room
|
|
105
|
+
around it.
|
|
106
|
+
- **Notes**: `d.note(..., variant="card" | "quote")` for decision cards and quotes on narrative slides.
|
|
107
|
+
- **Legend from CSS**: `legend { placement: bottom; columns: 4 }` moves and reshapes it (also `width`, `gap`,
|
|
108
|
+
`padding`, `swatch-width`, `swatch-height`, `display: none`, `legend::body { font-size }`); an explicit
|
|
109
|
+
`add_legend(position=...)` wins.
|
|
110
|
+
- **Band budget**: legend and steps take a 392 px band on the right. Plan content for about 1416 px of width
|
|
111
|
+
(with `layout.area` set to `[56, 176, 1808, 830]`), or put the legend at the bottom.
|
|
112
|
+
- **Style**: `legend`, `legend::title`, `legend::body`, `step`, `step::label`, `callout.warning`,
|
|
113
|
+
`callout::body`, `note.card`. A dark legend matches a dark cluster (see `examples/k8s.css`).
|
|
114
|
+
- **Specificity**: a rule like `group.cluster node` (0,1,2) beats `.networking` (0,1,0); write
|
|
115
|
+
`group.cluster .networking` so class colours, and with them the legend, still show.
|
|
116
|
+
|
|
117
|
+
## Fitting dense diagrams on one slide
|
|
118
|
+
|
|
119
|
+
The canvas is fixed at 1920×1080 and content is not scaled down. `check` reports
|
|
120
|
+
`layout: overflow` with the excess in pixels.
|
|
121
|
+
|
|
122
|
+
- **Size cards yourself.** Automatic nodes are at least 220 px wide. Pass a size to every node,
|
|
123
|
+
`N = {"w": 210, "h": 84}` then `d.box(..., **N)`, and widen only the few that need it.
|
|
124
|
+
- **Text room with an icon** is about `w − 102` px. A single word longer than that is reported as
|
|
125
|
+
`overflow-x`: widen that node or pick a shorter word. Titles wrap onto two lines; bodies keep to one.
|
|
126
|
+
- **Smaller type shrinks boxes**: `node::title { font-size: 19px }` and `node::body { font-size: 14px }`.
|
|
127
|
+
- **Tighten spacing** with group hints (`gap=24`, `padding=0`) and the root `"gap"` in `layout`.
|
|
128
|
+
- **Use the margins** when needed: `layout={"area": [56, 176, 1808, 830]}` (x, y, w, h).
|
|
129
|
+
- **Turn a row into a column.** When the width overflows and there is room below, stack: AZs as rows
|
|
130
|
+
of tier subnets, regions stacked, the control plane as a top row.
|
|
131
|
+
|
|
132
|
+
## Edges that don't cross boxes
|
|
133
|
+
|
|
134
|
+
- `flex` with `"route": "ortho"` draws elbowed edges but does **not** route around boxes. Connect
|
|
135
|
+
neighbours only: arrange columns so every edge goes to the next column or the next box in a stack.
|
|
136
|
+
- An edge that would skip over a box: reorder (chain `users → dns → cdn` rather than fanning out),
|
|
137
|
+
move one end to another column, or drop the edge and show the relationship with text or a class
|
|
138
|
+
(a `.blocked` pod instead of a "deny" arrow).
|
|
139
|
+
- Anchored edges (`d.edge(a.left, b.left)`) are drawn straight, not routed. Use them only between
|
|
140
|
+
boxes that face each other.
|
|
141
|
+
- `elk` routes around boxes but lays out wider and ignores group `direction`. Compare with
|
|
142
|
+
`spinagram render spec.py --layout elk` before switching.
|
|
143
|
+
|
|
144
|
+
## Backends
|
|
145
|
+
|
|
146
|
+
SVG and PNG draw everything exactly. In PowerPoint and Figma, `open-diamond` becomes a filled
|
|
147
|
+
diamond and `bar` is dropped, each with a warning; `opacity` blends the fill only. `font-family` is
|
|
148
|
+
checked but not applied: text keeps the theme font. PowerPoint may warn that a PNG fallback for some
|
|
149
|
+
Kubernetes SVG icons could not be made; recent PowerPoint versions show the SVG anyway.
|
|
150
|
+
|
|
151
|
+
## Done when
|
|
152
|
+
|
|
153
|
+
- `spinagram check` reports 0 issues.
|
|
154
|
+
- The PNG shows no overlaps, clipped text or edges crossing boxes.
|
|
155
|
+
- Classes describe meaning (`security`, `async`), and colours live only in stylesheets.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/* AWS accents, layered on showcase.css. Colours follow the AWS architecture icon categories.
|
|
2
|
+
Class names double as legend labels: `d.add_legend()` turns `.web-tier` into "Web tier". */
|
|
3
|
+
:root { --compute: #ED7100; --network: #8C4FFF; --database: #C925D1; --storage: #7AA116; --hybrid: #0EA5E9; }
|
|
4
|
+
|
|
5
|
+
.external { fill: #F8FAFC; stroke: #94A3B8; stroke-dasharray: 6 4; box-shadow: none; }
|
|
6
|
+
.edge-service { stroke: var(--network); }
|
|
7
|
+
.security { stroke: var(--sec); fill: #FFF5F5; }
|
|
8
|
+
.load-balancer { stroke: var(--network); fill: #F5F3FF; }
|
|
9
|
+
.web-tier { stroke: var(--compute); }
|
|
10
|
+
.app-tier { stroke: var(--compute); fill: #FFF7ED; }
|
|
11
|
+
.database { stroke: var(--database); fill: #FDF4FF; }
|
|
12
|
+
.storage { stroke: var(--storage); fill: #F7FEE7; }
|
|
13
|
+
|
|
14
|
+
group.edge { fill: #F8FAFC; stroke: var(--card-stroke); border-radius: 16px; }
|
|
15
|
+
group.edge::label { color: var(--muted); }
|
|
16
|
+
group.bare { fill-opacity: 0; stroke-width: 0; box-shadow: none; }
|
|
17
|
+
group.public { fill: #F0FDF4; stroke: var(--storage); }
|
|
18
|
+
group.public::label { color: #3F6212; }
|
|
19
|
+
group.private { fill: #EFF6FF; stroke: #147EBA; }
|
|
20
|
+
group.private::label { color: #1E40AF; }
|
|
21
|
+
group.data { fill: #FAF5FF; stroke: var(--database); }
|
|
22
|
+
group.data::label { color: #86198F; }
|
|
23
|
+
|
|
24
|
+
edge.replication { stroke: var(--database); stroke-dasharray: 6 4; marker-start: arrow; }
|
|
25
|
+
edge.replication::label { color: #86198F; }
|
|
26
|
+
|
|
27
|
+
/* Networking */
|
|
28
|
+
group.onprem { fill: #F8FAFC; stroke: #475569; stroke-dasharray: 2 4; border-radius: 0; }
|
|
29
|
+
group.onprem::label { color: #334155; }
|
|
30
|
+
.on-premises { stroke: #475569; }
|
|
31
|
+
.hybrid-link { stroke: var(--hybrid); fill: #F0F9FF; }
|
|
32
|
+
.transit-hub { stroke: var(--network); stroke-width: 3; fill: #F5F3FF; box-shadow: 0 0 22px rgba(140, 79, 255, 0.45); }
|
|
33
|
+
.transit-hub::title { color: #5B21B6; }
|
|
34
|
+
.egress { stroke: var(--network); }
|
|
35
|
+
.workload { border-radius: 6px; stroke: #16A34A; fill: #F0FDF4; }
|
|
36
|
+
#staging-vpc { stroke: #D97706; fill: #FFFBEB; }
|
|
37
|
+
#shared-services { stroke: #0891B2; fill: #ECFEFF; }
|
|
38
|
+
edge.private-circuit { stroke: var(--hybrid); stroke-width: 3; }
|
|
39
|
+
edge.backup-path { stroke: var(--hybrid); stroke-width: 3; stroke-dasharray: 8 5; }
|
|
40
|
+
edge.inspection { stroke: var(--sec); stroke-width: 3; }
|
|
41
|
+
edge.inspection::label { color: var(--sec); }
|
|
42
|
+
|
|
43
|
+
/* Annotations */
|
|
44
|
+
step { fill: #232F3E; stroke: #FFFFFF; }
|
|
45
|
+
step::label { color: #FF9900; }
|
|
46
|
+
legend::title { color: #232F3E; }
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""AWS hub-and-spoke network: Transit Gateway with centralized inspection and egress, after the AWS
|
|
2
|
+
reference architecture for multi-VPC networking, with an automatic legend and numbered traffic steps."""
|
|
3
|
+
from spinagram import Diagram
|
|
4
|
+
|
|
5
|
+
N = {"w": 200, "h": 80} # compact cards: the whole network fits one slide
|
|
6
|
+
|
|
7
|
+
d = Diagram("AWS02", "Hub-and-spoke networking on AWS",
|
|
8
|
+
subtitle="Transit Gateway hub · centralized inspection and egress · hybrid connectivity",
|
|
9
|
+
layout={"engine": "flex", "direction": "right", "gap": 32, "route": "ortho",
|
|
10
|
+
"area": [56, 176, 1808, 830]})
|
|
11
|
+
d.stylesheet("showcase.css")
|
|
12
|
+
d.stylesheet("aws.css")
|
|
13
|
+
|
|
14
|
+
with d.group(id="hybrid", class_="bare", direction="right", gap=32, align="center"):
|
|
15
|
+
with d.group("Corporate data center", id="dc", class_="onprem", direction="down", gap=24,
|
|
16
|
+
icon="aws:group-corporatedatacenter"):
|
|
17
|
+
office = d.box("Branch offices", text="SD-WAN", icon="aws:officebuilding", class_="external", **N)
|
|
18
|
+
cgw = d.box("Customer gateway", text="BGP ASN 65010", icon="aws:vpc-customer-gateway",
|
|
19
|
+
class_="on-premises", **N)
|
|
20
|
+
with d.group(id="links", class_="bare", direction="down", gap=24):
|
|
21
|
+
dx = d.box("Direct Connect", text="2 × 10 Gbps", icon="aws:direct-connect", class_="hybrid-link", **N)
|
|
22
|
+
vpn = d.box("VPN", text="IPsec backup", icon="aws:aws-siteto-site-vpn", class_="hybrid-link", **N)
|
|
23
|
+
with d.group("us-east-1 · network account", style="aws-region", direction="down", gap=28, padding=16):
|
|
24
|
+
with d.group(id="services", class_="bare", direction="right", gap=24):
|
|
25
|
+
with d.group("Inspection VPC", style="aws-vpc"):
|
|
26
|
+
nfw = d.box("Network Firewall", text="stateful IPS", icon="aws:network-firewall", class_="security", **N)
|
|
27
|
+
with d.group("Egress VPC", style="aws-vpc", direction="down", gap=20):
|
|
28
|
+
nat = d.box("NAT gateway", text="per AZ", icon="aws:vpcnat-gateway", class_="egress", **N)
|
|
29
|
+
igw = d.box("Internet gateway", text="to 0.0.0.0/0", icon="aws:vpc-internet-gateway", class_="egress", **N)
|
|
30
|
+
with d.group(id="core", class_="bare", direction="right", gap=60, align="center"):
|
|
31
|
+
tgw = d.box("Transit Gateway", text="routing hub", icon="aws:aws-transit-gateway",
|
|
32
|
+
class_="transit-hub", **N)
|
|
33
|
+
with d.group(id="spokes", class_="bare", direction="down", gap=14):
|
|
34
|
+
spokes = [d.box(name, text=cidr, icon="aws:group-virtualprivatecloud-vpc", class_="workload", **N)
|
|
35
|
+
for name, cidr, env in (("Prod VPC", "10.1.0.0/16", "prod"),
|
|
36
|
+
("Staging VPC", "10.2.0.0/16", "staging"),
|
|
37
|
+
("Shared services", "10.3.0.0/16", "shared"))]
|
|
38
|
+
|
|
39
|
+
office >> cgw
|
|
40
|
+
cgw.to(dx, "primary", class_="private-circuit")
|
|
41
|
+
cgw.to(vpn, "backup", class_="backup-path")
|
|
42
|
+
dx.to(tgw, "DX gateway", class_="private-circuit", step="Branch traffic reaches AWS over DX")
|
|
43
|
+
vpn.to(tgw, class_="backup-path")
|
|
44
|
+
for s in spokes:
|
|
45
|
+
tgw.to(s, arrow="both")
|
|
46
|
+
tgw.to(nfw, "inspect", class_="inspection", step="TGW sends traffic to inspection")
|
|
47
|
+
nfw.to(nat, step="Allowed flows leave via NAT")
|
|
48
|
+
nat.to(igw, step="The internet gateway egresses")
|
|
49
|
+
d.add_legend()
|
|
50
|
+
|
|
51
|
+
diagram = d
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""AWS three-tier web application, after the AWS reference architecture: an automatic legend from the CSS
|
|
2
|
+
classes and numbered steps along the request path."""
|
|
3
|
+
from spinagram import Diagram
|
|
4
|
+
|
|
5
|
+
N = {"w": 196, "h": 80} # compact cards: diagram, legend and steps share one slide
|
|
6
|
+
|
|
7
|
+
d = Diagram("AWS01", "Three-tier web application on AWS",
|
|
8
|
+
subtitle="Edge, web, app and data tiers across two Availability Zones",
|
|
9
|
+
layout={"engine": "flex", "direction": "right", "gap": 32, "route": "ortho",
|
|
10
|
+
"area": [56, 176, 1808, 830]})
|
|
11
|
+
d.stylesheet("showcase.css")
|
|
12
|
+
d.stylesheet("aws.css")
|
|
13
|
+
|
|
14
|
+
with d.group("Internet edge", id="edge", class_="edge", direction="down", gap=22, padding=16):
|
|
15
|
+
users = d.box("Users", text="web, mobile", icon="aws:users", class_="external", **N)
|
|
16
|
+
dns = d.box("Route 53", text="latency DNS", icon="aws:amazon-route53", class_="edge-service", **N)
|
|
17
|
+
cdn = d.box("CloudFront", text="CDN, TLS", icon="aws:cloudfront", class_="edge-service", w=216, h=80)
|
|
18
|
+
waf = d.box("AWS WAF", text="managed rules", icon="aws:awswaf", class_="security", **N)
|
|
19
|
+
s3 = d.box("S3", text="static origin", icon="aws:s3", class_="storage", **N)
|
|
20
|
+
with d.group("us-east-1", style="aws-region", padding=16):
|
|
21
|
+
with d.group("VPC 10.0.0.0/16", style="aws-vpc", direction="right", align="center", gap=12, padding=16):
|
|
22
|
+
alb = d.box("ALB", text="HTTPS :443", icon="aws:alb", class_="load-balancer", **N)
|
|
23
|
+
with d.group(id="azs", class_="bare", direction="down", gap=20, padding=0):
|
|
24
|
+
tiers = {}
|
|
25
|
+
for az, n in (("a", 0), ("b", 1)):
|
|
26
|
+
with d.group(f"Availability Zone {az.upper()}", style="aws-az", direction="right", gap=12, padding=16):
|
|
27
|
+
with d.group(f"Public 10.0.{n}.0/24", style="aws-subnet", class_="public", padding=14):
|
|
28
|
+
web = d.box("Web", text="Nginx · ASG", icon="aws:amazon-ec2", class_="web-tier",
|
|
29
|
+
id=f"web-{az}", **N)
|
|
30
|
+
with d.group(f"Private 10.0.1{n}.0/24", style="aws-subnet", class_="private", padding=14):
|
|
31
|
+
app = d.box("App", text="ECS Fargate", icon="aws:amazon-elastic-container-service",
|
|
32
|
+
class_="app-tier", id=f"app-{az}", **N)
|
|
33
|
+
with d.group(f"Data 10.0.2{n}.0/24", style="aws-subnet", class_="data", padding=14):
|
|
34
|
+
db = d.box("Aurora " + ("writer" if az == "a" else "reader"), text="PostgreSQL",
|
|
35
|
+
icon="aws:amazon-aurora", class_="database", id=f"db-{az}", **N)
|
|
36
|
+
tiers[az] = (web, app, db)
|
|
37
|
+
|
|
38
|
+
users.to(dns, "resolve", step="Users resolve the domain")
|
|
39
|
+
dns.to(cdn, "alias", step="Route 53 aliases to CloudFront")
|
|
40
|
+
cdn.to(waf, step="AWS WAF filters each request")
|
|
41
|
+
waf.to(alb, step="The ALB balances across zones")
|
|
42
|
+
for az, (web, app, db) in tiers.items():
|
|
43
|
+
alb >> web
|
|
44
|
+
if az == "a":
|
|
45
|
+
web.to(app, step="Web tier calls the app tier")
|
|
46
|
+
app.to(db, "SQL", step="App tier writes to Aurora")
|
|
47
|
+
else:
|
|
48
|
+
web >> app
|
|
49
|
+
app.to(db, "SQL")
|
|
50
|
+
d.edge(tiers["a"][2], tiers["b"][2], "replication", class_="replication")
|
|
51
|
+
d.add_legend()
|
|
52
|
+
|
|
53
|
+
diagram = d
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* Campus network: clean tiers, network symbols carry the colour. */
|
|
2
|
+
group.tier { fill: #F8FAFC; stroke: #E2E8F0; border-radius: 16px; }
|
|
3
|
+
group.tier::label { color: #64748B; }
|
|
4
|
+
group.bare { fill-opacity: 0; stroke-width: 0; box-shadow: none; }
|
|
5
|
+
|
|
6
|
+
.internet { fill: #F8FAFC; stroke: #94A3B8; stroke-dasharray: 6 4; box-shadow: none; }
|
|
7
|
+
.edge-gear { stroke: #DC2626; }
|
|
8
|
+
.core { stroke: #16A34A; stroke-width: 2.5; }
|
|
9
|
+
.access { stroke: #2563EB; }
|
|
10
|
+
.endpoint { border-radius: 40px; stroke: #94A3B8; }
|
|
11
|
+
|
|
12
|
+
edge { stroke: #94A3B8; stroke-width: 2.5; }
|
|
13
|
+
edge.uplink { stroke: #16A34A; stroke-width: 3; }
|
|
14
|
+
edge.redundant-link { stroke: #16A34A; stroke-width: 3; stroke-dasharray: 8 6; marker-start: arrow; }
|
|
15
|
+
|
|
16
|
+
/* The legend is placed and laid out here, not in the diagram code. */
|
|
17
|
+
legend { placement: bottom; columns: 7; padding: 18; swatch-width: 64; swatch-height: 24; fill: #F8FAFC; box-shadow: none; }
|
|
18
|
+
legend::title { font-size: 16px; color: #475569; }
|
|
19
|
+
legend::body { font-size: 15px; }
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""A campus network in three tiers: WAN edge, a redundant core and access switches serving Wi-Fi, phones and
|
|
2
|
+
desks, drawn with the Affinity symbols of the `network` pack in their colour and circle variants."""
|
|
3
|
+
from spinagram import Diagram
|
|
4
|
+
|
|
5
|
+
N = {"w": 220, "h": 92}
|
|
6
|
+
E = {"w": 210, "h": 80}
|
|
7
|
+
|
|
8
|
+
d = Diagram("NET01", "Campus network", subtitle="Redundant core, access layer and endpoints, left to right",
|
|
9
|
+
layout={"engine": "flex", "direction": "right", "rank_gap": 64, "route": "ortho",
|
|
10
|
+
"area": [56, 176, 1808, 830]})
|
|
11
|
+
d.stylesheet("showcase.css")
|
|
12
|
+
d.stylesheet("campus.css")
|
|
13
|
+
|
|
14
|
+
with d.group("WAN edge", id="edge", class_="tier", direction="down", gap=40, align="center"):
|
|
15
|
+
isp = d.box("ISP", text="2 × 10 Gb/s", icon="network:internet", class_="internet", **N)
|
|
16
|
+
router = d.box("Edge router", text="BGP, NAT", icon="network:router-red", class_="edge-gear", **N)
|
|
17
|
+
fw = d.box("Firewall", text="HA pair", icon="network:firewall-red", class_="edge-gear", **N)
|
|
18
|
+
with d.group("Core", id="core", class_="tier", direction="down", gap=72, align="center"):
|
|
19
|
+
core_a = d.box("Core A", text="L3, 100 Gb/s", icon="network:switch-multilayer-green", class_="core", **N)
|
|
20
|
+
core_b = d.box("Core B", text="L3, 100 Gb/s", icon="network:switch-multilayer-green", class_="core", **N)
|
|
21
|
+
with d.group("Access", id="access", class_="tier", direction="down", gap=24, align="center"):
|
|
22
|
+
floors = [d.box(f"Floor {i}", text="48 × 2.5 Gb/s", icon="network:switch", class_="access", id=f"floor-{i}",
|
|
23
|
+
**N) for i in (1, 2, 3)]
|
|
24
|
+
wlc = d.box("Wireless LAN", text="controller", icon="network:wlc", class_="access", **N)
|
|
25
|
+
with d.group("Endpoints", id="endpoints", class_="tier", direction="down", gap=24, align="center"):
|
|
26
|
+
ap = d.box("Access points", text="Wi-Fi 7", icon="network:wifi-plain", class_="endpoint", **E)
|
|
27
|
+
phone = d.box("IP phones", text="PoE", icon="network:ip-phone-circle", class_="endpoint", **E)
|
|
28
|
+
desk = d.box("Desks", text="1 Gb/s", icon="network:client-circle", class_="endpoint", **E)
|
|
29
|
+
printer = d.box("Printers", text="VLAN 40", icon="network:printer-circle", class_="endpoint", **E)
|
|
30
|
+
|
|
31
|
+
isp >> router >> fw
|
|
32
|
+
fw.to(core_a, class_="uplink")
|
|
33
|
+
fw.to(core_b, class_="uplink")
|
|
34
|
+
d.edge(core_a, core_b, "VSS", class_="redundant-link")
|
|
35
|
+
core_a.to([floors[0], floors[1]], class_="uplink")
|
|
36
|
+
core_b.to([floors[2], wlc], class_="uplink")
|
|
37
|
+
floors[0] >> ap
|
|
38
|
+
floors[1] >> phone
|
|
39
|
+
floors[2] >> desk
|
|
40
|
+
wlc.to(printer, "Wi-Fi")
|
|
41
|
+
d.callout(core_b, "Either core carries the whole campus: links and supervisors are redundant.",
|
|
42
|
+
title="No single point of failure", kind="info")
|
|
43
|
+
d.add_legend()
|
|
44
|
+
|
|
45
|
+
diagram = d
|