0

Is it possible to dynamically control the size of error bar caps? Like the caps of whiskers in the boxplots are controlled? So without setting the capsize to some fixed value. More should be seen from the following example.

fig = plt.figure()
gs = gridspec.GridSpec(2, 2)
ax0 = fig.add_subplot(gs[0])
ax1 = fig.add_subplot(gs[1])
ax2 = fig.add_subplot(gs[2])
ax3 = fig.add_subplot(gs[3])

n = 3
ax0.bar(list(range(n)),
       np.random.randint(1,20,n),
       yerr=np.random.randn(n),
       edgecolor="red",
       fill=False,
       capsize=5)

ax1.boxplot(np.random.normal(70, 25, (200,n)))

n = 12
ax2.bar(list(range(n)),
       np.random.randint(1,20,n),
       yerr=np.random.randn(n),
       edgecolor="red",
       fill=False,
       capsize= 5)

ax3.boxplot(np.random.normal(70, 25, (200,n)))

enter image description here


In more words:

Would it be possible to somehow control the capsize relative to the spacing between bins. If bins are on all integer numbers, I would like the width of a capsize to be bin_center +/- 0.25

TheoryX
  • 135
  • 10

2 Answers2

2

If I understand you correctly, you want the size of the error caps to adjust automatically to the width of your bars, as they do in a boxplot?

In that case, a simple way to get the desired behaviour is to pass a value to capsize= that is inversely proportional to the number of bars, since the number of bars will ultimately determine their width. You'll have to play around with the exact proportionality coefficient until you are happy with the results.

fig, axs = plt.subplots(1,2, figsize=(5,2.5))

for ax,n in zip(axs,[3,12]):
    ax.bar(list(range(n)),
       np.random.randint(1,20,n),
       yerr=np.random.randn(n),
       edgecolor="red",
       fill=False,
       capsize=30/n)  # <--- capsize inversly proportional to n

enter image description here

Diziet Asahi
  • 38,379
  • 7
  • 60
  • 75
  • If this is the only solution than I will accept the answer. But for bar plots this could be implemented as it is for whiskers in box plots. Thank you. – TheoryX Feb 09 '18 at 09:54
2

There is a huge difference between the caps of errorbars on bar plots and the whisker caps on box plots. The whisker caps are actual lines in the plot (i.e. they have a start and end point). The caps of errorbars are markers; the same marker as you'd get with marker="_" in a usual plot.

As a consequence the width of the whiskers is the difference between the two coordinates of the line, while the width of the errorbar caps is the markersize. The markersize is given in points, making it independent on the data units in the plot. This has several advantanges: They keep their size on screen, independent on the data scale, they stay the same when zooming the plot and they look nice on logarithmic scales.

So when asking to have the errorbar cap size in data units, you are essentially asking how to scale a marker in data units. As an example, one might look at this question or this one, which ask to scale a scatter in data units.

So the idea would be to calculate how many points a data unit is in the current plot and then set the markersize of the errorbar caps to a multiple of this number.

import matplotlib.pyplot as plt
import numpy as np; np.random.seed(1)

fig, (ax,ax2) = plt.subplots(ncols=2, figsize=(8,4))

n = 3
bar1 = ax.bar(list(range(n)),
       np.random.randint(1,20,n),
       yerr=np.random.randn(n),
       edgecolor="red", fill=False, capsize=1)

n = 12
bar2 = ax2.bar(list(range(n)),
       np.random.randint(1,20,n),
       yerr=np.random.randn(n),
       edgecolor="red", fill=False, capsize=1)

class BarCapSizer():
    def __init__(self, caps, size=1):
        self.size=size
        self.caps = caps
        self.ax = self.caps[0].axes
        self.resize()

    def resize(self):
        ppd=72./self.ax.figure.dpi
        trans = self.ax.transData.transform
        s =  ((trans((self.size,1))-trans((0,0)))*ppd)[0]
        for i,cap in enumerate(self.caps):
            cap.set_markersize(s)

size = 0.5 # data units
bcs1 =  BarCapSizer(bar1.errorbar.lines[1], size )        
bcs2 =  BarCapSizer(bar2.errorbar.lines[1], size )

plt.show()

enter image description here

This shows the capsize with 0.5 data units. The problem is now that this is again fixed. So if interactive zooming should preserve the markerscale in data units one would need to use an event handling mechanism to update the markersize. This can be done similar to what is shown here.

class BarCapSizer():
    def __init__(self, caps, size=1):
        self.size=size
        self.caps = caps
        self.ax = self.caps[0].axes
        self.resize()
        self.cid = ax.figure.canvas.mpl_connect('draw_event', self.update)

    def resize(self):
        ppd=72./self.ax.figure.dpi
        trans = self.ax.transData.transform
        s =  ((trans((self.size,1))-trans((0,0)))*ppd)[0]
        for i,cap in enumerate(self.caps):
            cap.set_markersize(s)

    def update(self,event=None):
        self.resize()
        self.timer = self.ax.figure.canvas.new_timer(interval=10)
        self.timer.single_shot = True
        self.timer.add_callback(lambda : self.ax.figure.canvas.draw_idle())
        self.timer.start()
ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712