Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion agents/anyplot/prompts/adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ These rules replace the catalogue's own rules where they differ:
- **Keep the plot.** Same plot type, same variant, same encodings and the same style. Change only what the user's data, the hints, the feedback and the change request need. Add no encodings, reference lines, highlights or callouts that nobody asked for.
- **Callouts and statistics.** A label or callout that states a number must compute it from `df` in an f-string (`f"r = {df[x].corr(df[y]):.2f}"`, written with the column variables of the code). A callout target comes from a rule over `df` (the maximum, the latest row), and a conditional claim is drawn only when `df` meets it. Remove a callout that cannot be derived.
- **Density.** Adapt marker sizes, line widths and alpha to the user's row count (see the VQ-03 table below).
- **Per-mark labels and decorations.** "The same style" covers elements whose count stays fixed (a title, a legend, one callout), not elements the catalogue draws once per bar or point of its few example marks: value labels, dots on bar ends, shadows. Their count grows with the user's data, so keep them only while each label fits the space of its mark and the plot stays legible, which rarely holds past about 25 marks; with more marks, drop them, or label only the extremes (the largest and the smallest) when the plot needs numbers. The server measures this (a `VQ-03` gate line counts the labels, the decorations and the pixels each label has).
- **Per-mark labels and decorations.** "The same style" covers elements whose count stays fixed (a title, a legend, one callout), not elements the catalogue draws once per bar or point of its few example marks: value labels, dots on bar ends, shadows. Apply the style guide's "Per-mark Labels and Decorations" rule to the user's number of marks, not the catalogue's. The server measures this (a `VQ-03` gate line counts the labels, the decorations and the pixels each label has).
- **Numbers.** Write every number in the plot text (tick labels, value labels, annotations, a number in the title) with the plot language's decimal and thousands marks and its words for large numbers, from the table "Number formats by plot language" below: `de` writes `5,30` and `6,5 Mio.`, `en` writes `5.30` and `6.5 M`. Python's `locale` is not available: format with an f-string, which writes `,` for thousands and `.` for decimals, then swap both marks through a placeholder: `f"{v:,.2f}".replace(",", "_").replace(".", DECIMAL).replace("_", THOUSANDS)`, with `DECIMAL` and `THOUSANDS` set once to the table's marks (`de`: `","` and `"."`; `fr`: `","` and `" "`; `en` keeps the f-string as it is), through a `matplotlib.ticker.FuncFormatter` for the axes and the same function for value labels (`ax.bar_label(container, labels=[...])`, never `fmt="%.2f"`, which always writes a point). Years, dates, identifiers and the cells of the user's data stay as they are. A plot language the table does not list keeps the catalogue's formats. The server checks the decimal mark of every numeric tick label and annotation (a `VQ-06` gate line).
- **Dates.** Use `matplotlib.dates.AutoDateLocator` with `ConciseDateFormatter` instead of a fixed locator, unless the change request asks for a specific one.
- **Safe code.** Use only the imports the code already has, plus, when needed, `numpy`, `pandas`, the plotting library, scipy's computing modules (`scipy.stats`, `scipy.interpolate`, `scipy.signal`, `scipy.optimize`, `scipy.fft`, `scipy.linalg`, ...) and these standard modules: `math`, `cmath`, `statistics`, `decimal`, `fractions`, `numbers`, `datetime`, `calendar`, `zoneinfo`, `collections`, `itertools`, `functools`, `operator`, `bisect`, `heapq`, `typing`, `warnings`, `textwrap`, `colorsys` and `re`. Never import `io`, `sys`, `pathlib`, `random`, `locale` or `string`. No file or network access, no `os` beyond the THEME line, no `.format(...)` (use f-strings), no `eval`, `exec`, `getattr`, `operator.attrgetter` or other reflection, no names with double underscores or private names of a module (`module._name`), no `class` statements, and no string literal longer than 200 characters.
Expand Down
10 changes: 10 additions & 0 deletions changelog.d/style-guide-label-density.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
### Changed

