SVG suggestions #4

Closed
opened 2026-05-19 20:42:08 +02:00 by g4borg · 1 comment
Owner

Improvements for the markdown-embedded-svg skill

Notes captured while reviewing concrete SVG output from agents in v1-superset-analysis/. The current skill focuses on renderer compatibility (which Markdown renderers sanitize <svg>, where to put files, GitHub strips, Gitea allows little, Obsidian allows most). That's the right thing to cover and it stays correct. But the skill says nothing about legibility, and the output suffers for it. Below is what to add.

Concrete failure modes observed

From v1-superset-analysis/images/action-density-map.svg (the matrix of 32 @actions by ViewSet × category):

  1. Text overflows its container. Each cell is 80×40 px and contains strings like "fetch, fetch_fixed, copy, copy_mapping" or "columns, map_columns, columns_detail, default_mapping". At 11px font that's ~280px of text in an 80px box. The text bleeds into adjacent cells; the chart becomes a soup of overlapping labels.

  2. White text on light-mid backgrounds. The CSS class .lbl { fill: #fff } is applied uniformly to text laid over fills #1f77b4 (dark blue — OK), #ff7f0e (mid orange — poor contrast), #2ca02c (mid green — marginal), #9467bd (mid purple — OK), #e377c2 (pink — bad). Same color choice for all backgrounds means the worst pairing is illegible.

  3. No background or halo behind text that sits on a colored field. When a label does land on a fill that doesn't match its text color (e.g. an empty #f4f4f4 cell with text overflowing from a neighbor), there's nothing to rescue it.

  4. Rotated column headers at -30° overlap downward into the matrix without measuring how much vertical margin they actually need.

  5. No empty-state markers. Empty cells are flat light gray — fine — but every other cell is busy, which makes empties feel like missing data rather than deliberate "no actions in this category."

What the skill should add — concrete rules

Rule 1 — Estimate max-text-width before sizing a container

For any text that lives inside a bounded shape (cell, box, button), the SVG author must 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, hover, sidebar).
    • Wrap to multiple <tspan> lines when truncation loses meaning.
  • Never assume CSS overflow rules — SVG doesn't clip text by default.

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-on-light" categories: #1f77b4, #d62728, #9467bd, #8c564b — use white text.
  • "Light-on-dark" categories: #aec7e8, #ffbb78, #98df8a — use dark text.

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 renderer's display width (typically 600-900px 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.

Suggested addition to the skill body

Add a section titled "Layout and legibility" between the renderer-compatibility section and the file-placement section. Bullet the seven rules above. Add one good and one bad worked example side by side — the bad one matching the action-density failure mode (text in narrow cells), the good one matching the sparse-cells-plus-legend pattern.

If the skill bundles assets, include a palettes.svg or palettes.md that names two or three pre-baked palettes (categorical-6-dark, categorical-6-light, sequential, diverging) with declared text-color pairings so an agent doesn't have to derive contrast from scratch each time.

# Improvements for the `markdown-embedded-svg` skill Notes captured while reviewing concrete SVG output from agents in `v1-superset-analysis/`. The current skill focuses on **renderer compatibility** (which Markdown renderers sanitize `<svg>`, where to put files, GitHub strips, Gitea allows little, Obsidian allows most). That's the right thing to cover and it stays correct. But the skill says nothing about **legibility**, and the output suffers for it. Below is what to add. ## Concrete failure modes observed From `v1-superset-analysis/images/action-density-map.svg` (the matrix of 32 `@action`s by ViewSet × category): 1. **Text overflows its container.** Each cell is 80×40 px and contains strings like `"fetch, fetch_fixed, copy, copy_mapping"` or `"columns, map_columns, columns_detail, default_mapping"`. At 11px font that's ~280px of text in an 80px box. The text bleeds into adjacent cells; the chart becomes a soup of overlapping labels. 2. **White text on light-mid backgrounds.** The CSS class `.lbl { fill: #fff }` is applied uniformly to text laid over fills `#1f77b4` (dark blue — OK), `#ff7f0e` (mid orange — poor contrast), `#2ca02c` (mid green — marginal), `#9467bd` (mid purple — OK), `#e377c2` (pink — bad). Same color choice for all backgrounds means the worst pairing is illegible. 3. **No background or halo behind text that sits on a colored field.** When a label *does* land on a fill that doesn't match its text color (e.g. an empty `#f4f4f4` cell with text overflowing from a neighbor), there's nothing to rescue it. 4. **Rotated column headers** at `-30°` overlap downward into the matrix without measuring how much vertical margin they actually need. 5. **No empty-state markers.** Empty cells are flat light gray — fine — but every other cell is busy, which makes empties feel like missing data rather than deliberate "no actions in this category." ## What the skill should add — concrete rules ### Rule 1 — Estimate max-text-width before sizing a container For any text that lives **inside** a bounded shape (cell, box, button), the SVG author must 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, hover, sidebar). - **Wrap to multiple `<tspan>` lines** when truncation loses meaning. - Never assume CSS overflow rules — SVG doesn't clip text by default. 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-on-light" categories: `#1f77b4`, `#d62728`, `#9467bd`, `#8c564b` — use **white** text. - "Light-on-dark" categories: `#aec7e8`, `#ffbb78`, `#98df8a` — use **dark** text. 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 renderer's display width (typically 600-900px 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. ## Suggested addition to the skill body Add a section titled **"Layout and legibility"** between the renderer-compatibility section and the file-placement section. Bullet the seven rules above. Add one good and one bad worked example side by side — the bad one matching the action-density failure mode (text in narrow cells), the good one matching the sparse-cells-plus-legend pattern. If the skill bundles assets, include a `palettes.svg` or `palettes.md` that names two or three pre-baked palettes (categorical-6-dark, categorical-6-light, sequential, diverging) with declared text-color pairings so an agent doesn't have to derive contrast from scratch each time.
Author
Owner

fixed

fixed
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dirigence/reliquary#4