2025, Nov 04 09:00
Exclude Totals from the Color Scale in Plotly Heatmaps while Keeping Grand Totals Visible (pandas pivot)
Learn to build a Plotly heatmap from a pandas pivot that keeps grand totals visible as text while excluding them from the color scale using go.Heatmap.
Plotly heatmaps are great for visualizing pivoted metrics, but a common pitfall appears when you add a per-row totals column. That single column ends up participating in the color scale, which makes the rest of the cells visually washed out or otherwise uninformative. The goal is to keep the totals visible, yet exclude them from coloring.
Problem setup
Consider a pivoted pandas DataFrame where we add a totals column and try to plot it with Plotly Express. The totals are useful context, but they also get colored, which disrupts the scale.
import numpy as np
import pandas as pd
import plotly.express as px
origin_cities = ['London', 'Tokio', 'Seoul', 'Paris', 'Tashkent', 'Washington', 'Moscow']
current_cities = ['London', 'Madrid', 'Tashkent', 'Seoul', 'Paris', 'Toronto', 'Washington', 'Istanbul', 'Hanoi', 'Manilla', 'Delhi', 'Busan', 'Moscow']
src_city = np.random.choice(origin_cities, size=1000)
dst_city = np.random.choice(current_cities, size=1000)
pay = np.random.randint(1300, 6900, size=1000)
base_df = pd.DataFrame({'src_city': src_city, 'dst_city': dst_city, 'pay': pay})
wide_tbl = pd.pivot_table(
base_df,
index='src_city',
columns='dst_city',
values='pay',
aggfunc='sum',
fill_value=0
)
wide_tbl['grand_total'] = wide_tbl.sum(axis=1)
col_order = ['grand_total'] + [c for c in wide_tbl.columns if c != 'grand_total']
wide_tbl = wide_tbl[col_order]
px.imshow(wide_tbl, text_auto=True)
What’s actually going wrong
The heatmap uses all provided values to compute and apply its color scale. When totals are placed alongside regular cells, they influence that scale. As a result, the heatmap colors no longer reflect the distribution you intended to highlight across the non-total columns.
Solution
The remedy is to decouple what gets colored from what gets displayed as text. Create one DataFrame for the heatmap values and set the totals column to None so it doesn’t receive a color. Create a separate DataFrame for the text labels so the totals still show up numerically. Because this approach relies on go.Heatmap, you also need to pass arrays, and reverse the row order: go.Heatmap renders the first array row at the bottom of the heatmap. Since the totals column becomes transparent, turning off gridlines avoids visual clutter; you can also adjust the background or theme if you want the totals column to read differently.
import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
origin_cities = ['London', 'Tokio', 'Seoul', 'Paris', 'Tashkent', 'Washington', 'Moscow']
current_cities = ['London', 'Madrid', 'Tashkent', 'Seoul', 'Paris', 'Toronto', 'Washington', 'Istanbul', 'Hanoi', 'Manilla', 'Delhi', 'Busan', 'Moscow']
np.random.seed(42)
src_city = np.random.choice(origin_cities, size=1000)
dst_city = np.random.choice(current_cities, size=1000)
pay = np.random.randint(1300, 6900, size=1000)
base_df = pd.DataFrame({'src_city': src_city, 'dst_city': dst_city, 'pay': pay})
wide_tbl = pd.pivot_table(
base_df,
index='src_city',
columns='dst_city',
values='pay',
aggfunc='sum',
fill_value=0
)
wide_tbl['grand_total'] = wide_tbl.sum(axis=1)
col_order = ['grand_total'] + [c for c in wide_tbl.columns if c != 'grand_total']
wide_tbl = wide_tbl[col_order]
# values for coloring: hide totals by setting them to None
heat_vals = wide_tbl.iloc[::-1].copy()
heat_vals['grand_total'] = None
# text for display: keep all values as strings
heat_text = wide_tbl.iloc[::-1].astype(str).copy()
z_data = heat_vals.to_numpy()
text_data = heat_text.to_numpy()
y_labels = heat_text.index.values
x_labels = heat_text.columns.values
fig = go.Figure()
fig.add_trace(go.Heatmap(
z=z_data,
y=y_labels,
x=x_labels,
text=text_data,
texttemplate="%{text:.2s}",
))
fig.update_layout(
xaxis=dict(showgrid=False),
yaxis=dict(showgrid=False),
)
fig.show()
Why this matters
Analytical heatmaps often need both visual gradients and contextual totals. Letting totals drive the color scale undermines the comparisons you want across regular cells. Separating what gets colored from what gets displayed preserves readability without compromising context.
Wrap-up
If a totals column is skewing your Plotly heatmap, keep the numbers visible as text but exclude them from the z-values by setting them to None. Switch to go.Heatmap, provide arrays for values and text, reverse row order to match expectations, and hide gridlines for a cleaner look. This small refactor keeps the color scale meaningful and the totals exactly where analysts expect to see them.