I have a Canvas class that inherits from the FigureCanvas class in Matplotlib.
from matplotlib.backends.backend_qt5agg import FigureCanvas
class Canvas(FigureCanvas):
def __init__(self):
fig = Figure(figsize=(5, 3))
super().__init__(fig)
And I display canvas figures in the PyQt5 window. The canvas size changes depending on the size of the window. When I print these canvases, it always prints at the current size of the canvas (solutions in How do I change the size of figures drawn with Matplotlib? and Specify figure size in centimeter in matplotlib do not work).
However, I would like to print them in a fixed size, no matter what the current size is. As far as I could tell from the internet, I cannot specify size in print_figure() function. How can I print all the figures at the fixed size?
Related
When creating a PyQt5 tab widget the background colour is whiter than the background of a normal widget. I am looking for a way to get the tab exact background colour.
Some good examples that relate to this question are:
Get the background color of a widget - really
How to make the background color the same as the form background?
The most common suggestions to get the background colour is to use:
widget.palette().color(QtGui.QPalette.Background)
# alternatively with QtGui.QPalette.Base
None of which gets the exact colour. The first being too dark and the later too white.
Whether this works on not also depends on the style and system you are using. On my case it is on a Linux Mint setup but the app is also intended for windows.
===== Using this to set the background of a matplotlib figure =====
The use case for this question is to keep the facecolor of a matplotlib figure consistent with the widget it is embed on.
Here is my example:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import matplotlib.pyplot as plt
import matplotlib as mpl
class Window(QtWidgets.QMainWindow):
""" Class to use the data pattern widget in a separate window"""
def __init__(self, *args, **kwargs):
super(Window, self).__init__(*args, **kwargs)
# ===== Setup the window =====
self.setWindowTitle("background")
self.resize(600, 400)
self.maintabs = QtWidgets.QTabWidget(self)
self.setCentralWidget(self.maintabs)
self.page = QtWidgets.QWidget(self)
self.mpl_layout = QtWidgets.QHBoxLayout(self.page)
self.maintabs.addTab(self.page, 'Demo tab')
# ===== Set up matplotlib canvas =====
self.mpl_canvas = None
# get background color from widget and convert it to RBG
pyqt_bkg = self.maintabs.palette().color(QtGui.QPalette.Background).getRgbF()
mpl_bkg = mpl.colors.rgb2hex(pyqt_bkg)
self.pltfig = mpl.figure.Figure()
self.pltfig.set_facecolor(mpl_bkg) # uses the background of mainwindow and not tab
self.plot_ax = self.pltfig.add_subplot(111)
self.addmpl(self.pltfig)
def addmpl(self, fig):
self.mpl_canvas = FigureCanvas(fig)
self.mpl_layout.addWidget(self.mpl_canvas)
self.mpl_canvas.draw()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = Window()
w.show()
sys.exit(app.exec())
Resulting in,
The colors of a palette are only a reference for how the style will actually paint widgets. Some widgets use gradients to show their contents (consider buttons, which usually "glow" the color given for the palette Button role). Some styles even paint some widgets using roles that seem unrelated to that type of widget.
QTabWidget behaves in a similar way (depending on the style), so even if you get its background, it will probably painted with a slightly different color.
A possible solution is to set the background of the widget using stylesheet, while still keeping the palette reference:
self.maintabs.setStyleSheet('background: palette(window);')
Note that this will make all children widgets of maintabs use that plain background color.
you can use stylesheet to set the background-color, like these:
self.maintabs.setStyleSheet("background-color: rgb(0,0,59)")
I am making a gui in tkinter with live, embedded matplotlib graphs. I am using FigureCanvasTkAgg for the canvas, NavigationToolbar2Tk for the navigation bar, and FuncAnimation to handle periodic updates of the given source of data.
The callback tied to FuncAnimation resets the data on a given line (i.e. the return value from Axes.plot(...)) every invocation (i.e. Line2D.set_data(...)). The callback also redetermines and applies the appropriate x- and y-axis limits to fit the new data via
axis.relim()
axis.autoscale_view()
where axis is an instance of AxesSubplot.
Before the navigation bar is used, this works great; any new data added is appropriately reflected in the graph and the axes automatically re-scale to fit it, which was my goal.
The problem I am facing is that if any of the functions on the navigation bar are used (pan, zoom, etc.) the re-scaling fails to work any longer, meaning the graph may grow out of view and the user's only way to see new data is to manually pan over to it or to manually zoom out, which is undesirable.
Realistically, this functionality make sense since it would be annoying to, for example, try to zoom in a part of the plot only to have it zoom out immediately to refit the axes to new data, which is why I had intended to add a tkinter.Checkbutton to temporarily disable the re-scaling.
I've tried to look into the source for the navigation bar, and it seems to change state on the axes and canvas which I can only assume is the problem, but I have so far been unsuccessful at finding a way to "undo" these changes. If such a way exists, I would bind it to a tkinter.Button or something so the automatic re-scaling can be re-enabled.
How might I fix this problem?
Below is a minimal example that demonstrates this problem.
import math
import itertools
import tkinter as tk
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.animation import FuncAnimation
def xydata_generator(func, div):
for num in itertools.count():
num = num / div
yield num, func(num)
class Plot(tk.Frame):
def __init__(self, master, data_source, interval=100, *args, **kwargs):
super().__init__(master, *args, **kwargs)
self.data_source = data_source
self.figure = Figure((5, 5), 100)
self.canvas = FigureCanvasTkAgg(self.figure, self)
self.nav_bar = NavigationToolbar2Tk(self.canvas, self)
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
self.axis = self.figure.add_subplot(111)
self.x_data = []
self.y_data = []
self.line = self.axis.plot([], [])[0] # Axes.plot returns a list
# Set the data to a mutable type so we only need to append to it then force the line to invalidate its cache
self.line.set_data(self.x_data, self.y_data)
self.ani = FuncAnimation(self.figure, self.update_plot, interval=interval)
def update_plot(self, _):
x, y = next(self.data_source) # (realistically the data source wouldn't be restricted to be a generator)
# Because the Line2D object stores a reference to the two lists, we need only update the lists and signal
# that the line needs to be updated.
self.x_data.append(x)
self.y_data.append(y)
self.line.recache_always()
self._refit_artists()
def _refit_artists(self):
self.axis.relim()
self.axis.autoscale_view()
root = tk.Tk()
data = xydata_generator(math.sin, 5)
plot = Plot(root, data)
plot.pack(fill=tk.BOTH, expand=True)
root.mainloop()
Turns out to be pretty simple. To reset the axes so that the calls to Axes.relim() and Axes.autoscale_view() take effect, one simply needs to call Axes.set_autoscale_on(True). This must be repeated every time the functions on the navigation bar (pan, zoom, etc.) are used.
As part of a big GUI effort that is meant to plotting complex scientific figures I am trying to speed up interactions and figure updates. So far I've been using canvas.draw() method to update any changes to any drawn object in the figure.
I won't be able to reproduce an example code as it's a thousands lines of code but this is a snapshot of what I am dealing with
the above figure is a fairly congested example image with 3 Axes, contour plot, path, arrow, png image, different transparency objects, shadows, lines, fills, colorbar, etc
normally a user will be playing with a GUI like the one above to add, delete update or modify any plotted object.
For such a figure any modification is slow because it calls canvas.draw() at the backend.
#self.__canvas.Refresh()
#self.__canvas.Update()
###self.__canvas.update() # 'FigureCanvasWxAgg' object has no attribute 'update'
#self.__canvas.Refresh()
#self.__canvas.flush_events()
#self.__canvas.blit(self.__selectedAxes.bbox)
self.__canvas.draw()
I have tried using all the above but only canvas.draw results in updating the figure all the others won't. So far I am not sure how to speed up re-drawing the image after updating only one object.
Also, according to this post blit results in memory leaks. Did anyone tried to verify this hypothesis ?
Any suggestion is appreciated
Instead of using self.__canvas.draw()and redrawing all data on the plots, you can use blitting. By useage of blitting you can add specific new elements to a plot instead of redrawing the whole thing. This saves a massive amount of time.
In order to start blitting, the canvas has to be drawn at least once somewhere in your code. Otherwise there will be nothing 'to blit to'. So unfortunately you can't get fully rid of self.__canvas.draw().
To blit a certain element, for example a rectangle, you will first have to add the rectangle element to the axes. A rectangle is a matplotlib.patch and to add a patch to the axes you will have to use: self.axes.add_patch(rectangle). Once added you will need to draw it on the axes by using: self.axes.draw_artist(rectangle). After it has been drawn, you can blit it to the canvas using: self.canvas.blit(self.axes.bbox).
Save the plot with the blitted element as a background image using: self.background = self.canvas.copy_from_bbox(self.axes.bbox) and restore it to your canvas using: self.canvas.restore_region(self.background).
Some example code that blits a Rectangle to the canvas:
import matplotlib
matplotlib.use('WXAgg')
from matplotlib.figure import Figure
from matplotlib.backends.backend.wxagg import FigureCanvasWxAgg as FigureCanvas
import wx
class Panel(wx.Frame):
wx.Frame.__init__(self, parent, id, 'Title')
self.figure = Figure()
self.axes = self.figure.add_subplot(111)
self.canvas = FigureCanvas(self, -1, self.figure)
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(self.canvas, proportion=1, flag= wx.ALL | wx.GROW)
"""Plot data and stuff to canvas...."""
self.canvas.draw()
self.background = self.canvas.copy_from_bbox(self.axes.bbox)
square = matplotlib.patches.Rectangle((xPos,yPos), width, height)
self.canvas.restore_region(self.background)
self.axes.add_patch(square)
self.axes.draw_artist(square)
self.canvas.blit(self.axes.bbox)
self.background = self.canvas.copy_from_bbox(self.axes.bbox)
Can be I made some typos. But you will get the idea of it.
I have embedded a matplotlib plot in my GUI like this:
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
fig = DataPlot()
self.canvas = FigureCanvas(fig)
self.vL_Frame.addWidget(self.canvas)
self.canvas.draw()
For the desired functionality of this plot, it is important know the actual cursor position in this plot → x and y position like in the default toolbar.
As I just need the cursor position and none of the icons of the default toolbar, I made my own toolbar like this (which I found here: How to modify the navigation toolbar easily in a matplotlib figure window?):
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
class MyToolbar(NavigationToolbar):
# only display the buttons we need
toolitems = [t for t in NavigationToolbar.toolitems if
t[0] in ()]
def __init__(self, *args, **kwargs):
super(MyToolbar, self).__init__(*args, **kwargs)
self.layout().takeAt(0)
self.toolbar = MyToolbar(self.canvas, self)
self.vL_cursor_Frame.addWidget(self.toolbar)
With that I got my cursor position, but it is 'cut' somehow. I use quotation marks, because I know that it happens because I don't show the icons from the default toolbar.
This is my result I couldn't make a screenshot with shown cursor position, but it appears next to the 'Save' button.
Does anyone know how to show the full cursor position? And how to delete this frame or shadow around my cursor position/customized toolbar?
Thank you in advance :)
I'm using Windows XP v3/Python 2.7 with Canopy and Anaconda package managers/editors.
I am using Python/Matplotlib to produce some Bland-Altman plots (statistical scatter plots) for publication.
After processing the data, the plt.show() command opens a new "Figure" window containing the plot, which looks fine.
I want to be able to use the dynamic pan and zoom commands in this window to interactively optimise the appearance of my plot, then save it as it appears in the window as a high resolution press-quality png image (400-600 dpi, 7 x 5 inches).
The default setting for saving images from the "Figure" window appears to be set to screen resolution (800 x 600 pixels), and I cannot find any options in this window which allow me to change these settings.
I've read other posts on this forum which explain how to directly save a plot from Python in higher resolution by using the following commands to manipulate dpi and image size, e.g.:
plt.figure(figsize=(18, 12), dpi=400)
plt.savefig("myplot.png", dpi = 400)
However, this is not the solution that I'm looking for; as I want to be able to modify the plot using the dynamic pan and zoom features of the "Figure" window before saving in a higher resolution than the default screen resolution.
I'd be grateful for your help.
Many thanks in anticipation & Happy New Year.
Dave
(UK)
Try this:
Determine how to set width and height using a pixels-to-inches converter, like in the following matplotlib documentation. Then try:
import matplotlib.pyplot as plt
fig = plt.figure(frameon=False)
fig.set_size_inches(width,height)
I had this issue in spyder and found changing the value in Preferences > iPython Console > Inline Backend > Resolution changes the resolution when I save figures from the built in window viewing application.
One may register an event upon a key press that would save the figure with some previously given size and dpi. The following uses a class that stores some figsize and dpi and upon pressing t wll change the figure size and dpi of the figure. It will then save this figure and restore the old size and dpi such that the figure on screen remains unchanged.
import matplotlib
matplotlib.use("TkAgg")
import matplotlib.pyplot as plt
fig,ax=plt.subplots()
ax.plot([1,3,1])
class AnySizeSaver():
def __init__(self, fig=None, figsize=None, dpi=None, filename=None):
if not fig: fig=plt.gcf()
self.fig = fig
if not figsize: figsize=self.fig.get_size_inches()
self.figsize=figsize
if not dpi: dpi=self.fig.dpi
self.dpi=dpi
if not filename: filename="myplot.png"
self.filename=filename
self.cid = self.fig.canvas.mpl_connect("key_press_event", self.key_press)
def key_press(self, event):
if event.key == "t":
self.save()
def save(self):
oldfigsize = self.fig.get_size_inches()
olddpi=self.fig.dpi
self.fig.set_size_inches(self.figsize)
self.fig.set_dpi(self.dpi)
self.fig.savefig(self.filename, dpi=self.dpi)
self.fig.set_size_inches(oldfigsize, forward=True)
self.fig.set_dpi(olddpi)
self.fig.canvas.draw_idle()
print(fig.get_size_inches())
ass = AnySizeSaver(fig=fig, figsize=(3,3), dpi=600)
plt.show()