The idea
A scatter plot shows two variables: x and y. Real questions often involve more: "do expensive products sell fewer units, and does that differ by category?" Plotly lets you encode extra variables in visual channels:
| Channel | Best for | Express argument |
|---|---|---|
| colour (hue) | categories, or a continuous number | color= |
| size | a positive quantity | size= |
| symbol (shape) | a few categories | symbol= |
Each extra channel adds information, and also effort for the reader. The skill is knowing which channel suits which variable, and when to stop.
import pandas as pd
import plotly.express as px
sales = pd.read_csv("sales.csv")
completed = sales[sales["status"] == "completed"]
products = (completed.groupby(["product", "category"], as_index=False)
.agg(price=("unit_price", "first"), units=("quantity", "sum"), revenue=("revenue", "sum")))
print(products.head())
product category price units revenue
0 Arabica Gayo 250g Coffee 95000 7 665000
1 Bamboo Plant Home 60000 4 240000
2 Desk Lamp LED Electronics 245000 6 1470000
3 Desk Organizer Stationery 89000 3 267000
4 French Press 600ml Kitchen 185000 2 370000
Colour for categories
A scatter plot of price against units sold shows each product as a dot. But are the coffees clustered in one corner and the electronics in another? Colouring the dots by category answers that at a glance:
fig = px.scatter(products, x="price", y="units", color="category", hover_name="product",
title="Price vs units sold, by category")
fig.show()
print(len(fig.data), [t.name for t in fig.data])
5 ['Coffee', 'Home', 'Electronics', 'Stationery', 'Kitchen']
With a categorical color=, Express creates one trace per category, each with its own colour and legend entry. Clicking a legend entry hides or shows that category, which is free interactivity.
Fix specific colours for specific categories with color_discrete_map, so "Coffee" is always brown in every chart of your report:
palette = {"Coffee": "#8B5E3C", "Kitchen": "#5669FD", "Electronics": "#262B80",
"Home": "#1FA37A", "Stationery": "#F59E0B"}
fig = px.scatter(products, x="price", y="units", color="category",
color_discrete_map=palette, hover_name="product")
fig.show()
Consistent colours across charts are a promise to the reader: "this colour always means this category". Define the palette once, in one place, and reuse it.
Colour for numbers
Pass a numeric column, and colour becomes a gradient with a colour bar:
fig = px.scatter(products, x="price", y="units", color="revenue",
color_continuous_scale="Blues", hover_name="product")
fig.show()
print(len(fig.data), fig.layout.coloraxis.colorbar.title.text)
1 revenue
One trace this time, with a continuous scale. Use sequential scales (light to dark, like Blues, Viridis) for quantities that go from low to high. Use diverging scales (like RdBu) only for values with a meaningful middle, such as growth around 0%.
Size for quantities
size= scales marker area by a positive number, turning a scatter into a bubble chart:
fig = px.scatter(products, x="price", y="units", size="revenue", color="category",
hover_name="product", size_max=40)
fig.show()
size_max sets the largest bubble's size in pixels. Size is the least precise channel: people can tell big from small, but not 2.1M from 2.4M. Use it for rough magnitude, and keep the exact number in the hover.
Symbol for a few categories
Colour isn't the only way to tell groups apart. Not everyone sees colours the same way, and charts often end up printed in black and white. symbol= gives each category its own marker shape, here to show the website and app channels:
by_channel = (completed.groupby(["product", "channel"], as_index=False)
.agg(price=("unit_price", "first"), units=("quantity", "sum")))
fig = px.scatter(by_channel, x="price", y="units", symbol="channel", color="channel",
hover_name="product")
fig.show()
Symbols work for two or three categories. Beyond that, readers can't tell a diamond from a pentagon at a glance. Using colour and symbol for the same variable (as above) is good practice: it still works for colour-blind readers and when printed in black and white.
Highlight, don't decorate
Colour is most powerful when it highlights. To make a point about one category, colour it and leave everything else grey:
products["highlight"] = products["category"].where(products["category"] == "Electronics", "Other")
fig = px.scatter(products, x="price", y="units", color="highlight",
color_discrete_map={"Electronics": "#5669FD", "Other": "#C5CAD6"},
hover_name="product", title="Electronics: high price, steady sales")
fig.show()
The title states the insight, and the colour points to it. That's a chart that argues something, instead of asking the reader to find it.
How many channels is too many?
x + y + colour is the sweet spot for most charts. Add size or symbol only when the extra variable answers the question being asked. If a chart needs a paragraph to explain its encodings, split it into two charts, or use facets (next module).
Why this matters for clean code
Treat colours like constants: define a palette dictionary once (CATEGORY_COLORS), import it everywhere, and every chart in a report stays consistent. Hard-coded hex values scattered across notebooks drift apart, and then "Coffee" is brown in one chart and blue in the next.
Recap
- Categorical
color=makes one trace per category with a legend; numericcolor=makes a gradient. color_discrete_mappins category colours;color_continuous_scalepicks a gradient (sequential for amounts, diverging around a midpoint).size=(withsize_max) encodes rough magnitude;symbol=suits 2-3 categories.- Pair colour with symbol for accessibility.
- Use colour to highlight one thing; grey out the rest; state the insight in the title.