> Chapter goal: Move beyond basic matplotlib charts into two professional-grade visualization libraries — seaborn for polished statistical graphics and plotly for fully interactive charts that executives can explore in a browser.
In This Chapter
- Where You Are in the Story
- 15.1 seaborn: Statistical Visualization on Top of matplotlib
- 15.2 seaborn Key Plot Types
- 15.3 plotly: Interactive Charts for the Modern Business
- 15.4 Interactive Features: Making Charts Work for the Audience
- 15.5 Combining Multiple plotly Traces
- 15.6 Saving plotly Charts
- 15.7 Building a Multi-Chart Dashboard with plotly Subplots
- 15.8 When to Use seaborn vs plotly vs matplotlib
- 15.9 Business Context: Executive Dashboards That Travel
- Summary
Chapter 15: Advanced Charts and Dashboards with seaborn and plotly
Chapter goal: Move beyond basic matplotlib charts into two professional-grade visualization libraries — seaborn for polished statistical graphics and plotly for fully interactive charts that executives can explore in a browser.
Where You Are in the Story
Priya has been making charts with matplotlib since Chapter 14. They're accurate, but when she emailed the regional breakdown to Sandra Chen, Sandra's first question was: "Can I click on the bars to see the underlying numbers?"
Matplotlib can't do that. Neither can a static PNG.
This chapter introduces two tools that change the equation. seaborn produces beautiful, statistically-informed charts with far less code than raw matplotlib. plotly produces charts that live in a browser — zoomable, hoverable, filterable — and can be saved as a single HTML file that anyone can open without installing Python.
Maya Reyes, building her consulting practice, has the same need from a different angle: she wants a clean heatmap showing which clients generate revenue in which months, and she wants a scatter plot she can share with her business coach.
Both of them will have working solutions by the end of this chapter.
15.1 seaborn: Statistical Visualization on Top of matplotlib
seaborn is a Python library built directly on top of matplotlib. It does not replace matplotlib — it extends it. Every seaborn plot is, underneath, a matplotlib figure. This means everything you learned in Chapter 14 still applies: plt.title(), plt.savefig(), plt.show(), figure sizes, all of it.
What seaborn adds:
- Higher-level plot types — one function call produces what would take 15 lines of matplotlib
- Built-in statistical summaries — bar plots show means with confidence intervals automatically
- Attractive default themes — seaborn's default look is publication-ready without customization
- Tight pandas integration — pass a DataFrame and column names; seaborn handles the rest
Install seaborn if you haven't already:
pip install seaborn
The standard import convention:
import seaborn as sns
import matplotlib.pyplot as plt
The sns alias is universal. You'll never see seaborn imported any other way in professional code.
15.1.1 Loading seaborn and Setting a Theme
seaborn ships with several built-in themes. Before you draw any chart, set the theme:
import seaborn as sns
import matplotlib.pyplot as plt
# Set a clean, professional theme
sns.set_theme(style="whitegrid", palette="muted")
The style parameter controls the background:
| Style | Appearance | Best Use |
|---|---|---|
"whitegrid" |
White background, horizontal grid lines | General business reports |
"white" |
White background, no grid | Clean presentations |
"darkgrid" |
Gray background, grid | Dense data, many series |
"dark" |
Gray background, no grid | High-contrast situations |
"ticks" |
White background, tick marks only | Academic / publication |
For business presentations, "whitegrid" is the default choice. It resembles what executives see in Excel and feels immediately familiar.
15.1.2 seaborn Color Palettes
Color choices communicate professionalism. seaborn provides named palettes that are perceptually uniform (meaning the visual difference between colors matches the data difference):
# View available palettes
sns.color_palette("muted") # Desaturated, professional
sns.color_palette("deep") # Standard, slightly saturated
sns.color_palette("pastel") # Light, presentation-friendly
sns.color_palette("colorblind") # Accessible to color-blind viewers
sns.color_palette("Blues") # Sequential, single hue — for magnitude
sns.color_palette("RdYlGn") # Diverging — for deviation from midpoint
A business rule of thumb: Use sequential palettes (single color, varying intensity) for data that goes from low to high with no natural midpoint — total sales, headcount, revenue. Use diverging palettes (two colors from a center) when data deviates from a benchmark — profit vs. loss, performance vs. target. Use categorical palettes (distinct colors) for grouping — regions, product lines, sales reps.
To set a global palette for all charts in a session:
sns.set_palette("muted")
Or set it per chart:
sns.barplot(data=sales_df, x="region", y="revenue", palette="Blues_d")
15.2 seaborn Key Plot Types
15.2.1 sns.barplot — Means with Confidence Intervals
sns.barplot is not simply a bar chart. It computes the mean of your y-variable for each x-category and draws confidence interval bars automatically. This makes it a genuine statistical visualization.
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
# Sample Acme sales data
sales_data = {
"region": ["Northeast", "Southeast", "Midwest", "West"] * 3,
"quarter": ["Q1", "Q1", "Q1", "Q1", "Q2", "Q2", "Q2", "Q2", "Q3", "Q3", "Q3", "Q3"],
"revenue": [
142000, 98000, 115000, 187000,
155000, 112000, 128000, 201000,
163000, 105000, 134000, 215000,
],
}
sales_df = pd.DataFrame(sales_data)
sns.set_theme(style="whitegrid", palette="muted")
fig, ax = plt.subplots(figsize=(10, 6))
sns.barplot(
data=sales_df,
x="region",
y="revenue",
hue="quarter", # Groups bars by quarter, with automatic legend
ax=ax,
)
ax.set_title("Quarterly Revenue by Region", fontsize=14, fontweight="bold")
ax.set_xlabel("Region")
ax.set_ylabel("Revenue ($)")
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda val, _: f"${val:,.0f}"))
plt.tight_layout()
plt.savefig("regional_revenue_barplot.png", dpi=150)
plt.show()
The hue parameter is seaborn's mechanism for adding a categorical dimension. It automatically creates grouped bars and a legend — something that takes considerable effort in raw matplotlib.
When the confidence intervals confuse stakeholders: Set ci=None (older seaborn versions) or errorbar=None (seaborn 0.12+) to suppress them:
sns.barplot(data=sales_df, x="region", y="revenue", errorbar=None)
15.2.2 sns.lineplot — Trends Over Time
sns.lineplot is the go-to for time-series data. It handles both single series and multiple series gracefully, and like barplot, it can display confidence bands when you have repeated measurements.
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
monthly_data = {
"month": list(range(1, 13)) * 2,
"revenue": [
142000, 158000, 145000, 172000, 185000, 191000,
178000, 195000, 212000, 198000, 221000, 245000, # 2023
128000, 141000, 135000, 158000, 162000, 174000,
168000, 179000, 195000, 182000, 203000, 228000, # 2022
],
"year": ["2023"] * 12 + ["2022"] * 12,
}
monthly_df = pd.DataFrame(monthly_data)
fig, ax = plt.subplots(figsize=(12, 5))
sns.lineplot(
data=monthly_df,
x="month",
y="revenue",
hue="year",
marker="o", # Show dots at each data point
linewidth=2,
ax=ax,
)
ax.set_title("Monthly Revenue: 2022 vs 2023", fontsize=14, fontweight="bold")
ax.set_xlabel("Month")
ax.set_ylabel("Revenue ($)")
ax.set_xticks(range(1, 13))
ax.set_xticklabels(
["Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
)
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda v, _: f"${v:,.0f}"))
plt.tight_layout()
plt.savefig("monthly_revenue_trend.png", dpi=150)
plt.show()
15.2.3 sns.scatterplot — Relationships Between Variables
Scatter plots reveal correlations, clusters, and outliers. seaborn's scatter plot goes beyond matplotlib's by letting you encode additional dimensions through color, size, and shape:
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
# Simulate order-level data
rng = np.random.default_rng(42)
n = 200
orders_df = pd.DataFrame({
"order_value": rng.lognormal(mean=7.5, sigma=0.8, size=n),
"gross_margin_pct": rng.normal(loc=0.38, scale=0.09, size=n).clip(0.05, 0.75),
"category": rng.choice(
["Office Supplies", "Technology", "Furniture"], size=n, p=[0.5, 0.3, 0.2]
),
"days_to_ship": rng.integers(1, 8, size=n),
})
fig, ax = plt.subplots(figsize=(10, 7))
sns.scatterplot(
data=orders_df,
x="order_value",
y="gross_margin_pct",
hue="category", # Color encodes product category
size="days_to_ship", # Dot size encodes shipping time
sizes=(30, 200), # Min and max dot size
alpha=0.7, # Slight transparency for overlapping points
ax=ax,
)
ax.set_title("Order Value vs Gross Margin by Category", fontsize=14, fontweight="bold")
ax.set_xlabel("Order Value ($)")
ax.set_ylabel("Gross Margin %")
ax.xaxis.set_major_formatter(plt.FuncFormatter(lambda v, _: f"${v:,.0f}"))
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda v, _: f"{v:.0%}"))
plt.tight_layout()
plt.savefig("order_margin_scatter.png", dpi=150)
plt.show()
15.2.4 sns.heatmap — Two-Dimensional Matrices
A heatmap encodes a matrix of numbers as colors. This is ideal for showing data across two categorical axes — for example, revenue by region and month, or response rate by email campaign and day of week.
sns.heatmap requires a pivot table as input — a 2D DataFrame where rows are one category, columns are another, and cell values are the metric.
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
# Build a region × month revenue matrix
regions = ["Northeast", "Southeast", "Midwest", "West"]
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
import numpy as np
rng = np.random.default_rng(7)
revenue_matrix = pd.DataFrame(
rng.integers(80_000, 250_000, size=(4, 12)),
index=regions,
columns=months,
)
fig, ax = plt.subplots(figsize=(14, 5))
sns.heatmap(
data=revenue_matrix,
annot=True, # Show numbers inside cells
fmt=",d", # Format as integer with commas
cmap="Blues", # Sequential blue palette
linewidths=0.5, # Thin lines between cells
linecolor="white",
cbar_kws={"label": "Revenue ($)"}, # Color bar label
ax=ax,
)
ax.set_title("Monthly Revenue by Region — 2023", fontsize=14, fontweight="bold")
ax.set_ylabel("Region")
ax.set_xlabel("Month")
plt.tight_layout()
plt.savefig("regional_revenue_heatmap.png", dpi=150)
plt.show()
Heatmaps are particularly powerful for quickly spotting seasonality (which months are consistently dark/light?) and geographic patterns (which region is consistently strongest?).
Choosing the right color map:
- "Blues" — sequential, good for all-positive values
- "RdYlGn" — diverging, good for values above/below a target (red=bad, green=good)
- "coolwarm" — diverging, symmetric around zero
- Add _r suffix to reverse any palette: "Blues_r" goes dark-to-light
15.2.5 sns.boxplot — Distributions and Outliers
A box plot (also called a box-and-whisker plot) shows the distribution of a numeric variable. The box spans the interquartile range (25th to 75th percentile), the center line is the median, and individual dots are outliers. This tells you far more than a mean alone.
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(12)
categories = ["Office Supplies", "Technology", "Furniture"]
n_per_cat = 150
order_values = np.concatenate([
rng.lognormal(mean=5.5, sigma=0.6, size=n_per_cat), # Office Supplies
rng.lognormal(mean=7.0, sigma=0.9, size=n_per_cat), # Technology
rng.lognormal(mean=7.8, sigma=0.7, size=n_per_cat), # Furniture
])
category_labels = np.repeat(categories, n_per_cat)
orders_df = pd.DataFrame({
"category": category_labels,
"order_value": order_values,
})
fig, ax = plt.subplots(figsize=(10, 6))
sns.boxplot(
data=orders_df,
x="category",
y="order_value",
palette="muted",
width=0.5,
flierprops={"marker": "o", "markersize": 4, "alpha": 0.5},
ax=ax,
)
ax.set_title("Order Value Distribution by Product Category", fontsize=14, fontweight="bold")
ax.set_xlabel("Category")
ax.set_ylabel("Order Value ($)")
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda v, _: f"${v:,.0f}"))
plt.tight_layout()
plt.savefig("order_value_boxplot.png", dpi=150)
plt.show()
Boxplots are underused in business contexts because many business people haven't been taught to read them. When you share a boxplot, include a brief note: "The box spans the middle 50% of orders; the line is the median; dots are outliers."
15.2.6 sns.pairplot — Multiple Variable Relationships at Once
sns.pairplot creates a grid of scatter plots showing the relationship between every pair of numeric columns in a DataFrame. It's useful for early exploration when you want to see what correlates with what.
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(42)
n = 120
metrics_df = pd.DataFrame({
"revenue": rng.lognormal(mean=12, sigma=0.5, size=n),
"gross_margin_pct": rng.normal(loc=0.38, scale=0.08, size=n).clip(0.1, 0.7),
"order_count": rng.integers(10, 200, size=n),
"avg_order_value": rng.lognormal(mean=7.5, sigma=0.4, size=n),
"region": rng.choice(["Northeast", "Southeast", "Midwest", "West"], size=n),
})
pair_grid = sns.pairplot(
data=metrics_df,
vars=["revenue", "gross_margin_pct", "order_count", "avg_order_value"],
hue="region",
diag_kind="kde", # Kernel density plot on diagonal instead of histogram
plot_kws={"alpha": 0.6, "s": 30},
)
pair_grid.figure.suptitle("Pairwise Metric Relationships by Region", y=1.02, fontsize=14)
pair_grid.figure.savefig("metrics_pairplot.png", dpi=120, bbox_inches="tight")
plt.show()
pairplot is best for internal exploration, not for sharing with executives. The output can be dense and requires explanation. Use it to find interesting relationships, then build focused single charts to communicate those relationships.
15.3 plotly: Interactive Charts for the Modern Business
plotly is a different kind of visualization library. Where matplotlib and seaborn produce static images (PNG, PDF, SVG), plotly produces interactive HTML-based charts. When you open a plotly chart:
- Hover over any data point to see its exact values
- Click legend items to show or hide series
- Drag to zoom into a region
- Double-click to reset the zoom
- Use built-in dropdowns and sliders to filter data dynamically
These are charts that live in a browser. You can save them as .html files and email them — the recipient opens the file in Chrome or Edge with no Python required.
Install the plotly package:
pip install plotly
plotly has two main interfaces:
- plotly express (
px) — High-level, one-function-call charts (recommended for most cases) - plotly graph objects (
go) — Low-level, more control, more code
This chapter focuses primarily on px because it maps cleanly to business use cases, with go introduced for subplots and custom layouts.
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
15.3.1 px.bar — Interactive Bar Charts
import pandas as pd
import plotly.express as px
sales_data = {
"region": ["Northeast", "Southeast", "Midwest", "West",
"Northeast", "Southeast", "Midwest", "West"],
"quarter": ["Q1", "Q1", "Q1", "Q1", "Q2", "Q2", "Q2", "Q2"],
"revenue": [142000, 98000, 115000, 187000, 155000, 112000, 128000, 201000],
}
sales_df = pd.DataFrame(sales_data)
fig = px.bar(
sales_df,
x="region",
y="revenue",
color="quarter",
barmode="group", # Side-by-side bars; use "stack" for stacked
title="Quarterly Revenue by Region",
labels={"revenue": "Revenue ($)", "region": "Region", "quarter": "Quarter"},
color_discrete_sequence=px.colors.qualitative.Set2,
text_auto="$.3s", # Show abbreviated $ values on bars (e.g., $142K)
)
fig.update_layout(
yaxis_tickformat="$,.0f",
plot_bgcolor="white",
paper_bgcolor="white",
font={"family": "Arial", "size": 12},
legend_title_text="Quarter",
)
# Save as interactive HTML
fig.write_html("regional_revenue_bar.html")
# Also save as static PNG (requires kaleido: pip install kaleido)
fig.write_image("regional_revenue_bar.png", width=900, height=500)
fig.show() # Opens in browser
The text_auto parameter with a format string is one of plotly's convenience features — it places readable labels directly on the bars without additional code.
15.3.2 px.line — Interactive Time Series
import pandas as pd
import plotly.express as px
monthly_df = pd.DataFrame({
"date": pd.date_range("2023-01-01", periods=12, freq="MS"),
"revenue": [142000, 158000, 145000, 172000, 185000, 191000,
178000, 195000, 212000, 198000, 221000, 245000],
"target": [150000] * 12,
})
# Melt to long format so both lines appear in a single px.line call
melted_df = monthly_df.melt(
id_vars="date",
value_vars=["revenue", "target"],
var_name="metric",
value_name="value",
)
fig = px.line(
melted_df,
x="date",
y="value",
color="metric",
title="Monthly Revenue vs Target — 2023",
labels={"value": "Amount ($)", "date": "Month", "metric": "Metric"},
markers=True,
)
# Style the target line as dashed
fig.for_each_trace(
lambda trace: trace.update(line={"dash": "dash"})
if trace.name == "target"
else ()
)
fig.update_layout(
yaxis_tickformat="$,.0f",
xaxis_tickformat="%b %Y",
hovermode="x unified", # Shows all series values on hover, aligned by x
plot_bgcolor="white",
)
fig.write_html("revenue_vs_target.html")
fig.show()
The hovermode="x unified" setting is worth memorizing. It makes the hover tooltip show all series at a given x position simultaneously, which is exactly what executives want when comparing revenue against a target.
15.3.3 px.scatter — Interactive Scatter Plots
import pandas as pd
import plotly.express as px
import numpy as np
rng = np.random.default_rng(42)
n = 200
orders_df = pd.DataFrame({
"order_value": rng.lognormal(mean=7.5, sigma=0.8, size=n),
"gross_margin_pct": rng.normal(loc=0.38, scale=0.09, size=n).clip(0.05, 0.75),
"category": rng.choice(["Office Supplies", "Technology", "Furniture"], n, p=[0.5, 0.3, 0.2]),
"order_id": [f"ORD-{i:04d}" for i in range(n)],
"rep_name": rng.choice(["Alice K.", "Bob L.", "Carol M.", "Dan N."], size=n),
})
fig = px.scatter(
orders_df,
x="order_value",
y="gross_margin_pct",
color="category",
hover_data=["order_id", "rep_name"], # Extra fields shown on hover
title="Order Value vs Gross Margin by Category",
labels={
"order_value": "Order Value ($)",
"gross_margin_pct": "Gross Margin %",
"category": "Category",
},
color_discrete_sequence=px.colors.qualitative.Set2,
opacity=0.7,
trendline="ols", # Adds ordinary-least-squares trend line per category
)
fig.update_layout(
xaxis_tickformat="$,.0f",
yaxis_tickformat=".0%",
plot_bgcolor="white",
)
fig.write_html("order_margin_scatter.html")
fig.show()
The trendline="ols" option deserves attention. plotly will calculate and draw a linear regression line for each category automatically. This requires statsmodels installed (pip install statsmodels), but the result is a fully annotated trend line with slope information on hover — something that would take significant effort in matplotlib.
15.3.4 px.pie and Why to Use Alternatives
Pie charts are simultaneously the most commonly requested and most frequently misused chart type in business. They work when you have 2–4 categories where the question is "what is the share?" and the differences between slices are obvious. They fail when:
- There are more than 5 categories (slices become too small to distinguish)
- The differences between categories are small (the eye cannot judge arc length accurately)
- You need to compare pies side by side (humans cannot compare angles across charts)
# A pie chart that works — only 4 clear categories
import plotly.express as px
region_data = {"region": ["Northeast", "Southeast", "Midwest", "West"],
"revenue": [842000, 573000, 621000, 1108000]}
region_df = pd.DataFrame(region_data)
fig = px.pie(
region_df,
names="region",
values="revenue",
title="Revenue Share by Region — 2023",
color_discrete_sequence=px.colors.qualitative.Set2,
)
fig.update_traces(textposition="inside", textinfo="percent+label")
fig.write_html("revenue_pie.html")
fig.show()
For most business cases, replace pie charts with:
- Bar chart — when you want to compare magnitudes
- Treemap — when you have hierarchical categories
- Sunburst — when hierarchy has multiple levels
15.3.5 px.sunburst and px.treemap — Hierarchical Data
When your data has a hierarchy — division → region → product line — two charts handle this elegantly.
A treemap uses nested rectangles where area represents magnitude:
import pandas as pd
import plotly.express as px
hierarchical_data = pd.DataFrame({
"division": ["B2B", "B2B", "B2B", "B2C", "B2C", "B2C",
"B2B", "B2B", "B2C", "B2C"],
"region": ["Northeast", "Midwest", "West", "Southeast", "West",
"Northeast", "Southeast", "West", "Midwest", "Northeast"],
"product": ["Supplies", "Technology", "Furniture", "Supplies", "Technology",
"Furniture", "Supplies", "Technology", "Furniture", "Supplies"],
"revenue": [185000, 142000, 98000, 73000, 215000,
67000, 121000, 163000, 89000, 54000],
})
fig = px.treemap(
hierarchical_data,
path=[px.Constant("Acme Corp"), "division", "region", "product"],
values="revenue",
title="Revenue Breakdown: Division > Region > Product",
color="revenue",
color_continuous_scale="Blues",
)
fig.update_layout(margin={"t": 50, "l": 25, "r": 25, "b": 25})
fig.write_html("revenue_treemap.html")
fig.show()
A sunburst uses nested rings:
fig = px.sunburst(
hierarchical_data,
path=["division", "region", "product"],
values="revenue",
title="Revenue Breakdown — Sunburst View",
color="revenue",
color_continuous_scale="Blues",
)
fig.write_html("revenue_sunburst.html")
fig.show()
Both charts are interactive: clicking a segment drills down into it. This makes them excellent for executive presentations where the presenter wants to explore the data live.
15.4 Interactive Features: Making Charts Work for the Audience
15.4.1 Hover Data
Every plotly chart supports hover tooltips. Customize what appears:
fig = px.bar(
sales_df,
x="region",
y="revenue",
hover_data={
"revenue": ":$,.0f", # Format revenue as dollars
"region": False, # Don't repeat the x-axis value
"quarter": True,
},
custom_data=["rep_count"], # Additional columns for hover templates
)
# Full custom hover template
fig.update_traces(
hovertemplate=(
"<b>%{x}</b><br>"
"Revenue: %{y:$,.0f}<br>"
"Reps: %{customdata[0]}"
"<extra></extra>" # Removes the trace name box
)
)
15.4.2 Dropdown Menus and Range Sliders
plotly supports native dropdown menus that let users switch between views without any server-side code:
import plotly.graph_objects as go
# Create a figure with a dropdown that switches between metrics
fig = go.Figure()
metrics = {"Revenue": revenue_series, "Orders": order_series, "Margin %": margin_series}
months = list(range(1, 13))
for metric_name, values in metrics.items():
fig.add_trace(
go.Scatter(
x=months,
y=values,
name=metric_name,
visible=(metric_name == "Revenue"), # Only show first trace initially
)
)
# Build the dropdown
buttons = []
for i, metric_name in enumerate(metrics):
visibility = [j == i for j in range(len(metrics))]
buttons.append({
"label": metric_name,
"method": "update",
"args": [{"visible": visibility}, {"title": f"Monthly {metric_name} — 2023"}],
})
fig.update_layout(
updatemenus=[{
"buttons": buttons,
"direction": "down",
"showactive": True,
"x": 0.1,
"y": 1.15,
}],
title="Monthly Revenue — 2023",
)
fig.write_html("metric_dropdown.html")
fig.show()
A range slider lets the user zoom into a time window without losing context:
fig.update_xaxes(
rangeslider_visible=True,
rangeselector={
"buttons": [
{"count": 3, "label": "3m", "step": "month", "stepmode": "backward"},
{"count": 6, "label": "6m", "step": "month", "stepmode": "backward"},
{"step": "all", "label": "Full Year"},
]
}
)
15.5 Combining Multiple plotly Traces
15.5.1 Adding Traces to a Figure
In plotly graph objects, a trace is a single data series. You build a figure by adding traces to it:
import plotly.graph_objects as go
fig = go.Figure()
# Add a bar chart trace
fig.add_trace(
go.Bar(
x=months,
y=revenue_values,
name="Revenue",
marker_color="#2196F3",
yaxis="y",
)
)
# Add a line chart trace on a second y-axis
fig.add_trace(
go.Scatter(
x=months,
y=margin_pct_values,
name="Gross Margin %",
line={"color": "#FF5722", "width": 2},
mode="lines+markers",
yaxis="y2", # References the second y-axis
)
)
fig.update_layout(
title="Revenue and Gross Margin — 2023",
yaxis={"title": "Revenue ($)", "tickformat": "$,.0f"},
yaxis2={
"title": "Gross Margin %",
"overlaying": "y",
"side": "right",
"tickformat": ".0%",
},
hovermode="x unified",
plot_bgcolor="white",
)
fig.show()
Dual-axis charts require care. When two metrics use the same x-axis but very different scales, a dual-axis chart is appropriate. But if both metrics are in the same units (dollars), use a single axis — dual axes are sometimes used misleadingly to make weak correlations look strong.
15.6 Saving plotly Charts
15.6.1 Interactive HTML
The default and most useful save format. The HTML file is completely self-contained — it includes all data and all JavaScript. The recipient needs only a browser:
fig.write_html(
"dashboard.html",
include_plotlyjs="cdn", # Links to CDN instead of embedding ~3MB JS
full_html=True,
config={"displayModeBar": True, "responsive": True},
)
Using include_plotlyjs="cdn" reduces file size dramatically (from ~3 MB to a few KB) but requires an internet connection to load the chart. Use include_plotlyjs=True (the default) for fully offline files.
15.6.2 Static PNG/PDF
Static images require the kaleido package:
pip install kaleido
fig.write_image("chart.png", width=1200, height=600, scale=2) # Retina quality
fig.write_image("chart.pdf")
fig.write_image("chart.svg") # Vector format, ideal for print
15.7 Building a Multi-Chart Dashboard with plotly Subplots
make_subplots arranges multiple charts in a grid layout within a single figure, and saves them as one HTML file:
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import pandas as pd
import numpy as np
# Create a 2×2 dashboard grid
fig = make_subplots(
rows=2,
cols=2,
subplot_titles=(
"Monthly Revenue Trend",
"Revenue by Region",
"Margin vs Order Value",
"Product Category Breakdown",
),
specs=[
[{"type": "scatter"}, {"type": "bar"}],
[{"type": "scatter"}, {"type": "pie"}],
],
)
# Top-left: revenue trend
fig.add_trace(
go.Scatter(x=months, y=revenue_values, mode="lines+markers", name="Revenue",
line={"color": "#2196F3", "width": 2}),
row=1, col=1,
)
# Top-right: regional bar
fig.add_trace(
go.Bar(x=regions, y=regional_revenue, name="By Region",
marker_color="#4CAF50"),
row=1, col=2,
)
# Bottom-left: scatter
fig.add_trace(
go.Scatter(x=order_values, y=margin_pcts, mode="markers",
name="Orders", marker={"opacity": 0.6, "color": "#FF9800"}),
row=2, col=1,
)
# Bottom-right: pie
fig.add_trace(
go.Pie(labels=categories, values=category_revenue, name="Products"),
row=2, col=2,
)
fig.update_layout(
title_text="Acme Corp Executive Dashboard — Q3 2023",
title_font={"size": 20},
height=700,
showlegend=False,
plot_bgcolor="white",
paper_bgcolor="#F8F9FA",
)
fig.write_html("acme_dashboard.html", include_plotlyjs="cdn")
fig.show()
This single file, when opened in a browser, gives Sandra Chen an interactive dashboard. She can hover over every data point, zoom into the trend, and share the file with her VP peers.
15.8 When to Use seaborn vs plotly vs matplotlib
The three libraries are complementary. Choosing between them is a matter of output format and audience:
| Criterion | matplotlib | seaborn | plotly |
|---|---|---|---|
| Primary output | PNG/PDF | PNG/PDF | HTML / PNG |
| Interactivity | None | None | Full |
| Code volume | High | Medium | Low (px) / High (go) |
| Statistical features | Manual | Built-in | Limited |
| Customization ceiling | Highest | High | High (go) |
| Best for | Custom print charts | Statistical analysis | Dashboards, sharing |
| Learning curve | Medium | Low | Low (px) |
Decision guide in plain English:
- You're including a chart in a PDF report or print document: use matplotlib or seaborn
- You're doing exploratory data analysis and want to understand distributions and correlations: use seaborn
- You're sharing a chart that stakeholders need to explore: use plotly (HTML)
- You need a polished single chart for a slide deck and will export as PNG: either seaborn or plotly (
write_image) - You need precise control over every visual element: matplotlib
- You're building a dashboard: plotly subplots, save as HTML
Many workflows combine all three. Use seaborn for initial exploration, plotly for the client-facing output, and matplotlib for anything that requires pixel-perfect print formatting.
15.9 Business Context: Executive Dashboards That Travel
The business case for plotly HTML dashboards is straightforward. Consider the alternative:
- Analyst builds charts in Excel or matplotlib
- Screenshots charts into a PowerPoint deck
- Emails the deck (5 MB, static)
- Recipient zooms into a blurry PNG to read small numbers
- Recipient emails back with questions about specific data points
- Analyst digs up the data, emails again
With a plotly HTML dashboard:
- Analyst builds dashboard in Python (once)
- Saves as
dashboard.html(~200 KB) - Emails the file
- Recipient hovers over any data point to see exact values
- Recipient filters by region using the legend
- No follow-up questions about specific numbers
The HTML file is re-generable from data. When next month's data comes in, run the script again — same file name, updated content. Schedule the script (Chapter 22) and the dashboard refreshes automatically.
Consideration for sensitive data: HTML files contain all the underlying data in JavaScript. If your dataset contains confidential information, send the HTML only to appropriate recipients, or use PNG export instead.
Summary
seaborn and plotly address different points in the business visualization workflow. seaborn excels at statistical charts for internal analysis — its automatic confidence intervals, built-in themes, and tight pandas integration make it the fastest path from a DataFrame to a publication-quality static chart. plotly excels at stakeholder communication — its interactive HTML output turns a data analysis into a self-service dashboard that travels via email and requires no Python to use.
The practical pattern for most business analysts: explore with seaborn, communicate with plotly.
In Chapter 16, you'll learn how to send data the other direction — from Python back into Excel — producing formatted workbooks that non-technical colleagues can work with in their native tool.
Next: Chapter 16 — Excel and CSV Integration: Python Meets Spreadsheets