mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
Every word the landing, the header menus, the phone sheet, the footer,
the help corner and the way in (sign-in, sign-up, the reset, the
invitation, the callback, onboarding, the 404) write goes through
@hanzo/i18n's t(): one sentence is one message, its figures, names and
links arguments; copy written as data is phrase('…') where written and
t(label) where drawn. Plans, the chart, the API states and the lines
lib/kai and lib/enso build for the landing do the same.
Right to left: <html lang dir> is set before the first paint (the
package's bootScript first in the head); a figure, clock, range or
price outside a sentence is isolated; sides are logical gui props, the
layout's [data-join] corners logical, a bar grows from the start;
arrows along the line mirror (Onward for "words ->" links). The repo
dates read in the reader's language.
Gates select by slot, never by an English label (rtl, help). New gate
translated.spec.ts: in Arabic the landing at 1440 and 390, every menu
open, the phone sheet, sign-in, sign-up and the app signed in hold no
authored English, nothing scrolls sideways, every range is isolated;
French the same for English's commonest words. What another package
draws with no words prop is listed by name (@hanzo/ui's Footer label,
@hanzo/build's composer row and suggestions).
@hanzo/i18n 0.5.3 (the catalogs: 1,443 keys, every offered locale
complete), @hanzo/app 0.2.1 (the app's words). The link gate imports
the terms versions from @hanzo/app, the one SDK main now resolves.
440 lines
20 KiB
TypeScript
440 lines
20 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* GPQA Diamond against what it costs: the accuracy-at-cost scatter /enso drew
|
|
* before its figures were read live (AccuracyCostScatter, removed in d0199249),
|
|
* drawn from the research API's runs when the page opens (lib/enso `field`).
|
|
* Up is accuracy, right is what 1,000 questions cost on a log axis. Each single
|
|
* model is its best run on the whole set, wearing its lab's mark on a plain disc;
|
|
* a published score is a dashed disc with its lab's mark, linking its source;
|
|
* Enso's tiers are ink discs wearing Hanzo's H (components/models/Maker). The
|
|
* frontier, the points no other beats on both, is computed and said in the alt
|
|
* text and each point's `data-front`, and not drawn (owner, 2026-10-17: "remove
|
|
* the line between dots"). What each point is (its set, its cost, its source)
|
|
* is in its tooltip and the alt text, not a legend.
|
|
*
|
|
* ONE PLOT, MEASURED TO ITS CARD. The SVG's user unit is a CSS pixel: the card's
|
|
* width is read (ResizeObserver) and the plot is laid out at exactly that width,
|
|
* so it fills the card at every size. Wide, it is 2.5 times as wide as tall
|
|
* (owner: "not so tall"), its type the token's 12px, a tick a rung of the
|
|
* one-three ladder; under PHONE px it is a little taller than wide, its type
|
|
* 11px and its discs smaller, a tick a decade. Labels are seated by `settle`,
|
|
* beside their dots, clear of every disc and label.
|
|
*
|
|
* Every colour is the page's ink or a token, so both themes draw it.
|
|
*
|
|
* A chart's axes do not turn on a right-to-left page: the plot is drawn left to
|
|
* right in every language (`direction` on the svg), and only its words are the
|
|
* reader's.
|
|
*/
|
|
import { useLayoutEffect, useRef, useState } from 'react'
|
|
import { typography } from '@hanzo/design'
|
|
import { phrase } from '@hanzo/i18n'
|
|
import { useI18n, type Say } from '@hanzo/i18n/react'
|
|
import { Ready } from '@/components/api/state'
|
|
import { Card } from '@/components/marketing/band'
|
|
import { Maker } from '@/components/models/Maker'
|
|
import { View } from '@/components/ui'
|
|
import { useEnso } from '@/hooks/useEnso'
|
|
import { LEAST, pct, short, usd, type Dot, type Field } from '@/lib/enso'
|
|
|
|
/** A rem token in CSS px, against the 16px root this site does not move; a token it cannot read stops the build here rather than drawing NaN. */
|
|
const unit = (rem: string) => {
|
|
const n = parseFloat(rem) * 16
|
|
if (!Number.isFinite(n)) throw new Error(`Chart: '${rem}' is not a length this chart can compute with`)
|
|
return n
|
|
}
|
|
|
|
/** Zen Mono's advance (0.6em) and the box a line of it is drawn in, measured for the scatter this replaces. */
|
|
const ADVANCE = 0.6
|
|
const RISE = 1.05
|
|
const DROP = 0.35
|
|
/** Daylight between a drawn edge and a label. */
|
|
const AIR = 2
|
|
/** Under this width, in px, the plot is the phone's; under LARGE, its type and discs are a step smaller. */
|
|
const PHONE = 600
|
|
const LARGE = 1000
|
|
/** The type: the label token at 12px, and 11px on a phone, never under. */
|
|
const TYPE = unit(typography['text-xs'])
|
|
const SMALL = TYPE - 1
|
|
/** How many lines a model's label may step away from its dot, with a leader, before it is left to the tooltip. */
|
|
const REACH = 2
|
|
/** How far, in px, a label keeps from a dot not its own that it sits level with or a leader passes. */
|
|
const CLEAR = 6
|
|
|
|
/** The discs: a model's, ours, our halo (the drawn edge), and the marks inside them. Ours stays the largest. */
|
|
type Disc = { r: number; ours: number; halo: number; mark: number; h: number }
|
|
type Geom = {
|
|
key: 'phone' | 'wide'
|
|
w: number
|
|
h: number
|
|
pad: { l: number; r: number; t: number; b: number }
|
|
type: number
|
|
disc: Disc
|
|
}
|
|
|
|
const clamp = (v: number, lo: number, hi: number) => Math.min(Math.max(v, lo), hi)
|
|
|
|
/** The plot for a card this wide: on a phone a little taller than wide, with decade ticks; above, 2.5 times as wide as tall. */
|
|
function geom(w: number): Geom {
|
|
return w < PHONE
|
|
? {
|
|
key: 'phone',
|
|
w,
|
|
h: clamp(Math.round(w * 1.3), 360, 560),
|
|
pad: { l: 28, r: 6, t: 26, b: 40 },
|
|
type: SMALL,
|
|
disc: { r: 8, ours: 10, halo: 13, mark: 11, h: 12 },
|
|
}
|
|
: {
|
|
key: 'wide',
|
|
w,
|
|
h: clamp(Math.round(w / 2.5), 380, 460),
|
|
pad: { l: 44, r: 20, t: 28, b: 44 },
|
|
// Under LARGE px the plot is as short and the points as close as a phone's: the phone's type, discs a step up.
|
|
type: w < LARGE ? SMALL : TYPE,
|
|
disc: w < LARGE ? { r: 9, ours: 11, halo: 14, mark: 12, h: 13 } : { r: 11, ours: 13, halo: 17, mark: 15, h: 15 },
|
|
}
|
|
}
|
|
|
|
/** The cost axis's rungs, dollars per 1,000 questions: a one-three ladder, and its decades. */
|
|
const LADDER = [0.01, 0.03, 0.1, 0.3, 1, 3, 10, 30, 100, 300, 1000]
|
|
const DECADES = [0.01, 0.1, 1, 10, 100, 1000]
|
|
|
|
/** The extents and ticks the points need: accuracy from the five under the lowest to 100; price on a phone from the decade under the cheapest to the decade over the dearest, wide from a quarter under the cheapest to a quarter over the dearest, its ticks the ladder's rungs between. */
|
|
function axes(dots: Dot[], g: Geom) {
|
|
const lo = Math.max(0, Math.floor((Math.min(...dots.map((d) => d.accuracy * 100)) - 1) / 5) * 5)
|
|
const prices = dots.map((d) => d.cost).filter((p) => p > 0)
|
|
const [min, max] = [Math.min(...prices) / 1.25, Math.max(...prices) * 1.25]
|
|
// A phone's axis runs decade to decade; a wide one only as far as the points, so they spread over all of it.
|
|
const rungs = g.key === 'phone' ? DECADES : LADDER
|
|
const p0 = g.key === 'phone' ? ([...rungs].reverse().find((t) => t <= min) ?? rungs[0]) : min
|
|
const p1 = g.key === 'phone' ? (rungs.find((t) => t >= max) ?? rungs.at(-1)!) : max
|
|
return {
|
|
acc: { lo, ticks: Array.from({ length: (100 - lo) / 5 + 1 }, (_, i) => lo + i * 5) },
|
|
price: { lo: p0, hi: p1, ticks: rungs.filter((t) => t >= p0 && t <= p1) },
|
|
}
|
|
}
|
|
type Axes = ReturnType<typeof axes>
|
|
|
|
const xOf = (price: number, a: Axes, g: Geom) => {
|
|
const lo = Math.log(a.price.lo)
|
|
const t = (Math.log(clamp(price, a.price.lo, a.price.hi)) - lo) / (Math.log(a.price.hi) - lo)
|
|
return g.pad.l + t * (g.w - g.pad.l - g.pad.r)
|
|
}
|
|
const yOf = (acc: number, a: Axes, g: Geom) => g.h - g.pad.b - ((Math.min(acc, 100) - a.acc.lo) / (100 - a.acc.lo)) * (g.h - g.pad.t - g.pad.b)
|
|
|
|
type Rect = { x0: number; x1: number; y0: number; y1: number }
|
|
const overlaps = (a: Rect, b: Rect) => a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1
|
|
|
|
/** A point placed: its centre, its label (empty when no seat is left), the label's left edge and baseline, and whether a leader joins them. */
|
|
type Placed = Dot & { cx: number; cy: number; head: string; x0: number; labelY: number; lead: boolean }
|
|
|
|
const edgeOf = (p: Dot, g: Geom) => (p.ours ? g.disc.halo : g.disc.r)
|
|
/** A glyph's advance: 0.6em, or the whole pixel a browser snaps a small size to (11px draws 7px a glyph, not 6.6). */
|
|
const advance = (g: Geom) => Math.max(g.type * ADVANCE, Math.round(g.type * ADVANCE))
|
|
const widthOf = (text: string, g: Geom) => text.length * advance(g)
|
|
function labelBox(text: string, x0: number, y: number, g: Geom): Rect {
|
|
return { x0, x1: x0 + widthOf(text, g), y0: y - g.type * RISE, y1: y + g.type * DROP }
|
|
}
|
|
function discBox(p: Placed, g: Geom): Rect {
|
|
const e = edgeOf(p, g) + 1
|
|
return { x0: p.cx - e, x1: p.cx + e, y0: p.cy - e, y1: p.cy + e }
|
|
}
|
|
|
|
/**
|
|
* Where a label may stand, nearest first: at `k` 0 beside its dot (east or west
|
|
* by the room there, then above, below and the four corners); at `k` 1 to
|
|
* REACH, `k` steps further out (up, down, across, or off a corner), joined to
|
|
* its dot by a leader.
|
|
*/
|
|
function seats(p: Placed, text: string, g: Geom, k: number): { x0: number; y: number }[] {
|
|
const w = widthOf(text, g)
|
|
const e = edgeOf(p, g)
|
|
const gap = e + AIR
|
|
const mid = p.cy + g.type * 0.35
|
|
const over = p.cy - e - AIR - g.type * DROP
|
|
const under = p.cy + e + AIR + g.type * RISE
|
|
const east = p.cx + gap
|
|
const west = p.cx - gap - w
|
|
const near = p.cx + gap + w <= g.w - g.pad.r
|
|
const sides = near ? [east, west] : [west, east]
|
|
const corners = [p.cx + e * 0.4, p.cx - e * 0.4 - w]
|
|
if (k === 0)
|
|
return [...sides.map((x0) => ({ x0, y: mid })), { x0: p.cx - w / 2, y: over }, { x0: p.cx - w / 2, y: under }, ...[over, under].flatMap((y) => corners.map((x0) => ({ x0, y })))]
|
|
const step = k * (g.type * (RISE + DROP) + AIR)
|
|
const across = k * g.type * 2
|
|
return [
|
|
...[mid + step, mid - step].flatMap((y) => sides.map((x0) => ({ x0, y }))),
|
|
...(near ? [east + across, west - across] : [west - across, east + across]).map((x0) => ({ x0, y: mid })),
|
|
{ x0: p.cx - w / 2, y: over - step },
|
|
{ x0: p.cx - w / 2, y: under + step },
|
|
...[over - step, under + step].flatMap((y) => corners.map((x0) => ({ x0, y }))),
|
|
]
|
|
}
|
|
|
|
/** Where a label was seated: its text, its left edge and baseline, and how many steps from its dot. */
|
|
type Seat = { head: string; x0: number; labelY: number; k: number }
|
|
/**
|
|
* Whether a label beside `p` sits level with another dot `q` instead: in `q`'s
|
|
* row (their heights overlap) and just across from it, or in its column and
|
|
* just over or under it, while not in `p`'s own row or column.
|
|
*/
|
|
function level(b: Rect, q: Placed, p: Placed, g: Geom): boolean {
|
|
const row = (d: Placed) => d.cy + edgeOf(d, g) > b.y0 && d.cy - edgeOf(d, g) < b.y1
|
|
const column = (d: Placed) => d.cx + edgeOf(d, g) > b.x0 && d.cx - edgeOf(d, g) < b.x1
|
|
const across = Math.max(b.x0 - q.cx, q.cx - b.x1, 0) - edgeOf(q, g)
|
|
const above = Math.max(b.y0 - q.cy, q.cy - b.y1, 0) - edgeOf(q, g)
|
|
return (row(q) && !row(p) && across < AIR + CLEAR) || (column(q) && !column(p) && above < AIR + CLEAR)
|
|
}
|
|
/** How far a box is from a point. */
|
|
const reach = (b: Rect, x: number, y: number) => Math.hypot(Math.max(b.x0 - x, 0, x - b.x1), Math.max(b.y0 - y, 0, y - b.y1))
|
|
|
|
/**
|
|
* Seats every label in one order: each takes the nearest free seat with its
|
|
* figure, else the nearest with its name alone (a model's, never ours: the
|
|
* figure is in its tooltip); a seat is free when it is inside the plot, clear
|
|
* of every disc, its own included, and every label seated before it; and,
|
|
* beside its dot, nearer it than any other, or, on a leader, beside no other
|
|
* dot, so no label reads as another's.
|
|
* Ours and the frontier's look to the ends of the plot and are always drawn;
|
|
* any other model's with no free seat within REACH lines is left to its
|
|
* tooltip, rather than drawn far from its dot, so no label is ever drawn on
|
|
* another thing or out of reach of its own dot.
|
|
*/
|
|
function seat(order: Placed[], g: Geom, discs: Rect[], full: (p: Placed) => string): Map<Placed, Seat | null> {
|
|
const out = new Map<Placed, Seat | null>()
|
|
const seated: Rect[] = []
|
|
for (const p of order) {
|
|
const texts = p.ours ? [full(p)] : [full(p), short(p.name)]
|
|
const fits = (text: string, x0: number, y: number, k: number) => {
|
|
const b = labelBox(text, x0, y, g)
|
|
const own = reach(b, p.cx, p.cy)
|
|
return (
|
|
b.x0 >= g.pad.l - 4 &&
|
|
b.x1 <= g.w - 2 &&
|
|
b.y0 >= g.pad.t - 8 &&
|
|
b.y1 <= g.h - g.pad.b - 2 &&
|
|
![...discs, ...seated].some((o) => overlaps(b, o)) &&
|
|
// Beside its dot, no other is nearer or level with it in another's row or column; off it on a leader, no other dot is beside it.
|
|
others.every((q) => (k === 0 ? reach(b, q.cx, q.cy) > own && !level(b, q, p, g) : reach(b, q.cx, q.cy) > edgeOf(q, g) + AIR + CLEAR))
|
|
)
|
|
}
|
|
const others = order.filter((q) => q !== p)
|
|
const far = p.ours || p.front ? Math.ceil((g.h - g.pad.t - g.pad.b) / (g.type * (RISE + DROP))) : REACH
|
|
let got: Seat | null = null
|
|
for (let k = 0; k <= far && !got; k++)
|
|
for (const text of texts) {
|
|
const s = seats(p, text, g, k).find((c) => fits(text, c.x0, c.y, k))
|
|
if (s) {
|
|
got = { head: text, x0: s.x0, labelY: s.y, k }
|
|
break
|
|
}
|
|
}
|
|
if (got) seated.push(labelBox(got.head, got.x0, got.labelY, g))
|
|
out.set(p, got)
|
|
}
|
|
return out
|
|
}
|
|
|
|
/**
|
|
* Seats the labels in a few orders (ours first, then the frontier's: by
|
|
* height, the most crowded first, right to left, left to right; and ours and
|
|
* the frontier's together, left to right and top down) and keeps the layout
|
|
* that drops the fewest labels, then moves ours least, then the frontier's,
|
|
* then drops the fewest figures, then stands the rest nearest.
|
|
*/
|
|
function settle(pts: Placed[], g: Geom, full: (p: Placed) => string): void {
|
|
const discs = pts.map((p) => discBox(p, g))
|
|
const crowd = (p: Placed) => pts.filter((q) => q !== p && Math.hypot(q.cx - p.cx, q.cy - p.cy) < 90).length
|
|
const rank = (by: (a: Placed, b: Placed) => number) =>
|
|
[...pts].sort((a, b) => Number(b.ours) - Number(a.ours) || Number(b.front) - Number(a.front) || by(a, b))
|
|
const lead = (by: (a: Placed, b: Placed) => number) => [...pts].sort((a, b) => Number(b.ours || b.front) - Number(a.ours || a.front) || by(a, b))
|
|
const orders = [
|
|
rank((a, b) => a.cy - b.cy),
|
|
rank((a, b) => crowd(b) - crowd(a) || a.cy - b.cy),
|
|
rank((a, b) => b.cx - a.cx),
|
|
rank((a, b) => a.cx - b.cx),
|
|
lead((a, b) => a.cx - b.cx),
|
|
lead((a, b) => a.cy - b.cy),
|
|
]
|
|
const weigh = (p: Placed) => (p.ours ? 1000 : p.front ? 100 : 10)
|
|
const cost = (m: Map<Placed, Seat | null>) =>
|
|
[...m.entries()].reduce((c, [p, s]) => c + (s ? (s.head === full(p) ? 0 : 100) + s.k * weigh(p) : 100_000), 0)
|
|
const best = orders.map((o) => seat(o, g, discs, full)).sort((a, b) => cost(a) - cost(b))[0]
|
|
for (const p of pts) {
|
|
const s = best.get(p)
|
|
Object.assign(p, s ? { head: s.head, x0: s.x0, labelY: s.labelY, lead: s.k > 0 } : { head: '' })
|
|
}
|
|
}
|
|
|
|
/** The leader from a dot's edge to the nearest point of its label's box. */
|
|
function leader(p: Placed, g: Geom): string {
|
|
const b = labelBox(p.head, p.x0, p.labelY, g)
|
|
const nx = clamp(p.cx, b.x0, b.x1)
|
|
const ny = clamp(p.cy, b.y0 - 1, b.y1 + 1)
|
|
const len = Math.hypot(nx - p.cx, ny - p.cy) || 1
|
|
const e = edgeOf(p, g)
|
|
return `${p.cx + ((nx - p.cx) / len) * e},${p.cy + ((ny - p.cy) / len) * e} ${nx},${ny}`
|
|
}
|
|
|
|
const MONO = 'var(--font-mono)'
|
|
const money = (v: number) => `$${v}`
|
|
const DASH = '3 2.5'
|
|
|
|
/** What a point is, in words, for its tooltip: its name and figure, its set or its source, and what it cost. */
|
|
function said(p: Dot, n: number, t: Say) {
|
|
const at = { name: p.name, score: pct(p.accuracy), cost: usd(p.cost) }
|
|
if (p.ours) return t('{name} at {cost} per 1,000, its cost on new questions: {score} · questions it has seen', at)
|
|
if (p.kind === 'reported')
|
|
return t('{name} {score} on GPQA Diamond, reported by {by}; {cost} per 1,000 questions estimated from its list price. {source}', { ...at, by: String(p.by), source: p.record })
|
|
return t('{name} {score}, a single model tested on the same {n} GPQA Diamond questions, {cost} per 1,000 of them', { ...at, n })
|
|
}
|
|
|
|
function Plot({ f, g }: { f: Field; g: Geom }) {
|
|
const { t } = useI18n()
|
|
const a = axes(f.dots, g)
|
|
const full = (p: Dot) => `${p.ours ? p.name : short(p.name)} ${pct(p.accuracy)}`
|
|
const placed: Placed[] = f.dots.map((d) => ({ ...d, cx: xOf(d.cost, a, g), cy: yOf(d.accuracy * 100, a, g), head: '', x0: 0, labelY: 0, lead: false }))
|
|
const at = new Map(placed.map((p) => [p.system, p]))
|
|
settle(placed, g, full)
|
|
const front = f.front.map((s) => at.get(s)!.name).join(', ')
|
|
const reported = f.dots.some((d) => d.kind === 'reported')
|
|
const half = (p: Dot) => (p.ours ? g.disc.h : g.disc.mark) / 2
|
|
const tick = (v: number) => Math.min(xOf(v, a, g), g.w - 2 - (money(v).length * advance(g)) / 2)
|
|
// The cost axis's words; the arrows point along the axis, which runs left to right in every language.
|
|
const per = t('$ per 1,000 questions')
|
|
const caption = g.key === 'phone' ? `${per} →` : `${t('cheaper')} ← ${per} → ${t('dearer')}`
|
|
const alt = [
|
|
t(
|
|
"GPQA Diamond accuracy against what 1,000 questions cost, on a log scale; models under {least}% are not drawn. Plain discs are single models tested on the same {n} questions, each at its best run and the cost it filed. Ink discs wearing Hanzo's H are Enso's tiers on questions it has seen, each at its tier's cost on new questions.",
|
|
{ least: Math.round(LEAST * 100), n: f.n },
|
|
),
|
|
reported
|
|
? t(
|
|
"Dashed discs are scores reported by a lab or an independent evaluation, each at its model's list price times the median tokens a question took in our runs of its lab's models, or of all of them.",
|
|
)
|
|
: null,
|
|
t('The frontier, the points no other beats on both accuracy and cost, is {front}.', { front }),
|
|
]
|
|
return (
|
|
<svg
|
|
viewBox={`0 0 ${g.w} ${g.h}`}
|
|
width={g.w}
|
|
height={g.h}
|
|
direction="ltr"
|
|
role="img"
|
|
aria-label={alt.filter(Boolean).join(' ')}
|
|
data-plot={g.key}
|
|
>
|
|
{a.acc.ticks.map((v) => (
|
|
<g key={`a${v}`}>
|
|
<line x1={g.pad.l} x2={g.w - g.pad.r} y1={yOf(v, a, g)} y2={yOf(v, a, g)} stroke="var(--border)" strokeWidth={1} />
|
|
<text x={g.pad.l - 8} y={yOf(v, a, g) + 4} textAnchor="end" fill="var(--muted-foreground)" fontSize={g.type} fontFamily={MONO}>
|
|
{v}
|
|
</text>
|
|
</g>
|
|
))}
|
|
{a.price.ticks.map((v) => (
|
|
<text key={`p${v}`} x={tick(v)} y={g.h - g.pad.b + 20} textAnchor="middle" fill="var(--muted-foreground)" fontSize={g.type} fontFamily={MONO}>
|
|
{money(v)}
|
|
</text>
|
|
))}
|
|
<text x={g.pad.l - 8} y={g.pad.t - 14} textAnchor="start" fill="var(--muted-foreground)" fontSize={g.type} fontFamily={MONO}>
|
|
GPQA Diamond %
|
|
</text>
|
|
<text x={g.w - g.pad.r} y={g.h - 6} textAnchor="end" fill="var(--muted-foreground)" fontSize={g.type} fontFamily={MONO}>
|
|
{caption}
|
|
</text>
|
|
|
|
{[...placed]
|
|
.sort((x, y) => Number(x.ours) - Number(y.ours))
|
|
.map((p) => {
|
|
const dot = (
|
|
<g key={p.system} data-dot={p.system} data-kind={p.ours ? 'ours' : p.kind} data-front={p.front ? '' : undefined}>
|
|
<title>{said(p, f.n, t)}</title>
|
|
{p.head && p.lead ? <polyline fill="none" stroke="var(--muted-foreground)" strokeOpacity={0.6} strokeWidth={1} points={leader(p, g)} /> : null}
|
|
{p.ours ? <circle cx={p.cx} cy={p.cy} r={g.disc.halo} fill="var(--foreground)" opacity={0.12} /> : null}
|
|
<circle
|
|
cx={p.cx}
|
|
cy={p.cy}
|
|
r={p.ours ? g.disc.ours : g.disc.r}
|
|
fill={p.ours ? 'var(--foreground)' : 'var(--background)'}
|
|
stroke={p.ours ? 'var(--foreground)' : 'var(--muted-foreground)'}
|
|
strokeWidth={1}
|
|
strokeDasharray={p.kind === 'reported' ? DASH : undefined}
|
|
/>
|
|
{p.ours || p.mark ? (
|
|
<foreignObject x={p.cx - half(p)} y={p.cy - half(p)} width={half(p) * 2} height={half(p) * 2} color={p.ours ? 'var(--background)' : 'var(--foreground)'}>
|
|
<Maker id={p.mark} ours={p.ours} size={half(p) * 2} />
|
|
</foreignObject>
|
|
) : null}
|
|
{p.head ? (
|
|
<text
|
|
x={p.x0}
|
|
y={p.labelY}
|
|
fontSize={g.type}
|
|
fontFamily={MONO}
|
|
fill={p.ours ? 'var(--foreground)' : 'var(--muted-foreground)'}
|
|
fontWeight={p.ours ? 700 : 400}
|
|
>
|
|
{p.head}
|
|
</text>
|
|
) : null}
|
|
</g>
|
|
)
|
|
return p.kind === 'reported' ? (
|
|
<a key={p.system} href={p.record} target="_blank" rel="noopener noreferrer">
|
|
{dot}
|
|
</a>
|
|
) : (
|
|
dot
|
|
)
|
|
})}
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
/** The card's content width, read as it changes; 0 until it is measured. */
|
|
function useWidth() {
|
|
const ref = useRef<HTMLDivElement>(null)
|
|
const [w, setW] = useState(0)
|
|
useLayoutEffect(() => {
|
|
const el = ref.current
|
|
if (!el) return
|
|
const read = () => setW(Math.floor(el.getBoundingClientRect().width))
|
|
read()
|
|
const watch = new ResizeObserver(read)
|
|
watch.observe(el)
|
|
return () => watch.disconnect()
|
|
}, [])
|
|
return [ref, w] as const
|
|
}
|
|
|
|
/** The plot, laid out at its card's width once that is measured. */
|
|
function Sized({ f }: { f: Field }) {
|
|
const [ref, w] = useWidth()
|
|
return <div ref={ref}>{w > 0 ? <Plot f={f} g={geom(w)} /> : null}</div>
|
|
}
|
|
|
|
/**
|
|
* The chart in its card: once the runs and the price list are read; reading, or
|
|
* unreachable, until then. Not drawn while no Enso tier is on the frontier
|
|
* (lib/enso `field`).
|
|
*/
|
|
export function Chart() {
|
|
return (
|
|
<Ready reading={useEnso()} what={phrase("Enso's research runs")}>
|
|
{(e) =>
|
|
e.field ? (
|
|
<View width="100%" data-chart="gpqa">
|
|
<Card p="$3" $sm={{ p: '$5' }}>
|
|
<Sized f={e.field} />
|
|
</Card>
|
|
</View>
|
|
) : null
|
|
}
|
|
</Ready>
|
|
)
|
|
}
|