@liggi/figkit 0.0.0-stage → 0.1.0

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/GUIDE.md ADDED
@@ -0,0 +1,273 @@
1
+ # figkit: explanatory images
2
+
3
+ Write a small HTML file, run the command below, and get a PNG to embed. The browser does the layout, so you never
4
+ work out coordinates or text widths: boxes size to their text, arrows route themselves around boxes, and the checks
5
+ measure the real result.
6
+
7
+ ```
8
+ npx figkit figure.html [--out figure.png]
9
+ ```
10
+
11
+ The file is just the `<main>` below; no doctype or `<head>` needed. Without `--out` the PNG goes to a temporary folder; the command prints where.
12
+ It prints the PNG, a preview of how small it sits inline on a phone (`figure-inline.png`, for looking at, never to
13
+ embed), and a list of problems. Fix every problem, open
14
+ the images and look, then embed the PNG: `![what it shows](/abs/path.png)`.
15
+
16
+ ## Before you draw
17
+
18
+ - One figure, one point. Write the sentence it must get across; that sentence is the `<h1>`, 14 words at most. Two points are two figures.
19
+ - Draw only when position carries meaning: a flow, parts of a system, what owns or contains what, a chart, a before
20
+ and after. Steps and comparisons with no shape are markdown lists and tables.
21
+ - Use real names and real numbers. Numbers that relate go in a shape (a split bar, bars, a chart), not a sentence.
22
+ - One colour for what the reader must notice (`accent`), at most one more with a second meaning (`second`), everything
23
+ else grey. Say what each colour means in the reading line, in that colour. `good` and `bad` are for results only.
24
+ - Cut every word that explains how you got there. Labels, not sentences.
25
+
26
+ ## Explaining a system
27
+
28
+ A question about structure ("what are the entities?", "where does X live?", "how do these relate?", "what loads
29
+ what?", "walk me through it") is answered with a picture first, not with numbered steps.
30
+
31
+ - Each figure is a plain `fig` with at most 5 boxes and no `group`. More than that is two or three figures in a row,
32
+ not one map: what exists, then one case through it, then what changes. A process or machine the code runs in is a
33
+ `tag` on the box, not a box around it. Use `fig detail` only when the reader asked for the whole map.
34
+ - Under each figure, write only what it cannot show: why, the exceptions, what to do. Never retell it as steps.
35
+ - Put a real instance in each box, with its kind as the smaller line: "Refunds over £500" over "Approval rule",
36
+ not a box that says only "Rule".
37
+ - Label where every flow starts and where its input comes from. An arrow that appears from nowhere gets asked about.
38
+ - When one thing feeds several, draw the copies: three workers each loading the catalogue is three copies and
39
+ three arrows; one shared copy is one box fanning out to the three. "How many" must be visible without reading.
40
+ - Name each thing once, and use that name in the title, the boxes and your prose.
41
+
42
+ ## Charts from a spec
43
+
44
+ When the whole figure is one chart of a type below, write a JSON spec instead of HTML. You give the data; figkit
45
+ does the layout, colours and labels. Use HTML (the rest of this guide) for anything else, including a chart that sits
46
+ beside boxes or arrows.
47
+
48
+ ```
49
+ npx figkit chart.json [--out chart.png]
50
+ ```
51
+
52
+ A spec that breaks a rule is refused with one line per problem; fix them and run it again. Then look at the images
53
+ and embed the PNG, as for any figure.
54
+
55
+ `split`: parts of a whole, as one bar. 2 to 8 parts that add up to the whole; group the small ones as "Other".
56
+
57
+ ```json
58
+ { "type": "split", "title": "Search brings in most October trial sign-ups",
59
+ "note": "By where the visitor came from", "unit": "%",
60
+ "parts": [ { "label": "Search", "value": 58, "tone": "accent" }, { "label": "Referral", "value": 27 },
61
+ { "label": "Ads", "value": 11 }, { "label": "Other", "value": 4 } ] }
62
+ ```
63
+
64
+ `bars`: a few numbers compared, one bar each from zero. Up to 12 bars.
65
+
66
+ ```json
67
+ { "type": "bars", "title": "The new checkout halves the time to pay",
68
+ "note": "Median time from cart to payment", "unit": " s",
69
+ "bars": [ { "label": "Current checkout", "value": 86 }, { "label": "New checkout", "value": 41, "tone": "good" } ] }
70
+ ```
71
+
72
+ `before-after`: one measure that changed, the two values side by side on one scale; `note` (required) says what
73
+ the number counts. For several things changing,
74
+ use `bars` or `dumbbell`.
75
+
76
+ ```json
77
+ { "type": "before-after", "title": "Late deliveries fell by three quarters after the route change",
78
+ "note": "Share of orders delivered late", "unit": "%",
79
+ "before": { "label": "Before 3 March", "value": 18 }, "after": { "label": "From 3 March", "value": 4.5, "tone": "good" } }
80
+ ```
81
+
82
+ `dumbbell`: up to 8 rows, each a move from one value to another (dates or numbers), drawn as an arrow with the gap
83
+ labelled. `fromLabel` and `toLabel` name the two ends once. `gap` on a row replaces the computed gap text.
84
+
85
+ ```json
86
+ { "type": "dumbbell", "title": "Every spring release shipped later than planned",
87
+ "fromLabel": "Planned", "toLabel": "Shipped",
88
+ "rows": [ { "label": "Offline mode", "from": "2026-03-02", "to": "2026-03-09" },
89
+ { "label": "Shared folders", "from": "2026-03-16", "to": "2026-04-06" } ] }
90
+ ```
91
+
92
+ `series`: a line over time (or any ordered x) with exactly one point marked and labelled. Points in order of x; the
93
+ mark's `x` must be one of them. The y axis starts at zero unless `"zero": false`.
94
+
95
+ ```json
96
+ { "type": "series", "title": "Launch day brought twice the usual sign-ups",
97
+ "note": "Trial sign-ups per day",
98
+ "points": [ { "x": "2026-10-12", "y": 96 }, { "x": "2026-10-13", "y": 91 }, { "x": "2026-10-14", "y": 212 },
99
+ { "x": "2026-10-15", "y": 131 }, { "x": "2026-10-16", "y": 98 } ],
100
+ "mark": { "x": "2026-10-14", "label": "212, launch day" } }
101
+ ```
102
+
103
+ Flows (boxes and arrows) have no spec: write them in HTML, below.
104
+
105
+ - `title` (required) is the point, 14 words at most. `note` is the reading line under it; `source` a footer.
106
+ - `unit` is printed straight after each number. `show` on an item replaces its printed value, e.g. `"11 of 29"`.
107
+ - `tone` is `accent` for what to notice, `good` or `bad` when it is a result; at most two tones in a chart. Leave the rest grey.
108
+ - `bars` takes `max` to fix the end of the axis (e.g. 100 for percentages) and a short `note` per bar.
109
+ - Labels are plain text, 40 characters at most. Put bars in reading order (before, then after).
110
+ - Examples: `examples/charts/` in the figkit package.
111
+
112
+ ## The page
113
+
114
+ ```html
115
+ <main class="fig"> <!-- add "detail" for a map to open and read; "paper" for a light figure for docs -->
116
+ <header>
117
+ <h1>The point, stated. <em>The part to notice</em></h1>
118
+ <p class="read">How to read it. <b class="t-accent">Orange</b> means code.</p>
119
+ </header>
120
+ ... the picture ...
121
+ <footer><p>Source and date, if the reader needs them.</p></footer>
122
+ </main>
123
+ ```
124
+
125
+ - In the `<h1>`, `<em>` turns the words accent-coloured italic and `<br>` sets a line break.
126
+ - `fig` is a glance figure, 960 wide: one point, big type, reads inline on a phone. At most 8 boxes, 8 arrows and 70 words.
127
+ - `fig detail` is 1360 wide, for a flow or map the reader asked for and opens full screen; to explain a system, use
128
+ several `fig` figures instead (see Explaining a system). At most 18 boxes, 18 arrows and 190 words.
129
+ Widen it with `style="--w: 1560px"` only when a row will not fit.
130
+ - Height follows the content. There is no fixed canvas to fill.
131
+ - Text must be at least 15px; the theme's sizes all pass. The phone line the command prints is information, and only the problems list needs fixing.
132
+
133
+ ## Sides that stack on a phone
134
+
135
+ Two or three columns compared side by side (before and after, now and proposed) shrink to unreadable text inline on
136
+ a phone. Add `sides` to the row or grid that holds them, `<div class="grid sides" style="--cols: 2">`, and the command
137
+ also draws a phone layout, `figure.phone.png`, with the columns one above the other at a narrower width. Embed only
138
+ `figure.png`; a viewer that knows the convention shows the phone layout on narrow screens, and any other shows the
139
+ figure. The checks run on both layouts.
140
+
141
+ - Use it for columns that each make sense alone. Arrows from one column to another get routed round the stack on a
142
+ phone, and the checks report it; leave those figures side by side.
143
+ - If the command says your columns will shrink on a phone, it found columns with no arrows between them that are not
144
+ marked as sides.
145
+
146
+ ## Layout: ordinary CSS
147
+
148
+ Use flex and grid; write inline `style` freely.
149
+
150
+ | Helper | Effect |
151
+ |---|---|
152
+ | `row`, `col` | flex row or column. Children of a row stretch to equal height, with text at the top |
153
+ | `grid` | equal columns; set `--cols` |
154
+ | `grow` | child takes an equal share of the width. Long labels then wrap, and no check flags that |
155
+ | `fit` | child keeps its natural width |
156
+ | `middle`, `top`, `bottom` | cross-axis alignment; `middle` stops boxes of different text length stretching |
157
+ | `center`, `between` | main-axis centring or space-between |
158
+ | `tight` | an 8px gap |
159
+ | `arrows`, `wide` | the gap for arrows; `wide` is 1.6 times `arrows`, for arrows with labels |
160
+
161
+ A row or column whose children are joined by arrows needs room for them: add `arrows` or `wide`, or set `column-gap`
162
+ to 44px or more. `between` combines with either. Put things that should line up in the same row, column or grid;
163
+ never nudge with margins.
164
+
165
+ ## Boxes
166
+
167
+ ```html
168
+ <div class="node accent" id="route">
169
+ <h3>Route to a carrier</h3>
170
+ <p>A table instead of code</p>
171
+ <span class="tag">routing.yaml</span>
172
+ </div>
173
+ ```
174
+
175
+ | Class | What it is |
176
+ |---|---|
177
+ | `node` | a box; add `accent`, `second`, `good`, `bad` for its colour |
178
+ | `node fill` | solid colour: the one thing to notice, or a start and end point |
179
+ | `node tint` | a wash of its colour: marks a set of boxes that share something, more quietly than `fill` |
180
+ | `node dashed` / `quiet` | optional or background things |
181
+ | `node bare` | text with no box that arrows can still attach to |
182
+ | `node pill` | rounded start or end; its text is centred, other nodes are left-aligned |
183
+ | `decision` | a diamond; put the question in a `<span>`, keep it short |
184
+ | `group` | a dashed container; its first child `<span class="label">` names it. `group flush` lines its contents up with the boxes outside it |
185
+ | `num` | a big number inside a node |
186
+ | `tag` | a small mono line: where something lives |
187
+ | `step` | a numbered badge on a node's corner |
188
+ | `note` | loose grey text; `lane-title` a column heading |
189
+
190
+ ## Arrows
191
+
192
+ ```html
193
+ <fig-arrow from="route" to="load" label="draft"></fig-arrow>
194
+ <fig-arrow from="check" to="route" accent label="Yes: start again" from-side="top" to-side="top"></fig-arrow>
195
+ ```
196
+
197
+ Put them anywhere inside the figure; `from` and `to` are element ids. Any element can be an end: a node, a group, a
198
+ `mark` on a screenshot, a `<span>` of text. Arrows keep clear of boxes and loose text, and arrows sharing an end and
199
+ a colour share a trunk.
200
+
201
+ - colour: `accent`, `second`, `good`, `bad`; `dashed`; `weight="3"`; `both` for two heads; `head="none"`
202
+ - `blocked`: a route that does not exist. The line stops short of the target at a cross, with no arrowhead
203
+ - `from-side` / `to-side`: `top`, `right`, `bottom`, `left` when the automatic side reads wrong
204
+ - `from-at` / `to-at`: 0 to 1 along that side, to separate two arrows on one side, such as a pair going there and back
205
+ - `route="curve"` for a loose pointer such as a note to a screenshot; `route="straight"` for a direct line
206
+ - `label-at="start"` or `"end"` to move a label
207
+
208
+ If the checks report a crossing or a route through a box, move the boxes; do not fight it with more attributes.
209
+
210
+ ## Numbers
211
+
212
+ Parts of a whole:
213
+
214
+ ```html
215
+ <fig-split unit="%">
216
+ <part value="58.6" accent>National post</part>
217
+ <part value="32.3">Parcel lockers</part>
218
+ <part value="9.1">Other</part>
219
+ </fig-split>
220
+ ```
221
+
222
+ Segments too thin for their label move it to a line under the bar. `show="11 of 29"` overrides the printed value.
223
+ `unit` is printed straight after the number, so write `unit=" s"` for a space. Parts must add up to a whole; for a real
224
+ time axis with marked points, use Plot below.
225
+
226
+ A few numbers compared:
227
+
228
+ ```html
229
+ <fig-bars unit="%" max="100">
230
+ <bar value="58.6" accent>National post</bar>
231
+ <bar value="32.3" note="cheapest">Parcel lockers</bar>
232
+ </fig-bars>
233
+ ```
234
+
235
+ Anything else (lines, dots, intervals, distributions, small multiples) is Observable Plot, already loaded with d3:
236
+
237
+ ```html
238
+ <div id="chart"></div>
239
+ <script>
240
+ fig.plot("#chart", ({ Plot, d3, width, theme }) => ({
241
+ height: 360, marginLeft: 44,
242
+ x: { label: null }, y: { label: null, grid: true, ticks: 4 },
243
+ marks: [
244
+ Plot.lineY(rows, { x: "day", y: "n", stroke: theme.soft, strokeWidth: 2.5 }),
245
+ Plot.dot([peak], { x: "day", y: "n", fill: theme.accent, r: 6 }),
246
+ Plot.text([peak], { x: "day", y: "n", text: () => "131 on 3 Feb", dx: 12, textAnchor: "start", fill: theme.accent, fontWeight: 600, fontSize: 17 }),
247
+ ],
248
+ }));
249
+ </script>
250
+ ```
251
+
252
+ `theme` has `ink`, `muted`, `soft`, `line`, `accent`, `second`, `good`, `bad`. Draw the series in `soft`, the thing to
253
+ notice in `accent`, and label it directly on the chart; no legends. Start bar axes at zero.
254
+
255
+ ## Screenshots
256
+
257
+ ```html
258
+ <div class="shot"><img src="/abs/path/page.png">
259
+ <div class="mark" id="m1" style="left: 3%; top: 30%; width: 94%; height: 36%"></div>
260
+ </div>
261
+ <div class="node bare" id="n1"><h3>One row per measure</h3></div>
262
+ <fig-arrow from="n1" to="m1" accent route="curve"></fig-arrow>
263
+ ```
264
+
265
+ ## What the checks catch, and what they cannot
266
+
267
+ Measured in the rendered page: text past its box or the frame, text too small, boxes and labels that overlap, arrows
268
+ through boxes, arrows crossing, edges that almost line up, too many words, boxes, arrows or colours, facts strung
269
+ together with separators, a missing or long title.
270
+
271
+ They cannot tell whether the picture is clear, true to the mechanism, or worth drawing. Look at the images yourself,
272
+ and before an important figure goes out, give the PNGs, without your reasoning, to someone who has not seen them
273
+ and ask what they take from it.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jason Liggi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,99 @@
1
- # Temporary Holding Version
1
+ # figkit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Explanatory figures from a little HTML or a chart spec. You write boxes, arrows and labels as ordinary HTML (or give a
4
+ chart's data as JSON); a headless browser does the layout, figkit routes the arrows, measures the result and tells you
5
+ what is wrong with it: text that is too small or runs out of its box, boxes that overlap, arrows that cross, too many
6
+ words for one picture. You get a PNG to embed in a chat, a doc or an issue.
7
+
8
+ It was written for AI agents that explain things to people, so the rules live in one file an agent can read
9
+ ([GUIDE.md](GUIDE.md)), and every problem comes back as a sentence that says what to change. People can use it the
10
+ same way.
11
+
12
+ ## Install
13
+
14
+ Node 20 or later, and a Chromium-based browser: figkit uses Chrome or Edge if one is installed, otherwise run
15
+ `npx playwright install chromium` once.
16
+
17
+ ```
18
+ npm install @liggi/figkit
19
+ npx figkit guide # prints GUIDE.md, the rules for writing figures
20
+ ```
21
+
22
+ ## A figure from HTML
23
+
24
+ Save this as `order.html`:
25
+
26
+ ```html
27
+ <main class="fig">
28
+ <header>
29
+ <h1>An order is <em>checked before it is charged</em></h1>
30
+ </header>
31
+ <div class="row wide">
32
+ <div class="node pill" id="cart"><h3>Basket</h3><p>3 items</p></div>
33
+ <div class="node accent" id="check"><h3>Fraud check</h3><p>Holds 1 order in 200</p></div>
34
+ <div class="node" id="pay"><h3>Charge the card</h3><p>£42.50</p></div>
35
+ </div>
36
+ <fig-arrow from="cart" to="check"></fig-arrow>
37
+ <fig-arrow from="check" to="pay" label="passed"></fig-arrow>
38
+ </main>
39
+ ```
40
+
41
+ ```
42
+ npx figkit order.html --out order.png
43
+ ```
44
+
45
+ ![An order is checked before it is charged: Basket, then Fraud check, then Charge the card](docs/readme-flow.png)
46
+
47
+ ## A chart from a spec
48
+
49
+ For one chart of a known type (`split`, `bars`, `before-after`, `dumbbell`, `series`) give the data and figkit does
50
+ the rest. Save this as `checkout.json`:
51
+
52
+ ```json
53
+ { "type": "bars", "title": "The new checkout halves the time to pay",
54
+ "note": "Median time from cart to payment", "unit": " s",
55
+ "bars": [ { "label": "Current checkout", "value": 86 },
56
+ { "label": "New checkout", "value": 41, "tone": "good" } ],
57
+ "source": "Invented example data" }
58
+ ```
59
+
60
+ ```
61
+ npx figkit checkout.json --out checkout.png
62
+ ```
63
+
64
+ ![Bar chart: the current checkout takes 86 seconds, the new one 41](docs/readme-bars.png)
65
+
66
+ A spec that breaks a rule is refused before anything is drawn, with one line per problem, for example a title over
67
+ 14 words or a split with more than 8 parts.
68
+
69
+ ## How problems are reported
70
+
71
+ Each run prints the PNG's path, a preview of how it sits inline on a phone, and the problems the checks found. Here
72
+ is the figure above with `class="row arrows middle"` instead of `class="row wide"`:
73
+
74
+ ```
75
+ 3 problems:
76
+ - #check overlaps the label of arrow check → pay.
77
+ - #pay overlaps the label of arrow check → pay.
78
+ - Almost aligned but not quite: #cart and #check (top edges, 2px apart); #cart and #check (bottom edges, 2px apart);
79
+ #cart and #pay (top edges, 2px apart); #cart and #pay (bottom edges, 2px apart). Put them in one row, column or
80
+ grid so they share the edge.
81
+ ```
82
+
83
+ Fix them and run it again until it says `No problems found by the checks.` The checks measure the rendered page; they
84
+ cannot tell whether the picture is clear or true, so look at it.
85
+
86
+ A figure whose columns are two sides to compare can add `class="sides"` to the row that holds them; figkit then also
87
+ writes `name.phone.png`, the same figure with the columns stacked, for viewers that show it on narrow screens.
88
+
89
+ ## Examples
90
+
91
+ `examples/` has a figure of each kind, all with invented data: a flow with a loop and decisions, a boundary between
92
+ two systems, a screenshot with a note, before and after, side by side, a split bar, line and timeline charts made
93
+ with [Observable Plot](https://observablehq.com/plot/), and `examples/charts/` has one spec per chart type. From a
94
+ clone, `npm run examples` renders them all into `out/` and fails if any reports a problem.
95
+
96
+ ## Licence
97
+
98
+ MIT. figkit's bundle includes Observable Plot and d3 (ISC); see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). The
99
+ fonts (Newsreader, Geist) are installed as dependencies under the SIL Open Font License.