2025, Oct 15 21:00

How to Plot Two 2D Histograms Side by Side in Matplotlib with a Single Shared Colorbar (No Pyplot Mixing)

Learn how to plot two Matplotlib 2D histograms side by side with one shared colorbar. Avoid pyplot/Axes mixing, use a shared Normalize and hist2d mappable.

When you place two 2D histograms side by side and expect a single shared colorbar, it’s easy to run into confusing behavior if you mix matplotlib’s explicit Axes interface with the implicit pyplot state machine. The most common symptom is that one subplot remains empty while the other shows your plot, and any colorbar logic refuses to cooperate.

Problem setup

The task is to read a CSV, derive day of week and week of year, and then visualize two weighted 2D histograms in a single row to compare how two values evolve over a year, with one shared colorbar.

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from datetime import datetime
weekday_names = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
dataset = pd.read_csv("Mod4Data.csv", nrows=366)
# Derive date parts
dataset["Date"] = dataset["Day"].apply(pd.to_datetime)
dataset["Day of Week"] = dataset["Date"].dt.dayofweek
dataset['week_of_year'] = dataset['Date'].dt.strftime('%W')
dataset['Month'] = dataset['Date'].dt.month
# Ensure numeric types
dataset['Value1'] = dataset['Value1'].astype(float)
dataset['Value2'] = dataset['Value2'].astype(float)
dataset['week_of_year'] = dataset['week_of_year'].astype(float)
canvas, axes_grid = plt.subplots(1, 2, figsize=(18, 10))
# Left plot attempt
axes_grid[0] = plt.hist2d(
    dataset["Day of Week"],
    dataset["week_of_year"],
    bins=[7, 52],
    weights=dataset["Value1"],
    vmin=1000,
    vmax=20000,
)
plt.title("Value 1 Amounts")
plt.yticks([2, 6.3348, 10.66, 14.99, 19.32, 23.65, 27.98, 32.31, 36.64, 40.97, 45.3, 49.63], month_names)
plt.xticks([0, 1, 2, 3, 4, 5, 6], weekday_names, rotation=45)
# Right plot attempt
second_hist = plt.hist2d(
    dataset["Day of Week"],
    dataset["week_of_year"],
    bins=[7, 52],
    weights=dataset["Value2"],
    vmin=1000,
    vmax=20000,
)
plt.title("Value 2 Amounts")
plt.yticks([2, 6.3348, 10.66, 14.99, 19.32, 23.65, 27.98, 32.31, 36.64, 40.97, 45.3, 49.63], month_names)
plt.xticks([0, 1, 2, 3, 4, 5, 6], weekday_names, rotation=45)

What goes wrong and why

There are two separate issues. First, the code switches between the explicit Axes workflow returned by plt.subplots and the implicit pyplot API that draws on the current axes. Assigning the result of plt.hist2d to axes_grid[0] overwrites the Axes object with the tuple that hist2d returns, so the original left subplot is lost. Subsequent pyplot calls such as plt.title and the second plt.hist2d render to whatever the current axes happens to be, not to the intended subplots.

Second, a single shared colorbar requires a proper mappable from one of the histograms. In the case of hist2d, the mappable is the fourth return value. A shared colorbar also assumes both plots map numbers to colors identically, which is ensured by using the same normalization.

Fixing the layout and the colorbar

The reliable approach is to stick to the explicit Axes interface throughout, capture the mappable from hist2d, and use a shared normalization across both plots so a single colorbar is valid.

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from matplotlib.colors import Normalize
weekday_names = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
# Input and feature engineering
frame = pd.read_csv("Mod4Data.csv", nrows=366)
frame["Date"] = frame["Day"].apply(pd.to_datetime)
frame["Day of Week"] = frame["Date"].dt.dayofweek
frame['week_of_year'] = frame['Date'].dt.strftime('%W')
frame['Month'] = frame['Date'].dt.month
frame['Value1'] = frame['Value1'].astype(float)
frame['Value2'] = frame['Value2'].astype(float)
frame['week_of_year'] = frame['week_of_year'].astype(float)
# Shared normalization ensures consistent color mapping across subplots
shared_norm = Normalize(vmin=1000, vmax=20000)
fig, ax_array = plt.subplots(nrows=1, ncols=2, figsize=(18, 10), layout="constrained")
# Left heatmap
_, _, _, img_left = ax_array[0].hist2d(
    frame["Day of Week"],
    frame["week_of_year"],
    bins=[7, 52],
    weights=frame["Value1"],
    norm=shared_norm,
)
ax_array[0].set_title("Value 1 Amounts")
ax_array[0].set_yticks([2, 6.3348, 10.66, 14.99, 19.32, 23.65, 27.98, 32.31, 36.64, 40.97, 45.3, 49.63])
ax_array[0].set_yticklabels(month_names)
ax_array[0].set_xticks([0, 1, 2, 3, 4, 5, 6])
ax_array[0].set_xticklabels(weekday_names, rotation=45)
# Right heatmap
_, _, _, img_right = ax_array[1].hist2d(
    frame["Day of Week"],
    frame["week_of_year"],
    bins=[7, 52],
    weights=frame["Value2"],
    norm=shared_norm,
)
ax_array[1].set_title("Value 2 Amounts")
ax_array[1].set_yticks([2, 6.3348, 10.66, 14.99, 19.32, 23.65, 27.98, 32.31, 36.64, 40.97, 45.3, 49.63])
ax_array[1].set_yticklabels(month_names)
ax_array[1].set_xticks([0, 1, 2, 3, 4, 5, 6])
ax_array[1].set_xticklabels(weekday_names, rotation=45)
# Single shared colorbar sourced from one mappable, applied to both axes
plt.colorbar(img_right, ax=ax_array, location="top")
plt.show()

Why this matters

Keeping a consistent API style avoids silent state bugs that are hard to spot and even harder to reproduce. When you want one colorbar across multiple histograms, the color mapping has to be identical, otherwise the same color would mean different values on each subplot. Using the same normalization guarantees that consistency, and providing a single mappable to the colorbar makes its intent explicit.

Takeaways

Use the Axes objects returned by plt.subplots and call plotting methods on them instead of switching back to the implicit pyplot interface. Do not overwrite Axes references with returned tuples from plotting calls. For a shared colorbar, pass the mappable returned by hist2d and apply a shared normalization so both panels map data to colors in the same way. When seeking help, a minimal reproducible example with a clear show of the figure makes the problem and the fix straightforward.

The article is based on a question from StackOverflow by Jen Reif and an answer by Liris.