// Reusable scatter with a least-squares trend + 95% CI band, styled to match the LMCA
// trend chart (reuses .trend-* / .tc-* CSS). Two configured wrappers are exported:
//   window.DtbenchCapChart  — DTBench capability (y) vs. Text Arena (x)
//   window.DtbenchEdtChart  — EDT preference (y) vs. DTBench capability (x)
// Click a company in the legend to toggle its models.
const { useState: useDtState } = React;

const DTC_DESK = { W: 1000, H: 460, ml: 66, mr: 24, mt: 28, mb: 58, vbX: 0 };
// Phone frames, same reasoning as trend-chart.jsx: the SVG scales to its column,
// so the viewBox IS the type size — a 520-unit box renders everything about 1.9x
// larger than the 1000-unit desktop one at the same on-screen width, with no font
// rules touched. Two shapes because the charts want different things: the five
// scatters stay near square (a portrait frame would crush their x-range for
// nothing), while the one time series goes tall like the methodology chart, where
// height is what a date axis actually needs. ml is wider than the trend chart's
// because these y labels run to four characters ("100%", "0.90").
const DTC_PHONE_SQUARE = { W: 520, H: 500, ml: 40, mr: 34, mt: 24, mb: 58, vbX: -19 };
const DTC_PHONE_TALL   = { W: 520, H: 660, ml: 40, mr: 34, mt: 24, mb: 58, vbX: -19 };
const DTC_PHONE_MQ = "(max-width: 760px)";

const DT_ORG_COLORS = {
  "OpenAI":    "#23A27D",
  "Anthropic": "#cc785c",
  "GDM":       "#F4B400",
  "Google DeepMind": "#F4B400",
  "Meta":      "#3b5bb5",
  "Mistral":   "#c8961c",
  "DeepSeek":  "#899DFF",
  "Qwen":      "#9a5bb0",
  "Alibaba":   "#8b5cb8",
  "SpaceXAI":  "#1a1a1a",
  "Z.ai":     "#d16aa0",
  "Xiaomi":    "#f26419",
  "Moonshot AI": "#5b6570",
  "MiniMax":   "#f03b5d",
  "Cohere":    "#bd6f92",
  "Reka":      "#7fae5a",
  "NVIDIA":    "#76b900",
  "Thinking Machines": "#9aa4ad",
  "Nous Research": "#9c7a54",
  "01.AI":     "#2aa198",
  "HuggingFace": "#eab308",
  "Other":     "#9a938a"
};

// date helpers for the time-axis chart (mirrors trend-chart.jsx):
// "YYYY-MM-DD" -> fractional month index; index -> "Mon 'YY".
function dtParseDate(s) {
  const [y, m, d] = String(s).split("-").map(Number);
  return y * 12 + (m - 1) + ((d || 1) - 1) / 31;
}
const DT_MONTH_ABBR = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
function dtFmtMonth(idx) {
  const i = Math.round(idx);
  const y = Math.floor(i / 12);
  const m = ((i % 12) + 12) % 12;
  return `${DT_MONTH_ABBR[m]} '${String(y).slice(2)}`;
}

// least-squares fit + 95% CI-of-mean + Pearson r; null if too few points
function dtFitTrend(pts) {
  const n = pts.length;
  if (n < 3) return null;
  const xbar = pts.reduce((a, p) => a + p.x, 0) / n;
  const ybar = pts.reduce((a, p) => a + p.y, 0) / n;
  let Sxx = 0, Sxy = 0, Syy = 0;
  for (const p of pts) {
    Sxx += (p.x - xbar) ** 2;
    Sxy += (p.x - xbar) * (p.y - ybar);
    Syy += (p.y - ybar) ** 2;
  }
  if (Sxx === 0) return null;
  const slope = Sxy / Sxx;
  const intercept = ybar - slope * xbar;
  const yhat = (x) => intercept + slope * x;
  let sse = 0;
  for (const p of pts) sse += (p.y - yhat(p.x)) ** 2;
  const s = Math.sqrt(sse / (n - 2));
  const se = (x) => s * Math.sqrt(1 / n + ((x - xbar) ** 2) / Sxx);
  const r = Syy > 0 ? Sxy / Math.sqrt(Sxx * Syy) : 0;
  return { yhat, se, t: 1.96, r };
}

function ScatterChart({
  id, title, data, orgOrder,
  xMin, xMax, yMin, yMax, xTicks, yTicks,
  xLabel, yLabel, xFmt, yFmt, xTipFmt, yTipFmt, caption, refLine, showR, rPos, frontierLabs, defaultLabs, sotaToggle,
  // "tall" for a time axis, otherwise the near-square phone frame.
  phoneFrame,
  // Hard maximum the metric can take (1 for a proportion, 100 for a 0-100 score).
  // yMax is only the axis top and deliberately sits above it to leave headroom,
  // so the fitted line and its CI band are clamped to yCeiling instead. Omit it
  // and no clamping happens.
  yCeiling,
}) {
  const [isPhone, setIsPhone] = useDtState(
    () => typeof window !== "undefined" && window.matchMedia
      ? window.matchMedia(DTC_PHONE_MQ).matches : false);
  React.useEffect(() => {
    if (!window.matchMedia) return;
    const mq = window.matchMedia(DTC_PHONE_MQ);
    const on = () => setIsPhone(mq.matches);
    on();
    mq.addEventListener ? mq.addEventListener("change", on) : mq.addListener(on);
    return () => (mq.removeEventListener ? mq.removeEventListener("change", on) : mq.removeListener(on));
  }, []);
  const DOT_R = isPhone ? 6 : 4;
  const AXT_X = isPhone ? 0 : 16;
  // Halve dense tick rows on the narrower frame; 5 or fewer already fit.
  const thin = (a) => (isPhone && a && a.length > 5 ? a.filter((_, i) => i % 2 === 0) : a);
  const xT = thin(xTicks), yT = thin(yTicks);

  // On phone the tick numbers and the rotated y-axis title share one narrow
  // strip, so a fixed ml set the gap between them by accident: whatever the
  // formatter happened to emit. The numbers are right-anchored, so a four-
  // character label ("0.40", "100%") grows one character further left than a
  // three-character one ("0.4") and lands on the title. Anchor the block by its
  // LEFT edge instead and let ml follow, so the clearance holds for any
  // formatter. The tick font is mono, so the width is just a character count.
  //
  // 6.4 is the clearance the three-character charts already had — the ones that
  // read as correctly separated. Three-character charts therefore come out
  // exactly where they are today (6.4 + 3 chars + 12 == the old ml of 40) and
  // only the four-character ones move.
  const CH_ADV = 7.2;        // JetBrains Mono advance at the 12px tick size
  const LABEL_LEFT = 6.4;    // left ink edge of the numbers, past the title baseline at AXT_X 0
  const LABEL_GAP = 12;      // numbers to axis line, unchanged
  const yLabelChars = (yT || []).reduce((m, v) => Math.max(m, String(yFmt ? yFmt(v) : v).length), 0);
  const yLabelX = LABEL_LEFT + yLabelChars * CH_ADV;
  const DTC_BASE = isPhone ? (phoneFrame === "tall" ? DTC_PHONE_TALL : DTC_PHONE_SQUARE) : DTC_DESK;
  const DTC = isPhone ? { ...DTC_BASE, ml: yLabelX + LABEL_GAP } : DTC_BASE;
  const [hover, setHover] = useDtState(null);
  // Optional SoTA/All model filter (mirrors the methodology trend chart). When
  // enabled the chart starts SoTA-only; "All" drops the filter. Points need a
  // `sota` flag on the data for this to do anything.
  const [showAll, setShowAll] = useDtState(false);
  // Two visibility modes share the rest of the chart:
  //   default   — `hidden` map + legend below the chart (click an org to hide it)
  //   labs mode — `frontierLabs` prop set: a single visibleLabs Set drives the
  //               chart (default = the frontier labs); a "Labs" chip row
  //               (Frontier / All presets) plus the legend render ABOVE the
  //               chart, mirroring the methodology trend chart.
  const labsMode = !!(frontierLabs && frontierLabs.length);
  const [hidden, setHidden] = useDtState({});
  const ttRef = React.useRef(null);
  const [ttFit, setTtFit] = useDtState({ flip: false, nudge: 0 });
  // labs-mode initial selection: the frontier preset, or every org in the
  // data when defaultLabs="all"
  const [visibleLabs, setVisibleLabs] = useDtState(() => new Set(
    defaultLabs === "all" ? (data || []).map((m) => m.org) : (frontierLabs || [])));
  const isHidden = (org) => (labsMode ? !visibleLabs.has(org) : !!hidden[org]);

  const allPts = (data || []).map((m) => ({ x: m.x, y: m.y, name: m.name, org: m.org, sota: m.sota }));
  // SoTA filter (if enabled) applies before the per-lab visibility filter.
  const modelPts = sotaToggle && !showAll ? allPts.filter((p) => p.sota) : allPts;
  const pts = modelPts.filter((p) => !isHidden(p.org));

  const px = (x) => DTC.ml + ((x - xMin) / (xMax - xMin)) * (DTC.W - DTC.ml - DTC.mr);
  const py = (y) => DTC.mt + (1 - (y - yMin) / (yMax - yMin)) * (DTC.H - DTC.mt - DTC.mb);

  const trend = dtFitTrend(pts);
  let linePath = "", bandPath = "";
  if (trend) {
    const samples = [];
    const steps = 60;
    // An extrapolated fit — and especially the upper edge of its CI band — can
    // run above a value the metric cannot definitionally reach, so cap it.
    const capY = (v) => (yCeiling == null ? v : Math.min(v, yCeiling));
    for (let i = 0; i <= steps; i++) {
      const x = xMin + (i / steps) * (xMax - xMin);
      const yc = trend.yhat(x);
      const half = trend.t * trend.se(x);
      samples.push({ x, yc: capY(yc), hi: capY(yc + half), lo: capY(yc - half) });
    }
    linePath = samples.map((s, i) => `${i ? "L" : "M"}${px(s.x).toFixed(1)},${py(s.yc).toFixed(1)}`).join(" ");
    bandPath =
      samples.map((s, i) => `${i ? "L" : "M"}${px(s.x).toFixed(1)},${py(s.hi).toFixed(1)}`).join(" ") + " " +
      samples.slice().reverse().map((s) => `L${px(s.x).toFixed(1)},${py(s.lo).toFixed(1)}`).join(" ") + " Z";
  }

  const colorFor = (org) => DT_ORG_COLORS[org] || "#9a938a";
  // Legend order: major labs first, then the rest by the chart's orgOrder;
  // the catch-all "Other" always sorts last.
  const MAJOR_LABS = ["OpenAI", "Anthropic", "Google DeepMind", "GDM"];
  const orgRank = (o) => {
    if (o === "Other") return 10000;
    const m = MAJOR_LABS.indexOf(o);
    if (m >= 0) return m;
    const i = orgOrder.indexOf(o);
    return 100 + (i < 0 ? 900 : i);
  };
  const orgsPresent = Array.from(new Set(modelPts.map((p) => p.org)))
    .sort((a, b) => orgRank(a) - orgRank(b));
  const toggleOrg = (org) => {
    if (labsMode) {
      setVisibleLabs((v) => {
        const next = new Set(v);
        if (next.has(org)) next.delete(org); else next.add(org);
        return next;
      });
    } else {
      setHidden((h) => ({ ...h, [org]: !h[org] }));
    }
  };
  // Labs presets (labs mode only): Frontier = frontierLabs ∩ available orgs;
  // All = every org. A chip is active only when the visible set exactly
  // matches its preset — a custom mix highlights neither.
  const frontierAvail = labsMode ? orgsPresent.filter((o) => frontierLabs.includes(o)) : [];
  const visibleAvail = labsMode ? orgsPresent.filter((o) => visibleLabs.has(o)) : [];
  const isFrontierView = labsMode && visibleAvail.length === frontierAvail.length
    && frontierAvail.every((o) => visibleLabs.has(o));
  const cy = DTC.mt + (DTC.H - DTC.mt - DTC.mb) / 2;
  const hoverP = hover != null && !isHidden(hover.org) ? hover : null;
  // Same measured tooltip placement as trend-chart.jsx: flip to the left of the
  // point only when the rendered box will not fit on the right, rather than at a
  // guessed fraction of the chart width.
  const hoverLeftPct = hoverP ? (px(hoverP.x) / DTC.W) * 100 : 0;
  React.useLayoutEffect(() => {
    const el = ttRef.current, host = el && el.offsetParent;
    if (!el || !host) return;
    const w = el.offsetWidth, hw = host.clientWidth;
    const cx = (hoverLeftPct / 100) * hw;
    // Prefer the right of the point; flip left if it will not fit. If it fits on
    // neither side — a tooltip wider than the plot — flipping alone would push it
    // off the opposite edge, so clamp it back inside as well.
    const flip = cx + 16 + w > hw - 2;
    const natural = flip ? cx - 16 - w : cx + 16;
    let nudge = 0;
    if (natural < 2) nudge = 2 - natural;
    else if (natural + w > hw - 2) nudge = (hw - 2 - w) - natural;
    setTtFit((prev) => (prev.flip === flip && Math.abs(prev.nudge - nudge) < 0.5
      ? prev : { flip, nudge }));
  }, [hoverP, hoverLeftPct]);
  const capCaption = typeof caption === "function" ? caption(trend) : caption;

  // r-label position: on charts with a ceiling reference line the label sits
  // directly under that line (left-aligned); charts without one share the
  // default top-of-plot spot (top-left, or top-right with rPos="tr").
  const refs = Array.isArray(refLine) ? refLine : refLine ? [refLine] : [];
  const ceilingRef = refs.find((rl) => /ceiling/i.test(rl.label || ""));
  const rLabelX = !ceilingRef && rPos === "tr" ? DTC.W - DTC.mr - 14 : DTC.ml + 14;
  // r sits 34px below the ceiling line — and on charts without one, 34px
  // below the top of the y-scale, so the label height matches everywhere
  // and the r can't be read as a ceiling value
  // Anchor to the topmost gridline rather than to yMax: where the axis top sits
  // above the last tick (EDT-over-time has yMax 0.95 vs a 0.9 top tick, exactly
  // the 34px offset) the label would otherwise land right on that gridline.
  const rTopTick = Array.isArray(yTicks) && yTicks.length ? Math.max(...yTicks) : yMax;
  const rLabelY = (ceilingRef ? py(ceilingRef.y) : py(rTopTick)) + 34;
  const rLabelAnchor = !ceilingRef && rPos === "tr" ? "end" : "start";

  const legend = (
    <div className="trend-legend">
      {orgsPresent.map((org) => (
        <button
          key={org}
          type="button"
          className={"tc-leg" + (isHidden(org) ? " is-off" : "")}
          onClick={() => toggleOrg(org)}
          title={isHidden(org) ? "Show " + org : "Hide " + org}>
          <span className="tc-leg-dot" style={{ "--leg-c": colorFor(org) }} />
          {org}
        </button>
      ))}
    </div>
  );

  return (
    <section className="trend-wrap" id={id}>
      <div className="trend-head">
        <div>
          <div className="eyebrow">Trend</div>
          <div className="trend-title">{title}</div>
        </div>
      </div>

      {(labsMode || sotaToggle) && (
        <div style={{ display: "flex", gap: "16px 20px", flexWrap: "wrap", margin: "0 auto 20px" }}>
          {sotaToggle && (
            <div className="filter-group">
              <div className="filter-label">Models</div>
              <div className="filter-opts">
                {[["SoTA", false], ["All", true]].map(([lbl, v]) => (
                  <button key={"m" + lbl} type="button" onClick={() => setShowAll(v)}
                    className={"filter-chip" + (showAll === v ? " is-active" : "")}>{lbl}</button>
                ))}
              </div>
            </div>
          )}
          {labsMode && (
            <div className="filter-group">
              <div className="filter-label">Labs</div>
              <div className="filter-opts">
                <button type="button" onClick={() => setVisibleLabs(new Set(frontierAvail))}
                  className={"filter-chip" + (isFrontierView ? " is-active" : "")}>Frontier</button>
                <button type="button" onClick={() => setVisibleLabs(new Set(orgsPresent))}
                  className={"filter-chip" + (visibleAvail.length === orgsPresent.length ? " is-active" : "")}>All</button>
              </div>
            </div>
          )}
        </div>
      )}

      {/* Labs mode: the chips above are presets over the same visible-labs set;
          clicking a legend entry toggles that one lab (filled = visible, hollow
          = hidden). A custom mix simply leaves neither chip highlighted. */}
      {labsMode && <div className="trend-labs">{legend}</div>}

      <div className="trend-plot">
        <svg viewBox={`${DTC.vbX} 0 ${DTC.W - DTC.vbX} ${DTC.H}`} className="trend-svg" preserveAspectRatio="xMidYMid meet">
          <defs>
            <clipPath id={`plot-clip-${id}`}>
              <rect x={DTC.ml} y={DTC.mt} width={DTC.W - DTC.ml - DTC.mr} height={DTC.H - DTC.mt - DTC.mb} />
            </clipPath>
          </defs>
          {yT.map((v) => (
            <g key={`y${v}`}>
              <line x1={DTC.ml} x2={DTC.W - DTC.mr} y1={py(v)} y2={py(v)} className="tc-grid" />
              <text x={isPhone ? yLabelX : DTC.ml - 12} y={py(v)} className="tc-ylabel" dominantBaseline="middle" textAnchor="end">{yFmt(v)}</text>
            </g>
          ))}
          {xT.map((v) => (
            <g key={`x${v}`}>
              <line x1={px(v)} x2={px(v)} y1={DTC.H - DTC.mb} y2={DTC.H - DTC.mb + 6} className="tc-tick" />
              <text x={px(v)} y={DTC.H - DTC.mb + 22} className="tc-xlabel" textAnchor="middle">{xFmt(v)}</text>
            </g>
          ))}
          <line x1={DTC.ml} x2={DTC.W - DTC.mr} y1={DTC.H - DTC.mb} y2={DTC.H - DTC.mb} className="tc-axis" />

          <text className="tc-axis-title" transform={`rotate(-90 ${AXT_X} ${cy})`} x={AXT_X} y={cy}
                textAnchor="middle" style={isPhone ? { fontSize: "13px" } : undefined}>{yLabel}</text>
          <text className="tc-xlabel" x={DTC.ml + (DTC.W - DTC.ml - DTC.mr) / 2} y={DTC.H - 6} textAnchor="middle">{xLabel}</text>

          {trend && (
            <g clipPath={`url(#plot-clip-${id})`}>
              <path d={bandPath} fill="var(--teal)" fillOpacity="0.10" stroke="none" />
              <path d={linePath} fill="none" stroke="var(--teal)" strokeWidth="2" />
            </g>
          )}

          {(Array.isArray(refLine) ? refLine : refLine ? [refLine] : []).map((rl, i) => (
            <g key={`rl${i}`}>
              <line
                x1={DTC.ml} x2={DTC.W - DTC.mr}
                y1={py(rl.y)} y2={py(rl.y)}
                stroke="var(--ink-2, #3a3730)" strokeWidth="1.25"
                strokeDasharray={rl.dash || "5 4"} strokeLinecap={rl.dash ? "round" : "butt"}
                strokeOpacity="0.55" />
              <text
                x={DTC.W - DTC.mr} y={py(rl.y) + (rl.labelBelow ? 15 : -7)}
                className="tc-xlabel" textAnchor="end"
                style={{ fontStyle: "italic", opacity: 0.7 }}>
                {rl.label}
              </text>
            </g>
          ))}

          {pts.map((p) => (
            <circle
              key={p.name}
              cx={px(p.x)}
              cy={py(p.y)}
              r={hoverP === p ? DOT_R + 2 : DOT_R}
              fill={colorFor(p.org)}
              fillOpacity="0.9"
              className="tc-pt"
              onMouseEnter={() => setHover(p)}
              onMouseLeave={() => setHover(null)}
            />
          ))}

          {/* Pearson correlation, recomputed from the plotted points each render */}
          {showR && trend && (
            <text
              x={rLabelX}
              y={rLabelY}
              className="tc-xlabel" textAnchor={rLabelAnchor}
              style={{ fontStyle: "italic", opacity: 0.7 }}>
              r = {trend.r.toFixed(2)}
            </text>
          )}
        </svg>

        {hoverP && (() => {
          const p = hoverP;
          const leftPct = hoverLeftPct;
          const topPct = (py(p.y) / DTC.H) * 100;
          return (
            <div ref={ttRef}
                 className={"tc-tooltip" + (ttFit.flip ? " tc-tooltip--flip" : "")}
                 style={{ left: `${leftPct}%`, top: `${topPct}%`, "--tt-nudge": `${ttFit.nudge}px` }}>
              <div className="tc-tt-name">{p.name}</div>
              <div className="tc-tt-meta mono-small">{p.org} · {xLabel.replace(/\s*\(.*\)$/, "")} {xTipFmt(p.x)}</div>
              <div className="tc-tt-score" style={{ color: colorFor(p.org) }}>{yTipFmt(p.y)}</div>
            </div>
          );
        })()}
      </div>

      {/* Key before caption: the caption is the last element of a figure and
          describes the whole thing, key included, so a key after it reads as
          orphaned — and the caption talks about colours the reader has not been
          given yet. In labs mode the legend instead sits above the plot with the
          Labs chips, where it is acting as a control rather than a key. */}
      {!labsMode && legend}

      {capCaption ? <p className="trend-figcaption mono-small">{capCaption}</p> : null}
    </section>
  );
}

function DtbenchCapChart() {
  const data = (window.DTBENCH_CAP || []).map((m) => ({ name: m.name, x: m.elo, y: m.cap, org: m.company }));
  return (
    <ScatterChart
      id="dt-cap"
      title="Capability vs. Text Arena"
      data={data}
      orgOrder={window.DTBENCH_CAP_ORGS || []}
      xMin={1030} xMax={1520} yMin={0.35} yMax={1.06} yCeiling={1}
      xTicks={[1100, 1200, 1300, 1400, 1500]} yTicks={[0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]}
      xLabel="Text Arena" yLabel="DTBench capability score"
      xFmt={(v) => v} yFmt={(v) => (v * 100).toFixed(0) + "%"}
      xTipFmt={(v) => Math.round(v)} yTipFmt={(v) => (v * 100).toFixed(0) + "%"}
      refLine={[{ y: 1.0, label: "Estimated ceiling" }, { y: 0.399, label: "Random guessing" }]}
      showR
      caption={() => (
        <>The y-axis shows the percent of DTBench capability questions each model answered correctly. The x-axis is the model's Text Arena Score. <a className="text-link" href="https://arena.ai/leaderboard/text" target="_blank" rel="noopener">Text Arena</a> ranks AI models based on blind human preference votes. The shaded band is the trend line's 95% Confidence Interval.</>)}
    />
  );
}

