Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug (?) - matplotlib window grows with repeated pressing of "plot" button #5030

Closed
4 of 7 tasks
rowlesmr opened this issue Dec 16, 2021 · 9 comments
Closed
4 of 7 tasks
Labels
Port - TK PySimpleGUI question Further information is requested

Comments

@rowlesmr
Copy link

Type of Issue Bug - PySimpleGUI or matplotlib?

Clicking "Plot" with Matplotlib >= 3.5.0 will make the plot window grow. With matplotlib==3.4.3, the plot window stays the same size.

Not sure if it's a PySimpleGUI thing or not, but I haven't been able to replicate it with pure mpl code


Operating System

Windows 10

PySimpleGUI Port

tkinter


Versions

Version information can be obtained by calling sg.main_get_debug_data()
Or you can print each version shown in ()

Python version: 3.9.4 (tags/v3.9.4:1f2e308, Apr 6 2021, 13:40:21) [MSC v.1928 64 bit (AMD64)]
port: tkinter
tkinter version: 8.6.9
PySimpleGUI version: 4.55.1
PySimpleGUI filename: C:\Users_____\Documents\GitHub\empty-venv\lib\site-packages\PySimpleGUI\PySimpleGUI.py

Matplotlib 3.5.1 and 3.4.3


Troubleshooting

These items may solve your problem. Please check those you've done by changing - [ ] to - [X]

  • Searched main docs for your problem www.PySimpleGUI.org
  • Looked for Demo Programs that are similar to your goal Demos.PySimpleGUI.org
  • If not tkinter - looked for Demo Programs for specific port
  • For non tkinter - Looked at readme for your specific port if not PySimpleGUI (Qt, WX, Remi)
  • Run your program outside of your debugger (from a command line)
  • Searched through Issues (open and closed) to see if already reported Issues.PySimpleGUI.org
  • Tried using the PySimpleGUI.py file on GitHub. Your problem may have already been fixed but not released

Detailed Description

