Visualisation — matplotlib, seaborn, plotly, and altair
One good default per situation, and the sins that make charts unreadable.
Why this matters
A 5-line chart is worth a thousand lines of text in a stand-up. Mastering matplotlib (the foundation), seaborn (statistical, beautiful defaults), and plotly (interactive, web-ready) lets you communicate quickly and explore datasets without leaving Python.
Learning objectives
- Use matplotlib's
pyplotand object-oriented APIs. - Use seaborn for fast statistical plots.
- Use plotly for interactive charts and dashboards.
- Style plots for reports / presentations.
- Save figures correctly (DPI, format, fonts).
1. Matplotlib in 60 seconds
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 2 * np.pi, 100)
plt.plot(x, np.sin(x), label="sin")
plt.plot(x, np.cos(x), label="cos")
plt.xlabel("x"); plt.ylabel("y")
plt.title("Trig functions")
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()Prefer the object-oriented API for anything non-trivial:
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(x, np.sin(x), label="sin")
ax.plot(x, np.cos(x), label="cos")
ax.set_xlabel("x"); ax.set_ylabel("y")
ax.set_title("Trig functions")
ax.legend(); ax.grid(alpha=0.3)
fig.tight_layout()
fig.savefig("trig.png", dpi=150)fig is the figure (canvas). ax is the axes (the plot inside). Multiple subplots:
fig, axes = plt.subplots(2, 2, figsize=(10, 8), sharex=True)
axes[0, 0].plot(x, np.sin(x))
axes[0, 1].plot(x, np.cos(x))
axes[1, 0].scatter(x, np.sin(x))
axes[1, 1].hist(np.random.randn(1000), bins=30)
for ax in axes.flat: ax.set_xlabel("x"); ax.set_ylabel("y")
fig.suptitle("Plots")
fig.tight_layout()Common plot types
ax.plot(x, y) # line
ax.scatter(x, y, c=color, s=size)
ax.bar(categories, heights)
ax.barh(categories, widths)
ax.hist(values, bins=30, density=True)
ax.boxplot(data)
ax.violinplot(data, showmedians=True)
ax.heatmap(...) # not built-in; see seaborn
ax.errorbar(x, y, yerr=err)
ax.fill_between(x, y_low, y_high, alpha=0.3)Styles
plt.style.use("seaborn-v0_8-whitegrid")
plt.style.use("dark_background")
plt.style.use("default")
import matplotlib as mpl
mpl.rcParams["figure.dpi"] = 100
mpl.rcParams["font.size"] = 12For consistent corporate / publication style, save your matplotlibrc and reuse.
Saving
fig.savefig("plot.png", dpi=300, bbox_inches="tight")
fig.savefig("plot.pdf")
fig.savefig("plot.svg")PNG for presentations (raster); PDF / SVG for publications (vector).
2. Seaborn — statistical defaults
import seaborn as sns
import pandas as pd
tips = sns.load_dataset("tips")
sns.set_theme(style="whitegrid", context="notebook")
# Categorical
sns.boxplot(data=tips, x="day", y="total_bill", hue="smoker")
sns.violinplot(data=tips, x="day", y="total_bill")
sns.stripplot(data=tips, x="day", y="total_bill", jitter=True)
sns.swarmplot(...)
# Bivariate
sns.scatterplot(data=tips, x="total_bill", y="tip", hue="time", size="size")
sns.lineplot(data=df, x="date", y="revenue", hue="region")
sns.regplot(data=tips, x="total_bill", y="tip") # with regression line
# Distribution
sns.histplot(tips.total_bill, kde=True, bins=30)
sns.kdeplot(data=tips, x="total_bill", hue="time")
sns.ecdfplot(tips.total_bill)
# Multi
sns.pairplot(tips, hue="time", diag_kind="kde")
sns.jointplot(data=tips, x="total_bill", y="tip", kind="hex")
sns.heatmap(corr_matrix, annot=True, cmap="vlag", center=0)sns.set_theme(context="paper" | "notebook" | "talk" | "poster") scales fonts / line widths.
Facets — small multiples
g = sns.FacetGrid(tips, col="time", row="smoker", height=3)
g.map(sns.scatterplot, "total_bill", "tip")
g.add_legend()
# or
sns.catplot(data=tips, x="day", y="total_bill", col="time", kind="box")
sns.relplot(data=tips, x="total_bill", y="tip", col="day", kind="scatter")For exploratory data analysis, FacetGrid / catplot / relplot are unbeatable.
3. Plotly — interactive + web-ready
import plotly.express as px
import plotly.graph_objects as go
# Express — quick
fig = px.scatter(tips, x="total_bill", y="tip", color="time", size="size",
hover_data=["day"], trendline="ols")
fig.show()
fig = px.line(daily, x="day", y="revenue", title="Daily revenue")
fig = px.bar(by_country, x="country", y="users", color="region")
fig = px.box(tips, x="day", y="total_bill")
fig = px.histogram(tips, x="total_bill", nbins=30, marginal="box")
# 3-D
fig = px.scatter_3d(df, x="x", y="y", z="z", color="cluster")
# Maps
fig = px.scatter_mapbox(df, lat="lat", lon="lon", color="value", zoom=10)Graph objects (low-level, full control)
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=y1, mode="lines+markers", name="A"))
fig.add_trace(go.Scatter(x=x, y=y2, mode="lines", name="B"))
fig.update_layout(title="...", xaxis_title="x", yaxis_title="y", template="plotly_white")
fig.show()Save / embed
fig.write_html("chart.html") # standalone interactive HTML
fig.write_image("chart.png") # needs kaleido (`uv add kaleido`)For dashboards / web apps: Streamlit, Dash, Gradio all integrate plotly natively.
4. When to use which
| Need | Tool |
|---|---|
| Quick scatter / line in a notebook | matplotlib or seaborn |
| Statistical / paneled exploration | seaborn |
| Interactive in browser, dashboards | plotly |
| Publication-quality vector | matplotlib (PDF/SVG) |
| Geospatial | plotly / folium / kepler.gl |
| Large data (>1M points) | datashader, plotly with WebGL (scattergl) |
5. Common patterns
Plot a pandas / polars DataFrame
# pandas built-in (delegates to matplotlib)
df.plot(x="day", y="revenue", figsize=(10, 4))
df.plot.scatter(x="x", y="y", c="z", colormap="viridis")
df.hist(bins=30, figsize=(10, 8))
df.boxplot(by="region", column="revenue")
# Polars → pandas for plotting (or use plotly direct)
df_pl.to_pandas().plot(...)Time series with rolling
ax = daily.revenue.plot(label="daily")
daily.revenue.rolling(7).mean().plot(ax=ax, label="7-day avg", linewidth=2)
ax.legend(); ax.set_title("Daily revenue")Heatmap of correlation
import seaborn as sns
sns.heatmap(df.corr(numeric_only=True), annot=True, fmt=".2f", cmap="vlag", center=0)Confusion matrix
from sklearn.metrics import ConfusionMatrixDisplay
ConfusionMatrixDisplay.from_predictions(y_true, y_pred, normalize="true", cmap="Blues")Bar chart sorted descending
top = data.value_counts().nlargest(10)
top[::-1].plot.barh() # reverse for top-to-bottom largest6. Styling tips
- Always label axes and add a title — others (and future-you) need context.
- Use colour-blind-safe palettes:
viridis,cividis,inferno,magmafor sequential;vlag/RdBufor diverging. - Avoid rainbow palettes (
jet,hsv) — they distort. - For categorical hues, use
tab10/tab20or seaborn'scolorblind. - Increase font size for presentations (
context="talk"in seaborn, ormpl.rcParams["font.size"] = 14). - Don't overload one chart. Multiple small charts beat one with 7 series.
7. Performance — million-point plots
Matplotlib slows past ~100k points. Options:
- Downsample.
- Aggregate (hexbin, 2D histogram).
- Use
plotlywithrender_mode="webgl"/scattergl. - Use datashader + bokeh / holoviews for genuinely large data:
import datashader as ds
import datashader.transfer_functions as tf
cvs = ds.Canvas(plot_width=400, plot_height=400)
agg = cvs.points(df, "x", "y")
img = tf.shade(agg)8. Worked example: end-of-week dashboard
import polars as pl
import seaborn as sns
import matplotlib.pyplot as plt
df = pl.read_parquet("daily_revenue.parquet").to_pandas()
df["dow"] = pd.to_datetime(df["day"]).dt.day_name()
sns.set_theme(style="whitegrid", context="talk")
fig, axes = plt.subplots(1, 2, figsize=(14, 4))
# Daily revenue with rolling avg
df.set_index("day")[["revenue", "revenue_7d"]].plot(ax=axes[0])
axes[0].set_title("Daily revenue (7-day avg)")
axes[0].set_ylabel("revenue ($)")
# Day-of-week boxplot
sns.boxplot(data=df, x="dow", y="revenue",
order=["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"],
ax=axes[1])
axes[1].set_title("Revenue by weekday")
axes[1].tick_params(axis="x", rotation=45)
fig.tight_layout()
fig.savefig("weekly_dashboard.png", dpi=150)Hands-on lab (1.5 hours)
- Make a line chart of a time series with two lines (raw + rolling mean). Use matplotlib OO API.
- Make a seaborn
pairplotof the Iris or Penguins dataset, coloured by species. - Plot a correlation heatmap with annotations.
- Make an interactive plotly scatter coloured by a category, with hover info.
- Save the same chart as PNG (300 DPI), PDF, and SVG.
- Style the matplotlib globally for a "report" look (font size, grid, no top/right spines).
- Bonus: build a Streamlit page that shows the daily-revenue chart and a date slider.
Common pitfalls
- Forgetting
plt.show()in scripts (notebooks auto-show; scripts don't). - Saving with default DPI (72) for print → blurry. Use 150-300.
- Forgetting
bbox_inches="tight"→ cropped labels. - Using
pyplot.gca()deep in code → relies on hidden state; use the OO API. - Plotting 1M points with
plt.scatter→ multi-second render. Aggregate. - Rainbow colormaps for sequential data.
Self-check
pyplotvs OO API — when use which?- Where does seaborn save you time over matplotlib?
- When use plotly over matplotlib?
- What's the right colormap for diverging data?
- How to embed plots in a web dashboard?
References
- Matplotlib docs: https://matplotlib.org/stable/contents.html.
- Seaborn docs: https://seaborn.pydata.org/.
- Plotly Python: https://plotly.com/python/.
- Fundamentals of Data Visualization, Claus Wilke (free online).
- Color Brewer 2: https://colorbrewer2.org/.
Sign in to save your progress and earn badges.