tkinter keep a handle alive - python

Hey there I have the following code:
import matplotlib as mpl
import numpy as np
import sys
if sys.version_info[0] < 3:
import Tkinter as tk
else:
import tkinter as tk
import matplotlib.backends.tkagg as tkagg
from matplotlib.backends.backend_agg import FigureCanvasAgg
def draw_figure(canvas, figure, loc=(0, 0)):
""" Draw a matplotlib figure onto a Tk canvas
loc: location of top-left corner of figure on canvas in pixels.
Inspired by matplotlib source: lib/matplotlib/backends/backend_tkagg.py
"""
figure_canvas_agg = FigureCanvasAgg(figure)
figure_canvas_agg.draw()
figure_x, figure_y, figure_w, figure_h = figure.bbox.bounds
figure_w, figure_h = int(figure_w), int(figure_h)
photo = tk.PhotoImage(master=canvas, width=figure_w, height=figure_h)
# Position: convert from top-left anchor to center anchor
canvas.create_image(loc[0] + figure_w/2, loc[1] + figure_h/2, image=photo)
# Unfortunately, there's no accessor for the pointer to the native renderer
tkagg.blit(photo, figure_canvas_agg.get_renderer()._renderer, colormode=2)
# Return a handle which contains a reference to the photo object
# which must be kept live or else the picture disappears
return photo
# Create a canvas
w, h = 300, 200
window = tk.Tk()
window.title("A figure in a canvas")
canvas = tk.Canvas(window, width=w, height=h)
canvas.pack()
# Generate some example data
X = np.linspace(0, 2 * np.pi, 50)
Y = np.sin(X)
# Create the figure we desire to add to an existing canvas
fig = mpl.figure.Figure(figsize=(2, 1))
ax = fig.add_axes([0, 0, 1, 1])
ax.plot(X, Y)
# Keep this handle alive, or else figure will disappear
fig_x, fig_y = 100, 100
fig_photo = draw_figure(canvas, fig, loc=(fig_x, fig_y))
fig_w, fig_h = fig_photo.width(), fig_photo.height()
# Add more elements to the canvas, potentially on top of the figure
canvas.create_line(200, 50, fig_x + fig_w / 2, fig_y + fig_h / 2)
canvas.create_text(200, 50, text="Zero-crossing", anchor="s")
Code from here:
https://matplotlib.org/2.1.1/gallery/user_interfaces/embedding_in_tk_canvas_sgskip.html
However I would like to draw different figures in a function. But when I try to do that my handle always dies and I do not see any figure on my tkinter....
Any ideas how to do keep a handle alive inside a function?

Related

Matplotlib mplcursors not working when figure size is Increased in tkinter canvas

