Legend picking breaks when drawing `plot`s instead of `scatter`s - python

Good day. This question is a follow-up of Why does legend-picking only works for `ax.twinx()` and not `ax`?.
The minimal code provided below plots two curves respectively on ax1 and ax2 = ax1.twinx(), their legend boxes are created and the bottom legend is moved to the top ax so that picker events can be used. Clicking on a legend item will hide/show the associated curve.
If ax.scatter(...) is used that works fine. If ax.plot(...) is used instead, legend picking suddenly breaks. Why? Nothing else is changed so that's quite confusing. I have tested several other plotting methods and none of them work as expected.
Here is a video of it in action: https://imgur.com/qsPYHKc.mp4
import matplotlib.pyplot as plt
import numpy as np
fig, ax1 = plt.subplots()
ax2 = ax1.twinx()
X = np.linspace(0, 2*np.pi, 100)
Y1 = X**0.5 * np.sin(X)
Y2 = -np.cos(X)
# This is a quick way to change the plotting function, simply modify n.
n = 0
function, container = [("scatter", "collections"),
("plot", "lines"),
("bar", "patches"),
("barbs", "collections"),
("quiver", "collections")][n]
getattr(ax1, function)(X, Y1, color="green", label="$Y_1$")
getattr(ax2, function)(X, Y2, color="red", label="$Y_2$")
# Put both legends on ax2 so that pick events also work for ax1's legend.
legend1 = ax1.legend(loc="upper left")
legend2 = ax2.legend(loc="upper right")
legend1.remove()
ax2.add_artist(legend1)
for n, legend in enumerate((legend1, legend2)):
legend_item = legend.legendHandles[0]
legend_item.set_gid(n+1)
legend_item.set_picker(10)
# When a legend element is picked, hide/show the associated curve.
def on_graph_pick_event(event):
gid = event.artist.get_gid()
print(f"Picked Y{gid}'s legend.")
ax = {1: ax1, 2: ax2}[gid]
for artist in getattr(ax, container):
artist.set_visible(not artist.get_visible())
plt.draw()
fig.canvas.mpl_connect("pick_event", on_graph_pick_event)

Ok, so I know this is not the answer, but the comments don't allow me to do this kind of brainstorming. I tried a couple of things, and noticed the following. When you print the axes of the legendHandles artists in your for loop, it returns None for both legends in the case of the scatter plot / PathCollection artists. However, in the case of the 'normal' plot / Line2D artists, it returns axes objects! And even more than that; even though in the terminal their representations seem to be the same (AxesSubplot(0.125,0.11;0.775x0.77)), if you check if they are == ax2, for the legendHandles artist of legend1 it returns False, while for the one of legend2, it returns True. What is happening here?
So I tried to not only remove legend1 from ax1 and add it again to ax2 but to also do the same with the legendHandles object. But it doesn't allow me to do that:
NotImplementedError: cannot remove artist
To me it looks like you found a bug, or at least inconsistent behaviour. Here is the code of what I tried so far, in case anybody else would like to play around with it further.
import matplotlib.pyplot as plt
import matplotlib
matplotlib.use('Qt5Agg')
import numpy as np
fig, ax1 = plt.subplots()
ax2 = ax1.twinx()
X = np.linspace(0, 2*np.pi, 100)
Y1 = X**0.5 * np.sin(X)
Y2 = -np.cos(X)
USE_LINES = True # <--- set this to True or False to test both cases.
if USE_LINES:
ax1.plot(X, Y1, color="green", label="$Y_1$")
ax2.plot(X, Y2, color="red", label="$Y_2$")
else:
ax1.scatter(X, Y1, color="green", label="$Y_1$")
ax2.scatter(X, Y2, color="red", label="$Y_2$")
# Put both legends on ax2 so that pick events also work for ax1's legend.
legend1 = ax1.legend(loc="upper left")
legend2 = ax2.legend(loc="upper right")
legend1.remove()
ax2.add_artist(legend1)
# legend1.legendHandles[0].remove()
# ax2.add_artist(legend1.legendHandles[0])
for n, legend in enumerate((legend1, legend2)):
legend_item = legend.legendHandles[0]
legend_item.set_gid(n+1)
legend_item.set_picker(10)
print(
f'USE_LINES = {USE_LINES}', f'legend{n+1}',
legend_item.axes.__repr__() == legend.axes.__repr__(),
legend_item.axes == legend.axes,
legend_item.axes.__repr__() == ax2.__repr__(),
legend_item.axes == ax2, type(legend_item),
)
# When a legend element is picked, hide/show the associated curve.
def on_graph_pick_event(event):
gid = event.artist.get_gid()
print(f"Picked Y{gid}'s legend.")
ax = {1: ax1, 2: ax2}[gid]
artist = ax.lines[0] if USE_LINES else ax.collections[0]
artist.set_visible(not artist.get_visible())
plt.draw()
fig.canvas.mpl_connect("pick_event", on_graph_pick_event)
plt.show()

