I’m using matplotlib to render graphs in a desktop application and I’m a little confused about matplotlib’s support of units. I want to let the user change the units shown on the x axis of a graph: all calculations will be done in nanometers, but the graph can be shown in either nanometers or gigahertz. (Note that there’s an inverse, not linear, relationship between these two units.)
There are two obvious ways to do this: “manually” or using matplotlib. In the former case matplotlib wouldn’t know anything about units; when the user changed the units I’d just recalculate all of my data points, change the axes’ bounds and labels, and tell matplotlib to redraw. But I’m hopeful that matplotlib has built-in functionality to do the same thing.
There is some light documentation about matplotlib.units
, but as far as I can tell this is intended for plotting arrays of user-specified objects instead of arrays of, say, floats. My data actually is an array of floats, and since there are hundreds of thousands of data points per graph I can’t wrap each value in some custom model class. (Even this simple example wraps each data point in a model class, and the entire example is so generic I can’t tell how I’d adapt it for my needs.) In any case, the original data points always have the same units, so the values only ever need to be converted to another unit.
How can I tell matplotlib about my units and then just use axes.set_xunit
or something to change between them?
Try to use a function instead of a float for conversion
The following example could serve as a start. Gigahertz GHz
is per nanosecond
and nm^-1
is per nano meter
, so this example would give nm^-1
I guess.
I took the mpl mock-up and made three adaptations:
Support a callable function for conversion
def value( self, unit ):
if callable(unit): # To handle a callable function for conversion
return unit(self._val)
...
Define function for conversion
one_by_x = lambda x: 1 / x if x != 0 else 0
....
Pass it
ax.plot( x, y, 'o', xunits=one_by_x )
....
ax.set_title("xunits = 1/x")
You define some functions and replot with the appropriate one. Or - for complication - even add another argument and have both: A constant scaling factor plus function.
The entire adapted example from http://matplotlib.org/examples/units/evans_test.html:
"""
A mockup "Foo" units class which supports
conversion and different tick formatting depending on the "unit".
Here the "unit" is just a scalar conversion factor, but this example shows mpl is
entirely agnostic to what kind of units client packages use
"""
from matplotlib.cbook import iterable
import matplotlib.units as units
import matplotlib.ticker as ticker
import matplotlib.pyplot as plt
class Foo:
def __init__( self, val, unit=1.0 ):
self.unit = unit
self._val = val * unit
def value( self, unit ):
if callable(unit):
return unit(self._val)
if unit is None: unit = self.unit
return self._val / unit
class FooConverter:
@staticmethod
def axisinfo(unit, axis):
'return the Foo AxisInfo'
if unit==1.0 or unit==2.0:
return units.AxisInfo(
majloc = ticker.IndexLocator( 8, 0 ),
majfmt = ticker.FormatStrFormatter("VAL: %s"),
label='foo',
)
else:
return None
@staticmethod
def convert(obj, unit, axis):
"""
convert obj using unit. If obj is a sequence, return the
converted sequence
"""
if units.ConversionInterface.is_numlike(obj):
return obj
if iterable(obj):
return [o.value(unit) for o in obj]
else:
return obj.value(unit)
@staticmethod
def default_units(x, axis):
'return the default unit for x or None'
if iterable(x):
for thisx in x:
return thisx.unit
else:
return x.unit
units.registry[Foo] = FooConverter()
# create some Foos
x = []
for val in range( 0, 50, 2 ):
x.append( Foo( val, 1.0 ) )
# and some arbitrary y data
y = [i for i in range( len(x) ) ]
one_by_x = lambda x: 1 / x if x != 0 else 0
# plot specifying units
fig = plt.figure()
fig.suptitle("Custom units")
fig.subplots_adjust(bottom=0.2)
ax = fig.add_subplot(1,2,2)
ax.plot( x, y, 'o', xunits=one_by_x )
for label in ax.get_xticklabels():
label.set_rotation(30)
label.set_ha('right')
ax.set_title("xunits = 1/x")
# plot without specifying units; will use the None branch for axisinfo
ax = fig.add_subplot(1,2,1)
ax.plot( x, y ) # uses default units
ax.set_title('default units')
for label in ax.get_xticklabels():
label.set_rotation(30)
label.set_ha('right')
plt.show()
I could not find a satisfactory way to do this using matplotlib—I didn’t want the overhead of wrapping each data value in an object. I decided just to do the unit conversion myself before passing the data to matplotlib.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With