I'm looking to create a barh plot where the index appears as a label in the center of the row and the values appear on the inside edges of the chart, essentially replicating Excel's 'inside end' and 'inside base' label positions
The following is a simple example to illustrate the problem and desired result:
In [1]:
# Setting up a normalised data frame:
data = {"Names": ["Amy", "Bob", "Chris"],
"Scores1": [70, 81, 23],
"Scores2": [30, 100, 63]}
df = pd.DataFrame(data).set_index("Names")
df_norm = df.div(df.sum(axis = 1), axis = 0)
df_norm
Out [1]:
Scores1 Scores2
Names
Amy 0.70 0.30
Bob 0.45 0.55
Chris 0.27 0.73
In [2]:
# Plotting stacked barh and tidying up the asthetics
ax = df_Norm.plot.barh(stacked = True, width = 0.85, color = ['r', 'orange'])
plt.legend(
bbox_to_anchor = (0.5, -0.1),
loc = "lower center",
borderaxespad = 0,
frameon = False,
ncol = 2
)
plt.xlim(0.05, 1)
ax.axis('off')
# 'Drawing' the required labels note that the index labels need to appear twice and overlap
# with alpha = 0.5 in the event that one of the bars does not exist e.g Scores1 = 0
plt.bar_label(ax.containers[0], label_type = 'edge', labels = data["Scores1"])
plt.bar_label(Ax.containers[0], label_type = 'center', labels = data["Names"], alpha = 0.5)
plt.bar_label(ax.containers[1], label_type = 'center', labels = data["Names"], alpha = 0.5)
plt.bar_label(ax.containers[1], label_type = 'edge', labels = data["Scores2"])
Out [2]:
Getting the right side labels is a simple task as the padding required to do so is constant, I found that k = -20 works for up to 3 digit numbers.
So changing the last line above to include a constant padding results in the following
In [3]:
k = -20
plt.bar_label(ax.containers[1], label_type = 'edge', labels = data["Scores2"], padding = k)
Out [3]:
Where I'm struggling is with the left and central sets of labels. The desired result is shown below, any help is appreciated.
You can use ax.text
for this. It takes x
and y
as coordinates, and s
as a string (desired "label").
In order to determine the correct coordinates, let's have a look at ax.containers
. We have two (1 for Score1
, 1 for Score2
). Both have an attribute patches
that stores a list of Rectangles
with the information that we need. Here's a print:
for idx_cont, cont in enumerate(ax.containers):
for idx_p, p in enumerate(cont.patches):
print(f'cont {idx_cont} -> patch {idx_p}: {p}')
cont 0 -> patch 0: Rectangle(xy=(0, -0.425), width=0.7, height=0.85, angle=0)
cont 0 -> patch 1: Rectangle(xy=(0, 0.575), width=0.447514, height=0.85, angle=0)
cont 0 -> patch 2: Rectangle(xy=(0, 1.575), width=0.267442, height=0.85, angle=0)
cont 1 -> patch 0: Rectangle(xy=(0.7, -0.425), width=0.3, height=0.85, angle=0)
cont 1 -> patch 1: Rectangle(xy=(0.447514, 0.575), width=0.552486, height=0.85, angle=0)
cont 1 -> patch 2: Rectangle(xy=(0.267442, 1.575), width=0.732558, height=0.85, angle=0)
E.g. cont 0 -> patch 0
references the red bar in the left bottom corner (Score1: 70
) and cont 1 -> patch 0
the orange bar in the right bottom corner (Score2: 30
). Both start at y == -0.425
, and all your bars have height=0.85
.
Using this knowledge, we can write something as follows instead of your plt.bar_label(ax.containers ...)
lines:
plt.xlim(0.05, 1)
ax.axis('off')
for idx_cont, cont in enumerate(ax.containers):
for idx_p, p in enumerate(cont.patches):
x,y = p.xy
#In order to align the text,
# use `va` (vertical alignment) and `ha`
if idx_cont == 0:
ax.text(0.1, y+(0.85/2), data["Scores1"][idx_p], fontsize=10,
ha='left', va='center')
ax.text(0.525, y+(0.85/2), data["Names"][idx_p], fontsize=10,
ha='center', va='center', alpha = 0.5)
else:
ax.text(0.95, y+(0.85/2), data["Scores2"][idx_p], fontsize=10,
ha='right', va='center')
Result: