mirror of
https://github.com/wassname/moral-maps.git
synced 2026-09-23 13:30:23 +08:00
Publish WVS capability selector
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
0c17c4be14
commit
08139484c0
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { VIEW, assertLayout, roundedHull } from './layout.js';
|
||||
import { VIEW, RINGS, LABEL_STEPS, assertLayout, box, hits, inside, roundedHull } from './layout.js';
|
||||
import './style.css';
|
||||
|
||||
const LOGO_ROOT = 'wvs/';
|
||||
@@ -16,10 +16,11 @@ function Tooltip({ active, geometry, id = 'model-tooltip', panel }) {
|
||||
const release = active.provenance.release_created && <span>release {active.provenance.release_created}</span>;
|
||||
return <aside id={id} className="tooltip" style={{ left, top }} role="status">
|
||||
<strong>{active.name}</strong>
|
||||
{active.tooltipIndex != null && <span>Intelligence Index: {active.tooltipIndex.toFixed(2)}</span>}
|
||||
{panel === 'secular' && <span>Secular-Rational: {active.y.toFixed(3)}</span>}
|
||||
{panel === 'self-expression' && <span>Self-expression: {active.tooltipValue.toFixed(3)}</span>}
|
||||
{!panel && <><span>Self-expression/Survival: {active.x.toFixed(3)}</span><span>Traditional/Secular-Rational: {active.y.toFixed(3)}</span></>}
|
||||
{release}
|
||||
{active.tooltipIndex == null && release}
|
||||
</aside>;
|
||||
}
|
||||
|
||||
@@ -38,9 +39,9 @@ function ModelMarker({ model, placement, geometry, setActive, clearActive, marke
|
||||
</g>;
|
||||
}
|
||||
|
||||
function ordinaryLeastSquares(models, value) {
|
||||
const x = models.map(model => Date.parse(model.provenance.release_created));
|
||||
const y = models.map(value);
|
||||
function ordinaryLeastSquares(models, xValue, yValue) {
|
||||
const x = models.map(xValue);
|
||||
const y = models.map(yValue);
|
||||
const uniqueX = new Set(x);
|
||||
if (uniqueX.size < 2) return null;
|
||||
const xMean = x.reduce((sum, value) => sum + value, 0) / x.length;
|
||||
@@ -53,70 +54,114 @@ function ordinaryLeastSquares(models, value) {
|
||||
return { slope, intercept, n: models.length, r2: total === 0 ? null : 1 - residual / total };
|
||||
}
|
||||
|
||||
function ReleaseScatter({ data, hidden, field, title }) {
|
||||
function ReleaseScatter({ data, hidden, field, title, axisMode }) {
|
||||
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 matched = useMemo(() => data.models.filter(model => Number.isFinite(model.provenance.intelligence_index))
|
||||
.toSorted((a, b) => a.provenance.intelligence_index - b.provenance.intelligence_index || a.name.localeCompare(b.name)), [data]);
|
||||
const plotted = axisMode === 'release-date' ? dated : matched;
|
||||
const visible = plotted.filter(model => !hidden.has(model.family));
|
||||
const coordinate = model => field === 'x' ? -model.x : model.y;
|
||||
const fit = ordinaryLeastSquares(visible, coordinate);
|
||||
const xValue = model => axisMode === 'release-date' ? Date.parse(model.provenance.release_created) : model.provenance.intelligence_index;
|
||||
const fit = ordinaryLeastSquares(visible, xValue, coordinate);
|
||||
const [active, setActive] = useState(null);
|
||||
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(coordinate);
|
||||
const minDate = Math.min(...dates), maxDate = Math.max(...dates);
|
||||
const xValues = plotted.map(xValue);
|
||||
const values = plotted.map(coordinate);
|
||||
const minX = Math.min(...xValues), maxX = Math.max(...xValues);
|
||||
const minValue = Math.min(...values), maxValue = Math.max(...values);
|
||||
const dateX = date => left + (date - minDate) / (maxDate - minDate) * (width - left - right);
|
||||
const plotX = value => left + (value - minX) / (maxX - minX || 1) * (width - left - right);
|
||||
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].reverse();
|
||||
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 visibleX = visible.map(xValue);
|
||||
const fitEnd = fit && [Math.min(...visibleX), Math.max(...visibleX)].map(value => [plotX(value), valueY(fit.intercept + fit.slope * value)]);
|
||||
const frontier = useMemo(() => {
|
||||
if (axisMode !== 'release-date') return [];
|
||||
let high = -Infinity;
|
||||
return visible.filter(model => Number.isFinite(model.provenance.intelligence_index)).filter(model => {
|
||||
if (model.provenance.intelligence_index <= high) return false;
|
||||
high = model.provenance.intelligence_index;
|
||||
return true;
|
||||
});
|
||||
}, [axisMode, visible]);
|
||||
const frontierPlacement = useMemo(() => {
|
||||
const bounds = { left, right: width - right, top, bottom: height - bottom };
|
||||
const taken = plotted.map(model => box(plotX(xValue(model)), valueY(coordinate(model)), 16, 16));
|
||||
const placement = {};
|
||||
for (const model of frontier) {
|
||||
const anchor = { x: plotX(xValue(model)), y: valueY(coordinate(model)) };
|
||||
const size = { width: model.name.length * 8 + 12, height: 18 };
|
||||
let found = null;
|
||||
for (const step of LABEL_STEPS) {
|
||||
for (const [dx, dy] of RINGS) {
|
||||
const candidate = box(anchor.x + dx * (step + size.width / 3), anchor.y + dy * step, size.width, size.height);
|
||||
if (inside(candidate, bounds) && !taken.some(obstacle => hits(candidate, obstacle))) { found = candidate; break; }
|
||||
}
|
||||
if (found) break;
|
||||
}
|
||||
if (!found) throw new Error(`no frontier label location for ${model.name}`);
|
||||
placement[model.name] = { ...found, anchor };
|
||||
taken.push(found);
|
||||
}
|
||||
return placement;
|
||||
}, [frontier, plotted, minX, maxX, minValue, maxValue]);
|
||||
const activate = (model, event) => {
|
||||
const box = event.currentTarget.closest('.scatter-shell').getBoundingClientRect();
|
||||
const point = event.currentTarget.getBoundingClientRect();
|
||||
setActive({ ...model, tooltipValue: coordinate(model), tooltipLeft: `${Math.min(82, (point.left - box.left) / box.width * 100)}%`, tooltipTop: `${Math.min(78, (point.top - box.top) / box.height * 100)}%` });
|
||||
setActive({ ...model, tooltipValue: coordinate(model), tooltipIndex: axisMode === 'capability' ? model.provenance.intelligence_index : null, 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={`${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} data-fit-n={fit?.n ?? 0} data-fit-r2={fit?.r2 ?? ''} data-fit-slope={fit?.slope ?? ''}>
|
||||
<svg id={`${panelId}-svg`} viewBox={`0 0 ${width} ${height}`} role="img" aria-labelledby={`${panelId}-svg-title ${panelId}-svg-desc`} data-axis-mode={axisMode} data-panel-model-count={visible.length} data-omitted-model-count={data.models.length - plotted.length} data-fit-n={fit?.n ?? 0} data-fit-r2={fit?.r2 ?? ''} data-fit-slope={fit?.slope ?? ''}>
|
||||
<title id={`${panelId}-svg-title`}>{title}</title>
|
||||
<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>
|
||||
<desc id={`${panelId}-svg-desc`}>Scatter plot with {axisMode === 'release-date' ? 'release date' : 'Artificial Analysis Intelligence Index'} on the horizontal axis and {yDirection.join(' to ')} increasing upward on the vertical axis. {visible.length} of {plotted.length} matched models are visible from {visibleFamilyNames(Object.groupBy(data.models, model => model.family), hidden).join(', ') || 'no families'}; {data.models.length - plotted.length} plotted models lack this x value and are omitted only here. 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 matched 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 x values 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={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} y={height - 18}>{axisMode === 'release-date' ? new Date(minX).toISOString().slice(0, 10) : minX.toFixed(1)}</text>
|
||||
<text className="scatter-tick" x={width - right} y={height - 18} textAnchor="end">{axisMode === 'release-date' ? new Date(maxX).toISOString().slice(0, 10) : maxX.toFixed(1)}</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}, R² {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} data-coordinate-value={coordinate(model)} display={hidden.has(model.family) ? 'none' : 'inline'}
|
||||
tabIndex="0" role="button" aria-label={`${model.name}, ${model.family}, ${model.provenance.release_created}`} aria-describedby={tooltipId}
|
||||
{plotted.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} data-capability-score={model.provenance.intelligence_index} data-coordinate-value={coordinate(model)} display={hidden.has(model.family) ? 'none' : 'inline'}
|
||||
tabIndex="0" role="button" aria-label={`${model.name}, ${axisMode === 'release-date' ? model.provenance.release_created : `Intelligence Index ${model.provenance.intelligence_index}`}`} 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(coordinate(model))} r="8" stroke={model.color} />
|
||||
<image href={`${LOGO_ROOT}${data.logos[model.family]}`} x={dateX(Date.parse(model.provenance.release_created)) - 5} y={valueY(coordinate(model)) - 5} width="10" height="10" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false" />
|
||||
<circle className="model-ring" cx={plotX(xValue(model))} cy={valueY(coordinate(model))} r="8" stroke={model.color} />
|
||||
<image href={`${LOGO_ROOT}${data.logos[model.family]}`} x={plotX(xValue(model)) - 5} y={valueY(coordinate(model)) - 5} width="10" height="10" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false" />
|
||||
</g>)}
|
||||
{axisMode === 'release-date' && frontier.map(model => <g key={`frontier-${model.name}`} className="frontier-label" data-frontier-model={model.name}><line x1={frontierPlacement[model.name].anchor.x} y1={frontierPlacement[model.name].anchor.y} x2={frontierPlacement[model.name].cx} y2={frontierPlacement[model.name].cy} /><text x={frontierPlacement[model.name].cx} y={frontierPlacement[model.name].cy + 4} textAnchor="middle">{model.name}</text></g>)}
|
||||
</svg>
|
||||
<Tooltip active={active} geometry={null} id={tooltipId} panel={field === 'y' ? 'secular' : 'self-expression'} />
|
||||
</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" />
|
||||
function ReleaseScatters({ data, hidden, axisMode, setAxisMode }) {
|
||||
const xLabel = axisMode === 'release-date' ? 'Release date' : data.capability_x.label;
|
||||
return <section className="release-panels" aria-label="Release-date and capability scatter panels">
|
||||
<div className="release-axis-selector">
|
||||
<label>Release-panel x axis <select aria-label="Release-panel x axis" aria-controls="release-y-svg release-x-svg" value={axisMode} onChange={event => setAxisMode(event.target.value)}>
|
||||
<option value="release-date">Release date</option>
|
||||
<option value="capability">{data.capability_x.label}</option>
|
||||
</select></label>
|
||||
<span><a href={data.capability_x.source_url}>{data.capability_x.label}</a>, saved {data.capability_x.fetched_utc.slice(0, 10)}. {axisMode === 'capability' ? `${data.capability_x.matched_models} matched, ${data.models.length - data.capability_x.matched_models} omitted.` : 'Release-date labels mark running Intelligence Index highs among shown mapped models.'}</span>
|
||||
</div>
|
||||
<ReleaseScatter data={data} hidden={hidden} field="y" axisMode={axisMode} title={`${xLabel} vs Secular-Rational`} />
|
||||
<ReleaseScatter data={data} hidden={hidden} field="x" axisMode={axisMode} title={`${xLabel} 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)));
|
||||
const [axisMode, setAxisMode] = useState(() => query.get('axis') === 'capability' ? 'capability' : 'release-date');
|
||||
const [active, setActive] = useState(() => data.models.find(model => model.name === (query.get('tooltip') || query.get('focus'))) ?? null);
|
||||
const focusName = query.get('focus');
|
||||
const focusRef = useRef(null);
|
||||
@@ -146,13 +191,6 @@ function Map({ data }) {
|
||||
<img src={`${LOGO_ROOT}${data.logos[family]}`} alt="" aria-hidden="true" />{family}
|
||||
</button>;
|
||||
})}</section>
|
||||
<div className="release-axis-selector">
|
||||
<label>Release-panel x axis <select aria-label="Release-panel x axis" value="release-date" onChange={() => {}}>
|
||||
<option value="release-date">Release date</option>
|
||||
<option value="capability" disabled>{data.capability_x.label}, unavailable pending redistribution permission</option>
|
||||
</select></label>
|
||||
<span>{data.capability_x.blocker}</span>
|
||||
</div>
|
||||
<div className="chart-shell">
|
||||
<svg viewBox={`0 0 ${VIEW.width} ${VIEW.height}`} role="img" aria-labelledby="map-svg-title map-svg-desc" data-median-x={data.median.x} data-median-y={data.median.y} data-visible-model-count={visibleModelCount}>
|
||||
<title id="map-svg-title">Frontier LLMs on the World Values Survey</title>
|
||||
@@ -173,8 +211,8 @@ function Map({ data }) {
|
||||
<Tooltip active={active} geometry={geometry} />
|
||||
</div>
|
||||
<p className="map-explanation">Since 1981, the World Values Survey has asked people in about ninety countries the same questions. Its axes run from Traditional to Secular-Rational and from Self-expression to Survival.</p>
|
||||
<ReleaseScatters data={data} hidden={hidden} />
|
||||
<p className="caption">Use the family controls to compare saved rated coordinates. The lines are weak descriptive correlations for the dated models currently shown; n and R² update with visibility. Hover or keyboard focus a mark for its coordinates and recorded release date. See the <a href="https://github.com/wassname/moral-maps">code and records</a>.</p>
|
||||
<ReleaseScatters data={data} hidden={hidden} axisMode={axisMode} setAxisMode={setAxisMode} />
|
||||
<p className="caption">Use the family controls to compare saved rated coordinates. The lines are weak descriptive correlations for the matched models currently shown; n and R² update with visibility. Hover or keyboard focus a mark for its values. See the <a href="https://github.com/wassname/moral-maps">code and records</a>.</p>
|
||||
</>;
|
||||
}
|
||||
|
||||
|
||||
@@ -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}.release-axis-selector{display:flex;align-items:baseline;gap:.65rem;margin:.15rem 0 .65rem;color:#5b554b;font-size:.82rem}.release-axis-selector select{font:inherit}.release-axis-selector span{color:#756f64}.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}.release-axis-selector{align-items:flex-start;flex-direction:column;gap:.15rem}.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}.release-axis-selector{display:flex;align-items:baseline;gap:.65rem;margin:.15rem 0 .65rem;color:#5b554b;font-size:.82rem}.release-axis-selector select{font:inherit}.release-axis-selector span{color:#756f64}.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}.frontier-label line{stroke:#aaa398;stroke-width:.8}.frontier-label text{fill:#4d4840;font-size:10px;font-weight:700;paint-order:stroke;stroke:var(--canvas);stroke-width:3px;stroke-linejoin:round}.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}.release-axis-selector{align-items:flex-start;flex-direction:column;gap:.15rem}.chip{font-size:.7rem}.poles text{font-size:14px}.country-label,.model-label{font-size:10px}.tooltip{max-width:230px;font-size:.75rem}}
|
||||
Reference in New Issue
Block a user