Skip to content

layout.grid enhancements for plotly.py to replace make_subplots #3507

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

Closed
jonmmease opened this issue Feb 2, 2019 · 5 comments
Closed

layout.grid enhancements for plotly.py to replace make_subplots #3507

jonmmease opened this issue Feb 2, 2019 · 5 comments
Labels
feature something new

Comments

@jonmmease
Copy link
Contributor

Background

plotly.py has a system for building figures with subplots that predates the plotly.js layout.grid system for subplots. I would like to replace this custom subplot system with layout.grid to reduce complexity inside plotly.py and add support for all trace types (plotly.py's system only supports Cartesian subplot types). This transition will also make the integration of px into plotly.py much smoother.

Needs

As far as I can tell, there are two features that are implemented in plotly.py's make_subplots function that are not supported by layout.grid.

The first two are both demonstrated in this example https://plot.ly/python/subplots/#custom-sized-subplot-with-subplot-titles.

  1. Spanned subplots: make_subplots allows individual axes to span multiple subplot grid entries
  2. Subplot titles: make_subplots has a system for positioning text annotations as titles for individual subplots
  3. column/row widths: make_subplots supports column_width and row_width arrays to support non-uniform widths of the rows and columns in the grid.
  4. insets: make_subplots supports the specification of insets. TBH I'm not really sure how this works and I don't know if there are any examples of this outside of the docstring. And this might not be something that makes sense to support in layout.grid. Here's the docstring
    insets (kwarg, list of dictionaries):
        Inset specifications.

        - Each item in 'insets' is a dictionary.
            The available keys are:

            * cell (tuple, default=(1,1)): (row, col) index of the
                subplot cell to overlay inset axes onto.
            * is_3d (boolean, default=False): flag for 3d scenes
            * l (float, default=0.0): padding left of inset
                  in fraction of cell width
            * w (float or 'to_end', default='to_end') inset width
                  in fraction of cell width ('to_end': to cell right edge)
            * b (float, default=0.0): padding bottom of inset
                  in fraction of cell height
            * h (float or 'to_end', default='to_end') inset height
                  in fraction of cell height ('to_end': to cell top edge)

API ideas

To kick things off, here's a idea for a potential plotly.js API

  1. We could add layout.grid.rowspan and layout.grid.colspan properties that may optionally be set to 2D arrays of integers. So the example above would have a a colspan property of
[[1, 1],
 [2, 0]]

which would imply a grid.subplots property of

[['xy'  , 'x2y2'],
 ['x3y3', null]]
  1. We could add a grid.titles property that is a 2D array of title strings. So the example above would have a grid.titles property of
[['First Subplot', 'Second Subplot'],
 ['Third Subplot' , null]]
  1. We could add grid.widths and grid.heights properties that would be 1D arrays of numbers. We could internally normalize each array and use the elements to determine the width/height of each column/row.

cc @nicolaskruchten

@nicolaskruchten
Copy link
Contributor

Yup, I agree this would be great! Subplot titles would be really handy, especially if they could be used for facet labelling somehow i.e. placed on the side as well as the top and if a subplot could have two titles, for the corner one ;)

Barring that, they would still be very handy for wrapped-facet labelling

@alexcjohnson
Copy link
Collaborator

Thanks for kicking this discussion off @jonmmease! Definitely a great set of features to have, and I think you've covered all the important functionality.

Spans

Your proposal works slightly differently from html tables, which seem to me the closest common analog. The difference is tables don't include entries (<td> elements) for the already-occupied cells at all, whereas you show them with null or 0. I think I prefer your version despite the divergence, but I think it's worth mentioning and exploring the implications. Let's take a somewhat more complex case:

+---+-+
|   | |
|   +-+
|   | |
+-+-+-+
| |   |
+-+---+

In your version this would look like:

colspan: [[2, 0, 1],
          [0, 0, 1], // <- last one: row 1, col 2
          [1, 2, 0]]
rowspan: [[2, 0, 1],
          [0, 0, 1],
          [1, 1, 0]]
subplots: [['xy'  , null  , 'x2y2'],
           [null  , null  , 'x3y3'],
           ['x4y4', 'x5y5', null  ]]

whereas using the table analogy it would be:

colspan: [[2,    1],
          [      1], // <- last (only) one: row 1, col 0
          [1, 2   ]]
rowspan: [[2,    1],
          [      1],
          [1, 1   ]]
subplots: [['xy'  ,        'x2y2'],
           [                'x3y3'],
           ['x4y4', 'x5y5'        ]]

If we're only talking about cartesian subplots, this difference doesn't matter much; these three arrays are all self-consistent either way. The table version is nice because we're not forced to add placeholder entries, we're just enumerating cells in the order we encounter them. On the other hand your version keeps the arrays rectangular, which seems easier to construct - you can make an empty array of the right dimensions, then place cells into it by putting the cell spec in the upper left corner and fill the rest of the cell with 0/null.

But seems to me yours has a clear advantage in referencing spanned grid cells in subplot.domain, to insert non-cartesian subplots in the grid. With the table version there's ambiguity: do you refer to the column and row number before the cells were merged, or to the indices in the grid spec arrays? With your version the two are identical, and moreover we can give subplot.domain its own colspan and rowspan, which by default inherits from the specified cell in grid, but if you prefer you can omit grid.colspan etc and just specify (col|row)span in subplot.domain.

Titles

If grid.titles is a 2D array as you describe, it should at least be an array of objects like layout.title. But it could also be a 1D array, of objects like layout.title plus attributes (col|row)[span] to target a subplot, and side: ('left'|'right'|'bottom'|'top') that sets the default position and rotation of the text. This would naturally allow multiple titles on one subplot, and avoid a sparse array in cases like just labeling the edges of a large grid.

There may still be edge cases this doesn't cover. The most flexible solution of all would be annotations with (x|y)ref: 'grid' and (col|row)[span] attrs. Shapes and images could also easily support such positioning.

Column/row widths

grid.widths and grid.heights sound great, normalization included! (x|y)gap becomes a little bit confusing, but we should be able to sort that out.

Insets

I agree, no need to build this into the grid, just make another pair of axes (or non-cartesian subplot) and specify domain manually. That covers more edge cases, like partial overlap, the only downside being the need to calculate the edges of the cell you want to reference - but it's pretty unusual to use an inset AND a grid anyway, and by the time you get to that level of refinement (which is likely about the last step before publication) you're probably not going to mind having to tweak this.

@etpinard etpinard added the feature something new label Feb 4, 2019
@etpinard
Copy link
Contributor

etpinard commented Feb 14, 2019

rowspan / colspan

But seems to me yours has a clear advantage in referencing spanned grid cells in subplot.domain,
do you refer to the column and row number before the cells were merged, or to the indices in the grid spec arrays? With your version the two are identical,

That's the winning argument for me. I'm voting for

colspan: [[2, 0, 1],
          [0, 0, 1], // <- last one: row 1, col 2
          [1, 2, 0]]
rowspan: [[2, 0, 1],
          [0, 0, 1],
          [1, 1, 0]]
subplots: [['xy'  , null  , 'x2y2'],
           [null  , null  , 'x3y3'],
           ['x4y4', 'x5y5', null  ]]

Subplot tItles

First referencing #2746

The most flexible solution of all would be annotations with (x|y)ref: 'grid' and (col|row)[span] attrs. Shapes and images could also easily support such positioning.

I think I'd vote for keeping subplot title out of layout.grid. (x|y)ref: 'grid' sounds like a general enough solution. It might make it slightly tricky for plotly.py to retain parity with the current make_subplots: it would need to "add" annotations behind the scenes. @jonmmease please let us know if that's a big deal.

Column/row widths

👍

Insets

I looked through plotly/plotly.py#170 to try to find why insets were added to make_subplots ... to no avail. Yeah 🔪 sounds ok!

@jonmmease
Copy link
Contributor Author

I think I'd vote for keeping subplot title out of layout.grid. (x|y)ref: 'grid' sounds like a general enough solution. It might make it slightly tricky for plotly.py to retain parity with the current make_subplots: it would need to "add" annotations behind the scenes. @jonmmease please let us know if that's a big deal.

I like this solution a lot, and it should be easy enough to support from plotly.py. Thanks @etpinard and @alexcjohnson for improving on my suggestion!

@gvwilson
Copy link
Contributor

Hi - this issue has been sitting for a while, so as part of our effort to tidy up our public repositories I'm going to close it. If it's still a concern, we'd be grateful if you could open a new issue (with a short reproducible example if appropriate) so that we can add it to our stack. Cheers - @gvwilson

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

No branches or pull requests

5 participants