How to add multiple data labels in a bar chart - python

I have two grouped bar charts of value changes between cases for two systems among 3 groups as below. Here I was able to add data labels to the bars using the code below (figure produced attached)
What I want to do is on top (or bottom for the negative change in value cases), add an extra data label that captures the % of the value changes as shown in the second figure with the 33% in red (I edited it in by hands). How do I achieve that from this code? Thank you in advance.
import matplotlib.pyplot as plt
import numpy as np
value_case0_system1 = np.array([30, 20, 40])
value_case1_system1 = np.array([20, 25, 50])
value_case2_system1 = np.array([10, 35, 45])
value_case1_system2 = np.array([60, 50, 40])
value_case2_system2 = np.array([50, 40, 55])
change_case0_to_case1_system1 = np.subtract(value_case1_system1,value_case0_system1)
change_case1_to_case2_system1 = np.subtract(value_case2_system1,value_case1_system1)
change_case1_to_case2_system2 = np.subtract(value_case2_system2,value_case1_system2)
fig, (ax0, ax1) = plt.subplots(nrows=1, ncols=2, sharey=True, figsize=(18,10))
labels = ['Group 1', 'Group 2', 'Group 3']
x = np.arange(len(labels))
ax0.set_ylabel('Change in Values', va='center', rotation='vertical',
fontsize=17, fontweight='bold')
width = 0.28
ax0.set_title('System 1', fontsize=17, fontweight='bold')
ax0.axhline(y=0, color='black', ls=':', lw=2)
ax0.set_xticks(x)
ax0.set_xticklabels(labels,fontsize=15)
rects1 = ax0.bar(x-width/2, change_case0_to_case1_system1, width, label='Case 0 to Case 1',
color='#292929', edgecolor='black', linewidth=1)
rects2 = ax0.bar(x+width/2, change_case1_to_case2_system1, width, label='Case 1 to Case 2',
color='#7f6d5f', edgecolor='black', linewidth=1)
ax0.bar_label(rects1, padding=3, fontsize=11)
ax0.bar_label(rects2, padding=3, fontsize=11)
leg = ax0.legend(loc="upper left", bbox_to_anchor=[0, 1],
ncol=1, fancybox=True)
ax0.legend(fontsize=15)
ax1.set_title('System 2', fontsize=17, fontweight='bold')
ax1.axhline(y=0, color='black', ls=':', lw=2)
ax1.set_xticks(x)
ax1.set_xticklabels(labels,fontsize=15)
rects3 = ax1.bar(x, change_case1_to_case2_system2, width, label='Case 1 to Case 2',
color='#7f6d5f', edgecolor='black', linewidth=1)
ax1.legend(shadow=True, fancybox=True)
ax1.bar_label(rects3, padding=3, fontsize=11)
leg = ax1.legend(loc="upper left", bbox_to_anchor=[0, 1],
ncol=1, fancybox=True)
ax1.legend(fontsize=15)
plt.tight_layout()
plt.show()

The code for the extra plot formatting has been left out, because it's not relevant for the answer. It can be added back, as per your requirements.
Each .bar_label colors the label globally, so unlike this answer, a second .bar_label needs to be added for the percent change, with a different color and padding
For each case-to-case, calculate the percent change, and set the string format in a list comprehension.
Set the list of string formatted calculations to the labels parameter in .bar_label.
Given the code in the OP, 6 lines of code need to be added, 3 for creating the list of labels, and 3 for adding the labels to the plot.
Additional resources:
matplotlib: Bar Label Demo
Adding value labels on a matplotlib bar chart
Tested in python 3.8.11, matplotlib 3.4.3
change_case0_to_case1_system1 = np.subtract(value_case1_system1, value_case0_system1)
# add list of string formatted percent change calculation
per_change_case0_to_case1_system1 = [f'({v}%)' for v in (change_case0_to_case1_system1 / value_case0_system1).round(2)*100]
change_case1_to_case2_system1 = np.subtract(value_case2_system1, value_case1_system1)
# add list of string formatted percent change calculation
per_change_case1_to_case2_system1 = [f'({v}%)' for v in (change_case1_to_case2_system1 / value_case1_system1).round(2)*100]
change_case1_to_case2_system2 = np.subtract(value_case2_system2, value_case1_system2)
# add list of string formatted percent change calculation
per_case1_to_case2_system2 = [f'({v}%)' for v in (change_case1_to_case2_system2 / value_case1_system2).round(2)*100]
fig, (ax0, ax1) = plt.subplots(nrows=1, ncols=2, sharey=True, figsize=(18,10))
labels = ['Group 1', 'Group 2', 'Group 3']
x = np.arange(len(labels))
width = 0.28
ax0.set_xticks(x)
ax0.set_xticklabels(labels, fontsize=15)
rects1 = ax0.bar(x-width/2, change_case0_to_case1_system1, width, label='Case 0 to Case 1', color='#292929', edgecolor='black', linewidth=1)
rects2 = ax0.bar(x+width/2, change_case1_to_case2_system1, width, label='Case 1 to Case 2', color='#7f6d5f', edgecolor='black', linewidth=1)
ax0.bar_label(rects1, padding=3, fontsize=11)
# add a second annotation with the string formatted labels
ax0.bar_label(rects1, labels=per_change_case0_to_case1_system1, padding=15, fontsize=11, color='red')
ax0.bar_label(rects2, padding=3, fontsize=11)
# add a second annotation with the string formatted labels
ax0.bar_label(rects2, labels=per_change_case1_to_case2_system1, padding=15, fontsize=11, color='red')
rects3 = ax1.bar(x, change_case1_to_case2_system2, width, label='Case 1 to Case 2', color='#7f6d5f', edgecolor='black', linewidth=1)
ax1.set_xticks(x)
ax1.set_xticklabels(labels,fontsize=15)
ax1.bar_label(rects3, padding=3, fontsize=11)
# add a second annotation with the string formatted labels
ax1.bar_label(rects3, labels=per_case1_to_case2_system2, padding=15, fontsize=11, color='red')
plt.tight_layout()
plt.show()

Related

x-labels rendered twice on matplotlib plot

I have a plot that uses 9 labels on the x-axis. However, because I have split up the plot into two axes, it seems that it requires 18 labels (hence the added list of empty strings) instead for some reason.
This seems to make the x-labels be rendered twice, making them seem to have a bold typeface. Image of the problem is attached.
And here is the current code I'm using. I apologize for the quality of the code. I am new to matplotlib.
benchmark_data = all_benchmark_data[loader.pingpongKey]
fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(9,7), dpi=80)
fig.subplots_adjust(hspace=0.05)
ax1.boxplot(benchmark_data.values())
ax2.boxplot(benchmark_data.values())
ax1.set_ylim(380, 650)
ax2.set_ylim(110, 180)
# hide the spines between ax and ax2
ax1.spines.bottom.set_visible(False)
ax2.spines.top.set_visible(False)
ax1.xaxis.tick_top()
ax1.tick_params(labeltop=False) # don't put tick labels at the top
ax2.xaxis.tick_bottom()
ax1.tick_params(axis='both', labelsize=10)
ax2.tick_params(axis='both', labelsize=10)
xlabels = ['', '', '', '', '', '', '', '', ''] + (list(benchmark_data.keys()))
ax1.set_xticklabels(xlabels)
ax1.set_ylabel('Time (ms)', fontsize=10)
ax1.yaxis.set_label_coords(-0.06,0)
#ax2.set_ylabel('Time (ms)', fontsize=10)
plt.xticks(fontsize=10, rotation=45)
ax1.yaxis.set_major_locator(ticker.MaxNLocator(nbins=5, min_n_ticks=5))
ax2.yaxis.set_major_locator(ticker.MaxNLocator(nbins=5, min_n_ticks=5))
d = .5 # proportion of vertical to horizontal extent of the slanted line
kwargs = dict(marker=[(-1, -d), (1, d)], markersize=12,
linestyle="none", color='k', mec='k', mew=1, clip_on=False)
ax1.plot([0, 1], [0, 0], transform=ax1.transAxes, **kwargs)
ax2.plot([0, 1], [1, 1], transform=ax2.transAxes, **kwargs)
plt.tight_layout()
plt.savefig('plots/boxplots/' + loader.pingpongKey + '-boxplot.png',
bbox_inches='tight')
Because you are using sharex=True, the second time you plot the boxplot you will create another 9 ticks that are added to the axis (which is in common between ax1 and ax2). The solution in your case is to turn off sharex (the axis will be aligned anyway) and set the xticklabels on ax2:
# No sharex.
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(9,7), dpi=80)
# ...
# Set ticks for ax2 instead of ax1 and only the 9 labels are needed.
ax2.set_xticklabels(list(benchmark_data.keys()))

Need Assistance: Unable to get datetime as x ticks with bar chart

I am trying to make a combined graph for one of my systems. I am trying to visualize time series data using bar chart along with line chart(with step).
This is a time series data therefore, I want to show datetime in the x label.
When I plot bar chart with df2['datetime'].index I get plots as expected but I do not get datetime as x-ticks. I believe that this is because I am using df2['datetime'].index.
When I plot bar chart with df2['datetime'] I get datetime in x-ticks however, the plot does not look realistic.
I am also attaching a snapshot of my data for better understanding. Any suggestions will be useful. Thank you.
fig,(ax0,ax1) = plt.subplots(1,2, figsize = (24,12))
ax0.bar(df2['datetime'].index, df2['T1'], width = 1, color='blue', align = 'edge', alpha = 0.5, label ='T1')
ax0.bar(df2['datetime'].index, -1*df2['T2'], width = 1, color='red', align = 'edge',alpha = 0.5, label ='T2')
ax0.step(df2['datetime'].index, df2['T3'], color='blue', linewidth=2, where = 'post', label ='T3')
ax0.step(df2['datetime'].index, -1*df2['T4'], color='red', linewidth=2, where = 'post', label ='T4')
ax00 = ax0.twinx()
ax00.step(df2['datetime'].index, df2['A1'], color='b', linestyle = '--', linewidth=1, where = 'post', label ='A1')
ax00.step(df2['datetime'].index, df2['A2'], color='r', linestyle = '--', linewidth=1, where = 'post', label ='A2')
ax00.set_ylabel('L2', fontsize=12, color='black')
ax0.set_ylabel("L1", fontsize=12, color='black')
ax0.set_xlabel("Datetime", fontsize=12, color='black')
ax0.set_title('Zone 2', fontsize=16, color='black')
ax0.grid(True)
ax0.legend(loc='upper left',fontsize = 12)
ax00.legend(loc='upper right',fontsize = 12)
ax1.bar(df2['datetime'], df2['T1'], width = 1, color='blue', align = 'edge', alpha = 0.5, label ='T1')
ax1.bar(df2['datetime'], -1*df2['T2'], width = 1, color='red', align = 'edge',alpha = 0.5, label ='T2')
ax1.step(df2['datetime'], df2['T3'], color='blue', linewidth=2, where = 'post', label ='T3')
ax1.step(df2['datetime'], -1*df2['T4'], color='red', linewidth=2, where = 'post', label ='T4')
ax01 = ax1.twinx()
ax01.step(df2['datetime'], df2['A1'], color='b', linestyle = '--', linewidth=1, where = 'post', label ='A1')
ax01.step(df2['datetime'], df2['A2'], color='r', linestyle = '--', linewidth=1, where = 'post', label ='A2')
ax01.set_ylabel('L2', fontsize=12, color='black')
ax1.set_ylabel("L1", fontsize=12, color='black')
ax1.set_xlabel("Datetime", fontsize=12, color='black')
ax1.set_title('Zone 1', fontsize=16, color='black')
ax1.grid(True)
ax1.legend(loc='upper left',fontsize = 12)[![enter image description here][1]][1]
ax01.legend(loc='upper right',fontsize = 12)
plt.show()
The problem probably comes from width = 1, because using datetime, a width of 1 is not equal to the size between two datapoints.
Try to use a smaller width in
ax1.bar(df2['datetime'], df2['T1'], width = 1, color='blue', align = 'edge', alpha = 0.5, label ='T1')
ax1.bar(df2['datetime'], -1*df2['T2'], width = 1, color='red', align = 'edge',alpha = 0.5, label ='T2')
You can also compute what exact width you need, but I'll let you search how to do that ;)

