Smooth React WVS hull boundaries

Co-Authored-By: PI[gpt-5.6-terra] <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
wassname
2026-09-17 07:52:58 +08:00
co-authored by PI[gpt-5.6-terra]
parent 92d7acef65
commit e9bbc08b69
13 changed files with 59 additions and 16 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,3 +1,3 @@
<script type="module" crossorigin src="./assets/index-DbBL7VOr.js"></script>
<script type="module" crossorigin src="./assets/index-DmSBZooV.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-BFIE5goS.css">
<div id="root"></div>
+20
View File
@@ -63,6 +63,26 @@ export function placeLabels(data, geometry) {
return placements;
}
export function roundedHull(points, geometry) {
const p = points.slice(0, -1).map(([x, y]) => ({ x: geometry.x(x), y: geometry.y(y) }));
if (p.length < 3) throw new Error('a closed hull needs three points');
const at = index => p[(index + p.length) % p.length];
const distance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
const toward = (a, b, amount) => ({ x: a.x + (b.x - a.x) * amount, y: a.y + (b.y - a.y) * amount });
const corner = index => {
const previous = at(index - 1), current = at(index), next = at(index + 1);
const radius = Math.min(0.24, 14 / distance(previous, current), 14 / distance(current, next));
return { before: toward(current, previous, radius), current, after: toward(current, next, radius) };
};
const first = corner(0);
let path = `M ${first.after.x} ${first.after.y}`;
for (let index = 1; index <= p.length; index += 1) {
const c = corner(index % p.length);
path += ` L ${c.before.x} ${c.before.y} Q ${c.current.x} ${c.current.y} ${c.after.x} ${c.after.y}`;
}
return `${path} Z`;
}
export function assertLayout(data) {
const geometry = projectGeometry(data);
const labels = placeLabels(data, geometry);
+3 -3
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { VIEW, assertLayout } from './layout.js';
import { VIEW, assertLayout, roundedHull } from './layout.js';
import './style.css';
function Tooltip({ active, geometry, data }) {
@@ -72,13 +72,13 @@ function Map({ data }) {
<g className="grid">{Array.from({ length: 8 }, (_, index) => <line key={`v${index}`} x1={geometry.bounds.left + index * (geometry.bounds.right - geometry.bounds.left) / 7} y1={geometry.bounds.top} x2={geometry.bounds.left + index * (geometry.bounds.right - geometry.bounds.left) / 7} y2={geometry.bounds.bottom} />)}{Array.from({ length: 6 }, (_, index) => <line key={`h${index}`} x1={geometry.bounds.left} y1={geometry.bounds.top + index * (geometry.bounds.bottom - geometry.bounds.top) / 5} x2={geometry.bounds.right} y2={geometry.bounds.top + index * (geometry.bounds.bottom - geometry.bounds.top) / 5} />)}</g>
<line className="median" x1={geometry.bounds.left} y1={yMedian} x2={geometry.bounds.right} y2={yMedian} />
<line className="median" x1={xMedian} y1={geometry.bounds.top} x2={xMedian} y2={geometry.bounds.bottom} />
{data.zone_hulls.map(zone => <polygon key={zone.name} className="zone" points={zone.points.map(([px, py]) => `${geometry.x(px)},${geometry.y(py)}`).join(' ')} stroke={zone.color} />)}
{data.zone_hulls.map(zone => <path key={zone.name} className="zone" d={roundedHull(zone.points, geometry)} stroke={zone.color} />)}
{data.countries.map(country => <g key={country.name}><circle className="country" data-country={country.name} data-x={country.x} data-y={country.y} cx={geometry.x(country.x)} cy={geometry.y(country.y)} r="3.5" fill={country.color} />{country.label && <text className="country-label" x={labels[`country:${country.name}`].cx} y={labels[`country:${country.name}`].cy + 4} textAnchor="middle">{country.name}</text>}</g>)}
{data.zone_hulls.map(zone => <text key={zone.name} className="zone-label" x={labels[`zone:${zone.name}`].cx} y={labels[`zone:${zone.name}`].cy + 5} textAnchor="middle" fill={zone.color}>{zone.name}</text>)}
{Object.entries(groups).map(([family, models]) => <g key={family} data-family={family} display={hidden.has(family) ? 'none' : 'inline'}>{models.map(model => <ModelMarker key={model.name} model={model} placement={labels} geometry={geometry} setActive={setActive} clearActive={clearActive} markerRef={model.name === focusName ? focusRef : null} logo={data.logos[model.family]} />)}</g>)}
<g className="poles"><line x1={xMedian} y1="62" x2={xMedian} y2={geometry.bounds.top} markerEnd="url(#arrow)" /><line x1={xMedian} y1={geometry.bounds.bottom} x2={xMedian} y2="838" markerEnd="url(#arrow)" /><line x1="64" y1={yMedian} x2={geometry.bounds.left} y2={yMedian} markerEnd="url(#arrow)" /><line x1={geometry.bounds.right} y1={yMedian} x2="1184" y2={yMedian} markerEnd="url(#arrow)" /><text x={xMedian} y="40" textAnchor="middle">{data.axis.y[1]}</text><text x={xMedian} y="870" textAnchor="middle">{data.axis.y[0]}</text><text x="25" y={yMedian + 7}>{data.axis.x[0]}</text><text x="1136" y={yMedian + 7} textAnchor="end">{data.axis.x[1]}</text></g>
<text className="map-title" x={geometry.bounds.left + 8} y={geometry.bounds.bottom - 34}>{data.title.split('\n').map((line, index) => <tspan key={line} x={geometry.bounds.left + 8} dy={index ? 17 : 0}>{line}</tspan>)}</text>
<text className="map-note" x={geometry.bounds.left + 8} y={geometry.bounds.bottom - 7}>{data.note.split('\n').map((line, index) => <tspan key={line} x={geometry.bounds.left + 8} dy={index ? 11 : 0}>{line}</tspan>)}</text>
<text className="map-note" x={geometry.bounds.right - 8} y={geometry.bounds.bottom - 20} textAnchor="end">{data.note.split('\n').map((line, index) => <tspan key={line} x={geometry.bounds.right - 8} dy={index ? 11 : 0}>{line}</tspan>)}</text>
</svg>
<Tooltip active={active} geometry={geometry} data={data} />
</div>
+5
View File
@@ -44,6 +44,10 @@ def main() -> None:
screenshot(page, f"{BASE}/", "wvs_vanilla_playwright.png")
screenshot(page, f"{BASE}/react/", "wvs_react_playwright_default.png")
page.wait_for_selector("svg .model-mark")
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(page.locator("svg").get_attribute("data-median-x")) == data["median"]["x"]
assert float(page.locator("svg").get_attribute("data-median-y")) == data["median"]["y"]
rendered_models = page.locator(".model-mark").evaluate_all(
@@ -108,6 +112,7 @@ def main() -> None:
"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",
"visual_contract": "four smooth SVG paths, no zone polygons, source note right of title",
"hashes": hashes,
}, indent=2))
@@ -8,8 +8,9 @@
},
"qwen_toggle": "clicked, family group hidden, country coordinates invariant",
"tooltip": "pointer hover and focus show model, family, coordinates, readout, samples and release metadata",
"visual_contract": "four smooth SVG paths, no zone polygons, source note right of title",
"hashes": {
"wvs_map_data.json": "b45e8cd6396b2d78570af67a5a01992a85ec7b2bb3bca776264aaeaa7057589a",
"index.html": "f376a62a94a4df7bac9bfb6252b227fa7aac77647e6c32d5b0a2158dd11fd04b"
"index.html": "092bb3ba210cbd2b857d1d459773fb81e2699310cb2fa9ed294e7f81aab12d0b"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 654 KiB

After

Width:  |  Height:  |  Size: 654 KiB

@@ -0,0 +1,17 @@
# React WVS smooth-hull review
Reviewer: reviewer-openai, image-only review, 2026-09-17.
Verdict: PASS for the supplied image-only review.
Observed:
- Blue West, brown African-Islamic, red East Asia and orange Latin America hulls appear continuously closed with rounded corners. No visible protruding spikes, closure gaps or kinked joins.
- React title is bottom-left; source/note is bottom-right and clear of plotted content.
- Axes, country dots, coloured outline-only regions and model clustering match the static visual grammar. Logos and leader-lined labels remain readable.
- Hover and keyboard-focus captures show an unclipped Qwen tooltip with coordinates, readout, samples and release date.
- Qwen hidden removes Qwen marks and its plot label, strikes through its control and leaves country/hull layout visibly unchanged.
Limit: screenshots establish rendered appearance, not mathematical continuity or actual keyboard event execution. The UAT JSON reports buffered hull, latest-label and coordinate-invariance checks.
-- PI[gpt-5.6-terra], preserving reviewer-openai observations