Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Preserve legend settings when regenerating in Qt figure options dialog#31553

Open
thesanatt wants to merge 2 commits intomatplotlib:mainfrom
thesanatt:fix/legend-settings-qt
Open

Preserve legend settings when regenerating in Qt figure options dialog#31553
thesanatt wants to merge 2 commits intomatplotlib:mainfrom
thesanatt:fix/legend-settings-qt

Conversation

@thesanatt
Copy link
Copy Markdown

Closes #17775

Summary

When using the Qt figure options dialog and ticking "(Re-)generate automatic legend", previously only ncols and draggable state were preserved. All other legend customizations (loc, fontsize, frameon, shadow, title, mode, spacing parameters, etc.) were lost.

This PR extracts and reapplies these properties from the old legend when regenerating.

Changes

  • lib/matplotlib/backends/qt_editor/figureoptions.py: Extract legend properties (loc, bbox_to_anchor, fontsize, frameon, shadow, framealpha, title, mode, columnspacing, labelspacing, handlelength, handletextpad) from the existing legend before regenerating, and pass them as kwargs to the new axes.legend() call.
  • lib/matplotlib/tests/test_figureoptions_legend.py: Added 3 tests covering property preservation, no-existing-legend case, and mode preservation.

AI Disclosure

I used AI assistance (Claude) for exploring the codebase and drafting parts of the implementation and tests. All code was reviewed and tested by me.

When using the Qt figure options dialog to regenerate a legend,
previously only ncols and draggable state were preserved. This
caused all other legend customizations (loc, fontsize, frameon,
shadow, title, mode, spacing, etc.) to be lost.

This change extracts and reapplies these properties when
regenerating the legend.

Closes matplotlib#17775
Copy link
Copy Markdown
Member

@timhoffm timhoffm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue stems from a larger architectural shortcoming: Legends are not designed to be modified after creation. This makes it necessary to recreate the legend, which results in loosing preexisting properties.

For now, we have to live with this workaround. But the implementation should be different as knowning legend properties is out of scope for the figureoptions dialog: Please create a helper method Legend._get_properties(). So that here you just do

new_legend = axes.legend(ncols=ncols, **old_legend._get_properties())

Please decide whether a white-list of properties or a exclude-list of all legend properties (https://matplotlib.org/stable/api/_as_gen/matplotlib.artist.ArtistInspector.html#matplotlib.artist.ArtistInspector.properties) is more suitable.

The tests are meaningless as they do not exercise the modified code path. Anyway, I would not test options dialog explicitly as that's too cumbersome. Instead think whether you can generate a meaningful non-tautological _get_properties() test. If not we may keep this untested as before.

Address review feedback from @timhoffm:
- Added Legend._get_properties() method that returns a dict of
  legend properties needed for recreation
- figureoptions.py now calls old_legend._get_properties() instead
  of extracting properties inline
- Updated tests to directly test _get_properties() method
- Tests cover basic properties, mode/bbox_to_anchor, roundtrip
  recreation, and no-existing-legend edge case
Copy link
Copy Markdown
Member

@timhoffm timhoffm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thesanatt there's too little thought, oversight and review from your side. In particular when using AI we expect that contributors thoroughly assess the output.

I can faster use an AI and iterate that to completion than doing multiple reviews cycles. So third party contributions using AI only make sense when you already catch most of the deficits of AI output.

Comment on lines 258 to 259
if new_legend:
new_legend.set_draggable(draggable)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to not include draggable in _get_properties()?

Comment thread lib/matplotlib/legend.py
settings when regenerating the legend.
"""
props = {
'loc': self._loc,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The list seems incomplete. On what basis did you select the properties?

Comment on lines +15 to +26
ax.legend(
loc='upper right',
fontsize=14,
frameon=False,
shadow=True,
title='My Legend',
ncols=2,
columnspacing=3.0,
labelspacing=1.5,
handlelength=4.0,
handletextpad=1.2,
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make it more compact and safe by not repeating the names and values

Suggested change
ax.legend(
loc='upper right',
fontsize=14,
frameon=False,
shadow=True,
title='My Legend',
ncols=2,
columnspacing=3.0,
labelspacing=1.5,
handlelength=4.0,
handletextpad=1.2,
)
initial_props = dict(
loc='upper right',
fontsize=14,
frameon=False,
shadow=True,
title='My Legend',
columnspacing=3.0,
labelspacing=1.5,
handlelength=4.0,
handletextpad=1.2,
)
legend = ax.legend(ncols=2, **props)

to be continued below

Comment on lines +31 to +41
assert props['loc'] == legend._loc
assert props['fontsize'] == 14
assert props['frameon'] is False
assert props['shadow'] is True
assert props['title'] == 'My Legend'
assert props['columnspacing'] == 3.0
assert props['labelspacing'] == 1.5
assert props['handlelength'] == 4.0
assert props['handletextpad'] == 1.2
assert 'bbox_to_anchor' not in props
assert 'mode' not in props
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert props['loc'] == legend._loc
assert props['fontsize'] == 14
assert props['frameon'] is False
assert props['shadow'] is True
assert props['title'] == 'My Legend'
assert props['columnspacing'] == 3.0
assert props['labelspacing'] == 1.5
assert props['handlelength'] == 4.0
assert props['handletextpad'] == 1.2
assert 'bbox_to_anchor' not in props
assert 'mode' not in props
assert props == initial_props

Make sure you include all props returned by _get_properties to get full coverage and be able to just assert dict equality.

handletextpad=1.2,
)

legend = ax.get_legend()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
legend = ax.get_legend()

assert 'bbox_to_anchor' not in props
assert 'mode' not in props

plt.close(fig)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
plt.close(fig)

Not needed. Closing is automatic in tests.

Comment thread lib/matplotlib/legend.py
Comment on lines +1123 to +1126
if self._bbox_to_anchor is not None:
props['bbox_to_anchor'] = self._bbox_to_anchor.bounds
if self._mode is not None:
props['mode'] = self._mode
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can these two not be always returned? If that is the case document that they are only conditionally present and explain why.

plt.close(fig)


def test_get_properties_roundtrip():
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this tests tell us more compared to test_get_properties_basic?

plt.close(fig)


def test_legend_regeneration_no_existing_legend():
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I completely do not understand this test. There is no regeneration. You only create one legend. This test does not touch any library code modified in this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

clone more legend settings when regenerating in Qt figure options dialog

2 participants