Skip to main content
Data Visualization
CHAPTER 19 Beginner

Building Dashboards with Plotly Dash

Updated: May 18, 2026
5 min read

# CHAPTER 19

Building Dashboards with Plotly Dash

1. Chapter Introduction

Plotly Dash transforms Python analytics into deployable web applications — no JavaScript required. This chapter builds a complete, interactive sales analytics dashboard with filters, KPI cards, and linked charts.

2. Dash Fundamentals

python
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
# Install: pip install dash dash-bootstrap-components

from dash import Dash, html, dcc, Input, Output, callback
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
import numpy as np

# Sample data
np.random.seed(42)
df = pd.DataFrame({
    'Date':    pd.date_range('2024-01-01', periods=365, freq='D'),
    'Revenue': np.random.normal(5000, 1200, 365) + np.linspace(0, 2000, 365),
    'Region':  np.random.choice(['North', 'South', 'East', 'West'], 365),
    'Product': np.random.choice(['Laptop', 'Phone', 'Monitor', 'Tablet'], 365),
    'Orders':  np.random.randint(20, 80, 365)
})
df['Quarter'] = df['Date'].dt.quarter.map({1:'Q1',2:'Q2',3:'Q3',4:'Q4'})

app = Dash(__name__)

# ─── LAYOUT ───
app.layout = html.Div([
    # Header
    html.Div([
        html.H1("Sales Analytics Dashboard", style={'color': 'white', 'margin': '0'}),
        html.P("Real-time Business Intelligence", style={'color': '#90CAF9', 'margin': '0'})
    ], style={'background': '#1565C0', 'padding': '20px 30px'}),

    # Filters
    html.Div([
        html.Div([
            html.Label("Region:", style={'fontWeight': 'bold'}),
            dcc.Dropdown(
                id='region-filter',
                options=[{'label': 'All Regions', 'value': 'All'}] +
                        [{'label': r, 'value': r} for r in df['Region'].unique()],
                value='All', clearable=False
            )
        ], style={'width': '23%', 'display': 'inline-block', 'marginRight': '2%'}),

        html.Div([
            html.Label("Quarter:", style={'fontWeight': 'bold'}),
            dcc.Dropdown(
                id='quarter-filter',
                options=[{'label': 'All Quarters', 'value': 'All'}] +
                        [{'label': q, 'value': q} for q in ['Q1','Q2','Q3','Q4']],
                value='All', clearable=False
            )
        ], style={'width': '23%', 'display': 'inline-block'}),
    ], style={'padding': '20px 30px', 'background': '#F5F5F5', 'borderBottom': '1px solid #E0E0E0'}),

    # KPI Row
    html.Div([
        html.Div(id='kpi-revenue', style={'width': '22%', 'display': 'inline-block', 'margin': '1%'}),
        html.Div(id='kpi-orders',  style={'width': '22%', 'display': 'inline-block', 'margin': '1%'}),
        html.Div(id='kpi-avg',     style={'width': '22%', 'display': 'inline-block', 'margin': '1%'}),
        html.Div(id='kpi-days',    style={'width': '22%', 'display': 'inline-block', 'margin': '1%'}),
    ], style={'padding': '20px 20px'}),

    # Charts Row 1
    html.Div([
        html.Div([dcc.Graph(id='revenue-trend')],
                  style={'width': '65%', 'display': 'inline-block'}),
        html.Div([dcc.Graph(id='region-pie')],
                  style={'width': '33%', 'display': 'inline-block'}),
    ], style={'padding': '0 20px'}),

    # Charts Row 2
    html.Div([
        html.Div([dcc.Graph(id='product-bar')],
                  style={'width': '48%', 'display': 'inline-block'}),
        html.Div([dcc.Graph(id='heatmap-chart')],
                  style={'width': '50%', 'display': 'inline-block'}),
    ], style={'padding': '0 20px 20px'}),

], style={'fontFamily': 'Inter, sans-serif', 'background': '#FAFAFA'})


def make_kpi_card(label, value, delta, color='#1565C0'):
    return html.Div([
        html.P(label, style={'color': '#666', 'fontSize': '12px', 'margin': '0 0 5px 0'}),
        html.H3(value, style={'color': color, 'margin': '0', 'fontSize': '28px', 'fontWeight': 'bold'}),
        html.P(delta, style={'color': '#4CAF50' if '+' in str(delta) else '#F44336',
                               'fontSize': '13px', 'margin': '5px 0 0 0'})
    ], style={'background': 'white', 'padding': '20px', 'borderRadius': '8px',
               'boxShadow': '0 2px 8px rgba(0,0,0,0.08)', 'borderTop': f'4px solid {color}'})


