DataLens

Color, size, and symbol encoding

Add a third or fourth variable to a chart with colour, marker size and symbols, and use colour to highlight instead of decorate.

Level: Intermediate · 7 min read

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

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:

ChannelBest forExpress argument
colour (hue)categories, or a continuous numbercolor=
sizea positive quantitysize=
symbol (shape)a few categoriessymbol=

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()
Key idea

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

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

Next: Faceting (small multiples) →