I am a newbie on tkinter and python. I am trying to use mplcursors in a matplotlib figure embedded in a tkinter canvas. The figure size in the tkinter canvas can be changed as discussed on SO here. I want to add mplcursors to hover the data on the plot.
When I do not change the figure size, mplcursors works.
However, the problem occurs when the figure size is increased (to say 30 inches) mplcursors hover does not show up.
I suspect the problem is using tkinter canvas scrollbar with mplcursors. And I am not sure how to fix it.
Any suggestions or ideas, if I am missing something ?
import tkinter as tk
from tkinter import ttk
from tkinter.simpledialog import askfloat
from matplotlib.figure import Figure
from matplotlib.axes import Axes
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import mplcursors
from matplotlib.colors import to_rgb
class InteractivePlot(tk.Frame):
def __init__(self,master,**kwargs):
super().__init__(master,**kwargs)
self._figure = Figure(dpi=150)
self._canvas = FigureCanvasTkAgg(self._figure, master=self)
buttonframe = tk.Frame(self)
self._sizebutton = tk.Button(buttonframe,text="Size (in.)", command=self._change_size)
self._dpibutton = tk.Button(buttonframe,text="DPI", command=self._change_dpi)
self._axis = self._figure.add_subplot(111)
# Plot some data just to have something to look at.
self._axis.plot([0,1,2,3,4,5],[1,1,3,3,5,5],label='Dummy Data')
self._cwidg = self._canvas.get_tk_widget()
self._scrx = ttk.Scrollbar(self,orient="horizontal", command=self._cwidg.xview)
self._scry = ttk.Scrollbar(self,orient="vertical", command=self._cwidg.yview)
self._cwidg.configure(yscrollcommand=self._scry.set, xscrollcommand=self._scrx.set)
self._cwidg.bind("<Configure>",self._refresh)
self._sizebutton.grid(row=0,column=0,sticky='w')
self._dpibutton.grid(row=0,column=1,sticky='w')
buttonframe.grid(row=0,column=0,columnspan=2,sticky='W')
self._cwidg. grid(row=1,column=0,sticky='news')
self._scrx. grid(row=2,column=0,sticky='ew')
self._scry. grid(row=1,column=1,sticky='ns')
self.rowconfigure(1,weight=1)
self.columnconfigure(0,weight=1)
## ADDED LINE
self.curs = mplcursors.cursor(self._axis, hover=mplcursors.HoverMode.Transient) # hover=True
# Refresh the canvas to show the new plot
self._canvas.draw()
# Figure size change button callback
def _change_size(self):
newsize = askfloat('Size','Input new size in inches')
if newsize is None:
return
w = newsize
h = newsize/1.8
self._figure.set_figwidth(w)
self._figure.set_figheight(h)
self._refresh()
# Figure DPI change button callback
def _change_dpi(self):
newdpi = askfloat('DPI', 'Input a new DPI for the figure')
if newdpi is None:
return
self._figure.set_dpi(newdpi)
self._refresh()
# Refresh function to make the figure canvas widget display the entire figure
def _refresh(self,event=None):
# Get the width and height of the *figure* in pixels
w = self._figure.get_figwidth()*self._figure.get_dpi()
h = self._figure.get_figheight()*self._figure.get_dpi()
# Generate a blank tkinter Event object
evnt = tk.Event()
# Set the "width" and "height" values of the event
evnt.width = w
evnt.height = h
# Set the width and height of the canvas widget
self._cwidg.configure(width=w,height=h)
self._cwidg.update_idletasks()
# Pass the generated event object to the FigureCanvasTk.resize() function
self._canvas.resize(evnt)
# Set the scroll region to *only* the area of the last canvas item created.
# Otherwise, the scrollregion will always be the size of the largest iteration
# of the figure.
self._cwidg.configure(scrollregion=self._cwidg.bbox(self._cwidg.find_all()[-1]))
root = tk.Tk()
plt = InteractivePlot(root,width=400,height=400)
plt.pack(fill=tk.BOTH,expand=True)
root.mainloop()

Why are my plots not appearing with set_data using Tkinter?

