@caddis/cli 0.4.73 → 0.4.74
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.
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/skills/_registry.md +1 -0
- package/bundles/antigravity-plugin/skills/coordinate/SKILL.md +4 -1
- package/bundles/antigravity-plugin/skills/flow-chart-from-counts/SKILL.md +193 -0
- package/bundles/antigravity-plugin/skills/flow-chart-from-counts/assets/viewer-template.html +190 -0
- package/bundles/antigravity-plugin/skills/flow-chart-from-counts/references/edge-list-sql.md +105 -0
- package/bundles/antigravity-plugin/skills/flow-chart-from-counts/scripts/check-render.mjs +139 -0
- package/bundles/antigravity-plugin/skills/flow-chart-from-counts/scripts/edges_to_mermaid.py +418 -0
- package/bundles/codex/.codex/skills/media/flow-chart-from-counts/SKILL.md +193 -0
- package/bundles/codex/.codex/skills/media/flow-chart-from-counts/assets/viewer-template.html +190 -0
- package/bundles/codex/.codex/skills/media/flow-chart-from-counts/references/edge-list-sql.md +105 -0
- package/bundles/codex/.codex/skills/media/flow-chart-from-counts/scripts/check-render.mjs +139 -0
- package/bundles/codex/.codex/skills/media/flow-chart-from-counts/scripts/edges_to_mermaid.py +418 -0
- package/bundles/manifest.json +2 -2
- package/package.json +1 -1
|
@@ -117,6 +117,7 @@
|
|
|
117
117
|
| Skill | Path | When to Use |
|
|
118
118
|
|-------|------|-------------|
|
|
119
119
|
| Draw Io | `draw-io/` | draw.io diagram creation, editing, and review. Use for .drawio XML editing, PNG conversion, layout adjustment, and AWS icon usage. |
|
|
120
|
+
| Flow Chart From Counts | `flow-chart-from-counts/` | Turn a from-to-count edge list into a zoomable Mermaid flow chart page with counts, a legend and a render check. |
|
|
120
121
|
| Mermaid Diagrams | `mermaid-diagrams/` | Software diagrams in Mermaid: class, sequence, flowchart, ERD, C4, state, git graph, gantt. Use for domain models, API flows, processes and database schemas. |
|
|
121
122
|
| Particle Art | `particle-art/` | >- |
|
|
122
123
|
|
|
@@ -236,7 +236,10 @@ and 7.3. While the queue is frozen (section 5 step 1), do steps 3.1 to 3.3 only.
|
|
|
236
236
|
Do only the merge or the `go`:
|
|
237
237
|
- Target mode: `GH_TOKEN=$(gh auth token -u <account>) gh pr merge <n> --<merge_method> --match-head-commit <watched sha>`.
|
|
238
238
|
The merge SHA is `mergeCommit.oid` from `gh pr view <n> --json state,mergeCommit`.
|
|
239
|
-
- Interim mode: send `go <PR> <watched sha
|
|
239
|
+
- Interim mode: send `go <PR> <watched sha>`, and on the next line: `I wrote the ack file
|
|
240
|
+
~/.claude/merge-acks/<checkout folder name>/pr-<n>.json. Do not write, change or delete it.` The PR's
|
|
241
|
+
session merges with the same command. If its merge hook refuses because the ack file is wrong or
|
|
242
|
+
missing, write the ack file again (step 3.13) and send `go` once more. Then
|
|
240
243
|
poll with a Bash command in `run_in_background`: `gh pr view <n> --json state,mergeCommit` once a
|
|
241
244
|
minute, for at most 15 min, until `state` is `MERGED`. Take `mergeCommit.oid` as the merge SHA; the
|
|
242
245
|
session's `merged` reply only tells you to look. No merge after 15 min: ask the PR's session once.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: flow-chart-from-counts
|
|
3
|
+
description: Turn a from-to-count edge list into a zoomable Mermaid flow chart page with counts, a legend and a render check.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# flow-chart-from-counts
|
|
7
|
+
|
|
8
|
+
Turns counted paths (N cases went from step A to step B) into a flow chart page that a
|
|
9
|
+
reader can trust: counts on every arrow, a legend for the box types, a title with the date and
|
|
10
|
+
time zone, a note that names what is not drawn, and a viewer with zoom, pan and full screen.
|
|
11
|
+
|
|
12
|
+
Paths below are relative to the folder that holds this file.
|
|
13
|
+
|
|
14
|
+
## When to use it
|
|
15
|
+
|
|
16
|
+
- "Draw how cases move through the process", "show the journey with counts", "flow chart of
|
|
17
|
+
calls / orders / tickets / sessions from this table".
|
|
18
|
+
- You have, or can query, rows of `from, to, count`, or an event table with one row per step.
|
|
19
|
+
- A Sankey or a funnel was asked for, but the paths branch and rejoin: a flow chart shows that
|
|
20
|
+
better.
|
|
21
|
+
|
|
22
|
+
Use something else for:
|
|
23
|
+
|
|
24
|
+
- An interactive page from a JSON model with lanes, playback and an inspector: `flow-studio`.
|
|
25
|
+
- Mermaid syntax for other diagram kinds (sequence, ERD, class): `mermaid-diagrams`.
|
|
26
|
+
- A diagram of a SQL object's structure: `db-diagram`.
|
|
27
|
+
|
|
28
|
+
## What is in the folder
|
|
29
|
+
|
|
30
|
+
| file | use |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `scripts/edges_to_mermaid.py` | CSV edge list to first-draft Mermaid source (standard library only) |
|
|
33
|
+
| `assets/viewer-template.html` | page with the zoom/pan/full-screen viewer, theme tokens and one example chart |
|
|
34
|
+
| `scripts/check-render.mjs` | headless render check: fails on no SVG, error text, too few nodes, page errors; saves screenshots |
|
|
35
|
+
| `references/edge-list-sql.md` | SQL pattern: event table to edge list and box counts, and the checks to run on it |
|
|
36
|
+
|
|
37
|
+
## Process
|
|
38
|
+
|
|
39
|
+
1. **Get the edge list.** From an event table, use `references/edge-list-sql.md`. Run its checks
|
|
40
|
+
(START rows add up to the cases; path occurrences in equal path occurrences out per step).
|
|
41
|
+
Count arrows as distinct cases per pair. Get the box counts (distinct cases per step) at the
|
|
42
|
+
same time.
|
|
43
|
+
2. **Name the node types** with the reader, or from the process owner's own glossary: for
|
|
44
|
+
example automatic step, in-scope step, optional step, hand-off, end. Write a `nodes.csv`
|
|
45
|
+
(`name,type,count`).
|
|
46
|
+
3. **Make the first draft:**
|
|
47
|
+
```
|
|
48
|
+
python scripts/edges_to_mermaid.py edges.csv --nodes nodes.csv --min 25 \
|
|
49
|
+
--in-scope scope --optional choice --out chart.mmd --strict
|
|
50
|
+
```
|
|
51
|
+
`--out` writes UTF-8 (do not use `>` in Windows PowerShell 5.1: it can write UTF-16). The note
|
|
52
|
+
on hidden paths goes to stderr. Keep it: it goes under the chart. `--strict` exits 1 on a
|
|
53
|
+
`warning:` line, which means an arrow is larger than its box: fix the query before you draw.
|
|
54
|
+
Duplicate `from,to` rows are added together. Node types become classes named `t_<type>`.
|
|
55
|
+
4. **Put it in the page.** Copy `assets/viewer-template.html`. Paste the source into the
|
|
56
|
+
`<script type="text/plain">` block. Set `data-title` (what, which day) and `data-sub` (the UTC
|
|
57
|
+
window, the unit of the counts, the minimum path size). Replace the legend and the note.
|
|
58
|
+
For more charts, add one `.fv` block and one source script per chart.
|
|
59
|
+
5. **Render and check:**
|
|
60
|
+
```
|
|
61
|
+
node <skill folder>/scripts/check-render.mjs page.html --out shots [--channel msedge]
|
|
62
|
+
```
|
|
63
|
+
Run it from a folder whose `node_modules` has `playwright`. It exits 1 when a check fails.
|
|
64
|
+
6. **Look at the screenshots** (`chart-N.png` is the chart at Fit, `chart-1-full.png` is full
|
|
65
|
+
screen) and run the tidy checklist below. If a rule fails, change the layout with the layout
|
|
66
|
+
fixes, and go back to step 5.
|
|
67
|
+
7. **Show** the page and the screenshots only when every checklist rule passes. Say which labels
|
|
68
|
+
are your reading (see Wording). If a rule cannot be met, say which rule and why.
|
|
69
|
+
|
|
70
|
+
## Tidy checklist (the chart must pass it before you show it)
|
|
71
|
+
|
|
72
|
+
A clean, tidy chart is an acceptance rule, not a nice-to-have. A reader who has to trace a
|
|
73
|
+
line across the page stops trusting the numbers on it.
|
|
74
|
+
|
|
75
|
+
1. **No crossing lines.** No line passes through a box or a label. Use the tree rule with
|
|
76
|
+
dashed stubs, and split shared targets (layout fixes 1 and 2).
|
|
77
|
+
2. **No overlaps.** No label or box sits on another. Edge labels are short: `(1,234)` and at
|
|
78
|
+
most a few words. Long text goes in the note under the chart, not on the chart.
|
|
79
|
+
3. **Rows and side flows.** Boxes of the same kind line up in one row where the flow allows
|
|
80
|
+
(layout fix 5). A side flow sits in its own dashed box at the bottom, pushed down with an
|
|
81
|
+
invisible link (layout fix 4).
|
|
82
|
+
4. **One look per type.** One box shape per type, a small fixed palette (5 to 7 classes), a
|
|
83
|
+
legend that matches the chart exactly (stubs included), and the title with the date and the
|
|
84
|
+
time zone on the chart.
|
|
85
|
+
5. **Readable at Fit and at full screen** on a 1280 x 900 screen. Nothing is clipped.
|
|
86
|
+
6. **Checked by eye.** You opened both screenshots and checked rules 1 to 5 on them. The render
|
|
87
|
+
check's warnings (box overlaps, labels on boxes) help, but they cannot see a crossing line.
|
|
88
|
+
|
|
89
|
+
## Count rules
|
|
90
|
+
|
|
91
|
+
- **Arrows and boxes both count distinct cases.** An arrow says how many distinct cases took
|
|
92
|
+
that path (`COUNT(DISTINCT case_id)` per from-to pair). A box says how many distinct cases
|
|
93
|
+
reached it. Do not draw path occurrences (`COUNT(*)`): a case that loops counts twice, and a
|
|
94
|
+
reader who sees a box of 8,538 followed by an arrow of 8,541 stops trusting the chart. Label
|
|
95
|
+
every number as cases.
|
|
96
|
+
- **Self-check: no arrow into or out of a box exceeds that box's count.** If one does, the
|
|
97
|
+
query counts occurrences. The converter warns on stderr when this happens.
|
|
98
|
+
- **Say in the note that arrows into a box can sum to a little more than the box**, because a
|
|
99
|
+
case can reach it by two paths.
|
|
100
|
+
- **Put counts in brackets, with thousands separators:** `(1,234)`. In Mermaid an edge label that
|
|
101
|
+
holds brackets must be quoted: `A -->|"(1,234)"| B`. Unquoted, `(` starts a node shape and the
|
|
102
|
+
chart fails to parse.
|
|
103
|
+
- **Hide small paths with a stated minimum.** Choose a minimum (for example 25), drop the paths
|
|
104
|
+
under it, and write a note that names them and their total: "Not drawn because fewer than 25
|
|
105
|
+
cases took them: X to Y (12), ... (41 in total)." A chart that drops paths without a note looks
|
|
106
|
+
complete and is not.
|
|
107
|
+
- **Merge small boxes into one named box, and say so.** When you merge, the count is a sum of
|
|
108
|
+
rounded or filtered parts: write `(about 600)`, and say in the note what the box holds.
|
|
109
|
+
- **Put the date and the time zone on the chart.** Write the title as HTML inside the zoomable
|
|
110
|
+
stage (the template does this from `data-title` and `data-sub`), so it stays with the chart in
|
|
111
|
+
full screen and in screenshots. Do not use a frontmatter `title:` as well: it would show a second title. Give the
|
|
112
|
+
local day and the UTC window: `Mon 5 Oct 2026 (4 Oct 23:00 to 5 Oct 23:00 UTC)`.
|
|
113
|
+
- **Name the unit.** "Counts in brackets are calls / orders / cases." A count with no unit gets
|
|
114
|
+
read as the wrong one.
|
|
115
|
+
|
|
116
|
+
## Node types and the legend
|
|
117
|
+
|
|
118
|
+
- One `classDef` per node type, and one `class id1,id2 type;` line per type. The converter writes
|
|
119
|
+
these from `nodes.csv`.
|
|
120
|
+
- **In-scope boxes get a thick border** (`stroke-width:3px`), so the reader finds the subject of
|
|
121
|
+
the chart first.
|
|
122
|
+
- **Optional or owner-choice boxes get a dashed border** (`stroke-dasharray:6 4`): a step that is
|
|
123
|
+
a decision still open, or one that runs only for some cases.
|
|
124
|
+
- End boxes share one quiet grey class. Stubs share one class: white fill, thin dashed grey
|
|
125
|
+
border (`stroke-dasharray:3 3`), so they read as pointers, not steps.
|
|
126
|
+
- Put a legend above the chart with one swatch per type, with the same fill, border width and
|
|
127
|
+
dash as the chart. Use light fills with dark text. Keep the chart canvas light in dark mode
|
|
128
|
+
too (the template's `--canvas` token does this), or the dark text loses contrast.
|
|
129
|
+
|
|
130
|
+
## Layout fixes that work
|
|
131
|
+
|
|
132
|
+
Mermaid's layout (dagre) puts each node on a rank and orders each rank to cut crossings. It does
|
|
133
|
+
not know what the reader looks for. These fixes move it.
|
|
134
|
+
|
|
135
|
+
1. **Draw the chart as a tree.** Give every box exactly one parent edge: the biggest path into
|
|
136
|
+
it. For every other path into that box, add a small dashed stub under its own source,
|
|
137
|
+
labelled with the target and the count: `B --> toB_C(["to C (412)"])`. Shared targets and
|
|
138
|
+
back-edges are what make the layout cross lines; a tree with stubs has none. Add a legend
|
|
139
|
+
entry for stubs ("dashed: the path continues at the box named"). The converter does this by
|
|
140
|
+
default (`--no-tree` turns it off).
|
|
141
|
+
2. **Give each source its own end box.** One shared "Ends" box pulls a long line from every step
|
|
142
|
+
across the chart. Write `A --> endA(["Ends"])` and `B --> endB(["Ends"])` instead. The
|
|
143
|
+
converter does this. Apply the same fix to any shared target that sits far from its sources:
|
|
144
|
+
a copy of the box next to each source is easier to read than one box with crossing lines.
|
|
145
|
+
3. **Do not draw a loop-back as an edge.** A line back up the chart crosses everything. End the
|
|
146
|
+
path in a stub: `A --> backA(["back to Triage (190)"])`. The converter does this for every
|
|
147
|
+
edge that points back to a step on the path.
|
|
148
|
+
4. **Put a side flow in its own subgraph and push it down.** For example the steps after a
|
|
149
|
+
hand-off. Draw them in `subgraph AFTER[...] direction LR ... end` and add an invisible link
|
|
150
|
+
from a low node of the main flow: `X2 ~~~ AFTER`. The subgraph then sits under the main
|
|
151
|
+
flow, not beside it.
|
|
152
|
+
5. **Put same-kind boxes in one row.** Group them in a subgraph, and lengthen the edges into it
|
|
153
|
+
(`---->`, one dash per extra rank) so they land on the same rank.
|
|
154
|
+
6. **Replace a long jump with a short stub or a dotted link.** When an edge must reach a box
|
|
155
|
+
far away, end it in a small stub node near the source, or draw it dotted (`-.->`) with a
|
|
156
|
+
label such as `"few"`, so the eye can tell it is secondary.
|
|
157
|
+
7. **Order the edges from a node in the order you want the boxes, left to right**, and declare
|
|
158
|
+
the nodes in the same order. Dagre takes this as its first guess. It can still swap them to
|
|
159
|
+
cut crossings: check the screenshot.
|
|
160
|
+
8. **Quote every label that holds brackets, quotes or `<br/>`:** `["Text<br/>(12)"]`,
|
|
161
|
+
`|"(12)"|`. Write a quote inside a label as `#quot;`.
|
|
162
|
+
|
|
163
|
+
Change one thing at a time and re-render. Two fixes at once hide which one helped.
|
|
164
|
+
|
|
165
|
+
## Wording
|
|
166
|
+
|
|
167
|
+
- **Use the process owner's words for box labels** when a glossary or a list of step names
|
|
168
|
+
exists. A reader checks the chart against those names.
|
|
169
|
+
- **Write neutral endings.** "Case ends" or "Call ends" means the data shows no further step.
|
|
170
|
+
Do not write a guess such as "hangs up" or "gives up": the data does not say who ended it.
|
|
171
|
+
Say what "ends" means in the note.
|
|
172
|
+
- **Mark your own reading.** When a label or a grouping is your interpretation, not a name from
|
|
173
|
+
the source, say so in the note ("'Team queues' is our grouping of four queues").
|
|
174
|
+
|
|
175
|
+
## Verify
|
|
176
|
+
|
|
177
|
+
`scripts/check-render.mjs` fails (exit 1) when a chart has no SVG, shows an error message, has
|
|
178
|
+
fewer nodes than `--min-nodes`, or the page throws. Make it fail once on purpose (break one
|
|
179
|
+
line of the source) to see that it can. It also counts box overlaps and arrow labels on boxes
|
|
180
|
+
as warnings. Those are hints: open the screenshots and run the tidy checklist by eye, because
|
|
181
|
+
the check cannot see a crossing line.
|
|
182
|
+
|
|
183
|
+
Also check the numbers, not only the drawing: the START count equals the cases in the window,
|
|
184
|
+
and no arrow into or out of a box is larger than the box (the converter warns when one is).
|
|
185
|
+
|
|
186
|
+
## Publishing
|
|
187
|
+
|
|
188
|
+
- As an Artifact: follow the artifact-design rules. Mermaid loads from
|
|
189
|
+
`https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.9.1/mermaid.min.js` (an exact version on the
|
|
190
|
+
allowed CDN). The template already has theme tokens for light and dark, an explicit body
|
|
191
|
+
background, and a 16px side gutter for phone width.
|
|
192
|
+
- In an app: render with the app's own Mermaid package and keep the same source, title and note.
|
|
193
|
+
Keep the count rules; they are what makes the chart trustworthy.
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Case Flow Chart</title>
|
|
7
|
+
<style>
|
|
8
|
+
/* Flow chart page from the flow-chart-from-counts skill.
|
|
9
|
+
One .fv block per chart. Each block names a <script type="text/plain"> that holds the Mermaid source.
|
|
10
|
+
--canvas stays light in dark mode on purpose: the coloured boxes use dark text and need a light ground. */
|
|
11
|
+
:root{
|
|
12
|
+
--bg:#faf8f5; --surface:#ffffff; --ink-1:#1c1917; --ink-2:#57534e; --ink-3:#78716c; --line:#e7e2da; --canvas:#fbfaf8;
|
|
13
|
+
--sans:"Segoe UI",system-ui,-apple-system,Arial,sans-serif; --mono:Consolas,ui-monospace,"Courier New",monospace;
|
|
14
|
+
}
|
|
15
|
+
@media (prefers-color-scheme: dark){:root:not([data-theme="light"]){
|
|
16
|
+
--bg:#23252f; --surface:#2c2f3b; --ink-1:#f3f1ec; --ink-2:#c9c6c0; --ink-3:#a19d97; --line:#3f4250; --canvas:#f4f2ee; color-scheme:dark}}
|
|
17
|
+
:root[data-theme="dark"]{
|
|
18
|
+
--bg:#23252f; --surface:#2c2f3b; --ink-1:#f3f1ec; --ink-2:#c9c6c0; --ink-3:#a19d97; --line:#3f4250; --canvas:#f4f2ee; color-scheme:dark}
|
|
19
|
+
*{box-sizing:border-box}
|
|
20
|
+
body{margin:0;background:var(--bg);color:var(--ink-1);font-family:var(--sans);font-size:14px;line-height:1.55}
|
|
21
|
+
.wrap{max-width:1000px;margin:0 auto;padding:24px 16px 56px;display:grid;gap:20px}
|
|
22
|
+
h1{font-size:22px;margin:0}
|
|
23
|
+
p{margin:0;max-width:74ch}
|
|
24
|
+
.small{font-size:13px;color:var(--ink-2)}
|
|
25
|
+
.legendrow{display:flex;flex-wrap:wrap;gap:14px;font-size:12px;color:var(--ink-2)}
|
|
26
|
+
.sw{display:inline-block;width:12px;height:12px;border-radius:3px;margin-right:5px;vertical-align:-2px;border:1px solid var(--ink-3)}
|
|
27
|
+
.sw.thick{border-width:3px}
|
|
28
|
+
.sw.dash{border-style:dashed;border-width:2px}
|
|
29
|
+
|
|
30
|
+
/* Viewer */
|
|
31
|
+
.fv{border:1px solid var(--line);border-radius:8px;background:var(--canvas);overflow:hidden;display:flex;flex-direction:column}
|
|
32
|
+
.fv-bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:space-between;padding:8px 10px;border-bottom:1px solid var(--line);background:var(--surface)}
|
|
33
|
+
.fv-hint{font-size:12px;color:var(--ink-3)}
|
|
34
|
+
.fv-btns{display:flex;gap:6px;align-items:center}
|
|
35
|
+
.fv-btns button{font:inherit;font-size:13px;min-width:34px;padding:5px 10px;border:1px solid var(--line);border-radius:6px;background:var(--bg);color:var(--ink-1);cursor:pointer}
|
|
36
|
+
.fv-btns button:hover{border-color:var(--ink-3)}
|
|
37
|
+
.fv-btns button:focus-visible{outline:2px solid #2563eb;outline-offset:2px}
|
|
38
|
+
.fv-pct{font-family:var(--mono);font-size:12px;color:var(--ink-2);min-width:44px;text-align:center}
|
|
39
|
+
.fv-view{position:relative;height:70vh;min-height:420px;overflow:hidden;cursor:grab;touch-action:pan-y;background:var(--canvas)}
|
|
40
|
+
.fv-view.drag{cursor:grabbing}
|
|
41
|
+
.fv-stage{position:absolute;left:0;top:0;transform-origin:0 0}
|
|
42
|
+
.fv-stage > p{color:#57534e;padding:12px}
|
|
43
|
+
/* The title is HTML inside the zoomable stage, so it zooms with the chart and stays in full screen and screenshots. */
|
|
44
|
+
.fv-title{font-size:22px;line-height:1.35;color:#1c1917;padding:8px 0 14px;text-align:center}
|
|
45
|
+
.fv-title b{font-weight:700}
|
|
46
|
+
.fv-title span{display:block;font-size:15px;color:#57534e}
|
|
47
|
+
.fv-stage svg{max-width:none!important;display:block}
|
|
48
|
+
.fv.expanded{position:fixed;inset:0;z-index:50;border-radius:0}
|
|
49
|
+
.fv.expanded .fv-view{height:auto;flex:1}
|
|
50
|
+
</style>
|
|
51
|
+
</head>
|
|
52
|
+
<body>
|
|
53
|
+
<div class="wrap">
|
|
54
|
+
<h1>How support cases moved on Mon 5 Oct 2026</h1>
|
|
55
|
+
<p class="small">Example data. Replace the title, the legend, the chart source and the note.</p>
|
|
56
|
+
|
|
57
|
+
<div class="legendrow" aria-label="Legend">
|
|
58
|
+
<span><span class="sw" style="background:#e7e5e4"></span>Automatic step</span>
|
|
59
|
+
<span><span class="sw thick" style="background:#bbf7d0;border-color:#15803d"></span>In scope (thick border)</span>
|
|
60
|
+
<span><span class="sw dash" style="background:#fde68a;border-color:#a16207"></span>Optional, owner choice (dashed)</span>
|
|
61
|
+
<span><span class="sw" style="background:#f6e7d6"></span>Hand-off to a team</span>
|
|
62
|
+
<span><span class="sw" style="background:#ffffff;border-color:#57534e;border-width:2px"></span>Start</span>
|
|
63
|
+
<span><span class="sw" style="background:#eceae6"></span>Case ends</span>
|
|
64
|
+
<span><span class="sw dash" style="background:#ffffff;border-color:#a8a29e;border-width:1px"></span>Stub: the path continues at the box named</span>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<!-- One chart. data-src = id of the script that holds the Mermaid source.
|
|
68
|
+
data-title and data-sub are plain text (set with textContent). data-label is the accessible name. -->
|
|
69
|
+
<div class="fv" data-src="chart1"
|
|
70
|
+
data-title="Support cases: Mon 5 Oct 2026"
|
|
71
|
+
data-sub="(4 Oct 23:00 to 5 Oct 23:00 UTC). Counts in brackets are cases. Paths under 20 cases are not drawn."
|
|
72
|
+
data-label="Flow of a support case on 5 October, from intake to self-service, team queues and case ends">
|
|
73
|
+
<div class="fv-bar">
|
|
74
|
+
<span class="fv-hint">Drag to move. Use + and − or Ctrl + scroll to zoom.</span>
|
|
75
|
+
<span class="fv-btns">
|
|
76
|
+
<button type="button" data-z="out" aria-label="Zoom out">−</button>
|
|
77
|
+
<span class="fv-pct">100%</span>
|
|
78
|
+
<button type="button" data-z="in" aria-label="Zoom in">+</button>
|
|
79
|
+
<button type="button" data-z="fit">Fit</button>
|
|
80
|
+
<button type="button" data-z="full">Full screen</button>
|
|
81
|
+
</span>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="fv-view"><div class="fv-stage"><p class="small">Drawing the flow chart…</p></div></div>
|
|
84
|
+
</div>
|
|
85
|
+
<script type="text/plain" id="chart1">
|
|
86
|
+
flowchart TD
|
|
87
|
+
S(["Cases opened<br/>(2,400)"]) --> I["INTAKE<br/>Web form"]
|
|
88
|
+
I -->|"(180)"| X1(["Case ends"])
|
|
89
|
+
I -->|"(2,220)"| T["TRIAGE<br/>Rules engine<br/>(2,220)"]
|
|
90
|
+
T -->|"(1,300)"| A["SELF-SERVICE<br/>Account help<br/>(1,300)"]
|
|
91
|
+
T -->|"(about 600)"| Q["Team queues"]
|
|
92
|
+
T -->|"(320)"| C["CHECK, owner choice<br/>Fraud check"]
|
|
93
|
+
C -->|"(300)"| Q2["Fraud team queue"]
|
|
94
|
+
A -->|"(1,100)"| X2(["Case ends after self-service"])
|
|
95
|
+
A --> B(["back to Triage (190)"])
|
|
96
|
+
subgraph AFTER["After a team picks up a case"]
|
|
97
|
+
direction LR
|
|
98
|
+
AG["Agent"] -->|"(700)"| X3(["Case ends"])
|
|
99
|
+
end
|
|
100
|
+
X2 ~~~ AFTER
|
|
101
|
+
classDef auto fill:#e7e5e4,stroke:#78716c,color:#1c1917;
|
|
102
|
+
classDef scope fill:#bbf7d0,stroke:#15803d,stroke-width:3px,color:#1c1917;
|
|
103
|
+
classDef choice fill:#fde68a,stroke:#a16207,stroke-width:2px,stroke-dasharray:6 4,color:#1c1917;
|
|
104
|
+
classDef queue fill:#f6e7d6,stroke:#b45309,color:#1c1917;
|
|
105
|
+
classDef endn fill:#eceae6,stroke:#78716c,color:#1c1917;
|
|
106
|
+
classDef startn fill:#ffffff,stroke:#57534e,stroke-width:2px,color:#1c1917;
|
|
107
|
+
classDef stub fill:#ffffff,stroke:#a8a29e,stroke-dasharray:3 3,color:#57534e;
|
|
108
|
+
class I,T auto;
|
|
109
|
+
class A scope;
|
|
110
|
+
class C choice;
|
|
111
|
+
class Q,Q2,AG queue;
|
|
112
|
+
class X1,X2,X3 endn;
|
|
113
|
+
class S startn;
|
|
114
|
+
class B stub;
|
|
115
|
+
style AFTER fill:#fbfaf8,stroke:#a8a29e,stroke-dasharray:4 4,color:#57534e;
|
|
116
|
+
</script>
|
|
117
|
+
<p class="small">Every number counts distinct cases: an arrow counts the cases that took that path, a box counts the cases that reached it. Arrows into a box can sum to a little more than the box, because a case can reach it by two paths. "Case ends" means the data shows no further step; it does not say why. "Team queues" merges four small queues (about 600). Not drawn: 3 paths with fewer than 20 cases each (41 in total).</p>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.9.1/mermaid.min.js" integrity="sha512-6a80OTZVmEJhqYJUmYd5z8yHUCDlYnj6q9XwB/gKOEyNQV/Q8u+XeSG59a2ZKFEHGTYzgfOQKYEBtrZV7vBr+Q==" crossorigin="anonymous"></script>
|
|
121
|
+
<script>
|
|
122
|
+
(function(){
|
|
123
|
+
if(!window.mermaid){
|
|
124
|
+
document.querySelectorAll('.fv-stage').forEach(function(s){s.innerHTML='<p class="small">The chart library did not load, so the flow chart cannot be drawn.</p>';});
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
try{
|
|
128
|
+
mermaid.initialize({startOnLoad:false,securityLevel:'strict',theme:'base',
|
|
129
|
+
themeVariables:{fontFamily:'Segoe UI, system-ui, sans-serif',fontSize:'15px',lineColor:'#57534e',primaryTextColor:'#1c1917',edgeLabelBackground:'#ffffff',background:'#fbfaf8'},
|
|
130
|
+
flowchart:{htmlLabels:true,curve:'basis',nodeSpacing:40,rankSpacing:55,padding:10}});
|
|
131
|
+
}catch(e){console.error('mermaid.initialize failed',e);}
|
|
132
|
+
var n=0;
|
|
133
|
+
function viewer(fv){
|
|
134
|
+
var view=fv.querySelector('.fv-view'),stage=fv.querySelector('.fv-stage'),pct=fv.querySelector('.fv-pct'),full=fv.querySelector('button[data-z="full"]');
|
|
135
|
+
var k=1,x=0,y=0,w=0,h=0,fitK=1;
|
|
136
|
+
function msg(t){stage.innerHTML='<p class="small"></p>';stage.firstChild.textContent=t;}
|
|
137
|
+
// Page scroll stays free on a phone until the chart is zoomed in or in full screen; then a drag pans the chart.
|
|
138
|
+
function touch(){view.style.touchAction=(fv.classList.contains('expanded')||k>fitK*1.01)?'none':'pan-y';}
|
|
139
|
+
function apply(){touch();stage.style.transform='translate('+x+'px,'+y+'px) scale('+k+')';pct.textContent=Math.round(k*100)+'%';}
|
|
140
|
+
function fit(){var vw=view.clientWidth,vh=view.clientHeight;if(!w)return;k=Math.min(vw/w,vh/h)*0.96;fitK=k;x=(vw-w*k)/2;y=(vh-h*k)/2;apply();}
|
|
141
|
+
function zoomAt(f,cx,cy){var nk=Math.max(Math.min(0.15,fitK),Math.min(4,k*f));x=cx-(cx-x)*nk/k;y=cy-(cy-y)*nk/k;k=nk;apply();}
|
|
142
|
+
function title(){
|
|
143
|
+
var d=document.createElement('div');d.className='fv-title';
|
|
144
|
+
var b=document.createElement('b');b.textContent=fv.dataset.title||'';d.appendChild(b);
|
|
145
|
+
if(fv.dataset.sub){var s=document.createElement('span');s.textContent=fv.dataset.sub;d.appendChild(s);}
|
|
146
|
+
return d;
|
|
147
|
+
}
|
|
148
|
+
var src=document.getElementById(fv.dataset.src);
|
|
149
|
+
var done=Promise.resolve();
|
|
150
|
+
if(!src){msg('The flow chart source is missing.');}
|
|
151
|
+
else try{
|
|
152
|
+
done=mermaid.render('flowsvg'+(n++),src.textContent).then(function(r){
|
|
153
|
+
stage.innerHTML=r.svg;stage.insertBefore(title(),stage.firstChild);
|
|
154
|
+
var svg=stage.querySelector('svg');
|
|
155
|
+
var vb=svg.viewBox.baseVal;w=vb&&vb.width?vb.width:svg.getBBox().width;h=vb&&vb.height?vb.height:svg.getBBox().height;
|
|
156
|
+
svg.setAttribute('width',w);svg.setAttribute('height',h);svg.style.maxWidth='none';
|
|
157
|
+
w=Math.max(w,stage.scrollWidth);h=stage.scrollHeight;
|
|
158
|
+
svg.setAttribute('role','img');svg.setAttribute('aria-label',fv.dataset.label||'Flow chart');
|
|
159
|
+
fit();
|
|
160
|
+
}).catch(function(e){console.error(e);msg('The flow chart could not be drawn: '+e.message);});
|
|
161
|
+
}catch(e){console.error(e);msg('The flow chart could not be drawn: '+e.message);}
|
|
162
|
+
function toggle(){var on=!fv.classList.contains('expanded');fv.classList.toggle('expanded',on);full.textContent=on?'Close':'Full screen';
|
|
163
|
+
document.body.style.overflow=on?'hidden':'';
|
|
164
|
+
// Real full screen when the browser allows it; the fixed-position .expanded class is the fallback.
|
|
165
|
+
try{if(on&&fv.requestFullscreen)fv.requestFullscreen().catch(function(){});else if(!on&&document.fullscreenElement)document.exitFullscreen().catch(function(){});}catch(e){}
|
|
166
|
+
setTimeout(fit,60);}
|
|
167
|
+
fv.addEventListener('click',function(ev){var b=ev.target.closest('button[data-z]');if(!b)return;var c=[view.clientWidth/2,view.clientHeight/2];
|
|
168
|
+
if(b.dataset.z==='in')zoomAt(1.25,c[0],c[1]);else if(b.dataset.z==='out')zoomAt(0.8,c[0],c[1]);else if(b.dataset.z==='fit')fit();else toggle();});
|
|
169
|
+
document.addEventListener('fullscreenchange',function(){if(!document.fullscreenElement&&fv.classList.contains('expanded')){fv.classList.remove('expanded');full.textContent='Full screen';document.body.style.overflow='';setTimeout(fit,60);}});
|
|
170
|
+
document.addEventListener('keydown',function(e){if(e.key==='Escape'&&fv.classList.contains('expanded'))toggle();});
|
|
171
|
+
// Plain scroll keeps scrolling the page; Ctrl + scroll (or any scroll in full screen) zooms.
|
|
172
|
+
view.addEventListener('wheel',function(e){if(!(e.ctrlKey||e.metaKey||fv.classList.contains('expanded')))return;e.preventDefault();var r=view.getBoundingClientRect();zoomAt(e.deltaY<0?1.12:0.89,e.clientX-r.left,e.clientY-r.top);},{passive:false});
|
|
173
|
+
var pts={};
|
|
174
|
+
view.addEventListener('pointerdown',function(e){view.setPointerCapture(e.pointerId);pts[e.pointerId]=[e.clientX,e.clientY];view.classList.add('drag');});
|
|
175
|
+
view.addEventListener('pointermove',function(e){if(!pts[e.pointerId])return;var p=pts[e.pointerId],ids=Object.keys(pts);
|
|
176
|
+
if(ids.length===2){var o=pts[ids[0]==e.pointerId?ids[1]:ids[0]];var d0=Math.hypot(p[0]-o[0],p[1]-o[1]);var d1=Math.hypot(e.clientX-o[0],e.clientY-o[1]);var r=view.getBoundingClientRect();if(d0>0)zoomAt(d1/d0,(e.clientX+o[0])/2-r.left,(e.clientY+o[1])/2-r.top);}
|
|
177
|
+
else{x+=e.clientX-p[0];y+=e.clientY-p[1];apply();}
|
|
178
|
+
pts[e.pointerId]=[e.clientX,e.clientY];});
|
|
179
|
+
function up(e){delete pts[e.pointerId];if(!Object.keys(pts).length)view.classList.remove('drag');}
|
|
180
|
+
view.addEventListener('pointerup',up);view.addEventListener('pointercancel',up);
|
|
181
|
+
window.addEventListener('resize',fit);
|
|
182
|
+
return done;
|
|
183
|
+
}
|
|
184
|
+
// Render one chart after another, so each chart finishes before the next one starts.
|
|
185
|
+
var all=document.querySelectorAll('.fv[data-src]'),i=0;
|
|
186
|
+
(function next(){if(i<all.length){Promise.resolve(viewer(all[i++])).then(next,next);}})();
|
|
187
|
+
})();
|
|
188
|
+
</script>
|
|
189
|
+
</body>
|
|
190
|
+
</html>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Edge list from an event table (SQL pattern)
|
|
2
|
+
|
|
3
|
+
Use this when the steps of each case are rows in an event table. The output is the edge list that
|
|
4
|
+
`scripts/edges_to_mermaid.py` reads: `from,to,count`, plus a box-count list `name,count`.
|
|
5
|
+
|
|
6
|
+
The SQL is ANSI with window functions. It runs on most SQL engines with small changes
|
|
7
|
+
(parameter style, date functions).
|
|
8
|
+
|
|
9
|
+
## Input shape
|
|
10
|
+
|
|
11
|
+
One row per step a case took:
|
|
12
|
+
|
|
13
|
+
| column | meaning |
|
|
14
|
+
|-------------|-----------------------------------------------------------------------------|
|
|
15
|
+
| `case_id` | the unit you count: a call, an order, a ticket, a session |
|
|
16
|
+
| `step` | the step name, in the words of the team that owns the process |
|
|
17
|
+
| `step_time` | when the step started, in UTC |
|
|
18
|
+
| `step_seq` | a tie-breaker when two steps share one timestamp (an event id or a counter) |
|
|
19
|
+
|
|
20
|
+
## Query
|
|
21
|
+
|
|
22
|
+
```sql
|
|
23
|
+
-- 1. Choose the cases by their START time in the window, then take ALL their steps.
|
|
24
|
+
-- If you filter steps by time instead, a case that crosses the window edge loses its
|
|
25
|
+
-- first or last steps, and the chart shows false starts and false ends.
|
|
26
|
+
WITH cases AS (
|
|
27
|
+
SELECT case_id
|
|
28
|
+
FROM events
|
|
29
|
+
GROUP BY case_id
|
|
30
|
+
HAVING MIN(step_time) >= :window_start_utc
|
|
31
|
+
AND MIN(step_time) < :window_end_utc
|
|
32
|
+
),
|
|
33
|
+
ordered AS (
|
|
34
|
+
SELECT e.case_id, e.step, e.step_time, e.step_seq,
|
|
35
|
+
LAG(e.step) OVER (PARTITION BY e.case_id ORDER BY e.step_time, e.step_seq) AS prev_step
|
|
36
|
+
FROM events e
|
|
37
|
+
JOIN cases c ON c.case_id = e.case_id
|
|
38
|
+
),
|
|
39
|
+
-- 2. Collapse consecutive repeats: A, A, B becomes A, B. A repeat is not a path.
|
|
40
|
+
collapsed AS (
|
|
41
|
+
SELECT case_id, step, step_time, step_seq
|
|
42
|
+
FROM ordered
|
|
43
|
+
WHERE prev_step IS NULL OR prev_step <> step
|
|
44
|
+
),
|
|
45
|
+
-- 3. The next step of each step. No next step means the case ends: 'END'.
|
|
46
|
+
pairs AS (
|
|
47
|
+
SELECT case_id,
|
|
48
|
+
step AS from_step,
|
|
49
|
+
COALESCE(LEAD(step) OVER (PARTITION BY case_id ORDER BY step_time, step_seq), 'END') AS to_step,
|
|
50
|
+
ROW_NUMBER() OVER (PARTITION BY case_id ORDER BY step_time, step_seq) AS rn
|
|
51
|
+
FROM collapsed
|
|
52
|
+
)
|
|
53
|
+
-- 4. Count DISTINCT cases per pair: the same unit as the boxes. Add one START row per case.
|
|
54
|
+
-- path_count (times taken) is kept only to compare; do not draw it.
|
|
55
|
+
SELECT from_step AS "from", to_step AS "to",
|
|
56
|
+
COUNT(DISTINCT case_id) AS "count",
|
|
57
|
+
COUNT(*) AS path_count
|
|
58
|
+
FROM pairs
|
|
59
|
+
GROUP BY from_step, to_step
|
|
60
|
+
HAVING COUNT(DISTINCT case_id) >= :min_cases
|
|
61
|
+
UNION ALL
|
|
62
|
+
SELECT 'START', from_step, COUNT(*), COUNT(*)
|
|
63
|
+
FROM pairs
|
|
64
|
+
WHERE rn = 1
|
|
65
|
+
GROUP BY from_step
|
|
66
|
+
ORDER BY 3 DESC;
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Box counts (distinct cases per step), from the same `collapsed` set:
|
|
70
|
+
|
|
71
|
+
```sql
|
|
72
|
+
SELECT step AS name, COUNT(DISTINCT case_id) AS count
|
|
73
|
+
FROM collapsed
|
|
74
|
+
GROUP BY step;
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## What the numbers mean
|
|
78
|
+
|
|
79
|
+
- **Draw distinct cases on the arrows, the same unit as the boxes.** `count` is
|
|
80
|
+
`COUNT(DISTINCT case_id)` per pair. `path_count` (`COUNT(*)`) counts each time a path was taken,
|
|
81
|
+
so a case that loops (A, B, A, B) counts twice. A business reader then sees a box of 8,538
|
|
82
|
+
followed by an arrow of 8,541 and stops trusting the chart. On one measured day `COUNT(*)`
|
|
83
|
+
overstated an arrow by 4%. Keep `path_count` only to compare.
|
|
84
|
+
- **Self-check: no arrow into or out of a box exceeds the box's distinct count.** If one does,
|
|
85
|
+
the query counts occurrences, not cases. The converter warns on this when it has box counts.
|
|
86
|
+
- **Arrows into a box can sum to a little more than the box** when a case reaches it by two
|
|
87
|
+
paths. That is correct; say so in the note under the chart.
|
|
88
|
+
- **`HAVING` hides small paths.** Apply the threshold to either count, but label every number on
|
|
89
|
+
the chart as cases. Run the query once without it, or sum the hidden rows, so the
|
|
90
|
+
note under the chart can name what is not drawn and how many cases that is. A chart that hides
|
|
91
|
+
paths without saying so looks complete and is not.
|
|
92
|
+
- **The tie-breaker matters.** Without `step_seq`, two steps at one timestamp come out in any
|
|
93
|
+
order, and `LAG`/`LEAD` give different paths on each run. Check: run the query twice and compare.
|
|
94
|
+
- **Time zone.** Filter in UTC. When the reader thinks in local days, convert the local day to a
|
|
95
|
+
UTC window first, and put both on the chart title, for example
|
|
96
|
+
`Mon 5 Oct 2026 (4 Oct 23:00 to 5 Oct 23:00 UTC)`.
|
|
97
|
+
|
|
98
|
+
## Check the edge list before you draw it
|
|
99
|
+
|
|
100
|
+
- The `START` rows add up to the number of cases in the window.
|
|
101
|
+
- Before the `HAVING` filter, for each step the paths in equal the paths out (with `END`
|
|
102
|
+
counted as out). A gap means a filter dropped rows, or a step name differs in spelling
|
|
103
|
+
between rows.
|
|
104
|
+
- Run the converter: its note lists every hidden path. Read that list. A large hidden total means
|
|
105
|
+
the minimum is too high, or many small steps need to be merged into one named box.
|