marathon-live / worldmap.py
kalamishere's picture
overnight fix batch: share links, unlock repaint, report cache, 23 bug fixes
1a05ceb verified
Raw
History Blame Contribute Delete
34.8 kB
"""The opening block of every report: three plain sentences and a world map.
Kalam's brief, twice: "open the page with high level analysis and summary plus
a world map of relevant regions highlighted with notes on hover — then when you
scroll down it all slots in for the reader." The hover half became a tap:
the team reads this on phones.
**What is here and what is not.** The cartography is not new. The country
outlines are the same vendored Natural Earth 110m paths
(`marathon/static/world_paths.json`, keyed ISO-2) that `marathon/trackmap.py`
already renders a standalone map from. This module reuses the file and the
verdict bands, and adapts them into the report.
**Why the interaction is anchors and not script.** Scripts injected into a
Gradio HTML component do not run — the same constraint that made `render.py`
resolve Deezer previews server-side. So every tappable thing here is an
`<a href="#market-XX">`, which is the browser's own behaviour and needs no
JavaScript at all. `render.py` gives each market card a matching `id`, so a
tap lands on the card, and the note the reader wants is the card.
Two of those live side by side on purpose:
- the countries on the map, each shown market's outline wrapped in an `<a>`;
- a chip row directly under the map, one chip per market card.
**Measured in the real thing, Gradio 6.19.0 at a 375px viewport.** The block
is in the ordinary document — no shadow DOM, no iframe — and both kinds of
anchor jump correctly: `scrollTop` 594 → 3952 with the card landing at the
14px `scroll-margin-top`. But Gradio's own chrome takes the map down to
261px wide, which puts Kenya's centre 4px from Uganda's and 7px from
Tanzania's. A marker dot big enough to see there is a blob over the exact
cluster that matters, and an invisible tap target big enough to hit is a
coin flip between three markets — a tap that opens the wrong card is a bug,
not an affordance. So there are no markers. The countries carry the colour,
and the chips are the interaction: 138 × 34px each, in rank order.
`<title>` on each coloured country gives the native tooltip where a pointer
exists. Touch browsers do not show it on long-press, so nothing load-bearing
is written there — it repeats what the card says.
"""
from __future__ import annotations
import html
import json
import re
from pathlib import Path
import livematch
ESC = html.escape
# Above this cosine, the nearest charting record IS the track being analysed.
# The re-embed parity checks land at 1.0000000 for the same audio through two
# code paths, and a released track measured from its own Deezer preview meets
# its corpus row at the same place. 0.995 leaves room for the preview being a
# different master or encode of the record. It happened the first time the
# archive was seeded: Joshua Baraka's "What Do I Know" read 100.0% in Uganda
# because it is charting in Uganda. Without a line saying so that reads as a
# spectacular fit rather than as the track recognising its own reflection.
SAME_RECORDING = 0.995
# Short band words. The long form with the cut-offs lives in render.CAPTION and
# is printed next to every chip; these are the same three verdicts inside a
# sentence.
BAND_WORD = {"place": "a direct fit",
"derive": "bridgeable with an edit or remix",
"pass": "no real connection"}
# --- the flat field --------------------------------------------------------
#
# A ranked list always has a top row, and a top row always looks like a
# finding. Sometimes it is not one. `reports/AU_CA_CHART_CHECK.md` traced
# Joshua Baraka's "What Do I Know" reading Australia, Canada and New Zealand
# as its three best markets at 84%: the charts were right, the audio was
# right, and the reading was still empty. The track scores 0.76–0.84 in every
# one of 88 markets (median 0.8088, sd 0.0204), clears the place line nowhere,
# and those three markets tie to the fourth decimal because they are the only
# three carrying one Punjabi record — Parmish Verma — "Dekhi Ja" — that
# happens to be its nearest texture neighbour. Drop that single sound and
# Australia falls below the global median.
#
# So the shape of the field is read before anything is said about a market.
# When the field is flat the opening says so, the map stops colouring anything
# as a choice, and the ranking below is introduced as the weak signal it is.
FLAT_SPREAD = 0.05
HERE = Path(__file__).resolve().parent
# The staged Space is flat — `livedeploy.stage` copies the vendored paths in
# next to this file, the same way it copies `livematch.py`. The dev checkout
# reads the one original in the package, so there is no second copy to drift.
PATH_CANDIDATES = (HERE / "world_paths.json",
HERE.parent / "marathon" / "static" / "world_paths.json")
_world: dict | None = None
def world() -> dict:
"""The vendored outlines, loaded once. `{viewBox, countries: {ISO2: {name, d}}}`."""
global _world
if _world is None:
for p in PATH_CANDIDATES:
if p.exists():
_world = json.loads(p.read_text())
break
else:
raise FileNotFoundError(
"world_paths.json not found in " +
" or ".join(str(p) for p in PATH_CANDIDATES))
return _world
# --- region families -------------------------------------------------------
#
# The first sentence of the report answers "which part of the world" before it
# answers "which country", because a green cluster across Kenya, Uganda and
# Tanzania is one finding and three numbers are not. Families are the coarse
# groupings a label actually plans against, not UN statistical regions:
# Latin America is one family rather than four, because that is how the
# markets are worked.
_FAMILY_MEMBERS = {
"East Africa": "KE UG TZ ET RW BI SO DJ ER SS MU MG KM SC",
"West Africa": "NG GH CI SN ML BF BJ TG GN LR SL GM NE MR CV GW",
"Central Africa": "CM CD CG GA TD CF GQ AO ST",
"Southern Africa": "ZA ZW MZ BW NA LS SZ ZM MW",
"North Africa": "EG MA DZ TN LY SD",
"the Middle East": "AE SA IL TR QA KW OM BH JO LB IQ IR YE PS SY",
"South Asia": "IN PK BD LK NP BT MV AF",
"Southeast Asia": "ID MY PH TH VN SG KH MM LA BN TL",
"East Asia": "JP KR TW HK CN MN KP",
"Western Europe": "GB IE FR DE NL BE LU AT CH",
"Northern Europe": "SE NO DK FI IS EE LV LT",
"Southern Europe": "ES PT IT GR CY MT HR SI RS AL BA ME MK XK",
"Central and Eastern Europe": "PL CZ SK HU RO BG UA RU BY MD",
"North America": "US CA",
"Latin America": ("MX GT HN SV NI CR PA DO JM CU TT PR HT BR AR CL CO PE "
"VE EC BO PY UY BZ GY SR BS BB"),
"Oceania": "AU NZ FJ PG NC VU SB WS TO",
}
FAMILY = {iso: fam for fam, isos in _FAMILY_MEMBERS.items()
for iso in isos.split()}
# The coarse grouping above the family. Read against the real YO YO result,
# the family alone undersold the finding: Kenya, Tanzania and Uganda are three
# of the eight closest markets, but Portugal sits at number three and Senegal
# and Nigeria at five and seven, so "2 of its 5 nearest markets are in East
# Africa" reads as a weak claim about a strong one. Six of the eight are in
# Africa. The continent is the headline and the family is the detail inside
# it.
_CONTINENT_OF = {
"East Africa": "Africa", "West Africa": "Africa",
"Central Africa": "Africa", "Southern Africa": "Africa",
"North Africa": "Africa",
"Western Europe": "Europe", "Northern Europe": "Europe",
"Southern Europe": "Europe", "Central and Eastern Europe": "Europe",
"North America": "the Americas", "Latin America": "the Americas",
"South Asia": "Asia", "Southeast Asia": "Asia", "East Asia": "Asia",
"the Middle East": "the Middle East", "Oceania": "Oceania",
}
CONTINENT = {iso: _CONTINENT_OF[fam] for iso, fam in FAMILY.items()}
# --- geometry --------------------------------------------------------------
_NUM = re.compile(r"-?\d+(?:\.\d+)?")
def centre(d: str) -> tuple[float, float] | None:
"""A point to hang a marker on: the vertex mean of the country's largest
subpath.
Largest, not all, because a mean over every subpath drags the United States
into the Pacific and Norway into the Atlantic — Alaska, Hawaii and a few
hundred islands outvote the mainland. Vertex mean rather than bounding-box
centre because a long thin country (Chile, Norway) has a box centre that
sits well off it.
"""
best_n, best = 0, None
for sub in d.split("M"):
nums = [float(n) for n in _NUM.findall(sub)]
xs, ys = nums[0::2], nums[1::2]
if len(xs) < 3 or len(xs) != len(ys):
continue
if len(xs) > best_n:
best_n = len(xs)
best = (sum(xs) / len(xs), sum(ys) / len(ys))
return best
# --- the summary sentences -------------------------------------------------
def _pct(x: float) -> str:
return livematch.pct(x)
# Every market whose outline the 110m country set does not carry. The corpus
# takes its market names out of that same file, so these came through with
# the ISO code as the name and the app printed them that way — on the market
# cards, on the chips, and inside the pitch prose a label manager pastes into
# a delivery form: "records charting in Czechia and SG" (23 Aug audit). Four
# of them are in this week's charts; the rest are here so the next small
# market to chart does not repeat it.
NO_OUTLINE = {
"AD": "Andorra", "AG": "Antigua and Barbuda", "AI": "Anguilla",
"AS": "American Samoa", "AW": "Aruba", "BB": "Barbados",
"BH": "Bahrain", "BM": "Bermuda", "CV": "Cape Verde", "CW": "Curaçao",
"DM": "Dominica", "FM": "Micronesia", "FO": "the Faroe Islands",
"GD": "Grenada", "GI": "Gibraltar", "GU": "Guam", "HK": "Hong Kong",
"JE": "Jersey", "KI": "Kiribati", "KM": "Comoros",
"KN": "St Kitts and Nevis", "KY": "the Cayman Islands", "LC": "St Lucia",
"LI": "Liechtenstein", "MC": "Monaco", "MH": "the Marshall Islands",
"MO": "Macao", "MT": "Malta", "MU": "Mauritius", "MV": "the Maldives",
"NR": "Nauru", "PW": "Palau", "SC": "Seychelles", "SG": "Singapore",
"SM": "San Marino", "ST": "São Tomé and Príncipe",
"SX": "Sint Maarten", "TO": "Tonga", "TV": "Tuvalu",
"VC": "St Vincent and the Grenadines", "VG": "the British Virgin Islands",
"VI": "the US Virgin Islands", "WS": "Samoa",
}
# Natural Earth's formal names are not what anyone says, and a chip row on a
# 375px phone cannot spend three words on one country: "United States of
# America" pushed the mix off the end of its own chip. Only the names that
# actually differ are listed; everything else comes through unchanged.
#
# This lived in `weekly.py` and only the sheet used it, so the report and the
# map — one tap away — said "United States of America" and "Dominican Rep."
# where the sheet said "United States" and "Dominican Republic" (23 Aug
# audit). One table, read by both.
SHORT_NAME = {
"GB": "Britain", "US": "United States", "KR": "South Korea",
"RU": "Russia", "AE": "the UAE", "CD": "DR Congo", "CI": "Côte d'Ivoire",
"TZ": "Tanzania", "LA": "Laos", "SY": "Syria", "VE": "Venezuela",
"BO": "Bolivia", "IR": "Iran", "MD": "Moldova", "MK": "North Macedonia",
"CZ": "Czechia", "VA": "the Vatican", "DO": "Dominican Republic",
"BA": "Bosnia and Herzegovina", "SS": "South Sudan",
"CF": "Central African Republic", "GQ": "Equatorial Guinea",
"SZ": "Eswatini", "TL": "Timor-Leste", "GW": "Guinea-Bissau",
}
# The same countries as they read inside a sentence: "at #1 in the United
# States". A chip cannot afford the article and a sentence cannot do without
# it.
IN_MARKET = {"US": "the United States", "PH": "the Philippines",
"NL": "the Netherlands", "AE": "the UAE",
"CD": "the DRC", "DO": "the Dominican Republic",
"GB": "Britain", "CZ": "Czechia",
"CF": "the Central African Republic", "GM": "the Gambia",
"BS": "the Bahamas", "MV": "the Maldives"}
def in_market(iso: str, *names: str) -> str:
"""The market's name as it reads after "in" or "into".
"charting in United States" is what a table of names gives you and what
a sentence cannot use. Everything not in the table reads the same in
both places and comes back through `market_name`.
"""
return IN_MARKET.get((iso or "").strip().upper()) or market_name(iso,
*names)
def market_name(iso: str, *names: str) -> str:
"""A market's name in words.
Takes whatever names the caller holds — the corpus's, the map's — in the
order it would have used them, and treats a name that is only the ISO
code as no name at all, because that is exactly what the markets above
carry. A code with no entry still comes back as itself: a code says less
than a name, and a wrong name says worse than nothing.
"""
code = (iso or "").strip().upper()
if code in SHORT_NAME:
return SHORT_NAME[code]
for n in names:
n = (n or "").strip()
if n and n.upper() != code:
return n
return NO_OUTLINE.get(code) or (iso or "")
def name_regions(regions: dict | None) -> dict | None:
"""Give every market in a result a name a reader can say.
Called where a result enters a renderer rather than at each of the dozen
places that print a market name, so a new one cannot be added without it.
It fixes the reading in place and is safe to run twice: a market that
already has a name keeps it.
"""
for iso, r in (regions or {}).items():
if isinstance(r, dict):
r["name"] = market_name(iso, r.get("name"))
return regions
def _name(regions: dict, iso: str) -> str:
return market_name(iso, regions.get(iso, {}).get("name"))
def name_of(region: dict, country: dict) -> str:
"""The corpus name when it has a real one, the Natural Earth name when the
corpus only carries the ISO code (SG, HK, MT, CV come through that way)."""
return region.get("name") or (country or {}).get("name") or ""
def _plural(n: int, unit: str) -> str:
""""1 market", "5 markets". A count that reads "1 markets" tells the
reader the page cannot count (23 Aug audit)."""
return f'{n} {unit if n == 1 else unit + "s"}'
def _join(names: list[str]) -> str:
if len(names) == 1:
return names[0]
return ", ".join(names[:-1]) + " and " + names[-1]
def field(res: dict) -> dict:
"""How much the market ranking is worth before any market is named.
Returns `{flat, n, lo, hi, median, shared}`. `flat` is the case above:
nothing clears the place line, and the strongest market sits within
`FLAT_SPREAD` of the median of every market measured. `shared` is the one
record that two or more of the leading markets both point at, when they
do — the fact that turns "three markets" back into "one sound".
The spread is taken over every region in the corpus, not the eight shown,
because the eight shown are the top of the very distribution being
questioned.
"""
regions = res.get("regions") or {}
# The track's own chart entry is not a market reading, and leaving it in
# hides exactly the case this function exists for: "What Do I Know" reads
# 100% in Uganda because it charts in Uganda, which puts the maximum above
# the place line and makes a dead-flat field look like a strong one.
bests = sorted(float(r["best"]) for r in regions.values()
if float(r["best"]) < SAME_RECORDING)
if not bests:
return {"flat": False, "n": 0, "lo": 0.0, "hi": 0.0, "median": 0.0,
"shared": None, "shared_markets": []}
mid = len(bests) // 2
median = bests[mid] if len(bests) % 2 else (bests[mid - 1] + bests[mid]) / 2
hi = bests[-1]
shown = [i for i in (res.get("shown_markets") or []) if i in regions
and regions[i]["best"] < SAME_RECORDING]
names = []
for iso in shown[:3]:
top = (regions[iso].get("top") or [None])[0]
names.append((top.get("artist"), top.get("title")) if top else None)
shared = (names[0] if len(names) >= 2 and None not in names
and len(set(names)) == 1 else None)
return {"flat": bool(hi < livematch.PLACE_THRESHOLD
and hi - median <= FLAT_SPREAD),
"n": len(bests), "lo": bests[0], "hi": hi, "median": median,
# How much company the leader has above the place line. On the
# real YO YO upload 44 of 88 markets clear it, because a
# full-length upload scores higher against everything than the
# 30-second previews the corpus is built from. A leader quoted
# with no company count reads as if it were alone up there.
"clearing": sum(1 for b in bests if b >= livematch.PLACE_THRESHOLD),
"shared": shared, "shared_markets": shown[:3] if shared else []}
def measured(res: dict, f: dict | None = None) -> str:
""""87 markets measured" — the count the field is read over.
It is not the number the report opens on. That one is every market in
the week; this one drops the markets the track already charts in,
because a record meeting its own chart entry is not a market reading.
Two counts four lines apart with nothing joining them read as arithmetic
that does not add up (23 Aug audit), so the sentence that names the
self-matched markets says they are left out of both.
"""
f = field(res) if f is None else f
return f'{f["n"]} markets measured'
def summary(res: dict) -> list[str]:
"""Two or three plain sentences, generated from the result alone.
Nothing here is a judgement the numbers do not already carry: which family
the markets cluster in, the leading markets with their verdict, and one
fact that changes how the rest of the page should be read — the track
meeting its own chart entry, or nothing clearing the direct-fit line, or
the record the top market's number is measured against.
"""
regions = res.get("regions") or {}
shown = [i for i in (res.get("shown_markets") or []) if i in regions]
if not shown:
return ["No market could be ranked for this track this week."]
self_isos = [i for i in shown if regions[i]["best"] >= SAME_RECORDING]
# The self-match is a fact about the archive, not a market. Everything the
# market sentences say is about markets the track is not already on.
others = [i for i in shown if i not in self_isos]
lead_pool = others or shown
f = field(res)
if f["flat"]:
lead = lead_pool[0]
# Both facts can be true at once, and "What Do I Know" is the record
# where they are: it charts in Uganda AND reads near-level everywhere
# else. Telling only one of them leaves the other looking like a find.
if self_isos:
# All of them. Naming only the first left the others wearing a
# SELF chip with nothing on the page explaining it.
named = _join([f'{ESC(_name(regions, i))} at '
f'{_pct(regions[i]["best"])}' for i in self_isos])
many = len(self_isos) > 1
opener = (
f'<b>{named} {"read" if many else "reads"} that high because '
f'the track is already charting '
f'{"in them" if many else "there"} and is being compared '
f'against itself.</b> Set that aside and no market stands '
f'out: this track reads between {_pct(f["lo"])} and '
f'{_pct(f["hi"])} across the other {f["n"]} markets')
else:
opener = (
f'<b>No market stands out.</b> This track reads between '
f'{_pct(f["lo"])} and {_pct(f["hi"])} across all '
f'{measured(res, f)}')
lines = [
f'{opener}, with the middle of that at {_pct(f["median"])}. '
f'Nothing clears the direct-fit line at '
f'{livematch.PLACE_THRESHOLD:.0%}, and the gap between the '
f'strongest of them and the middle is '
f'{(f["hi"] - f["median"]) * 100:.0f} points — so the ordering '
f'below carries little signal.'
]
if f["shared"]:
artist, title = f["shared"]
where = _join([_name(regions, i) for i in f["shared_markets"]])
lines.append(
f'Its top markets — {ESC(where)} — all rest on the same single '
f'record, <b>{ESC(artist or "—")}{ESC(title or "—")}</b>. '
f'They lead the list because they are the markets that record '
f'charts in, not because the track has a market there.')
else:
lines.append(
f'The strongest reading is {ESC(_name(regions, lead))} at '
f'{_pct(regions[lead]["best"])}, which is close enough to the '
f'middle to be a coincidence of which sounds happen to chart '
f'there this week.')
lines.append(
'Read the sections below for what the track sounds like. Treat '
'the market order as a list of places that have been checked, not '
'a ranking to act on.')
return lines
lines: list[str] = []
# 1. which part of the world — the continent leads, the family qualifies
pool = lead_pool[:8]
conts: dict[str, list[str]] = {}
fams: dict[str, list[str]] = {}
for iso in pool:
if iso in CONTINENT:
conts.setdefault(CONTINENT[iso], []).append(iso)
fams.setdefault(FAMILY[iso], []).append(iso)
# Ties break toward whichever grouping owns the highest-ranked market.
key = lambda kv: (len(kv[1]), -pool.index(kv[1][0])) # noqa: E731
top_cont = max(conts.items(), key=key) if conts else None
top_fam = max(fams.items(), key=key) if fams else None
if top_cont and len(top_cont[1]) * 2 >= len(pool):
cont, c_isos = top_cont
line = (f'<b>{len(c_isos)} of the {_plural(len(pool), "market")} '
f'ranked below {"is" if len(c_isos) == 1 else "are"} in '
f'{ESC(cont)}.</b>')
if top_fam and len(top_fam[1]) >= 2 and top_fam[0] != cont:
line += (f' {len(top_fam[1])} of them are in '
f'<b>{ESC(top_fam[0])}</b> — '
f'{_join([_name(regions, i) for i in top_fam[1]])}.')
lines.append(line)
elif top_fam and len(top_fam[1]) >= 2:
fam, isos = top_fam
lines.append(
f'<b>{len(isos)} of the {_plural(len(pool), "market")} ranked '
f'below {"is" if len(isos) == 1 else "are"} in '
f'{ESC(fam)}</b> ({_join([_name(regions, i) for i in isos])}). '
f'The rest are spread across the world rather than clustered '
f'next to them.')
else:
lines.append(
f'Its closest markets do not cluster: the {len(pool)} ranked '
f'below sit in {len(conts) or 1} different parts of the world, '
f'led by '
f'<b>{ESC(_name(regions, pool[0]))}</b>.')
# 2. the leading markets, with the verdict the number earns
lead = lead_pool[0]
b = livematch.band(regions[lead]["best"])
rest = lead_pool[1:3]
rest_s = ""
if rest:
rest_s = (' Then ' + _join([f'{_name(regions, i)} at {_pct(regions[i]["best"])}'
for i in rest]) + '.')
lines.append(
f'The strongest market is <b>{ESC(_name(regions, lead))}</b> at '
f'{_pct(regions[lead]["best"])}, which reads as {BAND_WORD[b]}.{rest_s}')
# 3. how much company the leader has, then the record behind its number.
#
# The company count is the sentence the first pass was missing. On the real
# YO YO upload Kenya reads 95% and 44 of the 88 markets also clear the
# place line, because a full-length upload scores higher against everything
# than the 30-second previews the corpus is built from — the bias the
# caveats at the foot of the report already name. Quoting the leader with
# no company count puts it alone at the top of a page where it is not.
third = []
if f["clearing"] > 1 and b == "place":
third.append(
f'{f["clearing"]} of the {measured(res, f)} clear that same '
f'line and the middle of the field is {_pct(f["median"])}, so '
f'{ESC(_name(regions, lead))} leads by '
f'{(f["hi"] - f["median"]) * 100:.0f} points rather than standing '
f'alone.')
elif b != "place":
third.append(
f'No market clears the direct-fit line at '
f'{livematch.PLACE_THRESHOLD:.0%} this week, so every market below '
f'is a derive: close enough to build an edit or a remix toward, '
f'not close enough to place as it stands.')
top = (regions[lead].get("top") or [None])[0]
if top:
third.append(
f'The record {ESC(_name(regions, lead))}\'s number is measured '
f'against is <b>{ESC(top.get("artist") or "—")} — '
f'{ESC(top.get("title") or "—")}</b>, the nearest of the '
f'{regions[lead]["matched"]} charting sounds there that could be '
f'measured.')
if third:
lines.append(" ".join(third))
if self_isos:
# Every one of them, not just the first: with three self-matched
# markets the other two wore a SELF chip with nothing on the page
# explaining it (23 Aug audit). This is also where the counts above
# are reconciled — the markets set aside here are the difference
# between "the 5 ranked below" and the week's 88.
named = _join([f'{ESC(_name(regions, i))} at {_pct(regions[i]["best"])}'
for i in self_isos])
many = len(self_isos) > 1
lines.append(
f'{named} {"read" if many else "reads"} that high because the '
f'track is already charting {"in them" if many else "there"} — it '
f'is being compared against itself, not measured against a '
f'market. {"They are" if many else "It is"} left out of the '
f'counts above.')
return lines
# --- the map ---------------------------------------------------------------
def svg(res: dict, linked: list[str] | None = None,
flat: bool = False) -> str:
"""The world, coloured by verdict band, with the carded markets linked.
Every market in the corpus is coloured, not only the eight with cards
below: the cluster is the finding, and eight dots on a blank world would
hide it. Only the carded ones become links, because those are the only
ones with somewhere to land.
`flat` drops the whole map to one tone for every charted market. A track
that reads the same everywhere has no map, and painting its top markets
in a confident colour would put a picture of a finding on top of a
paragraph saying there isn't one.
"""
w = world()
regions = res.get("regions") or {}
linked = [i for i in (linked or []) if i in regions]
esc = ESC
shapes = []
for iso, c in w["countries"].items():
r = regions.get(iso)
if not r:
shapes.append(f'<path class="cty" d="{c["d"]}"/>')
continue
# A self-match gets its own tone rather than the green its number
# would earn. It is the track's own chart entry, and painting it as
# the strongest market on the map is the one thing the card
# underneath spends a paragraph undoing.
if r["best"] >= SAME_RECORDING:
b = "self"
tip = (f'{name_of(r, c)}{_pct(r["best"])}, already charting '
f'here: the track is being compared against itself')
else:
b = livematch.band(r["best"])
tip = (f'{name_of(r, c)}{_pct(r["best"])}, near-level with '
f'every other market' if flat else
f'{name_of(r, c)}{_pct(r["best"])}, {BAND_WORD[b]}')
if iso not in linked:
tip += " (not in the closest markets below)"
shape = (f'<path class="cty {b} c-{esc(iso)}" d="{c["d"]}">'
f'<title>{esc(tip)}</title></path>')
if iso in linked:
# Both spellings of the SVG link: `href` is SVG2 and is what every
# current browser uses; `xlink:href` is what an old one needs.
shapes.append(f'<a href="#market-{esc(iso)}" '
f'xlink:href="#market-{esc(iso)}">{shape}</a>')
else:
shapes.append(shape)
return (f'<svg class="map{" flat" if flat else ""}" '
f'viewBox="{w["viewBox"]}" '
f'preserveAspectRatio="xMidYMid meet" role="img" '
f'xmlns:xlink="http://www.w3.org/1999/xlink" '
f'aria-label="'
+ ('World map: every market this track was measured against reads '
'near-level, so none is highlighted.' if flat else
'World map: markets this track sits closest to, coloured by '
'verdict.')
+ '">'
+ "".join(shapes) + '</svg>')
def _chips(res: dict, linked: list[str], flat: bool = False) -> str:
regions = res["regions"]
out = []
for iso in linked:
r = regions[iso]
if r["best"] >= SAME_RECORDING:
# No percentage on a self chip: the number is the record meeting
# its own chart entry, and 100% on a chip reads as the best
# market in the row. The word is the reading.
b, colour, value = "self", "var(--self)", "SELF"
elif flat:
b, colour, value = "flat", "var(--ink3)", _pct(r["best"])
else:
b = livematch.band(r["best"])
colour, value = f"var(--{b})", _pct(r["best"])
# A checkbox the label flips, not an anchor: an anchor threw the
# reader down the list and left them to scroll back (Kalam, 23 Aug).
# Selecting shows just that market's card directly under the map;
# tapping again clears. Same machinery as the sheet's market filter.
out.append(
f'<input type="checkbox" class="rf" id="rf-{ESC(iso)}">'
f'<label class="mchip {b}" for="rf-{ESC(iso)}">'
f'<span class="d {b}"></span>{ESC(r.get("name") or iso)}'
f'<span class="n" style="color:{colour}">{value}</span></label>')
rules = ['.ml .rf { display:none; }',
'.ml label.mchip { cursor:pointer; }',
# any market selected: every card hides, the map fades …
'.ml:has(.rf:checked) .g-fits .card[id^="market-"]'
' { display:none; }',
'.ml:has(.rf:checked) svg.map .cty { opacity:.18; }',
# … and the two counts of those cards go with them. "8 closest
# markets" over the one market left showing is a count of what
# is hidden (23 Aug audit). The clip and tag counts stay: a
# market filter does not touch those sections.
'.ml:has(.rf:checked) .g-fits h2 .tag { display:none; }',
'.ml:has(.rf:checked) .rtabs label[for="rt-fits"] .n'
' { display:none; }']
for iso in linked:
i = ESC(iso)
# … and each selected market re-opens its own card and keeps its
# colour on the map.
rules += [
f'.ml:has(#rf-{i}:checked) .g-fits #market-{i}'
f' {{ display:block; }}',
f'.ml:has(#rf-{i}:checked) svg.map .c-{i} {{ opacity:1; }}',
f'.ml:has(#rf-{i}:checked) label[for="rf-{i}"]'
f' {{ border-color:var(--ink); background:var(--inset); }}',
]
return ('<style>' + "".join(rules) + '</style>'
'<div class="chiprow">' + "".join(out) + '</div>')
SELF_KEY = '<span><i class="self"></i>already charting here</span>'
def legend(flat: bool, has_self: bool) -> str:
keys = (['<span><i class="flat"></i>measured this week, all within a few '
'points of each other</span>'] if flat else
['<span><i class="place"></i>direct fit</span>',
'<span><i class="derive"></i>bridgeable</span>',
'<span><i class="pass"></i>not adjacent</span>'])
if has_self:
keys.append(SELF_KEY)
keys.append('<span><i class="none"></i>no chart data this week</span>')
return '<div class="maplegend">' + "".join(keys) + '</div>'
def opening(res: dict, lead_action: str = "") -> str:
"""The whole block that sits above "Where it fits".
`lead_action` is the number-one action, already written — `render.py`
builds it with `_market_action`, the same function that writes the
sentence on the first market card, so there is one author of it. It
closes the summary rather than following the map: `design/PAGE_RUBRIC.md`
§2 puts what a reader can do above what they have to weigh up, and the
map is the weighing. Empty is a real state and gets no empty room — a
self-match and a flat field both arrive here as "".
"""
regions = res.get("regions") or {}
linked = [i for i in (res.get("shown_markets") or []) if i in regions]
flat = field(res)["flat"]
has_self = any(r["best"] >= SAME_RECORDING for r in regions.values())
out = ['<div class="lede">']
out += [f'<p>{s}</p>' for s in summary(res)]
if lead_action:
out.append(lead_action)
out.append('</div>')
out.append('<div class="mapwrap">')
out.append(svg(res, linked, flat=flat))
out.append(legend(flat, has_self))
out.append('</div>')
if linked:
out.append('<div class="lab">'
+ ('Markets checked — tap one to see just its card' if flat
else 'Tap a market to see just its card — tap again to show all')
+ '</div>')
out.append(_chips(res, linked, flat=flat))
# Four charting markets are smaller than the 110m outlines carry, so they
# are coloured nowhere on the map. Saying so beats a reader concluding the
# market was dropped.
off = [i for i in linked if i not in world()["countries"]]
if off:
out.append(
f'<div class="den" style="margin:0 0 6px">'
f'{_join([ESC(_name(regions, i)) for i in off])} '
f'{"is" if len(off) == 1 else "are"} too small to draw at this map '
f'scale — use the market {"button" if len(off) == 1 else "buttons"}'
f' above.</div>')
return "".join(out)