import matplotlib.pyplot as plt
from matplotlib.patches import Patch
import numpy as np
# --------------------------
# 1) Institutional trust data
# --------------------------
support_counts = {1: 2, 2: 4, 3: 29, 4: 33, 5: 69}
prog_counts_raw = {'No': 17, 'No, Not sure': 1, 'Not sure': 32, 'Yes': 86, 'Yes, No': 1}
effective_counts = {1: 1, 2: 4, 3: 26, 4: 51, 5: 55}
def likert_to_buckets(counts):
total = sum(counts.values())
neg = counts.get(1, 0) + counts.get(2, 0)
neu = counts.get(3, 0)
pos = counts.get(4, 0) + counts.get(5, 0)
return neg, neu, pos, total
def yns_to_buckets(counts):
neg = counts.get('No', 0)
pos = counts.get('Yes', 0)
neu = counts.get('Not sure', 0) + counts.get('Yes, No', 0) + counts.get('No, Not sure', 0)
total = neg + neu + pos
return neg, neu, pos, total
def to_pct(neg, neu, pos, total):
f = 100.0 / total if total else 0
return neg*f, neu*f, pos*f
sN, sU, sP = to_pct(*likert_to_buckets(support_counts))
pN, pU, pP = to_pct(*yns_to_buckets(prog_counts_raw))
eN, eU, eP = to_pct(*likert_to_buckets(effective_counts))
labels = [
"Support for integration",
"Programmes address racism",
"Effectiveness on inclusion"
]
rows = [(sN, sU, sP), (pN, pU, pP), (eN, eU, eP)]
# Consistent colors
NEG_COLOR = "#d73027" # negative (red)
NEU_COLOR = "#f7f7f7" # neutral (light gray)
POS_COLOR = "#2b8cbe" # positive (blue)
# --------------------------
# 2) Age distribution data
# --------------------------
ages = [
24,20,22,22,18,21,19,25,20,20,21,19,20,18,24,22,19,23,22,21,
21,21,20,20,19,18,19,21,20,22,21,20,20,20,20,20,20,19,20,20,
20,20,20,23,26,25,20,21,22,20,23,19,20,22,20,19,20,20,19,20,
19,20,21,20,20,21,20,18,21,19,20,19,20,20,23,22,21,22,20,20,
20,19,20,20,20,19,20,22,20,20,20,20,19,20,20,21,20,22,20,20,
19,20,22,21,21,23,21,20,20,20,20,22,19,19,21,22,24,19,19,22,
28,20,20,21,20,19,20,22,18,20,22,19,20
]
ages_arr = np.array(ages, dtype=int)
bins = np.arange(ages_arr.min()-0.5, ages_arr.max()+1.5, 1) # 1-year bins
counts, bin_edges = np.histogram(ages_arr, bins=bins)
centers = (bin_edges[:-1] + bin_edges[1:]) / 2.0
# Smooth histogram with a simple moving average
kernel = np.array([1, 2, 1], dtype=float)
kernel = kernel / kernel.sum()
smooth = np.convolve(counts, kernel, mode='same').astype(float)
if smooth.max() > 0:
smooth = smooth / smooth.max() # normalize 0–1
# Mode age (dominant)
unique, cts = np.unique(ages_arr, return_counts=True)
mode_age = int(unique[np.argmax(cts)]) # likely 20 or 21
# --------------------------
# Plot: main chart + inset
# --------------------------
fig, ax = plt.subplots(figsize=(12, 6))
# Diverging stacked bars
ypos = list(range(len(rows)))[::-1]
for y, (neg, neu, pos) in zip(ypos, rows):
ax.barh(y, -neg, left=0, align='center', color=NEG_COLOR, edgecolor='none')
ax.barh(y, neu, left=0, align='center', color=NEU_COLOR, edgecolor='none')
ax.barh(y, pos, left=neu, align='center', color=POS_COLOR, edgecolor='none')
ax.set_yticks(ypos)
ax.set_yticklabels(labels)
ax.set_xlim(-100, 100)
ax.set_xlabel("Share of respondents (%)")
ax.axvline(0, linewidth=1, color="#999999")
ax.set_title("Institutional perceptions")
# Segment annotations
def annotate(x_left, width, y, text):
if abs(width) < 4: return
ax.text(x_left + width/2, y, text, va='center', ha='center', fontsize=9)
for y, (neg, neu, pos) in zip(ypos, rows):
annotate(-neg, neg, y, f"{neg:.0f}%")
annotate(0, neu, y, f"{neu:.0f}%")
annotate(neu, pos, y, f"{pos:.0f}%")
# Legend
handles = [
Patch(facecolor=NEG_COLOR, edgecolor='none', label="Negative (1–2 or No)"),
Patch(facecolor=NEU_COLOR, edgecolor='none', label="Neutral (3 or Not sure / mixed)"),
Patch(facecolor=POS_COLOR, edgecolor='none', label="Positive (4–5 or Yes)")
]
ax.legend(handles=handles, loc="upper center", ncol=3, bbox_to_anchor=(0.5, 1.12), frameon=False)
# --------------------------
# Inset: small age distribution on the right
# --------------------------
mini = ax.inset_axes([0.15, 0.15, 0.22, 0.55]) # x0, y0, width, height
mini.set_facecolor("white")
mini.plot(centers, smooth, color=POS_COLOR, lw=2)
# Only one x tick at dominant age
mini.set_xticks([mode_age])
mini.set_xticklabels([str(mode_age)])
# No y ticks; vertical label
mini.set_yticks([])
mini.set_ylabel("age distribution", rotation=90)
# Clean look
for spine in ["top", "right"]:
mini.spines[spine].set_visible(False)
mini.spines["bottom"].set_alpha(0.4)
mini.spines["left"].set_alpha(0.4)
mini.set_title("Age distribution", pad=6, fontsize=10)
plt.tight_layout()
plt.show()