diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 633ce987269d..384987e3d036 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2059,8 +2059,16 @@ def get_width_height(self, *, physical=False): width, height : int The size of the figure, in points or pixels, depending on the backend. + + Notes + ----- + This method normally truncates the height/width to remove any fractional pixel. + However, if the height/width is extremely close to the integer pixel (within + 1e-8 pixel), the height/width is instead rounded up to account for + floating-point precision effects. """ - return tuple(int(size / (1 if physical else self.device_pixel_ratio)) + # The tolerance of 1e-8 covers a floating-point tick for even 100,000 pixels + return tuple(int(size / (1 if physical else self.device_pixel_ratio) + 1e-8) for size in self.figure.bbox.max) @classmethod diff --git a/lib/matplotlib/backends/_backend_gtk.py b/lib/matplotlib/backends/_backend_gtk.py index 0491db40e565..85c05b3e1c10 100644 --- a/lib/matplotlib/backends/_backend_gtk.py +++ b/lib/matplotlib/backends/_backend_gtk.py @@ -275,7 +275,7 @@ def set_message(self, s): self.message.set_markup(f'{escaped}') def draw_rubberband(self, event, x0, y0, x1, y1): - height = self.canvas.figure.bbox.height + height = self.canvas.get_width_height(physical=True)[1] y1 = height - y1 y0 = height - y0 rect = [int(val) for val in (x0, y0, x1 - x0, y1 - y0)] diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index a18a9bd8660f..97edbfa8bd06 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -327,9 +327,10 @@ def get_tk_widget(self): def _event_mpl_coords(self, event): # calling canvasx/canvasy allows taking scrollbars into account (i.e. # the top of the widget may have been scrolled out of view). + height = self.get_width_height(physical=True)[1] return (self._tkcanvas.canvasx(event.x), # flipy so y=0 is bottom of canvas - self.figure.bbox.height - self._tkcanvas.canvasy(event.y)) + height - self._tkcanvas.canvasy(event.y)) def motion_notify_event(self, event): MouseEvent("motion_notify_event", self, @@ -389,7 +390,7 @@ def scroll_event_windows(self, event): if w != self._tkcanvas: return x = self._tkcanvas.canvasx(event.x_root - w.winfo_rootx()) - y = (self.figure.bbox.height + y = (self.get_width_height(physical=True)[1] - self._tkcanvas.canvasy(event.y_root - w.winfo_rooty())) step = event.delta / 120 MouseEvent("scroll_event", self, @@ -676,7 +677,7 @@ def __init__(self, canvas, window=None, *, pack_toolbar=True): if window is None: window = canvas.get_tk_widget().master tk.Frame.__init__(self, master=window, borderwidth=2, - width=int(canvas.figure.bbox.width), height=50) + width=canvas.get_width_height()[0], height=50) # Avoid message_label expanding the toolbar size, and in turn expanding the # canvas size. # Without pack_propagate(False), when the user defines a small figure size @@ -774,7 +775,7 @@ def draw_rubberband(self, event, x0, y0, x1, y1): self.canvas._tkcanvas.delete(self.canvas._rubberband_rect_white) if self.canvas._rubberband_rect_black: self.canvas._tkcanvas.delete(self.canvas._rubberband_rect_black) - height = self.canvas.figure.bbox.height + height = self.canvas.get_width_height(physical=True)[1] y0 = height - y0 y1 = height - y1 self.canvas._rubberband_rect_black = ( diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index 1f2b6a45a07e..6fe5eca0d070 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -441,7 +441,7 @@ def draw(self): super().draw() def get_renderer(self): - w, h = self.figure.bbox.size + w, h = self.get_width_height(physical=True) key = w, h, self.figure.dpi reuse_renderer = (self._lastKey == key) if not reuse_renderer: diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 20a1a3c8f0a9..0cb54b31eab7 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -119,7 +119,7 @@ def _mpl_coords(self, event=None): x, y = event.x, event.y x = x * self.device_pixel_ratio # flip y so y=0 is bottom of canvas - y = self.figure.bbox.height - y * self.device_pixel_ratio + y = self.get_width_height(physical=True)[1] - y * self.device_pixel_ratio return x, y def scroll_event(self, widget, event): diff --git a/lib/matplotlib/backends/backend_gtk4.py b/lib/matplotlib/backends/backend_gtk4.py index 95b116e9a6ba..05594a76d5a2 100644 --- a/lib/matplotlib/backends/backend_gtk4.py +++ b/lib/matplotlib/backends/backend_gtk4.py @@ -117,7 +117,7 @@ def _mpl_coords(self, xy=None): x, y = xy x = x * self.device_pixel_ratio # flip y so y=0 is bottom of canvas - y = self.figure.bbox.height - y * self.device_pixel_ratio + y = self.get_width_height(physical=True)[1] - y * self.device_pixel_ratio return x, y def scroll_event(self, controller, dx, dy): diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index 39450ee32065..7cbf44c34767 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -305,7 +305,7 @@ def mouseEventCoords(self, pos=None): # (otherwise, it's already a QPoint) x = pos.x() # flip y so y=0 is bottom of canvas - y = self.figure.bbox.height / self.device_pixel_ratio - pos.y() + y = self.get_width_height()[1] - pos.y() return x * self.device_pixel_ratio, y * self.device_pixel_ratio def enterEvent(self, event): @@ -918,7 +918,7 @@ def set_message(self, s): self.locLabel.setText(s) def draw_rubberband(self, event, x0, y0, x1, y1): - height = self.canvas.figure.bbox.height + height = self.canvas.get_width_height(physical=True)[1] y1 = height - y1 y0 = height - y0 rect = [int(val) for val in (x0, y0, x1 - x0, y1 - y0)] diff --git a/lib/matplotlib/backends/backend_tkcairo.py b/lib/matplotlib/backends/backend_tkcairo.py index a6951c03c65a..6ecfcfcb8cf0 100644 --- a/lib/matplotlib/backends/backend_tkcairo.py +++ b/lib/matplotlib/backends/backend_tkcairo.py @@ -9,8 +9,7 @@ class FigureCanvasTkCairo(FigureCanvasCairo, FigureCanvasTk): def draw(self): - width = int(self.figure.bbox.width) - height = int(self.figure.bbox.height) + width, height = self.get_width_height(physical=True) surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) self._renderer.set_context(cairo.Context(surface)) self._renderer.dpi = self.figure.dpi diff --git a/lib/matplotlib/backends/backend_webagg_core.py b/lib/matplotlib/backends/backend_webagg_core.py index abd958fb0bcc..f1c6ae641feb 100644 --- a/lib/matplotlib/backends/backend_webagg_core.py +++ b/lib/matplotlib/backends/backend_webagg_core.py @@ -492,7 +492,7 @@ def add_web_socket(self, web_socket): assert hasattr(web_socket, 'send_binary') assert hasattr(web_socket, 'send_json') self.web_sockets.add(web_socket) - self.resize(*self.canvas.figure.bbox.size) + self.resize(*self.canvas.get_width_height(physical=True)) self._send_event('refresh') def remove_web_socket(self, web_socket): diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index 930a944b5274..3e07e5a14577 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -746,9 +746,9 @@ def _mpl_coords(self, pos=None): # flip y so y=0 is bottom of canvas if not wx.Platform == '__WXMSW__': scale = self.GetDPIScaleFactor() - return x*scale, self.figure.bbox.height - y*scale + return x*scale, self.get_width_height(physical=True)[1] - y*scale else: - return x, self.figure.bbox.height - y + return x, self.get_width_height(physical=True)[1] - y def _on_key_down(self, event): """Capture key press.""" @@ -1169,7 +1169,7 @@ def save_figure(self, *args): dialog.Destroy() def draw_rubberband(self, event, x0, y0, x1, y1): - height = self.canvas.figure.bbox.height + height = self.canvas.get_width_height(physical=True)[1] sf = 1 if wx.Platform == '__WXMSW__' else self.canvas.GetDPIScaleFactor() self.canvas._rubberband_rect = (x0/sf, (height - y0)/sf, x1/sf, (height - y1)/sf) diff --git a/lib/matplotlib/backends/backend_wxcairo.py b/lib/matplotlib/backends/backend_wxcairo.py index c53e6af4b873..3d8c73e7a414 100644 --- a/lib/matplotlib/backends/backend_wxcairo.py +++ b/lib/matplotlib/backends/backend_wxcairo.py @@ -8,8 +8,8 @@ class FigureCanvasWxCairo(FigureCanvasCairo, _FigureCanvasWxBase): def draw(self, drawDC=None): - size = self.figure.bbox.size.astype(int) - surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, *size) + width, height = self.get_width_height(physical=True) + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) self._renderer.set_context(cairo.Context(surface)) self._renderer.dpi = self.figure.dpi self.figure.draw(self._renderer) diff --git a/lib/matplotlib/tests/test_agg.py b/lib/matplotlib/tests/test_agg.py index c55f27e03a41..7ad21e83535c 100644 --- a/lib/matplotlib/tests/test_agg.py +++ b/lib/matplotlib/tests/test_agg.py @@ -387,3 +387,10 @@ def test_non_tuple_rgbaface(): fig.add_subplot(projection="3d").scatter( [0, 1, 2], [0, 1, 2], path_effects=[patheffects.Stroke(linewidth=4)]) fig.canvas.draw() + + +def test_rendered_height_floating_point_precision(): + fig = plt.figure(figsize=(1, 2.03), dpi=100) + assert fig.bbox.height < 203 # due to floating-point precision + fig.canvas.draw() + assert fig.canvas.buffer_rgba().shape == (203, 100, 4) diff --git a/lib/matplotlib/tests/test_backend_bases.py b/lib/matplotlib/tests/test_backend_bases.py index 0205eac42fb3..09b803ce58c6 100644 --- a/lib/matplotlib/tests/test_backend_bases.py +++ b/lib/matplotlib/tests/test_backend_bases.py @@ -581,3 +581,9 @@ def test_interactive_pan_zoom_events(tool, button, patch_vis, forward_nav, t_s): # Check if twin-axes are properly triggered assert ax_t.get_xlim() == pytest.approx(ax_t_twin.get_xlim(), abs=0.15) assert ax_b.get_xlim() == pytest.approx(ax_b_twin.get_xlim(), abs=0.15) + + +def test_get_width_height_floating_point_precision(): + fig = plt.figure(figsize=(1, 2.03), dpi=100) + assert fig.bbox.height < 203 # due to floating-point precision + assert fig.canvas.get_width_height() == (100, 203)