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
+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>