@liggi/figkit 0.2.0 → 0.4.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 +165 -7
- package/README.md +6 -5
- package/bin/fig.mjs +14 -6
- package/dist/viz.iife.js +16 -16
- package/examples/charts/bars-range.json +6 -0
- package/examples/charts/dots.json +6 -0
- package/examples/charts/groups.json +40 -0
- package/examples/charts/heatmap.json +231 -0
- package/examples/charts/lines.json +234 -0
- package/examples/charts/panels.json +62 -0
- package/examples/charts/scatter.json +6 -0
- package/examples/charts/sequence.json +67 -0
- package/examples/charts/series-range.json +6 -0
- package/examples/charts/units.json +4 -0
- package/package.json +1 -1
- package/runtime/fig.css +92 -4
- package/runtime/fig.js +17 -2
- package/viz/build.mjs +1 -1
- package/viz/draw.js +449 -17
- package/viz/sequence-spec.js +60 -0
- package/viz/sequence.js +171 -0
- package/viz/spec.js +262 -5
package/GUIDE.md
CHANGED
|
@@ -5,7 +5,7 @@ work out coordinates or text widths: boxes size to their text, arrows route them
|
|
|
5
5
|
measure the real result.
|
|
6
6
|
|
|
7
7
|
```
|
|
8
|
-
npx figkit figure.html [--out figure.png]
|
|
8
|
+
npx @liggi/figkit figure.html [--out figure.png]
|
|
9
9
|
```
|
|
10
10
|
|
|
11
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.
|
|
@@ -28,6 +28,8 @@ the images and look, then embed the PNG: ``.
|
|
|
28
28
|
A question about structure ("what are the entities?", "where does X live?", "how do these relate?", "what loads
|
|
29
29
|
what?", "walk me through it") is answered with a picture first, not with numbered steps.
|
|
30
30
|
|
|
31
|
+
- "What happens when…?", "walk me through a run" and "who calls whom?" are a `sequence` spec (below): the steps in
|
|
32
|
+
order between the parts, not a numbered list or a table of steps.
|
|
31
33
|
- "What are the entities and how do they relate?" is an `entities` spec (below), not boxes and arrows in HTML. It
|
|
32
34
|
draws references: which record holds a pointer to which. The order work happens in (approve, then label, then
|
|
33
35
|
train) is not a reference; it goes in prose, a list or a separate flow. A record that creates others still points
|
|
@@ -46,12 +48,12 @@ what?", "walk me through it") is answered with a picture first, not with numbere
|
|
|
46
48
|
|
|
47
49
|
## Charts and entities from a spec
|
|
48
50
|
|
|
49
|
-
When the whole figure is one chart
|
|
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
|
|
50
52
|
does the layout, colours and labels. Use HTML (the rest of this guide) for anything else, including a chart that sits
|
|
51
53
|
beside boxes or arrows.
|
|
52
54
|
|
|
53
55
|
```
|
|
54
|
-
npx figkit chart.json [--out chart.png]
|
|
56
|
+
npx @liggi/figkit chart.json [--out chart.png]
|
|
55
57
|
```
|
|
56
58
|
|
|
57
59
|
A spec that breaks a rule is refused with one line per problem; fix them and run it again. Then look at the images
|
|
@@ -105,6 +107,128 @@ mark's `x` must be one of them. The y axis starts at zero unless `"zero": false`
|
|
|
105
107
|
"mark": { "x": "2026-10-14", "label": "212, launch day" } }
|
|
106
108
|
```
|
|
107
109
|
|
|
110
|
+
`groups`: a few things measured for each of 2 to 6 groups, 2 to 4 series, one bar per series in each group, all
|
|
111
|
+
from zero. Use it when the point is a difference that shows in some groups and not others (before and after, one
|
|
112
|
+
segment against another); for one number per thing, use `bars`. Each group gets a heading and each bar its series
|
|
113
|
+
name and number, so there is no legend and no axis. `accent` is required: `{ "series" }` colours that series in every
|
|
114
|
+
group, `{ "series", "group" }` colours one bar; `"tone": "bad"` or `"good"` when the bar is a result.
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{ "type": "groups", "title": "Only the new carrier got slower in December",
|
|
118
|
+
"note": "Median days from dispatch to doorstep", "unit": " days",
|
|
119
|
+
"series": ["Old carrier", "New carrier"],
|
|
120
|
+
"groups": [ { "label": "November", "values": [2.2, 2.1] }, { "label": "December", "values": [2.4, 3.6] } ],
|
|
121
|
+
"accent": { "series": "New carrier", "group": "December", "tone": "bad" } }
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`lines`: 2 or 3 lines on one axis, each named at its right end in its own colour. `accent` (required) names the
|
|
125
|
+
line the title is about; the others are grey. `mark` labels one point on the accent line. `xTicks` sets the axis
|
|
126
|
+
labels, e.g. hours as 12am, 6am, 12pm, 6pm, rather than 0 to 23. More than 3 lines is `panels`.
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{ "type": "lines", "title": "Weekday orders peak at noon", "note": "Orders per hour",
|
|
130
|
+
"series": [ { "label": "Weekdays", "points": [ { "x": 0, "y": 3 }, { "x": 12, "y": 30 }, { "x": 23, "y": 4 } ] },
|
|
131
|
+
{ "label": "Weekends", "points": [ { "x": 0, "y": 4 }, { "x": 12, "y": 22 }, { "x": 23, "y": 6 } ] } ],
|
|
132
|
+
"accent": "Weekdays", "mark": { "x": 12, "label": "30 at noon" },
|
|
133
|
+
"xTicks": [ { "x": 0, "label": "12am" }, { "x": 12, "label": "12pm" } ] }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`panels`: small multiples, the same `groups` or `lines` chart drawn for 2 to 4 parts side by side, on one shared
|
|
137
|
+
scale so the panels compare; stacked on a phone. `chart` says which. For `groups`, `series` and `accent` are given once
|
|
138
|
+
at the top and each panel has its `groups`; for `lines`, `accent` is the line's label and every panel has the same
|
|
139
|
+
lines in the same order. Use it when one chart would need more than 3 lines or 4 series, or the parts measure
|
|
140
|
+
different things on the same scale.
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{ "type": "panels", "chart": "groups", "title": "The cheaper plan answers almost as fast as the premium one",
|
|
144
|
+
"note": "Share of tickets answered within an hour", "unit": "%", "max": 100,
|
|
145
|
+
"series": ["Basic", "Standard", "Premium"], "accent": { "series": "Standard" },
|
|
146
|
+
"panels": [ { "label": "Email", "groups": [ { "label": "Weekdays", "values": [61, 84, 89] } ] },
|
|
147
|
+
{ "label": "Chat", "groups": [ { "label": "Weekdays", "values": [72, 91, 94] } ] } ] }
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`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
|
|
152
|
+
colour counts, and the scale is drawn above the grid. Here the colour is the measure, so the one-accent rule moves:
|
|
153
|
+
`mark` outlines the one cell to notice in the accent colour and names it beside the scale. Cells show their numbers
|
|
154
|
+
when there is room. Columns label themselves at even steps, so give hours as "12am", "1am", … and they read 12am,
|
|
155
|
+
6am, 12pm, 6pm.
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{ "type": "heatmap", "title": "The help desk is busiest midweek mornings", "note": "Tickets opened per hour",
|
|
159
|
+
"rows": ["Mon", "Tue", "Wed"], "columns": ["9am", "10am", "11am", "12pm"],
|
|
160
|
+
"values": [ [22, 25, 27, 28], [23, 26, 29, 30], [24, 27, 30, 32] ],
|
|
161
|
+
"mark": { "row": "Wed", "column": "12pm", "label": "Busiest: Wed noon" } }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### How sure a number is
|
|
165
|
+
|
|
166
|
+
Most numbers are counted from a sample: 71% of 75 days is not 71% of 7,500. When the reader will compare such
|
|
167
|
+
numbers, draw how sure each one is.
|
|
168
|
+
|
|
169
|
+
Ranges on `bars`, `series` and `lines`: give each bar or point a `low` and a `high` around its value, and say once
|
|
170
|
+
in `range` what they are, in the reader's words ("likely range (95%)", "lowest to highest week"); it is printed under
|
|
171
|
+
the reading line beside the mark it describes. A bar steps back to a pale fill, its range is drawn across its end in
|
|
172
|
+
the bar's colour and the range's ends are printed after the value; a series or line gets a band. Accent the bar whose
|
|
173
|
+
range the title is about. Give every item a
|
|
174
|
+
range or none, since one without reads as certain. figkit refuses a range that does not contain its value, a range
|
|
175
|
+
with no `range` text, and a bar range below zero. `n` on a bar prints its sample size under its label, as "14 runs" when `each` names
|
|
176
|
+
the things counted (`"each": "runs"`), else "n = 14"; give every bar one or none. `ref` on a `series`,
|
|
177
|
+
`{ "y": 67, "label": "Jul to mid-Oct: 67%" }`, draws a dashed level to compare the line with, named in the key line. A percentage of fewer than 10 things is refused: show it as a count (`"show": "3 of 6"`) or draw `units`.
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{ "type": "bars", "title": "The new reply check catches more, but the ranges still overlap",
|
|
181
|
+
"note": "Share of unsafe replies caught, on the same test set", "unit": "%", "max": 100, "range": "likely range (95%)", "each": "replies",
|
|
182
|
+
"bars": [ { "label": "Current check", "value": 64, "low": 53, "high": 74, "n": 72 },
|
|
183
|
+
{ "label": "New check", "value": 78, "low": 67, "high": 86, "n": 72, "tone": "accent" } ] }
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The title claims only what the ranges allow: when they overlap, "catches more" needs a "but".
|
|
187
|
+
|
|
188
|
+
`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
|
+
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
|
+
Dots that would overlap stack upward and downward, so none hides another, and each group's count is printed. `note`
|
|
191
|
+
(required) says what one dot is. `each` names what a dot is, so counts read "50 runs". `summary` draws the `"median"` or `"mean"` as a line across each group, labelled;
|
|
192
|
+
`mark` rings one value and labels it; `zero` starts the axis at zero; `tone` on a group colours its dots.
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
{ "type": "dots", "title": "Most builds finish in four minutes; a few take over ten",
|
|
196
|
+
"note": "One dot per build: minutes from push to result", "unit": " min", "summary": "median", "zero": true, "each": "builds",
|
|
197
|
+
"groups": [ { "label": "Main branch", "values": [3.8, 4.1, 3.6, 11.8, 4.0, 12.6, 4.2] },
|
|
198
|
+
{ "label": "Pull requests", "values": [4.9, 5.2, 4.6, 5.8, 5.1], "tone": "accent" } ],
|
|
199
|
+
"mark": { "group": "Main branch", "value": 12.6, "label": "12.6 min, cache miss" } }
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`scatter`: two measures against each other, one dot per thing measured, up to 1000. It is its own type, not a mode
|
|
203
|
+
of `dots`, because it takes pairs and answers a different question: whether one measure moves with the other. `x`
|
|
204
|
+
and `y` each need a `label` saying what they measure, and take `unit`, `"log": true` for measures that span several
|
|
205
|
+
powers of ten (every value above zero; the axis runs to whole powers of ten), `zero`, and `time` (`"seconds"`,
|
|
206
|
+
`"minutes"`, `"hours"` or `"days"`, the unit the values are in) so ticks read 1 min, 1 h, 1 day, 1 wk.
|
|
207
|
+
`"medians": true` draws the median y of each band of x (each power of ten on a log axis, else fifths) in the accent
|
|
208
|
+
colour, with the middle half of the band's dots shaded behind it, for bands with 5 or more dots: the relation, or its
|
|
209
|
+
absence, drawn without fitting a model. `label` names a point beside it, at most 5; `tone` colours it.
|
|
210
|
+
figkit draws no fitted trend line: it claims more than dots do. Say the relation in the title only when the dots or
|
|
211
|
+
the medians show it, and "barely" when they mostly do not.
|
|
212
|
+
|
|
213
|
+
```json
|
|
214
|
+
{ "type": "scatter", "title": "Bigger pull requests wait longer for review",
|
|
215
|
+
"note": "One dot per merged pull request",
|
|
216
|
+
"x": { "label": "Lines changed", "log": true }, "y": { "label": "Time to first review", "time": "hours", "zero": true },
|
|
217
|
+
"points": [ { "x": 12, "y": 1.5 }, { "x": 120, "y": 5.5 }, { "x": 540, "y": 22, "label": "Search rewrite", "tone": "accent" } ] }
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`units`: a small count made concrete, "11 of 29" as 29 squares with 11 filled. `total` is at most 100: beyond that
|
|
221
|
+
the squares cannot be counted by eye, and a `split` bar of the shares reads better. Squares sit ten to a row and fill
|
|
222
|
+
column by column, so each part is one block from the left. The blocks show counts, not order: if when things happened
|
|
223
|
+
matters, draw it over time instead. Up to 3 `parts` fill squares in order (the first in the accent colour
|
|
224
|
+
unless it has a `tone`); `rest` (required) names the empty squares, so at least one is left. Counts are whole things.
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{ "type": "units", "title": "11 of the 29 flagged days were real problems",
|
|
228
|
+
"note": "One square per day the monitor flagged in October", "total": 29,
|
|
229
|
+
"parts": [ { "label": "real problems", "count": 11 } ], "rest": "false alarms" }
|
|
230
|
+
```
|
|
231
|
+
|
|
108
232
|
`entities`: what the things are and which points at which. 2 to 5 `records`, each with an `id` you use in `refs`,
|
|
109
233
|
a `name` in the reader's words, and up to 4 `fields` that say what it holds (put a table name here only if the reader
|
|
110
234
|
needs it). Each of up to 8 `refs` is a pointer the `from` record holds to the `to` record: `count` is how many `from`
|
|
@@ -129,9 +253,40 @@ in plain words, not in-house terms or button labels.
|
|
|
129
253
|
|
|
130
254
|
More than 5 records is two figures: the core records, then one corner of it with the core record it hangs off.
|
|
131
255
|
|
|
132
|
-
|
|
256
|
+
`sequence`: who calls whom, in order. Use it when the question is "what happens when…", "walk me through a run",
|
|
257
|
+
"who starts what": the order is the point and more than one part takes part. A table of steps (step, who starts it,
|
|
258
|
+
who is involved) is this picture without the shape; draw it instead. A flow of boxes and arrows shows what connects
|
|
259
|
+
to what, not when; use it for structure. 2 to 5 `participants`, left to right in the order the reader meets them, each
|
|
260
|
+
an `id`, a `name` and an optional `kind` (where it runs: "GitHub Actions", "Cloud Run job"). 2 to 12 `messages` in
|
|
261
|
+
the order they happen, each from one participant to another with a `label` saying what is asked for or sent. figkit
|
|
262
|
+
numbers the steps, so your prose can say "step 4". `"reply": true` is the answer to an earlier call, drawn dashed;
|
|
263
|
+
leave replies out when nothing comes back that the reader needs. A message from a participant to itself is work it
|
|
264
|
+
does on its own. `note` adds one short line under a step: a condition ("only if the tests pass"), a wait ("up to an
|
|
265
|
+
hour"). `repeats` frames steps `from` to `to` (numbered from 1) that run more than once, with a `label` such as "For
|
|
266
|
+
each model". Accent one participant, or the steps the title is about, not both. The title claims only what the
|
|
267
|
+
steps show.
|
|
268
|
+
|
|
269
|
+
```json
|
|
270
|
+
{ "type": "sequence", "title": "A failed card charge is retried before the shopper hears anything",
|
|
271
|
+
"participants": [ { "id": "app", "name": "Shop app", "kind": "browser" }, { "id": "api", "name": "Checkout API" },
|
|
272
|
+
{ "id": "pay", "name": "Card processor" } ],
|
|
273
|
+
"messages": [ { "from": "app", "to": "api", "label": "Place order" },
|
|
274
|
+
{ "from": "api", "to": "pay", "label": "Charge the card", "tone": "accent" },
|
|
275
|
+
{ "from": "pay", "to": "api", "label": "Approved or declined", "reply": true },
|
|
276
|
+
{ "from": "api", "to": "app", "label": "Order number", "reply": true } ],
|
|
277
|
+
"repeats": [ { "from": 2, "to": 3, "label": "Up to 3 tries" } ] }
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
More than 5 participants or 12 steps is two figures: the whole run with minor parts merged into one participant, then
|
|
281
|
+
the stage that needs detail. Leave out steps that only log or record what happened unless the question is about them.
|
|
282
|
+
|
|
283
|
+
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`.
|
|
133
285
|
|
|
134
286
|
- `title` (required) is the point, 14 words at most. `note` is the reading line under it; `source` a footer.
|
|
287
|
+
- `"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.
|
|
289
|
+
- Keep `note` to what is measured, and "lower is better" where it applies; dates and method go in `source`.
|
|
135
290
|
- `unit` is printed straight after each number. `show` on an item replaces its printed value, e.g. `"11 of 29"`.
|
|
136
291
|
- `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.
|
|
137
292
|
- `bars` takes `max` to fix the end of the axis (e.g. 100 for percentages) and a short `note` per bar.
|
|
@@ -141,7 +296,7 @@ Flows (boxes and arrows) have no spec: write them in HTML, below.
|
|
|
141
296
|
## The page
|
|
142
297
|
|
|
143
298
|
```html
|
|
144
|
-
<main class="fig"> <!-- add "detail" for a map to open and read; "paper" for a light figure for docs -->
|
|
299
|
+
<main class="fig"> <!-- add "detail" for a map to open and read; "paper" for a light figure for docs; "report" for a light chart in a page, in one sans font -->
|
|
145
300
|
<header>
|
|
146
301
|
<h1>The point, stated. <em>The part to notice</em></h1>
|
|
147
302
|
<p class="read">How to read it. <b class="t-accent">Orange</b> means code.</p>
|
|
@@ -262,7 +417,10 @@ A few numbers compared:
|
|
|
262
417
|
</fig-bars>
|
|
263
418
|
```
|
|
264
419
|
|
|
265
|
-
|
|
420
|
+
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
|
|
423
|
+
Observable Plot, already loaded with d3:
|
|
266
424
|
|
|
267
425
|
```html
|
|
268
426
|
<div id="chart"></div>
|
|
@@ -279,7 +437,7 @@ Anything else (lines, dots, intervals, distributions, small multiples) is Observ
|
|
|
279
437
|
</script>
|
|
280
438
|
```
|
|
281
439
|
|
|
282
|
-
`theme` has `ink`, `muted`, `soft`, `line`, `accent`, `second`, `good`, `bad`. Draw the series in `
|
|
440
|
+
`theme` has `ink`, `muted`, `soft`, `fill`, `fill2`, `line`, `accent`, `second`, `good`, `bad`. Draw the series in `fill`, the thing to
|
|
283
441
|
notice in `accent`, and label it directly on the chart; no legends. Start bar axes at zero.
|
|
284
442
|
|
|
285
443
|
## Screenshots
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Node 20 or later, and a Chromium-based browser: figkit uses Chrome or Edge if on
|
|
|
16
16
|
|
|
17
17
|
```
|
|
18
18
|
npm install @liggi/figkit
|
|
19
|
-
npx figkit guide # prints GUIDE.md, the rules for writing figures
|
|
19
|
+
npx @liggi/figkit guide # prints GUIDE.md, the rules for writing figures
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
## A figure from HTML
|
|
@@ -39,15 +39,16 @@ Save this as `order.html`:
|
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
```
|
|
42
|
-
npx figkit order.html --out order.png
|
|
42
|
+
npx @liggi/figkit order.html --out order.png
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|

