Add release-date fit controls to root WVS map

Co-Authored-By: PI[gpt-5.6-terra] <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
wassname
2026-09-17 13:59:58 +08:00
co-authored by PI[gpt-5.6-terra]
parent 25e2543af5
commit da2e70f3bb
22 changed files with 184 additions and 94 deletions
+32 -9
View File
@@ -34,18 +34,34 @@ function ModelMarker({ model, placement, geometry, setActive, clearActive, marke
onPointerEnter={() => setActive(model)} onPointerLeave={clearActive}
onFocus={() => setActive(model)} onBlur={clearActive}>
{leader && <line className="leader" x1={cx} y1={cy} x2={label.cx} y2={label.cy} />}
<circle className="model-ring" cx={cx} cy={cy} r="11" stroke={model.color} />
<image href={`${LOGO_ROOT}${logo}`} x={cx - 7} y={cy - 7} width="14" height="14" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false" />
<circle className="model-ring" cx={cx} cy={cy} r="8" stroke={model.color} />
<image href={`${LOGO_ROOT}${logo}`} x={cx - 5} y={cy - 5} width="10" height="10" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false" />
{label && <text className="model-label" x={label.cx} y={label.cy + 4} textAnchor="middle">{model.label}</text>}
</g>;
}
function ordinaryLeastSquares(models, field) {
const x = models.map(model => Date.parse(model.provenance.release_created));
const y = models.map(model => model[field]);
const uniqueX = new Set(x);
if (uniqueX.size < 2) return null;
const xMean = x.reduce((sum, value) => sum + value, 0) / x.length;
const yMean = y.reduce((sum, value) => sum + value, 0) / y.length;
const slope = x.reduce((sum, value, index) => sum + (value - xMean) * (y[index] - yMean), 0)
/ x.reduce((sum, value) => sum + (value - xMean) ** 2, 0);
const intercept = yMean - slope * xMean;
const residual = y.reduce((sum, value, index) => sum + (value - (intercept + slope * x[index])) ** 2, 0);
const total = y.reduce((sum, value) => sum + (value - yMean) ** 2, 0);
return { slope, intercept, n: models.length, r2: total === 0 ? null : 1 - residual / total };
}
function ReleaseScatter({ data, hidden, field, title }) {
const dated = useMemo(() => data.models.filter(model => Number.isFinite(Date.parse(model.provenance.release_created ?? '')))
.toSorted((a, b) => a.provenance.release_created.localeCompare(b.provenance.release_created) || a.name.localeCompare(b.name)), [data]);
const visible = dated.filter(model => !hidden.has(model.family));
const fit = ordinaryLeastSquares(visible, field);
const [active, setActive] = useState(null);
const width = 1200, height = 320, left = 74, right = 35, top = 42, bottom = 48;
const width = 1200, height = 320, left = 96, right = 35, top = 42, bottom = 48;
const dates = dated.map(model => Date.parse(model.provenance.release_created));
const values = dated.map(model => model[field]);
const minDate = Math.min(...dates), maxDate = Math.max(...dates);
@@ -54,6 +70,9 @@ function ReleaseScatter({ data, hidden, field, title }) {
const valueY = value => top + (maxValue - value) / (maxValue - minValue || 1) * (height - top - bottom);
const panelId = `release-${field}`;
const tooltipId = `${panelId}-tooltip`;
const yDirection = field === 'y' ? data.axis.y : data.axis.x;
const visibleDates = visible.map(model => Date.parse(model.provenance.release_created));
const fitEnd = fit && [Math.min(...visibleDates), Math.max(...visibleDates)].map(date => [dateX(date), valueY(fit.intercept + fit.slope * date)]);
const activate = (model, event) => {
const box = event.currentTarget.closest('.scatter-shell').getBoundingClientRect();
const point = event.currentTarget.getBoundingClientRect();
@@ -62,22 +81,26 @@ function ReleaseScatter({ data, hidden, field, title }) {
return <section className="release-panel" data-coordinate={field} aria-labelledby={`${panelId}-heading`}>
<h2 id={`${panelId}-heading`}>{title}</h2>
<div className="scatter-shell">
<svg viewBox={`0 0 ${width} ${height}`} role="img" aria-labelledby={`${panelId}-svg-title ${panelId}-svg-desc`} data-panel-model-count={visible.length}>
<svg viewBox={`0 0 ${width} ${height}`} role="img" aria-labelledby={`${panelId}-svg-title ${panelId}-svg-desc`} data-panel-model-count={visible.length} data-fit-n={fit?.n ?? 0} data-fit-r2={fit?.r2 ?? ''}>
<title id={`${panelId}-svg-title`}>{title}</title>
<desc id={`${panelId}-svg-desc`}>Scatter plot with release date on the horizontal axis and {field === 'y' ? 'Secular-Rational' : 'Self-expression'} on the vertical axis. {visible.length} dated models are visible from {visibleFamilyNames(Object.groupBy(data.models, model => model.family), hidden).join(', ') || 'no families'}. Each white-ring logo mark is a model. Hover or keyboard focus a mark for model-specific details.</desc>
<desc id={`${panelId}-svg-desc`}>Scatter plot with release date on the horizontal axis and {yDirection.join(' to ')} increasing upward on the vertical axis. {visible.length} dated models are visible from {visibleFamilyNames(Object.groupBy(data.models, model => model.family), hidden).join(', ') || 'no families'}. Each white-ring logo mark is a model. {fit ? `The thin line is an ordinary least squares descriptive fit to the ${fit.n} currently visible dated models${fit.r2 === null ? '; R squared is unavailable because the y values are constant' : `; R squared is ${fit.r2.toFixed(2)}`}.` : 'The fit is hidden because fewer than two distinct release dates are visible.'} Hover or keyboard focus a mark for model-specific details.</desc>
<defs><marker id={`${panelId}-arrow`} markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L6,3 z" className="scatter-arrow" /></marker><clipPath id={`${panelId}-fit-clip`}><rect x={left} y={top} width={width - left - right} height={height - top - bottom} /></clipPath></defs>
<rect className="canvas" width={width} height={height} />
<g className="scatter-grid">{Array.from({ length: 5 }, (_, index) => <line key={index} x1={left} x2={width - right} y1={top + index * (height - top - bottom) / 4} y2={top + index * (height - top - bottom) / 4} />)}</g>
<line className="scatter-axis" x1={left} x2={width - right} y1={height - bottom} y2={height - bottom} />
<line className="scatter-axis" x1={left} x2={left} y1={top} y2={height - bottom} />
<line className="scatter-axis" x1={left} x2={left} y1={height - bottom} y2={top} markerEnd={`url(#${panelId}-arrow)`} />
<text className="scatter-y-label" x="17" y={(top + height - bottom) / 2} textAnchor="middle" transform={`rotate(-90 17 ${(top + height - bottom) / 2})`}>{yDirection.join(' -> ')} {'↑'}</text>
<text className="scatter-tick" x={left} y={height - 18}>{new Date(minDate).toISOString().slice(0, 10)}</text>
<text className="scatter-tick" x={width - right} y={height - 18} textAnchor="end">{new Date(maxDate).toISOString().slice(0, 10)}</text>
<text className="scatter-tick" x={left - 8} y={top + 4} textAnchor="end">{maxValue.toFixed(2)}</text>
<text className="scatter-tick" x={left - 8} y={height - bottom} textAnchor="end">{minValue.toFixed(2)}</text>
{fitEnd && <g className="release-fit" data-fit-n={fit.n} data-fit-r2={fit.r2 ?? ''}><line clipPath={`url(#${panelId}-fit-clip)`} x1={fitEnd[0][0]} y1={fitEnd[0][1]} x2={fitEnd[1][0]} y2={fitEnd[1][1]} /> <text x={left + 6} y={top + 13}>OLS, n={fit.n}, {fit.r2 === null ? 'unavailable' : fit.r2.toFixed(2)}</text></g>}
{!fit && <text className="release-fit-unavailable" x={left + 6} y={top + 13}>Fit unavailable: fewer than two release dates</text>}
{dated.map(model => <g key={model.name} className="release-mark" data-family={model.family} data-release-model={model.name} data-release-date={model.provenance.release_created} display={hidden.has(model.family) ? 'none' : 'inline'}
tabIndex="0" role="button" aria-label={`${model.name}, ${model.family}, ${model.provenance.release_created}`} aria-describedby={tooltipId}
onPointerEnter={event => activate(model, event)} onPointerLeave={() => setActive(null)} onFocus={event => activate(model, event)} onBlur={() => setActive(null)}>
<circle className="model-ring" cx={dateX(Date.parse(model.provenance.release_created))} cy={valueY(model[field])} r="10" stroke={model.color} />
<image href={`${LOGO_ROOT}${data.logos[model.family]}`} x={dateX(Date.parse(model.provenance.release_created)) - 6.5} y={valueY(model[field]) - 6.5} width="13" height="13" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false" />
<circle className="model-ring" cx={dateX(Date.parse(model.provenance.release_created))} cy={valueY(model[field])} r="8" stroke={model.color} />
<image href={`${LOGO_ROOT}${data.logos[model.family]}`} x={dateX(Date.parse(model.provenance.release_created)) - 5} y={valueY(model[field]) - 5} width="10" height="10" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false" />
</g>)}
</svg>
<Tooltip active={active} geometry={null} data={data} id={tooltipId} />
@@ -153,7 +176,7 @@ function App() {
return <main>
<h1>How do AI models score on human values surveys? Which culture are they most similar to? Is it changing over time?</h1>
<p className="lede">To answer these we start with the <a href="https://www.worldvaluessurvey.org/">World Values Survey</a>, the standard culture map of the world. Since 1981 it has asked people in about ninety countries the same questions. Two axes drawn from it sort societies by how traditional or secular they are and how much they weigh survival over self-expression.</p>
<p className="caption">Use the family controls to compare saved model coordinates. Hover or keyboard focus a mark for its model-specific provenance. See the <a href="https://github.com/wassname/moral-maps">code and records</a>.</p>
<p className="caption">Use the family controls to compare saved model coordinates. The release-date panels fit ordinary least squares lines only to the dated models currently shown; n and update with visibility and describe correlation, not cause. Hover or keyboard focus a mark for model-specific provenance. See the <a href="https://github.com/wassname/moral-maps">code and records</a>.</p>
{data && <Map data={data} />}
</main>;
}
+1 -1
View File
@@ -1 +1 @@
:root{--ink:#333;--muted:#8a857a;--rule:#eceadf;--canvas:#faf8f2}*{box-sizing:border-box}body{margin:0;background:var(--canvas);color:var(--ink);font:16px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}main{max-width:1200px;margin:auto;padding:1.5rem 1rem 3rem}h1{font-size:1.65rem;line-height:1.2;margin:0 0 .35rem}.lede{max-width:75ch;color:#59544b}.controls{display:flex;flex-wrap:wrap;gap:.35rem;margin:1rem 0}.chip{display:flex;align-items:center;gap:.3rem;padding:.22rem .48rem;border:1px solid #d9d5ca;border-radius:999px;background:white;color:var(--ink);font:600 .78rem/1 inherit;cursor:pointer}.chip img{width:13px;height:13px}.chip:hover{border-color:#8a857a}.chip:focus-visible,.model-mark:focus-visible{outline:2px solid #2563eb;outline-offset:2px}.chip[aria-pressed="false"]{color:#8a857a;background:#f6f4ef;text-decoration:line-through}.chip[aria-pressed="false"] img{opacity:.3}.chart-shell{position:relative}.chart-shell svg{display:block;width:100%;height:auto;border:1px solid #ded9cc;background:var(--canvas)}.canvas{fill:var(--canvas)}.grid line{stroke:var(--rule);stroke-width:.7}.median{stroke:#c9c4b4;stroke-width:1.2}.zone{fill:none;stroke-width:2;opacity:.9}.country{stroke:white;stroke-width:.7;opacity:.88}.country-label,.model-label,.zone-label,.poles text{paint-order:stroke;stroke:var(--canvas);stroke-width:4px;stroke-linejoin:round}.country-label{fill:#222;font-size:11px}.model-ring{fill:white;stroke-width:2}.model-mark{cursor:default}.model-mark:focus{outline:none}.model-label{fill:#222;font-size:13px;font-weight:700}.leader{stroke:#aaa398;stroke-width:1}.zone-label{font-size:15px;font-style:italic;font-weight:700}.poles line{stroke:#999;stroke-width:1.8}.arrow{fill:#999}.poles text{fill:#555;font-size:18px;font-weight:700}.map-title{fill:#333;font-size:14px;font-weight:700}.map-note{fill:var(--muted);font-size:10px}.release-panels{display:grid;grid-template-columns:1fr;gap:1rem;margin-top:1.5rem}.release-panel{border-top:1px solid #ded9cc;padding-top:1rem}.release-panel h2{font-size:1rem;margin:0}.release-panel p{margin:.2rem 0 .5rem;color:#756f64;font-size:.82rem}.scatter-shell{position:relative}.scatter-shell svg{display:block;width:100%;height:auto;border:1px solid #ded9cc;background:var(--canvas)}.scatter-grid line{stroke:var(--rule);stroke-width:.7}.scatter-axis{stroke:#aaa398;stroke-width:1}.scatter-tick{fill:#756f64;font-size:11px}.release-mark{cursor:default}.release-mark:focus{outline:none}.tooltip{position:absolute;z-index:2;max-width:275px;transform:translate(14px,14px);pointer-events:none;border:1px solid #ddd7ca;border-radius:5px;padding:.45rem .6rem;background:rgba(255,255,255,.97);box-shadow:0 2px 8px #0002;font-size:.82rem;line-height:1.35}.tooltip strong,.tooltip span{display:block}.tooltip span{color:#5b554b}@media(max-width:700px){main{padding:.9rem .5rem 2rem}.chip{font-size:.7rem}.poles text{font-size:14px}.country-label,.model-label{font-size:10px}.tooltip{max-width:230px;font-size:.75rem}}
:root{--ink:#333;--muted:#8a857a;--rule:#eceadf;--canvas:#faf8f2}*{box-sizing:border-box}body{margin:0;background:var(--canvas);color:var(--ink);font:16px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}main{max-width:1200px;margin:auto;padding:1.5rem 1rem 3rem}h1{font-size:1.65rem;line-height:1.2;margin:0 0 .35rem}.lede{max-width:75ch;color:#59544b}.controls{display:flex;flex-wrap:wrap;gap:.35rem;margin:1rem 0}.chip{display:flex;align-items:center;gap:.3rem;padding:.22rem .48rem;border:1px solid #d9d5ca;border-radius:999px;background:white;color:var(--ink);font:600 .78rem/1 inherit;cursor:pointer}.chip img{width:13px;height:13px}.chip:hover{border-color:#8a857a}.chip:focus-visible,.model-mark:focus-visible{outline:2px solid #2563eb;outline-offset:2px}.chip[aria-pressed="false"]{color:#8a857a;background:#f6f4ef;text-decoration:line-through}.chip[aria-pressed="false"] img{opacity:.3}.chart-shell{position:relative}.chart-shell svg{display:block;width:100%;height:auto;border:1px solid #ded9cc;background:var(--canvas)}.canvas{fill:var(--canvas)}.grid line{stroke:var(--rule);stroke-width:.7}.median{stroke:#c9c4b4;stroke-width:1.2}.zone{fill:none;stroke-width:2;opacity:.9}.country{stroke:white;stroke-width:.7;opacity:.88}.country-label,.model-label,.zone-label,.poles text{paint-order:stroke;stroke:var(--canvas);stroke-width:4px;stroke-linejoin:round}.country-label{fill:#222;font-size:11px}.model-ring{fill:white;stroke-width:1.3}.model-mark{cursor:default}.model-mark:focus{outline:none}.model-label{fill:#222;font-size:13px;font-weight:700}.leader{stroke:#aaa398;stroke-width:1}.zone-label{font-size:15px;font-style:italic;font-weight:700}.poles line{stroke:#999;stroke-width:1.8}.arrow{fill:#999}.poles text{fill:#555;font-size:18px;font-weight:700}.map-title{fill:#333;font-size:14px;font-weight:700}.map-note{fill:var(--muted);font-size:10px}.release-panels{display:grid;grid-template-columns:1fr;gap:1rem;margin-top:1.5rem}.release-panel{border-top:1px solid #ded9cc;padding-top:1rem}.release-panel h2{font-size:1rem;margin:0}.release-panel p{margin:.2rem 0 .5rem;color:#756f64;font-size:.82rem}.scatter-shell{position:relative}.scatter-shell svg{display:block;width:100%;height:auto;border:1px solid #ded9cc;background:var(--canvas)}.scatter-grid line{stroke:var(--rule);stroke-width:.7}.scatter-axis{stroke:#aaa398;stroke-width:1}.scatter-arrow{fill:#aaa398}.scatter-y-label{fill:#555;font-size:12px;font-weight:700}.scatter-tick{fill:#756f64;font-size:11px}.release-fit line{stroke:#56514a;stroke-width:1.4;stroke-dasharray:4 3}.release-fit text,.release-fit-unavailable{fill:#56514a;font-size:11px;font-weight:700}.release-mark{cursor:default}.release-mark:focus{outline:none}.tooltip{position:absolute;z-index:2;max-width:275px;transform:translate(14px,14px);pointer-events:none;border:1px solid #ddd7ca;border-radius:5px;padding:.45rem .6rem;background:rgba(255,255,255,.97);box-shadow:0 2px 8px #0002;font-size:.82rem;line-height:1.35}.tooltip strong,.tooltip span{display:block}.tooltip span{color:#5b554b}@media(max-width:700px){main{padding:.9rem .5rem 2rem}.chip{font-size:.7rem}.poles text{font-size:14px}.country-label,.model-label{font-size:10px}.tooltip{max-width:230px;font-size:.75rem}}
+68 -37
View File
@@ -30,6 +30,12 @@ def svg_description(page, selector: str, title_id: str, desc_id: str) -> None:
assert svg.locator(f"desc#{desc_id}").text_content()
def country_positions(page) -> list[list[str]]:
return page.locator("circle.country").evaluate_all(
"nodes => nodes.map(node => [node.getAttribute('cx'), node.getAttribute('cy')])"
)
def main() -> None:
OUT.mkdir(parents=True, exist_ok=True)
chrome = shutil.which("google-chrome")
@@ -42,6 +48,8 @@ def main() -> None:
assert len(data["zone_hulls"]) == 4
assert len(data["latest_by_family"]) == 13
assert all(len(zone["points"]) >= 30 for zone in data["zone_hulls"])
dated = [model for model in data["models"] if model["provenance"]["release_created"]]
families = {model["family"] for model in data["models"]}
with sync_playwright() as playwright:
browser = playwright.chromium.launch(executable_path=chrome, headless=True, args=["--no-sandbox"])
@@ -50,8 +58,9 @@ def main() -> None:
page.on("pageerror", lambda error: errors.append(str(error)))
screenshot(page, f"{BASE}/img/wvs/wvs_map_iw.png", "wvs_static_playwright.png")
screenshot(page, f"{BASE}/wvs/", "wvs_vanilla_playwright.png")
assert page.locator("#map").count() == 1
screenshot(page, f"{BASE}/wvs/", "wvs_static_redirect.png")
page.wait_for_selector("svg .model-mark")
assert page.url == f"{BASE}/"
screenshot(page, f"{BASE}/", "wvs_react_root_playwright_default.png")
page.wait_for_selector("svg .model-mark")
@@ -59,37 +68,34 @@ def main() -> None:
assert page.locator("html").get_attribute("lang") == "en"
assert page.locator('meta[name="viewport"]').count() == 1
assert page.locator("h1").inner_text().startswith("How do AI models score on human values surveys?")
assert "React/SVG rendering" not in page.locator("main").inner_text()
assert "Artificial Analysis" not in page.locator("main").inner_text()
assert "Historical coordinates" not in page.locator("main").inner_text()
assert "Dated releases only" not in page.locator("main").inner_text()
visible_copy = page.locator("main").inner_text()
for absent in ("React/SVG rendering", "Artificial Analysis", "Historical coordinates", "Dated releases only"):
assert absent not in visible_copy
assert "ordinary least squares" in visible_copy
map_svg = page.locator("svg[data-median-x]")
svg_description(page, "svg[data-median-x]", "map-svg-title", "map-svg-desc")
assert "64 model marks" in map_svg.locator("desc").text_content()
assert page.locator("path.zone").count() == 4
assert page.locator("polygon.zone").count() == 0
assert page.locator(".map-note").get_attribute("text-anchor") == "end"
assert float(page.locator(".map-note").get_attribute("x")) > float(page.locator(".map-title").get_attribute("x"))
assert float(map_svg.get_attribute("data-median-x")) == data["median"]["x"]
assert float(map_svg.get_attribute("data-median-y")) == data["median"]["y"]
rendered_models = page.locator(".model-mark").evaluate_all(
models_dom = page.locator(".model-mark").evaluate_all(
"nodes => nodes.map(node => [node.dataset.model, Number(node.dataset.x), Number(node.dataset.y)])"
)
assert sorted(rendered_models) == sorted([[model["name"], model["x"], model["y"]] for model in data["models"]])
rendered_countries = page.locator("circle.country").evaluate_all(
assert sorted(models_dom) == sorted([[model["name"], model["x"], model["y"]] for model in data["models"]])
countries_dom = page.locator("circle.country").evaluate_all(
"nodes => nodes.map(node => [node.dataset.country, Number(node.dataset.x), Number(node.dataset.y)])"
)
assert rendered_countries == [[country["name"], country["x"], country["y"]] for country in data["countries"]]
assert countries_dom == [[country["name"], country["x"], country["y"]] for country in data["countries"]]
assert page.locator(".chip").count() == 13
assert all(name for name in page.locator(".chip").evaluate_all("nodes => nodes.map(node => node.getAttribute('aria-label') || node.innerText)"))
assert all(value == "true" for value in page.locator(".chip img").evaluate_all("nodes => nodes.map(node => node.getAttribute('aria-hidden'))"))
assert all(value == "true" for value in page.locator(".model-mark image, .release-mark image").evaluate_all("nodes => nodes.map(node => node.getAttribute('aria-hidden'))"))
assert all(page.locator(".model-mark").evaluate_all("nodes => nodes.map(node => node.getAttribute('aria-label'))"))
country_before = page.locator("circle.country").evaluate_all(
"nodes => nodes.map(node => [node.getAttribute('cx'), node.getAttribute('cy')])"
)
dated = [model for model in data["models"] if model["provenance"]["release_created"]]
assert all(page.locator(".chip").evaluate_all("nodes => nodes.map(node => node.innerText)"))
assert all(value == "true" for value in page.locator(".chip img, .model-mark image, .release-mark image").evaluate_all("nodes => nodes.map(node => node.getAttribute('aria-hidden'))"))
assert all(float(value) == 8 for value in page.locator(".model-ring").evaluate_all("nodes => nodes.map(node => node.getAttribute('r'))"))
assert all(width > 10 for width in page.locator(".chip img").evaluate_all("nodes => nodes.map(node => node.getBoundingClientRect().width)"))
country_before = country_positions(page)
assert page.locator(".release-panel").count() == 2
svg_description(page, '.release-panel[data-coordinate="y"] svg', "release-y-svg-title", "release-y-svg-desc")
svg_description(page, '.release-panel[data-coordinate="x"] svg', "release-x-svg-title", "release-x-svg-desc")
@@ -97,51 +103,75 @@ def main() -> None:
for panel in page.locator(".release-panel").all():
dates = panel.locator(".release-mark").evaluate_all("nodes => nodes.map(node => node.dataset.releaseDate)")
assert dates == sorted(dates)
page.screenshot(path=OUT / "wvs_react_playwright_release_panels.png", full_page=True)
assert panel.locator(".scatter-y-label").count() == 1
assert "->" in panel.locator(".scatter-y-label").text_content()
assert panel.locator(".scatter-axis[marker-end]").count() == 1
assert panel.locator(".release-fit").count() == 1
assert int(panel.locator("svg").get_attribute("data-fit-n")) == len(dated)
assert panel.locator(".release-fit line").count() == 1
page.screenshot(path=OUT / "wvs_react_playwright_release_fit_all_families.png", full_page=True)
page.get_by_role("button", name="qwen", exact=True).click()
page.wait_for_timeout(100)
qwen_group = map_svg.locator(':scope > g[data-family="qwen"]')
assert qwen_group.get_attribute("display") == "none"
assert map_svg.locator(':scope > g[data-family="qwen"]').get_attribute("display") == "none"
assert "qwen" not in map_svg.locator("desc").text_content().split("from ", 1)[1]
assert all(group.get_attribute("display") == "none" for group in page.locator('.release-panel [data-family="qwen"]').all())
assert all(group.get_attribute("display") != "none" for group in page.locator('.release-panel [data-family="muse"]').all())
country_after = page.locator("circle.country").evaluate_all(
"nodes => nodes.map(node => [node.getAttribute('cx'), node.getAttribute('cy')])"
)
assert country_after == country_before
assert country_positions(page) == country_before
page.screenshot(path=OUT / "wvs_react_root_playwright_qwen_hidden.png", full_page=True)
page.get_by_role("button", name="qwen", exact=True).click()
marker = page.locator('[data-model="qwen3.8-flash"]')
marker.locator(".model-ring").hover()
page.wait_for_selector("#model-tooltip")
tooltip_text = page.locator("#model-tooltip").inner_text()
for required in ("qwen3.8-flash", "qwen", "rated categorical response", "12 items x 12 samples"):
assert required in tooltip_text, required
assert required in page.locator("#model-tooltip").inner_text()
page.screenshot(path=OUT / "wvs_react_root_playwright_tooltip_hover.png", full_page=True)
marker.focus()
assert page.evaluate("document.activeElement.dataset.model") == "qwen3.8-flash"
assert page.locator("#model-tooltip").is_visible()
page.screenshot(path=OUT / "wvs_react_root_playwright_keyboard_focus.png", full_page=True)
release_marker = page.locator('.release-panel[data-coordinate="y"] [data-release-model="qwen3.8-flash"]')
release_marker.locator(".model-ring").hover()
release_y_marker = page.locator('.release-panel[data-coordinate="y"] [data-release-model="qwen3.8-flash"]')
release_y_marker.locator(".model-ring").hover()
page.wait_for_selector("#release-y-tooltip")
assert "qwen3.8-flash" in page.locator("#release-y-tooltip").inner_text()
page.screenshot(path=OUT / "wvs_react_root_playwright_release_panel_hover.png", full_page=True)
release_marker.focus()
release_y_marker.focus()
assert page.evaluate("document.activeElement.dataset.releaseModel") == "qwen3.8-flash"
assert page.locator("#release-y-tooltip").is_visible()
page.screenshot(path=OUT / "wvs_react_root_playwright_release_panel_keyboard_focus.png", full_page=True)
release_x_marker = page.locator('.release-panel[data-coordinate="x"] [data-release-model="qwen3.8-flash"]')
release_x_marker.locator(".model-ring").hover()
page.wait_for_selector("#release-x-tooltip")
assert page.locator("#release-x-tooltip").is_visible()
assert release_marker.get_attribute("aria-describedby") == "release-y-tooltip"
assert release_y_marker.get_attribute("aria-describedby") == "release-y-tooltip"
assert release_x_marker.get_attribute("aria-describedby") == "release-x-tooltip"
qwen_dated = sum(model["family"] == "qwen" and bool(model["provenance"]["release_created"]) for model in data["models"])
for family in sorted(families - {"qwen"}):
page.get_by_role("button", name=family, exact=True).click()
page.wait_for_timeout(100)
for family in families:
expected = "inline" if family == "qwen" else "none"
assert map_svg.locator(f':scope > g[data-family="{family}"]').get_attribute("display") == expected
for panel in page.locator(".release-panel").all():
assert int(panel.locator("svg").get_attribute("data-fit-n")) == qwen_dated
assert panel.locator(".release-fit").count() == 1
assert country_positions(page) == country_before
page.screenshot(path=OUT / "wvs_react_playwright_release_fit_qwen_only.png", full_page=True)
page.get_by_role("button", name="qwen", exact=True).click()
page.get_by_role("button", name="muse", exact=True).click()
page.wait_for_timeout(100)
for family in families:
expected = "inline" if family == "muse" else "none"
assert map_svg.locator(f':scope > g[data-family="{family}"]').get_attribute("display") == expected
for panel in page.locator(".release-panel").all():
assert int(panel.locator("svg").get_attribute("data-fit-n")) == 0
assert panel.locator(".release-fit").count() == 0
assert panel.locator(".release-fit-unavailable").count() == 1
assert country_positions(page) == country_before
page.screenshot(path=OUT / "wvs_react_playwright_release_fit_insufficient.png", full_page=True)
screenshot(page, f"{BASE}/wvs/react/", "wvs_react_compat_redirect.png")
page.wait_for_selector("svg .model-mark")
assert page.url == f"{BASE}/"
@@ -159,13 +189,14 @@ def main() -> None:
)}
print(json.dumps({
"root": "React app loaded at /",
"compatibility": "/wvs/ static retained; /wvs/react/ redirects to root",
"compatibility": "/wvs/ and /wvs/react/ redirect to the root React app; shared data remains at /wvs/wvs_map_data.json",
"shared": {"models": 64, "countries": 90, "buffered_hulls": 4, "latest_labels": 13,
"numeric_model_country_median_equality": "verified against DOM data attributes"},
"svg_accessibility": "main map and both release panels have stable title/desc aria-labelledby; descriptions update after family visibility change",
"qwen_toggle": "clicked, map and dated-panel family groups hidden, country coordinates invariant",
"tooltip": "map and release-panel pointer hover and focus show model-specific fields",
"release_panels": {"panels": 2, "dated_models": len(dated), "date_order": "DOM order verified", "no_paths": True},
"release_panels": {"panels": 2, "dated_models": len(dated), "date_order": "DOM order verified",
"ols": "line, n, and R squared recompute from currently visible dated models; fewer than two distinct dates hide the fit"},
"copy": "reader-facing WVS introduction; no model-count, archaeological, implementation, or Artificial Analysis text",
"hashes": hashes,
}, indent=2))