mirror of
https://github.com/wassname/moral-maps.git
synced 2026-09-23 13:30:23 +08:00
Add dated release scatter panels to React WVS map
Co-Authored-By: PI[gpt-5.6-terra] <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
co-authored by
PI[gpt-5.6-terra]
parent
0be556bcef
commit
f93a6092cf
@@ -3,13 +3,13 @@ import { createRoot } from 'react-dom/client';
|
||||
import { VIEW, assertLayout, roundedHull } from './layout.js';
|
||||
import './style.css';
|
||||
|
||||
function Tooltip({ active, geometry, data }) {
|
||||
function Tooltip({ active, geometry, data, id = 'model-tooltip' }) {
|
||||
if (!active) return null;
|
||||
const { provenance } = active;
|
||||
const left = `${Math.min(86, geometry.x(active.x) / VIEW.width * 100)}%`;
|
||||
const top = `${Math.min(82, geometry.y(active.y) / VIEW.height * 100)}%`;
|
||||
const left = active.tooltipLeft ?? `${Math.min(86, geometry.x(active.x) / VIEW.width * 100)}%`;
|
||||
const top = active.tooltipTop ?? `${Math.min(82, geometry.y(active.y) / VIEW.height * 100)}%`;
|
||||
const samples = provenance.items === null ? 'historical coordinate' : `${provenance.items} items x ${provenance.samples} samples`;
|
||||
return <aside id="model-tooltip" className="tooltip" style={{ left, top }} role="status">
|
||||
return <aside id={id} className="tooltip" style={{ left, top }} role="status">
|
||||
<strong>{active.name}</strong>
|
||||
<span>{active.family}</span>
|
||||
<span>{data.axis.x[0]} {'->'} {data.axis.x[1]}: {active.x.toFixed(3)}</span>
|
||||
@@ -34,6 +34,54 @@ function ModelMarker({ model, placement, geometry, setActive, clearActive, marke
|
||||
</g>;
|
||||
}
|
||||
|
||||
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 [active, setActive] = useState(null);
|
||||
const width = 1200, height = 320, left = 74, 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);
|
||||
const minValue = Math.min(...values), maxValue = Math.max(...values);
|
||||
const dateX = date => left + (date - minDate) / (maxDate - minDate) * (width - left - right);
|
||||
const valueY = value => top + (maxValue - value) / (maxValue - minValue || 1) * (height - top - bottom);
|
||||
const activate = (model, event) => {
|
||||
const box = event.currentTarget.closest('.scatter-shell').getBoundingClientRect();
|
||||
const point = event.currentTarget.getBoundingClientRect();
|
||||
setActive({ ...model, tooltipLeft: `${Math.min(82, (point.left - box.left) / box.width * 100)}%`, tooltipTop: `${Math.min(78, (point.top - box.top) / box.height * 100)}%` });
|
||||
};
|
||||
return <section className="release-panel" data-coordinate={field} aria-labelledby={`release-${field}`}>
|
||||
<h2 id={`release-${field}`}>{title}</h2>
|
||||
<p>Dated releases only. Positions are descriptive, not a capability trend.</p>
|
||||
<div className="scatter-shell">
|
||||
<svg viewBox={`0 0 ${width} ${height}`} role="img" aria-label={title} data-panel-model-count={dated.length}>
|
||||
<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} />
|
||||
<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>
|
||||
{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="release-tooltip"
|
||||
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={`../${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" />
|
||||
</g>)}
|
||||
</svg>
|
||||
<Tooltip active={active} geometry={null} data={data} id="release-tooltip" />
|
||||
</div>
|
||||
</section>;
|
||||
}
|
||||
|
||||
function ReleaseScatters({ data, hidden }) {
|
||||
return <section className="release-panels" aria-label="Release-date scatter panels">
|
||||
<ReleaseScatter data={data} hidden={hidden} field="y" title="Release date vs Secular-Rational" />
|
||||
<ReleaseScatter data={data} hidden={hidden} field="x" title="Release date vs Self-expression" />
|
||||
</section>;
|
||||
}
|
||||
|
||||
function Map({ data }) {
|
||||
const query = new URLSearchParams(location.search);
|
||||
const [hidden, setHidden] = useState(() => new Set(query.get('hide')?.split(',').filter(Boolean)));
|
||||
@@ -60,9 +108,8 @@ function Map({ data }) {
|
||||
return <>
|
||||
<section className="controls" aria-label="Model-family visibility">{Object.entries(groups).map(([family, models]) => {
|
||||
const visible = !hidden.has(family);
|
||||
const latest = data.latest_by_family[family].name;
|
||||
return <button key={family} className="chip" type="button" aria-pressed={visible} onClick={() => toggle(family)}>
|
||||
<img src={`../${data.logos[family]}`} alt="" />{family}: {latest}
|
||||
<img src={`../${data.logos[family]}`} alt="" />{family}
|
||||
</button>;
|
||||
})}</section>
|
||||
<div className="chart-shell">
|
||||
@@ -82,13 +129,14 @@ function Map({ data }) {
|
||||
</svg>
|
||||
<Tooltip active={active} geometry={geometry} data={data} />
|
||||
</div>
|
||||
<ReleaseScatters data={data} hidden={hidden} />
|
||||
</>;
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [data, setData] = useState(null);
|
||||
useEffect(() => { fetch('../wvs_map_data.json').then(response => response.json()).then(setData); }, []);
|
||||
return <main><h1>Frontier LLMs on the World Values Survey</h1><p className="lede">React/SVG rendering of the shared WVS coordinate artifact. White-ring marks use locally saved lab logos. Focus or hover a model for its measured readout and release provenance.</p>{data && <Map data={data} />}<p className="caption">Historical coordinates are recovered rounded display values. Coder and vision-language Qwen variants are visible but not used in direct-instruct family trends. The static map remains available at <a href="../">/wvs/</a>.</p></main>;
|
||||
return <main><h1>Frontier LLMs on the World Values Survey</h1><p className="lede">React/SVG rendering of the shared WVS coordinate artifact. White-ring marks use locally saved lab logos. Focus or hover a model for its measured readout and release provenance.</p>{data && <Map data={data} />}</main>;
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('root')).render(<App/>);
|
||||
|
||||
@@ -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,.caption{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}.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}.caption{font-size:.9rem;margin-top:1rem}@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: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}}
|
||||
+35
-10
@@ -46,10 +46,13 @@ def main() -> None:
|
||||
page.wait_for_selector("svg .model-mark")
|
||||
assert page.locator("path.zone").count() == 4
|
||||
assert page.locator("polygon.zone").count() == 0
|
||||
assert "64 models" not in page.locator("main").inner_text()
|
||||
assert "Historical coordinates" not in page.locator("main").inner_text()
|
||||
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(page.locator("svg").get_attribute("data-median-x")) == data["median"]["x"]
|
||||
assert float(page.locator("svg").get_attribute("data-median-y")) == data["median"]["y"]
|
||||
map_svg = page.get_by_role("img", name="Frontier LLMs on the World Values Survey")
|
||||
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(
|
||||
"nodes => nodes.map(node => [node.dataset.model, Number(node.dataset.x), Number(node.dataset.y)])"
|
||||
)
|
||||
@@ -61,22 +64,32 @@ def main() -> None:
|
||||
country_before = page.locator("circle.country").evaluate_all(
|
||||
"nodes => nodes.map(node => [node.getAttribute('cx'), node.getAttribute('cy')])"
|
||||
)
|
||||
qwen_group = page.locator('svg > g[data-family="qwen"]')
|
||||
dated = [model for model in data["models"] if model["provenance"]["release_created"]]
|
||||
qwen_group = map_svg.locator(':scope > g[data-family="qwen"]')
|
||||
assert qwen_group.get_attribute("display") in (None, "inline")
|
||||
assert page.locator('svg [data-family="qwen"] .model-label').count() == 1
|
||||
assert map_svg.locator('[data-family="qwen"] .model-label').count() == 1
|
||||
assert page.locator(".release-panel").count() == 2
|
||||
assert page.locator(".release-mark").count() == len(dated) * 2
|
||||
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)
|
||||
assert page.locator(".release-trajectory").count() == 0
|
||||
page.screenshot(path=OUT / "wvs_react_playwright_release_panels.png", full_page=True)
|
||||
|
||||
page.get_by_role("button", name="qwen: qwen3.8-flash").click()
|
||||
page.get_by_role("button", name="qwen", exact=True).click()
|
||||
page.wait_for_timeout(100)
|
||||
assert qwen_group.get_attribute("display") == "none"
|
||||
assert page.locator('svg [data-family="qwen"] .model-label').count() == 1
|
||||
assert page.locator('svg [data-family="muse"] .model-label').count() == 1
|
||||
assert map_svg.locator('[data-family="qwen"] .model-label').count() == 1
|
||||
assert map_svg.locator('[data-family="muse"] .model-label').count() == 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
|
||||
page.screenshot(path=OUT / "wvs_react_playwright_qwen_hidden.png", full_page=True)
|
||||
|
||||
page.get_by_role("button", name="qwen: qwen3.8-flash").click()
|
||||
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")
|
||||
@@ -89,6 +102,16 @@ def main() -> None:
|
||||
assert page.evaluate("document.activeElement.dataset.model") == "qwen3.8-flash"
|
||||
assert page.locator("#model-tooltip").is_visible()
|
||||
page.screenshot(path=OUT / "wvs_react_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()
|
||||
page.wait_for_selector("#release-tooltip")
|
||||
assert "qwen3.8-flash" in page.locator("#release-tooltip").inner_text()
|
||||
page.screenshot(path=OUT / "wvs_react_playwright_release_panel_hover.png", full_page=True)
|
||||
release_marker.focus()
|
||||
assert page.evaluate("document.activeElement.dataset.releaseModel") == "qwen3.8-flash"
|
||||
assert page.locator("#release-tooltip").is_visible()
|
||||
page.screenshot(path=OUT / "wvs_react_playwright_release_panel_keyboard_focus.png", full_page=True)
|
||||
browser.close()
|
||||
|
||||
if errors:
|
||||
@@ -110,8 +133,10 @@ def main() -> None:
|
||||
print(json.dumps({
|
||||
"shared": {"models": 64, "countries": 90, "buffered_hulls": 4, "latest_labels": 13,
|
||||
"numeric_model_country_median_equality": "verified against DOM data attributes"},
|
||||
"qwen_toggle": "clicked, family group hidden, country coordinates invariant",
|
||||
"tooltip": "pointer hover and focus show model, family, coordinates, readout, samples and release metadata",
|
||||
"qwen_toggle": "clicked, map and dated-panel family groups hidden, country coordinates invariant",
|
||||
"tooltip": "map and release-panel pointer hover and focus show model, family, coordinates, readout, samples and release metadata",
|
||||
"release_panels": {"panels": 2, "dated_models": len(dated), "date_order": "DOM order verified", "no_paths": True},
|
||||
"copy": "source caption only; no model-count wording or archaeological paragraph",
|
||||
"visual_contract": "four smooth SVG paths, no zone polygons, source note right of title",
|
||||
"hashes": hashes,
|
||||
}, indent=2))
|
||||
|
||||
Reference in New Issue
Block a user