From 03904bccde4efa3d9c0120f30569cd5d2fffcdd7 Mon Sep 17 00:00:00 2001 From: YushuoSun Date: Tue, 8 Sep 2026 22:10:05 +1000 Subject: [PATCH 1/2] Preserve legend settings during Qt figure option regen Signed-off-by: YushuoSun --- .../backends/qt_editor/figureoptions.py | 43 +++++++++- lib/matplotlib/tests/test_backend_qt.py | 79 +++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/qt_editor/figureoptions.py b/lib/matplotlib/backends/qt_editor/figureoptions.py index cd4e9583cce5..535a5a7fe2f5 100644 --- a/lib/matplotlib/backends/qt_editor/figureoptions.py +++ b/lib/matplotlib/backends/qt_editor/figureoptions.py @@ -7,6 +7,7 @@ from itertools import chain from matplotlib import cbook, cm, colors as mcolors, markers, image as mimage +from matplotlib.patches import BoxStyle from matplotlib.backends.qt_compat import QtGui from matplotlib.backends.qt_editor import _formlayout from matplotlib.dates import DateConverter, num2date @@ -251,8 +252,48 @@ def apply_callback(data): if axes.legend_ is not None: old_legend = axes.get_legend() draggable = old_legend._draggable is not None + bbox_to_anchor = old_legend._bbox_to_anchor + bbox_to_anchor_kwargs = {} + if bbox_to_anchor is not None: + # Keep the same bbox anchor and transform. + bbox_to_anchor_kwargs = { + "bbox_to_anchor": bbox_to_anchor._bbox, + "bbox_transform": bbox_to_anchor._transform, + } + ncols = old_legend._ncols - new_legend = axes.legend(ncols=ncols) + new_legend = axes.legend( + loc=old_legend._loc, + ncols=ncols, + numpoints=old_legend.numpoints, + markerscale=old_legend.markerscale, + scatterpoints=old_legend.scatterpoints, + borderpad=old_legend.borderpad, + labelspacing=old_legend.labelspacing, + handlelength=old_legend.handlelength, + handleheight=old_legend.handleheight, + handletextpad=old_legend.handletextpad, + borderaxespad=old_legend.borderaxespad, + columnspacing=old_legend.columnspacing, + mode=old_legend._mode, + fancybox=isinstance( + old_legend.legendPatch.get_boxstyle(), + BoxStyle.Round, + ), + shadow=old_legend.shadow, + title=old_legend.get_title().get_text(), + title_fontproperties=old_legend.get_title().get_fontproperties(), + frameon=old_legend.get_frame_on(), + framealpha=old_legend.legendPatch.get_alpha(), + edgecolor=old_legend.legendPatch.get_edgecolor(), + facecolor=old_legend.legendPatch.get_facecolor(), + linewidth=old_legend.legendPatch.get_linewidth(), + alignment=old_legend.get_alignment(), + prop=old_legend.prop, + **bbox_to_anchor_kwargs, + ) + else: + new_legend = axes.legend(ncols=ncols) if new_legend: new_legend.set_draggable(draggable) diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index ae24effe505f..b737c06ffbe7 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -218,6 +218,85 @@ def test_figureoptions(): fig.canvas.manager.toolbar.edit_parameters() +@pytest.mark.backend('QtAgg', skip_on_importerror=True) +def test_figureoptions_preserves_legend_settings(): + from matplotlib.backends.qt_editor import figureoptions + + fig, ax = plt.subplots() + ax.plot([0, 1], [0, 1], label="line a") + ax.plot([0, 1], [1, 0], label="line b") + + old_legend = ax.legend( + loc="lower left", + bbox_to_anchor=(0.1, 0.2, 0.3, 0.4), + ncols=2, + numpoints=2, + markerscale=1.5, + borderpad=0.9, + labelspacing=0.6, + handlelength=1.8, + handleheight=0.9, + handletextpad=0.7, + borderaxespad=0.3, + columnspacing=1.6, + mode="expand", + fancybox=True, + shadow=True, + title="Legend title", + frameon=True, + framealpha=0.4, + alignment="right", + ) + old_bbox = old_legend._bbox_to_anchor + + def fake_fedit(datalist, **kwargs): + general = list(datalist[0][0]) + general[-1] = True + payload = [general] + if len(datalist) > 1: + payload.append([curve[0] for curve in datalist[1]]) + if len(datalist) > 2: + payload.append([mappable[0] for mappable in datalist[2]]) + kwargs["apply"](payload) + + with mock.patch( + "matplotlib.backends.qt_editor.figureoptions._formlayout.fedit", + side_effect=fake_fedit, + ): + figureoptions.figure_edit(ax) + + new_legend = ax.get_legend() + + assert new_legend._loc == old_legend._loc + assert new_legend._ncols == old_legend._ncols + assert new_legend.numpoints == old_legend.numpoints + assert new_legend.markerscale == old_legend.markerscale + assert new_legend.scatterpoints == old_legend.scatterpoints + assert new_legend.borderpad == old_legend.borderpad + assert new_legend.labelspacing == old_legend.labelspacing + assert new_legend.handlelength == old_legend.handlelength + assert new_legend.handleheight == old_legend.handleheight + assert new_legend.handletextpad == old_legend.handletextpad + assert new_legend.borderaxespad == old_legend.borderaxespad + assert new_legend.columnspacing == old_legend.columnspacing + assert new_legend._mode == old_legend._mode + assert new_legend.get_alignment() == old_legend.get_alignment() + assert new_legend.shadow == old_legend.shadow + assert new_legend.get_frame_on() == old_legend.get_frame_on() + assert new_legend.legendPatch.get_alpha() == pytest.approx( + old_legend.legendPatch.get_alpha()) + assert new_legend.legendPatch.get_linewidth() == pytest.approx( + old_legend.legendPatch.get_linewidth()) + assert list(new_legend.legendPatch.get_facecolor()) == pytest.approx( + list(old_legend.legendPatch.get_facecolor())) + assert list(new_legend.legendPatch.get_edgecolor()) == pytest.approx( + list(old_legend.legendPatch.get_edgecolor())) + assert new_legend.get_title().get_text() == old_legend.get_title().get_text() + assert new_legend.prop == old_legend.prop + assert new_legend._bbox_to_anchor._bbox.bounds == old_bbox._bbox.bounds + assert new_legend._bbox_to_anchor._transform == old_bbox._transform + + @pytest.mark.backend('QtAgg', skip_on_importerror=True) def test_save_figure_return(tmp_path): fig, ax = plt.subplots() From 3dbb6cbb0407fc3d424bff8f6660a261ccba1e41 Mon Sep 17 00:00:00 2001 From: YushuoSun Date: Wed, 9 Sep 2026 23:27:04 +1000 Subject: [PATCH 2/2] test(qt): fix legend callback payload --- lib/matplotlib/tests/test_backend_qt.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index b737c06ffbe7..9690bebee5c3 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -249,14 +249,24 @@ def test_figureoptions_preserves_legend_settings(): ) old_bbox = old_legend._bbox_to_anchor + def form_values(form): + values = [] + for label, value in form: + if label is None: + continue + if isinstance(value, (list, tuple)): + value = value[0] + values.append(value) + return values + def fake_fedit(datalist, **kwargs): - general = list(datalist[0][0]) + general = form_values(datalist[0][0]) general[-1] = True payload = [general] if len(datalist) > 1: - payload.append([curve[0] for curve in datalist[1]]) + payload.append([form_values(curve[0]) for curve in datalist[1][0]]) if len(datalist) > 2: - payload.append([mappable[0] for mappable in datalist[2]]) + payload.append([form_values(mappable[0]) for mappable in datalist[2][0]]) kwargs["apply"](payload) with mock.patch(