8000 Move pixel ratio handling into FigureCanvasBase. · matplotlib/matplotlib@877598d · GitHub
[go: up one dir, main page]

Skip to content

Commit 877598d

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 877598d

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_device_pixel_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)
0