1

I am trying to create a barplot, where the bars contains additional information by letting arrows inside the bar point to cardinal point.

For now I only managed to create one with colored bars. Since 8 colors for the direction plus one for nonexisting are needed, it's not as good as I hoped. There is also the difficulty to distinguish colors.

import matplotlib.pyplot as plt
import numpy as np

x = range(12)
y_bar = np.random.random(12)

colors = ['green', 'yellow', 'blue', 'pink', 'orange']
bar_cols = [ colors[int(b*100) % len(colors)] for b in y_bar]

plt.bar(x, y_bar, color=bar_cols)
plt.show()

Therefore arrows, which point in a direction that is provided by a separate variable, are intuitive and easy to see.

I have no clue how to do that. I already tried using hatches, but there seem to be only a limited set of symbols.

Is there a way to get some arrows in the bars?

EDIT: Here is a pciture, how it can look like, what I am after. The shapes of the arrows can be different. Sure, more pillars are expected. There might even be some without any arrows due to mission data. barchar with arrows

Thomas Kühn
  • 9,412
  • 3
  • 47
  • 63
Blob
  • 75
  • 5
  • 2
    Can you explain a bit better what you have in mind? I think one can certainly achieve what you want with an overlay of two plots, but first we would have to know what exactly you are after -- can you maybe draw a picture? – Thomas Kühn Jan 19 '18 at 08:09
  • Any chance this question will still be edited to clarify what is desired and what problem there is in achieving that (see [ask])? Else we might also just close it as unclear?! – ImportanceOfBeingErnest Jan 20 '18 at 17:18
  • I added an example image, what I am aiming for. – Blob Jan 21 '18 at 04:20
  • So what is the data according to which the number of arrows and their direction would be defined? – ImportanceOfBeingErnest Jan 21 '18 at 15:59
  • Does it really have to be arrows or is something like [this](https://stackoverflow.com/a/14279510/2454357) enough? – Thomas Kühn Jan 21 '18 at 16:37
  • @ImportanceOfBeingErnest The direction of the arrows is defined by the wind direction (N, NO, O...). How many arrows inside a pillar just depends on the hight, which is defined by how strong the wind was. – Blob Jan 22 '18 at 00:35
  • @ThomasKühn Like I already wrote, I tried these hatches, but they are not really suitable for this. – Blob Jan 22 '18 at 00:39

2 Answers2

3

Here is a solution that uses ax.annotate to draw the arrows inside each bar. As the OP was not very clear about how the arrows should look like, I divided each bar into rectangles (I call them squares in the code, but they are only squares if you fix the aspect ratio of the plot) and drew one centred arrow per rectangle with orientation given by an angle that is supposed to be provided by the user (here in a vector called wind_direction).

Within the code I set the aspect ratio of the Axes to the aspect ratio of the x- and y-limits, which makes the Axes square-shaped and thus makes it easy to draw arrows of the same length, independent of their orientation. If that is not needed, the corresponding line can be commented out. If the arrows have to be of the same length without that limitation, the figure aspect ratio has to be calculated, see for instance here how to do that.

I also annotated each bar with the wind direction in degrees, just to make it easy to check that the arrows correspond to the given wind direction.

from matplotlib import pyplot as plt
import numpy as np

fig,ax = plt.subplots()

x = np.arange(12)
wind_strength = np.random.random(12)
wind_direction = np.linspace(0,2*np.pi,12, endpoint = False)

colors = ['green', 'yellow', 'blue', 'pink', 'orange']
bar_cols = [colors[i%len(colors)] for i,s in enumerate(wind_strength)]

bars = ax.bar(x,wind_strength, color=bar_cols)

##computing the aspect ratio of the plot ranges:
xlim = ax.get_xlim()
ylim = ax.get_ylim()
aspect = (xlim[1]-xlim[0])/(ylim[1]-ylim[0])


##comment out this line if you don't care about the arrows being the
##same length
ax.set_aspect(aspect)


##dividing each bar into 'squares' and plotting an arrow into each square
##with orientation given by wind direction
for bar,angle in zip(bars,wind_direction):
    (x1,y1),(x2,y2) = bar.get_bbox().get_points()
    w = x2-x1
    h = w/aspect
    x_mid = (x1+x2)/2

    dx = np.sin(angle)*w
    dy = np.cos(angle)*h

    ##the top and bottom of the current square:
    y_bottom = y1
    y_top = y_bottom+h

    ##draw at least one arrow (for very small bars)
    first = True

    while y_top < y2 or first:
        y_mid = (y_top+y_bottom)/2

        ax.annotate(
            '',xytext=(x_mid-dx/2,y_mid-dy/2),
            xy=(x_mid+dx/2,y_mid+dy/2),
            arrowprops=dict(arrowstyle="->"),
        )

        ##next square
        y_bottom = y_top
        y_top += h

        ##first arrow drawn:
        first = False

    ##annotating the wind direction:
    ax.text(x_mid, y2+0.05, '{}'.format(int(180*angle/np.pi)), ha = 'center')

plt.show()

The final result looks like this:

result of the above code

Hope this helps.

Thomas Kühn
  • 9,412
  • 3
  • 47
  • 63
  • I initially thought about a solution like that but it seemed very complicated to get the transforms right such that it would show the correct angle independent on the axes aspect. But I guess I was overthinking it a bit, since in the end you probably set up the plot as it should be and not scale it afterwards anyways. – ImportanceOfBeingErnest Jan 31 '18 at 23:35
  • Thanks. This solution fits my needs. The arrows are also nice. – Blob Feb 02 '18 at 03:34
2

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}:

enter image description here

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

enter image description here


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.
ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • This is really nice! Can you explain how the registering of the new hatch works? I only see a `matplotlib.hatch._hatch_types.append(ArrowHatch)` and in the `__init__` function `if hatch[:3] == "arr"`, but how does the hatch machinery know which hatch shape to use when you do `f = 'arr{{{}}}{{9}}{{3}}'.format(a)`? There seems to be no identifier. – Thomas Kühn Feb 01 '18 at 06:02
  • @ThomasKühn I updated the answer with some explanation. – ImportanceOfBeingErnest Feb 01 '18 at 10:03
  • Thank you. I'm always again amazed about what is possible with matplotlib. – Thomas Kühn Feb 01 '18 at 11:25