Related
In the code posted in the question How can I draw a multiple 3d-curves picture by Python?, plot method is called twice and since the points to plot are not resetting, the lines are drown on top of the other. But instead of plot() if we try with the scatter method, we can see points plotted in different location. Why does this change in the behavior?
The code is copied below
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import numpy as np
import math as mt
from mpl_toolkits.mplot3d import Axes3D
t=2 #t can be changed
fig = plt.figure()
ax=Axes3D(fig)
#data
def unitilize(x,y,z):
r=mt.sqrt(x**2+y**2+z**2)
return x/r, y/r, z/r
def g_1(x,y,z):
x=t*x
z=z/t
x,y,z=unitilize(x,y,z)
return x,y,z
stepCnt=10000 ######step
#########data#################
xs = np.empty((stepCnt + 1,))
ys = np.empty((stepCnt + 1,))
zs = np.empty((stepCnt + 1,))
#Setting initial values
def huatu(x,y,z):
xs[0], ys[0], zs[0] =unitilize(x,y,z)
for i in range(stepCnt):
xs[i+1],ys[i+1],zs[i+1]=g_1(xs[i], ys[i], zs[i])
return xs,ys,zs
xs3,ys3,zs3=huatu(1,10,40)
ax.plot(xs3, ys3, zs3, color='b', marker='x')
xs2,ys2,zs2=huatu(1,0,40)
ax.plot(xs2, ys2, zs2, color='r', marker='o')
plt.show()
Plot output:
Scatter output:
So, you found something really weird, the precise source of which I haven't been able to track down. The bottom line is that lines drawn by Axes3D.plot (and Axes.plot which is how these are actually created) don't copy their input data but rather work with a view. This implies that the plot can change when the data is subsequently mutated. For some reason Axes.plot, which also uses views, doesn't reproduce this mutability. This might have something to do with how Axes3D objects are updated, I don't really know.
Anyway, Axes3D.scatter on the other hand creates PathCollection objects (cast to PathCollection3D), which have much more complicated internal workings. As far as I can tell, these objects (already in 2d) work with an ._offsets property, which is an ndarray built from input coordinates. By construction these arrays are independent from the input data.
Let's compare the cases for plot to see what I mean. For a usual two-dimensional plot:
import numpy as np
import matplotlib.pyplot as plt
fig,ax = plt.subplots()
# first set data to zero
# we'll use an ndarray as input, otherwise there's no chance to get a view
x = np.arange(3)
y = np.array([0.0,0.0,0.0])
# plot the flat line
pl, = ax.plot(x,y,'o-')
# change the axes for better comparison later; not actually relevant
ax.set_ylim([0,4])
# see that the input data are kept as views
print(pl.get_xdata().base is x) # True
print(pl.get_ydata().base is y) # True
# mutating x would actually change pl.get_xdata() and vice versa
# mutate y to get a nontrivial line
y[:] = [1,2,3]
# update the canvas in an interactive plot
# plt.show() probably suffices non-interactively
fig.canvas.draw()
plt.show()
The result contains the original flat zero line:
Note that the few print calls in the middle verify that the data attached to the line objects created by plot are indeed views (rather than copies) of the input data, so the lack of effect here is due to how modifying the data is being reflected on the plots.
Compare the 3d case:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure()
ax = fig.add_subplot(111,projection='3d')
# first set data to zero
# we'll use an ndarray as input, otherwise there's no chance to get a view
x = np.arange(3)
y = np.array([0.0,0.0,0.0])
z = np.array([0.0,0.0,0.0])
# plot the flat line
pl, = ax.plot(x,y,z,'o-')
# change the axes to see the result; not actually relevant
ax.set_ylim([0,4])
ax.set_zlim([0,4])
# mutate y,z to get a nontrivial line
y[:] = [1,2,3]
z[:] = [1,2,3]
# update the canvas in an interactive plot
# plt.show() probably suffices non-interactively
fig.canvas.draw()
plt.show()
We're doing the exact same thing only with a 3d axes object (and one more dimension), and here's the result:
As you can see, the plot is nicely updated by the mutation of the original source arrays, in complete contrast with the 2d case.
I'm not really sure how this happens; Axes3D.plot outsources most of the problem to Axes.plot (well, the 2d part), and then pulls out all the data along the third dimension. Since the lines are created by Axes.plot in both cases, it's not surprising that neither copy their input data.
Axes3D.scatter quite similarly lets Axes.scatter do the 2d job. While I don't understand how the plot case differs between 2d and 3d, I find this part easier to understand: PathCollection(3D) objects are much more complicated and can't be assembled without decoupling yourself from the original data arrays.
So in the code in your question, the function that generates the data to plot actually mutates (and returns) the same arrays xs,ys,zs. Since essentially the same array is used for every plot, the result you see depends on whether the plotting call is sensitive to mutation of its data source. For Axes3D.plot this is the case, and thus the second call to the data generating function modifies the first plot; while for Axes3D.scatter mutation of the data source doesn't affect the plot, thus both plots are visible as expected.
If you want to see really weird, try my 3d example using list input instead of ndarray:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure()
ax = fig.add_subplot(111,projection='3d')
# first set data to zero, but lists this time
x = np.arange(3)
y = [0.0,0.0,0.0]
z = [0.0,0.0,0.0]
# plot the flat line
pl, = ax.plot(x,y,z,'o-')
# change the axes to see the result; not actually relevant
ax.set_ylim([0,4])
ax.set_zlim([0,4])
# mutate y,z to get a nontrivial line
y[:] = [1,2,3]
z[:] = [1,2,3]
# update the canvas in an interactive plot
# plt.show() probably suffices non-interactively
fig.canvas.draw()
plt.show()
I'd expect in this case that the input lists are converted to ndarrays, and thus mutation doesn't do anything and we get a flat zero line. This is not the case:
Apparently the y coordinates doesn't change, but the z coordinates get mutated. Now this is weird! The key is the underlying data array of the plot:
print(pl._verts3d)
# (array([0, 1, 2]), array([ 0., 0., 0.]), [1, 2, 3])
print(pl._verts3d[2] is z)
# True
When Axes3D.plot hacks the z coordinates into the plot by calling mplot3d.art3d.line_2d_to_3d, the function grabs the existing x and y arrays from the 2d plot and just slaps the z coordinates next to them.
In other words, Axes.plot converts the input list y to an array, and at this step mutation of y doesn't affect the plot. The z input, on the other hand, is treated separately and it emerges unharmed after everything is done. This is how mutating both y and z ended up only changing z.
As a concluding remark, I looked around the matplotlib issue page, and found this relevant discussion for the 2d case. The resolution seems to be that it's by design that 2d plots don't copy their data, since this more often than not would add unnecessary overhead. I can also see how the 3d case is handled differently, and this leads to surprising behaviour down the line.
Anyway, I don't think it's reasonable to mutate the data passed to plotting methods. If you're doing this on purpose, use dedicated methods such as pl.set_xdata(). Then again this is not possible for the 3d plots (where x/ydata properties are reinterpreted to refer to different kinds of coordinates). So my suggestion is to not mutate the source arrays, or to manually pass a copy in case you want to mutate these later. Mutation can't be prohibited, but I can also see why the matplotlib developers don't want to copy every single input in every case. So the most probable solution i that the user shouldn't mutate their raw data. Something tells me that the person who wrote the code in the question didn't realize that they were mutating their inputs to begin with, which means that we're still to see a valid use case where input arrays are being mutated on purpose.
I want to use MatPlotLib to plot a graph, where the plot changes over time. At every time step, an additional data point will be added to the plot. However, there should only be one graph displayed, whose appearance evolves over time.
In my test example, the plot is a simple linear plot (y = x). Here is what I have tried:
for i in range(100):
x = range(i)
y = range(i)
plt.plot(x, y)
plt.ion()
plt.show()
time.sleep(1)
However, what happens here is that multiple windows are created, so that by the end of the loop I have 100 windows. Also, I have noticed that for the most recent window, it is just a white window, and the plot only appears on the next step.
So, my two questions are:
1) How can I change my code so that only a single window is displayed, whose contents changes over time?
2) How can I change my code so that for the most recent timestep, the plot is actually displayed on the window, rather than it only displaying a white window?
Thanks!
(1)
You can set plt.ion() at the beginning and plot all graphs to the same window. Within the loop use plt.draw() to show the graph and plt.pause(t) to make a pause. Note that t can be very small, but the command needs to be there for the animation to work on most backends.
You might want to clear the axes before plotting new content using plt.gca().cla().
import matplotlib.pyplot as plt
plt.ion()
for i in range(100):
x = range(i)
y = range(i)
# plt.gca().cla() # optionally clear axes
plt.plot(x, y)
plt.title(str(i))
plt.draw()
plt.pause(0.1)
plt.show(block=True) # block=True lets the window stay open at the end of the animation.
Alternatively to this very simple approach, use any of the examples for animations provided in http://matplotlib.org/examples/animation/index.html
(2)
In order to get each plot in a new window, use plt.figure() and remove plt.ion(). Also only show the windows at the end:
import matplotlib.pyplot as plt
for i in range(100):
x = range(i)
y = range(i)
plt.figure()
plt.plot(x, y)
plt.title(str(i))
plt.show()
Note that you might find that in both cases the first plot is empty simply because for i=0, range(i) == [] is an empty list without any points. Even for i=1 there is only one point being plotted, but of course no line can connect a single point with itself.
I think the best way is to create one line plot and then update data in it. Then you will have single window and single graph that will continuously update.
import matplotlib.pyplot as plt
plt.ion()
fig = plt.figure(figsize=(16,8))
axes = fig.add_subplot(111)
data_plot=plt.plot(0,0)
line, = axes.plot([],[])
for i in range(100):
x = range(i)
y = range(i)
line.set_ydata(y)
line.set_xdata(x)
if len(y)>0:
axes.set_ylim(min(y),max(y)+1) # +1 to avoid singular transformation warning
axes.set_xlim(min(x),max(x)+1)
plt.title(str(i))
plt.draw()
plt.pause(0.1)
plt.show(block=True)
When updating an "imshow" plot in matplotlib, it's best to use im.set_data, rather than using ax.imshow repeatedly in the loop. But what if the extent of the data is changing? Is it possible to update the extent of the data on each iteration of the loop?
Here is an example:
import numpy as np
import matplotlib.pyplot as plt
import time
ax = plt.subplot(111)
plt.ion()
plt.show()
count = 0
for size in np.linspace(1,3,10):
x = np.linspace(-size,size,100)
y = np.linspace(-size,size,100)
X,Y = np.meshgrid(x,y)
R = (X**2+Y**2)**0.5
Z = np.sin(R)/R
ext =(-size,size,-size,size)
if count == 0:
im = plt.imshow(Z,extent=ext)
else:
im.set_data(Z)
# Update the extent of the data
plt.draw()
plt.pause(0.5)
ax.set_xlim(-size,size)
ax.set_ylim(-size,size)
count += 1
plt.ioff()
plt.show()
The colored region should take up the entire axes if I could update the extent properly.
In your example, im.set_extent(ext).
More generally, though, almost any kwarg you can pass in to a matplotlib artist during initialization will have get_foo and set_foo methods. (That's actually how initialization works and how artist.set(...) and plt.setp works, as well.)
If you're looking for how to change a given property, the first place to look is a set_<name> method.
There are exceptions to this. For example, scatter returns a Collection, so you need to call set_offsets instead of set_xy to change the x, y data. Generally speaking, though, it's consistent.
I acquire some data in two arrays: one for the time, and one for the value. When I reach 1000 points, I trigger a signal and plot these points (x=time, y=value).
I need to keep on the same figure the previous plots, but only a reasonable number to avoid slowing down the process. For example, I would like to keep 10,000 points on my graph.
The matplotlib interactive plot works fine, but I don't know how to erase the first points and it slows my computer very quickly.
I looked into matplotlib.animation, but it only seems to repeat the same plot, and not really actualise it.
I'm really looking for a light solution, to avoid any slowing.
As I acquire for a very large amount of time, I erase the input data on every loop (the 1001st point is stored in the 1st row and so on).
Here is what I have for now, but it keeps all the points on the graph:
import matplotlib.pyplot as plt
def init_plot():
plt.ion()
plt.figure()
plt.title("Test d\'acqusition", fontsize=20)
plt.xlabel("Temps(s)", fontsize=20)
plt.ylabel("Tension (V)", fontsize=20)
plt.grid(True)
def continuous_plot(x, fx, x2, fx2):
plt.plot(x, fx, 'bo', markersize=1)
plt.plot(x2, fx2, 'ro', markersize=1)
plt.draw()
I call the init function once, and the continous_plot is in a process, called every time I have 1000 points in my array.
The lightest solution you may have is to replace the X and Y values of an existing plot. (Or the Y value only, if your X data does not change. A simple example:
import matplotlib.pyplot as plt
import numpy as np
import time
fig = plt.figure()
ax = fig.add_subplot(111)
# some X and Y data
x = np.arange(10000)
y = np.random.randn(10000)
li, = ax.plot(x, y)
# draw and show it
ax.relim()
ax.autoscale_view(True,True,True)
fig.canvas.draw()
plt.show(block=False)
# loop to update the data
while True:
try:
y[:-10] = y[10:]
y[-10:] = np.random.randn(10)
# set the new data
li.set_ydata(y)
fig.canvas.draw()
time.sleep(0.01)
except KeyboardInterrupt:
break
This solution is quite fast, as well. The maximum speed of the above code is 100 redraws per second (limited by the time.sleep), I get around 70-80, which means that one redraw takes around 4 ms. But YMMV depending on the backend, etc.
Use a fixed size array and plot that using matplot.
import collections
array = collections.deque([None] * 1000, maxlen=1000)
Whenver you append to the array it will remove the first element.
I know I'm late to answer this question, bt for your issue you could look into the "joystick" package. It is based on the line.set_data() and canvas.draw() methods, with optional axes re-scaling. It also allows for interactive text logging or image plotting (in addition to graph plotting).
No need to do your own loops in a separate thread, the package takes care of it, just give the update frequency you wish. Plus the console remains available for additional monitoring commands.
See http://www.github.com/ceyzeriat/joystick/ or https://pypi.python.org/pypi/joystick (use pip install joystick to install)
try:
import joystick as jk
import numpy as np
import time
class test(jk.Joystick):
# initialize the infinite loop decorator
_infinite_loop = jk.deco_infinite_loop()
def _init(self, *args, **kwargs):
"""
Function called at initialization, see the doc
"""
self._t0 = time.time() # initialize time
self.xdata = np.array([self._t0]) # time x-axis
self.ydata = np.array([0.0]) # fake data y-axis
# create a graph frame
self.mygraph = self.add_frame(jk.Graph(name="test", size=(500, 500), pos=(50, 50), fmt="go-", xnpts=10000, xnptsmax=10000, xylim=(None, None, 0, 1)))
#_infinite_loop(wait_time=0.2)
def _generate_data(self): # function looped every 0.2 second to read or produce data
"""
Loop starting with the simulation start, getting data and
pushing it to the graph every 0.2 seconds
"""
# concatenate data on the time x-axis
self.xdata = jk.core.add_datapoint(self.xdata, time.time(), xnptsmax=self.mygraph.xnptsmax)
# concatenate data on the fake data y-axis
self.ydata = jk.core.add_datapoint(self.ydata, np.random.random(), xnptsmax=self.mygraph.xnptsmax)
self.mygraph.set_xydata(t, self.ydata)
t = test()
t.start()
t.stop()
To be totally interactive, you could use Bokeh for this. Concretely you could use an update function that is called every X ms and stream the new data.
Here there is a fragment I use:
def update():
candle_data.stream(new_data, 300)
plot = figure(x_axis_type='datetime',x_range=(start_day, final_day), width=1500, height=900, title='Live Chart', sizing_mode='scale_both')
plot.segment(x0='time', y0='highest', x1='time', y1='lowest', color='black', source=candle_data)
plot.vbar(x='time', width = 0.5*60*60*50 ,bottom='open', top='close',fill_color='color', line_color='black', source = candle_data)
doc.add_root(column([plot]))
doc.add_periodic_callback(update, 20000)
doc.title = "Candle Data Live Rates"
I have a python script that reads in a data file and displays one figure with four plots using the matplotlib library. The data file is being updated every few seconds since it is an output file for a different piece of software that is running concurrently. I would like the four plots in my matplotlib figure to refresh themselves using the updated data file every 20 seconds. The way I've implemented this is as follows:
import pylab as pl
import time
pl.ion()
fig = pl.figure()
while True:
f = open('data.out', 'rb')
#code to parse data and plot four charts
ax = fig.add_subplot(2,2,1)
#...
ax = fig.add_subplot(2,2,4)
#...
pl.draw()
time.sleep(20)
This works, but I lose functionality of the zoom and pan buttons which normally work if pl.show() is called. This is not optimal. However, if pl.show() is substituted for pl.draw(), the script no longer updates the plots. Is there a way to dynamically update a plot without completely losing the zoom/pan functionality?
Your code is a little too vague to know what is going on.
I can offer this:
You should retain normal functionality if you create your subplots once, saving all the axes objects and then calling show().
Subsequent changes to those subplots could be done like this:
#inside while loop
for i in #subplotlist
ax[i].clear() #ax[i] is the axis object of the i'th subplot
ax[i].plot(#plotstuff)
ax[i].draw()
The toolbar for zooming and panning can be added by hand if you so desire.
As you are developping a sofware, I supposed you may have a multi-threaded approach.
So in this case using an infinite while loop is a bad idea, like you are holding up your main thread.
In addition when it comes to GUI it’s also a bad idea to interfere abruptly with GUI internal threads (wxPython for instance) and you should have an event driven design approach in order to not abruptly interrupt other threads (and that will cause the crash of your application).
The use of a timer will do the job.
A timer would do these actions in the following script :
1/ call a function to clear previous artist
2 / replot the data
3/ apply changes to canvas
4/ create another identical timer in the following design way : a timer who calls another identical timer after doing its job
Like I do not have access to your datas, I created a random data provider for the illustration.
The defined variable delay_repeat allows you to program in seconds the refresh.
import pylab as pl
import random
from threading import Timer
def dataprovider():
return [random.randint(0, 8) for i in range(8)]
def random_col():
return ['blue', 'red', 'green', 'orange'][random.randint(0,3)]
# .... #
fig = pl.figure()
axes = [fig.add_subplot(2,2,i) for i in range(1,5)]
paths = [ax.scatter(x=dataprovider(), y=dataprovider(), marker = '+', c=random_col()) for ax in axes]
# .... #
def clear_arts(paths, all_arts=-1):
if all_arts < 0:
all_arts = len(paths)
for path in paths[:all_arts]:
path.remove()
def refresh_arts(paths, delay_repeat):
# 1 - clear previous artists
clear_arts(paths,all_arts=-1)
# 2 - Get artists paths for cleaning
paths = [ax.scatter(x=dataprovider(), y=dataprovider(), marker = '+', c=random_col()) for ax in axes]
# 3 - Apply changes
fig.canvas.draw_idle()
# 4 - Create another timer
Timer(delay_repeat, refresh_arts, (paths, delay_repeat)).start()
# 4- Create a timer that will run function with arguments args and keyword arguments kwargs,
# after interval seconds have passed.
delay_repeat = 2
Timer(delay_repeat, refresh_arts, (paths, delay_repeat)).start()
# print("process continues here")
pl.show()
You can do it like this. It accept x,y as list and output a scatter plot plus a linear trend on the same plot.
from IPython.display import clear_output
from matplotlib import pyplot as plt
%matplotlib inline
def live_plot(x, y, figsize=(7,5), title=''):
clear_output(wait=True)
plt.figure(figsize=figsize)
plt.xlim(0, training_steps)
plt.ylim(0, 100)
x= [float(i) for i in x]
y= [float(i) for i in y]
if len(x) > 1:
plt.scatter(x,y, label='axis y', color='k')
m, b = np.polyfit(x, y, 1)
plt.plot(x, [x * m for x in x] + b)
plt.title(title)
plt.grid(True)
plt.xlabel('axis x')
plt.ylabel('axis y')
plt.show();
you just need to call live_plot(x, y) inside a loop. here's how it looks: