I want to create a plot that shows genomic coding regions as arrows that may contain colorfully highlighted domain regions. In principle it is something like this:
import numpy as np
import matplotlib.patches as patches
import matplotlib.pyplot as plt
def test(bar_height=0.8, figsize=(10, 6), arrow_headlen=0.2, dpi=600):
X0 = np.arange(0, 10, 1)
X1 = X0 + 2
Y = np.arange(0, 10, 1)
fig = plt.figure(figsize=figsize)
ax = fig.add_subplot(111)
data_2_px = ax.transData.transform # data domain to figure
px_2_data = ax.transData.inverted().transform # figure to data domain
# get arrow head_length as fraction of arrow width
# so that it doesnt grow longer with longer x-axis
dy = bar_height * arrow_headlen
dpx = data_2_px([(0, dy)]) - data_2_px([(0, 0)])
arrowlen = (px_2_data([(dpx[0, 1], dpx[0, 0])]) - px_2_data([(0, 0)]))[0, 0]
ax.barh(y=Y, left=X0, width=X1 - X0, height=bar_height, color="0.5")
for y, x1 in zip(Y, X1):
yl = y - 0.49 * bar_height # low arrow corner (avoid being draw 1 px too low)
yh = y + 0.49 * bar_height # high arrow corner (avoid being draw 1 px too high)
arrow = patches.Polygon([(x1, yl), (x1, yh), (x1 + arrowlen, y)], color="0.5")
ax.add_patch(arrow)
# highlight parts of arrows
ax.barh(y=Y, left=X0 + 0.5, width=(X1 - X0) / 2, height=bar_height, color="blue")
fig.savefig("./test_from_savefig.png", dpi=dpi)
plt.show()
This draws 10 transcript "arrows" in gray and each of these transcripts contains a region highlighted in blue.
When plt.show()
opens this in a viewer and I save it from there I get this image (A):
The picture that is saved by fig.savefig()
with higher DPI however gives this image (B):
As you can see the arrow heads are suddenly not flush with the arrow base anymore. It seems that they were scaled differently than the bars. But both of them are defined in the data domain. So shouldn't they still be flush.
Image A is what I want to create:
However, I also want to be able to save this plot as raster graphic in a higher resolution.
FancyArrow
?FancyArrow
would be a more straight-forward way of defining arrows.
However, they are not drawn in a very reproducible way.
Sometimes a FancyArrow
is drawn 1 pixel higher or lower.
This means If I draw a gray FancyArrow
and then a blue rectangle over it, there will sometimes be some visible misalignment (e.g. a 1 pixel gray line visible behind the blue area).
I have found that only barh
is able to draw a bar of different colors that actually looks like it belongs together.
To have the arrow heads flush with the arrow bodies even when modifying the dpi
value, set linewidth=0
in the patches.Polygon
initialization.
Your code then gives this image (saved as PNG with 600 DPI)
Please note that for some reason, now a thin white line appears at the conjunction of some body-head pairs (see the second one starting from top, for example).
To go deeper into this latter issue, I have refactored the code using the rectangle corners to define the triangle vertex (which I paste below), but the result looks just the same.
import numpy as np
import matplotlib.patches as patches
import matplotlib.pyplot as plt
def test(arrow_height=0.8, arrow_bodylen=2.0, arrow_headlen=0.2,
figsize=(10, 6), dpi=600):
# Define points corresponding to arrow tails
X_tail = np.arange(0, 10, 1)
Y = np.arange(0, 10, 1)
# Create the figure
fig = plt.figure(figsize=figsize)
ax = fig.add_subplot(111)
# Draw arrow bodies (i.e. rectangles)
h_bars = ax.barh(y=Y,
left=X_tail,
width=arrow_bodylen,
height=arrow_height,
color="0.5",
linewidth = 0 # though it does not seem to have any effect
)
# Loop through rectangles to add heads (i.e. triangles) to arrow bodies
for bar in h_bars.patches:
# `get_corners()` return the rectangle corners, starting from `bar.xy`
# (here it is lower-left corner) in counter-clockwise order.
bar_LL, bar_LR, bar_TR, bar_TL = bar.get_corners()
assert bar_LR[0] == bar_TR[0] # probably could be removed...
# Define the point corresponding to the arrow tip
arrow_tip = ( bar_LR[0] + arrow_height * arrow_headlen,
(bar_LR[1] + bar_TR[1]) / 2
)
# Define a triangle between the right end of the bar and the arrow tip
arrow = patches.Polygon([bar_LR,
bar_TR,
arrow_tip
],
color="0.5",
linewidth = 0
)
ax.add_patch(arrow) # draw the triangle
# Highlight parts of the arrow bodies
ax.barh(y=Y,
left=X_tail + 0.25 * arrow_bodylen,
width=arrow_bodylen / 2,
height=arrow_height,
color="blue",
linewidth = 0
)
fig.savefig("./test_from_savefig.png", dpi=dpi)
plt.show()
test()
Hope this helps at least a bit!