Search code examples
pythonpandasdataframematplotlibstacked-chart

How to label each bar of a stacked bar plot with percentage of total values?


I am trying to plot a stacked bar plot where I have my data plotted as shown in the figure below. The labels are the exact values for each container. I would like to put these in percentage values of the total value of each bar. So far, I failed to do so. Your help would be appreciated.

Here is the code for labels:

for i, rect in enumerate(ax.patches):
    # Find where everything is located
    height = rect.get_height()
    width = rect.get_width()
    x = rect.get_x()
    y = rect.get_y()
    label_text = f"{height:.02f}"

    label_x = x + width / 2
    label_y = y + height / 2
    ax.text(
        label_x,
        label_y,
        label_text,
        ha="center",
        va="center",
        fontsize=4,
        weight="bold",
    )

enter image description here


Solution

  • Given the following toy dataframe:

    import pandas as pd
    from matplotlib import pyplot as plt
    
    df = pd.DataFrame(
        {
            "A": {2019: 125, 2020: 124, 2021: 50, 2022: 63},
            "B": {2019: 129, 2020: 40, 2021: 85, 2022: 47},
            "C": {2019: 126, 2020: 95, 2021: 51, 2022: 44},
            "D": {2019: 99, 2020: 120, 2021: 106, 2022: 117,},
        }
    )
    print(df)
    # Output
            A    B    C    D
    2019  125  129  126   99
    2020  124   40   95  120
    2021   50   85   51  106
    2022   63   47   44  117
    

    Here is one way to do it:

    # Setup figure
    fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(7, 4))
    
    # Add bars
    ax.bar(df.index, df["A"], label="A")
    ax.bar(df.index, df["B"], bottom=df["A"], label="B")
    ax.bar(df.index, df["C"], bottom=df["A"] + df["B"], label="C")
    ax.bar(df.index, df["D"], bottom=df["A"] + df["B"] + df["C"], label="D")
    
    # Add percentages as labels
    for idx in df.index:
        start = 0
        for col in df.columns:
            y = df.loc[idx, col]
            value = df.loc[idx, col]
            total = df.loc[idx, :].sum()
            ax.text(
                x=idx,
                y=start + y / 2,
                s=f"{round(100 * value / total, 1)}%",
                fontsize=10,
                ha="center",
                color="w",
            )
            start += y
    
    # Add other useful informations
    plt.xticks(df.index, df.index)
    ax.legend()
    
    plt.show()
    

    Which outputs:

    enter image description here