How to get a Matplotlib figure to scroll + resize properly in a Tkinter GUI

后端 未结 2 1920
小蘑菇
小蘑菇 2020-12-15 13:59

I have a Tkinter GUI which displays a Matplotlib plot (Python 2.7.3 with Matplotlib 1.2.0rc2) and lets the user configure certain aspects of the plot. The plots tend to get

相关标签:
2条回答
  • 2020-12-15 14:22

    I just bumped in the same problem - and as far as I can see (by experimentation), beyond figure.set_size_inches(), you must also set the new size of mplCanvas and of the canvas-created window for it, before doing figure.canvas.draw() (which then also forces one to use global vars - or class definitions). Also, no need to "grid" mplCanvas apparently - as it is already a child of canvas, which is already "grid"ded. And probably want to anchor NW, so at each resize, the plot is redrawn at 0,0 at top-left corner.

    Here is what worked for me (I also tried with "internal" frame as in Python Tkinter scrollbar for frame, but that didn't work; some of that is left at end of 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())
    """
    

    An interesting note is that mplCanvas will obey the sizing if it grows larger (as in click on "larger") - but keep the old size if it gets 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))
    ()
    

    Same behavior of mplCanvas can be seen in Python3.2, as well... not sure if this is a bug of sorts, or I too am not understanding something right :)

    Note also that this scaling in this way doesn't handle resizing of fonts of axes/tics etc (fonts will try to remain the same size); this is what I can eventually get with above code (truncated tics):

    screenshot of code

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

    Anyways, hope this helps,
    Cheers!

    0 讨论(0)
  • 2020-12-15 14:28

    Right; after the scrollbar discussion in this answer, I ended up going through this:

    • How do I set the figure title and axes labels font size in Matplotlib?
    • How to change the font size on a matplotlib plot
    • Python subplots leaving space for common axis labels
    • Exact figure size in matplotlib with title, axis labels
    • Matplotlib subplots_adjust hspace so titles and xlabels don't overlap?

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

    smallest medium large

    For very small sizes, labels again start disappearing - but it still holds OK for a range of sizes.

    Note that for newer matplotlib (>= 1.1.1), there is a function figure.tight_layout() that does the margins (but not the font size) for cases like this (it is a single subplot) - but if you're using an older matplotlib, you can do figure.subplots_adjust(left=0.2, bottom=0.15, top=0.86) which is what this example does; and has been 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 did try to see if I can copy tight_layout for older matplotlib - unfortunately, it requires a rather complex set of functions included from tight_layout.py, which in turn require that Figure and Axes also have specific specifications, not present in v.0.99)

    Since subplots_adjust takes relative parameters (from 0.0 to 1.0), we can in principle just set them once - and hope they hold for our desired scale range. For the rest (scaling of fonts and labelpad) 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()
    
    0 讨论(0)
提交回复
热议问题