- **Catalogue rubric: per-mark labels must fit their marks.** The style
guide gains a "Per-mark Labels and Decorations" rule (value labels, dots on
bar ends and shadows drawn once per bar or point stay only while each fits
its mark, which rarely holds past about 25 marks; otherwise drop them or
label the extremes), and the VQ-03 rubric and the review's proportional
sizing check deduct crowded per-mark labels in proportion. The agent's
adapter now points at this one shared rule instead of carrying its own
copy (#12189).
2 changes: 1 addition & 1 deletion docs/concepts/agent-network.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ Host gates: R1 (exit code and outputs present) and R2 (PNG hardening: `lstat` wi

**Scripts and fonts.** Plot text can be in the scripts most users' data and languages bring: the renderer image installs Noto Sans, Noto Sans CJK and the Noto Sans faces of the scripts DejaVu Sans lacks (37 files, 46.8 MB, Regular and Bold where the family has both), and the harness appends them to every font lookup, so a Chinese title or a Devanagari tick label draws with the right glyphs while the code's own first font stays in charge of Latin text. Right-to-left scripts work: the pinned matplotlib lays text out through libraqm, so Arabic and Hebrew render shaped and in reading order, within one label aligned by the code's `ha`. Three limits remain, and G10 reports the first and the last: a `$…$` math string uses matplotlib's math fonts without fallback and draws `¤` for every character they lack, so non-Latin words belong outside math text; Han characters take the Japanese regional forms of the CJK face until a caller passes the plot's language (`text.language`, a follow-up with the language chip); and a character outside the installed families draws as a Last Resort placeholder box. Most pictographs are not in that last group, because Noto Sans Symbols2 or DejaVu Sans draws them as monochrome outlines (📈, 📊, ⚠, 😀), but 🔥, ✅, the flag letters, Tibetan and Mongolian have no glyph in any installed font. G10 counts those characters deterministically from matplotlib's own reports, names up to three of them with their code points (`U+1F525 🔥 fire`), tells the repair that a user's column name stays as it is, and keeps the line in the result's residual lines even when the reviewer passed the render, because a measured missing glyph is not a judgement.

**Label density and number formats.** Two findings of the live tests of 2026-10-14/15 became measured gates. The catalogue draws some elements once per mark of its few example marks: bar-grouped puts a value label, a dot and a drop shadow on each of its 12 bars, and the adapter kept them on the 52 bars of 26 regions times two series, because "keep the same style" and the reviewer's house-style exemption covered them. The harness's `density_report` (probe field `density`, per axes) counts the bars and points, the texts at a data coordinate whose anchor sits at a bar's end, inside a bar's segment (`bar_label(label_type="center")`) or on a point (`ax.annotate`, `ax.bar_label`, `ax.text`; heatmap cell text and callouts in axes coordinates are not value labels, and neither is a text matplotlib clips away in a zoomed view), which of them are numbers, the dots on bar ends (scatter marks or `plot(..., "o")` markers) and the shadowed bars, and measures the median bar label width against the space between neighbouring labelled bars (unlabelled bars between them, such as an offset shadow series, do not shrink it). Bar ends come from the bars' data coordinates, so an inverted axis works. G11 writes a VQ-03 line (owner, 2026-10-15: the thresholds stay) when more than `MAX_VALUE_LABELS` (25) bar positions carry a label (a stacked bar's segment labels count once), when more than 25 numeric labels sit on points (named points, the point of scatter-annotated, never count), from six labels on when a bar label is wider than 90 % of the space per bar, or when more than `MAX_DECORATIONS` (25) dots or shadows are drawn. The adapter prompt and the reviewer's and scorer's house-style exemption now say that an element whose count grows with the data is not house style. Second, number formats followed English whatever the plot language ("6.0 M" and "5.30" in German plots), and `locale` is a banned import. `language.NUMBER_FORMATS` is the server's table of the decimal mark, the thousands mark and the words for large numbers per language of `ENGLISH_NAMES` (`de`: `,`, `.`, Tsd./Mio./Mrd.); the adapter's static instruction carries it with the rule to format through a `FuncFormatter` or an f-string whose marks are swapped through a placeholder (`.replace(",", "_").replace(".", DECIMAL).replace("_", THOUSANDS)`), exact for the space-grouped languages too. A region such as `de-CH` with its apostrophe (7'184'003) is a follow-up of its own setting (owner, 2026-10-15). G12 reads the probe's `labels` field (the drawn texts by kind, from block F) and its `tick_axes` field (`harness.tick_axes`: whether each axis's tick labels are numbers, dates, categories or labels the code set itself), and writes a VQ-06 line when an annotation, or a tick label of a numeric axis, uses the other decimal mark, mathtext read as drawn: a single point followed by other than three digits in a decimal-comma language (`5.30`, `6.0 M`, `5.30 €`), a point after a whole part of 0 (`0.125`), a point after a comma, or commas grouping thousands; dates (a zero-padded `14.10` in an annotation reads as a day and month), years, version strings, titles, legend entries and tick labels the code set itself never count, and no label text reaches the line.
**Label density and number formats.** Two findings of the live tests of 2026-10-14/15 became measured gates. The catalogue draws some elements once per mark of its few example marks: bar-grouped puts a value label, a dot and a drop shadow on each of its 12 bars, and the adapter kept them on the 52 bars of 26 regions times two series, because "keep the same style" and the reviewer's house-style exemption covered them. The harness's `density_report` (probe field `density`, per axes) counts the bars and points, the texts at a data coordinate whose anchor sits at a bar's end, inside a bar's segment (`bar_label(label_type="center")`) or on a point (`ax.annotate`, `ax.bar_label`, `ax.text`; heatmap cell text and callouts in axes coordinates are not value labels, and neither is a text matplotlib clips away in a zoomed view), which of them are numbers, the dots on bar ends (scatter marks or `plot(..., "o")` markers) and the shadowed bars, and measures the median bar label width against the space between neighbouring labelled bars (unlabelled bars between them, such as an offset shadow series, do not shrink it). Bar ends come from the bars' data coordinates, so an inverted axis works. G11 writes a VQ-03 line (owner, 2026-10-15: the thresholds stay) when more than `MAX_VALUE_LABELS` (25) bar positions carry a label (a stacked bar's segment labels count once), when more than 25 numeric labels sit on points (named points, the point of scatter-annotated, never count), from six labels on when a bar label is wider than 90 % of the space per bar, or when more than `MAX_DECORATIONS` (25) dots or shadows are drawn. The adapter prompt and the reviewer's and scorer's house-style exemption now say that an element whose count grows with the data is not house style. The rule itself has one source shared with the catalogue pipeline: the "Per-mark Labels and Decorations" section of `prompts/default-style-guide.md` and the VQ-03 paragraph of `prompts/quality-criteria.md`, which the adapter reads verbatim; `adapter.md` only points at it. Second, number formats followed English whatever the plot language ("6.0 M" and "5.30" in German plots), and `locale` is a banned import. `language.NUMBER_FORMATS` is the server's table of the decimal mark, the thousands mark and the words for large numbers per language of `ENGLISH_NAMES` (`de`: `,`, `.`, Tsd./Mio./Mrd.); the adapter's static instruction carries it with the rule to format through a `FuncFormatter` or an f-string whose marks are swapped through a placeholder (`.replace(",", "_").replace(".", DECIMAL).replace("_", THOUSANDS)`), exact for the space-grouped languages too. A region such as `de-CH` with its apostrophe (7'184'003) is a follow-up of its own setting (owner, 2026-10-15). G12 reads the probe's `labels` field (the drawn texts by kind, from block F) and its `tick_axes` field (`harness.tick_axes`: whether each axis's tick labels are numbers, dates, categories or labels the code set itself), and writes a VQ-06 line when an annotation, or a tick label of a numeric axis, uses the other decimal mark, mathtext read as drawn: a single point followed by other than three digits in a decimal-comma language (`5.30`, `6.0 M`, `5.30 €`), a point after a whole part of 0 (`0.125`), a point after a comma, or commas grouping thousands; dates (a zero-padded `14.10` in an annotation reads as a day and month), years, version strings, titles, legend entries and tick labels the code set itself never count, and no label text reaches the line.

