Producing a variable number of figures in one callback

Hello,

I have a piece of code that selects a feature from a dropdown menu and then produces figure with n_columns * n_rows subplots.

Where n_columns is always 3 and n_rows is the number of unique classes for each feature and it is therefore variable.

This is working fine.

However, as I am not able to produce separate callbacks from subplots, I am trying to change the code so that it produces one figure (with three columns/subplots) for unique class for the selected feature.

Here is my code:


### CALLBACKS ###

@app.callback(
	[Output('month-selection', 'figure')],
	#[Output('nf', 'integer')],
	[Output(str(f), 'figure') for f in range(nf+1)],
	[Output('table-daily', 'data'),
	Output('table-daily', 'columns')],
	[Output('selected-date', 'children')],
	[Input('month-dropdown', 'value')],
	[Input('feat-dropdown', 'value')],
	[Input('datepicker', 'value')]
)
def update_output(month_year, feature, cdate):
	
	global df
	#global nf
	
	###################
	### MONTHY DATA ###
	###################
	
	df2 = df.loc[df['trade_date'] == month_year]
	
	df_plot = df2.assign(lh_count=df['lh']).groupby(['lh']).agg({'quote_amount_eur_abs':'sum', 'lh_count':'count'}).reset_index()
	
	df_plot.columns = ['Status', 'Value Traded €', 'Nbr of Trades']
	
	df_plot['% Value Traded'] = round(100 * df_plot['Value Traded €'] / df_plot['Value Traded €'].sum(), 3)
	df_plot['% Trades'] = round(100 * df_plot['Nbr of Trades'] / df_plot['Nbr of Trades'].sum(), 3)
	df_plot['Value Traded €'] = df_plot['Value Traded €'].round(2)
	
	df_plot = df_plot[['Status', '% Trades', 'Nbr of Trades', '% Value Traded', 'Value Traded €']]
	
	df_plot['colour'] = ''
	
	df_plot.loc[df_plot['Status'] == 'A. Better', 'colour'] = 'darkorange'
	df_plot.loc[df_plot['Status'] == 'B. Equal', 'colour'] = 'forestgreen'
	df_plot.loc[df_plot['Status'] == 'C. Success', 'colour'] = 'darkslateblue'
	df_plot.loc[df_plot['Status'] == 'D. Worse', 'colour'] = 'crimson'
	
	df_plot['Nbr of Trades'] = df_plot['Nbr of Trades'].apply('{:,}'.format)
	df_plot['Value Traded €'] = df_plot['Value Traded €'].apply('{0:,.2f}'.format)
	
	outcome = ['A. Better', 'B. Equal', 'C. Success', 'D. Worse']
	
	subplot_titles = ['Performance Summary — ' + month_year + '<br> <br>', '% Number of Trades — ' + month_year + '<br> <br>', '% Value Traded — ' + month_year + '<br> <br>']
	
	specs = [[{'type': 'table'}, {'type': 'pie'}, {'type': 'pie'}]]
	
	fig = make_subplots(rows=1, cols=3, specs=specs,
	                    subplot_titles=subplot_titles)
	
	fig.add_trace(
	go.Table(
	    header=dict(values=['Status', '% Trades', 'Trades', '% Value', 'Value €'],
	                fill_color='darkorange',
	                align='left'),
	    cells=dict(values=[df_plot['Status'], df_plot['% Trades'], df_plot['Nbr of Trades'], df_plot['% Value Traded'], df_plot['Value Traded €']],
	               fill_color='white',
	               align='left')),
	    row=1, col=1
	)
	
	fig.add_trace(
	    go.Pie(labels=df_plot['Status'], values=df_plot['% Trades'], sort=False, pull=[0.1, 0.1, 0, 0], hole=.6, marker=dict(colors=df_plot['colour']), legendgroup = '1'),
	    row=1, col=2
	)
	
	fig.add_trace(
	    go.Pie(labels=df_plot['Status'], values=df_plot['% Value Traded'], sort=False, pull=[0.1, 0.1, 0, 0], hole=.6, marker=dict(colors=df_plot['colour']), legendgroup = '1'),
	    row=1, col=3
	)
	
	###

	feats = df2[feature].unique().tolist()
	nf = len(feats)
	
	# nrows = len(feats)
	
	nrows = 1
	row = 1
	
	specs = [[{'type': 'table'}, {'type': 'pie'}, {'type': 'pie'}]]
	
	#subplot_titles = []
	
	#for feat in feats:
	#	subplot_titles.extend(['Performance Summary ' + feature + ' ' + feat + ' <br> <br>', '% Number of Trades ' + feature + ' ' + feat + ' <br> <br>', '% Value Traded ' + feature + ' ' + feat + ' <br> <br>'])

	#row = 1
	fe = 0
	figures = [{} for _ in range(nf+1)]
	for feat in feats:
		
		subplot_titles = ['Performance Summary ' + feature + ' ' + feat + ' <br> <br>', '% Number of Trades ' + feature + ' ' + feat + ' <br> <br>', '% Value Traded ' + feature + ' ' + feat + ' <br> <br>']
		
		#figures[fe] = make_subplots(rows=nrows, cols=3, specs=specs, subplot_titles=subplot_titles)
		figures[fe] = make_subplots(rows=nrows, cols=3, specs=specs, subplot_titles=subplot_titles)
	
		df_temp = df2[df2[feature] == feat]
	
		df_plotd = df_temp.assign(lh_count=df_temp['lh']).groupby(['lh']).agg({'quote_amount_eur_abs':'sum', 'lh_count':'count'}).reset_index()
		
		df_plotd.columns = ['Status', 'Value Traded €', 'Nbr of Trades']
		
		df_plotd['% Value Traded'] = round(100 * df_plotd['Value Traded €'] / df_plotd['Value Traded €'].sum(), 3)
		df_plotd['% Trades'] = round(100 * df_plotd['Nbr of Trades'] / df_plotd['Nbr of Trades'].sum(), 3)
		
		df_plotd = df_plotd[['Status', '% Trades', 'Nbr of Trades', '% Value Traded', 'Value Traded €']]
		
		missing = list(set(outcome) - set(df_plotd['Status'].unique().tolist()))

		for i in missing:
			df_plotd.loc[df_plotd.shape[0]] = [i, 0, 0, 0, 0]
		
		df_plotd = df_plotd.sort_values(by=['Status'], ascending=True)
		
		df_plotd['colour'] = ''
	
		df_plotd.loc[df_plotd['Status'] == 'A. Better', 'colour'] = 'darkorange'
		df_plotd.loc[df_plotd['Status'] == 'B. Equal', 'colour'] = 'forestgreen'
		df_plotd.loc[df_plotd['Status'] == 'C. Success', 'colour'] = 'darkslateblue'
		df_plotd.loc[df_plotd['Status'] == 'D. Worse', 'colour'] = 'crimson'
		
		df_plotd['Nbr of Trades'] = df_plotd['Nbr of Trades'].apply('{:,}'.format)
		df_plotd['Value Traded €'] = df_plotd['Value Traded €'].apply('{0:,.2f}'.format)
	
		figures[fe].add_trace(
		go.Table(
		    header=dict(values=['Status', '% Trades', 'Trades', '% Value', 'Value €'],
		                fill_color='darkorange',
		                align='left'),
		    cells=dict(values=[df_plotd['Status'], df_plotd['% Trades'], df_plotd['Nbr of Trades'], df_plotd['% Value Traded'], df_plotd['Value Traded €']],
		               fill_color='lavender',
		               align='left')),
		    row=row, col=1
		)
		
		figures[fe].add_trace(
		    go.Pie(labels=df_plotd['Status'], values=df_plotd['% Trades'], sort=False, pull=[0.1, 0.1, 0, 0], hole=.6, marker=dict(colors=df_plotd['colour']), legendgroup = str(row)),
		    row=row, col=2
		)
		
		figures[fe].add_trace(
		    go.Pie(labels=df_plotd['Status'], values=df_plotd['% Value Traded'], sort=False, pull=[0.1, 0.1, 0, 0], hole=.6, marker=dict(colors=df_plotd['colour']), legendgroup = str(row)),
		    row=row, col=3
		)
		
		figures[fe].update_layout(title_x=0, title_y=1, height=300)
		
		fe = fe + 1
		#row = row + 1
		
	#fig_det.update_layout(title_x=0, title_y=1, height=nrows*300, legend_tracegroupgap = 224)
	
	###
	
	df3 = df[df['trade_date_original'] == cdate].copy()
	df3 = df3[['operation_id', 'input_time', 'broker_timestamp',  'booking_time', 'created_time', 'operation', 'broker', 'symbol', 'price', 
			'lowest_platform', 'lowest', 'highest_platform', 'highest', 'lh']]
	columns = [{'name': col, 'id': col} for col in df3.columns]
	data = df3.to_dict(orient='records')
	
	#return fig, fig_det, data, columns, cdate.split('T')[0]
	return fig, *figures, data, columns, cdate.split('T')[0]


calendar = [dmc.MantineProvider(
	theme={
		'fontFamily': 'Arial, Helvetica, sans-serif',
		'primaryColor': 'orange',
		},
		inherit=True,
		withGlobalStyles=True,
		withNormalizeCSS=True,
		children=[dmc.DatePicker(
			id='datepicker',
			label='Date Selection',
			minDate=df['trade_date_original'].min(),
			maxDate=df['trade_date_original'].max(),
			value=df['trade_date_original'].max(),
			clearable=False,
			style={'width': 160})])]

##############
### LAYOUT ###
##############

button_style = {'background-color': 'darkorange', 'color': 'grey', 'fontWeight': 'bold', 'fontFamily': 'Arial, Helvetica, sans-serif', 'fontSize': 12, 'border': '0px', 'border-radius': '5%'}

app.layout = html.Div([
	html.Div([html.Label('CRYPTO EXECUTION PERFORMANCE', style={'fontWeight': 'bold', 'fontFamily': 'Arial, Helvetica, sans-serif', 'color': 'darkorange', 'fontSize': 18})]),
	html.Br(),
	html.Div([html.Label('MONTHLY EVOLUTION', style={'fontWeight': 'bold', 'fontFamily': 'Arial, Helvetica, sans-serif', 'color': 'darkorange', 'fontSize': 16})]),
	html.Div([
	dcc.Graph(id='time-ntrans',
		figure=fig1, responsive=True)], style={'display': 'inline-block'}),
	html.Div([
	dcc.Graph(id='time-volume',
		figure=fig2, responsive=True)], style={'display': 'inline-block'}),
	html.Div([html.Label('MONTHLY PERFORMANCE', style={'fontWeight': 'bold', 'fontFamily': 'Arial, Helvetica, sans-serif', 'color': 'darkorange', 'fontSize': 16})]),
	html.Div([html.Label('Month Selection', style={'fontFamily': 'Arial, Helvetica, sans-serif', 'color': '#000039', 'fontSize': 14}), 
	html.Br(),
	dcc.Dropdown(month_list, month_list[-1], id='month-dropdown', searchable=False, clearable=False, style=button_style)],
	style={'width':'120px', 'margin-left': '20px', 'margin-right': '60px', 'margin-top': '10px', 'verticalAlign': 'top'}),
	html.Div([dcc.Graph(id='month-selection', className='user-select')]),
	html.Div([html.Label('DETAILED PERFORMANCE', style={'fontWeight': 'bold', 'fontFamily': 'Arial, Helvetica, sans-serif', 'color': 'darkorange', 'fontSize': 16})]),
	html.Div([html.Label('Feature Selection', style={'fontFamily': 'Arial, Helvetica, sans-serif', 'color': '#000039', 'fontSize': 14}), 
	html.Br(),
	dcc.Dropdown(columns, columns[3], id='feat-dropdown', searchable=False, clearable=False, style=button_style)],
	style={'width':'120px', 'margin-left': '20px', 'margin-top': '10px', 'verticalAlign': 'top'}),
	#html.Div([dcc.Graph(id='feat-selection', className='user-select')]),
	#html.Div([html.Label(id='nf')]),
	html.Div(*[dcc.Graph(id=str(f), figure={}) for f in range(nf + 1)]),
	html.Div([html.Label('DAILY EVOLUTION', style={'fontWeight': 'bold', 'fontFamily': 'Arial, Helvetica, sans-serif', 'color': 'darkorange', 'fontSize': 16})]),
	html.Div([
	dcc.Graph(id='time-ntransd',
		figure=fig3, responsive=True)], style={'display': 'inline-block'}),
	html.Div([
	dcc.Graph(id='time-volumed',
		figure=fig4, responsive=True)], style={'display': 'inline-block'}),
	html.Br(), html.Br(),
	html.Div([html.Label('DAILY TABLE', style={'fontWeight': 'bold', 'fontFamily': 'Arial, Helvetica, sans-serif', 'color': 'darkorange', 'fontSize': 16}),
	dmc.Text(id='selected-date', style={'fontFamily': 'Arial, Helvetica, sans-serif', 'color': 'white', 'fontSize': 14})]),
	html.Div(calendar, 
			style={'margin-top': '-10px', 'margin-left': '20px', 'verticalAlign': 'top'}),
	html.Br(),
	html.Div([
	dash_table.DataTable(id='table-daily',
				style_header={'backgroundColor': 'darkorange',
					'color': 'white',
					'fontWeight': 'bold', 'fontFamily': 'Arial, Helvetica, sans-serif', 'fontSize': 12
				},
				style_data={'backgroundColor': 'white',
					'color': 'slategray',
					'fontFamily': 'Arial, Helvetica, sans-serif', 'fontSize': 12
				},
				style_filter = {
					'color': 'LightGray',
					'fontFamily': 'Arial, Helvetica, sans-serif', 'fontSize': 12
				},
				fill_width=False,
				editable=True,
				filter_action='native',
				filter_options={'case': 'insensitive'},
				sort_action='native',
				sort_mode='multi',
				#column_selectable='single',
				row_selectable='single',
				#selected_columns=[],
				selected_rows=[],
				selected_row_ids=[],
				#fixed_rows={'headers': True},
				page_action='none',
				style_table={'height': '700px', 'width': '1600px', 'overflowY': 'auto'},
				export_format='csv',
				export_headers='display',
				#page_current=0,
				#page_size=10
	)]),
])

The key issue is how to export a variable number of *figures at once. Specially given than nf is a local variable, which is not recognised either in the callback or by in the layout.

HI @edmoman.

What do you mean by that?

Hi @AIMPED,

I mean, from the n_columns * n_rows figure, can I create callbacks from each specific subplot to produce new tables and figures with specific information from that subplot?

This was my first intention, but I could not find a way to do it. Reading through this forum, I came to the conclusion that it would be best to produce different divs instead of subplots.

I agree with that notion and that is what I am trying to accomplish right now.

The issue is that the number of divs (rows) required is variable, as it depends on how many unique classes there are in the selected feature.

I found some guidance on how to produce an array of figures (*figure), however, I do not seem to be able to make this work.

Help would be much appreciated.

Best,

Ed

Hi @edmoman ,

Yes, I would do this. Concerning the variable number of Divs to create: That is a perfect use case for pattern matching callbacks.

An example:

OK, it is working! Thanks a million!

The trick was to return the divs and not the figures.

In a nutshell:

THE CALLBACK

[Output('multifigure-container', 'children')]

THE FUNCTION

figures = []
    for feat in feats:
        CREATE YOUR FIGURES
        figures.append(figure)

complex_div = html.Div([dcc.Graph(id='multi_' + str(i), figure=figures[i]) for i in range(nf)])

return complex_div

THE LAYOUT

html.Div(id='multifigure-container'),

The next step for me is to modify this code to create dbc with rows and columns, which should be relatively trivial now that I understand the principle.

2 Likes

Glad you figured it out. I highly recommend using pattern matching callbacks for this kind of application, though.

1 Like