0

I'm trying to build a simple state-tracking function for an Axes instance in Matplotlib. Every time I create a new axes object (either directly or through other functions like subplots()), I want the instance to have a bound method, a.next_color(), that I can use to cycle through colors as I create new lines to add to the axes. I wrote something like this:

def set_color_sequence(colors = ['r', 'g', 'b', 'c', 'm', 'y']):
    i = [0]
    def cf(self):
        i[0] += 1
        return colors[(i[0]-1) % len(colors)]
    return cf

And thought I was being clever by adding it to the parent class:

plt.Axes.next_color = set_color_sequence()

The problem is that the state variable i seems to be shared by all Axes instances, instead of each new instance having its own state. What's an elegant way to ensure that all new instances have their own state tracking function? (BTW, I'd like to do this without modifying the original matplotlib code.)

rcs
  • 11
  • 3
  • 1
    You could subclass it? `class MyAxes(plt.Axes): def next_color(self): ...` – bheklilr Jun 27 '14 at 20:22
  • Perhaps [this](http://stackoverflow.com/questions/13831549/get-matplotlib-color-cycle-state) will help? Also, why not create a separate object / function instead of attaching a method that you need to call manually (assuming you don't modify the class further)? – AMacK Jun 27 '14 at 21:59

3 Answers3

0

Your existing function will work provided you assign the next_color attribute to an instance of Axes rather than the class itself.

First of all, with set_color_sequence you are basically implementing a generator in a roundabout way. To simplify things, we can achieve the same thing in one line using itertools.cycle:

from itertools import cycle
...
axes_instance.next_color = cycle(['r', 'g', 'b', 'c', 'm', 'y']).next

In fact, this is the way that matplotlib keeps track of where it is in the color cycle. For example, if you take a look at an instance of matplotlib.axes._subplots.AxesSubplot you will see that it has the attribute _get_lines.color_cycle, which is an itertools.cycle (try calling color_cycle.next()).

Now take a look at these two examples:

class MyClass1(object):

    # next_color is an attribute of the *class itself*
    next_color = cycle(['r', 'g', 'b', 'c', 'm', 'y']).next


class MyClass2(object):

    def __init__(self):

        # next_color is an attribute of *this instance* of the class
        self.next_color = cycle(['r', 'g', 'b', 'c', 'm', 'y']).next

In the first case, what happens is that the assignment

next_color = cycle(['r', 'g', 'b', 'c', 'm', 'y]).next

gets evaluated once and only once, when the class is first imported. This means that whenever you create a new instance of MyClass1, its next_color attribute will point at the exact same itertools.cycle instance, and therefore all instances of MyClass1 will share a common state:

a = MyClass1()
b = MyClass1()

print a.next_color is b.next_color
# True

print a.next_color(), a.next_color(), b.next_color()
#  r g b

However, the __init__ method gets called again and again whenever a new instance of the class is being created. As a consequence, every instance of MyClass2 gets its own itertools.cycle, and therefore its own state:

a = MyClass2()
b = MyClass2()

print a.next_color is b.next_color
# False

print a.next_color(), a.next_color(), b.next_color()
# r g r

If your goal is to subclass plt.Axes, you need to put your assignment somewhere where it will get called by each new instance of your subclass (probably in __init__). However, if you just want to add this method to an existing instance then literally all you need to do is:

axes_instance.next_color = get_color_sequence()
Community
  • 1
  • 1
ali_m
  • 62,795
  • 16
  • 193
  • 270
  • Thanks for elucidating the issues here. Accepting this answer because it provides two great strategies -- subclassing and assigning per instance. I went with the strategy in my answer because (a) it's more terse and (b) can be applied in a quick setup script and never thought about again. – rcs Jun 28 '14 at 08:38
0

AMacK's comment led me to a simple solution:

def next_color(self):
    return next(self._get_lines.color_cycle)
plt.Axes.next_color = next_color

This doesn't have the custom color-sequence behavior I outline above, but it does get the per-instance behavior I was looking for in code that is terse and intelligible. (Presumably I could directly override the color_cycle iterator if I wanted to generate a custom sequence.)

rcs
  • 11
  • 3
  • OK, fair enough, but why not just call `self._get_lines.color_cycle.next()` directly? Also, whilst you can override `color_cycle`, remember that you'll either have to do it on a per-instance basis (or else subclass `plt.Axes`) in order for each instance to have its own state. – ali_m Jun 28 '14 at 16:07
0

matplotlib already has something like this. Given ax1 an instance of AxesSubplot (or anything that derives Axes), you can access the line properties using _get_lines. This includes the prop_cycler, which can be overwritten using:

ax2._get_lines.prop_cycler = ax1._get_lines.prop_cycler

For example:

# Generate some data
x = linspace(-10, 10)
y_lin = 2*x
y_quad = x**2

# Create empty axes
_, ax1 = subplots()

# Plot linear
l1 = ax1.plot(y_lin)
ax1.set_ylabel('linear', color=l1[0].get_color())
ax1.tick_params('y', colors=l1[0].get_color())

# Generate secondary y-axis
ax2 = ax1.twinx()
# Make sure that plots on ax2 continue color cycle
ax2._get_lines.prop_cycler = ax1._get_lines.prop_cycler

# Plot quadratic
l2 = ax2.plot(y_quad)
ax2.set_ylabel('quadratic', color=l2[0].get_color())
ax2.tick_params('y', colors=l2[0].get_color())

Which outputs:

Matplotlib output after copying <code>prop_cycler</code> state

Leaving out ax2._get_lines.prop_cycler = ax1._get_lines.prop_cycler gets us:

Matplotlib output without copying `prop_cycler

Note: If you work within jupyter (or nteract) using the %pylab inline magic (so so you won't have direct access to the matplotlib.pyplot module). So something like plt.Axes is a little cumbersome.