Tkinter DPI bug depends on how import is entered? - python

I have a weird question. In essence: why does the way I enter the pyplot import line into iPython influence my plot?!
Try this (on Windows 10):
Use a high-DPI main monitor set to 200%
Start a fresh iPython console
Enter the three blocks of code below one by one, using any of these ways for any block:
just typing, OR
pasting: pressing Ctrl+V, OR
pasting: clicking the right mouse button, OR
pressing Up to get it from history
To execute each block, just press Enter
Check whether the whole axis is visible
Close iPython and try again
The minimal reproducible example:
import matplotlib as mpl
mpl.use('TkAgg')
import matplotlib.pyplot as plt
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(2)
fig = plt.figure()
w = fig.canvas.manager.window
print(w.winfo_screenwidth(), w.winfo_screenheight())
w.wm_geometry('1600x800+60+0')
fig.canvas.flush_events()
plt.plot(1, 'x')
plt.show(block=False)
Now the result depends on how the first two blocks have been entered. If one or both of them used option 1 or 2, an incorrect monitor size is reported (1920x1080), but I do get a 'correct' plot (with tiny fonts):
If both import lines have been entered using option 3 or 4 however, the reported monitor size is correct (3840x2160), but I get incorrectly sized/zoomed plots (with normal font size):
The behavior depends on how import lines (or the 'run script' they are in) are entered!? Not how they are run, that's just Enter.
Any idea what causes this? Or how to fix it? Other than remembering to always do Ctrl+V instead of history or right click...
Is this a bug I can report somewhere?
Changing the window size manually a bit afterwards makes the axis fit inside the window. But I would like to script it. And the manual resize does not fix all differences: options 1 and 2 keep using tiny fonts.
Explicitly resetting SetProcessDpiAwareness(0) prevents the issue (1 and 2 keep it): monitor reported as 1920x1080, axes fit inside figure, but a larger window and normal size fonts.
Specifying dpi=192 (or something) explicitly with plt.figure() does not help or change anything.
The used backend is the TkAgg Windows default. The exact same happens with TkCairo. The WxAgg and QtAgg alternatives work ok, using window.SetClientSize(1600, 800) resp. manager.resize(1600, 800) (they work like SetProcessDpiAwareness(0)). So I guess the issue is specific to tkinter.
Using python.exe (instead of ipython.exe) always shows the incorrect version. AFAICS I have not changed the High DPI compatibility settings for iPython.

Almost all differences are gone when I do this:
fig.dpi = windll.user32.GetDpiForWindow(w.winfo_id()) / 0.96
This leaves w.winfo_screenwidth/height() halved and small tool buttons when using options 1 and 2. But the window is 1600 pixels wide as specified (758 resp. 728 high, due to the button bar), fonts are readable (200% as configured in Windows) and the axes fit inside the figure.
I must remember not to use dpi= with plt.figure(), because that messes things up again.
I'm still curious where the difference comes from, but this way I can use the default Tk backend without issues.

Related

Require Jupyter notebook to render matplotlib notebook figure before closing

Introduction and examples.
It is a widely known
best practice to
close matplotlib figures after opening them
to avoid consuming too much memory.
In a standalone Python script, for example,
I might do something like this:
fig, ax = plt.subplots()
ax.plot(x, y1);
plt.show() # stop here and wait for user to finish
plt.close(fig)
Similarly, for a Jupyter notebook
it's a common pattern to create a figure in one cell
and then close it in the next cell.
A minimal example might look something like this,
where each blank line is a new cell:
%matplotlib notebook
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 10, 100)
y1 = np.sin(x)
fig, ax = plt.subplots()
ax.plot(x, y1);
plt.close(fig)
However, this has a distinct disadvantage:
the figure is blank when run with "Kernel" -> "Restart & Run All"
or "Cell" -> "Run All".
That is, the figure doesn't display until all cells finish evaluating,
but the figure doesn't have time to render
since we are using plt.close right afterward.
Here's an example screenshot based on the example above:
This is different from running each cell individually:
as long as I run each cell slowly enough,
I can get each figure to display:
Full Jupyter notebook .ipynb files are available here:
https://github.com/nbeaver/jupyter-figure-rendering-tests
Inadequate workarounds.
The simplest workaround is start from the top
and manually step through each cell,
wait for it to render, and then move onto the next cell.
I don't consider this an acceptable workaround,
as it becomes impractically laborious for large notebooks
and is generally contrary to the purpose of an executable notebook environment.
Another workaround is to simply never call plt.close(fig).
This is not a good option for several reasons:
It results in excess memory usage,
as evidenced by warnings about opening too many figures.
On some machines or resource-constrained environments
this may prevent the notebook execution from completing at all.
Even in environments with abundant memory,
cursor tracking and interactive functionality like zoom or pan
becomes very slow when many figures are open at once.
As mentioned above, in general none of the figures will
display until the final cell has finished executing,
which is undesirable for notebooks
where execution time may be long for certain cells further down.
Another workaround is to use %matplotlib inline
instead of %matplotlib notebook.
This is unsatisfactory also:
The inline mode does not permit interactive inspection of the figure
such as cursor position values or pan and zoom.
This functionality may be desirable or essential for analysis.
The inline and notebook settings cannot in general be toggled on a per-cell basis,
so effectively this is an all-or-nothing setting.
Even if it were possible to toggle between inline and notebook,
I would prefer to only use notebook and then
close the figure in a subsequent cell,
so that I can return to the cell later
and re-run it to get the interactive controls
without needing to edit the cell.
An analogous workaround is to call savefig for each cell with a figure
and then browse the generated images with an external image viewing program.
While this allows limited zooming and panning,
it doesn't give cursor positions
and it's really not comparable to the interactive notebook plots.
Criteria and current workaround.
Here are my requirements:
The effect of "Restart & Run All" must render all figures eventually;
no figures can be left blank.
Use %matplotlib notebook for all cells
so that I can re-run the cell later and inspect the figure interactively.
Allow plt.close(fig) after each cell so that notebook
resources can be conserved.
Essentially, I would like a way to force the kernel
to render the current figure
before proceeding on to plt.close(fig).
My hope is that this is a well-known behavior
and that I've simply missed something.
Here's what I have tried so far that didn't help at all:
plt.show() at the end of a cell or between cells.
fig.show() at the end of a cell or between cells.
plt.ioff() at the end of a cell or between cells.
time.sleep(1) at the end of a cell or between cells.
plt.pause(1) at the end of a cell or between cells.
fig.canvas.draw_idle() at the end of a cell or between cells.
Doing from IPython.display import display and then display(fig)
at the end of a cell or between cells.
calling plt.close('all') at end of notebook instead of between each cell.
Currently, the best I've been able to do
is call fig.canvas.draw() in a separate cell
between the figure and the cell with plt.close(fig).
Using the example above, here's what this looks like:
%matplotlib notebook
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 10, 100)
y1 = np.sin(x)
fig, ax = plt.subplots()
ax.plot(x, y1);
fig.canvas.draw()
plt.close(fig)
This works reliably in smaller notebooks,
and for a while I thought this had solved the problem.
However, this doesn't always work;
particularly in large notebooks with many figures,
some of them still come out blank,
suggesting that fig.canvas.draw() adds some delay
but is not always sufficient,
perhaps due to a race condition.
Since fig.canvas.draw() is not a documented method
for making Jupyter notebooks render the current figure,
I would hesitate to describe this as a bug,
although it seems to be the closely related to this matplotlib issue,
which ultimately seems to be a Jupyter bug:
The simplest work around may be to put the input() call in the next cell or to add plt.gcf().canvas.draw() above the input call. This will still result in "dead" figures (which may not have caught up to the hi-dpi settings of your browser), but they will at least show.
I've observed this behavior in many combinations of matplotlib and Jupyter,
including matplotlib version 2.1.1 and 3.5.1
and Jupyter version 4.4.0 and 6.4.8.
I've also observed it in both Google Chrome 99.0.4844.51 and Firefox 97.0.2
and on both Windows 10 and Ubuntu 18.04.6.
Related questions (not duplicates):
Programmatically Stop Interaction for specific Figure in Jupyter notebook
Specify where in output matplotlib figures are rendered in jupyter using figure handles
Get Jupyter notebook to display matplotlib figures in real-time
Force matplotlib to fully render plots in an IPython/Jupyter notebook cell before advancing to next cell

Bokeh shows plot blurred on windows

I am using bokeh for plotting. With my current settings, bokeh shows some text and lines kind of blurred (anti-aliased?). This is not really noticeable on my monitor, but on some projectors, especially when doing screenshots and inserting them into presentations, it looks weird.
As requested, a minimum working example:
from bokeh.plotting import figure, show
p = figure(plot_width=1000, plot_height=600,
title="TestTitle", x_axis_label = "Length [cm]", y_axis_label="Height [m]")
p.xaxis.axis_label_text_font_style = "normal"
p.xaxis.axis_label_text_font_size = "12pt"
show(p)
I am using Windows 7 and tried this in the current versions of Chrome and IE. Python 3.6, bokeh version 0.13.0. The first image is taken from the example, the second is taken from my real code:
I tried different figure sizes, but the problem persists - maybe I am trying the wrong ones? Anything I can do about this, except simply trying different sizes until something "works"? Setting px instead of pts seems to make it worse...
Regarding the text, it is rendered on to a raster HTML canvas, and the details of how this is done are entirely dependent on the browser canvas implementation. (FWIW things look better on any browser on OSX than the above image.) There's not anything we can to change how a specific browser renders text, and not much I can suggest except to make much bigger canvas sizes if you need to show something extremely magnified.
Regarding the aliasing of the axes and tick marks, I can't reproduce anything like that on any OSX browser (Safari, FF, or Chrome). It definitely appears to be a Windows-specific issue. It's possible there are issues that could be addressed, e.g a different HiDPI setting, or half-pixel offsets, but it would take investigation to try and determine what can be done. A Github issue with details would be appropriate, but I can't speculate when it might be addressed (we are under-resourced and no core contributors are regular windows users).

Change plot window size in IPython notebook

I am using IPython Notebook to make some plots. For most of them I use
%matplotlib inline but for a few I change to %matplotlib notebook to be able to zoom in and stuff like that.
Now to the problem, the window displaying my plot (independent of what matplotlib setting I am using) suddenly became much smaller. Not the figure itself, just the window. I am really confused why this happened, if it was beacause I was switching between the two matplotlib settings or something else I made by mistake.
It is really annoying since I have to scroll in the window to se my whole figure unless I want to minimize it a lot. So if you have any idea how to make the plot window larger, please enlighten me.
Here you can se an example of what I mean by window: Small window. The 'window' according to me is where you can see "Figure 1" and the red button and ends where it cuts my plot.
Click to expand fully and show smaller. Double click to completely collapse it.

Increase resolution of figure for saving

Is there a way to increase the resolution of a figure when saving it using the matplotlib toolbar save button?
I tried increasing the dpi but it doesn't seem to make much of a difference when using the save button on the toolbar.
This is how I was increasing the dpi to what the user specified.
if self.txtDPI.toPlainText() == "":
DPI = 120
else:
DPI = int(self.txtDPI.toPlainText())
self.tempfig.set_dpi(DPI)
I have a GUI that the figure is on and underneath it is the matplotlib toolbar so they can edit the chart. I am trying to get it to save the figure with the set dpi when the user hits the "save" button on the matplotlib toolbar. I thought drawing the figure with the user input dpi would make it save the figure with that dpi but it doesn't. It also makes the chart go off the "canvas" if the user increases the dpi above 120.
EDIT:
I got it to work by doing the following:
import matplotlib as mpl
mpl.rcParams['savefig.dpi'] = DPI
Thank you for all your suggestions!
If you're happy to set the dpi before generating the figure then I would suggest setting the rcParams. This can be done either in a matplotlibrc file, or if you just have one script that you want to increase the dpi then add something like this:
import matplotlib.pyplot as plt
plt.rcParams['savefig.dpi'] = 500
If on the other hand you want to be able to set the dpi when you save the figure, then you will need to extend the interactive matplotlib window. Here is an example of how that can be done in matplotlib alone
EDIT:
An easy way to add the interactivity would be to make use of the IPython interactive widgets. Here is a screenshot of how this could work:
Every time you move the slider, it calls plot with the updated value of dpi, so the figure is resaved. If the figure is particularly large and slow to generate you may want to use interact_manual instead. In order to do this just install the IPython notebook with version greater than 3.0.

Python on windows, open plot windows next to each other

I am using EPD-python2.7 on windows7. In my python program I end up creating 4-5 figures in separate plot windows. By default the plot windows get stacked on top of each other. Every time I have to drag and replace each of these windows away and distribute over the screen area.
(Q1) Is there any way to set it automatically to have plot windows created next to each other? As shown below in the attached image (it is the screenshot of my second external screen).
(Q2) I have a second (extra) screen, and ideally I would like to have the plot windows created next to each other the second screen, when every time I run my program
You can choose the location of your plot but it is dependant on backend. To check this,
import matplotlib
matplotlib.get_backend()
and then see this answer for various ways to adjust.
For example, this works for me on linux with Qt4Agg,
import matplotlib.pyplot as plt
#Choose the correct dimensions for your first screen
FMwidth = 1280
FMheight = 1024
#Choose the correct dimensions for your second screen
SMwidth = 1280
SMheight = 1024
fm = plt.get_current_fig_manager()
#Works with QT on linux
fm.window.setGeometry(FMwidth,0,SMwidth,SMheight)
This may be better for windows
fm.window.wm_geometry("+500+0")
You may also be able to get the screen size(s) from,
from win32api import GetSystemMetrics
width = GetSystemMetrics(0)
weight = GetSystemMetrics(1)
You can easily create a counter which increments whenever you create a plot and adjusts this specified position so the next plot is next to the previous one each time. However, the size and layout are much easier if you use subplots, for example to set up your 2 by 3 grid,
#Setup 2 by 3 grid of subplots
fig, axs = plt.subplots(2,3,figsize=(width,height))
axs[0,0].plot(x, np.sinc(x))
axs[1,0].hist(y)
etc
You can then use your counter to specify which plot you are currently using and increment this whenever you plot.
I had the same question. What wasn't obvious for me when I looked through the answers is that when you have a second monitor, you can get to it by just using coordinates that are relative to your first monitor. For example, I have a 4k monitor above my 1080p primary monitor, and I get put figures onto it by using negative values for the y position.
mgr = plt.get_current_fig_manager()
mgr.window.move(-400,-2000)
plt.show()
Apparently it knows my monitor arrangement from Windows.

Categories

Resources