@liggi/figkit 0.4.0 → 0.6.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 CHANGED
@@ -30,14 +30,20 @@ what?", "walk me through it") is answered with a picture first, not with numbere
30
30
 
31
31
  - "What happens when…?", "walk me through a run" and "who calls whom?" are a `sequence` spec (below): the steps in
32
32
  order between the parts, not a numbered list or a table of steps.
33
+ - "What's the lifecycle of X?" and "what states can it be in?" are a `states` spec (below): the states one thing
34
+ moves through and what moves it on, not boxes and arrows in HTML.
33
35
  - "What are the entities and how do they relate?" is an `entities` spec (below), not boxes and arrows in HTML. It
34
36
  draws references: which record holds a pointer to which. The order work happens in (approve, then label, then
35
37
  train) is not a reference; it goes in prose, a list or a separate flow. A record that creates others still points
36
38
  at them: draw the pointers it holds, not the steps it runs. Draw every pointer a record holds, even when another
37
39
  line reaches the same record by a longer way; leaving one out says the column is not there.
38
- - 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,
39
- not one map: what exists, then one case through it, then what changes. A process or machine the code runs in is a
40
- `tag` on the box, not a box around it. Use `fig detail` only when the reader asked for the whole map.
40
+ - "How does X fit together?", "what are all the parts of X?", "give me the map of X": when the question really is
41
+ about the whole system and it has more than 5 parts worth naming, draw a `map` spec (below): every part in its
42
+ subsystem and every link, laid out by figkit. Follow it with a plain `fig` of the part the question is about,
43
+ so the reader knows where to look; a map alone answers "what exists", not "what matters here".
44
+ - Otherwise each figure is a plain `fig` with at most 5 boxes and no `group`. More than that is two or three figures
45
+ in a row: what exists, then one case through it, then what changes. A process or machine the code runs in is a
46
+ `tag` on the box, not a box around it. A system of more than 5 parts is a `map`, not a hand-placed `fig detail`.
41
47
  - Under each figure, write only what it cannot show: why, the exceptions, what to do. Never retell it as steps.
42
48
  - Put a real instance in each box, with its kind as the smaller line: "Refunds over £500" over "Approval rule",
43
49
  not a box that says only "Rule".
@@ -48,7 +54,7 @@ what?", "walk me through it") is answered with a picture first, not with numbere
48
54
 
49
55
  ## Charts and entities from a spec
50
56
 
51
- When the whole figure is one chart, one entities picture or one sequence of a type below, write a JSON spec instead of HTML. You give the data; figkit
57
+ When the whole figure is one chart, one entities picture, one sequence, one lifecycle or one system map of a type below, write a JSON spec instead of HTML. You give the data; figkit
52
58
  does the layout, colours and labels. Use HTML (the rest of this guide) for anything else, including a chart that sits
53
59
  beside boxes or arrows.
54
60
 
@@ -76,6 +82,21 @@ and embed the PNG, as for any figure.
76
82
  "bars": [ { "label": "Current checkout", "value": 86 }, { "label": "New checkout", "value": 41, "tone": "good" } ] }
77
83
  ```
78
84
 
85
+ A bar chart that compares every bar with one level, such as production or the current version, takes `ref`: the
86
+ level's `value` and a `label` naming it in words ("Production (ash-prod-27b)", never a bare number). figkit draws it as
87
+ its own first row in the second colour, with a dashed line from its end down through every bar; bars above it are
88
+ accented and the rest are grey, and the key says what the colour means. The colours are figkit's, so a `tone` on a bar
89
+ is refused, and `note` is required so the reader knows what the level's number counts. With ranges, a bar counts as
90
+ above only when its whole range is above. `show` on the ref replaces its printed value, e.g. `"50%, a tie"`.
91
+
92
+ ```json
93
+ { "type": "bars", "title": "Two of the four new layouts beat the current page",
94
+ "note": "Share of visitors who finished checkout", "unit": "%",
95
+ "ref": { "label": "Current page", "value": 61 },
96
+ "bars": [ { "label": "One-page form", "value": 68 }, { "label": "Saved cards first", "value": 64 },
97
+ { "label": "Address lookup", "value": 60 }, { "label": "Guest by default", "value": 57 } ] }
98
+ ```
99
+
79
100
  `before-after`: one measure that changed, the two values side by side on one scale; `note` (required) says what
80
101
  the number counts. For several things changing,
81
102
  use `bars` or `dumbbell`.
@@ -148,7 +169,9 @@ different things on the same scale.
148
169
  ```
