I’m trying to update a plot dynamically within a for loop and I can’t get it to work. I wonder if anyone can help?
I get a bit confused between passing the figure vs axes and how to update. I’ve been trying to use
display.clear_output(wait=True)
display.display(plt.gcf())
time.sleep(2)
but it’s not doing what I want it to.
I'm trying to:
1. add objects to a grid (setupGrid2)
2. at a timestep - move each object in random direction (makeMove2)
3. update the position of each object visually on the grid (updateGrid2)
My problem is with 3. I'd like to clear the previous step, so that just the new location for each object is displayed. The goal to show the objects dynamically moving around the grid.
I'd also like to work with the ax object created in setupGrid2, so that I can set the plot variables (title, legend etc.) in one place and update that chart.
Grateful for any help.
Sample code below (for running in jupyter notebook):
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import time
import pylab as pl
from IPython import display
def setupGrid2(norows,nocols,noobjects):
#each object needs current grid position (x and y coordinate)
objects = np.zeros(noobjects)
ObjectPos = np.zeros(shape=(noobjects,2))
#put objects randomly on grid
for i in range (noobjects):
ObjectPos[i][0] = np.random.uniform(0,norows)
ObjectPos[i][1] = np.random.uniform(0,nocols)
#plot objects on grid
fig = plt.figure(1,figsize=(15,5))
ax = fig.add_subplot(1,1,1)
x,y = zip(*ObjectPos)
ax.scatter(x, y,c="b", label='Initial positions')
ax.grid()
plt.show()
return ax,ObjectPos
def updateGrid2(ax,ObjPos):
x,y = zip(*ObjPos)
plt.scatter(x, y)
display.clear_output(wait=True)
display.display(plt.gcf())
time.sleep(0.1)
#move object in a random direction
def makeMove2(object,xpos,ypos):
#gets a number: 1,2,3 or 4
direction = int(np.random.uniform(1,4))
if (direction == 1):
ypos = ypos+1
if (direction == 2):
ypos = ypos - 1
if (direction == 3):
xpos = xpos+1
if (direction == 4):
xpos = xpos-1
return xpos,ypos
def Simulation2(rows,cols,objects,steps):
ax,ObjPos = setupGrid2(rows,cols,objects)
for i in range(steps):
for j in range (objects):
xpos = ObjPos[j][0]
ypos = ObjPos[j][1]
newxpos,newypos = makeMove2(j,xpos,ypos)
ObjPos[j][0] = newxpos
ObjPos[j][1] = newypos
updateGrid2(ax,ObjPos)
Simulation2(20,20,2,20)
It seems you want to update the scatter, instead of producing a new scatter for each frame. That would be shown in this question. Of course you can still use display when running this in jupyter instead of the shown solutions with ion or FuncAnimation.
Leaving the code from the question mostly intact this might look as follows.
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import time
import pylab as pl
from IPython import display
def setupGrid2(norows,nocols,noobjects):
#each object needs current grid position (x and y coordinate)
objects = np.zeros(noobjects)
ObjectPos = np.zeros(shape=(noobjects,2))
#put objects randomly on grid
for i in range (noobjects):
ObjectPos[i,0] = np.random.uniform(0,norows)
ObjectPos[i,1] = np.random.uniform(0,nocols)
#plot objects on grid
fig = plt.figure(1,figsize=(15,5))
ax = fig.add_subplot(1,1,1)
ax.axis([0,nocols+1,0,norows+1])
x,y = zip(*ObjectPos)
scatter = ax.scatter(x, y,c="b", label='Initial positions')
ax.grid()
return ax,scatter,ObjectPos
def updateGrid2(ax,sc,ObjPos):
sc.set_offsets(ObjPos)
display.clear_output(wait=True)
display.display(plt.gcf())
time.sleep(0.1)
#move object in a random direction
def makeMove2(object,xpos,ypos):
#gets a number: 1,2,3 or 4
direction = int(np.random.uniform(1,4))
if (direction == 1):
ypos = ypos+1
if (direction == 2):
ypos = ypos - 1
if (direction == 3):
xpos = xpos+1
if (direction == 4):
xpos = xpos-1
return xpos,ypos
def Simulation2(rows,cols,objects,steps):
ax,scatter,ObjPos = setupGrid2(rows,cols,objects)
for i in range(steps):
for j in range (objects):
xpos = ObjPos[j,0]
ypos = ObjPos[j,1]
newxpos,newypos = makeMove2(j,xpos,ypos)
ObjPos[j,0] = newxpos
ObjPos[j,1] = newypos
updateGrid2(ax,scatter,ObjPos)
Simulation2(20,20,3,20)
Related
I'm making a program which takes a random list of data and will plot it.
I want the colour of the graph to change if it goes above a certain value.
https://matplotlib.org/gallery/lines_bars_and_markers/multicolored_line.html
Matplotlib has an entry on doing just this but it seems to require using a function as input for the graph not using lists.
Does anyone know how to either convert this to work for lists or another way of doing so?
Here's my code so far (without my horrific failed attempts to colour code them)
from matplotlib import pyplot as plt
import random
import sys
import numpy as np
#setting the max and min values where I want the colour to change
A_min = 2
B_max = 28
#makes lists for later
A_min_lin = []
B_max_lin = []
#simulating a corruption of the data where it returns all zeros
sim_crpt = random.randint(0,10)
print(sim_crpt)
randomy = []
if sim_crpt == 0:
randomy = []
#making the empty lists for corrupted data
for i in range(0,20):
randomy.append(0)
print(randomy)
else:
#making a random set of values for the y axis
for i in range(0,20):
n = random.randint(0,30)
randomy.append(n)
print(randomy)
#making an x axis for time
time = t = np.arange(0, 20, 1)
#Making a list to plot a straight line showing where the maximum and minimum values
for i in range(0, len(time)):
A_min_lin.append(A_min)
B_max_lin.append(B_max)
#Testing to see if more than 5 y values are zero to return if it's corrupted
tracker = 0
for i in (randomy):
if i == 0:
tracker += 1
if tracker > 5:
sys.exit("Error, no data")
#ploting and showing the different graphs
plt.plot(time,randomy)
plt.plot(time,A_min_lin)
plt.plot(time,B_max_lin)
plt.legend(['Data', 'Minimum for linear', "Maximum for linear"])
plt.show
You can use np.interp to generate the fine-grain data to plot:
# fine grain time
new_time = np.linspace(time.min(), time.max(), 1000)
# interpolate the y values
new_randomy = np.interp(new_time, time, randomy)
# this is copied from the link with few modification
points = np.array([new_time, new_randomy]).T.reshape(-1, 1, 2)
segments = np.concatenate([points[:-1], points[1:]], axis=1)
fig, axs = plt.subplots()
norm = plt.Normalize(new_randomy.min(), new_randomy.max())
lc = LineCollection(segments, cmap='viridis', norm=norm)
# Set the values used for colormapping
lc.set_array(new_randomy[1:])
lc.set_linewidth(2)
line = axs.add_collection(lc)
fig.colorbar(line, ax=axs)
# set the limits
axs.set_xlim(new_time.min(), new_time.max())
axs.set_ylim(new_randomy.min(), new_randomy.max())
plt.show()
Output:
I am using Multicursor to get a cursor on every graph.
I want to show the value of the datapoint, which is hit by the cursor, inside a legend during hovering over the graphs, like this
Actually I have thought that this is a standard feature of matplotlib respectively Multicursor, but it seems not. Did someone already something like this or do I have to implement it by my own.
I already found this post matplotlib multiple values under cursor, but this could be just the beginning for the implementation I want.
I have developed a solution.
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.widgets import MultiCursor
from bisect import bisect_left
fig = plt.figure(figsize=(15, 8))
# create random graph with 60 datapoints, 0 till 59
x = list(range(0,60))
axes_list = []
def createRandomGraph(ax,x):
y = np.random.randint(low=0, high=15, size=60)
data.append(y)
ax.plot(x,y, marker='.')
def take_closest(myList, myNumber):
"""
Assumes myList is sorted. Returns closest value to myNumber.
If two numbers are equally close, return the smallest number.
"""
pos = bisect_left(myList, myNumber)
if pos == 0:
return myList[0]
if pos == len(myList):
return myList[-1]
before = myList[pos - 1]
after = myList[pos]
if after - myNumber < myNumber - before:
return after, pos
else:
return before, pos-1
def show_Legend(event):
#get mouse coordinates
mouseXdata = event.xdata
# the value of the closest data point to the current mouse position shall be shown
closestXValue, posClosestXvalue = take_closest(data[0], mouseXdata)
i = 1
for ax in axes_list:
datalegend = ax.text(1.05, 0.5, data[i][posClosestXvalue], fontsize=7,
verticalalignment='top', bbox=props, transform=ax.transAxes)
ax.draw_artist(datalegend)
# this remove is required because otherwise after a resizing of the window there is
# an artifact of the last label, which lies behind the new one
datalegend.remove()
i +=1
fig.canvas.update()
# store the x value of the graph in the first element of the list
data = [x]
# properties of the legend labels
props = dict(boxstyle='round', edgecolor='black', facecolor='wheat', alpha=1.5)
for i in range(5):
if(i>0):
# all plots share the same x axes, thus during zooming and panning
# we will see always the same x section of each graph
ax = plt.subplot(5, 1, i+1, sharex=ax)
else:
ax = plt.subplot(5, 1, i+1)
axes_list.append(ax)
createRandomGraph(ax,x)
multi = MultiCursor(fig.canvas, axes_list, color='r', lw=1)
# function show_Legend is called while hovering over the graphs
fig.canvas.mpl_connect('motion_notify_event', show_Legend)
plt.show()
The output looks like this
Maybe you like it and find it useful
I am stuck again with interactive plotting with matplotlib.
Everything else works like a charm (hovering and clicking of objects in a figure) but if I zoom the shown figure and it will be updated, zooming rectangle will remain in the new figure. Probably I have to reset zooming settings somehow but I couldn't find out the correct method to do it from other StackOverflow questions (clearing the figure is not obviously enough).
I built a toy example to illustrate the problem. Four points are attached to four images and they are plotted to the figure. With interactive-mode by inserting cursor on top of chosen point, it shows related image in a imagebox. After one point is clicked, program waits 2 seconds and updates the view by rotating all the samples 15 degrees.
The problem occurs when current view is zoomed and then its updated. Zoom-to-rectangle will start automatically and after clicking once anywhere in the figure, the rectangle is gone without doing anything. This is shown in below image. I just want to have normal cursor after figure is updated.
Here is the code for the toy example:
import matplotlib.pyplot as plt
import matplotlib as mpl
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
import numpy as np
import copy
def initialize_figure(fignum):
plt.figure(fignum)
plt.clf()
def draw_interactive_figures(new_samples, images):
global new_samples_tmp, images_tmp, offset_image_tmp, image_box_tmp, fig_tmp, x_tmp, y_tmp
initialize_figure(1)
plt.ion()
fig_tmp = plt.gcf()
images_tmp = copy.deepcopy(images)
offset_image_tmp = OffsetImage(images_tmp[0,:,:,:])
image_box_tmp = (40., 40.)
x_tmp = new_samples[:,0]
y_tmp = new_samples[:,1]
new_samples_tmp = copy.deepcopy(new_samples)
update_plot()
fig_tmp.canvas.mpl_connect('motion_notify_event', hover)
fig_tmp.canvas.mpl_connect('button_press_event', click)
plt.show()
fig_tmp.canvas.start_event_loop()
plt.ioff()
def update_plot():
global points_tmp, annotationbox_tmp
ax = plt.gca()
points_tmp = plt.scatter(*new_samples_tmp.T, s=14, c='b', edgecolor='k')
annotationbox_tmp = AnnotationBbox(offset_image_tmp, (0,0), xybox=image_box_tmp, xycoords='data', boxcoords='offset points', pad=0.3, arrowprops=dict(arrowstyle='->'))
ax.add_artist(annotationbox_tmp)
annotationbox_tmp.set_visible(False)
def hover(event):
if points_tmp.contains(event)[0]:
inds = points_tmp.contains(event)[1]['ind']
ind = inds[0]
w,h = fig_tmp.get_size_inches()*fig_tmp.dpi
ws = (event.x > w/2.)*-1 + (event.x <= w/2.)
hs = (event.y > h/2.)*-1 + (event.y <= h/2.)
annotationbox_tmp.xybox = (image_box_tmp[0]*ws, image_box_tmp[1]*hs)
annotationbox_tmp.set_visible(True)
annotationbox_tmp.xy =(x_tmp[ind], y_tmp[ind])
offset_image_tmp.set_data(images_tmp[ind,:,:])
else:
annotationbox_tmp.set_visible(False)
fig_tmp.canvas.draw_idle()
def click(event):
if points_tmp.contains(event)[0]:
inds = points_tmp.contains(event)[1]['ind']
ind = inds[0]
initialize_figure(1)
update_plot()
plt.scatter(x_tmp[ind], y_tmp[ind], s=20, marker='*', c='y')
plt.pause(2)
fig_tmp.canvas.stop_event_loop()
fig_tmp.canvas.draw_idle()
def main():
fig, ax = plt.subplots(1, figsize=(7, 7))
points = np.array([[1,1],[1,-1],[-1,1],[-1,-1]])
zero_layer = np.zeros([28,28])
one_layer = np.ones([28,28])*255
images = np.array([np.array([zero_layer, zero_layer, one_layer]).astype(np.uint8),np.array([zero_layer, one_layer, zero_layer]).astype(np.uint8),np.array([one_layer, zero_layer, zero_layer]).astype(np.uint8),np.array([one_layer, zero_layer, one_layer]).astype(np.uint8)])
images = np.transpose(images, (0,3,2,1))
theta = 0
delta = 15 * (np.pi/180)
rotation_matrix = np.array([[np.cos(theta),-np.sin(theta)],[np.sin(theta),np.cos(theta)]])
while True:
rotated_points = np.matmul(points, rotation_matrix)
draw_interactive_figures(rotated_points, images)
theta += delta
rotation_matrix = np.array([[np.cos(theta),-np.sin(theta)],[np.sin(theta),np.cos(theta)]])
if __name__== "__main__":
main()
Thanks in advance!
I'm providing you with a starting point here. The following is a script that creates a plot and allows you to add new points by clicking on the axes. For each point one may mouse hover and show a respective image.
import matplotlib.pyplot as plt
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
import numpy as np
class MyInteractivePlotter():
def __init__(self):
self.fig, self.ax = plt.subplots()
self.ax.set(xlim=(0,1), ylim=(0,1))
self.points = np.array([[0.5,0.5]]) # will become N x 2 array
self.images = [np.random.rand(10,10)]
self.scatter = self.ax.scatter(*self.points.T)
self.im = OffsetImage(self.images[0], zoom=5)
self.ab = AnnotationBbox(self.im, (0,0), xybox=(50., 50.), xycoords='data',
boxcoords="offset points", pad=0.3,
arrowprops=dict(arrowstyle="->"))
# add it to the axes and make it invisible
self.ax.add_artist(self.ab)
self.ab.set_visible(False)
self.cid = self.fig.canvas.mpl_connect("button_press_event", self.onclick)
self.hid = self.fig.canvas.mpl_connect("motion_notify_event", self.onhover)
def add_point(self):
# Update points (here, we just add a new random point)
self.points = np.concatenate((self.points, np.random.rand(1,2)), axis=0)
# For each points there is an image. (Here, we just add a random one)
self.images.append(np.random.rand(10,10))
# Update the scatter plot to show the new point
self.scatter.set_offsets(self.points)
def onclick(self, event):
self.add_point()
self.fig.canvas.draw_idle()
def onhover(self, event):
# if the mouse is over the scatter points
if self.scatter.contains(event)[0]:
# find out the index within the array from the event
ind, = self.scatter.contains(event)[1]["ind"]
# make annotation box visible
self.ab.set_visible(True)
# place it at the position of the hovered scatter point
self.ab.xy = self.points[ind,:]
# set the image corresponding to that point
self.im.set_data(self.images[ind])
else:
#if the mouse is not over a scatter point
self.ab.set_visible(False)
self.fig.canvas.draw_idle()
m = MyInteractivePlotter()
plt.show()
I would suggest you take this and add your functionality into it. Once you stumble upon a problem you can use it to ask for clarifications.
I'm trying to find a peak of an fft of a signal to be used for a further analysis of the signal. I'm using a SpanSelect of data and doing an fft, represented as a frequency spectrum. I really wanted to have the plot be interactive and the user click a point to be further analyzed, but I don't see a way to do that so would like a way to find local frequency peaks. The frequency spectrum may look like this:
So I would want a way to return the frequency that has a peak at 38 hz for example. Is there a way to do this?
use argrelextrema for finding local maxima:
import numpy as np
from scipy.signal import argrelextrema
from matplotlib.pyplot import *
np.random.seed()
x = np.random.random(50)
m = argrelextrema(x, np.greater) #array of indexes of the locals maxima
y = [x[m] for i in m]
plot(x)
plot(m, y, 'rs')
show()
You can do something like that using matplotlib widgets, for example check out the lasso method of selecting points.
You can then use the selected point in any form of analysis you need.
EDIT: Combined lasso and SpanSelect widget from matplotlib examples
#!/usr/bin/env python
from __future__ import print_function
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import SpanSelector, LassoSelector
from matplotlib.path import Path
import matplotlib.pyplot as plt
try:
raw_input
except NameError:
# Python 3
raw_input = input
class SelectFromCollection(object):
"""Select indices from a matplotlib collection using `LassoSelector`.
Selected indices are saved in the `ind` attribute. This tool highlights
selected points by fading them out (i.e., reducing their alpha values).
If your collection has alpha < 1, this tool will permanently alter them.
Note that this tool selects collection objects based on their *origins*
(i.e., `offsets`).
Parameters
----------
ax : :class:`~matplotlib.axes.Axes`
Axes to interact with.
collection : :class:`matplotlib.collections.Collection` subclass
Collection you want to select from.
alpha_other : 0 <= float <= 1
To highlight a selection, this tool sets all selected points to an
alpha value of 1 and non-selected points to `alpha_other`.
"""
def __init__(self, ax, collection, alpha_other=0.3):
self.canvas = ax.figure.canvas
self.collection = collection
self.alpha_other = alpha_other
self.xys = collection.get_offsets()
self.Npts = len(self.xys)
# Ensure that we have separate colors for each object
self.fc = collection.get_facecolors()
if len(self.fc) == 0:
raise ValueError('Collection must have a facecolor')
elif len(self.fc) == 1:
self.fc = np.tile(self.fc, self.Npts).reshape(self.Npts, -1)
self.lasso = LassoSelector(ax, onselect=self.onselect)
self.ind = []
def onselect(self, verts):
path = Path(verts)
self.ind = np.nonzero([path.contains_point(xy) for xy in self.xys])[0]
self.fc[:, -1] = self.alpha_other
self.fc[self.ind, -1] = 1
self.collection.set_facecolors(self.fc)
self.canvas.draw_idle()
def disconnect(self):
self.lasso.disconnect_events()
self.fc[:, -1] = 1
self.collection.set_facecolors(self.fc)
self.canvas.draw_idle()
def onselect(xmin, xmax):
indmin, indmax = np.searchsorted(x, (xmin, xmax))
indmax = min(len(x)-1, indmax)
thisx = x[indmin:indmax]
thisy = y[indmin:indmax]
line2.set_data(thisx, thisy)
ax2.set_xlim(thisx[0], thisx[-1])
ax2.set_ylim(thisy.min(), thisy.max())
fig.canvas.draw()
if __name__ == '__main__':
plt.ion()
fig = plt.figure(figsize=(8,6))
ax = fig.add_subplot(211, axisbg='#FFFFCC')
x = np.arange(0.0, 5.0, 0.01)
y = np.sin(2*np.pi*x) + 0.5*np.random.randn(len(x))
ax.plot(x, y, '-')
ax.set_ylim(-2,2)
ax.set_title('Press left mouse button and drag to test')
ax2 = fig.add_subplot(212, axisbg='#FFFFCC')
line2, = ax2.plot(x, y, '-')
pts = ax2.scatter(x, y)
# set useblit True on gtkagg for enhanced performance
span = SpanSelector(ax, onselect, 'horizontal', useblit=True,
rectprops=dict(alpha=0.5, facecolor='red') )
selector = SelectFromCollection(ax2, pts)
plt.draw()
raw_input('Press any key to accept selected points')
print("Selected points:")
print(selector.xys[selector.ind])
selector.disconnect()
# Block end of script so you can check that the lasso is disconnected.
raw_input('Press any key to quit')
The contour plot demo shows how you can plot the curves with the level value plotted over them, see below.
Is there a way to do this same thing for a simple line plot like the one obtained with the code below?
import matplotlib.pyplot as plt
x = [1.81,1.715,1.78,1.613,1.629,1.714,1.62,1.738,1.495,1.669,1.57,1.877,1.385]
y = [0.924,0.915,0.914,0.91,0.909,0.905,0.905,0.893,0.886,0.881,0.873,0.873,0.844]
# This is the string that should show somewhere over the plotted line.
line_string = 'name of line'
# plotting
plt.plot(x,y)
plt.show()
You could simply add some text (MPL Gallery) like
import matplotlib.pyplot as plt
import numpy as np
x = [1.81,1.715,1.78,1.613,1.629,1.714,1.62,1.738,1.495,1.669,1.57,1.877,1.385]
y = [0.924,0.915,0.914,0.91,0.909,0.905,0.905,0.893,0.886,0.881,0.873,0.873,0.844]
# This is the string that should show somewhere over the plotted line.
line_string = 'name of line'
# plotting
fig, ax = plt.subplots(1,1)
l, = ax.plot(x,y)
pos = [(x[-2]+x[-1])/2., (y[-2]+y[-1])/2.]
# transform data points to screen space
xscreen = ax.transData.transform(zip(x[-2::],y[-2::]))
rot = np.rad2deg(np.arctan2(*np.abs(np.gradient(xscreen)[0][0][::-1])))
ltex = plt.text(pos[0], pos[1], line_string, size=9, rotation=rot, color = l.get_color(),
ha="center", va="center",bbox = dict(ec='1',fc='1'))
def updaterot(event):
"""Event to update the rotation of the labels"""
xs = ax.transData.transform(zip(x[-2::],y[-2::]))
rot = np.rad2deg(np.arctan2(*np.abs(np.gradient(xs)[0][0][::-1])))
ltex.set_rotation(rot)
fig.canvas.mpl_connect('button_release_event', updaterot)
plt.show()
which gives
This way you have maximum control.
Note, the rotation is in degrees and in screen not data space.
Update:
As I recently needed automatic label rotations which update on zooming and panning, thus I updated my answer to account for these needs. Now the label rotation is updated on every mouse button release (the draw_event alone was not triggered when zooming). This approach uses matplotlib transformations to link the data and screen space as discussed in this tutorial.
Based on Jakob's code, here is a function that rotates the text in data space, puts labels near a given x or y data coordinate, and works also with log plots.
def label_line(line, label_text, near_i=None, near_x=None, near_y=None, rotation_offset=0, offset=(0,0)):
"""call
l, = plt.loglog(x, y)
label_line(l, "text", near_x=0.32)
"""
def put_label(i):
"""put label at given index"""
i = min(i, len(x)-2)
dx = sx[i+1] - sx[i]
dy = sy[i+1] - sy[i]
rotation = np.rad2deg(math.atan2(dy, dx)) + rotation_offset
pos = [(x[i] + x[i+1])/2. + offset[0], (y[i] + y[i+1])/2 + offset[1]]
plt.text(pos[0], pos[1], label_text, size=9, rotation=rotation, color = line.get_color(),
ha="center", va="center", bbox = dict(ec='1',fc='1'))
x = line.get_xdata()
y = line.get_ydata()
ax = line.get_axes()
if ax.get_xscale() == 'log':
sx = np.log10(x) # screen space
else:
sx = x
if ax.get_yscale() == 'log':
sy = np.log10(y)
else:
sy = y
# find index
if near_i is not None:
i = near_i
if i < 0: # sanitize negative i
i = len(x) + i
put_label(i)
elif near_x is not None:
for i in range(len(x)-2):
if (x[i] < near_x and x[i+1] >= near_x) or (x[i+1] < near_x and x[i] >= near_x):
put_label(i)
elif near_y is not None:
for i in range(len(y)-2):
if (y[i] < near_y and y[i+1] >= near_y) or (y[i+1] < near_y and y[i] >= near_y):
put_label(i)
else:
raise ValueError("Need one of near_i, near_x, near_y")