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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@ in the README).
never appended report exactly what they did before.

### Fixed
- **The native raster exporter silently deleted every character outside its
glyph atlas.** No glyph and no advance, so `Zürich` came out of `to_png()` as
`Zrich`, a `format="€,.0f"` axis lost its symbol on every tick, and a CJK tick
label exported as blank space — while the same figure's SVG rendered all of
them correctly. The atlas now carries Latin-1 Supplement and Latin Extended-A
(every Latin-script language) plus non-ASCII currency, and anything still
unbaked — CJK, Cyrillic, emoji — renders as the U+FFFD replacement box with
its advance reserved, so the limitation is visible rather than silent (§28).
Costs ~48 KB of baked coverage (+4% on the core dylib).
- **The categorical palette cycled per trace instead of per series.** A box is
four traces and a stem is two, so four box series under a four-color
`xy.theme(palette=...)` all wore `palette[0]`, and eight box series drew two
Expand Down
16 changes: 16 additions & 0 deletions scripts/gen_font.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@

# Printable ASCII. Axis labels/titles/legends mostly stay within this set.
FIRST, LAST = 0x20, 0x7E
# Latin-1 Supplement + Latin Extended-A letters: every language written in the
# Latin script that xy is likely to label a chart in. Without these a `Café` or
# `Zürich` tick label lost the accented letter *entirely* — no glyph, no
# advance, no warning — while the same chart's SVG rendered it correctly.
# Ranges, not a hand-typed string, so the coverage is auditable.
_LATIN = "".join(chr(c) for c in [*range(0x00C0, 0x0180)] if chr(c).isalpha())
# Currency beyond ASCII `$`. `xy.y_axis(format="€,.0f")` is a first-class
# feature, so its symbol has to survive to_png().
_CURRENCY = "¢£¤¥₣₤₦₩₪₫€₭₮₱₲₴₹₺₽₿"
# Extra codepoints for the pyplot shim's TeX-subset → unicode conversion
# (greek, super/subscripts, math operators) plus typography mpl emits (−, µ).
EXTRA = sorted(
Expand All @@ -30,6 +39,13 @@
"⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁼⁽⁾ⁿⁱ"
"₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎ₐₑₕᵢₖₗₘₙₒₚᵣₛₜᵤᵥₓ"
"×·±∓≤≥≠≈∞√°→←∂∇∫∝∈−–—‘’“”…µ"
"§¶†‡•‰≡⌀"
+ _LATIN
+ _CURRENCY
# U+FFFD is the fallback the rasterizer substitutes for anything still
# missing, so a character xy cannot draw shows up as a visible box
# instead of vanishing (§28: never silent).
+ "�"
)
)
PX = 16 # render size; runtime scales this coverage bitmap to the requested size.
Expand Down
20 changes: 14 additions & 6 deletions spec/api/styling.md
Original file line number Diff line number Diff line change
Expand Up @@ -698,12 +698,20 @@ so small labels are slightly less refined than a browser's.
**Native text coverage.** The atlas is a generated grayscale DejaVu Sans sheet
(`src/font.rs`, regenerated by `scripts/gen_font.py`) baked at a 16 px base
cell and blitted and scaled at runtime, so glyph coverage is a fixed set: ASCII
32–126 plus 110 enumerated extras (Greek, common math operators, arrows,
super/subscripts, typographic quotes and dashes). A codepoint outside that set
— CJK, Cyrillic, Arabic, emoji, most accented Latin — has no glyph and is
**silently skipped**, with no tofu box and no advance reserved, so a title,
tick label, legend entry, or annotation containing one renders shortened rather
than raising. Use `engine=xy.Engine.chromium` for full Unicode text.
32–126 plus 329 enumerated extras — the Latin-1 Supplement and Latin Extended-A
letters (every language written in the Latin script), non-ASCII currency
symbols, Greek, common math operators, arrows, super/subscripts, and
typographic quotes and dashes. `format="€,.0f"` and a `Zürich` tick label
therefore survive `to_png()` intact.

A codepoint outside that set — CJK, Cyrillic, Arabic, emoji — renders as
U+FFFD, the replacement box, and reserves that glyph's advance. It used to be
**silently skipped**, with no box and no advance, so `Zürich` exported as
`Zrich` and a CJK label exported as blank space while the same figure's SVG was
correct; §28 asks that a decision the engine makes on the user's behalf be
visible, and a box is the visible form of "this renderer cannot draw that".
Zero-width and control characters are still dropped — they have nothing to
show. Use `engine=xy.Engine.chromium` for full Unicode text.

The atlas bounds the native **raster** formats (PNG, JPEG, WebP) only. The
other two native formats carry their own text contracts: SVG emits real
Expand Down
Binary file added spec/assets/raster-glyph-coverage.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions spec/design/rust-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,12 @@ src/ # 15,423 lines shipped, 8 modules
# byte-identical to the serial path) and the fused
# raster→PNG encode via `png`/fdeflate.
# No unsafe; owns the crate's one third-party dep.
font.rs (1039) # generated by scripts/gen_font.py — do not hand-edit.
font.rs (2503) # generated by scripts/gen_font.py — do not hand-edit.
# Baked DejaVu Sans grayscale coverage atlas for
# raster.rs: 205 glyph records (advance, w, h, left,
# raster.rs: 424 glyph records (advance, w, h, left,
# top, coverage offset/len) at BASE_PX=16 plus the
# row-major coverage bytes, and EXTRA_CODEPOINTS, the
# sorted table of the 110 non-ASCII codepoints. Data
# sorted table of the 329 non-ASCII codepoints. Data
# only — no shaping, no FreeType, no unsafe. Coverage
# limits are a product constraint, not a detail: §2.1.
css.rs (788) # tiered CSS value/color validation behind `xy_css_check`.
Expand Down
Loading
Loading