Skip to content

Avoid stretching altair charts by default to the full width of the notebook #8596

Closed

Description

@joelostblom

Thanks for your great work on marimo! I've recently started using it more frequently and I'm quite enjoying it as a fresh take on the notebook experience! One aspect I have found surprising is that Altair charts are stretching the full width of the notebook by default. When looking into it, I noticed that marimo changes the specification of the charts internally to set width='container' automatically on all charts. I wonder if you could share the motivation behind this and if you would consider changing the default behavior, as I think it has undesired effects for both discrete and continuous axis scales. I'll include my thoughts around this below, let me know what you think!

Sorry this became longer than intended; I just want to give sufficient motivation to why I think avoiding stretching the charts is preferable, since I understand it can be a big ask to change a default setting

Discrete scales

In Altair/Vega-Lite, the width for categorical charts (like bars and rectangles) are per default set based on the number of categories. This ensures a consistent visual representation for each category regardless of how many categories are present overall (ie there is a fixed relationship between the data and the graphical space it occupies). In general, I think it is also more difficult to see what it going on in a chart with really wide bars that span the entire screen. This is an example of a chart I think is quite difficult to grok because of the giant bars (Github is actually making this look slightly better than what it does in the notebook because the width of the container here is more narrow, but I still think it clearly shows the issue):

import altair as alt
import pandas as pd

source = pd.DataFrame(
    {
        "a": ["A", "B", "C", "D", "E"],
        "b": [28, 55, 43, 91, 81],
    }
)

bars = alt.Chart(source).mark_bar().encode(x="a", y="b")
bars
Image

Varying the chart width based on the container width also means that horizontal and vertical bar charts of the same data now look completely different although they represent the same relationship of data being mapped visually:

bars.encode(x='b', y='a')
Image

Setting a constant width per category alleviates these issues and also makes it clear that horizontal and vertical bar are just rotations of the same graphical view.

bars.properties(width=alt.Step(20)).encode(x='b', y='a')
Image
bars.properties(width=alt.Step(20))
Image

Continuous scales

When two continuous scale takes up widely different visual space, they can become difficult to compare and give illusions of how two variables relate to each other. Here is an example, where the slope between x and y is underappreciated when the x-axis is artificially much longer than the y-axis:

points = alt.Chart(
    alt.sequence(start=0, stop=10, step=0.05)
).mark_circle(size=40, opacity=1).encode(
    x='data:Q',
    y='data:Q',
)
points
Image

Although x and y spans the same range, we can also see that x is given different tick marks than y, which makes it more difficult to detect that they are in fact spanning the exact same range. This relation and the correct slope, are both easier to detect with an equal aspect ratio:

points['data'] = alt.sequence(start=0, stop=10, step=0.2)
points.properties(width=300)
Image

Another way to see that the shape of the data is distorted is to plot some data of a known shape, e.g. a circle:
Image

If we plot with an equal aspect ration, the data retains it shape:
Image

Note that I'm not arguing that an equal aspect ratio is always the best choice. E.g. for histograms and timeseries, it often makes sense to have one axis be longer than the other, and if the scales are widely different we should think about whether we want to set their ranges manually. But all these scenarios benefit from being intentional choices by the user and a square default is a good neutral choice that doesn't have the potential to mislead in unexpected ways. If you are interested, we had some discussion about this previously in Altair, and there is also a similar recommendation in The Best Practices for Data Visualization.

Documentation examples

It might be unexpected to copy the code from an example from the Altair docs and then get a different looking figure in marimo. For example, if I try to re-create the heatmap gallery example:

Image

I end up with the following figure in marimo:

Image

It is not immediately obvious what went wrong to make the chart look so different from the docs (heatmaps are also a good example of a chart that benefits from square or near square visual elements). I noticed that for other visualization libraries such as Matplotlit/Seaborn, and Plotly the default for the respective library is honored.

I also noticed a couple of smaller issues with changing the width, such as implementing special rules for some chart types and that some configuration options now are not working as intended. If it is important with an easy way to stretch charts width wise, maybe there could be an option in the settings menu that toggles on the current behavior, but that the default is to keep the same settings that Altair set by default?

Thanks for reading all of this!

Full notebook
import marimo

__generated_with = "0.19.6"
app = marimo.App(width="full")


@app.cell
def _():
    import altair as alt
    import pandas as pd

    source = pd.DataFrame(
        {
            "a": ["A", "B", "C", "D", "E"],
            "b": [28, 55, 43, 91, 81],
        }
    )

    bars = alt.Chart(source).mark_bar().encode(x="a", y="b")
    bars
    return alt, bars, pd


@app.cell
def _(bars):
    bars.encode(x='b', y='a')
    return


@app.cell
def _(bars):
    bars.configure_view(discreteWidth=300)
    return


@app.cell
def _(alt, bars):
    bars.properties(width=alt.Step(20)).encode(x='b', y='a')
    return


@app.cell
def _(alt, bars):
    bars.properties(width=alt.Step(20))
    return


@app.cell
def _(alt, pd):
    # import altair as alt
    import numpy as np
    # import pandas as pd

    # Compute x^2 + y^2 across a 2D grid
    x, y = np.meshgrid(range(-5, 5), range(-5, 5))
    z = x**2 + y**2

    # Convert this grid to columnar data expected by Altair
    source2 = pd.DataFrame({"x": x.ravel(), "y": y.ravel(), "z": z.ravel()})

    alt.Chart(source2).mark_rect().encode(x="x:O", y="y:O", color="z:Q")
    return


@app.cell
def _(alt):
    # import altair as alt

    circular = alt.sequence(start=0, stop=6.28318, step=0.05, as_='t')

    circle = (
        alt.Chart(circular)
        .transform_calculate(
            x="cos(datum.t)",
            y="sin(datum.t)"
        )
        .mark_line()
        .encode(
            x=alt.X('x:Q'),
            y=alt.Y('y:Q'),
            order='t:Q'
        )
        # .properties(width=400, height=400)
    )

    circle
    return (circle,)


@app.cell
def _(circle):
    circle.properties(width=400, height=400)
    return


@app.cell
def _(alt):
    points = alt.Chart(
        alt.sequence(start=0, stop=10, step=0.05)
    ).mark_circle(size=40, opacity=1).encode(
        x='data:Q',
        y='data:Q',
    )
    points
    return (points,)


@app.cell
def _(alt, points):
    points['data'] = alt.sequence(start=0, stop=10, step=0.2)
    points.properties(width=300)
    return


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

Activity

  1. dmadisetti commented on Mar 6, 2026

    @dmadisetti
    Member

    Thank you for the detailed post! width=container does fill real-estate but I can see why it might not be an ideal default. I'll see if I can find out more details from the team

  2. changed the title [-]Avoid stretching altair charts to the full width of the notebook[/-] [+]Avoid stretching altair charts by default to the full width of the notebook[/+] on Mar 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions