Skip to content

[Bug]: Figure does not scale to window size #22822

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

Open
thomas-haslwanter opened this issue Apr 11, 2022 · 6 comments
Open

[Bug]: Figure does not scale to window size #22822

thomas-haslwanter opened this issue Apr 11, 2022 · 6 comments
Labels

Comments

@thomas-haslwanter
Copy link

Bug summary

When changing the window size of a figure, the figure does not scale immediately, so part of it is obscured. Only after a manual re-scaling (even if minute) the figure is displayed correctly.

Code for reproduction

import numpy as np
import matplotlib.pyplot as plt

# Generate some data
t = np.arange(0, 10, 0.1)
x = np.cos(t)

fig, ax = plt.subplots(1,1)

# Put the figure in the top-left half of the screen
cur_win = fig.canvas.manager.window     # Current window
screen_size = cur_win.wm_maxsize()      # Get the screen size
# top-left half of the window:
(pos_x, pos_y, width, height) = (0, 0, screen_size[0]//2, screen_size[1]//2)
position = f'{width}x{height}+{pos_x}+{pos_y}'  # Window geometry string
cur_win.geometry(position)

ax.plot(t, x)
plt.show()

Actual outcome

scaling_problem_01

Expected outcome

When the size of the window is then MANUALLY changed, the figure immediately changes to the accurate scale:
scaling_problem_02

Additional information

The bug appears from Matplotlib 3.5.1 onward

Operating system

Windows 11

Matplotlib Version

3.5.1

Matplotlib Backend

TkAgg

Python version

3.9.10

Jupyter version

No response

Installation

No response

@oscargus
Copy link
Member

For what it is worth: this does not seem to be an issue on main using Linux (CentOS 7). So either it has been fixed in main or it is a platform dependent issue.

@tacaswell
Copy link
Member

I can reproduce this (with the additional issue that it bigger than half of my screen but it may be half of the size of the union of my two screens?). I think this is related to hi-dpi screens.

If I do:

import numpy as np
import matplotlib.pyplot as plt

# Generate some data
t = np.arange(0, 10, 0.1)
x = np.cos(t)

fig, ax = plt.subplots(1,1)
print(fig.canvas.device_pixel_ratio)
fig.canvas.mpl_connect('resize_event', lambda *args, **kwargs: print(args, kwargs))
# Put the figure in the top-left half of the screen
cur_win = fig.canvas.manager.window     # Current window
screen_size = cur_win.wm_maxsize()      # Get the screen size
# top-left half of the window:
(pos_x, pos_y, width, height) = (0, 0, screen_size[0]//2, screen_size[1]//2)
position = f'{width}x{height}+{pos_x}+{pos_y}'  # Window geometry string
cur_win.geometry(position)

ax.plot(t, x)
plt.show()
print(fig.canvas.device_pixel_ratio)

I see

1
(<matplotlib.backend_bases.ResizeEvent object at 0x7f227e56b590>,) {}
1.68

and the 1.68 is the value that comes out of my system via

ratio = round(self._tkcanvas.tk.call('tk', 'scaling') / (96 / 72), 2)
(which is seems to be computing by looking and xrandr computing from the reported physical sizes (in mm and pixels).

I think what is happening here is:

  • you create (but do not show) the tk window, the device_pixel_ratio is 1
  • you set the window size in pixels
  • Matplotlib sorts out what size that is in inches (because figures are ground-truth sized in inches)
  • when the window is shown by tk
  • on activate we update the device pixel ratio (which is now 1.68 on my machine)
  • this does not trigger a resize of the window (in pixels) or the figure (in inches)
  • when we render the figure we use an effectively higher dpi -> too many pixels -> only top left is shown
  • when you tweak the window size we go through the dance again, but this time using the right device pixel ratio which makes the figure smaller in inches, but we then render the right number of pixels

If I change the code to

import numpy as np
import matplotlib.pyplot as plt

# Generate some data
t = np.arange(0, 10, 0.1)
x = np.cos(t)

fig, ax = plt.subplots(1,1)
print(fig.canvas.device_pixel_ratio)
fig.canvas.mpl_connect('resize_event', lambda *args, **kwargs: print(args, kwargs))
# Put the figure in the top-left half of the screen
cur_win = fig.canvas.manager.window     # Current window
screen_size = cur_win.wm_maxsize()      # Get the screen size
# top-left half of the window:
(pos_x, pos_y, width, height) = (0, 0, screen_size[0]//2, screen_size[1]//2)
position = f'{width}x{height}+{pos_x}+{pos_y}'  # Window geometry string
plt.pause(.1)  # <- NOTE THIS LINE it forces the window to display before we resize it!
cur_win.geometry(position)

ax.plot(t, x)
plt.show(); print(fig.canvas.device_pixel_ratio)

I get a figure that shows the whole figure and is placed at the top-left of the theoretical union of my two screens (which are set up like |- ).

attn @QuLogic This is related to the issues we were chasing down last week. attn @richardsheridan Should we be ignoring this scaling on *nix?

@jklymak
Copy link
Member

jklymak commented Apr 12, 2022

That all looks right, but I'm not sure we need to support changing the window size without changing the figure size. In this case doesn't the user just need to fire the window resize callback manually since they are changing the window size manually?

@tacaswell
Copy link
Member

The issue is that when the window size is initially changed we have bad information about what the effective dpi should be so compute the wrong figure size to forward on.

I think the core of the issue is that Tk handles the scaling / hi-dpi differently than the other backends.

@richardsheridan
Copy link
Contributor

That all looks right, but I'm not sure we need to support changing the window size without changing the figure size. In this case doesn't the user just need to fire the window resize callback manually since they are changing the window size manually?

I'm leaning toward this. FigureCanvasTk.resize doesn't seem to be the right API though. It's necessary to call either resize or the geometry method after the event loop runs a bit... something between 10 and 100 ms on my machine, kind of awkward. I had some success using mpl_connect:

fig.canvas.mpl_connect('resize_event', lambda *args, **kwargs: print(args, kwargs) or cur_win.geometry(position))

which has the amusing side-effect of immediately resetting the window geometry if you try to resize it with the mouse. A real workaround would need to immediately un-register the callback.

@QuLogic
Copy link
Member

QuLogic commented Apr 13, 2022

I think the core of the issue is that Tk handles the scaling / hi-dpi differently than the other backends.

Yes, on Windows, the 'standard' monitor is 96 DPI, and for monitors with HiDPI scaling, we then increase that value by whatever that scale is. The 'scaling' value we use within Tk is the DPI over a 'standard' 72 DPI.

This does not appear to be the case on Linux, and possibly is not true on macOS either. The Tk 'scaling' value is the true DPI (or at least whatever X wants to tell it it is) over 72 DPI and so we end up setting a fake HiDPI mode, even if that's not enabled on the system. The initial mixed DPI support was only intended for Windows, and so Linux/macOS don't even rescale properly when moving across monitors. I still haven't figured out how to do the rescaling on those systems (and I don't think it's really possible on X.)

All that means, I think we need to ignore 'scaling' on non-Windows systems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants