Build a Responsive Dash + Plotly Financial Dashboard with Callbacks for Local and Cloud Use

Overview

This tutorial walks through building an interactive financial dashboard using Dash, Plotly and Bootstrap. It demonstrates how to generate sample data, design a responsive layout with controls and visualizations, and wire Dash callbacks so UI controls update charts, metrics and tables in real time. The same approach works both locally and in cloud environments such as Google Colab.

Dependencies and imports

Start by installing and importing the primary libraries and initializing a random seed so sample data is reproducible.

!pip install dash plotly pandas numpy dash-bootstrap-components


import dash
from dash import dcc, html, Input, Output, callback, dash_table
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import dash_bootstrap_components as dbc


print("Generating sample data...")
np.random.seed(42)

Generating synthetic stock data

To prototype an interactive dashboard you can create a synthetic multi-ticker dataset with prices, volumes, daily returns and technical indicators such as a 20-day moving average and rolling volatility. This ensures the dashboard remains fully functional without depending on external feeds while you develop.

start_date = datetime(2023, 1, 1)
end_date = datetime(2024, 12, 31)
dates = pd.date_range(start=start_date, end=end_date, freq='D')
stock_names = ['AAPL', 'GOOGL', 'MSFT', 'AMZN', 'TSLA']


all_data = []
base_prices = {'AAPL': 150, 'GOOGL': 120, 'MSFT': 250, 'AMZN': 100, 'TSLA': 200}


for stock in stock_names:
   print(f"Creating data for {stock}...")
   base_price = base_prices[stock]
  
   n_days = len(dates)
   returns = np.random.normal(0.0005, 0.025, n_days) 
   prices = np.zeros(n_days)
   prices[0] = base_price
  
   for i in range(1, n_days):
       prices[i] = prices[i-1] * (1 + returns[i])
  
   volumes = np.random.lognormal(15, 0.5, n_days).astype(int)
  
   stock_df = pd.DataFrame({
       'Date': dates,
       'Stock': stock,
       'Price': prices,
       'Volume': volumes,
       'Returns': np.concatenate([[0], np.diff(prices) / prices[:-1]]),
       'Sector': np.random.choice(['Technology', 'Consumer', 'Automotive'], 1)[0]
   })
  
   all_data.append(stock_df)


df = pd.concat(all_data, ignore_index=True)


df['Date'] = pd.to_datetime(df['Date'])
df_sorted = df.sort_values(['Stock', 'Date']).reset_index(drop=True)


print("Calculating technical indicators...")
df_sorted['MA_20'] = df_sorted.groupby('Stock')['Price'].transform(lambda x: x.rolling(20, min_periods=1).mean())
df_sorted['Volatility'] = df_sorted.groupby('Stock')['Returns'].transform(lambda x: x.rolling(30, min_periods=1).std())


df = df_sorted.copy()


print(f"Data generated successfully! Shape: {df.shape}")
print(f"Date range: {df['Date'].min()} to {df['Date'].max()}")
print(f"Stocks: {df['Stock'].unique().tolist()}")

Dashboard layout and controls

The app layout uses Bootstrap rows and cards to organize controls and visualizations. Controls include a multi-select dropdown for tickers, a date-range picker, chart style radio buttons and a toggle to show moving averages. The main area contains the primary price chart, supporting volume and returns charts, metric cards and a sortable/filterable data table.

app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])


