Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 3385d1a

Browse files
committed
Move pixel ratio handling into FigureCanvasBase.
This is already implemented in two backends (Qt5 and nbAgg), and I plan to implement it in TkAgg, so it's better to remove the repetition.
1 parent 3798e5f commit 3385d1a

File tree

7 files changed

+99
-74
lines changed

7 files changed

+99
-74
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1723,6 +1723,10 @@ def __init__(self, figure):
17231723
self.toolbar = None # NavigationToolbar2 will set me
17241724
self._is_idle_drawing = False
17251725

1726+
# We don't want to scale up the figure DPI more than once.
1727+
figure._original_dpi = figure.dpi
1728+
self._device_pixel_ratio = 1
1729+
17261730
@property
17271731
def callbacks(self):
17281732
return self.figure._canvas_callbacks
@@ -2040,12 +2044,68 @@ def draw_idle(self, *args, **kwargs):
20402044
with self._idle_draw_cntx():
20412045
self.draw(*args, **kwargs)
20422046

2047+
@property
2048+
def device_pixel_ratio(self):
2049+
"""
2050+
The ratio of logical to physical pixels used for the canvas on screen.
2051+
2052+
By default, this is 1, meaning physical and logical pixels are the same
2053+
size. Subclasses that support High DPI screens may set this property to
2054+
indicate that said ratio is different. All Matplotlib interaction,
2055+
unless working directly with the canvas, remains in logical pixels.
2056+
2057+
"""
2058+
return self._device_pixel_ratio
2059+
2060+
def _set_device_pixel_ratio(self, ratio):
2061+
"""
2062+
Set the ratio of logical to physical pixels used for the canvas.
2063+
2064+
Subclasses that support High DPI screens can set this property to
2065+
indicate that said ratio is different. The canvas itself will be
2066+
created at the physical size, while the client side will use the
2067+
logical size. Implementations that support High DPI screens should use
2068+
physical pixels for events so that transforms back to Axes space are
2069+
correct.
2070+
2071+
By default, this is 1, meaning physical and logical pixels are the same
2072+
size.
2073+
2074+
Parameters
2075+
----------
2076+
ratio : float
2077+
The ratio of logical to physical pixels used for the canvas.
2078+
2079+
Returns
2080+
-------
2081+
bool
2082+
Whether the ratio has changed. Backends may interpret this as a
2083+
signal to resize the window, repaint the canvas, or change any
2084+
other relevant properties.
2085+
"""
2086+
if self._device_pixel_ratio == ratio:
2087+
return False
2088+
# In cases with mixed resolution displays, we need to be careful if the
2089+
# device pixel ratio changes - in this case we need to resize the
2090+
# canvas accordingly. Some backends provide events that indicate a
2091+
# change in DPI, but those that don't will update this before drawing.
2092+
dpi = ratio * self.figure._original_dpi
2093+
self.figure._set_dpi(dpi, forward=False)
2094+
self._device_pixel_ratio = ratio
2095+
return True
2096+
20432097
def get_width_height(self):
20442098
"""
2045-
Return the figure width and height in points or pixels
2046-
(depending on the backend), truncated to integers.
2099+
Return the figure width and height in integral points or pixels.
2100+
2101+
Returns
2102+
-------
2103+
width, height : int
2104+
The size of the figure, in points or pixels, depending on the
2105+
backend.
20472106
"""
2048-
return int(self.figure.bbox.width), int(self.figure.bbox.height)
2107+
return tuple(int(size / self.device_pixel_ratio)
2108+
for size in self.figure.bbox.max)
20492109

20502110
@classmethod
20512111
def get_supported_filetypes(cls):

lib/matplotlib/backends/backend_qt5.py

Lines changed: 8 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -221,15 +221,6 @@ def __init__(self, figure):
221221
_create_qApp()
222222
super().__init__(figure=figure)
223223

224-
# We don't want to scale up the figure DPI more than once.
225-
# Note, we don't handle a signal for changing DPI yet.
226-
figure._original_dpi = figure.dpi
227-
self._update_figure_dpi()
228-
# In cases with mixed resolution displays, we need to be careful if the
229-
# dpi_ratio changes - in this case we need to resize the canvas
230-
# accordingly.
231-
self._dpi_ratio_prev = self._dpi_ratio
232-
233224
self._draw_pending = False
234225
self._is_drawing = False
235226
self._draw_rect_callback = lambda painter: None
@@ -241,28 +232,13 @@ def __init__(self, figure):
241232
palette = QtGui.QPalette(QtCore.Qt.white)
242233
self.setPalette(palette)
243234

244-
def _update_figure_dpi(self):
245-
dpi = self._dpi_ratio * self.figure._original_dpi
246-
self.figure._set_dpi(dpi, forward=False)
247-
248-
@property
249-
def _dpi_ratio(self):
250-
return _devicePixelRatioF(self)
251-
252235
def _update_pixel_ratio(self):
253-
# We need to be careful in cases with mixed resolution displays if
254-
# dpi_ratio changes.
255-
if self._dpi_ratio != self._dpi_ratio_prev:
256-
# We need to update the figure DPI.
257-
self._update_figure_dpi()
258-
self._dpi_ratio_prev = self._dpi_ratio
236+
if self._set_device_pixel_ratio(_devicePixelRatioF(self)):
259237
# The easiest way to resize the canvas is to emit a resizeEvent
260238
# since we implement all the logic for resizing the canvas for
261239
# that event.
262240
event = QtGui.QResizeEvent(self.size(), self.size())
263241
self.resizeEvent(event)
264-
# resizeEvent triggers a paintEvent itself, so we exit this one
265-
# (after making sure that the event is immediately handled).
266242

267243
def _update_screen(self, screen):
268244
# Handler for changes to a window's attached screen.
@@ -278,10 +254,6 @@ def showEvent(self, event):
278254
window.screenChanged.connect(self._update_screen)
279255
self._update_screen(window.screen())
280256