Later languages reuse the CI commands: `Rscript plot.R`; `julia --project=/opt/julia plot.jl` with a stacked `JULIA_DEPOT_PATH` and a pinned `JULIA_CPU_TARGET`; `node /opt/anyplot/js-render/render.mjs plot.js` with `window.ANYPLOT_DATA` injected; bokeh with `BOKEH_RESOURCES=inline`, `SE_OFFLINE=true` and a shipped chromedriver; plotly with local or disabled MathJax.

Expand Down
7 changes: 6 additions & 1 deletion prompts/default-style-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,10 @@ Marker and line sizes should adapt to how dense the data is — no fixed values:
- **Few series (1–3)** → thicker lines for prominence.
- **Many series (> 5)** → thinner lines so all stay visible.

### Per-mark Labels and Decorations

Some elements are drawn once per bar or point: value labels, dots on bar ends, shadows, a text next to each point. Unlike a title, a legend or a single callout, their count grows with the data, so a choice that suits 8 bars can bury 50. Keep them only while each label fits the space of its mark (a bar label no wider than its bar's slot) and the plot stays legible, which rarely holds past about 25 marks. With more marks, drop them, or label only the extremes (the largest and the smallest value) when the plot needs numbers. Text that is the encoding itself (heatmap cell values, a word cloud, a table) follows its spec instead.

### General Rules
- Elements should be **~2-3× larger** than the library's standard defaults.
- When in doubt, make it bigger.
Expand All @@ -292,7 +296,7 @@ Marker and line sizes should adapt to how dense the data is — no fixed values:

## Proportional Sizing

The review step checks these proportions visually from the source PNG. There are no hard pixel thresholds — violations cost points in the existing VQ-01 (Text Legibility), VQ-02 (No Overlap), VQ-05 (Layout & Canvas) categories rather than triggering a separate pass/fail. This keeps the contract holistic: a single visual problem (oversized short axis label, overlapping ticks) can reduce points in multiple categories.
The review step checks these proportions visually from the source PNG. There are no hard pixel thresholds — violations cost points in the existing VQ-01 (Text Legibility), VQ-02 (No Overlap), VQ-03 (Element Visibility), VQ-05 (Layout & Canvas) categories rather than triggering a separate pass/fail. This keeps the contract holistic: a single visual problem (oversized short axis label, overlapping ticks) can reduce points in multiple categories.

- **Title proportion:** Title comfortably takes ~50–70% of the plot width. The mandated `{spec-id} · {lang} · {lib} · anyplot.ai` title is ~67 chars long and naturally fills 70–85% at the style-guide default fontsize — this is **expected**, not a violation. Only flag if title overflows past ~90% of width OR fontsize is too generous for the content. **When the optional `{Descriptive Title} · ` prefix is used, or when `{spec-id}` is long (e.g. `network-bipartite-weighted`), the total title runs past ~67 chars and the default fontsize will overflow** — the generator must shrink the title fontsize via the linear formula `fontsize = round(default × 67 / len(title))` (capped at the library-specific floor). See `prompts/plot-generator.md` "Title fontsize must scale with title length" for the per-library defaults and floors.
- **Axis label proportionality:** Short labels ("Date", "Year") should not dominate the axis with oversized fontsizes. Long descriptive labels ("Fläche von Häusern in Quadratmetern") are fine as long as they don't overflow — the "no overflow" rule covers that case.
Expand All @@ -301,6 +305,7 @@ The review step checks these proportions visually from the source PNG. There are
- **No overlap:** Text never overlaps with other text or with data elements.
- **No overflow:** No text extends beyond axis bounds, plot bounds, or frame.
- **Marker / line density appropriateness:** Element sizes match the data-density heuristic above (sparse → prominent; dense → smaller + alpha).
- **Per-mark label density:** Value labels, dots on bar ends and shadows follow "Per-mark Labels and Decorations" above; labels that crowd their marks cost VQ-03, labels that overlap cost VQ-02.

---

Expand Down
2 changes: 2 additions & 0 deletions prompts/quality-criteria.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ Data elements must be visible and adapted to data density.

**Legend glyphs count as elements.** Size-legend circles, color swatches and line samples must be visible in **both** themes and must match the marks they explain (same fill, outline, alpha and shape). A glyph drawn in the page color, at near-zero alpha, or without the marks' fill is an invisible element: deduct VQ-03 in proportion, and also SC-04 when the encoding it explains becomes unreadable. Look at the legend in each render — do not assume it inherited the marks' styling. The weakness should name the likely fix (e.g. ggplot2 `guides(size = guide_legend(override.aes = list(fill = ..., alpha = ...)))`, or setting the legend marker color explicitly from the palette).

**Per-mark labels and decorations count as elements.** Value labels, dots on bar ends and shadows drawn once per bar or point must stay legible at the plot's number of marks (style guide, "Per-mark Labels and Decorations"). When they crowd the marks (a label wider than its bar's slot, rotated or shrunk to squeeze in, a band of text over many bars), deduct VQ-03 in proportion to how much of the plot they bury, and VQ-02 where they overlap. Labels that fit, or a few labelled extremes, are no deduction.

### VQ-04: Color Accessibility (2 Points)

This check covers **contrast and CVD safety beyond palette choice** — e.g., adequate luminance difference between overlapping series, sufficient alpha for overlapping markers, no red-green as the sole distinguishing signal when overriding the palette.
Expand Down
Loading
Loading