|
|
46
46
|
|
|
47
47
|
## A chart from a spec
|
|
48
48
|
|
|
49
|
-
For one chart of a known type (`split`, `bars`, `before-after`, `dumbbell`, `series`
|
|
50
|
-
records and the references between them, give the data and figkit does the
|
|
49
|
+
For one chart of a known type (`split`, `bars`, `before-after`, `dumbbell`, `series`, `groups`, `lines`, `panels`,
|
|
50
|
+
`heatmap`, `dots`, `scatter`, `units`), or an `entities` picture of records and the references between them, give the data and figkit does the
|
|
51
|
+
rest, including the layout. Add `"style": "report"` for a light chart that sits in a page. Save this as `checkout.json`:
|
|
51
52
|
|
|
52
53
|
```json
|
|
53
54
|
{ "type": "bars", "title": "The new checkout halves the time to pay",
|
|
@@ -58,7 +59,7 @@ records and the references between them, give the data and figkit does the rest,
|
|
|
58
59
|
```
|
|
59
60
|
|
|
60
61
|
```
|
|
61
|
-
npx figkit checkout.json --out checkout.png
|
|
62
|
+
npx @liggi/figkit checkout.json --out checkout.png
|
|
62
63
|
```
|
|
63
64
|
|
|
64
65
|

|
package/bin/fig.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import os from 'node:os';
|
|
9
9
|
import path from 'node:path';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
10
11
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
11
12
|
import { chromium } from 'playwright-core';
|
|
12
13
|
import { ensureBuilt } from '../viz/build.mjs';
|
|
@@ -14,6 +15,12 @@ import { parseSpec } from '../viz/spec.js';
|
|
|
14
15
|
|
|
15
16
|
const HOME = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
16
17
|
const url = (...p) => pathToFileURL(path.join(HOME, ...p)).href;
|
|
18
|
+
/** A dependency's file, wherever npm put the package: figkit's own node_modules, or one hoisted above it. */
|
|
19
|
+
const dep = (name, ...p) => {
|
|
20
|
+
const dir = createRequire(import.meta.url).resolve.paths(name).map((d) => path.join(d, name)).find((d) => fs.existsSync(d));
|
|
21
|
+
if (!dir) { console.error(`figkit: cannot find ${name}; run npm install in the figkit folder.`); process.exit(1); }
|
|
22
|
+
return pathToFileURL(path.join(dir, ...p)).href;
|
|
23
|
+
};
|
|
17
24
|
const args = process.argv.slice(2);
|
|
18
25
|
|
|
19
26
|
if (args.length === 0 || args[0] === '-h' || args[0] === '--help') {
|
|
@@ -39,18 +46,19 @@ if (/\.json$/i.test(src)) {
|
|
|
39
46
|
console.log(`This chart spec has ${spec.problems.length} problem${spec.problems.length === 1 ? '' : 's'}:\n${spec.problems.map((p) => ` - ${p}`).join('\n')}`);
|
|
40
47
|
process.exit(1);
|
|
41
48
|
}
|
|
42
|
-
|
|
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(' ');
|
|
50
|
+
body = `<main class="fig ${cls}"><script type="application/json" id="figkit-spec">${JSON.stringify(spec.spec).replace(/</g, '\\u003c')}</script></main>`;
|
|
43
51
|
} else if (!/class="[^"]*\bfig\b/.test(body)) body = `<main class="fig">\n${body}\n</main>`;
|
|
44
52
|
await ensureBuilt();
|
|
45
53
|
|
|
46
54
|
const fonts = ['newsreader/400.css', 'newsreader/400-italic.css', 'geist-sans/400.css', 'geist-sans/500.css', 'geist-sans/600.css', 'geist-mono/400.css', 'geist-mono/500.css']
|
|
47
|
-
.map((f) => `<link rel="stylesheet" href="${
|
|
55
|
+
.map((f) => `<link rel="stylesheet" href="${dep('@fontsource/' + f.split('/')[0], f.split('/')[1])}">`).join('\n');
|
|
48
56
|
const page_html = `<!doctype html><html><head><meta charset="utf-8">
|
|
49
57
|
<base href="${pathToFileURL(path.dirname(src) + path.sep).href}">
|
|
50
58
|
${fonts}
|
|
51
59
|
<link rel="stylesheet" href="${url('runtime/fig.css')}">
|
|
52
|
-
<script src="${
|
|
53
|
-
<script src="${
|
|
60
|
+
<script src="${dep('d3', 'dist/d3.min.js')}"></script>
|
|
61
|
+
<script src="${dep('@observablehq/plot', 'dist/plot.umd.min.js')}"></script>
|
|
54
62
|
<script src="${url('dist/viz.iife.js')}"></script>
|
|
55
63
|
<script src="${url('runtime/fig.js')}"></script>
|
|
56
64
|
<style>body { display: inline-block; }</style>
|
|
@@ -93,7 +101,7 @@ try {
|
|
|
93
101
|
if (phone) await page.addInitScript(() => { window.__figPhone = true; });
|
|
94
102
|
await page.goto(pathToFileURL(wrapped).href);
|
|
95
103
|
await page.waitForFunction('window.__figDone === true', null, { timeout: 30000 });
|
|
96
|
-
const r = await page.evaluate(() => ({ problems: window.fig.problems, info: window.fig.info, sides: !!document.querySelector('.fig .sides, .fig .entities'), entities: !!document.querySelector('.fig .entities') }));
|
|
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') }));
|
|
97
105
|
await page.locator('.fig').first().screenshot({ path: shot });
|
|
98
106
|
await page.close();
|
|
99
107
|
return { ...r, problems: [...errors.map((e) => `Script error: ${e}`), ...r.problems] };
|
|
@@ -108,7 +116,7 @@ try {
|
|
|
108
116
|
|
|
109
117
|
const { info } = desk;
|
|
110
118
|
const lines = [`Figure: ${png} (${info.width * 2}×${info.height * 2})`];
|
|
111
|
-
if (mob) lines.push(`Phone layout: ${phonePng} (${mob.info.width * 2}×${mob.info.height * 2}), ${desk.entities ? 'the records in two columns' : 'the sides stacked'}, 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.panels ? 'the panels stacked' : desk.stack ? 'the sides stacked' : 'at phone width'}, for a viewer to show on a phone`);
|
|
112
120
|
lines.push(`Phone inline: ${inline} (preview only)`);
|
|
113
121
|
const ph = (mob ?? desk).info.phone;
|
|
114
122
|
const sizes = [['the title', ph?.title], ['headings', ph?.heading], ['body', ph?.body]].filter(([, v]) => v != null).map(([k, v]) => `${k} ${v}px`);
|