@liggi/figkit 0.4.0 → 0.5.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 +94 -7
- package/bin/fig.mjs +3 -3
- package/dist/viz.iife.js +81 -18
- package/examples/charts/bars-ref.json +6 -0
- package/examples/charts/estimates.json +7 -0
- package/examples/charts/heatmap-signed.json +5 -0
- package/examples/charts/states.json +71 -0
- package/package.json +1 -1
- package/runtime/fig.css +39 -0
- package/runtime/fig.js +73 -3
- package/viz/draw.js +155 -14
- package/viz/spec.js +59 -6
- package/viz/states-spec.js +161 -0
- package/viz/states.js +203 -0
package/GUIDE.md
CHANGED
|
@@ -30,6 +30,8 @@ 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
|
|
@@ -48,7 +50,7 @@ what?", "walk me through it") is answered with a picture first, not with numbere
|
|
|
48
50
|
|
|
49
51
|
## Charts and entities from a spec
|
|
50
52
|
|
|
51
|
-
When the whole figure is one chart, one entities picture or one
|
|
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
|
|
52
54
|
does the layout, colours and labels. Use HTML (the rest of this guide) for anything else, including a chart that sits
|
|
53
55
|
beside boxes or arrows.
|
|
54
56
|
|
|
@@ -76,6 +78,21 @@ and embed the PNG, as for any figure.
|
|
|
76
78
|
"bars": [ { "label": "Current checkout", "value": 86 }, { "label": "New checkout", "value": 41, "tone": "good" } ] }
|
|
77
79
|
```
|
|
78
80
|
|
|
81
|
+
A bar chart that compares every bar with one level, such as production or the current version, takes `ref`: the
|
|
82
|
+
level's `value` and a `label` naming it in words ("Production (ash-prod-27b)", never a bare number). figkit draws it as
|
|
83
|
+
its own first row in the second colour, with a dashed line from its end down through every bar; bars above it are
|
|
84
|
+
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
|
|
85
|
+
is refused, and `note` is required so the reader knows what the level's number counts. With ranges, a bar counts as
|
|
86
|
+
above only when its whole range is above. `show` on the ref replaces its printed value, e.g. `"50%, a tie"`.
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{ "type": "bars", "title": "Two of the four new layouts beat the current page",
|
|
90
|
+
"note": "Share of visitors who finished checkout", "unit": "%",
|
|
91
|
+
"ref": { "label": "Current page", "value": 61 },
|
|
92
|
+
"bars": [ { "label": "One-page form", "value": 68 }, { "label": "Saved cards first", "value": 64 },
|
|
93
|
+
{ "label": "Address lookup", "value": 60 }, { "label": "Guest by default", "value": 57 } ] }
|
|
94
|
+
```
|
|
95
|
+
|
|
79
96
|
`before-after`: one measure that changed, the two values side by side on one scale; `note` (required) says what
|
|
80
97
|
the number counts. For several things changing,
|
|
81
98
|
use `bars` or `dumbbell`.
|
|
@@ -148,7 +165,9 @@ different things on the same scale.
|
|
|
148
165
|
```
|
|
149
166
|
|
|
150
167
|
`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.
|
|
168
|
+
is always more. When any value is below zero (a change, a gap, a correlation), the grid is signed and the colour runs
|
|
169
|
+
from orange below zero through pale at zero to blue above, equally strong at equal distances from zero, with the
|
|
170
|
+
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
171
|
colour counts, and the scale is drawn above the grid. Here the colour is the measure, so the one-accent rule moves:
|
|
153
172
|
`mark` outlines the one cell to notice in the accent colour and names it beside the scale. Cells show their numbers
|
|
154
173
|
when there is room. Columns label themselves at even steps, so give hours as "12am", "1am", … and they read 12am,
|
|
@@ -185,6 +204,28 @@ the things counted (`"each": "runs"`), else "n = 14"; give every bar one or none
|
|
|
185
204
|
|
|
186
205
|
The title claims only what the ranges allow: when they overlap, "catches more" needs a "but".
|
|
187
206
|
|
|
207
|
+
`estimates`: signed results with their ranges, one row each around a zero line (a forest plot): a gap between two
|
|
208
|
+
groups, a change against control, a correlation. Use it when the sign is the point and each number has a range: "+7.4
|
|
209
|
+
(+2.7 to +12.4)". For numbers counted up from zero, use `bars`; for a move from one value to another, `dumbbell`. Every
|
|
210
|
+
row needs `low` and `high`, and `range` says once what they are. `note` (required) says what the number is and which
|
|
211
|
+
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
|
|
212
|
+
chooses the colours: a row whose range clears zero is filled in the accent colour, one whose range includes zero is a
|
|
213
|
+
hollow grey dot, and the key says so; a `tone` is refused. With `"better": "higher"` or `"lower"`, a row clear of zero
|
|
214
|
+
is `good` or `bad` instead. Up to 12 rows; `n` and `each` as on `bars`. Numbers keep the decimal places you give, with
|
|
215
|
+
their sign. A range that does not contain its value, or a range of no width, is refused.
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{ "type": "estimates", "title": "Free delivery lifts orders in two regions; the north's range includes zero",
|
|
219
|
+
"note": "Change in orders per 100 visitors, free delivery minus paid", "range": "95% interval",
|
|
220
|
+
"sides": { "below": "Paid better", "above": "Free better" },
|
|
221
|
+
"rows": [ { "label": "South", "value": 2.4, "low": 1.1, "high": 3.7 },
|
|
222
|
+
{ "label": "Midlands", "value": 1.8, "low": 0.6, "high": 3.0 },
|
|
223
|
+
{ "label": "North", "value": 0.7, "low": -0.9, "high": 2.3 } ] }
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The title says a result only for rows whose range clears zero; a row whose range includes zero is "no clear change",
|
|
227
|
+
not a small gain.
|
|
228
|
+
|
|
188
229
|
`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
230
|
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
231
|
Dots that would overlap stack upward and downward, so none hides another, and each group's count is printed. `note`
|
|
@@ -280,12 +321,57 @@ steps show.
|
|
|
280
321
|
More than 5 participants or 12 steps is two figures: the whole run with minor parts merged into one participant, then
|
|
281
322
|
the stage that needs detail. Leave out steps that only log or record what happened unless the question is about them.
|
|
282
323
|
|
|
324
|
+
`states`: the states one thing moves through and what moves it on: a pull request, an order, a ticket, a person
|
|
325
|
+
in a study. Use it for "what's the lifecycle of X?", "what states can it be in?", "what happens after it's
|
|
326
|
+
approved?": the question is about one thing and where it can go next. A `sequence` is about several parts taking turns;
|
|
327
|
+
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
|
|
328
|
+
that holds there: "A restart needs a new review"). Mark the one state things begin in `"start": true`, and the states
|
|
329
|
+
they stop in `"end": true`: nothing ever leaves an end, so a mark that comes and goes on a state (the current
|
|
330
|
+
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`
|
|
331
|
+
another with a `label` saying what makes the move happen. figkit draws the longest run from the start down one
|
|
332
|
+
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
|
|
333
|
+
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
|
|
334
|
+
move the other way (`"back": "Restore"`), drawn with a head at both ends.
|
|
335
|
+
|
|
336
|
+
Counts: `count` on every move is how many of the things made that move at least once, `each` names them
|
|
337
|
+
(`"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
|
|
338
|
+
than it. `"shares": true` adds each move's share of those who
|
|
339
|
+
reached the state it leaves; a loop back brings nothing new into that base. figkit says what the counts mean under
|
|
340
|
+
the reading line, and refuses counts on only some moves, a move counted higher than reached the state it leaves,
|
|
341
|
+
moves out of a state passed through once adding up to more than reached it, and a share of fewer than 10.
|
|
342
|
+
|
|
343
|
+
It also refuses a lifecycle with no start or two, an end with a way out, a state nothing leaves that is not marked
|
|
344
|
+
end, a state the start cannot reach, a move to its own state (put that in the state's `note`), and two moves between
|
|
345
|
+
the same states the same way. Accent one state or the moves the title is about. The title claims only what the moves
|
|
346
|
+
show.
|
|
347
|
+
|
|
348
|
+
```json
|
|
349
|
+
{ "type": "states", "title": "An order waits after a declined card until a new card is added",
|
|
350
|
+
"entry": "Shopper checks out",
|
|
351
|
+
"states": [ {"id": "waiting", "name": "Awaiting payment", "start": true},
|
|
352
|
+
{"id": "declined", "name": "Card declined", "note": "Shopper emailed", "tone": "accent"},
|
|
353
|
+
{"id": "paid", "name": "Paid"},
|
|
354
|
+
{"id": "packed", "name": "Packed"},
|
|
355
|
+
{"id": "shipped", "name": "Shipped", "end": true},
|
|
356
|
+
{"id": "cancelled", "name": "Cancelled", "end": true} ],
|
|
357
|
+
"moves": [ {"from": "waiting", "to": "paid", "label": "Charge succeeds"},
|
|
358
|
+
{"from": "waiting", "to": "declined", "label": "Charge fails"},
|
|
359
|
+
{"from": "declined", "to": "waiting", "label": "New card added"},
|
|
360
|
+
{"from": "paid", "to": "packed", "label": "Warehouse picks"},
|
|
361
|
+
{"from": "packed", "to": "shipped", "label": "Carrier collects"},
|
|
362
|
+
{"from": ["paid", "packed"], "to": "cancelled", "label": "Shopper cancels"} ] }
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
A states picture holds 7 states, not the 5 boxes of other figures: each state is a short name read in order down one
|
|
366
|
+
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
|
+
main path with the side branches merged into one state, then the branch that needs detail.
|
|
368
|
+
|
|
283
369
|
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`.
|
|
370
|
+
things happen in is the point, use `sequence`; when one thing's states are the point, use `states`.
|
|
285
371
|
|
|
286
372
|
- `title` (required) is the point, 14 words at most. `note` is the reading line under it; `source` a footer.
|
|
287
373
|
- `"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
|
|
374
|
+
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
375
|
- Keep `note` to what is measured, and "lower is better" where it applies; dates and method go in `source`.
|
|
290
376
|
- `unit` is printed straight after each number. `show` on an item replaces its printed value, e.g. `"11 of 29"`.
|
|
291
377
|
- `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.
|
|
@@ -418,8 +504,8 @@ A few numbers compared:
|
|
|
418
504
|
```
|
|
419
505
|
|
|
420
506
|
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
|
|
507
|
+
them when the chart is the whole figure. Dots, ranges, signed estimates, scatters and small counts have specs too
|
|
508
|
+
(`dots`, `estimates`, `scatter`, ranges on `bars` and `series`, `units`). Anything else (a distribution of thousands, a chart beside boxes) is
|
|
423
509
|
Observable Plot, already loaded with d3:
|
|
424
510
|
|
|
425
511
|
```html
|
|
@@ -452,7 +538,8 @@ notice in `accent`, and label it directly on the chart; no legends. Start bar ax
|
|
|
452
538
|
|
|
453
539
|
## What the checks catch, and what they cannot
|
|
454
540
|
|
|
455
|
-
Measured in the rendered page: text past its box or the frame, text too small,
|
|
541
|
+
Measured in the rendered page: text past its box or the frame, text too small, text too faint against what is drawn
|
|
542
|
+
behind it (a contrast under 3 to 1), boxes and labels that overlap, arrows
|
|
456
543
|
through boxes, arrows crossing, edges that almost line up, too many words, boxes, arrows or colours, facts strung
|
|
457
544
|
together with separators, a missing or long title.
|
|
458
545
|
|
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' : '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, 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') }));
|
|
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,7 +116,7 @@ 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.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`);
|