Skip to content

Decouple Matplotlib render resolution (DPI) from display size #9124

@daizutabi

Description

@daizutabi

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:

  1. Render at exactly the DPI specified in the figure (or a global config).
  2. 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.)

  • Yes

Alternatives

No response

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions