40

let say I have this code:

num_rows = 10
num_cols = 1
fig, axs = plt.subplots(num_rows, num_cols, sharex=True)
for i in xrange(num_rows):
     ax = axs[i]
     ax.plot(np.arange(10), np.arange(10)**i)
plt.show()

the result figure has too much info and now I want to pick 1 of the axes and draw it alone in a new figure

I tried doing something like this

def on_click(event):
    axes = event.inaxes.get_axes()
    fig2 = plt.figure(15)
    fig2.axes.append(axes)
    fig2.show()

fig.canvas.mpl_connect('button_press_event', on_click)

but it didn't quite work. what would be the correct way to do it? searching through the docs and throw SE gave hardly any useful result

edit:

I don't mind redrawing the chosen axes, but I'm not sure how can I tell which of the axes was chosen so if that information is available somehow then it is a valid solution for me

edit #2:

so I've managed to do something like this:

def on_click(event):
    fig2 = plt.figure(15)
    fig2.clf()
    for line in event.inaxes.axes.get_lines():
         xydata = line.get_xydata()
         plt.plot(xydata[:, 0], xydata[:, 1])
    fig2.show()

which seems to be "working" (all the other information is lost - labels, lines colors, lines style, lines width, xlim, ylim, etc...) but I feel like there must be a nicer way to do it

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
user2717954
  • 1,822
  • 2
  • 17
  • 28
  • Almost identical to [python - matplotlib: can I create AxesSubplot objects, then add them to a Figure instance? - Stack Overflow](https://stackoverflow.com/questions/6309472/matplotlib-can-i-create-axessubplot-objects-then-add-them-to-a-figure-instance) ; nevertheless in this case the new axes only need to be displayed in "new figure" instead of inserting it into some existing figure so pickle alone suffices. – user202729 Jun 26 '22 at 02:37

2 Answers2

36

Copying the axes

The inital answer here does not work, we keep it for future reference and also to see why a more sophisticated approach is needed.

#There are some pitfalls on the way with the initial approach. 
#Adding an `axes` to a figure can be done via `fig.add_axes(axes)`. However, at this point, 
#the axes' figure needs to be the figure the axes should be added to. 
#This may sound a bit like running in circles but we can actually set the axes' 
#figure as `axes.figure = fig2` and hence break out of this.

#One might then also position the axes in the new figure to take the usual dimensions. 
#For this a dummy axes can be added first, the axes can change its position to the position 
#of the dummy axes and then the dummy axes is removed again. In total, this would look as follows.

import matplotlib.pyplot as plt
import numpy as np

num_rows = 10
num_cols = 1
fig, axs = plt.subplots(num_rows, num_cols, sharex=True)
for i in xrange(num_rows):
     ax = axs[i]
     ax.plot(np.arange(10), np.arange(10)**i)
     
     
def on_click(event):
    axes = event.inaxes
    if not axes: return   
    fig2 = plt.figure()
    axes.figure=fig2
    fig2.axes.append(axes)
    fig2.add_axes(axes)
    
    dummy = fig2.add_subplot(111)
    axes.set_position(dummy.get_position())
    dummy.remove()
    fig2.show()

fig.canvas.mpl_connect('button_press_event', on_click)


plt.show()

#So far so good, however, be aware that now after a click the axes is somehow 
#residing in both figures, which can cause all sorts of problems, e.g. if you
# want to resize or save the initial figure.

Instead, the following will work:

Pickling the figure

The problem is that axes cannot be copied (even deepcopy will fail). Hence to obtain a true copy of an axes, you may need to use pickle. The following will work. It pickles the complete figure and removes all but the one axes to show.

import matplotlib.pyplot as plt
import numpy as np
import pickle
import io

num_rows = 10
num_cols = 1
fig, axs = plt.subplots(num_rows, num_cols, sharex=True)
for i in range(num_rows):
     ax = axs[i]
     ax.plot(np.arange(10), np.arange(10)**i)

def on_click(event):

    if not event.inaxes: return
    inx = list(fig.axes).index(event.inaxes)
    buf = io.BytesIO()
    pickle.dump(fig, buf)
    buf.seek(0)
    fig2 = pickle.load(buf) 

    for i, ax in enumerate(fig2.axes):
        if i != inx:
            fig2.delaxes(ax)
        else:
            axes=ax

    axes.change_geometry(1,1,1)
    fig2.show()

fig.canvas.mpl_connect('button_press_event', on_click)

plt.show()

Recreate plots

The alternative to the above is of course to recreate the plot in a new figure each time the axes is clicked. To this end one may use a function that creates a plot on a specified axes and with a specified index as input. Using this function during figure creation as well as later for replicating the plot in another figure ensures to have the same plot in all cases.

import matplotlib.pyplot as plt
import numpy as np

num_rows = 10
num_cols = 1
colors = plt.rcParams["axes.prop_cycle"].by_key()["color"]
labels = ["Label {}".format(i+1) for i in range(num_rows)]

def myplot(i, ax):
    ax.plot(np.arange(10), np.arange(10)**i, color=colors[i])
    ax.set_ylabel(labels[i])


fig, axs = plt.subplots(num_rows, num_cols, sharex=True)
for i in xrange(num_rows):
     myplot(i, axs[i])


def on_click(event):
    axes = event.inaxes
    if not axes: return
    inx = list(fig.axes).index(axes)
    fig2 = plt.figure()
    ax = fig2.add_subplot(111)
    myplot(inx, ax)
    fig2.show()

fig.canvas.mpl_connect('button_press_event', on_click)

plt.show()
ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • thanks! this seems to be the right direction but like you said "axes is somehow residing in both figures". so I'm not really getting the expected results (in the code you posted, try clicking on some of the axes, after a while the new figure will be created always with the same graph no matter where you clicked) – user2717954 Aug 22 '17 at 08:13
  • to reproduce: 1. click on the top most axes (where y=x), a correct figure opens 2. click on the bottom most axes (where y=x^10), a correct figure opens 3. click on any other graph, the figure from 2. opens – user2717954 Aug 22 '17 at 08:57
  • your second solution works great for me. I'll accept it (although I do think a way to do it using the "first" approach is much cleaner and more reasonable to be possible to do) – user2717954 Aug 22 '17 at 09:00
  • 1
    I see the problem. I updated the answer with a way to use the initial axes. It needs to do a complete copy of the figure using `pickle`. – ImportanceOfBeingErnest Aug 22 '17 at 09:38
  • 1
    ha! amazing! and amazingly stupid (the hack that achieves it, not the answer itself) if you ask me. I also tried using deepcopy and saw it's not possible, never would have thought about trying to pickle. many thanks to you – user2717954 Aug 22 '17 at 09:41
  • sorry for pushing it but is there any chance that using figure manipulations (zoom, scroll, etc) will be enabled using your solution? currently I'm getting an AttributeError: 'CallbackRegistry' object has no attribute 'callbacks' – user2717954 Oct 31 '17 at 10:39
  • Are you talking about the solution "Pickling the figure"? When I run it as it is, I can zoom and pan as expected. If this is indeed a problem for you I would recommend asking a new question with all necessary details to reproduce the issue (versions of OS and libraries, the backend in use, a [mcve], the complete error tracebackand best a screenshot). – ImportanceOfBeingErnest Oct 31 '17 at 11:31
  • yes I was talking about the pickle solution. but you are right I'll ask a new question – user2717954 Oct 31 '17 at 11:59
  • You could just have used pickle.dumps and pickle.loads instead of using io.BytesIO – jmetz Feb 20 '21 at 15:09
  • 2
    Recommend replacing your 4 lines using BytesIO round-trip, with: `fig2 = pickle.loads(pickle.dumps(fig))` – jmetz Feb 20 '21 at 15:10
  • This great pickle trick really saved me. I made a single fig object with a plot, then later attached it to two separate backends (one for display in qt, and another for writing to disk) which was causing some issues. Attaching pickle copies to the different backends solved my issues. – RTbecard May 24 '21 at 14:51
1

If you have, for example, a plot with three lines generated by the function plot_something, you can do something like this:

fig, axs = plot_something()
ax = axs[2]
l = list(ax.get_lines())[0]
l2 = list(ax.get_lines())[1]
l3 = list(ax.get_lines())[2]
plot(l.get_data()[0], l.get_data()[1])
plot(l2.get_data()[0], l2.get_data()[1])
plot(l3.get_data()[0], l3.get_data()[1])
ylim(0,1)

enter image description here

Homero Esmeraldo
  • 1,864
  • 2
  • 18
  • 34
  • 4
    all the other information is lost - labels, lines colors, lines style, lines width, xlim, ylim, etc... – user2717954 Oct 31 '19 at 04:54
  • yes, you probably can do something similar for each one of those. I just wanted to point out that the data, which is probably the most important part, is available from the lines. I think that is pretty cool. – Homero Esmeraldo Oct 31 '19 at 08:18
  • you can see in my edit #2: part of the question that I tried something similar – user2717954 Oct 31 '19 at 08:31
  • 1
    you are right, I hadn't read that part because I was too focused on trying to find the answer. Sorry. I guess I can leave my answer here which has a figure and maybe calls the attention of people to this possibility, if that is ok. – Homero Esmeraldo Oct 31 '19 at 15:00