@liggi/figkit 0.5.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
@@ -37,9 +37,13 @@ what?", "walk me through it") is answered with a picture first, not with numbere
37
37
  train) is not a reference; it goes in prose, a list or a separate flow. A record that creates others still points
38
38
  at them: draw the pointers it holds, not the steps it runs. Draw every pointer a record holds, even when another
39
39
  line reaches the same record by a longer way; leaving one out says the column is not there.
40
- - 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,
41
- not one map: what exists, then one case through it, then what changes. A process or machine the code runs in is a
42
- `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`.
43
47
  - Under each figure, write only what it cannot show: why, the exceptions, what to do. Never retell it as steps.
44
48
  - Put a real instance in each box, with its kind as the smaller line: "Refunds over £500" over "Approval rule",
45
49
  not a box that says only "Rule".
@@ -50,7 +54,7 @@ what?", "walk me through it") is answered with a picture first, not with numbere
50
54
 
51
55
  ## Charts and entities from a spec
52
56
 
53
- When the whole figure is one chart, one entities picture, one sequence or one lifecycle 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
54
58
  does the layout, colours and labels. Use HTML (the rest of this guide) for anything else, including a chart that sits
55
59
  beside boxes or arrows.
56
60
 
@@ -366,6 +370,57 @@ A states picture holds 7 states, not the 5 boxes of other figures: each state is
366
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
367
371
  main path with the side branches merged into one state, then the branch that needs detail.
368
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
+
369
424
  Flows (boxes and arrows) have no spec: write them in HTML, below. A flow says what connects to what; when the order
370
425
  things happen in is the point, use `sequence`; when one thing's states are the point, use `states`.
371
426
 
@@ -394,8 +449,8 @@ things happen in is the point, use `sequence`; when one thing's states are the p
394
449
 
395
450
  - In the `<h1>`, `<em>` turns the words accent-coloured italic and `<br>` sets a line break.
396
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.
397
- - `fig detail` is 1360 wide, for a flow or map the reader asked for and opens full screen; to explain a system, use
398
- 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.
399
454
  Widen it with `style="--w: 1560px"` only when a row will not fit.
400
455
  - Height follows the content. There is no fixed canvas to fill.
401
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.
@@ -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' : spec.spec.type === 'states' ? 'states-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 .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'), 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.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`);
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}`);