Guide de visualisation
Une bibliothèque de modèles de graphiques stylisés et prêts pour la présentation, destinés
aux rapports de santé, disponibles en Python (Matplotlib) et R (ggplot2). Chaque entrée
est complète et autonome : dépliez Afficher le code, choisissez votre langage, et tout ce
qui est nécessaire à la reproduction du résultat affiché en dessous s’y trouve — imports,
données, style et mise en page. Remplacez le dataframe d’exemple par vos propres données et le
style est conservé. Les types de graphiques et le style suivent la charte graphique du HIC.
Le notebook source Python est analysis/spatial-analysis/Bar-Plots.ipynb.
Les jeux de données de ces exemples sont des données fictives d’illustration, utilisées uniquement pour démontrer la mise en page et le style. Ils ne contiennent aucune donnée réelle de santé, de patients ou d’établissements.
Barres horizontales
Barres horizontales avec une catégorie mise en évidence
Classe des catégories nommées et attire l’œil sur une seule ligne grâce à une couleur d’accent unique, le reste servant de contexte neutre.
Afficher le code
Python
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()
Barres horizontales, deux séries
Compare deux mesures par catégorie (par exemple deux périodes ou deux indicateurs) à l’aide de barres appariées.
Afficher le code
Python
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()
Barres horizontales groupées avec sous-catégories
Décompose chaque catégorie en barres de sous-catégories groupées, accompagnées d’étiquettes de groupe.
Afficher le code
Python
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: two forecast vintages (June / September) per district and year
df = pd.DataFrame({
"country": ["Burera", "Burera", "Nyabihu", "Nyabihu", "Rulindo",
"Rulindo", "Gasabo", "Gasabo", "Musanze", "Musanze"],
"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", ascending=False) # 2012, 2011
# 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()
Barres horizontales classées avec mise en évidence
Une liste classée plus longue où une seule barre porte le message.
Afficher le code
Python
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()
Colonnes verticales
Graphique en colonnes verticales
Catégories ordonnées ou courte série temporelle se lisant de gauche à droite.
Afficher le code
Python
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()
Graphique en colonnes verticales, style alternatif
Le même motif de colonnes avec une palette et une mise en évidence différentes.
Afficher le code
Python
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()
Graphique en colonnes verticales avec une barre mise en évidence
Un graphique en colonnes qui met en évidence une seule période ou catégorie.
Afficher le code
Python
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 house 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 (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()
Colonnes empilées
Montre comment un total se décompose en parties au fil du temps. Notre charte graphique limite les graphiques empilés à quatre catégories avec la palette standard — au-delà, remplacez-les par d’autres couleurs et utilisez le gris pour « Autres ».
Afficher le code
Python
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
# Data
df = pd.DataFrame({
"year": np.arange(2008, 2018),
"paid_search": [22, 25, 29, 34, 40, 47, 55, 64, 74, 85],
"display": [10, 11, 13, 15, 18, 21, 25, 30, 36, 43],
"classified": [8, 8, 9, 10, 11, 12, 13, 14, 15, 16],
"online_video": [2, 3, 4, 6, 8, 11, 15, 20, 27, 35],
})
# Colors (house stack order: darkest at the bottom)
COLORS = {
"paid_search": "#1a4a7a",
"display": "#5fc4c0",
"classified": "#c9a84c",
"online_video": "#2d7a4a",
}
LABELS = {
"paid_search": "Paid search",
"display": "Display",
"classified": "Classified",
"online_video": "Online video",
}
BG_COLOR = "#dce8f0"
print(df.to_string(index=False))
fig, ax = plt.subplots(figsize=(6, 4.8))
fig.patch.set_facecolor(BG_COLOR)
ax.set_facecolor(BG_COLOR)
# Forecast panel behind the last two columns
ax.axvspan(2015.5, 2017.6, color="#c3d6e4", zorder=1)
ax.text(2016.55, 196, "FORECAST", fontsize=7.5, color="#555555",
ha="center", va="top")
# Stacked bars
bottom = np.zeros(len(df))
for key, color in COLORS.items():
ax.bar(df["year"], df[key], bottom=bottom, color=color,
width=0.75, label=LABELS[key], zorder=2)
bottom += np.asarray(df[key], dtype=float)
# Axes
ax.set_xlim(2007.4, 2017.6)
ax.set_xticks([2008, 2010, 2012, 2014, 2016, 2017])
ax.set_xticklabels(["2008", "10", "12", "14", "16", "17"])
ax.set_ylim(0, 210)
ax.set_yticks([0, 50, 100, 150, 200])
ax.yaxis.tick_right()
ax.xaxis.set_tick_params(labelsize=9, colors="#555555", length=0)
ax.yaxis.set_tick_params(labelsize=9, colors="#555555", 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
ax.legend(loc="upper left", frameon=False, fontsize=8,
handlelength=1.0, handleheight=0.9, labelspacing=0.4)
# Red badge
fig.add_artist(plt.Rectangle((0.02, 0.975), 0.05, 0.014,
transform=fig.transFigure,
color="#c0392b", clip_on=False))
# Title block
fig.text(0.02, 0.968, "Getting sociable", fontsize=12, fontweight="bold",
va="top", color="#1a1a1a", transform=fig.transFigure)
fig.text(0.02, 0.933, "Global internet-advertising spending, $bn", fontsize=9,
va="top", color="#444444", transform=fig.transFigure)
# Footer
fig.text(0.02, 0.02, "Source: ZenithOptimedia",
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.05, 1, 0.82])
plt.show()
Courbes
Courbe, deux séries
Le choix par défaut pour les séries temporelles. La palette maison ordonne les couleurs des courbes ainsi : bleu foncé, puis sarcelle, puis doré — jusqu’à six séries, quatre étant un plafond pratique.
Afficher le code
Python
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
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
# Data — yields over the 13 years following each period start
years = np.arange(1, 14)
japan = np.array([8.0, 7.2, 6.1, 5.2, 4.4, 3.5, 3.0, 2.4, 1.9, 1.7, 1.5, 1.4, 1.3])
us = np.array([6.6, 6.0, 5.2, 4.6, 4.2, 4.4, 4.7, 4.2, 3.6, 3.2, 2.6, 2.2, 2.0])
# Colors
COLOR_JAPAN = "#1a4a7a"
COLOR_US = "#5fc4c0"
BG_COLOR = "#dce8f0"
fig, ax = plt.subplots(figsize=(6, 4.8))
fig.patch.set_facecolor(BG_COLOR)
ax.set_facecolor(BG_COLOR)
ax.plot(years, japan, color=COLOR_JAPAN, linewidth=2.2,
solid_capstyle="round", label="January 1989 (Japan)", zorder=3)
ax.plot(years, us, color=COLOR_US, linewidth=2.2,
solid_capstyle="round", label="January 1999 (US)", zorder=3)
# Axes
ax.set_xlim(0.6, 13.4)
ax.set_xticks([1, 5, 10, 13])
ax.set_xlabel("Years since start", fontsize=9, color="#555555")
ax.set_ylim(0, 8.4)
ax.set_yticks([0, 2, 4, 6, 8])
ax.yaxis.tick_right()
ax.xaxis.set_tick_params(labelsize=9, colors="#555555", length=0)
ax.yaxis.set_tick_params(labelsize=9, colors="#555555", 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 under the title block
fig.text(0.02, 0.885, "Period beginning:", fontsize=8.5, va="top",
color="#444444", transform=fig.transFigure)
fig.legend(loc="upper left", bbox_to_anchor=(0.02, 0.87), frameon=False,
fontsize=8.5, ncol=2, handlelength=1.4, columnspacing=1.2)
# Red badge
fig.add_artist(plt.Rectangle((0.02, 0.975), 0.05, 0.014,
transform=fig.transFigure,
color="#c0392b", clip_on=False))
# Title block
fig.text(0.02, 0.968, "Where Tokyo leads", fontsize=12, fontweight="bold",
va="top", color="#1a1a1a", transform=fig.transFigure)
fig.text(0.02, 0.933, "Ten-year government-bond yields, %", fontsize=9,
va="top", color="#444444", transform=fig.transFigure)
# Footer
fig.text(0.02, 0.02, "Source: Thomson Reuters",
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.05, 1, 0.76])
plt.show()
Aires empilées
Montre un total et sa composition en même temps. Étiquetez les bandes directement sur le graphique plutôt que d’utiliser une légende.
Afficher le code
Python
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
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
# Data
years = np.arange(2002, 2012)
china = np.array([0.3, 0.4, 0.6, 0.8, 1.1, 1.5, 1.9, 2.4, 2.9, 3.2])
other = np.array([0.9, 1.0, 1.2, 1.5, 1.8, 2.2, 2.4, 2.6, 3.0, 3.3])
# Colors
COLOR_CHINA = "#1a4a7a"
COLOR_OTHER = "#5fc4c0"
BG_COLOR = "#dce8f0"
fig, ax = plt.subplots(figsize=(5.5, 4.8))
fig.patch.set_facecolor(BG_COLOR)
ax.set_facecolor(BG_COLOR)
ax.stackplot(years, china, other,
colors=[COLOR_CHINA, COLOR_OTHER], zorder=2)
# In-chart labels instead of a legend
ax.text(2009.6, 1.2, "China", color="white", fontsize=9,
fontweight="bold", ha="center", zorder=3)
ax.text(2009.2, 4.3, "Other\ndeveloping\ncountries", color="#0e3f4a", fontsize=9,
fontweight="bold", ha="center", linespacing=1.3, zorder=3)
# Axes
ax.set_xlim(2002, 2011)
ax.set_xticks([2002, 2004, 2006, 2008, 2010, 2011])
ax.set_xticklabels(["2002", "04", "06", "08", "10*", "11*"])
ax.set_ylim(0, 6.6)
ax.set_yticks([0, 2, 4, 6])
ax.yaxis.tick_right()
ax.xaxis.set_tick_params(labelsize=9, colors="#555555", length=0)
ax.yaxis.set_tick_params(labelsize=9, colors="#555555", 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.05, 0.014,
transform=fig.transFigure,
color="#c0392b", clip_on=False))
# Title block
fig.text(0.02, 0.968, "Foreign hoards", fontsize=12, fontweight="bold",
va="top", color="#1a1a1a", transform=fig.transFigure)
fig.text(0.02, 0.933, "Developing-country currency reserves\n$trn", fontsize=9,
va="top", color="#444444", transform=fig.transFigure, linespacing=1.4)
# Footer
fig.text(0.02, 0.02, "Source: IMF",
fontsize=8, color="#666666", transform=fig.transFigure)
fig.text(0.78, 0.02, "*Forecast",
fontsize=8, color="#666666", transform=fig.transFigure)
plt.tight_layout(rect=[0, 0.05, 1, 0.80])
plt.show()
Nuage de points
Nuage de points annoté
Met en relation deux variables continues, en étiquetant les points directement plutôt qu’au moyen d’une légende.
Afficher le code
Python
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; Health Intelligence Center",
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")
Thermomètre
Graphique à intervalle de points
Compare deux valeurs par catégorie — deux périodes, deux scénarios — avec des extrémités en points reliées par une ligne d’intervalle. Notre charte graphique recommande les extrémités en points lorsque de simples lignes thermomètre deviennent peu lisibles.
Afficher le code
Python
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
# Data
df = pd.DataFrame({
"district": ["Gasabo", "Kicukiro", "Nyarugenge", "Musanze", "Rubavu",
"Huye", "Rusizi", "Nyagatare", "Burera", "Rwamagana"],
"june": [310, 285, 300, 240, 255, 220, 205, 190, 175, 160],
"september": [385, 350, 330, 310, 290, 270, 240, 250, 210, 205],
})
# Colors
COLOR_JUNE = "#5fc4c0"
COLOR_SEP = "#1a4a7a"
COLOR_RANGE = "#9bb8c8"
BG_COLOR = "#dce8f0"
print(df.to_string(index=False))
fig, ax = plt.subplots(figsize=(6, 5))
fig.patch.set_facecolor(BG_COLOR)
ax.set_facecolor(BG_COLOR)
# Range lines with dot terminals
y = np.arange(len(df))[::-1]
ax.hlines(y, df["june"], df["september"],
color=COLOR_RANGE, linewidth=1.6, zorder=2)
ax.scatter(df["june"], y, s=42, color=COLOR_JUNE, zorder=3, label="June")
ax.scatter(df["september"], y, s=42, color=COLOR_SEP, zorder=3, label="September")
# Axes
ax.set_xlim(0, 420)
ax.set_xticks([0, 100, 200, 300, 400])
ax.xaxis.tick_top()
ax.xaxis.set_tick_params(labelsize=9, colors="#555555", length=0)
ax.set_yticks(y)
ax.set_yticklabels(df["district"], 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)
# Legend
ax.legend(loc="lower right", frameon=False, fontsize=8.5,
handletextpad=0.3, borderaxespad=0.2)
# Red badge
fig.add_artist(plt.Rectangle((0.02, 0.975), 0.05, 0.014,
transform=fig.transFigure,
color="#c0392b", clip_on=False))
# Title block
fig.text(0.02, 0.968, "Catching up", fontsize=12, fontweight="bold",
va="top", color="#1a1a1a", transform=fig.transFigure)
fig.text(0.02, 0.933, "Outpatient consultations per 1,000 population*",
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.68, 0.02, "*Illustrative data",
fontsize=8, color="#666666", transform=fig.transFigure)
plt.tight_layout(rect=[0, 0.04, 1, 0.82])
plt.show()
Camembert / anneau
Graphique en anneau
Utilisez les camemberts et anneaux avec prudence : au-delà de quatre catégories, préférez des barres empilées. Limitez-vous à six parts au maximum, ordonnez-les dans le sens horaire à partir de midi et utilisez le gris pour « Autres ».
Afficher le code
Python
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
# 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
# Data
labels = ["Jaguar Land Rover", "Tata Steel", "Tata Consultancy Services",
"Tata Chemicals", "Taj (hotels)", "Others"]
values = [19.0, 19.4, 4.9, 0.5, 0.4, 1.2]
# Colors — standard order, grey last for "Others"
colors = ["#1a4a7a", "#5fc4c0", "#c9a84c", "#2d7a4a", "#8b3030", "#b8c4cc"]
BG_COLOR = "#dce8f0"
fig, ax = plt.subplots(figsize=(6.5, 4.4))
fig.patch.set_facecolor(BG_COLOR)
# Doughnut: a pie with a wedge width < 1
wedges, _ = ax.pie(values, colors=colors, startangle=90, counterclock=False,
wedgeprops={"width": 0.42, "edgecolor": BG_COLOR,
"linewidth": 1})
ax.text(0, 0, "Total\n45.4", ha="center", va="center",
fontsize=11, fontweight="bold", color="#1a1a1a", linespacing=1.2)
# Color-keyed labels with values, house style
legend_labels = [f"{l} {v}" for l, v in zip(labels, values)]
ax.legend(wedges, legend_labels, loc="center left", bbox_to_anchor=(1.0, 0.5),
frameon=False, fontsize=8.5, labelspacing=0.8, handlelength=0.8,
handleheight=1.1)
# Red badge
fig.add_artist(plt.Rectangle((0.02, 0.975), 0.045, 0.014,
transform=fig.transFigure,
color="#c0392b", clip_on=False))
# Title block
fig.text(0.02, 0.968, "Naan bigger", fontsize=12, fontweight="bold",
va="top", color="#1a1a1a", transform=fig.transFigure)
fig.text(0.02, 0.933, "Tata's workforce in Britain, September 2011\n'000",
fontsize=9, va="top", color="#444444", transform=fig.transFigure,
linespacing=1.4)
# Footer
fig.text(0.02, 0.02, "Source: Tata",
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.02, 0.05, 0.72, 0.80])
plt.show()