@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.
@@ -0,0 +1,40 @@
1
+ {
2
+ "type": "groups",
3
+ "style": "report",
4
+ "title": "Only the new carrier got slower in December",
5
+ "note": "Median days from dispatch to doorstep",
6
+ "unit": " days",
7
+ "series": [
8
+ "Old carrier",
9
+ "New carrier"
10
+ ],
11
+ "groups": [
12
+ {
13
+ "label": "October",
14
+ "values": [
15
+ 2.1,
16
+ 2.0
17
+ ]
18
+ },
19
+ {
20
+ "label": "November",
21
+ "values": [
22
+ 2.2,
23
+ 2.1
24
+ ]
25
+ },
26
+ {
27
+ "label": "December",
28
+ "values": [
29
+ 2.4,
30
+ 3.6
31
+ ]
32
+ }
33
+ ],
34
+ "accent": {
35
+ "series": "New carrier",
36
+ "group": "December",
37
+ "tone": "bad"
38
+ },
39
+ "source": "Invented example data"
40
+ }
@@ -0,0 +1,231 @@
1
+ {
2
+ "type": "heatmap",
3
+ "style": "report",
4
+ "title": "The help desk is busiest midweek mornings",
5
+ "note": "Tickets opened per hour",
6
+ "rows": [
7
+ "Mon",
8
+ "Tue",
9
+ "Wed",
10
+ "Thu",
11
+ "Fri",
12
+ "Sat",
13
+ "Sun"
14
+ ],
15
+ "columns": [
16
+ "12am",
17
+ "1am",
18
+ "2am",
19
+ "3am",
20
+ "4am",
21
+ "5am",
22
+ "6am",
23
+ "7am",
24
+ "8am",
25
+ "9am",
26
+ "10am",
27
+ "11am",
28
+ "12pm",
29
+ "1pm",
30
+ "2pm",
31
+ "3pm",
32
+ "4pm",
33
+ "5pm",
34
+ "6pm",
35
+ "7pm",
36
+ "8pm",
37
+ "9pm",
38
+ "10pm",
39
+ "11pm"
40
+ ],
41
+ "values": [
42
+ [
43
+ 3,
44
+ 2,
45
+ 1,
46
+ 1,
47
+ 1,
48
+ 2,
49
+ 6,
50
+ 14,
51
+ 22,
52
+ 25,
53
+ 27,
54
+ 28,
55
+ 30,
56
+ 29,
57
+ 28,
58
+ 27,
59
+ 26,
60
+ 24,
61
+ 20,
62
+ 16,
63
+ 12,
64
+ 9,
65
+ 6,
66
+ 4
67
+ ],
68
+ [
69
+ 3,
70
+ 2,
71
+ 1,
72
+ 1,
73
+ 1,
74
+ 2,
75
+ 6,
76
+ 15,
77
+ 23,
78
+ 26,
79
+ 28,
80
+ 29,
81
+ 32,
82
+ 30,
83
+ 29,
84
+ 28,
85
+ 27,
86
+ 25,
87
+ 21,
88
+ 17,
89
+ 13,
90
+ 9,
91
+ 6,
92
+ 4
93
+ ],
94
+ [
95
+ 3,
96
+ 2,
97
+ 1,
98
+ 1,
99
+ 1,
100
+ 2,
101
+ 6,
102
+ 15,
103
+ 24,
104
+ 27,
105
+ 29,
106
+ 30,
107
+ 32,
108
+ 31,
109
+ 30,
110
+ 29,
111
+ 28,
112
+ 26,
113
+ 22,
114
+ 17,
115
+ 13,
116
+ 10,
117
+ 6,
118
+ 4
119
+ ],
120
+ [
121
+ 3,
122
+ 2,
123
+ 1,
124
+ 1,
125
+ 1,
126
+ 2,
127
+ 6,
128
+ 14,
129
+ 22,
130
+ 26,
131
+ 28,
132
+ 29,
133
+ 31,
134
+ 30,
135
+ 29,
136
+ 28,
137
+ 27,
138
+ 24,
139
+ 20,
140
+ 16,
141
+ 12,
142
+ 9,
143
+ 6,
144
+ 4
145
+ ],
146
+ [
147
+ 3,
148
+ 2,
149
+ 1,
150
+ 1,
151
+ 1,
152
+ 2,
153
+ 5,
154
+ 13,
155
+ 20,
156
+ 22,
157
+ 24,
158
+ 25,
159
+ 27,
160
+ 26,
161
+ 25,
162
+ 24,
163
+ 23,
164
+ 22,
165
+ 18,
166
+ 14,
167
+ 11,
168
+ 8,
169
+ 5,
170
+ 4
171
+ ],
172
+ [
173
+ 2,
174
+ 1,
175
+ 1,
176
+ 1,
177
+ 1,
178
+ 1,
179
+ 4,
180
+ 8,
181
+ 13,
182
+ 15,
183
+ 16,
184
+ 17,
185
+ 18,
186
+ 17,
187
+ 17,
188
+ 16,
189
+ 16,
190
+ 14,
191
+ 12,
192
+ 10,
193
+ 7,
194
+ 5,
195
+ 4,
196
+ 2
197
+ ],
198
+ [
199
+ 2,
200
+ 1,
201
+ 1,
202
+ 1,
203
+ 1,
204
+ 1,
205
+ 3,
206
+ 8,
207
+ 12,
208
+ 14,
209
+ 15,
210
+ 15,
211
+ 16,
212
+ 16,
213
+ 15,
214
+ 15,
215
+ 14,
216
+ 13,
217
+ 11,
218
+ 9,
219
+ 7,
220
+ 5,
221
+ 3,
222
+ 2
223
+ ]
224
+ ],
225
+ "mark": {
226
+ "row": "Wed",
227
+ "column": "12pm",
228
+ "label": "Busiest: Wed noon"
229
+ },
230
+ "source": "Invented example data"
231
+ }
@@ -0,0 +1,234 @@
1
+ {
2
+ "type": "lines",
3
+ "style": "report",
4
+ "title": "Weekday orders peak at noon",
5
+ "note": "Orders per hour",
6
+ "series": [
7
+ {
8
+ "label": "Weekdays",
9
+ "points": [
10
+ {
11
+ "x": 0,
12
+ "y": 3
13
+ },
14
+ {
15
+ "x": 1,
16
+ "y": 2
17
+ },
18
+ {
19
+ "x": 2,
20
+ "y": 1
21
+ },
22
+ {
23
+ "x": 3,
24
+ "y": 1
25
+ },
26
+ {
27
+ "x": 4,
28
+ "y": 1
29
+ },
30
+ {
31
+ "x": 5,
32
+ "y": 2
33
+ },
34
+ {
35
+ "x": 6,
36
+ "y": 6
37
+ },
38
+ {
39
+ "x": 7,
40
+ "y": 14
41
+ },
42
+ {
43
+ "x": 8,
44
+ "y": 22
45
+ },
46
+ {
47
+ "x": 9,
48
+ "y": 25
49
+ },
50
+ {
51
+ "x": 10,
52
+ "y": 27
53
+ },
54
+ {
55
+ "x": 11,
56
+ "y": 28
57
+ },
58
+ {
59
+ "x": 12,
60
+ "y": 30
61
+ },
62
+ {
63
+ "x": 13,
64
+ "y": 29
65
+ },
66
+ {
67
+ "x": 14,
68
+ "y": 28
69
+ },
70
+ {
71
+ "x": 15,
72
+ "y": 27
73
+ },
74
+ {
75
+ "x": 16,
76
+ "y": 26
77
+ },
78
+ {
79
+ "x": 17,
80
+ "y": 24
81
+ },
82
+ {
83
+ "x": 18,
84
+ "y": 20
85
+ },
86
+ {
87
+ "x": 19,
88
+ "y": 16
89
+ },
90
+ {
91
+ "x": 20,
92
+ "y": 12
93
+ },
94
+ {
95
+ "x": 21,
96
+ "y": 9
97
+ },
98
+ {
99
+ "x": 22,
100
+ "y": 6
101
+ },
102
+ {
103
+ "x": 23,
104
+ "y": 4
105
+ }
106
+ ]
107
+ },
108
+ {
109
+ "label": "Weekends",
110
+ "points": [
111
+ {
112
+ "x": 0,
113
+ "y": 4
114
+ },
115
+ {
116
+ "x": 1,
117
+ "y": 3
118
+ },
119
+ {
120
+ "x": 2,
121
+ "y": 2
122
+ },
123
+ {
124
+ "x": 3,
125
+ "y": 1
126
+ },
127
+ {
128
+ "x": 4,
129
+ "y": 1
130
+ },
131
+ {
132
+ "x": 5,
133
+ "y": 1
134
+ },
135
+ {
136
+ "x": 6,
137
+ "y": 2
138
+ },
139
+ {
140
+ "x": 7,
141
+ "y": 4
142
+ },
143
+ {
144
+ "x": 8,
145
+ "y": 8
146
+ },
147
+ {
148
+ "x": 9,
149
+ "y": 13
150
+ },
151
+ {
152
+ "x": 10,
153
+ "y": 17
154
+ },
155
+ {
156
+ "x": 11,
157
+ "y": 20
158
+ },
159
+ {
160
+ "x": 12,
161
+ "y": 22
162
+ },
163
+ {
164
+ "x": 13,
165
+ "y": 22
166
+ },
167
+ {
168
+ "x": 14,
169
+ "y": 21
170
+ },
171
+ {
172
+ "x": 15,
173
+ "y": 20
174
+ },
175
+ {
176
+ "x": 16,
177
+ "y": 19
178
+ },
179
+ {
180
+ "x": 17,
181
+ "y": 18
182
+ },
183
+ {
184
+ "x": 18,
185
+ "y": 17
186
+ },
187
+ {
188
+ "x": 19,
189
+ "y": 15
190
+ },
191
+ {
192
+ "x": 20,
193
+ "y": 13
194
+ },
195
+ {
196
+ "x": 21,
197
+ "y": 10
198
+ },
199
+ {
200
+ "x": 22,
201
+ "y": 8
202
+ },
203
+ {
204
+ "x": 23,
205
+ "y": 6
206
+ }
207
+ ]
208
+ }
209
+ ],
210
+ "accent": "Weekdays",
211
+ "mark": {
212
+ "x": 12,
213
+ "label": "30 at noon"
214
+ },
215
+ "xTicks": [
216
+ {
217
+ "x": 0,
218
+ "label": "12am"
219
+ },
220
+ {
221
+ "x": 6,
222
+ "label": "6am"
223
+ },
224
+ {
225
+ "x": 12,
226
+ "label": "12pm"
227
+ },
228
+ {
229
+ "x": 18,
230
+ "label": "6pm"
231
+ }
232
+ ],
233
+ "source": "Invented example data"
234
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "type": "panels",
3
+ "style": "report",
4
+ "chart": "groups",
5
+ "title": "The cheaper plan answers almost as fast as the premium one",
6
+ "note": "Share of tickets answered within an hour",
7
+ "unit": "%",
8
+ "max": 100,
9
+ "series": [
10
+ "Basic",
11
+ "Standard",
12
+ "Premium"
13
+ ],
14
+ "accent": {
15
+ "series": "Standard"
16
+ },
17
+ "panels": [
18
+ {
19
+ "label": "Email",
20
+ "groups": [
21
+ {
22
+ "label": "Weekdays",
23
+ "values": [
24
+ 61,
25
+ 84,
26
+ 89
27
+ ]
28
+ },
29
+ {
30
+ "label": "Weekends",
31
+ "values": [
32
+ 38,
33
+ 70,
34
+ 74
35
+ ]
36
+ }
37
+ ]
38
+ },
39
+ {
40
+ "label": "Chat",
41
+ "groups": [
42
+ {
43
+ "label": "Weekdays",
44
+ "values": [
45
+ 72,
46
+ 91,
47
+ 94
48
+ ]
49
+ },
50
+ {
51
+ "label": "Weekends",
52
+ "values": [
53
+ 55,
54
+ 83,
55
+ 86
56
+ ]
57
+ }
58
+ ]
59
+ }
60
+ ],
61
+ "source": "Invented example data"
62
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liggi/figkit",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Explanatory figures for agents and people: write a little HTML or a chart spec, get a laid-out PNG and a list of what is wrong with it.",
5
5
  "license": "MIT",
6
6
  "type": "module",
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(--soft); 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; }
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(--soft); border-radius: 3px; }
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(--soft); height: 26px; background: var(--tone); border-radius: 3px; min-width: 3px; flex: 0 0 auto; }
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(--soft); }
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,33 @@ 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; }
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;
@@ -635,7 +635,10 @@
635
635
  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
