Skip to content

to_png() static export loses title/subtitle/axis ticks/labels/legend when a layer uses linetype=0 #1509

Description

@nsiicm0

Summary

I have been experiencing an odd behavior when a plot contains any layer with linetype=0 (I have been using this "trick" to annotate (think markers of interesting things) my plots by making a line invisible while still showing e.g. an arrowhead via arrow=... - this is legacy code that I have built upon, I am certain there might be better ways nowadays), the static PNG export (ggplot.to_png() / ggsave(..., format="png"), Only the raw geometry (bars, points, the visible arrowhead, etc.) is drawn.

The interactive HTML/JS rendering (_repr_html_(), browser display) is not affected — the plot renders correctly there. This makes the bug easy to miss during development (e.g. in Jupyter or a web app preview) and only surface when exporting a static image (e.g. for embedding into PowerPoint, PDF, or email reports).

Environment

  • lets-plot version: 4.11.0
  • OS: Linux (reproduced in a headless container)
  • Python: 3.12

Minimal Reproduction

The following code is a derivation

from io import BytesIO
import pandas as pd
import lets_plot as lp

df = pd.DataFrame({
    "x": ["A", "B", "C", "D"],
    "y": [10, 20, 15, 25],
})

# BROKEN: linetype=0 on geom_segment wipes out title/subtitle/axis ticks/labels/legend
p_broken = (
    lp.ggplot(df, lp.aes(x="x", y="y"))
    + lp.geom_bar(stat="identity")
    + lp.theme_bw()
    + lp.labs(title="Title is missing", subtitle="Subtitle is missing too",
              x="X axis label missing", y="Y axis label missing")
    + lp.ggsize(800, 500)
    + lp.geom_segment(x=2, y=25, xend=3, yend=25, linetype=0, arrow=lp.arrow(type="closed"))
)
with open("broken.png", "wb") as f:
    p_broken.to_png(f)

# WORKING: identical plot, only difference is the segment keeps its default linetype
p_ok = (
    lp.ggplot(df, lp.aes(x="x", y="y"))
    + lp.geom_bar(stat="identity")
    + lp.theme_bw()
    + lp.labs(title="Title is missing", subtitle="Subtitle is missing too",
              x="X axis label missing", y="Y axis label missing")
    + lp.ggsize(800, 500)
    + lp.geom_segment(x=2, y=25, xend=3, yend=25, arrow=lp.arrow(type="closed"))
)
with open("ok.png", "wb") as f:
    p_ok.to_png(f)

Expected Result

Both broken.png and ok.png should render identically apart from the visibility of the connecting line segment (hidden vs. visible), i.e. both should show:

  • Title: "Title is missing"
  • Subtitle: "Subtitle is missing too"
  • X axis label + tick labels (A, B, C, D)
  • Y axis label + tick labels (0-25)
  • Gridlines from theme_bw()

Actual Result

  • ok.png (no linetype=0): renders correctly — title, subtitle, axis ticks/labels, gridlines all present.
Image
  • broken.png (with linetype=0 on the geom_segment layer): title, subtitle, axis ticks, axis labels, and gridlines are all missing. Only the bare bar geometry (and the arrowhead) is drawn on a blank canvas.
Image

Additional Notes

  • This issue shall not serve as a canvas to argue about the actual purpose of the borderline edgecase of linetype=0 on a segment geometry. It shall purely show the unexpected behavior of basically destroying any plot information such as title, subtitle, labels, ticks, etc.
  • The interactive/HTML rendering path (_repr_html_()) renders p_broken correctly — this appears to be isolated to the static Skia PNG export path only.
  • We discovered this while exporting plots with an "arrow pointing at the last data point" annotation pattern:
    lp.geom_segment(x=x0, y=y0, xend=x1, yend=y1, arrow=lp.arrow(type="closed"), linetype=0, show_legend=False)
    which is a common idiom for hiding the connecting line while keeping only the arrowhead visible. Removing linetype=0 (accepting a visible connector line, or using an alternative to hide it, e.g. alpha=0 — untested) avoids the bug.
  • Bisection confirmed:
    • arrow=... alone: not the cause.
    • fill="white" on the segment: not the cause.
    • linetype=0: is the cause — removing only this parameter (keeping everything else identical) fixes the export.
  • This was reproduced consistently across multiple real-world plots (bar charts with theme_bw(), discrete x-axis with 50+ categories, ggtb() toolbar, legend from scale_fill_manual()), not just the minimal example above, so it does not appear to depend on plot complexity/data volume — the minimal repro triggers it with a trivial 4-row dataset.

Impact

Any application that exports lets-plot charts to static images (PNG/PDF/PPTX embedding, etc.) using this common "hide line, show arrow" pattern will silently produce images missing critical context (title, axis meaning, legend), which can be misleading in reports/dashboards if not caught visually.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions