Matplotlib Axes.legend() in Python: a practical guide for real plots

I’ve lost count of how many times a plot looked “done” until the legend ruined it: labels overlapped data, duplicate entries ballooned into a paragraph, or a last-minute style change made the legend unreadable in a report. Legends feel simple when you have two lines, but real plots often have multiple artists (lines, scatters, bars, fills), multiple axes, and multiple audiences (you now, your teammate later, your future self in six months).

Here’s the good news: matplotlib.axes.Axes.legend() is predictable once you understand how it gathers “handles” (the things drawn) and “labels” (the text you see), and how placement and layout interact with your axes geometry.

I’m going to walk you through how ax.legend() decides what to show, when you should pass handles/labels explicitly, how I place legends without hiding data, and a few patterns I rely on in production charts: multiple legends on one axes, legends for scatter size/color, and sanity checks that keep exported figures consistent. Every example is runnable as-is.

What Axes.legend() really does (and what it returns)

When you call ax.legend(), Matplotlib builds a matplotlib.legend.Legend object and attaches it to that specific Axes. That distinction matters: legends belong somewhere, and Axes.legend() makes the legend part of one axes’ layout and draw cycle.

A few practical implications I keep in mind:

  • ax.legend() is axes-scoped. If you have subplots, each axes can have its own legend. If you want a single legend shared across subplots, you’ll usually reach for fig.legend(...) instead.
  • The legend is built from artists. Lines (Line2D), patches (Patch), collections (PathCollection from scatter), containers (bars), and more can become legend entries.
  • You get a real object back. The return value is a Legend instance, so you can modify it after creation (font sizes, alignment, frame style, etc.).

Here’s the smallest “real plot” example that shows the return value and a couple of common formatting tweaks:

import numpy as np

import matplotlib.pyplot as plt

x = np.linspace(0, 2 * np.pi, 400)

y1 = np.sin(x)

y2 = np.cos(x)

fig, ax = plt.subplots(figsize=(8, 4))

ax.plot(x, y1, label=‘sin(x)‘, linewidth=2)

ax.plot(x, y2, label=‘cos(x)‘, linewidth=2)

legend = ax.legend(loc=‘upper right‘, frameon=True, title=‘Signals‘)

legend.getframe().setalpha(0.9)

ax.set_xlabel(‘x (radians)‘)

ax.set_ylabel(‘value‘)

ax.grid(True, alpha=0.25)

fig.tight_layout()

plt.show()

I’m intentionally calling tight_layout() here because legend placement and layout management are connected. If you’re generating figures for docs/CI snapshots, consistent layout is half the battle.

The Legend object is your “post-creation” control panel

I rarely get the legend perfect on the first call. Because ax.legend() returns the legend object, I can do targeted tweaks without rewriting the call:

  • legend.set_title(‘...‘) (or pass title= initially)
  • legend.get_texts() to iterate over label text objects
  • legend.get_frame() to style the frame (alpha, edge color, line width)
  • legend.setbboxto_anchor(...) to adjust placement after inspecting a layout

A quick pattern I use when I want the legend to match the rest of the plot typography:

legend = ax.legend(loc=‘upper left‘, frameon=True)

for text in legend.get_texts():

text.set_fontsize(10)

legend.getframe().setlinewidth(0.8)

It’s not fancy, but it’s reliable.

How Matplotlib decides which labels appear

The “automatic” legend behavior is just a set of rules.

When you call ax.legend() with no arguments, Matplotlib effectively does:

  • Look at artists on the axes that support legend entries.
  • Read each artist’s label (artist.get_label()).
  • Ignore labels that start with an underscore (the default label for many artists is something like ‘_line0‘).
  • Use the remaining (handle, label) pairs to populate the legend.

That underscore rule explains a classic bug: you plotted something, but the legend is empty. In my experience, it’s nearly always one of these:

  • You never passed label=... to the plotting call.
  • You set a label but called ax.legend() before creating the artist.
  • You created artists with labels that start with _.

A simple debugging move I like is: print the handles and labels Matplotlib sees.

import matplotlib.pyplot as plt

fig, ax = plt.subplots()

ax.plot([0, 1, 2], [0, 1, 0], label=‘Model A‘)

ax.plot([0, 1, 2], [0, 0.8, 0.2], label=‘Model B‘)

handles, labels = ax.getlegendhandles_labels()

print(‘labels:‘, labels)

ax.legend()

plt.show()

If labels is empty or wrong, fix the labels at the source (the plot calls) unless you have a strong reason to override.

