How to get Matplotlib shape for scrolling + resizing in Tkinter GUI

I have a Tkinter GUI that displays a Matplotlib graph (Python 2.7.3 with Matplotlib 1.2.0rc2) and allows the user to customize certain aspects of the graph. Charts tend to get large, so the figure is wrapped in a scrollable canvas. One aspect of setting up a chart is to resize it.

Now that the plot scrolls correctly, on the one hand, and resizing works, and on the other, the two operations do not work in combination. Below is a script to demonstrate the effect. (Sorry for the length, I could not reduce it). You can scroll through the plot (using the scroll bars), and you can make it smaller and larger (using the buttons). However, whenever you scroll, the reset figure matches its original size. Obviously, I would like the shape to not be resized using the scroll bars.

import math from Tkinter import Tk, Button, Frame, Canvas, Scrollbar import Tkconstants from matplotlib import pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg def addScrollingFigure(figure, frame): # set up a canvas with scrollbars canvas = Canvas(frame) canvas.grid(row=0, column=0, sticky=Tkconstants.NSEW) xScrollbar = Scrollbar(frame, orient=Tkconstants.HORIZONTAL) yScrollbar = Scrollbar(frame) xScrollbar.grid(row=1, column=0, sticky=Tkconstants.EW) yScrollbar.grid(row=0, column=1, sticky=Tkconstants.NS) canvas.config(xscrollcommand=xScrollbar.set) xScrollbar.config(command=canvas.xview) canvas.config(yscrollcommand=yScrollbar.set) yScrollbar.config(command=canvas.yview) # plug in the figure figAgg = FigureCanvasTkAgg(figure, canvas) mplCanvas = figAgg.get_tk_widget() mplCanvas.grid(sticky=Tkconstants.NSEW) # and connect figure with scrolling region canvas.create_window(0, 0, window=mplCanvas) canvas.config(scrollregion=canvas.bbox(Tkconstants.ALL)) def changeSize(figure, factor): oldSize = figure.get_size_inches() print "old size is", oldSize figure.set_size_inches([factor * s for s in oldSize]) print "new size is", figure.get_size_inches() print figure.canvas.draw() if __name__ == "__main__": root = Tk() root.rowconfigure(0, weight=1) root.columnconfigure(0, weight=1) frame = Frame(root) frame.grid(column=0, row=0, sticky=Tkconstants.NSEW) frame.rowconfigure(0, weight=1) frame.columnconfigure(0, weight=1) figure = plt.figure(dpi=150, figsize=(4, 4)) plt.plot(xrange(10), [math.sin(x) for x in xrange(10)]) addScrollingFigure(figure, frame) buttonFrame = Frame(root) buttonFrame.grid(row=0, column=1, sticky=Tkconstants.NS) biggerButton = Button(buttonFrame, text="larger", command=lambda : changeSize(figure, 1.5)) biggerButton.grid(column=0, row=0) smallerButton = Button(buttonFrame, text="smaller", command=lambda : changeSize(figure, .5)) smallerButton.grid(column=0, row=1) root.mainloop() 

I think I am missing something about how the plot and the scrolling canvas are related to each other; I tried reconfiguring the scroll canvas (with canvas.create_window(...) and canvas.config(...) ) after each changeSize call, but that didn't help. One of the alternatives I came across was to restore the entire setting (image, canvas, scrollbars) after each resize. (However, besides seeming a little cruel, I had a problem with the fact that I could not get rid of old figures that would be disposed of properly, which causes the program to accumulate quite a lot of memory over time.)

So, does anyone have any ideas on how to get these scrollbars to behave properly after resizing operations?

+4
source share
2 answers

on right; after discussing the scroll in this answer , I ended up with this:

.. and I think I managed to get a kind of scaling code that also scales (a few) labels and indents, so (approximately) the whole plot fits inside (note that the second image uses the "medium" scale from imgur)

smallestmediumlarge

For very small sizes, labels begin to disappear again - but it is still supported for different sizes.

Note that for the newer matplotlib (> = 1.1.1) there is a figure.tight_layout() function that makes the fields (but not the font size) for such cases (this is one subplot), but if you use the older matplotlib , you you can do figure.subplots_adjust(left=0.2, bottom=0.15, top=0.86) , which this example does; and was tested in:

 $ python2.7 -c 'import matplotlib; print(matplotlib.__version__)' 0.99.3 $ python3.2 -c 'import matplotlib; print(matplotlib.__version__)' 1.2.0 

(I tried to check if I can copy tight_layout for the older matplotlib - unfortunately, this requires a rather complicated set of functions included in tight_layout.py , which, in turn, require that the figure and axes also have specific specifications, not introduced in version 1.0.

Since subplots_adjust accepts relative parameters (from 0.0 to 1.0), we can basically just set them once - and hope that they remain for our desired scale range. For the rest (scaling fonts and invoices) see the code below:

 import math import sys if sys.version_info[0] < 3: from Tkinter import Tk, Button, Frame, Canvas, Scrollbar import Tkconstants else: from tkinter import Tk, Button, Frame, Canvas, Scrollbar import tkinter.constants as Tkconstants import matplotlib from matplotlib import pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import pprint, inspect frame = None canvas = None ax = None def printBboxes(label=""): global canvas, mplCanvas, interior, interior_id, cwid, figure print(" "+label, "canvas.bbox:", canvas.bbox(Tkconstants.ALL), "mplCanvas.bbox:", mplCanvas.bbox(Tkconstants.ALL), "subplotpars:", figure.subplotpars.__dict__ ) def addScrollingFigure(figure, frame): global canvas, mplCanvas, interior, interior_id, cwid # set up a canvas with scrollbars canvas = Canvas(frame) canvas.grid(row=1, column=1, sticky=Tkconstants.NSEW) xScrollbar = Scrollbar(frame, orient=Tkconstants.HORIZONTAL) yScrollbar = Scrollbar(frame) xScrollbar.grid(row=2, column=1, sticky=Tkconstants.EW) yScrollbar.grid(row=1, column=2, sticky=Tkconstants.NS) canvas.config(xscrollcommand=xScrollbar.set) xScrollbar.config(command=canvas.xview) canvas.config(yscrollcommand=yScrollbar.set) yScrollbar.config(command=canvas.yview) # plug in the figure figAgg = FigureCanvasTkAgg(figure, canvas) mplCanvas = figAgg.get_tk_widget() # and connect figure with scrolling region cwid = canvas.create_window(0, 0, window=mplCanvas, anchor=Tkconstants.NW) printBboxes("Init") changeSize(figure, 1) def changeSize(figure, factor): global canvas, mplCanvas, interior, interior_id, frame, cwid oldSize = figure.get_size_inches() print("old size is", oldSize) figure.set_size_inches([factor * s for s in oldSize]) wi,hi = [i*figure.dpi for i in figure.get_size_inches()] print("new size is", figure.get_size_inches()) print("new size pixels: ", wi,hi) mplCanvas.config(width=wi, height=hi) ; printBboxes("A") canvas.itemconfigure(cwid, width=wi, height=hi) ; printBboxes("B") canvas.config(scrollregion=canvas.bbox(Tkconstants.ALL),width=200,height=200) tz.set_fontsize(tz.get_fontsize()*factor) for item in ([ax.title, ax.xaxis.label, ax.yaxis.label] + ax.get_xticklabels() + ax.get_yticklabels()): item.set_fontsize(item.get_fontsize()*factor) ax.xaxis.labelpad = ax.xaxis.labelpad*factor ax.yaxis.labelpad = ax.yaxis.labelpad*factor #figure.tight_layout() # matplotlib > 1.1.1 figure.subplots_adjust(left=0.2, bottom=0.15, top=0.86) figure.canvas.draw() ; printBboxes("C") print() if __name__ == "__main__": global root, figure root = Tk() root.rowconfigure(1, weight=1) root.columnconfigure(1, weight=1) frame = Frame(root) frame.grid(column=1, row=1, sticky=Tkconstants.NSEW) frame.rowconfigure(1, weight=1) frame.columnconfigure(1, weight=1) figure = plt.figure(dpi=150, figsize=(4, 4)) ax = figure.add_subplot(111) ax.plot(range(10), [math.sin(x) for x in range(10)]) #tz = figure.text(0.5,0.975,'The master title',horizontalalignment='center', verticalalignment='top') tz = figure.suptitle('The master title') ax.set_title('Tk embedding') ax.set_xlabel('X axis label') ax.set_ylabel('Y label') print(tz.get_fontsize()) # 12.0 print(ax.title.get_fontsize(), ax.xaxis.label.get_fontsize(), ax.yaxis.label.get_fontsize()) # 14.4 12.0 12.0 addScrollingFigure(figure, frame) buttonFrame = Frame(root) buttonFrame.grid(row=1, column=2, sticky=Tkconstants.NS) biggerButton = Button(buttonFrame, text="larger", command=lambda : changeSize(figure, 1.2)) biggerButton.grid(column=1, row=1) smallerButton = Button(buttonFrame, text="smaller", command=lambda : changeSize(figure, 0.833)) smallerButton.grid(column=1, row=2) qButton = Button(buttonFrame, text="quit", command=lambda : sys.exit(0)) qButton.grid(column=1, row=3) root.mainloop() 
+3
source

I just ran into the same problem - and as far as I can see (experimenting), besides figure.set_size_inches() , you should also set the new size of mplCanvas and the window created by the canvas for it before doing figure.canvas.draw() (which also forces us to use global vars - or class definitions). In addition, there is no need for a β€œgrid” mplCanvas , apparently, since it is already a child of the canvas , which is already a β€œgrid”. And you probably want to bind NW, so each time you resize the plot redraws to 0.0 in the upper left corner.

Here's what worked for me (I also tried with the "internal" frame, as in the Python Tkinter scrollbar for the frame , but that didn't work; remains at the end of the snippet):

 import math import sys if sys.version_info[0] < 3: from Tkinter import Tk, Button, Frame, Canvas, Scrollbar import Tkconstants else: from tkinter import Tk, Button, Frame, Canvas, Scrollbar import tkinter.constants as Tkconstants from matplotlib import pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import pprint frame = None canvas = None def printBboxes(label=""): global canvas, mplCanvas, interior, interior_id, cwid print(" "+label, "canvas.bbox:", canvas.bbox(Tkconstants.ALL), "mplCanvas.bbox:", mplCanvas.bbox(Tkconstants.ALL)) def addScrollingFigure(figure, frame): global canvas, mplCanvas, interior, interior_id, cwid # set up a canvas with scrollbars canvas = Canvas(frame) canvas.grid(row=1, column=1, sticky=Tkconstants.NSEW) xScrollbar = Scrollbar(frame, orient=Tkconstants.HORIZONTAL) yScrollbar = Scrollbar(frame) xScrollbar.grid(row=2, column=1, sticky=Tkconstants.EW) yScrollbar.grid(row=1, column=2, sticky=Tkconstants.NS) canvas.config(xscrollcommand=xScrollbar.set) xScrollbar.config(command=canvas.xview) canvas.config(yscrollcommand=yScrollbar.set) yScrollbar.config(command=canvas.yview) # plug in the figure figAgg = FigureCanvasTkAgg(figure, canvas) mplCanvas = figAgg.get_tk_widget() #mplCanvas.grid(sticky=Tkconstants.NSEW) # and connect figure with scrolling region cwid = canvas.create_window(0, 0, window=mplCanvas, anchor=Tkconstants.NW) printBboxes("Init") canvas.config(scrollregion=canvas.bbox(Tkconstants.ALL),width=200,height=200) def changeSize(figure, factor): global canvas, mplCanvas, interior, interior_id, frame, cwid oldSize = figure.get_size_inches() print("old size is", oldSize) figure.set_size_inches([factor * s for s in oldSize]) wi,hi = [i*figure.dpi for i in figure.get_size_inches()] print("new size is", figure.get_size_inches()) print("new size pixels: ", wi,hi) mplCanvas.config(width=wi, height=hi) ; printBboxes("A") #mplCanvas.grid(sticky=Tkconstants.NSEW) canvas.itemconfigure(cwid, width=wi, height=hi) ; printBboxes("B") canvas.config(scrollregion=canvas.bbox(Tkconstants.ALL),width=200,height=200) figure.canvas.draw() ; printBboxes("C") print() if __name__ == "__main__": root = Tk() root.rowconfigure(1, weight=1) root.columnconfigure(1, weight=1) frame = Frame(root) frame.grid(column=1, row=1, sticky=Tkconstants.NSEW) frame.rowconfigure(1, weight=1) frame.columnconfigure(1, weight=1) figure = plt.figure(dpi=150, figsize=(4, 4)) plt.plot(range(10), [math.sin(x) for x in range(10)]) addScrollingFigure(figure, frame) buttonFrame = Frame(root) buttonFrame.grid(row=1, column=2, sticky=Tkconstants.NS) biggerButton = Button(buttonFrame, text="larger", command=lambda : changeSize(figure, 1.5)) biggerButton.grid(column=1, row=1) smallerButton = Button(buttonFrame, text="smaller", command=lambda : changeSize(figure, .5)) smallerButton.grid(column=1, row=2) root.mainloop() """ interior = Frame(canvas) #Frame(mplCanvas) #cannot interior_id = canvas.create_window(0, 0, window=interior)#, anchor=Tkconstants.NW) canvas.config(scrollregion=canvas.bbox("all"),width=200,height=200) canvas.itemconfigure(interior_id, width=canvas.winfo_width()) interior_id = canvas.create_window(0, 0, window=interior)#, anchor=Tkconstants.NW) canvas.config(scrollregion=canvas.bbox("all"),width=200,height=200) canvas.itemconfigure(interior_id, width=canvas.winfo_width()) """ 

It is interesting to note that mplCanvas will obey size if it becomes larger (as when clicking on β€œbigger”), but keep the old size if it becomes smaller:

 $ python2.7 test.py (' Init', 'canvas.bbox:', (0, 0, 610, 610), 'mplCanvas.bbox:', (0, 0, 600, 600)) ## here click "larger": ('old size is', array([ 4.06666667, 4.06666667])) ('new size is', array([ 6.1, 6.1])) ('new size pixels: ', 915.0, 915.0) (' A', 'canvas.bbox:', (0, 0, 925, 925), 'mplCanvas.bbox:', (0, 0, 926, 926)) (' B', 'canvas.bbox:', (0, 0, 915, 915), 'mplCanvas.bbox:', (0, 0, 926, 926)) (' C', 'canvas.bbox:', (0, 0, 915, 915), 'mplCanvas.bbox:', (0, 0, 926, 926)) () ## here click "larger": ('old size is', array([ 6.1, 6.1])) ('new size is', array([ 9.15, 9.15])) ('new size pixels: ', 1372.4999999999998, 1372.4999999999998) (' A', 'canvas.bbox:', (0, 0, 915, 915), 'mplCanvas.bbox:', (0, 0, 926, 926)) (' B', 'canvas.bbox:', (0, 0, 1372, 1372), 'mplCanvas.bbox:', (0, 0, 926, 926)) (' C', 'canvas.bbox:', (0, 0, 1372, 1372), 'mplCanvas.bbox:', (0, 0, 1372, 1372)) () ## here click "smaller": ('old size is', array([ 9.14666667, 9.14666667])) ('new size is', array([ 4.57333333, 4.57333333])) ('new size pixels: ', 686.0, 686.0) (' A', 'canvas.bbox:', (0, 0, 1372, 1372), 'mplCanvas.bbox:', (0, 0, 1372, 1372)) (' B', 'canvas.bbox:', (0, 0, 686, 686), 'mplCanvas.bbox:', (0, 0, 1372, 1372)) (' C', 'canvas.bbox:', (0, 0, 686, 686), 'mplCanvas.bbox:', (0, 0, 1372, 1372)) () 

The same mplCanvas behavior can also be seen in Python3.2 ... not sure if this is a bug, or I don’t understand something correctly either :)

Please also note that this scaling in this way does not handle changing font sizes of axes / ticks, etc. (fonts will try to stay the same size); this is what I can end up with with the code above (truncated ticks):

screenshot of code

... and it gets even worse if you add axis labels, etc.

Anyway, hope this helps,
Hooray!

+2
source

Source: https://habr.com/ru/post/1443672/


All Articles