Skip to content

Conversation

@doronbehar
Copy link
Contributor

@doronbehar doronbehar commented Dec 1, 2025

PR summary

The RadioButtons widget now supports arranging buttons in a 2D grid by passing a list of lists of strings as the labels parameter. Each inner list represents a row in the grid.

Key features:

  • Active index and index_selected remain as single integers for the
    flattened array (reading left-to-right, top-to-bottom)
  • Column positions are automatically calculated based on the maximum
    text width in each column for optimal spacing
  • Text offset is now consistent between 1D and 2D layouts (0.10)
  • Includes new example: galleries/examples/widgets/radio_buttons_grid.py

🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com

PR checklist

@github-actions github-actions bot added topic: widgets/UI Documentation: examples files in galleries/examples labels Dec 1, 2025
@doronbehar doronbehar mentioned this pull request Dec 1, 2025
6 tasks
@doronbehar doronbehar force-pushed the RadioButtonsGrid branch 2 times, most recently from b660bc5 to d74610e Compare December 1, 2025 13:13
@github-actions github-actions bot added the Documentation: API files in lib/ and doc/api label Dec 1, 2025
@doronbehar
Copy link
Contributor Author

I don't know how to fix the other rstcheck CI errors...

@rcomer
Copy link
Member

rcomer commented Dec 4, 2025

I have opened #30810 for the rstcheck errors.

Copy link
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.

I'm slightly concerned with the 2D API:

  • a horizontal layout must be created via labels=[['button1', 'button2', 'button3']] which is not quite intuitive
  • OTOH 2D radio buttons are quite rare

So I'm wondering whether only supporting a 1D list and an orientation parameter would be better.

@story645
Copy link
Member

story645 commented Dec 4, 2025

a horizontal layout must be created via labels=[['button1', 'button2', 'button3']] which is not quite intuitive

That's also how .subplot_mosaic works - it's slightly annoying but also not really a big deal

@doronbehar
Copy link
Contributor Author

a horizontal layout must be created via labels=[['button1', 'button2', 'button3']] which is not quite intuitive

That's also how .subplot_mosaic works - it's slightly annoying but also not really a big deal

I agree. I added a commit that explains this explicitly. I was also wondering, would you be interested in a similar change to CheckButtons?

@timhoffm
Copy link
Member

timhoffm commented Dec 6, 2025

The difference with subplot_mosaic is that in general you want explicit placing in 2D and "N elements in a row" is just a special case. I'd claim that one never actually wants to define "button_i is in the third row of the second column." It's rather that you have a logical 1D list of elements and want to arrange them, most commonly all vertical, or all horizontal. The better analogy would be legend(), not subplot_mosaic() (though one could make a better API than controlling the behavior via ncols).

One option would be a layout parameter (naming and details t.b.d.) with values

  • "horizontal"
  • "vertical"
  • (rows, columns) as a grid, maybe extending this with a fill-order, e.g. (2, 5, "row-first").
    This would also be a reasonable extension for legend() and could replace ncols there in the long term.

@timhoffm
Copy link
Member

timhoffm commented Dec 6, 2025

I was also wondering, would you be interested in a similar change to CheckButtons?

Yes, CheckButtons should be handled analogously, but first let’s clarify the API.

@doronbehar
Copy link
Contributor Author

One option would be a layout parameter (naming and details t.b.d.) with values

  • "horizontal"
  • "vertical"
  • (rows, columns) as a grid, maybe extending this with a fill-order, e.g. (2, 5, "row-first").

This would also be a reasonable extension for legend() and could replace ncols there in the long term.

This indeed feels like a pretty good API. I won't mind implementing it, but you should note that at the moment it is possible to use:

buttons=[["A", "B"], ["C"]]

Which produces:

image

So are we sure we don't want to allow this behavior?

@timhoffm
Copy link
Member

timhoffm commented Dec 6, 2025

you should note that at the moment it is possible to use:

buttons=[["A", "B"], ["C"]]

You can achieve the same with labels=["A", "B", "C"], layout=(2, 2) - maybe ... layout=(2, 2, "row-first") depending on how we fill by default.

I concede that more complex arrangements like

A B
C
D E

are not possible. - We can later expand that API by either allowing None as an empty placeholder in the list or by then introducing 2D labels for explicit placement. But I would leave such extensions to when actual user requests arise.

@doronbehar
Copy link
Contributor Author

OK that's pretty convincing. Thinking about this again, at the moment I avoided taking care of inputs like: labels=["A", "B", "C"], layout=(2, 2) because TBH it doesn't look that nice and as you said it can be implemented in the future with None, while still demanding the amount of labels will fit the multiplication of the layout tuple.

As for layout=(2, 2, "row-first"), it can be achieved via reordering the input labels, so I haven't implemented support for this too.

Now this feature is thoroughly tested, both the Python and Rst examples.

doronbehar and others added 2 commits December 7, 2025 12:44
The RadioButtons widget now supports arranging buttons in a 2D grid by
passing a list of lists of strings as the labels parameter. Each inner
list represents a row in the grid.

Key features:
- Active index and index_selected remain as single integers for the
  flattened array (reading left-to-right, top-to-bottom)
- Column positions are automatically calculated based on the maximum
  text width in each column for optimal spacing
- Text offset is now consistent between 1D and 2D layouts (0.10)
- Includes new example: galleries/examples/widgets/radio_buttons_grid.py

Closes matplotlib#13374

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@doronbehar
Copy link
Contributor Author

@timhoffm since I used the same algorithm to distribute the buttons and texts for horizontal layouts too, there are tests failing now due to slight differences in the positions of the elements. Would you accept these differences?

check_radio_buttons[png]

My distribution:

test_radio_buttons png

Current distribution:

test_radio_buttons png -expected

test_radio_buttons[png]

My distribution:

check_radio_buttons

Current distribution:

check_radio_buttons

@timhoffm
Copy link
Member

timhoffm commented Dec 7, 2025

Overall, I find this a bit too invasive to immediately roll out; in particular because (1) it temporarily adds artists and triggers a draw and (2) it changes the positions of the buttons.

Let's do the following: factor the position calculation out into a separate function, and introduce layout=None, which reproduces the existing behavior

        if layout is None:
            button_xs, button_ys, label_xs, label_ys = _legacy_radio_layout(labels, ...)
        else:
            button_xs, button_ys, label_xs, label_ys = _radio_layout(labels, ...)

        [...]

        self.labels = [
            ax.text(x, y, label, transform=ax.transAxes,
                    horizontalalignment="left", verticalalignment="center",
                    **props)
            for x, y, label, props in zip(label_xs, label_ys, labels, label_props)]
        [...]
        self._buttons = ax.scatter(button_xs, button_ys, **radio_props)

By defining layout=None we can keep the existing behavior by default for now and make the new layout opt-in.

@doronbehar
Copy link
Contributor Author

Indeed outsourcing this layout algorithm would be useful also for CheckButtons. This should be good now (TBH, here on NixOS I experience an RMS 6.101 failure for test_check_radio_buttons_image[png] even on branch main, but that's out of scope for this PR).

activecolor : :mpltype:`color`
The color of the selected button. The default is ``'blue'`` if not
specified here or in *radio_props*.
layout : None, {"vertical", "horizontal"} or (int, int), default: None
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
layout : None, {"vertical", "horizontal"} or (int, int), default: None
layout : None or {"vertical", "horizontal"} or (int, int), default: None

numpydoc convention is to separate all types by "or"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

numpydoc convention is to separate all types by "or"

I used an or between the literal strings too, to make it clear the set {"vertical", "horizontal"} is not allowed.

Copy link
Member

@timhoffm timhoffm Dec 8, 2025

Choose a reason for hiding this comment

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

Okish, but in general https://numpydoc.readthedocs.io/en/latest/format.html#parameters

When a parameter can only assume one of a fixed set of values, those values can be listed in braces, with the default appearing first

There's no explicit rule how to combine this with or (int, int) though. But I'd slighly prefer None or {"vertical", "horizontal"} or (int, int)

@doronbehar
Copy link
Contributor Author

Maybe before accepting this change, I should perform this change to CheckButtons?

@timhoffm
Copy link
Member

timhoffm commented Dec 8, 2025

As you wish. The inclusion of CheckButtons is small enough so that it can be done here, but it can also be done in a followup PR.

@doronbehar doronbehar changed the title RadioButtons: Add 2D grid labels layout support {Radio,Check}Buttons: Add 2D grid labels layout support Dec 8, 2025
@doronbehar
Copy link
Contributor Author

Python 3.11 on ubuntu-22.04 (Minimum Versions) failures are unrelated...

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants