61af13521c
Extract inline SVG rules to references/inline-svg.md for progressive disclosure. Add references/layout-legibility.md with 7 rules for readable SVG output (text sizing, contrast, margins, chart patterns, palettes) based on concrete failure modes from issue #4. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
84 lines
4.8 KiB
Markdown
84 lines
4.8 KiB
Markdown
# Layout and Legibility Rules for SVG Diagrams
|
||
|
||
These rules prevent the most common legibility failures in agent-generated SVG: text overflow, poor contrast, and missing margins. They apply to both external `.svg` files and inline SVG.
|
||
|
||
## Rule 1 — Estimate max-text-width before sizing a container
|
||
|
||
For any text that lives **inside** a bounded shape (cell, box, button), estimate the rendered width of the longest string:
|
||
|
||
- Rough bound: `text_width_px ≈ chars × font_size × 0.6` for proportional fonts at common weights.
|
||
- If the container is narrower than the longest text, choose one of:
|
||
- **Widen the container** (preferred when there's space).
|
||
- **Truncate to a short label** (e.g. a count, a code, or an abbreviation) and put the full text outside (legend, footnote table, sidebar).
|
||
- **Wrap to multiple `<tspan>` lines** when truncation loses meaning.
|
||
- SVG doesn't clip text by default — never assume CSS overflow rules apply.
|
||
|
||
Default to **counts + short labels inside the chart, full names in a legend below**. For dense categorical charts this is almost always the right answer.
|
||
|
||
## Rule 2 — Text-over-background needs a contrast contract
|
||
|
||
If a `<text>` element renders on top of an arbitrary background, declare which class of background it's designed for:
|
||
|
||
- **Light-bg text** (`fill: #1a1a1a` or similar): legible on whites, light grays, light pastels.
|
||
- **Dark-bg text** (`fill: #ffffff` or similar): legible on saturated mid-to-dark colors only.
|
||
- **High-contrast halo** (`paint-order="stroke fill"` + `stroke-width="3"` + `stroke="#fff"`): legible on anything; use when the background is unpredictable or varies.
|
||
|
||
Never apply one fill class across multiple background colors without verifying contrast for each. WCAG large-text minimum is **3:1**; for text-in-data-viz aim for **4.5:1** to leave margin.
|
||
|
||
When colors are picked by category, group them into a "dark fills" palette (use white text) and a "light fills" palette (use dark text). Don't mix.
|
||
|
||
## Rule 3 — Backgrounds for text on uncertain surfaces
|
||
|
||
When text is placed near or over a colored region whose color depends on data, give the text its own background:
|
||
|
||
- A `<rect>` behind the text, sized to its bounding box (compute as `chars × font_size × 0.6 + padding`), with `fill="#fff"` and a small `rx` for softness.
|
||
- Or a halo via `paint-order="stroke fill"` with a white stroke.
|
||
|
||
For category legends, axis labels, and chart titles — anything that sits in the chart's "frame" rather than inside a data shape — always give them a clean background or place them in the margin.
|
||
|
||
## Rule 4 — Reserve margin for rotated labels
|
||
|
||
Rotated text (typically column headers at `-30°` to `-45°`) extends beyond its anchor in both x and y. Rule of thumb: an N-character rotated header at θ degrees and font-size F needs:
|
||
- horizontal reach: `cos(θ) × chars × F × 0.6`
|
||
- vertical reach: `sin(θ) × chars × F × 0.6`
|
||
|
||
Plus the anchor offset. Always add 20% padding. If the rotated headers extend into the chart body, increase the chart's top margin until they don't.
|
||
|
||
## Rule 5 — Default chart pattern: sparse cells, full legend below
|
||
|
||
For categorical density / heatmap / matrix charts:
|
||
|
||
```
|
||
[ Title ]
|
||
[ Rotated category headers (with margin) ]
|
||
[ Row label │ count │ count │ . │ count │ count │ . ] (cells: just the number, color-coded)
|
||
[ Row label │ . │ count │ count │ . │ count │ . ]
|
||
[ ... ]
|
||
[ ]
|
||
[ Legend / details table: ]
|
||
[ Row × Category → action_a, action_b, action_c ]
|
||
[ Row × Category → action_d ]
|
||
[ ... ]
|
||
```
|
||
|
||
Counts inside cells; names in the table below. This pattern composes well across renderers and prevents the overflow trap entirely.
|
||
|
||
## Rule 6 — Stick to a small palette with declared roles
|
||
|
||
Categorical palettes should be picked once, with their **role** declared:
|
||
|
||
- "Dark fills" (use **white** text): `#1f77b4`, `#d62728`, `#9467bd`, `#8c564b`
|
||
- "Light fills" (use **dark** text): `#aec7e8`, `#ffbb78`, `#98df8a`
|
||
|
||
Avoid mid-tones (`#ff7f0e`, `#2ca02c`, `#e377c2`) unless the text color is matched per-color. If the chart needs more than 6 categories, that's usually a sign the categorisation is too fine — group first.
|
||
|
||
## Rule 7 — Sanity check the rendered output
|
||
|
||
Before saving, mentally render at the target display width (typically 600–750px effective in Markdown), and ask:
|
||
- Can I read every label?
|
||
- Does any text overlap another shape?
|
||
- Is there a category whose color makes its text invisible?
|
||
- Are empty cells distinguishable from filled ones at a glance?
|
||
|
||
If any answer is "no", iterate before checking in.
|