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

Skip to content

Commit 8e3c274

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 183018c commit 8e3c274

File tree

7 files changed

+108
-73
lines changed

7 files changed

+108
-73
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1729,6 +1729,9 @@ def __init__(self, figure=None):
17291729
self.mouse_grabber = None # the axes currently grabbing mouse
17301730
self.toolbar = None # NavigationToolbar2 will set me
17311731
self._is_idle_drawing = False
1732+
# We don't want to scale up the figure DPI more than once.
1733+
figure._original_dpi = figure.dpi
1734+
self._device_pixel_ratio = 1
17321735

17331736
callbacks = property(lambda self: self.figure._canvas_callbacks)
17341737
button_pick_id = property(lambda self: self.figure._button_pick_id)
@@ -2054,12 +2057,73 @@ def draw_idle(self, *args, **kwargs):
20542057
with self._idle_draw_cntx():
20552058
self.draw(*args, **kwargs)
20562059

2060+
@property
2061+
def device_pixel_ratio(self):
2062+
"""
2063+
The ratio of physical to logical pixels used for the canvas on screen.
2064+
2065+
By default, this is 1, meaning physical and logical pixels are the same
2066+
size. Subclasses that support High DPI screens may set this property to
2067+
indicate that said ratio is different. All Matplotlib interaction,
2068+
unless working directly with the canvas, remains in logical pixels.
2069+
2070+
"""
2071+
return self._device_pixel_ratio
2072+
2073+
def _set_device_pixel_ratio(self, ratio):
2074+
"""
2075+
Set the ratio of physical to logical pixels used for the canvas.
2076+
2077+
Subclasses that support High DPI screens can set this property to
2078+
indicate that said ratio is different. The canvas itself will be
2079+
created at the physical size, while the client side will use the
2080+
logical size. Thus the DPI of the Figure will change to be scaled by
2081+
this ratio. Implementations that support High DPI screens should use
2082+
physical pixels for events so that transforms back to Axes space are
2083+
correct.
2084+
2085+
By default, this is 1, meaning physical and logical pixels are the same
2086+
size.
2087+
2088+
Parameters
2089+
----------
2090+
ratio : float
2091+
The ratio of logical to physical pixels used for the canvas.
2092+
2093+
Returns
2094+
-------
2095+
bool
2096+
Whether the ratio has changed. Backends may interpret this as a
2097+
signal to resize the window, repaint the canvas, or change any
2098+
other relevant properties.
2099+
"""
2100+
if self._device_pixel_ratio == ratio:
2101+
return False
2102+
# In cases with mixed resolution displays, we need to be careful if the
2103+
# device pixel ratio changes - in this case we need to resize the
2104+
# canvas accordingly. Some backends provide events that indicate a
2105+
# change in DPI, but those that don't will update this before drawing.
2106+
dpi = ratio * self.figure._original_dpi
2107+
self.figure._set_dpi(dpi, forward=False)
2108+
self._device_pixel_ratio = ratio
2109+
return True
2110+
20572111
def get_width_height(self):
20582112
"""
2059-
Return the figure width and height in points or pixels
2060-
(depending on the backend), truncated to integers.
2113+
Return the figure width and height in integral points or pixels.
2114+
2115+
When the figure is used on High DPI screens (and the backend supports
2116+
it), the truncation to integers occurs after scaling by the device
2117+
pixel ratio.
2118+
2119+
Returns
2120+
-------
2121+
width, height : int
2122+
The size of the figure, in points or pixels, depending on the
2123+
backend.
20612124
"""
2062-
return int(self.figure.bbox.width), int(self.figure.bbox.height)
2125+
return tuple(int(size / self.device_pixel_ratio)
2126+
for size in self.figure.bbox.max)
20632127

20642128
@classmethod
20652129
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
@@ -213,15 +213,6 @@ def __init__(self, figure=None):
213213
_create_qApp()
214214
super().__init__(figure=figure)
215215

216-
# We don't want to scale up the figure DPI more than once.
217-
# Note, we don't handle a signal for changing DPI yet.
218-
self.figure._original_dpi = self.figure.dpi
219-
self._update_figure_dpi()
220-
# In cases with mixed resolution displays, we need to be careful if the
221-
# dpi_ratio changes - in this case we need to resize the canvas
222-
# accordingly.
223-
self._dpi_ratio_prev = self._dpi_ratio
224-
225216
self._draw_pending = False
226217
self._is_drawing = False
227218
self._draw_rect_callback = lambda painter: None
@@ -233,28 +224,13 @@ def __init__(self, figure=None):
233224
palette = QtGui.QPalette(QtCore.Qt.white)
234225
self.setPalette(palette)
235226

236-
def _update_figure_dpi(self):
237-
dpi = self._dpi_ratio * self.figure._original_dpi
238-
self.figure._set_dpi(dpi, forward=False)
239-
240-
@property
241-
def _dpi_ratio(self):
242-
return _devicePixelRatioF(self)
243-
244227
def _update_pixel_ratio(self):
245-
# We need to be careful in cases with mixed resolution displays if
246-
# dpi_ratio changes.
247-
if self._dpi_ratio != self._dpi_ratio_prev:
248-
# We need to update the figure DPI.
249-
self._update_figure_dpi()
250-
self._dpi_ratio_prev = self._dpi_ratio
228+
if self._set_device_pixel_ratio(_devicePixelRatioF(self)):
251229
# The easiest way to resize the canvas is to emit a resizeEvent
252230
# since we implement all the logic for resizing the canvas for
253231
# that event.
254232
event = QtGui.QResizeEvent(self.size(), self.size())
255233
self.resizeEvent(event)
256-
# resizeEvent triggers a paintEvent itself, so we exit this one
257-
# (after making sure that the event is immediately handled).
258234

259235
def _update_screen(self, screen):
260236
# Handler for changes to a window's attached screen.
@@ -270,10 +246,6 @@ def showEvent(self, event):
270246
window.screenChanged.connect(self._update_screen)
271247
self._update_screen(window.screen())
272248

