@ctxr/skill-frontend-excellence 0.1.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/CHANGELOG.md +37 -0
- package/LICENSE +21 -0
- package/README.md +114 -0
- package/SKILL.md +227 -0
- package/package.json +63 -0
- package/references/accessibility.md +396 -0
- package/references/audit-workflow.md +390 -0
- package/references/components.md +247 -0
- package/references/data-viz.md +457 -0
- package/references/defects.md +152 -0
- package/references/design.md +513 -0
- package/references/forms.md +485 -0
- package/references/lighthouse.md +242 -0
- package/references/motion.md +642 -0
- package/references/performance.md +416 -0
- package/references/pre-launch.md +342 -0
- package/references/responsive.md +519 -0
- package/references/seo.md +422 -0
- package/references/ui-ux.md +565 -0
- package/scripts/check-no-dashes.mjs +90 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
# Data Visualization
|
|
2
|
+
|
|
3
|
+
Framework-agnostic guidance on charts, tables, and data-dense interfaces. Library-agnostic; the same principles apply to D3, Chart.js, Recharts, ECharts, Highcharts, Vega-Lite, and others.
|
|
4
|
+
|
|
5
|
+
## The Hierarchy of Data Display
|
|
6
|
+
|
|
7
|
+
When showing data, walk this list. The first that fits is the right choice.
|
|
8
|
+
|
|
9
|
+
1. **One number with context.** "$48,231 monthly recurring revenue" beats a chart with one data point.
|
|
10
|
+
2. **Comparison or ranking** (top 5, top 10). A list with bars beats a pie chart.
|
|
11
|
+
3. **Trend over time.** A line chart.
|
|
12
|
+
4. **Distribution.** Histogram, box plot.
|
|
13
|
+
5. **Composition.** Stacked bar, treemap.
|
|
14
|
+
6. **Relationship between two variables.** Scatter plot.
|
|
15
|
+
7. **Geographic.** Map.
|
|
16
|
+
8. **Detailed records.** Table.
|
|
17
|
+
|
|
18
|
+
If you reach for a chart and a table or summary number works, use the simpler thing.
|
|
19
|
+
|
|
20
|
+
## Choosing the Right Chart
|
|
21
|
+
|
|
22
|
+
| Data | Best chart | Avoid |
|
|
23
|
+
|------|-----------|-------|
|
|
24
|
+
| Single value | Stat card with sparkline | Chart |
|
|
25
|
+
| Trend over time | Line chart | Pie chart |
|
|
26
|
+
| Compare 2-10 categories | Bar chart (horizontal if labels long) | Pie chart |
|
|
27
|
+
| Compare 10-30 categories | Horizontal bar | Vertical bar (cramped on mobile) |
|
|
28
|
+
| Compare > 30 categories | Top N + "Other" or table | Bar chart with all |
|
|
29
|
+
| Composition (parts of whole, 2-5 categories) | Pie or donut | Pie with > 5 |
|
|
30
|
+
| Composition over time | Stacked area | Pie carousel |
|
|
31
|
+
| Distribution | Histogram or box plot | Bar chart of raw values |
|
|
32
|
+
| Two-variable relationship | Scatter plot | Two side-by-side bar charts |
|
|
33
|
+
| Multiple metrics over time | Multi-line, small multiples | Stacked line (hides individual values) |
|
|
34
|
+
| Geographic | Choropleth map | Pie chart |
|
|
35
|
+
| Hierarchical composition | Treemap or sunburst | Stacked bar |
|
|
36
|
+
| Funnel (sequential drop-off) | Funnel chart | Bar chart |
|
|
37
|
+
| Network / relationships | Graph / network diagram | Table |
|
|
38
|
+
|
|
39
|
+
### Pie/donut rules
|
|
40
|
+
|
|
41
|
+
- Maximum 5 categories. Beyond that, switch to bar.
|
|
42
|
+
- Sum to 100%.
|
|
43
|
+
- Order by size (largest first, clockwise from 12 o'clock).
|
|
44
|
+
- Direct-label slices when space allows; legend if not.
|
|
45
|
+
- Don't 3D pie. Ever.
|
|
46
|
+
- Don't compare two pies side by side; use a stacked bar.
|
|
47
|
+
|
|
48
|
+
### Bar chart rules
|
|
49
|
+
|
|
50
|
+
- Bar baseline at zero. Always.
|
|
51
|
+
- Vertical bars for short labels, horizontal for long.
|
|
52
|
+
- Order by value (descending) unless there's a natural order (months, ranks).
|
|
53
|
+
- Spacing between bars: about 30-40% of bar width.
|
|
54
|
+
- Categorical x-axis on horizontal; numerical on vertical.
|
|
55
|
+
|
|
56
|
+
### Line chart rules
|
|
57
|
+
|
|
58
|
+
- Time on the x-axis.
|
|
59
|
+
- Multiple lines: maximum 5 before they tangle.
|
|
60
|
+
- Distinct color or dash pattern per line.
|
|
61
|
+
- Direct-label lines at the right end where space allows.
|
|
62
|
+
- Connect data points; don't put markers on every point unless you want emphasis.
|
|
63
|
+
|
|
64
|
+
## Axes
|
|
65
|
+
|
|
66
|
+
### Labels
|
|
67
|
+
|
|
68
|
+
- Always label both axes.
|
|
69
|
+
- Include units in the axis label or as a suffix on tick labels ("$", "%", "ms").
|
|
70
|
+
- Don't rotate labels 90 degrees on horizontal bars; just go horizontal-bar layout.
|
|
71
|
+
- Auto-skip labels when they overlap (every other, or larger interval).
|
|
72
|
+
|
|
73
|
+
### Ticks
|
|
74
|
+
|
|
75
|
+
- Use round numbers (10, 20, 50, 100), not 7, 23, 47.
|
|
76
|
+
- 4-7 ticks per axis is usually right.
|
|
77
|
+
- Y-axis baseline at zero for bar charts. Truncating the y-axis exaggerates differences.
|
|
78
|
+
- For dramatic small variations, add a "broken axis" indicator (zigzag) to be honest about it.
|
|
79
|
+
|
|
80
|
+
### Time axis
|
|
81
|
+
|
|
82
|
+
- Match granularity to the time range:
|
|
83
|
+
- < 1 day: hours
|
|
84
|
+
- 1-30 days: days
|
|
85
|
+
- 1-12 months: weeks or months
|
|
86
|
+
- 1-5 years: months or quarters
|
|
87
|
+
- > 5 years: years
|
|
88
|
+
- Allow user to change granularity (day/week/month).
|
|
89
|
+
- Show the date format consistent with the user's locale.
|
|
90
|
+
|
|
91
|
+
### Number formatting
|
|
92
|
+
|
|
93
|
+
- Locale-aware via `Intl.NumberFormat`.
|
|
94
|
+
- Compact notation for large numbers: `1.2M`, `4.7K`.
|
|
95
|
+
- Currency: respect locale and currency code.
|
|
96
|
+
- Percentages: 1 decimal (`23.4%`) for finance, no decimals (`23%`) for categorical.
|
|
97
|
+
|
|
98
|
+
## Color in Charts
|
|
99
|
+
|
|
100
|
+
### Single series
|
|
101
|
+
|
|
102
|
+
- Use the brand primary or a single neutral.
|
|
103
|
+
- Add a slight gradient (top to bottom) only on filled area charts; never on bar charts (looks gimmicky).
|
|
104
|
+
|
|
105
|
+
### Multiple series
|
|
106
|
+
|
|
107
|
+
- Sequential (ordered, e.g., low to high values): single hue, light to dark. Use a perceptually uniform scale (viridis, magma).
|
|
108
|
+
- Diverging (centered, e.g., positive vs negative): two-hue diverging palette (blue-white-red).
|
|
109
|
+
- Categorical (no order, e.g., Series A vs Series B vs Series C): qualitative palette, distinct hues. Use a colorblind-safe palette.
|
|
110
|
+
|
|
111
|
+
### Colorblind-safe palettes
|
|
112
|
+
|
|
113
|
+
Avoid red/green pairs alone. Use:
|
|
114
|
+
|
|
115
|
+
- Tol's "bright" palette: blue, red, green, yellow, cyan, purple, gray (distinguishable in most colorblind types).
|
|
116
|
+
- ColorBrewer's "Set2" or "Dark2".
|
|
117
|
+
- Always supplement color with shape/pattern/label so meaning isn't lost.
|
|
118
|
+
|
|
119
|
+
### Pattern alongside color
|
|
120
|
+
|
|
121
|
+
For stacked bars or pies, use a subtle hatching/dot pattern in addition to color. Color carries the primary signal, pattern carries redundancy:
|
|
122
|
+
|
|
123
|
+
```css
|
|
124
|
+
.series-a { fill: var(--color-1); }
|
|
125
|
+
.series-b { fill: var(--color-2); background-image: repeating-linear-gradient(45deg, transparent, transparent 4px, rgba(0,0,0,0.1) 4px, rgba(0,0,0,0.1) 8px); }
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Contrast against background
|
|
129
|
+
|
|
130
|
+
Data marks (bars, lines) need at least 3:1 contrast against the chart background. Text labels need 4.5:1.
|
|
131
|
+
|
|
132
|
+
In dark mode, lighten the data marks. Don't just keep light-mode colors.
|
|
133
|
+
|
|
134
|
+
## Legends
|
|
135
|
+
|
|
136
|
+
- Always show a legend for multi-series charts.
|
|
137
|
+
- Place near the chart, not detached below a scroll fold.
|
|
138
|
+
- Order matches data (largest to smallest, or alphabetical).
|
|
139
|
+
- For interactive charts, legends are clickable to toggle series visibility.
|
|
140
|
+
- Direct labeling (label at the line/bar) often beats a separate legend.
|
|
141
|
+
|
|
142
|
+
## Tooltips
|
|
143
|
+
|
|
144
|
+
- Show on hover (desktop) or tap (mobile).
|
|
145
|
+
- Show the exact value (numeric labels are too small for crowded charts; tooltips fill in).
|
|
146
|
+
- Label which series, the x value, and the y value.
|
|
147
|
+
- Anchor near the cursor; don't make the user scan.
|
|
148
|
+
- Persist long enough to read (don't dismiss on tiny mouse movement).
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
[X-axis label, e.g., March 12]
|
|
152
|
+
Series A: 12,400 (+18%)
|
|
153
|
+
Series B: 8,200 (-3%)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
For accessibility, tooltip content must be reachable via keyboard (focus on data point reveals tooltip).
|
|
157
|
+
|
|
158
|
+
## Direct Labeling
|
|
159
|
+
|
|
160
|
+
For small datasets, label values directly on the chart instead of forcing the eye to travel to an axis:
|
|
161
|
+
|
|
162
|
+
- Bar chart with 3-5 bars: put the value on or above each bar.
|
|
163
|
+
- Line chart: put the value at the rightmost point of each line.
|
|
164
|
+
- Pie chart: put values inside slices (or outside with a leader line if too small).
|
|
165
|
+
|
|
166
|
+
## Animation
|
|
167
|
+
|
|
168
|
+
### Entrance animation
|
|
169
|
+
|
|
170
|
+
- Bars rise from the baseline, 400-600ms staggered by 30-50ms per bar.
|
|
171
|
+
- Lines draw from left to right, 600-800ms total.
|
|
172
|
+
- Pies sweep from 12 o'clock clockwise, 800-1000ms.
|
|
173
|
+
|
|
174
|
+
### Update animation
|
|
175
|
+
|
|
176
|
+
When data changes:
|
|
177
|
+
|
|
178
|
+
- Animate from old to new values, 300-500ms.
|
|
179
|
+
- Use ease-out.
|
|
180
|
+
- Animate the path's `d` attribute or use a library helper for smooth transitions.
|
|
181
|
+
|
|
182
|
+
### When to disable
|
|
183
|
+
|
|
184
|
+
- `prefers-reduced-motion: reduce`: skip entrance animation; show data immediately.
|
|
185
|
+
- Live-updating data views: no animation on every refresh; just update.
|
|
186
|
+
- Print: no animation.
|
|
187
|
+
|
|
188
|
+
```js
|
|
189
|
+
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
190
|
+
chart.options.animation = reduced ? false : { duration: 600, easing: 'easeOutQuart' };
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Accessibility
|
|
194
|
+
|
|
195
|
+
### Screen readers
|
|
196
|
+
|
|
197
|
+
Charts are inherently visual. Provide a text alternative:
|
|
198
|
+
|
|
199
|
+
- Concise summary near the chart describing the key insight (one sentence stating the trend or comparison the chart shows).
|
|
200
|
+
- A data table either inline or in a `<details>` toggle, with the same data the chart shows.
|
|
201
|
+
- For complex charts, use `aria-describedby` to link the chart to a longer description.
|
|
202
|
+
|
|
203
|
+
```html
|
|
204
|
+
<figure>
|
|
205
|
+
<figcaption>Chart caption describing what is plotted</figcaption>
|
|
206
|
+
<div class="chart" role="img" aria-labelledby="chart-summary">
|
|
207
|
+
<svg>...</svg>
|
|
208
|
+
</div>
|
|
209
|
+
<p id="chart-summary">
|
|
210
|
+
One sentence stating the key insight the chart conveys, in plain language, with the headline numbers.
|
|
211
|
+
</p>
|
|
212
|
+
<details>
|
|
213
|
+
<summary>View data table</summary>
|
|
214
|
+
<table>
|
|
215
|
+
...
|
|
216
|
+
</table>
|
|
217
|
+
</details>
|
|
218
|
+
</figure>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Keyboard
|
|
222
|
+
|
|
223
|
+
- Interactive elements (data points, legend items) must be focusable.
|
|
224
|
+
- Arrow keys navigate between data points (left/right for adjacent, up/down for series).
|
|
225
|
+
- Enter/Space activates (toggles legend, drills down).
|
|
226
|
+
- Esc closes any popover or expanded view.
|
|
227
|
+
|
|
228
|
+
### Touch targets
|
|
229
|
+
|
|
230
|
+
Interactive chart elements (data points, legend toggles) need at least 44x44 touch area. For dense charts, expand the hit area beyond the visual element:
|
|
231
|
+
|
|
232
|
+
```html
|
|
233
|
+
<circle cx="100" cy="50" r="4" />
|
|
234
|
+
<circle cx="100" cy="50" r="22" fill="transparent" pointer-events="all" />
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The visible dot is 4px; the touch target is 22px (44px diameter).
|
|
238
|
+
|
|
239
|
+
## Responsive Charts
|
|
240
|
+
|
|
241
|
+
### Reflow at small widths
|
|
242
|
+
|
|
243
|
+
Charts must adapt or simplify on small screens:
|
|
244
|
+
|
|
245
|
+
- Vertical bars become horizontal bars (long labels easier).
|
|
246
|
+
- Multi-line charts collapse to top-N or to small multiples.
|
|
247
|
+
- Pie charts stack labels below.
|
|
248
|
+
- Axis ticks reduce to 3-5 from 7-10.
|
|
249
|
+
|
|
250
|
+
### Container query
|
|
251
|
+
|
|
252
|
+
Charts that appear in different layout slots benefit from container queries:
|
|
253
|
+
|
|
254
|
+
```css
|
|
255
|
+
.chart-container { container-type: inline-size; }
|
|
256
|
+
|
|
257
|
+
@container (max-width: 480px) {
|
|
258
|
+
.chart .legend { display: none; }
|
|
259
|
+
.chart .axis-label { font-size: 0.75rem; }
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Re-render on resize
|
|
264
|
+
|
|
265
|
+
For SVG-based charts, listen to resize and re-render with new dimensions. Throttle to 100-200ms.
|
|
266
|
+
|
|
267
|
+
## Loading and Error States
|
|
268
|
+
|
|
269
|
+
### Loading
|
|
270
|
+
|
|
271
|
+
- Show a skeleton or shimmer matching the chart's eventual shape.
|
|
272
|
+
- Don't show a blank axis frame; that looks broken.
|
|
273
|
+
|
|
274
|
+
### Empty data
|
|
275
|
+
|
|
276
|
+
- Show a message: "No data for this period yet."
|
|
277
|
+
- Optionally a placeholder chart (greyed out) with the message overlaid.
|
|
278
|
+
- Action: "Try a different range" or "Connect data source".
|
|
279
|
+
|
|
280
|
+
### Error
|
|
281
|
+
|
|
282
|
+
- Show an error message with retry: "Couldn't load chart. Retry?"
|
|
283
|
+
- Don't show a broken/clipped chart.
|
|
284
|
+
|
|
285
|
+
## Large Datasets
|
|
286
|
+
|
|
287
|
+
For 1000+ data points:
|
|
288
|
+
|
|
289
|
+
- **Aggregate.** Group by hour/day/week to reduce point count.
|
|
290
|
+
- **Sample.** Show every Nth point.
|
|
291
|
+
- **Summarize.** Show the trend; provide drill-down for detail.
|
|
292
|
+
- **Virtualize.** For long tables, only render visible rows.
|
|
293
|
+
- **Server-side processing.** Aggregate on the server; ship summarized data.
|
|
294
|
+
- **Web Workers.** For client-side heavy aggregation, move work off the main thread.
|
|
295
|
+
|
|
296
|
+
Anti-pattern: shipping 50,000 points to a `<canvas>` chart and expecting smooth interaction.
|
|
297
|
+
|
|
298
|
+
## Tables
|
|
299
|
+
|
|
300
|
+
### Anatomy
|
|
301
|
+
|
|
302
|
+
```html
|
|
303
|
+
<table>
|
|
304
|
+
<caption>Table caption describing the data</caption>
|
|
305
|
+
<thead>
|
|
306
|
+
<tr>
|
|
307
|
+
<th scope="col">Row category</th>
|
|
308
|
+
<th scope="col">Metric A</th>
|
|
309
|
+
<th scope="col">Metric B</th>
|
|
310
|
+
</tr>
|
|
311
|
+
</thead>
|
|
312
|
+
<tbody>
|
|
313
|
+
<tr>
|
|
314
|
+
<th scope="row">Row label</th>
|
|
315
|
+
<td>4,200,000</td>
|
|
316
|
+
<td>+12%</td>
|
|
317
|
+
</tr>
|
|
318
|
+
...
|
|
319
|
+
</tbody>
|
|
320
|
+
</table>
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
- `<caption>` describes the table.
|
|
324
|
+
- `<th scope="col">` for column headers, `<th scope="row">` for row headers.
|
|
325
|
+
- For complex tables (multi-level headers), use `headers="..."` to associate cells with their headers.
|
|
326
|
+
|
|
327
|
+
### Visual rules
|
|
328
|
+
|
|
329
|
+
- Tabular figures (`font-variant-numeric: tabular-nums`) for numeric columns.
|
|
330
|
+
- Right-align numbers, left-align text, center-align symbols/icons.
|
|
331
|
+
- Subtle row dividers (low-contrast borders) rather than alternating row colors. Alternating colors look dated and reduce readability.
|
|
332
|
+
- Sticky header for long tables (`position: sticky` on `<thead>`).
|
|
333
|
+
- Hover state on rows for navigability.
|
|
334
|
+
|
|
335
|
+
### Sorting
|
|
336
|
+
|
|
337
|
+
- Click column header to sort ascending; click again for descending.
|
|
338
|
+
- Show current sort direction with an arrow icon.
|
|
339
|
+
- Use `aria-sort="ascending|descending|none"` on the column header.
|
|
340
|
+
- Multi-column sort: hold Shift to add additional sorts.
|
|
341
|
+
|
|
342
|
+
### Filtering
|
|
343
|
+
|
|
344
|
+
- Per-column filters: dropdown, search, range slider depending on data type.
|
|
345
|
+
- Active filters visible as chips above the table.
|
|
346
|
+
- "Clear all" link.
|
|
347
|
+
|
|
348
|
+
### Selection
|
|
349
|
+
|
|
350
|
+
- Checkbox per row.
|
|
351
|
+
- Select-all checkbox in the header (with indeterminate state for partial selection).
|
|
352
|
+
- Show count of selected rows.
|
|
353
|
+
- Bulk actions appear when rows selected.
|
|
354
|
+
|
|
355
|
+
### Pagination vs infinite scroll
|
|
356
|
+
|
|
357
|
+
- **Pagination** for tables: predictable, accessible, supports deep linking.
|
|
358
|
+
- **Infinite scroll** for activity feeds: low-stakes, sequential reading.
|
|
359
|
+
- Don't mix.
|
|
360
|
+
|
|
361
|
+
### Mobile tables
|
|
362
|
+
|
|
363
|
+
For wide tables on small screens:
|
|
364
|
+
|
|
365
|
+
- **Horizontal scroll** with sticky first column (the row identifier).
|
|
366
|
+
- **Card view**: each row becomes a card, fields as label/value pairs.
|
|
367
|
+
- **Collapse**: show a few key columns, expand row to see all.
|
|
368
|
+
|
|
369
|
+
```css
|
|
370
|
+
.table-scroll {
|
|
371
|
+
overflow-x: auto;
|
|
372
|
+
scrollbar-width: thin;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.table-scroll table {
|
|
376
|
+
min-inline-size: 720px;
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## Stat Cards / KPI Tiles
|
|
381
|
+
|
|
382
|
+
For data summaries, the most important number gets a stat card:
|
|
383
|
+
|
|
384
|
+
```
|
|
385
|
+
[ Stat Card ]
|
|
386
|
+
[ Label: short description of the metric ]
|
|
387
|
+
[ Value: the formatted number, largest type ]
|
|
388
|
+
[ Change: direction icon + delta vs comparison period ]
|
|
389
|
+
[ Sparkline: subtle line chart of recent history ]
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Rules:
|
|
393
|
+
|
|
394
|
+
- Label first (small, muted).
|
|
395
|
+
- Value largest (display type).
|
|
396
|
+
- Change indicator with direction icon and color (green up, red down). Pair with text so color isn't the only signal.
|
|
397
|
+
- Optional sparkline. Subtle (single neutral color), no axis.
|
|
398
|
+
- Optional click affordance to drill down.
|
|
399
|
+
|
|
400
|
+
## Real-Time Updates
|
|
401
|
+
|
|
402
|
+
For live-updating data:
|
|
403
|
+
|
|
404
|
+
- Update without animation (or very subtle pulse on changed values).
|
|
405
|
+
- Don't shift other content. New rows arrive at the top with a brief highlight.
|
|
406
|
+
- Allow pause/resume so users can read without flicker.
|
|
407
|
+
- Show last-updated timestamp.
|
|
408
|
+
|
|
409
|
+
## Common Data Viz Mistakes
|
|
410
|
+
|
|
411
|
+
- Pie chart with > 5 categories.
|
|
412
|
+
- Bar chart with truncated y-axis (exaggerates differences).
|
|
413
|
+
- Line chart with too many series tangling.
|
|
414
|
+
- 3D charts.
|
|
415
|
+
- Tooltip showing only a value with no series/x-axis context.
|
|
416
|
+
- Color-only encoding (red good / red bad without text or icon).
|
|
417
|
+
- Chart that's a bar chart on desktop and a different chart entirely on mobile (jarring).
|
|
418
|
+
- Chart with no axis labels.
|
|
419
|
+
- Chart with no units.
|
|
420
|
+
- Chart that loads with a flicker (no skeleton, then sudden content).
|
|
421
|
+
- Animation that runs on every data refresh (every 5s in a live data view).
|
|
422
|
+
- Chart in dark mode with light-mode colors.
|
|
423
|
+
- Inaccessible chart with no text alternative.
|
|
424
|
+
- Tables without sorting or filtering on data-heavy views.
|
|
425
|
+
- Tables with row colors so loud they distract from data.
|
|
426
|
+
- Numbers without locale formatting (US format in a German UI).
|
|
427
|
+
- Tooltip that doesn't work on touch.
|
|
428
|
+
|
|
429
|
+
## Self-Healing for Data Viz
|
|
430
|
+
|
|
431
|
+
Before declaring work complete:
|
|
432
|
+
|
|
433
|
+
- [ ] Right chart for the data
|
|
434
|
+
- [ ] Bar charts have zero baseline
|
|
435
|
+
- [ ] Pies have <= 5 categories
|
|
436
|
+
- [ ] Axes labeled with units
|
|
437
|
+
- [ ] Colorblind-safe palette
|
|
438
|
+
- [ ] Color is never the only signal (paired with shape/pattern/label)
|
|
439
|
+
- [ ] Legend shown and accessible
|
|
440
|
+
- [ ] Tooltip works on hover AND tap
|
|
441
|
+
- [ ] Chart has text alternative (summary or table)
|
|
442
|
+
- [ ] Interactive elements keyboard-accessible
|
|
443
|
+
- [ ] Touch targets >= 44x44
|
|
444
|
+
- [ ] Responsive: simplifies/reflows on small screens
|
|
445
|
+
- [ ] Loading skeleton matches eventual layout
|
|
446
|
+
- [ ] Empty state has a message and action
|
|
447
|
+
- [ ] Error state has retry
|
|
448
|
+
- [ ] Numbers locale-formatted
|
|
449
|
+
- [ ] Animation respects `prefers-reduced-motion`
|
|
450
|
+
- [ ] Tested in light AND dark mode
|
|
451
|
+
- [ ] Tables: sorting, sticky header, mobile-adaptive
|
|
452
|
+
|
|
453
|
+
## See Also
|
|
454
|
+
|
|
455
|
+
- [accessibility.md](accessibility.md) for chart and table accessibility
|
|
456
|
+
- [responsive.md](responsive.md) for chart reflow patterns
|
|
457
|
+
- [motion.md](motion.md) for chart animation
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Common Visual Defects and Geometry Checks
|
|
2
|
+
|
|
3
|
+
A symptom-to-fix lookup for visible defects, plus a canonical programmatic geometry sweep that runs against every audited route at both capture viewports. Use this file when something looks wrong and you want the standard fix, or when you want a deterministic check that catches issues screenshots can miss.
|
|
4
|
+
|
|
5
|
+
## How to Use
|
|
6
|
+
|
|
7
|
+
1. Identify the symptom on the rendered page or in a screenshot.
|
|
8
|
+
2. Look it up in the defect table below.
|
|
9
|
+
3. Apply the standard fix at the right layer (component, page, or design token).
|
|
10
|
+
4. Re-run the geometry sweep on the affected route at both audit viewports.
|
|
11
|
+
5. Re-capture before-and-after screenshots and confirm the fix did not regress neighbors.
|
|
12
|
+
|
|
13
|
+
The defect table is one big table by design: one search lands you on the symptom regardless of which topic it might also live under. Topical references point readers here rather than restating the table.
|
|
14
|
+
|
|
15
|
+
## Defect Lookup Table
|
|
16
|
+
|
|
17
|
+
| Defect | Likely cause | Fix |
|
|
18
|
+
|--------|--------------|-----|
|
|
19
|
+
| Button shifts oddly on hover | Parent and child both transform | Disable child transform inside the animated card or move the transform to one layer |
|
|
20
|
+
| Dark line after sticky header | Border/shadow/background mismatch between header and section top | Normalize header border, shadow, and section top background so the seam is invisible |
|
|
21
|
+
| Dropdown not centered | Absolute position tied to item edge | Use `left: 50%`, `transform: translateX(-50%)`, clamp width on small screens |
|
|
22
|
+
| Mobile dropdown off-canvas | Desktop hover rule overrides mobile rule | Add a media-specific override and constrain the dropdown to the viewport |
|
|
23
|
+
| Card row uneven heights | Content-dependent heights with no stretch | Use grid stretch, flex column, `height: 100%`, and stable `min-height` |
|
|
24
|
+
| Only nested CTA clickable | Card visually behaves as a link but the anchor wraps only the CTA | Make the full card an anchor, or use a button-card pattern with a stretched pseudo-element on the title link; never nest anchors, which is invalid HTML per the `<a>` content model |
|
|
25
|
+
| Duplicate arrows | CSS pseudo-element arrow plus a text or icon arrow | Choose one arrow source and suppress the other by component class |
|
|
26
|
+
| Pricing implies wrong plan | Ambiguous trial or setup wording on shared cards | State paid plan terms and fees plainly; remove repeated misleading phrases |
|
|
27
|
+
| Social icons too far apart or too small | Independent margins and small hit boxes | Use fixed 44px boxes and `gap` |
|
|
28
|
+
| Cross or check icon off-center | Pseudo-element dimensions mismatch the icon box | Use a square icon box with absolutely centered pseudo-elements, or inline SVG |
|
|
29
|
+
| Mobile horizontal scroll | Hidden off-canvas element, wide image, long unbreakable word, or `100vw` misuse | Inspect `scrollWidth`; constrain media; use `min-width: 0` on grid children; avoid `width: 100vw` inside padded containers |
|
|
30
|
+
| Low-quality page versus reference | Thin sections, repeated template copy, generic stock images | Add useful sections, vary copy, use relevant images, tighten hierarchy |
|
|
31
|
+
| Same widget looks different across pages | Duplicated markup, page-local CSS, no component contract | Extract a shared component or normalize markup and classes to one contract; see [components.md](components.md) |
|
|
32
|
+
| Many card variants without purpose | Ad hoc class combinations applied page by page | Define named variants in the component and map old usages into them |
|
|
33
|
+
| Integration or logo tiles inconsistent in size | Each page hand-codes image and card rules | Create one logo tile component with a fixed image box, label treatment, and grid behavior |
|
|
34
|
+
| CTA bands vary randomly | Page-specific final sections | Create one final-CTA component with explicit variants for centered, split, compact, or gradient |
|
|
35
|
+
| Focus ring clipped | Parent uses `overflow: hidden` with insufficient padding | Move the ring to an `outline` (which paints outside the box) or add internal padding so the ring fits |
|
|
36
|
+
| Focus ring invisible on dark surfaces | Single-color ring chosen for light mode only | Use a ring color with 3:1 contrast against both surface and resting element state, in both light and dark mode |
|
|
37
|
+
| Hero image causes CLS | Missing intrinsic `width` and `height` | Declare width and height as attributes; reserve space with `aspect-ratio` |
|
|
38
|
+
| Late-loading hero image hurts LCP | Hero image is `loading="lazy"` or behind hydration | Use `loading="eager"` and `fetchpriority="high"` on the hero LCP image only |
|
|
39
|
+
| Webfont flash on first paint | `font-display: block` or no `font-display` set | Use `font-display: swap` for body and `optional` for display; preload at most one critical weight |
|
|
40
|
+
| Unstyled flash on hydration | Critical CSS not inlined | Inline above-the-fold CSS under 14 KB so it fits in the first round trip |
|
|
41
|
+
| Button label truncates on mobile | Fixed width or `white-space: nowrap` with insufficient room | Constrain by `max-width`, allow wrap, or shorten the label |
|
|
42
|
+
| Form validation appears late | Validation runs on every keystroke, then debounces | Validate on blur for new errors; clear errors live as the user types |
|
|
43
|
+
| Modal scrolls with the page | Body scroll not locked when modal is open | Lock body scroll on open, restore on close; restore the scroll position |
|
|
44
|
+
| Modal traps keyboard but not screen reader | Background is interactive in the accessibility tree | Mark background `inert` (or `aria-hidden="true"` plus `pointer-events: none`) while modal is open |
|
|
45
|
+
| Skip link overflows the viewport while hidden | `position: absolute; left: -9999px` can still extend the document layout box, or the focus state inherits the off-screen position | Use the modern `clip-path: inset(100%)` plus `position: absolute; width: 1px; height: 1px; overflow: hidden` pattern when hidden; switch to `clip-path: inset(0); position: fixed; top: 1rem; left: 1rem` on `:focus` |
|
|
46
|
+
| Tables overflow on mobile | Fixed-width table without scroll wrapper | Wrap in a scroll container with `overflow-x: auto`, or transform to cards below the breakpoint |
|
|
47
|
+
| Empty state is blank | No specific message and no primary action | Add a specific message, the condition that fills it, and a primary action that resolves it |
|
|
48
|
+
|
|
49
|
+
## Programmatic Geometry Sweep
|
|
50
|
+
|
|
51
|
+
The geometry sweep catches defects that are easy to miss in a screenshot pass: sub-44 hit targets buried in long pages, hidden text overflow on edge cases, viewport bleed from a single rogue element, duplicate arrows that read fine in isolation but stack across siblings.
|
|
52
|
+
|
|
53
|
+
Run from a headless browser of your choice (Puppeteer, Playwright, or equivalent). The snippet below uses only standard DOM and CSSOM APIs:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
const issues = await page.evaluate(() => {
|
|
57
|
+
const out = [];
|
|
58
|
+
const visible = (el) => {
|
|
59
|
+
const r = el.getBoundingClientRect();
|
|
60
|
+
const cs = getComputedStyle(el);
|
|
61
|
+
return r.width > 0 && r.height > 0 && cs.display !== "none"
|
|
62
|
+
&& cs.visibility !== "hidden" && parseFloat(cs.opacity) > 0;
|
|
63
|
+
};
|
|
64
|
+
const nameFor = (el) =>
|
|
65
|
+
el.tagName.toLowerCase()
|
|
66
|
+
+ (el.className
|
|
67
|
+
? "." + String(el.className).trim().split(/\s+/).slice(0, 3).join(".")
|
|
68
|
+
: "");
|
|
69
|
+
|
|
70
|
+
if (document.scrollingElement.scrollWidth > window.innerWidth + 1) {
|
|
71
|
+
out.push({ type: "viewport-bleed", value: document.scrollingElement.scrollWidth, viewport: window.innerWidth });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const el of document.querySelectorAll("body *")) {
|
|
75
|
+
if (!visible(el)) continue;
|
|
76
|
+
const cs = getComputedStyle(el);
|
|
77
|
+
if (el.clientWidth > 0 && el.scrollWidth > el.clientWidth + 2 && cs.overflowX === "visible") {
|
|
78
|
+
const text = (el.textContent || "").trim().replace(/\s+/g, " ").slice(0, 80);
|
|
79
|
+
if (text && !["HTML", "BODY", "MAIN", "SECTION"].includes(el.tagName)) {
|
|
80
|
+
out.push({ type: "text-overflow", selector: nameFor(el), text });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const el of document.querySelectorAll("a, button, input, select, textarea, [role='button'], .btn")) {
|
|
86
|
+
if (!visible(el)) continue;
|
|
87
|
+
const r = el.getBoundingClientRect();
|
|
88
|
+
if (r.width < 44 || r.height < 44) {
|
|
89
|
+
out.push({ type: "small-target", selector: nameFor(el), width: Math.round(r.width), height: Math.round(r.height) });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const el of document.querySelectorAll("a, .btn-text")) {
|
|
94
|
+
const text = (el.textContent || "").replace(/\s+/g, " ").trim();
|
|
95
|
+
if (/→\s*→|›\s*›|»\s*»/.test(text)) out.push({ type: "duplicate-arrow", text });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return out;
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The sweep returns nine check categories. Run it on every audited route at both capture viewports.
|
|
103
|
+
|
|
104
|
+
| # | Check | Triggers when |
|
|
105
|
+
|---|-------|---------------|
|
|
106
|
+
| 1 | Viewport bleed | `document.scrollingElement.scrollWidth` exceeds `window.innerWidth` by more than 1 px |
|
|
107
|
+
| 2 | Visible text overflow | An element's `scrollWidth` exceeds its `clientWidth` by more than 2 px while `overflow-x` is `visible` |
|
|
108
|
+
| 3 | Small interactive target | A visible interactive element measures less than 44 by 44 CSS pixels |
|
|
109
|
+
| 4 | Duplicate arrow | A link or text-button label contains stacked arrow glyphs (`→ →`, `› ›`, `» »`) |
|
|
110
|
+
| 5 | Pricing or card clickable mismatch | A card visually behaves as a link but only an inner CTA is the anchor (extend the sweep with a per-project selector list) |
|
|
111
|
+
| 6 | Header or dropdown centering | The dropdown's horizontal center is more than 4 px from its trigger center (extend with the per-project selector) |
|
|
112
|
+
| 7 | Mobile drawer scroll lock | Body scroll is not locked while the drawer is open (extend with a per-project trigger sequence) |
|
|
113
|
+
| 8 | Focus state presence | A keyboard-focusable element produces no visible outline change on focus (extend with a per-project focus drive) |
|
|
114
|
+
| 9 | Project-specific extension | Open slot for negative letter-spacing, banned colors, or any rule the project standardizes on |
|
|
115
|
+
|
|
116
|
+
The base snippet covers checks 1 through 4 directly. Checks 5 through 8 require selectors and triggers specific to the project under audit; add them to the same `evaluate` call. Check 9 is the slot for any rule the project enforces on top of the standard nine.
|
|
117
|
+
|
|
118
|
+
Filter false positives only when you can explain them: hidden off-canvas content, intentionally overflowing dropdown internals that do not affect the page viewport, or a tap target that is intentionally part of a larger ancestor hit area. Document the filter so the next run does not re-discover it.
|
|
119
|
+
|
|
120
|
+
## Per-Check Thresholds
|
|
121
|
+
|
|
122
|
+
The thresholds below align with the North Star Targets in the entry SKILL.md and the touch-target rule in [ui-ux.md](ui-ux.md). They do not introduce new bars; they make the bars measurable.
|
|
123
|
+
|
|
124
|
+
| Check | Threshold | Source |
|
|
125
|
+
|-------|-----------|--------|
|
|
126
|
+
| Capture viewports | `1440x900` desktop, `375x812` mobile | [responsive.md](responsive.md) breakpoints table |
|
|
127
|
+
| Touch target minimum | 44 by 44 CSS pixels | Rule 25 in SKILL.md, [ui-ux.md](ui-ux.md) hit targets table |
|
|
128
|
+
| Horizontal overflow tolerance | 0 px on mobile, 0 px on desktop | [responsive.md](responsive.md) |
|
|
129
|
+
| Duplicate arrow count | 0 across all visible labels | This file, defect table |
|
|
130
|
+
| Dropdown centering tolerance | 4 px from trigger center | This file, geometry sweep |
|
|
131
|
+
| Focus ring contrast | 3:1 against surface and resting state | [accessibility.md](accessibility.md) contrast targets |
|
|
132
|
+
| LCP image declared dimensions | Both `width` and `height` present | Rule 1 in SKILL.md, [performance.md](performance.md) |
|
|
133
|
+
|
|
134
|
+
A run is "clean" when every check returns zero issues at both capture viewports.
|
|
135
|
+
|
|
136
|
+
## Acceptance
|
|
137
|
+
|
|
138
|
+
A polish pass is not complete until:
|
|
139
|
+
|
|
140
|
+
- The geometry sweep returns zero issues on every audited route at `1440x900` and `375x812`.
|
|
141
|
+
- Every defect found in the lookup table has been fixed at the right layer (component, page, or design token).
|
|
142
|
+
- Re-captured screenshots show the fix and no regression on neighbors.
|
|
143
|
+
|
|
144
|
+
Until then, the work is not done. Do not present an incomplete visual pass as complete.
|
|
145
|
+
|
|
146
|
+
## See Also
|
|
147
|
+
|
|
148
|
+
- [audit-workflow.md](audit-workflow.md) for the full multi-page audit procedure
|
|
149
|
+
- [components.md](components.md) for component contracts and drift detection
|
|
150
|
+
- [accessibility.md](accessibility.md) for focus, contrast, and skip-link patterns
|
|
151
|
+
- [responsive.md](responsive.md) for capture viewports and breakpoints
|
|
152
|
+
- [performance.md](performance.md) for LCP, font, and image strategy
|