@sema-agent/server 1.196.0 → 1.197.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/dist/config.d.ts.map +1 -1
- package/dist/config.js +10 -1
- package/dist/config.js.map +1 -1
- package/package.json +2 -1
- package/skills/code-review.md +28 -0
- package/skills/commit-push-pr.md +77 -0
- package/skills/dataviz/SKILL.md +112 -0
- package/skills/dataviz/references/anti-patterns.md +119 -0
- package/skills/dataviz/references/choosing-a-form.md +57 -0
- package/skills/dataviz/references/color-formula.md +113 -0
- package/skills/dataviz/references/components.md +39 -0
- package/skills/dataviz/references/interaction.md +60 -0
- package/skills/dataviz/references/marks-and-anatomy.md +97 -0
- package/skills/dataviz/references/palette.md +149 -0
- package/skills/dataviz/scripts/validate_palette.js +262 -0
- package/skills/find-skills.md +148 -0
- package/skills/init.md +28 -0
- package/skills/keybindings-help.md +294 -0
- package/skills/loop.md +50 -0
- package/skills/run-skill-generator.md +493 -0
- package/skills/run.md +148 -0
- package/skills/schedule.md +48 -0
- package/skills/security-review.md +181 -0
- package/skills/simplify.md +64 -0
- package/skills/update-config.md +93 -0
- package/skills/verify.md +334 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Choosing a form
|
|
2
|
+
|
|
3
|
+
Decide this **before** color. The data's job picks the form — and sometimes the
|
|
4
|
+
right form is not a chart.
|
|
5
|
+
|
|
6
|
+
## Is it even a chart?
|
|
7
|
+
|
|
8
|
+
| The data is… | Use | Not |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| A single current value (+ maybe a trend) | **Stat tile** (value + delta + sparkline) | A one-bar bar chart |
|
|
11
|
+
| A handful of headline numbers | **KPI row** of stat tiles | A grouped bar chart |
|
|
12
|
+
| The one number a dashboard leads with | **Hero figure** (≥48px, sans) | — |
|
|
13
|
+
| A single ratio against a limit | **Meter** (same-ramp track) | A pie of 2 slices |
|
|
14
|
+
| More than ~7 classes that all carry meaning | A **table** (or table + chart) | More colors |
|
|
15
|
+
|
|
16
|
+
If a chart *is* right, pick the type by the job:
|
|
17
|
+
|
|
18
|
+
## The job → the type
|
|
19
|
+
|
|
20
|
+
| Job (what the reader must do) | Default form | Color job |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| Compare magnitude, low → high | bar / column; **heatmap** for a grid | sequential (one hue) |
|
|
23
|
+
| Trend over time | line; area for a single series | sequential or 1 categorical |
|
|
24
|
+
| Tell distinct series apart | grouped/stacked bar, multi-line | **categorical** |
|
|
25
|
+
| One series is the point, rest are context | **emphasis** (highlight one, gray the rest) | 1 hue + gray |
|
|
26
|
+
| Above/below a baseline; Δ to target | diverging bar, or line vs baseline | diverging |
|
|
27
|
+
| Part-to-whole | **stacked bar** (go horizontal for many / long-named categories) | categorical |
|
|
28
|
+
| Ordered-scale share (Likert, sentiment, agree↔disagree) | **diverging stacked bar**, centered on neutral | diverging |
|
|
29
|
+
| Before → after per item | dumbbell | 1 hue, 2 shades |
|
|
30
|
+
|
|
31
|
+
## The rules behind the table
|
|
32
|
+
|
|
33
|
+
- **Sequential is the safe default.** One hue, more-is-darker. It stays legible and
|
|
34
|
+
consistent and is hard to misread. Reach for it unless the data's job is
|
|
35
|
+
specifically *identity* or *polarity*.
|
|
36
|
+
- **Categorical is for when the series ARE the subject** — and it has a real cost:
|
|
37
|
+
it can bury the one data point that actually matters. If the story is "this one
|
|
38
|
+
went up," that's **emphasis**, not categorical.
|
|
39
|
+
- **Emphasis** = the most underused form. One series in the accent hue, the rest in
|
|
40
|
+
the de-emphasis gray. Often the honest answer to "make this chart clearer."
|
|
41
|
+
- **Texture is an opt-in expression, not a default form.** It earns its place only
|
|
42
|
+
for accessibility (full CVD), print/export, and `forced-colors`. Never decorative.
|
|
43
|
+
→ see `marks-and-anatomy.md`.
|
|
44
|
+
|
|
45
|
+
## Series-count ladder (categorical)
|
|
46
|
+
|
|
47
|
+
| Series | Treatment |
|
|
48
|
+
|---|---|
|
|
49
|
+
| 1–3 | color alone is comfortable for everyone; direct-label |
|
|
50
|
+
| 4 | the CVD floor enters — direct labels become mandatory, not a courtesy |
|
|
51
|
+
| 5–6 | soft cap; legend or small multiples |
|
|
52
|
+
| 7–8 | token ceiling; past it, fold the tail into "Other," facet into small multiples, or use composite encoding (hue × shape) |
|
|
53
|
+
|
|
54
|
+
Never solve "too many series" by generating more hues. A generated 9th hue is
|
|
55
|
+
indistinguishable from an existing one under CVD and breaks every check.
|
|
56
|
+
|
|
57
|
+
---
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Color formula
|
|
2
|
+
|
|
3
|
+
Color is **not hand-picked**. Every chart color does exactly one of four jobs, and a
|
|
4
|
+
palette is legal only if it passes six checks. The checks are the product — they are
|
|
5
|
+
what makes a palette safe to change and what lets the same method run on any design
|
|
6
|
+
system's ramps.
|
|
7
|
+
|
|
8
|
+
## The four jobs
|
|
9
|
+
|
|
10
|
+
| Job | What it encodes | Structure |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| **Categorical** | identity (which series) | 8 hues, fixed order, assigned in sequence, never cycled |
|
|
13
|
+
| **Ordinal** | position in a sequence (funnel stage, tier, bucket) | one hue, monotone lightness steps; light end still ≥ 2:1 on surface |
|
|
14
|
+
| **Sequential** | magnitude (how much) | one hue, steps 100→700, light→dark; flips anchor in dark |
|
|
15
|
+
| **Diverging** | polarity (which side of a baseline) | two hues + a neutral gray midpoint; equal steps per arm |
|
|
16
|
+
| **Status** | state (good→critical) | a small fixed scale, reserved meaning, always icon+label |
|
|
17
|
+
|
|
18
|
+
**Categorical or ordinal?** If swapping the category order would change the
|
|
19
|
+
meaning — funnel stages, size tiers (S/M/L), age bands, cohort buckets — it is
|
|
20
|
+
**ordinal** and takes a one-hue ramp so the reader sees the order in the color.
|
|
21
|
+
If swapping would not — product names, teams, regions, endpoints — it is
|
|
22
|
+
**nominal categorical** and each bar takes the *same* slot-1 hue (one series,
|
|
23
|
+
so no legend box — the title names it), or slots 1..N when there are N separate
|
|
24
|
+
series. Never color nominal bars by their value: that spends the identity channel
|
|
25
|
+
re-encoding what bar length already shows.
|
|
26
|
+
|
|
27
|
+
## The six checks
|
|
28
|
+
|
|
29
|
+
Every categorical color — current or proposed — must pass all six.
|
|
30
|
+
|
|
31
|
+
1. **Fixed hue anchors.** Eight families in a fixed order. The order is the
|
|
32
|
+
CVD-safety mechanism; it never changes. *(structural — enforced, not measured)*
|
|
33
|
+
2. **Lightness band per mode.** OKLCH L ≈ 0.43–0.77 light; ≈ 0.48–0.67 dark. *(validator)*
|
|
34
|
+
3. **Chroma floor.** OKLCH C ≥ ~0.10 — below it a hue reads as gray and stops doing
|
|
35
|
+
identity work. *(validator)*
|
|
36
|
+
4. **CVD separation.** Machado-2009 ΔE ≥ 12 target / ≥ 8 floor (floor legal only with
|
|
37
|
+
secondary encoding), under protanopia & deuteranopia. *Adjacent* pairs for
|
|
38
|
+
stacks/bars/lines (only neighbors touch — assignment never skips); **all pairs for
|
|
39
|
+
scatter, bubble, choropleth, and small-multiples**, where any two marks can sit side
|
|
40
|
+
by side — pass `--pairs all` there or a real collapse stays hidden. *(validator)*
|
|
41
|
+
5. **Contrast vs surface.** ≥ 3:1 for marks; conditionally relaxed where values are
|
|
42
|
+
readable another way (visible labels or the table view). *(validator)*
|
|
43
|
+
6. **Documented palette only.** Every slot is a hex from the instance file
|
|
44
|
+
(`palette.md` or its equivalent) — no eyeballed values. *(structural; for a
|
|
45
|
+
customer's ramps, snap to nearest — below)*
|
|
46
|
+
|
|
47
|
+
## Run the checks — never eyeball them
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
node scripts/validate_palette.js \
|
|
51
|
+
"#2a78d6,#1baf7a,#eda100,#008300,#4a3aa7,#e34948,#e87ba4,#eb6834" --mode light
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
(`scripts/` is relative to this skill's base directory, shown at the top of the prompt.)
|
|
55
|
+
|
|
56
|
+
(or load it as `<script type="module">` in the chart's own page — it reads
|
|
57
|
+
`data-palette` off `<body>` and logs a `console.table` report)
|
|
58
|
+
|
|
59
|
+
Reports each computable check (2–5) with PASS / WARN / FAIL plus the worst CVD pair.
|
|
60
|
+
Exit 0 = no hard FAIL (WARN bands — floor-band CVD 8–12 and sub-3:1 contrast relief —
|
|
61
|
+
still exit 0 and require secondary encoding); exit 1 on any FAIL. Run once per mode
|
|
62
|
+
(`--mode dark --surface "#1a1a19"`), and add
|
|
63
|
+
`--pairs all` for scatter / bubble / map / small-multiples charts (where any two marks
|
|
64
|
+
can be neighbors — the default adjacent check would hide a collapse). For an
|
|
65
|
+
**ordinal** ramp pass `--ordinal` — it switches to the ramp checks (monotone L,
|
|
66
|
+
adjacent ΔL ≥ 0.06, light-end contrast ≥ 2.0:1, single hue) instead of the
|
|
67
|
+
categorical six.
|
|
68
|
+
A WARN on CVD (8–12 floor) is legal **only** if you also ship secondary encoding
|
|
69
|
+
(direct labels, gaps, or texture). A WARN on contrast is **not dismissable** — it
|
|
70
|
+
obligates a relief channel (visible direct labels or the table view); shipping the
|
|
71
|
+
sub-3:1 fill with neither is a fail.
|
|
72
|
+
|
|
73
|
+
**Scope — what the validator does and doesn't cover.** These six checks validate a
|
|
74
|
+
*categorical* palette (series identity). They do **not** judge a lone status/text
|
|
75
|
+
color or a sequential ramp. For a single status or text color, run a WCAG *text*-
|
|
76
|
+
contrast check (4.5:1 normal, 3:1 large) — `validate_palette.js` exports
|
|
77
|
+
`contrast(a, b)` for exactly this. For sequential/diverging, the check is lightness
|
|
78
|
+
monotonicity across the ramp, not adjacency CVD — running the categorical validator on
|
|
79
|
+
a sequential ramp **will FAIL by design** (it spans the band; steps sit close), which
|
|
80
|
+
is expected, not a real failure; don't "fix" a good ramp to satisfy it.
|
|
81
|
+
|
|
82
|
+
## Snap-to-passing (any design system)
|
|
83
|
+
|
|
84
|
+
Given a customer's ramps and a desired order:
|
|
85
|
+
1. For each slot, pick the step whose OKLCH L sits in the mode's band and C ≥ floor.
|
|
86
|
+
2. Run the validator. For any adjacent pair below ΔE 12, nudge one slot ± a step
|
|
87
|
+
(hold its hue, move its lightness) and re-run.
|
|
88
|
+
3. Repeat until the worst adjacent pair clears the floor. Function preserved, the
|
|
89
|
+
customer's hues kept.
|
|
90
|
+
|
|
91
|
+
## Themes
|
|
92
|
+
|
|
93
|
+
The slot **order** is a separable, named choice — a *theme* — on the same hues and
|
|
94
|
+
the same six checks. Each design system names a default order and any alternates;
|
|
95
|
+
swapping themes tunes the mood without touching the method. A surface adopts one
|
|
96
|
+
theme and freezes it; never mix themes within a dashboard. (See `palette.md`.)
|
|
97
|
+
|
|
98
|
+
**Deriving an order when a system has no theme yet:** don't guess. Enumerate candidate
|
|
99
|
+
orderings of the system's hues, run the validator on each, and pick the one that
|
|
100
|
+
maximizes the *minimum adjacent* CVD ΔE. (Seeding from a known-good order by hue-family
|
|
101
|
+
analogy, then optimizing, is fine — this is exactly how the default in
|
|
102
|
+
`palette.md` was derived.)
|
|
103
|
+
|
|
104
|
+
## Status is fixed
|
|
105
|
+
|
|
106
|
+
Status never follows the theme — it is a small fixed scale (good → warning → serious
|
|
107
|
+
→ critical) with reserved meaning, on steps deliberately distinct from the categorical
|
|
108
|
+
slots so a status color never impersonates a series, and always paired with an
|
|
109
|
+
icon + label (on a light surface warning and serious sit below 3:1 by design —
|
|
110
|
+
the pairing is the mitigation). (Exact steps in `palette.md`.) The collision rule: when a series *means* good/bad (error rate, pass/fail) it wears
|
|
111
|
+
status tokens; when it's just "series 4" it wears categorical — never both in one chart.
|
|
112
|
+
|
|
113
|
+
---
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Components — the pieces a chart is made of
|
|
2
|
+
|
|
3
|
+
A chart is built from these parts, assembled in plain HTML/SVG. Tier 0 is the
|
|
4
|
+
foundation everything mounts on; the System tier is what makes the method
|
|
5
|
+
portable (and is, itself, this skill).
|
|
6
|
+
|
|
7
|
+
## Tier 0 — Foundations
|
|
8
|
+
- **Color roles** — categorical (8 × light/dark), sequential ramps, diverging pairs,
|
|
9
|
+
status (4), de-emphasis / "Other", grayscale chart furniture (axis/grid/label/surface).
|
|
10
|
+
Defined as CSS custom properties at the top of the HTML — see `palette.md`.
|
|
11
|
+
- **Texture fill** — the directional fill + 45°/135° rotations.
|
|
12
|
+
- **Chart container** — a `<figure>` (or card `<div>`) that owns responsive
|
|
13
|
+
sizing, title/caption, and the **table-view toggle** (the accessibility twin
|
|
14
|
+
of every chart). **Any fixed height includes the x-axis band** (plot height
|
|
15
|
+
+ axis labels) so the card never gets a nested vertical scroll; prefer
|
|
16
|
+
letting the container grow with its content.
|
|
17
|
+
- **Legend** (toggle-to-isolate, texture-aware swatches) · **Tooltip** · **Axis** · **Data label**.
|
|
18
|
+
|
|
19
|
+
## Tier 1 — The charts people ask for
|
|
20
|
+
- **Bar chart** — grouped + stacked, thin-bar default, horizontal + vertical.
|
|
21
|
+
- **Line chart** — multi-series, soft-fill area variant, accessibility markers.
|
|
22
|
+
- **Stat tile** — value + delta + optional sparkline (the figure contract).
|
|
23
|
+
- **Meter / progress track** — same-ramp tracks.
|
|
24
|
+
|
|
25
|
+
## Tier 2 — Rounding out the kit
|
|
26
|
+
- **Area chart** (stacked, band-edge = line) · **Sparkline** · **Heatmap**
|
|
27
|
+
- **Scale legend** (sequential / diverging) · **Chart filters / time range** · **Empty state**
|
|
28
|
+
|
|
29
|
+
## System tier — becomes the skill
|
|
30
|
+
- **Six-checks validator** — `scripts/validate_palette.js` (palette validation).
|
|
31
|
+
- **Theming engine** — snap a customer's ramps to passing values (color-formula.md).
|
|
32
|
+
- **Chart-type heuristic** — pick the form (choosing-a-form.md).
|
|
33
|
+
- **Table-view generator** — the WCAG-clean equivalent of any chart.
|
|
34
|
+
|
|
35
|
+
Notes: part-to-whole rides on the stacked bar chart; donut stays deprioritized.
|
|
36
|
+
Small multiples is a layout pattern over these, not a separate piece. Scatter
|
|
37
|
+
joins Tier 2 if scatter-heavy surfaces land.
|
|
38
|
+
|
|
39
|
+
---
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Interaction — tooltips & filters
|
|
2
|
+
|
|
3
|
+
An HTML chart is interactive by default — the hover layer is part of the deliverable,
|
|
4
|
+
not an upgrade. Omitting it is the exception (a bare stat tile), never the default.
|
|
5
|
+
Design it with the same care as the static render.
|
|
6
|
+
|
|
7
|
+
## Tooltips & hover
|
|
8
|
+
|
|
9
|
+
Tooltips **enhance, they never gate**: every value a tooltip shows is also reachable
|
|
10
|
+
without it, through direct labels or the table view. Same details on keyboard focus
|
|
11
|
+
as on hover.
|
|
12
|
+
|
|
13
|
+
- **The crosshair finds the X.** A vertical hairline tracks the pointer and snaps to
|
|
14
|
+
the nearest data position. Readers aim at a date, never at a 2px line.
|
|
15
|
+
- **On bars and cells, the mark is the hit target.** No crosshair — each bar, segment,
|
|
16
|
+
dot, or heat-cell carries its own `pointermove`/`focus` tooltip showing category and
|
|
17
|
+
value, and the hovered mark lifts (slight lighten or outline) so the reader sees it respond.
|
|
18
|
+
- **One tooltip, every series.** The readout lists every series at that X — the
|
|
19
|
+
pointer never has to land on a line or a fill to get a value.
|
|
20
|
+
- **Labels are untrusted data — use `textContent`.** Series and category names
|
|
21
|
+
often come from CSV headers, tool output, or API responses. Insert them into
|
|
22
|
+
tooltip/legend/table DOM with `textContent` or `createTextNode`, never via
|
|
23
|
+
`innerHTML` string concatenation.
|
|
24
|
+
- **Values lead, labels follow.** In the tooltip the value is the Strong,
|
|
25
|
+
high-contrast element and the series name is secondary — the legend's hierarchy
|
|
26
|
+
inverted, because here the reader has the series and wants the number.
|
|
27
|
+
- **Line keys, not boxes.** Tooltip rows key their series with a short stroke of the
|
|
28
|
+
series color; at tooltip density a filled box is data-weight ink doing a label's
|
|
29
|
+
job. (Legends still mirror the mark: rect for bars/areas, line for lines.)
|
|
30
|
+
- **The hit target is bigger than the mark.** A mark's hover/focus area includes its
|
|
31
|
+
2px surface gap and then some — never only the painted pixels. An 8px scatter dot is a
|
|
32
|
+
pinpoint nobody hits reliably; give each point a transparent hit area of at least
|
|
33
|
+
**24px**, or — for dense scatter — a nearest-point / Voronoi layer so the pointer only
|
|
34
|
+
has to be *closest*, not dead-center. (The crosshair already does this for the X on
|
|
35
|
+
line and bar charts; scatter and bubble need the per-point version.)
|
|
36
|
+
- **A value pushed off its mark lives in the tooltip.** When a label won't fit inside a
|
|
37
|
+
small bar (see `marks-and-anatomy.md`), that bar's hit area carries the value on hover
|
|
38
|
+
and focus — the tooltip is its overflow home, and the table view keeps it reachable
|
|
39
|
+
without hovering at all.
|
|
40
|
+
|
|
41
|
+
## Filters & time ranges
|
|
42
|
+
|
|
43
|
+
Every monitoring dashboard needs the same controls. These are **standard UI, not
|
|
44
|
+
chart marks** — build them with ordinary HTML form controls styled to match the
|
|
45
|
+
chart chrome. Dataviz only adds composition rules:
|
|
46
|
+
|
|
47
|
+
- **One row, above the charts.** Filters sit in a single left-aligned row above the
|
|
48
|
+
content they scope — never inside a chart card, never per-chart. If one chart needs
|
|
49
|
+
its own range, it's a different dashboard.
|
|
50
|
+
- **Date range first.** It's the filter every reader reaches for; presets (today,
|
|
51
|
+
last 7 / 30 / 90 days) before a custom range.
|
|
52
|
+
- **Filters scope everything below them.** Every chart, stat, and table re-renders
|
|
53
|
+
against the same slice, so the numbers always agree.
|
|
54
|
+
- **Refetch keeps the frame.** While data reloads, charts hold their previous render
|
|
55
|
+
at reduced opacity — no skeleton, no layout jump, no flash.
|
|
56
|
+
|
|
57
|
+
A good date picker lists presets as rows (nobody fights a calendar grid for "last 30
|
|
58
|
+
days"), marks selection with a 16px bold check, keeps hover a ghost wash so it never
|
|
59
|
+
competes with selection, and tucks the custom range behind a hairline in the footer.
|
|
60
|
+
(See `palette.md` for the reference spec.)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Marks & anatomy
|
|
2
|
+
|
|
3
|
+
The quiet, considered look is a few fixed specs plus two pieces of negative space.
|
|
4
|
+
The data is the only thing allowed to be loud.
|
|
5
|
+
|
|
6
|
+
## Mark specs (fixed across every chart)
|
|
7
|
+
|
|
8
|
+
| Mark | Spec |
|
|
9
|
+
|---|---|
|
|
10
|
+
| Bar / column | **≤ 24px thick** (cap it — never fill the slot; let the band's leftover be air); **4px rounded data-end, square at the baseline**; grows from a single baseline |
|
|
11
|
+
| Line | **2px**, round join/cap |
|
|
12
|
+
| Marker / end-dot | **≥ 8px** (r ≥ 4), filled with the series color |
|
|
13
|
+
| Area fill | the series hue at **~10% opacity** (a wash, never a saturated block) |
|
|
14
|
+
| Gridlines / axes | one-step-off-surface gray, **hairline (1px), solid** (never dashed), recessive |
|
|
15
|
+
|
|
16
|
+
## The two spacers (white doing the separating)
|
|
17
|
+
|
|
18
|
+
- **Surface gap.** A **2px gap** in the surface color separates touching marks — every
|
|
19
|
+
segment of a stacked bar, and every adjacent (touching) bar, the same width. Keep it
|
|
20
|
+
one consistent width across a stack; neighbors one step apart read distinct because of
|
|
21
|
+
the gap, not a stroke drawn around them.
|
|
22
|
+
- **Surface ring.** Dots and end-markers carry a **2px ring in the surface color**,
|
|
23
|
+
so they stay legible where they cross a line or overlap each other. The ring is part
|
|
24
|
+
of the mark's hover/hit target, not just spacing — see `interaction.md` (small dots
|
|
25
|
+
are easy to under-size for hover).
|
|
26
|
+
|
|
27
|
+
Never draw a border around a mark to separate it. The gap and the ring are the
|
|
28
|
+
mechanism; a stroke adds data-weight ink that isn't data.
|
|
29
|
+
|
|
30
|
+
## Labels & legend
|
|
31
|
+
|
|
32
|
+
A **legend is always present for two or more series** — the dependable identity
|
|
33
|
+
channel; never make the reader rely on color-matching alone. Direct labels then ride
|
|
34
|
+
the marks to *supplement* it. **A single series needs no legend box**: there is only
|
|
35
|
+
one color, so the chart's title or subtitle already says what is plotted. A box with
|
|
36
|
+
one swatch restates the title and costs space.
|
|
37
|
+
|
|
38
|
+
- **Label selectively — never a number on every point.** A value beside every dot or
|
|
39
|
+
segment is chaos and goes unread. Label the endpoint, the extreme, or the one series
|
|
40
|
+
the story is about; let the axis, the legend, and the tooltip/table carry the rest.
|
|
41
|
+
Direct labels work *because* they are sparing — flood the chart and they stop working.
|
|
42
|
+
- **Direct labels before gridlines; gridlines before a second axis.**
|
|
43
|
+
- **A label that won't fit doesn't get clipped — measure first.** Only place a label
|
|
44
|
+
*inside* a bar or stacked segment when the rendered text fits with comfortable
|
|
45
|
+
padding on both sides. If it doesn't fit: for a whole bar/column, move the label
|
|
46
|
+
outside the bar end (or to the tooltip if there's no room outside either); for an
|
|
47
|
+
*interior* stacked segment (which has no free end),
|
|
48
|
+
skip the inline label and let the legend + tooltip carry it. Either way the value
|
|
49
|
+
stays in the table view, so nothing is gated. Never use `overflow: hidden` on the
|
|
50
|
+
segment to "solve" it — that crops the first/last characters and is worse than no
|
|
51
|
+
label. Text never overflows or is clipped by its own mark.
|
|
52
|
+
- Bars → value at the tip. Columns → value on the cap. Lines → value at the end.
|
|
53
|
+
- Y-axis ticks: round to clean numbers (0 / 1,000 / 2,000), thousands-comma'd; they
|
|
54
|
+
carry the values you didn't directly label, so keep them unless every value is labeled.
|
|
55
|
+
- **Text never wears the data color.** Marks — bars, lines, dots, area fills — carry
|
|
56
|
+
the series color; labels, values, legends, and axis text use **text tokens**
|
|
57
|
+
(primary / secondary / muted). A light categorical hue (yellow, aqua) is illegible
|
|
58
|
+
as text on the surface. Identity comes from the colored mark *beside* the text — a
|
|
59
|
+
dot, a short line-key, a swatch — never from coloring the text itself. A label set
|
|
60
|
+
*inside* a colored fill (a stacked segment, a map tile) is the one exception: pick
|
|
61
|
+
white or ink by the fill's luminance so it always clears contrast.
|
|
62
|
+
- **When end-labels collide, don't stack them.** Direct end-labels work when series
|
|
63
|
+
separate at the right edge. When lines converge, nudging labels apart vertically
|
|
64
|
+
detaches them from their lines and reads as noise — instead use **leader lines**
|
|
65
|
+
(a thin connector from label to line-end), facet into **small multiples**, or fall
|
|
66
|
+
back to the legend + tooltip. Past ~4 converging series, small multiples is usually right.
|
|
67
|
+
|
|
68
|
+
## Figures — when the form is a number
|
|
69
|
+
|
|
70
|
+
- **Stat tile** contract: `label` (sentence case, no trailing colon) · `value` (Sans
|
|
71
|
+
semibold, auto-compact: 1,284 / 12.9K / $4.2M) · `delta` (optional; signed,
|
|
72
|
+
vs a named period; color = direction × whether up is good) · `trend` (optional;
|
|
73
|
+
12-point sparkline in the de-emphasis hue, current period in the accent).
|
|
74
|
+
- **Meter:** the fill carries severity (accent → warning → danger); the unfilled
|
|
75
|
+
track is a **lighter step of the same ramp** (blue-on-blue, etc.) so state reads
|
|
76
|
+
across the whole bar.
|
|
77
|
+
- **Hero figure.** The single number a dashboard leads with, ≥48px, in the same
|
|
78
|
+
sans as everything else (never a display or serif face — it reads as off-brand
|
|
79
|
+
decoration). Exactly one per view.
|
|
80
|
+
- **Proportional figures for big numbers; tabular only in columns.** A large
|
|
81
|
+
standalone value (hero figure, stat-tile value) uses the font's default
|
|
82
|
+
proportional figures — `tabular-nums` gives every digit the width of a `0`, so a
|
|
83
|
+
number like `121` looks loose at display sizes. Reserve
|
|
84
|
+
`font-variant-numeric: tabular-nums` for columns of numbers that must align
|
|
85
|
+
vertically (table rows, axis ticks).
|
|
86
|
+
|
|
87
|
+
## Texture — the backup channel (opt-in)
|
|
88
|
+
|
|
89
|
+
Where hue fails — full-severity CVD, grayscale print, `forced-colors` — texture
|
|
90
|
+
carries identity. One directional hand-drawn fill, used at **45° and its 135° mirror
|
|
91
|
+
only** (never horizontal/vertical — those read as gridlines/bars). Inked tone-on-tone
|
|
92
|
+
(a step from the fill's own ramp), equal loudness across slots. On value scales the
|
|
93
|
+
texture is *ordered* (rotation steps with magnitude; arm angle carries the diverging
|
|
94
|
+
sign) so it never misstates the value. Triggered by an accessibility setting, print,
|
|
95
|
+
or `forced-colors` — never on by default. (See `palette.md`.)
|
|
96
|
+
|
|
97
|
+
---
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Reference palette
|
|
2
|
+
|
|
3
|
+
This is the **reference instance** of the data-viz method: every parameter the
|
|
4
|
+
method needs, filled in with a validated default palette. The rest of the skill
|
|
5
|
+
is system-agnostic — **to target your brand, substitute this file's values** and
|
|
6
|
+
re-run the validator. Nothing else changes.
|
|
7
|
+
|
|
8
|
+
## How to use these values
|
|
9
|
+
|
|
10
|
+
Everything below is plain hex. In an HTML chart, **define the slots you use as
|
|
11
|
+
CSS custom properties in a local `<style>` block** at the top of the file, then
|
|
12
|
+
reference them by role throughout — so the light/dark values swap in one place,
|
|
13
|
+
and the chart body is written against roles rather than raw hex:
|
|
14
|
+
|
|
15
|
+
```css
|
|
16
|
+
.viz-root {
|
|
17
|
+
--surface-1: #fcfcfb; /* chart surface */
|
|
18
|
+
--text-primary: #0b0b0b;
|
|
19
|
+
--text-secondary: #52514e;
|
|
20
|
+
--series-1: #2a78d6; /* categorical slot 1 */
|
|
21
|
+
/* …only the roles this chart uses */
|
|
22
|
+
}
|
|
23
|
+
@media (prefers-color-scheme: dark) {
|
|
24
|
+
.viz-root {
|
|
25
|
+
--surface-1: #1a1a19;
|
|
26
|
+
--text-primary: #ffffff;
|
|
27
|
+
--text-secondary: #c3c2b7;
|
|
28
|
+
--series-1: #3987e5;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Categorical palette
|
|
34
|
+
|
|
35
|
+
Both modes are selected. The dark column is the same eight hues stepped for the
|
|
36
|
+
dark surface, not a separate palette:
|
|
37
|
+
|
|
38
|
+
| Slot | Hue | Light | Dark |
|
|
39
|
+
|------|-----|-------|------|
|
|
40
|
+
| 1 | blue | `#2a78d6` | `#3987e5` |
|
|
41
|
+
| 2 | aqua | `#1baf7a` | `#199e70` |
|
|
42
|
+
| 3 | yellow | `#eda100` | `#c98500` |
|
|
43
|
+
| 4 | green | `#008300` | `#008300` |
|
|
44
|
+
| 5 | violet | `#4a3aa7` | `#9085e9` |
|
|
45
|
+
| 6 | red | `#e34948` | `#e66767` |
|
|
46
|
+
| 7 | magenta | `#e87ba4` | `#d55181` |
|
|
47
|
+
| 8 | orange | `#eb6834` | `#d95926` |
|
|
48
|
+
|
|
49
|
+
Light-mode worst adjacent CVD ΔE is 24.2 — well clear of the ≥12 target. Three
|
|
50
|
+
light-mode slots (aqua, yellow, magenta) sit below 3:1 contrast on the light
|
|
51
|
+
surface: the **relief rule** applies (ship visible direct labels or the table
|
|
52
|
+
view). The dark steps were chosen for the dark band (OKLCH L ≈ 0.48–0.67, ≥ 3:1
|
|
53
|
+
on the dark surface) and validated as a set — worst adjacent ΔE 10.3, the floor
|
|
54
|
+
band, so four-plus series lean on direct labels or texture in dark mode too.
|
|
55
|
+
|
|
56
|
+
The slot **ordering** is the CVD-safety mechanism, not cosmetic — it was derived
|
|
57
|
+
by enumerating orderings and picking the one that maximizes the minimum adjacent
|
|
58
|
+
ΔE (see `color-formula.md` § Themes). When you swap in your brand's hues, do the
|
|
59
|
+
same: run the validator on candidate orderings and keep the best.
|
|
60
|
+
|
|
61
|
+
## Sequential hue
|
|
62
|
+
|
|
63
|
+
Default single hue: **blue**, light→dark. When two sequential contexts appear at
|
|
64
|
+
once, the second takes the next categorical slot's hue (aqua), each as its own
|
|
65
|
+
one-hue ramp.
|
|
66
|
+
|
|
67
|
+
| step | hex | step | hex | step | hex | step | hex |
|
|
68
|
+
|---|---|---|---|---|---|---|---|
|
|
69
|
+
| 100 | `#cde2fb` | 250 | `#86b6ef` | 400 | `#3987e5` | 550 | `#1c5cab` |
|
|
70
|
+
| 150 | `#b7d3f6` | 300 | `#6da7ec` | 450 | `#2a78d6` | 600 | `#184f95` |
|
|
71
|
+
| 200 | `#9ec5f4` | 350 | `#5598e7` | 500 | `#256abf` | 650 | `#104281` |
|
|
72
|
+
| | | | | | | 700 | `#0d366b` |
|
|
73
|
+
|
|
74
|
+
The full 100→700 range is for **sequential** encoding (continuous magnitude —
|
|
75
|
+
heatmaps, choropleths) where the lightest step means "near zero" and is allowed
|
|
76
|
+
to recede toward the surface. For an **ordinal** ramp (discrete ordered marks —
|
|
77
|
+
funnel stages, tiers — validated with `--ordinal`), the step nearest the surface
|
|
78
|
+
must still clear 2:1: on light, start no lighter than **step 250** (`#86b6ef`,
|
|
79
|
+
2.06:1); on dark, go no darker than **step 600** (`#184f95`, 2.15:1).
|
|
80
|
+
|
|
81
|
+
## Diverging pair
|
|
82
|
+
|
|
83
|
+
**blue ↔ red** — warm/cool poles that read as opposite. Neutral midpoint is gray
|
|
84
|
+
(light `#f0efec`, dark `#383835`). Equal step count per arm. (blue↔aqua was
|
|
85
|
+
rejected — both cool, the midpoint doesn't read as "nothing".)
|
|
86
|
+
|
|
87
|
+
## Status palette (fixed — never themed)
|
|
88
|
+
|
|
89
|
+
| role | hex | light-surface contrast | dark-surface contrast |
|
|
90
|
+
|---|---|---|---|
|
|
91
|
+
| good | `#0ca30c` | 3.27 | 5.19 |
|
|
92
|
+
| warning | `#fab219` | 1.79 | 9.49 |
|
|
93
|
+
| serious | `#ec835a` | 2.57 | 6.60 |
|
|
94
|
+
| critical | `#d03b3b` | 4.68 | 3.62 |
|
|
95
|
+
|
|
96
|
+
Dark: same four steps — all clear 3:1 on the dark surface (`#1a1a19`) and remain
|
|
97
|
+
distinct from the dark categorical slots. On the light surface, warning and
|
|
98
|
+
serious are sub-3:1 by design; the **icon + label** pairing is the mitigation, so
|
|
99
|
+
a status color never carries meaning alone. These steps are deliberately distinct
|
|
100
|
+
from the categorical slots so a status color never impersonates a series.
|
|
101
|
+
|
|
102
|
+
## Texture fill (the accessibility channel)
|
|
103
|
+
|
|
104
|
+
One hand-drawn **"Lines"** fill, used at **45° and its 135° mirror only**. Inked
|
|
105
|
+
tone-on-tone (a darker step of the fill's own ramp). On value scales it is
|
|
106
|
+
*ordered* (rotation steps with magnitude; arm angle carries the diverging sign).
|
|
107
|
+
Triggered by the accessibility setting, print, or `forced-colors` — never
|
|
108
|
+
decorative, never on by default.
|
|
109
|
+
|
|
110
|
+
## Surfaces (for the validator)
|
|
111
|
+
|
|
112
|
+
- Light chart surface: `#fcfcfb`
|
|
113
|
+
- Dark chart surface: `#1a1a19`
|
|
114
|
+
|
|
115
|
+
These are the validator's built-in defaults. **When you swap in your own
|
|
116
|
+
palette, re-run against your own surfaces:**
|
|
117
|
+
`--surface <your-light> --mode light` and `--surface <your-dark> --mode dark` —
|
|
118
|
+
contrast and band results are only meaningful against the surface the chart
|
|
119
|
+
actually renders on.
|
|
120
|
+
|
|
121
|
+
## Chart chrome & ink
|
|
122
|
+
|
|
123
|
+
| Role | Light | Dark |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| Chart surface | `#fcfcfb` | `#1a1a19` |
|
|
126
|
+
| Page plane | `#f9f9f7` | `#0d0d0d` |
|
|
127
|
+
| Primary ink | `#0b0b0b` | `#ffffff` |
|
|
128
|
+
| Secondary ink | `#52514e` | `#c3c2b7` |
|
|
129
|
+
| Muted (axis/labels) | `#898781` | `#898781` |
|
|
130
|
+
| Gridline (hairline) | `#e1e0d9` | `#2c2c2a` |
|
|
131
|
+
| Baseline / axis | `#c3c2b7` | `#383835` |
|
|
132
|
+
| Delta ↑ good (success text) | `#006300` | `#0ca30c` |
|
|
133
|
+
| Border (hairline ring) | `rgba(11,11,11,0.10)` | `rgba(255,255,255,0.10)` |
|
|
134
|
+
|
|
135
|
+
## Filter controls
|
|
136
|
+
|
|
137
|
+
Filters are standard UI, not chart components — the chart layer only adds the
|
|
138
|
+
composition rules in `interaction.md`. A date-range control is a list of preset
|
|
139
|
+
rows (today, last 7/30/90 days, month-to-date) with selection marked by a 16px
|
|
140
|
+
bold check, hover as a ghost wash, and custom range behind a hairline in the
|
|
141
|
+
footer. Dimension filters are a standard combobox.
|
|
142
|
+
|
|
143
|
+
## Typeface & figures
|
|
144
|
+
|
|
145
|
+
Everything — including the hero figure — stays in the system sans: `system-ui,
|
|
146
|
+
-apple-system, "Segoe UI", sans-serif`. No display or serif face anywhere. Large
|
|
147
|
+
standalone numbers (hero figure, stat-tile values) use the default proportional
|
|
148
|
+
figures; reserve `font-variant-numeric: tabular-nums` for columns that must align
|
|
149
|
+
vertically (table rows, axis ticks). Substitute your brand's UI sans here.
|