@liggi/figkit 0.2.0 → 0.3.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 +62 -3
- package/README.md +3 -2
- package/bin/fig.mjs +14 -6
- package/dist/viz.iife.js +12 -12
- 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/package.json +1 -1
- package/runtime/fig.css +36 -4
- package/runtime/fig.js +10 -2
- package/viz/draw.js +165 -4
- package/viz/spec.js +121 -2
package/GUIDE.md
CHANGED
|
@@ -105,6 +105,60 @@ mark's `x` must be one of them. The y axis starts at zero unless `"zero": false`
|
|
|
105
105
|
"mark": { "x": "2026-10-14", "label": "212, launch day" } }
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
+
`groups`: a few things measured for each of 2 to 6 groups, 2 to 4 series, one bar per series in each group, all
|
|
109
|
+
from zero. Use it when the point is a difference that shows in some groups and not others (before and after, one
|
|
110
|
+
segment against another); for one number per thing, use `bars`. Each group gets a heading and each bar its series
|
|
111
|
+
name and number, so there is no legend and no axis. `accent` is required: `{ "series" }` colours that series in every
|
|
112
|
+
group, `{ "series", "group" }` colours one bar; `"tone": "bad"` or `"good"` when the bar is a result.
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{ "type": "groups", "title": "Only the new carrier got slower in December",
|
|
116
|
+
"note": "Median days from dispatch to doorstep", "unit": " days",
|
|
117
|
+
"series": ["Old carrier", "New carrier"],
|
|
118
|
+
"groups": [ { "label": "November", "values": [2.2, 2.1] }, { "label": "December", "values": [2.4, 3.6] } ],
|
|
119
|
+
"accent": { "series": "New carrier", "group": "December", "tone": "bad" } }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`lines`: 2 or 3 lines on one axis, each named at its right end in its own colour. `accent` (required) names the
|
|
123
|
+
line the title is about; the others are grey. `mark` labels one point on the accent line. `xTicks` sets the axis
|
|
124
|
+
labels, e.g. hours as 12am, 6am, 12pm, 6pm, rather than 0 to 23. More than 3 lines is `panels`.
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{ "type": "lines", "title": "Weekday orders peak at noon", "note": "Orders per hour",
|
|
128
|
+
"series": [ { "label": "Weekdays", "points": [ { "x": 0, "y": 3 }, { "x": 12, "y": 30 }, { "x": 23, "y": 4 } ] },
|
|
129
|
+
{ "label": "Weekends", "points": [ { "x": 0, "y": 4 }, { "x": 12, "y": 22 }, { "x": 23, "y": 6 } ] } ],
|
|
130
|
+
"accent": "Weekdays", "mark": { "x": 12, "label": "30 at noon" },
|
|
131
|
+
"xTicks": [ { "x": 0, "label": "12am" }, { "x": 12, "label": "12pm" } ] }
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`panels`: small multiples, the same `groups` or `lines` chart drawn for 2 to 4 parts side by side, on one shared
|
|
135
|
+
scale so the panels compare; stacked on a phone. `chart` says which. For `groups`, `series` and `accent` are given once
|
|
136
|
+
at the top and each panel has its `groups`; for `lines`, `accent` is the line's label and every panel has the same
|
|
137
|
+
lines in the same order. Use it when one chart would need more than 3 lines or 4 series, or the parts measure
|
|
138
|
+
different things on the same scale.
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{ "type": "panels", "chart": "groups", "title": "The cheaper plan answers almost as fast as the premium one",
|
|
142
|
+
"note": "Share of tickets answered within an hour", "unit": "%", "max": 100,
|
|
143
|
+
"series": ["Basic", "Standard", "Premium"], "accent": { "series": "Standard" },
|
|
144
|
+
"panels": [ { "label": "Email", "groups": [ { "label": "Weekdays", "values": [61, 84, 89] } ] },
|
|
145
|
+
{ "label": "Chat", "groups": [ { "label": "Weekdays", "values": [72, 91, 94] } ] } ] }
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`heatmap`: a grid of values over two dimensions (day by hour, team by week), coloured from pale to dark blue: darker
|
|
149
|
+
is always more. Up to 12 `rows` and 31 `columns`; `values` is one array per row. `note` (required) says what the
|
|
150
|
+
colour counts, and the scale is drawn above the grid. Here the colour is the measure, so the one-accent rule moves:
|
|
151
|
+
`mark` outlines the one cell to notice in the accent colour and names it beside the scale. Cells show their numbers
|
|
152
|
+
when there is room. Columns label themselves at even steps, so give hours as "12am", "1am", … and they read 12am,
|
|
153
|
+
6am, 12pm, 6pm.
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{ "type": "heatmap", "title": "The help desk is busiest midweek mornings", "note": "Tickets opened per hour",
|
|
157
|
+
"rows": ["Mon", "Tue", "Wed"], "columns": ["9am", "10am", "11am", "12pm"],
|
|
158
|
+
"values": [ [22, 25, 27, 28], [23, 26, 29, 30], [24, 27, 30, 32] ],
|
|
159
|
+
"mark": { "row": "Wed", "column": "12pm", "label": "Busiest: Wed noon" } }
|
|
160
|
+
```
|
|
161
|
+
|
|
108
162
|
`entities`: what the things are and which points at which. 2 to 5 `records`, each with an `id` you use in `refs`,
|
|
109
163
|
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
164
|
needs it). Each of up to 8 `refs` is a pointer the `from` record holds to the `to` record: `count` is how many `from`
|
|
@@ -132,6 +186,9 @@ More than 5 records is two figures: the core records, then one corner of it with
|
|
|
132
186
|
Flows (boxes and arrows) have no spec: write them in HTML, below.
|
|
133
187
|
|
|
134
188
|
- `title` (required) is the point, 14 words at most. `note` is the reading line under it; `source` a footer.
|
|
189
|
+
- `"style": "report"` draws a light chart in one sans font, for a chart that sits in a page: a research report, a doc.
|
|
190
|
+
It also draws a phone layout, `chart.phone.png`. Leave it out for the default dark figure for a dark chat.
|
|
191
|
+
- Keep `note` to what is measured, and "lower is better" where it applies; dates and method go in `source`.
|
|
135
192
|
- `unit` is printed straight after each number. `show` on an item replaces its printed value, e.g. `"11 of 29"`.
|
|
136
193
|
- `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
194
|
- `bars` takes `max` to fix the end of the axis (e.g. 100 for percentages) and a short `note` per bar.
|
|
@@ -141,7 +198,7 @@ Flows (boxes and arrows) have no spec: write them in HTML, below.
|
|
|
141
198
|
## The page
|
|
142
199
|
|
|
143
200
|
```html
|
|
144
|
-
<main class="fig"> <!-- add "detail" for a map to open and read; "paper" for a light figure for docs -->
|
|
201
|
+
<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
202
|
<header>
|
|
146
203
|
<h1>The point, stated. <em>The part to notice</em></h1>
|
|
147
204
|
<p class="read">How to read it. <b class="t-accent">Orange</b> means code.</p>
|
|
@@ -262,7 +319,9 @@ A few numbers compared:
|
|
|
262
319
|
</fig-bars>
|
|
263
320
|
```
|
|
264
321
|
|
|
265
|
-
|
|
322
|
+
Grouped bars, several lines, small multiples and heat maps have specs (`groups`, `lines`, `panels`, `heatmap`); use
|
|
323
|
+
them when the chart is the whole figure. Anything else (dots, intervals, distributions, a chart beside boxes) is
|
|
324
|
+
Observable Plot, already loaded with d3:
|
|
266
325
|
|
|
267
326
|
```html
|
|
268
327
|
<div id="chart"></div>
|
|
@@ -279,7 +338,7 @@ Anything else (lines, dots, intervals, distributions, small multiples) is Observ
|
|
|
279
338
|
</script>
|
|
280
339
|
```
|
|
281
340
|
|
|
282
|
-
`theme` has `ink`, `muted`, `soft`, `line`, `accent`, `second`, `good`, `bad`. Draw the series in `
|
|
341
|
+
`theme` has `ink`, `muted`, `soft`, `fill`, `fill2`, `line`, `accent`, `second`, `good`, `bad`. Draw the series in `fill`, the thing to
|
|
283
342
|
notice in `accent`, and label it directly on the chart; no legends. Start bar axes at zero.
|
|
284
343
|
|
|
285
344
|
## Screenshots
|
package/README.md
CHANGED
|
@@ -46,8 +46,9 @@ npx figkit order.html --out order.png
|
|
|
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`), 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",
|
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' : '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 .panels, .fig.report'), entities: !!document.querySelector('.fig .entities'), 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.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`);
|