Repository navigation
Description
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
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')
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')
bars.properties(width=alt.Step(20))
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
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)
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:

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

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:
I end up with the following figure in marimo:
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
dmadisetti commentedon Mar 6, 2026
Thank you for the detailed post!
width=containerdoes 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[-]Avoid stretching altair charts to the full width of the notebook[/-][+]Avoid stretching altair charts by default to the full width of the notebook[/+]