My program (developed with GTK using glade) receives some data and has the option to display a seperate window containing a matplotlib scatterplot that represents the data.
My problem is that if the user closes the graph window and reopens it, no graph is displayed. It is just a blank GTK Window. I'm sure there is a simple fix, but there aren't many resources available that are relevant to my issue (or GTK and matlplotlib integration for that matter).
I have created a Module for my scatterplot so I can easily reuse it. I am just trying to get it to work, so the code isn't structured perfectly.
##Scatterplot Module:
import gtk
import matplotlib
from matplotlib.backends.backend_gtkagg import FigureCanvasGTKAgg as FigureCanvas
from matplotlib.figure import Figure
class ScatterPlot:
def __init__(self):
self.window = gtk.Window()
self.window.connect("destroy", lambda x: self.destroy())
self.window.set_default_size(500,400)
self.is_hidden = False
self.figure = Figure(figsize = (5,4), dpi=100)
self.ax = self.figure
self.ax = self.ax.add_subplot(111)
self.canvas = FigureCanvas(self.figure)
self.window.add(self.canvas)
self.Xs = list()
self.Ys = list()
def set_axis(self, xLimit = (0,384) , yLimit = (0,100)):
self.ax.set_xlim(xLimit)
self.ax.set_ylim(yLimit)
def plot(self, xs, ys):
self.Xs.extend([xs])
self.Ys.extend([ys])
self.ax.plot(xs,ys,'bo')
def update(self):
self.window.add(self.canvas)
def set_title(self, title):
self.ax.set_title(title)
def show(self):
self.window.show_all()
self.is_hidden = False
def hide(self):
self.window.hide()
self.is_hidden = True
def destroy(self):
self.window.destroy()
I call the module like so:
class GUI:
def __init__(self):
self.scatterplot = scatterplot.ScatterPlot()
#When the user presses the "Graph" button it calls the following function
def graph():
self.scatterplot.plot(someDataX, someDataY)
self.scatterplot.set_axis()
self.scatterplot.set_title("Some Title")
self.scatterplot.show()
(This was just an example of what my code looks like.)
When the scatterplot is closed, I am calling self.window.destroy instead of self.window.hide. When reopening is attempted, I call the same graph() function but, as stated above, the GTK Window does not display the graph. (When I first open it, it displays perfectly)
My speculations:
Should I be calling .hide() instead of .destroy()?
Is there a piece of code in scatterplot's constructor that needs to be called again to create the plot?
Or should I just re-instantiate the plot every time graph() is called?
My Solution:
From:
class ScatterPlot:
def __init__(self):
#remove the following two lines
self.canvas = FigureCanvas(self.figure)
self.window.add(self.canvas)
Move the two lines of code to show()
def show(self):
self.canvas = FigureCanvas(self.figure)
self.window.add(self.canvas)
self.window.show_all()
self.is_hidden = False
Moving these two lines of code allows the graph to be displayed when re-opening the window.
Sidenote: Calling both .destroy() or .show() when closing the window will work. I'm not sure which one is better though.
Related
Thanks for the help in advance.
I'm trying to update a matplotlib imshow plot when a slider is moved (Python 3.7.4), but nothing is changing when the update function is called, despite calling canvas.draw which I thought would be all I needed to solve the problem. The code also needs to be embedded in Tkinter. This code will reproduce the problem:
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
class mainApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.image = np.zeros((10, 10))
fig = Figure()
self.mainAx = fig.add_subplot(111)
self.drawing = self.mainAx.imshow(self.image)
self.graphCanvas = FigureCanvasTkAgg(fig, master=self)
self.graphCanvas.draw()
self.graphCanvas.get_tk_widget().pack(side="top", fill="both", expand=True)
self.slider = tk.Scale(master=self, command=self.updateGraph, orient="horizontal")
self.slider.pack(fill="x")
def updateGraph(self, e):
self.image = np.zeros((10, 10))
self.image[self.slider.get()//10, self.slider.get()%10] = 1
self.drawing.set_data(self.image)
self.graphCanvas.draw_idle()
main = mainApp()
main.mainloop()
Calling mainAx.imshow(self.image) works but this is far slower and I would like this program to be as fast as possible. I think the issue lies with the draw_idle but I don't know what else I should be doing. It also doesn't work with the regular canvas.draw() function
Thanks,
Adam
change your update function to this.
def updateGraph(self, e):
self.image = np.zeros((10, 10))
self.image[self.slider.get()//10, self.slider.get()%10] = 1
self.drawing = self.mainAx.imshow(self.image)
self.graphCanvas.draw()
I would also modify your command to wait until a final value is chosen on the slider, unless you want to see it change, you can find how to do this here with an event binding or a delay. TkInter, slider: how to trigger the event only when the iteraction is complete?
I did the following example code just to test how to integrate an animated matplotlib plot with pygtk. However, I get some unexpected behaviors when I run it.
First, when I run my program and click on the button (referred to as button1 in the code), there is another external blank window which shows up and the animated plot starts only after closing this window.
Secondly, when I click on the button many times, it seems that there is more animations which are created on top of each other (which gives the impression that the animated plot speeds up). I have tried to call animation.FuncAnimation inside a thread (as you can see in the comment at end of the function on_button1_clicked), but the problem still the same.
Thirdly, is it a good practice to call animation.FuncAnimation in a thread to allow the user to use the other functions of the gui ? Or should I rather create a thread inside the method animate (I guess this will create too many threads quickly) ? I am not sure how to proceed.
Here is my code:
import gtk
from random import random
import numpy as np
from multiprocessing.pool import ThreadPool
import matplotlib.pyplot as plt
import matplotlib.animation as animation
#from matplotlib.backends.backend_gtk import FigureCanvasGTK as FigureCanvas
from matplotlib.backends.backend_gtkagg import FigureCanvasGTKAgg as FigureCanvas
#from matplotlib.backends.backend_gtkcairo import FigureCanvasGTKCairo as FigureCanvas
class HelloWorld:
def __init__(self):
interface = gtk.Builder()
interface.add_from_file('interface.glade')
self.dialog1 = interface.get_object("dialog1")
self.label1 = interface.get_object("label1")
self.entry1 = interface.get_object("entry1")
self.button1 = interface.get_object("button1")
self.hbox1 = interface.get_object("hbox1")
self.fig, self.ax = plt.subplots()
self.X = [random() for x in range(10)]
self.Y = [random() for x in range(10)]
self.line, = self.ax.plot(self.X, self.Y)
self.canvas = FigureCanvas(self.fig)
# self.hbox1.add(self.canvas)
self.hbox1.pack_start(self.canvas)
interface.connect_signals(self)
self.dialog1.show_all()
def gtk_widget_destroy(self, widget):
gtk.main_quit()
def on_button1_clicked(self, widget):
name = self.entry1.get_text()
self.label1.set_text("Hello " + name)
self.ani = animation.FuncAnimation(self.fig, self.animate, np.arange(1, 200), init_func=self.init, interval=25, blit=True)
'''
pool = ThreadPool(processes=1)
async_result = pool.apply_async(animation.FuncAnimation, args=(self.fig, self.animate, np.arange(1, 200)), kwds={'init_func':self.init, 'interval':25, 'blit':True} )
self.ani = async_result.get()
'''
plt.show()
def animate(self, i):
# Read XX and YY from a file or whateve
XX = [random() for x in range(10)] # just as an example
YY = [random() for x in range(10)] # just as an example
self.line.set_xdata( XX )
self.line.set_ydata( YY )
return self.line,
def init(self):
self.line.set_ydata(np.ma.array(self.X, mask=True))
return self.line,
if __name__ == "__main__":
HelloWorld()
gtk.main()
I want to connect a QPushButton in my QMainWindow with a class that i created using Matplotlib so i can show a grid when i push the button. This is a part of the code:
class Ventana(QMainWindow):
def __init__(self):
super(QMainWindow, self).__init__()
self.channel = ViewWidget()
#I add a toolbar and i put the button in here
self.toolbar2.addWidget(self.btn_showgrid)
self.btn_showgrid.setEnabled(True)
self.connect(self.btn_showgrid, SIGNAL("clicked()"), self.showGrid)
def showGrid(self):
self.btn_showgrid.setEnabled(False)
self.channel.axes.grid(True)
class ViewWidget(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.mainWidget = QWidget()
self.setCentralWidget(self.mainWidget)
layout = QVBoxLayout()
self.mainWidget.setLayout(layout)
self.figure_canvas = FigureCanvas(Figure())
layout.addWidget(self.figure_canvas, 10)
self.axes = self.figure_canvas.figure.add_subplot(111)
x = np.arange(0,5,0.1)
y = np.sin(x)
self.axes.plot(x,y)
My method called showGrid set the button to "disable" (is what i want to when the button is pushed) but it does not shows the grid. What am i doing wrong?
Hope you can help me. Thanks for your answers
------------------------- EDIT --------------------------------
I´ve made a few changes. I created the QPushButton and i added it to the toolbar of the plot.
# create a simple widget to extend the navigation toolbar
self.btn_showgrid = QPushButton("Show Grid")
self.btn_showgrid.setEnabled(True)
self.btn_hidegrid = QPushButton("Hide Grid")
self.btn_hidegrid.setEnabled(False)
layout = QVBoxLayout()
self.mainWidget.setLayout(layout)
self.figure_canvas = FigureCanvas(Figure())
layout.addWidget(self.figure_canvas, 10)
self.axes = self.figure_canvas.figure.add_subplot(111)
self.axes.grid(False)
x = np.arange(0,5,0.1)
y = np.sin(x)
self.axes.plot(x,y)
I also put a line: self.axes.grid(False) as you can see above. And at last i created this method:
def showGrid(self):
self.btn_showgrid.setEnabled(False)
self.btn_hidegrid.setEnabled(True)
self.axes.grid(True)
self.axes.draw()
The problem now is that when i push the button, it only hides like it should, but the grid does no show. But if create a new plot in the same `QMainWindow, it works!!!!
I think i need to refresh the plot at the moment i make self.axes.grid(True), but the draw() does not work. How can i accomplish this? I mean, refresh the plot?
You need to tell the canvas to re-draw. Drawing can be expensive so updating the state of the artists does not trigger a re-draw (the pyplot API does do that but you should not use that here). I think
def showGrid(self):
self.btn_showgrid.setEnabled(False)
self.channel.axes.grid(True)
self.channel.canvas.draw_idle()
will do the trick. The call to draw_idle tells Qt to, the next time it re-paints window to also trigger an Agg redraw for mpl, and to please schedule a repaint 'soon'.
I try to embed matplotlib into PyQT GUI made in Qt designer. In .ui on a widget tab I defined:
GraphWidget, that is a QWidget
and
Graph1Layout, that is a QVBoxLayout.
The code is below. I took some parts of the code from other application written by other person (class ParentCanvas and class PlotCanvas) so I do not completely understand what is going on there, but as far as I googled, that is the almost standard way to include matplotlib widget in GUI. I shortened my existing code and existing GUI to do a little example.
What is intended: I want to plot some points whenever I click run and delete the graph when I click stop (I tried just to clear the points from graph but without success).
Is this correct implementation?
Maybe somebody can make it more simple or better?
Is there a better way to delete widget?
How to delete only points on the plot?
I am a beginner in OOP and python in general. Any other comments for improving the code are welcome.
from __future__ import division
import sys,os
from PyQt4 import QtGui, QtCore, uic # Used to create and modify PyQt4 Objects.
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas # Used in creating the plots and graphs.
from matplotlib.figure import Figure
class ParentCanvas(FigureCanvas):
"""Ultimately, this is a QWidget (as well as a FigureCanvasAgg, etc.)."""
def __init__(self, parent=None, width=5, height=4, dpi=100):
fig = Figure(figsize=(width, height), dpi=dpi)
fig.set_edgecolor('w')
fig.set_facecolor('w')
self.axes = fig.add_subplot(111)
self.axes.hold(False)
FigureCanvas.__init__(self, fig)
self.setParent(parent)
FigureCanvas.setSizePolicy(self, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
FigureCanvas.updateGeometry(self)
class PlotCanvas(ParentCanvas):
""" Creates a matplotlib canvas QWidget inheriting the features set out in ParentCanvas."""
def compute_initial_figure(self):
self.axes.set_xbound(lower=0, upper=1)
self.axes.set_ybound(lower=0, upper=1)
self.axes.set_ylabel('Best Fitness Function')
self.axes.set_xlabel('Population')
class Main(QtGui.QMainWindow):
def __init__(self, parent=None):
super(Main, self).__init__(parent)
uic.loadUi('Test.ui', self)
self.SignalsAndSlots()
self.firstrun = True
self.iter=0
def SignalsAndSlots(self): # Function setting up the signals and slots.
self.btnStopOpt.clicked.connect(self.clickStop)
self.btnRun.clicked.connect(self.clickRun)
def clickStop(self): # Function that runs when the "Stop" button is clicked.
self.firstrun = True
self.iter =0
self.gb.setParent(None)
def clickRun(self): # Function that runs when the "Run Optimization" button is clicked.
if self.firstrun == True :
#graph setup
self.gb = PlotCanvas(self.Graph1Widget, width=10, height=7, dpi=70)
self.gb.axes.set_ylabel('Objective Value')
self.gb.axes.set_xlabel('Generation')
self.Graph1Layout.addWidget(self.gb)
self.firstrun = False
self.UpdateGraph(self.iter)
self.iter+=1
def UpdateGraph(self, iter): # Function that creates a graph
best = [0,0,0,3,5,9,12,30]
average = [0,0,0,1,2,3,14,20]
t = range(iter+1) # Will be used to store the generation numbers.
s = best[:iter+1] # Will be used to store the global best values.
self.gb.axes.hold(True) # Holds the graph so we can plot the global best and average scores as two separate scatter plots on to one graph.
self.gb.axes.scatter(t, s, c='b', label='Best objective value')
if iter == 0: # In the first generation we need to make a legend for the graph, this only need to be made once.
self.gb.axes.legend(loc='upper center', bbox_to_anchor=(0.5, 1.09), fancybox=True, shadow=True, ncol=5) # Legend content is determined by the labels above. Location is determined by bbox_to_anchor.
self.gb.axes.autoscale_view(True,False,True)
self.gb.draw()
return
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
window = Main(None) # instantiation
app.setActiveWindow(window)
window.show() # show window
sys.exit(app.exec_()) # Exit from Python
# when the code works but it's meaningless to include it
### When I can't get this part to work and I'd need your code
How do you hide or show an Axes object (subplot) in matplotlib so you can toggle between different Axes in the same figure?
I'm using matplotlib to display graphics in a Tkinter GUI and I'd like to use radiobuttons to switch between different axes in the same figure.
Basically I'll have some radiobuttons linked to a IntVar():
graphic_version = tk.IntVar()
tk.Radiobutton(root, text='Option1', variable=graphic_version, value=1).pack()
tk.Radiobutton(root, text='Option2', variable=graphic_version, value=2).pack()
Then I'd trace the IntVar() with a custom methods updating my figure with the requested graphic:
choice.trace("w", lambda choice: myGraphic.showGraphic(version))
so that everytime the user clicks a radiobutton the figure is updated with a different version of the plot. Now the problem is I have no idea how to do the showGraphic properly. Lets say I use this class system to get 2 different versions of plotting the same data:
class Version1():
def __init__(self, ax, data):
self.ax = ax #This is a Axes object
self.data = self._formatDataV1(data)
self._draw()
self._setOptions()
self.hide()
def _formatDataV1(self, data):
#Here I manipulate the raw data to extract the info I need for this version
#Just a bunch of math algorithms it works fine
def _setOptions(self):
#Here I can overwrite or change settings specific for this version
def _draw(self):
self.ax.bar(self.data[0], self.data[1], width=1, color='red')
self._setOptions()
def hide(self):
###How do I remove the ax without affecting the figure?
def show(self):
###If I want to see this version again I don't want the cost of redrawing
class Version2():
def __init__(self, ax, data):
self.ax = ax #This is a Axes object
self.data = self._formatDataV1(data)
self._draw()
self._setOptions()
self.hide()
def _formatDataV2(self, data):
#The data is manipulated differently here to extract new information
def _setOptions(self):
#These options are specific to the version2 to get the display right
def _draw(self): #Drawing a different version of the graphic with differently formated data
self.ax.plot(self.data[0], self.data[1])
self._setOptions()
def hide(self):
###How do I remove the ax without affecting the figure?
def show(self):
###If I want to see this version again I don't want the cost of redrawing
class MyGraphic(tk.LabelFrame):
def __init__(self, root, data, **options):
#I use the labelframe only as a container to make things pretty
tk.LabelFrame.__init__(self, root, text="My 1337 graphic : ", **options)
self.data = data
self.fig = mpl.figure.Figure()
self.ax = self.fig.add_subplot(111)
self._drawCanvas() #This is just for Tkinter compatibility
self.my_versions = {}
self.my_versions.update({'v1' : Version1(self.ax, self.data)})
self.my_versions.update({'v2' : Version2(self.ax, self.data)})
def _drawCanvas(self):
self.canvas = FigureCanvasTkAgg(self.figure, master=self)
self.canvas.show()
self.canvas.get_tk_widget().grid()
self.canvas._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
def _setOptions(self, **kwargs):
#Here I can set options common to both versions of the graphic
def showGraphic(self, graphic_version):
for i, item in enumerate(self.my_versions):
item.hide()
if graphic_version == 1:
self.my_versions['v1'].show()
elif graphic_version == 2:
self.my_versions['v2'].show()
self._setOptions()
Sorry for the lengthy post but I rather include too many details and edit out those who are not necessary when it's solved.
Basically I want to be able to hide and show different ax on the same figure depending on the choice made by my user. The missing parts of the puzzle are myGraphic.show() and myGraphic.hide().
I'm also a complete matplotlib newby I tried this design because it seemed clear and easy to implement additional versions when needed but design inputs are also really appreciated.
You can remove axes from a figure with figure.delaxes (and add with add_axes):
http://matplotlib.org/api/figure_api.html#matplotlib.figure.Figure.delaxes
I managed to solve it with figure.clear() and figure.add_axes(ax)
I'll try to edit a clean question-answer tomorrow when I have time with a minimal example of how to toggle different version of a plot on the same figure in Tkinter.