Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use scatter for check boxes instead of Rectangle #24474

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

chahak13
Copy link
Contributor

@chahak13 chahak13 commented Nov 16, 2022

ref: #24471

With the current implementation, the boxes get stretched into rectangles if the aspect ratio is not maintained. To overcome this, the boxes are now created using scatter instead to maintain their shapes.

Documentation and Tests

  • Has pytest style unit tests (and pytest passes)
  • Documentation is sphinx and numpydoc compliant (the docs should build without error).
  • New plotting related features are documented with examples.

Release Notes

  • New features are marked with a .. versionadded:: directive in the docstring and documented in doc/users/next_whats_new/
  • API changes are marked with a .. versionchanged:: directive in the docstring and documented in doc/api/next_api_changes/
  • Release notes conform with instructions in next_whats_new/README.rst or next_api_changes/README.rst

With the current implementation, the boxes get stretched into rectangles
if the aspect ratio is not maintained. To overcome this, the boxes are
now created using scatter instead to maintain their shapes.
if self.drawon:
self.ax.figure.canvas.draw()
Copy link
Contributor Author

@chahak13 chahak13 Nov 16, 2022

Choose a reason for hiding this comment

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

@anntzer I have a preliminary implementation for square check boxes but I'm running into one small issue. When I try running this, everything works as intended when we change the figure size (the check boxes remain square), but the unit test fails. For some reason, calling set_active(0) in code doesn't change the status of the checkboxes. The issue is arising in these lines. The status changes correctly based on the code that I added but after executing self.ax.figure.canvas.draw(), the colors for all the scatter markers change back to what they were initially set to be, and not the changed ones. This is happening only in the unit tests and not while actually checking it using a GUI backend. Any ideas/suggestions?

Copy link
Contributor Author

@chahak13 chahak13 Nov 16, 2022

Choose a reason for hiding this comment

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

I feel like I'm missing something but can't see it right now.

Copy link
Contributor

@anntzer anntzer Nov 16, 2022

Choose a reason for hiding this comment

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

If you do some print-debugging and grep for places that could touch facecolors, you'll see that Collection.update_scalarmappable gets called at some point which causes the facecolors to be reset, but you'll need to figure out by yourself why (because I don't actually know immediately).

Copy link
Contributor Author

@chahak13 chahak13 Nov 16, 2022

Choose a reason for hiding this comment

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

That helps, thanks! I'll try figuring out why this is happening. The main reason why I was getting confused was that it happened only when set_active was explicitly called but worked as intended with the actual figure. Should be able to figure this out now though, hopefully.

Copy link
Contributor Author

@chahak13 chahak13 Nov 16, 2022

Choose a reason for hiding this comment

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

@anntzer this is actually true for the radio buttons too! Small code to reproduce (based on the checkbox example)

radio = RadioButtons(rax, labels, active=1, activecolor="k")
print(radio.value_selected)
print(radio._buttons.get_facecolor())
print("Changing active button from 1 to 0")
radio.set_active(0)
print(radio.value_selected)
print(radio._buttons.get_facecolor())

Output:

4 Hz
[[0. 0. 0. 0.]
 [0. 0. 0. 1.]
 [0. 0. 0. 0.]]
Changing active button from 1 to 0
2 Hz
[[0. 0. 0. 0.]
 [0. 0. 0. 1.]
 [0. 0. 0. 0.]]

This shows that the facecolors for the buttons don't change even after set_active(0) is called, even though value_selected is correct. The problem arises because of these lines

# The flags are initialized to None to ensure this returns True
# the first time it is called.
edge0 = self._edge_is_mapped
face0 = self._face_is_mapped

Since it sets the flags such that it returns True the first time,

else:
self._set_facecolor(self._original_facecolor)
if self._edge_is_mapped:
self._edgecolors = self._mapped_colors
else:
self._set_edgecolor(self._original_edgecolor)
self.stale = True

set the original colors again. Since update_scalarmappable gets called the first time they're drawn, when creating a figure, all functionalities work as everything works as expected after the second call. Even in this case, if there are any set_active calls after the first one, they work fine.

Copy link
Contributor

@anntzer anntzer Nov 16, 2022

Choose a reason for hiding this comment

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

Thanks for the investigation, I have opened #24479 to track this. Sorry for sending you down this rabbit hole, perhaps this was not so easy after all...

Copy link
Contributor Author

@chahak13 chahak13 Nov 16, 2022

Choose a reason for hiding this comment

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

Thanks, it was fun. But now with the question on hand, this bug stops me to run get_status for the check boxes correctly. I guess one workaround could be that I maintain an active array similar to value_active in radio buttons but that'll be a hack and I'm on the fence about having an extra array to handle that when it can be done with the colors itself. That'll pass the tests though, and everything works in a figure anyway. Suggestions?

p = Rectangle(xy=(x, y), width=w, height=h, edgecolor='black',
facecolor=axcolor, transform=ax.transAxes)
ys = np.linspace(1, 0, len(labels)+2)[1:-1]
text_size = mpl.rcParams["font.size"] / 2
Copy link
Contributor

@oscargus oscargus Nov 16, 2022

Choose a reason for hiding this comment

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

This seems unused? (Otherwise I would have asked why it is halved?)

Copy link
Contributor Author

@chahak13 chahak13 Nov 16, 2022

Choose a reason for hiding this comment

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

That's a mistake. It's supposed to be used for marker size. As to why it is halved, it's because radio buttons are also half the text size, that's why. I think it's arbitrary.

@tacaswell
Copy link
Member

tacaswell commented Nov 16, 2022

I do not think that going with scatter is the correct approach here. Looking at the internals of Rectangle we have a (private) knob to account for the aspect ratio

# Required for RectangleSelector with axes aspect ratio != 1
# The patch is defined in data coordinates and when changing the
# selector with square modifier and not in data coordinates, we need
# to correct for the aspect ratio difference between the data and
# display coordinate systems. Its value is typically provide by
# Axes._get_aspect_ratio()
self._aspect_ratio_correction = 1.0

I suspect that making sure this gets set on draw is a much simpler way to solve this problem.

@chahak13
Copy link
Contributor Author

chahak13 commented Nov 17, 2022

@tacaswell Just making sure that I'm understanding correctly, but if we do that then we're kinda working on the current implementation itself, right? In that case, we'll also have to make a call on should the checkbox size increase, while maintaining the square shape or not?

@timhoffm
Copy link
Member

timhoffm commented Nov 17, 2022

I do not think that going with scatter is the correct approach here.

@tacaswell why? Using markers instead of cobbling the box together from basic artists has some appeal. You don't have to bother with aspect. You get pixel snapping. You have only a single artist to change for updating the check state. And you can switch to more complex markers like a check symbol relatively easy.

Note also that @anntzer did the same thing in #24455 with radio buttons.

if colors.same_color(
self._crosses.get_facecolor()[index], colors.to_rgba("none")
):
self._crosses.get_facecolor()[index] = colors.to_rgba("k")
else:
self._crosses.get_facecolor()[index] = colors.to_rgba("none")
Copy link
Member

@ksunden ksunden Nov 17, 2022

Choose a reason for hiding this comment

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

The point made in #24479 is that editing the output of the getter function is not guaranteed to keep those changes (it works, sometimes, because it is not a copy that is returned, but there are other mechanisms at play)

more properly this would be:

facecolors = self._crosses.get_faceolor()
facecolors[index] = colors.to_rgba("k") # or "none"
self._crosses.set_facecolor(facecolors)

This calls the setter which updates the internal state properly

(This also does need to be corrected in the radio button implementation)

@anntzer
Copy link
Contributor

anntzer commented Nov 17, 2022

From a quick look, _aspect_ratio_correction, introduced in #20839 for the rectangle selector widget, seems actually brittle, as 1) it will basically never work for polar plots (not a concern here, but one for rectangle selectors in screen space), and 2) it makes one of the two axises the "main" one relative to the other (I think for circles the radius will effectively be the radius in relative units along the x axis, not along the y axis), whereas the proper thing to do would be to have a radius in pixels (basically proportional to the font size, to make the radio button close to the size of the label).
If we really want to do this with individual artists we'd need Circle to support having different transforms for center and radius (a bit like how PathCollections have both a transform and an offset_transform, which is what we're effectively relying on here).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants