@liggi/figkit 0.4.0 → 0.6.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 +154 -12
- package/THIRD_PARTY_NOTICES.md +48 -1
- package/bin/fig.mjs +5 -3
- package/dist/viz.iife.js +86 -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/map.json +269 -0
- package/examples/charts/states.json +71 -0
- package/package.json +2 -1
- package/runtime/fig.css +56 -0
- package/runtime/fig.js +218 -20
- package/viz/draw.js +168 -14
- package/viz/map-spec.js +121 -0
- package/viz/map.js +513 -0
- package/viz/spec.js +64 -6
- package/viz/states-spec.js +161 -0
- package/viz/states.js +203 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{ "type": "bars", "title": "Two of the four new layouts beat the current page",
|
|
2
|
+
"note": "Share of visitors who finished checkout", "unit": "%",
|
|
3
|
+
"ref": { "label": "Current page", "value": 61 },
|
|
4
|
+
"bars": [ { "label": "One-page form", "value": 68 }, { "label": "Saved cards first", "value": 64 },
|
|
5
|
+
{ "label": "Address lookup", "value": 60 }, { "label": "Guest by default", "value": 57 } ],
|
|
6
|
+
"source": "Invented data, for illustration." }
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{ "type": "estimates", "title": "Free delivery lifts orders in two regions; the north's range includes zero",
|
|
2
|
+
"note": "Change in orders per 100 visitors, free delivery minus paid", "range": "95% interval",
|
|
3
|
+
"sides": { "below": "Paid better", "above": "Free better" },
|
|
4
|
+
"rows": [ { "label": "South", "value": 2.4, "low": 1.1, "high": 3.7 },
|
|
5
|
+
{ "label": "Midlands", "value": 1.8, "low": 0.6, "high": 3.0 },
|
|
6
|
+
{ "label": "North", "value": 0.7, "low": -0.9, "high": 2.3 } ],
|
|
7
|
+
"source": "Invented data, for illustration." }
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{ "type": "heatmap", "title": "The new menu helped every region except the north",
|
|
2
|
+
"note": "Change in weekly orders per store after the new menu", "unit": "%",
|
|
3
|
+
"rows": ["South", "Midlands", "North"], "columns": ["Wk 1", "Wk 2", "Wk 3", "Wk 4"],
|
|
4
|
+
"values": [ [3.1, 4.4, 5.0, 4.8], [1.2, 2.6, 3.3, 3.9], [-2.4, -3.8, -1.9, -2.7] ],
|
|
5
|
+
"source": "Invented data, for illustration." }
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "map",
|
|
3
|
+
"title": "Every grocery order passes through the order service before stores pick or vans deliver",
|
|
4
|
+
"subsystems": [
|
|
5
|
+
{
|
|
6
|
+
"id": "shop",
|
|
7
|
+
"name": "Storefront",
|
|
8
|
+
"kind": "web and mobile"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": "orders",
|
|
12
|
+
"name": "Orders",
|
|
13
|
+
"kind": "orders team"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "stores",
|
|
17
|
+
"name": "Store fulfilment",
|
|
18
|
+
"kind": "stores team"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "delivery",
|
|
22
|
+
"name": "Delivery",
|
|
23
|
+
"kind": "logistics team"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "outside",
|
|
27
|
+
"name": "Outside services"
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"parts": [
|
|
31
|
+
{
|
|
32
|
+
"id": "web",
|
|
33
|
+
"name": "Shop website",
|
|
34
|
+
"kind": "Next.js app",
|
|
35
|
+
"in": "shop"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "mobile",
|
|
39
|
+
"name": "Shop app",
|
|
40
|
+
"kind": "iOS and Android",
|
|
41
|
+
"in": "shop"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "gateway",
|
|
45
|
+
"name": "API gateway",
|
|
46
|
+
"kind": "Envoy",
|
|
47
|
+
"in": "shop"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "search",
|
|
51
|
+
"name": "Product search",
|
|
52
|
+
"kind": "OpenSearch",
|
|
53
|
+
"in": "shop"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"id": "basket",
|
|
57
|
+
"name": "Basket service",
|
|
58
|
+
"kind": "Go service",
|
|
59
|
+
"in": "shop"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": "order",
|
|
63
|
+
"name": "Order service",
|
|
64
|
+
"kind": "Go service",
|
|
65
|
+
"in": "orders",
|
|
66
|
+
"tone": "accent"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"id": "orderdb",
|
|
70
|
+
"name": "Orders database",
|
|
71
|
+
"kind": "Postgres",
|
|
72
|
+
"in": "orders"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": "payment",
|
|
76
|
+
"name": "Payment service",
|
|
77
|
+
"kind": "Go service",
|
|
78
|
+
"in": "orders"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"id": "fraud",
|
|
82
|
+
"name": "Fraud check",
|
|
83
|
+
"kind": "rules engine",
|
|
84
|
+
"in": "orders"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"id": "events",
|
|
88
|
+
"name": "Order events",
|
|
89
|
+
"kind": "Kafka topic",
|
|
90
|
+
"in": "orders"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"id": "planner",
|
|
94
|
+
"name": "Picking planner",
|
|
95
|
+
"kind": "nightly batch",
|
|
96
|
+
"in": "stores"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"id": "stock",
|
|
100
|
+
"name": "Store stock",
|
|
101
|
+
"kind": "per-store counts",
|
|
102
|
+
"in": "stores"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"id": "picker",
|
|
106
|
+
"name": "Picker app",
|
|
107
|
+
"kind": "handheld scanner",
|
|
108
|
+
"in": "stores"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"id": "subs",
|
|
112
|
+
"name": "Substitution rules",
|
|
113
|
+
"kind": "YAML in repo",
|
|
114
|
+
"in": "stores"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"id": "slots",
|
|
118
|
+
"name": "Van slots",
|
|
119
|
+
"kind": "1-hour windows",
|
|
120
|
+
"in": "delivery"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"id": "routes",
|
|
124
|
+
"name": "Route planner",
|
|
125
|
+
"kind": "OR-Tools job",
|
|
126
|
+
"in": "delivery"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"id": "driver",
|
|
130
|
+
"name": "Driver app",
|
|
131
|
+
"kind": "Android",
|
|
132
|
+
"in": "delivery"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"id": "tracking",
|
|
136
|
+
"name": "Live tracking",
|
|
137
|
+
"kind": "WebSocket service",
|
|
138
|
+
"in": "delivery"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"id": "cards",
|
|
142
|
+
"name": "Card processor",
|
|
143
|
+
"kind": "Stripe",
|
|
144
|
+
"in": "outside"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"id": "sms",
|
|
148
|
+
"name": "Text messages",
|
|
149
|
+
"kind": "Twilio",
|
|
150
|
+
"in": "outside"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"id": "maps",
|
|
154
|
+
"name": "Road map data",
|
|
155
|
+
"kind": "Mapbox",
|
|
156
|
+
"in": "outside"
|
|
157
|
+
}
|
|
158
|
+
],
|
|
159
|
+
"links": [
|
|
160
|
+
{
|
|
161
|
+
"from": "web",
|
|
162
|
+
"to": "gateway",
|
|
163
|
+
"tone": "accent"
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"from": "mobile",
|
|
167
|
+
"to": "gateway",
|
|
168
|
+
"tone": "accent"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"from": "gateway",
|
|
172
|
+
"to": "search",
|
|
173
|
+
"label": "queries"
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"from": "gateway",
|
|
177
|
+
"to": "basket",
|
|
178
|
+
"tone": "accent"
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"from": "search",
|
|
182
|
+
"to": "stock"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"from": "basket",
|
|
186
|
+
"to": "slots",
|
|
187
|
+
"label": "books a slot"
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"from": "basket",
|
|
191
|
+
"to": "order",
|
|
192
|
+
"label": "checkout",
|
|
193
|
+
"tone": "accent"
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"from": "order",
|
|
197
|
+
"to": "payment",
|
|
198
|
+
"label": "charge"
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"from": "payment",
|
|
202
|
+
"to": "fraud"
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"from": "payment",
|
|
206
|
+
"to": "cards"
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"from": "order",
|
|
210
|
+
"to": "orderdb",
|
|
211
|
+
"label": "writes"
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"from": "order",
|
|
215
|
+
"to": "events",
|
|
216
|
+
"label": "publishes",
|
|
217
|
+
"tone": "accent"
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"from": "events",
|
|
221
|
+
"to": "planner",
|
|
222
|
+
"tone": "accent",
|
|
223
|
+
"label": "new order"
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"from": "events",
|
|
227
|
+
"to": "routes",
|
|
228
|
+
"tone": "accent",
|
|
229
|
+
"label": "new order"
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"from": "planner",
|
|
233
|
+
"to": "picker",
|
|
234
|
+
"label": "pick lists"
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"from": "picker",
|
|
238
|
+
"to": "stock",
|
|
239
|
+
"label": "takes off stock"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"from": "picker",
|
|
243
|
+
"to": "subs"
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"from": "routes",
|
|
247
|
+
"to": "maps"
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"from": "routes",
|
|
251
|
+
"to": "driver",
|
|
252
|
+
"label": "route stops"
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
"from": "driver",
|
|
256
|
+
"to": "tracking",
|
|
257
|
+
"label": "location"
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
"from": "tracking",
|
|
261
|
+
"to": "sms",
|
|
262
|
+
"label": "ETA texts"
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"from": "slots",
|
|
266
|
+
"to": "routes"
|
|
267
|
+
}
|
|
268
|
+
]
|
|
269
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "states",
|
|
3
|
+
"title": "An order waits after a declined card until a new card is added",
|
|
4
|
+
"entry": "Shopper checks out",
|
|
5
|
+
"states": [
|
|
6
|
+
{
|
|
7
|
+
"id": "waiting",
|
|
8
|
+
"name": "Awaiting payment",
|
|
9
|
+
"start": true
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"id": "declined",
|
|
13
|
+
"name": "Card declined",
|
|
14
|
+
"note": "Shopper emailed",
|
|
15
|
+
"tone": "accent"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": "paid",
|
|
19
|
+
"name": "Paid"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "packed",
|
|
23
|
+
"name": "Packed"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "shipped",
|
|
27
|
+
"name": "Shipped",
|
|
28
|
+
"end": true
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "cancelled",
|
|
32
|
+
"name": "Cancelled",
|
|
33
|
+
"end": true
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"moves": [
|
|
37
|
+
{
|
|
38
|
+
"from": "waiting",
|
|
39
|
+
"to": "paid",
|
|
40
|
+
"label": "Charge succeeds"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"from": "waiting",
|
|
44
|
+
"to": "declined",
|
|
45
|
+
"label": "Charge fails"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"from": "declined",
|
|
49
|
+
"to": "waiting",
|
|
50
|
+
"label": "New card added"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"from": "paid",
|
|
54
|
+
"to": "packed",
|
|
55
|
+
"label": "Warehouse picks"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"from": "packed",
|
|
59
|
+
"to": "shipped",
|
|
60
|
+
"label": "Carrier collects"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"from": [
|
|
64
|
+
"paid",
|
|
65
|
+
"packed"
|
|
66
|
+
],
|
|
67
|
+
"to": "cancelled",
|
|
68
|
+
"label": "Shopper cancels"
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liggi/figkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"agents"
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
|
+
"@dagrejs/dagre": "^3.1.3",
|
|
37
38
|
"@fontsource/geist-mono": "^5.3.0",
|
|
38
39
|
"@fontsource/geist-sans": "^5.3.0",
|
|
39
40
|
"@fontsource/newsreader": "^5.3.0",
|
package/runtime/fig.css
CHANGED
|
@@ -274,3 +274,59 @@ fig-bars .fb-label .fb-n { display: block; font-size: var(--small); color: var(-
|
|
|
274
274
|
.fig.phone fig-bars { grid-template-columns: minmax(0, 1fr); row-gap: 4px; }
|
|
275
275
|
.fig.phone fig-bars .fb-label { margin-top: 10px; }
|
|
276
276
|
.fig.phone fig-bars .fb-label .fb-n { display: inline; margin-left: 8px; }
|
|
277
|
+
|
|
278
|
+
/* estimates: signed values with ranges around a zero line; what each side of zero means under the axis */
|
|
279
|
+
.fig .estimates { position: relative; }
|
|
280
|
+
|
|
281
|
+
.fig .range-key i.est-dot { width: 14px; height: 14px; border-radius: 50%; border: 2.5px solid; box-sizing: border-box; }
|
|
282
|
+
.fig .est-sides { position: relative; height: calc(var(--small) * 1.4); margin-top: -6px; font-size: var(--small); color: var(--muted); }
|
|
283
|
+
.fig .est-sides span { position: absolute; top: 0; white-space: nowrap; }
|
|
284
|
+
|
|
285
|
+
/* bars with a reference: its own first row in the second colour, and a dashed line from its end through the rest */
|
|
286
|
+
fig-bars.has-ref { position: relative; }
|
|
287
|
+
fig-bars .fb-label.ref { color: var(--second); }
|
|
288
|
+
fig-bars .fb-refline { position: absolute; width: 0; border-left: 2px dashed var(--second); pointer-events: none; }
|
|
289
|
+
.fig .range-key i.swatch { width: 18px; height: 14px; border-radius: 2px; background: var(--accent); }
|
|
290
|
+
fig-bars.has-ref .fb-label.ref + .fb-track .fb-bar { opacity: .55; }
|
|
291
|
+
/* on a phone the names sit above the bars, so the reference line stops at each name rather than crossing it */
|
|
292
|
+
.fig.phone fig-bars.has-ref .fb-label { position: relative; z-index: 1; justify-self: start; background: var(--paper); padding-right: 8px; }
|
|
293
|
+
.fig .hm-key i.hm-signed { position: relative; margin-bottom: calc(var(--small) * 1.3); }
|
|
294
|
+
.fig .hm-key i.hm-signed b { font-style: normal; position: absolute; top: 100%; transform: translateX(-50%); font-weight: 400; color: var(--muted); padding-top: 2px; line-height: 1; }
|
|
295
|
+
.fig .hm-key i.hm-signed b::before { content: ''; position: absolute; left: 50%; top: -14px; height: 14px; border-left: 2px solid var(--paper); }
|
|
296
|
+
fig-bars.has-ref .fb-val:not(.lead) { color: var(--muted); }
|
|
297
|
+
/* Against a reference, a bar that clears it is solid accent (a faded orange reads as brown on dark); its range is drawn in ink on top. */
|
|
298
|
+
fig-bars.has-ref .fb-bar.dim.accent { opacity: 1; }
|
|
299
|
+
fig-bars.has-ref .fb-range.accent { --tone: var(--ink); }
|
|
300
|
+
|
|
301
|
+
/* ---------- states: what one thing moves through (a spec of type "states") ---------- */
|
|
302
|
+
.fig .states { display: grid; column-gap: 120px; row-gap: 76px; align-items: stretch; justify-content: start; }
|
|
303
|
+
.fig.phone .states { column-gap: 64px; row-gap: 68px; }
|
|
304
|
+
.fig.phone .state { padding: 12px 10px; }
|
|
305
|
+
.fig.phone .state > h3 { font-size: var(--body); }
|
|
306
|
+
.fig .state { text-align: center; align-items: center; justify-content: center; justify-self: stretch; }
|
|
307
|
+
.fig .state > p { margin-top: 4px; }
|
|
308
|
+
.fig .state > .st-stayed { color: var(--ink); }
|
|
309
|
+
.fig .node.bare.st-dot { width: 22px; height: 22px; padding: 0; border-radius: 50%; background: var(--line); justify-self: center; align-self: center; }
|
|
310
|
+
.fig .fig-wire-label .fig-wire-sub { display: block; font-weight: 400; font-variant-numeric: tabular-nums; }
|
|
311
|
+
.fig .st-key { font-size: var(--small); }
|
|
312
|
+
.fig .state.mid { align-self: center; }
|
|
313
|
+
/* the bracket beside the states that share one move, which leads to it with one arrow */
|
|
314
|
+
.fig .node.bare.st-bracket { justify-self: end; width: 14px; margin-right: -30px; padding: 0; border: 1.5px solid var(--line); border-left: 0; border-radius: 0 4px 4px 0; background: none; }
|
|
315
|
+
.fig.phone .node.bare.st-bracket { margin-right: -24px; width: 10px; }
|
|
316
|
+
|
|
317
|
+
/* ---------- map: a whole system, parts in subsystem regions (a spec of type "map"), laid out by dagre and packed by figkit ---------- */
|
|
318
|
+
.fig .map { position: relative; }
|
|
319
|
+
.fig .map > .map-part, .fig .map-region > .map-part { position: absolute; max-width: 260px; min-width: 120px; padding: 10px 14px; gap: 2px; }
|
|
320
|
+
.fig .map-part > h3 { font-size: var(--body); line-height: 1.2; }
|
|
321
|
+
.fig .map-part > p { font-size: var(--small); white-space: nowrap; }
|
|
322
|
+
.fig .map-region { position: absolute; display: block; padding: 0; border: 0; border-radius: 14px; background: color-mix(in srgb, var(--ink) 4%, var(--paper)); box-shadow: inset 0 0 0 1px var(--hair); }
|
|
323
|
+
.fig .map-region > .mp-head { position: absolute; top: 14px; left: 20px; display: flex; flex-direction: column; gap: 2px; white-space: nowrap; }
|
|
324
|
+
.fig .map-region > .mp-head > h2 { font-family: var(--serif); font-size: calc(var(--h) * 1.35); font-weight: 400; line-height: 1.1; color: var(--ink); }
|
|
325
|
+
.fig .map > svg.map-wires { position: absolute; inset: 0; pointer-events: none; overflow: visible; }
|
|
326
|
+
.fig .map .map-label { padding: 0 4px; line-height: 1.25; }
|
|
327
|
+
.fig .map-overview > .map-sub { max-width: 230px; min-width: 150px; }
|
|
328
|
+
.fig .map-sub > ul { list-style: none; margin: 6px 0 0; padding: 0; display: flex; flex-direction: column; gap: 1px; }
|
|
329
|
+
.fig .map-sub > ul li { font-size: var(--small); color: var(--muted); }
|
|
330
|
+
.fig .map-sub > .mp-count { color: var(--soft); }
|
|
331
|
+
/* A map's link label outlined, so a line visibly runs into it rather than stopping short of a floating word. */
|
|
332
|
+
.fig:has(.map) .fig-wire-label { box-shadow: inset 0 0 0 1px var(--hair); }
|