Search code examples
pythonpandasmatplotlibchartspie-chart

Stacked pie chart - how to set inner piechart labels dependent on outer piechart


I have a stacked pie chart with labels in percentage. The outer pie chart has labels as I want them to be; however, the labels for the inner chart should be based on where it falls under the outer pie chart. For example, in the image below, the percentage labels of the inner pie chart should sum up to 100% for each color of the outer pie chart. Currently, for the red outer pie chart, the inner pie chart has 11.0%, 12.0%, 7.0% which is the percentage of itself that is not taking the outer chart into account. I'd want them to display something like 35%, 38%, 27% and same for green and yellow colors of the outer pie chart where the inner chart labels are dependent upon the outer pie chart color.

enter image description here

Here's a reproducible example

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

options = ['RED','YELLOW','GREEN']
color_list1 = []
color_list2 = []

for i in np.arange(100):
    color1 = random.choice(options)
    color2 = random.choice(options)
    color_list1.append(color1)
    color_list2.append(color2)

test_df = pd.DataFrame(list(zip(color_list1, color_list2)), columns =['Outer_Color', 'Inner_Color'])

outer = pd.DataFrame(test_df.groupby('Outer_Color').size())
inner = pd.DataFrame(test_df.groupby(['Outer_Color', 'Inner_Color']).size())
inner_labels = inner.index.get_level_values(1)


my_pal = {"RED": "r", "YELLOW": "yellow", "GREEN":"g"}

fig, ax = plt.subplots(figsize=(14,7))
size = 0.3

ax.pie(outer.values.flatten(), radius=1,
#        labels=outer.index,
       autopct='%1.1f%%', pctdistance=0.85, # labeldistance=0.2,
       wedgeprops=dict(width=size, edgecolor='w'),
       colors=[my_pal[key] for key in outer.index])

ax.pie(inner.values.flatten(), radius=1-size, 
#        labels = inner_labels,
       autopct='%1.1f%%', pctdistance=0.7, # labeldistance=1.2,
       wedgeprops=dict(width=size, edgecolor='w'),
       colors=[my_pal[key] for key in inner_labels])

plt.show()

Solution

  • An approach you can take is to calculate the percentage of the inner wedge relative to the outer wedge and make new labels to then pass when you do ax.pie. For example:

    # Construct inner labels
        inner_str_label = []  # new labels storage
        n_inside = 3  # number of inner wedges per outer wedge
        for idx, outer_value in enumerate(outer.values.flatten()):
            # Get inner values for each outer wedge
            for inner_value in inner.values.flatten()[idx*n_inside:idx*n_inside+n_inside]:
                # format for only 1 decimal place
                new_label = f"{'{0:.1f}'.format(inner_value/outer_value*100)}%"
                inner_str_label.append(new_label)
    

    Now we can pass the new labels in ax.pie using labels:

    import pandas as pd
    import numpy as np
    import random
    import matplotlib.pyplot as plt
    
    
    if __name__ == '__main__':
    
        options = ['RED', 'YELLOW', 'GREEN']
        color_list1 = []
        color_list2 = []
    
        for i in np.arange(100):
            color1 = random.choice(options)
            color2 = random.choice(options)
            color_list1.append(color1)
            color_list2.append(color2)
    
        test_df = pd.DataFrame(list(zip(color_list1, color_list2)), columns =['Outer_Color', 'Inner_Color'])
    
        outer = pd.DataFrame(test_df.groupby('Outer_Color').size())
        inner = pd.DataFrame(test_df.groupby(['Outer_Color', 'Inner_Color']).size())
    
        my_pal = {"RED": "r", "YELLOW": "yellow", "GREEN": "g"}
    
        fig, ax = plt.subplots(figsize=(14, 7))
        size = 0.3
    
        ax.pie(outer.values.flatten(), radius=1,
               #        labels=outer.index,
               autopct='%1.1f%%', pctdistance=0.85,  # labeldistance=0.2,
               wedgeprops=dict(width=size, edgecolor='w'),
               colors=[my_pal[key] for key in outer.index])
    
        # Construct inner labels
        inner_str_label = []  # new labels storage
        n_inside = 3  # number of inner wedges per outer wedge
        for idx, outer_value in enumerate(outer.values.flatten()):
            # Get inner values for each outer wedge
            for inner_value in inner.values.flatten()[idx*n_inside:idx*n_inside+n_inside]:
                # format for only 1 decimal place
                new_label = f"{'{0:.1f}'.format(inner_value/outer_value*100)}%"
                inner_str_label.append(new_label)
    
        wedges, labels = ax.pie(inner.values.flatten(), radius=1-size, labels=inner_str_label, labeldistance=0.8,
                                wedgeprops=dict(width=size, edgecolor='w'),
                                colors=[my_pal[key] for key in inner.index.get_level_values(1)])
    
        # Fix inner label position
        for string_label in labels:
            string_label.update({"rotation": 0, "horizontalalignment": "center", "verticalalignment": "center"})
    
        plt.show()
    

    fig_example

    Note that this is assuming each outer wedge has the same amount of inner wedges. If the number of inner wedges was irregular, the approach would be similar but you set the number of inner wedges per outer wedges in a list:

        # Construct inner labels
        inner_str_label = []  # new labels storage
        n_inside_list = [2, 3, 2]  # number of inner wedges per outer wedge
        for idx, outer_value in enumerate(outer.values.flatten()):
            # Get inner values for each outer wedge
            n_inside = n_inside_list[idx]
            for inner_value in inner.values.flatten()[idx*n_inside:idx*n_inside+n_inside]:
                # format for only 1 decimal place
                new_label = f"{'{0:.1f}'.format(inner_value/outer_value*100)}%"
                inner_str_label.append(new_label)
    

    Hope this helps - cheers!