The idea
Bar charts and histograms look alike, rectangles side by side, but they answer different questions:
- A bar chart compares categories: revenue per product category, orders per city.
- A histogram shows the distribution of one number: how order values are spread, how many orders fall between Rp 100.000 and Rp 200.000.
This module uses Toko Kita's order lines, sales.csv:
import pandas as pd
import plotly.express as px
sales = pd.read_csv("sales.csv")
completed = sales[sales["status"] == "completed"]
print(completed[["product", "category", "quantity", "revenue", "channel"]].head(3))
product category quantity revenue channel
0 Desk Organizer Stationery 1 89000 app
1 Arabica Gayo 250g Coffee 3 285000 web
2 Pour-over Kettle Kitchen 3 960000 app
A bar chart from aggregated data
The manager's first question is usually "which category earns the most?". That's a comparison between categories, so a bar chart fits. One thing to know: px.bar draws one bar per row of the DataFrame you give it. The sales data has one row per order line, so you add up revenue per category first, then chart the result:
by_category = (completed.groupby("category", as_index=False)["revenue"].sum()
.sort_values("revenue", ascending=False))
fig = px.bar(by_category, x="category", y="revenue", title="Completed revenue by category")
fig.show()
Two habits are already in this code:
- Aggregate in pandas, draw in Plotly. The chart gets exactly one row per bar.
- Sort the bars. Unless categories have a natural order (sizes, months, ratings), sort by value so the ranking is visible at a glance.
If you pass unaggregated rows, px.bar stacks one thin segment per row. It looks almost right, but hovering shows dozens of slivers. Aggregate first.
Horizontal bars for long labels
Product names are long. Rotated labels are hard to read; horizontal bars aren't:
by_product = (completed.groupby("product", as_index=False)["revenue"].sum()
.sort_values("revenue"))
fig = px.bar(by_product, x="revenue", y="product", orientation="h",
title="Completed revenue by product")
fig.show()
Swap x and y, and set orientation="h". Note the sort is ascending here: Plotly draws the first row at the bottom, so ascending order puts the biggest bar on top.
Labels on the bars
text_auto=True writes each bar's value on it. A format string controls how:
fig = px.bar(by_category, x="category", y="revenue", text_auto=".3s",
title="Completed revenue by category")
fig.show()
print(fig.data[0].texttemplate)
%{y:.3s}
.3s means "3 significant digits with an SI suffix", so 5113000 shows as 5.11M. With values on the bars, readers don't have to trace gridlines, and you can often drop the y-axis entirely.
Two categories: grouped or stacked
The next question is often "and how does that split between the website and the app?". You can show a second category inside each bar by colouring it, with color=:
by_cat_channel = completed.groupby(["category", "channel"], as_index=False)["revenue"].sum()
fig = px.bar(by_cat_channel, x="category", y="revenue", color="channel",
title="Revenue by category and channel")
fig.show()
print(fig.layout.barmode)
relative
Express stacks coloured bars by default (barmode="relative"). Stacked bars show the total and each part's contribution. To compare the parts with each other, put them side by side:
fig = px.bar(by_cat_channel, x="category", y="revenue", color="channel",
barmode="group", title="Revenue by category and channel")
fig.show()
| Use | When the question is |
|---|---|
stacked (barmode="relative") | "What's the total, and what's it made of?" |
grouped (barmode="group") | "Is web bigger than app in each category?" |
Histograms: the shape of one number
A different kind of question: are most orders small, big, or somewhere in between? You're not comparing categories now, you're looking at the spread of one number across all the orders. That's what a histogram shows:
fig = px.histogram(completed, x="revenue", nbins=10, title="Order line values")
fig.show()
px.histogram takes raw rows (no aggregating first): it sorts the values into bins (ranges) and counts the rows in each. nbins suggests how many bins to use. The shape tells a story: most lines are small, with a long tail of a few big ones. That "skewed right" shape is typical of money data, and it's why the median often describes a "typical" order better than the mean.
A histogram can also sum instead of count, which turns it into a quick bar chart of raw rows:
fig = px.histogram(completed, x="category", y="revenue", histfunc="sum",
title="Revenue by category (from raw rows)")
fig.show()
That's handy for exploration. For final charts, aggregate explicitly in pandas, so the numbers are easy to check.
Bars must start at zero
A bar's length encodes its value. If the axis starts at 1,000,000 instead of 0, a bar for 2,000,000 looks twice as long as one for 1,500,000, though it's only a third bigger. Plotly starts bar axes at zero by default; don't change that for bars. (Line charts are different: they encode change, so a zoomed-in axis can be fine.)
Why this matters for clean code
The cleanest chart code has three visible steps: filter (completed), aggregate (groupby + sum, sorted), draw (one px.bar call). Each step can be checked on its own, and the chart can't silently double-count, because you looked at the aggregated table before drawing it.
Recap
- Bar chart = compare categories; histogram = distribution of one number.
- Aggregate in pandas, then
px.bar; sort bars by value unless the order is natural. orientation="h"(with x and y swapped) for long labels; sort ascending to put the biggest on top.text_auto=Trueor a format like".3s"labels the bars.color=splits bars; stacked by default,barmode="group"for side by side.px.histogram(df, x=..., nbins=...)bins raw values; bar axes start at zero.