A deliberate “do not show this” label

Sometimes I want an artist on the axes but never in the legend (helper lines, thresholds, confidence bands that I’m explaining elsewhere). The simplest rule is still the underscore rule: give it a label that starts with _.

ax.axhline(0, color=‘black‘, linewidth=1, alpha=0.4, label=‘_baseline‘)

It’s explicit, stable, and it won’t surprise the next person reading the code.

Duplicate labels: friend in quick plots, enemy in production

Matplotlib does not automatically deduplicate labels for you in a “smart” way. If you plot in a loop and every series says label=‘train‘, you’ll get repeated entries.

When I want deduplication, I do it explicitly:

import matplotlib.pyplot as plt

fig, ax = plt.subplots()

for seed in range(3):

ax.plot([0, 1, 2], [0, 1 – 0.1 seed, 0.2 seed], label=‘train‘)

handles, labels = ax.getlegendhandles_labels()

dedup = dict(zip(labels, handles)) # keeps last handle for each label

ax.legend(dedup.values(), dedup.keys(), loc=‘best‘)

ax.set_title(‘Deduplicated legend entries‘)

plt.show()

This is intentionally straightforward. If you need stable ordering (and I often do), I use a “first-seen wins” pattern:

handles, labels = ax.getlegendhandles_labels()

seen = set()

handles_out = []

labels_out = []

for h, l in zip(handles, labels):

if l in seen:

continue

seen.add(l)

handles_out.append(h)

labels_out.append(l)

ax.legend(handlesout, labelsout)

That keeps ordering deterministic even if later artists reuse the same label.

Ordering matters more than people think

Even when labels are correct, the legend can read “wrong” if the ordering doesn’t match the story you’re telling (baseline first, then experiment; worst-case last; “Other” at the end). The most direct fix is: explicitly pass handles in the order you want.

handles, labels = ax.getlegendhandles_labels()

order = [labels.index(‘baseline‘), labels.index(‘experiment‘)]

ax.legend([handles[i] for i in order], [labels[i] for i in order])

It’s not glamorous, but it’s the difference between a legend that explains and a legend that distracts.

Handles vs labels: when I take manual control

Axes.legend() accepts positional args and keyword args, but the core control is always the same: you can provide handles, labels, or both.

  • handles: list of artists (lines, patches, collections) to show.
  • labels: list of strings to display.

If you provide only handles, Matplotlib uses each handle’s current label. If you provide only labels, Matplotlib tries to match them to existing handles (easy to get wrong in a busy axes). In practice, I either:

1) Let Matplotlib auto-detect from artists, or

2) Pass both handles and labels explicitly.

Proxy artists: legends for things that aren’t a single artist

A legend entry can represent a visual concept even if it doesn’t map to one plotted object. The pattern is: make a “proxy” artist (often a Patch), then pass it as a handle.

import numpy as np

import matplotlib.pyplot as plt

from matplotlib.patches import Patch

x = np.linspace(0, 10, 200)

y = np.sin(x)

fig, ax = plt.subplots(figsize=(8, 4))

line, = ax.plot(x, y, color=‘tab:blue‘, label=‘signal‘)

ax.fill_between(x, y – 0.2, y + 0.2, color=‘tab:blue‘, alpha=0.15)

band_proxy = Patch(facecolor=‘tab:blue‘, alpha=0.15, edgecolor=‘none‘)

ax.legend(

handles=[line, band_proxy],

labels=[‘signal‘, ‘±0.2 band‘],

loc=‘upper right‘,

frameon=True,

)

ax.set_title(‘Legend with a proxy artist‘)

ax.grid(True, alpha=0.25)

fig.tight_layout()

plt.show()

This is one of those “small” techniques that makes plots explain themselves without extra annotation text.

Placement and layout: keeping legends readable without hiding data

Legend placement looks like a styling problem, but it’s actually geometry.

loc and bboxtoanchor

  • loc chooses where the legend box is anchored relative to a reference box.
  • bboxtoanchor lets you define that reference box.

If you want the legend outside the axes (a common choice for dense charts), bboxtoanchor is your tool:

import numpy as np

import matplotlib.pyplot as plt

x = np.arange(1, 13)

spend = np.array([12, 11, 13, 14, 15, 16, 19, 18, 17, 16, 15, 14])

revenue = spend * 1.8 + np.array([2, -1, 0, 1, 2, 3, 2, 1, 0, -1, -2, -1])

fig, ax = plt.subplots(figsize=(9, 4))

ax.plot(x, spend, marker=‘o‘, label=‘ad spend‘)

ax.plot(x, revenue, marker=‘s‘, label=‘revenue‘)

ax.legend(

loc=‘center left‘,

bboxtoanchor=(1.02, 0.5),

borderaxespad=0.0,

frameon=True,

)

ax.set_xticks(x)

ax.set_xlabel(‘month‘)

ax.set_title(‘Legend outside the axes‘)

ax.grid(True, alpha=0.25)

fig.tight_layout()

plt.show()

I recommend this for presentations and dashboards where the legend must stay visible even if the plot area is tight.

The “savefig trap” when the legend is outside the axes

If you place the legend outside the axes and then save the figure, the legend can get cut off unless your layout includes it.

Two robust ways I handle this:

1) Use a layout manager that accounts for extra artists (more on this in a second).

2) Save with a tight bounding box:

fig.savefig(‘plot.png‘, dpi=200, bboxinches=‘tight‘, padinches=0.05)

If I’m generating assets for docs, I almost always use bbox_inches=‘tight‘ for safety when legends are outside.

tightlayout() vs constrainedlayout

In practice, I pick one strategy per project and stick to it.

  • fig.tight_layout() is explicit: you call it at the end, after you’ve created your legend/title/labels.
  • constrained_layout=True (passed at figure creation) is more automatic and often better at balancing multi-axes figures.

A good “set it and forget it” pattern for multi-panel figures is:

fig, axs = plt.subplots(2, 2, figsize=(10, 6), constrained_layout=True)

Then keep legend calls inside each axes. If I’m doing an outside-axes legend with bboxtoanchor, I still sanity-check exports, because layout interactions can vary depending on how dense the text is.

Layout knobs I actually use

These are the legend kwargs that show up in my own code reviews:

  • ncol: multi-column legends for many entries.
  • fontsize: keep it consistent with the rest of the figure.
  • title and title_fontsize: legends read better with a short title.
  • frameon, framealpha, facecolor, edgecolor: improve contrast.
  • handlelength, handletextpad, columnspacing, labelspacing: spacing fixes when things look cramped.

If your legend overlaps data and you’re tempted to “just move it until it fits,” consider whether the legend belongs outside the axes or whether the plot needs fewer legend entries.

Legends for bars, histograms, and “container” artists

Line plots make legends feel easy because each call returns a single Line2D. Bars and histograms are different: you often get a container of patches.

Bar charts: one label per series

For grouped bars, I generally label each series once:

import numpy as np

import matplotlib.pyplot as plt

x = np.arange(4)

a = np.array([5, 7, 6, 8])

b = np.array([6, 6, 7, 9])

fig, ax = plt.subplots(figsize=(8, 4))

w = 0.35

ax.bar(x – w/2, a, width=w, label=‘Plan‘, color=‘tab:blue‘, alpha=0.85)

ax.bar(x + w/2, b, width=w, label=‘Actual‘, color=‘tab:orange‘, alpha=0.85)

ax.set_xticks(x)

ax.set_xticklabels([‘Q1‘, ‘Q2‘, ‘Q3‘, ‘Q4‘])

ax.legend(loc=‘upper left‘, frameon=True)

ax.set_title(‘Grouped bars with a simple legend‘)

ax.grid(axis=‘y‘, alpha=0.25)

fig.tight_layout()

plt.show()

The legend stays compact because it explains series identity, not each individual bar.

Stacked bars: legend as “composition map”

Stacked bars are a case where the legend usually represents parts of a whole (categories). The trick is to label each stacked layer, not each bar:

import numpy as np

import matplotlib.pyplot as plt

x = np.arange(3)

cats = {

‘Search‘: np.array([30, 28, 35]),

‘Social‘: np.array([12, 15, 10]),

‘Email‘: np.array([8, 6, 7]),

}

fig, ax = plt.subplots(figsize=(8, 4))

bottom = np.zeros_like(x, dtype=float)

colors = {‘Search‘: ‘tab:blue‘, ‘Social‘: ‘tab:green‘, ‘Email‘: ‘tab:purple‘}

for name, values in cats.items():

ax.bar(x, values, bottom=bottom, label=name, color=colors[name], alpha=0.9)

bottom = bottom + values

ax.set_xticks(x)

ax.set_xticklabels([‘Week 1‘, ‘Week 2‘, ‘Week 3‘])

ax.legend(title=‘Channel‘, loc=‘upper left‘, ncol=3, frameon=True)

ax.set_title(‘Stacked bars: legend explains composition‘)

ax.grid(axis=‘y‘, alpha=0.25)

fig.tight_layout()

plt.show()

Here I’m using ncol=3 because stacked-bar legends can get wide quickly.

Histograms: avoid accidental “double meaning”

A histogram legend can mislead if the plot has both filled areas and outlines. If the audience is likely to interpret the legend marker as “a line,” I sometimes use proxy patches so the legend matches the filled style.

Multiple legends on the same Axes (the pattern Matplotlib expects)

By default, an axes has one legend slot. If you call ax.legend() twice, the second call replaces the first.

When I need two legends (for example: one legend for line identity and another for style meaning), I create the first legend, add it back as an artist, then create the second.

import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(8, 4))

line_a, = ax.plot([1, 2, 3], [1, 2, 2], label=‘Service A‘, color=‘black‘, linewidth=3, linestyle=‘:‘)

line_b, = ax.plot([1, 2, 3], [2, 1.5, 1], label=‘Service B‘, color=‘tab:green‘, linewidth=3)

legendservices = ax.legend(handles=[linea, line_b], loc=‘upper center‘, ncol=2, frameon=True)

ax.addartist(legendservices)

# A second legend that explains style (proxy handles)

style_current, = ax.plot([], [], color=‘tab:blue‘, linewidth=3, label=‘current‘)

style_baseline, = ax.plot([], [], color=‘tab:blue‘, linewidth=3, linestyle=‘–‘, label=‘baseline‘)

ax.legend(handles=[stylecurrent, stylebaseline], loc=‘lower center‘, ncol=2, frameon=True)

ax.set_title(‘Two legends on one axes‘)

ax.grid(True, alpha=0.25)

fig.tight_layout()

plt.show()

Two notes:

  • I used empty plot calls (ax.plot([], [])) as proxy handles for styles. That’s a legit technique when you want a handle that looks like a line but doesn’t correspond to real data.
  • If you find yourself doing this often, consider whether a caption or annotation would communicate better than a second legend.

Scatter plots: legends for categories, and legends for sizes

Scatter plots are where legends can get confusing fast, because a single PathCollection can represent multiple points with varying color and size.

Category legend (multiple scatter calls)

A simple approach is to call ax.scatter(...) separately per category with label=.... That’s still my default for category plots because the legend is straightforward.

import numpy as np

import matplotlib.pyplot as plt

rng = np.random.default_rng(7)

fig, ax = plt.subplots(figsize=(8, 4))

for name, color in [(‘A‘, ‘tab:green‘), (‘B‘, ‘tab:blue‘), (‘C‘, ‘tab:orange‘)]:

n = 60

x = rng.random(n)

y = rng.random(n)

sizes = 400 * rng.random(n) + 40

ax.scatter(x, y, s=sizes, c=color, alpha=0.35, label=f‘group {name}‘)

ax.legend(loc=‘upper right‘, frameon=True, title=‘category‘)

ax.grid(True, alpha=0.25)

ax.set_title(‘Scatter category legend‘)

fig.tight_layout()

plt.show()

Size legend (one scatter call)

When size encodes a value (requests, latency, confidence), I like to add a separate legend for size. Matplotlib can generate size handles via legend_elements().

import numpy as np

import matplotlib.pyplot as plt

rng = np.random.default_rng(42)

x = rng.random(120)

y = rng.random(120)

throughput = rng.uniform(50, 500, size=120)

fig, ax = plt.subplots(figsize=(8, 4))

sc = ax.scatter(x, y, s=throughput, c=‘tab:blue‘, alpha=0.35)

sizehandles, sizelabels = sc.legend_elements(prop=‘sizes‘, num=4, alpha=0.35)

legendsizes = ax.legend(sizehandles, size_labels, loc=‘upper left‘, title=‘throughput‘)

ax.addartist(legendsizes)

ax.legend([sc], [‘datapoints‘], loc=‘upper right‘, frameon=True)

ax.grid(True, alpha=0.25)

ax.set_title(‘Separate size legend for scatter‘)

fig.tight_layout()

plt.show()

The key idea: don’t force a single legend to explain multiple encodings if it becomes hard to read. Two small legends often beat one overloaded block of text.

Color in scatter: when a legend is the wrong tool

If scatter color encodes a continuous variable (a gradient), a colorbar is usually clearer than a legend. I treat it as a rule of thumb:

  • Discrete groups → legend.
  • Continuous scale → colorbar.

Example:

import numpy as np

import matplotlib.pyplot as plt

rng = np.random.default_rng(0)

x = rng.normal(size=300)

y = rng.normal(size=300)

z = np.sqrt(x2 + y2) # continuous magnitude

fig, ax = plt.subplots(figsize=(8, 4))

sc = ax.scatter(x, y, c=z, cmap=‘viridis‘, s=40, alpha=0.8)

cbar = fig.colorbar(sc, ax=ax)

cbar.set_label(‘magnitude‘)

ax.set_title(‘Continuous color → colorbar, not legend‘)

ax.grid(True, alpha=0.25)

fig.tight_layout()

plt.show()

Trying to cram a continuous scale into a legend usually produces labels that are either too many (unreadable) or too few (misleading).

Twin axes and shared legends (the explicit pattern)

If you use ax.twinx(), each axes has its own artists. Calling ax.legend() only captures that axes’ handles. If you want one combined legend, gather handles from both axes and pass them explicitly.

import numpy as np

import matplotlib.pyplot as plt

x = np.arange(1, 8)

requests = np.array([120, 150, 180, 160, 200, 240, 220])

latency_ms = np.array([80, 75, 90, 85, 95, 110, 105])

fig, ax1 = plt.subplots(figsize=(8, 4))

ax2 = ax1.twinx()

line_req, = ax1.plot(x, requests, color=‘tab:blue‘, marker=‘o‘, label=‘requests‘)

linelat, = ax2.plot(x, latencyms, color=‘tab:red‘, marker=‘s‘, label=‘p95 latency (ms)‘)

handles = [linereq, linelat]

labels = [h.get_label() for h in handles]

ax1.legend(handles, labels, loc=‘upper left‘, frameon=True)

ax1.set_xlabel(‘day‘)

ax1.set_ylabel(‘requests‘)

ax2.set_ylabel(‘ms‘)

ax1.grid(True, alpha=0.25)

ax1.set_title(‘Combined legend across twin axes‘)

fig.tight_layout()

plt.show()

That pattern is simple, explicit, and avoids surprises.

Advanced control: making legend entries match what people perceive

When the legend “feels wrong,” it’s usually because the legend marker doesn’t communicate the same thing as the plot. A few examples:

  • The plot uses thick lines but the legend sample looks thin.
  • The plot uses semi-transparent fills but the legend patch looks too faint.
  • The scatter sizes vary but the legend sizes don’t reflect the scale you intended.

My go-to legend readability tweaks

These are small, but they add up:

  • Use a title when the legend encodes meaning, not just names (title=‘Channel‘, title=‘Model‘).
  • Control marker scaling when points are large/small (markerscale=...).
  • Increase spacing when text is tight (labelspacing=..., handletextpad=...).
  • Improve contrast for print (frameon=True, framealpha=1.0, edgecolor=‘0.85‘).

Example with spacing and markerscale:

legend = ax.legend(

loc=‘upper left‘,

frameon=True,

markerscale=1.2,

labelspacing=0.6,

handletextpad=0.8,

borderpad=0.6,

)

If you’re producing PDFs for reports, these “micro” adjustments often matter more than changing the colormap.

Common mistakes, edge cases, and performance notes

This is the checklist I run through when a legend looks “off.”

Mistake 1: calling legend() too early

If you call ax.legend() before plotting artists, you’ll get an empty legend or a partial one. In scripts that build plots in steps, I put ax.legend(...) near the end of the plotting function.

Mistake 2: mixing ax.legend() and fig.legend() without intent

  • Use ax.legend() when the legend describes artists on one axes.
  • Use fig.legend() when you want a shared legend across subplots.

If you use both, be explicit about what each represents.

Mistake 3: too many legend entries

Legends don’t scale well to dozens of entries. If you have 30 lines:

  • Consider grouping (one handle per group).
  • Consider interactive tooling (hover tooltips) if you’re not targeting static images.
  • Consider labeling lines directly at their endpoints if the chart is meant for print.

From a runtime perspective, legend creation is usually fast for a handful of items, but it can become noticeable when you have many entries with complex markers and fonts. For typical reports, you might not care; for dashboards that redraw frequently, you should keep legends small and stable.

Mistake 4: loc=‘best‘ in heavy plots

loc=‘best‘ is convenient because it tries to avoid overlapping data, but that “try” can do extra work. If you’re rendering many figures in a pipeline or redrawing frequently, choosing an explicit location (‘upper right‘, ‘center left‘, etc.) keeps things more predictable.

Mistake 5: legend readability in exports

If you export to SVG/PDF for docs, small font differences show up more than on-screen. I recommend setting a consistent font size and using tightlayout() or constrainedlayout=True (and then sticking with one approach across your project).

Mistake 6: label text that’s too long

If your labels read like sentences, the legend will dominate the figure. I treat long labels as a smell:

  • Move context into the title/subtitle or caption.
  • Use short labels and define acronyms once.
  • Split legend entries into columns (ncol=2 or ncol=3).

Mistake 7: “legend says one thing, plot says another”

This happens when your plot uses multiple encodings (color + linestyle + marker) but the legend only communicates one of them. The fix is usually either:

  • Two legends (identity vs meaning), or
  • A legend plus a note/annotation, or
  • Simplify encodings (my favorite option when possible).

A small helper I actually reuse: stable dedupe + optional ordering

If you’re plotting in loops, you’ll eventually write legend cleanup code. Here’s a helper that stays readable and doesn’t require any special dependencies:

def legenddedupe(ax, , loc=‘best‘, title=None, order=None, *legendkwargs):

handles, labels = ax.getlegendhandles_labels()

seen = set()

handles_out = []

labels_out = []

for h, l in zip(handles, labels):

if not l or l.startswith(‘_‘):

continue

if l in seen:

continue

seen.add(l)

handles_out.append(h)

labels_out.append(l)

if order is not None:

index = {l: i for i, l in enumerate(labels_out)}

handlesout = [handlesout[index[l]] for l in order if l in index]

labels_out = [l for l in order if l in index]

return ax.legend(handlesout, labelsout, loc=loc, title=title, legend_kwargs)

I like that it doesn’t “hide” Matplotlib behavior—everything is built on top of getlegendhandles_labels().

How I keep legend work “modern” in 2026

Matplotlib hasn’t stopped being relevant, but the way we ship plots has changed. In teams, plots are code, and you want them to be reproducible and reviewable.

Here’s what I recommend if you’re building plots that matter (reports, research, product metrics):

Concern

Traditional approach

Modern approach (what I do) —

— Reproducibility

Run a notebook manually

Put plotting in functions; pin dependencies; seed randomness Visual regressions

Someone eyeballs images

Snapshot tests (for example with a Matplotlib image-compare test) in CI Style consistency

Copy/paste rcParams

Centralize style in one module; enforce via code review Legend placement

Manual nudging per plot

Standardize loc + outside-legend patterns; save with tight bounding boxes Accessibility

“Looks fine to me”

Use colorblind-safe palettes; don’t rely on color alone; increase font/contrast

My “production defaults” for legends

When I’m writing plotting utilities for a team, I prefer defaults that prevent common failure modes:

  • Legends don’t cover data by default (outside placement for dense charts).
  • Legend text is consistent (font size tied to global style).
  • Exported figures never clip legend text (bbox_inches=‘tight‘ when needed).
  • Ordering is deterministic (explicit ordering or stable dedupe).

If you want one practical takeaway: treat legend code as part of the visualization logic, not as decoration. It’s where you encode the meaning of the marks.

Quick decision guide: when to use ax.legend() vs alternatives

I keep this mental model:

  • Use ax.legend() when the legend explains artists on one axes.
  • Use fig.legend() when you need one legend for multiple subplots.
  • Use a colorbar when color encodes a continuous variable.
  • Use direct labels (text near lines) when there are too many legend entries.
  • Use a caption/annotation when the legend is becoming a paragraph.

If you’re unsure, ask: “Will a reader understand the plot without me talking over it?” The legend is usually where that question gets answered.

Expansion Strategy

Add new sections or deepen existing ones with:

  • Deeper code examples: More complete, real-world implementations
  • Edge cases: What breaks and how to handle it
  • Practical scenarios: When to use vs when NOT to use
  • Performance considerations: Before/after comparisons (use ranges, not exact numbers)
  • Common pitfalls: Mistakes developers make and how to avoid them
  • Alternative approaches: Different ways to solve the same problem

If Relevant to Topic

  • Modern tooling and AI-assisted workflows (for infrastructure/framework topics)
  • Comparison tables for Traditional vs Modern approaches
  • Production considerations: deployment, monitoring, scaling

If you want, I can also add a dedicated section on fig.legend(...) for shared legends across subplots (including how to collect handles from multiple axes cleanly) while keeping the same tone and structure.

Scroll to Top