Issue
I have a matplotlib plot where certain points get annotated. I have worked out how to do the annotations themselves, including arrows and everything. However, I need to add a line to each annotation, next to the text of the annotation. It should run in parallel to the text, with a certain offset from the text in points. The length of the line is based on a percentage value, that each annotated point has. Ideally I would like a line that's always the same length (roughly 15 text characters, which is the max length of the text in the annotations) but has a let's say red and grey portion, based on the percentage value mentioned. Any help or suggestions is greatly appreciated.
Edit: Here is a minimum example of some mock data points:
import numpy as np
import matplotlib.pyplot as plt
x=[2, 3, 4, 6, 7, 8, 10, 11]
y=[1, 3, 4, 2, 3, 1, 5, 2]
tx=[3, 4, 5, 6, 7, 8, 9, 10]
yd=dict(zip(x, y))
plt.scatter(x, y)
plt.xlim(0, 14)
plt.ylim(0, 8)
tspace=list(np.linspace(.05, .95, len(tx)))
tsd=dict(zip(tx, tspace))
arpr = {"arrowstyle": "-",
"connectionstyle": "arc,angleA=-90,armA=20,angleB=90,armB=20,rad=10"}
for i, j in zip(x, tx):
plt.annotate("foo bar baz", (i, yd[i]), (tsd[j], .75),
textcoords="axes fraction", arrowprops=arpr,
annotation_clip=False, rotation="vertical")
And here is a comparison of current vs. desired output:
Solution
I have since found a solution, albeit a hacky one, and without the ideal "grey boxes", but it's fine for my purposes and I'll share it here if it might help someone. If anyone knows an improvement, please feel free to contribute. Thanks to @DerekO for providing a useful input, which I incorporated into my solution.
This is adapted from this matplotlib demo. I simply shifted the custom box to outside of the text and modified width and height with an additional parameter for the percentage. I had to split it into two actual annotations, because the arrow would not start at the correct location using the custom box, but this way it works fine. The scaling/zooming now behaves well and follows the text.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.path import Path
from matplotlib.patches import BoxStyle
class MyStyle(BoxStyle._Base):
def __init__(self, pad, per=1.):
self.pad = pad
self.per = per
super().__init__()
def transmute(self, x0, y0, width, height, mutation_size):
# padding
pad = mutation_size * self.pad
# width and height with padding added.
width = width + 2.*pad
width *= self.per
height = 8.
# boundary of the padded box
x0, y0 = x0-pad, y0-pad,
x1, y1 = x0+width, y0-height
cp = [(x0, y0),
(x1, y0),
(x1, y1),
(x0, y1),
(x0, y0)]
com = [Path.MOVETO,
Path.LINETO,
Path.LINETO,
Path.LINETO,
Path.CLOSEPOLY]
path = Path(cp, com)
return path
# register the custom style
BoxStyle._style_list["percent"] = MyStyle
x=[2, 3, 4, 6, 7, 8, 10, 11]
y=[1, 3, 4, 2, 3, 1, 5, 2]
tx=[3, 4, 5, 6, 7, 8, 9, 10]
yd=dict(zip(x, y))
fig,ax = plt.subplots(1,1)
plt.scatter(x, y)
x_min, x_max = 0, 14
y_min, y_max = 0, 8
y_text_end = 0.75*(y_max-y_min)
plt.xlim(0, 14)
plt.ylim(0, 8)
tspace=list(np.linspace(.05, .95, len(tx)))
# tsd=dict(zip(tx, tspace))
# random percentage values to demonstrate the bar functionality
bar_percentages = [0.95, 0.9, 0.8, 0.6, 0.4, 0.2, 0.1, 0.05]
arpr = {"arrowstyle": "-",
"connectionstyle": "arc,angleA=-90,armA=20,angleB=90,armB=20,rad=10"}
## axes fraction is convenient but it's important to be able to access the exact coordinates for the Rectangle function
for i, x_val in enumerate(x):
plt.annotate("", (x_val, yd[x_val]), (tspace[i]*(x_max-x_min), y_text_end),
arrowprops=arpr, annotation_clip=False, rotation="vertical",)
plt.annotate("foo bar baz", (x_val, yd[x_val]), (tspace[i]*(x_max-x_min), y_text_end),
annotation_clip=False, rotation="vertical",
va="bottom", ha="right",
bbox=dict(boxstyle=f"percent,pad=.2,per={bar_percentages[i]}",
fc="red",
ec="none"))
del BoxStyle._style_list["percent"]
plt.show()
Answered By - VY_CMa
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.