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

Too much space between subplots using facet_row in express #1930

Closed
robroc opened this issue Nov 27, 2019 · 19 comments
Closed

Too much space between subplots using facet_row in express #1930

robroc opened this issue Nov 27, 2019 · 19 comments

Comments

@robroc
Copy link

robroc commented Nov 27, 2019

plotly==4.1.1
plotly-express==0.4.0
jupyter-lab==1.1.4

When using px.scatter with color and facet_row, the figure adds a ton of space between subplots. Need to give it excessive height to make plots square.

Also, .update_layout( showlegend=False) is not removing the legend. The figure below has 20 subplots.

px.scatter(comps_year, x = 'count', y = 'sum', color = 'YEAR', facet_row= 'COMPANY_CLEAN', width = 500, height = 9000).update_layout(showlegend=False)

Capture

When I remove the width and height parameters, it renders like this:

Capture2

@nicolaskruchten
Copy link
Contributor

Hey Roberto!

Sounds like you want 20ish roughly-square facets? With Plotly 4.3.0 you can pass facet_col_wrap=5 to get 5 rows of 4!

Also, unintuitive as it might be, the thing on the right is not a "legend" in Plotly-land, rather it's a "color bar" and it can be hidden with .update_layout(colorbar_showscale=False).

Finally: you can keep plotly_express around if you like but it's not really doing anything any more... it just re-exports plotly.express, so you may as well just import that from now on.

Hope this helps!

@robroc
Copy link
Author

robroc commented Nov 27, 2019

Well, it seems like I missed this pretty major update. facet_col_wrap is exactly what I needed. However, colorbar_showscale is not being accepted as a valid property for update_layout.

Thanks, Nick!

@nicolaskruchten
Copy link
Contributor

Sorry, typo'ed: .update_layout(coloraxis_showscale=False)

@nicolaskruchten
Copy link
Contributor

Oh and I never answered your original question: the reason you get all this padding is because the padding is specified as a percentage of the overall figure size, rather than being fixed. So stretching the figure will stretch the plots and the padding in equal measure ;)

@robroc
Copy link
Author

robroc commented Dec 12, 2019

@nicolaskruchten Is there a way to specify the padding? I'd like to keep the subplots taller but close together.

@nicolaskruchten
Copy link
Contributor