636
 
637
637
  // Too much on the page.
638
- const words = R.innerText.split(/\s+/).filter(Boolean).length;
638
+ // Panels repeat the same labels in each panel; a label read once counts once.
639
+ const panelText = R.querySelector('.panels') ? [...new Set([...R.querySelectorAll('.panels text, .panels h2')].map((e) => e.textContent.trim()))].join(' ') : null;
640
+ 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;
641
+ const words = (panelText !== null ? outside + ' ' + panelText : R.innerText).split(/\s+/).filter(Boolean).length;
639
642
  const nodes = R.querySelectorAll('.node:not(.bare), .decision').length, arrows = (fig.wires ?? []).length;
640
643
  const budget = detail ? { words: 190, nodes: 18, arrows: 18 } : { words: 70, nodes: 5, arrows: 8 };
641
644
  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 +647,11 @@
644
647
  const tones = TONES.filter((t) => R.querySelector(`.${t}, [${t}], .t-${t}`));
645
648
  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
649
  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.`); }
650
+ // A face that never loaded draws in whatever the machine has, and every measurement above is of that fallback.
651
+ const faces = [...document.fonts];
652
+ const families = [...new Set(['--sans', '--serif'].map((v) => css(v).split(',')[0].replace(/["']/g, '').trim()))];
653
+ const missing = families.filter((f) => !faces.some((x) => x.family.replace(/["']/g, '') === f && x.status === 'loaded'));
654
+ 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
655
  const h1 = R.querySelector('h1');
648
656
  if (!h1) P.push('No <h1>: give the figure a title that states its point.');
649
657
  else if (h1.innerText.split(/\s+/).length > 14) P.push('The title is over 14 words; state the point more shortly.');