When upgrading from matplotlib 3.4.3 to 3.5.1, the behaviour of the matplotlib fig changed. (Code below is derived from one of the matplotlib demo programs and #4860). Upon repeated pressing of the "Plot" button, the plot grows to the right and down with the DPI increasing exponentially.

With matplotlib 3.4.3 the behaviour is nice. In v3.5.1, the plot grows.

I haven't been able to reproduce the behaviour in pure matplotlib code, so I was wondering if there is something going on in the background in PySimpleGUI.

Code To Duplicate

Code taken from #4860 - added print in event loop to show figure dpi

import PySimpleGUI as sg
import numpy as np

"""
    Embedding the Matplotlib toolbar into your application
"""

# ------------------------------- This is to include a matplotlib figure in a Tkinter canvas
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk


def draw_figure_w_toolbar(canvas, fig, canvas_toolbar):
    if canvas.children:
        for child in canvas.winfo_children():
            child.destroy()
    if canvas_toolbar.children:
        for child in canvas_toolbar.winfo_children():
            child.destroy()
    figure_canvas_agg = FigureCanvasTkAgg(fig, master=canvas)
    figure_canvas_agg.draw()
    toolbar = Toolbar(figure_canvas_agg, canvas_toolbar)
    toolbar.update()
    figure_canvas_agg.get_tk_widget().pack(side='right', fill='both', expand=1)


class Toolbar(NavigationToolbar2Tk):
    def __init__(self, *args, **kwargs):
        super(Toolbar, self).__init__(*args, **kwargs)


# ------------------------------- you have to play with this size
fig = plt.figure(num=1)
DPI = fig.dpi
fig.set_size_inches(400 * 2 / float(DPI), 400 / float(DPI))
# -------------------------------

# ------------------------------- PySimpleGUI CODE

sg.set_options(scaling=DPI / 72)

layout = [
    [sg.T('Graph: y=sin(x)')],
    [sg.B('Plot'), sg.B('Exit')],
    [sg.T('Controls:')],
    [sg.Sizer(v_pixels=60), sg.Canvas(key='controls_cv')],
    [sg.T('Figure:')],
    [sg.Column(
        layout=[
            [sg.Canvas(key='fig_cv',
                       # it's important that you set this size
                       size=(400 * 2, 400),
                       expand_x=True,
                       expand_y=True,
                       )]
        ],
        background_color='#DAE0E6',
        pad=(0, 0),
        expand_x=True,
        expand_y=True,
    )],
    [sg.B('Alive?')]

]

window = sg.Window('Graph with controls', layout, resizable=True, finalize=True)

while True:
    event, values = window.read()

    print(f"{fig.get_dpi()=}")
    if event in (sg.WIN_CLOSED, 'Exit'):  # always,  always give a way out!
        break
    elif event == 'Plot':
        # ------------------------------- PASTE YOUR MATPLOTLIB CODE HERE
        x = np.linspace(0, 2 * np.pi)
        y = np.sin(x)
        plt.plot(x, y)
        plt.title('y=sin(x)')
        plt.xlabel('X')
        plt.ylabel('Y')
        plt.grid()

        # ------------------------------- Instead of plt.show()
        draw_figure_w_toolbar(window['fig_cv'].TKCanvas, fig, window['controls_cv'].TKCanvas)

window.close()

Watcha Makin?

pdCIFplotter - it's on PyPI

@jason990420
Copy link
Collaborator

jason990420 commented Dec 16, 2021

IMO, it is cause by the figure maybe also deleted in function draw_figure_w_toolbar, and will be rebuild later.
Same situation happened in tkinter code, not sure what's wrong inside both of matplotlib and tkinter.

Here, just clear the content of figure and not to delete it.
Update code as following

from random import randint
import PySimpleGUI as sg
import numpy as np

"""
    Embedding the Matplotlib toolbar into your application
"""

# ------------------------------- This is to include a matplotlib figure in a Tkinter canvas
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk


def draw_figure_w_toolbar(canvas, fig, canvas_toolbar):
    figure_canvas_agg = FigureCanvasTkAgg(fig, master=canvas)
    figure_canvas_agg.draw()
    toolbar = Toolbar(figure_canvas_agg, canvas_toolbar)
    toolbar.update()
    figure_canvas_agg.get_tk_widget().pack(side='right', fill='both', expand=1)
    return figure_canvas_agg, toolbar

def draw(figure_canvas_agg, toolbar):
    figure_canvas_agg.draw()
    toolbar.update()

class Toolbar(NavigationToolbar2Tk):
    def __init__(self, *args, **kwargs):
        super(Toolbar, self).__init__(*args, **kwargs)


# ------------------------------- you have to play with this size
fig = plt.figure(num=1)
DPI = fig.dpi
fig.set_size_inches(400 * 2 / float(DPI), 400 / float(DPI))
# -------------------------------

# ------------------------------- PySimpleGUI CODE

sg.set_options(scaling=DPI / 72)

layout = [
    [sg.T('Graph: y=sin(x)')],
    [sg.B('Plot'), sg.B('Exit')],
    [sg.T('Controls:')],
    [sg.Sizer(v_pixels=60), sg.Canvas(key='controls_cv')],
    [sg.T('Figure:')],
    [sg.Column(
        layout=[
            [sg.Canvas(key='fig_cv',
                       # it's important that you set this size
                       size=(400 * 2, 400),
                       expand_x=True,
                       expand_y=True,
                       )]
        ],
        background_color='#DAE0E6',
        pad=(0, 0),
        expand_x=True,
        expand_y=True,
    )],
    [sg.B('Alive?')]

]

window = sg.Window('Graph with controls', layout, resizable=True, finalize=True)
figure_canvas_agg, toolbar = draw_figure_w_toolbar(window['fig_cv'].TKCanvas, fig, window['controls_cv'].TKCanvas)
while True:
    event, values = window.read()

    print(f"{fig.get_dpi()=}")
    if event in (sg.WIN_CLOSED, 'Exit'):  # always,  always give a way out!
        break
    elif event == 'Plot':
        # ------------------------------- PASTE YOUR MATPLOTLIB CODE HERE
        plt.clf()
        delta = randint(0, 359)/np.pi
        x = np.linspace(0, 2 * np.pi)
        y = np.sin(x + delta)
        plt.plot(x, y)
        plt.title('y=sin(x)')
        plt.xlabel('X')
        plt.ylabel('Y')
        plt.grid()
        draw(figure_canvas_agg, toolbar)

window.close()

@jason990420 jason990420 added Port - TK PySimpleGUI question Further information is requested labels Dec 16, 2021
@rowlesmr
Copy link
Author

rowlesmr commented Dec 17, 2021

That does fix it, and is probably a better way to do it (rather than redrawing everything.

I've just found that removing the line

sg.set_options(scaling=DPI / 72)

from the original code also fixes the problem.

.

Argh! and now I can't reproduce the original issue! I have changed my monitor set up, so that might be a thing...
edit: yes, monitor change triggers the original issue. On my laptop-only screen, it grows. Through my docking station, it works nicely.

@rowlesmr
Copy link
Author

OK, some more playing around, and I've gotten reproducible failing and passing of the plot as per my original code.

If the options set by sg.set_options() include scaling and/or dpi_awareness, the bug is there. If not, it is not.

I don't have a non-windows machine to test it on.

.

and now that behaviour has just changed, and dpi_awareness doesn't seem to affect it! but scaling still does.

and now I've disconnected and reconnect my monitors, and the absence or presence of set_options doesn't seem to matter for my test code (above), and the absence of options does matter for my real code,

arg. I hate this type of bug. Something that matplotlib has done with dpi calculations has broken something, but I don't know how to even begin to write up a bug report for this...

@jason990420
Copy link
Collaborator

jason990420 commented Dec 17, 2021

Removing the following line doesn't fix this issue in my WIN10.

sg.set_options(scaling=DPI / 72)

Lot of conditions tested, only my previous code fix it.

Destroy widget maybe not clear everything in memory and it still need to redraw everything, plt.clf() should be better than it.

@rowlesmr
Copy link
Author

I just tried with your code, and I agree. The behaviour is different between the test code and the actual code I want to work.

I'm going to leave it alone for a bit to think some more on it, and then go through my actual code to try and construct a MWE.

@rowlesmr
Copy link
Author

rowlesmr commented Dec 17, 2021

Ok. A MWE is below that follows the structure of how I've written things in my actual program.

I think the problem arises because of the scaling set for the screen in the display properties. With dpi_awareness turned on, screen scale set to 125%, and using matplotlib 3.5.1, the plot gets bigger with each press of the plot button. The very first printed dpi is 100, but thereafter, it is 125. If I set the screen scale to 150 or 175%, then this value goes to 150 or 175. Crucially, if the screen scale is 100%, then the plot is well-behaved.

It makes no difference if the ctypes.windll.shcore.SetProcessDpiAwareness(1) level is set at 1 or 2. 0 turns it off, so it works then, just as it would with dpi_awareness not set.

https://youtu.be/7c_Rt_BDfnM

import PySimpleGUI as sg
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import matplotlib.pyplot as plt
import matplotlib as mpl


class PlotData:
    def __init__(self, m_canvas_x, m_canvas_y):
        self.canvas_x = m_canvas_x
        self.canvas_y = m_canvas_y

    def plot(self, x, y, fig):
        dpi = plt.gcf().get_dpi()
        print(f"{dpi=}")

        if fig:
            plt.close(fig)
        fig, ax = plt.subplots(1, 1)
        fig = plt.gcf()
        fig.set_size_inches(self.canvas_x / float(dpi), self.canvas_y / float(dpi))
        print(f"{fig.get_size_inches()=}")
        fig.set_tight_layout(True)
        plt.margins(x=0)

        ax.plot(x, y, label="a legend entry")
        ax.set_xlabel("X ordinate")
        ax.set_ylabel("Y ordinate")
        plt.title("Title", loc="left")

        return fig


# if this next line is there, and you're using matplotlib 3.5.1,
#  the plot grows when you press "plot" multiple times.
#  matplotlib v3.4.3 works fine.
#  What does this line do? If dpi_awarenes is not None, then the following is activated in PySimpleGUI
#         if running_windows():
#             if platform.release() == "7":
#                 ctypes.windll.user32.SetProcessDPIAware()
#             elif platform.release() == "8" or platform.release() == "10":
#                 ctypes.windll.shcore.SetProcessDpiAwareness(1)
sg.set_options(dpi_awareness=True)

canvas_x = 600
canvas_y = 300

plot = PlotData(canvas_x, canvas_y)
figure = None
figure_agg = None

x_data = [1, 2, 3, 4, 5]
y_data = [[2, 6, 4, 7, 9], [7, 3, 7, 3, 5]]


def update_plot(x, y, window):
    global figure_agg, figure
    figure = plot.plot(x, y, figure)
    figure_agg = draw_figure_w_toolbar(window["single_plot"].TKCanvas, figure,
                                       window["single_matplotlib_controls"].TKCanvas)


# https://github.com/PySimpleGUI/PySimpleGUI/blob/master/DemoPrograms/Demo_Matplotlib_Embedded_Toolbar.py
# https://github.com/PySimpleGUI/PySimpleGUI/issues/3989#issuecomment-794005240
def draw_figure_w_toolbar(canvas, figure, canvas_toolbar):
    print(f"{canvas.winfo_width()=}, {canvas.winfo_height()=}")
    if canvas.children:
        for child in canvas.winfo_children():
            child.destroy()
    if canvas_toolbar.children:
        for child in canvas_toolbar.winfo_children():
            child.destroy()
    figure_canvas_agg = FigureCanvasTkAgg(figure, master=canvas)
    figure_canvas_agg.draw()
    toolbar = Toolbar(figure_canvas_agg, canvas_toolbar)
    toolbar.update()
    figure_canvas_agg.get_tk_widget().pack(side='right', fill='both', expand=1)
    return figure_canvas_agg


# https://github.com/PySimpleGUI/PySimpleGUI/blob/master/DemoPrograms/Demo_Matplotlib_Embedded_Toolbar.py
class Toolbar(NavigationToolbar2Tk):
    def __init__(self, *args, **kwargs):
        super(Toolbar, self).__init__(*args, **kwargs)


layout = \
    [
        [sg.Button("Plot", key="plot_data"), sg.Button("Exit", key="exit")],
        [sg.Column(layout=[[sg.Canvas(size=(canvas_x, canvas_y), key="single_plot", expand_x=True, expand_y=True)]], pad=(0, 0), expand_x=True, expand_y=True)],
        [sg.Sizer(v_pixels=60), sg.Canvas(key="single_matplotlib_controls")]
    ]


def gui() -> None:
    window = sg.Window("Data plotter", layout, finalize=True, resizable=True)
    print(f"{mpl.__version__=}")
    i = 0
    while True:
        event, values = window.read()
        i += 1
        print(f"{i=}")
        if event in (None, "exit"):
            break
        elif event == "plot_data":
            update_plot(x_data, y_data[i % 2], window)


if __name__ == "__main__":
    gui()

@jason990420
Copy link
Collaborator

jason990420 commented Dec 17, 2021

Not spent much time on detail about scale, DPI and dpi_awareness.
All I have to confirm is my code work in my WIN10, and all code to destroy children of Canvas failed, including the latest one.

@rowlesmr
Copy link
Author

It's a matplotlib bug.

matplotlib/matplotlib#21875
https://discourse.matplotlib.org/t/figures-are-wrong-size-after-using-tk-filedialog-askopenfilename/22458/2

The last comment on the discourse thread is particularly telling.

@P-Kaempf
Copy link

P-Kaempf commented Feb 3, 2022

Hi rowlesmr,

try to open the PySimpleGUI window with a fixed size, like that:

def gui() -> None:
window = sg.Window("Data plotter", layout, size=(640,480), finalize=True, resizable=True)
print(f"{mpl.version=}")

at least on a Mac the plot will stay the same size, regardless how often "Plot" is clicked. The plot is drawn too small at first but then with the right size an instance later. Please try yourself!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Port - TK PySimpleGUI question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants