Inspired by @pascscha's original answer, I made some slight changes to his function, so there is no need for all x ticks to have the same number of bars. That is, we no longer presume that we have the same amount of bars to plot per x and the following function takes care of that by centering each bar in the corresponding x relative to its assigned amount of bars.
from matplotlib import pyplot as plt
def bar_plot(ax, data, colors=None, total_width=0.8, single_width=1, legend=True):
"""Draws a bar plot with multiple bars per data point.
Parameters
----------
ax : matplotlib.pyplot.axis
The axis we want to draw our plot on.
data: dictionary
A dictionary containing the data we want to plot. Keys are the names of the
data, the items is a list of the values.
Example:
```
data = {
"x":[1,2,3],
"y":[1,2,3],
"z":[1,2,3],
}
```
If there is a `None` value in the list, the bar will be missing for the corresponding `x` and the remaining bars
will be centered around the x tick.
colors : array-like, optional
A list of colors which are used for the bars. If None, the colors
will be the standard matplotlib color cyle. (default: None)
total_width : float, optional, default: 0.8
The width of a bar group. 0.8 means that 80% of the x-axis is covered
by bars and 20% will be spaces between the bars.
single_width: float, optional, default: 1
The relative width of a single bar within a group. 1 means the bars
will touch eachother within a group, values less than 1 will make
these bars thinner.
legend: bool, optional, default: True
If this is set to true, a legend will be added to the axis.
"""
# Check if colors where provided, otherwhise use the default color cycle
if colors is None:
colors = plt.rcParams["axes.prop_cycle"].by_key()["color"]
# Number of bars per group
n_bars = len(data)
# The width of a single bar
bar_width = total_width / n_bars
# List containing handles for the drawn bars, used for the legend
bars = []
# Build a bars_per_x dictionary depending on the number of values that are not None
bars_per_x = {}
for _, values_list in data.items():
for i, value in enumerate(values_list):
if value is not None:
if i not in bars_per_x:
bars_per_x[i] = 0
bars_per_x[i] += 1
# Instead of using i in calculating the offset, we now use the i_per_x[x]
i_per_x = {}
# Iterate over all data
for i, (name, values) in enumerate(data.items()):
# Draw a bar for every value of that type
for x, y in enumerate(values):
if x not in i_per_x:
i_per_x[x] = 0
if y is not None:
# The offset in x direction of that bar
x_offset = (i_per_x[x] - bars_per_x[x] / 2) * bar_width + bar_width / 2
bar = ax.bar(x + x_offset, y, width=bar_width * single_width, color=colors[i % len(colors)])
i_per_x[x] += 1
# Add a handle to the last drawn bar, which we'll need for the legend
bars.append(bar[0])
# Draw legend if we need
if legend:
ax.legend(bars, data.keys())
if __name__ == "__main__":
# Usage example:
data = {
"a": [1, 2, 3, 2, 1],
"b": [2, 3, 4, 3, 1],
"d": [5, 9, 2, 1, 8],
}
fig, ax = plt.subplots(1, 2)
bar_plot(ax[0], data, total_width=0.8, single_width=0.9)
ax[0].set_title("Complete data")
# If one of the bars is missing, we put None
data_with_missing_bars = {
"a": [1, None, None, None, None],
"b": [2, 3, 4, 3, 1],
"d": [5, 9, None, 1, 8],
}
bar_plot(ax[1], data_with_missing_bars, total_width=0.8, single_width=0.9)
ax[1].set_title("With missing bars")
plt.show()
By doing so, you can get a plot like this:
