Annotate point on axes with automatic tick formatting in matplotlib? - python

Consider this example code, derived from Matplotlib Indicate Point on X and Y Axis :
import numpy as np
import matplotlib.pyplot as plt
class PointMarker():
def __init__(self, ax, point, **kwargs):
self.ax = ax
self.point = point
if "line" in kwargs:
self.c = kwargs.get("line").get_color()
else:
self.c = kwargs.get("color", "b")
self.ls=kwargs.get("linestyle", ':')
self.vline, = self.ax.plot([],[],color=self.c,linestyle=self.ls)
self.hline, = self.ax.plot([],[],color=self.c,linestyle=self.ls)
self.draw()
def draw(self):
xmin = ax.get_xlim()[0]
ymin = ax.get_ylim()[0]
self.vline.set_data([self.point[0], self.point[0]], [ymin,self.point[1]])
self.hline.set_data([xmin, self.point[0]], [self.point[1], self.point[1]])
class PointMarkers():
pointmarkers = []
def add(self,ax, point, **kwargs ):
pm = PointMarker(ax, point, **kwargs)
self.pointmarkers.append(pm)
def update(self, event=None):
for pm in self.pointmarkers:
pm.draw()
x = np.arange(1,17)
y = np.log(x)
ax = plt.subplot(111)
line = plt.plot(x,y)
# register the markers
p = PointMarkers()
p.add(ax,[x[5],y[5]], line=line[0])
# connect event listener
cid = plt.gcf().canvas.mpl_connect("draw_event", p.update)
plt.grid(True)
plt.show()
What I would like to do, is to keep the automatic tick label formatting on the axis - and insert text labels that would annotate the point; so something like this (where I've manually added the annotating text labels):
Basically, if the graph is zoomed in, and the ticks/tick labels change, I would like the annotation labels to also be present (if they are still in view, of course) ...
I would be OK with either placing the annotation labels below abscissa/to left of ordinate (as drawn above) - or, with replacing the automatic tick labels with the annotation labels, where they overlap (so in above example, the "6" tick label of the abscissa would be removed and replaced with "my_point_X").
How can an annotation like this be implemented?

We set it up with reference to the official references.
The off-axis position was set manually. I have little experience with this task so there may be a better way to do it.
import numpy as np
import matplotlib.pyplot as plt
class PointMarker():
def __init__(self, ax, point, **kwargs):
self.ax = ax
self.point = point
if "line" in kwargs:
self.c = kwargs.get("line").get_color()
else:
self.c = kwargs.get("color", "b")
self.ls=kwargs.get("linestyle", ':')
self.vline, = self.ax.plot([],[],color=self.c,linestyle=self.ls)
self.hline, = self.ax.plot([],[],color=self.c,linestyle=self.ls)
self.draw()
def draw(self):
xmin = ax.get_xlim()[0]
ymin = ax.get_ylim()[0]
self.vline.set_data([self.point[0], self.point[0]], [ymin,self.point[1]])
self.hline.set_data([xmin, self.point[0]], [self.point[1], self.point[1]])
class PointMarkers():
pointmarkers = []
def add(self,ax, point, **kwargs ):
pm = PointMarker(ax, point, **kwargs)
self.pointmarkers.append(pm)
def update(self, event=None):
for pm in self.pointmarkers:
pm.draw()
x = np.arange(1,17)
y = np.log(x)
fig = plt.figure(figsize=(4,3),dpi=144) # update
ax = fig.add_subplot(111) # update
# ax = plt.subplot(111)
line = plt.plot(x,y)
# register the markers
p = PointMarkers()
p.add(ax,[x[5],y[5]], line=line[0])
# update start
# x_points = x[5]/x.max()
# y_points = y[5]/y.max()
ax.annotate('my_point_Y', xy=(0.3, 1.75), xycoords='data', color='r', fontsize=9)
ax.annotate('my_point_X', xy=(5.0, -0.1), xycoords='data', color='r', fontsize=9)
# update end
# connect event listener
cid = plt.gcf().canvas.mpl_connect("draw_event", p.update)
plt.grid(True)
plt.show()

Related

Is there a way to reset fig.canvas.manager.set_window_title()? (overlapping window titles)

I tried to do a wrap function around the plotting class, however when I save my plots first function looks good, however in second fig_name overlaps on each other. I am guessing the problems is fig.canvas.manager.set_window_title(fig_name)(I use this name function because I might use more than one axis plot in one figure). Here is my image that I would like to fix:
My example code:
import numpy as np
import matplotlib.pyplot as plt
# figure creating and saving wrapper
def figure_wrapper(func):
def wrapper(*args,**kwargs):
fig, ax, x_label, y_label, axs_name, fig_name = func(*args,**kwargs)
for ax_name in axs_name:
ax.set_title(ax_name, fontweight='bold', size='22')
ax.legend()
ax.set_xlabel(x_label, fontweight='bold')
ax.set_ylabel(y_label, fontweight='bold')
fig.canvas.manager.set_window_title(fig_name)
fig.savefig(fig_name, bbox_inches='tight', dpi=1200)
plt.close(fig)
return wrapper
# plotting class
class plotting():
def __init__(self):
self.fig=plt.figure()
#figure_wrapper
def plot(self,x,y):
ax_name = 'plot1'
axs_name = [ax_name]
fig_name = "Some nice name for plot figure 1"
ax = self.fig.add_subplot(label=ax_name)
x_label = "R \u03A9"
y_label = "L \u03BCm"
ax.plot(x,y,label= '1')
ax.set_title(ax_name, fontweight='bold', size='22')
return self.fig, ax, x_label, y_label, axs_name, fig_name
#figure_wrapper
def scatter(self,x,y,):
ax_name = 'scatter1'
axs_name = [ax_name]
fig_name = "Even better name for plot figure 2"
ax = self.fig.add_subplot(label=ax_name)
x_label = "R \u03A9"
y_label = "L \u03BCm"
ax.scatter(x, y,label= '2')
return self.fig, ax, x_label, y_label, axs_name, fig_name
# data for plotting
x = np.linspace(0, 10, 10)
y = np.linspace(20, 30, 10)
x1 = np.linspace(20, 10, 10)
y1 = np.linspace(60, 30, 10)
# execution of class
plotting = plotting()
plotting.plot(x,y)
plotting.scatter(x,y)
You have to create new figure for every plot
#figure_wrapper
def plot(self, x, y):
self.fig = plt.figure()
# ... code ...
#figure_wrapper
def scatter(self, x, y):
self.fig = plt.figure()
# ... code ...
Or you have to clear() existing figure.
#figure_wrapper
def plot(self, x, y):
self.fig.clear()
# ... code ...
#figure_wrapper
def scatter(self, x, y):
self.fig.clear()
# ... code ...
Problem can be because every plot uses self.fig.add_subplot(label=ax_name) so it adds new plot to existing plot. And it has nothing to do with set_title()

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()

Matplotlib Animation does not update within class

I'm writing a simple class to plot a sensor value in real-time; however, the animation does not run within the class.
I've tried to return the animation object to have an instance outside of the class but this does not work.
To my understanding, this is the same issue as raised in GitHub #1656
import datetime as dt
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from random import random
class Animate:
def __init__(self, sensor):
# Create figure for plotting
self.fig = plt.figure()
self.ax = self.fig.add_subplot(1, 1, 1)
self.xs = []
self.ys = []
self.ylabel = sensor
self.readings = 20
# This function is called periodically from FuncAnimation
def _update(self, i, xs, ys):
# Get sensor value
value = random()
# Add x and y to lists
self.xs.append(dt.datetime.now().strftime('%H:%M:%S.%f'))
self.ys.append(value)
# Limit x and y lists to 20 items
self.xs = self.xs[-self.readings:]
self.ys = self.ys[-self.readings:]
# Draw x and y lists
self.ax.clear()
self.ax.plot(xs, ys)
# Format plot
plt.xticks(rotation=45, ha='right')
plt.subplots_adjust(bottom=0.30)
plt.title(self.ylabel + ' over Time')
plt.ylabel(self.ylabel)
def start(self):
print('Starting')
# Set up plot to call animate() function periodically
self.anim = animation.FuncAnimation(self.fig, self._update, fargs=(self.xs, self.ys), interval=200)
plt.show();
rand = Animate('Torque')
rand.start();
your variables xs and ys are already named self.xs and self.ys, which are accessible in the class namespace; you do not need to pass them to self.update
import datetime as dt
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from random import random
class Animate:
def __init__(self, sensor):
# Create figure for plotting
self.fig = plt.figure()
self.ax = self.fig.add_subplot(1, 1, 1)
self.xs = []
self.ys = []
self.ylabel = sensor
self.readings = 20
# This function is called periodically from FuncAnimation
def _update(self, i):
# Read temperature (Celsius) from TMP102
temp_c = random()
# Add x and y to lists
self.xs.append(dt.datetime.now().strftime('%H:%M:%S.%f'))
self.ys.append(temp_c)
# Limit x and y lists to 20 items
self.xs = self.xs[-self.readings:]
self.ys = self.ys[-self.readings:]
# Draw x and y lists
self.ax.clear()
self.ax.plot(self.xs, self.ys)
# Format plot
plt.xticks(rotation=45, ha='right')
plt.subplots_adjust(bottom=0.30)
plt.title(self.ylabel + ' over Time')
plt.ylabel(self.ylabel)
def start(self):
print('Starting')
# Set up plot to call animate() function periodically
self.anim = animation.FuncAnimation(self.fig, self._update, interval=200)
plt.show()
rand = Animate('Torque')
rand.start()

Python: how to get coordinates on mouse click using matplotlib.canvas

I am writing a class to process images. In that class, I want to define a method that can allow me to return coordinates of the mouse clicks. I can get the coordinates as an attribute but if I call the method to return the coordinates, I get an empty tuple
Here is the code:
import cv2
import matplotlib.pyplot as plt
class TestClass():
def __init__(self):
self.fname = 'image.jpg'
self.img = cv2.imread(self.fname)
self.point = ()
def getCoord(self):
fig = plt.figure()
ax = fig.add_subplot(111)
plt.imshow(self.img)
cid = fig.canvas.mpl_connect('button_press_event', self.__onclick__)
return self.point
def __onclick__(self,click):
self.point = (click.xdata,click.ydata)
return self.point
Your code works for me, as long as I insert plt.show() after mpl_connect in getCoord:
def getCoord(self):
fig = plt.figure()
ax = fig.add_subplot(111)
plt.imshow(self.img)
cid = fig.canvas.mpl_connect('button_press_event', self.__onclick__)
plt.show()
return self.point

Saving scatterplot animations with matplotlib

I've been trying to save an animated scatterplot with matplotlib, and I would prefer that it didn't require totally different code for viewing as an animated figure and for saving a copy. The figure shows all the datapoints perfectly after the save completes.
This code is a modified version of Giggi's on Animating 3d scatterplot in matplotlib, with a fix for the colors from Yann's answer on Matplotlib 3D scatter color lost after redraw (because colors will be important on my video, so I want to make sure they work).
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np
from mpl_toolkits.mplot3d import Axes3D
FLOOR = -10
CEILING = 10
class AnimatedScatter(object):
def __init__(self, numpoints=5):
self.numpoints = numpoints
self.stream = self.data_stream()
self.angle = 0
self.fig = plt.figure()
self.fig.canvas.mpl_connect('draw_event',self.forceUpdate)
self.ax = self.fig.add_subplot(111,projection = '3d')
self.ani = animation.FuncAnimation(self.fig, self.update, interval=100,
init_func=self.setup_plot, blit=True,frames=20)
def change_angle(self):
self.angle = (self.angle + 1)%360
def forceUpdate(self, event):
self.scat.changed()
def setup_plot(self):
X = next(self.stream)
c = ['b', 'r', 'g', 'y', 'm']
self.scat = self.ax.scatter(X[:,0], X[:,1], X[:,2] , c=c, s=200, animated=True)
self.ax.set_xlim3d(FLOOR, CEILING)
self.ax.set_ylim3d(FLOOR, CEILING)
self.ax.set_zlim3d(FLOOR, CEILING)
return self.scat,
def data_stream(self):
data = np.zeros(( self.numpoints , 3 ))
xyz = data[:,:3]
while True:
xyz += 2 * (np.random.random(( self.numpoints,3)) - 0.5)
yield data
def update(self, i):
data = next(self.stream)
#data = np.transpose(data)
self.scat._offsets3d = ( np.ma.ravel(data[:,0]) , np.ma.ravel(data[:,1]) , np.ma.ravel(data[:,2]) )
plt.draw()
return self.scat,
def show(self):
plt.show()
if __name__ == '__main__':
a = AnimatedScatter()
a.ani.save("movie.avi", codec='avi')
a.show()
A perfectly valid .avi is generated by this, but it's blank for all of the four seconds except for the axes. The actual figure always shows exactly what I want to see. How can I populate the save function's plots the same way I populate a normally running animation, or is it possible in matplotlib?
EDIT: Using a scatter call in the update (without setting the bounds as in the initializer) causes the .avi to show the axes growing, showing that the data is being run each time, it's just not showing on the video itself.
I am using matplotlib 1.1.1rc with Python 2.7.3.
Remove blit=True from FuncAnimation and animated=True from scatter and it works. I suspect that there is something going wrong with the logic that makes sure only the artists that need to be updated are updated/redrawn between frames (rather than just re-drawing everything).
Below is exactly what I ran and I got the expected output movie:
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np
from mpl_toolkits.mplot3d import Axes3D
FLOOR = -10
CEILING = 10
class AnimatedScatter(object):
def __init__(self, numpoints=5):
self.numpoints = numpoints
self.stream = self.data_stream()
self.angle = 0
self.fig = plt.figure()
self.fig.canvas.mpl_connect('draw_event',self.forceUpdate)
self.ax = self.fig.add_subplot(111,projection = '3d')
self.ani = animation.FuncAnimation(self.fig, self.update, interval=100,
init_func=self.setup_plot, frames=20)
def change_angle(self):
self.angle = (self.angle + 1)%360
def forceUpdate(self, event):
self.scat.changed()
def setup_plot(self):
X = next(self.stream)
c = ['b', 'r', 'g', 'y', 'm']
self.scat = self.ax.scatter(X[:,0], X[:,1], X[:,2] , c=c, s=200)
self.ax.set_xlim3d(FLOOR, CEILING)
self.ax.set_ylim3d(FLOOR, CEILING)
self.ax.set_zlim3d(FLOOR, CEILING)
return self.scat,
def data_stream(self):
data = np.zeros(( self.numpoints , 3 ))
xyz = data[:,:3]
while True:
xyz += 2 * (np.random.random(( self.numpoints,3)) - 0.5)
yield data
def update(self, i):
data = next(self.stream)
self.scat._offsets3d = ( np.ma.ravel(data[:,0]) , np.ma.ravel(data[:,1]) , np.ma.ravel(data[:,2]) )
return self.scat,
def show(self):
plt.show()
if __name__ == '__main__':
a = AnimatedScatter()
a.ani.save("movie.avi", codec='avi')
a.show()

Categories