706 lines
31 KiB
Python
706 lines
31 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
TBOTE Project: Global Findings chart regeneration.
|
|
Editorial dark theme matching the site aesthetic.
|
|
No em dashes. No AI-looking styling.
|
|
"""
|
|
|
|
import matplotlib
|
|
matplotlib.use('Agg')
|
|
import matplotlib.pyplot as plt
|
|
import matplotlib.patches as mpatches
|
|
import matplotlib.patheffects as pe
|
|
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
|
|
import numpy as np
|
|
import os
|
|
|
|
OUT = '/tmp/gf_images/output'
|
|
os.makedirs(OUT, exist_ok=True)
|
|
|
|
# ── Site palette ──
|
|
BG = '#0d0d0d'
|
|
BG_CARD = '#1a1a1a'
|
|
BG_PANEL = '#1e1e1e'
|
|
BORDER = '#2e2e2e'
|
|
RED = '#c22d2d'
|
|
RED_LT = '#e04545'
|
|
RED_DK = '#8b1a1a'
|
|
WHITE = '#f0f0f0'
|
|
TEXT = '#e0e0e0'
|
|
TEXT_DIM = '#999999'
|
|
TEXT_MUT = '#666666'
|
|
BLUE = '#4a90d9'
|
|
BLUE_DK = '#2d5a8a'
|
|
AMBER = '#d4943a'
|
|
GREEN = '#4a9a5a'
|
|
|
|
FONT = 'DejaVu Sans'
|
|
FONT_M = 'DejaVu Sans Mono'
|
|
|
|
def setup_style():
|
|
plt.rcParams.update({
|
|
'figure.facecolor': BG,
|
|
'axes.facecolor': BG,
|
|
'axes.edgecolor': BORDER,
|
|
'axes.labelcolor': TEXT_DIM,
|
|
'text.color': TEXT,
|
|
'xtick.color': TEXT_DIM,
|
|
'ytick.color': TEXT_DIM,
|
|
'grid.color': '#1f1f1f',
|
|
'grid.linewidth': 0.5,
|
|
'font.family': 'sans-serif',
|
|
'font.sans-serif': [FONT],
|
|
'axes.grid': False,
|
|
'figure.dpi': 150,
|
|
'savefig.dpi': 150,
|
|
'savefig.bbox': 'tight',
|
|
'savefig.pad_inches': 0.3,
|
|
'savefig.facecolor': BG,
|
|
})
|
|
|
|
setup_style()
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 1. GLOBAL AGE VERIFICATION LEGISLATION TIMELINE
|
|
# ═══════════════════════════════════════════════════════════════
|
|
def chart_global_av_timeline():
|
|
fig, ax = plt.subplots(figsize=(16, 9))
|
|
ax.set_facecolor(BG)
|
|
|
|
events = [
|
|
(2022.0, 1, 'EU Digital Services Act signed', RED, 'left'),
|
|
(2022.15, 2, 'Louisiana: first US state AV law', BLUE, 'left'),
|
|
(2022.3, 3, 'California AADC signed (AB 2273)', BLUE, 'left'),
|
|
(2022.5, 4, 'Brazil PL 2628 introduced', AMBER, 'left'),
|
|
(2022.65, 5, 'US Senate receives KOSA', BLUE, 'left'),
|
|
(2022.8, 6, 'UK Online Safety Bill introduced', RED, 'left'),
|
|
(2022.95, 7, 'EU Chat Control proposal', RED, 'left'),
|
|
(2023.3, 4, 'UK Online Safety Act enacted', RED, 'right'),
|
|
(2023.6, 6, 'Australia social media ban passed', GREEN, 'right'),
|
|
(2024.2, 2, 'France age verification mandate', RED, 'right'),
|
|
(2024.0, 5, 'IEEE 2089.1 AV standard published', TEXT_DIM, 'right'),
|
|
(2024.5, 7, 'Malaysia, Indonesia AV laws', GREEN, 'right'),
|
|
(2024.8, 1, 'UK VPN usage surges 1,400%', '#e8e855', 'right'),
|
|
(2025.1, 3, 'Display FARA filing (California)', AMBER, 'left'),
|
|
(2025.4, 6, 'UK device surveillance amendment', RED, 'right'),
|
|
(2025.6, 4, 'Brazil Digital ECA signed', AMBER, 'right'),
|
|
(2026.0, 2, 'Brazil Digital ECA enforcement', AMBER, 'right'),
|
|
(2026.1, 5, 'UK Ofcom AV enforcement begins', RED, 'right'),
|
|
]
|
|
|
|
# Background year bands
|
|
for yr in range(2022, 2027):
|
|
if yr % 2 == 0:
|
|
ax.axvspan(yr, yr+1, color='#111111', alpha=0.3)
|
|
|
|
# Inflection point marker
|
|
ax.axvline(x=2022, color=RED, linewidth=1.5, linestyle='-', alpha=0.6)
|
|
ax.text(2022.05, 7.7, '2022: INFLECTION POINT', fontsize=8, fontweight='bold',
|
|
color=RED, va='top', fontfamily=FONT)
|
|
|
|
# Plot events
|
|
for x, y, label, color, align in events:
|
|
ax.scatter(x, y, s=50, color=color, zorder=5, edgecolors='none')
|
|
ha = 'left' if align == 'left' else 'right'
|
|
offset = 0.06 if align == 'left' else -0.06
|
|
ax.text(x + offset, y, label, fontsize=7.5, color=TEXT,
|
|
va='center', ha=ha, fontfamily=FONT)
|
|
|
|
# Legend
|
|
legend_items = [
|
|
(RED, 'UK Legislation'),
|
|
(AMBER, 'Brazil Legislation'),
|
|
(BLUE, 'US Legislation'),
|
|
(GREEN, 'Other Jurisdictions'),
|
|
]
|
|
for i, (c, lbl) in enumerate(legend_items):
|
|
ax.scatter(2025.5, 8.0 - i*0.45, s=30, color=c, zorder=5)
|
|
ax.text(2025.58, 8.0 - i*0.45, lbl, fontsize=7, color=TEXT_DIM,
|
|
va='center', fontfamily=FONT)
|
|
|
|
ax.set_xlim(2021.7, 2026.5)
|
|
ax.set_ylim(0.2, 8.5)
|
|
ax.set_xlabel('Year', fontsize=10, labelpad=10)
|
|
ax.set_xticks([2022, 2023, 2024, 2025, 2026])
|
|
ax.set_yticks([])
|
|
ax.tick_params(axis='x', length=0, pad=8)
|
|
ax.spines['top'].set_visible(False)
|
|
ax.spines['right'].set_visible(False)
|
|
ax.spines['left'].set_visible(False)
|
|
ax.spines['bottom'].set_color(BORDER)
|
|
|
|
fig.suptitle('Global Age Verification Legislation Timeline',
|
|
fontsize=16, fontweight='bold', color=WHITE, y=0.96, fontfamily=FONT)
|
|
ax.set_title('"The same organizations appear across every jurisdiction"',
|
|
fontsize=10, color=TEXT_DIM, style='italic', pad=15, fontfamily=FONT)
|
|
|
|
fig.savefig(f'{OUT}/chart_global_av_timeline.png')
|
|
plt.close()
|
|
print(' [1/10] chart_global_av_timeline.png')
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 2. THE STANDARDS CAPTURE LOOP (Kidron)
|
|
# ═══════════════════════════════════════════════════════════════
|
|
def chart_kidron_standards_capture():
|
|
fig, ax = plt.subplots(figsize=(10, 10))
|
|
ax.set_facecolor(BG)
|
|
ax.set_xlim(-2.2, 2.2)
|
|
ax.set_ylim(-2.5, 2.5)
|
|
ax.set_aspect('equal')
|
|
ax.axis('off')
|
|
|
|
fig.suptitle('The Standards Capture Loop',
|
|
fontsize=18, fontweight='bold', color=WHITE, y=0.94, fontfamily=FONT)
|
|
ax.set_title('One person connects legislation, standards, certification, and profit',
|
|
fontsize=10, color=TEXT_DIM, pad=20, fontfamily=FONT)
|
|
|
|
nodes = [
|
|
(0, 1.6, 'Baroness Kidron\nwrites legislation\n(UK AADC, OSA, CA AADC)', RED, AMBER),
|
|
(1.5, 0.5, 'Co-vice-chairs\nIEEE 2089.1\nAV Standard', RED, AMBER),
|
|
(1.5, -0.9, 'AVPA Executive Dir.\nchairs IEEE 2089.1\ncertification', RED, BLUE),
|
|
(0, -1.7, 'Yoti certified\nunder IEEE 2089.1\nRevenue: $47.9M', RED, GREEN),
|
|
(-1.5,-0.9, 'Meta pays Yoti\nfor Instagram\nage verification', RED, '#e8e855'),
|
|
(-1.5, 0.5, 'Legislation mandates\nage verification\n= creates market', RED, RED),
|
|
]
|
|
|
|
# Draw arrows between nodes (circular)
|
|
for i in range(len(nodes)):
|
|
x1, y1 = nodes[i][0], nodes[i][1]
|
|
x2, y2 = nodes[(i+1) % len(nodes)][0], nodes[(i+1) % len(nodes)][1]
|
|
dx = x2 - x1
|
|
dy = y2 - y1
|
|
dist = np.sqrt(dx**2 + dy**2)
|
|
# Shorten arrows
|
|
shrink = 0.42 / dist
|
|
sx, sy = x1 + dx * shrink, y1 + dy * shrink
|
|
ex, ey = x2 - dx * shrink, y2 - dy * shrink
|
|
ax.annotate('', xy=(ex, ey), xytext=(sx, sy),
|
|
arrowprops=dict(arrowstyle='->', color=TEXT_MUT,
|
|
lw=1.5, connectionstyle='arc3,rad=0.08'))
|
|
|
|
# Draw nodes
|
|
for x, y, label, ring_color, accent in nodes:
|
|
circle = plt.Circle((x, y), 0.4, fill=True, facecolor=BG_CARD,
|
|
edgecolor=ring_color, linewidth=2)
|
|
ax.add_patch(circle)
|
|
ax.text(x, y, label, fontsize=7, color=TEXT, ha='center', va='center',
|
|
fontfamily=FONT, linespacing=1.4)
|
|
|
|
fig.savefig(f'{OUT}/chart_kidron_standards_capture.png')
|
|
plt.close()
|
|
print(' [2/10] chart_kidron_standards_capture.png')
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 3. CROSS-BORDER FUNDING NETWORK
|
|
# ═══════════════════════════════════════════════════════════════
|
|
def chart_cross_border_funding():
|
|
fig, ax = plt.subplots(figsize=(14, 9))
|
|
ax.set_facecolor(BG)
|
|
ax.set_xlim(-0.5, 10.5)
|
|
ax.set_ylim(-0.5, 7)
|
|
ax.axis('off')
|
|
|
|
fig.suptitle('Cross-Border Funding Network',
|
|
fontsize=16, fontweight='bold', color=WHITE, y=0.96, fontfamily=FONT)
|
|
ax.set_title('The same funders appear in every jurisdiction',
|
|
fontsize=10, color=TEXT_DIM, pad=15, fontfamily=FONT)
|
|
|
|
def draw_box(x, y, w, h, label, border_color, bg=BG_CARD):
|
|
box = FancyBboxPatch((x - w/2, y - h/2), w, h,
|
|
boxstyle="round,pad=0.1", facecolor=bg,
|
|
edgecolor=border_color, linewidth=1.5)
|
|
ax.add_patch(box)
|
|
ax.text(x, y, label, fontsize=7.5, color=TEXT, ha='center', va='center',
|
|
fontfamily=FONT, linespacing=1.4)
|
|
|
|
# Tier labels
|
|
ax.text(-0.3, 6, 'FUNDERS', fontsize=7, color=TEXT_MUT, fontweight='bold',
|
|
rotation=90, va='center', fontfamily=FONT)
|
|
ax.text(-0.3, 3.5, 'ADVOCACY\nORGS', fontsize=7, color=TEXT_MUT, fontweight='bold',
|
|
rotation=90, va='center', ha='center', fontfamily=FONT)
|
|
ax.text(-0.3, 1, 'LEGISLATION', fontsize=7, color=TEXT_MUT, fontweight='bold',
|
|
rotation=90, va='center', fontfamily=FONT)
|
|
|
|
# Tier 1: Funders
|
|
funders = [
|
|
(1.5, 6, 'Oak\nFoundation'),
|
|
(3.5, 6, 'Omidyar\nNetwork'),
|
|
(5.5, 6, 'Luminate\n(Omidyar)'),
|
|
(7.5, 6, 'Imaginable\nFutures\n(Omidyar)'),
|
|
(9.5, 6, 'Rae/Sefaira\nFamily'),
|
|
]
|
|
for x, y, lbl in funders:
|
|
draw_box(x, y, 1.6, 0.7, lbl, AMBER)
|
|
|
|
# Tier 2: Advocacy
|
|
orgs = [
|
|
(1, 3.5, '5Rights\n(UK)'),
|
|
(3, 3.5, 'ECPAT'),
|
|
(5, 3.5, 'SaferNet\nBrasil'),
|
|
(7, 3.5, 'Common\nSense\nMedia (US)'),
|
|
(8.5, 3.5, 'Data Privacy\nBrasil'),
|
|
(10, 3.5, 'Instituto\nAlana (BR)'),
|
|
]
|
|
for x, y, lbl in orgs:
|
|
draw_box(x, y, 1.4, 0.7, lbl, RED)
|
|
|
|
# Tier 3: Legislation
|
|
laws = [
|
|
(1, 1, 'UK OSA\n+ AADC'),
|
|
(3, 1, 'EU DSA\n+ AI Act'),
|
|
(5.5, 1, 'Brazil\nDigital ECA'),
|
|
(7.5, 1, 'US KOSA\n+ COPPA 2.0'),
|
|
(9.5, 1, 'California\nAADC'),
|
|
]
|
|
for x, y, lbl in laws:
|
|
draw_box(x, y, 1.4, 0.6, lbl, GREEN)
|
|
|
|
# Draw connections (funder -> org -> legislation)
|
|
connections = [
|
|
# Oak Foundation connections
|
|
(1.5, 5.65, 1, 3.85), # Oak -> 5Rights
|
|
(1.5, 5.65, 3, 3.85), # Oak -> ECPAT
|
|
(1.5, 5.65, 5, 3.85), # Oak -> SaferNet
|
|
# Omidyar connections
|
|
(3.5, 5.65, 7, 3.85), # Omidyar -> Common Sense
|
|
(3.5, 5.65, 8.5, 3.85), # Omidyar -> Data Privacy BR
|
|
# Luminate connections
|
|
(5.5, 5.65, 1, 3.85), # Luminate -> 5Rights
|
|
(5.5, 5.65, 8.5, 3.85), # Luminate -> Data Privacy BR
|
|
# Imaginable Futures
|
|
(7.5, 5.65, 10, 3.85), # Imaginable -> Alana
|
|
# Rae/Sefaira
|
|
(9.5, 5.65, 1, 3.85), # Rae -> 5Rights
|
|
# Orgs -> Legislation
|
|
(1, 3.15, 1, 1.3), # 5Rights -> UK
|
|
(1, 3.15, 9.5, 1.3), # 5Rights -> CA AADC
|
|
(3, 3.15, 3, 1.3), # ECPAT -> EU
|
|
(5, 3.15, 5.5, 1.3), # SaferNet -> Brazil
|
|
(7, 3.15, 7.5, 1.3), # Common Sense -> US
|
|
(10, 3.15, 5.5, 1.3), # Alana -> Brazil
|
|
]
|
|
for x1, y1, x2, y2 in connections:
|
|
ax.plot([x1, x2], [y1, y2], color=TEXT_MUT, linewidth=0.7, alpha=0.5)
|
|
|
|
# Horizontal tier lines
|
|
ax.axhline(y=4.8, color=BORDER, linewidth=0.5, xmin=0.05, xmax=0.95)
|
|
ax.axhline(y=2.3, color=BORDER, linewidth=0.5, xmin=0.05, xmax=0.95)
|
|
|
|
fig.savefig(f'{OUT}/chart_cross_border_funding.png')
|
|
plt.close()
|
|
print(' [3/10] chart_cross_border_funding.png')
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 4. META DEPUTIES CRIMINAL RECORDS
|
|
# ═══════════════════════════════════════════════════════════════
|
|
def chart_meta_deputies_criminal():
|
|
fig, ax = plt.subplots(figsize=(12, 7))
|
|
|
|
names = ['Sostenes\nCavalcante', 'Gustavo\nGayer', 'Jadyel\nAlencar',
|
|
'Fernando\nMaximo', 'Gilvan da\nFederal']
|
|
fraud = [0.5, 0.1, 48.0, 30.0, 0]
|
|
seized = [0.47, 0.07, 0, 0, 0]
|
|
|
|
x = np.arange(len(names))
|
|
w = 0.55
|
|
|
|
bars1 = ax.bar(x, fraud, w, color=RED, label='Suspected Fraud (R$ Millions)', zorder=3)
|
|
bars2 = ax.bar(x, seized, w, bottom=fraud, color=RED_DK,
|
|
label='Cash Seized (R$ Millions)', zorder=3)
|
|
|
|
# Value labels
|
|
for i, (f, s) in enumerate(zip(fraud, seized)):
|
|
total = f + s
|
|
if total > 0:
|
|
ax.text(i, total + 0.8, f'R${f:.0f}M' if f >= 1 else f'R${f}M',
|
|
ha='center', va='bottom', fontsize=8.5, color=TEXT,
|
|
fontweight='bold', fontfamily=FONT)
|
|
if s > 0:
|
|
ax.text(i + 0.32, f + s/2, f'R${s}M',
|
|
ha='left', va='center', fontsize=7, color=TEXT_DIM,
|
|
fontfamily=FONT)
|
|
|
|
ax.set_xticks(x)
|
|
ax.set_xticklabels(names, fontsize=9, fontfamily=FONT)
|
|
ax.set_ylabel('R$ Millions', fontsize=10, fontfamily=FONT)
|
|
ax.set_ylim(0, 55)
|
|
ax.spines['top'].set_visible(False)
|
|
ax.spines['right'].set_visible(False)
|
|
ax.spines['left'].set_color(BORDER)
|
|
ax.spines['bottom'].set_color(BORDER)
|
|
ax.tick_params(axis='x', length=0, pad=10)
|
|
ax.yaxis.grid(True, color='#1f1f1f', linewidth=0.5)
|
|
ax.set_axisbelow(True)
|
|
|
|
ax.legend(loc='upper right', fontsize=8, framealpha=0, labelcolor=TEXT_DIM)
|
|
|
|
fig.suptitle('Every Deputy Meta Contacted on PL 2628 Voting Day',
|
|
fontsize=14, fontweight='bold', color=WHITE, y=0.96, fontfamily=FONT)
|
|
ax.set_title('Is Under Criminal Investigation or Convicted',
|
|
fontsize=11, color=RED, pad=10, fontfamily=FONT)
|
|
|
|
fig.savefig(f'{OUT}/chart_meta_deputies_criminal.png')
|
|
plt.close()
|
|
print(' [4/10] chart_meta_deputies_criminal.png')
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 5. ALENCAR FINANCIAL PROFILE
|
|
# ═══════════════════════════════════════════════════════════════
|
|
def chart_alencar_financial():
|
|
fig, ax = plt.subplots(figsize=(12, 7))
|
|
|
|
categories = [
|
|
'Declared Assets',
|
|
'Suspected Money\nLaundering',
|
|
'COVID Fraud\n(MPF damages)',
|
|
'Unpaid\nChild Support',
|
|
]
|
|
values = [107.5, 48.0, 19.0, 0.036]
|
|
colors = [RED_DK, RED, RED, RED_LT]
|
|
|
|
y = np.arange(len(categories))
|
|
bars = ax.barh(y, values, height=0.55, color=colors, zorder=3)
|
|
|
|
for i, (v, bar) in enumerate(zip(values, bars)):
|
|
label = f'R${v:.1f}M' if v >= 1 else f'R${v*1000:.0f}K'
|
|
ax.text(v + 1.5, i, label, va='center', ha='left',
|
|
fontsize=10, fontweight='bold', color=TEXT, fontfamily=FONT)
|
|
|
|
ax.set_yticks(y)
|
|
ax.set_yticklabels(categories, fontsize=9.5, fontfamily=FONT)
|
|
ax.set_xlabel('R$ Millions', fontsize=10, fontfamily=FONT)
|
|
ax.set_xlim(0, 130)
|
|
ax.invert_yaxis()
|
|
ax.spines['top'].set_visible(False)
|
|
ax.spines['right'].set_visible(False)
|
|
ax.spines['left'].set_color(BORDER)
|
|
ax.spines['bottom'].set_color(BORDER)
|
|
ax.tick_params(axis='y', length=0, pad=10)
|
|
ax.xaxis.grid(True, color='#1f1f1f', linewidth=0.5)
|
|
ax.set_axisbelow(True)
|
|
|
|
fig.suptitle("Jadyel Alencar: Rapporteur of Brazil's Child Protection Law",
|
|
fontsize=14, fontweight='bold', color=WHITE, y=0.96, fontfamily=FONT)
|
|
ax.set_title('Financial Profile (R$ Millions)',
|
|
fontsize=11, color=TEXT_DIM, pad=10, fontfamily=FONT)
|
|
|
|
# Footer annotation
|
|
fig.text(0.5, 0.02,
|
|
'Criminal conviction for stolen medical supplies | 93 judicial processes | Prison order for R$6,306 child support',
|
|
ha='center', fontsize=7.5, color=TEXT_MUT, fontfamily=FONT)
|
|
|
|
fig.savefig(f'{OUT}/chart_alencar_financial.png')
|
|
plt.close()
|
|
print(' [5/10] chart_alencar_financial.png')
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 6. META REVOLVING DOOR: BRAZIL
|
|
# ═══════════════════════════════════════════════════════════════
|
|
def chart_meta_revolving_door_brazil():
|
|
fig, ax = plt.subplots(figsize=(12, 8))
|
|
ax.set_facecolor(BG)
|
|
ax.set_xlim(0, 10)
|
|
ax.set_ylim(0, 8)
|
|
ax.axis('off')
|
|
|
|
fig.suptitle("Meta's Revolving Door: Brazil",
|
|
fontsize=16, fontweight='bold', color=WHITE, y=0.95, fontfamily=FONT)
|
|
ax.set_title('19 government relations professionals. Two-thirds previously worked in government.',
|
|
fontsize=9.5, color=TEXT_DIM, pad=15, fontfamily=FONT)
|
|
|
|
def draw_panel(x, y, w, h, title, border_color):
|
|
box = FancyBboxPatch((x, y), w, h, boxstyle="round,pad=0.15",
|
|
facecolor=BG_CARD, edgecolor=border_color, linewidth=1.5)
|
|
ax.add_patch(box)
|
|
ax.text(x + w/2, y + h - 0.25, title, fontsize=10, fontweight='bold',
|
|
color=border_color, ha='center', va='top', fontfamily=FONT)
|
|
|
|
# Government panel (left)
|
|
draw_panel(0.5, 0.5, 3.2, 6.5, 'GOVERNMENT', BLUE)
|
|
# Meta panel (right)
|
|
draw_panel(6.3, 0.5, 3.2, 6.5, 'META', RED)
|
|
|
|
people = [
|
|
('Dario Durigan', 'Finance Minister\n(March 2026)', 'WhatsApp Dir.\n(2020-23)'),
|
|
('Yana Dumaresq', 'Deputy Minister\nof Economy\n(2019-21)', 'Meta Policy'),
|
|
('Murillo Laranjeira', 'Petri Consultancy\n(7 years)', 'Meta Sr. Dir.\nPublic Policy'),
|
|
('Tais Niffinegger', 'Anatel +\nPresidency', 'Meta'),
|
|
('Kaliana Kalache', 'Senate staff', 'Meta Dir.\n(promoted after\nBR law win)'),
|
|
]
|
|
|
|
for i, (name, gov_role, meta_role) in enumerate(people):
|
|
ypos = 6.0 - i * 1.15
|
|
|
|
# Government role
|
|
ax.text(1.2, ypos, gov_role, fontsize=7, color=TEXT_DIM, ha='left', va='center',
|
|
fontfamily=FONT, linespacing=1.3)
|
|
|
|
# Name (center)
|
|
ax.text(5, ypos, name, fontsize=9.5, fontweight='bold', color=WHITE,
|
|
ha='center', va='center', fontfamily=FONT,
|
|
bbox=dict(boxstyle='round,pad=0.3', facecolor=BG_PANEL,
|
|
edgecolor=BORDER, linewidth=1))
|
|
|
|
# Meta role
|
|
ax.text(8.8, ypos, meta_role, fontsize=7, color=TEXT_DIM, ha='right', va='center',
|
|
fontfamily=FONT, linespacing=1.3)
|
|
|
|
# Arrows
|
|
ax.annotate('', xy=(4.1, ypos), xytext=(3.5, ypos),
|
|
arrowprops=dict(arrowstyle='->', color=AMBER, lw=1.2))
|
|
ax.annotate('', xy=(5.9, ypos), xytext=(6.5, ypos),
|
|
arrowprops=dict(arrowstyle='<-', color=AMBER, lw=1.2))
|
|
|
|
fig.savefig(f'{OUT}/chart_meta_revolving_door_brazil.png')
|
|
plt.close()
|
|
print(' [6/10] chart_meta_revolving_door_brazil.png')
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 7. BRAZILIAN SECOM ADVERTISING SPEND
|
|
# ═══════════════════════════════════════════════════════════════
|
|
def chart_brazil_secom_meta_spend():
|
|
fig, ax = plt.subplots(figsize=(12, 7))
|
|
|
|
years = ['2023', '2024', '2025']
|
|
total = [47, 42, 129.6]
|
|
meta = [47 * 0.85, 42 * 0.82, 35.8]
|
|
|
|
x = np.arange(len(years))
|
|
w = 0.35
|
|
|
|
bars_total = ax.bar(x - w/2, total, w, color=RED_DK, label='Total Internet Ad Spend', zorder=3)
|
|
bars_meta = ax.bar(x + w/2, meta, w, color=RED, label='Meta (Facebook + Instagram)', zorder=3)
|
|
|
|
# Value labels
|
|
for i, (t, m) in enumerate(zip(total, meta)):
|
|
ax.text(i - w/2, t + 2, f'R${t:.0f}M' if t == int(t) else f'R${t:.1f}M',
|
|
ha='center', va='bottom', fontsize=9, fontweight='bold',
|
|
color=TEXT, fontfamily=FONT)
|
|
ax.text(i + w/2, m + 2, f'R${m:.1f}M',
|
|
ha='center', va='bottom', fontsize=9, fontweight='bold',
|
|
color=TEXT, fontfamily=FONT)
|
|
|
|
ax.set_xticks(x)
|
|
ax.set_xticklabels(years, fontsize=11, fontfamily=FONT)
|
|
ax.set_ylabel('Millions (R$)', fontsize=10, fontfamily=FONT)
|
|
ax.set_ylim(0, 150)
|
|
ax.spines['top'].set_visible(False)
|
|
ax.spines['right'].set_visible(False)
|
|
ax.spines['left'].set_color(BORDER)
|
|
ax.spines['bottom'].set_color(BORDER)
|
|
ax.tick_params(axis='x', length=0, pad=10)
|
|
ax.yaxis.grid(True, color='#1f1f1f', linewidth=0.5)
|
|
ax.set_axisbelow(True)
|
|
ax.legend(loc='upper left', fontsize=8.5, framealpha=0, labelcolor=TEXT_DIM)
|
|
|
|
fig.suptitle('Brazilian Federal Government (SECOM)',
|
|
fontsize=14, fontweight='bold', color=WHITE, y=0.96, fontfamily=FONT)
|
|
ax.set_title('Internet Advertising Spend',
|
|
fontsize=11, color=TEXT_DIM, pad=10, fontfamily=FONT)
|
|
|
|
fig.text(0.5, 0.02,
|
|
'Source: Nucleo, Poder360 | A former Meta executive (Dario Durigan) became Finance Minister March 2026',
|
|
ha='center', fontsize=7.5, color=TEXT_MUT, fontfamily=FONT)
|
|
|
|
fig.savefig(f'{OUT}/chart_brazil_secom_meta_spend.png')
|
|
plt.close()
|
|
print(' [7/10] chart_brazil_secom_meta_spend.png')
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 8. CONSELHO DIGITAL: ZERO CAPITAL
|
|
# ═══════════════════════════════════════════════════════════════
|
|
def chart_conselho_digital_zero_capital():
|
|
fig, ax = plt.subplots(figsize=(12, 8))
|
|
|
|
companies = ['Google', 'Meta', 'Amazon', 'TikTok', 'Discord',
|
|
'Uber', 'Kwai', 'Hotmart']
|
|
mcap = [2100, 1500, 1900, 220, 15, 170, 30, 5]
|
|
colors_list = [RED, RED, RED, RED, BLUE_DK, BLUE_DK, BLUE_DK, BLUE_DK]
|
|
|
|
x = np.arange(len(companies))
|
|
bars = ax.bar(x, mcap, width=0.6, color=colors_list, zorder=3)
|
|
|
|
for i, (c, v) in enumerate(zip(companies, mcap)):
|
|
label = f'${v}B'
|
|
ax.text(i, v + 30, label, ha='center', va='bottom',
|
|
fontsize=9, fontweight='bold', color=TEXT, fontfamily=FONT)
|
|
|
|
ax.set_xticks(x)
|
|
ax.set_xticklabels(companies, fontsize=9, fontfamily=FONT, rotation=0)
|
|
ax.set_ylabel('Market Cap (USD Billions)', fontsize=10, fontfamily=FONT)
|
|
ax.set_ylim(0, 2400)
|
|
ax.spines['top'].set_visible(False)
|
|
ax.spines['right'].set_visible(False)
|
|
ax.spines['left'].set_color(BORDER)
|
|
ax.spines['bottom'].set_color(BORDER)
|
|
ax.tick_params(axis='x', length=0, pad=10)
|
|
ax.yaxis.grid(True, color='#1f1f1f', linewidth=0.5)
|
|
ax.set_axisbelow(True)
|
|
|
|
# Callout box
|
|
callout_text = ('Single registered officer\n'
|
|
'R$0 declared capital\n'
|
|
'No public financial reports\n'
|
|
'No auditable records\n'
|
|
'Estatuto behind "restricted access"')
|
|
ax.text(6.5, 1500, callout_text, fontsize=7.5, color=TEXT,
|
|
fontfamily=FONT, linespacing=1.5, va='center',
|
|
bbox=dict(boxstyle='round,pad=0.5', facecolor=BG_CARD,
|
|
edgecolor=RED, linewidth=1.5))
|
|
|
|
fig.suptitle("Conselho Digital do Brasil: Members' Combined Market Cap",
|
|
fontsize=14, fontweight='bold', color=WHITE, y=0.96, fontfamily=FONT)
|
|
ax.set_title("vs. Entity's Declared Capital: R$0.00",
|
|
fontsize=11, color=RED, pad=10, fontfamily=FONT)
|
|
|
|
fig.savefig(f'{OUT}/chart_conselho_digital_zero_capital.png')
|
|
plt.close()
|
|
print(' [8/10] chart_conselho_digital_zero_capital.png')
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 9. SERPRO DATA ACCESS
|
|
# ═══════════════════════════════════════════════════════════════
|
|
def chart_serpro_data_access():
|
|
fig, ax = plt.subplots(figsize=(13, 7))
|
|
|
|
categories = [
|
|
'ABIN: 76M citizens\' driver data\n(Intercept Brasil, 2020)',
|
|
'PRF: 80M+ biometric database\npurchased off-books (2022)',
|
|
'Serpro employee: STF ministers\'\ntax records accessed (2026)',
|
|
'ANPD: Datavalid operates without\nfull LGPD legal basis',
|
|
'Digital ECA: All platform users\nmust be verified via Serpro',
|
|
]
|
|
values = [76, 80, 0, 85, 200]
|
|
labels_r = ['76M records', '80M records', 'Targeted access', '85M records', '200M records']
|
|
colors_list = [RED_DK, RED_DK, RED, RED, RED_LT]
|
|
|
|
y = np.arange(len(categories))
|
|
bars = ax.barh(y, values, height=0.55, color=colors_list, zorder=3)
|
|
|
|
# Special handling for zero-width bar
|
|
for i, (v, lbl) in enumerate(zip(values, labels_r)):
|
|
xpos = max(v, 5) + 3
|
|
ax.text(xpos, i, lbl, va='center', ha='left',
|
|
fontsize=9.5, fontweight='bold', color=TEXT, fontfamily=FONT)
|
|
|
|
ax.set_yticks(y)
|
|
ax.set_yticklabels(categories, fontsize=8.5, fontfamily=FONT)
|
|
ax.set_xlabel('Records Affected (Millions)', fontsize=10, fontfamily=FONT)
|
|
ax.set_xlim(0, 240)
|
|
ax.invert_yaxis()
|
|
ax.spines['top'].set_visible(False)
|
|
ax.spines['right'].set_visible(False)
|
|
ax.spines['left'].set_color(BORDER)
|
|
ax.spines['bottom'].set_color(BORDER)
|
|
ax.tick_params(axis='y', length=0, pad=10)
|
|
ax.xaxis.grid(True, color='#1f1f1f', linewidth=0.5)
|
|
ax.set_axisbelow(True)
|
|
|
|
fig.suptitle("Serpro: Brazil's Government Data Broker",
|
|
fontsize=14, fontweight='bold', color=WHITE, y=0.96, fontfamily=FONT)
|
|
ax.set_title('33 Billion Transactions/Year, Intelligence Agency Access',
|
|
fontsize=11, color=TEXT_DIM, pad=10, fontfamily=FONT)
|
|
|
|
fig.text(0.5, 0.02,
|
|
'Serpro\'s "sovereign cloud" runs on AWS Outpost (CIA partnership) and Google Distributed Cloud | US CLOUD Act applies',
|
|
ha='center', fontsize=7.5, color=TEXT_MUT, fontfamily=FONT)
|
|
|
|
fig.savefig(f'{OUT}/chart_serpro_data_access.png')
|
|
plt.close()
|
|
print(' [9/10] chart_serpro_data_access.png')
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 10. BARCLAYS / PALANTIR / META FINANCIAL LAYER
|
|
# ═══════════════════════════════════════════════════════════════
|
|
def chart_barclays_palantir_meta():
|
|
fig, ax = plt.subplots(figsize=(13, 9))
|
|
ax.set_facecolor(BG)
|
|
ax.set_xlim(0, 12)
|
|
ax.set_ylim(0, 9)
|
|
ax.axis('off')
|
|
|
|
fig.suptitle('The Financial Layer',
|
|
fontsize=16, fontweight='bold', color=WHITE, y=0.95, fontfamily=FONT)
|
|
ax.set_title('Barclays, Palantir, and Meta: Overlapping ownership, regulation, and contracts',
|
|
fontsize=9.5, color=TEXT_DIM, pad=15, fontfamily=FONT)
|
|
|
|
def box(x, y, label, border_color, w=1.8, h=0.6):
|
|
b = FancyBboxPatch((x - w/2, y - h/2), w, h,
|
|
boxstyle="round,pad=0.1", facecolor=BG_CARD,
|
|
edgecolor=border_color, linewidth=1.5)
|
|
ax.add_patch(b)
|
|
ax.text(x, y, label, fontsize=8, color=TEXT, ha='center', va='center',
|
|
fontfamily=FONT, linespacing=1.3)
|
|
return (x, y)
|
|
|
|
def arrow(x1, y1, x2, y2, label='', color=TEXT_MUT, style='-'):
|
|
ax.annotate('', xy=(x2, y2), xytext=(x1, y1),
|
|
arrowprops=dict(arrowstyle='->', color=color, lw=1,
|
|
linestyle=style))
|
|
if label:
|
|
mx, my = (x1+x2)/2, (y1+y2)/2
|
|
ax.text(mx, my + 0.15, label, fontsize=6.5, color=TEXT_MUT,
|
|
ha='center', va='bottom', fontfamily=FONT,
|
|
bbox=dict(boxstyle='round,pad=0.15', facecolor=BG,
|
|
edgecolor='none'))
|
|
|
|
# Top row
|
|
box(2.5, 7.5, 'Barclays', BLUE)
|
|
box(9.5, 7.5, 'Meta', RED)
|
|
|
|
# Middle row
|
|
box(2.5, 5.5, 'FCA\n(regulator)', BLUE)
|
|
box(6, 5.5, 'Peter Thiel', AMBER)
|
|
box(9.5, 5.5, 'Yoti', GREEN)
|
|
|
|
# Bottom row
|
|
box(4.5, 3, 'Palantir', AMBER)
|
|
box(7.5, 3, 'Founders\nFund', AMBER)
|
|
|
|
# Bottom
|
|
box(4.5, 1, 'Serpro\n(Brazil govt)', GREEN)
|
|
|
|
# Connections
|
|
arrow(2.5, 7.2, 2.5, 5.8, 'FCA regulates\nBarclays')
|
|
arrow(2.5, 7.2, 9.5, 7.8, '£1.4B in shares', AMBER, '-')
|
|
arrow(9.5, 7.2, 9.5, 5.8, 'Pays for Instagram\nage verification')
|
|
arrow(9.5, 7.2, 6, 5.8, 'Thiel: board\nsince 2005', TEXT_MUT, '--')
|
|
arrow(6, 5.2, 4.5, 3.3, 'Co-founder')
|
|
arrow(6, 5.2, 7.5, 3.3, 'Founder')
|
|
arrow(4.5, 5.2, 4.5, 3.3, 'Palantir processes\nFCA data') # FCA connection to Palantir (adjust coords)
|
|
arrow(7.5, 2.7, 4.5, 1.3, '$150M dividend')
|
|
arrow(4.5, 2.7, 4.5, 1.3, 'Palantir since 2022')
|
|
|
|
# Fix FCA to Palantir arrow
|
|
arrow(2.8, 5.2, 4.2, 3.3, 'Palantir processes\nFCA data')
|
|
|
|
fig.savefig(f'{OUT}/chart_barclays_palantir_meta.png')
|
|
plt.close()
|
|
print(' [10/10] chart_barclays_palantir_meta.png')
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# GENERATE ALL
|
|
# ═══════════════════════════════════════════════════════════════
|
|
print('Generating charts...')
|
|
chart_global_av_timeline()
|
|
chart_kidron_standards_capture()
|
|
chart_cross_border_funding()
|
|
chart_meta_deputies_criminal()
|
|
chart_alencar_financial()
|
|
chart_meta_revolving_door_brazil()
|
|
chart_brazil_secom_meta_spend()
|
|
chart_conselho_digital_zero_capital()
|
|
chart_serpro_data_access()
|
|
chart_barclays_palantir_meta()
|
|
print('Done. Output in:', OUT)
|