seaborn icon indicating copy to clipboard operation
seaborn copied to clipboard

Allow the user to specify a figure to reuse when creating a FacetGrid

Open Erotemic opened this issue 2 years ago • 9 comments

Currently FacetGrid and its consumers relplot and catplot will always create a new figure when you call them. In my workflow, I often like to create the figures or axes beforehand and then pass these as arguments to seaborn functions. This works with axes level plots line lineplot and scatterplot, where you can pass the ax parameter, and if you don't it uses plt.gca().

I would like to propose that FacetGrid should take a parameter fig=None, and if specified it uses it, otherwise it uses the existing code as such:

if fig is None:
        with mpl.rc_context({"figure.autolayout": False}):
            fig = plt.figure(figsize=figsize)

That would allow me to force specific plots to be associated with specific figure numbers, which helps me keep track of things as I work. I can make this PR if there is interest in this feature. I made a draft here: #2831 and I will work on the remaining bullet points if there is interest.

Erotemic avatar Jun 02 '22 14:06 Erotemic

How does this option work together with the height= and aspect= parameters? How are a variable number of columns and rows handled? Will pre-existing subplots and other elements be erased? How will the extra space for the figure legend be created?

jhncls avatar Jun 02 '22 20:06 jhncls

This has definitely come up before, I think I have articulated some thoughts. Basically, making this possible is straightforward; your approach is the right one. The blocker has always been thinking through all the edge cases that would result from enabling this, given that FacetGrid was written with the assumption that it would always own its figure.

@jhncls hits on several of the important points. I assume that the main usecase here is when working with a subfigure, (I am not sure I see a need for it otherwise, except maybe to use a figure that is not plugged intopyplot, e.g. in a GUI interface)?

The main thing is the fact that FacetGrid and PairGrid put the legend outside the space of subplots and resize the figure that they are in to make space for it They also call tight_layout, using a bounding box that assumes the grid is the only thing in the figure. Neither operation would work well in a subfigure context.

The edge cases around the size parameters in the constructor would take some care — you'd want to fire a helpful warning if they are used, but they current have non-null default values. This adds some complexity, although I think the API decisions are reasonably straightforward.

There may be other considerations I am not immediately thinking of.

Another thing to note: this would need to be rolled out to the other Grid objects as well, and the figure-level functions that use them should probably get the fig parameter as well. So it would be a not-insubstantial amount of work.

mwaskom avatar Jun 02 '22 22:06 mwaskom

I'll take a look at these cases and try to formulate a coherent plan. I think the general strategy is going to be: if the user is specifying the figure, they are responsible for setting it up in a "clean" way, and if they don't then results may not be expected (although I agree it's a good idea to fire off a warning if we can detect these cases).

For all API decisions, the defaults when fig=None will be functionally unchanged.

For height and aspect they could either:

  • continue working as is, and the user's figure is resized unless they specifically disable them
  • change them to an "auto" default, which has different behavior based on if fig is specified or not.

I'm in favor of the "auto" approach, and if they are specified as numbers they are always applied. I think the same approach will work with calling "tight_layout".

The legend issue is a tricky one that I will have to think about, but I think it can be dealt with in a sensible way.

I assume that the main usecase here is when working with a subfigure, (I am not sure I see a need for it otherwise, except maybe to use a figure that is not plugged intopyplot, e.g. in a GUI interface)?

That is a use case. But I simply want to control the number of figures on my screen by assigning them all numbers. I don't want FacetGrid to always pop open a new figure. I want it to reuse an existing one. I don't think I care if it clears it. I might care if it is resized, but it probably isn't that big of a deal.

Another thing to note: this would need to be rolled out to the other Grid objects as well, and the figure-level functions that use them should probably get the fig parameter as well. So it would be a not-insubstantial amount of work.

I'll probably work on this on and off. I think I care enough about it where I'm not going to abandon it, but I'm also more interested in just having the feature rather than working on implementing it. So if anyone wants to get to it before I do that would be helpful.

Erotemic avatar Jun 02 '22 23:06 Erotemic

if the user is specifying the figure, they are responsible for setting it up in a "clean" way, and if they don't then results may not be expected

Easy enough to say, but you're not going to be the one dealing with the bug reports ...

mwaskom avatar Jun 02 '22 23:06 mwaskom

but you're not going to be the one dealing with the bug reports ...

You can always @ me :wink:

More seriously, I do think it's the right approach and I think we can set this up in a way to minimize foot-shooting.

Erotemic avatar Jun 03 '22 00:06 Erotemic

To be clear — I am in favor of adding this functionality (and it already exists in the new objects interface). I just expect it to take some careful thought about how to handle all the edge cases.

mwaskom avatar Jun 04 '22 19:06 mwaskom

@Erotemic I'm curious, would it suit your use case if you could pass in something like fig_kws to FacetGrid (and the figure-level plot functions)? That allows you to specify the num and clear args. I logged this a few days ago (https://github.com/mwaskom/seaborn/issues/2868), before seeing your issue just now.

Does that approach strike a good balance between allowing the user some control over the figure creation, without opening the door to likely problems?

davidgilbertson avatar Jun 24 '22 01:06 davidgilbertson

Yes, that would likely be sufficient.

Erotemic avatar Jun 24 '22 01:06 Erotemic

@Erotemic the below is a horribly hacky workaround, but it will allow you to open one figure window, and the re-execute your code and have it re-use that same figure window.

It hijacks the call from Seaborn into plt.figure() and adds in num and clear properties. You could alternatively just have it return a figure you've already created (after setting the figsize that Seaborn passes through).

if "figure_original" not in locals():
    figure_original = plt.figure

    def figure_new(**kwargs):
        import inspect

        if "seaborn" in inspect.stack()[1].filename and "num" not in kwargs:
            print("Seaborn calling plt.figure()")
            kwargs["num"] = "Seaborn Figure"
            kwargs["clear"] = True
            
        return figure_original(**kwargs)

    plt.figure = figure_new

davidgilbertson avatar Jul 01 '22 06:07 davidgilbertson