Visualisation — matplotlib, seaborn, plotly, and altair

One good default per situation, and the sins that make charts unreadable.

📊 Module 5 6 min read Not started

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

  1. Use matplotlib's pyplot and object-oriented APIs.
  2. Use seaborn for fast statistical plots.
  3. Use plotly for interactive charts and dashboards.
  4. Style plots for reports / presentations.
  5. Save figures correctly (DPI, format, fonts).

1. Matplotlib in 60 seconds

python
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:

python
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:

python
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

python
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

python
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"] = 12

For consistent corporate / publication style, save your matplotlibrc and reuse.

Saving

python
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

python
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

python
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

python
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)

python
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

python
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

NeedTool
Quick scatter / line in a notebookmatplotlib or seaborn
Statistical / paneled explorationseaborn
Interactive in browser, dashboardsplotly
Publication-quality vectormatplotlib (PDF/SVG)
Geospatialplotly / folium / kepler.gl
Large data (>1M points)datashader, plotly with WebGL (scattergl)

5. Common patterns

Plot a pandas / polars DataFrame

python
# 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

python
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

python
import seaborn as sns
sns.heatmap(df.corr(numeric_only=True), annot=True, fmt=".2f", cmap="vlag", center=0)

Confusion matrix

python
from sklearn.metrics import ConfusionMatrixDisplay
ConfusionMatrixDisplay.from_predictions(y_true, y_pred, normalize="true", cmap="Blues")

Bar chart sorted descending

python
top = data.value_counts().nlargest(10)
top[::-1].plot.barh()                  # reverse for top-to-bottom largest

6. Styling tips

  • Always label axes and add a title — others (and future-you) need context.
  • Use colour-blind-safe palettes: viridis, cividis, inferno, magma for sequential; vlag / RdBu for diverging.
  • Avoid rainbow palettes (jet, hsv) — they distort.
  • For categorical hues, use tab10 / tab20 or seaborn's colorblind.
  • Increase font size for presentations (context="talk" in seaborn, or mpl.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 plotly with render_mode="webgl" / scattergl.
  • Use datashader + bokeh / holoviews for genuinely large data:
python
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

python
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)

  1. Make a line chart of a time series with two lines (raw + rolling mean). Use matplotlib OO API.
  2. Make a seaborn pairplot of the Iris or Penguins dataset, coloured by species.
  3. Plot a correlation heatmap with annotations.
  4. Make an interactive plotly scatter coloured by a category, with hover info.
  5. Save the same chart as PNG (300 DPI), PDF, and SVG.
  6. Style the matplotlib globally for a "report" look (font size, grid, no top/right spines).
  7. Bonus: build a Streamlit page that shows the daily-revenue chart and a date slider.

Common pitfalls

  1. Forgetting plt.show() in scripts (notebooks auto-show; scripts don't).
  2. Saving with default DPI (72) for print → blurry. Use 150-300.
  3. Forgetting bbox_inches="tight" → cropped labels.
  4. Using pyplot.gca() deep in code → relies on hidden state; use the OO API.
  5. Plotting 1M points with plt.scatter → multi-second render. Aggregate.
  6. Rainbow colormaps for sequential data.

Self-check

  1. pyplot vs OO API — when use which?
  2. Where does seaborn save you time over matplotlib?
  3. When use plotly over matplotlib?
  4. What's the right colormap for diverging data?
  5. How to embed plots in a web dashboard?

References

Sign in to save your progress and earn badges.