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:
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):
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.
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.
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:
In the above case, there were 56 rows with spacing of 0.00357
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