Skip to main content

Plotly for Interactive Dashboards

📚 What You'll Learn

By the end of this lesson, you will be able to:

⏱️ Estimated Time: 45–60 minutes

🎯 Project: Build a six-panel interactive sales dashboard that combines time-series, categorical, and heatmap views, then export it as a shareable HTML file.

Create Stunning Interactive Visualizations! 🚀

Plotly is a powerful Python library that creates interactive, publication-quality graphs. Unlike static visualizations, Plotly charts respond to user interactions - hover for details, zoom into areas of interest, pan across data, and even update in real-time. Perfect for dashboards, presentations, and data exploration!

Why Plotly?

Plotly brings your data to life with interactivity that static plots can't match:

Installation and Setup

# Install Plotly
# pip install plotly

# Import necessary libraries
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import pandas as pd
import numpy as np

# For displaying in Jupyter notebooks
import plotly.offline as pyo
pyo.init_notebook_mode(connected=True)

# For saving static images (requires kaleido)
# pip install kaleido

print("Plotly version:", plotly.__version__)

Plotly Express - Quick Interactive Plots

Plotly Express is the high-level API that creates complete figures with one function call:

# Load sample data
df = px.data.iris()
tips = px.data.tips()
gapminder = px.data.gapminder()

# Scatter plot with hover data
fig = px.scatter(df, x="sepal_width", y="sepal_length", 
                 color="species", size="petal_length",
                 hover_data=['petal_width'],
                 title="Iris Dataset Explorer")
fig.show()

# Bar chart with animation
fig = px.bar(tips, x="day", y="total_bill", color="sex",
             title="Average Bill by Day and Gender",
             barmode="group", height=400)
fig.show()

# Line plot with multiple traces
df_stocks = px.data.stocks()
fig = px.line(df_stocks, x='date', y=df_stocks.columns[1:],
              title='Stock Prices Over Time',
              labels={'value': 'Price', 'variable': 'Company'})
fig.show()

Interactive Features

Hover Information

# Customizing hover information
fig = px.scatter(tips, x="total_bill", y="tip", 
                 color="time", size="size",
                 hover_name="day",  # Bold title in hover
                 hover_data={
                     "total_bill": ":.2f",  # Format with 2 decimals
                     "tip": ":.2f",
                     "size": True,  # Show size
                     "time": False  # Hide time (already in color)
                 },
                 labels={"total_bill": "Total Bill ($)",
                        "tip": "Tip ($)",
                        "size": "Party Size"},
                 title="Restaurant Tips Analysis")

# Custom hover template
fig.update_traces(
    hovertemplate="%{hovertext}
" + "Bill: $%{x:.2f}
" + "Tip: $%{y:.2f}
" + "Party of %{marker.size}
" + "" # Removes trace name ) fig.show()

Zoom and Pan Controls

# Configure zoom and pan behavior
fig = px.scatter(df, x="sepal_width", y="sepal_length", color="species")

# Update layout with specific zoom settings
fig.update_layout(
    dragmode='zoom',  # 'pan', 'zoom', 'select', 'lasso'
    hovermode='closest',  # 'x', 'y', 'closest', False
    xaxis=dict(
        rangeslider=dict(visible=True),  # Add range slider
        rangeselector=dict(  # Add range selector buttons
            buttons=list([
                dict(count=1, label="1m", step="month", stepmode="backward"),
                dict(count=6, label="6m", step="month", stepmode="backward"),
                dict(count=1, label="YTD", step="year", stepmode="todate"),
                dict(step="all")
            ])
        )
    )
)
fig.show()

Creating Animated Visualizations

# Animated bubble chart
fig = px.scatter(gapminder, x="gdpPercap", y="lifeExp", 
                 animation_frame="year",  # Animation through years
                 animation_group="country",  # Group by country
                 size="pop", color="continent",
                 hover_name="country",
                 log_x=True,  # Log scale for x-axis
                 size_max=60,
                 range_x=[100, 100000], range_y=[25, 90],
                 title="Life Expectancy vs GDP Per Capita Over Time")

# Customize animation
fig.layout.updatemenus[0].buttons[0].args[1]["frame"]["duration"] = 1000  # 1 second per frame
fig.layout.updatemenus[0].buttons[0].args[1]["transition"]["duration"] = 500  # 0.5 second transition

fig.show()

# Animated bar race
df_continents = gapminder.groupby(['year', 'continent']).sum().reset_index()
fig = px.bar(df_continents, x="pop", y="continent", 
             animation_frame="year", orientation='h',
             range_x=[0, 4000000000],
             title="Population by Continent Over Time")
fig.show()

3D Visualizations

# 3D scatter plot
fig = px.scatter_3d(df, x='sepal_length', y='sepal_width', z='petal_width',
                    color='species', symbol='species',
                    title="Iris Dataset in 3D")
fig.update_layout(scene=dict(
    xaxis_title="Sepal Length",
    yaxis_title="Sepal Width",
    zaxis_title="Petal Width"
))
fig.show()

# 3D surface plot
x = np.linspace(-5, 5, 50)
y = np.linspace(-5, 5, 50)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))

