The idea
A default chart speaks in column names: revenue, order_date, qty_sold. Your audience speaks in "Revenue (Rp)", "Order date" and "Units sold". The difference between a chart people glance at and one they trust is often just labels, number formats and a good hover box. All three take a line or two.
import pandas as pd
import plotly.express as px
sales = pd.read_csv("sales.csv")
completed = sales[sales["status"] == "completed"]
monthly = (completed.assign(month=completed["order_date"].str[:7])
.groupby("month", as_index=False)
.agg(revenue=("revenue", "sum"), orders=("order_id", "nunique")))
print(monthly)
month revenue orders
0 2024-01 1693000 6
1 2024-02 1752000 4
2 2024-03 1027000 2
3 2024-04 3987000 7
4 2024-05 2552000 5
5 2024-06 825000 2
labels=: rename columns for the chart
Express uses column names as axis titles, legend titles and hover labels. labels= maps column names to display names, everywhere at once:
fig = px.bar(monthly, x="month", y="revenue",
labels={"month": "Month", "revenue": "Revenue (Rp)"},
title="Completed revenue per month")
fig.show()
print(fig.layout.xaxis.title.text, "|", fig.layout.yaxis.title.text)
Month | Revenue (Rp)
Prefer labels= over renaming DataFrame columns just for a chart: your data keeps code-friendly names, and the chart gets human ones.
You can also set titles after creating the figure, which works for Graph Objects figures too:
fig.update_xaxes(title_text="Month (2024)")
fig.update_yaxes(title_text="Revenue (Rupiah)")
Number formats on the axis
Plotly writes 4,000,000 as 4M by default. For a Rupiah audience, you might want separators and a prefix. tickformat uses the same mini-language as the text_auto formats in the previous module:
| Format | 3987000 shows as | Use for |
|---|---|---|
"," | 3,987,000 | full numbers with separators |
".2s" | 4.0M | compact |
".1%" | (of 0.237) 23.7% | shares stored as fractions |
"d" | 3987000 | plain integers |
fig = px.bar(monthly, x="month", y="revenue",
labels={"month": "Month", "revenue": "Revenue"})
fig.update_yaxes(tickprefix="Rp ", tickformat=",")
fig.show()
print(repr(fig.layout.yaxis.tickprefix), repr(fig.layout.yaxis.tickformat))
'Rp ' ','
tickprefix and ticksuffix add text before or after each tick label, like "Rp " or " units".
Plotly uses an English-style comma as the thousands separator. Indonesian convention is a dot (Rp 3.987.000). To switch the whole chart, set fig.update_layout(separators=",."): the first character is the decimal mark, the second the thousands separator.
Category order
Axis categories appear in the order of the data, or alphabetically. Force an order with category_orders:
by_channel = completed.groupby("channel", as_index=False)["revenue"].sum()
fig = px.bar(by_channel, x="channel", y="revenue",
category_orders={"channel": ["web", "app"]})
fig.show()
Useful for natural orders that aren't alphabetical: weekdays, sizes (S, M, L), ratings, or "Q1, Q2, Q3, Q4".
Hover text: what appears on mouse-over
The hover box is where interactive charts beat static ones. Express shows the x and y values by default. Add more context with hover_name (bold title) and hover_data (extra rows):
fig = px.scatter(monthly, x="orders", y="revenue", hover_name="month",
hover_data={"orders": True, "revenue": ":,"},
labels={"orders": "Orders", "revenue": "Revenue (Rp)"})
fig.show()
hover_name="month"puts the month in bold at the top.hover_datais a dict:Trueshows a column,Falsehides one, and a format string like":,"formats it (the colon starts the format).
hovertemplate: full control
For complete control, write the hover text yourself with hovertemplate. Values are inserted with %{...}:
fig = px.bar(monthly, x="month", y="revenue")
fig.update_traces(hovertemplate="<b>%{x}</b><br>Revenue: Rp %{y:,}<extra></extra>")
fig.show()
print(fig.data[0].hovertemplate)
<b>%{x}</b><br>Revenue: Rp %{y:,}<extra></extra>
%{x}and%{y:,}insert values, with an optional format after the colon.<br>is a line break,<b>...</b>bold.<extra></extra>removes the grey trace-name box that otherwise appears beside the hover.
To show a column that isn't on an axis, pass it as custom_data in Express and reference it as %{customdata[0]}:
fig = px.bar(monthly, x="month", y="revenue", custom_data=["orders"])
fig.update_traces(hovertemplate="%{x}: Rp %{y:,} from %{customdata[0]} orders<extra></extra>")
fig.show()
Why this matters for clean code
Labels are the user interface of your analysis. A chart titled "revenue by month" with an axis reading 4M invites "4 million what?" A clean chart answers the questions before they're asked: what is measured, in which unit, over what period. Keep the formatting in a few explicit lines after the px call, so the "what" (data) and the "how it looks" (labels) stay separate and easy to change.
Recap
labels={"col": "Nice name"}renames columns in axes, legends and hover at once.update_xaxes(title_text=...)/update_yaxes(...)set titles on any figure.tickformat(",",".2s",".1%"),tickprefix,ticksuffixformat axis numbers;separators=",."switches to Indonesian style.category_orders={"col": [...]}fixes category order.hover_name,hover_dataadd context;hovertemplatewith%{x},%{y:,},%{customdata[0]}gives full control.