Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I use matplotlib.units to switch between the units displayed on a graph?

Tags:

matplotlib

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?

like image 436
bdesham Avatar asked Jan 21 '14 23:01

bdesham


2 Answers

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()
like image 117
embert Avatar answered Sep 24 '22 18:09

embert


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.

like image 33
bdesham Avatar answered Sep 22 '22 18:09

bdesham