Code
import pandas as pd
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplotschokotto
April 21, 2026
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.
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)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()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.