I am working on a matplotlib figure embedded in a tkinter gui in Python.
First a FigureCanvasTkAgg is created, which includes a previously created object, which contains a matplotlib figure and then it is drawn. This part works perfectly fine.
Afterwards I want to refresh the canvas / its content based on an user action. If the method to refresh the canvas is called, the figure object is refreshed and the canvas is redrawn.
This works as well, the figure is updated, but for some strange reason, the canvas resizes, so it shrinks to about one quarter of its original size. If I print the size of the canvas, I can see that its size changed.
I have tried to resize the canvas back to its original size after the refreshment, without success.
I would be thankful for any input. I have added a shortend version of my code.
#canvas object which is displayed on the gui
class Canvas:
def __init__(self, parent):
#the map_figure object is create
self.map_figure = map_figure()
#the matplotlib figure is extracted from the map_figure
self.figure = self.map_figure.get_figure()
#the canvas is created using the figure and the parent
self.canvas = FigureCanvasTkAgg(self.figure, master=parent)
#I tried both, to manually set the canvas size to the window dimensions (which are
#described by w and h and using the .pack() parameters, both are working fine
self.canvas.get_tk_widget().config(width=w,height=h)
#self.canvas.get_tk_widget().pack(fill='both', expand=True)
self.canvas.get_tk_widget().pack()
#canvas is drawn
self.canvas.draw()
#its size is printed
print(self.get_size())
def refresh(self, parent, point):
#the map_figure of the canvas is refresh
self.map_figure.refresh(data)
#the matplotlib figure is extracted again
self.canvas.figure = self.map_figure.get_figure()
#canvas is redrawn
self.canvas.draw()
#the canvas size is now different for some reason even after calling
#self.canvas.get_tk_widget().config(width=w,height=h) before this again
print(self.canvas.get_width_height())
#the map_figure class if relevant
class map_figure:
def __init__(self, data):
self.figure = self.create_figure(data)
def get_figure(self):
return self.figure
def create_figure(self, data):
#creating a matplotlib figure, closing if there was one before
plt.close(fig=None)
fig = plt.figure()
#creating a figure using the data here
return fig
#refreshing the figure using new data
def refresh(self, data):
self.figure = self.create_figure(data)
Here are also two picture to visualize my problem:
Before
After
clf()
Instead of closing the figure each time you refresh it, you could clear it:
def create_figure(self, date):
#creating a matplotlib figure, closing if there was one before
try:
self.figure.clf()
fig = self.figure
except:
fig = plt.figure()
#creating a figure using the data here
...
Related
I am getting some behaviour I do not understand when I put a matplotlib plot in a tkinter GUI on macOS. The size of the plot seems to depend on whether I have an external monitor plugged in or not.
If the external monitor (27" 1440p) is plugged in, the plot is small; if only the internal monitor (14" 3024x1964) is used, the plot is bigger.
External:
Internal:
To show that the size of the plot is different, here is the GUI opened on the external monitor and dragged onto the internal monitor:
I really do not understand what is going on here as the size of the figure (fig_size in inches) doesn't seem to correspond to anything, but the size of the plot is modulated by changing the dpi parameter.
I would like some way to keep the size of the plot consistent in regard to the rest of the GUI. Note that the size of the plot changes in relation to the 'WIDGET' label.
Minimal working example used to produce the screenshots:
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.pyplot as plt
from tkinter import ttk
import tkinter as tk
class Application(tk.Frame, object):
def __init__(self, master=None):
# Call baseclass constructor.
super(Application, self).__init__(master)
self.fig_size = (6.5, 4)
self.dpi = 100
self.notebook = ttk.Notebook(root)
self.notebook.pack(pady=10, expand=True)
## Buid window with tabs ##
width = 600
height = 400
self.main = tk.Frame(self.notebook, width=width, height=height)
self.main.pack(fill='both', expand=True)
self.notebook.add(self.main, text='TAB 1')
ttk.Style().map("TNotebook.Tab", foreground=[("selected", "#000000")])
self.initialise_main(self.main)
def initialise_main(self, tab):
lbl = tk.Label(tab, text='WIDGET')
self.fig, self.ax = plt.subplots(
1, 1,
figsize=self.fig_size,
dpi=self.dpi,
)
self.line, = self.ax.plot([], []) # empty graph
self.ax.clear()
self.GUIFig = FigureCanvasTkAgg(self.fig, tab)
self.ax.ticklabel_format(useOffset=False)
lbl.grid(row = 0, column=0)
self.GUIFig.get_tk_widget().grid(row=1, column=0)
if __name__ == '__main__':
root = tk.Tk()
app = Application()
app.master.title('MRE')
app.mainloop()
NOTE: this example may not be completely miminimal but I wasn't sure if the OOP approach would change anything so I wanted to keep that consistent.
I haven't had frame size changing on me, but to keep my plots within the frame, I get the dpi and set figure size relative to that and the frame size, and fill the frame with the plot. I also restrict the frame from changing size by setting self.frame2_fileplot.grid_propagate(False)
# get frame size parameters (update frame parameters first)
self.frame2_fileplot.update()
dpi = self.root.winfo_fpixels('1i')
plotwidth = self.frame2_fileplot.winfo_width() / dpi
plotheight = self.frame2_fileplot.winfo_height() / dpi
# create plot
self.plot_figure_fileplot_main = Figure(figsize = (plotwidth, plotheight),
dpi = dpi, frameon = False, tight_layout = True)
Using Python 3.8, Matplotlib 3.3.2, embedding a figure canvas in a GTK3 GUI.
Problem Statement:
I have a program with a GTK3 GUI, and hundreds of variables in a dropdown menu, each with x/y data to plot. I'm attempting to add a NavigationToolbar below my figure so the user can zoom in and out. This works on the initial figure, but when the user selects a different variable, and the line's data is updated with axis.lines[0].set_data(x,y), the Reset Original View button on the toolbar resets to the bounds of the first data set, instead of the new data set (assuming the user zoomed in on the first data set).
Is there some way to tell a NavigationToolbar that the plotted data in a figure has changed, so resetting the view works properly?
Sample Code:
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas
from matplotlib.backends.backend_gtk3 import NavigationToolbar2GTK3 as NavigationToolbar
import matplotlib.pyplot as plt
import numpy as np
class PlotDemo(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title="Navigation Demo")
self.set_border_width(4)
self.set_default_size(850, 650)
self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.add(self.box)
buttonBox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
self.box.add(buttonBox)
sineButton = Gtk.Button(label='SIN')
sineButton.connect('clicked', self.plotSin)
buttonBox.add(sineButton)
cosineButton = Gtk.Button(label='COS')
cosineButton.connect('clicked', self.plotCos)
buttonBox.add(cosineButton)
self.figure, self.axis = plt.subplots()
self.time = np.arange(0.0, 3.0, 0.01)
self.sin = 100*np.sin(2*np.pi*self.time)
self.cos = np.cos(2*np.pi*self.time)
self.axis.plot(self.time, self.sin)
self.axis.set_xlim(0, 3)
self.canvas = FigureCanvas(self.figure)
self.canvas.set_size_request(800, 600)
self.canvas.draw()
self.navigation = NavigationToolbar(self.canvas, self)
self.box.add(self.canvas)
self.box.add(self.navigation)
self.show_all()
def plotSin(self, _unused_widget):
self.update(self.time, self.sin)
def plotCos(self, _unused_widget):
self.update(self.time, self.cos)
def update(self, x, y):
self.axis.lines[0].set_data(x, y)
self.autoScaleY()
def autoScaleY(self):
self.axis.relim()
self.axis.autoscale(axis='y')
self.canvas.draw()
win = PlotDemo()
win.connect("destroy", Gtk.main_quit)
win.show_all()
Gtk.main()
Steps to Reproduce Issue:
1. Run script. Plotted SIN data should be visible.
2. Click "Zoom to rectangle" button on toolbar
3. Zoom in on the SIN plot
4. Click "Reset original view" button on toolbar
5. Click "COS" button at top
6. Initial view should be correct, since I auto-scale the Y-Axis
7. Zoom (optional), and then click "Reset original view" button again
Problem: Step 7 zooms back out to the original Y-Axis limits [-100, 100]
Desired Behavior: Reset view to appropriate limits for the new data set [-1, 1]
I guess I was a bit too quick to post. With a little more digging, I found a solution.
NavigationToolbar2GTK3 inherits from NavigationToolbar2, which has an update() method that clears the navigation stack, which is what the home() method refers to.
In the sample code, changing my update() method to this solved it:
def update(self, x, y):
self.axis.lines[0].set_data(x, y)
self.autoScaleY()
self.navigation.update()
I try to draw a graph from the data I have in the file. For example, a set of data with several chart points is no problem and it is drawn. However, the amount of data I have to draw is constantly growing and it is currently about 15000 points. When I try to load and draw them, the application interface crashes. My code is below. The data file is here: testdata.txt Could you please tell me how to deal with it?
import sys
from PyQt5.QtWidgets import QDialog, QApplication, QPushButton, QVBoxLayout
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
import matplotlib.pyplot as plt
class Window(QDialog):
def __init__(self):
super().__init__()
title = "Wykresy"
self.setWindowTitle(title)
# a figure instance to plot on
self.figure = plt.figure()
# this is the Canvas Widget that
# displays the 'figure'it takes the
# 'figure' instance as a parameter to __init__
self.canvas = FigureCanvas(self.figure)
# this is the Navigation widget
# it takes the Canvas widget and a parent
self.toolbar = NavigationToolbar(self.canvas, self)
# Just some button connected to 'plot' method
self.button = QPushButton('Plot')
# adding action to the button
self.button.clicked.connect(self.plot)
# creating a Vertical Box layout
layout = QVBoxLayout()
# adding tool bar to the layout
layout.addWidget(self.toolbar)
# adding canvas to the layout
layout.addWidget(self.canvas)
# adding push button to the layout
layout.addWidget(self.button)
# setting layout to the main window
self.setLayout(layout)
self.showMaximized()
def plot(self):
with open('testdata.txt') as f:
lines = f.readlines()
x = [line.split('\t')[0] for line in lines]
y = [line.split('\t')[1] for line in lines]
# clearing old figure
self.figure.clear()
# create an axis
ax = self.figure.add_subplot(111)
# plot data
ax.plot(x, y, c = 'r', label = 'temperature')
self.figure.autofmt_xdate()
# refresh canvas
self.canvas.draw()
# driver code
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Window()
window.show()
# loop
sys.exit(app.exec_())
The main bottleneck seems to be the autofmt_xdate() call, which adds a date label for every one of those 15k points. This happens because your x labels aren't actually dates; as far as pyplot is concerned, they're just arbitrary strings, so it doesn't know which ones to keep and which ones to throw away. Something similar is happening for the y labels too.
To parse x into datetime objects and y into floats:
from datetime import datetime
...
x = [datetime.strptime(line.split('\t')[0], '%Y-%m-%d %H:%M:%S') for line in lines]
y = [float(line.split('\t')[1]) for line in lines]
Now I get a single tick per hour on the x axis, and one per 2.5 degrees on the y axis. Rendering is nearly instantaneous.
You should also consider downsampling your data before trying to plot it. 15000 points is way more than fits in the horizontal resolution of a typical computer screen anyway.
To add to #Thomas' answer, you could use pandas to read the file, which may be faster than looping through the content.
(...)
def plot(self):
df = pd.read_csv('testdata.txt', sep='\t', header=None, parse_dates=[0])
(...)
# plot data
ax.plot(df[0], df[1], c='r', label='temperature')
I've got some issues about blitting a matplotlib plot, which is itself embedded in a Tkinter GUI - the whole program will eventually run on a Raspberry Pi. The question involves various levels, this is my first question, so sorry in advance for any unclarities.
In few words, what I'm doing is this: I'm working on a Tk GUI to read out a number of sensors simultaneously and I'd like to have some real-time updating of the sensor data on said GUI.
I'd like to have each measurable quantity on a separate frame, which is why I decided to set up a class for each Sensor. One of the sensors is a Flow Sensor, which is read out and plotted as follows:
import Tkinter as Tk
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from Backend import Backend #self written data acquisition handler
#global variables
t0 = datetime.now() #start time of program
#Returns time difference in seconds
def time_difference(t1, t2):
delta = t2-t1
delta = delta.total_seconds()
return delta
# Define Class for Flow data display
class FlowFig():
def __init__(self, master): #master:Parent frame for plot
#Initialize plot data
self.t = []
self.Flow = []
#Initialize plot for FlowFig
self.fig = plt.figure(figsize=(4,4))
self.ax = self.fig.add_subplot(111)
self.ax.set_title("Flow Control Monitor")
self.ax.set_xlabel("Time")
self.ax.set_ylabel("Flow")
self.ax.axis([0,100,0,5])
self.line = self.ax.plot(self.t, self.Flow, '-')
#Set up canvas
self.canvas = FigureCanvasTkAgg(self.fig, master = master)
self.canvas.show()
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
self.ax.grid(True)
# Initialize handler for data aqcuisition
self.Data= Backend()
self.Data.initialize()
#update figure
def update(self):
# get new data values
self.t.append(time_difference(t0, datetime.now()))
Flow,_,_ = self.Data.get_adc_val(1)
self.Flow.append(Flow)
# shorten data vector if too long
if len(self.t) > 200:
del self.t[0]
del self.Flow[0]
#adjust xlims, add new data to plot
self.ax.set_xlim([np.min(self.t), np.max(self.t)])
self.line[0].set_data(self.t, self.Flow)
#blit new data into old frame
self.canvas.restore_region(self.background)
self.ax.draw_artist(self.line[0])
self.canvas.blit(self.ax.bbox)
root.after(25, Flowmonitor.Flowdata.update) #Recursive update
#Flow Frame of GUI
class FlowPage(Tk.Frame):
def __init__(self, parent, controller):
Tk.Frame.__init__(self,parent)
self.parent = parent
self.FlowPlot = FlowFig(self)
self.FlowPlot.canvas.get_tk_widget().grid(row=0, column=0, rowspan=9, columnspan=9)
# Mainloop
root= Tk.Tk()
root.rowconfigure(0, weight=1)
root.columnconfigure(0, weight=1)
Flowmonitor = FlowPage(root, root)
Flowmonitor.grid(row =0, column=0, rowspan =10, columnspan=10)
Flowmonitor.rowconfigure(0, weight=1)
Flowmonitor.columnconfigure(0, weight=1)
root.after(25, Flowmonitor.FlowPlot.update)
root.mainloop()
What troubles me with my resulting images is this:
When I use the statement copy_from_bbox(self.ax.bbox) I get a graph like this
Obviously, the size of the blitted background doesn't fit the image into which it is blitted. So, I tried to blit the figure's bbox instead (copy_from_bbox(self.fig.bbox)) and got this
Versions of these shifts happen with all combinations of fig.bbox and ax.bbox,
So here come my actual questions:
Can anybody help me find the bug in my above code which causes the missmatch? I'm aware that it is probably very simple yet subtle misstake. It seems very much related to this thread, yet I can't quite glue it together, using bbox.expanded() in the argument of copy_from_bbox() doesn't do much of a difference
.blit() vs. .draw() has already been discussed here.
But since speed is of the essence for my application I think I have to blit. Redrawing the plot gives me framerates of fps=10, whereas blitting runs almost 10x faster. In any case - is there a way to update one of the axes (e.g. time axis) while using blit? (The answer to this is probably closely related to question No.1 )
Lastly, a rather basic question about my application alltogether: Since my sensordata is currently fetched within an infinite, recursive loop - is it possible to run several of such loops in parallel or should I rather go for threading instead, making my code considerably more complex? What are the risks of running infinite, recursive loops? Or should these be avoided in general?
After days of blitting back and forth I'm rather confused about the possibilities regarding ax/fig blitting, so any help regarding the matter is much, much appreciated^^
Please let me know if you need more info about anything, I hope I could illustrate my problem well.
Thanks a lot for your help!
In short: the solution
This was written in Python3, but it should be virtually the exact same in your version of Python2
import tkinter as Tk
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
# from Backend import Backend #self written data acquisition handler
import random
#global variables
t0 = datetime.now() #start time of program
#Returns time difference in seconds
def time_difference(t1, t2):
delta = t2-t1
delta = delta.total_seconds()
return delta
# Define Class for Flow data display
class FlowFig():
def __init__(self, master): #master:Parent frame for plot
#Initialize plot data
self.t = []
self.Flow = []
#Initialize plot for FlowFig
self.fig = plt.figure(figsize=(4,4))
self.ax = self.fig.add_subplot(111)
self.ax.set_title("Flow Control Monitor")
self.ax.set_xlabel("Time")
self.ax.set_ylabel("Flow")
self.ax.axis([0,100,0,5])
self.line = self.ax.plot(self.t, self.Flow, '-')
#Set up canvas
self.canvas = FigureCanvasTkAgg(self.fig, master = master)
self.canvas.draw()
self.ax.add_line(self.line[0])
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
self.ax.grid(True)
# # Initialize handler for data aqcuisition
# self.Data= Backend()
# self.Data.initialize()
#update figure
def update(self):
# get new data values
self.t.append(time_difference(t0, datetime.now()))
Flow = random.uniform(1, 5)
self.Flow.append(Flow)
# shorten data vector if too long
if len(self.t) > 200:
del self.t[0]
del self.Flow[0]
#adjust xlims, add new data to plot
self.ax.set_xlim([np.min(self.t), np.max(self.t)])
self.line[0].set_data(self.t, self.Flow)
#blit new data into old frame
self.canvas.restore_region(self.background)
self.ax.draw_artist(self.line[0])
self.canvas.blit(self.ax.bbox)
self.canvas.flush_events()
root.after(1,self.update)
#Flow Frame of GUI
class FlowPage(Tk.Frame):
def __init__(self, parent, controller):
Tk.Frame.__init__(self,parent)
self.parent = parent
self.FlowPlot = FlowFig(self)
self.FlowPlot.canvas.get_tk_widget().grid(row=0, column=0, rowspan=9, columnspan=9)
# Mainloop
root= Tk.Tk()
root.rowconfigure(0, weight=1)
root.columnconfigure(0, weight=1)
Flowmonitor = FlowPage(root, root)
Flowmonitor.grid(row =0, column=0, rowspan =10, columnspan=10)
Flowmonitor.rowconfigure(0, weight=1)
Flowmonitor.columnconfigure(0, weight=1)
root.after(25, Flowmonitor.FlowPlot.update)
root.mainloop()
The Changes
Moving to python3
I moved
import Tkinter as Tk
to
import tkinter as Tk
Simulating your adc values
I moved
from Backend import Backend
to
import random
Because I don't have access to the Backend, I just used a random number generator (obviously it isn't the best example of ADC readings, but it is good enough for a tester)
Setting up the canvas
I moved
self.canvas = FigureCanvasTkAgg(self.fig, master = master)
self.canvas.show()
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
self.ax.grid(True)
to
self.canvas = FigureCanvasTkAgg(self.fig, master = master)
self.canvas.draw()
self.ax.add_line(self.line[0])
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
self.ax.grid(True)
You must first draw() the canvas, because otherwise matplotlib will throw an error AttributeError: draw_artist can only be used after an initial draw which caches the renderer.
Then, we now add the self.line, with no values in it. Currently it is just an empty point sitting on the canvas.
Simulating ADC
From:
Flow,_,_ = self.Data.get_adc_val(1)
to
Flow = random.uniform(1, 5)
Obviously you could keep your own code for this
Looping with after
Your system of using the after function wasn't entirely correct, as you should have inherited the Flowmonitor.Flowdata from the pre-existing window. Otherwise you would be updating values that simply don't exist, hence, I replaced it with a self. function
root.after(25, Flowmonitor.Flowdata.update)
to
self.canvas.flush_events()
root.after(1,self.update)
I decreased the after value, to show that the window could continue plotting correctly when doing it even faster!
The flush_events() function causes the window to properly update and keep track of other things it's doing!
Answer to question 3
I'd thoroughly dissuade you from going down the threading route, because it is awful with tkinter. The amount of issues and loop-holes you have to jump through are awful, and quite often, even with threading, the program still begins to feel quite slow and unresponsive.
I am trying to code with python3 a GUI that plots 4 different graph in 4 respective layout : speed, height, coordinates and the angle. Right now I am able to draw the figure in each respective layout. However, I have no idea how to plot different function into each graph. I used a method to randomly generate 10 points to plot. When the method is called, it plots the same graph into each 4 canvas.
So my question is **if there is anyway to plot different function respectively to a figure(one plot per graph)?**I am pretty new to python3 and would be grateful for any help provided.
If possible, I would like to avoid using many subplots in a figure since a layout for each figures exist already.
Here is what the current GUI looks like when I call the test method that generates random points, you can see that
the same graph are plotted in each canvas
I will also mention, if it adds any constraints, that this code will eventually be used to plots graph that will update with data coming from another thread.
And here's the code:
from PyQt4 import QtGui
from .flight_dataUI import Ui_Dialog
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
import random
class FlightData(QtGui.QDialog, Ui_Dialog):
def __init__(self, parent=None):
QtGui.QDialog.__init__(self, parent)
self.setupUi(self)
self.figure = plt.figure() # FlightData.figure = matplotlib.pyplot.figure()
# Creates a figure widget self.name = FigureCanvas(self.figure)
self.speedGraph = FigureCanvas(self.figure)
self.heightGraph = FigureCanvas(self.figure)
self.mapGraph = FigureCanvas(self.figure)
self.angleGraph = FigureCanvas(self.figure)
# -------------------------------------------------------------
self.speedLayout.addWidget(self.speedGraph) # insert widget "speedGraph" in speedLayout
self.heightLayout.addWidget(self.heightGraph) # insert widget "heightGraph" in heightLayout
self.mapLayout.addWidget(self.mapGraph) # insert widget "mapGraph" in mapLayout
self.angleLayout.addWidget(self.angleGraph) # insert widget "angleGraph" in angleLayout
self.ax = self.figure.add_subplot(111)
self.ax.hold(False)
self.init_widgets()
def init_widgets(self):
self.analyseButton.clicked.connect(self.open_analysedata)
self.draw_plot()
def open_analysedata(self):
self.done(2) # Closes and delete Dialog window et and return the int 2 as a results in main_window.py
def draw_plot(self):
data = [random.random() for i in range(10)]
self.ax.plot(data, '-*')
If embedding do not import pyplot, it's global state will only cause you trouble. You are using the same figure to initialize all 4 FigureCanvas objects. You want to do something like:
from matplotlib.figure import Figure
class FlightData(QtGui.QDialog, Ui_Dialog):
def __init__(self, parent=None):
QtGui.QDialog.__init__(self, parent)
self.setupUi(self)
self.figs = {}
self.canvas = {}
self.axs = {}
plot_names = ['speed', 'height', 'map', 'angle']
for pn in plot_names:
fig = Figure()
self.canvas[pn] = FigureCanvas(fig)
ax = fig.add_subplot(1, 1, 1)
self.figs[pn] = fig
self.axs[pn] = ax
# -------------------------------------------------------------
self.speedLayout.addWidget(self.canvas['speed'])
self.heightLayout.addWidget(self.canvas['height'])
self.mapLayout.addWidget(self.canvas['map'])
self.angleLayout.addWidget(self.canvas['angle'])
def draw_plot(self, target, data):
self.axs[target].plot(data, '-*')
self.canvas[target].draw_idle()