From a40620af1b77696914d0f07863a267d3a99cea35 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 6 Apr 2017 23:40:31 -0400 Subject: [PATCH 1/6] FIX: internally Matplotlib works in screen pixels not logical pixels --- lib/matplotlib/backends/backend_qt5.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 0c71385eb338..50465ff110f2 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -261,17 +261,20 @@ def leaveEvent(self, event): FigureCanvasBase.leave_notify_event(self, guiEvent=event) def mouseEventCoords(self, pos): - """ - Calculate mouse coordinates in logical pixels. + """Calculate mouse coordinates in physical pixels + + Qt5 use logical pixels, but the figure is scaled to physical + pixels for rendering. Transform to physical pixels so that + all of the down-stream transforms work as expected. + + Also, the origin is different and needs to be corrected. - Qt5 and Matplotlib use logical pixels, but the figure is scaled to - physical pixels for rendering. Also, the origin is different and needs - to be corrected. """ + dpi_ratio = self._dpi_ratio x = pos.x() # flip y so y=0 is bottom of canvas - y = self.figure.bbox.height / self._dpi_ratio - pos.y() - return x, y + y = self.figure.bbox.height / dpi_ratio - pos.y() + return x * dpi_ratio, y * dpi_ratio def mousePressEvent(self, event): x, y = self.mouseEventCoords(event.pos()) From 39b86d606bf4a09bf332f43d2cd11698f3426bc1 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 7 Apr 2017 20:29:00 -0400 Subject: [PATCH 2/6] MNT: tweak the toolbar for Qt Do not over-size the icons --- lib/matplotlib/backends/backend_qt5.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 50465ff110f2..5aa4a08c1ea6 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -582,7 +582,9 @@ def __init__(self, canvas, parent, coordinates=True): def _icon(self, name): if is_pyqt5(): name = name.replace('.png', '_large.png') - return QtGui.QIcon(os.path.join(self.basedir, name)) + pm = QtGui.QPixmap(os.path.join(self.basedir, name)) + pm.setDevicePixelRatio(self.canvas._dpi_ratio) + return QtGui.QIcon(pm) def _init_toolbar(self): self.basedir = os.path.join(matplotlib.rcParams['datapath'], 'images') @@ -592,7 +594,7 @@ def _init_toolbar(self): self.addSeparator() else: a = self.addAction(self._icon(image_file + '.png'), - text, getattr(self, callback)) + text, getattr(self, callback)) self._actions[callback] = a if callback in ['zoom', 'pan']: a.setCheckable(True) @@ -614,7 +616,7 @@ def _init_toolbar(self): QtCore.Qt.AlignRight | QtCore.Qt.AlignTop) self.locLabel.setSizePolicy( QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Ignored)) + QtWidgets.QSizePolicy.Ignored)) labelAction = self.addWidget(self.locLabel) labelAction.setVisible(True) From 5a6826ff088d60d90a229595ac7364302a77d27e Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 7 Apr 2017 20:29:30 -0400 Subject: [PATCH 3/6] MNT: Always use scaling with Qt if available --- lib/matplotlib/backends/backend_qt5.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 5aa4a08c1ea6..4041991bd757 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -149,7 +149,11 @@ def _create_qApp(): qApp = app if is_pyqt5(): - qApp.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps) + try: + qApp.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps) + qApp.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling) + except AttributeError: + pass class Show(ShowBase): @@ -159,6 +163,7 @@ def mainloop(self): global qApp qApp.exec_() + show = Show() From d739943d85133a9e363ff8560e62ab3e978529f4 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 7 Apr 2017 20:33:35 -0400 Subject: [PATCH 4/6] ENH: setting the DPI properly propagates Set the figure size back to the current value to push the dpi (and hence rendered size) change through. --- lib/matplotlib/figure.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 8f3b3afe7c03..288ff450a25e 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -323,9 +323,10 @@ def __init__(self, if frameon is None: frameon = rcParams['figure.frameon'] - self.dpi_scale_trans = Affine2D() - self.dpi = dpi self.bbox_inches = Bbox.from_bounds(0, 0, *figsize) + self.dpi_scale_trans = Affine2D().scale(dpi, dpi) + # do not use property as it will trigger + self._dpi = dpi self.bbox = TransformedBbox(self.bbox_inches, self.dpi_scale_trans) self.frameon = frameon @@ -413,6 +414,7 @@ def _get_dpi(self): def _set_dpi(self, dpi): self._dpi = dpi self.dpi_scale_trans.clear().scale(dpi, dpi) + self.set_size_inches(*self.get_size_inches()) self.callbacks.process('dpi_changed', self) dpi = property(_get_dpi, _set_dpi) @@ -710,13 +712,15 @@ def set_size_inches(self, w, h=None, forward=True): self.bbox_inches.p1 = w, h if forward: - ratio = getattr(self.canvas, '_dpi_ratio', 1) - dpival = self.dpi / ratio - canvasw = w * dpival - canvash = h * dpival - manager = getattr(self.canvas, 'manager', None) - if manager is not None: - manager.resize(int(canvasw), int(canvash)) + canvas = getattr(self, 'canvas') + if canvas is not None: + ratio = getattr(self.canvas, '_dpi_ratio', 1) + dpival = self.dpi / ratio + canvasw = w * dpival + canvash = h * dpival + manager = getattr(self.canvas, 'manager', None) + if manager is not None: + manager.resize(int(canvasw), int(canvash)) self.stale = True def get_size_inches(self): From 533f0bbb015effa38860df39c0bb184a32128516 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 7 Apr 2017 21:21:16 -0400 Subject: [PATCH 5/6] TST: add test for dpi change --- lib/matplotlib/tests/test_figure.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index 677bf50da23e..f5ce06b0d942 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -214,6 +214,18 @@ def test_figaspect(): assert h / w == 1 +@cleanup(style='default') +def test_change_dpi(): + fig = plt.figure(figsize=(4, 4)) + fig.canvas.draw() + assert fig.canvas.renderer.height == 400 + assert fig.canvas.renderer.width == 400 + fig.dpi = 50 + fig.canvas.draw() + assert fig.canvas.renderer.height == 200 + assert fig.canvas.renderer.width == 200 + + if __name__ == "__main__": import nose nose.runmodule(argv=['-s', '--with-doctest'], exit=False) From 278e8efe8f48102d324f828b26d7e858355f7edd Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 10 Apr 2017 16:04:47 -0400 Subject: [PATCH 6/6] FIX: fix up savefig dpi if scaled for screen If `_original_dpi` is tacked on to the figure instance by a backend canvas use that for 'figure' dpi instead of the dpi scaled for the screen. --- lib/matplotlib/backend_bases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index a8c81b05df73..bc5257bab60e 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2157,7 +2157,7 @@ def print_figure(self, filename, dpi=None, facecolor=None, edgecolor=None, dpi = rcParams['savefig.dpi'] if dpi == 'figure': - dpi = self.figure.dpi + dpi = getattr(self.figure, '_original_dpi', self.figure.dpi) if facecolor is None: facecolor = rcParams['savefig.facecolor']