0

I'm trying to make a barplot in python, but I'm looking to add target markers as benchmark values on the barplot.

The line doesn't seem fit correctly?

Does anyone know what's going on or an alternate way to implement this?

I need to place lines on the bars(bar width) and not the entire bars for the benchmark value.

Basically I just want 1 line for each category(on the respective bar itself) showing this benchmark on the plot. I have tried implementing the same using vline in python, however the output is weird.

import seaborn as sns
import matplotlib.pyplot as plt

# create example data
data = {'x': ['A', 'B', 'C', 'D'],
        'y': [10, 5, 8, 12]}

# create bar plot
ax = sns.barplot(y='x', x='y', data=data, color='blue', width= 0.4, orient='h')

# add target lines
targets = {'B': 6}
for x, target_value in targets.items():
    ax.axvline(x=target_value, color='red', linestyle='--', linewidth=1, ymin=data['x'].index(x)-0.15, ymax=data['x'].index(x)+0.15)
    print(target_value,data['x'].index(x))
# display plot
plt.show()

Current Result

output

Expected output with multiple categories

Expected output with multiple categories

Using hardcoded values and trial and error would be a huge pain since I need to do this over multiple(40+) plots.

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158

3 Answers3

0

Even though your y-axis is now categorical, I believe axvline is still treating it as a vertical axis from 0 to 1, which makes the target marker show up in an incorrect spot.

I got something matching what I believe you intended with trial and error in the code below. Notice the values for ymin and ymax will evaluate to positive decimals between 0 and 1 (0.53 and 0.73 respectively) with the index for 'B' being 1.

import seaborn as sns
import matplotlib.pyplot as plt

data = {'x': ['A', 'B', 'C', 'D'],
        'y': [10,5,8,12]}

ax = sns.barplot(y='x', x='y', data = data, color='blue', orient='h')

targets = {'B': 6}
for x, target_value in targets.items():
    ax.axvline(x=target_value, color='red', linestyle="--", linewidth=1, ymin=data['x'].index(x)-0.47, ymax=data['x'].index(x)-0.27)
    print(target_value, data['x'].index(x))
    
plt.show()

enter image description here

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
PhDr_B
  • 1
  • 1
  • I need to do this for all categories and generate the lines on a dynamic basis given the benchmark values. Do you know of a way in which i would be able to accommodate that? Eg, another graph would have 5 categories – Pranav Sekhar Mar 15 '23 at 02:49
0

This might be an option for you.

import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd

# create example data as df
data = pd.DataFrame({
        'x': ['A', 'B', 'C', 'D'],
        'y': [10, 5, 8, 12]
        })

#prep plot
mywidth = .4
mylen = 1 / len(data) 
test = mywidth * mylen

# create bar plot
ax = sns.barplot(y='x', x='y', data=data, color='blue', width= mywidth, orient='h')

# add targets to df
data['targets'] = [2,6,3,7]

#draw targets
for i in data['targets']:
    mystart = 1 - (data[data['targets']==i].index )/len(data)
    pad = .05
    ax.axvline(x=i, color='red', linestyle='--', linewidth=1, ymax=mystart-pad, ymin=mystart-mylen+pad)

barplot

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Mar 16 '23 at 16:56
0

Data and Imports

import matplotlib.pyplot as plt
import seaborn as sns

# create example data
data = {'x': ['A', 'B', 'C', 'D'],
        'y': [10, 5, 8, 12]}

Option 1

# create bar plot
fig, ax = plt.subplots(figsize=(9, 6))
sns.barplot(y='x', x='y', data=data, color='tab:blue', width=0.4, ax=ax)

# extract x, y and text for each xtick label
ytls = [(v.get_position()[0], v.get_position()[1], v.get_text()) for v in ax.get_yticklabels()]

# set the targets
targets = {'A': 6, 'B': 4, 'C': 1, 'D': 2}

# iterate through the bar containers
for c in ax.containers:
    
    # extract the value for the bottom edge of the bar patch
    y0s = [p.get_y() for p in c]
    
    # iterate through the values
    for y0, (x, y, t) in zip(y0s, ytls):

        diff = y - y0
        ymin = y - diff
        ymax = y + diff
        
        # remove the print lines, they're just for demonstrating
        print(f'Bottom Corner of Patch: {y0}')
        print(f'x: {x} y: {y} text: {t}')
        print(f'Patch diff from center: {diff} ymin: {ymin} ymax: {ymax}')
        print('\n')
        
        # add the lines
        ax.vlines(x=targets[t], color='red', linestyle='-', linewidth=3, ymin=ymin, ymax=ymax)

printed output

Bottom Corner of Patch: -0.2
x: 0 y: 0 text: A
Patch diff from center: 0.2 ymin: -0.2 ymax: 0.2


Bottom Corner of Patch: 0.8
x: 0 y: 1 text: B
Patch diff from center: 0.19999999999999996 ymin: 0.8 ymax: 1.2


Bottom Corner of Patch: 1.8
x: 0 y: 2 text: C
Patch diff from center: 0.19999999999999996 ymin: 1.8 ymax: 2.2


Bottom Corner of Patch: 2.8
x: 0 y: 3 text: D
Patch diff from center: 0.20000000000000018 ymin: 2.8 ymax: 3.2

Option 2

  • Since width of the bars is being set manually, you can simply divide by 2 to get the coordinates of the edges.
# create bar plot
width = 0.4
fig, ax = plt.subplots(figsize=(9, 6))
sns.barplot(y='x', x='y', data=data, color='tab:blue', width=width, ax=ax)

# set the targets
targets = {'A': 6, 'B': 4, 'C': 1, 'D': 2}

# extract y and text for each xtick label, calculate ymin and ymax and extract the target x
ytls = [(targets[v.get_text()], v.get_position()[1] - width/2, v.get_position()[1] + width/2) for v in ax.get_yticklabels()]

# iterate through the tuples in ytls
for x, ymin, ymax in ytls:
    
    # add the vertical lines
    ax.vlines(x=x, color='red', linestyle='-', linewidth=3, capstyle='butt', ymin=ymin, ymax=ymax)

Option 2 Consolidated

  • Note that .vlines accepts a list or array of values for x, ymin, and ymax.
# create bar plot
width = 0.4
fig, ax = plt.subplots(figsize=(9, 6))
sns.barplot(y='x', x='y', data=data, color='tab:blue', width=width, ax=ax)

# set the targets
targets = {'A': 6, 'B': 4, 'C': 1, 'D': 2}

# extract y and text for each xtick label, calculate ymin and ymax and extract the target x
x, ymins, ymaxs = list(zip(*[(targets[v.get_text()], v.get_position()[1] - width/2, v.get_position()[1] + width/2) for v in ax.get_yticklabels()]))

# add vertical lines
ax.vlines(x=x, color='red', linestyle='-', linewidth=3, capstyle='butt', ymin=ymins, ymax=ymaxs)

Plot Result

enter image description here

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158