function DtbenchEdtChart() {
  const data = (window.DTBENCH_EDT || []).map((m) => ({ name: m.name, x: m.cap, y: m.edt, org: m.company }));
  return (
    <ScatterChart
      id="dt-edt"
      title="EDT preference vs. capability"
      data={data}
      orgOrder={window.DTBENCH_EDT_ORGS || []}
      xMin={0.4} xMax={1.0} yMin={0.4} yMax={0.9} yCeiling={1}
      xTicks={[0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]} yTicks={[0.4, 0.5, 0.6, 0.7, 0.8, 0.9]}
      xLabel="DTBench capability score" yLabel="% of answers that match EDT recommendation"
      xFmt={(v) => (v * 100).toFixed(0) + "%"} yFmt={(v) => (v * 100).toFixed(0) + "%"}
      xTipFmt={(v) => (v * 100).toFixed(0) + "%"} yTipFmt={(v) => (v * 100).toFixed(0) + "% EDT"}
      showR
      frontierLabs={["OpenAI", "Anthropic", "Google DeepMind"]}
      defaultLabs="all"
      caption={() =>
        `The y-axis shows the percent of the time the answer recommended by evidential decision theory (EDT) is preferred to the answer recommended by causal decision theory (CDT). More capable models tend to prefer the EDT-recommended answer. The shaded band is the trend line's 95% Confidence Interval.`}
    />
  );
}

