Visualization Guide
A library of styled, presentation-ready chart templates for health reporting, built with
Matplotlib. Each entry is complete and self-contained: expand Show the code to see
everything needed to reproduce the output below it — imports, data, styling, and layout.
Swap the sample dataframe for your own and the styling carries over. The source notebook is
analysis/spatial-analysis/Bar-Plots.ipynb.
The datasets in these examples are illustrative placeholders used only to demonstrate layout and styling. They contain no real health, patient, or facility data.
Horizontal bar
Horizontal bar with a highlighted category
Ranks named categories and draws the eye to one row with a single accent color, keeping the rest as neutral context.
Show the code
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
import pandas as pd
# Font setup
available = {f.name for f in fm.fontManager.ttflist}
FONT = next(
(f for f in ["Gill Sans", "Gill Sans MT", "URW Classico", "Trebuchet MS"]
if f in available),
None
)
if FONT:
plt.rcParams["font.family"] = FONT
print(f"Using font: {FONT}")
else:
print("Gill Sans not found – using matplotlib default. "
"Install it and re-run for the exact look.")
# Data
HIGHLIGHT = "Spain"
COLOR_DEFAULT = "#1a4a7a"
COLOR_HIGHLIGHT = "#5fc4c0"
BG_COLOR = "#dce8f0"
df = pd.DataFrame({
"country": ["Germany", "Spain", "Austria", "France", "Luxembourg",
"Portugal", "Britain", "Denmark", "Sweden"],
"consultations_per_person": [8.2, 7.5, 6.8, 6.7, 6.3, 5.8, 5.1, 4.7, 2.9],
})
df["highlight"] = df["country"] == HIGHLIGHT
df["color"] = df["highlight"].map({True: COLOR_HIGHLIGHT, False: COLOR_DEFAULT})
df["y_pos"] = range(len(df) - 1, -1, -1) # top-to-bottom order
print(df.to_string(index=False))
fig, ax = plt.subplots(figsize=(7, 5))
fig.patch.set_facecolor(BG_COLOR)
ax.set_facecolor(BG_COLOR)
# Bars
ax.barh(df["y_pos"], df["consultations_per_person"],
color=df["color"], height=0.6)
# Axes
ax.set_xlim(0, 8.5)
ax.set_xticks([0, 2, 4, 6, 8])
ax.xaxis.set_tick_params(labelsize=9, colors="#555555")
ax.xaxis.tick_top()
ax.set_yticks(df["y_pos"])
ax.set_yticklabels(
[f"$\\bf{{{c}}}$" if c == HIGHLIGHT else c for c in df["country"]],
fontsize=10
)
ax.tick_params(axis="y", length=0)
# Grid & spines
ax.xaxis.grid(True, color="white", linewidth=0.8, zorder=0)
ax.set_axisbelow(True)
for spine in ax.spines.values():
spine.set_visible(False)
ax.tick_params(axis="x", length=0)
# Red badge
fig.add_artist(plt.Rectangle((0.02, 0.975), 0.055, 0.016,
transform=fig.transFigure,
color="#c0392b", clip_on=False))
# Title block
fig.text(0.02, 0.965, "Salud", fontsize=12, fontweight="bold",
va="top", color="#1a1a1a", transform=fig.transFigure)
fig.text(0.02, 0.930, "Doctors' consultations per person", fontsize=9,
va="top", color="#333333", transform=fig.transFigure)
fig.text(0.02, 0.905, "Selected countries, 2009", fontsize=8.5,
va="top", color="#555555", transform=fig.transFigure)
# Footer
fig.text(0.02, 0.02, "Source: Health Management Information System",
fontsize=8, color="#666666", transform=fig.transFigure)
fig.text(0.79, 0.02, "Health Intelligence Center",
fontsize=8, color="#666666", transform=fig.transFigure)
# rect=[left, bottom, right, top] — lower 'top' pushes the plot area down
plt.tight_layout(rect=[0, 0.03, 1, 0.78])
plt.show()
Horizontal bar, two series
Compares two measures per category (for example two periods or two indicators) with paired bars.
Show the code
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
import pandas as pd
import numpy as np
# Font setup
available = {f.name for f in fm.fontManager.ttflist}
FONT = next(
(f for f in ["Gill Sans", "Gill Sans MT", "URW Classico", "Trebuchet MS"]
if f in available),
None
)
if FONT:
plt.rcParams["font.family"] = FONT
print(f"Using font: {FONT}")
else:
print("Gill Sans not found – using matplotlib default.")
# Data
df = pd.DataFrame({
"country": ["Rubavu", "Rusizi", "Bugesera", "Nyabihu", "Musanze",
"Burera", "Rulindo", "Gasabo", "Nyamasheke"],
"middle_class": [80, 72, 60, 58, 55, 50, 62, 50, 48],
"low_income": [68, 63, 50, 62, 38, 42, 55, 48, 42],
})
# Reverse so Chile appears on top
df = df.iloc[::-1].reset_index(drop=True)
print(df.to_string(index=False))
# Colors
COLOR_MIDDLE = "#1a4a7a"
COLOR_LOW = "#5fc4c0"
BG_COLOR = "#dce8f0"
# Layout
n = len(df)
y = np.arange(n)
height = 0.35 # bar height for each trace
fig, ax = plt.subplots(figsize=(7, 5.5))
fig.patch.set_facecolor(BG_COLOR)
ax.set_facecolor(BG_COLOR)
# Bars
ax.barh(y + height / 2, df["middle_class"], height=height,
color=COLOR_MIDDLE, label="Middle class")
ax.barh(y - height / 2, df["low_income"], height=height,
color=COLOR_LOW, label="Low income")
# Axes
ax.set_xlim(0, 85)
ax.set_xticks([0, 20, 40, 60, 80])
ax.xaxis.set_tick_params(labelsize=9, colors="#555555")
ax.xaxis.tick_top()
ax.set_yticks(y)
ax.set_yticklabels(df["country"], fontsize=10, fontweight="bold")
ax.tick_params(axis="y", length=0)
# Grid & spines
ax.xaxis.grid(True, color="white", linewidth=0.8, zorder=0)
ax.set_axisbelow(True)
for spine in ax.spines.values():
spine.set_visible(False)
ax.tick_params(axis="x", length=0)
# Legend
legend = ax.legend(
loc="lower right",
fontsize=9,
frameon=False,
handlelength=1.2,
handleheight=0.9,
handletextpad=0.5,
)
# Red badge
fig.add_artist(plt.Rectangle((0.02, 0.965), 0.045, 0.016,
transform=fig.transFigure,
color="#c0392b", clip_on=False))
# Title block
fig.text(0.02, 0.955, "Defenders of democracy",
fontsize=12, fontweight="bold", va="top",
color="#1a1a1a", transform=fig.transFigure)
fig.text(0.02, 0.915,
"Respondents saying that honest elections held regularly\n"
"with a choice of at least two political parties are very\n"
"important, 2007, %",
fontsize=8.5, va="top", color="#444444",
transform=fig.transFigure, linespacing=1.5)
# Footer
fig.text(0.02, 0.02, "Source: Pew Global Attitudes Survey",
fontsize=8, color="#666666", transform=fig.transFigure)
fig.text(0.72, 0.02, "Health Intelligence Center",
fontsize=8, color="#666666", transform=fig.transFigure)
plt.tight_layout(rect=[0, 0.03, 1, 0.78])
plt.show()
Grouped horizontal bar with sub-categories
Breaks each category into grouped sub-category bars with group labels.
Show the code
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
import matplotlib.text as mtext
import pandas as pd
import numpy as np
# Font setup
available = {f.name for f in fm.fontManager.ttflist}
FONT = next(
(f for f in ["Gill Sans", "Gill Sans MT", "URW Classico", "Trebuchet MS"]
if f in available),
None
)
if FONT:
plt.rcParams["font.family"] = FONT
print(f"Using font: {FONT}")
else:
print("Gill Sans not found – using matplotlib default.")
# Data
df = pd.DataFrame({
"country": ["Burera", "Nyabihu", "Rulindo", "Gasabo",
"Musanze","Rwamagana","Rubavu", "Gakenke",
"Nyarugenge", "Gicumbi"],
"year": [2011, 2012] * 5,
"june": [3.0, 1.8, 2.1, 1.4, 2.0, 1.5, 0.8, 1.1, 1.0, 0.3],
"september": [2.7, 1.3, 1.7, 1.0, 1.6, 1.1, 0.7, 0.3, 0.6, 0.1],
})
print(df.to_string(index=False))
# Colors
COLOR_JUNE = "#1a4a7a"
COLOR_SEP = "#5fc4c0"
BG_COLOR = "#dce8f0"
# Build y positions
# Layout (bottom→top): Italy_2012, Italy_2011, gap, Spain_2012, Spain_2011 …
# Each year-row occupies 2 bar slots stacked: june on top, sep below.
# june centre = y_row + BAR_H/2
# sep centre = y_row - BAR_H/2
BAR_H = 0.30 # height of each individual bar
ROW_STEP = BAR_H * 2 + 0.05 # vertical space per year-row
GROUP_GAP = 0.30 # extra gap between country groups
countries = df["country"].unique() # original order: Germany … Italy
positions = []
y = 0
# Build bottom-up: last country first, within each country 2012 first
for country in reversed(countries):
sub = df[df["country"] == country].sort_values("year") # 2011, 2012
# Place 2012 at bottom of group, 2011 above it
for _, row in sub.iterrows():
positions.append({
"country": country,
"year": int(row["year"]),
"june": row["june"],
"sep": row["september"],
"y": y
})
y += ROW_STEP
y += GROUP_GAP
pos_df = pd.DataFrame(positions)
# After building bottom-up: lower y = 2012, higher y = 2011 ✓
# Plot
fig, ax = plt.subplots(figsize=(6, 6.5))
fig.patch.set_facecolor(BG_COLOR)
ax.set_facecolor(BG_COLOR)
for _, r in pos_df.iterrows():
# June on top (+BAR_H/2), September below (-BAR_H/2)
ax.barh(r["y"] + BAR_H / 2, r["june"], height=BAR_H,
color=COLOR_JUNE, align="center")
ax.barh(r["y"] - BAR_H / 2, r["sep"], height=BAR_H,
color=COLOR_SEP, align="center")
# Y-axis: year labels only, country drawn via ax.text
ax.set_yticks(pos_df["y"])
ax.set_yticklabels([str(r["year"]) for _, r in pos_df.iterrows()],
fontsize=9, color="#555555")
ax.tick_params(axis="y", length=0, pad=2)
# Draw country name centred between its two year rows
for country in countries:
rows = pos_df[pos_df["country"] == country]
mid_y = rows["y"].mean()
ax.text(-0.18, mid_y, country,
ha="right", va="center", fontsize=9, fontweight="bold",
transform=ax.get_yaxis_transform())
# X-axis
ax.set_xlim(0, 3.3)
ax.set_xticks([0, 1, 2, 3])
ax.xaxis.set_tick_params(labelsize=9, colors="#555555")
ax.xaxis.tick_top()
ax.tick_params(axis="x", length=0)
# Grid & spines
ax.xaxis.grid(True, color="white", linewidth=0.8, zorder=0)
ax.set_axisbelow(True)
for spine in ax.spines.values():
spine.set_visible(False)
# Legend
from matplotlib.patches import Patch
legend_elements = [Patch(facecolor=COLOR_JUNE, label="June"),
Patch(facecolor=COLOR_SEP, label="September")]
ax.legend(handles=legend_elements, loc="lower right", fontsize=8,
frameon=False, title="Forecasts made in:", title_fontsize=8)
# Red badge
fig.add_artist(plt.Rectangle((0.02, 0.975), 0.045, 0.013,
transform=fig.transFigure,
color="#c0392b", clip_on=False))
# Title block
fig.text(0.02, 0.968, "Autumn fall", fontsize=12, fontweight="bold",
va="top", color="#1a1a1a", transform=fig.transFigure)
fig.text(0.02, 0.936, "GDP, % increase on previous year", fontsize=9,
va="top", color="#444444", transform=fig.transFigure)
# Footer
fig.text(0.02, 0.02, "Source: HMIS",
fontsize=8, color="#666666", transform=fig.transFigure)
fig.text(0.79, 0.02, "Health Intelligence Center",
fontsize=8, color="#666666", transform=fig.transFigure)
plt.tight_layout(rect=[0, 0.03, 1, 0.80])
plt.show()
Ranked horizontal bar with emphasis
A longer ranked list where one bar carries the story.
Show the code
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
import pandas as pd
import numpy as np
# Font setup
available = {f.name for f in fm.fontManager.ttflist}
FONT = next(
(f for f in ["Gill Sans", "Gill Sans MT", "URW Classico", "Trebuchet MS"]
if f in available),
None
)
if FONT:
plt.rcParams["font.family"] = FONT
print(f"Using font: {FONT}")
else:
print("Gill Sans not found – using matplotlib default.")
# Data
df = pd.DataFrame({
"sector": ["Internet", "Software", "Tech hardware", "IT services",
"Health care", "Total", "Leisure", "Chemicals",
"Oil, gas and fuels", "Metals and mining", "Property",
"Energy equipment"],
"change": [57, 34, 20, 16, 10, 4, -3, -5, -18, -22, -28, -58],
"bold": [False, False, False, False, False, True,
False, False, False, False, False, False],
})
# Reverse so Internet is on top
df = df.iloc[::-1].reset_index(drop=True)
print(df.to_string(index=False))
# Colors
COLOR_BAR = "#1a4a7a"
COLOR_ZERO = "#c0392b"
BG_COLOR = "#dce8f0"
# Plot
fig, ax = plt.subplots(figsize=(6, 6.5))
fig.patch.set_facecolor(BG_COLOR)
ax.set_facecolor(BG_COLOR)
y = np.arange(len(df))
ax.barh(y, df["change"], color=COLOR_BAR, height=0.6, zorder=2)
# Zero line (red)
ax.axvline(x=0, color=COLOR_ZERO, linewidth=1.5, zorder=3)
# Axes
ax.set_xlim(-65, 65)
ax.set_xticks([-60, -40, -20, 0, 20, 40, 60])
ax.xaxis.set_tick_params(labelsize=9, colors="#555555")
ax.xaxis.tick_top()
ax.tick_params(axis="x", length=0)
ax.set_yticks(y)
ax.set_yticklabels(df["sector"], fontsize=10)
ax.tick_params(axis="y", length=0)
# Bold "Total" label
for label, (_, row) in zip(ax.get_yticklabels(), df.iterrows()):
if row["bold"]:
label.set_fontweight("bold")
# Grid & spines
ax.xaxis.grid(True, color="white", linewidth=0.8, zorder=0)
ax.set_axisbelow(True)
for spine in ax.spines.values():
spine.set_visible(False)
# Red badge
fig.add_artist(plt.Rectangle((0.02, 0.975), 0.045, 0.013,
transform=fig.transFigure,
color="#c0392b", clip_on=False))
# Title block
fig.text(0.02, 0.968, "Another dotcom boom", fontsize=12, fontweight="bold",
va="top", color="#1a1a1a", transform=fig.transFigure)
fig.text(0.02, 0.935, "Change in worldwide capital spending*, %",
fontsize=9, va="top", color="#444444", transform=fig.transFigure)
fig.text(0.02, 0.912, "2014-17 forecast",
fontsize=9, va="top", color="#444444", transform=fig.transFigure)
# Footer
fig.text(0.02, 0.02, "Source: Goldman Sachs",
fontsize=8, color="#666666", transform=fig.transFigure)
fig.text(0.68, 0.02, "*In dollar terms",
fontsize=8, color="#666666", transform=fig.transFigure)
plt.tight_layout(rect=[0, 0.03, 1, 0.78])
plt.show()
Vertical column
Vertical column chart
Ordered categories or a short time series reading left to right.
Show the code
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
import pandas as pd
# Font setup
available = {f.name for f in fm.fontManager.ttflist}
FONT = next(
(f for f in ["Gill Sans", "Gill Sans MT", "URW Classico", "Trebuchet MS"]
if f in available),
None
)
if FONT:
plt.rcParams["font.family"] = FONT
print(f"Using font: {FONT}")
else:
print("Gill Sans not found – using matplotlib default.")
# Data
df = pd.DataFrame({
"year": ["2004","06","08","10","11","12","13","14","15*"],
"gdp": [ 11.7, 12.6, 11.2, 10.0, 11.2, 10.9, 9.7, 10.3, 8.7],
"estimate": [ False, False, False, False, False, False, False, False, True],
})
# Colors
COLOR_DEFAULT = "#1a4a7a"
COLOR_ESTIMATE = "#5fc4c0"
BG_COLOR = "#dce8f0"
df["color"] = df["estimate"].map({False: COLOR_DEFAULT, True: COLOR_ESTIMATE})
print(df.to_string(index=False))
# Plot
fig, ax = plt.subplots(figsize=(5, 5))
fig.patch.set_facecolor(BG_COLOR)
ax.set_facecolor(BG_COLOR)
x = range(len(df))
ax.bar(x, df["gdp"], color=df["color"], width=0.75, zorder=2)
# Axes
ax.set_xlim(-0.5, len(df) - 0.5)
ax.set_ylim(0, 14)
ax.set_yticks([0, 2, 4, 6, 8, 10, 12, 14])
ax.yaxis.set_tick_params(labelsize=9, colors="#555555")
ax.yaxis.tick_right() # y-axis on the right
ax.tick_params(axis="y", length=0)
ax.set_xticks(list(x))
ax.set_xticklabels(df["year"], fontsize=9, color="#555555")
ax.tick_params(axis="x", length=0)
# Grid & spines
ax.yaxis.grid(True, color="white", linewidth=0.8, zorder=0)
ax.set_axisbelow(True)
for spine in ax.spines.values():
spine.set_visible(False)
# Red badge
fig.add_artist(plt.Rectangle((0.02, 0.975), 0.055, 0.014,
transform=fig.transFigure,
color="#c0392b", clip_on=False))
# Title block
fig.text(0.02, 0.968, "African lion", fontsize=12, fontweight="bold",
va="top", color="#1a1a1a", transform=fig.transFigure)
fig.text(0.02, 0.935, "Ethiopia's GDP, % change on a year earlier",
fontsize=9, va="top", color="#444444", transform=fig.transFigure)
# Footer
fig.text(0.02, 0.02, "Source: IMF",
fontsize=8, color="#666666", transform=fig.transFigure)
fig.text(0.02, 0.055, "*Estimate",
fontsize=8, color="#666666", transform=fig.transFigure)
fig.text(0.79, 0.02, "Health Intelligence Center",
fontsize=8, color="#666666", transform=fig.transFigure)
plt.tight_layout(rect=[0, 0.06, 1, 0.80])
plt.show()
Vertical column chart, alternate styling
The same column pattern with a different palette and emphasis.
Show the code
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
import matplotlib.patches as mpatches
import pandas as pd
import numpy as np
# Font setup
available = {f.name for f in fm.fontManager.ttflist}
FONT = next(
(f for f in ["Gill Sans", "Gill Sans MT", "URW Classico", "Trebuchet MS"]
if f in available),
None
)
if FONT:
plt.rcParams["font.family"] = FONT
print(f"Using font: {FONT}")
else:
print("Gill Sans not found – using matplotlib default.")
# Data
df = pd.DataFrame({
"period": ["20 years", "10 years", "5 years"],
"equities": [8.5, 7.5, 8.5],
"all_property": [9.5, 8.0, 13.5],
"rural": [14.5, 13.5, 13.0],
"forestry": [11.0, 18.5, 20.5],
})
print(df.to_string(index=False))
# Colors
COLORS = {
"equities": "#1a4a7a",
"all_property": "#5fc4c0",
"rural": "#c9a84c",
"forestry": "#2d7a4a",
}
LABELS = {
"equities": "Equities",
"all_property": "All property",
"rural": "Rural property",
"forestry": "Forestry",
}
BG_COLOR = "#dce8f0"
keys = list(COLORS.keys())
n_groups = len(df)
n_bars = len(keys)
width = 0.18 # width of each bar
group_w = width * n_bars + 0.08 # total group width incl. gap
x = np.arange(n_groups)
# Plot
fig, ax = plt.subplots(figsize=(6, 5))
fig.patch.set_facecolor(BG_COLOR)
ax.set_facecolor(BG_COLOR)
for i, key in enumerate(keys):
offsets = x - (n_bars - 1) * width / 2 + i * width
ax.bar(offsets, df[key], width=width, color=COLORS[key],
label=LABELS[key], zorder=2)
# Axes
ax.set_xlim(-0.5, n_groups - 0.5)
ax.set_ylim(0, 22)
ax.set_yticks([0, 5, 10, 15, 20])
ax.yaxis.set_tick_params(labelsize=9, colors="#555555")
ax.yaxis.tick_right()
ax.tick_params(axis="y", length=0)
ax.set_xticks(x)
ax.set_xticklabels(df["period"], fontsize=9, color="#555555")
ax.tick_params(axis="x", length=0)
# Grid & spines
ax.yaxis.grid(True, color="white", linewidth=0.8, zorder=0)
ax.set_axisbelow(True)
for spine in ax.spines.values():
spine.set_visible(False)
# Legend
handles = [mpatches.Patch(facecolor=COLORS[k], label=LABELS[k]) for k in keys]
ax.legend(handles=handles, ncol=2, fontsize=8, frameon=False,
loc="upper left", bbox_to_anchor=(0, 1.02),
handlelength=1.0, handleheight=0.9, columnspacing=1.0)
# Red badge
fig.add_artist(plt.Rectangle((0.02, 0.975), 0.055, 0.014,
transform=fig.transFigure,
color="#c0392b", clip_on=False))
# Title block
fig.text(0.02, 0.968, "The mighty jungle", fontsize=12, fontweight="bold",
va="top", color="#1a1a1a", transform=fig.transFigure)
fig.text(0.02, 0.935, "Britain, annualised rate of return\non investment*, %",
fontsize=9, va="top", color="#444444",
transform=fig.transFigure, linespacing=1.5)
# Footer
fig.text(0.02, 0.02, "Sources: MSCI; JP Morgan",
fontsize=8, color="#666666", transform=fig.transFigure)
fig.text(0.75, 0.02, "*To 2014",
fontsize=8, color="#666666", transform=fig.transFigure)
fig.text(0.79, 0.055, "Health Intelligence Center",
fontsize=8, color="#666666", transform=fig.transFigure)
plt.tight_layout(rect=[0, 0.06, 1, 0.75])
plt.show()
Vertical column chart with a highlighted bar
A column chart that highlights a single period or category.
Show the code
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
import matplotlib.patches as mpatches
import numpy as np
# Font setup
available = {f.name for f in fm.fontManager.ttflist}
FONT = next(
(f for f in ["Gill Sans", "Gill Sans MT", "URW Classico", "Trebuchet MS"]
if f in available),
None
)
if FONT:
plt.rcParams["font.family"] = FONT
print(f"Using font: {FONT}")
else:
print("Gill Sans not found – using matplotlib default.")
# Data
# Approximate net private capital inflows/outflows in $bn, 1994–2011
# Values estimated from the chart image
years = [
1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003,
2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011
]
values = [
-4, -5, -23, -18, -8, -18, -25, -15, -8, -7,
-35, -30, 40, 80, -130, -50, -35, 5
]
# Two-tone: teal for positive inflows, darker teal for outflows (negative)
# The chart uses a medium teal for most bars and a lighter teal for the
# 2006-2008 positive bars — we'll use a single accent for positive values
COLOR_NEG = "#3a9aa0" # muted teal (outflows)
COLOR_POS = "#1a6a7a" # darker teal (inflows)
COLOR_ZERO = "#c0392b" # red zero line
BG_COLOR = "#dce8f0" # light blue-grey Economist background
bar_colors = [COLOR_POS if v >= 0 else COLOR_NEG for v in values]
# Plot
fig, ax = plt.subplots(figsize=(5, 5.5))
fig.patch.set_facecolor(BG_COLOR)
ax.set_facecolor(BG_COLOR)
x = np.arange(len(years))
ax.bar(x, values, color=bar_colors, width=0.75, zorder=2)
# Zero / baseline (red)
ax.axhline(y=0, color=COLOR_ZERO, linewidth=1.5, zorder=3)
# Axes
ax.set_ylim(-160, 120)
ax.set_yticks([100, 50, 0, -50, -100, -150])
ax.yaxis.set_tick_params(labelsize=9, colors="#555555")
ax.yaxis.tick_right()
ax.tick_params(axis="y", length=0)
# X-axis: show subset of year labels to match original
label_map = {
1994: "1994", 1996: "96", 1998: "98", 2000: "2000", 2002: "02",
2004: "04", 2006: "06", 2008: "08", 2011: "11*"
}
xtick_positions = [i for i, yr in enumerate(years) if yr in label_map]
xtick_labels = [label_map[years[i]] for i in xtick_positions]
ax.set_xticks(xtick_positions)
ax.set_xticklabels(xtick_labels, fontsize=8.5, color="#555555")
ax.tick_params(axis="x", length=0)
ax.xaxis.tick_bottom()
# Grid & spines
ax.yaxis.grid(True, color="white", linewidth=0.8, zorder=0)
ax.set_axisbelow(True)
for spine in ax.spines.values():
spine.set_visible(False)
# Red badge (Economist house style)
fig.add_artist(plt.Rectangle(
(0.02, 0.975), 0.045, 0.013,
transform=fig.transFigure,
color="#c0392b", clip_on=False
))
# Title block
fig.text(0.02, 0.968, "The money drain",
fontsize=12, fontweight="bold",
va="top", color="#1a1a1a", transform=fig.transFigure)
fig.text(0.02, 0.935, "Net private capital inflows/outflows, $bn",
fontsize=9, va="top", color="#444444", transform=fig.transFigure)
# Footer
fig.text(0.02, 0.02, "Source: Central Bank of Russia",
fontsize=8, color="#666666", transform=fig.transFigure)
fig.text(0.55, 0.02, "*To June 30th",
fontsize=8, color="#666666", transform=fig.transFigure)
plt.tight_layout(rect=[0, 0.04, 1, 0.80])
plt.show()
Scatter
Annotated scatter plot
Relates two continuous variables, labelling points directly instead of using a legend.
Show the code
import matplotlib
matplotlib.rcParams['text.usetex'] = False
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
import matplotlib.patches as mpatches
import numpy as np
available = {f.name for f in fm.fontManager.ttflist}
FONT = next((f for f in ["Gill Sans","Gill Sans MT","URW Classico","Trebuchet MS"] if f in available), None)
if FONT:
plt.rcParams["font.family"] = FONT
C_MW = "#8b3030" # Midwest — dark red (highlighted)
C_OTHER = "#3bbfbf" # All other — teal
C_GRAY = "#9e9e9e" # Large cities — grey
BG = "#dce8f0"
RED_LINE= "#c0392b"
RED_BAR = "#c0392b"
np.random.seed(42)
def make_counties(n, health_mu, health_sd, swing_mu, swing_sd, size_mu, size_sd):
"""Simulate county-level data. Replace with real arrays when available."""
health = np.random.normal(health_mu, health_sd, n)
swing = np.random.normal(swing_mu, swing_sd, n)
size = np.abs(np.random.normal(size_mu, size_sd, n)) + 10
return health, swing, size
# Other regions (teal) — spread across full x-range, mixed swing
h_ot, s_ot, sz_ot = make_counties(600, 48, 8, -2, 16, 20, 18)
# Midwest (dark red) — clustered at lower health, stronger R swing
h_mw, s_mw, sz_mw = make_counties(450, 38, 5, 16, 10, 18, 14)
# Large cities (grey) — few, larger bubbles, less partisan swing
h_gr = np.array([35, 45, 52, 58, 40, 30 ])
s_gr = np.array([-5, -8, -12, -2, -15, -3 ])
sz_gr = np.array([180, 220, 300, 250, 160, 140 ])
# Named/highlighted counties — white-outlined dots with leader lines
named = {
"Jefferson, OH": (32, 42),
"Knox, OH": (43, 32),
}
# Figure
fig, ax = plt.subplots(figsize=(7.4, 5.6))
fig.patch.set_facecolor(BG)
ax.set_facecolor(BG)
fig.subplots_adjust(left=0.11, right=0.76, top=0.77, bottom=0.18)
# Layer order: other (bottom) → grey cities → Midwest (top)
ax.scatter(h_ot, s_ot, s=sz_ot, color=C_OTHER, alpha=0.55,
linewidths=0, zorder=2)
ax.scatter(h_gr, s_gr, s=sz_gr, color=C_GRAY, alpha=0.65,
linewidths=0.5, edgecolors='white', zorder=3)
ax.scatter(h_mw, s_mw, s=sz_mw, color=C_MW, alpha=0.60,
linewidths=0, zorder=4)
# Named counties — white-filled dot with dark outline + leader line + label
for name, (x, y) in named.items():
ax.scatter(x, y, s=38, color='white', linewidths=1.2,
edgecolors=C_MW, zorder=6)
ox = 1.2 if "Knox" in name else 1.0
ax.annotate(name, xy=(x, y), xytext=(x + ox, y + 3.5),
fontsize=8, color="#1a1a1a", zorder=7,
arrowprops=dict(arrowstyle="-", color="#555555", lw=0.7))
# Region label inside cluster
ax.text(37, 14, "Midwest", fontsize=13, fontweight="bold",
color="black", alpha=0.85, zorder=5, style='italic')
# Zero reference line
ax.axhline(0, color=RED_LINE, linewidth=1.0, zorder=2)
# Axes
ax.set_xlim(18, 72)
ax.set_ylim(-55, 55)
ax.set_xticks([20, 30, 40, 50, 60, 70])
ax.set_yticks([-50, -25, 0, 25, 50])
ax.xaxis.set_tick_params(labelsize=8.5, colors="#555555", length=0)
ax.yaxis.set_tick_params(labelsize=8.5, colors="#555555", length=0)
ax.xaxis.grid(True, color="white", linewidth=0.8, zorder=0)
ax.yaxis.grid(True, color="white", linewidth=0.8, zorder=0)
ax.set_axisbelow(True)
for sp in ax.spines.values():
sp.set_visible(False)
# Axis labels
ax.set_xlabel("Index of county health metrics*", fontsize=9,
color="#444444", labelpad=22)
ax.text(0.02, -0.13, "◄ WORSE HEALTH", fontsize=7.5, color="#555555",
transform=ax.transAxes, va='top')
ax.text(0.98, -0.13, "BETTER HEALTH ►", fontsize=7.5, color="#555555",
ha='right', transform=ax.transAxes, va='top')
ax.text(-0.10, 0.75, "MORE REPUBLICAN", fontsize=7.5, color="#555555",
transform=ax.transAxes, va='center', ha='center', rotation=90)
ax.text(-0.10, 0.25, "LESS REPUBLICAN", fontsize=7.5, color="#555555",
transform=ax.transAxes, va='center', ha='center', rotation=90)
# Right-side y-axis description
fig.text(0.77, 0.77,
"Change in Republican margin over\nDemocrats, 2012-16, % points",
fontsize=7.8, color="#444444", va="top", ha="left",
linespacing=1.4, transform=fig.transFigure)
# Bubble size legend
lx, ly = 0.790, 0.62
circ = mpatches.Circle((lx + 0.025, ly - 0.018), 0.026,
transform=fig.transFigure, fill=False,
edgecolor="#555555", linewidth=0.9, clip_on=False)
fig.add_artist(circ)
fig.text(lx + 0.060, ly, "Voting\neligible\npopulation",
fontsize=7.5, color="#1a1a1a", va="center",
transform=fig.transFigure, linespacing=1.3)
# Red badge + Title
fig.add_artist(plt.Rectangle((0.06, 0.942), 0.038, 0.008,
transform=fig.transFigure, color=RED_BAR, clip_on=False))
fig.text(0.06, 0.938, "Vitality and the vote",
fontsize=11, fontweight="bold", va="top", color="#1a1a1a",
transform=fig.transFigure)
fig.text(0.06, 0.910,
"United States, health metrics against swing to Donald Trump",
fontsize=8.5, va="top", color="#444444", transform=fig.transFigure)
fig.text(0.06, 0.890, "By county",
fontsize=8, va="top", color="#444444", transform=fig.transFigure)
# Footer
fig.text(0.06, 0.022,
"Sources: Atlas of US Presidential Elections; Census Bureau;\n"
"IPUMS, University of Minnesota; Institute for Health Metrics\n"
"and Evaluation; The Economist",
fontsize=7, color="#666666", transform=fig.transFigure, linespacing=1.4)
fig.text(0.52, 0.022,
"*Weighted index of obesity, diabetes,\nheavy drinking, physical exercise and\nlife expectancy, 2010-12",
fontsize=7, color="#666666", transform=fig.transFigure, linespacing=1.4)
print("Saved: vitality_vote_midwest.png")