Related

How does python draw on the specified image?

I want to write the operations and parameters that I usually use in drawing in a function. In the future, just pass x and y to draw according to the default parameters. But now the question I am facing is, how do I determine which picture plt.plot is drawing on? For example, I want to draw two curves on a picture.
def draw(x,y):
... %some operations
plt.plot(x,y) % draw picture operations
... %some operations
draw(x,y),
dray(x2,y2)
How to ensure that these two curves are drawn on a picture. That is, what parameters do I need to pass to make plt.plot focus on the picture I specify.
def plotLine(coordinate,figName='test',xylabel=[],ax=None):
# assert(len(coordinate)<=2)
if (len(coordinate)==2) :
x=coordinate[0]
y=coordinate[1]
assert(len(x)==len(y))
else:
y=coordinate
x =np.linspace(0,len(y)-1,len(y))
minn=min(y)
maxx=max(y)
plt.switch_backend('Agg')
if ax == None:
fig,ax = plt.subplots()
fig = plt.figure(num=None, figsize=(3.5, 1.5), dpi=300, facecolor='w')
plt.subplots_adjust(right = 0.98, top = 0.98, bottom=0.35,left=0.32,wspace=0, hspace=0.2)
ax.set_xlim([0,len(x)])
ax.set_ylim([0,maxx+maxx/3])
plt.xticks(fontsize=5)
plt.yticks(fontsize=5)
bar_width = 0.35
opacity = 0.8
lsmarkersize = 2.5
lslinewidth = 0.6
ax.plot(x,y,'-', linewidth=1, markersize=lsmarkersize, markeredgewidth=0)
plt.savefig(figName+".png",bbox_inches='tight',dpi=500)
# os.system("code "+figName+".png")
if ax!=None:
return ax
else:
return plt.gca()
x=[1,2,3,4,5,6]
y=[1,2,3,4,4,5]
ax = plotLine([x,y])
x=[1,2,3,4,5,6]
y=[12,13,14,15,16,17]
plotLine([x,y],ax=ax)
I tried to pass ax as a parameter. But the picture drawn at the end is blank.
You can use subplots to specify the axes to plot on. For example, create a figure with a single subplot:
fig, ax = plt.subplots()
ax.plot(x, y)
For your function you could do the following
fig, ax = plt.subplots()
def draw(x, y, ax):
ax.plot(x, y)
def dray(x2, y2, ax):
ax.plot(x2, y2)
I am not attempting to modify your code. This is more a general approach answer. Imho, it is better (in terms of keeping track of what's going on) to define the figure and plots outside the function and doing only the actual plotting inside the function.
import numpy as np
from matplotlib import pyplot as plt
np.random.seed(123)
#the plotting function, taking ax and label as optional parameters
def draw_the_line(x, y, current_ax=None, current_label=None):
if not current_ax:
current_ax=plt.gca()
if not current_label:
current_label="missing label"
current_ax.plot(x, y, label=current_label)
plt.sca(current_ax)
fig, (ax1, ax2) = plt.subplots(2, figsize=(6, 8))
#normal plot into panel 1
x1 = np.arange(6)
y1 = np.random.randint(1, 10, len(x1))
draw_the_line(x1, y1, ax1, "data1")
#normal plot into panel 2
x2 = np.arange(5)
y2 = np.random.randint(10, 20, len(x2))
draw_the_line(x2, y2, ax2, "data2")
#plot into panel 1 with missing label
x3 = np.arange(4)
y3 = np.random.randint(20, 30, len(x3))
draw_the_line(x3, y3, ax1)
#plot into the last panel used
x4 = np.arange(3)
y4 = np.random.randint(30, 40, len(x4))
draw_the_line(x4, y4, current_label="data4")
ax1.legend()
ax2.legend()
plt.show()
Sample output:

Add figures in new subplot in Python [duplicate]

Looking at the matplotlib documentation, it seems the standard way to add an AxesSubplot to a Figure is to use Figure.add_subplot:
from matplotlib import pyplot
fig = pyplot.figure()
ax = fig.add_subplot(1,1,1)
ax.hist( some params .... )
I would like to be able to create AxesSubPlot-like objects independently of the figure, so I can use them in different figures. Something like
fig = pyplot.figure()
histoA = some_axes_subplot_maker.hist( some params ..... )
histoA = some_axes_subplot_maker.hist( some other params ..... )
# make one figure with both plots
fig.add_subaxes(histo1, 211)
fig.add_subaxes(histo1, 212)
fig2 = pyplot.figure()
# make a figure with the first plot only
fig2.add_subaxes(histo1, 111)
Is this possible in matplotlib and if so, how can I do this?
Update: I have not managed to decouple creation of Axes and Figures, but following examples in the answers below, can easily re-use previously created axes in new or olf Figure instances. This can be illustrated with a simple function:
def plot_axes(ax, fig=None, geometry=(1,1,1)):
if fig is None:
fig = plt.figure()
if ax.get_geometry() != geometry :
ax.change_geometry(*geometry)
ax = fig.axes.append(ax)
return fig
Typically, you just pass the axes instance to a function.
For example:
import matplotlib.pyplot as plt
import numpy as np
def main():
x = np.linspace(0, 6 * np.pi, 100)
fig1, (ax1, ax2) = plt.subplots(nrows=2)
plot(x, np.sin(x), ax1)
plot(x, np.random.random(100), ax2)
fig2 = plt.figure()
plot(x, np.cos(x))
plt.show()
def plot(x, y, ax=None):
if ax is None:
ax = plt.gca()
line, = ax.plot(x, y, 'go')
ax.set_ylabel('Yabba dabba do!')
return line
if __name__ == '__main__':
main()
To respond to your question, you could always do something like this:
def subplot(data, fig=None, index=111):
if fig is None:
fig = plt.figure()
ax = fig.add_subplot(index)
ax.plot(data)
Also, you can simply add an axes instance to another figure:
import matplotlib.pyplot as plt
fig1, ax = plt.subplots()
ax.plot(range(10))
fig2 = plt.figure()
fig2.axes.append(ax)
plt.show()
Resizing it to match other subplot "shapes" is also possible, but it's going to quickly become more trouble than it's worth. The approach of just passing around a figure or axes instance (or list of instances) is much simpler for complex cases, in my experience...
The following shows how to "move" an axes from one figure to another. This is the intended functionality of #JoeKington's last example, which in newer matplotlib versions is not working anymore, because axes cannot live in several figures at once.
You would first need to remove the axes from the first figure, then append it to the next figure and give it some position to live in.
import matplotlib.pyplot as plt
fig1, ax = plt.subplots()
ax.plot(range(10))
ax.remove()
fig2 = plt.figure()
ax.figure=fig2
fig2.axes.append(ax)
fig2.add_axes(ax)
dummy = fig2.add_subplot(111)
ax.set_position(dummy.get_position())
dummy.remove()
plt.close(fig1)
plt.show()
For line plots, you can deal with the Line2D objects themselves:
fig1 = pylab.figure()
ax1 = fig1.add_subplot(111)
lines = ax1.plot(scipy.randn(10))
fig2 = pylab.figure()
ax2 = fig2.add_subplot(111)
ax2.add_line(lines[0])
TL;DR based partly on Joe nice answer.
Opt.1: fig.add_subplot()
def fcn_return_plot():
return plt.plot(np.random.random((10,)))
n = 4
fig = plt.figure(figsize=(n*3,2))
#fig, ax = plt.subplots(1, n, sharey=True, figsize=(n*3,2)) # also works
for index in list(range(n)):
fig.add_subplot(1, n, index + 1)
fcn_return_plot()
plt.title(f"plot: {index}", fontsize=20)
Opt.2: pass ax[index] to a function that returns ax[index].plot()
def fcn_return_plot_input_ax(ax=None):
if ax is None:
ax = plt.gca()
return ax.plot(np.random.random((10,)))
n = 4
fig, ax = plt.subplots(1, n, sharey=True, figsize=(n*3,2))
for index in list(range(n)):
fcn_return_plot_input_ax(ax[index])
ax[index].set_title(f"plot: {index}", fontsize=20)
Outputs respect.
Note: Opt.1 plt.title() changed in opt.2 to ax[index].set_title(). Find more Matplotlib Gotchas in Van der Plas book.
To go deeper in the rabbit hole. Extending my previous answer, one could return a whole ax, and not ax.plot() only. E.g.
If dataframe had 100 tests of 20 types (here id):
dfA = pd.DataFrame(np.random.random((100,3)), columns = ['y1', 'y2', 'y3'])
dfB = pd.DataFrame(np.repeat(list(range(20)),5), columns = ['id'])
dfC = dfA.join(dfB)
And the plot function (this is the key of this whole answer):
def plot_feature_each_id(df, feature, id_range=[], ax=None, legend_bool=False):
feature = df[feature]
if not len(id_range): id_range=set(df['id'])
legend_arr = []
for k in id_range:
pass
mask = (df['id'] == k)
ax.plot(feature[mask])
legend_arr.append(f"id: {k}")
if legend_bool: ax.legend(legend_arr)
return ax
We can achieve:
feature_arr = dfC.drop('id',1).columns
id_range= np.random.randint(len(set(dfC.id)), size=(10,))
n = len(feature_arr)
fig, ax = plt.subplots(1, n, figsize=(n*6,4));
for i,k in enumerate(feature_arr):
plot_feature_each_id(dfC, k, np.sort(id_range), ax[i], legend_bool=(i+1==n))
ax[i].set_title(k, fontsize=20)
ax[i].set_xlabel("test nr. (id)", fontsize=20)

Updating a plot doesn't clear old plots, if event trigger came from another figure?

I am updating a line plot. There is an event trigger that starts this updating. If the trigger came from the figure that contains the plot, everything is fine. However, if the trigger came from another figure, then weird results happen: the line that's been updated appears to leave its trace uncleared.
Here is an example:
import matplotlib.pyplot as plt
import numpy as np
def onclick(event):
for ii in np.linspace(0., np.pi, 100):
y1 = y * np.sin(ii)
line1.set_ydata(y1)
ax.draw_artist(line1)
line2.set_ydata(-y1)
ax2.draw_artist(line2)
ax2.set_ylim(y1.min(), y1.max())
fig.canvas.update()
plt.pause(0.1)
x = np.linspace(0., 2*np.pi, 100)
y = np.sin(x)
fig = plt.figure()
ax = fig.add_subplot(1, 2, 1)
line1 = ax.plot(x, y)[0]
ax2 = fig.add_subplot(1, 2, 2)
line2 = ax2.plot(x, y)[0]
fig2 = plt.figure()
cid = fig2.canvas.mpl_connect('button_press_event', onclick)
plt.show()
What I see on screen:
Please note, if you resize the plot, or save it as figure, then all the residue image will be gone.
On the other hand, if change one line to:
cid = fig2.canvas.mpl_connect('button_press_event', onclick)
then it is correct. The animation works as intended.
Not sure what fig.canvas.update() would do. If you replace that line by
fig.canvas.draw_idle()
it should work as expected. In that case you would not need to draw the artists individually.

Z-order across axes when using matplotlib's twinx [duplicate]

In pyplot, you can change the order of different graphs using the zorder option or by changing the order of the plot() commands. However, when you add an alternative axis via ax2 = twinx(), the new axis will always overlay the old axis (as described in the documentation).
Is it possible to change the order of the axis to move the alternative (twinned) y-axis to background?
In the example below, I would like to display the blue line on top of the histogram:
import numpy as np
import matplotlib.pyplot as plt
import random
# Data
x = np.arange(-3.0, 3.01, 0.1)
y = np.power(x,2)
y2 = 1/np.sqrt(2*np.pi) * np.exp(-y/2)
data = [random.gauss(0.0, 1.0) for i in range(1000)]
# Plot figure
fig = plt.figure()
ax1 = fig.add_subplot(111)
ax2 = ax1.twinx()
ax2.hist(data, bins=40, normed=True, color='g',zorder=0)
ax2.plot(x, y2, color='r', linewidth=2, zorder=2)
ax1.plot(x, y, color='b', linewidth=2, zorder=5)
ax1.set_ylabel("Parabola")
ax2.set_ylabel("Normal distribution")
ax1.yaxis.label.set_color('b')
ax2.yaxis.label.set_color('r')
plt.show()
Edit: For some reason, I am unable to upload the image generated by this code. I will try again later.
You can set the zorder of an axes, ax.set_zorder(). One would then need to remove the background of that axes, such that the axes below is still visible.
ax2 = ax1.twinx()
ax1.set_zorder(10)
ax1.patch.set_visible(False)

Preventing execution until points are selected on matplotlib graph

I want to select 4 points on a matplotlib graph and operate on those points as soon as 4 four points are clicked on.
The code below will indeed store the 4 points in the variable points but does not wait for the four points to be selected. I tried adding a for loop and tried threading here but neither option worked. How can I solve this problem?
fig = plt.figure()
ax = fig.add_subplot(111)
image = np.load('path-to-file.npy')
tfig = ax.imshow(image)
points = []
def onclick(event):
global points
points.append((event.xdata, event.ydata))
cid = fig.canvas.mpl_connect('button_press_event', onclick)
# this line will cause an error because the four points haven't been
# selected yet
firstPoint = points[0]
In this case, you might consider using plt.ginput instead of "rolling your own".
As a quick example:
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.set(title='Select 4 points')
xy = plt.ginput(4)
x, y = zip(*xy)
ax.fill(x, y, color='lightblue')
ax.plot(x, y, ls='', mfc='red', marker='o')
plt.show()

Categories