Using Pandas, Matplotlib, or Seaborn, how can I create a stacked and categorized Bar Chart?


I have done extensive searching, and while other questions/answers get me part of the way there, none of them directly address my problem of creating a categorized and stacked barplot.

What gets me part of the way there:

g = sns.catplot(x="Step",y="Time",data=df_test,kind="bar")
for ax in g.axes.flat[1:]:
    sns.despine(ax=ax, left=True)
for ax in g.axes.flat:
    ax.tick_params(axis='x', labelrotation=90)

Expected / Desired Result

Actual Result

How can I have the barplot not repeat the individual steps for each category then label the categories below the x-axis labels (steps)?

Asked By: Colton Campbell



To avoid the extensive x-axis, you need to set sharex=False. Also, make sure the 'Step' column is of string type (not pd.Categorical as this will cause ticks for all categories in each subplot).

To create the overall bars, you could loop through the generated bars, calculate their total height, and the overall position.

Here is some example code:

import matplotlib
from matplotlib import pyplot as plt
import seaborn as sns
import pandas as pd
import numpy as np

df_test = pd.DataFrame({'Step': [f'Step{i + 1}' for i in range(6)],
                        'Time': [5, 6, 7, 5, 10, 3],
                        'Process': [f'Process{i // 2 + 1}' for i in range(6)]})

g = sns.catplot(x='Step', y='Time', data=df_test, col='Process', kind='bar', width=0.9,
                hue='Process', dodge=False, palette=['dodgerblue', 'yellow', 'chartreuse'], saturation=0.90,
                sharex=False, sharey=True, height=4, aspect=0.7)
for ax in g.axes.flat[1:]:
    sns.despine(ax=ax, left=True)
for process, ax in g.axes_dict.items():
    bars = [b for b in ax.patches if type(b) == matplotlib.patches.Rectangle and not np.isnan(b.get_height())]
    if len(bars) > 0:
        sum_heights = sum(b.get_height() for b in bars)
        overall_rect = plt.Rectangle(xy=(bars[0].get_x() - 0.05, 0),
                                     width=bars[-1].get_x() - bars[0].get_x() + bars[-1].get_width() + 0.10,
                                     color=bars[0].get_facecolor(), alpha=0.3)
    ax.tick_params(axis='x', labelrotation=30)
    ax.tick_params(axis='y', length=0)
    ax.autoscale_view() # update x and y limits to include new rectangle

seaborn bar plot with overall sums per subplot

PS: Aligning the xlabels when the rotated x tick labels have different lengths, doesn’t seem to be simple. I tried replacing the xlabel with a text using axes coordinates (0.5 is the fixed horizontal center, -0.6 needs to be adjusted to your data):

    # ax.set_xlabel(process)
    ax.text(0.5, -0.6, process, transform=ax.transAxes, ha='center', va='center')
Answered By: JohanC