Issue
I am trying to plot an arrow in Matplotlib as shown in the following picture.
The arrow's color changes gradually from black to blue from the tail to the head. I searched the existing answers, and I found a few relevant posts: Using Colormap with Annotate Arrow in Matplotlib, Matplotlib: How to get a colour-gradient as an arrow next to a plot?, Arrow with color gradient in matplotlib [duplicate]
However, the arrow plotted in the previous answer is straight. Here, I need an arrow with the arc. I think normally this is achieved by using connectionstyle
options with arc
or rad
. Unfortunately, it seems neither matplotlib.patches.FancyArrowPatch
nor matplotlib.pyplot.annotate
supports the color to be defined by colormap directly.
Could you please tell me how I can do this?
Solution
Plotting multiple overlapping arrows of different lengths and colors might work. You can adjust their length by changing the tail shrinking factors like this:
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from matplotlib.colors import LinearSegmentedColormap, ListedColormap
import numpy as np
def annotation_with_arc_arrow(ax, text, xy, xytext, color, headwidth, shrinkA, text_alpha=1):
"""Annotates a certain point on a plot with a simple arrow of a specified color, head width,
shrinkA factor, and text alpha value (to be able to generate transparent text)"""
text_props = {'alpha': text_alpha}
ax.annotate(text,
xy=xy,
xycoords='data',
xytext=xytext,
textcoords='data',
size=20,
arrowprops=dict(facecolor=color,
ec='none',
arrowstyle='simple, head_width={0}'.format(headwidth),
shrinkA=shrinkA,
connectionstyle="arc3,rad=0.3"
),
**text_props
)
def gradient_annotation(ax, text, xy, xytext, cmap):
"""Annotates a certain point on a plot using a bent arrow with gradient color from tail to head"""
# draw a headless arrow of the very first color from the map, with text
annotation_with_arc_arrow(ax, text, xy, xytext, cmap(0), 0, 0)
# draw many overlapping headless arrows of varying colors and shrinkA factors with transparent text
last_cmap_index = cmap.N-1
for i in range(1, last_cmap_index):
annotation_with_arc_arrow(ax, text, xy, xytext, cmap(i), 0, i, 0)
# finally, draw an arrow of the very last color and shrinkA factor having a head of size 0.5 and transparent text
annotation_with_arc_arrow(ax, text, xy, xytext, cmap(last_cmap_index), 0.5, last_cmap_index, 0)
ax = plt.subplot(111)
# generate a custom blue-black colormap
N = 256
vals = np.ones((N, 4))
vals[:, 0] = np.linspace(0, 0, N)
vals[:, 1] = np.linspace(0, 0, N)
vals[:, 2] = np.linspace(0, 1, N)
cmap = ListedColormap(vals)
gradient_annotation(ax, 'Test', (0.2, 0.2), (0.8, 0.8), cmap)
plt.show()
The text annotation and arrow's head should be drawn only once to have a relatively clean result:
Unfortunately, many other colormaps and arrow styles produce a bit dirty result with this solution. Perhaps you should try various ways of changing colors, setting shrinking factors, and choosing the "drawing direction" of the arrow (from tail to head or from head to tail).
UPDATE:
Alternatively, you could define arrow lengths by drawing a transparent head patch (patchB) of a gradually increasing radius each time you draw an arrow:
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap, ListedColormap
import numpy as np
import math
import matplotlib.patches as mpatches
def annotation_with_arc_arrow(ax, text, xy, xytext, color, headwidth, patchB_radius, text_alpha=1):
"""Annotates a certain point on a plot with a simple arrow of a specified color, head width,
patchB radius, and text alpha value (to be able to generate transparent text)"""
text_props = {'alpha': text_alpha}
# transparent circle, limiting the arrow
patchB = mpatches.Circle(xy, patchB_radius, alpha=0)
ax.add_artist(patchB)
ax.annotate(text,
xy=xy,
xycoords='data',
xytext=xytext,
textcoords='data',
size=20,
arrowprops=dict(facecolor=color,
ec='none',
arrowstyle='simple, head_width={0}'.format(headwidth),
patchB=patchB,
connectionstyle="arc3,rad=0.3"
),
**text_props
)
def gradient_annotation(ax, text, xy, xytext, cmap):
"""Annotates a certain point on a plot using a bent arrow with gradient color from tail to head"""
# get the absolute differences in coordinates of the annotated point and the text
dx = abs(xy[0] - xytext[0])
dy = abs(xy[1] - xytext[1])
# make those differences slightly smaller to compute a slightly smaller patch radius
dx = dx - dx/50
dy = dy - dy/50
# get a radius, which is slightly smaller than the distance between the annotated point and the text
r = math.sqrt(dx**2 + dy**2)
# draw an arrow of the very first color from the map, with a head and text
annotation_with_arc_arrow(ax, text, xy, xytext, cmap(0), 0.5, 0)
# draw many overlapping headless arrows of varying colors and patchB radii, with transparent text
# transparent patchB with a gradually increasing radius limits the arrow size
for i in range(1, cmap.N):
# compute a fraction of the maximum patchB radius
r_i = r * (i/cmap.N)
annotation_with_arc_arrow(ax, text, xy, xytext, cmap(i), 0, r_i, 0)
ax = plt.subplot(111)
# generate a custom blue-black colormap
N = 256
vals = np.ones((N, 4))
vals[:, 0] = np.linspace(0, 0, N)
vals[:, 1] = np.linspace(0, 0, N)
vals[:, 2] = np.linspace(1, 0, N)
cmap = ListedColormap(vals)
gradient_annotation(ax, 'Test', (0.2, 0.2), (0.8, 0.8), cmap)
plt.show()
This code produces a nearly identical picture:
But it allows you to safely rescale the picture without losing the original gradient color. The result might look a bit ugly if the arrow is very wide though. You might need to adjust the distribution of colors in the color map.
Answered By - Ratislaus
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.