0

I would like to have a common legend at the end of my "multi chart area". "weeks_df_list" is a pandas DataFrame. my code is:


    #https://stackoverflow.com/questions/41625077/python-pandas-split-a-timeserie-per-month-or-week
    weeks_df_list = [g for n, g in daily_data_df.groupby(_pd.Grouper(key='Transaction Date', freq='W'))]

    for my_df in weeks_df_list:
        my_df['day_of_the_week'] = my_df['Transaction Date'].dt.weekday_name
        my_df.set_index(keys=['day_of_the_week'], drop=True, inplace=True)

    fig, axs = plt.subplots(number_of_charts, 1, sharex=True, figsize=[8, 17])

    # Adjust horizontal space between axes
    fig.subplots_adjust(hspace=.5)
    for i in range(number_of_charts):
        print("i:", i)
        #axs[i].set_yticks(np.arange(-0.9, 1.0, 0.4))
        #axs[i].set_ylim(-1, 1)
        #axs[i] = weeks_df_list[i]['pct_daily_vol'].multiply(100).round(1).plot(label='% Daily Volumes')
        #percent daily
        axs[i].plot(weeks_df_list[i]['pct_daily_vol'].multiply(100).round(2), label='% Daily Volumes',
                     color='blue')
        axs[i].yaxis.set_major_formatter(mtick.PercentFormatter())
        axs[i].legend(loc=2)
        #percent daily max
        axs[i].plot(weeks_df_list[i]['pct_daily_limit'].multiply(100).round(2), label='% Daily Limit',
                     color='orange')
        axs[i].yaxis.set_major_formatter(mtick.PercentFormatter())
        axs[i].legend(loc=0)


        #secondary axis
        axs_2 = axs[i].twinx()
        axs_2.plot(weeks_df_list[i]['vwap'], label='VWAP Paid', color='green')
        axs_2.legend(loc=3)


        #comon variables
        axs[i].set_yticks(_np.arange(0, 100, 20))
        axs[i].set_ylim(0, 100)
        axs[i].set_title('Week:' + str(i + 1))
        axs[i].grid(True)


    plt.show()

My data are:

day_of_the_week;Transaction Date;Volume;vwap;mylow;myhigh;myopen;myclose;myvolume;20d_vol_avg;25%_limit;pct_daily_vol;pct_daily_limit
Monday;2019-09-02;35807;53.24725612310441;52.9;54.0;53.75;53.0;192570;246338.0;61584.0;0.18594277405618737;0.5814334892179787
Tuesday;2019-09-03;51200;52.923418945312505;52.75;53.25;53.25;53.1;231631;241551.0;60388.0;0.22104122505191448;0.847850566337683
Wednesday;2019-09-04;45100;52.97544235033262;52.5;53.4;53.35;53.0;220595;243379.0;60845.0;0.20444706362338222;0.7412277097542938
Thursday;2019-09-05;59000;51.50618474576272;51.2;52.0;51.65;51.55;740694;246378.0;61594.0;0.07965502623215524;0.9578855083287333
Friday;2019-09-06;59100;51.47736971235195;50.95;52.0;51.6;51.4;512996;273752.0;68438.0;0.1152055766516698;0.8635553347555451
Monday;2019-09-09;59100;51.450917935702215;51.15;51.7;51.2;51.25;215956;290220.0;72555.0;0.27366685806367963;0.8145544759148232
Tuesday;2019-09-10;60900;50.00561674876848;49.38;51.25;51.25;50.25;418767;289580.0;72395.0;0.14542693192156975;0.8412183161820568
Wednesday;2019-09-11;60800;50.00684062500002;49.56;50.45;50.45;49.7;335791;296832.0;74208.0;0.18106500769824088;0.8193186718413109
Thursday;2019-09-12;60800;50.0199384868421;49.66;50.3;49.88;50.2;241223;305352.0;76338.0;0.2520489339739577;0.7964578584715345
Friday;2019-09-13;60600;50.20141881188121;49.9;50.45;50.05;50.0;221205;292716.0;73179.0;0.27395402454736556;0.828106423974091
Monday;2019-09-16;61200;49.713364379084986;49.14;50.1;50.05;49.26;268788;293007.0;73252.0;0.22768873610429036;0.8354720690220062
Tuesday;2019-09-17;61300;49.60541109298533;48.96;50.2;49.26;50.0;364572;293632.0;73408.0;0.16814236968280613;0.8350588491717524
Wednesday;2019-09-18;60800;50.02049095394736;49.64;50.2;49.92;50.1;207805;304150.0;76038.0;0.2925819879213686;0.7996001999000499
Thursday;2019-09-19;60500;50.27256446280997;50.05;50.45;50.25;50.3;191168;304872.0;76218.0;0.3164755607633077;0.7937757485108504
Friday;2019-09-20;60700;50.136443822075755;49.86;50.35;50.1;50.3;375839;298466.0;74616.0;0.1615053254185968;0.8134984453736464
Monday;2019-09-23;60500;50.228577685950434;49.86;50.45;49.86;50.1;212277;296375.0;74094.0;0.2850049699213763;0.8165303533349529
Tuesday;2019-09-24;37295;50.85666282343475;49.9;51.3;49.9;51.3;348997;301849.0;75462.0;0.10686338277979467;0.49422225756009647
Wednesday;2019-09-25;39000;50.91075897435897;50.55;51.4;50.85;51.25;357430;305476.0;76369.0;0.10911227373191953;0.5106784166350221
Thursday;2019-09-26;22300;51.8501143497758;51.2;52.2;51.2;52.0;484304;312316.0;78079.0;0.04604545905051373;0.2856081660881927
Friday;2019-09-27;22300;51.96707174887891;51.4;52.3;51.95;52.15;111409;325248.0;81312.0;0.2001633620264072;0.27425226288862653

So far i get the legend on each chart but I would like to have only one legend at the bottom of my "multichart area". Any idea, input links, would be much appreciated. I tried:

Click

and few others, but clearly I am missing something.

I have cleared some pics. So trying to use of @SpghttCd :

    fig.subplots_adjust(hspace=.5)
    for i, ax in enumerate(axs):
        print("i:", i)
        #percent daily
        axs[i].plot(weeks_df_list[i]['pct_daily_vol'].multiply(100).round(2), label=('_', '')[i>0] + '% Daily Volumes',
                     color='blue')
        axs[i].yaxis.set_major_formatter(mtick.PercentFormatter())
        #percent daily max
        axs[i].plot(weeks_df_list[i]['pct_daily_limit'].multiply(100).round(2), label=('_', '')[i>0] + '% Daily Limit',
                     color='orange')
        axs[i].yaxis.set_major_formatter(mtick.PercentFormatter())
        #secondary axis
        axs_2 = axs[i].twinx()
        axs_2.plot(weeks_df_list[i]['vwap'], label=('_', '')[i>0] + 'VWAP Paid', color='green')

        #comon variables
        axs[i].set_yticks(_np.arange(0, 100, 20))
        axs[i].set_ylim(0, 100)
        axs[i].set_title('Week:' + str(i + 1))
        axs[i].grid(True)

    fig.legend(loc=8, ncol=3)
    plt.tight_layout(rect=[0, .05, 1, 1])
    plt.show()

I get :

enter image description here

Please correct me.

Je Je
  • 508
  • 2
  • 8
  • 23

2 Answers2

1

From - afaik - matplotlib 3.1 on you can use a figure legend, i.e. just use legend as before but not as a method of plt or your axs but with fig:

fig.legend()

in your example for bottom center and three columns e.g.:

fig.legend(loc=8, ncol=3)

with regards to overlapping:
legend can only create a legend and place it anywhere, to prevent overlapping you'd need plt.tight_layout() with a proper rect value, e.g.

plt.tight_layout(rect=[0, .05, 1, 1])

and for the duplicates:
legend collects all labelled plots, so of course this is the reason why you get duplicates when creating your plots in a loop like you do above.

However, you can prevent this with a nice little Feature: a prescribed underscore suppresses a label to be added to the legend, e.g. label='VWAP Paid' would not show up in the legend.
Knowing this, you can add underscores depending on the counter of your loop for example:

label=('', '_')[i>0] + 'VWAP Paid'

btw you really should consider using

for i, ax in enumerate(axs):    # ...(axs.flatten()): if you would have several rows _and_ colmns

instead of

for i in range(number_of_charts):

It opens the opportunity to write ax instead of axs[i] but still provides i as a counter variable if you need it (e.g. for adding underscores except in the first loop... :) )


EDIT:

this would be your code with my suggestions:

fig, axs = plt.subplots(len(weeks_df_list), sharex=True, figsize=[8, 17])

fig.subplots_adjust(hspace=.5)
for i, (ax, df) in enumerate(zip(axs, weeks_df_list)):
    print("i:", i)
    #percent daily
    ax.plot(df['pct_daily_vol'].multiply(100).round(2), label=('', '_')[i>0] + '% Daily Volumes', color='blue')
    ax.yaxis.set_major_formatter(mtick.PercentFormatter())
    #percent daily max
    ax.plot(df['pct_daily_limit'].multiply(100).round(2), label=('', '_')[i>0] + '% Daily Limit', color='orange')
    ax.yaxis.set_major_formatter(mtick.PercentFormatter())
    #secondary axis
    axs_2 = ax.twinx()
    axs_2.plot(df['vwap'], label=('', '_')[i>0] + 'VWAP Paid', color='green')

    #comon variables
    ax.set_yticks(np.arange(0, 100, 20))
    ax.set_ylim(0, 100)
    ax.set_title('Week:' + str(i + 1))
    ax.grid(True)

