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
{active.name}
{active.family}
{data.axis.x[0]} {'->'} {data.axis.x[1]}: {active.x.toFixed(3)}
{data.axis.y[0]} {'->'} {data.axis.y[1]}: {active.y.toFixed(3)}
{provenance.readout}, {samples}
{provenance.release_created ? `release ${provenance.release_created}` : provenance.release_source}
;
}
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 toggle(family)}>
{family}
;
})}
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} )}
>;
}
function App() {
const [data, setData] = useState(null);
useEffect(() => { fetch('wvs/wvs_map_data.json').then(response => response.json()).then(setData); }, []);
return
How do AI models score on human values surveys? Which culture are they most similar to? Is it changing over time?
To answer these we start with the World Values Survey , the standard culture map of the world. Since 1981 it has asked people in about ninety countries the same questions. Two axes drawn from it sort societies by how traditional or secular they are and how much they weigh survival over self-expression.
Use the family controls to compare saved model coordinates. Hover or keyboard focus a mark for its model-specific provenance. See the code and records .
{data && }
;
}
createRoot(document.getElementById('root')).render( );