3

I want to add a scale indicator to a plot like the one labelled '10kpc' in the (otherwise) empty plot below. So basically, the axis use one unit of measure and I want to indicate a length in the plot in a different unit. It has to have the same style as below, i.e. a |----| bar with text above.

Is there a convenient way in matplotlib to do that or do I have to draw three lines (two small vertical, one horizontal) and add the text? An ideal solution would not even require me to set coordinates in the data dimensions, i.e. I just say something along the line of horizontalalignment='left', verticalalignment='bottom', transform=ax.transAxes and specify only the width in data coordinates.

I fought with annotate() and arrow() and their documentations for quiet a bit until I concluded, they were not exactly useful, but I might be wrong.

Edit:

The code below is the closest, I have come so far. I still don't like having to specify the x-coordinates in the data coordinate system. The only thing I want to specify in data is the width of the bar. The rest should be placed in the plot system and ideally the bar should be placed relative to the text (a few pixels above).

import matplotlib.pyplot as plt 
import matplotlib.transforms as tfrms
plt.imshow(somedata)
plt.colorbar()
ax = plt.gca()
trans = tfrms.blended_transform_factory( ax.transData, ax.transAxes )
plt.errorbar( 5, 0.06, xerr=10*arcsecperkpc/2, color='k', capsize=5, transform=trans )
plt.text( 5, 0.05, '10kpc',  horizontalalignment='center', verticalalignment='top', transform=trans )

Empty plot with scale indicator

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
con-f-use
  • 3,772
  • 5
  • 39
  • 60
  • 1
    This might be helpful? https://pypi.python.org/pypi/matplotlib-scalebar – DavidG Apr 06 '17 at 15:12
  • It would be, but I have to match the style of the plot above. So it to look exactly like that – con-f-use Apr 06 '17 at 15:22
  • Have you considered using a horizontal ``errorbar`` and ``annotate``? – mommermi Apr 06 '17 at 19:04
  • @con-f-use I am so glad to have found your post after an hour of searching, it really helped. @mommermi Could you provide some example code with `errorbar` and `annotate`, it would help future readers! – NeStack Oct 02 '19 at 12:44

1 Answers1

6

Here is a code that adds a horizontal scale bar (or scale indicator or scalebar) to a plot. The bar's width is given in data units, while the height of the edges is in fraction of axes units.

The solution is based on an AnchoredOffsetbox, which contains a VPacker. The VPacker has a label in its lower row, and an AuxTransformBox in its upper row.
The key here is that the AnchoredOffsetbox is positioned relative to the axes, using the loc argument similar to the legend positioning (e.g. loc=4 denotes the lower right corner). However, the AuxTransformBox contains a set of elements, which are positioned inside the box using a transformation. As transformation we can choose a blended transform which transforms x coordinates according to the data transform of the axes and y coordinates according to the axes transform. A tranformation which does this is actually the xaxis_transform of the axes itself. Supplying this transform to the AuxTransformBox allows us to specify the artists within (which are Line2Ds in this case) in a useful way, e.g. the line of the bar will be Line2D([0,size],[0,0]).

All of this can be packed into a class, subclassing the AnchoredOffsetbox, such that it is easy to be used in an existing code.

import matplotlib.pyplot as plt
import matplotlib.offsetbox
from matplotlib.lines import Line2D
import numpy as np; np.random.seed(42)

x = np.linspace(-6,6, num=100)
y = np.linspace(-10,10, num=100)
X,Y = np.meshgrid(x,y)
Z = np.sin(X)/X+np.sin(Y)/Y

fig, ax = plt.subplots()
ax.contourf(X,Y,Z, alpha=.1)
ax.contour(X,Y,Z, alpha=.4)

class AnchoredHScaleBar(matplotlib.offsetbox.AnchoredOffsetbox):
    """ size: length of bar in data units
        extent : height of bar ends in axes units """
    def __init__(self, size=1, extent = 0.03, label="", loc=2, ax=None,
                 pad=0.4, borderpad=0.5, ppad = 0, sep=2, prop=None, 
                 frameon=True, linekw={}, **kwargs):
        if not ax:
            ax = plt.gca()
        trans = ax.get_xaxis_transform()
        size_bar = matplotlib.offsetbox.AuxTransformBox(trans)
        line = Line2D([0,size],[0,0], **linekw)
        vline1 = Line2D([0,0],[-extent/2.,extent/2.], **linekw)
        vline2 = Line2D([size,size],[-extent/2.,extent/2.], **linekw)
        size_bar.add_artist(line)
        size_bar.add_artist(vline1)
        size_bar.add_artist(vline2)
        txt = matplotlib.offsetbox.TextArea(label, minimumdescent=False)
        self.vpac = matplotlib.offsetbox.VPacker(children=[size_bar,txt],  
                                 align="center", pad=ppad, sep=sep) 
        matplotlib.offsetbox.AnchoredOffsetbox.__init__(self, loc, pad=pad, 
                 borderpad=borderpad, child=self.vpac, prop=prop, frameon=frameon,
                 **kwargs)

ob = AnchoredHScaleBar(size=3, label="3 units", loc=4, frameon=True,
                       pad=0.6,sep=4, linekw=dict(color="crimson"),) 
ax.add_artist(ob)
plt.show()

enter image description here

In order to achieve a result as desired in the question, you can set the frame off and adjust the linewidth. Of course the transformation from the units you want to show (kpc) into data units (km?) needs to be done by yourself.

ikpc = lambda x: x*3.085e16 #x in kpc, return in km
ob = AnchoredHScaleBar(size=ikpc(10), label="10kpc", loc=4, frameon=False,
                       pad=0.6,sep=4, linekw=dict(color="k", linewidth=0.8))

enter image description here

ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • Great answer, thanks. Any idea how to control the position of the scale bar? I would like to place it at some (x,y) coordinates. I tried to use `bbox_to_anchor`, but I got confused with the bbox coordinate system. – normanius Jan 22 '20 at 23:04
  • 1
    @normanius Right. I updated the answer code. So now it would be possible to pass something like `bbox_to_anchor=(0.1,.1), bbox_transform=ax.transAxes` as keyword arguments. – ImportanceOfBeingErnest Jan 22 '20 at 23:44
  • Ah, thanks for the update. I was missing the `bbox_transform` argument. :) – normanius Jan 23 '20 at 01:01
  • One more thing: in which units are the padding parameters measured? I can't figure it out. – normanius Jan 23 '20 at 01:47
  • 1
    Should be units of font size. I.e. if font size is 10 pt and pad is 0.5, then it should be 5pt padding. – ImportanceOfBeingErnest Jan 23 '20 at 12:06
  • Is there a way to modify the class to change the line color and the text color at the same time, using keyword arguments? i was able to change the text color using: `txt = matplotlib.offsetbox.TextArea(label, minimumdescent=False, textprops=dict(color="white")`, but would like to use keyword arguments to accomplish this. thanks in advance – T Walker May 09 '20 at 22:12
  • @TWalker Create a new `textkw` argument to pass to the text area. – ImportanceOfBeingErnest May 10 '20 at 17:20