fig.legend(loc=8, ncol=3)
plt.tight_layout(rect=[0, .05, 1, 1])

enter image description here

SpghttCd
  • 10,510
  • 2
  • 20
  • 25
  • So you would say this is a duplicate of https://stackoverflow.com/questions/9834452/how-do-i-make-a-single-legend-for-many-subplots-with-matplotlib ? In that case you can just mark as such. – ImportanceOfBeingErnest Oct 01 '19 at 21:48
  • fig.legend(loc=8, ncol=2), does put the legend lower down the charts but still is on the area of one chart and duplicates every chart legend of each chart. So i have 4+ legend per serie – Je Je Oct 01 '19 at 22:04
  • 1
    @ImportanceOfBeingErnest "So you would say...?" - sorry I don't understand. If you found a duplicate, you should mark it, shouldn't you...? You sound as if I'd obviously knew about that duplicate but still created an answer against better judgement... – SpghttCd Oct 01 '19 at 22:28
  • The question contains a link to a question which itself is marked as duplicate of one which shows how to use `fig.legend()`. So yes, I would kind of assume that you would at least suspect that this isn't the first time someone asks about it, and at least have a quick glance if someone links to a question. The "would you say..." means that maybe you did actually follow that link and have a good argument why it's not a duplicate. – ImportanceOfBeingErnest Oct 01 '19 at 22:34
  • @ImportanceOfBeingErnest, I still get duplicate legends – Je Je Oct 01 '19 at 22:43
  • @ImportanceOfBeingErnest I think I understand. In fact: I did follow the link, only noticing that there is no occurance of `fig.legend()`, returned back and wrote the answer. I thought that was ok, but of course one thing is clear: independent from this concrete link, the topic alone is more than likely already asked before. And I have to admit: I already decided to first better investigate against duplicates before answering, but still fail in this topic from time to time... – SpghttCd Oct 01 '19 at 22:44
  • @NonoLondon with regards to duplicated legend entries you might want to have a look at my edit – SpghttCd Oct 01 '19 at 22:46
  • In short: __1.__ remove all lines like `axs[i].legend`. __2.__ add `fig.legend(loc=8, ncol=3)` instead. __3.__ add sth like `plt.tight_layout(rect=[0, .05, 1, 1])` __4.__ put `('_', '')[i>0] + ` to the left of all labels – SpghttCd Oct 01 '19 at 23:05
  • @SpghttCd I have edited my code but still get duplicates. Any thoughts? – Je Je Oct 01 '19 at 23:33
  • @ImportanceOfBeingErnest isn't it the principal of this website to help each others? When you ask a question on this website, multiple answers are given to the user, and the example you you sent didn't prompt because it had very low keywords! – Je Je Oct 01 '19 at 23:36
  • 1
    @NonoLondon Sure: I confused the indexing: not `('_', '')[i>0]` but `('', '_')[i>0]` (or `('_', '')[i==0]`). Sorry, I'll edit... – SpghttCd Oct 02 '19 at 04:51
  • 1
    @SpghttCd Thank you so much, I can now use the loop. Much appreciate your time. Tvm – Je Je Oct 03 '19 at 19:44
1

Not sure if this will work for you as I have not tested it but worth a try.


#https://stackoverflow.com/questions/41625077/python-pandas-split-a-timeserie-per-month-or-week
weeks_df_list = [g for n, g in daily_data_df.groupby(_pd.Grouper(key='Transaction Date', freq='W'))]

for my_df in weeks_df_list:
    my_df['day_of_the_week'] = my_df['Transaction Date'].dt.weekday_name
    my_df.set_index(keys=['day_of_the_week'], drop=True, inplace=True)

fig, axs = plt.subplots(number_of_charts, 1, sharex=True, figsize=[8, 17])

