Skip to content

Commit afd6e05

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 c57edc4 commit afd6e05

File tree

7 files changed

+74
-67
lines changed

7 files changed

+74
-67
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 40 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._pixel_ratio = 1
1729+
17261730
@property
17271731
def callbacks(self):
17281732
return self.figure._canvas_callbacks
@@ -2040,12 +2044,45 @@ def draw_idle(self, *args, **kwargs):
20402044
with self._idle_draw_cntx():
20412045
self.draw(*args, **kwargs)
20422046

2047+
@property
2048+
def pixel_ratio(self):
2049+
"""
2050+
The ratio of logical to physical pixels used for the canvas.
2051+
2052+
Subclasses that support High DPI screens can set this property to
2053+
indicate that said ratio is different. The canvas itself will be
2054+
created at the physical size, while the display and any events will use
2055+
the logical size (and such implementations should ensure that events do
2056+
so.)
2057+
2058+
By default, this is 1, meaning physical and logical pixels are the same
2059+
size.
2060+
"""
2061+
return self._pixel_ratio
2062+
2063+
@pixel_ratio.setter
2064+
def pixel_ratio(self, ratio):
2065+
# In cases with mixed resolution displays, we need to be careful if the
2066+
# pixel_ratio changes - in this case we need to resize the canvas
2067+
# accordingly. Some backends provide events that indicate a change in
2068+
# DPI, but those that don't will update this before drawing.
2069+
if self._pixel_ratio != ratio:
2070+
dpi = ratio * self.figure._original_dpi
2071+
self.figure._set_dpi(dpi, forward=False)
2072+
self._pixel_ratio = ratio
2073+
20432074
def get_width_height(self):
20442075
"""
2045-
Return the figure width and height in points or pixels
2046-
(depending on the backend), truncated to integers.
2076+
Return the figure width and height in integral points or pixels.
2077+
2078+
Returns
2079+
-------
2080+
width, height : int
2081+
The size of the figure, in points or pixels, depending on the
2082+
backend.
20472083
"""
2048-
return int(self.figure.bbox.width), int(self.figure.bbox.height)
2084+
return tuple(int(size / self.pixel_ratio)
2085+
for size in self.figure.bbox.max)
20492086

20502087
@classmethod
20512088
def get_supported_filetypes(cls):

lib/matplotlib/backends/backend_qt5.py

Lines changed: 13 additions & 35 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,22 +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-
# As described in __init__ above, we need to be careful in cases with
254-
# mixed resolution displays if dpi_ratio is changing between painting
255-
# events.
256-
if self._dpi_ratio != self._dpi_ratio_prev:
236+
# We need to be careful in cases with mixed resolution displays if
237+
# pixel_ratio changes.
238+
current_pixel_ratio = _devicePixelRatioF(self)
239+
if self.pixel_ratio != current_pixel_ratio:
257240
# We need to update the figure DPI.
258-
self._update_figure_dpi()
259-
self._dpi_ratio_prev = self._dpi_ratio
241+
self.pixel_ratio = current_pixel_ratio
260242
# The easiest way to resize the canvas is to emit a resizeEvent
261243
# since we implement all the logic for resizing the canvas for
262244
# that event.
@@ -283,10 +265,6 @@ def showEvent(self, event):
283265
window.screenChanged.connect(self._update_screen)
284266
self._update_screen(window.screen())
285267

286-
def get_width_height(self):
287-
w, h = FigureCanvasBase.get_width_height(self)
288-
return int(w / self._dpi_ratio), int(h / self._dpi_ratio)
289-
290268
def enterEvent(self, event):
291269
try:
292270
x, y = self.mouseEventCoords(event.pos())
@@ -309,11 +287,11 @@ def mouseEventCoords(self, pos):
309287
310288
Also, the origin is different and needs to be corrected.
311289
"""
312-
dpi_ratio = self._dpi_ratio
290+
pixel_ratio = self.pixel_ratio
313291
x = pos.x()
314292
# flip y so y=0 is bottom of canvas
315-
y = self.figure.bbox.height / dpi_ratio - pos.y()
316-
return x * dpi_ratio, y * dpi_ratio
293+
y = self.figure.bbox.height / pixel_ratio - pos.y()
294+
return x * pixel_ratio, y * pixel_ratio
317295

318296
def mousePressEvent(self, event):
319297
x, y = self.mouseEventCoords(event.pos())
@@ -374,8 +352,8 @@ def keyReleaseEvent(self, event):
374352
FigureCanvasBase.key_release_event(self, key, guiEvent=event)
375353

376354
def resizeEvent(self, event):
377-
w = event.size().width() * self._dpi_ratio
378-
h = event.size().height() * self._dpi_ratio
355+
w = event.size().width() * self.pixel_ratio
356+
h = event.size().height() * self.pixel_ratio
379357
dpival = self.figure.dpi
380358
winch = w / dpival
381359
hinch = h / dpival
@@ -473,7 +451,7 @@ def blit(self, bbox=None):
473451
if bbox is None and self.figure:
474452
bbox = self.figure.bbox # Blit the entire canvas if bbox is None.
475453
# repaint uses logical pixels, not physical pixels like the renderer.
476-
l, b, w, h = [int(pt / self._dpi_ratio) for pt in bbox.bounds]
454+
l, b, w, h = [int(pt / self.pixel_ratio) for pt in bbox.bounds]
477455
t = b + h
478456
self.repaint(l, self.rect().height() - t, w, h)
479457

@@ -494,11 +472,11 @@ def drawRectangle(self, rect):
494472
# Draw the zoom rectangle to the QPainter. _draw_rect_callback needs
495473
# to be called at the end of paintEvent.
496474
if rect is not None:
497-
x0, y0, w, h = [int(pt / self._dpi_ratio) for pt in rect]
475+
x0, y0, w, h = [int(pt / self.pixel_ratio) for pt in rect]
498476
x1 = x0 + w
499477
y1 = y0 + h
500478
def _draw_rect_callback(painter):
501-
pen = QtGui.QPen(QtCore.Qt.black, 1 / self._dpi_ratio)
479+
pen = QtGui.QPen(QtCore.Qt.black, 1 / self.pixel_ratio)
502480
pen.setDashPattern([3, 3])
503481
for color, offset in [
504482
(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.pixel_ratio
46+
height = rect.height() * self.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.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.pixel_ratio * self.width())
21+
height = int(self.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.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: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,6 @@ def __init__(self, *args, **kwargs):
137137
# to the connected clients.
138138
self._current_image_mode = 'full'
139139

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

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

329-
def handle_set_dpi_ratio(self, event):
330-
dpi_ratio = event.get('dpi_ratio', 1)
331-
if dpi_ratio != self._dpi_ratio:
332-
# We don't want to scale up the figure dpi more than once.
333-
if not hasattr(self.figure, '_original_dpi'):
334-
self.figure._original_dpi = self.figure.dpi
335-
self.figure.dpi = dpi_ratio * self.figure._original_dpi
336-
self._dpi_ratio = dpi_ratio
325+
def handle_set_pixel_ratio(self, event):
326+
pixel_ratio = event.get('pixel_ratio', 1)
327+
if pixel_ratio != self.pixel_ratio:
328+
self.pixel_ratio = pixel_ratio
337329
self._force_full = True
338330
self.draw_idle()
339331

@@ -427,7 +419,7 @@ def _get_toolbar(self, canvas):
427419
def resize(self, w, h, forward=True):
428420
self._send_event(
429421
'resize',
430-
size=(w / self.canvas._dpi_ratio, h / self.canvas._dpi_ratio),
422+
size=(w / self.canvas.pixel_ratio, h / self.canvas.pixel_ratio),
431423
forward=forward)
432424

433425
def set_window_title(self, title):

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ 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_pixel_ratio', { pixel_ratio: fig.ratio });
6767
}
6868
fig.send_message('refresh', {});
6969
};
@@ -158,7 +158,7 @@ mpl.figure.prototype._init_canvas = function () {
158158

159159
this.ratio = (window.devicePixelRatio || 1) / backingStore;
160160
if (this.ratio !== 1) {
161-
fig.send_message('set_dpi_ratio', { dpi_ratio: this.ratio });
161+
fig.send_message('set_pixel_ratio', { pixel_ratio: this.ratio });
162162
}
163163

164164
var rubberband_canvas = (this.rubberband_canvas = document.createElement(

lib/matplotlib/tests/test_backend_qt.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ def on_key_press(event):
161161
def test_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'
@@ -174,8 +174,6 @@ def test_pixel_ratio_change():
174174

175175
def set_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,6 +184,9 @@ 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.pixel_ratio == ratio
189+
189190
qt_canvas.manager.show()
190191
size = qt_canvas.size()
191192
screen = qt_canvas.window().windowHandle().screen()
@@ -196,7 +197,7 @@ def set_pixel_ratio(ratio):
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)
@@ -209,7 +210,7 @@ def set_pixel_ratio(ratio):
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)
@@ -222,7 +223,7 @@ def set_pixel_ratio(ratio):
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)