Matplotlib/Seaborn: ValueError when annotating 2nd subplot (annotating 1st subplot was successful)

I created this figure with two subplots and wanted to annotate both subplots separately (see figure and code below)
# Declare fig ax
fig, ax = plt.subplots(2,1,figsize=[12,10], sharey=True)
# Line plot for ax[0]
sns.lineplot(x=['2020-01-01', '2020-01-31'], y=[32.7477, 49.6184], ax=ax[0], marker='o', markersize=10)
# Add title for ax[0]
ax[0].set(title='Figure 1', xlabel=None, ylabel="Index")
ax[0].tick_params(axis = 'x', rotation = 45)
# Annotation for ax[0]
ax[0].text(0, 55, 52.53)
ax[0].text(0.4, 45, "Some annotation here", horizontalalignment='center', size='medium', color='black')
ax[0].text(1, 52, 49.62, horizontalalignment='center', size='medium', color='black')
# Line plot for ax[1]
sns.lineplot(x="date", y="ari", data=df_ari_jan, ax=ax[1], marker='o', markersize=10)
# Add title for ax[1]
ax[1].set(title='Figure 2', xlabel=None, ylabel="Index")
ax[1].set_xticks(df_ari_jan["date"].values)
ax[1].tick_params(axis = 'x', rotation = 45)
# Annotation for ax[1]
ax[1].text(-1, 0, 52.53)
fig.tight_layout()
Annotating the first subplot was fine, but I kept getting this ValueError: Image size of 15006958x630 pixels is too large. It must be less than 2^16 in each direction. when trying to annotate the 2nd one.
fig.tight_layout() wouldn't work either UserWarning: Tight layout not applied. The left and right margins cannot be made large enough to accommodate all axes decorations.
Any idea as to why?

Set up labels and legend using plt.subplots()

