JetBrains / JetBrains/lets-plot
`to_png()` static export loses title/subtitle/axis ticks/labels/legend when a layer uses `linetype=0`
- Dominant language
- Kotlin
- Stars
- 1.8k
- Forks
- 60
- PR merge metrics
- No merged PRs in 30d
Description
## 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
```python
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:
```python
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.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with the static export path reached by ggplot.to_png() or ggsave(..., format="png") and compare it with _repr_html(); reproduce the minimal geom_segment case using linetype=0. Trace how that parameter affects rendering, then verify that the PNG retains the title, subtitle, axes, gridlines, and legend while hiding only the connector.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- kotlin, python
- Domain
- data-visualization
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 62/100