3

How can this vertical grouped bar chart be changed to a horizontal bar chart (grouped, and stacked)? I need help to alter the code such that the bars are displayed horizontally instead of vertically.

import matplotlib.pyplot as plt
import numpy as np

N = 9
labels = ['L', 'S', 'S', 'M', 'W', 'W', 'S', 'R', 'C']    
M_means = [1, 45, 28, 11, 4, 7, 1, 0.02, 0.3]
PO_means = [3, 58, 17, 8, 3, 8, 1, 0.06, 1]
K_means = [1, 44, 30, 11, 3, 7, 1, 0.01, 0.5]

x = np.arange(len(labels))  # the label locations
width = 0.30  # the width of the bars

fig, ax = plt.subplots(figsize=(15, 9))
rects1 = ax.bar(x - width, M_means, width, label='M S and K', color=('#b02a2a'))
rects2 = ax.bar(x, PO_means, width, label='P O S and K', color=('#055cad'))
rects3 = ax.bar(x + width, K_means, width, label='M K', color=('#0b7d53'))

# Add some text for labels, title and custom x-axis tick labels, etc.
ax.set_ylabel('% of workday', fontsize=32)
#ax.set_title('Scores by group and gender')
ax.set_xticks(x) 
ax.set_xticklabels(labels, fontsize=32, rotation=15) 
ax.legend(loc='upper right', frameon=False, fontsize=32, markerscale=2)

ax.bar_label(rects1, size = 32, padding=20, rotation=90) 
ax.bar_label(rects2, size = 32,  padding=20, rotation=90) 
ax.bar_label(rects3, size = 32,  padding=20, rotation=90)
plt.xticks(ha='center') 
for tick in ax.xaxis.get_major_ticks():
    tick.label.set_fontsize(32) 
for tick in ax.yaxis.get_major_ticks():
    tick.label.set_fontsize(32) 
plt.ylim(0, 100) 
plt.gca().spines['right'].set_color('none')
plt.gca().spines['top'].set_color('none')
#fig.tight_layout()
plt.show()

enter image description here

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
Svein
  • 47
  • 3

2 Answers2

1

Functionally, only two changes are needed:

  1. Change ax.bar to ax.barh
  2. Swap set_x* methods with set_y* methods, e.g. set_xticks() -> set_yticks() and so on

Semantically, the variables x and width should also be renamed to y and height.

import matplotlib.pyplot as plt
import numpy as np

N = 9
labels = list('LSSMWWSRC')
M_means = [1, 45, 28, 11, 4, 7, 1, 0.02, 0.3]
K_means = [2, 40, 21, 18, 3, 3, 2, 0.52, 0.3]
PO_means = [3, 58, 17, 8, 3, 8, 1, 0.06, 1]
K = [1, 44, 30, 11, 3, 7, 1, 0.01, 0.5]

# rename x/width to y/height
y = np.arange(len(labels))
height = 0.30

fig, ax = plt.subplots()

# use ax.barh instead of ax.bar
rects1 = ax.barh(y - height, M_means, height, label='M S and K', color='#b02a2a')
rects2 = ax.barh(y, PO_means, height, label='P O S and K', color='#055cad')
rects3 = ax.barh(y + height, K_means, height, label='M K', color='#0b7d53')

# swap set_x* methods with set_y* methods
ax.set_xlabel('% of workday')
ax.set_yticks(y)
ax.set_yticklabels(labels)
ax.legend(loc='upper right', frameon=False, markerscale=2)

ax.bar_label(rects1, padding=10)
ax.bar_label(rects2, padding=10)
ax.bar_label(rects3, padding=10)

# ...

grouped bars converted to hbars

tdy
  • 36,675
  • 19
  • 86
  • 83
0
  • The easiest solution is to load the data into a pandas.DataFrame, and then use pandas.DataFrame.plot with kind='barh'. This is easier because pandas uses matplotlib as the default plotting backend, and the API groups the bars automatically.
    • This reduces the code to 14 lines (not including imports).
    • When using 'barh', xlabel= applies to the y-axis. Therefore, xlabel='' removes the y-axis label.
    • Adjust figsize=(12, 10) if planning to use smaller / larger font sizes.
  • See Adding value labels on a matplotlib bar chart for additional details about using .bar_label.
  • Tested in python 3.10, pandas 1.4.2, matplotlib 3.5.1
import pandas as pd
import matplotlib.pylot as plt

# data
labels = ['L', 'S', 'S', 'M', 'W', 'W', 'S', 'R', 'C']    
M_means = [1, 45, 28, 11, 4, 7, 1, 0.02, 0.3]
PO_means = [3, 58, 17, 8, 3, 8, 1, 0.06, 1]
K_means = [1, 44, 30, 11, 3, 7, 1, 0.01, 0.5]

# create a dict with the keys as the desired legend labels
data = {'labels': labels, 'M S and K': M_means, 'P O S and K': PO_means, 'M K': K_means}
# create dataframe
df = pd.DataFrame(data)

# plot: specify y=[...] if only certain columns are desired
ax = df.plot(kind='barh', x='labels', width=.85, figsize=(12, 10), xlabel='', color=['#b02a2a', '#055cad', '#0b7d53'])
ax.set_xlabel('% of workday', fontsize=15)
ax.set_xlim(0, 100)

ax.legend(loc='upper right', frameon=False, fontsize=15, markerscale=2)

for c in ax.containers:
    ax.bar_label(c, label_type='edge', padding=1, size=15)
    
ax.tick_params(axis='both', which='both', labelsize=15)

ax.spines[['top', 'right']].set_visible(False)

enter image description here

Stacked

  • To manually create the stacked bar without pandas, see Horizontal stacked bar chart in Matplotlib
  • Use the parameter stacked=True
  • Some bar patches are to small for the label, so custom labels have been passed to the labels= parameter in .bar_label
    • Using := requires at least python 3.8. Otherwise use labels = [f'{v.get_width():.0f}' if v.get_width() > 1 else '' for v in c]
ax = df.plot(kind='barh', x='labels', width=.85, figsize=(12, 10), xlabel='',
             color=['#b02a2a', '#055cad', '#0b7d53'], stacked=True)
ax.set_xlabel('% of workday', fontsize=15)
ax.set_xlim(0, 100)

ax.legend(loc='upper right', frameon=False, fontsize=15, markerscale=2)

for c in ax.containers:
    # custom labels only show label size for values greater than 1
    labels = [f'{w:.0f}' if (w := v.get_width()) > 1 else '' for v in c]
    ax.bar_label(c, labels=labels, label_type='center', padding=1, size=15)
    
ax.tick_params(axis='both', which='both', labelsize=15)

ax.spines[['top', 'right']].set_visible(False)

enter image description here

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158