Description
Currently, in marimo's Matplotlib backend (similar to Jupyter), the rendering DPI is coupled with the display dimensions. When a user increases fig.dpi to get a sharper image, the plot physically expands in the notebook, often breaking the layout.
Matplotlib (Current Behavior: Coupled)
When changing fig.dpi, the display size scales linearly with the DPI.
- At
dpi=20, the plot is tiny.
- At
dpi=200, the plot is huge.
Altair (Desired Behavior: Decoupled)
Altair correctly separates rendering resolution from display size.
- Whether
ppi=20 or ppi=200, the plot maintains its intended dimensions in the notebook. Only the internal sharpness changes.
Reproduction Code
import marimo
__generated_with = "0.23.0"
app = marimo.App(width="medium")
with app.setup:
import altair as alt
import matplotlib.pyplot as plt
import polars as pl
@app.cell
def _():
data = pl.DataFrame({"x": 1, "y": 1})
fig, ax = plt.subplots(figsize=(2, 1))
ax.scatter(data["x"], data["y"])
ax.set(xlim=(0, 1), ylim=(0, 1))
chart = (
alt.Chart(data)
.mark_point()
.encode(x="x", y="y")
.properties(width=2 * 72, height=1 * 72)
)
fig.dpi
return chart, fig
@app.cell
def _(fig):
plt.rcParams["savefig.format"] = "svg"
fig
return
@app.cell
def _(fig):
plt.rcParams["savefig.format"] = "png"
fig
return
@app.cell
def _(fig):
fig.dpi = 20
fig
return
@app.cell
def _(fig):
fig.dpi = 200
fig
return
@app.cell
def _(chart):
alt.renderers.enable("svg")
chart
return
@app.cell
def _(chart):
alt.renderers.enable("png")
chart
return
@app.cell
def _(chart):
alt.renderers.enable("png", ppi=20)
chart
return
@app.cell
def _(chart):
alt.renderers.enable("png", ppi=200)
chart
return
if __name__ == "__main__":
app.run()
Suggested solution
Instead of the implicit *2 / //2 logic for "retina", marimo should:
- Render at exactly the DPI specified in the figure (or a global config).
- Calculate the display size by scaling the actual pixels back to a 100 DPI equivalent (Matplotlib's default).
Proposed Logic:
# 1. Use the figure's actual DPI (no magic doubling)
render_dpi = fig.figure.dpi
fig.figure.savefig(buf, format="png", bbox_inches="tight", dpi=render_dpi)
# 2. Extract actual rendered pixels
width, height = _extract_png_dimensions(png_bytes)
# 3. Scale back to a 100 DPI reference for display
# This ensures the display size is consistently scaled based on Matplotlib's
# default 100 DPI, correctly preserving intended dimensions (plus padding).
factor = render_dpi / 100
mimebundle = {
"image/png": data_url,
METADATA_KEY: {
"image/png": {
"width": width / factor,
"height": height / factor,
}
},
}
Why this is better:
- Consistency: A figure with
figsize=(width, height) will have a consistent display size in the browser, regardless of the fig.dpi.
- Control: Users can set
fig.dpi = 200 to get a "Retina" quality image without affecting the layout.
- Simplicity: Removes the "magic"
*2.
Are you willing to submit a PR? (You must receive approval from the team before submitting a PR.)
Alternatives
No response
Additional context
No response
Description
Currently, in marimo's Matplotlib backend (similar to Jupyter), the rendering DPI is coupled with the display dimensions. When a user increases
fig.dpito get a sharper image, the plot physically expands in the notebook, often breaking the layout.Matplotlib (Current Behavior: Coupled)
When changing
fig.dpi, the display size scales linearly with the DPI.dpi=20, the plot is tiny.dpi=200, the plot is huge.Altair (Desired Behavior: Decoupled)
Altair correctly separates rendering resolution from display size.
ppi=20orppi=200, the plot maintains its intended dimensions in the notebook. Only the internal sharpness changes.Reproduction Code
Suggested solution
Instead of the implicit
*2///2logic for "retina", marimo should:Proposed Logic:
Why this is better:
figsize=(width, height)will have a consistent display size in the browser, regardless of thefig.dpi.fig.dpi = 200to get a "Retina" quality image without affecting the layout.*2.Are you willing to submit a PR? (You must receive approval from the team before submitting a PR.)
Alternatives
No response
Additional context
No response