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

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
  • lib/matplotlib
    • < 10000 div id=":R2dtddab:" class="PRIVATE_TreeView-item-content prc-TreeView-TreeViewItemContent-f0r0b">backends
  • tests
  • 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/backe 2851 nds/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)
    0