273-
def get_width_height(self):
274-
w, h = FigureCanvasBase.get_width_height(self)
275-
return int(w / self._dpi_ratio), int(h / self._dpi_ratio)
276-
277249
def enterEvent(self, event):
278250
try:
279251
x, y = self.mouseEventCoords(event.pos())
@@ -296,11 +268,10 @@ def mouseEventCoords(self, pos):
296268
297269
Also, the origin is different and needs to be corrected.
298270
"""
299-
dpi_ratio = self._dpi_ratio
300271
x = pos.x()
301272
# flip y so y=0 is bottom of canvas
302-
y = self.figure.bbox.height / dpi_ratio - pos.y()
303-
return x * dpi_ratio, y * dpi_ratio
273+
y = self.figure.bbox.height / self.device_pixel_ratio - pos.y()
274+
return x * self.device_pixel_ratio, y * self.device_pixel_ratio
304275

305276
def mousePressEvent(self, event):
306277
x, y = self.mouseEventCoords(event.pos())
@@ -361,8 +332,8 @@ def keyReleaseEvent(self, event):
361332
FigureCanvasBase.key_release_event(self, key, guiEvent=event)
362333

363334
def resizeEvent(self, event):
364-
w = event.size().width() * self._dpi_ratio
365-
h = event.size().height() * self._dpi_ratio
335+
w = event.size().width() * self.device_pixel_ratio
336+
h = event.size().height() * self.device_pixel_ratio
366337
dpival = self.figure.dpi
367338
winch = w / dpival
368339
hinch = h / dpival
@@ -460,7 +431,7 @@ def blit(self, bbox=None):
460431
if bbox is None and self.figure:
461432
bbox = self.figure.bbox # Blit the entire canvas if bbox is None.
462433
# repaint uses logical pixels, not physical pixels like the renderer.
463-
l, b, w, h = [int(pt / self._dpi_ratio) for pt in bbox.bounds]
434+
l, b, w, h = [int(pt / self.device_pixel_ratio) for pt in bbox.bounds]
464435
t = b + h
465436
self.repaint(l, self.rect().height() - t, w, h)
466437

@@ -481,11 +452,11 @@ def drawRectangle(self, rect):
481452
# Draw the zoom rectangle to the QPainter. _draw_rect_callback needs
482453
# to be called at the end of paintEvent.
483454
if rect is not None:
484-
x0, y0, w, h = [int(pt / self._dpi_ratio) for pt in rect]
455+
x0, y0, w, h = [int(pt / self.device_pixel_ratio) for pt in rect]
485456
x1 = x0 + w
486457
y1 = y0 + h
487458
def _draw_rect_callback(painter):
488-
pen = QtGui.QPen(QtCore.Qt.black, 1 / self._dpi_ratio)
459+
pen = QtGui.QPen(QtCore.Qt.black, 1 / self.device_pixel_ratio)
489460
pen.setDashPattern([3, 3])
490461
for color, offset in [
491462
(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: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,6 @@ def __init__(self, *args, **kwargs):
138138
# to the connected clients.
139139
self._current_image_mode = 'full'
140140

141-
# Store the DPI ratio of the browser. This is the scaling that
142-
# occurs automatically for all images on a HiDPI display.
143-
self._dpi_ratio = 1
144-
145141
def show(self):
146142
# show the figure window
147143
from matplotlib.pyplot import show
@@ -311,8 +307,8 @@ def handle_refresh(self, event):
311307
self.draw_idle()
312308

313309
def handle_resize(self, event):
314-
x, y = event.get('width', 800), event.get('height', 800)
315-
x, y = int(x) * self._dpi_ratio, int(y) * self._dpi_ratio
310+
x = int(event.get('width', 800)) * self.device_pixel_ratio
311+
y = int(event.get('height', 800)) * self.device_pixel_ratio
316312
fig = self.figure
317313
# An attempt at approximating the figure size in pixels.
318314
fig.set_size_inches(x / fig.dpi, y / fig.dpi, forward=False)
@@ -327,14 +323,15 @@ def handle_send_image_mode(self, event):
327323
# The client requests notification of what the current image mode is.
328324
self.send_event('image_mode', mode=self._current_image_mode)
329325

326+
def handle_set_device_pixel_ratio(self, event):
327+
self._handle_set_device_pixel_ratio(event.get('device_pixel_ratio', 1))
328+
330329
def handle_set_dpi_ratio(self, event):
331-
dpi_ratio = event.get('dpi_ratio', 1)
332-
if dpi_ratio != self._dpi_ratio:
333-
# We don't want to scale up the figure dpi more than once.
334-
if not hasattr(self.figure, '_original_dpi'):
335-
self.figure._original_dpi = self.figure.dpi
336-
self.figure.dpi = dpi_ratio * self.figure._original_dpi
337-
self._dpi_ratio = dpi_ratio
330+
# This handler is for backwards-compatibility with older ipympl.
331+
self._handle_set_dpi_ratio(event.get('dpi_ratio', 1))
332+
333+
def _handle_set_device_pixel_ratio(self, device_pixel_ratio):
334+
if self._set_device_pixel_ratio(device_pixel_ratio):
338335
self._force_full = True
339336
self.draw_idle()
340337

@@ -426,7 +423,8 @@ def _get_toolbar(self, canvas):
426423
def resize(self, w, h, forward=True):
427424
self._send_event(
428425
'resize',
429-
size=(w / self.canvas._dpi_ratio, h / self.canvas._dpi_ratio),
426+
size=(w / self.canvas.device_pixel_ratio,
427+
h / self.canvas.device_pixel_ratio),
430428
forward=forward)
431429

432430
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
@@ -166,10 +166,10 @@ def on_key_press(event):
166166

167167

168168
@pytest.mark.backend('Qt5Agg', skip_on_importerror=True)
169-
def test_pixel_ratio_change():
169+
def test_device_pixel_ratio_change():
170170
"""
171171
Make sure that if the pixel ratio changes, the figure dpi changes but the
172-
widget remains the same physical size.
172+
widget remains the same logical size.
173173
"""
174174

175175
prop = 'matplotlib.backends.backend_qt5.FigureCanvasQT.devicePixelRatioF'
@@ -180,10 +180,8 @@ def test_pixel_ratio_change():
180180
qt_canvas = fig.canvas
181181
qt_canvas.show()
182182

183-
def set_pixel_ratio(ratio):
183+
def set_device_pixel_ratio(ratio):
184184
p.return_value = ratio
185-
# Make sure the mocking worked
186-
assert qt_canvas._dpi_ratio == ratio
187185

188186
# The value here doesn't matter, as we can't mock the C++ QScreen
189187
# object, but can override the functional wrapper around it.
@@ -194,43 +192,46 @@ def set_pixel_ratio(ratio):
194192
qt_canvas.draw()
195193
qt_canvas.flush_events()
196194

195+
# Make sure the mocking worked
196+
assert qt_canvas.device_pixel_ratio == ratio
197+
197198
qt_canvas.manager.show()
198199
size = qt_canvas.size()
199200
screen = qt_canvas.window().windowHandle().screen()
200-
set_pixel_ratio(3)
201+
set_device_pixel_ratio(3)
201202

202203
# The DPI and the renderer width/height change
203204
assert fig.dpi == 360
204205
assert qt_canvas.renderer.width == 1800
205206
assert qt_canvas.renderer.height == 720
206207

207-
# The actual widget size and figure physical size don't change
208+
# The actual widget size and figure logical size don't change.
208209
assert size.width() == 600
209210
assert size.height() == 240
210211
assert qt_canvas.get_width_height() == (600, 240)
211212
assert (fig.get_size_inches() == (5, 2)).all()
212213

213-
set_pixel_ratio(2)
214+
set_device_pixel_ratio(2)
214215

215216
# The DPI and the renderer width/height change
216217
assert fig.dpi == 240
217218
assert qt_canvas.renderer.width == 1200
218219
assert qt_canvas.renderer.height == 480
219220

220-
# The actual widget size and figure physical size don't change
221+
# The actual widget size and figure logical size don't change.
221222
assert size.width() == 600
222223
assert size.height() == 240
223224
assert qt_canvas.get_width_height() == (600, 240)
224225
assert (fig.get_size_inches() == (5, 2)).all()
225226

226-
set_pixel_ratio(1.5)
227+
set_device_pixel_ratio(1.5)
227228

228229
# The DPI and the renderer width/height change
229230
assert fig.dpi == 180
230231
assert qt_canvas.renderer.width == 900
231232
assert qt_canvas.renderer.height == 360
232233

233-
# The actual widget size and figure physical size don't change
234+
# The actual widget size and figure logical size don't change.
234235
assert size.width() == 600
235236
assert size.height() == 240
236237
assert qt_canvas.get_width_height() == (600, 240)

0 commit comments

Comments
 (0)