I am using plt.sublot to create a figure with 12 subplots that share the same data so I want to show the labels and legend for one of them. I am accessing and plotting the data from a dictionary that contains pandas dataframes each with 20 columns(labels). Here my code:
fig, axes = plt.subplots(nrows=3, ncols=4, sharex=True, sharey=True)
plt.subplots_adjust(left = 0.06, bottom = 0.1, right = 0.8, top=0.9,
wspace=0.15, hspace=0.15)
fig.suptitle('HMC Water Balance', fontsize = 20, y= 0.95, x=0.45)
axes[0,0].plot(HMC_hydrographs['outlet'])
axes[0,1].plot(HMC_hydrographs['Outlet00'])
axes[0,2].plot(HMC_hydrographs['Outlet01'])
axes[0,3].plot(HMC_hydrographs['Outlet02'], label =
'Q_total','Q_reset','Q_river_initial', ...'20th_column_name')
ax = axes[0,3]
ax.legend(loc=0, prop={'size':8})
axes[1,0].plot(HMC_hydrographs['Outlet03'])
ax = axes[1,0]
ax.set_ylabel('Flux (m$^3$/s)', labelpad=10, fontsize = 18)
axes[1,1].plot(HMC_hydrographs['Outlet04'])
axes[1,2].plot(HMC_hydrographs['Outlet05'])
axes[1,3].plot(HMC_hydrographs['Outlet06'])
axes[2,0].plot(HMC_hydrographs['Outlet07'])
axes[2,1].plot(HMC_hydrographs['Outlet08'])
ax = axes[2,1]
ax.set_xlabel('Time (days)', fontsize = 18)
ax.xaxis.set_label_coords(1.1,-0.2)
axes[2,2].plot(HMC_hydrographs['Outlet09'])
axes[2,3].plot(HMC_hydrographs['Outlet10'])
I get the error:
File "<ipython-input-249-7e4552c68d90>", line 8
axes[0,3].plot(HMC_hydrographs['Outlet02'], label =
'Q_total','Q_reset','Q_river_initial')
^
SyntaxError: positional argument follows keyword argument
For what I understand the label argument takes only one argument but I have more than one label!
Please help me understand how to call the labels so they show like when I plot a single graph like:
fig = plt.figure()
ax = HMC_hydrographs['Outlet01'].plot()
Individual plot showing the correct labels
Not sure of the reasoning behind it but the way I managed to get the legend to show was to specify the labels directly into the legend argument, not using the 'label' argument. Here is the code:
fig, axes = plt.subplots(nrows=3, ncols=4, sharex=True, sharey=True)
plt.subplots_adjust(left = 0.06, bottom = 0.1, right = 0.8, top=0.9,
wspace=0.15, hspace=0.15)
fig.suptitle('HMC Water Balance', fontsize = 20, y= 0.95, x=0.45)
axes[0,0].plot(HMC_hydrographs['outlet'])
axes[0,1].plot(HMC_hydrographs['Outlet00'])
axes[0,2].plot(HMC_hydrographs['Outlet01'])
axes[0,3].plot(HMC_hydrographs['Outlet02'])
ax = axes[0,3]
ax.legend(hydro_header, bbox_to_anchor=(1.05, 1), loc=2,
borderaxespad=0.)
axes[1,0].plot(HMC_hydrographs['Outlet03'])
ax = axes[1,0]
ax.set_ylabel('Flux (m$^3$/s)', labelpad=10, fontsize = 18)
axes[1,1].plot(HMC_hydrographs['Outlet04'])
axes[1,2].plot(HMC_hydrographs['Outlet05'])
axes[1,3].plot(HMC_hydrographs['Outlet06'])
axes[2,0].plot(HMC_hydrographs['Outlet07'])
axes[2,1].plot(HMC_hydrographs['Outlet08'])
ax = axes[2,1]
ax.set_xlabel('Time (days)', fontsize = 18)
ax.xaxis.set_label_coords(1.1,-0.2)
axes[2,2].plot(HMC_hydrographs['Outlet09'])
axes[2,3].plot(HMC_hydrographs['Outlet10'])
hydro_header contains a list with the column names(labels) for my plots that I got by using:
hydro_header = list(HMC_hydrographs['outlet'])
Final figure with the subplots and legend

Python: Combined Legend for Matplotlib Subplot

