Chris Parmer — home

Slope chart

life expectancy in 1950 and 2023, twenty countries

Example from Plotly for highly customizable print-ready data visualization · shared helpers

Slope chart — life expectancy in 1950 and 2023, twenty countries

Python Code

"""Slope chart — life expectancy in 1950 and 2023, twenty countries.

Two columns of values joined by a line, labelled at both ends, with the
labels nudged apart so none overlap. The three largest gains are the only
colour; everything else is set in grey so the eye lands on them first.
"""
from pathlib import Path

from _shared import fetch_csv, save, base_layout, titles, INK, MUTED, FAINT, GRID, VIOLET

import numpy as np
import plotly.graph_objects as go

CHART_NUM = 5
URL = "https://ourworldindata.org/grapher/life-expectancy.csv?csvType=full&useColumnShortNames=true"
COUNTRIES = ["Japan", "South Korea", "China", "India", "Brazil", "Mexico", "Nigeria", "Ethiopia",
             "United States", "United Kingdom", "France", "Germany", "Russia", "Turkey", "Indonesia",
             "Bangladesh", "Egypt", "Iran", "Vietnam", "South Africa"]
Y0, Y1 = 1950, 2023


def spread(values, min_gap):
    """Push label positions apart (in data units) until no two are closer than min_gap."""
    order = np.argsort(values)
    pos = np.array(values, float)
    for _ in range(200):
        moved = False
        for a, b in zip(order[:-1], order[1:]):
            if pos[b] - pos[a] < min_gap:
                shift = (min_gap - (pos[b] - pos[a])) / 2
                pos[a] -= shift
                pos[b] += shift
                moved = True
        if not moved:
            break
    return pos


def generate():
    df = fetch_csv(URL)
    col = [c for c in df.columns if c.startswith("life_expectancy")][0]
    df = df[df["entity"].isin(COUNTRIES) & df["year"].isin([Y0, Y1])]
    wide = df.pivot(index="entity", columns="year", values=col).dropna()
    wide["gain"] = wide[Y1] - wide[Y0]
    top = set(wide.nlargest(3, "gain").index)

    fig = go.Figure()
    left_pos = spread(wide[Y0].to_numpy(), 1.45)
    right_pos = spread(wide[Y1].to_numpy(), 1.45)
    for (name, row), lp, rp in zip(wide.iterrows(), left_pos, right_pos):
        hot = name in top
        color = VIOLET if hot else FAINT
        fig.add_trace(go.Scatter(
            x=[0, 1], y=[row[Y0], row[Y1]], mode="lines+markers", name=name,
            line=dict(color=color, width=2.5 if hot else 1.5),
            marker=dict(size=7 if hot else 5, color=color),
            hovertemplate=f"{name}: %{{y:.1f}} years<extra></extra>",
        ))
        lab = dict(size=13, color=VIOLET if hot else INK)
        fig.add_annotation(x=0, y=lp, text=f"{name}  <b>{row[Y0]:.0f}</b>", xanchor="right", xshift=-12,
                           showarrow=False, font=lab)
        fig.add_annotation(x=1, y=rp, text=f"<b>{row[Y1]:.0f}</b>  {name}" + (f"  <span style='color:{MUTED}'>+{row['gain']:.0f}</span>" if hot else ""),
                           xanchor="left", xshift=12, showarrow=False, font=lab)

    for x, yr in [(0, Y0), (1, Y1)]:
        fig.add_annotation(x=x, y=1.0, yref="paper", text=f"<b>{yr}</b>", showarrow=False,
                           font=dict(size=15, color=INK), yanchor="bottom", yshift=4)

    fig.update_layout(**base_layout(margin=dict(l=60, r=60, t=140, b=60)))
    fig.update_xaxes(visible=False, range=[-0.55, 1.85])
    fig.update_yaxes(visible=False, range=[wide[Y0].min() - 2, right_pos.max() + 2])
    for x in (0, 1):
        fig.add_shape(type="line", x0=x, x1=x, y0=0, y1=1, yref="paper", line=dict(color=GRID, width=1), layer="below")
    titles(fig, "Life expectancy has risen everywhere, fastest in Asia",
           "Life expectancy at birth in 1950 and 2023, years. The three largest gains are highlighted.",
           "Source: UN World Population Prospects (2024) and Human Mortality Database, via Our World in Data.", lift=22)
    save(CHART_NUM, fig, width=1280, height=900)

Made with Plotly