How to add group labels for bar charts
Since I could not find a built-in solution for this in matplotlib, I coded my own:
#!/usr/bin/env python
from matplotlib import pyplot as plt
def mk_groups(data):
try:
newdata = data.items()
except:
return
thisgroup = []
groups = []
for key, value in newdata:
newgroups = mk_groups(value)
if newgroups is None:
thisgroup.append((key, value))
else:
thisgroup.append((key, len(newgroups[-1])))
if groups:
groups = [g + n for n, g in zip(newgroups, groups)]
else:
groups = newgroups
return [thisgroup] + groups
def add_line(ax, xpos, ypos):
line = plt.Line2D([xpos, xpos], [ypos + .1, ypos],
transform=ax.transAxes, color='black')
line.set_clip_on(False)
ax.add_line(line)
def label_group_bar(ax, data):
groups = mk_groups(data)
xy = groups.pop()
x, y = zip(*xy)
ly = len(y)
xticks = range(1, ly + 1)
ax.bar(xticks, y, align='center')
ax.set_xticks(xticks)
ax.set_xticklabels(x)
ax.set_xlim(.5, ly + .5)
ax.yaxis.grid(True)
scale = 1. / ly
for pos in xrange(ly + 1): # change xrange to range for python3
add_line(ax, pos * scale, -.1)
ypos = -.2
while groups:
group = groups.pop()
pos = 0
for label, rpos in group:
lxpos = (pos + .5 * rpos) * scale
ax.text(lxpos, ypos, label, ha='center', transform=ax.transAxes)
add_line(ax, pos * scale, ypos)
pos += rpos
add_line(ax, pos * scale, ypos)
ypos -= .1
if __name__ == '__main__':
data = {'Room A':
{'Shelf 1':
{'Milk': 10,
'Water': 20},
'Shelf 2':
{'Sugar': 5,
'Honey': 6}
},
'Room B':
{'Shelf 1':
{'Wheat': 4,
'Corn': 7},
'Shelf 2':
{'Chicken': 2,
'Cow': 1}
}
}
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
label_group_bar(ax, data)
fig.subplots_adjust(bottom=0.3)
fig.savefig('label_group_bar_example.png')
The mk_groups
function takes a dictionary (or anything with an items() method, like collections.OrderedDict
) and converts it to a data format that is then used to create the chart. It is basically a list of the form:
[ [(label, bars_to_span), ...], ..., [(tick_label, bar_value), ...] ]
The add_line
function creates a vertical line in the subplot at the specified positions (in axes coordinates).
The label_group_bar
function takes a dictionary and creates the bar chart in the subplot with the labels beneath. The result from the example then looks like this.
Easier or better solutions and suggestions are still very much appreciated.
How to add two tiers of labels for matplotlib stacked group barplot
A simple solution would be to concatenate all the x-values, all the bar-heights and all the tick labels. And then draw them in one go (there is no need for sorting):
import matplotlib.pyplot as plt
import numpy as np
width = 0.25
x = np.arange(1, 7)
fig, ax = plt.subplots(figsize=(10, 6))
tick_labels_1 = ['1'] * len(x)
tick_labels_2 = ['2'] * len(x)
tick_labels_3 = ['3'] * len(x)
shift1_rbc = np.random.uniform(1100, 1200, 6)
shift2_rbc = np.random.uniform(900, 1000, 6)
shift3_rbc = np.random.uniform(1000, 1100, 6)
shift1_plt = np.random.uniform(600, 700, 6)
shift2_plt = np.random.uniform(400, 500, 6)
shift3_plt = np.random.uniform(500, 600, 6)
shift1_ffp = np.random.uniform(250, 300, 6)
shift2_ffp = np.random.uniform(150, 200, 6)
shift3_ffp = np.random.uniform(200, 250, 6)
all_x = np.concatenate([x - 0.4, x - 0.1, x + 0.2])
ax.bar(all_x, np.concatenate([shift1_rbc, shift2_rbc, shift3_rbc]), width,
tick_label=tick_labels_1 + tick_labels_2 + tick_labels_3,
color='crimson', label='red')
ax.bar(all_x, np.concatenate([shift1_plt, shift2_plt, shift3_plt]),
width * .7, color='dodgerblue', label='blue')
ax.bar(all_x, np.concatenate([shift1_ffp, shift2_ffp, shift3_ffp]),
width * .5, color='limegreen', label='green')
ax.margins(x=0.02)
ax.legend(title='Data', bbox_to_anchor=(0.99, 1), loc='upper left')
for spine in ['top', 'right']:
ax.spines[spine].set_visible(False)
ax.set_xticks(x - 0.1001, minor=True)
ax.set_xticklabels(['January', 'February', 'March', 'April', 'May', 'June'], minor=True)
ax.tick_params(axis='x', which='minor', length=0, pad=18)
plt.tight_layout()
plt.show()
PS: To get 3 layers of labels, one could use newlines:
tick_labels_1 = ['1\n4\n7'] * len(x)
tick_labels_2 = ['2\n5\n8'] * len(x)
tick_labels_3 = ['3\n6\n9'] * len(x)
How to add multiple data labels in a bar chart
- The code for the extra plot formatting has been left out, because it's not relevant for the answer. It can be added back, as per your requirements.
- Each
.bar_label
colors the label globally, so unlike this answer, a second.bar_label
needs to be added for the percent change, with a differentcolor
andpadding
- For each case-to-case, calculate the percent change, and set the string format in a list comprehension.
- Set the list of string formatted calculations to the
labels
parameter in.bar_label
.
- Set the list of string formatted calculations to the
- Given the code in the OP, 6 lines of code need to be added, 3 for creating the list of labels, and 3 for adding the labels to the plot.
- Additional resources:
- matplotlib: Bar Label Demo
- Adding value labels on a matplotlib bar chart
- Tested in
python 3.8.11
,matplotlib 3.4.3
change_case0_to_case1_system1 = np.subtract(value_case1_system1, value_case0_system1)
# add list of string formatted percent change calculation
per_change_case0_to_case1_system1 = [f'({v}%)' for v in (change_case0_to_case1_system1 / value_case0_system1).round(2)*100]
change_case1_to_case2_system1 = np.subtract(value_case2_system1, value_case1_system1)
# add list of string formatted percent change calculation
per_change_case1_to_case2_system1 = [f'({v}%)' for v in (change_case1_to_case2_system1 / value_case1_system1).round(2)*100]
change_case1_to_case2_system2 = np.subtract(value_case2_system2, value_case1_system2)
# add list of string formatted percent change calculation
per_case1_to_case2_system2 = [f'({v}%)' for v in (change_case1_to_case2_system2 / value_case1_system2).round(2)*100]
fig, (ax0, ax1) = plt.subplots(nrows=1, ncols=2, sharey=True, figsize=(18,10))
labels = ['Group 1', 'Group 2', 'Group 3']
x = np.arange(len(labels))
width = 0.28
ax0.set_xticks(x)
ax0.set_xticklabels(labels, fontsize=15)
rects1 = ax0.bar(x-width/2, change_case0_to_case1_system1, width, label='Case 0 to Case 1', color='#292929', edgecolor='black', linewidth=1)
rects2 = ax0.bar(x+width/2, change_case1_to_case2_system1, width, label='Case 1 to Case 2', color='#7f6d5f', edgecolor='black', linewidth=1)
ax0.bar_label(rects1, padding=3, fontsize=11)
# add a second annotation with the string formatted labels
ax0.bar_label(rects1, labels=per_change_case0_to_case1_system1, padding=15, fontsize=11, color='red')
ax0.bar_label(rects2, padding=3, fontsize=11)
# add a second annotation with the string formatted labels
ax0.bar_label(rects2, labels=per_change_case1_to_case2_system1, padding=15, fontsize=11, color='red')
rects3 = ax1.bar(x, change_case1_to_case2_system2, width, label='Case 1 to Case 2', color='#7f6d5f', edgecolor='black', linewidth=1)
ax1.set_xticks(x)
ax1.set_xticklabels(labels,fontsize=15)
ax1.bar_label(rects3, padding=3, fontsize=11)
# add a second annotation with the string formatted labels
ax1.bar_label(rects3, labels=per_case1_to_case2_system2, padding=15, fontsize=11, color='red')
plt.tight_layout()
plt.show()
Adding data labels to a horizontal bar chart in matplotlib
Adding xlabel and ylabel should solve,
plt.xlabel("Cost")
plt.ylabel("Category")
You might also want to create the dataframe:
import pandas as pd
df = {}
df["Category"] = Category
df["Cost"] = Cost
df = pd.DataFrame.from_dict(df)
For adding the data value of each of the bar you can modify your code as follows:
# First make a subplot, so that axes is available containing the function bar_label.
fig, ax = plt.subplots()
g=ax.barh(df['Category'], df['Cost'])
ax.set_xlabel("Cost")
ax.set_ylabel("Category")
ax.bar_label(g, label_type="center") # This provides the labelling, this only available at higher version. You can do pip install -U matplotlib
plt.show()
Reference:
- Axis Label
- matplotlib 3.4.2 and above has this
Output:
How to add vertically centered labels in bar chart matplotlib
As of matplotlib 3.4.0, use Axes.bar_label
:
label_type='center'
places the labels at the center of the barsrotation=90
rotates them 90 deg
Since this is a regular bar chart, we only need to label one bar container ax1.containers[0]
:
ax1.bar_label(ax1.containers[0], label_type='center', rotation=90, color='white')
But if this were a grouped/stacked bar chart, we should iterate all ax1.containers
:
for container in ax1.containers:
ax1.bar_label(container, label_type='center', rotation=90, color='white')
seaborn version
I just noticed the question text asks about seaborn, in which case we can use sns.barplot
and sns.pointplot
. We can still use bar_label
with seaborn via the underlying axes.
import pandas as pd
import seaborn as sns
# put the lists into a DataFrame
df = pd.DataFrame({'a': a, 'b': b, 'c': c})
# create the barplot and vertically centered labels
ax1 = sns.barplot(data=df, x='c', y='a', color='green')
ax1.bar_label(ax1.containers[0], label_type='center', rotation=90, color='white')
ax12 = ax1.twinx()
ax12.set_ylim(bottom=0, top=1, emit=True, auto=False)
# create the pointplot with x=[0, 1, 2, ...]
# this is because that's where the bars are located (due to being categorical)
sns.pointplot(ax=ax12, data=df.reset_index(), x='index', y='b', color='red')
Related Topics
Usb Automatic Detection in Python for Linux Env
How to Capture Mouseevents and Keyevents Using Python in Background on Linux
List Comprehension Rebinds Names Even After Scope of Comprehension. Is This Right
Creating a Symbolic in Shared Volume of Docker and Accessing It in Host MAChine
Why Can't Python Sockets Resolve Url's with Http in It
What Does "The Following Packages Will Be Superseded by a Higher Priority Channel" Mean
Detect and Exclude Outliers in a Pandas Dataframe
How Does Zip(*[Iter(S)]*N) Work in Python
Determine Whether Integer Is Between Two Other Integers
How to Add Group Labels for Bar Charts in Matplotlib
Python Extract Pattern Matches
What Exactly Is a "Raw String Regex" and How to Use It
Imploding a List for Use in a Python MySQLdb in Clause