Plotly for Interactive Dashboards
📚 What You'll Learn
By the end of this lesson, you will be able to:
- Explain how Plotly differs from static libraries and when interactivity is worth it
- Create charts quickly with Plotly Express and control every detail with Graph Objects
- Customize hover tooltips, zoom, pan, and range selectors for exploration
- Build animated, 3D, statistical, and geographic visualizations
- Assemble multi-chart dashboards with
make_subplotsand export them as HTML or images - Recognize when to graduate from a figure to a full Dash web app
⏱️ 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:
- 🎯 Interactive by Default: Hover tooltips, zoom, pan, and selection tools built-in
- 📊 40+ Chart Types: From basic to advanced statistical and 3D visualizations
- 🌐 Web-Ready: Export as HTML for easy sharing and embedding
- ⚡ Plotly Express: Create complex plots with one line of code
- 🎨 Professional Styling: Beautiful themes and customization options
- 📱 Responsive: Automatically adjusts to different screen sizes
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
- Use Scattergl for Large Datasets: Replace
go.Scatterwithgo.Scatterglfor 10,000+ points - Aggregate Data: Pre-aggregate data when possible instead of plotting raw points
- Limit Frames in Animations: Too many animation frames can slow down rendering
- Use DataShader: For millions of points, consider using datashader with Plotly
- Optimize Hover Data: Limit hover_data to essential information
- WebGL Rendering: Use
render_mode='webgl'for better performance
Common Patterns
Practice Exercises
Exercise 1: Interactive Time Series
Create an interactive time series plot with:
- Range slider for date selection
- Buttons for different time ranges (1M, 3M, 6M, 1Y, All)
- Multiple traces with hover information
- Annotations for key events
Exercise 2: Animated Geographic Data
Build an animated choropleth map showing:
- Data changing over time
- Custom hover templates
- Play/pause controls
- Color scale representing values
Exercise 3: Multi-Page Dashboard
Create a dashboard with:
- At least 6 different chart types
- Consistent color scheme
- Interactive elements (dropdowns, sliders)
- Export functionality
Key Takeaways
- 📊 Plotly creates interactive visualizations perfect for exploration and presentation
- ⚡ Plotly Express provides one-line solutions for common plots
- 🎯 Graph Objects offer fine-grained control over every aspect
- 🌐 Export options include HTML, static images, and online sharing
- 🚀 Dash integration enables full web applications
- 📱 Responsive design works across devices automatically
Further Resources
- Plotly Python Documentation
- Plotly Express Guide
- Dash Documentation
- Plotly Community Forum
- Plotly GitHub Repository
📓 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_subplotslets 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
- Take a dataset of your own and build one Plotly Express chart, then customize its hover template with
update_traces. - Combine at least three chart types into a single
make_subplotsfigure and export it withwrite_html. - Write your Learning Journal entry for this 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!