@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/runtime/fig.css
CHANGED
|
@@ -8,6 +8,8 @@ html, body { margin: 0; padding: 0; background: transparent; }
|
|
|
8
8
|
--paper: #2d3142; --panel: #252938; --ink: #f5f5f5; --muted: #c3c6cf; --soft: #949eb2; --line: #8d95ad; --hair: rgba(245,245,245,.16);
|
|
9
9
|
--accent: #f08a59; --second: #739fdf; --good: #6fc392; --bad: #ef6b73;
|
|
10
10
|
--on-fill: #1d2030;
|
|
11
|
+
/* grey marks: the ones not to notice; fill2 a darker grey for a second series */
|
|
12
|
+
--fill: var(--soft); --fill2: var(--line);
|
|
11
13
|
--sans: 'Geist Sans', 'Geist', system-ui, sans-serif;
|
|
12
14
|
--serif: 'Newsreader', Georgia, serif;
|
|
13
15
|
--mono: 'Geist Mono', ui-monospace, monospace;
|
|
@@ -135,7 +137,7 @@ fig-arrow { display: none; }
|
|
|
135
137
|
/* ---------- split bar: parts of a whole ---------- */
|
|
136
138
|
fig-split { display: flex; flex-direction: column; gap: 10px; }
|
|
137
139
|
fig-split .fs-bar { display: flex; gap: 3px; height: 58px; }
|
|
138
|
-
fig-split .fs-seg { --tone: var(--
|
|
140
|
+
fig-split .fs-seg { --tone: var(--fill); background: var(--tone); color: var(--on-fill); min-width: 4px; display: flex; align-items: center; gap: 10px; padding: 0 14px; overflow: hidden; white-space: nowrap; }
|
|
139
141
|
fig-split .fs-seg:first-child { border-radius: 8px 0 0 8px; } fig-split .fs-seg:last-child { border-radius: 0 8px 8px 0; }
|
|
140
142
|
fig-split .fs-seg.fs-out { padding: 0; }
|
|
141
143
|
fig-split .fs-seg.fs-out > * { display: none; }
|
|
@@ -151,7 +153,7 @@ fig-change { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0,
|
|
|
151
153
|
fig-change .fc-side { display: flex; flex-direction: column; gap: 10px; }
|
|
152
154
|
fig-change .fc-when { font-size: var(--small); font-weight: 600; color: var(--muted); }
|
|
153
155
|
fig-change .fc-num { font-family: var(--serif); font-weight: 400; font-size: calc(var(--num) * 1.5); line-height: 1; letter-spacing: -.01em; color: var(--ink); }
|
|
154
|
-
fig-change .fc-bar { height: 14px; min-width: 3px; background: var(--
|
|
156
|
+
fig-change .fc-bar { height: 14px; min-width: 3px; background: var(--fill); border-radius: 3px; }
|
|
155
157
|
fig-change .before .fc-num { color: var(--muted); }
|
|
156
158
|
fig-change .after .fc-num { color: var(--tone); }
|
|
157
159
|
fig-change .after .fc-bar { background: var(--tone); }
|
|
@@ -161,7 +163,7 @@ fig-change .fc-arrow { color: var(--soft); display: flex; }
|
|
|
161
163
|
fig-bars { display: grid; grid-template-columns: max-content minmax(0, 1fr); column-gap: 16px; row-gap: 10px; align-items: center; }
|
|
162
164
|
fig-bars .fb-label { font-size: var(--body); }
|
|
163
165
|
fig-bars .fb-track { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
164
|
-
fig-bars .fb-bar { --tone: var(--
|
|
166
|
+
fig-bars .fb-bar { --tone: var(--fill); height: 26px; background: var(--tone); border-radius: 3px; min-width: 3px; flex: 0 0 auto; }
|
|
165
167
|
fig-bars .fb-val { font-family: var(--serif); font-size: 1.3em; font-weight: 400; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
166
168
|
fig-bars .fb-val.lead { color: var(--tone); }
|
|
167
169
|
fig-bars .fb-sub { font-weight: 400; color: var(--soft); font-size: var(--small); margin-left: 6px; }
|
|
@@ -170,7 +172,7 @@ fig-bars .fb-sub { font-weight: 400; color: var(--soft); font-size: var(--small)
|
|
|
170
172
|
.fig .chart { min-width: 0; }
|
|
171
173
|
.fig .fd-key { display: flex; gap: 28px; margin: 0 0 18px; font-size: var(--small); font-weight: 600; color: var(--soft); }
|
|
172
174
|
.fig .fd-key span { display: inline-flex; align-items: center; gap: 8px; }
|
|
173
|
-
.fig .fd-key i { display: inline-block; width: 13px; height: 13px; border-radius: 50%; background: var(--
|
|
175
|
+
.fig .fd-key i { display: inline-block; width: 13px; height: 13px; border-radius: 50%; background: var(--fill); }
|
|
174
176
|
.fig .fd-key .fd-to i { width: 0; height: 0; border-radius: 0; background: none; border-left: 13px solid currentColor; border-top: 7.5px solid transparent; border-bottom: 7.5px solid transparent; }
|
|
175
177
|
.fig .fd-key .fd-to.back i { transform: scaleX(-1); }
|
|
176
178
|
.fig.chart > footer { border-top: 0; padding-top: 0; }
|
|
@@ -186,3 +188,89 @@ fig-bars .fb-sub { font-weight: 400; color: var(--soft); font-size: var(--small)
|
|
|
186
188
|
.fig .t-second.t-second { color: var(--second); }
|
|
187
189
|
.fig .t-good.t-good { color: var(--good); } .fig .t-bad.t-bad { color: var(--bad); }
|
|
188
190
|
.fig .decision:not(.accent):not(.second):not(.good):not(.bad) { --tone: var(--line); }
|
|
191
|
+
|
|
192
|
+
/* ---------- report: a light chart that sits in a page (a research report, a doc), set in one sans ---------- */
|
|
193
|
+
.fig.report {
|
|
194
|
+
--paper: #ffffff; --panel: #ffffff; --ink: #1d2130; --muted: #5b6170; --soft: #7a8092; --line: #9aa0ad; --hair: rgba(29,33,48,.12);
|
|
195
|
+
--fill: #b4b8c2; --fill2: #7d8391;
|
|
196
|
+
--accent: #cf5a23; --second: #2f6bc4; --good: #2f8f5b; --bad: #c9414a; --on-fill: #ffffff;
|
|
197
|
+
--serif: var(--sans);
|
|
198
|
+
--w: 720px; --pad: 32px; --gap: 14px;
|
|
199
|
+
--title: 26px; --read: 18px; --h: 18px; --body: 17px; --small: 16px; --num: 30px;
|
|
200
|
+
border: 1px solid #e6e2d8; border-radius: 10px; gap: 20px;
|
|
201
|
+
}
|
|
202
|
+
.fig.panels-fig { --w: 1000px; }
|
|
203
|
+
.fig.panels-fig.phone { --w: 560px; }
|
|
204
|
+
.fig.report h1 { font-weight: 600; letter-spacing: -.01em; line-height: 1.2; }
|
|
205
|
+
.fig.report > header { gap: 6px; }
|
|
206
|
+
.fig.report > footer { border-top: 0; padding-top: 0; }
|
|
207
|
+
.fig.report.phone { --w: 400px; --pad: 20px; --title: 22px; --read: 16px; --body: 16px; --small: 15px; }
|
|
208
|
+
.fig.report fig-bars .fb-val, .fig.report fig-split .fs-seg b, .fig.report fig-change .fc-num { font-weight: 500; letter-spacing: 0; }
|
|
209
|
+
.fig.report fig-bars .fb-val { font-size: 1.05em; }
|
|
210
|
+
.fig.report fig-bars .fb-val.lead { font-weight: 600; }
|
|
211
|
+
|
|
212
|
+
/* panels: the same chart drawn for each part, side by side, one above the other on a phone */
|
|
213
|
+
.fig .panels { display: grid; grid-template-columns: repeat(var(--cols, 2), minmax(0, 1fr)); column-gap: 36px; row-gap: 28px; }
|
|
214
|
+
.fig.phone .panels { grid-template-columns: minmax(0, 1fr); }
|
|
215
|
+
.fig .panel > h2 { font-size: calc(var(--h) * 1.15); margin: 0 0 4px; }
|
|
216
|
+
|
|
217
|
+
/* heat map scale: what the colour means, at readable size */
|
|
218
|
+
.fig .hm-key { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; font-size: var(--small); color: var(--muted); margin: 0 0 12px; }
|
|
219
|
+
.fig .hm-key i { display: inline-block; width: 160px; height: 14px; border-radius: 3px; }
|
|
220
|
+
.fig.phone .hm-key i { width: 84px; }
|
|
221
|
+
|
|
222
|
+
/* ---------- sequence: who calls whom, in order (a spec of type "sequence") ---------- */
|
|
223
|
+
.fig .sequence { position: relative; }
|
|
224
|
+
.fig .sequence > svg { position: absolute; left: 0; top: 0; overflow: visible; }
|
|
225
|
+
.fig .seq-lanes { position: relative; }
|
|
226
|
+
.fig .seq-who { position: absolute; }
|
|
227
|
+
.fig .seq-who { text-align: center; align-items: center; justify-content: center; }
|
|
228
|
+
.fig.phone .seq-who { padding: 10px 8px; }
|
|
229
|
+
.fig.phone .seq-who > h3 { font-size: var(--body); }
|
|
230
|
+
/* The paper shows only behind the words themselves, line by line, so a line passing a label stops just at its text. */
|
|
231
|
+
.fig .seq-msg { position: absolute; box-sizing: border-box; width: max-content; display: flex; gap: 10px; align-items: baseline; padding: 1px 6px; }
|
|
232
|
+
.fig .seq-ink { background: var(--paper); box-decoration-break: clone; -webkit-box-decoration-break: clone; padding: 1px 0; }
|
|
233
|
+
.fig .seq-n { font-size: var(--small); font-variant-numeric: tabular-nums; color: var(--soft); flex: 0 0 auto; min-width: 2ch; text-align: right; }
|
|
234
|
+
.fig .seq-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
235
|
+
.fig .seq-note { font-size: var(--small); color: var(--muted); }
|
|
236
|
+
.fig .seq-msg.reply .seq-text { color: var(--muted); }
|
|
237
|
+
.fig .seq-msg.accent .seq-text, .fig .seq-msg.accent .seq-n { color: var(--accent); }
|
|
238
|
+
.fig .seq-msg.accent .seq-text { font-weight: 600; }
|
|
239
|
+
.fig .seq-msg.accent .seq-note { font-weight: 400; }
|
|
240
|
+
.fig .seq-frame-label { position: absolute; white-space: nowrap; background: var(--paper); padding: 0 8px; font-size: var(--small); font-weight: 600; color: var(--muted); }
|
|
241
|
+
.fig .seq-lanes.tight .seq-who { padding: 8px 4px; }
|
|
242
|
+
.fig .seq-lanes.tight .seq-who > h3 { font-size: var(--small); }
|
|
243
|
+
|
|
244
|
+
/* ranges: what they are, said once under the reading line; on bars a line with end caps across the bar's end */
|
|
245
|
+
.fig .range-key { display: flex; flex-wrap: wrap; column-gap: 28px; row-gap: 6px; margin: 2px 0 0; font-size: var(--small); color: var(--muted); }
|
|
246
|
+
.fig .range-key > span { display: inline-flex; align-items: center; gap: 10px; }
|
|
247
|
+
.fig .range-key i { flex: 0 0 auto; }
|
|
248
|
+
.fig .range-key i.dash { width: 34px; height: 0; border-top: 2px dashed var(--ink); }
|
|
249
|
+
.fig .range-key i.whisker { position: relative; width: 34px; height: 14px; border-left: 2px solid var(--ink); border-right: 2px solid var(--ink); }
|
|
250
|
+
.fig .range-key i.whisker::after { content: ''; position: absolute; left: 0; right: 0; top: 6px; border-top: 2px solid var(--ink); }
|
|
251
|
+
.fig .range-key i.median { width: 34px; height: 4px; border-radius: 2px; }
|
|
252
|
+
.fig .range-key i.band { width: 34px; height: 14px; border-radius: 2px; background: var(--fill); opacity: .45; }
|
|
253
|
+
fig-bars .fb-track { position: relative; }
|
|
254
|
+
fig-bars .fb-range { --tone: var(--ink); position: absolute; top: 50%; height: 12px; margin-top: -6px; border-left: 2px solid var(--tone); border-right: 2px solid var(--tone); pointer-events: none; }
|
|
255
|
+
fig-bars .fb-range::after { content: ''; position: absolute; left: 0; right: 0; top: 4.5px; border-top: 3px solid var(--tone); }
|
|
256
|
+
fig-bars .fb-bar.dim { opacity: .4; }
|
|
257
|
+
fig-bars .fb-val .fb-sub + .fb-sub { margin-left: 10px; }
|
|
258
|
+
fig-bars .fb-label .fb-n { display: block; font-size: var(--small); color: var(--soft); }
|
|
259
|
+
|
|
260
|
+
/* dots: the count of a single group, above its dots */
|
|
261
|
+
.fig .dots-n { margin: 0 0 4px; font-size: var(--small); color: var(--muted); }
|
|
262
|
+
|
|
263
|
+
/* units: one square per thing counted */
|
|
264
|
+
.fig .units { display: flex; flex-direction: column; gap: 22px; }
|
|
265
|
+
.fig .un-grid { display: grid; }
|
|
266
|
+
.fig .un-grid i { width: var(--un); height: var(--un); border-radius: 4px; }
|
|
267
|
+
.fig .un-grid i.empty, .fig .un-key i.empty { background: var(--hair); }
|
|
268
|
+
.fig .un-key { display: flex; flex-wrap: wrap; column-gap: 28px; row-gap: 8px; font-size: var(--body); color: var(--muted); }
|
|
269
|
+
.fig .un-key span { display: inline-flex; align-items: center; gap: 8px; }
|
|
270
|
+
.fig .un-key i { width: 18px; height: 18px; border-radius: 3px; display: inline-block; }
|
|
271
|
+
.fig .un-key b { color: var(--ink); font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
272
|
+
|
|
273
|
+
/* bars on a phone: the name and count above each bar, so the bar and its range get the full width */
|
|
274
|
+
.fig.phone fig-bars { grid-template-columns: minmax(0, 1fr); row-gap: 4px; }
|
|
275
|
+
.fig.phone fig-bars .fb-label { margin-top: 10px; }
|
|
276
|
+
.fig.phone fig-bars .fb-label .fb-n { display: inline; margin-left: 8px; }
|
package/runtime/fig.js
CHANGED
|
@@ -544,7 +544,7 @@
|
|
|
544
544
|
if (!el) { fig.problems.push(`fig.plot: nothing matches ${target}.`); return; }
|
|
545
545
|
el.classList.add('chart');
|
|
546
546
|
const width = Math.floor(el.clientWidth) || 640;
|
|
547
|
-
const t = { ink: css('--ink'), muted: css('--muted'), soft: css('--soft'), line: css('--line'), hair: css('--hair'), accent: css('--accent'), second: css('--second'), good: css('--good'), bad: css('--bad'), paper: css('--paper'), panel: css('--panel') };
|
|
547
|
+
const t = { ink: css('--ink'), muted: css('--muted'), soft: css('--soft'), fill: css('--fill'), fill2: css('--fill2'), line: css('--line'), hair: css('--hair'), accent: css('--accent'), second: css('--second'), good: css('--good'), bad: css('--bad'), paper: css('--paper'), panel: css('--panel') };
|
|
548
548
|
try {
|
|
549
549
|
const opts = build({ Plot: window.Plot, d3: window.d3, width, theme: t });
|
|
550
550
|
const size = parseFloat(css('--small')) || 15;
|
|
@@ -567,6 +567,13 @@
|
|
|
567
567
|
for (const b of marks.slice(i + 1)) if (overlap(a.r, b.r) && !a.e.contains(b.e) && !b.e.contains(a.e)) P.push(`Chart labels overlap: "${a.e.textContent.trim()}" and "${b.e.textContent.trim()}".`);
|
|
568
568
|
}
|
|
569
569
|
|
|
570
|
+
// Chart labels over dots: a label drawn across a dot hides the value it sits on.
|
|
571
|
+
const dots = [...R.querySelectorAll('.chart svg circle')].filter(visible).map(rectOf).map((r) => grow(r, -1.5));
|
|
572
|
+
for (const a of marks) {
|
|
573
|
+
const hit = dots.filter((d) => overlap(grow(a.r, -1), d)).length;
|
|
574
|
+
if (hit) P.push(`Chart label covers ${hit === 1 ? 'a dot' : hit + ' dots'}: "${a.e.textContent.trim()}".`);
|
|
575
|
+
}
|
|
576
|
+
|
|
570
577
|
// Text: too small, cut off, or past its box.
|
|
571
578
|
const small = [];
|
|
572
579
|
for (const o of obs.flatMap((b) => (b.text ? [b.el] : [...b.el.querySelectorAll('*')].filter((e) => [...e.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim()))))) {
|
|
@@ -635,7 +642,10 @@
|
|
|
635
642
|
if (near.length) P.push(`Almost aligned but not quite: ${near.slice(0, 6).join('; ')}${near.length > 6 ? `; and ${near.length - 6} more` : ''}. Put them in one row, column or grid so they share the edge.`);
|
|
636
643
|
|
|
637
644
|
// Too much on the page.
|
|
638
|
-
|
|
645
|
+
// Panels repeat the same labels in each panel; a label read once counts once.
|
|
646
|
+
const panelText = R.querySelector('.panels') ? [...new Set([...R.querySelectorAll('.panels text, .panels h2')].map((e) => e.textContent.trim()))].join(' ') : null;
|
|
647
|
+
const outside = R.querySelector('.panels') ? [...R.children].filter((c) => !c.matches('script, style') && !c.querySelector('.panels') && !c.matches('.panels')).map((c) => c.innerText).join(' ') : null;
|
|
648
|
+
const words = (panelText !== null ? outside + ' ' + panelText : R.innerText).split(/\s+/).filter(Boolean).length;
|
|
639
649
|
const nodes = R.querySelectorAll('.node:not(.bare), .decision').length, arrows = (fig.wires ?? []).length;
|
|
640
650
|
const budget = detail ? { words: 190, nodes: 18, arrows: 18 } : { words: 70, nodes: 5, arrows: 8 };
|
|
641
651
|
if (words > budget.words) P.push(`${words} words; a ${detail ? 'detail' : 'glance'} figure holds about ${budget.words}. Cut sentences that explain, and keep labels.`);
|
|
@@ -644,6 +654,11 @@
|
|
|
644
654
|
const tones = TONES.filter((t) => R.querySelector(`.${t}, [${t}], .t-${t}`));
|
|
645
655
|
if (tones.length > 2) P.push(`${tones.length} colours carry meaning (${tones.join(', ')}); use one for what the reader must notice and at most one more.`);
|
|
646
656
|
for (const o of obs) if (o.text || o.el.matches('.node')) { const t = o.el.innerText ?? ''; if ((t.match(/ [·|•] /g) ?? []).length >= 1 && !o.el.querySelector('*')) P.push(`${name(o.el)} strings facts together with separators; give each fact its own line or cut one.`); }
|
|
657
|
+
// A face that never loaded draws in whatever the machine has, and every measurement above is of that fallback.
|
|
658
|
+
const faces = [...document.fonts];
|
|
659
|
+
const families = [...new Set(['--sans', '--serif'].map((v) => css(v).split(',')[0].replace(/["']/g, '').trim()))];
|
|
660
|
+
const missing = families.filter((f) => !faces.some((x) => x.family.replace(/["']/g, '') === f && x.status === 'loaded'));
|
|
661
|
+
if (missing.length) P.push(`figkit's fonts did not load (${missing.join(', ')}); the figure is drawn in fallback fonts. Reinstall figkit with its dependencies.`);
|
|
647
662
|
const h1 = R.querySelector('h1');
|
|
648
663
|
if (!h1) P.push('No <h1>: give the figure a title that states its point.');
|
|
649
664
|
else if (h1.innerText.split(/\s+/).length > 14) P.push('The title is over 14 words; state the point more shortly.');
|
package/viz/build.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
|
|
6
6
|
const HOME = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
7
|
-
const SOURCES = ['viz
|
|
7
|
+
const SOURCES = [...fs.readdirSync(path.join(HOME, 'viz')).filter((f) => f.endsWith('.js')).map((f) => 'viz/' + f), 'package.json'].map((f) => path.join(HOME, f));
|
|
8
8
|
const OUT = path.join(HOME, 'dist/viz.iife.js');
|
|
9
9
|
|
|
10
10
|
const mtime = (f) => (fs.existsSync(f) ? fs.statSync(f).mtimeMs : 0);
|