marimo icon indicating copy to clipboard operation
marimo copied to clipboard

Altair charts with width property are turned into bitmaps

Open codydunne opened this issue 8 months ago • 8 comments

Describe the bug

It is valuable to be able to set the width and height of an Altair chart if you want to have particular aspect ratios or export bitmaps, svgs, PDFs.

When Altair charts are specified with the width property e.g.,

.properties(
    width=500
)

they become rendered as bitmaps rather than vector graphics. E.g., Image vs. this where the only difference is the width and height properties being specified: Image (The first is just a bitmap screencap of the vector graphic for this issue.)

The problem occurs whether you specify the width with .properties or in the call to alt.chart. E.g., alt.Chart(cars, width=500)

Surprisingly, it doesn't seem to be a problem if just height is specified!

Using alt.renderers.enable('svg') fixes some issues, but adds others.

Note that I had to restart the kernel after every change as there seemed to be some issue with cell rendering not being updated? I'm unclear on that.

Environment

{
  "marimo": "0.12.3",
  "OS": "Windows",
  "OS Version": "11",
  "Processor": "Intel64 Family 6 Model 165 Stepping 5, GenuineIntel",
  "Python Version": "3.11.11",
  "Binaries": {
    "Browser": "129.0.6668.90",
    "Node": "v22.9.0"
  },
  "Dependencies": {
    "click": "8.1.8",
    "docutils": "0.21.2",
    "itsdangerous": "2.2.0",
    "jedi": "0.19.2",
    "markdown": "3.7",
    "narwhals": "1.33.0",
    "packaging": "24.2",
    "psutil": "7.0.0",
    "pygments": "2.19.1",
    "pymdown-extensions": "10.14.3",
    "pyyaml": "6.0.2",
    "ruff": "0.11.3",
    "starlette": "0.46.1",
    "tomlkit": "0.13.2",
    "typing-extensions": "4.13.1",
    "uvicorn": "0.34.0",
    "websockets": "15.0.1"
  },
  "Optional Dependencies": {
    "altair": "5.5.0",
    "anywidget": "0.9.18",
    "pandas": "2.2.3",
    "pyarrow": "19.0.1"
  },
  "Experimental Flags": {}
}

Code to reproduce

See the WASM version: https://marimo.app/l/2hyx0v

Here is an alternate version exploring what happens when you add alt.renderers.enable('svg'): https://marimo.app/l/bwmiw1 (though this doesn't work in WSAM due to missing a wheel needed for Altair SVG export... It works locally.)

The code for the former is here:

import marimo

__generated_with = "0.11.31-dev3"
app = marimo.App()


@app.cell
def _():
    import marimo as mo
    import altair as alt
    import vega_datasets
    cars = vega_datasets.data.cars()
    return alt, cars, mo, vega_datasets


@app.cell
def _(alt, cars):
    # The normal chart works fine
    chart = alt.Chart(cars).mark_point().encode(
        x='Horsepower',
        y='Miles_per_Gallon',
        color='Origin',
    )
    chart
    return (chart,)


@app.cell
def _(chart):
    # Adding width and height makes everything bitmap
    chartSized = chart.properties(
            width=500,
            height=100
        )
    chartSized
    return (chartSized,)


@app.cell
def _(chartSized, mo):
    # Wrapping it doesn't work
    mo.ui.altair_chart(chartSized)
    return


@app.cell
def _(chartSized, mo):
    # Stacks don't work either
    mo.vstack([
        chartSized
    ])
    return


if __name__ == "__main__":
    app.run()

codydunne avatar Apr 03 '25 20:04 codydunne

I'm not sure I totally understand. Is this an issue on marimo?

Can you reproduce this in the vega playground?

mscolnick avatar Apr 03 '25 20:04 mscolnick

I don't see the problem using the vega editor. Here are examples. After loading them in the vega editor, I changed the settings to show SVG and they all worked fine. This seems to be a rendering issue in Marimo, rather than Altair or Vega.

Unsized chart from Jupyterlab Altair

Sized chart from JupyterLab Altair

At least per https://jsondiff.com/, the only difference is the latter has the width and height properties.

If I make with Marimo

Unsized chart in Marimo Altair

Sized chart with Marimo Altair

The difference between these two is that the unsized has "width": "container", while the sized has "height": 100, "width": 500.

The difference between the Jupyterlab and Marimo generated unsized files is that Marimo adds

    "usermeta": {
        "embedOptions": {
        }
    },
    "width": "container"

codydunne avatar Apr 03 '25 21:04 codydunne

I'm still unclear what the marimo issue is. We do default to width "container" because it looks better and more responsive.

But at the end of the day, is anything wrong with the spec? Or is the spec ok, but just rendered wrong when in marimo?

We then pass this spec to the Vega library for rendering, without doing anything fancy.

mscolnick avatar Apr 04 '25 02:04 mscolnick

I think the spec is fine, at least as rendered by vega on their editor and Altair in a jupyter notebook. My best guess is that somehow the rendering in Marimo is different and it is introducing the bitmap problem.

codydunne avatar Apr 04 '25 03:04 codydunne

Thanks for debugging. We don't do anything with the spec (unless wrapped in mo.ui.altair()). We just pass it directly to react-vega which uses the vega library underneath to render the spec.

It could be a version issue with the vega spec or an upstream issue on Vega.

mscolnick avatar Apr 04 '25 14:04 mscolnick

Also I am not sure i can reproduce what you are seeing, even from the example and playground you linked. How can you tell what is Bitmap vs Vector Graphics?

mscolnick avatar Apr 04 '25 14:04 mscolnick

You're welcome! Thanks for all your work on this.

You should be able to see the issue in the WASM version I uploaded. If you use the browser zoom on the first two images, you should see that the top stays nice and crisp while the second gets fuzzy. The same thing happens when I launch marimo locally.

However, the problem doesn't show up in the Vega editor with any of the specs.

codydunne avatar Apr 04 '25 20:04 codydunne

Oh wow, yes i see that now (by zooming in). That's a much better indicator for me - and will help me debug this.

mscolnick avatar Apr 07 '25 17:04 mscolnick

The "width": "container" default explains something I ran into. Specifically, some compounds charts are tuned with configure_view:

chart.configure_view(
    continuousWidth=200,
)

normally it's equivalent to chart.properties(width=200), but it takes no effect when main width property is set to "container".

liquidcarbon avatar May 14 '25 10:05 liquidcarbon