Skip to content

aprender-viz: GGPlot::title/xlab/ylab are accepted and silently discarded — never drawn, and #[allow(dead_code)] hides it #3590

Closed as not planned

Description

@noahgift

The defect

GGPlot's builder accepts a title and both axis labels and draws none of them. The values are taken, stored, and discarded.

Measured on the published aprender-viz 0.68.2:

  • GGPlot::title() (src/grammar/ggplot.rs:170) stores into GGPlot.title.

  • GGPlot::xlab() (:177) and ylab() (:184) store into GGPlot.xlab / GGPlot.ylab.

  • build() (:194) carries only title into BuiltGGPlot (:209) — and the field is declared

    #[allow(dead_code)]
    title: Option<String>,
  • xlab and ylab are not carried into BuiltGGPlot at all.

  • BuiltGGPlot::to_framebuffer → render_panel draws background, scales, grid, layers, axes and border. grep -n 'self\.title\|self\.xlab\|self\.ylab' over ggplot.rs returns only the three setters and the one build() copy — no draw site.

So three builder methods are silent no-ops. Nothing warns, nothing errors, and the compiler is explicitly told not to notice.

Receipt

A ~15-line program against the published crate:

let built = GGPlot::new()
    .data_xy(&xs, &ys)
    .aes(Aes::new().x("x").y("y"))
    .geom(Geom::point())
    .dimensions(1920, 1080)
    .title("grammar path probe")
    .xlab("x")
    .ylab("sin(x)*40+50")
    .build()?;
PngEncoder::write_to_file(&built.to_framebuffer()?, "grammar.png")?;

The PNG is a correct figure — panel, grid, axes, 200 scaled points — with no title and no axis labels anywhere on it. build() returned Ok. Nothing in the API suggested the three calls had no effect.

The positive control is already in the source

#[allow(dead_code)] on BuiltGGPlot.title is the confession, and it is also the test: remove the allow and the compiler names the bug. That makes a cheap, non-vacuous regression guard — if the field is ever read by a draw path, the allow is unnecessary; while it is necessary, the feature is absent.

For xlab/ylab the equivalent control is that they do not reach BuiltGGPlot at all, so the guard is a test asserting the rendered output contains the label text (or, once outlines land, the corresponding glyph paths).

Fix, either way — but not silently

Draw them, or refuse them. A builder method that discards its argument is worse than one that does not exist, because the caller has no way to find out. If titles are deliberately out of scope for the grammar path, title/xlab/ylab should be removed or return an error rather than accept and drop.

Consumer context

Found by apex (paiml/apex, APEX-001), a first-party consumer, while probing what the published crate can emit. apex binds every rendered string to a claim id and will not ship a figure whose text silently vanishes — so this is a correctness issue for it, not a cosmetic one. Sibling of the SVG-path gap filed alongside this.

Activity

  1. added
    bugSomething isn't working
    P1High priority
    on Sep 20, 2026
  2. added this to the 0.73.0 milestone on Sep 23, 2026
  3. noahgift commented on Sep 23, 2026

    @noahgift
    MemberAuthor

    milestone → 0.73.0 per docs/roadmaps/milestone-triage-2026-09-23.md (#4024), operator approval 2026-09-23 (relayed by aprender-cf). Reason: debt ratchet pillar D (#3997: backlog, docs, packaging), slice 4

  4. modified the milestones: 0.73.0, backlog on Sep 25, 2026
  5. modified the milestones: backlog, 0.72.0 on Sep 25, 2026
  6. noahgift commented on Sep 26, 2026

    @noahgift
    MemberAuthor

    Receipt: .title/.xlab/.ylab are now drawn or refused, never dropped. Branch fix/3590-ggplot-labels @ e5bf28342, off main. No PR: aprender is at its open-PR cap, so this is a fold candidate for stage/0.70.1.

    Behaviour. A set label is resolved at build():

    • Without the text-path feature: there is no way to draw text, so build() returns an Err that names the builder method, e.g. "GGPlot::xlab cannot be drawn…".
    • With text-path: .font(bytes) pins the font, and there is deliberately no default font.
      • A missing font is an Err.
      • A font that draws no glyph for the label is an Err too, since an empty outline would be a silent drop by another route.
      • A resolved label is filled with the nonzero winding rule and one sample per pixel centre, in theme.text_color:
        • the title in the top margin;
        • xlab in the bottom margin;
        • ylab rotated a quarter turn in the left margin.
    • An empty label string needs no font.

    Tests.

    • grammar lib tests pass: 135 on default features, 139 with --features text-path.
    • clippy --features text-path --all-targets -D warnings is clean, and cargo fmt is clean.
    • The host-bound test tests/ggplot_labels_3590.rs (--ignored, DejaVuSans) counts ink in each margin: labelled must exceed unlabelled by more than 20 px.

    Mutants. Each one is planted, turns its guard RED, and the file is then restored and checked with cmp.

    mutant RED guard
    no refusal without text-path test_ggplot_label_without_a_font_is_refused
    empty-outline check removed test_ggplot_font_that_draws_no_glyph_is_refused
    title not drawn each_label_is_drawn_in_its_own_margin (host)
    winding ignored in the fill fill_path_nonzero_keeps_the_hole_of_a_reversed_inner_contour
  7. modified the milestones: 0.72.0, backlog on Sep 27, 2026
  8. noahgift commented on Oct 14, 2026

    @noahgift
    MemberAuthor

    merged into #5312 (master ticket for epic #3994; triage rule 7b, 2026-10-14). It is a checklist row there; reopen this issue when work on it starts or its PR opens.

  9. noahgift commented on Oct 14, 2026

    @noahgift
    MemberAuthor

    Moved: this sub-ticket is now a row in #5334 (aprender-viz: draw titles and axis labels, ship runnable Facet/Coord examples), split out of #5312 by theme.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High prioritybugSomething isn't workingowner:la-0.71Owned by the 0.71 look-ahead slotsize:MK̂ 3-4 h

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions