MakeoverMonday: Global EV Sales by Region - Who Leads Adoption

MakeoverMonday
Python
Data Viz
Battery and plug-in sales from IEA via Our World in Data: China, Europe, US, and the rest of the world compared over time
Author

chokotto

Published

April 21, 2026

Overview

Electric car sales have shifted from niche experiments to a structural market force. This makeover uses regional aggregates published by Our World in Data (IEA Global EV Outlook) to compare how China, Europe, the United States, and the rest of the world scale up.

Dataset

Code
import pandas as pd
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
Code
url = "https://ourworldindata.org/grapher/electric-car-sales.csv"
raw = pd.read_csv(url)
raw.columns = raw.columns.str.replace(" ", "_")

regions = ["China", "Europe", "United States", "Rest of World"]
ev = raw[raw["Entity"].isin(regions)].copy()
ev = ev.rename(columns={"Electric_cars_sold": "sales"})
ev["Year"] = ev["Year"].astype(int)
ev = ev.sort_values(["Entity", "Year"])

wide = ev.pivot(index="Year", columns="Entity", values="sales").fillna(0)
wide["four_sum"] = wide[regions].sum(axis=1)
for c in regions:
    key = "pct_" + c.replace(" ", "_")
    wide[key] = np.where(wide["four_sum"] > 0, wide[c] / wide["four_sum"] * 100, 0)

My Makeover

China pulls away while other regions climb steadily

Code
colors = {
    "China": CN,
    "Europe": EU,
    "United States": US,
    "Rest of World": ROW,
}

fig = go.Figure()
for entity in regions:
    sub = ev[ev["Entity"] == entity].sort_values("Year")
    fig.add_trace(go.Scatter(
        x=sub["Year"],
        y=sub["sales"],
        mode="lines",
        name=entity,
        line=dict(color=colors[entity], width=3 if entity == "China" else 2),
        hovertemplate=f"{entity}<br>%{{x}}: %{{y:,.0f}}<extra></extra>",
    ))

latest = ev["Year"].max()
last_cn = ev[(ev["Entity"] == "China") & (ev["Year"] == latest)]["sales"].sum()

fig.update_layout(
    **THEME,
    title=make_title(
        f"Electric car sales by region: China reached {last_cn:,.0f} units in {int(latest)}"
    ),
    height=450,
    margin=dict(l=60, r=30, t=100, b=100),
    xaxis=dict(title=""),
    yaxis=dict(title="Electric cars sold (per year)"),
    legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
)

add_legend_note(fig, "Line width emphasizes China vs other regions")
add_source(fig)
assert_no_title_overlap(fig)

# X post: chart-1.png in post folder (works whether cwd is project root or post dir)
from pathlib import Path
import os

_post = "2026-04-21-makeover-monday"
qproj = os.environ.get("QUARTO_PROJECT_DIR")
cwd = Path.cwd()
if qproj:
    chart_path = Path(qproj) / "posts" / _post / "chart-1.png"
elif cwd.name == _post:
    chart_path = cwd / "chart-1.png"
else:
    chart_path = cwd / "posts" / _post / "chart-1.png"
chart_path.parent.mkdir(parents=True, exist_ok=True)
fig.write_image(str(chart_path), width=1200, height=600, scale=2)
fig.show()

Share of the four-region total tilts toward China

Code
years = wide.index.tolist()
fig2 = go.Figure()
fig2.add_trace(go.Scatter(
    x=years, y=wide["pct_China"], name="China",
    stackgroup="one", mode="lines", line=dict(width=0.5),
    fillcolor="rgba(220,38,38,0.35)",
    hovertemplate="China: %{y:.1f}%<extra></extra>",
))
fig2.add_trace(go.Scatter(
    x=years, y=wide["pct_Europe"], name="Europe",
    stackgroup="one", mode="lines", line=dict(width=0.5),
    fillcolor="rgba(37,99,235,0.3)",
    hovertemplate="Europe: %{y:.1f}%<extra></extra>",
))
fig2.add_trace(go.Scatter(
    x=years, y=wide["pct_United_States"], name="United States",
    stackgroup="one", mode="lines", line=dict(width=0.5),
    fillcolor="rgba(217,119,6,0.28)",
    hovertemplate="US: %{y:.1f}%<extra></extra>",
))
fig2.add_trace(go.Scatter(
    x=years, y=wide["pct_Rest_of_World"], name="Rest of World",
    stackgroup="one", mode="lines", line=dict(width=0.5),
    fillcolor="rgba(100,116,139,0.25)",
    hovertemplate="Rest of World: %{y:.1f}%<extra></extra>",
))

fig2.update_layout(
    **THEME,
    title=make_title(
        "Share of combined China + Europe + US + Rest of World sales (100% stacked)"
    ),
    height=450,
    margin=dict(l=60, r=30, t=100, b=100),
    xaxis=dict(title=""),
    yaxis=dict(title="Percent of four-region total", range=[0, 100]),
    legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
)

add_source(fig2)
assert_no_title_overlap(fig2)
fig2.show()

This post is part of Makeover Monday.