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.
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.
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.
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 viaarrow=...- 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-plotversion: 4.11.0Minimal Reproduction
The following code is a derivation
Expected Result
Both
broken.pngandok.pngshould render identically apart from the visibility of the connecting line segment (hidden vs. visible), i.e. both should show:theme_bw()Actual Result
ok.png(nolinetype=0): renders correctly — title, subtitle, axis ticks/labels, gridlines all present.broken.png(withlinetype=0on thegeom_segmentlayer): 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.Additional Notes
_repr_html_()) rendersp_brokencorrectly — this appears to be isolated to the static Skia PNG export path only.linetype=0(accepting a visible connector line, or using an alternative to hide it, e.g.alpha=0— untested) avoids the bug.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.theme_bw(), discrete x-axis with 50+ categories,ggtb()toolbar, legend fromscale_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.