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()
Related
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()
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.
I would like to use button_press_events to draw lines on a plot.
The following code does that, but the line coordinates follows on each other.
import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(111)
# Plot some random data
values = np.random.rand(4,1);
graph_1, = ax.plot(values, label='original curve')
graph_2, = ax.plot([], marker='.')
# Keep track of x/y coordinates
xcoords = []
ycoords = []
def onclick(event):
xcoords.append(event.xdata)
ycoords.append(event.ydata)
# Update plotted coordinates
graph_2.set_xdata(xcoords)
graph_2.set_ydata(ycoords)
# Refresh the plot
fig.canvas.draw()
cid = fig.canvas.mpl_connect('button_press_event', onclick)
plt.show()
How do I separate the events so every second click results in a separate line?
Something like this should do the trick.
Note that there are many things that should be improved in this code, such as detecting whether the click was detected inside or outside the axes, but it should put you on the right track.
fig = plt.figure()
ax = fig.add_subplot(111)
# Plot some random data
values = np.random.rand(4,1);
graph_1, = ax.plot(values, label='original curve')
# Keep track of x/y coordinates
lines = []
xcoords = []
ycoords = []
def onclick(event):
xcoords.append(event.xdata)
ycoords.append(event.ydata)
if len(xcoords)==2:
lines.append(ax.plot(xcoords,ycoords,'.-'))
xcoords[:] = []
ycoords[:] = []
# Refresh the plot
fig.canvas.draw()
cid = fig.canvas.mpl_connect('button_press_event', onclick)
plt.show()
Here's some code that does scatter plot of a number of different series using matplotlib and then adds the line y=x:
import numpy as np, matplotlib.pyplot as plt, matplotlib.cm as cm, pylab
nseries = 10
colors = cm.rainbow(np.linspace(0, 1, nseries))
all_x = []
all_y = []
for i in range(nseries):
x = np.random.random(12)+i/10.0
y = np.random.random(12)+i/5.0
plt.scatter(x, y, color=colors[i])
all_x.extend(x)
all_y.extend(y)
# Could I somehow do the next part (add identity_line) if I haven't been keeping track of all the x and y values I've seen?
identity_line = np.linspace(max(min(all_x), min(all_y)),
min(max(all_x), max(all_y)))
plt.plot(identity_line, identity_line, color="black", linestyle="dashed", linewidth=3.0)
plt.show()
In order to achieve this I've had to keep track of all the x and y values that went into the scatter plot so that I know where identity_line should start and end. Is there a way I can get y=x to show up even if I don't have a list of all the points that I plotted? I would think that something in matplotlib can give me a list of all the points after the fact, but I haven't been able to figure out how to get that list.
You don't need to know anything about your data per se. You can get away with what your matplotlib Axes object will tell you about the data.
See below:
import numpy as np
import matplotlib.pyplot as plt
# random data
N = 37
x = np.random.normal(loc=3.5, scale=1.25, size=N)
y = np.random.normal(loc=3.4, scale=1.5, size=N)
c = x**2 + y**2
# now sort it just to make it look like it's related
x.sort()
y.sort()
fig, ax = plt.subplots()
ax.scatter(x, y, s=25, c=c, cmap=plt.cm.coolwarm, zorder=10)
Here's the good part:
lims = [
np.min([ax.get_xlim(), ax.get_ylim()]), # min of both axes
np.max([ax.get_xlim(), ax.get_ylim()]), # max of both axes
]
# now plot both limits against eachother
ax.plot(lims, lims, 'k-', alpha=0.75, zorder=0)
ax.set_aspect('equal')
ax.set_xlim(lims)
ax.set_ylim(lims)
fig.savefig('/Users/paul/Desktop/so.png', dpi=300)
Et voilĂ
In one line:
ax.plot([0,1],[0,1], transform=ax.transAxes)
No need to modify the xlim or ylim.
Starting with matplotlib 3.3 this has been made very simple with the axline method which only needs a point and a slope. To plot x=y:
ax.axline((0, 0), slope=1)
You don't need to look at your data to use this because the point you specify (i.e. here (0,0)) doesn't actually need to be in your data or plotting range.
If you set scalex and scaley to False, it saves a bit of bookkeeping. This is what I have been using lately to overlay y=x:
xpoints = ypoints = plt.xlim()
plt.plot(xpoints, ypoints, linestyle='--', color='k', lw=3, scalex=False, scaley=False)
or if you've got an axis:
xpoints = ypoints = ax.get_xlim()
ax.plot(xpoints, ypoints, linestyle='--', color='k', lw=3, scalex=False, scaley=False)
Of course, this won't give you a square aspect ratio. If you care about that, go with Paul H's solution.
I have a scatter plot that is composed of different calls for scatter:
import matplotlib.pyplot as plt
import numpy as np
def onpick3(event):
index = event.ind
print '--------------'
print index
artist = event.artist
print artist
fig_handle = plt.figure()
x,y = np.random.rand(10),np.random.rand(10)
x1,y1 = np.random.rand(10),np.random.rand(10)
axes_size = 0.1,0.1,0.9,0.9
ax = fig_handle.add_axes(axes_size)
p = ax.scatter (x,y, marker='*', s=60, color='r', picker=True, lw=2)
p1 = ax.scatter (x1,y1, marker='*', s=60, color='b', picker=True, lw=2)
fig_handle.canvas.mpl_connect('pick_event', onpick3)
plt.show()
I'd like the points to be clickable, and get the x,y of the selected indexes.
However since scatter is being called more than once, I get the same indexes twice, so I cant use x[index] inside the onpick3 method
Is there a straightforward way to get the points?
It seems that event.artist gives back the same PathCollection that is given back from scatter (p and p1 in this case).
But I couldn't find any way to use it to extract the x,y of the selected indexes
Tried using event.artist.get_paths() - but it doesn't seem to be giving back all the scatter points, but only the one that I clicked on..so I'm really not sure what event.artist is giving back and what are the event.artist.get_paths() function is giving back
EDIT
it seems that event.artist._offsets gives an array with the relevant offsets, but for some reason when trying to use event.artist.offsetsI get
AttributeError: 'PathCollection' object has no attribute 'offsets'
(although if I understand the docs, it should be there)
To get the x, y coordinates for the collection that scatter returns, use event.artist.get_offsets() (Matplotlib has explicit getters and setters for mostly historical reasons. All get_offsets does is return self._offsets, but the public interface is through the "getter".).
So, to complete your example:
import matplotlib.pyplot as plt
import numpy as np
def onpick3(event):
index = event.ind
xy = event.artist.get_offsets()
print '--------------'
print xy[index]
fig, ax = plt.subplots()
x, y = np.random.random((2, 10))
x1, y1 = np.random.random((2, 10))
p = ax.scatter(x, y, marker='*', s=60, color='r', picker=True)
p1 = ax.scatter(x1, y1, marker='*', s=60, color='b', picker=True)
fig.canvas.mpl_connect('pick_event', onpick3)
plt.show()
However, if you're not varying things by a 3rd or 4th variable, you may not want to use scatter to plot points. Use plot instead. scatter returns a collection that's much more difficult to work with than the Line2D that plot returns. (If you do go the route of using plot, you'd use x, y = artist.get_data().)
Finally, not to plug my own project too much, but if you might find mpldatacursor useful. It abstracts away a lot of you're doing here.
If you decide to go that route, your code would look similar to: