Updating pyplot maker's position on tkinter button click - python

I am struggling to figure out a way to plot a graph in which values of x axis and y axis comes from two tkinter spinbox where the range of spinbox for the x axis is 125 to 8000 and y axis is -10 to 125, it plots from one point in the graph to the other when a tkinter button is pressed based on the values provided by the spinboxes.
The sample code is:
from tkinter import *
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
from matplotlib.figure import Figure
import numpy as np
import collections
class PlotClass():
def __init__(self):
fig = Figure(figsize=(5,5),dpi=70,facecolor='cyan')
ax = fig.subplots()
ax.set_xlim(100,9000)
ax.set_ylim(130,-10)
x = [125,250,500,1000,2000,4000,8000]
ticks = [125,250,500,"1K","2K","4K","8K"]
xm = [750,1500,3000,6000]
ax.set_xscale('log', basex=2)
ax.set_xticks(x)
ax.set_xticks(xm, minor=True)
ax.set_xticklabels(ticks)
ax.set_xticklabels([""]*len(xm), minor=True)
ax.yaxis.set_ticks([120,110,100,90,80,70,60,50,40,30,20,10,0,-10])
self.line2,= ax.plot([],[],'-o',markersize=15.0,mew=2)
ax.grid(color="grey")
ax.grid(axis="x", which='minor',color="grey", linestyle="--")
self.canvas = canvas = FigureCanvasTkAgg(fig, master=master)
canvas.show()
canvas.get_tk_widget().grid(column=0,row=2,columnspan=3,rowspan=15)
self.spin = Spinbox(master, from_=125,to=8000,command=self.action)
self.spin.grid(column=5,row=2)
self.spin2 = Spinbox(master, from_=-10,to=125,command=self.action)
self.spin2.grid(column=5,row=3)
self.button = Button(master, text="plot here",command=self.plot)
self.button.grid(column=5,row=4)
def linecreate(self, x=1000,y=20):
X,Y = self.line2.get_data()
if x in X:
ch = list(X)
counti = ch.count(x)
Y[counti] = y
print("Working")
print(Y)
self.canvas.draw_idle()
else:
X = np.append(X,[x])
Y = np.append(Y,[y])
self.line2.set_data(X,Y)
self.canvas.draw_idle()
def plot(self):
self.linecreate(float(self.spin.get()),float(self.spin2.get()))
master = Tk()
plotter = PlotClass()
plotter.ok(125,10)
master.mainloop()
Now the problem is, it needs to check if a particular marker is already in the same x axis, it generally plots a new line to where a new value is added, but I need a way such that when the value of the x axis is 125 and it has already been plotted once to any value like 50 in the y axis, and then it went to plot 500 in x and 90 in y, and finally it again tries to plot 125 in x and 20 in y, the graph creates a marker, but I need to just redraw the old plot with the new Y value without creating a new marker.
In the above code I tried If x in X: to check if the x axis already have been plotted, even though I could check that but I cannot replace the Y axis value with the new value and redraw it.

You forgot to update the plot with self.line2.set_data(X,Y) in case the value already exists.
def linecreate(self, x=1000,y=20):
X,Y = self.line2.get_data()
if x in X:
ch = list(X)
counti = ch.index(x)
Y[counti] = y
else:
X = np.append(X,[x])
Y = np.append(Y,[y])
self.line2.set_data(X,Y)
self.canvas.draw_idle()

Related

Pyplot: Control texts clip in one direction

I am trying to change the plot window extents on an annotated plot to "zoom" into a certain window of interest. My text annotations fall outside of the plot window. If I use clip_on = True then all the text is hidden, but I just want to trim the text outside of the x-axis.
import matplotlib.pyplot as plt
x = [0,2,4,6,8,10]
y = [3,2,5,9,6,7]
plt.plot(x,y)
for i in range(len(x)):
plt.text(x[i],11, '%d' %y[i])
plt.axis([0,5,0,10])
Full data:
Reduced window:
Desired output:
This isn't fancy at all, but it works for x_max 1 to 10:
import matplotlib.pyplot as plt
x = [0,2,4,6,8,10]
y = [3,2,5,9,6,7]
x_max = 5
plot_axis = [0,x_max,0,10]
if plot_axis[1] == x[-1:][0]:
range_set = range(len(x))
else:
try:
x_idx = x.index(plot_axis[1])
except:
x_idx = x.index(plot_axis[1]-1)
range_set = range(x_idx+1)
plt.plot(x,y)
for i in range_set:
plt.text(x[i],11, '%d' %y[i])
plt.axis(plot_axis)
plt.show()
Note: There isn't a sanity check for x_max = 0 or >10 implemented, but from your plt.axis([0,5,0,10]) it seems you had this in manually in check anyway.

Python - figure settings in plotting loop

Hi
I have a little problem, I made a loop which creates 3 plots in every iteration and set text
on x label to be rotated but it works only for last fig in a row. I am not sure how to affect first and second figure.
def multi_scatter(x_list, y):
sns.set(style='whitegrid', rc={"grid.linewidth": 0.2})
sns.set_context("paper", font_scale=2)
for x in range(0, len(x_list)):
if x == 0 or x % 3:
chart = sns.pairplot(data=ds_train,
y_vars=[y],
x_vars=[x_list[x], x_list[x+1], x_list[x+2]],
height = 10)
plt.xticks(rotation = 45)
plt.show()
else:
continue
Thank You in advance
This is becuase you defined chart but never extract the axes from chart. You need to specify what the axes are in order to set xticklabels. Try to add these lines in your code (see the inner for loop):
def multi_scatter(x_list, y):
sns.set(style='whitegrid', rc={"grid.linewidth": 0.2})
sns.set_context("paper", font_scale=2)
for x in range(0, len(x_list)):
if x == 0 or x % 3:
chart = sns.pairplot(data=ds_train,
y_vars=[y],
x_vars=[x_list[x], x_list[x+1], x_list[x+2]],
height = 10)
for ax in chart.axes.flat:
ax.tick_params(axis='x', labelrotation=45 )
else:
continue
I did not test it without access to your data, so please let me know if it works!

Python Bokeh: Restart X axis to 0 on Zoom

I have code below that creates a simple line x-y plot.
When I zoom in, I want the x-axis ticker to start at 0 again instead of 3.9/whatever the x point of the zoom was as in the image.
No Zoom:
After Zooming:
How do I do that?
Code:
from bokeh.io import output_file, show, save
from bokeh.layouts import column
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource
data = []
x = list(range(11))
y0 = x
y1 = [10 - xx for xx in x]
y2 = [abs(xx - 5) for xx in x]
source = ColumnDataSource(data=dict(x=x, y0=y0, y1=y1, y2=y2))
for i in range(3):
p = figure(title="Title " + str(i), plot_width=300, plot_height=300)
if len(data):
p.x_range = data[0].x_range
p.y_range = data[0].y_range
p.circle('x', 'y0', size=10, color="navy", alpha=0.5, legend_label='line1', source=source)
p.legend.location = 'top_right'
p.legend.click_policy = "hide"
data.append(p)
plot_col = column(data)
# show the results
show(plot_col)
This is an unusual requirement, and none of the built-in things behave this way. If you zoom in to the interval [4,7], the the range will be updated [4, 7], and so then the axis will display labels for [4, 7]. If it will suffice to simply display different tick labels, even while the underlying range start/end remain their usual values, then you could use a Custom Extension to generate whatever customized labels you want. There is an example in the User's Guide that already does almost exactly what you want already:
https://docs.bokeh.org/en/latest/docs/user_guide/extensions_gallery/ticking.html#userguide-extensions-examples-ticking
You might also be able to do something even more simply with a FuncTickFormatter, e.g. (untested)
p.xaxis.formatter = FuncTickFormatter(code="""
return tick - ticks[0]
""")

Python: Animated 3D Scatterplot gets slow

My program plots the positions of particles in my file for every time step. Unfortunately it gets slower and slower although I used matplotlib.animation. Where is the bottleneck?
My data file for two particles looks like the following:
# x y z
# t1 1 2 4
# 4 1 3
# t2 4 0 4
# 3 2 9
# t3 ...
My script:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import mpl_toolkits.mplot3d.axes3d as p3
import matplotlib.animation as animation
# Number of particles
numP = 2
# Dimensions
DIM = 3
timesteps = 2000
with open('//home//data.dat', 'r') as fp:
particleData = []
for line in fp:
line = line.split()
particleData.append(line)
x = [float(item[0]) for item in particleData]
y = [float(item[1]) for item in particleData]
z = [float(item[2]) for item in particleData]
# Attaching 3D axis to the figure
fig = plt.figure()
ax = p3.Axes3D(fig)
# Setting the axes properties
border = 1
ax.set_xlim3d([-border, border])
ax.set_ylim3d([-border, border])
ax.set_zlim3d([-border, border])
def animate(i):
global x, y, z, numP
#ax.clear()
ax.set_xlim3d([-border, border])
ax.set_ylim3d([-border, border])
ax.set_zlim3d([-border, border])
idx0 = i*numP
idx1 = numP*(i+1)
ax.scatter(x[idx0:idx1],y[idx0:idx1],z[idx0:idx1])
ani = animation.FuncAnimation(fig, animate, frames=timesteps, interval=1, blit=False, repeat=False)
plt.show()
I would suggest to use pyqtgraph in this case. Citation from the docs:
Its primary goals are 1) to provide fast, interactive graphics for
displaying data (plots, video, etc.) and 2) to provide tools to aid in
rapid application development (for example, property trees such as
used in Qt Designer).
You can check out some examples after the installation:
import pyqtgraph.examples
pyqtgraph.examples.run()
This small code snippet generates 1000 random points and displays them in a 3D scatter plot by constantly updating the opacity, similar to the 3D scatter plot example in pyqtgraph.examples:
from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph.opengl as gl
import numpy as np
app = QtGui.QApplication([])
w = gl.GLViewWidget()
w.show()
g = gl.GLGridItem()
w.addItem(g)
#generate random points from -10 to 10, z-axis positive
pos = np.random.randint(-10,10,size=(1000,3))
pos[:,2] = np.abs(pos[:,2])
sp2 = gl.GLScatterPlotItem(pos=pos)
w.addItem(sp2)
#generate a color opacity gradient
color = np.zeros((pos.shape[0],4), dtype=np.float32)
color[:,0] = 1
color[:,1] = 0
color[:,2] = 0.5
color[0:100,3] = np.arange(0,100)/100.
def update():
## update volume colors
global color
color = np.roll(color,1, axis=0)
sp2.setData(color=color)
t = QtCore.QTimer()
t.timeout.connect(update)
t.start(50)
## Start Qt event loop unless running in interactive mode.
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, PYQT_VERSION'):
QtGui.QApplication.instance().exec_()
Small gif to give you an idea of the performance:
EDIT:
Displaying multiple points at every single time step is a little bit tricky since the gl.GLScatterPlotItem takes only (N,3)-arrays as point locations, see here. You could try to make a dictionary of ScatterPlotItems where each of them includes all time steps for a specific point. Then one would need to adapt the update function accordingly. You can find an example below where pos is an (100,10,3)-array representing 100 time steps for each point. I reduced the update time to 1000 ms for a slower animation.
from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph.opengl as gl
import numpy as np
app = QtGui.QApplication([])
w = gl.GLViewWidget()
w.show()
g = gl.GLGridItem()
w.addItem(g)
pos = np.random.randint(-10,10,size=(100,10,3))
pos[:,:,2] = np.abs(pos[:,:,2])
ScatterPlotItems = {}
for point in np.arange(10):
ScatterPlotItems[point] = gl.GLScatterPlotItem(pos=pos[:,point,:])
w.addItem(ScatterPlotItems[point])
color = np.zeros((pos.shape[0],10,4), dtype=np.float32)
color[:,:,0] = 1
color[:,:,1] = 0
color[:,:,2] = 0.5
color[0:5,:,3] = np.tile(np.arange(1,6)/5., (10,1)).T
def update():
## update volume colors
global color
for point in np.arange(10):
ScatterPlotItems[point].setData(color=color[:,point,:])
color = np.roll(color,1, axis=0)
t = QtCore.QTimer()
t.timeout.connect(update)
t.start(1000)
## Start Qt event loop unless running in interactive mode.
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()
Keep in mind that in this examples, all points are shown in the scatter plot, however, the color opacity (4th dimension in the color array) is updated in every time step to get an animation. You could also try to update the points instead of the color to get better performance...
I would guess your bottleneck is calling ax.scatter and ax.set_xlim3d and similar in every frame in the animation.
Ideally, you should make a call to scatter once, then use the object returned by scatter and its set_... properties in the animate function (more details here).
I can't figure out how to do it with scatter, but if you use ax.plot(x, y, z, 'o') instead, you can then follow the demo method here.
Using some random data for x, y, z. It would work like this
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import mpl_toolkits.mplot3d.axes3d as p3
import matplotlib.animation as animation
from numpy.random import random
# Number of particles
numP = 2
# Dimensions
DIM = 3
timesteps = 2000
x, y, z = random(timesteps), random(timesteps), random(timesteps)
# Attaching 3D axis to the figure
fig = plt.figure()
ax = p3.Axes3D(fig)
# Setting the axes properties
border = 1
ax.set_xlim3d([-border, border])
ax.set_ylim3d([-border, border])
ax.set_zlim3d([-border, border])
line = ax.plot(x[:1], y[:1], z[:1], 'o')[0]
def animate(i):
global x, y, z, numP
idx1 = numP*(i+1)
# join x and y into single 2 x N array
xy_data = np.c_[x[:idx1], y[:idx1]].T
line.set_data(xy_data)
line.set_3d_properties(z[:idx1])
ani = animation.FuncAnimation(fig, animate, frames=timesteps, interval=1, blit=False, repeat=False)
plt.show()

Matplotlib: determining when mouse is in axis text rectangle

So it's somewhat well known that in matplotlib zoom, pressing 'x' or 'y' when zooming will zoom on only the x or y axis. I would like to modify this slightly by subclassing the NavigationToolbar2 in backend_bases.py
Specifically, I would like to have the functionality that if the mouse is in the region on the canvas below a plot (or above, depending on where I have put my axes), or to the left or right of the plot, to choose to zoom with _zoom_mode = 'x' or 'y'. (In addition to keeping the default zoom to rect functionality when the mouse is inside the plot.)
Looking at backend_bases, it appears the method I need to modify is press_zoom
def press_zoom(self, event):
if event.button=1:
self._button_pressed=1
elif event.button == 3:
self._button_pressed=3
else:
self._button_pressed=None
return
x, y = event.x, event.y
# push the current view to define home if stack is empty
if self._views.empty(): self.push_current()
self._xypress=[]
for i, a in enumerate(self.canvas.figure.get_axes()):
if (x is not None and y is not None and a.in_axes(event) and
a.get_navigate() and a.can_zoom()) :
self._xypress.append(( x, y, a, i, a.viewLim.frozen(),
a.transData.frozen() ))
id1 = self.canvas.mpl_connect('motion_notify_event', self.drag_zoom)
id2 = self.canvas.mpl_connect('key_press_event',
self._switch_on_zoom_mode)
id3 = self.canvas.mpl_connect('key_release_event',
self._switch_off_zoom_mode)
self._ids_zoom = id1, id2, id3
self._zoom_mode = event.key
self.press(event)
and add an elif to the big if statement there and use it to set the zoom mode there, but what I cannot figure out (yet), is how to tell if the mouse is in the region appropriate for 'x' or 'y' zoom mode.
So, how can I tell when the mouse is just outside of a plot?
By converting the x and y coordinate into Axes coordinates. You can tell if they're just outside the axes, if they're less than 0 or larger than 1.
Here is a simple example of how it could work.
def is_just_outside(fig, event):
x,y = event.x, event.y
print x, y
for ax in fig.axes:
xAxes, yAxes = ax.transAxes.inverted().transform([x, y])
print xAxes, yAxes
if (-0.02 < xAxes < 0) | (1 < xAxes < 1.02):
print "just outside x-axis"
if (-0.02 < yAxes < 0) | (1 < yAxes < 1.02):
print "just outside y-axis"
x = np.linspace(-np.pi,np.pi,100)
y = np.sin(x)
fig = plt.figure()
plt.plot(x,y)
ax = fig.add_subplot(111)
fig.canvas.mpl_connect('button_press_event', lambda e: is_just_outside(fig, e))
plt.show()

Categories

Resources