fig = go.Figure(data=[go.Surface(z=Z, x=x, y=y)])
fig.update_layout(title='3D Surface: sin(sqrt(x² + y²))',
                  scene=dict(
                      xaxis_title='X',
                      yaxis_title='Y',
                      zaxis_title='Z'
                  ),
                  autosize=False,
                  width=700, height=700)
fig.show()

Statistical Charts

Box Plots and Violin Plots

# Interactive box plot
fig = px.box(tips, x="day", y="total_bill", color="smoker",
             notched=True,  # Show confidence interval
             title="Bill Distribution by Day",
             hover_data=["tip"])
fig.update_traces(quartilemethod="exclusive")  # or "inclusive", "linear"
fig.show()

# Violin plot with box inside
fig = px.violin(tips, x="day", y="total_bill", color="sex",
                box=True,  # Show box plot inside
                points="all",  # Show all points
                title="Bill Distribution (Violin + Box + Points)")
fig.show()

Heatmaps and Correlation Matrices

# Correlation heatmap
df_corr = df.select_dtypes(include=[np.number]).corr()

fig = px.imshow(df_corr,
                text_auto=True,  # Show values
                aspect="auto",
                color_continuous_scale="RdBu",
                title="Correlation Matrix")
fig.update_xaxes(side="bottom")
fig.show()

# Time series heatmap
# Create sample time series data
dates = pd.date_range('2024-01-01', periods=365, freq='D')
hours = np.arange(24)
data = np.random.randn(365, 24) + np.sin(np.arange(365) * 2 * np.pi / 365)[:, np.newaxis]

fig = go.Figure(data=go.Heatmap(
    z=data,
    x=hours,
    y=dates,
    colorscale='Viridis',
    hoverongaps=False,
    hovertemplate='Date: %{y|%Y-%m-%d}
Hour: %{x}:00
Value: %{z:.2f}' )) fig.update_layout(title="Hourly Data Throughout the Year", xaxis_title="Hour of Day", yaxis_title="Date") fig.show()

Geographic Visualizations

# Choropleth map
fig = px.choropleth(gapminder, 
                    locations="iso_alpha",
                    color="lifeExp",
                    hover_name="country",
                    animation_frame="year",
                    color_continuous_scale="Viridis",
                    title="Life Expectancy by Country Over Time",
                    range_color=[20, 80])
fig.update_layout(geo=dict(showframe=False, showcoastlines=True))
fig.show()

# Scatter map
fig = px.scatter_geo(gapminder[gapminder['year']==2007],
                     locations="iso_alpha",
                     size="pop",
                     hover_name="country",
                     color="continent",
                     projection="natural earth",
                     title="World Population in 2007")
fig.show()

# Map with custom data
import plotly.graph_objects as go

fig = go.Figure(data=go.Scattergeo(
    lon = [-73.9842, -118.2437, -87.6298],
    lat = [40.7128, 34.0522, 41.8781],
    text = ['New York', 'Los Angeles', 'Chicago'],
    mode = 'markers+text',
    marker = dict(
        size = [30, 20, 25],
        color = ['red', 'blue', 'green'],
        line = dict(width=1, color='white')
    ),
    textposition="bottom center"
))

fig.update_layout(
    title = 'Major US Cities',
    geo = dict(
        scope='usa',
        showland = True,
        landcolor = 'rgb(243, 243, 243)',
        countrycolor = 'rgb(204, 204, 204)',
    ),
)
fig.show()

Creating Subplots

# Create subplots with different chart types
from plotly.subplots import make_subplots

fig = make_subplots(
    rows=2, cols=2,
    specs=[[{"type": "scatter"}, {"type": "bar"}],
           [{"type": "heatmap"}, {"type": "scatter3d"}]],
    subplot_titles=("Scatter Plot", "Bar Chart", 
                    "Heatmap", "3D Scatter"),
    vertical_spacing=0.1,
    horizontal_spacing=0.1
)

# Add traces to subplots
fig.add_trace(
    go.Scatter(x=[1, 2, 3, 4], y=[10, 11, 12, 13], 
               mode='lines+markers', name='Trend'),
    row=1, col=1
)

fig.add_trace(
    go.Bar(x=['A', 'B', 'C'], y=[5, 10, 15], name='Values'),
    row=1, col=2
)

fig.add_trace(
    go.Heatmap(z=[[1, 2, 3], [4, 5, 6], [7, 8, 9]], 
               showscale=False),
    row=2, col=1
)

fig.add_trace(
    go.Scatter3d(x=[1, 2, 3], y=[2, 3, 4], z=[5, 6, 7],
                 mode='markers', marker=dict(size=10)),
    row=2, col=2
)

fig.update_layout(height=600, showlegend=True,
                  title_text="Multiple Chart Types in Subplots")
fig.show()

Building an Interactive Dashboard

# Complete interactive dashboard example
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import pandas as pd
import numpy as np

# Generate sample data
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=365, freq='D')

# Sales data
sales_data = pd.DataFrame({
    'date': dates,
    'sales': np.cumsum(np.random.randn(365)) + 1000,
    'costs': np.cumsum(np.random.randn(365) * 0.5) + 500,
    'customers': np.random.poisson(100, 365),
    'region': np.random.choice(['North', 'South', 'East', 'West'], 365),
    'product': np.random.choice(['A', 'B', 'C'], 365)
})
sales_data['profit'] = sales_data['sales'] - sales_data['costs']

# Create dashboard
fig = make_subplots(
    rows=3, cols=2,
    subplot_titles=('Revenue & Costs Over Time', 'Customer Distribution',
                    'Sales by Region', 'Product Performance',
                    'Profit Trend', 'Daily Metrics Heatmap'),
    specs=[[{"secondary_y": True}, {"type": "box"}],
           [{"type": "bar"}, {"type": "pie"}],
           [{"secondary_y": False}, {"type": "heatmap"}]],
    vertical_spacing=0.1,
    horizontal_spacing=0.15
)

# 1. Revenue & Costs Over Time (with secondary y-axis)
fig.add_trace(
    go.Scatter(x=sales_data['date'], y=sales_data['sales'],
               name='Sales', line=dict(color='green', width=2)),
    row=1, col=1, secondary_y=False
)
fig.add_trace(
    go.Scatter(x=sales_data['date'], y=sales_data['costs'],
               name='Costs', line=dict(color='red', width=2)),
    row=1, col=1, secondary_y=False
)
fig.add_trace(
    go.Scatter(x=sales_data['date'], y=sales_data['customers'],
               name='Customers', line=dict(color='blue', width=1, dash='dot')),
    row=1, col=1, secondary_y=True
)

# 2. Customer Distribution
for region in sales_data['region'].unique():
    region_data = sales_data[sales_data['region'] == region]
    fig.add_trace(
        go.Box(y=region_data['customers'], name=region),
        row=1, col=2
    )

# 3. Sales by Region
region_sales = sales_data.groupby('region')['sales'].sum()
fig.add_trace(
    go.Bar(x=region_sales.index, y=region_sales.values,
           marker_color=['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4']),
    row=2, col=1
)

# 4. Product Performance (Pie Chart)
product_sales = sales_data.groupby('product')['sales'].sum()
fig.add_trace(
    go.Pie(labels=product_sales.index, values=product_sales.values,
           hole=0.3),  # Donut chart
    row=2, col=2
)

# 5. Profit Trend with Moving Average
window = 30
sales_data['profit_ma'] = sales_data['profit'].rolling(window=window).mean()
fig.add_trace(
    go.Scatter(x=sales_data['date'], y=sales_data['profit'],
               name='Daily Profit', mode='lines',
               line=dict(color='lightgray', width=1)),
    row=3, col=1
)
fig.add_trace(
    go.Scatter(x=sales_data['date'], y=sales_data['profit_ma'],
               name=f'{window}-Day MA', mode='lines',
               line=dict(color='purple', width=2)),
    row=3, col=1
)

# 6. Daily Metrics Heatmap
# Prepare data for heatmap
pivot_data = sales_data.pivot_table(
    index=sales_data['date'].dt.dayofweek,
    columns=sales_data['date'].dt.week,
    values='sales',
    aggfunc='mean'
)
fig.add_trace(
    go.Heatmap(z=pivot_data.values,
               x=pivot_data.columns,
               y=['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
               colorscale='Viridis'),
    row=3, col=2
)

# Update layout
fig.update_xaxes(title_text="Date", row=1, col=1)
fig.update_xaxes(title_text="Region", row=2, col=1)
fig.update_xaxes(title_text="Date", row=3, col=1)
fig.update_xaxes(title_text="Week of Year", row=3, col=2)

fig.update_yaxes(title_text="Amount ($)", row=1, col=1, secondary_y=False)
fig.update_yaxes(title_text="Customers", row=1, col=1, secondary_y=True)
fig.update_yaxes(title_text="Customers", row=1, col=2)
fig.update_yaxes(title_text="Sales ($)", row=2, col=1)
fig.update_yaxes(title_text="Profit ($)", row=3, col=1)
fig.update_yaxes(title_text="Day of Week", row=3, col=2)

fig.update_layout(
    title_text="Sales Dashboard - 2024",
    showlegend=True,
    height=900,
    hovermode='x unified'
)

fig.show()

Customizing Appearance

Themes and Templates

# Available themes
themes = ['plotly', 'plotly_white', 'plotly_dark', 'ggplot2', 
          'seaborn', 'simple_white', 'none']

# Apply a theme
fig = px.scatter(df, x="sepal_width", y="sepal_length", 
                 color="species", template="plotly_dark",
                 title="Dark Theme Example")
fig.show()

# Custom theme
import plotly.io as pio

# Create custom template
pio.templates["custom"] = go.layout.Template(
    layout=go.Layout(
        colorway=['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'],
        font=dict(family="Arial, sans-serif", size=12, color="#2C3E50"),
        title=dict(font=dict(size=24, color="#34495E")),
        hovermode="closest",
        plot_bgcolor="#F8F9FA",
        paper_bgcolor="white",
        xaxis=dict(gridcolor="#E0E0E0", zerolinecolor="#E0E0E0"),
        yaxis=dict(gridcolor="#E0E0E0", zerolinecolor="#E0E0E0")
    )
)

# Use custom template
fig = px.bar(tips, x="day", y="total_bill", color="sex",
             template="custom", title="Custom Theme Example")
fig.show()

Annotations and Shapes

# Add annotations
fig = px.scatter(tips, x="total_bill", y="tip")

# Add text annotation
fig.add_annotation(
    x=50, y=10,
    text="High spender, high tipper",
    showarrow=True,
    arrowhead=2,
    arrowsize=1,
    arrowwidth=2,
    arrowcolor="red",
    font=dict(size=12, color="red")
)

# Add shapes
fig.add_shape(
    type="rect",
    x0=30, y0=5, x1=50, y1=10,
    fillcolor="LightSalmon",
    opacity=0.3,
    layer="below",
    line=dict(width=2, color="red")
)

# Add reference lines
fig.add_hline(y=tips['tip'].mean(), line_dash="dash", 
              annotation_text="Average Tip")
fig.add_vline(x=tips['total_bill'].mean(), line_dash="dash",
              annotation_text="Average Bill")

fig.show()

Saving and Sharing

# Save as HTML (interactive)
fig.write_html("dashboard.html")

# Save as static image (requires kaleido)
fig.write_image("plot.png", width=1200, height=800, scale=2)
fig.write_image("plot.pdf")
fig.write_image("plot.svg")

# Save with custom config
config = {
    'displayModeBar': True,  # Show toolbar
    'displaylogo': False,  # Hide Plotly logo
    'modeBarButtonsToRemove': ['pan2d', 'lasso2d'],
    'toImageButtonOptions': {
        'format': 'png',
        'filename': 'custom_image',
        'height': 500,
        'width': 700,
        'scale': 1
    }
}
fig.show(config=config)

# Export to JSON
fig.write_json("figure.json")

# Share online (requires Chart Studio account)
import chart_studio.plotly as py
py.plot(fig, filename='my-dashboard', auto_open=True)

Integration with Dash

For full web applications with Plotly, use Dash:

# Basic Dash app example
# pip install dash

import dash
from dash import dcc, html, Input, Output
import plotly.express as px

# Initialize app
app = dash.Dash(__name__)

# Layout
app.layout = html.Div([
    html.H1("Interactive Dashboard"),
    
    dcc.Dropdown(
        id='dropdown-selection',
        options=[
            {'label': 'Sepal Length', 'value': 'sepal_length'},
            {'label': 'Sepal Width', 'value': 'sepal_width'},
            {'label': 'Petal Length', 'value': 'petal_length'},
            {'label': 'Petal Width', 'value': 'petal_width'}
        ],
        value='sepal_length'
    ),
    
    dcc.Graph(id='graph-content')
])

# Callback for interactivity
@app.callback(
    Output('graph-content', 'figure'),
    Input('dropdown-selection', 'value')
)
def update_graph(value):
    df = px.data.iris()
    fig = px.histogram(df, x=value, color='species', 
                       nbins=30, title=f'Distribution of {value}')
    return fig

# Run app
if __name__ == '__main__':
    app.run_server(debug=True)

Performance Tips

Common Patterns

graph TD A[Data Type] --> B{Static or Interactive?} B -->|Static| C[Use Matplotlib/Seaborn] B -->|Interactive| D[Use Plotly] D --> E{Quick or Custom?} E -->|Quick| F[Plotly Express] E -->|Custom| G[Graph Objects] F --> H[px.scatter] F --> I[px.line] F --> J[px.bar] G --> K[go.Figure] K --> L[add_trace] K --> M[update_layout] K --> N[update_traces] D --> O{Deployment} O --> P[HTML Export] O --> Q[Dash App] O --> R[Jupyter]

Practice Exercises

Exercise 1: Interactive Time Series

Create an interactive time series plot with:

  1. Range slider for date selection
  2. Buttons for different time ranges (1M, 3M, 6M, 1Y, All)
  3. Multiple traces with hover information
  4. Annotations for key events

Exercise 2: Animated Geographic Data

Build an animated choropleth map showing:

  1. Data changing over time
  2. Custom hover templates
  3. Play/pause controls
  4. Color scale representing values

Exercise 3: Multi-Page Dashboard

Create a dashboard with:

  1. At least 6 different chart types
  2. Consistent color scheme
  3. Interactive elements (dropdowns, sliders)
  4. Export functionality

Key Takeaways

Further Resources

📓 Learning Journal

Keep a learning journal — digital or physical. After this lesson, take a few minutes to write down:

  • Key concepts you learned
  • Techniques that clicked for you
  • Questions or confusion points to revisit
  • Ideas you want to try
  • Your progress and feelings about learning this

✍️ This lesson's prompt: Plotly Express can draw a rich chart in a single line, while Graph Objects give you total control. Think of a chart you want to make — would you reach for the quick one-liner or the detailed builder, and what does that choice tell you about the trade-off between speed and control?

📝 Lesson Summary

🎓 Key Takeaways

  • Plotly produces interactive, web-ready figures where hover, zoom, pan, and selection come built in — no extra code.
  • Plotly Express is the fast high-level API for common charts; Graph Objects give fine-grained control when you need it.
  • make_subplots lets you combine many chart types — including secondary y-axes and 3D — into one dashboard.
  • Figures export to standalone HTML, static images (via kaleido), or JSON, and Dash turns them into full web apps.

🎉 What You've Accomplished

You can now build the kind of interactive charts people actually explore — animated bubble charts, 3D surfaces, choropleth maps, and multi-panel dashboards — and share them as a single HTML file that works in any browser.

❓ Common Questions at This Stage

When should I use Plotly Express versus Graph Objects?

Start with Plotly Express — it handles most charts in one line and returns a normal figure you can still tweak. Drop down to Graph Objects when you need something Express doesn't cover, like mixing chart types in subplots or fully custom traces and layouts.

How do I share an interactive Plotly chart with someone?

Use fig.write_html("chart.html") to get a self-contained file anyone can open in a browser — no Python required. For static needs, fig.write_image() exports PNG, PDF, or SVG (install kaleido first).

My chart is slow with a lot of points. What can I do?

Switch scatter traces to go.Scattergl (WebGL) for 10,000+ points, pre-aggregate your data, limit the fields in hover, and keep animation frames modest. For millions of points, pair Plotly with datashader.

🔭 Looking Ahead

Next you'll meet another web-native visualization library and see how the same interactive goals can be met with a different tool — and when each one is the better fit for your project.

✅ Before the Next Lesson

🌟 Encouragement for the Journey

You just turned static numbers into visualizations people can explore with their own hands. That interactivity is what makes data feel alive — and you can now create it in a single line of Python. Keep experimenting!