2025, Nov 01 09:00
Fix X-axis sorting in Altair layered bar charts with error bars using EncodingSortField
Learn why X-axis sorting breaks in Altair layered bar charts with error bars and how EncodingSortField fixes it. Step-by-step example, code, and practical tips.
Layered charts in Altair are a go-to pattern for combining bars with intervals or uncertainty bands. A common need is to order the X axis by a quantitative measure on Y. This is straightforward with bars and rules, but things get confusing when error bars enter the picture: the same sort suddenly falls back to alphabetical order.
Reproducing the issue
The example below shows a bar chart layered with either rules or error bars. Sorting the X axis by the Y metric works with rules, but not with error bars, even though each layer sorts correctly on its own.
import polars as pl
import altair as alt
data_tbl = pl.DataFrame({
"label": ["A", "B", "C", "D"],
"avg": [1.2, 2.5, 0.9, 3.1],
"lo": [1.0, 2.2, 0.85, 2.8],
"hi": [1.4, 2.8, 0.95, 3.4]
})
base_cols = alt.Chart(data_tbl).mark_bar().encode(
x=alt.X("label:N").sort("-y"),
y=alt.Y("avg:Q")
)
err_overlay = base_cols.mark_errorbar().encode(
y="lo:Q",
y2="hi:Q"
)
rule_overlay = base_cols.mark_rule().encode(
y="lo:Q",
y2="hi:Q"
)
# Doesn't sort
base_cols + err_overlay
# Does sort
base_cols + rule_overlay
Each layer sorts correctly in isolation. The combined bars + error bars revert to alphabetical ordering on the X axis, while bars + rules respect the sort by Y.
What’s going on
The observed behavior comes down to how the sort is specified. The shorthand string-based sort on X using "-y" can be ignored when layering with error bars, whereas the same shorthand works when layering with rules. The practical implication is that the implicit sort by the Y channel is not consistently honored for mark_errorbar in a layered chart. Using an explicit field-based sort resolves the inconsistency.
The fix
Define the sort via EncodingSortField on the X channel and point it at the quantitative measure you want to drive the order. This keeps the correct order when layering with error bars.
import polars as pl
import altair as alt
data_tbl = pl.DataFrame({
"label": ["A", "B", "C", "D"],
"avg": [1.2, 2.5, 0.9, 3.1],
"lo": [1.0, 2.2, 0.85, 2.8],
"hi": [1.4, 2.8, 0.95, 3.4]
})
base_cols = alt.Chart(data_tbl).mark_bar().encode(
x=alt.X("label:N").sort(alt.EncodingSortField(field="avg", op="min")),
y=alt.Y("avg:Q")
)
err_overlay = base_cols.mark_errorbar().encode(
y="lo:Q",
y2="hi:Q"
)
# Now sorts correctly
base_cols + err_overlay
More details: github issue.
Why this matters
Axis ordering is not a cosmetic detail. It shapes how rankings, extremes, and patterns read at a glance. When layering uncertainty with mark_errorbar, the default shorthand sort may silently degrade into alphabetical order, muddling comparisons. An explicit EncodingSortField anchors the sort to the intended metric and makes the chart robust to layering nuances.
Takeaways
If you need to sort a layered chart’s X axis by a Y-derived value and you include error bars, prefer an explicit sort with EncodingSortField on the X channel. Point it at the quantitative field driving the bar height and specify the aggregation that matches your design. This tiny change prevents unexpected reordering and keeps your layered visualizations consistent.