149
170
 
150
171
  `heatmap`: a grid of values over two dimensions (day by hour, team by week), coloured from pale to dark blue: darker
151
- is always more. Up to 12 `rows` and 31 `columns`; `values` is one array per row. `note` (required) says what the
172
+ is always more. When any value is below zero (a change, a gap, a correlation), the grid is signed and the colour runs
173
+ from orange below zero through pale at zero to blue above, equally strong at equal distances from zero, with the
174
+ numbers printed with their sign; the scale shows 0 between the two halves. Numbers keep the decimal places you give. Up to 12 `rows` and 31 `columns`; `values` is one array per row. `note` (required) says what the
152
175
  colour counts, and the scale is drawn above the grid. Here the colour is the measure, so the one-accent rule moves:
153
176
  `mark` outlines the one cell to notice in the accent colour and names it beside the scale. Cells show their numbers
154
177
  when there is room. Columns label themselves at even steps, so give hours as "12am", "1am", … and they read 12am,
@@ -185,6 +208,28 @@ the things counted (`"each": "runs"`), else "n = 14"; give every bar one or none
185
208
 
186
209
  The title claims only what the ranges allow: when they overlap, "catches more" needs a "but".
187
210
 
211
+ `estimates`: signed results with their ranges, one row each around a zero line (a forest plot): a gap between two
212
+ groups, a change against control, a correlation. Use it when the sign is the point and each number has a range: "+7.4
213
+ (+2.7 to +12.4)". For numbers counted up from zero, use `bars`; for a move from one value to another, `dumbbell`. Every
214
+ row needs `low` and `high`, and `range` says once what they are. `note` (required) says what the number is and which
215
+ way round ("matched minus not matched"). `sides` names what each side of zero means, printed under the axis and meeting at zero; keep each to two or three words, or figkit reports it. figkit
216
+ chooses the colours: a row whose range clears zero is filled in the accent colour, one whose range includes zero is a
217
+ hollow grey dot, and the key says so; a `tone` is refused. With `"better": "higher"` or `"lower"`, a row clear of zero
218
+ is `good` or `bad` instead. Up to 12 rows; `n` and `each` as on `bars`. Numbers keep the decimal places you give, with
219
+ their sign. A range that does not contain its value, or a range of no width, is refused.
220
+
221
+ ```json
222
+ { "type": "estimates", "title": "Free delivery lifts orders in two regions; the north's range includes zero",
223
+ "note": "Change in orders per 100 visitors, free delivery minus paid", "range": "95% interval",
224
+ "sides": { "below": "Paid better", "above": "Free better" },
225
+ "rows": [ { "label": "South", "value": 2.4, "low": 1.1, "high": 3.7 },
226
+ { "label": "Midlands", "value": 1.8, "low": 0.6, "high": 3.0 },
227
+ { "label": "North", "value": 0.7, "low": -0.9, "high": 2.3 } ] }
228
+ ```
229
+
230
+ The title says a result only for rows whose range clears zero; a row whose range includes zero is "no clear change",
231
+ not a small gain.
232
+
188
233
  `dots`: every value as a dot, for a sample small enough to look at one by one (up to 300 a group, 600 in all), in 1
189
234
  to 6 groups on one axis. Use it instead of a bar of the average when the spread or a few odd values are the point.
190
235
  Dots that would overlap stack upward and downward, so none hides another, and each group's count is printed. `note`
@@ -280,12 +325,108 @@ steps show.
280
325
  More than 5 participants or 12 steps is two figures: the whole run with minor parts merged into one participant, then
281
326
  the stage that needs detail. Leave out steps that only log or record what happened unless the question is about them.
282
327
 
328
+ `states`: the states one thing moves through and what moves it on: a pull request, an order, a ticket, a person
329
+ in a study. Use it for "what's the lifecycle of X?", "what states can it be in?", "what happens after it's
330
+ approved?": the question is about one thing and where it can go next. A `sequence` is about several parts taking turns;
331
+ a flow is about what connects to what. 2 to 7 `states`, each an `id`, a `name` and an optional one-line `note` (a rule
332
+ that holds there: "A restart needs a new review"). Mark the one state things begin in `"start": true`, and the states
333
+ they stop in `"end": true`: nothing ever leaves an end, so a mark that comes and goes on a state (the current
334
+ default, a pin) is that state's `note`, not an end. `entry` labels the way in ("Card created"). 1 to 10 `moves`, each `from` one state `to`
335
+ another with a `label` saying what makes the move happen. figkit draws the longest run from the start down one
336
+ column, the other states beside it, and a move back to an earlier state as a loop up the left side. `from` can be a
337
+ list of states for one move any of them can make (retire, cancel), drawn as one bracket beside them and one arrow; `back` names the same
338
+ move the other way (`"back": "Restore"`), drawn with a head at both ends.
339
+
340
+ Counts: `count` on every move is how many of the things made that move at least once, `each` names them
341
+ (`"people"`), and the start's `count` is how many entered. `stayed` on a state that is not an end is how many went no further
342
+ than it. `"shares": true` adds each move's share of those who
343
+ reached the state it leaves; a loop back brings nothing new into that base. figkit says what the counts mean under
344
+ the reading line, and refuses counts on only some moves, a move counted higher than reached the state it leaves,
345
+ moves out of a state passed through once adding up to more than reached it, and a share of fewer than 10.
346
+
347
+ It also refuses a lifecycle with no start or two, an end with a way out, a state nothing leaves that is not marked
348
+ end, a state the start cannot reach, a move to its own state (put that in the state's `note`), and two moves between
349
+ the same states the same way. Accent one state or the moves the title is about. The title claims only what the moves
350
+ show.
351
+
352
+ ```json
353
+ { "type": "states", "title": "An order waits after a declined card until a new card is added",
354
+ "entry": "Shopper checks out",
355
+ "states": [ {"id": "waiting", "name": "Awaiting payment", "start": true},
356
+ {"id": "declined", "name": "Card declined", "note": "Shopper emailed", "tone": "accent"},
357
+ {"id": "paid", "name": "Paid"},
358
+ {"id": "packed", "name": "Packed"},
359
+ {"id": "shipped", "name": "Shipped", "end": true},
360
+ {"id": "cancelled", "name": "Cancelled", "end": true} ],
361
+ "moves": [ {"from": "waiting", "to": "paid", "label": "Charge succeeds"},
362
+ {"from": "waiting", "to": "declined", "label": "Charge fails"},
363
+ {"from": "declined", "to": "waiting", "label": "New card added"},
364
+ {"from": "paid", "to": "packed", "label": "Warehouse picks"},
365
+ {"from": "packed", "to": "shipped", "label": "Carrier collects"},
366
+ {"from": ["paid", "packed"], "to": "cancelled", "label": "Shopper cancels"} ] }
367
+ ```
368
+
369
+ A states picture holds 7 states, not the 5 boxes of other figures: each state is a short name read in order down one
370
+ path, and an ordinary lifecycle is four or five steps plus one or two ways off it. More than 7 is two figures: the
371
+ main path with the side branches merged into one state, then the branch that needs detail.
372
+
373
+ `map`: a whole system, its parts grouped by subsystem and the links between them, laid out by figkit. Use it when
374
+ the question is about the whole system ("how does X fit together?", "what are all the parts?"); for one path through
375
+ it use `sequence`, and for one corner of it a plain `fig`. 2 to 6 `subsystems`, each an `id`, a `name` and an optional
376
+ `kind` (the folder or team it belongs to). 6 to 24 `parts`, each an `id`, a `name` in the reader's words, a `kind` as
377
+ the smaller line (the file, the service, what it is: "lib/detectors/gate.ts", "Postgres") and `in`, the subsystem it
378
+ belongs to; something outside the system, such as GitHub or a database another team runs, still goes in a subsystem,
379
+ "Outside". At most 8 parts in one subsystem. Up to 30 `links`, each `from` the part that calls, reads or sends `to`
380
+ the part called, read or written, with an optional `label` of a few words ("writes scores", "pick lists"). Label the
381
+ links the question is about and leave the obvious ones bare: every label is one more thing to place in a crowded
382
+ picture. A label sits on its own line, never beside it, because between subsystems lines run side by side and a word
383
+ beside one reads as naming its neighbour; figkit moves it along the line to a clear spot and refuses the figure when
384
+ there is none, naming the label to drop. A real map of 23 parts kept 18 of its 21 labels. Draw a link one way only, from the part that starts the exchange; the reply is implied. `"tone": "accent"` on the parts and links the question
385
+ is about, the same ones the title names.
386
+
387
+ figkit lays the parts out in layers inside each subsystem, packs the subsystems into rows that read largest on a full
388
+ screen, and routes every link around the boxes. With links accented, it places the accented path first: the subsystems
389
+ it passes through go one after another, lined up so the path runs as one straight line, or steps aside where a part is
390
+ in the way, and the other subsystems go beside it, routed around it. It refuses a map whose accented path still turns
391
+ back on itself, and names any grey link that crosses it. Accent links that follow one another in one direction.
392
+ A map with an accented path holds 28 links, and at most 3 grey links on any part of the path: the path takes a part's
393
+ top and bottom, so grey links come in from the sides. Measured on a real map of 24 parts, its path stayed one straight
394
+ run with no grey link crossing it at 15 to 26 links, and failed at 27 (4 grey links on one part of the path), 29 and
395
+ 30 (lines on top of each other, or 17 to 21 crossings); a second real map held at 28 links. The phone layout draws each subsystem as one box listing its parts,
396
+ with the number of links between subsystems on the arrows. It refuses an id that names no part or subsystem, a part in
397
+ no subsystem, a part or subsystem with no link or no parts, two links between the same parts, either way, a link from
398
+ a part to itself, and two parts with the same name. After drawing it reports labels that collide and links that run on
399
+ top of each other; up to one crossing per 2 links is allowed and named in a note.
400
+
401
+ ```json
402
+ { "type": "map", "title": "Every order passes through the order service before stores see it",
403
+ "subsystems": [ { "id": "shop", "name": "Storefront" }, { "id": "orders", "name": "Orders" },
404
+ { "id": "stores", "name": "Store fulfilment" } ],
405
+ "parts": [ { "id": "web", "name": "Shop website", "kind": "Next.js app", "in": "shop" },
406
+ { "id": "basket", "name": "Basket service", "kind": "Go service", "in": "shop" },
407
+ { "id": "order", "name": "Order service", "kind": "Go service", "in": "orders", "tone": "accent" },
408
+ { "id": "events", "name": "Order events", "kind": "Kafka topic", "in": "orders" },
409
+ { "id": "planner", "name": "Picking planner", "kind": "nightly batch", "in": "stores" },
410
+ { "id": "picker", "name": "Picker app", "kind": "handheld scanner", "in": "stores" } ],
411
+ "links": [ { "from": "web", "to": "basket" },
412
+ { "from": "basket", "to": "order", "label": "checkout", "tone": "accent" },
413
+ { "from": "order", "to": "events", "label": "publishes" },
414
+ { "from": "events", "to": "planner" }, { "from": "planner", "to": "picker", "label": "pick lists" } ] }
415
+ ```
416
+
417
+ A map holds 24 parts, not the 5 boxes of other figures, because it is opened full screen and read region by region,
418
+ not taken in at a glance. 24 parts and 30 links is where real systems stop reading: drawn from their code, two systems
419
+ of 23 and 24 parts with 28 and 30 links crossed 9 and 11 times and could be followed full screen; at 32 links one of
420
+ them crossed anywhere from 12 to 20 times as small changes moved its regions, and at 31 to 34 parts and 45 to 48 links
421
+ they crossed 28 to 49 times and could not be followed. More than 24 is two maps: the subsystems with the busiest one merged to a few parts, then
422
+ that subsystem as its own map. The title still claims one thing the map shows.
423
+
283
424
  Flows (boxes and arrows) have no spec: write them in HTML, below. A flow says what connects to what; when the order
284
- things happen in is the point, use `sequence`.
425
+ things happen in is the point, use `sequence`; when one thing's states are the point, use `states`.
285
426
 
286
427
  - `title` (required) is the point, 14 words at most. `note` is the reading line under it; `source` a footer.
287
428
  - `"style": "report"` draws a light chart in one sans font, for a chart that sits in a page: a research report, a doc.
288
- It also draws a phone layout, `chart.phone.png`. Leave it out for the default dark figure for a dark chat.
429
+ It also draws a phone layout, `chart.phone.png`, as do `estimates`, `heatmap` and `bars` with a `ref` in either style. Leave it out for the default dark figure for a dark chat.
289
430
  - Keep `note` to what is measured, and "lower is better" where it applies; dates and method go in `source`.
290
431
  - `unit` is printed straight after each number. `show` on an item replaces its printed value, e.g. `"11 of 29"`.
291
432
  - `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.
@@ -308,8 +449,8 @@ things happen in is the point, use `sequence`.
308
449
 
309
450
  - In the `<h1>`, `<em>` turns the words accent-coloured italic and `<br>` sets a line break.
310
451
  - `fig` is a glance figure, 960 wide: one point, big type, reads inline on a phone. At most 5 boxes, 8 arrows and 70 words.
311
- - `fig detail` is 1360 wide, for a flow or map the reader asked for and opens full screen; to explain a system, use
312
- several `fig` figures instead (see Explaining a system). At most 18 boxes, 18 arrows and 190 words.
452
+ - `fig detail` is 1360 wide, for a flow the reader asked for and opens full screen; to explain a system, use
453
+ several `fig` figures or a `map` spec instead (see Explaining a system). At most 18 boxes, 18 arrows and 190 words.
313
454
  Widen it with `style="--w: 1560px"` only when a row will not fit.
314
455
  - Height follows the content. There is no fixed canvas to fill.
315
456
  - 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.
@@ -418,8 +559,8 @@ A few numbers compared:
418
559
  ```
419
560
 
420
561
  Grouped bars, several lines, small multiples and heat maps have specs (`groups`, `lines`, `panels`, `heatmap`); use
421
- them when the chart is the whole figure. Dots, ranges, scatters and small counts have specs too
422
- (`dots`, `scatter`, ranges on `bars` and `series`, `units`). Anything else (a distribution of thousands, a chart beside boxes) is
562
+ them when the chart is the whole figure. Dots, ranges, signed estimates, scatters and small counts have specs too
563
+ (`dots`, `estimates`, `scatter`, ranges on `bars` and `series`, `units`). Anything else (a distribution of thousands, a chart beside boxes) is
423
564
  Observable Plot, already loaded with d3:
424
565
 
425
566
  ```html
@@ -452,7 +593,8 @@ notice in `accent`, and label it directly on the chart; no legends. Start bar ax
452
593
 
453
594
  ## What the checks catch, and what they cannot
454
595
 
455
- Measured in the rendered page: text past its box or the frame, text too small, boxes and labels that overlap, arrows
596
+ Measured in the rendered page: text past its box or the frame, text too small, text too faint against what is drawn
597
+ behind it (a contrast under 3 to 1), boxes and labels that overlap, arrows
456
598
  through boxes, arrows crossing, edges that almost line up, too many words, boxes, arrows or colours, facts strung
457
599
  together with separators, a missing or long title.
458
600
 
@@ -1,6 +1,6 @@
1
1
  # Third-party notices
2
2
 
3
- `dist/viz.iife.js` bundles Observable Plot and the d3 modules it uses. Their licences follow. The fonts (Newsreader, Geist Sans, Geist Mono, under the SIL Open Font License 1.1), Playwright (Apache-2.0), d3 (ISC) and Zod (MIT) are installed as ordinary dependencies and are not copied into this package.
3
+ `dist/viz.iife.js` bundles Observable Plot and the d3 modules it uses, and dagre with its graphlib for laying out maps. Their licences follow. The fonts (Newsreader, Geist Sans, Geist Mono, under the SIL Open Font License 1.1), Playwright (Apache-2.0), d3 (ISC) and Zod (MIT) are installed as ordinary dependencies and are not copied into this package.
4
4
 
5
5
  ## @observablehq/plot
6
6
 
@@ -630,3 +630,50 @@ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
630
630
  THIS SOFTWARE.
631
631
  ```
632
632
 
633
+ ## @dagrejs/dagre
634
+
635
+ ```
636
+ Copyright (c) 2012-2014 Chris Pettitt
637
+
638
+ Permission is hereby granted, free of charge, to any person obtaining a copy
639
+ of this software and associated documentation files (the "Software"), to deal
640
+ in the Software without restriction, including without limitation the rights
641
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
642
+ copies of the Software, and to permit persons to whom the Software is
643
+ furnished to do so, subject to the following conditions:
644
+
645
+ The above copyright notice and this permission notice shall be included in
646
+ all copies or substantial portions of the Software.
647
+
648
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
649
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
650
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
651
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
652
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
653
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
654
+ THE SOFTWARE.
655
+ ```
656
+
657
+ ## @dagrejs/graphlib
658
+
659
+ ```
660
+ Copyright (c) 2012-2014 Chris Pettitt
661
+
662
+ Permission is hereby granted, free of charge, to any person obtaining a copy
663
+ of this software and associated documentation files (the "Software"), to deal
664
+ in the Software without restriction, including without limitation the rights
665
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
666
+ copies of the Software, and to permit persons to whom the Software is
667
+ furnished to do so, subject to the following conditions:
668
+
669
+ The above copyright notice and this permission notice shall be included in
670
+ all copies or substantial portions of the Software.
671
+
672
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
673
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
674
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
675
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
676
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
677
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
678
+ THE SOFTWARE.
679
+ ```
package/bin/fig.mjs CHANGED
@@ -46,7 +46,7 @@ if (/\.json$/i.test(src)) {
46
46
  console.log(`This chart spec has ${spec.problems.length} problem${spec.problems.length === 1 ? '' : 's'}:\n${spec.problems.map((p) => ` - ${p}`).join('\n')}`);
47
47
  process.exit(1);
48
48
  }
49
- const cls = [spec.spec.type === 'entities' ? 'entities-fig' : spec.spec.type === 'sequence' ? 'sequence-fig' : 'chart', spec.spec.type === 'panels' ? 'panels-fig' : '', spec.spec.style ?? ''].filter(Boolean).join(' ');
49
+ const cls = [spec.spec.type === 'entities' ? 'entities-fig' : spec.spec.type === 'sequence' ? 'sequence-fig' : spec.spec.type === 'states' ? 'states-fig' : spec.spec.type === 'map' ? 'detail map-fig' : 'chart', spec.spec.type === 'panels' ? 'panels-fig' : '', spec.spec.style ?? ''].filter(Boolean).join(' ');
50
50
  body = `<main class="fig ${cls}"><script type="application/json" id="figkit-spec">${JSON.stringify(spec.spec).replace(/</g, '\\u003c')}</script></main>`;
51
51
  } else if (!/class="[^"]*\bfig\b/.test(body)) body = `<main class="fig">\n${body}\n</main>`;
