at1623
at1623

Reputation: 129

Annotate values for stacked horizontal bar plot

I'm trying to annotate the values for a stacked horizontal bar graph created using pandas. Current code is below

import pandas as pd 
import matplotlib.pyplot as plt
import numpy as np

d = {'group 1': [1, 2, 5, 7, 4, 5, 10],
     'group 2': [5, 6, 1, 8, 2, 6, 2],
     'group 3': [12, 2, 2, 4, 4, 8, 4]}
df = pd.DataFrame(d)

ax = df.plot.barh(stacked=True, figsize=(10,12))

for p in ax.patches:
    ax.annotate(str(p.get_x()), xy=(p.get_x(), p.get_y()+0.2))

plt.legend(bbox_to_anchor=(0, -0.15), loc=3, prop={'size': 14}, frameon=False)

The problem is the annotation method I used gives the x starting points and not the values of each segment. I'd like to be able to annotate values of each segment in the center of each segment for each of the bars.

example

edit: for clarity, what I would like to achieve is something like this where the values are centered horizontally (and vertically) for each segment:

enter image description here

Upvotes: 5

Views: 2490

Answers (3)

Trenton McKinney
Trenton McKinney

Reputation: 62403

  • From matplotlib 3.4.0 use matplotlib.pyplot.bar_label
    • The labels parameter can be used to customize annotations, but it's not required.
    • See this answer for additional details and examples.
  • Each group of containers must be iterated through to add labels.
  • Tested in python 3.10, pandas 1.4.2, matplotlib 3.5.1

Horizontal Stacked

d = {'group 1': [1, 2, 5, 7, 4, 5, 10],
     'group 2': [5, 6, 1, 8, 2, 6, 2],
     'group 3': [12, 2, 2, 4, 4, 8, 4]}
df = pd.DataFrame(d)

# add tot to sort the bars
df['tot'] = df.sum(axis=1)

# sort
df = df.sort_values('tot')

# plot all columns except tot
ax = df.iloc[:, :-1].plot.barh(stacked=True, figsize=(10, 12))

# iterate through each group of bars
for c in ax.containers:

    # format the number of decimal places (if needed) and replace 0 with an empty string
    labels = [f'{w:.0f}' if (w := v.get_width()) > 0 else '' for v in c ]
    
    ax.bar_label(c, labels=labels, label_type='center')

enter image description here

Horizontal Grouped

  • Not stacked is a better presentation of the data, because it is easier to compare bar lengths visually.
# plot all columns except tot
ax = df.iloc[:, :-1].plot.barh(stacked=False, figsize=(8, 9))

# iterate through each group of bars
for c in ax.containers:

    # format the number of decimal places (if needed) and replace 0 with an empty string
    labels = [f'{w:.0f}' if (w := v.get_width()) > 0 else '' for v in c ]
    
    ax.bar_label(c, labels=labels, label_type='center')

enter image description here

df view

   group 1  group 2  group 3  tot
2        5        1        2    8
1        2        6        2   10
4        4        2        4   10
6       10        2        4   16
0        1        5       12   18
3        7        8        4   19
5        5        6        8   19

Upvotes: 0

rpanai
rpanai

Reputation: 13437

Another possible solution is to get your df.values to a flatten array via values = df.values.flatten("F")

%matplotlib inline
import pandas as pd 
import matplotlib.pyplot as plt
import numpy as np

d = {'group 1': [1, 2, 5, 7, 4, 5, 10],
     'group 2': [5, 6, 1, 8, 2, 6, 2],
     'group 3': [12, 2, 2, 4, 4, 8, 4]}
df = pd.DataFrame(d)

ax = df.plot.barh(stacked=True, figsize=(10,12))

values = df.values.flatten("F")

for i, p in enumerate(ax.patches):
    ax.annotate(str(values[i]), xy=(p.get_x()+ values[i]/2, p.get_y()+0.2))

plt.legend(bbox_to_anchor=(0, -0.15), loc=3, prop={'size': 14}, frameon=False);

enter image description here

Upvotes: 4

James
James

Reputation: 36608

You can use the patches bbox to get the information you want.

ax = df.plot.barh(stacked=True, figsize=(10, 12))
for p in ax.patches:
    left, bottom, width, height = p.get_bbox().bounds
    ax.annotate(str(width), xy=(left+width/2, bottom+height/2), 
                ha='center', va='center')

enter image description here

Upvotes: 7

Related Questions