@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 +61 -6
- package/THIRD_PARTY_NOTICES.md +48 -1
- package/bin/fig.mjs +5 -3
- package/dist/viz.iife.js +47 -42
- package/examples/charts/map.json +269 -0
- package/package.json +2 -1
- package/runtime/fig.css +17 -0
- package/runtime/fig.js +146 -18
- package/viz/draw.js +13 -0
- package/viz/map-spec.js +121 -0
- package/viz/map.js +513 -0
- package/viz/spec.js +8 -3
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
|
-
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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
|
|
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.
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -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}`);
|