"""Draw the study's charts as SVG from results/portals.csv.
python3 make_charts.py results/portals.csv out_dir/
Standard library only. Every chart states its own basis in the image, because
an embedded chart travels without the page's caption.
"""
import csv, os, sys
from xml.sax.saxutils import escape
NAMES = {"armor": "Armor Games", "coolmath": "Coolmath Games", "abcya": "ABCya", "kizi": "Kizi",
"y8": "Y8", "itch": "itch.io", "friv": "Friv", "manxlab": "MANXLAB (ours)"}
BG, CARD, TEXT, MUTED, RULE = "#08120f", "#0d1a16", "#f0ede4", "#8fa89c", "#1d2f29"
HOT, MID, COOL = "#e8735f", "#e0b35a", "#3bd48f"
FONT = "Inter, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
BASIS = ("Median of 10 games per portal (8 for MANXLAB), each the median of 3 loads. "
"Chrome, US connection, 28 Sep 2026, nothing clicked.")
CREDIT = "manxlab.com/research/free-game-trackers"
def colour(v):
return HOT if v >= 100 else MID if v >= 5 else COOL
def fmt(v):
v = float(v)
return f"{v:,.0f}" if v == int(v) else f"{v:,.1f}"
def wrap(text, n):
words, lines, cur = text.split(), [], ""
for w in words:
if len(cur) + len(w) + 1 > n:
lines.append(cur)
cur = w
else:
cur = (cur + " " + w).strip()
return lines + [cur]
def bars(path, title, subtitle, rows, value_key, range_keys=None, unit=""):
W, L, R, top, band = 720, 170, 170, 110, 40
rows = sorted(rows, key=lambda r: -float(r[value_key]))
vmax = max(float(r[value_key]) for r in rows) or 1
H = top + band * len(rows) + 110
s = [f'')
open(path, "w").write("\n".join(s))
print("wrote", path, f"{W}x{H}")
def timeline(path, rows):
W, H, L, R, top, bottom = 720, 470, 70, 210, 110, 370
title = "The auction starts before the game does"
sub = "Advertising and tracking companies contacted, counted at 5, 10, 30 and 60 seconds after the page opened."
xs = [5, 10, 30, 60]
keep = [r for r in rows if r["portal"] in ("armor", "coolmath", "abcya", "manxlab")]
vmax = max(float(r["ad_tracking_companies_60s"]) for r in keep)
px = lambda i: L + (W - L - R) * i / (len(xs) - 1)
py = lambda v: bottom - (bottom - top) * v / vmax
s = [f'')
open(path, "w").write("\n".join(s))
print("wrote", path, f"{W}x{H}")
def main(portals_csv, out_dir):
rows = list(csv.DictReader(open(portals_csv)))
os.makedirs(out_dir, exist_ok=True)
bars(os.path.join(out_dir, "companies.svg"),
"Ad and tracking companies, first minute",
"Advertising and tracking companies a free game page contacted in its first 60 seconds, before anything was clicked.",
rows, "ad_tracking_companies_60s", ("ad_tracking_companies_60s_min", "ad_tracking_companies_60s_max"))
bars(os.path.join(out_dir, "cookies.svg"),
"Outside domains setting cookies",
"Domains other than the portal's own that had set a cookie after 60 seconds, before anything was clicked.",
rows, "third_party_cookie_domains_60s", ("third_party_cookie_domains_60s_min", "third_party_cookie_domains_60s_max"))
bars(os.path.join(out_dir, "requests.svg"),
"Network requests, first minute",
"Requests a free game page made in its first 60 seconds, before anything was clicked.",
rows, "requests_60s", ("requests_60s_min", "requests_60s_max"))
timeline(os.path.join(out_dir, "timeline.svg"), rows)
if __name__ == "__main__":
main(*sys.argv[1:])