I am trying to improve my plotting function. I want to plot data using my plotGraph function coming from an EEG board in real-time, pulling samples from an LSL # 250Hz. Previously, I had a functional version using the regular self.ax.plot(x,y), clearing the data with self.ax.clear() every time the plot needed to refresh. Nonetheless, some profiling showed that my code was taking way too much time to plot in comparison to the rest of it.
One of the suggestions I got was to use set_data instead of plot and clear. I have multiple lines of data that I want to plot simultaneously, so I tried following Matplotlib multiple animate multiple lines, which you can see below (adapted code). Also, I was told to use self.figure.canvas.draw_idle(), which I tried, but I'm not sure if I did it correctly.
Unfortunately, it didn't work, the graph is not updating and I can't seem to find why. I'm aware that the source I just mentioned uses animation.FuncAnimation but I'm not sure that would be the problem. Is it?
Any ideas of why none of my lines are showing in my canvas' graph?
import tkinter as tk
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import numpy as np
class AppWindow:
def plotGraph(self, x, y):
for lnum,line in enumerate(self.lines):
line.set_data(x[:], y[:, lnum])
self.figure.canvas.draw_idle()
plt.ylabel('Magnitude', fontsize = 9, color = tx_color)
plt.xlabel('Freq', fontsize = 9, color = tx_color)
self.figure.canvas.draw()
def __init__(self):
self.root = tk.Tk() #start of application
self.canvas = tk.Canvas(self.root, height = 420, width = 780, bg =
bg_color, highlightthickness=0)
self.canvas.pack(fill = 'both', expand = True)
self.figure = plt.figure(figsize = (5,6), dpi = 100)
self.figure.patch.set_facecolor(sc_color)
self.ax = self.figure.add_subplot(111)
self.ax.clear()
self.line, = self.ax.plot([], [], lw=1, color = tx_color)
self.line.set_data([],[])
#place graph
self.chart_type = FigureCanvasTkAgg(self.figure, self.canvas)
self.chart_type.get_tk_widget().pack()
self.lines = []
numchan = 8 #let's say I have 8 channels
for index in range(numchan):
lobj = self.ax.plot([],[], lw=2, color=tx_color)[0]
self.lines.append(lobj)
for line in self.lines:
line.set_data([],[])
def start(self):
self.root.mainloop()
You chart is empty because you are plotting empty arrays:
line.set_data([],[])
If you fill in the line arrays, the chart plots correctly.
Try this code. It updates the chart with new random data every second.
import tkinter as tk
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import numpy as np
import random
bg_color='grey'
tx_color='green'
sc_color='linen'
numchan = 8
chlen = 100
xvals=[(x-40)/20 for x in range(chlen)] # X coordinates
chcolors= ['gold','blue','green','maroon','red','brown','purple','cyan']
class AppWindow:
def plotGraph(self):
self.figure.canvas.draw_idle()
plt.ylabel('Magnitude', fontsize = 9, color = tx_color)
plt.xlabel('Freq', fontsize = 9, color = tx_color)
self.figure.canvas.draw()
def UpdateChannelData(self): # callback with new data
# fake random data
for i,ch in enumerate(self.chdata):
for p in range(len(ch)):
ch[p] += (random.random()-.5)/100
self.lines[i].set_data(xvals, ch)
self.plotGraph()
self.root.after(100, self.UpdateChannelData) # simulate next call
def __init__(self):
global chzero
self.root = tk.Tk() #start of application
self.canvas = tk.Canvas(self.root, height = 420, width = 780, bg = bg_color, highlightthickness=0)
self.canvas.pack(fill = 'both', expand = True)
self.figure = plt.figure(figsize = (5,6), dpi = 100)
self.figure.patch.set_facecolor(sc_color)
self.ax = self.figure.add_subplot(111)
self.ax.clear()
self.line, = self.ax.plot([], [], lw=1, color = tx_color)
self.line.set_data([],[])
#place graph
self.chart_type = FigureCanvasTkAgg(self.figure, self.canvas)
self.chart_type.get_tk_widget().pack()
self.lines = []
#numchan = 8 #let's say I have 8 channels
for index in range(numchan):
lobj = self.ax.plot([],[], lw=1, color=chcolors[index])[0]
self.lines.append(lobj)
# set flat data
self.chdata = [[0 for x in range(chlen)] for ch in range(numchan)]
self.root.after(1000, self.UpdateChannelData) # start data read
def start(self):
self.root.mainloop()
AppWindow().start()
Output:

How do I change the subplot parameters having a Figure in a window in Tkinter ? As for example I want to add the xlabel and ylabel

I have an application that should get data from a sensor into a live graph, a subplot that is added into a Figure.
I have now a problem after adding the subplot that I don't know how to change the plot parameters as xlabel, ylabel. This works if I import plt, but not if I import a Figure that will be further added to the window in Tkinter.
#file livegraph.py
import matplotlib.animation as animation
import datetime
#this is a draft for the liveGraph class
#the objective is to get live data from a sensor
class liveGraph:
#by default define the interval as being 1000 mSec
intervalAnim = 1000
def __init__(self,fig):
self.xax = 0
self.xs = []
self.ys = []
self.ax = fig.add_subplot(111)
self.ax.set_xlabel('teeeest')
#fig.title('Graph test')
#fig.set_xlabel("Time")
#fig.ylabel("% SMS")
self.anim = animation.FuncAnimation(fig, self.animate, interval = self.intervalAnim)
def animate(self,i):
self.xs.append(self.xax)
self.ys.append(datetime.datetime.now().second)
self.xax+=1
self.ax.clear()
self.ax.plot(self.xs,self.ys)
if self.xax > 90:
self.anim.event_source.stop()
from tkinter import *
from matplotlib import style
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from livegraph import liveGraph
# Define the main_screen as a tkinter
app_screen = Tk() # create a GUI window
app_screen.geometry("1920x1080") # set the configuration of GUI window
app_screen.resizable(width=True,height=True)
app_screen.title("Testare izolator") # set the title of GUI window
style.use('bmh')
#figure represents the graphic part of the system
figure = Figure(figsize=(10, 5), facecolor='white',frameon=True)
figure.suptitle('This is the figure title', fontsize=12)
#figure.add_gridspec(10,10)
#this are some parameters that I can easily change if I am using plt
# plt.title('Graph test')
#plt.xlabel("Time")
#plt.ylabel("% SMS")
#x = [ 0, 1, 2, 3, 4 ]
#y = [ 0, 1, 2, 3, 4 ]
#lines = plt.plot(x, y)
#plt.grid()
#plt.axis([0,10,0,10])
#plt.setp(lines, color= "b")
canvas = FigureCanvasTkAgg(figure, app_screen)
canvas.get_tk_widget().pack(side=TOP, anchor =NW, padx=100, pady=10)
newAnimation = liveGraph(figure)
app_screen.mainloop() # start the GUI
You should use self.ax. to add elements
self.ax.set_xlabel('teeeest')
self.ax.set_title('Graph test')
self.ax.set_xlabel("Time")
self.ax.set_ylabel("% SMS")
but then there is other problem because self.ax.clear() removes these elements.
First method:
If you use self.ax.clear() then you remove labels and you have to put labels again and again
def animate(self, i):
self.xs.append(self.xax)
#self.ys.append(datetime.datetime.now().second)
self.ys.append(random.randint(0, 10))
self.xax += 1
self.ax.clear()
self.ax.plot(self.xs,self.ys)
if self.xax > 90:
self.anim.event_source.stop()
self.ax.set_xlabel('teeeest')
self.ax.set_title('Graph test')
self.ax.set_xlabel("Time")
self.ax.set_ylabel("% SMS")
Second method:
To add elements only once you have to remove self.ax.clear() and instead of plot() you should create empty plot in `init
self.ax = fig.add_subplot(111)
self.ax.set_xlabel('teeeest')
self.ax.set_title('Graph test')
self.ax.set_xlabel("Time")
self.ax.set_ylabel("% SMS")
self.line, = self.ax.plot([], [])
and in animation use set_data() to update data in existing plot
self.line.set_data(self.xs, self.ys)
but it will not rescale plot and you will have to do it manually (if you want to rescale it)
self.ax.relim() # recalculate limits
self.ax.autoscale_view(True,True,True) # rescale using limits
Full code for first method
import matplotlib.animation as animation
import datetime
import random
#this is a draft for the liveGraph class
#the objective is to get live data from a sensor
class liveGraph:
#by default define the interval as being 1000 mSec
intervalAnim = 1000
def __init__(self, fig):
self.xax = 0
self.xs = []
self.ys = []
self.ax = fig.add_subplot(111)
self.ax.set_xlabel('teeeest')
self.anim = animation.FuncAnimation(fig, self.animate, interval=self.intervalAnim)
def animate(self, i):
self.xs.append(self.xax)
#self.ys.append(datetime.datetime.now().second)
self.ys.append(random.randint(0, 10))
self.xax += 1
self.ax.clear()
self.ax.plot(self.xs,self.ys)
if self.xax > 90:
self.anim.event_source.stop()
self.ax.set_title('Graph test')
self.ax.set_xlabel("Time")
self.ax.set_ylabel("% SMS")
from tkinter import *
from matplotlib import style
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
#from livegraph import liveGraph
# Define the main_screen as a tkinter
app_screen = Tk() # create a GUI window
app_screen.geometry("1920x1080") # set the configuration of GUI window
app_screen.resizable(width=True, height=True)
app_screen.title("Testare izolator") # set the title of GUI window
style.use('bmh')
#figure represents the graphic part of the system
figure = Figure(figsize=(10, 5), facecolor='white', frameon=True)
figure.suptitle('This is the figure title', fontsize=12)
#figure.add_gridspec(10,10)
#this are some parameters that I can easily change if I am using plt
#plt.title('Graph test')
#plt.xlabel("Time")
#plt.ylabel("% SMS")
#x = [ 0, 1, 2, 3, 4 ]
#y = [ 0, 1, 2, 3, 4 ]
#lines = plt.plot(x, y)
#plt.grid()
#plt.axis([0,10,0,10])
#plt.setp(lines, color= "b")
canvas = FigureCanvasTkAgg(figure, app_screen)
canvas.get_tk_widget().pack(side=TOP, anchor=NW, padx=100, pady=10)
newAnimation = liveGraph(figure)
#canvas.draw()
app_screen.mainloop() # start the GUI
Full code for second method
import matplotlib.animation as animation
import datetime
import random
#this is a draft for the liveGraph class
#the objective is to get live data from a sensor
class liveGraph:
#by default define the interval as being 1000 mSec
intervalAnim = 1000
def __init__(self, fig):
self.xax = 0
self.xs = []
self.ys = []
self.ax = fig.add_subplot(111)
self.ax.set_xlabel('teeeest')
self.ax.set_title('Graph test')
self.ax.set_xlabel("Time")
self.ax.set_ylabel("% SMS")
# create empty plot at start
self.line, = self.ax.plot([], [])
self.anim = animation.FuncAnimation(fig, self.animate, interval=self.intervalAnim)
def animate(self, i):
self.xs.append(self.xax)
#self.ys.append(datetime.datetime.now().second)
self.ys.append(random.randint(0, 2))
self.xax += 1
# update data in existing plot
self.line.set_data(self.xs, self.ys)
# rescale plot (if you need it)
self.ax.relim() # recalculate limits
self.ax.autoscale_view(True,True,True) # rescale using limits
if self.xax > 90:
self.anim.event_source.stop()
from tkinter import *
from matplotlib import style
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
#from livegraph import liveGraph
# Define the main_screen as a tkinter
app_screen = Tk() # create a GUI window
app_screen.geometry("1920x1080") # set the configuration of GUI window
app_screen.resizable(width=True, height=True)
app_screen.title("Testare izolator") # set the title of GUI window
style.use('bmh')
#figure represents the graphic part of the system
figure = Figure(figsize=(10, 5), facecolor='white', frameon=True)
figure.suptitle('This is the figure title', fontsize=12)
#figure.add_gridspec(10,10)
#this are some parameters that I can easily change if I am using plt
#plt.title('Graph test')
#plt.xlabel("Time")
#plt.ylabel("% SMS")
#x = [ 0, 1, 2, 3, 4 ]
#y = [ 0, 1, 2, 3, 4 ]
#lines = plt.plot(x, y)
#plt.grid()
#plt.axis([0,10,0,10])
#plt.setp(lines, color= "b")
canvas = FigureCanvasTkAgg(figure, app_screen)
canvas.get_tk_widget().pack(side=TOP, anchor=NW, padx=100, pady=10)
newAnimation = liveGraph(figure)
#canvas.draw()
app_screen.mainloop() # start the GUI

How to change width of a plot while zooming with tkinter toolbar

I have a problem with the zoom of the NavigationToolbar2Tk from tkinter. I set a fixed width=200 for my plot. When I zoom into the plot, this width remains (logical). The problem is, when I zoom in, the bars with a width=200 are too big and the plot becomes confusing when two bars are side by side or behind each other. How can I change the width=10 when zooming is activ?
My previous code is below:
# ___________________________________________________________________________
# Library
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import tkinter as tk
import numpy as np
# ___________________________________________________________________________
# Maintenance Array
Maintenance_km = np.array([0,3,400,400,1700,1850,1600,4,1000,1000,2000,3000,3040,3040,80000,80090])
Maintenance_cost = np.array([4,500,500,1000,2000,2040,2040,10,40,500,2400,2700,2850,2600,3000,3150])
# ___________________________________________________________________________
# Main
Vis = tk.Tk()
Vis.title("Main") # titel
# ___________________________________________________________________________
# Plot
fig, ax = plt.subplots(1, 1, figsize=(20,5), facecolor = "white")
Plot_Maintenace_cost2 = plt.bar(Maintenance_km, Maintenance_cost,
bottom=-0,
color="#C5E0B4",
ec="black",
width=200,
label="Maintenance_cost")
ax.spines["bottom"].set_position("zero")
ax.spines["top"].set_color("none")
ax.spines["right"].set_color("none")
ax.spines["left"].set_color("none")
ax.tick_params(axis="x", length=20)
_, xmax = ax.get_xlim()
ymin, ymax = ax.get_ylim()
ax.set_xlim(-15, xmax)
ax.set_ylim(ymin, ymax+10) # legend
ax.text(xmax, -5, "km", ha="right", va="top", size=14)
plt.legend(ncol=5, loc="upper left")
plt.tight_layout()
# ___________________________________________________________________________
# Canvas, Toolbar
canvas = FigureCanvasTkAgg(fig, master=Vis)
canvas.draw() # TK-Drawingarea
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)
canvas._tkcanvas.pack(side = tk.TOP, fill = tk.BOTH, expand = True)
toolbar = NavigationToolbar2Tk(canvas, Vis)
toolbar.update()
Vis.mainloop()
You could updated the plot when the limits of the x-axis change (like in this question)
def on_xlims_change(axes):
# Get the current limits of the x axis
x_lim = ax.get_xlim()
x_width = x_lim[1] - x_lim[0]
# Just an example, you can use your own logic here
new_width = x_width / 200 # Ensure a fixed ratio of xaxis to bar width
# new_width = 200 if x_width > 10000 else 10
# Update all bars
for b in Plot_Maintenace_cost2:
b.set_width(new_width)
ax.callbacks.connect('xlim_changed', on_xlims_change)

Size in pixels of x-axis from a matplotlib figure embedded in a PyQt5 window

I've got a live matplotlib graph in a PyQt5 window:
You can read more about how I got this code working here:
How to make a fast matplotlib live plot in a PyQt5 GUI
Please copy-paste the code below to a python file, and run it with Python 3.7:
#####################################################################################
# #
# PLOT A LIVE GRAPH IN A PYQT WINDOW #
# #
#####################################################################################
from __future__ import annotations
from typing import *
import sys
import os
from PyQt5 import QtWidgets, QtCore
from matplotlib.backends.backend_qt5agg import FigureCanvas
import matplotlib as mpl
import matplotlib.figure as mpl_fig
import matplotlib.animation as anim
import matplotlib.style as style
import numpy as np
style.use('ggplot')
class ApplicationWindow(QtWidgets.QMainWindow):
'''
The PyQt5 main window.
'''
def __init__(self):
super().__init__()
# 1. Window settings
self.setGeometry(300, 300, 800, 400)
self.setWindowTitle("Matplotlib live plot in PyQt")
self.frm = QtWidgets.QFrame(self)
self.frm.setStyleSheet("QWidget { background-color: #eeeeec; }")
self.lyt = QtWidgets.QVBoxLayout()
self.frm.setLayout(self.lyt)
self.setCentralWidget(self.frm)
# 2. Place the matplotlib figure
self.myFig = MyFigureCanvas(x_len=200, y_range=[0, 100], interval=20)
self.lyt.addWidget(self.myFig)
# 3. Show
self.show()
return
class MyFigureCanvas(FigureCanvas, anim.FuncAnimation):
'''
This is the FigureCanvas in which the live plot is drawn.
'''
def __init__(self, x_len:int, y_range:List, interval:int) -> None:
'''
:param x_len: The nr of data points shown in one plot.
:param y_range: Range on y-axis.
:param interval: Get a new datapoint every .. milliseconds.
'''
FigureCanvas.__init__(self, mpl_fig.Figure())
# Range settings
self._x_len_ = x_len
self._y_range_ = y_range
# Store two lists _x_ and _y_
x = list(range(0, x_len))
y = [0] * x_len
# Store a figure and ax
self._ax_ = self.figure.subplots()
self._ax_.set_ylim(ymin=self._y_range_[0], ymax=self._y_range_[1])
self._line_, = self._ax_.plot(x, y)
# Call superclass constructors
anim.FuncAnimation.__init__(self, self.figure, self._update_canvas_, fargs=(y,), interval=interval, blit=True)
return
def _update_canvas_(self, i, y) -> None:
'''
This function gets called regularly by the timer.
'''
y.append(round(get_next_datapoint(), 2)) # Add new datapoint
y = y[-self._x_len_:] # Truncate list _y_
self._line_.set_ydata(y)
# Print size of bounding box (in pixels)
bbox = self.figure.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted())
width, height = bbox.width * self.figure.dpi, bbox.height * self.figure.dpi
print(f"bbox size in pixels = {width} x {height}")
return self._line_,
# Data source
# ------------
n = np.linspace(0, 499, 500)
d = 50 + 25 * (np.sin(n / 8.3)) + 10 * (np.sin(n / 7.5)) - 5 * (np.sin(n / 1.5))
i = 0
def get_next_datapoint():
global i
i += 1
if i > 499:
i = 0
return d[i]
if __name__ == "__main__":
qapp = QtWidgets.QApplication(sys.argv)
app = ApplicationWindow()
qapp.exec_()
1. The problem
I need to know the nr of pixels from x_min to x_max:
Please notice that the x-axis actually goes beyond the x_min and x_max borders. I don't need to know the total length. Just the length from x_min to x_max.
2. What I tried so far
I already found a way to get the graph's bounding box. Notice the following codelines in the _update_canvas_() function:
# Print size of bounding box (in pixels)
bbox = self.figure.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted())
width, height = bbox.width * self.figure.dpi, bbox.height * self.figure.dpi
print(f"bbox size in pixels = {width} x {height}")
That gave me a bounding box size of 778.0 x 378.0 pixels. It's a nice starting point, but I don't know how to proceed from here.
I also noticed that this bounding box size isn't printed out correctly from the first go. The first run of the _update_canvas_() function prints out a bouding box of 640.0 x 480.0 pixels, which is just plain wrong. From the second run onwards, the printed size is correct. Why?
Edit
I tried two solutions. The first one is based on a method described by #ImportanceOfBeingErnes (see Axes class - set explicitly size (width/height) of axes in given units) and the second one is based on the answer from #Eyllanesc.
#####################################################################################
# #
# PLOT A LIVE GRAPH IN A PYQT WINDOW #
# #
#####################################################################################
from __future__ import annotations
from typing import *
import sys
import os
from PyQt5 import QtWidgets, QtCore
from matplotlib.backends.backend_qt5agg import FigureCanvas
import matplotlib as mpl
import matplotlib.figure as mpl_fig
import matplotlib.animation as anim
import matplotlib.style as style
import numpy as np
style.use('ggplot')
def get_width_method_a(ax, dpi, canvas):
l = float(ax.figure.subplotpars.left)
r = float(ax.figure.subplotpars.right)
x, y, w, h = ax.figure.get_tightbbox(renderer=canvas.get_renderer()).bounds
return float(dpi) * float(w - (l + r))
def get_width_eyllanesc(ax):
""" Based on answer from #Eyllanesc"""
""" See below """
y_fake = 0
x_min, x_max = 0, 200
x_pixel_min, _ = ax.transData.transform((x_min, y_fake))
x_pixel_max, _ = ax.transData.transform((x_max, y_fake))
return x_pixel_max - x_pixel_min
class ApplicationWindow(QtWidgets.QMainWindow):
'''
The PyQt5 main window.
'''
def __init__(self):
super().__init__()
# 1. Window settings
self.setGeometry(300, 300, 800, 400)
self.setWindowTitle("Matplotlib live plot in PyQt")
self.frm = QtWidgets.QFrame(self)
self.frm.setStyleSheet("QWidget { background-color: #eeeeec; }")
self.lyt = QtWidgets.QVBoxLayout()
self.frm.setLayout(self.lyt)
self.setCentralWidget(self.frm)
# 2. Place the matplotlib figure
self.myFig = MyFigureCanvas(x_len=200, y_range=[0, 100], interval=20)
self.lyt.addWidget(self.myFig)
# 3. Show
self.show()
return
class MyFigureCanvas(FigureCanvas, anim.FuncAnimation):
'''
This is the FigureCanvas in which the live plot is drawn.
'''
def __init__(self, x_len:int, y_range:List, interval:int) -> None:
'''
:param x_len: The nr of data points shown in one plot.
:param y_range: Range on y-axis.
:param interval: Get a new datapoint every .. milliseconds.
'''
FigureCanvas.__init__(self, mpl_fig.Figure())
# Range settings
self._x_len_ = x_len
self._y_range_ = y_range
# Store two lists _x_ and _y_
x = list(range(0, x_len))
y = [0] * x_len
# Store a figure and ax
self._ax_ = self.figure.subplots()
self._ax_.set_ylim(ymin=self._y_range_[0], ymax=self._y_range_[1])
self._line_, = self._ax_.plot(x, y)
self._line_.set_ydata(y)
print("")
print(f"width in pixels (first call, method is 'method_a') = {get_width_method_a(self._ax_, self.figure.dpi, self)}")
print(f"width in pixels (first call, method is 'eyllanesc') = {get_width_eyllanesc(self._ax_)}")
# Call superclass constructors
anim.FuncAnimation.__init__(self, self.figure, self._update_canvas_, fargs=(y,), interval=interval, blit=True)
return
def _update_canvas_(self, i, y) -> None:
'''
This function gets called regularly by the timer.
'''
y.append(round(get_next_datapoint(), 2)) # Add new datapoint
y = y[-self._x_len_:] # Truncate list _y_
self._line_.set_ydata(y)
print("")
print(f"width in pixels (method is 'method_a') = {get_width_method_a(self._ax_, self.figure.dpi, self)}")
print(f"width in pixels (method is 'eyllanesc') = {get_width_eyllanesc(self._ax_)}")
return self._line_,
# Data source
# ------------
n = np.linspace(0, 499, 500)
d = 50 + 25 * (np.sin(n / 8.3)) + 10 * (np.sin(n / 7.5)) - 5 * (np.sin(n / 1.5))
i = 0
def get_next_datapoint():
global i
i += 1
if i > 499:
i = 0
return d[i]
if __name__ == "__main__":
qapp = QtWidgets.QApplication(sys.argv)
app = ApplicationWindow()
qapp.exec_()
Conclusions:
The correct answer is 550 pixels, which is what I measured on a printscreen. Now, I get the following output printed when I run the program:
width in pixels (first call, method is 'method_a') = 433.0972222222222
width in pixels (first call, method is 'eyllanesc') = 453.1749657377798
width in pixels (method is 'method_a') = 433.0972222222222
width in pixels (method is 'eyllanesc') = 453.1749657377798
width in pixels (method is 'method_a') = 540.0472222222223
width in pixels (method is 'eyllanesc') = 550.8908177249887
...
The first call for both methods gives the wrong result.
From the third(!) call onwards, they both give pretty good results, with the method from #Eyllanesc being the winner.
How do I fix the problem of the wrong result for the first call?
For an old answer I had to do calculation, which in your case is:
y_fake = 0
x_min, x_max = 0, 200
x_pixel_min, _ = self._ax_.transData.transform((x_min, y_fake))
x_pixel_max, _ = self._ax_.transData.transform((x_max, y_fake))
print(
f"The length in pixels between x_min: {x_min} and x_max: {x_max} is: {x_pixel_max - x_pixel_min}"
)
Note:
The calculations take into account what is painted, so in the first moments it is still being painted so the results are correct but our eyes cannot distinguish them. If you want to obtain the correct size without the animation you must calculate that value when the painting is stabilized, which is difficult to calculate, a workaround is to use a QTimer to make the measurement a moment later:
# ...
self._ax_ = self.figure.subplots()
self._ax_.set_ylim(ymin=self._y_range_[0], ymax=self._y_range_[1])
self._line_, = self._ax_.plot(x, y)
QtCore.QTimer.singleShot(100, self.calculate_length)
# ...
def calculate_length(self):
y_fake = 0
x_min, x_max = 0, 200
x_pixel_min, _ = self._ax_.transData.transform((x_min, y_fake))
x_pixel_max, _ = self._ax_.transData.transform((x_max, y_fake))
print(
f"The length in pixels between x_min: {x_min} and x_max: {x_max} is: {x_pixel_max - x_pixel_min}"
)

Categories