import React, { useEffect, useMemo, useRef, useState } from 'react'; import { createRoot } from 'react-dom/client'; import { VIEW, assertLayout, roundedHull } from './layout.js'; import './style.css'; const LOGO_ROOT = 'wvs/'; function visibleFamilyNames(groups, hidden) { return Object.keys(groups).filter(family => !hidden.has(family)); } function Tooltip({ active, geometry, data, id = 'model-tooltip' }) { if (!active) return null; const { provenance } = active; 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 ; } function ModelMarker({ model, placement, geometry, setActive, clearActive, markerRef, logo }) { const cx = geometry.x(model.x), cy = geometry.y(model.y); const label = model.label ? placement[`model:${model.name}`] : null; const leader = label && Math.hypot(label.cx - cx, label.cy - cy) > 20; return setActive(model)} onPointerLeave={clearActive} onFocus={() => setActive(model)} onBlur={clearActive}> {leader && } {label && {model.label}} ; } 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 [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 panelId = `release-${field}`; const tooltipId = `${panelId}-tooltip`; 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

{title}

{title} 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. {Array.from({ length: 5 }, (_, index) => )} {new Date(minDate).toISOString().slice(0, 10)} {new Date(maxDate).toISOString().slice(0, 10)} {maxValue.toFixed(2)} {minValue.toFixed(2)} {dated.map(model => activate(model, event)} onPointerLeave={() => setActive(null)} onFocus={event => activate(model, event)} onBlur={() => setActive(null)}> )}
; } function ReleaseScatters({ data, hidden }) { return
; } function Map({ data }) { const query = new URLSearchParams(location.search); const [hidden, setHidden] = useState(() => new Set(query.get('hide')?.split(',').filter(Boolean))); 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); const groups = useMemo(() => Object.groupBy(data.models, model => model.family), [data]); const { labels, geometry } = useMemo(() => assertLayout(data), [data]); const visibleFamilies = visibleFamilyNames(groups, hidden); const visibleModelCount = data.models.filter(model => !hidden.has(model.family)).length; useEffect(() => { if (focusName) focusRef.current?.focus(); }, [focusName]); const clearActive = event => { if (event.currentTarget.matches(':focus')) return; setActive(null); }; const toggle = family => setHidden(old => { const next = new Set(old); if (next.has(family)) next.delete(family); else next.add(family); return next; }); const xMedian = geometry.x(data.median.x), yMedian = geometry.y(data.median.y); return <>
{Object.entries(groups).map(([family]) => { const visible = !hidden.has(family); return ; })}
Frontier LLMs on the World Values Survey World Values Survey cultural map. Horizontal direction runs from Self-expression on the left to Survival on the right. Vertical direction runs from Traditional below to Secular-Rational above. Coloured dots are selected WVS countries, outlines are cultural regions, and white-ring logo marks are models. {visibleModelCount} model marks are visible from {visibleFamilies.join(', ') || 'no families'}. Use the family controls to hide marks and labels. Hover or keyboard focus a model for model-specific details. {Array.from({ length: 8 }, (_, index) => )}{Array.from({ length: 6 }, (_, index) => )} {data.zone_hulls.map(zone => )} {data.countries.map(country => {country.label && {country.name}})} {data.zone_hulls.map(zone => {zone.name})} {Object.entries(groups).map(([family, models]) => {models.map(model => )})} {data.axis.y[1]}{data.axis.y[0]}{data.axis.x[0]}{data.axis.x[1]} {data.title.split('\n').map((line, index) => {line})} {data.note.split('\n').map((line, index) => {line})}