The idea
Plotly is the charting library this course uses for interactive charts: every chart can be hovered, zoomed and downloaded, in a notebook, a Streamlit app or a web page. It comes with two ways to build a chart:
- Plotly Express (
import plotly.express as px): one function call per chart, straight from a pandas DataFrame. Fast, sensible defaults. - Graph Objects (
import plotly.graph_objects as go): build the chart piece by piece. More code, full control.
They aren't rivals. Express is built on top of Graph Objects: every px function returns a Graph Objects figure. So the real skill is knowing when Express is enough, and how to reach into the figure when it isn't.
The same chart, twice
Revenue per category, the Express way:
import pandas as pd
import plotly.express as px
by_category = pd.DataFrame({
"category": ["Electronics", "Kitchen", "Coffee", "Home", "Stationery"],
"revenue": [5113000, 2610000, 1805000, 1560000, 748000],
})
fig = px.bar(by_category, x="category", y="revenue", title="Completed revenue by category")
fig.show()
The Graph Objects way:
import plotly.graph_objects as go
fig = go.Figure(
data=[go.Bar(x=by_category["category"], y=by_category["revenue"])],
layout=go.Layout(title="Completed revenue by category"),
)
fig.show()
Press Try it on both: the charts look nearly identical. The difference is in how you describe them:
- Express takes a DataFrame and column names. It works out the rest: axis titles from the column names, hover labels, colours.
- Graph Objects takes arrays of values in a trace (
go.Bar) and a layout. Nothing is automatic: notice the Graph Objects chart has no axis titles until you add them.
What a figure is made of
Every Plotly figure, however it was made, has the same two parts:
fig = px.bar(by_category, x="category", y="revenue")
print(type(fig).__name__)
print(len(fig.data), fig.data[0].type)
print(fig.layout.xaxis.title.text, "|", fig.layout.yaxis.title.text)
Figure
1 bar
category | revenue
| Part | Holds | Example |
|---|---|---|
fig.data | a list of traces: the marks (bars, lines, dots) and their values | one bar trace |
fig.layout | everything around the marks: title, axes, legend, colours, size | axis titles category, revenue |
A Plotly figure is just a structured description (data + layout) that the browser draws. Express writes that description for you; Graph Objects lets you write it yourself. Either way, you can change any part afterwards.
That last point is the most practical one. Start with Express, then adjust the figure with methods like update_layout, update_traces and add_trace:
fig = px.bar(by_category, x="category", y="revenue", title="Completed revenue by category")
fig.add_trace(go.Scatter(x=by_category["category"], y=[2000000] * 5,
mode="lines", name="Target"))
fig.update_layout(yaxis_title="Revenue (Rp)")
fig.show()
An Express bar chart, plus a Graph Objects line for the target, plus a layout tweak. This "Express first, then customise" style is how most professional Plotly code looks.
Express loves tidy data
Express is quickest when your DataFrame is tidy (also called long): one row per observation, one column per variable. Compare two shapes of the same numbers:
wide = pd.DataFrame({
"month": ["Jan", "Feb", "Mar"],
"web": [1200, 1100, 900],
"app": [500, 650, 640],
})
long = wide.melt(id_vars="month", var_name="channel", value_name="revenue")
print(wide)
print(long)
month web app
0 Jan 1200 500
1 Feb 1100 650
2 Mar 900 640
month channel revenue
0 Jan web 1200
1 Feb web 1100
2 Mar web 900
3 Jan app 500
4 Feb app 650
5 Mar app 640
With the long version, one Express call draws a line per channel, coloured and labelled automatically:
fig = px.line(long, x="month", y="revenue", color="channel", markers=True)
fig.show()
color="channel" says "split the data by this column". That only works when "channel" is a column, which is why melt (from the pandas reshaping module) is the most common preparation step before an Express chart.
When to use which
| Situation | Use |
|---|---|
| Exploring data, standard charts (bar, line, scatter, histogram, box) | Express |
| Data is in a tidy DataFrame | Express |
| A standard chart that needs a few tweaks | Express, then update_* / add_trace |
| Mixing unusual traces, building a chart trace by trace | Graph Objects |
| Values aren't in a DataFrame, or need precise control of every trace | Graph Objects |
| Subplots of different chart types, dropdowns, custom interactivity | Graph Objects (later modules) |
Start with Express nearly every time. Reach for Graph Objects when Express can't express it.
Why this matters for clean code
A chart is code too, and the same rules apply: the shortest clear version wins. One px call on a tidy DataFrame is easier to read and change than forty lines of trace building. Keep data preparation (filtering, melting, aggregating) separate from charting, so each step does one thing.
Recap
px(Express) builds a chart from a DataFrame in one call;go(Graph Objects) builds it from traces.- Every
pxfunction returns ago.Figure; you can modify it afterwards. - A figure =
fig.data(traces) +fig.layout(everything else). - Express works best with tidy/long data; use
meltto get there;color=splits by a column. - Default to Express; customise with
update_layout,update_traces,add_trace; use Graph Objects for full control.