function DtbenchAttitudeTimeChart() {
  const rows = (window.DTBENCH_ATTITUDE_TIME || [])
    .filter((m) => m.released && Number.isFinite(dtParseDate(m.released)));
  const data = rows.map((m) => ({ name: m.name, x: dtParseDate(m.released), y: m.edt, org: m.company, sota: m.sota }));
  // x domain: earliest release on the left, today on the right (so new releases
  // fill in the gap rather than rescaling the axis); quarterly ticks.
  const xs = data.map((d) => d.x);
  const now = new Date();
  const todayX = now.getFullYear() * 12 + now.getMonth() + (now.getDate() - 1) / 31;
  const minX = xs.length ? Math.min(...xs) : todayX;
  const maxX = Math.max(todayX, ...(xs.length ? xs : [todayX]));
  const x0 = minX - 1, x1 = maxX + 1;
  const xTicks = [];
  for (let m = Math.ceil(x0 / 3) * 3; m <= x1; m += 3) xTicks.push(m);
  return (
    <ScatterChart
      id="dt-attitude-time"
      title="EDT preference over time"
      data={data}
      orgOrder={window.DTBENCH_ATTITUDE_TIME_ORGS || []}
      xMin={x0} xMax={x1} yMin={0.4} yMax={0.95} yCeiling={1} phoneFrame="tall"
      xTicks={xTicks} yTicks={[0.4, 0.5, 0.6, 0.7, 0.8, 0.9]}
      xLabel="Release date" yLabel="% of answers that match EDT recommendation"
      xFmt={dtFmtMonth} yFmt={(v) => (v * 100).toFixed(0) + "%"}
      xTipFmt={dtFmtMonth} yTipFmt={(v) => (v * 100).toFixed(0) + "% EDT"}
      showR
      sotaToggle
      frontierLabs={["OpenAI", "Anthropic", "Google DeepMind"]}
      defaultLabs="all"
      caption={() =>
        `The y-axis shows the percent of the time the answer recommended by evidential decision theory (EDT) is preferred to the answer recommended by causal decision theory (CDT), plotted against its release date. Preference for the EDT-recommended answer has risen over time as models have become more capable. The shaded band is the trend line's 95% Confidence Interval.`}
    />
  );
}

window.ScatterChart = ScatterChart;
window.DT_ORG_COLORS_PUBLIC = DT_ORG_COLORS;
window.DtbenchCapChart = DtbenchCapChart;
window.DtbenchEdtChart = DtbenchEdtChart;
window.DtbenchAttitudeTimeChart = DtbenchAttitudeTimeChart;