# Adjust horizontal space between axes
fig.subplots_adjust(hspace=.5)
for i in range(number_of_charts):
    print("i:", i)
    #axs[i].set_yticks(np.arange(-0.9, 1.0, 0.4))
    #axs[i].set_ylim(-1, 1)
    #axs[i] = weeks_df_list[i]['pct_daily_vol'].multiply(100).round(1).plot(label='% Daily Volumes')
    #percent daily
    axs[i].plot(weeks_df_list[i]['pct_daily_vol'].multiply(100).round(2), label='% Daily Volumes',
                 color='blue')
    axs[i].yaxis.set_major_formatter(mtick.PercentFormatter())
    #percent daily max
    axs[i].plot(weeks_df_list[i]['pct_daily_limit'].multiply(100).round(2), label='% Daily Limit',
                 color='orange')
    axs[i].yaxis.set_major_formatter(mtick.PercentFormatter())


    #secondary axis
    axs_2 = axs[i].twinx()
    axs_2.plot(weeks_df_list[i]['vwap'], label='VWAP Paid', color='green')


    #comon variables
    axs[i].set_yticks(_np.arange(0, 100, 20))
    axs[i].set_ylim(0, 100)
    axs[i].set_title('Week:' + str(i + 1))
    axs[i].grid(True)

fig.legend(loc=0)


plt.show()

Apologies if the results are not what you wanted.

After seeing the question's author's comment, I am editing the post by adding one more suggestion. The idea is to remove all the pyplot coded legends and make our own legends. Please note the necessary import at the top (Line2D). Please add it to to your code at the top.

from matplotlib.lines import Line2D

#https://stackoverflow.com/questions/41625077/python-pandas-split-a-timeserie-per-month-or-week
weeks_df_list = [g for n, g in daily_data_df.groupby(_pd.Grouper(key='Transaction Date', freq='W'))]

for my_df in weeks_df_list:
    my_df['day_of_the_week'] = my_df['Transaction Date'].dt.weekday_name
    my_df.set_index(keys=['day_of_the_week'], drop=True, inplace=True)

fig, axs = plt.subplots(number_of_charts, 1, sharex=True, figsize=[8, 17])

# Adjust horizontal space between axes
fig.subplots_adjust(hspace=.5)
for i in range(number_of_charts):
    print("i:", i)
    #axs[i].set_yticks(np.arange(-0.9, 1.0, 0.4))
    #axs[i].set_ylim(-1, 1)
    #axs[i] = weeks_df_list[i]['pct_daily_vol'].multiply(100).round(1).plot()
    #percent daily
    axs[i].plot(weeks_df_list[i]['pct_daily_vol'].multiply(100).round(2),color='blue')
    axs[i].yaxis.set_major_formatter(mtick.PercentFormatter())
    #percent daily max
    axs[i].plot(weeks_df_list[i]['pct_daily_limit'].multiply(100).round(2),color='orange')
    axs[i].yaxis.set_major_formatter(mtick.PercentFormatter())


    #secondary axis
    axs_2 = axs[i].twinx()
    axs_2.plot(weeks_df_list[i]['vwap'],color='green')


    #comon variables
    axs[i].set_yticks(_np.arange(0, 100, 20))
    axs[i].set_ylim(0, 100)
    axs[i].set_title('Week:' + str(i + 1))
    axs[i].grid(True)

Manual_Legends = [Line2D([0],[0],color='blue',label='% Daily Volumes'),Line2D([0],[0],color='orange',label='% Daily Volumes'),Line2D([0],[0],color='green',label='VWAP Paid')]
plt.legend(handles=Manual_Legends,loc='lower center',,bbox_to_anchor=(0.5,-0.35),ncol=3,title='Legend of the plot')
plt.show()

After seeing that you have already posted the data, I could make it work at my end. Please see the attachment below. Is this what you wanted? Please let me know.

enter image description here

Amit
  • 2,018
  • 1
  • 8
  • 12
  • because loc=0 is for best location that pyplot thinks. Instead of fig.legend(loc=0) use what @SpghttCd has suggested you. loc=8 stands for bottom of the figure. – Amit Oct 01 '19 at 21:59
  • @NonoLondon I edited the answer by adding a new answer. Please see if it works. Since I don't have access to your data, I have not tested it. – Amit Oct 01 '19 at 22:30
  • @NonoLondon .. To some extent I could make it work. – Amit Oct 01 '19 at 22:58
  • Yes! but with the legend below the chart. Tx Very much – Je Je Oct 02 '19 at 00:01
  • 1
    @NonoLondon Please check. I edited the answer and the attached image. If this is not what you wanted then don't hesitate to ask for another edit. – Amit Oct 02 '19 at 00:21
  • Txand great, any idea of how to have the legend serie' names horizontal, rather than vertical? – Je Je Oct 02 '19 at 00:34
  • You mean the three legends side by side and not on top of each other? – Amit Oct 02 '19 at 00:36
  • yes, exactly what i mean. with ideal world being that they tabulate(in case there are more next time) – Je Je Oct 02 '19 at 00:41