lib/streamlit/.agents/skills/developing-with-streamlit/references/data-display.md
Present data clearly.
Prefer Streamlit's native charts for simple cases.
st.line_chart(df, x="date", y="revenue")
st.bar_chart(df, x="category", y="count")
st.scatter_chart(df, x="age", y="salary")
st.area_chart(df, x="date", y="value")
Native charts support additional parameters: color for series grouping, stack for bar/area stacking, size for scatter point sizing, horizontal for horizontal bars. See the chart API reference for full options.
Use clear labels—not column names or abbreviations. Skip x_label/y_label if the column names are already readable.
# BAD: cryptic column names without labels
st.line_chart(df, x="dt", y="rev")
# GOOD: readable columns, no labels needed
st.line_chart(df, x="date", y="revenue")
# GOOD: cryptic columns, add labels
st.line_chart(df, x="dt", y="rev", x_label="Date", y_label="Revenue")
Use Altair when you need more control. Altair is bundled with Streamlit (no extra install), while Plotly requires an additional package. Pick one and stay consistent throughout your app.
import altair as alt
chart = alt.Chart(df).mark_line().encode(
x=alt.X("date:T", title="Date"),
y=alt.Y("revenue:Q", title="Revenue ($)"),
color="region:N"
)
st.altair_chart(chart)
When to use Altair:
use_container_widthDo not use use_container_width. It is deprecated — Streamlit elements now stretch to fill their container by default. Use the width parameter instead: width="stretch" (equivalent to use_container_width=True) or width="content" (equivalent to use_container_width=False). Remove use_container_width when you see it, and never add it to new code.
# BAD: deprecated parameter
st.dataframe(df, use_container_width=True)
st.altair_chart(chart, use_container_width=True)
# GOOD: just omit it — stretch is the default
st.dataframe(df)
st.altair_chart(chart)
# GOOD: explicitly set width when you need content-sized (non-stretching)
st.dataframe(df, width="content")
Use column_config where it adds value—formatting currencies, showing progress bars, displaying links or images. Don't add config just for labels or tooltips that don't meaningfully improve readability. Works with both st.dataframe and st.data_editor.
st.dataframe(
df,
column_config={
"revenue": st.column_config.NumberColumn(
"Revenue",
format="$%.2f"
),
"completion": st.column_config.ProgressColumn(
"Progress",
min_value=0,
max_value=100
),
"url": st.column_config.LinkColumn("Website"),
"logo": st.column_config.ImageColumn("Logo"),
"created_at": st.column_config.DatetimeColumn(
"Created",
format="MMM DD, YYYY"
),
"internal_id": None, # Hide non-essential columns
},
hide_index=True,
)
Note on hiding columns: Setting a column to None hides it from the UI, but the data is still sent to the frontend. For truly sensitive data, pre-filter the DataFrame before displaying.
Dataframe best practices:
hide_index=Truedf = df.set_index("customer_name") before displayingNone in config (but pre-filter for sensitive data)Column types:
AreaChartColumn → Area sparklinesAudioColumn → Audio playbackBarChartColumn → Bar sparklinesCheckboxColumn → Boolean as checkboxDateColumn → Date only (no time)DatetimeColumn → Dates with formattingImageColumn → ImagesJSONColumn → Display JSON objectsLineChartColumn → Sparkline chartsLinkColumn → Clickable linksListColumn → Display lists/arraysMultiselectColumn → Multi-value selectionNumberColumn → Numbers with formattingProgressColumn → Progress barsSelectboxColumn → Editable dropdownTextColumn → Text with formattingTimeColumn → Time only (no date)VideoColumn → Video playback| Widget | Use When |
|---|---|
st.dataframe | Large datasets, interactive exploration, sorting, filtering, row selection |
st.data_editor | Users need to modify data (edit cells, add/delete rows) |
st.table | Small static datasets, Markdown-formatting and extended Pandas Styler support |
Use st.dataframe with on_select for row selection — do not use st.data_editor with a checkbox column for selection-only use cases.
Use column_config for all value formatting (numbers, dates, percentages). Only use Pandas Styler for coloring (background gradients, highlights).
# BAD: Styler for formatting — AI tends to overuse this
styled = df.style.format({"revenue": "${:.2f}", "growth": "{:.1%}"})
st.dataframe(styled)
# GOOD: column_config for formatting
st.dataframe(df, column_config={
"revenue": st.column_config.NumberColumn(format="$%.2f"),
"growth": st.column_config.NumberColumn(format="percent"),
"created": st.column_config.DatetimeColumn(format="MMM DD, YYYY"),
})
# GOOD: Styler for colors only + column_config for formatting
styled = df.style.background_gradient(subset=["revenue"], cmap="Greens")
st.dataframe(styled, column_config={
"revenue": st.column_config.NumberColumn(format="$%.2f"),
})
Percentage formatting: Use NumberColumn(format="percent") for 0-1 values, or format="%.2f%%" for already-multiplied values.
edited_df = st.data_editor(
df,
key="my_editor",
num_rows="dynamic", # allow adding/deleting rows
disabled=["id", "created"], # lock specific columns
)
Access edit details via st.session_state["my_editor"]["edited_rows"].
Double-input anti-pattern — assigning the result back to the same session state used as input causes every other edit to disappear:
# BAD: creates feedback loop — edits disappear on every other interaction
st.session_state.df = st.data_editor(st.session_state.df)
st.session_state.df["total"] = st.session_state.df["qty"] * st.session_state.df["price"]
# GOOD: use key parameter, compute from the returned value
edited = st.data_editor(df, key="editor")
edited["total"] = edited["qty"] * edited["price"]
st.dataframe(edited)
Use st.dataframe (not st.data_editor) for selection:
event = st.dataframe(df, on_select="rerun", selection_mode="multi-row")
selected_indices = event.selection.rows
selected_data = df.iloc[selected_indices]
Selection modes: "single-row", "multi-row", "single-column", "multi-column", "single-cell", "multi-cell".
When creating an empty DataFrame for st.data_editor, set explicit dtypes to avoid type inference issues:
df = pd.DataFrame({
"label": pd.Series(dtype="string"),
"amount": pd.Series(dtype="float"),
})
st.data_editor(df)
Keep important columns visible while scrolling horizontally:
st.dataframe(
df,
column_config={
"Title": st.column_config.TextColumn(pinned=True), # Always visible
"Rating": st.column_config.ProgressColumn(min_value=0, max_value=10),
},
hide_index=True,
)
Add chart_data and chart_type to metrics for visual context.
values = [700, 720, 715, 740, 762, 755, 780]
st.metric(
label="Developers",
value="762k",
delta="-7.42% (MoM)",
delta_color="inverse",
chart_data=values,
chart_type="line" # or "bar"
)
Note: Sparklines only show y-values and ignore x-axis spacing. Use them for evenly-spaced data (like daily or weekly snapshots). For irregularly-spaced time series, use a proper chart instead.
See dashboards.md for composing metrics into dashboard layouts.