Unfortunately there is not a way to control the padding, no :(

@nicolaskruchten
Copy link
Contributor

You could iterate through the yaxis and xaxis keys in fig.layout and manually set the domain to resize the plots. Kinda manual/kinda gross but will work.

@harisbal
Copy link

Joining the party late but this can probably helpful.

The modification of the axes' domain can be simplified using the make_subpots function.
make_subpots allows for vertical and horizontal spacing therefore it can provide us with the required domains.

We first create a subplot to use as a template.

from plotly.subplots import make_subplots
sp = make_subplots(rows=nrows, cols=ncols, horizontal_spacing=0.1, vertical_spacing=0.1)

nrows and ncols must be calculated based on the plot we want to imitate.

Then we produce our plot
fig = px.line(df, x, y, facet_col='FR', facet_col_wrap=2)

Finally we iterate and fix the domains based on the sp . The issue is that facet plots and subplots arrange the plots differently. The following function resolves this issue.

def modify_facet_spacing(fig, nrows, ncols, hs, vs):
    
    import re
    pattern = r'[xy]axis(\d+)?'
    
    sp = make_subplots(rows=nrows, cols=ncols, horizontal_spacing=hs, vertical_spacing=vs)
    
    # the subplots arange plots differently than faceted plots 
    # subplots: top left, facets: bottom left
    #pdb.set_trace()
    spt_rng = np.arange(1, (nrows*ncols)+1)
    fct_rng = np.flip(spt_rng.reshape(nrows, ncols), axis=0).flatten()
    
    repl = dict(zip(fct_rng, spt_rng))
    
    for el in fig.layout:
        if re.match(pattern, el):
                       
            axis_fct = el
            axis_fct_n = int(re.match(pattern, axis_fct).groups(0)[0])
            
            if 'yaxis' in axis_fct: 
                if axis_fct_n == 0:
                    #pdb.set_trace()
                    axis_fct_n = 1
                    axis_fct += str(axis_fct_n)
                    
                axis_spt = axis_fct.replace(str(axis_fct_n), str(repl[axis_fct_n]))
            else:
                axis_spt = axis_fct
        
            if axis_fct.endswith('axis1'):
                axis_fct = axis_fct[:-1]
            
            if axis_spt.endswith('axis1'):
                axis_spt = axis_spt[:-1]
                
            fig.layout[axis_fct].update(domain=sp.layout[axis_spt].domain)

The function has not been thoroughly tested, therefore use with caution. Pay attention to provide the helper function with the appropriate number of expected rows and cols.

Hope that helps!

Before modification:
image

After modification:
image

@cnicol-gwlogic
Copy link

cnicol-gwlogic commented Jun 17, 2020

@harisbal - thanks for that - was very useful for me. One addition I made was to adjust the facet column title y coordinates as follows, because they were staying at the original (padded downward) locations, progressively shifting toward the bottom of each chart, which gets really bad for large facet arrays:

def modify_facet_spacing(fig, ncols, hs, vs):

    import re, math, numpy as np

    pattern = r'[xy]axis(\d+)?'

    fct_titles = fig.layout['annotations']
    nrows = int(math.ceil(len(fct_titles) / ncols))  #this needs revision for cases where plotly express has removed redundant rows (if for eg the bottom row or two are empty of charts)

    sp = make_subplots(rows=nrows, cols=ncols, horizontal_spacing=hs, vertical_spacing=vs)

    # the subplots arange plots differently than faceted plots 
    # subplots: top left, facets: bottom left
    #pdb.set_trace()
    spt_rng = np.arange(1, (nrows*ncols)+1)
    fct_rng = np.flip(spt_rng.reshape(nrows, ncols), axis=0).flatten()
    repl = dict(zip(fct_rng, spt_rng))

   empty_cells = (nrows * ncols) - len(fct_titles)

    for el in fig.layout:

        if re.match(pattern, el):
            axis_fct = el
            axis_fct_n = int(re.match(pattern, axis_fct).groups(0)[0])

            if 'yaxis' in axis_fct:
                if axis_fct_n == 0:
                    #pdb.set_trace()
                    axis_fct_n = 1
                    axis_fct += str(axis_fct_n)
                axis_spt = axis_fct.replace(str(axis_fct_n), str(repl[axis_fct_n]))
            else:
                axis_spt = axis_fct

            spt_index = int(re.match(pattern, axis_spt).groups(0)[0]) - 1
            fct_index = axis_fct_n - 1

            if axis_fct.endswith('axis1'):
                axis_fct = axis_fct[:-1]
            if axis_spt.endswith('axis1'):
                axis_spt = axis_spt[:-1]
            fig.layout[axis_fct].update(domain=sp.layout[axis_spt].domain)
            
            if 'yaxis' in axis_fct:
                # facets are nrows*ncols in size behind the scenes, but 
                # fct_titles is only ncharts long (eg if 1 plot on last row) -
                # the rest of the plots on that row are empty
                if fct_index - empty_cells >= 0:
                    fct_titles[fct_index - empty_cells]['y'] = sp.layout[axis_spt].domain[1]

    fig.layout['annotations'] = fct_titles

@harisbal
Copy link

Glad I could help. Very nice improvement.
Just out of curiosity the 5 in this line spt_index = int(axis_spt[-(len(axis_spt) - 5):]) - 1 is specific to your case right?

@harisbal
Copy link

I had also tried to address the issue, but your version is probably more robust

def modify_facet_spacing(fig, nrows, ncols, hs, vs, xannot=1, yannot=1.05, ammend_annots=True):
    
    import re
    
    sp = make_subplots(rows=nrows, cols=ncols, horizontal_spacing=hs, vertical_spacing=vs)
    
    # the subplots arange plots differently than faceted plots 
    # subplots: top left, facets: bottom left
    #pdb.set_trace()
    spt_rng = np.arange(1, (nrows*ncols)+1)
    fct_rng = np.flip(spt_rng.reshape(nrows, ncols), axis=0).flatten()
    
    repl = dict(zip(fct_rng, spt_rng))
    pattern = r'[xy]axis(\d+)?'
    
    for el in fig.layout:
        if re.match(pattern, el):
                       
            axis_fct = el
            axis_fct_n = int(re.match(pattern, axis_fct).groups(0)[0])
            
            if 'yaxis' in axis_fct: 
                if axis_fct_n == 0:
                    axis_fct_n = 1
                    axis_fct += str(axis_fct_n)
                    
                axis_spt = axis_fct.replace(str(axis_fct_n), str(repl[axis_fct_n]))
            else:
                axis_spt = axis_fct
            
            fig.layout[axis_fct].update(domain=sp.layout[axis_spt].domain)
            
            if ammend_annots:
                axis_fct_n = int(re.match(pattern, axis_fct).groups(0)[0])
                if axis_fct_n == 0:
                    axis_fct_n = 1
                # Assumption that annotations follow the same order with axes
                if axis_fct[0] == 'x':
                    dom = fig.layout[axis_fct]['domain']
                    mid = dom[0] + ((dom[1] - dom[0])/2) * xannot
                    fig.layout.annotations[axis_fct_n-1].update(xref='paper', x=mid, xanchor='center')
                elif axis_fct[0] == 'y':
                    top = fig.layout[axis_fct]['domain'][1] * yannot
                    fig.layout.annotations[axis_fct_n-1].update(yref='paper', y=top, yanchor='middle')
                else:
                    print('Unrecognised axis type')

@cnicol-gwlogic
Copy link

cnicol-gwlogic commented Jun 17, 2020

Glad I could help. Very nice improvement.
Just out of curiosity the 5 in this line spt_index = int(axis_spt[-(len(axis_spt) - 5):]) - 1 is specific to your case right?

Yep you're right, that should be smarter than as I have it. Secondary axes etc might break it. I'll update that here at some point. Cheers
...Edit - updated in my code / comment above. Good pickup, thanks.

@nicolaskruchten
Copy link
Contributor

We should probably add facet_row_spacing as a kwarg :) and same for col

@kaburelabs
Copy link

Hi Guys, It helped me a lot...

I have a good question, how can I change the order of the charts of the facet_row?

Also, I'm trying to set a custom color to the bars, but it's applying only to the first frame of the animation. How to change the color to all frames?

@nicolaskruchten
Copy link
Contributor

@kaburelabs this is a bit off-topic, but you can use category_orders to control order of anything categorical, and color_discrete_map to map specific colors to categories: https://plotly.com/python/styling-plotly-express/

@kaburelabs
Copy link

Thank you, Nicolas!
I have implemented it looping through the frames and setting the colors for each chart

@cnicol-gwlogic
Copy link

cnicol-gwlogic commented Jun 19, 2020

Oh and I never answered your original question: the reason you get all this padding is because the padding is specified as a percentage of the overall figure size, rather than being fixed. So stretching the figure will stretch the plots and the padding in equal measure ;)

@nicolaskruchten - I wonder if this concept is a reason for #2026 (this affects me too at times) and #2556. I haven't looked into the underlying code, but isn't that guaranteed to mean negative plot area y dimensions (within each facet) beyond a certain number of facet rows? Would an "easy fix" (excuse me) for these issues be to change this padding to be a % of each facet's domain rather than % of the overall figure size?

Chris.

@nicolaskruchten
Copy link
Contributor

@cnicol-gwlogic yes, that would be reasonably easy but might be a breaking change, and in any case would lead to ugly plots in many cases unless folks "tune" height and width, so the easiest thing to do might be to allow folks to override the built-in default themselves.

@nicolaskruchten
Copy link
Contributor

As of plotly 4.9, we now have new facet_row_spacing and facet_col_spacing arguments: https://plotly.com/python/facet-plots/#controlling-facet-spacing

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

No branches or pull requests

5 participants