Skip to content

Convert guides to ggproto #4879

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

Merged
merged 131 commits into from
Apr 24, 2023
Merged

Convert guides to ggproto #4879

merged 131 commits into from
Apr 24, 2023

Conversation

teunbrand
Copy link
Collaborator

@teunbrand teunbrand commented Jun 18, 2022

This is a WIP PR addressing #3329.
Since the aim is a substantial overhaul of the guide system, it would be good to gather some feedback along the way. I was hoping we could align on some vision early on, which would make subsequent steps easier. For now, I made a first step by making a parent Guide class and GuideAxis to get the ball rolling. This step is small enough that it wouldn't matter if we decide on a totally different direction.

A few comments from my side that aren't in the code itself:

  • The general aim was to modularise code. I hope this can serve the purposes of countering code duplication and making it easier to extend.
  • The current GuideAxis is visually identical to the previous axis guide.
  • Also had to implement GuideNone to get this to work.
  • Two types of test are currently failing (locally):
    • Almost all visual tests. This is merely because the order of grobs in the axis has changed, which matters for the svg files, but not for the human eye. I can reverse-engineer the previous order if required.
    • One test for 'Using non-position guides for position scales results in an informative error', which should be resolved when guide_legend() is also converted.

The type of feedback I was hoping to get is (not limited to) the following:

  • General feedback about the design.
  • The naming of arguments, parameters, functions etc.
  • The 'statelessness' of ggproto objects. I know this should be pursued, but I couldn't immediately figure out how to transition the previously stateful S3 classes to a stateless ggproto class.
  • In the draw-method (previously the guide_gengrob() method), I just pass around the entire params list to every function that might use it. I don't know whether it is a bad thing, but if it might be useful if there was some elegant way to pass just the parameters it needs, without resorting to very verbose code.

Some magic github incantations:
Fix #4879, Fix #4364, Fix #4930, Fix #2728, Fix #4650, Fix #4958, Fix #5152

@teunbrand teunbrand marked this pull request as draft June 18, 2022 21:57
Merge branch 'main' of https://github.com/tidyverse/ggplot2 into tidyverse-main2

# Conflicts:
#	R/guides-.r
#	R/guides-axis.r
@teunbrand
Copy link
Collaborator Author

Hi @thomasp85, do you think you want to course-correct this WIP-PR at some point (whenever is convenient for you, I know these are busy times with the conference and all) or should I continue with the next guide? I was thinking about guide_legend next.

@thomasp85
Copy link
Member

I'll take a look next week as conference gets behind me. Thanks for reminding me

Copy link
Member

@thomasp85 thomasp85 left a comment

Choose a reason for hiding this comment

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

I would like to challenge you to think about a way where we can make the guide implementations themselves stateless. Not for the purity of heart, but more as a way to make the implementation more strict and understandable. Making it stateless requires you to think about the input and output of each function call more clearly and I think this will benefit us going forward. If you'd like to discuss this in more detail let me know and I'll set up a meeting

@teunbrand
Copy link
Collaborator Author

Thanks for taking a look, Thomas!

make the guide implementations themselves stateless

My intuition here would then be to tinker with the build_guides function to decompose the various states of the gdefs variable to explicitly pass around things like params, key and other parts that make the classes currently stateful. I'd have to figure out how this differs between position and non-position guides to see if we can reduce code duplication. Does that direction sound reasonable to you?

@thomasp85
Copy link
Member

Yes exactly. Having a clear expected output of a method makes it so much easier to build up intuition for what it is supposed to do. You may also think about having a Guides class that orchestrates it all and keeps all the stateful information but I'll leave that up to you

@thomasp85
Copy link
Member

Maybe instead of testing we should make sure we have a very clear understanding of which values in the guides and guidelist are susceptible to change during a rendering pass, and then make sure that changes in these values does not leak into a new rendering pass...

@thomasp85
Copy link
Member

This is the sort of situation I am eager to avoid #4475

Comment on lines +128 to +131
# `guides` is the only initially mutable field.
# It gets populated as a user adds `+ guides(...)` to a plot by the
# `Guides$add()` method.
guides = list(),
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This should be the only mutable field up to the point of rendering

Comment on lines +289 to +301
# Setup routine for resolving and validating guides based on paired scales.
#
# The output of the setup is a child `Guides` class with two additional
# mutable fields, both of which are parallel to the child's `Guides$guides`
# field.
#
# 1. The child's `Guides$params` manages all parameters of a guide that may
# need to be updated during subsequent steps. This ensures that we never need
# to update the `Guide` itself and risk reference class shenanigans.
#
# 2. The child's `Guides$aesthetics` holds the aesthetic name of the scale
# that spawned the guide. The `Coord`'s own build methods need this to
# correctly pick the primary and secondary guides.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I tried to clarify here a bit that two other fields are also mutable

Comment on lines +368 to +374
ggproto(
NULL, self,
guides = new_guides,
# Extract the guide's params to manage separately
params = lapply(new_guides, `[[`, "params"),
aesthetics = aesthetics
)
Copy link
Collaborator Author

@teunbrand teunbrand Apr 16, 2023

Choose a reason for hiding this comment

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

Here are these mutable fields added to a child ggproto object and the original guide's parameters are copied. If the user has provided a guide directly, e.g. x = guide_axis() instead of x = "axis", the Guides class has never touched the guide_axis() other than to read out available_aes and such.

Comment on lines +57 to +62
ggproto(
NULL, super,
params = params,
elements = elems,
available_aes = available_aes
)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The guide constructor never updates the class with a user's input. It takes the user's input and fuses it to the guide's defaults, which lives on in a child instead of modifying the parent.

@teunbrand
Copy link
Collaborator Author

I've reordered the Guides class a bit so that many methods follow the order in which they are called. I also added some clarifying comments about what the mutable fields are. Because the diff of the last commit is difficult to read, I've pointed towards the relevant bits in the comments above.

Copy link
Member

@thomasp85 thomasp85 left a comment

Choose a reason for hiding this comment

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

Okay let's do this :-)

But maybe wait until Monday with merging just to avoid the "never push to prod on a friday" situation :-)

@teunbrand

This comment was marked as resolved.

@teunbrand
Copy link
Collaborator Author

Good call to wait after the weekend, I just caught a bug that I should've caught before :)

@thomasp85
Copy link
Member

I don't think GitHub picks up fix keywords in comments so you may want to add them to the first message instead

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