app.layout = dbc.Container([
   dbc.Row([
       dbc.Col([
           html.H1(" Advanced Financial Dashboard", className="text-center mb-4"),
           html.P(f"Interactive dashboard with {len(df)} data points across {len(stock_names)} stocks",
                  className="text-center text-muted"),
           html.Hr()
       ])
   ]),
  
   dbc.Row([
       dbc.Col([
           dbc.Card([
               dbc.CardBody([
                   html.H5(" Dashboard Controls", className="card-title"),
                  
                   html.Label("Select Stocks:", className="fw-bold mt-3"),
                   dcc.Dropdown(
                       id='stock-dropdown',
                       options=[{'label': f'{stock} ({base_prices[stock]})', 'value': stock}
                               for stock in stock_names],
                       value=['AAPL', 'GOOGL'], 
                       multi=True,
                       placeholder="Choose stocks to analyze..."
                   ),
                  
                   html.Label("Date Range:", className="fw-bold mt-3"),
                   dcc.DatePickerRange(
                       id='date-picker-range',
                       start_date='2023-06-01',
                       end_date='2024-06-01',
                       display_format='YYYY-MM-DD',
                       style={'width': '100%'}
                   ),
                  
                   html.Label("Chart Style:", className="fw-bold mt-3"),
                   dcc.RadioItems(
                       id='chart-type',
                       options=[
                           {'label': ' Line Chart', 'value': 'line'},
                           {'label': ' Area Chart', 'value': 'area'},
                           {'label': ' Scatter Plot', 'value': 'scatter'}
                       ],
                       value='line',
                       labelStyle={'display': 'block', 'margin': '5px'}
                   ),
                  
                   dbc.Checklist(
                       id='show-ma',
                       options=[{'label': ' Show Moving Average', 'value': 'show'}],
                       value=[],
                       style={'margin': '10px 0'}
                   ),
               ])
           ], className="h-100")
       ], width=3),
      
       dbc.Col([
           dbc.Card([
               dbc.CardHeader(" Stock Price Analysis"),
               dbc.CardBody([
                   dcc.Graph(id='main-chart', style={'height': '450px'})
               ])
           ])
       ], width=9)
   ], className="mb-4"),
  
   dbc.Row([
       dbc.Col([
           dbc.Card([
               dbc.CardBody([
                   html.H4(id="avg-price", className="text-primary mb-0"),
                   html.Small("Average Price", className="text-muted")
               ])
           ])
       ], width=3),
       dbc.Col([
           dbc.Card([
               dbc.CardBody([
                   html.H4(id="total-volume", className="text-success mb-0"),
                   html.Small("Total Volume", className="text-muted")
               ])
           ])
       ], width=3),
       dbc.Col([
           dbc.Card([
               dbc.CardBody([
                   html.H4(id="price-range", className="text-info mb-0"),
                   html.Small("Price Range", className="text-muted")
               ])
           ])
       ], width=3),
       dbc.Col([
           dbc.Card([
               dbc.CardBody([
                   html.H4(id="data-points", className="text-warning mb-0"),
                   html.Small("Data Points", className="text-muted")
               ])
           ])
       ], width=3)
   ], className="mb-4"),
  
   dbc.Row([
       dbc.Col([
           dbc.Card([
               dbc.CardHeader(" Trading Volume"),
               dbc.CardBody([
                   dcc.Graph(id='volume-chart', style={'height': '300px'})
               ])
           ])
       ], width=6),
       dbc.Col([
           dbc.Card([
               dbc.CardHeader(" Returns Distribution"),
               dbc.CardBody([
                   dcc.Graph(id='returns-chart', style={'height': '300px'})
               ])
           ])
       ], width=6)
   ], className="mb-4"),
  
   dbc.Row([
       dbc.Col([
           dbc.Card([
               dbc.CardHeader(" Latest Stock Data"),
               dbc.CardBody([
                   dash_table.DataTable(
                       id='data-table',
                       columns=[
                           {'name': 'Stock', 'id': 'Stock'},
                           {'name': 'Date', 'id': 'Date'},
                           {'name': 'Price ($)', 'id': 'Price', 'type': 'numeric',
                            'format': {'specifier': '.2f'}},
                           {'name': 'Volume', 'id': 'Volume', 'type': 'numeric',
                            'format': {'specifier': ',.0f'}},
                           {'name': 'Daily Return (%)', 'id': 'Returns', 'type': 'numeric',
                            'format': {'specifier': '.2%'}}
                       ],
                       style_cell={'textAlign': 'center', 'fontSize': '14px', 'padding': '10px'},
                       style_header={'backgroundColor': 'rgb(230, 230, 230)', 'fontWeight': 'bold'},
                       style_data_conditional=[
                           {
                               'if': {'filter_query': '{Returns} > 0'},
                               'backgroundColor': '#d4edda'
                           },
                           {
                               'if': {'filter_query': '{Returns} < 0'},
                               'backgroundColor': '#f8d7da'
                           }
                       ],
                       page_size=15,
                       sort_action="native",
                       filter_action="native"
                   )
               ])
           ])
       ])
   ])
], fluid=True)

Wiring callbacks to drive interactivity

A single callback function can return multiple outputs: the main price figure, the volume and returns figures, the table rows and the metric values. The callback filters the dataframe by selected stocks and date range, selects the chart type (line, area, scatter), optionally overlays moving averages and computes summary metrics for display.

@callback(
   [Output('main-chart', 'figure'),
    Output('volume-chart', 'figure'),
    Output('returns-chart', 'figure'),
    Output('data-table', 'data'),
    Output('avg-price', 'children'),
    Output('total-volume', 'children'),
    Output('price-range', 'children'),
    Output('data-points', 'children')],
   [Input('stock-dropdown', 'value'),
    Input('date-picker-range', 'start_date'),
    Input('date-picker-range', 'end_date'),
    Input('chart-type', 'value'),
    Input('show-ma', 'value')]
)
def update_all_charts(selected_stocks, start_date, end_date, chart_type, show_ma):
   print(f"Callback triggered with stocks: {selected_stocks}")
  
   if not selected_stocks:
       selected_stocks = ['AAPL']
  
   filtered_df = df[
       (df['Stock'].isin(selected_stocks)) &
       (df['Date'] >= start_date) &
       (df['Date'] <= end_date)
   ].copy()
  
   print(f"Filtered data shape: {filtered_df.shape}")
  
   if filtered_df.empty:
       filtered_df = df[df['Stock'].isin(selected_stocks)].copy()
       print(f"Using all available data. Shape: {filtered_df.shape}")
  
   if chart_type == 'line':
       main_fig = px.line(filtered_df, x='Date', y='Price', color='Stock',
                         title=f'Stock Prices - {chart_type.title()} View',
                         labels={'Price': 'Price ($)', 'Date': 'Date'})
   elif chart_type == 'area':
       main_fig = px.area(filtered_df, x='Date', y='Price', color='Stock',
                         title=f'Stock Prices - {chart_type.title()} View',
                         labels={'Price': 'Price ($)', 'Date': 'Date'})
   else: 
       main_fig = px.scatter(filtered_df, x='Date', y='Price', color='Stock',
                            title=f'Stock Prices - {chart_type.title()} View',
                            labels={'Price': 'Price ($)', 'Date': 'Date'})
  
   if 'show' in show_ma:
       for stock in selected_stocks:
           stock_data = filtered_df[filtered_df['Stock'] == stock]
           if not stock_data.empty:
               main_fig.add_scatter(
                   x=stock_data['Date'],
                   y=stock_data['MA_20'],
                   mode='lines',
                   name=f'{stock} MA-20',
                   line=dict(dash='dash', width=2)
               )
  
   main_fig.update_layout(height=450, showlegend=True, hovermode='x unified')
  
   volume_fig = px.bar(filtered_df, x='Date', y='Volume', color='Stock',
                      title='Daily Trading Volume',
                      labels={'Volume': 'Volume (shares)', 'Date': 'Date'})
   volume_fig.update_layout(height=300, showlegend=True)
  
   returns_fig = px.histogram(filtered_df.dropna(subset=['Returns']),
                             x='Returns', color='Stock',
                             title='Daily Returns Distribution',
                             labels={'Returns': 'Daily Returns', 'count': 'Frequency'},
                             nbins=50)
   returns_fig.update_layout(height=300, showlegend=True)
  
   if not filtered_df.empty:
       avg_price = f"${filtered_df['Price'].mean():.2f}"
       total_volume = f"{filtered_df['Volume'].sum():,.0f}"
       price_range = f"${filtered_df['Price'].min():.0f} - ${filtered_df['Price'].max():.0f}"
       data_points = f"{len(filtered_df):,}"
      
       table_data = filtered_df.nlargest(100, 'Date')[
           ['Stock', 'Date', 'Price', 'Volume', 'Returns']
       ].round(4).to_dict('records')
      
       for row in table_data:
           row['Date'] = row['Date'].strftime('%Y-%m-%d') if pd.notnull(row['Date']) else ''
   else:
       avg_price = "No data"
       total_volume = "No data"
       price_range = "No data"
       data_points = "0"
       table_data = []
  
   return (main_fig, volume_fig, returns_fig, table_data,
           avg_price, total_volume, price_range, data_points)

Run the app locally or inline in Colab

The code shows a typical if name == ‘main’ block to start the Dash server. In notebooks or Colab you can use inline modes available in some environments; for local development switch to the standard app.run(debug=True).

if __name__ == '__main__':
   print("Starting Dash app...")
   print("Available data preview:")
   print(df.head())
   print(f"Total rows: {len(df)}")
  
   app.run(mode='inline', port=8050, debug=True, height=1000)
  
   # app.run(debug=True)

Practical tips and next steps