2

I want to change the color of a bar in matplotlib's grouped barplot if it meets a certain condition. I'm plotting two bars for each species - one for today and one for avg, where avg contains yerr errorbars that show the 10th and 90th percentile values.

Now I want the avg bar to be green if today's length value > 10th percentile, and red if today's length value < 10th percentile.

I tried the solutions in these posts

but the bars are always green.

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

z = pd.DataFrame(data={'length': [40,35,34,40,36,39,38,44,40,39,35,46],
                       'species': ['A','A','A','A','B','B','B','B','C','C','C','C'],
                       'type': ['today','avg','10pc','90pc','today','avg','10pc','90pc','today','avg','10pc','90pc']
                      },
                )
z['Date'] = pd.to_datetime('2021-09-20')
z.set_index('Date',inplace=True)

z0 = z.loc[(z.type=='today') | (z.type=='avg')] # average length and today's length
z1 = z.loc[(z.type=='10pc') | (z.type=='90pc')] # 10th and 90th percentile

z2 = []
for n in z.species.unique().tolist():
    dz = z.loc[(z.species==n) & (z.type=='today'),'length'].values[0] - z.loc[(z.species==n) & (z.type=='10pc'),'length'].values[0]
    if dw>0:
        z2.append(1)
    else:
        z2.append(0)

errors = z1.pivot_table(columns=[z1.index,'species'],index='type',values=['length']).values
avgs = z0.length[z0.type=='avg'].values
bars = np.stack((np.absolute(errors-avgs), np.zeros([2,z1.species.unique().size])), axis=0)

col = ['pink']
for k in z2:
    if k==1:
        col.append('g') # length within 10% bounds = green
    else:
        col.append('r') # length outside 10% bounds = red

fig, ax = plt.subplots()
z0.pivot(index='species', columns='type', values='length').plot(kind='bar', yerr=bars, ax=ax, color=col, capsize=0)
ax.set_title(z0.index[0].strftime('%d %b %Y'), fontsize=16)
ax.set_xlabel('species', fontsize=14)
ax.set_ylabel('length (cm)', fontsize=14)

plt.show()

enter image description here

Medulla Oblongata
  • 3,771
  • 8
  • 36
  • 75

1 Answers1

1

One way is to overwrite the colors after creating the plot. First you need to change the line that initialize col with

col = ['pink']*z['species'].nunique()

to get the numbers of avg bars, then the same for loop to add g or r depending on your case. Finally, change this

fig, ax = plt.subplots()
z0.pivot(index='species', columns='type', values='length')\
  .plot(kind='bar', yerr=bars, ax=ax, 
        color=['pink','g'], capsize=0) # here use only pink and g
# here overwrite the colors 
for p, c in zip(ax.patches, col):
    p.set_color(c)
ax.set_title...

Note that the legend for today is green even if you have a red bar, could be confusing.

Here is the full working example, adding the red entry in the legend thanks to this answer

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches # import this extra

z = pd.DataFrame(data={'length': [40,35,34,40,36,39,38,44,40,39,35,46],
                       'species': ['A','A','A','A','B','B','B','B','C','C','C','C'],
                       'type': ['today','avg','10pc','90pc','today','avg','10pc','90pc','today','avg','10pc','90pc']
                      },
                )
z['Date'] = pd.to_datetime('2021-09-20')
z.set_index('Date',inplace=True)

z0 = z.loc[(z.type=='today') | (z.type=='avg')] # average length and today's length
z1 = z.loc[(z.type=='10pc') | (z.type=='90pc')] # 10th and 90th percentile

z2 = []
for n in z.species.unique().tolist():
    dz = z.loc[(z.species==n) & (z.type=='today'),'length'].values[0] - z.loc[(z.species==n) & (z.type=='10pc'),'length'].values[0]
    if dz>0:
        z2.append(1)
    else:
        z2.append(0)

errors = z1.pivot_table(columns=[z1.index,'species'],index='type',values=['length']).values
avgs = z0.length[z0.type=='avg'].values
bars = np.stack((np.absolute(errors-avgs), np.zeros([2,z1.species.unique().size])), axis=0)

col = ['pink']*z['species'].nunique()
for k in z2:
    if k==1:
        col.append('g') # length within 10% bounds = green
    else:
        col.append('r') # length outside 10% bounds = red
print(col)
# ['pink', 'pink', 'pink', 'g', 'r', 'g']

fig, ax = plt.subplots()
z0.pivot(index='species', columns='type', values='length').plot(kind='bar', yerr=bars, ax=ax, color=['pink','g'], capsize=0)
for p, c in zip(ax.patches, col):
    p.set_color(c)
ax.set_title(z0.index[0].strftime('%d %b %Y'), fontsize=16)
ax.set_xlabel('species', fontsize=14)
ax.set_ylabel('length (cm)', fontsize=14)

handles = None
labels = None

if 0 in z2: ## add the entry on the legend only when there is red bar
    # where some data has already been plotted to ax
    handles, labels = ax.get_legend_handles_labels()
    # manually define a new patch 
    patch = mpatches.Patch(color='r', label='today')
    # handles is a list, so append manual patch
    handles.append(patch) 
    # plot the legend
    plt.legend(handles=handles)
else:
    # plot the legend when there isn't red bar
    plt.legend(handles=handles)

plt.show()

and I get the red bar enter image description here

Medulla Oblongata
  • 3,771
  • 8
  • 36
  • 75
Ben.T
  • 29,160
  • 6
  • 32
  • 54