Point picker event_handler drawing line and displaying coordinates in matplotlib - python

I have the following class that draws a vertical line through the y-axis, so that when I click on it a horizontal line gets drawn at the location.
My goal is to get the y-coordinate to actually print right at the y-axis where the horizontal line gets drawn. For testing, I am trying to print a title with the y-ccordinate, but it is not working as expected.
What I am trying to really accomplish is to make the y-axis on a bar plot clickable so that the user can select a point on the y-axis, and then a bunch of "stuff" happens (including the drawing of a horizontal line). I really see no other way to make this happen, other than drawing a plottable vertical line through the y-axis to make it pickable. This seems rather kludgey, but I have not had any success with any other methods.
import matplotlib.pyplot as plt
class PointPicker(object):
def __init__(self):
self.fig = plt.figure()
self.ax = self.fig.add_subplot(111)
self.lines2d, = self.ax.plot((0, 0), (-6000, 10000), 'k-', linestyle='-',picker=5)
self.fig.canvas.mpl_connect('pick_event', self.onpick)
self.fig.canvas.mpl_connect('key_press_event', self.onpress)
fig.canvas.mpl_connect('button_press_event', self.onclick)
def onpress(self, event):
"""define some key press events"""
if event.key.lower() == 'q':
sys.exit()
def onpick(self,event):
x = event.mouseevent.xdata
y = event.mouseevent.ydata
L = self.ax.axhline(y=y)
print(y)
ax.axvspan(0, 0, facecolor='y', alpha=0.5, picker=10)
self.fig.canvas.draw()
def onclick(event):
self.fig.canvas.set_title('Selected item came from {}'.format(event.ydata))
print(event.xdata, event.ydata)
if __name__ == '__main__':
plt.ion()
p = PointPicker()
plt.show()
Assuming there is no ther way to achieve my end result, all is well with this method, except I cannot for the life of me get the title to print (using the self.fig.canvas.set_title('Selected item came from {}'.format(event.ydata)).

You can use the 'button_press_event' to connect to a method, which verifies that the click has happened close enough to the yaxis spine and then draws a horizontal line using the clicked coordinate.
import matplotlib.pyplot as plt
class PointPicker(object):
def __init__(self, ax, clicklim=0.05):
self.fig=ax.figure
self.ax = ax
self.clicklim = clicklim
self.horizontal_line = ax.axhline(y=.5, color='y', alpha=0.5)
self.text = ax.text(0,0.5, "")
print self.horizontal_line
self.fig.canvas.mpl_connect('button_press_event', self.onclick)
def onclick(self, event):
if event.inaxes == self.ax:
x = event.xdata
y = event.ydata
xlim0, xlim1 = ax.get_xlim()
if x <= xlim0+(xlim1-xlim0)*self.clicklim:
self.horizontal_line.set_ydata(y)
self.text.set_text(str(y))
self.text.set_position((xlim0, y))
self.fig.canvas.draw()
if __name__ == '__main__':
fig = plt.figure()
ax = fig.add_subplot(111)
ax.bar([0,2,3,5],[4,5,1,3], color="#dddddd")
p = PointPicker(ax)
plt.show()

Related

Show/hide a plot in a multiplot display with a few clicks [duplicate]

I'm using pyplot to display a line graph of up to 30 lines. I would like to add a way to quickly show and hide individual lines on the graph. Pyplot does have a menu where you can edit line properties to change the color or style, but its rather clunky when you want to hide lines to isolate the one you're interested in. Ideally, I'd like to use checkboxes on the legend to show and hide lines. (Similar to showing and hiding layers in image editors like Paint.Net) I'm not sure if this is possible with pyplot, so I am open to other modules as long as they're somewhat easy to distribute.
If you'd like, you can hook up a callback to the legend that will show/hide lines when they're clicked. There's a simple example here: http://matplotlib.org/examples/event_handling/legend_picking.html
Here's a "fancier" example that should work without needing to manually specify the relationship of the lines and legend markers (Also has a few more features).
(Updated version in August 2019, as a response to repeated reports about this not working correctly; now it should! For the old version see version history)
import numpy as np
import matplotlib.pyplot as plt
def main():
x = np.arange(10)
fig, ax = plt.subplots()
for i in range(1, 31):
ax.plot(x, i * x, label=r'$y={}x$'.format(i))
ax.legend(loc='upper left', bbox_to_anchor=(1.05, 1),
ncol=2, borderaxespad=0)
fig.subplots_adjust(right=0.55)
fig.suptitle('Right-click to hide all\nMiddle-click to show all',
va='top', size='large')
leg = interactive_legend()
return fig, ax, leg
def interactive_legend(ax=None):
if ax is None:
ax = plt.gca()
if ax.legend_ is None:
ax.legend()
return InteractiveLegend(ax.get_legend())
class InteractiveLegend(object):
def __init__(self, legend):
self.legend = legend
self.fig = legend.axes.figure
self.lookup_artist, self.lookup_handle = self._build_lookups(legend)
self._setup_connections()
self.update()
def _setup_connections(self):
for artist in self.legend.texts + self.legend.legendHandles:
artist.set_picker(10) # 10 points tolerance
self.fig.canvas.mpl_connect('pick_event', self.on_pick)
self.fig.canvas.mpl_connect('button_press_event', self.on_click)
def _build_lookups(self, legend):
labels = [t.get_text() for t in legend.texts]
handles = legend.legendHandles
label2handle = dict(zip(labels, handles))
handle2text = dict(zip(handles, legend.texts))
lookup_artist = {}
lookup_handle = {}
for artist in legend.axes.get_children():
if artist.get_label() in labels:
handle = label2handle[artist.get_label()]
lookup_handle[artist] = handle
lookup_artist[handle] = artist
lookup_artist[handle2text[handle]] = artist
lookup_handle.update(zip(handles, handles))
lookup_handle.update(zip(legend.texts, handles))
return lookup_artist, lookup_handle
def on_pick(self, event):
handle = event.artist
if handle in self.lookup_artist:
artist = self.lookup_artist[handle]
artist.set_visible(not artist.get_visible())
self.update()
def on_click(self, event):
if event.button == 3:
visible = False
elif event.button == 2:
visible = True
else:
return
for artist in self.lookup_artist.values():
artist.set_visible(visible)
self.update()
def update(self):
for artist in self.lookup_artist.values():
handle = self.lookup_handle[artist]
if artist.get_visible():
handle.set_visible(True)
else:
handle.set_visible(False)
self.fig.canvas.draw()
def show(self):
plt.show()
if __name__ == '__main__':
fig, ax, leg = main()
plt.show()
This allows you to click on legend items to toggle their corresponding artists on/off. For example, you can go from this:
To this:
Thanks for the post! I extended the class above such that it can handle multiple legends - such as for example if you are using subplots. (I am sharing it here since i could not find any other example somewhere else... and it might be handy for someone else...)
class InteractiveLegend(object):
def __init__(self):
self.legends = []
self.figures = []
self.lookup_artists = []
self.lookup_handles = []
self.host = socket.gethostname()
def add_legends(self, legend):
self.legends.append(legend)
def init_legends(self):
for legend in self.legends:
self.figures.append(legend.axes.figure)
lookup_artist, lookup_handle = self._build_lookups(legend)
#print("init", type(lookup))
self.lookup_artists.append(lookup_artist)
self.lookup_handles.append(lookup_handle)
self._setup_connections()
self.update()
def _setup_connections(self):
for legend in self.legends:
for artist in legend.texts + legend.legendHandles:
artist.set_picker(10) # 10 points tolerance
for figs in self.figures:
figs.canvas.mpl_connect('pick_event', self.on_pick)
figs.canvas.mpl_connect('button_press_event', self.on_click)
def _build_lookups(self, legend):
labels = [t.get_text() for t in legend.texts]
handles = legend.legendHandles
label2handle = dict(zip(labels, handles))
handle2text = dict(zip(handles, legend.texts))
lookup_artist = {}
lookup_handle = {}
for artist in legend.axes.get_children():
if artist.get_label() in labels:
handle = label2handle[artist.get_label()]
lookup_handle[artist] = handle
lookup_artist[handle] = artist
lookup_artist[handle2text[handle]] = artist
lookup_handle.update(zip(handles, handles))
lookup_handle.update(zip(legend.texts, handles))
#print("build", type(lookup_handle))
return lookup_artist, lookup_handle
def on_pick(self, event):
#print event.artist
handle = event.artist
for lookup_artist in self.lookup_artists:
if handle in lookup_artist:
artist = lookup_artist[handle]
artist.set_visible(not artist.get_visible())
self.update()
def on_click(self, event):
if event.button == 3:
visible = False
elif event.button == 2:
visible = True
else:
return
for lookup_artist in self.lookup_artists:
for artist in lookup_artist.values():
artist.set_visible(visible)
self.update()
def update(self):
for idx, lookup_artist in enumerate(self.lookup_artists):
for artist in lookup_artist.values():
handle = self.lookup_handles[idx][artist]
if artist.get_visible():
handle.set_visible(True)
else:
handle.set_visible(False)
self.figures[idx].canvas.draw()
def show(self):
plt.show()
use it as follow:
leg1 = ax1.legend(loc='upper left', bbox_to_anchor=(1.05, 1), ncol=2, borderaxespad=0)
leg2 = ax2.legend(loc='upper left', bbox_to_anchor=(1.05, 1), ncol=2, borderaxespad=0)
fig.subplots_adjust(right=0.7)
interactive_legend = InteractiveLegend()
interactive_legend.add_legends(leg1)
interactive_legend.add_legends(leg2)
interactive_legend.init_legends()
interactive_legend.show()
Inspired by #JoeKington's answer, here is what I use (a slightly modified version, that doesn't require ax, fig but can work directly with plt.plot(...); also plt.legend() is kept outside out the scope of the main object):
Ready-to-use example pltinteractivelegend.py:
import numpy as np
import matplotlib.pyplot as plt
class InteractiveLegend(object):
def __init__(self, legend=None):
if legend == None:
legend = plt.gca().get_legend()
self.legend = legend
self.fig = legend.axes.figure
self.lookup_artist, self.lookup_handle = self._build_lookups(legend)
self._setup_connections()
self.update()
def _setup_connections(self):
for artist in self.legend.texts + self.legend.legendHandles:
artist.set_picker(10) # 10 points tolerance
self.fig.canvas.mpl_connect('pick_event', self.on_pick)
self.fig.canvas.mpl_connect('button_press_event', self.on_click)
def _build_lookups(self, legend):
labels = [t.get_text() for t in legend.texts]
handles = legend.legendHandles
label2handle = dict(zip(labels, handles))
handle2text = dict(zip(handles, legend.texts))
lookup_artist = {}
lookup_handle = {}
for artist in legend.axes.get_children():
if artist.get_label() in labels:
handle = label2handle[artist.get_label()]
lookup_handle[artist] = handle
lookup_artist[handle] = artist
lookup_artist[handle2text[handle]] = artist
lookup_handle.update(zip(handles, handles))
lookup_handle.update(zip(legend.texts, handles))
return lookup_artist, lookup_handle
def on_pick(self, event):
handle = event.artist
if handle in self.lookup_artist:
artist = self.lookup_artist[handle]
artist.set_visible(not artist.get_visible())
self.update()
def on_click(self, event):
if event.button == 3:
visible = False
elif event.button == 2:
visible = True
else:
return
for artist in self.lookup_artist.values():
artist.set_visible(visible)
self.update()
def update(self):
for artist in self.lookup_artist.values():
handle = self.lookup_handle[artist]
if artist.get_visible():
handle.set_visible(True)
else:
handle.set_visible(False)
self.fig.canvas.draw()
if __name__ == '__main__':
for i in range(20):
plt.plot(np.random.randn(1000), label=i)
plt.legend()
leg = InteractiveLegend()
plt.show()
Usage as a library:
import numpy as np
import matplotlib.pyplot as plt
import pltinteractivelegend
for i in range(20):
plt.plot(np.random.randn(1000), label=i)
plt.legend()
leg = pltinteractivelegend.InteractiveLegend() # mandatory: keep the object with leg = ...; else it won't work
plt.show()

python - how can I display cursor on all axes vertically but only on horizontally on axes with mouse pointer on top

I would like the cursor to be visible across all axes vertically but only visible horizontally for the axis that the mouse pointer is on.
This is a code exert of what I am using at the moment.
import matplotlib.pyplot as plt
from matplotlib.widgets import MultiCursor
fig = plt.figure(facecolor='#07000d')
ax1 = plt.subplot2grid((2,4), (0,0), rowspan=1,colspan=4, axisbg='#aaaaaa')
ax2 = plt.subplot2grid((2,4), (1,0), rowspan=1,colspan=4, axisbg='#aaaaaa')
multi = MultiCursor(fig.canvas, (ax1, ax2), color='r', lw=.5, horizOn=True, vertOn=True)
plt.show()
This is what I have. What I would like is to have the horizontal cursor to be only visible for the axis that mouse pointer is on but have the vertical visible for both.
So I came up with a solution. I made my own custom cursor using mouse events wrapped in a class. Works great. You can add your axes in an array/list.
import numpy as np
import matplotlib.pyplot as plt
class CustomCursor(object):
def __init__(self, axes, col, xlimits, ylimits, **kwargs):
self.items = np.zeros(shape=(len(axes),3), dtype=np.object)
self.col = col
self.focus = 0
i = 0
for ax in axes:
axis = ax
axis.set_gid(i)
lx = ax.axvline(ymin=ylimits[0],ymax=ylimits[1],color=col)
ly = ax.axhline(xmin=xlimits[0],xmax=xlimits[1],color=col)
item = list([axis,lx,ly])
self.items[i] = item
i += 1
def show_xy(self, event):
if event.inaxes:
self.focus = event.inaxes.get_gid()
for ax in self.items[:,0]:
self.gid = ax.get_gid()
for lx in self.items[:,1]:
lx.set_xdata(event.xdata)
if event.inaxes.get_gid() == self.gid:
self.items[self.gid,2].set_ydata(event.ydata)
self.items[self.gid,2].set_visible(True)
plt.draw()
def hide_y(self, event):
for ax in self.items[:,0]:
if self.focus == ax.get_gid():
self.items[self.focus,2].set_visible(False)
if __name__ == '__main__':
fig = plt.figure(facecolor='#07000d')
ax1 = plt.subplot2grid((2,4), (0,0), rowspan=1,colspan=4, axisbg='#aaaaaa')
ax2 = plt.subplot2grid((2,4), (1,0), rowspan=1,colspan=4, axisbg='#aaaaaa')
cc = CustomCursor([ax1,ax2], col='red', xlimits=[0,100], ylimits=[0,100], markersize=30,)
fig.canvas.mpl_connect('motion_notify_event', cc.show_xy)
fig.canvas.mpl_connect('axes_leave_event', cc.hide_y)
plt.tight_layout()
plt.show()

Python 2.7: How to make data points appear on an empty canvas plot window

I want to plot xcoord vs. ycoord on a canvas. What I get right now is only an empty plot window (1 high x 1 wide) .
How can I make the datapoints appear in the plot window?
I have checked the xcoord and ycoord arrays. They exist and they are correct.
class SurveyRoute(SurveyPlotWidget):
"""docstring for SurveyRoute"""
def __init__(self, survey_name, parent=None, model=None):
self.survey_name = survey_name
SurveyPlotWidget.__init__(self, parent, model)
def read_coordinate_file(self, survey_name):
self.coords = station_coordinates.get_coordinates_all(survey_name)
df = pd.DataFrame(self.coords,index=['UTM X','UTM Y','depth'])
df = DataFrame.transpose(df)
self.xcoord = df['UTM X'].values.tolist()
self.ycoord = df['UTM Y'].values.tolist()
print self.xcoord
def on_hover(self, event):
self.fig_text.set_text('')
contains, attrd = self.points.contains(event)
if contains == True:
ind = attrd['ind'][0]
self.fig_text.set_text('bm {}'.format(self.model.benchmarks[ind]))
self.canvas.draw()
def plot_coords(self):
fig = plt.figure()
plt.plot(self.xcoord, self.ycoord, marker='o', ms=10, linestyle='', alpha=1.0, color='r', picker = True)[0]
plt.xlabel('UTM x-coordinate')
plt.ylabel('UTM y-coordinate')
fig.canvas.mpl_connect('pick_event', self.on_hover)
fig.canvas.draw()
running the class
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
obj = SurveyRoute('mikkel')
obj.read_coordinate_file('mikkel')
obj.plot_coords()
obj.show()
app.exec_()
fig = plt.figure()
should be removed and
fig.canvas.mpl_connect('pick_event', self.on_hover)
fig.canvas.draw()
should be substituted by
self.canvas.mpl_connect('pick_event', self.on_hover)
self.canvas.draw()

Connect a matplotlib widget using keyboard event

Hi I am making a small program to select data from a plot. I would like to use the inbuilt matplotlib.widgets but I am misunderstanding the basics of connecting/disconnecting to the figure canvas.
Below is an example of the code I had in mind. If I declare the cursor/span widgets outside the event manager class the widgets are launched properly. However, this is not the case within the Event_Manager class.
But how should I modify the code to launch these widgets using a key event? Also how could I disconect/reconect them, also using key events?
import matplotlib.widgets as widgets
import numpy as np
import matplotlib.pyplot as plt
def Span_Manager(Wlow,Whig):
print Wlow,Whig
return
class Event_Manager:
def __init__(self, fig, ax):
self.fig = fig
self.ax = ax
self.redraw()
def redraw(self):
self.fig.canvas.draw()
def connect(self):
self.fig.canvas.mpl_connect('key_press_event', self.onKeyPress)
def onKeyPress(self, ev):
if ev.key == 'q':
print 'activate span selector'
Span = widgets.SpanSelector(ax, Span_Manager, 'horizontal', useblit=False, rectprops=dict(alpha=1, facecolor='Blue'))
elif ev.key == 'w':
print 'activate cursor'
cursor = widgets.Cursor(ax, useblit=True, color='red', linewidth=2)
self.redraw()
fig = plt.figure(figsize=(8, 6))
ax = fig.add_subplot(111)
x, y = 4*(np.random.rand(2, 100)-.5)
ax.plot(x, y, 'o')
ax.set_xlim(-2, 2)
ax.set_ylim(-2, 2)
mat_wid = Event_Manager(fig, ax)
mat_wid.connect()
# Span = widgets.SpanSelector(ax, Span_Manager, 'horizontal', useblit=False, rectprops=dict(alpha=1, facecolor='Blue'))
# cursor = widgets.Cursor(ax, useblit=True, color='red', linewidth=2)
plt.show()

Dynamically adding a vertical line to matplotlib plot

I'm trying to add vertical lines to a matplotlib plot dynmically when a user clicks on a particular point.
import matplotlib.pyplot as plt
import matplotlib.dates as mdate
class PointPicker(object):
def __init__(self,dates,values):
self.fig = plt.figure()
self.ax = self.fig.add_subplot(111)
self.lines2d, = self.ax.plot_date(dates, values, linestyle='-',picker=5)
self.fig.canvas.mpl_connect('pick_event', self.onpick)
self.fig.canvas.mpl_connect('key_press_event', self.onpress)
def onpress(self, event):
"""define some key press events"""
if event.key.lower() == 'q':
sys.exit()
def onpick(self,event):
x = event.mouseevent.xdata
y = event.mouseevent.ydata
print self.ax.axvline(x=x, visible=True)
x = mdate.num2date(x)
print x,y,type(x)
if __name__ == '__main__':
import numpy as np
import datetime
dates=[datetime.datetime.now()+i*datetime.timedelta(days=1) for i in range(100)]
values = np.random.random(100)
plt.ion()
p = PointPicker(dates,values)
plt.show()
Here's an (almost) working example. When I click a point, the onpick method is indeed called and the data seems to be correct, but no vertical line shows up. What do I need to do to get the vertical line to show up?
Thanks
You need to update the canvas drawing (self.fig.canvas.draw()):
def onpick(self,event):
x = event.mouseevent.xdata
y = event.mouseevent.ydata
L = self.ax.axvline(x=x)
self.fig.canvas.draw()

Categories