Python Plotly and Dash: Interactive Data Visualization
Plotly produces publication-quality interactive charts that run in the browser — zoom, hover, filter, and export without any JavaScript. Dash wraps Plotly charts in a full web application framework with reactive callbacks, making it possible to build analytics dashboards entirely in Python. This guide covers Plotly Express for rapid charting, graph_objects for full control, and Dash for interactive multi-page dashboards with live data.
Table of Contents
Plotly Express: Quick Charts
pip install plotly dash pandas
import plotly.express as px
import pandas as pd
import numpy as np
df = pd.DataFrame({
"month": pd.date_range("2026-01-01", periods=12, freq="ME"),
"revenue": [45, 52, 61, 58, 67, 72, 80, 78, 85, 90, 95, 102],
"costs": [30, 33, 38, 35, 40, 42, 45, 44, 48, 50, 52, 55],
"region": ["North"] * 6 + ["South"] * 6,
})
# Line chart
fig = px.line(df, x="month", y="revenue", color="region",
title="Monthly Revenue by Region",
labels={"revenue": "Revenue ($K)", "month": "Month"},
markers=True)
fig.update_layout(hovermode="x unified")
fig.write_html("revenue.html") # save as interactive HTML
fig.show() # open in browser
# Bar chart
fig = px.bar(df, x="month", y=["revenue", "costs"],
barmode="group", title="Revenue vs Costs")
# Scatter plot with trendline
df2 = px.data.iris()
fig = px.scatter(df2, x="sepal_width", y="sepal_length",
color="species", size="petal_length",
trendline="ols", hover_data=["petal_width"])
# Histogram
fig = px.histogram(df2, x="sepal_length", nbins=30,
color="species", barmode="overlay", opacity=0.7)
# Box plot
fig = px.box(df2, x="species", y="petal_length",
color="species", points="all", notched=True)
# Heatmap / correlation matrix
corr = df2.select_dtypes("number").corr()
fig = px.imshow(corr, text_auto=True, color_continuous_scale="RdBu_r",
title="Feature Correlation Matrix")
# Choropleth map
df_map = px.data.gapminder().query("year == 2007")
fig = px.choropleth(df_map, locations="iso_alpha",
color="gdpPercap", hover_name="country",
color_continuous_scale="Viridis",
title="GDP Per Capita 2007")
graph_objects: Full Control
import plotly.graph_objects as go
# Custom candlestick chart (financial)
dates = pd.date_range("2026-01-01", periods=30)
opens = 100 + np.random.randn(30).cumsum()
highs = opens + abs(np.random.randn(30))
lows = opens - abs(np.random.randn(30))
closes = opens + np.random.randn(30) * 0.5
fig = go.Figure(go.Candlestick(
x=dates, open=opens, high=highs, low=lows, close=closes,
increasing_line_color="green", decreasing_line_color="red",
))
fig.update_layout(title="Stock Price", xaxis_rangeslider_visible=False)
# Waterfall chart
fig = go.Figure(go.Waterfall(
name="2026 P&L",
orientation="v",
measure=["absolute", "relative", "relative", "relative", "total"],
x=["Revenue", "COGS", "Gross Profit", "OpEx", "Net Income"],
y=[500, -200, 300, -150, 150],
connector={"line": {"color": "rgb(63, 63, 63)"}},
decreasing={"marker": {"color": "red"}},
increasing={"marker": {"color": "green"}},
totals={"marker": {"color": "blue"}},
))
# Funnel chart
fig = go.Figure(go.Funnel(
y=["Website Visits", "Leads", "Qualified", "Proposals", "Closed"],
x=[10000, 3000, 1200, 400, 120],
textinfo="value+percent initial",
marker={"color": ["#636EFA", "#EF553B", "#00CC96", "#AB63FA", "#FFA15A"]},
))
# Gauge chart — KPI indicator
fig = go.Figure(go.Indicator(
mode="gauge+number+delta",
value=87.5,
delta={"reference": 80},
gauge={"axis": {"range": [0, 100]},
"bar": {"color": "darkblue"},
"steps": [{"range": [0, 50], "color": "lightgray"},
{"range": [50, 75], "color": "gray"}],
"threshold": {"line": {"color": "red", "width": 4},
"thickness": 0.75, "value": 90}},
title={"text": "Customer Satisfaction Score"},
))
Subplots and Multi-Panel Layouts
from plotly.subplots import make_subplots
import plotly.graph_objects as go
fig = make_subplots(
rows=2, cols=2,
subplot_titles=("Revenue Trend", "Cost Breakdown", "Margin %", "Regional Share"),
specs=[[{"type": "scatter"}, {"type": "bar"}],
[{"type": "scatter"}, {"type": "pie"}]],
)
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
revenue = [45, 52, 61, 58, 67, 72]
costs = [30, 33, 38, 35, 40, 42]
fig.add_trace(go.Scatter(x=months, y=revenue, name="Revenue", line={"color": "blue"}), row=1, col=1)
fig.add_trace(go.Bar(x=months, y=costs, name="Costs", marker_color="orange"), row=1, col=2)
fig.add_trace(go.Scatter(x=months, y=[(r-c)/r*100 for r,c in zip(revenue, costs)],
name="Margin %", fill="tozeroy"), row=2, col=1)
fig.add_trace(go.Pie(labels=["North", "South", "East", "West"],
values=[35, 28, 22, 15], hole=0.4), row=2, col=2)
fig.update_layout(height=600, title_text="Executive Dashboard", showlegend=False)
fig.show()
Dash: Your First Dashboard
from dash import Dash, dcc, html, Input, Output
import plotly.express as px
import pandas as pd
app = Dash(__name__)
df = px.data.gapminder()
app.layout = html.Div([
html.H1("World Development Dashboard", style={"textAlign": "center"}),
html.Div([
html.Label("Select Year:"),
dcc.Slider(
id="year-slider",
min=df["year"].min(),
max=df["year"].max(),
step=5,
value=2007,
marks={str(y): str(y) for y in df["year"].unique()},
),
], style={"margin": "20px"}),
html.Div([
html.Div([
dcc.Dropdown(
id="continent-filter",
options=[{"label": c, "value": c} for c in df["continent"].unique()],
value=None, multi=True, placeholder="Filter by continent...",
),
], style={"width": "48%", "display": "inline-block"}),
]),
dcc.Graph(id="main-scatter"),
dcc.Graph(id="bar-chart"),
])
if __name__ == "__main__":
app.run(debug=True)
Callbacks and Interactivity
from dash import callback, Input, Output, State, no_update
import plotly.express as px
@callback(
Output("main-scatter", "figure"),
Output("bar-chart", "figure"),
Input("year-slider", "value"),
Input("continent-filter", "value"),
)
def update_charts(selected_year, selected_continents):
filtered = df[df["year"] == selected_year]
if selected_continents:
filtered = filtered[filtered["continent"].isin(selected_continents)]
scatter = px.scatter(
filtered, x="gdpPercap", y="lifeExp",
size="pop", color="continent",
hover_name="country", log_x=True, size_max=60,
title=f"Life Expectancy vs GDP per Capita ({selected_year})",
)
bar = px.bar(
filtered.groupby("continent")["pop"].sum().reset_index(),
x="continent", y="pop", color="continent",
title="Population by Continent",
labels={"pop": "Population"},
)
return scatter, bar
# Chained callbacks
@callback(
Output("detail-panel", "children"),
Input("main-scatter", "clickData"),
)
def show_country_detail(click_data):
if not click_data:
return "Click a country to see details."
country = click_data["points"][0]["hovertext"]
data = df[df["country"] == country].sort_values("year")
return html.Div([
html.H3(country),
dcc.Graph(figure=px.line(data, x="year", y=["gdpPercap", "lifeExp"],
title=f"{country} over time")),
])
Live Updating Charts
from dash import dcc, html, callback, Input, Output
import plotly.graph_objects as go
from collections import deque
import random
import time
# Circular buffer for streaming data
data_x = deque(maxlen=100)
data_y = deque(maxlen=100)
app.layout = html.Div([
dcc.Graph(id="live-chart"),
dcc.Interval(id="interval", interval=1000, n_intervals=0), # 1 second
])
@callback(
Output("live-chart", "figure"),
Input("interval", "n_intervals"),
)
def update_live_chart(n):
data_x.append(time.time())
data_y.append(random.gauss(50, 10))
fig = go.Figure(go.Scatter(
x=list(data_x), y=list(data_y),
mode="lines+markers",
line={"color": "royalblue"},
))
fig.update_layout(
title="Live Sensor Data",
xaxis_title="Time", yaxis_title="Value",
uirevision="constant", # preserve zoom/pan across updates
)
Deployment
# Production server with gunicorn
# gunicorn app:server -w 4 -b 0.0.0.0:8050
# app.py — expose the Flask server
from dash import Dash
app = Dash(__name__)
server = app.server # Flask WSGI app
# Docker
# FROM python:3.12-slim
# RUN pip install dash gunicorn pandas plotly
# COPY . .
# CMD ["gunicorn", "app:server", "-w", "4", "-b", "0.0.0.0:8050"]
Frequently Asked Questions
- Plotly vs Matplotlib — when to use which?
- Matplotlib is better for publication figures (PDF/SVG output, LaTeX integration, precise layout control) and is the standard for scientific papers. Plotly is better for interactive web dashboards, business analytics, and any scenario where end users need to explore data. Dash is the right tool when you need a full interactive web app without JavaScript.
- How do I handle large datasets in Dash?
- For datasets above ~100K points, use server-side aggregation in callbacks before plotting. WebGL-based charts (
px.scatterwithrender_mode="webgl") handle millions of points on the client. For truly massive data, use Datashader to rasterize before passing to Plotly. - Can Dash be used with FastAPI?
- Yes. Mount the Dash app's Flask server as a sub-application:
from starlette.middleware.wsgi import WSGIMiddleware; api.mount("/dashboard", WSGIMiddleware(dash_app.server)). Alternatively, run Dash and FastAPI as separate services behind a reverse proxy like Nginx.