0

So I have been trying to get a legend working for a figure with multiple subplots that contain pie charts. The pie charts can contain any number of manufactures from a set list of manufacturers, but each one may not necessarily contain the same number of manufacturers. I have created a legend using fig.legend() and it has all the correct manufacturer names, but depending on which pie chart has which manufacturers the colors don't always match properly. Below is what I am currently seeing with one set of test data: Legend not matching colors

From what I can tell so far, it seems that it pulls the colors from the last plotted ax, and if not all the manufacturers are there it then recycles the colors. With the same test data set, I have a different chart set up with only two subplots and the legend works fine there as both pies have all the manufacturers, again see below: Legend matching colors correctly

The latest iteration of my code is trying to grab a list of the wedges from each ax to pass to the legend but still no luck. The code for the legend I am using is:

  fig.legend(wed, manf_names, bbox_to_anchor=(0.55,0.45), loc='center', ncol=3, prop={'size':8},
        shadow=True, fancybox=True)

Where 'wed' is the list of wedges from all the ax's. The generation of the pies themselves is somewhat dynamic based on the data recieved, but is basically built into if statements. So the code below for one chart is basically the same for all the potential pies. The code below is the minimal reproducible example.

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

#####   DATA   #######
manfdict = {
    "Modality": {
        0: 1.0,
        1: 1.0,
        2: 1.0,
        3: 1.0,
        4: 14.0,
        5: 14.0,
        6: 14.0,
        7: 14.0,
        8: 14.0,
        9: 30.0,
        10: 30.0,
        11: 30.0,
        12: 30.0,
    },
    "Manufacturer": {
        0: "Alcon",
        1: "Bausch & Lomb",
        2: "Johnson & Johnson",
        3: "Cooper Vision",
        4: "Johnson & Johnson",
        5: "Bausch & Lomb",
        6: "Alcon",
        7: "Cooper Vision",
        8: "Clerio Vision",
        9: "Alcon",
        10: "Bausch & Lomb",
        11: "Cooper Vision",
        12: "Johnson & Johnson",
    },
    "count": {
        0: 1380,
        1: 1058,
        2: 634,
        3: 269,
        4: 433,
        5: 33,
        6: 18,
        7: 6,
        8: 4,
        9: 1350,
        10: 639,
        11: 589,
        12: 132,
    },
}
modmanf = pd.DataFrame.from_dict(manfdict)

mod_daily = "Daily"
mod_weekly = "1 Week"
mod_biweekly = "2 Week"
mod_monthly = "Monthly"

manf_colors = {
    "Alcon": "#F68F30",
    "Bausch & Lomb": "#99CC33",
    "Cooper Vision": "#FFC85B",
    "Clerio Vision": "#165616",
    "Johnson & Johnson": "#35B1E1",
    "Menicon": "#CCCCCC",
    "VTI": "#2F36BA",
    "Other": "#B34D35",
}

seaborn.set_theme()
######################################################################
# Sum by Manufacturer and modality
######################################################################
# modmanf = built_data.groupby(['Modality'], as_index=False)['Manufacturer'].value_counts()  ##------  Left in to demonstrate how the modmanf data is generated
# manfdict = modmanf.to_dict()                                                              ##------  Left in to show how the data was transfered to dictonary
# print(manfdict)
modmanf["Modality"] = modmanf["Modality"].astype("string")
modmanf.loc[modmanf.Modality == "1.0", "Modality"] = mod_daily
modmanf.loc[modmanf.Modality == "7.0", "Modality"] = mod_weekly
modmanf.loc[modmanf.Modality == "14.0", "Modality"] = mod_biweekly
modmanf.loc[modmanf.Modality == "30.0", "Modality"] = mod_monthly

dailywear = modmanf[(modmanf["Modality"] == mod_daily)]
# print(dailywear)      #-- For Debugging

weeklywear = modmanf[(modmanf["Modality"] == mod_weekly)]
# print(weeklywear)     #-- For Debugging

biweeklywear = modmanf[(modmanf["Modality"] == mod_biweekly)]
# print(biweeklywear)   #-- For Debugging

monthlywear = modmanf[(modmanf["Modality"] == mod_monthly)]
# print(monthlywear)    #-- For Debugging

manf_names = modmanf["Manufacturer"].unique()
# print(manf_names)      #-- For Debugging

unique_subplots = modmanf["Modality"].unique()

sub_names = unique_subplots.tolist()
sub_count = len(sub_names)

Position = range(1, sub_count + 1)

fig = plt.figure(8)
fig.tight_layout(pad=1.0)
wed = []
for k in range(sub_count):
    # -- add every single subplot to the figure with a for loop --
    ax = fig.add_subplot(1, sub_count, Position[k])
    # -- Daily
    if sub_names[k] == "Daily":

        explode_array = dailywear["count"]
        explode_array = explode_array.to_numpy()
        percents = explode_array * 100 / explode_array.sum()

        wedges, texts = ax.pie(
            dailywear["count"],
            colors=[manf_colors[key] for key in manf_names],
            explode=(explode_array == max(explode_array)) * 0.1,
            textprops={"size": 8},
            labeldistance=1.1,
            wedgeprops={"linewidth": 0.0, "edgecolor": "lightsteelblue"},
            startangle=42,
            shadow=True,
        )
        ax.set_title("Daily", fontdict={"fontsize": 8})
        ax.axis("equal")
        wed.extend(wedges)

        kw = dict(arrowprops=dict(color="black", arrowstyle="-"), va="top", size=8)
        label = ["%1.1f %%" % (l) for l in zip(percents)]
        for p, l in zip(wedges, label):
            ang = (p.theta2 - p.theta1) / 2.0 + p.theta1
            y = np.sin(np.deg2rad(ang))
            x = np.cos(np.deg2rad(ang))
            xtext = 1.0
            ytext = 1.4
            xmod = 0
            if float(l.strip("%")) / 100 < 0.01:
                xtext = xtext + 0.45
                ytext = ytext + 0.17
            if float(l.strip("%")) / 100 < 0.02 and float(l.strip("%")) / 100 > 0.011:
                xtext = xtext + 0.15
                ytext = ytext + 0.05
            if float(l.strip("%")) / 100 < 0.04 and float(l.strip("%")) / 100 > 0.02:
                xtext = xtext + 0.15
                ytext = ytext + 0.01
            horizontalalignment = {-1: "right", 1: "left"}[int(np.sign(x))]
            connectionstyle = "angle,angleA=0,angleB={}".format(ang)
            kw["arrowprops"].update({"connectionstyle": connectionstyle})
            ax.annotate(
                l,
                xy=(x + xmod, y + xmod),
                xytext=(xtext * np.sign(x), ytext * y),
                horizontalalignment=horizontalalignment,
                **kw
            )

    # -- Weekly
    if sub_names[k] == "Week":

        explode_array = weeklywear["count"]
        explode_array = explode_array.to_numpy()
        percents = explode_array * 100 / explode_array.sum()

        wedges, texts = ax.pie(
            weeklywear["count"],
            colors=[manf_colors[key] for key in manf_names],
            explode=(explode_array == max(explode_array)) * 0.1,
            textprops={"size": 8},
            labeldistance=1.1,
            wedgeprops={"linewidth": 0.0, "edgecolor": "lightsteelblue"},
            startangle=42,
            shadow=True,
        )
        ax.set_title("Weekly", fontdict={"fontsize": 8})
        ax.axis("equal")
        wed.extend(wedges)

        kw = dict(arrowprops=dict(color="black", arrowstyle="-"), va="top", size=8)
        label = ["%1.1f %%" % (l) for l in zip(percents)]
        for p, l in zip(wedges, label):
            ang = (p.theta2 - p.theta1) / 2.0 + p.theta1
            y = np.sin(np.deg2rad(ang))
            x = np.cos(np.deg2rad(ang))
            xtext = 1.0
            ytext = 1.4
            xmod = 0
            if float(l.strip("%")) / 100 < 0.01:
                xtext = xtext + 0.45
                ytext = ytext + 0.17
            if float(l.strip("%")) / 100 < 0.02 and float(l.strip("%")) / 100 > 0.011:
                xtext = xtext + 0.15
                ytext = ytext + 0.05
            if float(l.strip("%")) / 100 < 0.04 and float(l.strip("%")) / 100 > 0.02:
                xtext = xtext + 0.15
                ytext = ytext + 0.01
            horizontalalignment = {-1: "right", 1: "left"}[int(np.sign(x))]
            connectionstyle = "angle,angleA=0,angleB={}".format(ang)
            kw["arrowprops"].update({"connectionstyle": connectionstyle})
            ax.annotate(
                l,
                xy=(x + xmod, y + xmod),
                xytext=(xtext * np.sign(x), ytext * y),
                horizontalalignment=horizontalalignment,
                **kw
            )

    # -- 2 Week
    if sub_names[k] == "2 Week":

        explode_array = biweeklywear["count"]
        explode_array = explode_array.to_numpy()
        percents = explode_array * 100 / explode_array.sum()

        wedges, texts = ax.pie(
            biweeklywear["count"],
            colors=[manf_colors[key] for key in manf_names],
            explode=(explode_array == max(explode_array)) * 0.1,
            textprops={"size": 8},
            labeldistance=1.1,
            wedgeprops={"linewidth": 0.0, "edgecolor": "lightsteelblue"},
            startangle=42,
            shadow=True,
        )
        ax.set_title("Bi-Weekly", fontdict={"fontsize": 8})
        ax.axis("equal")
        wed.extend(wedges)

        kw = dict(arrowprops=dict(color="black", arrowstyle="-"), va="top", size=8)
        label = ["%1.1f %%" % (l) for l in zip(percents)]
        for p, l in zip(wedges, label):
            ang = (p.theta2 - p.theta1) / 2.0 + p.theta1
            y = np.sin(np.deg2rad(ang))
            x = np.cos(np.deg2rad(ang))
            xtext = 1.0
            ytext = 1.4
            xmod = 0
            if float(l.strip("%")) / 100 < 0.01:
                xtext = xtext + 0.45
                ytext = ytext + 0.17
            if float(l.strip("%")) / 100 < 0.02 and float(l.strip("%")) / 100 > 0.011:
                xtext = xtext + 0.15
                ytext = ytext + 0.05
            if float(l.strip("%")) / 100 < 0.04 and float(l.strip("%")) / 100 > 0.02:
                xtext = xtext + 0.15
                ytext = ytext + 0.01
            horizontalalignment = {-1: "right", 1: "left"}[int(np.sign(x))]
            connectionstyle = "angle,angleA=0,angleB={}".format(ang)
            kw["arrowprops"].update({"connectionstyle": connectionstyle})
            ax.annotate(
                l,
                xy=(x + xmod, y + xmod),
                xytext=(xtext * np.sign(x), ytext * y),
                horizontalalignment=horizontalalignment,
                **kw
            )

    # -- Monthly
    if sub_names[k] == "Monthly":

        print(monthlywear)

        explode_array = monthlywear["count"]
        explode_array = explode_array.to_numpy()
        percents = explode_array * 100 / explode_array.sum()

        wedges, texts = ax.pie(
            monthlywear["count"],
            colors=[manf_colors[key] for key in manf_names],
            explode=(explode_array == max(explode_array)) * 0.1,
            textprops={"size": 8},
            labeldistance=1.1,
            wedgeprops={"linewidth": 0.0, "edgecolor": "lightsteelblue"},
            startangle=42,
            shadow=True,
        )
        ax.set_title("Monthly", fontdict={"fontsize": 8})
        ax.axis("equal")
        wed.extend(wedges)

        kw = dict(arrowprops=dict(color="black", arrowstyle="-"), va="top", size=8)
        label = ["%1.1f %%" % (l) for l in zip(percents)]
        for p, l in zip(wedges, label):
            ang = (p.theta2 - p.theta1) / 2.0 + p.theta1
            y = np.sin(np.deg2rad(ang))
            x = np.cos(np.deg2rad(ang))
            xtext = 1.0
            ytext = 1.4
            xmod = 0
            if float(l.strip("%")) / 100 < 0.01:
                xtext = xtext + 0.45
                ytext = ytext + 0.17
            if float(l.strip("%")) / 100 < 0.02 and float(l.strip("%")) / 100 > 0.011:
                xtext = xtext + 0.15
                ytext = ytext + 0.05
            if float(l.strip("%")) / 100 < 0.04 and float(l.strip("%")) / 100 > 0.02:
                xtext = xtext + 0.15
                ytext = ytext + 0.01
            horizontalalignment = {-1: "right", 1: "left"}[int(np.sign(x))]
            connectionstyle = "angle,angleA=0,angleB={}".format(ang)
            kw["arrowprops"].update({"connectionstyle": connectionstyle})
            ax.annotate(
                l,
                xy=(x + xmod, y + xmod),
                xytext=(xtext * np.sign(x), ytext * y),
                horizontalalignment=horizontalalignment,
                **kw
            )

# ------------  Title and Legend for modality/replacement chart -------
print(manf_names)
print(wed)
fig.legend(
    wed,
    manf_names,
    bbox_to_anchor=(0.55, 0.45),
    loc="center",
    ncol=3,
    prop={"size": 8},
    shadow=True,
    fancybox=True,
)
fig.suptitle("Dollars by Manufacturer / Replacement Schedule", fontsize=12, y=1.01)
# set the spacing between subplots
plt.subplots_adjust(
    left=0.01, bottom=0.50, right=0.99, top=0.92, wspace=0.60, hspace=0.01
)
# plt.subplot_tool()  # -- Keep for chart adjustment
plt.show()  # -- /

There is also a dictionary that maps specific colors to specific manufacturers for consistency across the report. I have managed to implement that just fine everywhere else except this one spot. My luck I am probably missing something quite simple, but I have only started using matplotlib around a month ago and any help would be very much appreciated.

EDIT The code has been edited to create a reproducible example of the issue I am having. I tried to shrink it as much as possible, but this was the best I could do.

5
  • are you doing this in vc code interactive or jupyter notebook or colabs ?
    – darren
    Commented Oct 18, 2022 at 16:25
  • also, please add sufficient code for a minimal reproducible example: stackoverflow.com/help/minimal-reproducible-example
    – darren
    Commented Oct 18, 2022 at 16:26
  • @D.L Currently I am running interactively in VS Code with pyodbc to connect to our dev server for test data. All the graphs and tables get spit out to a pdf report that is currently being saved to a local directory. Also I will work on extracting the code necessary to reproduce the issue with some form of test data but it is a rather large program and data set so it may take me bit to uncouple everything to be able to run independently.
    – Kgrover3
    Commented Oct 18, 2022 at 16:45
  • okay, i thought so. i had this issue and it was because the variables are stored in memory in the interactive in vs code. (the could also be other reasons). so you might need to restart the kernel to flush the memory.
    – darren
    Commented Oct 18, 2022 at 16:52
  • @D.L Just ran the live beta test version that is linked through our admin site. Even running "live" the issue still remains... That version is being called through a Flask route and runs on an IBM i system completely independently from my local test environment.
    – Kgrover3
    Commented Oct 18, 2022 at 16:58

0