I'm trying to make a combined legend in a Jupiter Notebook. When I try various codes from examples, I get an empty legend. The examples work fine copied, but something goes wrong when I implement it into my own code. Any ideas?
Result:
Code:
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, sharex=True, figsize=(15,10))
l1 = ax1.plot(time[18206:18226],tpm2[18206:18226], 'r', label='Chilbolton 2')
ax1.set_title('Difference in Hydrometeor Count Per Minute Over Time')
ax1.set_ylim([0,14000])
ax1.grid(b=True, which='major', color='k', linestyle='--', alpha=0.5)
l2 = ax2.plot(time[18206:18226],tpm1[18206:18226], 'b', label='Chilbolton 2')
ax2.set_ylim([0,14000])
ax2.grid(b=True, which='major', color='k', linestyle='--', alpha=0.5)
l3 = ax3.plot(time[18206:18226],diff[18206:18226], 'k', label='D.P.M.')
ax3.plot(time[18206:18226],np.zeros(20),'k--')
ax3.set_xlabel('Time (10th February to 29th April)')
ax3.set_ylim([-3000,3000])
ax3.grid(b=True, which='major', color='k', linestyle='--', alpha=0.5)
#plt.legend( handles=[l1, l2, l3], labels=['l1','l2','l3'],loc="upper left", bbox_to_anchor=[0, 1],
# ncol=2, shadow=True, title="Legend", fancybox=True)
fig.legend((l1, l2, l3), ('Line 1', 'Line 2', 'Line 3'), 'upper left')
# ('Chilbolton 2','Chilbolton 2','D.P.M.'), loc = (0.5, 0), ncol=1 )
plt.ylabel('Hydrometeor Count (#)')
# Fine-tune figure; make subplots close to each other and hide x ticks for
# all but bottom plot.
#f.subplots_adjust(hspace=0)
plt.setp([a.get_xticklabels() for a in f.axes[-1:]], rotation=90, visible=True)
plt.show()
ax.plot() returns a list of line artists, even if you are only plotting just one line. So when you write l1 = ax1.plot(...), a list of length-1 is assigned to l1. Ditto for l2 and l3. This causes a problem for fig.legend(), which needs just the line artist objects.
You can fix this problem in a number of ways. The most commonly-used method is syntax like:
l1, = ax1.plot(...
Inserting the comma assigns the only element of the returned list to l1. You could also do l1 = ax1.plot(...)[0]. Or, in your case, you could modify your legend call to fig.legend((l1[0],l2[0],l3[0]),...) .
So,
import maptlotlib.pyplot as plt
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, sharex=True, figsize=(15,10))
l1, = ax1.plot([0,1],[0,14000])
ax1.set_title('Difference in Hydrometeor Count Per Minute Over Time')
ax1.set_ylim([0,14000])
ax1.grid(b=True, which='major', color='k', linestyle='--', alpha=0.5)
l2, = ax2.plot([0,1],[0,14000])
ax2.set_ylim([0,14000])
ax2.grid(b=True, which='major', color='k', linestyle='--', alpha=0.5)
l3, = ax3.plot([0,1],[-3000,3000])
ax3.plot(time[18206:18226],np.zeros(20),'k--')
ax3.set_xlabel('Time (10th February to 29th April)')
ax3.set_ylim([-3000,3000])
ax3.grid(b=True, which='major', color='k', linestyle='--', alpha=0.5)
fig.legend((l1, l2, l3), ('Line 1', 'Line 2', 'Line 3'), 'upper left')
As a workaround, you could create your own custom legend using Patch:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import random
tx = range(20)
t1 = np.random.randint(0, 14000, 20)
t2 = np.random.randint(0, 14000, 20)
t3 = np.random.randint(-3000, 3000, 20)
labels = ['Chilbolton 2', 'Chilbolton 2', 'D.P.M.']
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, sharex=True, figsize=(15,10))
l1 = ax1.plot(tx, t1, 'r', label=labels[0])
ax1.set_title('Difference in Hydrometeor Count Per Minute Over Time')
ax1.set_ylim([0,14000])
ax1.grid(b=True, which='major', color='k', linestyle='--', alpha=0.5)
l2 = ax2.plot(tx,t2, 'b', label=labels[1])
ax2.set_ylim([0,14000])
ax2.grid(b=True, which='major', color='k', linestyle='--', alpha=0.5)
l3 = ax3.plot(tx,t3, 'k', label=labels[2])
ax3.plot(tx,np.zeros(20),'k--')
ax3.set_xlabel('Time (10th February to 29th April)')
ax3.set_ylim([-3000,3000])
ax3.grid(b=True, which='major', color='k', linestyle='--', alpha=0.5)
# Create custom legend
leg1 = mpatches.Patch(color='r')
leg2 = mpatches.Patch(color='b')
leg3 = mpatches.Patch(color='k')
fig.legend(handles=[leg1, leg2, leg3], labels=labels, loc="upper left")
plt.ylabel('Hydrometeor Count (#)')
# Fine-tune figure; make subplots close to each other and hide x ticks for
# all but bottom plot.
#f.subplots_adjust(hspace=0)
plt.setp([a.get_xticklabels() for a in fig.axes[-1:]], rotation=90, visible=True)
plt.show()
Giving you:

Categories

Resources