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

1 Answer 1

2

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

Sign up to request clarification or add additional context in comments.

4 Comments

the bar is still green
@MedullaOblongata see edit, added the extra color in the legend, and I do get red for the bar
thanks, this is great - I added an if/else statement in your code so that the legend doesn't contain a red entry when all the today bars are green
@MedullaOblongata you don't really need the else as by default the legend would be only with green and not the red, but good idea :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.