# ─── CALLBACKS ───
@callback(
    [Output('kpi-revenue', 'children'), Output('kpi-orders', 'children'),
     Output('kpi-avg', 'children'),    Output('kpi-days', 'children'),
     Output('revenue-trend', 'figure'), Output('region-pie', 'figure'),
     Output('product-bar', 'figure'),  Output('heatmap-chart', 'figure')],
    [Input('region-filter', 'value'), Input('quarter-filter', 'value')]
)
def update_dashboard(region, quarter):
    filtered = df.copy()
    if region != 'All':  filtered = filtered[filtered['Region'] == region]
    if quarter != 'All': filtered = filtered[filtered['Quarter'] == quarter]

    total_rev = filtered['Revenue'].sum()
    total_orders = filtered['Orders'].sum()
    avg_order = total_rev / max(total_orders, 1)
    days = len(filtered)

    kpis = [
        make_kpi_card('Total Revenue', f'${total_rev:,.0f}', '+12.3%', '#1565C0'),
        make_kpi_card('Total Orders',  f'{total_orders:,}',   '+8.7%',  '#2E7D32'),
        make_kpi_card('Avg Order',     f'${avg_order:.2f}',  '+3.1%',  '#E65100'),
        make_kpi_card('Active Days',   f'{days}',            '',       '#6A1B9A'),
    ]

    # Line chart
    daily = filtered.groupby('Date')['Revenue'].sum().reset_index()
    daily['MA7'] = daily['Revenue'].rolling(7).mean()
    fig_line = go.Figure()
    fig_line.add_trace(go.Scatter(x=daily['Date'], y=daily['Revenue'],
                                    mode='lines', name='Daily', line_color='#90CAF9', line_width=1))
    fig_line.add_trace(go.Scatter(x=daily['Date'], y=daily['MA7'],
                                    mode='lines', name='7-day MA', line_color='#1565C0', line_width=2.5))
    fig_line.update_layout(title='Revenue Trend', template='plotly_white', height=300,
                            margin=dict(l=40,r=20,t=40,b=30), hovermode='x unified')

    # Pie
    region_rev = filtered.groupby('Region')['Revenue'].sum()
    fig_pie = px.pie(values=region_rev.values, names=region_rev.index, hole=0.4,
                      title='Revenue by Region', template='plotly_white')
    fig_pie.update_layout(height=300, margin=dict(l=20,r=20,t=40,b=20))

    # Bar
    prod_rev = filtered.groupby('Product')['Revenue'].sum().sort_values(ascending=True)
    fig_bar = px.bar(x=prod_rev.values, y=prod_rev.index, orientation='h',
                      title='Revenue by Product', template='plotly_white',
                      color=prod_rev.values, color_continuous_scale='Blues')
    fig_bar.update_layout(height=300, margin=dict(l=20,r=20,t=40,b=30), showlegend=False)

    # Heatmap
    pivot = filtered.groupby(['Product', 'Quarter'])['Revenue'].sum().unstack(fill_value=0)
    fig_heat = px.imshow(pivot, text_auto='.0f', title='Revenue Heatmap (Product × Quarter)',
                          color_continuous_scale='Blues', template='plotly_white')
    fig_heat.update_layout(height=300, margin=dict(l=20,r=20,t=40,b=30))

    return kpis + [fig_line, fig_pie, fig_bar, fig_heat]


if __name__ == '__main__':
    app.run(debug=True, port=8050)
# Visit: http://127.0.0.1:8050

3. Common Mistakes

  • Callback without matching IDs: Every Input and Output ID must exactly match a component id. A mismatch silently breaks the callback.
  • Heavy computation in callbacks: Don't re-query a database on every dropdown change — cache results with @lru_cache or use Dash's dcc.Store.

4. MCQs

Question 1

Dash @callback decorator is for?

Question 2

dcc.Dropdown(id='region') id must?

Question 3

dcc.Graph(id='chart') renders?

Question 4

html.Div([...], style={...}) in Dash?

Question 5

Dash app runs by default on?

Question 6

Input('region-filter', 'value') listens to?

Question 7

Best practice for heavy DB queries in callbacks?

Question 8

debug=True in app.run() enables?

Question 9

Dash is built on top of?

Question 10

dcc.Store component is for?

5. Interview Questions

  • Q: How does Dash's callback system work?
  • Q: How would you optimize a Dash dashboard with slow database queries?

6. Summary

Plotly Dash: layouts with html.* components, charts with dcc.Graph, reactivity with @callback linking Input (filters) to Output (charts). Deploy with app.run(debug=True). Key optimization: cache expensive computations outside callbacks. Dash runs on Flask — deployable to Heroku, AWS, Azure.

7. Next Chapter Recommendation

In Chapter 20: Data Storytelling Techniques, we learn how to sequence charts into narratives that drive action — the skill that separates analysts from influencers.

Finish this Chapter

Save your progress on your learning path and prepare for coding interview challenges.

Discussion

Join the discussion

Log in or create a free account to participate.

Sort: ·