Set custom median line color and set tick-label colors to boxplot face colors

Question:

I’m using this nice boxplot graph, answer from @Parfait.

  1. I got an out of bound error on j and had to use range(i*5,i*5+5). Why?
  2. I’d like to set the median to a particular color, let’s say red. medianprops=dict(color="red") won’t work. How to do it?
  3. How to set the y-axis tick labels to the same color as the boxes?

Disclaimer: I don’t know what I’m doing.

Here’s the code using random data :

# import the required library 
import numpy as np 
import pandas as pd 
import matplotlib.pyplot as plt
import seaborn as sns

import string 
import matplotlib.colors as mc
import colorsys

# data
df = pd.DataFrame(np.random.normal(np.random.randint(5,15),np.random.randint(1,5),size=(100, 16)), columns=list(string.ascii_uppercase)[:16])

# Boxplot
fig, ax = plt.subplots(figsize=(9, 10))
medianprops=dict(color="red")
ax = sns.boxplot(data=df, orient="h", showfliers=False, palette = "husl")
ax = sns.stripplot(data=df, orient="h", jitter=True, size=7, alpha=0.5, palette = "husl")     # show data points
ax.set_title("Title")
plt.xlabel("X label")

def lighten_color(color, amount=0.5):  
    # --------------------- SOURCE: @IanHincks ---------------------
    try:
        c = mc.cnames[color]
    except:
        c = color
    c = colorsys.rgb_to_hls(*mc.to_rgb(c))
    return colorsys.hls_to_rgb(c[0], 1 - amount * (1 - c[1]), c[2])

for i,artist in enumerate(ax.artists):
    # Set the linecolor on the artist to the facecolor, and set the facecolor to None
    col = lighten_color(artist.get_facecolor(), 1.2)
    artist.set_edgecolor(col)    

    # Each box has 6 associated Line2D objects (to make the whiskers, fliers, etc.)
    # Loop over them here, and use the same colour as above
    for j in range(i*5,i*5+5):
        line = ax.lines[j]
        line.set_color(col)
        line.set_mfc(col)
        line.set_mec(col)
        #line.set_linewidth(0.5)

enter image description here

Asked By: macxpat

||

Answers:

I just answer point 2. of my question.

After tinkering, I found this to work :

    # Each box has 5 associated Line2D objects (the whiskers and median)
    # Loop over them here, and use the same colour as above
    n=5  # this was for tinkering
    for j in range(i*n,i*n+n):
        if j != i*n+4 : line = ax.lines[j]  # not the median
        line.set_color(col)

Again, I don’t know what I’m doing. So someone more knowledgeable may provide a more valuable answer.

I removed the stripplot for better clarity.

enter image description here

Answered By: macxpat

Update: changed division by len(ax.artists) to len(ax.get_yticklabels(), to make the code more robust and also work with the latest (0.12.2) seaborn version.

To change the color of the median, you can use the medianprops in sns.boxplot(..., medianprops=...). If you also set a unique label, that label can be tested again when iterating through the lines.

To know how many lines belong to each boxplot, you can divide the number of lines by the number of artists (just after the boxplot has been created, before other elements have been added to the plot). Note that a line potentially has 3 colors: the line color, the marker face color and the marker edge color. Matplotlib creates the fliers as an invisible line with markers. The code below thus also changes these colors to make it more robust to different options and possible future changes.

Looping simultaneously through the boxes and the y tick labels allows copying the color. Making them a bit larger and darker helps for readability.

import matplotlib.pyplot as plt
from matplotlib.colors import rgb_to_hsv, hsv_to_rgb, to_rgb
import seaborn as sns
import pandas as pd
import numpy as np

def enlighten(color, factor=0.5):
    h, s, v = rgb_to_hsv(to_rgb(color))
    return hsv_to_rgb((h, s, 1 - factor * (1 - v)))

def endarken(color, factor=0.5):
    h, s, v = rgb_to_hsv(to_rgb(color))
    return hsv_to_rgb((h, s, factor * v))

df = pd.DataFrame(np.random.normal(1, 5, size=(100, 16)).cumsum(axis=0),
                  columns=['Hydrogen', 'Helium', 'Lithium', 'Beryllium', 'Boron', 'Carbon', 'Nitrogen', 'Oxygen',
                           'Fluorine', 'Neon', 'Sodium', 'Magnesium', 'Aluminum', 'Silicon', 'Phosphorus', 'Sulfur'])

sns.set_style('white')
fig, ax = plt.subplots(figsize=(9, 10))

colors = sns.color_palette("husl", len(df.columns))
sns.boxplot(data=df, orient="h", showfliers=False, palette='husl',
            medianprops=dict(color="yellow", label='median'), ax=ax)

lines_per_boxplot = len(ax.lines) // len(ax.get_yticklabels())
for i, (box, ytick) in enumerate(zip(ax.artists, ax.get_yticklabels())):
    ytick.set_color(endarken(box.get_facecolor()))
    ytick.set_fontsize(20)
    color = enlighten(box.get_facecolor())
    box.set_color(color)
    for lin in ax.lines[i * lines_per_boxplot: (i + 1) * lines_per_boxplot]:
        if lin.get_label() != 'median':
            lin.set_color(color)
            lin.set_markerfacecolor(color)
            lin.set_markeredgecolor(color)
sns.stripplot(data=df, orient="h", jitter=True, size=7, alpha=0.5, palette='husl', ax=ax)
sns.despine(ax=ax)
ax.set_title("Title")
ax.set_xlabel("X label")
plt.tight_layout()
plt.show()

sns.boxplot with line colors changed to boxplot colors

Answered By: JohanC
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.