Python Charts

Python plotting and visualization demystified

How to Create a Slope Chart for Comparing Two Time Points

Build a slope chart in Matplotlib for comparing a value across two time points, including label decluttering and highlighting a category.

A slope chart plots two time points on the x-axis, one line per category running between them, with the category name and value labeled directly at each end rather than through a legend. The slope of each line is the whole point: a steep upward line is immediate visual shorthand for "this grew a lot," and lines crossing each other shows a change in rank between the two points, both of which a table of the same numbers would leave you to work out by comparison.

Quick Example

Two x-positions, one line per category between them, with ax.text() doing the labeling that a legend would otherwise handle.

import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(7.5, 7.5))
x_left, x_right = 0, 1

for _, row in df.iterrows():
    ax.plot([x_left, x_right], [row["2015"], row["2025"]],
            color="#7a7a7a", linewidth=1.8, marker="o")
    ax.text(x_left - 0.05, row["2015"], f"{row['country']}  {row['2015']:.1f}%", ha="right", va="center")
    ax.text(x_right + 0.05, row["2025"], f"{row['2025']:.1f}%  {row['country']}", ha="left", va="center")

ax.set_xticks([x_left, x_right])
ax.set_xticklabels(["2015", "2025"])
ax.set_yticks([])
for spine in ["top", "right", "left", "bottom"]:
    ax.spines[spine].set_visible(False)

A first pass at this chart runs into a real problem the moment two categories land close together in value: their labels overlap and become unreadable, which is exactly what happens with USA, Japan, and India clustered near the bottom of this dataset.

Preventing overlapping labels

The fix is a small decluttering pass: sort the values, then push any labels that are closer together than some minimum gap apart from each other, while a thin leader line keeps each label visually connected to its actual point.

import numpy as np

def declutter(values, min_gap):
    order = np.argsort(values)
    sorted_vals = np.array(values)[order].astype(float)
    adjusted = sorted_vals.copy()
    for i in range(1, len(adjusted)):
        if adjusted[i] - adjusted[i - 1] < min_gap:
            adjusted[i] = adjusted[i - 1] + min_gap
    # re-center so the whole block doesn't drift away from the real values
    adjusted += sorted_vals.mean() - adjusted.mean()
    result = np.empty_like(adjusted)
    result[order] = adjusted
    return result

left_label_y = declutter(df["2015"].values, min_gap=3.2)
right_label_y = declutter(df["2025"].values, min_gap=3.2)

Slope chart comparing renewable energy share by country between 2015 and 2025, with decluttered labels and leader lines

Each label now sits at its adjusted declutter() position instead of its true data value, with a short gray line connecting it back to the actual point on the slope. min_gap is in the same units as the data (percentage points here) and needs tuning to the font size in use; too small and labels still overlap, too large and labels drift noticeably far from their points.

Highlighting one line among many

Once there are more than four or five lines, a slope chart gets visually busy fast. Graying out every line except the one being discussed is a common, effective fix, borrowed from the same idea as an "annotation" in a bar chart.

highlight = {"China"}

for _, row in df.iterrows():
    is_hl = row["country"] in highlight
    ax.plot([x_left, x_right], [row["2015"], row["2025"]],
            color="#e76f51" if is_hl else "#d5d5d5",
            linewidth=2.6 if is_hl else 1.6,
            zorder=3 if is_hl else 2)

Slope chart highlighting one country's line in color while the rest are grayed out

China's line was the steepest in the original chart, but it took real effort to spot among six similarly-styled lines; highlighted, it's the first thing the eye lands on. Match the label color and weight to the highlighted line's color (bold, colored text for China's two labels above) so the emphasis carries all the way through the chart, not just the line itself. zorder matters here too: draw the highlighted line last (or with a higher zorder) so it renders on top of the grayed-out ones rather than getting visually buried underneath a crossing line.

Practical Tips

  • Label directly on the chart at both ends rather than using a legend; that's what makes a slope chart readable without the eye having to jump back and forth to a key.
  • Always build in label decluttering once there's any realistic chance of two values landing close together; it's rarely optional in practice, not just an edge case.
  • A short leader line from a decluttered label back to its real point keeps the chart honest, so a nudged label position never gets mistaken for the actual data.
  • Highlight at most one or two lines at a time; graying out everything else works because it's rare, not because gray backgrounds are inherently better.
  • A slope chart with dozens of categories stops being readable regardless of decluttering; past roughly 8-10 lines, consider a dumbbell plot sorted by change, or filtering to the categories that matter for the point being made.

Similar Topics