pythonplotlyplotly-dashscatter-plotdashboard

Facet_row_spacing increasing with increasing facet rows


I'm sure there is a simple solution to this but after digging around for a few hours, I can't seem to find the answer. In short - the more rows I add to a faceted series of scatterplots, the greater the gap between the rows (despite trying to hardcode in the desired row gap height)

I have a button on a Dash dashboard that generates a plotly scatter plot based on data housed in a table:

enter image description here

The function to generate the plot:

def generate_all_curves(unique_key_list):
    hit_curve_data_df = df_curves[df_curves['Unique_key'].isin(unique_key_list)]
    num_rows = math.ceil(len(unique_key_list)/2)
    print("Num rows: "+str(num_rows))
    facet_row_max_spacing = 1/num_rows
    if facet_row_max_spacing > 0.04:
        facet_row_max_spacing = 0.04
    print("Spacing: "+str(facet_row_max_spacing))
    all_curves_figure = px.scatter(hit_curve_data_df, x='Temps', y='Smooth Fluorescence', color='Subplot', color_discrete_sequence=['#FABF73','#F06D4E'],
                        hover_name='Well', hover_data={'Final_decision':False, 'Assay_Plate':False, 'Temps':False, 'Smooth Fluorescence':False, 'Error':True, 'Ctrl_Tm_z-score':True}, #Hover data (Tooltip in Spotfire)
                        facet_col='Unique_key', facet_col_wrap=2, facet_col_spacing=0.08,facet_row_spacing = facet_row_max_spacing,#Facet plots by plate and only allow 2 columns. Column spacing had to be adjusted to allow for individual y-axes
                        render_mode = 'auto', height = 200*num_rows) #Height of plots is equal to half the number of plates (coz 2 columns) with each plot 300px high. Width will have to be adjusted
    return all_curves_figure

And then the callback to generate the graphs based on the rows present in a data table:

@app.callback(
    Output('all_graphs_div', 'children'),
    [Input('generate', 'n_clicks'),
     Input('clear', 'n_clicks')],
    [State('results_table_datatable', 'data')])
def update_or_clear_graphs(generate_clicks, clear_clicks, current_table_data):
    ctx = callback_context
    if not ctx.triggered:
        raise PreventUpdate
    trigger_id = ctx.triggered[0]['prop_id'].split('.')[0]
    if trigger_id == 'generate':
        if generate_clicks > 0:
            key_list = [d['Unique_key'] for d in current_table_data]
            new_figure = generate_all_curves(key_list)
            new_figure.update_yaxes(matches=None)
            new_figure.for_each_yaxis(lambda yaxis: yaxis.update(showticklabels=True))
            new_figure.update_layout(height=200 * len(key_list)/2) #Each plot is 400px high (i.e. 200 is half of 400)
            graph_object = dcc.Graph(id='all_graphs_object', figure=new_figure, style={'font-family': 'Arial'})
            return graph_object
    elif trigger_id == 'clear':
        if clear_clicks > 0:
            return None
    raise PreventUpdate

And finally, the layout of these objects (just the relevant section)

            #Child A: Data table 
            html.Div([
                    #Data table
                    html.Div([
                    dash_table.DataTable(
                        default_datatable.to_dict('records'), 
                        [{'name': i, 'id': i} for i in default_datatable.loc[:, ['Source_Plate','Well','Subplot','Compound','Fraction','Final_Tm','No. Std Dev','Relative_amplitude','Unique_key']]],
                        id = 'results_table_datatable',
                        hidden_columns=['Unique_key'],
                        css=[{'selector': '.show-hide', 'rule': 'display: none'},{'selector':'.export','rule': 'margin:5px'}],
                        row_deletable=True,
                        sort_action='native',
                        export_format='xlsx',
                        style_data_conditional=data_table_style_data_conditional, 
                        style_table = {'height':'400px','overflow-y':'scroll'},
                        style_as_list_view=True, style_cell={'fontSize':12, 'font-family':'Arial'}, style_header = {'backgroundColor': '#cce6ff','fontWeight': 'bold'}),#Styling of table
                    ], id = 'results_table'),
                    #Generate all hit graphs button
                    html.Div([html.Button('Generate all graphs', id='generate', n_clicks=None, style = {'margin-top':'20px', 'margin-right': '20px'})], style = {'display':'inline-block','vertical-align':'top'}),
                    #Clear all graphs button
                    html.Div([html.Button('Clear all graphs', id='clear', n_clicks=0, style = {'margin-top':'20px'})],style = {'display':'inline-block','vertical-align':'top'}),
                    #Div with the plots
                    html.Div([],id = 'all_graphs_div', style = {'height':'500px','overflow-y':'scroll'})
                    ], 
                id='left_panel',
                style = {'display':'inline-block','vertical-align':'top', 'width': '50%','overflow-y':'scroll','overflow-x':'scroll', 'height':'90%', 'padding':'10px','margin-top':'5px'}), #Styling of table container

As you'll see, I've tried to dynamically change the facet_row_spacing. I try and keep it at 0.04 when I only have a handful of plots, as that looks most aesthetically pleasing. Once it's below that mark, I let it drop to whatever it needs to be to work.

The trouble comes in when I start increasing the number of plots. Despite my attempts to define the facet row spacing, this space seems to increase with an increasing number of facet plots.

E.g. Here are the plots when there are 9 plots (i.e. split into two columns = 5 rows, spacing apparently set to 0.04): enter image description here

However, when I bump this up to 18 plots (9 rows, the spacing is still apparently set to 0.04 as per my print statements), but you'll notice that the space between each row of plots has increased.

enter image description here

Things got weird when I tried cranking this up to 56 rows (112 plots), which were (according to the print statements) supposed to have a facet_row_spacing of 0.0178571429. However, that inter-row spacing is just getting bigger and bigger as the number of plots or rows of plots increases.

enter image description here

What on earth is going on and what am I doing wrong to have this weird inverse behaviour? Is there something equivalent to new_figure.update_layout(facet_row_spacing = x) that I can put in the call back?

EDIT: Following suggestion of:

height = 200*num_rows
facet_row_max_spacing = 40/height

The plots have definitely become less squashed (yay!) but now the curves are no longer sitting inside their designated plot areas:

enter image description here

In the above case, there were 56 rows with spacing of 0.00357


Solution

  • This happens because the facet_row_spacing value is specified in fractions of the figure height, which is set to 200*num_rows, so you would need to set this value as x/(200*num_rows) in order to obtain a consistent spacing regardless of the height, no just x/num_rows.

    If 0.04 appears to be fine with 5 rows, then the following should work fine :

    height = 200*num_rows
    facet_row_max_spacing = 40/height