281-
def get_width_height(self):
282-
w, h = FigureCanvasBase.get_width_height(self)
283-
return int(w / self._dpi_ratio), int(h / self._dpi_ratio)
284-
285257
def enterEvent(self, event):
286258
try:
287259
x, y = self.mouseEventCoords(event.pos())
@@ -304,11 +276,10 @@ def mouseEventCoords(self, pos):
304276
305277
Also, the origin is different and needs to be corrected.
306278
"""
307-
dpi_ratio = self._dpi_ratio
308279
x = pos.x()
309280
# flip y so y=0 is bottom of canvas
310-
y = self.figure.bbox.height / dpi_ratio - pos.y()
311-
return x * dpi_ratio, y * dpi_ratio
281+
y = self.figure.bbox.height / self.device_pixel_ratio - pos.y()
282+
return x * self.device_pixel_ratio, y * self.device_pixel_ratio
312283

313284
def mousePressEvent(self, event):
314285
x, y = self.mouseEventCoords(event.pos())
@@ -369,8 +340,8 @@ def keyReleaseEvent(self, event):
369340
FigureCanvasBase.key_release_event(self, key, guiEvent=event)
370341

371342
def resizeEvent(self, event):
372-
w = event.size().width() * self._dpi_ratio
373-
h = event.size().height() * self._dpi_ratio
343+
w = event.size().width() * self.device_pixel_ratio
344+
h = event.size().height() * self.device_pixel_ratio
374345
dpival = self.figure.dpi
375346
winch = w / dpival
376347
hinch = h / dpival
@@ -468,7 +439,7 @@ def blit(self, bbox=None):
468439
if bbox is None and self.figure:
469440
bbox = self.figure.bbox # Blit the entire canvas if bbox is None.
470441
# repaint uses logical pixels, not physical pixels like the renderer.
471-
l, b, w, h = [int(pt / self._dpi_ratio) for pt in bbox.bounds]
442+
l, b, w, h = [int(pt / self.device_pixel_ratio) for pt in bbox.bounds]
472443
t = b + h
473444
self.repaint(l, self.rect().height() - t, w, h)
474445

@@ -489,11 +460,11 @@ def drawRectangle(self, rect):
489460
# Draw the zoom rectangle to the QPainter. _draw_rect_callback needs
490461
# to be called at the end of paintEvent.
491462
if rect is not None:
492-
x0, y0, w, h = [int(pt / self._dpi_ratio) for pt in rect]
463+
x0, y0, w, h = [int(pt / self.device_pixel_ratio) for pt in rect]
493464
x1 = x0 + w
494465
y1 = y0 + h
495466
def _draw_rect_callback(painter):
496-
pen = QtGui.QPen(QtCore.Qt.black, 1 / self._dpi_ratio)
467+
pen = QtGui.QPen(QtCore.Qt.black, 1 / self.device_pixel_ratio)
497468
pen.setDashPattern([3, 3])
498469
for color, offset in [
499470
(QtCore.Qt.black, 0), (QtCore.Qt.white, 3)]:

lib/matplotlib/backends/backend_qt5agg.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ def paintEvent(self, event):
4242
# scale rect dimensions using the screen dpi ratio to get
4343
# correct values for the Figure coordinates (rather than
4444
# QT5's coords)
45-
width = rect.width() * self._dpi_ratio
46-
height = rect.height() * self._dpi_ratio
45+
width = rect.width() * self.device_pixel_ratio
46+
height = rect.height() * self.device_pixel_ratio
4747
left, top = self.mouseEventCoords(rect.topLeft())
4848
# shift the "top" by the height of the image to get the
4949
# correct corner for our coordinate system
@@ -61,7 +61,7 @@ def paintEvent(self, event):
6161

6262
qimage = QtGui.QImage(buf, buf.shape[1], buf.shape[0],
6363
QtGui.QImage.Format_ARGB32_Premultiplied)
64-
_setDevicePixelRatio(qimage, self._dpi_ratio)
64+
_setDevicePixelRatio(qimage, self.device_pixel_ratio)
6565
# set origin using original QT coordinates
6666
origin = QtCore.QPoint(rect.left(), rect.top())
6767
painter.drawImage(origin, qimage)

lib/matplotlib/backends/backend_qt5cairo.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,8 @@ def draw(self):
1717
super().draw()
1818

1919
def paintEvent(self, event):
20-
dpi_ratio = self._dpi_ratio
21-
width = int(dpi_ratio * self.width())
22-
height = int(dpi_ratio * self.height())
20+
width = int(self.device_pixel_ratio * self.width())
21+
height = int(self.device_pixel_ratio * self.height())
2322
if (width, height) != self._renderer.get_canvas_width_height():
2423
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
2524
self._renderer.set_ctx_from_surface(surface)
@@ -32,7 +31,7 @@ def paintEvent(self, event):
3231
# QImage under PySide on Python 3.
3332
if QT_API == 'PySide':
3433
ctypes.c_long.from_address(id(buf)).value = 1
35-
_setDevicePixelRatio(qimage, dpi_ratio)
34+
_setDevicePixelRatio(qimage, self.device_pixel_ratio)
3635
painter = QtGui.QPainter(self)
3736
painter.eraseRect(event.rect())
3837
painter.drawImage(0, 0, qimage)

lib/matplotlib/backends/backend_webagg_core.py

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,6 @@ def __init__(self, *args, **kwargs):
172172
# to the connected clients.
173173
self._current_image_mode = 'full'
174174

175-
# Store the DPI ratio of the browser. This is the scaling that
176-
# occurs automatically for all images on a HiDPI display.
177-
self._dpi_ratio = 1
178-
179175
def show(self):
180176
# show the figure window
181177
from matplotlib.pyplot import show
@@ -345,8 +341,8 @@ def handle_refresh(self, event):
345341
self.draw_idle()
346342

347343
def handle_resize(self, event):
348-
x, y = event.get('width', 800), event.get('height', 800)
349-
x, y = int(x) * self._dpi_ratio, int(y) * self._dpi_ratio
344+
x = int(event.get('width', 800)) * self.device_pixel_ratio
345+
y = int(event.get('height', 800)) * self.device_pixel_ratio
350346
fig = self.figure
351347
# An attempt at approximating the figure size in pixels.
352348
fig.set_size_inches(x / fig.dpi, y / fig.dpi, forward=False)
@@ -361,14 +357,9 @@ def handle_send_image_mode(self, event):
361357
# The client requests notification of what the current image mode is.
362358
self.send_event('image_mode', mode=self._current_image_mode)
363359

364-
def handle_set_dpi_ratio(self, event):
365-
dpi_ratio = event.get('dpi_ratio', 1)
366-
if dpi_ratio != self._dpi_ratio:
367-
# We don't want to scale up the figure dpi more than once.
368-
if not hasattr(self.figure, '_original_dpi'):
369-
self.figure._original_dpi = self.figure.dpi
370-
self.figure.dpi = dpi_ratio * self.figure._original_dpi
371-
self._dpi_ratio = dpi_ratio
360+
def handle_set_device_pixel_ratio(self, event):
361+
device_pixel_ratio = event.get('device_pixel_ratio', 1)
362+
if self._set_device_pixel_ratio(device_pixel_ratio):
372363
self._force_full = True
373364
self.draw_idle()
374365

@@ -462,7 +453,8 @@ def _get_toolbar(self, canvas):
462453
def resize(self, w, h, forward=True):
463454
self._send_event(
464455
'resize',
465-
size=(w / self.canvas._dpi_ratio, h / self.canvas._dpi_ratio),
456+
size=(w / self.canvas.device_pixel_ratio,
457+
h / self.canvas.device_pixel_ratio),
466458
forward=forward)
467459

468460
def set_window_title(self, title):

lib/matplotlib/backends/web_backend/js/mpl.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ mpl.figure = function (figure_id, websocket, ondownload, parent_element) {
6363
fig.send_message('supports_binary', { value: fig.supports_binary });
6464
fig.send_message('send_image_mode', {});
6565
if (fig.ratio !== 1) {
66-
fig.send_message('set_dpi_ratio', { dpi_ratio: fig.ratio });
66+
fig.send_message('set_device_pixel_ratio', {
67+
device_pixel_ratio: fig.ratio,
68+
});
6769
}
6870
fig.send_message('refresh', {});
6971
};

lib/matplotlib/tests/test_backend_qt.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,10 @@ def on_key_press(event):
158158

159159

160160
@pytest.mark.backend('Qt5Agg')
161-
def test_pixel_ratio_change():
161+
def test_device_pixel_ratio_change():
162162
"""
163163
Make sure that if the pixel ratio changes, the figure dpi changes but the
164-
widget remains the same physical size.
164+
widget remains the same logical size.
165165
"""
166166

167167
prop = 'matplotlib.backends.backend_qt5.FigureCanvasQT.devicePixelRatioF'
@@ -172,10 +172,8 @@ def test_pixel_ratio_change():
172172
qt_canvas = fig.canvas
173173
qt_canvas.show()
174174

175-
def set_pixel_ratio(ratio):
175+
def set_device_pixel_ratio(ratio):
176176
p.return_value = ratio
177-
# Make sure the mocking worked
178-
assert qt_canvas._dpi_ratio == ratio
179177

180178
# The value here doesn't matter, as we can't mock the C++ QScreen
181179
# object, but can override the functional wrapper around it.
@@ -186,43 +184,46 @@ def set_pixel_ratio(ratio):
186184
qt_canvas.draw()
187185
qt_canvas.flush_events()
188186

187+
# Make sure the mocking worked
188+
assert qt_canvas.device_pixel_ratio == ratio
189+
189190
qt_canvas.manager.show()
190191
size = qt_canvas.size()
191192
screen = qt_canvas.window().windowHandle().screen()
192-
set_pixel_ratio(3)
193+
set_device_pixel_ratio(3)
193194

194195
# The DPI and the renderer width/height change
195196
assert fig.dpi == 360
196197
assert qt_canvas.renderer.width == 1800
197198
assert qt_canvas.renderer.height == 720
198199

199-
# The actual widget size and figure physical size don't change
200+
# The actual widget size and figure logical size don't change.
200201
assert size.width() == 600
201202
assert size.height() == 240
202203
assert qt_canvas.get_width_height() == (600, 240)
203204
assert (fig.get_size_inches() == (5, 2)).all()
204205

205-
set_pixel_ratio(2)
206+
set_device_pixel_ratio(2)
206207

207208
# The DPI and the renderer width/height change
208209
assert fig.dpi == 240
209210
assert qt_canvas.renderer.width == 1200
210211
assert qt_canvas.renderer.height == 480
211212

212-
# The actual widget size and figure physical size don't change
213+
# The actual widget size and figure logical size don't change.
213214
assert size.width() == 600
214215
assert size.height() == 240
215216
assert qt_canvas.get_width_height() == (600, 240)
216217
assert (fig.get_size_inches() == (5, 2)).all()
217218

218-
set_pixel_ratio(1.5)
219+
set_device_pixel_ratio(1.5)
219220

220221
# The DPI and the renderer width/height change
221222
assert fig.dpi == 180
222223
assert qt_canvas.renderer.width == 900
223224
assert qt_canvas.renderer.height == 360
224225

225-
# The actual widget size and figure physical size don't change
226+
# The actual widget size and figure logical size don't change.
226227
assert size.width() == 600
227228
assert size.height() == 240
228229
assert qt_canvas.get_width_height() == (600, 240)

0 commit comments

Comments
 (0)