Skip to Content

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()
charts/doctors_consultations.png

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()
charts/defenders_of_democracy.png

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()
charts/autumn_fall_gdp.png

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()
charts/dotcom_boom_capex.png

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()
charts/african_lion_gdp.png

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()
charts/mighty_jungle_returns.png

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()
charts/money_drain.png

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")
charts/vitality_vote_midwest.png
Last updated on