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
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
:root{--ink:#333;--muted:#8a857a;--rule:#eceadf;--canvas:#faf8f2}*{box-sizing:border-box}body{background:var(--canvas);color:var(--ink);margin:0;font:16px/1.5 -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}main{max-width:1200px;margin:auto;padding:1.5rem 1rem 3rem}h1{margin:0 0 .35rem;font-size:1.65rem;line-height:1.2}.lede{color:#59544b;max-width:75ch}.controls{flex-wrap:wrap;gap:.35rem;margin:1rem 0;display:flex}.chip{color:var(--ink);cursor:pointer;background:#fff;border:1px solid #d9d5ca;border-radius:999px;align-items:center;gap:.3rem;padding:.22rem .48rem;font:600 .78rem/1 inherit;display:flex}.chip img{width:13px;height:13px}.chip:hover{border-color:#8a857a}.chip:focus-visible,.model-mark:focus-visible{outline-offset:2px;outline:2px solid #2563eb}.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{background:var(--canvas);border:1px solid #ded9cc;width:100%;height:auto;display:block}.canvas{fill:var(--canvas)}.grid line{stroke:var(--rule);stroke-width:.7px}.median{stroke:#c9c4b4;stroke-width:1.2px}.zone{fill:none;stroke-width:2px;opacity:.9}.country{stroke:#fff;stroke-width:.7px;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:#fff;stroke-width:2px}.model-mark{cursor:default}.model-mark:focus{outline:none}.model-label{fill:#222;font-size:13px;font-weight:700}.leader{stroke:#aaa398;stroke-width:1px}.zone-label{font-size:15px;font-style:italic;font-weight:700}.poles line{stroke:#999;stroke-width:1.8px}.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{grid-template-columns:1fr;gap:1rem;margin-top:1.5rem;display:grid}.release-panel{border-top:1px solid #ded9cc;padding-top:1rem}.release-panel h2{margin:0;font-size:1rem}.release-panel p{color:#756f64;margin:.2rem 0 .5rem;font-size:.82rem}.scatter-shell{position:relative}.scatter-shell svg{background:var(--canvas);border:1px solid #ded9cc;width:100%;height:auto;display:block}.scatter-grid line{stroke:var(--rule);stroke-width:.7px}.scatter-axis{stroke:#aaa398;stroke-width:1px}.scatter-tick{fill:#756f64;font-size:11px}.release-mark{cursor:default}.release-mark:focus{outline:none}.tooltip{z-index:2;pointer-events:none;background:#fffffff7;border:1px solid #ddd7ca;border-radius:5px;max-width:275px;padding:.45rem .6rem;font-size:.82rem;line-height:1.35;position:absolute;transform:translate(14px,14px);box-shadow:0 2px 8px #0002}.tooltip strong,.tooltip span{display:block}.tooltip span{color:#5b554b}@media (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}}
+1
View File
@@ -0,0 +1 @@
:root{--ink:#333;--muted:#8a857a;--rule:#eceadf;--canvas:#faf8f2}*{box-sizing:border-box}body{background:var(--canvas);color:var(--ink);margin:0;font:16px/1.5 -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}main{max-width:1200px;margin:auto;padding:1.5rem 1rem 3rem}h1{margin:0 0 .35rem;font-size:1.65rem;line-height:1.2}.lede{color:#59544b;max-width:75ch}.controls{flex-wrap:wrap;gap:.35rem;margin:1rem 0;display:flex}.chip{color:var(--ink);cursor:pointer;background:#fff;border:1px solid #d9d5ca;border-radius:999px;align-items:center;gap:.3rem;padding:.22rem .48rem;font:600 .78rem/1 inherit;display:flex}.chip img{width:13px;height:13px}.chip:hover{border-color:#8a857a}.chip:focus-visible,.model-mark:focus-visible{outline-offset:2px;outline:2px solid #2563eb}.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{background:var(--canvas);border:1px solid #ded9cc;width:100%;height:auto;display:block}.canvas{fill:var(--canvas)}.grid line{stroke:var(--rule);stroke-width:.7px}.median{stroke:#c9c4b4;stroke-width:1.2px}.zone{fill:none;stroke-width:2px;opacity:.9}.country{stroke:#fff;stroke-width:.7px;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:#fff;stroke-width:1.3px}.model-mark{cursor:default}.model-mark:focus{outline:none}.model-label{fill:#222;font-size:13px;font-weight:700}.leader{stroke:#aaa398;stroke-width:1px}.zone-label{font-size:15px;font-style:italic;font-weight:700}.poles line{stroke:#999;stroke-width:1.8px}.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{grid-template-columns:1fr;gap:1rem;margin-top:1.5rem;display:grid}.release-panel{border-top:1px solid #ded9cc;padding-top:1rem}.release-panel h2{margin:0;font-size:1rem}.release-panel p{color:#756f64;margin:.2rem 0 .5rem;font-size:.82rem}.scatter-shell{position:relative}.scatter-shell svg{background:var(--canvas);border:1px solid #ded9cc;width:100%;height:auto;display:block}.scatter-grid line{stroke:var(--rule);stroke-width:.7px}.scatter-axis{stroke:#aaa398;stroke-width:1px}.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.4px;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{z-index:2;pointer-events:none;background:#fffffff7;border:1px solid #ddd7ca;border-radius:5px;max-width:275px;padding:.45rem .6rem;font-size:.82rem;line-height:1.35;position:absolute;transform:translate(14px,14px);box-shadow:0 2px 8px #0002}.tooltip strong,.tooltip span{display:block}.tooltip span{color:#5b554b}@media (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}}
+2 -2
View File
@@ -4,8 +4,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Frontier LLMs on the World Values Survey</title>
<script type="module" crossorigin src="./assets/index-CpA1DfjE.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-Ctfw88gw.css">
<script type="module" crossorigin src="./assets/index-Co4wqFEb.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-CyPTsa3p.css">
</head>
<body>
<div id="root"></div>
+12 -42
View File
@@ -1,43 +1,13 @@
<!doctype html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Frontier LLMs on the World Values Survey</title>
<style>
body { margin: 0; background: #faf8f2; color: #292524; font: 16px system-ui, sans-serif; }
main { max-width: 1100px; margin: auto; padding: 1rem; } h1 { margin: .2rem 0; } p { max-width: 78ch; }
#controls { display: flex; flex-wrap: wrap; gap: .4rem; margin: 1rem 0; }
button { border: 2px solid var(--color); border-radius: 1rem; padding: .3rem .7rem; background: var(--color); color: white; cursor: pointer; font-weight: 600; }
button.off { background: white; color: #292524; opacity: .5; text-decoration: line-through; }
svg { width: 100%; height: auto; background: #faf8f2; border: 1px solid #292524; }
.zone { fill: none; stroke: #94a3b8; stroke-width: 2; stroke-dasharray: 5 3; } .country { fill: #64748b; stroke: white; stroke-width: .6; }
.star { stroke: white; stroke-width: 1; } .zone-label { font-weight: 700; font-style: italic; fill: #475569; }
</style>
<main>
<h1>Frontier LLMs on the World Values Survey</h1>
<p>Rated sampling, 12 items x 12 samples for new points. Filled chips are visible; an outlined, struck-through chip hides that familys stars. Each chip carries the familys latest-release name. Grey dots are WVS countries, coloured stars are models, and dashed hulls are cultural regions. Coordinates, labels, family membership and colours come from <code>wvs_map_data.json</code>, the generated artifact shared with the static map.</p>
<p><a href="react/">Try the matching React/SVG renderer.</a></p>
<div id="controls" aria-label="Model family visibility"></div>
<svg id="map" viewBox="0 0 1000 720" role="img" aria-label="WVS map of model and country coordinates"></svg>
<p>Historical coordinates are recovered rounded display values. Coder and vision-language Qwen variants are visible but are specialized variants, not direct-instruct size-trend evidence. See the repository CI table and request ledger for sample and protocol evidence.</p>
</main>
<script>
const NS = 'http://www.w3.org/2000/svg';
const el = (tag, attrs = {}, text) => { const node = document.createElementNS(NS, tag); for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, value); if (text) node.textContent = text; return node; };
const hull = pts => { const p = [...pts].sort((a,b) => a.x-b.x || a.y-b.y); const cross = (o,a,b) => (a.x-o.x)*(b.y-o.y)-(a.y-o.y)*(b.x-o.x); const half = a => { const out=[]; for (const x of a) { while (out.length > 1 && cross(out.at(-2),out.at(-1),x) <= 0) out.pop(); out.push(x); } return out; }; return [...half(p), ...half([...p].reverse()).slice(1,-1)]; };
fetch('wvs_map_data.json').then(r => { if (!r.ok) throw new Error(`data: ${r.status}`); return r.json(); }).then(data => {
const svg = document.querySelector('#map'), margin = 70, all = [...data.countries, ...data.models];
const minX=Math.min(...all.map(p=>p.x)), maxX=Math.max(...all.map(p=>p.x)), minY=Math.min(...all.map(p=>p.y)), maxY=Math.max(...all.map(p=>p.y));
const x=v=>margin+(v-minX)/(maxX-minX)*(1000-2*margin), y=v=>720-margin-(v-minY)/(maxY-minY)*(720-2*margin);
svg.append(el('line',{x1:margin,y1:y((minY+maxY)/2),x2:1000-margin,y2:y((minY+maxY)/2),class:'axis'})); svg.append(el('line',{x1:x((minX+maxX)/2),y1:margin,x2:x((minX+maxX)/2),y2:720-margin,class:'axis'}));
for (const [name,members] of Object.entries(data.zones)) { const points=data.countries.filter(p=>members.includes(p.name)); if(points.length<3) continue; const h=hull(points); svg.append(el('polygon',{points:h.map(p=>`${x(p.x)},${y(p.y)}`).join(' '),class:'zone'})); const c=h.reduce((sum,p)=>({x:sum.x+p.x/h.length,y:sum.y+p.y/h.length}),{x:0,y:0}); svg.append(el('text',{x:x(c.x),y:y(c.y),class:'zone-label','text-anchor':'middle'},name)); }
for (const p of data.countries) { const c=el('circle',{cx:x(p.x),cy:y(p.y),r:3,class:'country'}); c.append(el('title',{},p.name)); svg.append(c); }
const groups=new Map(); for(const p of data.models) groups.set(p.family,[...(groups.get(p.family)||[]),p]); const hidden=new Set(new URLSearchParams(location.search).get('hide')?.split(',').filter(Boolean));
for (const [family,points] of groups) { const group=el('g',{'data-family':family}); for(const p of points) { const star=el('path',{d:`M ${x(p.x)} ${y(p.y)-8} L ${x(p.x)+2.4} ${y(p.y)-2.4} L ${x(p.x)+8} ${y(p.y)-2.4} L ${x(p.x)+3.6} ${y(p.y)+1.6} L ${x(p.x)+5.2} ${y(p.y)+7} L ${x(p.x)} ${y(p.y)+4} L ${x(p.x)-5.2} ${y(p.y)+7} L ${x(p.x)-3.6} ${y(p.y)+1.6} L ${x(p.x)-8} ${y(p.y)-2.4} L ${x(p.x)-2.4} ${y(p.y)-2.4} Z`,fill:p.color,class:'star'}); star.append(el('title',{},p.name)); group.append(star); } svg.append(group);
const latest=points.find(p=>p.label)?.label ?? points[0].name, button=document.createElement('button'); button.textContent=`${family}: ${latest}`; button.style.setProperty('--color',points[0].color);
const labelOffsets={grok:{dx:-72,dy:-30},muse:{dx:12,dy:-11},mistral:{dx:-96,dy:-26},llama:{dx:-96,dy:12}};
for (const p of points.filter(p=>p.label)) { const {dx,dy}=labelOffsets[family]??{dx:11,dy:-11}; group.append(el('line',{x1:x(p.x),y1:y(p.y),x2:x(p.x)+dx*.82,y2:y(p.y)+dy*.82,stroke:p.color,'stroke-width':1})); group.append(el('text',{x:x(p.x)+dx,y:y(p.y)+dy,fill:p.color,'font-size':10,'font-weight':700},p.label)); }
let visible = !hidden.has(family); const setVisible=on=>{visible=on;group.setAttribute('display',on?'inline':'none');button.classList.toggle('off',!on);button.setAttribute('aria-pressed',String(on));}; setVisible(visible); button.onclick=()=>setVisible(!visible); document.querySelector('#controls').append(button);
}
for(const [text,px,py,anchor] of [[data.axis.x[0],margin,690,'start'],[data.axis.x[1],1000-margin,690,'end'],[data.axis.y[0],500,700,'middle'],[data.axis.y[1],500,28,'middle']]) svg.append(el('text',{x:px,y:py,'text-anchor':anchor,fill:'#57534e','font-weight':700},text));
}).catch(error=>{document.body.append(`Could not load map data: ${error.message}`);});
</script>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="refresh" content="0; url=../">
<link rel="canonical" href="../">
<title>Frontier LLMs on the World Values Survey</title>
</head>
<body>
<p>The WVS map moved to <a href="../">the site root</a>.</p>
</body>
</html>
+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))
Binary file not shown.

Before

Width:  |  Height:  |  Size: 372 KiB

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

@@ -0,0 +1,25 @@
{
"root": "React app loaded at /",
"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": 48,
"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": {
"wvs_map_data.json": "c0b7aa0554ebbf5d819308932e058f4ff6e19d1e817ffebbbe381dbd2d063ffe",
"index.html": "324afb7ec7e686e1eb9b7f660f51665475ffcd55e9f546f528674781aab771a6"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 372 KiB

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 KiB

After

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 395 KiB

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 387 KiB

After

Width:  |  Height:  |  Size: 383 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 KiB

After

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 776 KiB

After

Width:  |  Height:  |  Size: 773 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

@@ -0,0 +1,41 @@
# Fresh-eyes visual review
Read root `AGENTS.md`; no additional `AGENTS.md` found under `slop`. Review limited to the six supplied screenshots. No files edited, code inspected, or browser checks run.
## Verdict
No blocking visual defect observed at the supplied 1440px desktop width. Visibility states visibly change the fitted lines and statistics without disturbing the chart layout.
## Observations
All paths below are relative to `slop/research/wvs/20260916_openrouter/`.
- **Root/redirect appearance:** `wvs_react_root_playwright_default.png` and `wvs_static_redirect.png` show the same complete page appearance: introduction, family chips, culture map, and two release-date panels. Neither shows an error, blank state, or redirect interstitial. Screenshots cannot establish the actual URL or redirect mechanism.
- **Marker hierarchy:** In those default views, plotted logos have thin rings and small footprints relative to the labeled control chips. They do not resemble oversized buttons. The dense purple cluster remains visually prominent through repetition, and individual logos are harder to distinguish than chip logos.
- **Vertical labels:** Both scatter panels have readable, unclipped vertical labels with directional arrows: "Traditional -> Secular-Rational up" and "Self-expression -> Survival up". The second panel heading, "Release date vs Self-expression", names the opposite endpoint from its upward direction. This is not demonstrably incorrect, but readers must notice the axis label to interpret an upward trend.
- **Fit readability and visibility changes:** Dashed gray lines remain distinguishable from pale gridlines. The small, bold upper-left annotations are readable in these captures.
| Screenshot | Secular-Rational annotation | Second-panel annotation |
|---|---|---|
| `wvs_react_root_playwright_default.png` | `OLS, n=48, R2 0.28` | `OLS, n=48, R2 0.26` |
| `wvs_react_playwright_release_fit_all_families.png` | `OLS, n=48, R2 0.28` | `OLS, n=48, R2 0.26` |
| `wvs_react_root_playwright_qwen_hidden.png` | `OLS, n=9, R2 0.08` | `OLS, n=9, R2 0.22` |
| `wvs_react_playwright_release_fit_qwen_only.png` | `OLS, n=39, R2 0.21` | `OLS, n=39, R2 0.24` |
The Qwen-hidden first-panel line slopes downward, unlike the default upward line. Its second-panel line becomes steeper and occupies the recent-date portion. Qwen-only lines also visibly differ from the all-family fits.
- **Insufficient data:** `wvs_react_playwright_release_fit_insufficient.png` shows only Muse enabled, a single mark in each release panel, and "Fit unavailable: fewer than two release dates". No fitted line or stale numerical fit annotation is visible. The message is readable and contained within each panel.
- **Qwen-hide geometry:** Comparing the two root screenshots, the Qwen chip becomes muted and struck through, purple Qwen marks and the `qwen3.8-flash` label disappear, and surviving marks, country outlines, chart frames, and axis extents appear stationary. Large empty historical areas remain in the release panels, consistent with keeping the original date domain rather than rescaling.
- **Copy:** The introductory explanation explicitly limits fitting to "dated models currently shown" and says statistics "describe correlation, not cause." That scope is useful and consistent with the pictured state changes. "Hover or keyboard focus" describes interactions that these screenshots do not verify.
## Nonblocking concern and disproof check
**Directional naming may invite reversed interpretation.** In all six screenshots, "Release date vs Self-expression" accompanies an axis pointing upward toward "Survival". An upward line could be mistaken for increasing self-expression if the reader follows the heading alone.
This is a visual interpretation risk, not an established sign error. Check the coordinate definition and ask a reader to explain what an upward line means without prompting; correct interpretation would weaken this concern.
## Limits
These images demonstrate distinct rendered states, not live interaction or numerical correctness. They do not establish redirect behavior, fit calculations, keyboard access, tooltips, responsive layout, or zero-point/same-date edge cases. No earlier marker design was supplied, so reduced prominence over time cannot be verified; only the current marker-versus-chip hierarchy can be assessed.
-- reviewer-openai