52
52
  await ensureBuilt();
@@ -101,7 +101,7 @@ try {
101
101
  if (phone) await page.addInitScript(() => { window.__figPhone = true; });
102
102
  await page.goto(pathToFileURL(wrapped).href);
103
103
  await page.waitForFunction('window.__figDone === true', null, { timeout: 30000 });
104
- const r = await page.evaluate(() => ({ problems: window.fig.problems, info: window.fig.info, sides: !!document.querySelector('.fig .sides, .fig .entities, .fig .sequence, .fig .panels, .fig.report'), entities: !!document.querySelector('.fig .entities'), sequence: !!document.querySelector('.fig .sequence'), panels: !!document.querySelector('.fig .panels'), stack: !!document.querySelector('.fig .sides') }));
104
+ const r = await page.evaluate(() => ({ problems: window.fig.problems, notes: window.fig.notes ?? [], info: window.fig.info, sides: !!document.querySelector('.fig .sides, .fig .map, .fig .entities, .fig .sequence, .fig .states, .fig .panels, .fig .estimates, .fig .heatmap, .fig fig-bars.has-ref, .fig.report'), states: !!document.querySelector('.fig .states'), entities: !!document.querySelector('.fig .entities'), sequence: !!document.querySelector('.fig .sequence'), panels: !!document.querySelector('.fig .panels'), map: !!document.querySelector('.fig .map'), stack: !!document.querySelector('.fig .sides') }));
105
105
  await page.locator('.fig').first().screenshot({ path: shot });
106
106
  await page.close();
107
107
  return { ...r, problems: [...errors.map((e) => `Script error: ${e}`), ...r.problems] };
@@ -116,12 +116,14 @@ try {
116
116
 
117
117
  const { info } = desk;
118
118
  const lines = [`Figure: ${png} (${info.width * 2}×${info.height * 2})`];
119
- if (mob) lines.push(`Phone layout: ${phonePng} (${mob.info.width * 2}×${mob.info.height * 2}), ${desk.entities ? 'the records in two columns' : desk.sequence ? 'the participants narrower' : desk.panels ? 'the panels stacked' : desk.stack ? 'the sides stacked' : 'at phone width'}, for a viewer to show on a phone`);
119
+ if (mob) lines.push(`Phone layout: ${phonePng} (${mob.info.width * 2}×${mob.info.height * 2}), ${desk.map ? 'each subsystem as one box' : desk.entities ? 'the records in two columns' : desk.sequence ? 'the participants narrower' : desk.states ? 'the states narrower' : desk.panels ? 'the panels stacked' : desk.stack ? 'the sides stacked' : 'at phone width'}, for a viewer to show on a phone`);
120
120
  lines.push(`Phone inline: ${inline} (preview only)`);
121
121
  const ph = (mob ?? desk).info.phone;
122
122
  const sizes = [['the title', ph?.title], ['headings', ph?.heading], ['body', ph?.body]].filter(([, v]) => v != null).map(([k, v]) => `${k} ${v}px`);
123
123
  if (sizes.length) lines.push(`Inline on a phone: ${sizes.join(', ')}; under about 6px only reads after a tap to zoom.`);
124
124
  if (!mob && info.unmarked) lines.push('Its columns sit side by side and will shrink on a phone. If they are two sides to compare, add class "sides" to the row or grid that holds them, and a stacked phone layout is drawn too.');
125
+ for (const n of desk.notes) lines.push(`Note: ${n}`);
126
+ for (const n of mob?.notes ?? []) lines.push(`Phone layout note: ${n}`);
125
127
  const all = [...desk.problems, ...(mob?.problems ?? []).map((p) => `Phone layout: ${p}`)];
126
128
  lines.push(all.length ? `\n${all.length} problem${all.length === 1 ? '' : 's'}:` : '\nNo problems found by the checks.');
127
129
  for (const p of all) lines.push(` - ${p}`);