One option to draw arrows inside bars is indeed hatching. This is however a little involved. One needs to create some custom hatch, as shown in this answer: How to fill a polygon with a custom hatch in matplotlib? Here we can be use a path of an arrow.
In the following we therefore subclass matplotlib.hatch.Shapes
and create some path of an arrow. The problem is now that we need some parameter to plug into the hatching, to be able to define the angle. We can then define a custom hatching pattern which I chose to look like this
hatch="arr{angle}{size}{density}"
where
- angle: integer number between 0 and 360
- size: some integer between 2 and 20
- density: some integer >= 1
This is similar to my previous answer on this question.
Depeding on the angle the path gets rotated and size and density determine basically how many arrows of what size are shown. Note that not all parameters look good, some lead to overlaps in the hatch.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.offsetbox
import matplotlib.hatch
from matplotlib.patches import Polygon
class ArrowHatch(matplotlib.hatch.Shapes):
"""
Arrow hatch. Use with hatch="arr{angle}{size}{density}"
angle: integer number between 0 and 360
size: some integer between 2 and 20
density: some integer >= 1
"""
filled = True
size = 1
def __init__(self, hatch, density):
v1 = [[.355,0], [.098, .1], [.151,.018], [-.355,.018]]
v2 = np.copy(v1)[::-1]
v2[:,1] *= -1
v = np.concatenate((v1,v2))
self.path = Polygon(v, closed=True, fill=False).get_path()
self.num_lines = 0
if len(hatch) >= 5:
if hatch[:3] == "arr":
h = hatch[3:].strip("{}").split("}{")
angle = np.deg2rad(float(h[0]))
self.size = float(h[1])/10.
d = int(h[2])
self.num_rows = 2*(int(density)//6*d)
self.num_vertices = (self.num_lines + 1) * 2
R = np.array([[np.cos(angle), -np.sin(angle)],
[np.sin(angle), np.cos(angle)]])
self.shape_vertices = np.dot(R,self.path.vertices.T).T
self.shape_codes = self.path.codes
matplotlib.hatch.Shapes.__init__(self, hatch, density)
matplotlib.hatch._hatch_types.append(ArrowHatch)
n = 7
a = 1017
x = np.arange(n)
y = np.linspace(0.2*a,a,len(x))
fig, ax = plt.subplots()
bar = ax.bar(x,y, ec="k", color="lightblue")
angles = [0,45,360-45,90,225,360-90,160]
for b, a in zip(bar, angles):
f = 'arr{{{}}}{{9}}{{3}}'.format(a)
b.set_hatch(f)
plt.show()
Output with {angle}{9}{3}
:

Output with {angle}{11}{2}
:

An explanation about the hatching patterns and how they are managed in the following. Or in other words, how does the
ArrowHatch
know that it should create a hatch?
The hatching will be applied using the vertices of any hatch inside of
matplotlib.hatch._hatch_types
. This is why we need to append our
ArrowHatch
class to this list. Depending on whether this class has the attribute
num_vertices
greater than zero, it will contribute to the final hatching. This is why we set it to
self.num_lines = 0
in the init function. However if the
hatch
, which is a string supplied to every class in the
_hatch_types
list contains our matching pattern, we set
self.num_rows
to something other than 0 (it should be an even number for Shapes, as they are produced in 2 shifted rows), such that it can contribute to the hatching.
This concept is a little exotic, as essentially each class itself decides for itself whether or not to take part in the hatching depending on the
hatch
string. On the one hand this is very convenient, because it allows to easily combine different hatch types, e.g.
"///++oo"
. On the other hand it makes it hard to use for hatches, which require an input parameter, as the angle in this case. And one also needs to take care not to use any character in the hatching, which is used by other hatches; for example I initially wanted to use something like
"arrow45.8,9,2"
, which does not work because
o
,
.
and
,
are other valid hatch types, such that the result would show some dots all over the place.