Skip to content

fix(colors): emit exact rgb255() colours instead of snapping to ~8 names - #15

Merged
BenHuddart merged 4 commits into
mainfrom
fix/exact-rgb-colors
Jul 31, 2026
Merged

fix(colors): emit exact rgb255() colours instead of snapping to ~8 names#15
BenHuddart merged 4 commits into
mainfrom
fix/exact-rgb-colors

Conversation

@BenHuddart

Copy link
Copy Markdown
Owner

Summary

rgb_to_gle snapped every hex/RGB colour onto ~8 named GLE colours by dominant channel: greys (#8c8c8c, #999999) rendered magenta, #bbbbbb rendered white (invisible), the matplotlib tab10 cycle collapsed onto 5 colours, and C0C9 strings weren't recognised at all (all black) — distinct series silently colliding with no warning.

GLE supports exact colours natively, so the fix emits them: recognised GLE colour names (now the full 151-name table, any case) pass through as names; everything else emits as rgb255(r,g,b). Also added: #RGB shorthand, C0C9/tab:* cycle references, clamping, idempotent pass-through of formed colour expressions, and gle_color_to_rgb255() for exact inversion (the GUI swatches now use it instead of a 21-entry approximate dict). The parser gained _collect_color() so rgb255(...) (which lexes into six tokens) round-trips exactly at all four colour-reading sites. The deliberate approximation nearest_gle_color() is untouched and documented as the opt-in path.

Behaviour change: previously-snapped figures now render the requested colours, and stored colour values are rgb255(...) rather than approximate names — documented in COLORS_AND_MARKERS.md.

Test plan

  • New tests/unit/test_color_fidelity.py (47 tests) asserting on generated GLE text across every emission context (lines, markers, errorbars incl. the bars-only path at all capsizes, bar fills, region fills, contours, text) plus parser round-trip and a golden-battery builder; three near-identical greys stay distinct; both C0C9 and tab:* give ten distinct colours.
  • Full suite: 1533 passed, 2 skipped (6 old-contract assertions updated deliberately).
  • Compiled verification through GLE 4.3.10: before/after PNGs confirm greys/purple/tab10 all distinct after, magenta/invisible/black before.

🤖 Generated with Claude Code

BenHuddart and others added 4 commits July 31, 2026 00:57
``rgb_to_gle`` mapped every hex string / RGB tuple onto one of about eight
named GLE colours by dominant channel. The result was silent, unreported
colour substitution: ``#8c8c8c`` and ``#999999`` (greys) both rendered
MAGENTA, ``#bbbbbb`` rendered WHITE (invisible on a white page), and
``#9467bd`` (matplotlib's tab purple) rendered MAGENTA -- so two distinct
series could collide onto one colour with nothing to warn the user.

GLE accepts ``rgb255(r,g,b)`` (and ``rgb(r,g,b)`` with 0-1 floats) anywhere
a colour name is accepted, so the snapping bought nothing. Now:

- a recognised GLE colour NAME (any case, all 151 GLE knows) passes through
  as that name;
- everything else -- hex ``#RRGGBB``/``#RGB``, RGB tuples, and matplotlib's
  ``C0``..``C9`` / ``tab:`` cycle references -- becomes an exact
  ``rgb255(r,g,b)`` expression;
- an already-formed colour expression passes through (whitespace-normalised),
  which is what makes a parser round trip idempotent.

Colours reach every emission context (lines, markers, error bars including
the bars-only ``color <c>`` path, bar fills, fill-between, contours, text)
as an opaque token, so all of them now carry the exact colour.

The parser side gains ``_collect_color``: a colour expression lexes into
several tokens (``rgb255`` ``(`` ``140`` ``,`` ...), so reading one token
recovered only the function name. Applied to dataset attributes, ``bar
... fill``, ``fill dA,dB color`` and ``set color``.

The GUI colour swatches now invert both stored forms via the new
``colors.gle_color_to_rgb255``, so a picked colour is stored and redisplayed
exactly rather than snapped to the nearest palette entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…round trip

Asserts on the generated GLE text: the reported greys/purples emit their
exact rgb255 values with no MAGENTA/WHITE substitution anywhere; three
near-identical greys stay three distinct colours; matplotlib's default cycle
(C0-C9 and the tab: names alike) yields ten DISTINCT emitted colours where
snapping collapsed it to five; named colours still emit as names.

Covers every emission context -- line, line+marker, marker-only, scatter,
errorbar (marker, line, and the bars-only ``color <c>`` path at every
capsize), bar fill, fill-between, contour, text, and the *_from_file
series -- plus the parser round trip: reopening preserves each colour and
re-emitting is byte-identical, including a hand-written script whose
``rgb255( 140 , 140 , 140 )`` spans six lexer tokens.

Adds an ``exact_rgb_colors`` builder to the golden battery so the existing
fixed-point and to_dict-equivalence machinery exercises rgb255 too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
States the new contract up front (name in -> name out; anything else ->
exact rgb255), calls out the pre-1.8.2 snapping as a behaviour change with
the workaround for anyone who relied on it, and documents the newly accepted
inputs: every GLE colour name, #RGB shorthand, the C0-C9 / tab: cycle, and
pass-through of colour expressions. Notes that rgb255 needs no GLE newer than
the 4.3+ gleplot already requires (verified against 4.3.10), and points at
nearest_gle_color for deliberate snapping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The GUI colour swatch never called it, and now stores picked colours exactly
as rgb255 anyway. Describe it for what it is: an opt-in helper for callers who
deliberately want a named approximation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@BenHuddart
BenHuddart merged commit 25bad36 into main Jul 31, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant