@walterra/pi-charts 0.0.1
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/README.md +72 -0
- package/extensions/vega-chart/index.ts +306 -0
- package/extensions/vega-chart/vega-lite-reference.md +1597 -0
- package/package.json +43 -0
|
@@ -0,0 +1,1597 @@
|
|
|
1
|
+
# Vega-Lite Visualization Reference
|
|
2
|
+
|
|
3
|
+
A comprehensive reference for creating data visualizations using Vega-Lite specifications. This guide follows best practices from the [UW Interactive Data Lab Visualization Curriculum](https://idl.uw.edu/visualization-curriculum/intro.html) and established visualization research.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
Dependencies are **auto-installed** via `uv` (Python package manager) when first using this extension:
|
|
8
|
+
|
|
9
|
+
- `uv` is auto-installed if not present
|
|
10
|
+
- Python 3, `altair`, `pandas`, `vl-convert-python` are managed by uv automatically
|
|
11
|
+
|
|
12
|
+
If auto-install fails, manual installation:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# Install uv
|
|
16
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
17
|
+
|
|
18
|
+
# Run with dependencies (uv handles Python + packages)
|
|
19
|
+
uv run --with altair --with pandas --with vl-convert-python python3 your_script.py
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
If dependencies cannot be installed, the tool returns an error with instructions. Do NOT fall back to ASCII charts.
|
|
23
|
+
|
|
24
|
+
## Philosophy
|
|
25
|
+
|
|
26
|
+
> "A visualization is a mapping from data to visual properties. The key insight is that this mapping should be **declarative** rather than imperative."
|
|
27
|
+
|
|
28
|
+
Vega-Lite embodies a **grammar of graphics**: you describe _what_ you want to visualize, not _how_ to draw it. This enables:
|
|
29
|
+
|
|
30
|
+
- Concise specifications
|
|
31
|
+
- Automatic inference of scales, axes, legends
|
|
32
|
+
- Composable multi-view displays
|
|
33
|
+
- Reproducible visualizations
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Critical Pitfalls (Read First!)
|
|
38
|
+
|
|
39
|
+
> ⚠️ **These issues cause silent failures. Your chart will render but show wrong/missing data.**
|
|
40
|
+
|
|
41
|
+
### 1. Dot-Notation Field Names
|
|
42
|
+
|
|
43
|
+
**Problem:** Field names containing dots (e.g., `room.name`, `host.ip`, `metric.value`) are interpreted as nested object paths.
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
// Data from ES|QL: {"room.name": "Kitchen", "temp": 21}
|
|
47
|
+
// Vega-Lite looks for: {room: {name: "Kitchen"}}
|
|
48
|
+
// Result: "undefined" in labels, collapsed bars, broken legends
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Solution:** Always transform data to use simple field names:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
// ❌ BROKEN
|
|
55
|
+
{"data": {"values": [{"room.name": "Kitchen"}]}}
|
|
56
|
+
|
|
57
|
+
// ✅ WORKS
|
|
58
|
+
{"data": {"values": [{"room": "Kitchen"}]}}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 2. Horizontal Bar Chart Label Truncation
|
|
62
|
+
|
|
63
|
+
Y-axis labels get cut off on horizontal bar charts. Always set `labelLimit`:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
"y": {"field": "category", "axis": {"labelLimit": 200}}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Or use vertical bars with angled labels:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
"x": {"field": "category", "axis": {"labelAngle": -45, "labelLimit": 120}}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 3. Facet/Repeat Incompatibility
|
|
76
|
+
|
|
77
|
+
Top-level `facet` and `repeat` with `spec` fail in Altair v6. Use encoding-based faceting:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
// ❌ FAILS
|
|
81
|
+
{"facet": {"column": {"field": "region"}}, "spec": {...}}
|
|
82
|
+
|
|
83
|
+
// ✅ WORKS
|
|
84
|
+
{"encoding": {"column": {"field": "region", "type": "nominal"}, ...}}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 4. Aspect Ratio Mistakes
|
|
88
|
+
|
|
89
|
+
- **Time series too tall:** Makes trends unreadable. Use 3:1 or 4:1 (width:height)
|
|
90
|
+
- **Bar charts too short:** Labels get truncated. Give adequate height per category
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
// Time series: wide
|
|
94
|
+
{"width": 600, "height": 200}
|
|
95
|
+
|
|
96
|
+
// Bar chart with 8 categories: give height
|
|
97
|
+
{"width": 450, "height": 300}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 5. Legends vs Direct Labels
|
|
101
|
+
|
|
102
|
+
Legends force the reader's eye to jump back and forth. Label lines directly:
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"layer": [
|
|
107
|
+
{ "mark": "line" },
|
|
108
|
+
{
|
|
109
|
+
"mark": { "type": "text", "align": "left", "dx": 5 },
|
|
110
|
+
"transform": [{ "filter": "datum.x == datum.max_x" }],
|
|
111
|
+
"encoding": { "text": { "field": "series" } }
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Specification Structure
|
|
120
|
+
|
|
121
|
+
A Vega-Lite specification is a JSON object:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
|
126
|
+
"data": { ... },
|
|
127
|
+
"mark": "...",
|
|
128
|
+
"encoding": { ... },
|
|
129
|
+
"width": 600,
|
|
130
|
+
"height": 400
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Core Components
|
|
135
|
+
|
|
136
|
+
| Component | Description |
|
|
137
|
+
| ----------- | --------------------------------------------------- |
|
|
138
|
+
| `data` | Input data (inline values, URL, or named dataset) |
|
|
139
|
+
| `mark` | Geometric shape (bar, line, point, area, etc.) |
|
|
140
|
+
| `encoding` | Mapping of data fields to visual channels |
|
|
141
|
+
| `transform` | Data transformations (filter, aggregate, calculate) |
|
|
142
|
+
| `config` | Styling defaults |
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Data Types
|
|
147
|
+
|
|
148
|
+
Understanding data types is fundamental to choosing appropriate visual encodings.
|
|
149
|
+
|
|
150
|
+
| Type | Symbol | Description | Example | Appropriate Channels |
|
|
151
|
+
| ---------------- | ------ | ------------------------ | ---------------------------- | ------------------------------ |
|
|
152
|
+
| **Nominal** | `N` | Categories without order | country, product type | color hue, shape, row/column |
|
|
153
|
+
| **Ordinal** | `O` | Ordered categories | rating (low/med/high), month | position, color value, size |
|
|
154
|
+
| **Quantitative** | `Q` | Continuous numbers | temperature, revenue | position, size, color gradient |
|
|
155
|
+
| **Temporal** | `T` | Date/time values | timestamp, date | position (time axis) |
|
|
156
|
+
|
|
157
|
+
### Type Selection Guidelines
|
|
158
|
+
|
|
159
|
+
- **Nominal**: Use when equality comparison matters (A = B?)
|
|
160
|
+
- **Ordinal**: Use when rank order matters (A < B?)
|
|
161
|
+
- **Quantitative**: Use when magnitude/distance matters (A - B = ?)
|
|
162
|
+
- **Temporal**: Use for time-based data with calendar semantics
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Encoding Channels
|
|
167
|
+
|
|
168
|
+
Channels map data fields to visual properties.
|
|
169
|
+
|
|
170
|
+
### Position Channels
|
|
171
|
+
|
|
172
|
+
```json
|
|
173
|
+
"encoding": {
|
|
174
|
+
"x": {"field": "date", "type": "temporal"},
|
|
175
|
+
"y": {"field": "value", "type": "quantitative"},
|
|
176
|
+
"x2": {"field": "end_date"},
|
|
177
|
+
"y2": {"field": "high_value"}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
| Channel | Description | Best For |
|
|
182
|
+
| -------------------- | --------------------------- | ---------------------- |
|
|
183
|
+
| `x`, `y` | Primary position | All data types |
|
|
184
|
+
| `x2`, `y2` | Secondary position (ranges) | Range bars, error bars |
|
|
185
|
+
| `xOffset`, `yOffset` | Position offset within band | Grouped/dodged bars |
|
|
186
|
+
|
|
187
|
+
### Mark Property Channels
|
|
188
|
+
|
|
189
|
+
```json
|
|
190
|
+
"encoding": {
|
|
191
|
+
"color": {"field": "category", "type": "nominal"},
|
|
192
|
+
"size": {"field": "population", "type": "quantitative"},
|
|
193
|
+
"shape": {"field": "region", "type": "nominal"},
|
|
194
|
+
"opacity": {"field": "confidence", "type": "quantitative"}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
| Channel | Description | Best For |
|
|
199
|
+
| ------------- | ------------------ | -------------------------------------- |
|
|
200
|
+
| `color` | Fill/stroke color | Nominal (hue), Quantitative (gradient) |
|
|
201
|
+
| `size` | Mark size/area | Quantitative values |
|
|
202
|
+
| `shape` | Point symbol shape | Nominal (≤6 categories) |
|
|
203
|
+
| `opacity` | Transparency | Quantitative, overlapping data |
|
|
204
|
+
| `strokeWidth` | Line thickness | Quantitative |
|
|
205
|
+
| `strokeDash` | Dash pattern | Nominal (≤3 categories) |
|
|
206
|
+
|
|
207
|
+
### Text & Tooltip Channels
|
|
208
|
+
|
|
209
|
+
```json
|
|
210
|
+
"encoding": {
|
|
211
|
+
"text": {"field": "label"},
|
|
212
|
+
"tooltip": [
|
|
213
|
+
{"field": "name", "title": "Country"},
|
|
214
|
+
{"field": "value", "title": "GDP", "format": ",.0f"}
|
|
215
|
+
]
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Facet Channels
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
"encoding": {
|
|
223
|
+
"row": {"field": "region", "type": "nominal"},
|
|
224
|
+
"column": {"field": "year", "type": "ordinal"}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Mark Types
|
|
231
|
+
|
|
232
|
+
### Basic Marks
|
|
233
|
+
|
|
234
|
+
| Mark | Use Case | Example |
|
|
235
|
+
| -------- | -------------------- | ------------------ |
|
|
236
|
+
| `point` | Scatter plots | `"mark": "point"` |
|
|
237
|
+
| `circle` | Filled scatter plots | `"mark": "circle"` |
|
|
238
|
+
| `square` | Matrix displays | `"mark": "square"` |
|
|
239
|
+
| `bar` | Bar charts | `"mark": "bar"` |
|
|
240
|
+
| `line` | Time series, trends | `"mark": "line"` |
|
|
241
|
+
| `area` | Volume over time | `"mark": "area"` |
|
|
242
|
+
| `tick` | Strip plots | `"mark": "tick"` |
|
|
243
|
+
| `rule` | Reference lines | `"mark": "rule"` |
|
|
244
|
+
| `text` | Labels | `"mark": "text"` |
|
|
245
|
+
| `rect` | Heatmaps | `"mark": "rect"` |
|
|
246
|
+
| `arc` | Pie/donut charts | `"mark": "arc"` |
|
|
247
|
+
|
|
248
|
+
### Composite Marks
|
|
249
|
+
|
|
250
|
+
| Mark | Use Case |
|
|
251
|
+
| ----------- | ------------------------- |
|
|
252
|
+
| `boxplot` | Distribution summary |
|
|
253
|
+
| `errorbar` | Uncertainty visualization |
|
|
254
|
+
| `errorband` | Confidence intervals |
|
|
255
|
+
|
|
256
|
+
### Mark Properties
|
|
257
|
+
|
|
258
|
+
```json
|
|
259
|
+
"mark": {
|
|
260
|
+
"type": "bar",
|
|
261
|
+
"color": "#4c78a8",
|
|
262
|
+
"opacity": 0.8,
|
|
263
|
+
"cornerRadius": 2,
|
|
264
|
+
"strokeWidth": 0
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Scales
|
|
271
|
+
|
|
272
|
+
Scales map data values to visual values.
|
|
273
|
+
|
|
274
|
+
### Scale Types
|
|
275
|
+
|
|
276
|
+
| Type | Description | Use For |
|
|
277
|
+
| --------- | ------------------- | --------------------------- |
|
|
278
|
+
| `linear` | Linear mapping | Quantitative data |
|
|
279
|
+
| `log` | Logarithmic | Wide-ranging values, ratios |
|
|
280
|
+
| `sqrt` | Square root | Area-based size encoding |
|
|
281
|
+
| `pow` | Power scale | Custom nonlinear |
|
|
282
|
+
| `time` | Time-based | Temporal data |
|
|
283
|
+
| `utc` | UTC time | Cross-timezone data |
|
|
284
|
+
| `ordinal` | Discrete categories | Nominal/ordinal |
|
|
285
|
+
| `band` | Discrete with width | Bar charts |
|
|
286
|
+
| `point` | Discrete points | Dot plots |
|
|
287
|
+
|
|
288
|
+
### Scale Configuration
|
|
289
|
+
|
|
290
|
+
```json
|
|
291
|
+
"encoding": {
|
|
292
|
+
"x": {
|
|
293
|
+
"field": "value",
|
|
294
|
+
"type": "quantitative",
|
|
295
|
+
"scale": {
|
|
296
|
+
"domain": [0, 100],
|
|
297
|
+
"range": [0, 400],
|
|
298
|
+
"zero": true,
|
|
299
|
+
"nice": true,
|
|
300
|
+
"clamp": true
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Color Scales
|
|
307
|
+
|
|
308
|
+
```json
|
|
309
|
+
"encoding": {
|
|
310
|
+
"color": {
|
|
311
|
+
"field": "temperature",
|
|
312
|
+
"type": "quantitative",
|
|
313
|
+
"scale": {
|
|
314
|
+
"scheme": "viridis",
|
|
315
|
+
"domain": [-10, 40]
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**Recommended Color Schemes:**
|
|
322
|
+
|
|
323
|
+
| Type | Schemes |
|
|
324
|
+
| ----------- | ----------------------------------------------- |
|
|
325
|
+
| Sequential | `viridis`, `blues`, `greens`, `oranges`, `reds` |
|
|
326
|
+
| Diverging | `redblue`, `redyellowblue`, `spectral` |
|
|
327
|
+
| Categorical | `category10`, `tableau10`, `set1` |
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Transforms
|
|
332
|
+
|
|
333
|
+
Data transformations within the spec.
|
|
334
|
+
|
|
335
|
+
### Filter
|
|
336
|
+
|
|
337
|
+
```json
|
|
338
|
+
"transform": [
|
|
339
|
+
{"filter": "datum.year == 2020"},
|
|
340
|
+
{"filter": {"field": "country", "oneOf": ["USA", "China", "India"]}}
|
|
341
|
+
]
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Calculate
|
|
345
|
+
|
|
346
|
+
```json
|
|
347
|
+
"transform": [
|
|
348
|
+
{"calculate": "datum.revenue - datum.cost", "as": "profit"},
|
|
349
|
+
{"calculate": "datum.value * 100 / datum.total", "as": "percentage"}
|
|
350
|
+
]
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Aggregate
|
|
354
|
+
|
|
355
|
+
```json
|
|
356
|
+
"transform": [
|
|
357
|
+
{
|
|
358
|
+
"aggregate": [
|
|
359
|
+
{"op": "mean", "field": "temperature", "as": "avg_temp"},
|
|
360
|
+
{"op": "count", "as": "n"}
|
|
361
|
+
],
|
|
362
|
+
"groupby": ["month", "location"]
|
|
363
|
+
}
|
|
364
|
+
]
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**Aggregation Operations:** `count`, `sum`, `mean`, `median`, `min`, `max`, `stdev`, `variance`, `q1`, `q3`, `distinct`, `values`
|
|
368
|
+
|
|
369
|
+
### Bin
|
|
370
|
+
|
|
371
|
+
```json
|
|
372
|
+
"encoding": {
|
|
373
|
+
"x": {
|
|
374
|
+
"bin": true,
|
|
375
|
+
"field": "temperature"
|
|
376
|
+
},
|
|
377
|
+
"y": {"aggregate": "count"}
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Or explicit:
|
|
382
|
+
|
|
383
|
+
```json
|
|
384
|
+
"transform": [
|
|
385
|
+
{"bin": {"maxbins": 20}, "field": "value", "as": "value_bin"}
|
|
386
|
+
]
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Time Unit
|
|
390
|
+
|
|
391
|
+
```json
|
|
392
|
+
"encoding": {
|
|
393
|
+
"x": {
|
|
394
|
+
"timeUnit": "yearmonth",
|
|
395
|
+
"field": "date"
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**Time Units:** `year`, `quarter`, `month`, `week`, `day`, `dayofyear`, `date`, `hours`, `minutes`, `seconds`, `milliseconds`, `yearmonth`, `yearmonthdate`, `monthdate`, `hoursminutes`
|
|
401
|
+
|
|
402
|
+
### Window
|
|
403
|
+
|
|
404
|
+
```json
|
|
405
|
+
"transform": [
|
|
406
|
+
{
|
|
407
|
+
"window": [
|
|
408
|
+
{"op": "row_number", "as": "rank"},
|
|
409
|
+
{"op": "sum", "field": "value", "as": "cumulative"}
|
|
410
|
+
],
|
|
411
|
+
"sort": [{"field": "value", "order": "descending"}]
|
|
412
|
+
}
|
|
413
|
+
]
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Fold (Unpivot)
|
|
417
|
+
|
|
418
|
+
```json
|
|
419
|
+
"transform": [
|
|
420
|
+
{"fold": ["temp_min", "temp_max"], "as": ["measure", "value"]}
|
|
421
|
+
]
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Pivot
|
|
425
|
+
|
|
426
|
+
```json
|
|
427
|
+
"transform": [
|
|
428
|
+
{"pivot": "category", "value": "amount", "groupby": ["date"]}
|
|
429
|
+
]
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Regression & Loess
|
|
433
|
+
|
|
434
|
+
```json
|
|
435
|
+
"transform": [
|
|
436
|
+
{"regression": "y", "on": "x", "method": "linear"}
|
|
437
|
+
]
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
```json
|
|
441
|
+
"transform": [
|
|
442
|
+
{"loess": "y", "on": "x", "bandwidth": 0.3}
|
|
443
|
+
]
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
## Multi-View Composition
|
|
449
|
+
|
|
450
|
+
Composition is fundamental to professional data visualization.
|
|
451
|
+
|
|
452
|
+
### Layer
|
|
453
|
+
|
|
454
|
+
Superimpose multiple marks on shared axes.
|
|
455
|
+
|
|
456
|
+
```json
|
|
457
|
+
{
|
|
458
|
+
"layer": [
|
|
459
|
+
{
|
|
460
|
+
"mark": "area",
|
|
461
|
+
"encoding": {
|
|
462
|
+
"x": { "field": "date", "type": "temporal" },
|
|
463
|
+
"y": { "field": "value", "type": "quantitative" }
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
"mark": { "type": "line", "color": "black" },
|
|
468
|
+
"encoding": {
|
|
469
|
+
"x": { "field": "date", "type": "temporal" },
|
|
470
|
+
"y": { "field": "value", "type": "quantitative" }
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
]
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
**Use Cases:**
|
|
478
|
+
|
|
479
|
+
- Area with line overlay
|
|
480
|
+
- Points with trend line
|
|
481
|
+
- Bars with reference rules
|
|
482
|
+
- Confidence bands with mean line
|
|
483
|
+
|
|
484
|
+
### Horizontal Concatenation (hconcat)
|
|
485
|
+
|
|
486
|
+
```json
|
|
487
|
+
{
|
|
488
|
+
"hconcat": [
|
|
489
|
+
{"mark": "bar", "encoding": {...}},
|
|
490
|
+
{"mark": "line", "encoding": {...}}
|
|
491
|
+
]
|
|
492
|
+
}
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Vertical Concatenation (vconcat)
|
|
496
|
+
|
|
497
|
+
```json
|
|
498
|
+
{
|
|
499
|
+
"vconcat": [
|
|
500
|
+
{"mark": "bar", "encoding": {...}},
|
|
501
|
+
{"mark": "line", "encoding": {...}}
|
|
502
|
+
]
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### Facet (Small Multiples)
|
|
507
|
+
|
|
508
|
+
Partition data into sub-plots.
|
|
509
|
+
|
|
510
|
+
```json
|
|
511
|
+
{
|
|
512
|
+
"facet": {
|
|
513
|
+
"column": { "field": "region", "type": "nominal" },
|
|
514
|
+
"row": { "field": "year", "type": "ordinal" }
|
|
515
|
+
},
|
|
516
|
+
"spec": {
|
|
517
|
+
"mark": "point",
|
|
518
|
+
"encoding": {
|
|
519
|
+
"x": { "field": "x", "type": "quantitative" },
|
|
520
|
+
"y": { "field": "y", "type": "quantitative" }
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
Or using encoding channels:
|
|
527
|
+
|
|
528
|
+
```json
|
|
529
|
+
{
|
|
530
|
+
"mark": "bar",
|
|
531
|
+
"encoding": {
|
|
532
|
+
"x": { "field": "value", "type": "quantitative" },
|
|
533
|
+
"y": { "field": "category", "type": "nominal" },
|
|
534
|
+
"column": { "field": "region", "type": "nominal" }
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### Repeat
|
|
540
|
+
|
|
541
|
+
Generate multiple views from a template.
|
|
542
|
+
|
|
543
|
+
```json
|
|
544
|
+
{
|
|
545
|
+
"repeat": {
|
|
546
|
+
"column": ["temp", "humidity", "pressure"]
|
|
547
|
+
},
|
|
548
|
+
"spec": {
|
|
549
|
+
"mark": "line",
|
|
550
|
+
"encoding": {
|
|
551
|
+
"x": { "field": "date", "type": "temporal" },
|
|
552
|
+
"y": { "field": { "repeat": "column" }, "type": "quantitative" }
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
**SPLOM (Scatter Plot Matrix):**
|
|
559
|
+
|
|
560
|
+
```json
|
|
561
|
+
{
|
|
562
|
+
"repeat": {
|
|
563
|
+
"row": ["mpg", "hp", "weight"],
|
|
564
|
+
"column": ["mpg", "hp", "weight"]
|
|
565
|
+
},
|
|
566
|
+
"spec": {
|
|
567
|
+
"mark": "point",
|
|
568
|
+
"encoding": {
|
|
569
|
+
"x": { "field": { "repeat": "column" }, "type": "quantitative" },
|
|
570
|
+
"y": { "field": { "repeat": "row" }, "type": "quantitative" }
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### Resolve
|
|
577
|
+
|
|
578
|
+
Control how scales/axes/legends are shared or independent.
|
|
579
|
+
|
|
580
|
+
```json
|
|
581
|
+
{
|
|
582
|
+
"layer": [...],
|
|
583
|
+
"resolve": {
|
|
584
|
+
"scale": {"y": "independent"},
|
|
585
|
+
"axis": {"y": "independent"},
|
|
586
|
+
"legend": {"color": "independent"}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
## Common Chart Patterns (Complete Working Examples)
|
|
594
|
+
|
|
595
|
+
> All examples use inline data with simple field names to avoid dot-notation issues.
|
|
596
|
+
|
|
597
|
+
### Horizontal Bar Chart with Value Labels
|
|
598
|
+
|
|
599
|
+
```json
|
|
600
|
+
{
|
|
601
|
+
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
|
602
|
+
"title": { "text": "Sales by Region", "anchor": "start" },
|
|
603
|
+
"width": 400,
|
|
604
|
+
"height": 200,
|
|
605
|
+
"data": {
|
|
606
|
+
"values": [
|
|
607
|
+
{ "region": "North America", "sales": 65000 },
|
|
608
|
+
{ "region": "Europe", "sales": 48000 },
|
|
609
|
+
{ "region": "Asia Pacific", "sales": 35000 },
|
|
610
|
+
{ "region": "Latin America", "sales": 12000 }
|
|
611
|
+
]
|
|
612
|
+
},
|
|
613
|
+
"layer": [
|
|
614
|
+
{ "mark": { "type": "bar", "cornerRadiusEnd": 3 } },
|
|
615
|
+
{
|
|
616
|
+
"mark": { "type": "text", "align": "left", "dx": 5, "fontSize": 11 },
|
|
617
|
+
"encoding": { "text": { "field": "sales", "format": "," } }
|
|
618
|
+
}
|
|
619
|
+
],
|
|
620
|
+
"encoding": {
|
|
621
|
+
"y": {
|
|
622
|
+
"field": "region",
|
|
623
|
+
"type": "nominal",
|
|
624
|
+
"sort": "-x",
|
|
625
|
+
"title": null,
|
|
626
|
+
"axis": { "labelLimit": 150 }
|
|
627
|
+
},
|
|
628
|
+
"x": { "field": "sales", "type": "quantitative", "title": "Sales ($)" },
|
|
629
|
+
"color": { "value": "#4c78a8" }
|
|
630
|
+
},
|
|
631
|
+
"config": { "view": { "stroke": null } }
|
|
632
|
+
}
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
### Vertical Bar Chart (Safer for Many Categories)
|
|
636
|
+
|
|
637
|
+
```json
|
|
638
|
+
{
|
|
639
|
+
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
|
640
|
+
"width": 450,
|
|
641
|
+
"height": 300,
|
|
642
|
+
"data": {
|
|
643
|
+
"values": [
|
|
644
|
+
{ "month": "Jan", "revenue": 4200 },
|
|
645
|
+
{ "month": "Feb", "revenue": 3800 },
|
|
646
|
+
{ "month": "Mar", "revenue": 5100 }
|
|
647
|
+
]
|
|
648
|
+
},
|
|
649
|
+
"mark": { "type": "bar", "cornerRadius": 3 },
|
|
650
|
+
"encoding": {
|
|
651
|
+
"x": { "field": "month", "type": "ordinal", "title": null, "axis": { "labelAngle": 0 } },
|
|
652
|
+
"y": {
|
|
653
|
+
"field": "revenue",
|
|
654
|
+
"type": "quantitative",
|
|
655
|
+
"title": "Revenue ($)",
|
|
656
|
+
"scale": { "zero": true }
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
### Time Series with Area, Line, Points
|
|
663
|
+
|
|
664
|
+
```json
|
|
665
|
+
{
|
|
666
|
+
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
|
667
|
+
"title": { "text": "Daily Temperature", "subtitle": "December 2025", "anchor": "start" },
|
|
668
|
+
"width": 600,
|
|
669
|
+
"height": 200,
|
|
670
|
+
"data": {
|
|
671
|
+
"values": [
|
|
672
|
+
{ "date": "2025-12-01", "temp": 18.5 },
|
|
673
|
+
{ "date": "2025-12-08", "temp": 21.2 },
|
|
674
|
+
{ "date": "2025-12-15", "temp": 22.0 },
|
|
675
|
+
{ "date": "2025-12-22", "temp": 19.1 },
|
|
676
|
+
{ "date": "2025-12-29", "temp": 17.8 }
|
|
677
|
+
]
|
|
678
|
+
},
|
|
679
|
+
"layer": [
|
|
680
|
+
{ "mark": { "type": "area", "opacity": 0.2, "color": "#e45756" } },
|
|
681
|
+
{ "mark": { "type": "line", "color": "#e45756", "strokeWidth": 2 } },
|
|
682
|
+
{ "mark": { "type": "point", "color": "#e45756", "filled": true, "size": 50 } },
|
|
683
|
+
{
|
|
684
|
+
"mark": { "type": "rule", "strokeDash": [4, 4], "color": "#999" },
|
|
685
|
+
"encoding": { "y": { "datum": 20 } }
|
|
686
|
+
}
|
|
687
|
+
],
|
|
688
|
+
"encoding": {
|
|
689
|
+
"x": { "field": "date", "type": "temporal", "title": null, "axis": { "format": "%b %d" } },
|
|
690
|
+
"y": {
|
|
691
|
+
"field": "temp",
|
|
692
|
+
"type": "quantitative",
|
|
693
|
+
"title": "Temperature (°C)",
|
|
694
|
+
"scale": { "domain": [15, 25] }
|
|
695
|
+
}
|
|
696
|
+
},
|
|
697
|
+
"config": { "view": { "stroke": null } }
|
|
698
|
+
}
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
### Multi-Line Chart with Direct Labels
|
|
702
|
+
|
|
703
|
+
```json
|
|
704
|
+
{
|
|
705
|
+
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
|
706
|
+
"title": { "text": "Performance by Team", "anchor": "start" },
|
|
707
|
+
"width": 500,
|
|
708
|
+
"height": 280,
|
|
709
|
+
"data": {
|
|
710
|
+
"values": [
|
|
711
|
+
{ "date": "2025-01", "team": "Alpha", "score": 72 },
|
|
712
|
+
{ "date": "2025-02", "team": "Alpha", "score": 78 },
|
|
713
|
+
{ "date": "2025-03", "team": "Alpha", "score": 85 },
|
|
714
|
+
{ "date": "2025-01", "team": "Beta", "score": 65 },
|
|
715
|
+
{ "date": "2025-02", "team": "Beta", "score": 70 },
|
|
716
|
+
{ "date": "2025-03", "team": "Beta", "score": 68 }
|
|
717
|
+
]
|
|
718
|
+
},
|
|
719
|
+
"layer": [
|
|
720
|
+
{
|
|
721
|
+
"mark": { "type": "line", "strokeWidth": 2.5, "point": { "filled": true, "size": 50 } }
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
"transform": [{ "filter": "datum.date == '2025-03'" }],
|
|
725
|
+
"mark": { "type": "text", "align": "left", "dx": 8, "fontSize": 12, "fontWeight": "bold" },
|
|
726
|
+
"encoding": { "text": { "field": "team" } }
|
|
727
|
+
}
|
|
728
|
+
],
|
|
729
|
+
"encoding": {
|
|
730
|
+
"x": { "field": "date", "type": "ordinal", "title": null },
|
|
731
|
+
"y": {
|
|
732
|
+
"field": "score",
|
|
733
|
+
"type": "quantitative",
|
|
734
|
+
"title": "Score",
|
|
735
|
+
"scale": { "domain": [60, 90] }
|
|
736
|
+
},
|
|
737
|
+
"color": {
|
|
738
|
+
"field": "team",
|
|
739
|
+
"type": "nominal",
|
|
740
|
+
"scale": { "range": ["#4c78a8", "#f58518"] },
|
|
741
|
+
"legend": null
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
### Heatmap with Labels
|
|
748
|
+
|
|
749
|
+
```json
|
|
750
|
+
{
|
|
751
|
+
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
|
752
|
+
"title": { "text": "Activity by Day and Hour", "anchor": "start" },
|
|
753
|
+
"width": 400,
|
|
754
|
+
"height": 250,
|
|
755
|
+
"data": {
|
|
756
|
+
"values": [
|
|
757
|
+
{ "day": "Mon", "hour": "9am", "activity": 45 },
|
|
758
|
+
{ "day": "Mon", "hour": "12pm", "activity": 78 },
|
|
759
|
+
{ "day": "Mon", "hour": "3pm", "activity": 62 },
|
|
760
|
+
{ "day": "Tue", "hour": "9am", "activity": 52 },
|
|
761
|
+
{ "day": "Tue", "hour": "12pm", "activity": 85 },
|
|
762
|
+
{ "day": "Tue", "hour": "3pm", "activity": 70 }
|
|
763
|
+
]
|
|
764
|
+
},
|
|
765
|
+
"mark": { "type": "rect", "cornerRadius": 2 },
|
|
766
|
+
"encoding": {
|
|
767
|
+
"x": { "field": "hour", "type": "ordinal", "title": null },
|
|
768
|
+
"y": { "field": "day", "type": "nominal", "title": null },
|
|
769
|
+
"color": {
|
|
770
|
+
"field": "activity",
|
|
771
|
+
"type": "quantitative",
|
|
772
|
+
"scale": { "scheme": "blues" },
|
|
773
|
+
"title": "Activity"
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
### Grouped Bar Chart
|
|
780
|
+
|
|
781
|
+
```json
|
|
782
|
+
{
|
|
783
|
+
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
|
784
|
+
"width": 400,
|
|
785
|
+
"height": 250,
|
|
786
|
+
"data": {
|
|
787
|
+
"values": [
|
|
788
|
+
{ "category": "A", "group": "2024", "value": 28 },
|
|
789
|
+
{ "category": "A", "group": "2025", "value": 35 },
|
|
790
|
+
{ "category": "B", "group": "2024", "value": 45 },
|
|
791
|
+
{ "category": "B", "group": "2025", "value": 52 }
|
|
792
|
+
]
|
|
793
|
+
},
|
|
794
|
+
"mark": { "type": "bar", "cornerRadius": 2 },
|
|
795
|
+
"encoding": {
|
|
796
|
+
"x": { "field": "category", "type": "nominal", "title": null },
|
|
797
|
+
"y": { "field": "value", "type": "quantitative", "title": "Value" },
|
|
798
|
+
"xOffset": { "field": "group", "type": "nominal" },
|
|
799
|
+
"color": {
|
|
800
|
+
"field": "group",
|
|
801
|
+
"type": "nominal",
|
|
802
|
+
"title": "Year",
|
|
803
|
+
"scale": { "range": ["#4c78a8", "#72b7b2"] }
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
### Stacked Bar Chart
|
|
810
|
+
|
|
811
|
+
```json
|
|
812
|
+
{
|
|
813
|
+
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
|
814
|
+
"width": 400,
|
|
815
|
+
"height": 250,
|
|
816
|
+
"data": {
|
|
817
|
+
"values": [
|
|
818
|
+
{ "month": "Jan", "category": "Product A", "sales": 30 },
|
|
819
|
+
{ "month": "Jan", "category": "Product B", "sales": 25 },
|
|
820
|
+
{ "month": "Feb", "category": "Product A", "sales": 35 },
|
|
821
|
+
{ "month": "Feb", "category": "Product B", "sales": 28 }
|
|
822
|
+
]
|
|
823
|
+
},
|
|
824
|
+
"mark": "bar",
|
|
825
|
+
"encoding": {
|
|
826
|
+
"x": { "field": "month", "type": "ordinal", "title": null },
|
|
827
|
+
"y": { "field": "sales", "type": "quantitative", "stack": "zero", "title": "Sales" },
|
|
828
|
+
"color": { "field": "category", "type": "nominal", "title": null }
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
### Scatter Plot with Trend Line
|
|
834
|
+
|
|
835
|
+
```json
|
|
836
|
+
{
|
|
837
|
+
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
|
838
|
+
"width": 400,
|
|
839
|
+
"height": 300,
|
|
840
|
+
"data": {
|
|
841
|
+
"values": [
|
|
842
|
+
{ "x": 1, "y": 2.1 },
|
|
843
|
+
{ "x": 2, "y": 3.8 },
|
|
844
|
+
{ "x": 3, "y": 4.2 },
|
|
845
|
+
{ "x": 4, "y": 5.5 },
|
|
846
|
+
{ "x": 5, "y": 6.1 },
|
|
847
|
+
{ "x": 6, "y": 7.9 }
|
|
848
|
+
]
|
|
849
|
+
},
|
|
850
|
+
"layer": [
|
|
851
|
+
{
|
|
852
|
+
"mark": { "type": "point", "filled": true, "size": 60, "opacity": 0.7 }
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
"mark": { "type": "line", "color": "firebrick", "strokeWidth": 2 },
|
|
856
|
+
"transform": [{ "regression": "y", "on": "x" }]
|
|
857
|
+
}
|
|
858
|
+
],
|
|
859
|
+
"encoding": {
|
|
860
|
+
"x": { "field": "x", "type": "quantitative", "title": "X Variable" },
|
|
861
|
+
"y": { "field": "y", "type": "quantitative", "title": "Y Variable" }
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
### Box Plot
|
|
867
|
+
|
|
868
|
+
```json
|
|
869
|
+
{
|
|
870
|
+
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
|
871
|
+
"width": 400,
|
|
872
|
+
"height": 250,
|
|
873
|
+
"data": {
|
|
874
|
+
"values": [
|
|
875
|
+
{ "category": "A", "value": 10 },
|
|
876
|
+
{ "category": "A", "value": 15 },
|
|
877
|
+
{ "category": "A", "value": 20 },
|
|
878
|
+
{ "category": "A", "value": 25 },
|
|
879
|
+
{ "category": "B", "value": 30 },
|
|
880
|
+
{ "category": "B", "value": 35 },
|
|
881
|
+
{ "category": "B", "value": 40 },
|
|
882
|
+
{ "category": "B", "value": 45 }
|
|
883
|
+
]
|
|
884
|
+
},
|
|
885
|
+
"mark": "boxplot",
|
|
886
|
+
"encoding": {
|
|
887
|
+
"x": { "field": "category", "type": "nominal", "title": null },
|
|
888
|
+
"y": { "field": "value", "type": "quantitative", "title": "Value" }
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
### ❌ AVOID: Donut Chart
|
|
894
|
+
|
|
895
|
+
Use sorted bar chart instead - see "Never Use Pie or Donut Charts" in Best Practices.
|
|
896
|
+
|
|
897
|
+
---
|
|
898
|
+
|
|
899
|
+
## Advanced Techniques
|
|
900
|
+
|
|
901
|
+
### Layered Area with Line and Points
|
|
902
|
+
|
|
903
|
+
```json
|
|
904
|
+
{
|
|
905
|
+
"layer": [
|
|
906
|
+
{
|
|
907
|
+
"mark": { "type": "area", "opacity": 0.3 },
|
|
908
|
+
"encoding": {
|
|
909
|
+
"x": { "field": "date", "type": "temporal" },
|
|
910
|
+
"y": { "field": "value", "type": "quantitative" }
|
|
911
|
+
}
|
|
912
|
+
},
|
|
913
|
+
{
|
|
914
|
+
"mark": { "type": "line" },
|
|
915
|
+
"encoding": {
|
|
916
|
+
"x": { "field": "date", "type": "temporal" },
|
|
917
|
+
"y": { "field": "value", "type": "quantitative" }
|
|
918
|
+
}
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
"mark": { "type": "point", "filled": true },
|
|
922
|
+
"encoding": {
|
|
923
|
+
"x": { "field": "date", "type": "temporal" },
|
|
924
|
+
"y": { "field": "value", "type": "quantitative" }
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
]
|
|
928
|
+
}
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
### Line with Trend Overlay
|
|
932
|
+
|
|
933
|
+
```json
|
|
934
|
+
{
|
|
935
|
+
"layer": [
|
|
936
|
+
{
|
|
937
|
+
"mark": { "type": "point", "opacity": 0.3 },
|
|
938
|
+
"encoding": {
|
|
939
|
+
"x": { "field": "x", "type": "quantitative" },
|
|
940
|
+
"y": { "field": "y", "type": "quantitative" }
|
|
941
|
+
}
|
|
942
|
+
},
|
|
943
|
+
{
|
|
944
|
+
"mark": { "type": "line", "color": "firebrick" },
|
|
945
|
+
"transform": [{ "regression": "y", "on": "x" }],
|
|
946
|
+
"encoding": {
|
|
947
|
+
"x": { "field": "x", "type": "quantitative" },
|
|
948
|
+
"y": { "field": "y", "type": "quantitative" }
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
]
|
|
952
|
+
}
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
### Dual-Axis Chart
|
|
956
|
+
|
|
957
|
+
```json
|
|
958
|
+
{
|
|
959
|
+
"layer": [
|
|
960
|
+
{
|
|
961
|
+
"mark": "bar",
|
|
962
|
+
"encoding": {
|
|
963
|
+
"x": { "field": "date", "type": "temporal" },
|
|
964
|
+
"y": { "field": "precipitation", "type": "quantitative", "title": "Precipitation (mm)" }
|
|
965
|
+
}
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
"mark": { "type": "line", "color": "firebrick" },
|
|
969
|
+
"encoding": {
|
|
970
|
+
"x": { "field": "date", "type": "temporal" },
|
|
971
|
+
"y": { "field": "temperature", "type": "quantitative", "title": "Temperature (°C)" }
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
],
|
|
975
|
+
"resolve": { "scale": { "y": "independent" } }
|
|
976
|
+
}
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
### Faceted Dashboard
|
|
980
|
+
|
|
981
|
+
```json
|
|
982
|
+
{
|
|
983
|
+
"vconcat": [
|
|
984
|
+
{
|
|
985
|
+
"hconcat": [
|
|
986
|
+
{
|
|
987
|
+
"repeat": {"row": ["x1", "x2"], "column": ["x1", "x2"]},
|
|
988
|
+
"spec": {
|
|
989
|
+
"mark": "point",
|
|
990
|
+
"encoding": {
|
|
991
|
+
"x": {"field": {"repeat": "column"}, "type": "quantitative"},
|
|
992
|
+
"y": {"field": {"repeat": "row"}, "type": "quantitative"}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
"repeat": {"row": ["y1", "y2"]},
|
|
998
|
+
"spec": {
|
|
999
|
+
"layer": [
|
|
1000
|
+
{"mark": "bar", "encoding": {...}},
|
|
1001
|
+
{"mark": "rule", "encoding": {...}}
|
|
1002
|
+
]
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
]
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
"facet": {"column": {"field": "category"}},
|
|
1009
|
+
"spec": {"mark": "bar", "encoding": {...}}
|
|
1010
|
+
}
|
|
1011
|
+
]
|
|
1012
|
+
}
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
---
|
|
1016
|
+
|
|
1017
|
+
## Best Practices
|
|
1018
|
+
|
|
1019
|
+
### 1. Never Use Pie or Donut Charts
|
|
1020
|
+
|
|
1021
|
+
Humans cannot accurately compare arc lengths or angles. **Always use sorted bar charts instead.**
|
|
1022
|
+
|
|
1023
|
+
```json
|
|
1024
|
+
// ❌ AVOID - pie/donut charts
|
|
1025
|
+
{"mark": {"type": "arc", "innerRadius": 50}}
|
|
1026
|
+
|
|
1027
|
+
// ✅ USE - sorted horizontal bar chart
|
|
1028
|
+
{
|
|
1029
|
+
"mark": "bar",
|
|
1030
|
+
"encoding": {
|
|
1031
|
+
"y": {"field": "category", "sort": "-x"},
|
|
1032
|
+
"x": {"field": "value"}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
```
|
|
1036
|
+
|
|
1037
|
+
### 2. Use Color to Encode Data, Not Decorate
|
|
1038
|
+
|
|
1039
|
+
Color should highlight, not decorate:
|
|
1040
|
+
|
|
1041
|
+
- **Single series = single color** (don't add rainbow gradients)
|
|
1042
|
+
- Reserve color for encoding **meaningful data dimensions** (e.g., floor level, status)
|
|
1043
|
+
- Prefer **sequential** schemes for quantitative data
|
|
1044
|
+
- Use **categorical** schemes only for nominal data (≤10 categories)
|
|
1045
|
+
|
|
1046
|
+
**Good - color encodes floor:**
|
|
1047
|
+
|
|
1048
|
+
```json
|
|
1049
|
+
"color": {"field": "floor", "scale": {"domain": ["Ground", "Basement"], "range": ["#4c78a8", "#72b7b2"]}}
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
**Bad - color is decoration:**
|
|
1053
|
+
|
|
1054
|
+
```json
|
|
1055
|
+
"color": {"field": "category", "scale": {"scheme": "rainbow"}}
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
### 3. Annotate Values Directly on Bars
|
|
1059
|
+
|
|
1060
|
+
Don't make readers estimate from axis - show the number:
|
|
1061
|
+
|
|
1062
|
+
```json
|
|
1063
|
+
{
|
|
1064
|
+
"layer": [
|
|
1065
|
+
{ "mark": "bar" },
|
|
1066
|
+
{
|
|
1067
|
+
"mark": { "type": "text", "align": "left", "dx": 5, "fontSize": 11 },
|
|
1068
|
+
"encoding": { "text": { "field": "value", "format": "," } }
|
|
1069
|
+
}
|
|
1070
|
+
]
|
|
1071
|
+
}
|
|
1072
|
+
```
|
|
1073
|
+
|
|
1074
|
+
### 4. Sort by Value, Not Alphabetically
|
|
1075
|
+
|
|
1076
|
+
```json
|
|
1077
|
+
"encoding": {
|
|
1078
|
+
"y": {"field": "category", "sort": "-x"}
|
|
1079
|
+
}
|
|
1080
|
+
```
|
|
1081
|
+
|
|
1082
|
+
### 5. Use Proper Aspect Ratios
|
|
1083
|
+
|
|
1084
|
+
| Chart Type | Aspect Ratio | Example |
|
|
1085
|
+
| ------------ | ------------------------ | ------------------------- |
|
|
1086
|
+
| Time series | 3:1 to 4:1 (wide) | `width: 600, height: 200` |
|
|
1087
|
+
| Bar chart | Height per category | 8 bars → `height: 300` |
|
|
1088
|
+
| Heatmap | Based on data dimensions | Match row/column count |
|
|
1089
|
+
| Scatter plot | Square or slight wide | `width: 400, height: 350` |
|
|
1090
|
+
|
|
1091
|
+
```json
|
|
1092
|
+
// ❌ WRONG - time series too tall
|
|
1093
|
+
{"width": 400, "height": 600}
|
|
1094
|
+
|
|
1095
|
+
// ✅ CORRECT - time series wide
|
|
1096
|
+
{"width": 600, "height": 200}
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
### 6. Direct Label Instead of Legends
|
|
1100
|
+
|
|
1101
|
+
Legends force eye movement. Label directly on the chart:
|
|
1102
|
+
|
|
1103
|
+
```json
|
|
1104
|
+
{
|
|
1105
|
+
"layer": [
|
|
1106
|
+
{
|
|
1107
|
+
"mark": { "type": "line", "strokeWidth": 2 },
|
|
1108
|
+
"encoding": { "color": { "field": "series", "legend": null } }
|
|
1109
|
+
},
|
|
1110
|
+
{
|
|
1111
|
+
"mark": { "type": "text", "align": "left", "dx": 8, "fontWeight": "bold" },
|
|
1112
|
+
"transform": [{ "filter": "datum.date == '2025-12-29'" }],
|
|
1113
|
+
"encoding": {
|
|
1114
|
+
"x": { "field": "date", "type": "temporal" },
|
|
1115
|
+
"y": { "field": "value", "type": "quantitative" },
|
|
1116
|
+
"text": { "field": "series" },
|
|
1117
|
+
"color": { "field": "series", "legend": null }
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
]
|
|
1121
|
+
}
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
### 7. Add Reference Lines for Context
|
|
1125
|
+
|
|
1126
|
+
Help readers interpret values:
|
|
1127
|
+
|
|
1128
|
+
```json
|
|
1129
|
+
{
|
|
1130
|
+
"layer": [
|
|
1131
|
+
{"mark": "bar", "encoding": {...}},
|
|
1132
|
+
{
|
|
1133
|
+
"mark": {"type": "rule", "strokeDash": [4, 4], "color": "#999"},
|
|
1134
|
+
"encoding": {"y": {"datum": 20}}
|
|
1135
|
+
},
|
|
1136
|
+
{
|
|
1137
|
+
"mark": {"type": "text", "align": "left", "dx": 5, "color": "#666"},
|
|
1138
|
+
"encoding": {
|
|
1139
|
+
"y": {"datum": 20},
|
|
1140
|
+
"text": {"value": "Target: 20"}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
]
|
|
1144
|
+
}
|
|
1145
|
+
```
|
|
1146
|
+
|
|
1147
|
+
### 8. Include Titles and Subtitles
|
|
1148
|
+
|
|
1149
|
+
Explain what the reader is looking at:
|
|
1150
|
+
|
|
1151
|
+
```json
|
|
1152
|
+
"title": {
|
|
1153
|
+
"text": "Temperature by Room",
|
|
1154
|
+
"subtitle": "Daily averages, December 2025",
|
|
1155
|
+
"anchor": "start",
|
|
1156
|
+
"fontSize": 16,
|
|
1157
|
+
"subtitleFontSize": 12,
|
|
1158
|
+
"subtitleColor": "#666"
|
|
1159
|
+
}
|
|
1160
|
+
```
|
|
1161
|
+
|
|
1162
|
+
### 9. Use Small Multiples Over Complexity
|
|
1163
|
+
|
|
1164
|
+
Instead of overloading one chart with many encodings, use faceting:
|
|
1165
|
+
|
|
1166
|
+
```json
|
|
1167
|
+
{
|
|
1168
|
+
"mark": "line",
|
|
1169
|
+
"encoding": {
|
|
1170
|
+
"column": { "field": "region", "type": "nominal" },
|
|
1171
|
+
"x": { "field": "date", "type": "temporal" },
|
|
1172
|
+
"y": { "field": "value", "type": "quantitative" }
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
```
|
|
1176
|
+
|
|
1177
|
+
---
|
|
1178
|
+
|
|
1179
|
+
## Configuration
|
|
1180
|
+
|
|
1181
|
+
### Global Config
|
|
1182
|
+
|
|
1183
|
+
```json
|
|
1184
|
+
{
|
|
1185
|
+
"config": {
|
|
1186
|
+
"view": { "stroke": null, "continuousWidth": 400, "continuousHeight": 300 },
|
|
1187
|
+
"axis": { "labelFontSize": 12, "titleFontSize": 14 },
|
|
1188
|
+
"title": { "fontSize": 16, "fontWeight": "bold" },
|
|
1189
|
+
"legend": { "labelFontSize": 11, "titleFontSize": 12 },
|
|
1190
|
+
"bar": { "color": "#4c78a8" },
|
|
1191
|
+
"line": { "strokeWidth": 2 },
|
|
1192
|
+
"point": { "size": 60 }
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
```
|
|
1196
|
+
|
|
1197
|
+
### Typography
|
|
1198
|
+
|
|
1199
|
+
```json
|
|
1200
|
+
"config": {
|
|
1201
|
+
"font": "Helvetica Neue",
|
|
1202
|
+
"axis": {
|
|
1203
|
+
"labelFont": "Helvetica Neue",
|
|
1204
|
+
"titleFont": "Helvetica Neue"
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
```
|
|
1208
|
+
|
|
1209
|
+
---
|
|
1210
|
+
|
|
1211
|
+
## Professional Theming
|
|
1212
|
+
|
|
1213
|
+
Consistent theming across charts creates a professional, cohesive look. These themes align with the Graphviz theming in this skill.
|
|
1214
|
+
|
|
1215
|
+
### Visual Design Principles
|
|
1216
|
+
|
|
1217
|
+
Three approaches to visualization design, each suited to different audiences:
|
|
1218
|
+
|
|
1219
|
+
#### Minimalist (Data-Ink Ratio)
|
|
1220
|
+
|
|
1221
|
+
Maximize data, minimize non-data ink:
|
|
1222
|
+
|
|
1223
|
+
- Remove decorative elements that don't encode information
|
|
1224
|
+
- Use position and length as primary encodings (most accurate)
|
|
1225
|
+
- Avoid chartjunk (3D effects, gradients, unnecessary fills)
|
|
1226
|
+
- Every pixel should convey data
|
|
1227
|
+
|
|
1228
|
+
**When to apply:** Technical documentation, data-dense dashboards, publications.
|
|
1229
|
+
|
|
1230
|
+
**Limitation:** Can be too sparse for audiences who need visual anchors.
|
|
1231
|
+
|
|
1232
|
+
#### Communicative (Functional Clarity)
|
|
1233
|
+
|
|
1234
|
+
Emphasize communication over minimalism:
|
|
1235
|
+
|
|
1236
|
+
- **Functional decoration is not chartjunk** — grid lines, reference marks aid reading
|
|
1237
|
+
- **Redundant encoding for critical data** — alerts need color + size + position
|
|
1238
|
+
- **Know your audience** — label for the least technical viewer
|
|
1239
|
+
- **Guide the eye** — create visual hierarchy that leads to the insight
|
|
1240
|
+
- **Title states the insight** — not just what the chart shows, but what it means
|
|
1241
|
+
|
|
1242
|
+
**When to apply:** Operational dashboards, presentations to mixed audiences, alerting systems.
|
|
1243
|
+
|
|
1244
|
+
#### Modern SaaS (Clean Professional)
|
|
1245
|
+
|
|
1246
|
+
Clean, professional design for modern tooling:
|
|
1247
|
+
|
|
1248
|
+
- **White space is design** — let elements breathe
|
|
1249
|
+
- **One accent color** — gray scale for structure, color for emphasis only
|
|
1250
|
+
- **No gradients, no shadows** — flat, honest design
|
|
1251
|
+
- **Typography carries hierarchy** — weight and size, not decoration
|
|
1252
|
+
- **Thin lines, subtle gridlines** — `#e5e5e5` not black
|
|
1253
|
+
|
|
1254
|
+
**When to apply:** Product documentation, engineering blogs, modern dashboards.
|
|
1255
|
+
|
|
1256
|
+
#### Choosing an Approach
|
|
1257
|
+
|
|
1258
|
+
| Audience | Approach | Style |
|
|
1259
|
+
| ---------------------- | ------------- | ------------------------- |
|
|
1260
|
+
| Engineers reading docs | Minimalist | Sparse, data-dense |
|
|
1261
|
+
| Ops team monitoring | Communicative | Clear, redundant encoding |
|
|
1262
|
+
| Product/stakeholders | Modern SaaS | Clean, professional |
|
|
1263
|
+
| Print/publication | Minimalist | High information density |
|
|
1264
|
+
|
|
1265
|
+
### Light Theme
|
|
1266
|
+
|
|
1267
|
+
```json
|
|
1268
|
+
{
|
|
1269
|
+
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
|
1270
|
+
"background": "white",
|
|
1271
|
+
"config": {
|
|
1272
|
+
"font": "Arial",
|
|
1273
|
+
"title": {
|
|
1274
|
+
"color": "#171717",
|
|
1275
|
+
"subtitleColor": "#737373",
|
|
1276
|
+
"fontSize": 16,
|
|
1277
|
+
"subtitleFontSize": 12,
|
|
1278
|
+
"anchor": "start"
|
|
1279
|
+
},
|
|
1280
|
+
"axis": {
|
|
1281
|
+
"labelColor": "#525252",
|
|
1282
|
+
"titleColor": "#525252",
|
|
1283
|
+
"gridColor": "#e5e5e5",
|
|
1284
|
+
"domainColor": "#d4d4d4",
|
|
1285
|
+
"tickColor": "#d4d4d4"
|
|
1286
|
+
},
|
|
1287
|
+
"legend": {
|
|
1288
|
+
"labelColor": "#525252",
|
|
1289
|
+
"titleColor": "#525252"
|
|
1290
|
+
},
|
|
1291
|
+
"view": { "stroke": null }
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
```
|
|
1295
|
+
|
|
1296
|
+
**Light palette:**
|
|
1297
|
+
|
|
1298
|
+
| Element | Color | Usage |
|
|
1299
|
+
| -------------- | --------- | ------------------------- |
|
|
1300
|
+
| Background | `#ffffff` | Chart background |
|
|
1301
|
+
| Primary text | `#171717` | Titles, labels |
|
|
1302
|
+
| Secondary text | `#525252` | Axis labels, annotations |
|
|
1303
|
+
| Muted text | `#737373` | Subtitles, secondary info |
|
|
1304
|
+
| Grid lines | `#e5e5e5` | Subtle grid |
|
|
1305
|
+
| Domain/tick | `#d4d4d4` | Axis lines |
|
|
1306
|
+
| Normal data | `#a3a3a3` | Default bars, lines |
|
|
1307
|
+
| Error/alert | `#dc2626` | Problem indicators |
|
|
1308
|
+
| Warning | `#f97316` | Threshold lines |
|
|
1309
|
+
|
|
1310
|
+
### Dark Theme
|
|
1311
|
+
|
|
1312
|
+
```json
|
|
1313
|
+
{
|
|
1314
|
+
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
|
1315
|
+
"background": "#0a0a0a",
|
|
1316
|
+
"config": {
|
|
1317
|
+
"font": "Arial",
|
|
1318
|
+
"title": {
|
|
1319
|
+
"color": "#e5e5e5",
|
|
1320
|
+
"subtitleColor": "#a3a3a3",
|
|
1321
|
+
"fontSize": 16,
|
|
1322
|
+
"subtitleFontSize": 12,
|
|
1323
|
+
"anchor": "start"
|
|
1324
|
+
},
|
|
1325
|
+
"axis": {
|
|
1326
|
+
"labelColor": "#a3a3a3",
|
|
1327
|
+
"titleColor": "#a3a3a3",
|
|
1328
|
+
"gridColor": "#262626",
|
|
1329
|
+
"domainColor": "#404040",
|
|
1330
|
+
"tickColor": "#404040"
|
|
1331
|
+
},
|
|
1332
|
+
"legend": {
|
|
1333
|
+
"labelColor": "#a3a3a3",
|
|
1334
|
+
"titleColor": "#a3a3a3"
|
|
1335
|
+
},
|
|
1336
|
+
"view": { "stroke": null }
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
```
|
|
1340
|
+
|
|
1341
|
+
**Dark palette:**
|
|
1342
|
+
|
|
1343
|
+
| Element | Color | Usage |
|
|
1344
|
+
| -------------- | --------- | ----------------------------------------- |
|
|
1345
|
+
| Background | `#0a0a0a` | Chart background |
|
|
1346
|
+
| Primary text | `#e5e5e5` | Titles, labels |
|
|
1347
|
+
| Secondary text | `#a3a3a3` | Axis labels, annotations |
|
|
1348
|
+
| Muted text | `#737373` | Secondary info |
|
|
1349
|
+
| Grid lines | `#262626` | Subtle grid |
|
|
1350
|
+
| Domain/tick | `#404040` | Axis lines |
|
|
1351
|
+
| Normal data | `#525252` | Default bars, lines |
|
|
1352
|
+
| Error/alert | `#f87171` | Problem indicators (lighter for contrast) |
|
|
1353
|
+
| Warning | `#fb923c` | Threshold lines |
|
|
1354
|
+
|
|
1355
|
+
### Color Encoding Strategy
|
|
1356
|
+
|
|
1357
|
+
Use color sparingly and consistently:
|
|
1358
|
+
|
|
1359
|
+
```json
|
|
1360
|
+
{
|
|
1361
|
+
"color": {
|
|
1362
|
+
"condition": { "test": "datum.error_rate > 5", "value": "#dc2626" },
|
|
1363
|
+
"value": "#a3a3a3"
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
```
|
|
1367
|
+
|
|
1368
|
+
**Rules:**
|
|
1369
|
+
|
|
1370
|
+
1. **Gray is the default** — only use color when it encodes meaning
|
|
1371
|
+
2. **Red for errors only** — don't dilute its meaning
|
|
1372
|
+
3. **One accent per chart** — avoid rainbow palettes
|
|
1373
|
+
4. **Consistent across dashboard** — same colors mean same things
|
|
1374
|
+
|
|
1375
|
+
### Title with Insight
|
|
1376
|
+
|
|
1377
|
+
Don't just label what the chart shows—state what it means:
|
|
1378
|
+
|
|
1379
|
+
```json
|
|
1380
|
+
{
|
|
1381
|
+
"title": {
|
|
1382
|
+
"text": "Service Error Rates",
|
|
1383
|
+
"subtitle": "telegram-bot exceeds 5% SLO threshold",
|
|
1384
|
+
"color": "#171717",
|
|
1385
|
+
"subtitleColor": "#737373",
|
|
1386
|
+
"anchor": "start"
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
```
|
|
1390
|
+
|
|
1391
|
+
**Bad:** "Error Rate by Service"
|
|
1392
|
+
**Good:** "telegram-bot exceeds 5% SLO threshold"
|
|
1393
|
+
|
|
1394
|
+
### Reference Lines for Context
|
|
1395
|
+
|
|
1396
|
+
Add threshold/target lines to give context:
|
|
1397
|
+
|
|
1398
|
+
```json
|
|
1399
|
+
{
|
|
1400
|
+
"layer": [
|
|
1401
|
+
{
|
|
1402
|
+
"data": {"values": [{"threshold": 5}]},
|
|
1403
|
+
"mark": {"type": "rule", "strokeDash": [4, 4], "strokeWidth": 1.5},
|
|
1404
|
+
"encoding": {
|
|
1405
|
+
"y": {"field": "threshold", "type": "quantitative"},
|
|
1406
|
+
"color": {"value": "#f97316"}
|
|
1407
|
+
}
|
|
1408
|
+
},
|
|
1409
|
+
{
|
|
1410
|
+
"data": {"values": [{"threshold": 5, "label": "5% SLO"}]},
|
|
1411
|
+
"mark": {"type": "text", "align": "right", "dy": -8, "fontSize": 10},
|
|
1412
|
+
"encoding": {
|
|
1413
|
+
"y": {"field": "threshold", "type": "quantitative"},
|
|
1414
|
+
"x": {"value": "width"},
|
|
1415
|
+
"text": {"field": "label"},
|
|
1416
|
+
"color": {"value": "#f97316"}
|
|
1417
|
+
}
|
|
1418
|
+
},
|
|
1419
|
+
{
|
|
1420
|
+
"mark": "bar",
|
|
1421
|
+
"encoding": {...}
|
|
1422
|
+
}
|
|
1423
|
+
]
|
|
1424
|
+
}
|
|
1425
|
+
```
|
|
1426
|
+
|
|
1427
|
+
### Direct Labeling over Legends
|
|
1428
|
+
|
|
1429
|
+
Place labels directly on data points instead of using legends:
|
|
1430
|
+
|
|
1431
|
+
```json
|
|
1432
|
+
{
|
|
1433
|
+
"layer": [
|
|
1434
|
+
{
|
|
1435
|
+
"mark": { "type": "line", "strokeWidth": 2 },
|
|
1436
|
+
"encoding": {
|
|
1437
|
+
"color": { "field": "service", "legend": null }
|
|
1438
|
+
}
|
|
1439
|
+
},
|
|
1440
|
+
{
|
|
1441
|
+
"mark": { "type": "text", "align": "left", "dx": 8, "fontSize": 11 },
|
|
1442
|
+
"transform": [{ "filter": "datum.time == 'final_point'" }],
|
|
1443
|
+
"encoding": {
|
|
1444
|
+
"text": { "field": "service" },
|
|
1445
|
+
"color": { "field": "service", "legend": null }
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
]
|
|
1449
|
+
}
|
|
1450
|
+
```
|
|
1451
|
+
|
|
1452
|
+
### Value Labels on Bars
|
|
1453
|
+
|
|
1454
|
+
Add the actual values as text marks:
|
|
1455
|
+
|
|
1456
|
+
```json
|
|
1457
|
+
{
|
|
1458
|
+
"layer": [
|
|
1459
|
+
{
|
|
1460
|
+
"mark": { "type": "bar", "cornerRadiusEnd": 3 },
|
|
1461
|
+
"encoding": {
|
|
1462
|
+
"y": { "field": "service", "sort": "-x" },
|
|
1463
|
+
"x": { "field": "count" }
|
|
1464
|
+
}
|
|
1465
|
+
},
|
|
1466
|
+
{
|
|
1467
|
+
"mark": { "type": "text", "align": "left", "dx": 5, "fontSize": 11 },
|
|
1468
|
+
"encoding": {
|
|
1469
|
+
"y": { "field": "service", "sort": "-x" },
|
|
1470
|
+
"x": { "field": "count" },
|
|
1471
|
+
"text": { "field": "count", "format": ",.0f" },
|
|
1472
|
+
"color": { "value": "#525252" }
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
]
|
|
1476
|
+
}
|
|
1477
|
+
```
|
|
1478
|
+
|
|
1479
|
+
### Complete Example: Service Health Dashboard
|
|
1480
|
+
|
|
1481
|
+
```json
|
|
1482
|
+
{
|
|
1483
|
+
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
|
1484
|
+
"title": {
|
|
1485
|
+
"text": "Service Error Rates",
|
|
1486
|
+
"subtitle": "telegram-bot exceeds 5% SLO threshold",
|
|
1487
|
+
"color": "#171717",
|
|
1488
|
+
"subtitleColor": "#737373",
|
|
1489
|
+
"fontSize": 16,
|
|
1490
|
+
"subtitleFontSize": 12,
|
|
1491
|
+
"anchor": "start"
|
|
1492
|
+
},
|
|
1493
|
+
"width": 450,
|
|
1494
|
+
"height": 180,
|
|
1495
|
+
"background": "white",
|
|
1496
|
+
"config": {
|
|
1497
|
+
"font": "Arial",
|
|
1498
|
+
"axis": {
|
|
1499
|
+
"labelColor": "#525252",
|
|
1500
|
+
"titleColor": "#525252",
|
|
1501
|
+
"gridColor": "#f5f5f5",
|
|
1502
|
+
"domainColor": "#d4d4d4",
|
|
1503
|
+
"tickColor": "#d4d4d4"
|
|
1504
|
+
},
|
|
1505
|
+
"view": { "stroke": null }
|
|
1506
|
+
},
|
|
1507
|
+
"layer": [
|
|
1508
|
+
{
|
|
1509
|
+
"data": { "values": [{ "threshold": 5 }] },
|
|
1510
|
+
"mark": { "type": "rule", "strokeDash": [4, 4], "strokeWidth": 1.5 },
|
|
1511
|
+
"encoding": {
|
|
1512
|
+
"x": { "field": "threshold", "type": "quantitative" },
|
|
1513
|
+
"color": { "value": "#f97316" }
|
|
1514
|
+
}
|
|
1515
|
+
},
|
|
1516
|
+
{
|
|
1517
|
+
"data": {
|
|
1518
|
+
"values": [
|
|
1519
|
+
{ "service": "telegram-bot", "error_rate": 8.05 },
|
|
1520
|
+
{ "service": "web-api", "error_rate": 0.33 },
|
|
1521
|
+
{ "service": "web-client", "error_rate": 0.23 },
|
|
1522
|
+
{ "service": "mcp-server", "error_rate": 0.16 }
|
|
1523
|
+
]
|
|
1524
|
+
},
|
|
1525
|
+
"mark": { "type": "bar", "cornerRadiusEnd": 3 },
|
|
1526
|
+
"encoding": {
|
|
1527
|
+
"y": {
|
|
1528
|
+
"field": "service",
|
|
1529
|
+
"type": "nominal",
|
|
1530
|
+
"sort": "-x",
|
|
1531
|
+
"title": null,
|
|
1532
|
+
"axis": { "labelLimit": 150 }
|
|
1533
|
+
},
|
|
1534
|
+
"x": {
|
|
1535
|
+
"field": "error_rate",
|
|
1536
|
+
"type": "quantitative",
|
|
1537
|
+
"title": "Error Rate %",
|
|
1538
|
+
"scale": { "domain": [0, 10] }
|
|
1539
|
+
},
|
|
1540
|
+
"color": {
|
|
1541
|
+
"condition": { "test": "datum.error_rate > 5", "value": "#dc2626" },
|
|
1542
|
+
"value": "#a3a3a3"
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
},
|
|
1546
|
+
{
|
|
1547
|
+
"data": {
|
|
1548
|
+
"values": [
|
|
1549
|
+
{ "service": "telegram-bot", "error_rate": 8.05 },
|
|
1550
|
+
{ "service": "web-api", "error_rate": 0.33 },
|
|
1551
|
+
{ "service": "web-client", "error_rate": 0.23 },
|
|
1552
|
+
{ "service": "mcp-server", "error_rate": 0.16 }
|
|
1553
|
+
]
|
|
1554
|
+
},
|
|
1555
|
+
"mark": { "type": "text", "align": "left", "dx": 5, "fontSize": 11 },
|
|
1556
|
+
"encoding": {
|
|
1557
|
+
"y": { "field": "service", "sort": "-x" },
|
|
1558
|
+
"x": { "field": "error_rate" },
|
|
1559
|
+
"text": { "field": "error_rate", "format": ".2f" },
|
|
1560
|
+
"color": {
|
|
1561
|
+
"condition": { "test": "datum.error_rate > 5", "value": "#dc2626" },
|
|
1562
|
+
"value": "#525252"
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
]
|
|
1567
|
+
}
|
|
1568
|
+
```
|
|
1569
|
+
|
|
1570
|
+
### Quick Reference: Quality Checklist
|
|
1571
|
+
|
|
1572
|
+
Before finalizing any chart, verify:
|
|
1573
|
+
|
|
1574
|
+
- [ ] **Title states the insight** (not just what the chart shows)
|
|
1575
|
+
- [ ] **Subtitle provides context** if needed
|
|
1576
|
+
- [ ] **One accent color** — red for errors, gray for normal
|
|
1577
|
+
- [ ] **Sort bars by value** (`sort: "-x"`) not alphabetically
|
|
1578
|
+
- [ ] **Value labels** on bars for precise reading
|
|
1579
|
+
- [ ] **Direct labels** on lines instead of legends
|
|
1580
|
+
- [ ] **Reference lines** for thresholds/targets with labels
|
|
1581
|
+
- [ ] **Adequate dimensions** — 3:1 for time series, height for bar charts
|
|
1582
|
+
- [ ] **Simple field names** — no dots in field names
|
|
1583
|
+
- [ ] **Consistent theming** — same colors mean same things across charts
|
|
1584
|
+
|
|
1585
|
+
## References
|
|
1586
|
+
|
|
1587
|
+
- [Vega-Lite Documentation](https://vega.github.io/vega-lite/docs/)
|
|
1588
|
+
- [Vega-Lite Examples](https://vega.github.io/vega-lite/examples/)
|
|
1589
|
+
- [UW Visualization Curriculum](https://idl.uw.edu/visualization-curriculum/intro.html)
|
|
1590
|
+
- [Altair Documentation](https://altair-viz.github.io/)
|
|
1591
|
+
|
|
1592
|
+
### Visual Design References
|
|
1593
|
+
|
|
1594
|
+
- _Sémiologie Graphique_ (1967) — semiotics of graphics, visual variables
|
|
1595
|
+
- _The Visual Display of Quantitative Information_ (1983) — data-ink ratio, minimalist approach
|
|
1596
|
+
- _The Functional Art_, _How Charts Lie_ — clarity over minimalism, communicative approach
|
|
1597
|
+
- _Visualization Analysis and Design_ (2014) — systematic approach to visualization design
|