Adding and Customizing Legends in Pandas & Seaborn Plots

Data Manipulation Mar 18, 2026

Adding and Customizing Legends in Pandas & Seaborn Plots

A well-designed legend is essential for making your plots clear and professional. In 2026, properly customizing legends in Pandas and Seaborn helps viewers quickly understand what each line, bar, or marker represents, especially when layering multiple series or using the hue parameter.

TL;DR — Key Legend Techniques

  • Use label= when plotting multiple series
  • Use plt.legend() to control position, title, and style
  • Use hue in Seaborn for automatic legends
  • Place legends outside the plot when space is limited

1. Basic Legend with Pandas Plot

import pandas as pd
import matplotlib.pyplot as plt

# Create a small demo file so this example runs as-is
# (in your project, sales_data.csv is your real export)
import numpy as np
rng = np.random.default_rng(42)
n = 60
pd.DataFrame({
    "order_date": pd.date_range("2026-01-01", periods=n, freq="3D"),
    "customer_id": rng.integers(101, 121, n),
    "customer_name": rng.choice([" ana", "ben ", "chen", "dana"], n),
    "product_id": rng.integers(1, 9, n),
    "region": rng.choice(["North", "South", "East", "West"], n),
    "category": rng.choice(["Electronics", "Computers", "Books", "Home"], n),
    "payment_method": rng.choice(["card", "cash", "wallet"], n),
    "segment": rng.choice(["consumer", "business"], n),
    "quantity": rng.integers(1, 6, n),
    "amount": rng.uniform(50, 2500, n).round(2),
    "discount": rng.choice([0.0, 0.05, 0.1, 0.2], n),
}).assign(
    price=lambda d: (d["amount"] / d["quantity"]).round(2),
    profit=lambda d: (d["amount"] * rng.uniform(0.05, 0.3, n)).round(2),
).assign(
    ship_date=lambda d: d["order_date"] + pd.Timedelta(days=2),
    status=lambda d: np.where(d["amount"] > 2000, "priority", "standard"),
).to_csv("sales_data.csv", index=False)

df = pd.read_csv("sales_data.csv", parse_dates=["order_date"])
monthly = df.resample("ME", on="order_date").agg({"amount": "sum", "quantity": "sum"}).reset_index()

plt.figure(figsize=(11, 6))

plt.plot(monthly["order_date"], monthly["amount"], label="Total Sales", linewidth=2.5)
plt.plot(monthly["order_date"], monthly["quantity"], label="Total Quantity", linewidth=2.5)

plt.title("Monthly Sales and Quantity Trend")
plt.ylabel("Value")
plt.xlabel("Date")
plt.legend(title="Metrics", fontsize=10, title_fontsize=11)
plt.grid(True, alpha=0.3)
plt.show()

2. Professional Legend with Seaborn

import seaborn as sns

plt.figure(figsize=(12, 7))

sns.lineplot(
    data=df,
    x="order_date",
    y="amount",
    hue="region",
    linewidth=2.2,
    marker="o",
    markersize=5
)

plt.title("Sales Trends by Region")
plt.ylabel("Sales Amount")
plt.xlabel("Date")

# Customizing the legend
plt.legend(
    title="Region",
    title_fontsize=12,
    fontsize=10,
    loc="upper left",
    bbox_to_anchor=(1.02, 1),   # Place legend outside the plot
    frameon=True
)
plt.tight_layout()
plt.show()

3. Advanced Legend Customization

plt.figure(figsize=(10, 6))

sns.scatterplot(
    data=df,
    x="quantity",
    y="amount",
    hue="region",
    size="profit",
    alpha=0.7
)

plt.title("Quantity vs Amount by Region")
plt.legend(
    title="Region & Profit",
    bbox_to_anchor=(1.05, 1),
    loc='upper left',
    ncol=1,                    # Single column legend
    frameon=True,
    shadow=True
)
plt.tight_layout()
plt.show()

4. Best Practices for Legends in 2026

  • Always add a meaningful title to your legend
  • Use bbox_to_anchor=(1.02, 1) to place legends outside the plot when space is tight
  • Use fontsize and title_fontsize to control readability
  • Keep the number of legend items reasonable (ideally ≤ 8)
  • Use plt.tight_layout() after adjusting legend position
  • For Seaborn plots with hue, customize the legend after plotting

Making Legends Useful

A legend explains what each colour or line means, so it must use readable names and never hide data. Labels come from the label= argument, from DataFrame column names, or from the hue column in seaborn.

Control placement with loc="best" or move the legend outside the axes with bbox_to_anchor. Give it a title, and remove it when a single series is already explained by the chart title.

Worked Example: Placing a Legend Outside

Three product lines are plotted with a titled legend placed to the right of the chart.

import pandas as pd
import matplotlib.pyplot as plt

df = pd.DataFrame({
    "Books": [30, 35, 40, 38],
    "Home": [50, 48, 55, 60],
    "Computers": [20, 28, 26, 35],
}, index=["Q1", "Q2", "Q3", "Q4"])

fig, ax = plt.subplots(figsize=(8, 4))
df.plot(ax=ax, marker="o")
ax.legend(title="Category", loc="upper left", bbox_to_anchor=(1.02, 1))
ax.set_ylabel("Revenue (USD thousands)")
fig.tight_layout()
print([t.get_text() for t in ax.get_legend().get_texts()])

pandas uses the column names as legend labels automatically. bbox_to_anchor moves the box just outside the right edge, and tight_layout makes room for it.

Common Mistakes

  • Leaving default labels such as _child0.
  • Placing the legend over important data.
  • Adding a legend to a chart with one series.
  • Using cryptic column names as labels.

Quick Summary

  • Labels come from label=, column names or hue.
  • Use loc and bbox_to_anchor for placement.
  • Add a legend title for clarity.
  • Remove legends that add nothing.

Conclusion

A good legend can make or break the clarity of your visualization. In 2026, spending a little extra time customizing legends with proper titles, positioning, and styling significantly improves the professionalism and readability of your plots. Whether using Pandas .plot() or Seaborn, always ensure your legend clearly communicates what each element represents.

Next steps:

  • Review your existing plots and improve their legends by adding titles, adjusting position, and using bbox_to_anchor when needed

Related Tutorials

Share:
Last updated: March 2026
Built-in Functions in Python 2026 str() in Python 2026: String Conversion + Modern Formatting & Best Practices memoryview with JAX in Python 2026: Zero-Copy NumPy → JAX Array Interop + Efficient ML Examples memoryview with TensorFlow in Python 2026: Zero-Copy NumPy → Tensor Interop + GPU Pinning & ML Examples memoryview with TensorFlow in Python 2026: Zero-Copy NumPy → Tensor Interop + ML Examples memoryview with NumPy & PyTorch in Python 2026: Zero-Copy Views, Efficient Slicing & ML Interop Examples memoryview with NumPy in Python 2026: Zero-Copy Views, Efficient Slicing & Real ML Examples memoryview() in Python 2026: Zero-Copy Magic for Large Binary Data + Real Examples import() in Python 2026: How It Works, Security Risks & Modern Alternatives (importlib) zip() in Python 2026: How to Use It, strict=True Behavior & Real-World Examples vars() in Python 2026: Accessing Object Namespace + Modern Introspection Patterns type() in Python 2026: Dynamic Type Inspection & Object Creation + Modern Patterns tuple() in Python 2026: Immutable Sequences + Modern Patterns & Best Practices super() in Python 2026: Method Resolution & Modern Inheritance Patterns sum() in Python 2026: Summing Iterables + Modern Numeric Patterns & Best Practices staticmethod() in Python 2026: Static Methods, Utility Functions & Modern Best Practices sorted() in Python 2026: Sorting Iterables + Modern Patterns & Best Practices slice() in Python 2026: Creating Slice Objects + Modern Patterns & Best Practices setattr() in Python 2026: Dynamic Attribute Setting + Modern Patterns & Safety set() in Python 2026: Mutable Sets Creation + Modern Patterns & Best Practices