$sloprun.dev

Join-Order Explainer

demo sandbox: allow-scripts · csp: default-src 'none'

sandboxed and isolated in your browser · never enter a real password in a demo

▶ 5 ran · ✓ 0 worked
i ran it — no login needed:
share: preview embed ↗
post it anywhere:
email the card:

0 reported it broke · the ▶ count is distinct visitors who ran it, counted once each, anonymously

Four tables, one query, twenty-four possible left-deep join orders — and every one of them returns the identical answer. This demo enumerates all of them against a toy star schema (10M orders, 1M users, 10k products, 50 regions), costs each one as rows scanned, ranks them, and draws the winner as a join tree with a one-sentence explanation of why it wins. On the default numbers the best plan touches 22.0M rows and the worst touches 20.0B: a 909× spread for the same result set.

Start by clicking a bad plan near the bottom of the list — two thirds of them lose to a cross product between tables that share no key. Then hit fat dimension to grow regions to 2M rows and watch the "start with the smallest table" heuristic break in real time, or push the orders.product_id selectivity up to model a duplicated key and watch the final join swell to a billion rows while the planner still keeps it for last.

Embed this demo — put a runnable demo in your blog post

Paste this where you write. It renders a live, runnable demo inline anywhere raw HTML / iframes are allowed — Ghost, WordPress, Notion, Discourse, your own site.

<iframe src="https://sloprun.dev/embed/join-order" width="100%" height="640" loading="lazy" allow="" style="border:1px solid #E3E2DC;border-radius:10px" title="Join-Order Explainer — a runnable demo on sloprun.dev"></iframe>
preview ↗

On Medium and dev.to the plain link becomes a rich preview card that links back here — they don't run third-party iframes, so paste the URL there and the reader clicks through to run it. https://sloprun.dev/p/join-order

Source

the code you see is the code that just ran raw ↗
Source — the code you see is the code that just ran 992 lines 44.3 KB index.html
demos/join-order/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<title>Join-Order Explainer</title>
<style>
/* sloprun design tokens — inline this block into every demo (self-contained rule).
   Identity: instrument-panel. Machine facts in mono; human words in sans.
   Green is EARNED: only for "it ran / it worked" states, never decoration. */
:root {
  --bg: #F6F6F3;          /* neutral paper, not cream */
  --surface: #FFFFFF;
  --ink: #1A1D21;
  --muted: #5A6068;
  --line: #E3E2DC;
  --accent: #C05600;      /* phosphor amber, darkened for light bg */
  --accent-soft: #FFF3E6;
  --run: #1A7F37;         /* earned green */
  --run-soft: #E7F4EA;
  --danger: #C93C37;
  --radius: 10px;
  --font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
    --line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
    --run: #3FB950; --run-soft: #16281B; --danger: #F47067;
  }
}
:root[data-theme="dark"] {
  --bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
  --line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
  --run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
body { background: var(--bg); color: var(--ink); font-family: var(--font-sans); }

/* ---------- base ---------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body { margin: 0; line-height: 1.5; overflow-x: hidden; }
h1, h2, h3, p, ul, figure { margin: 0; }
button, input { font: inherit; color: inherit; }

.wrap { max-width: 1140px; margin: 0 auto; padding: 22px 16px 40px; }

:where(a, button, input, [tabindex]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ---------- header ---------- */
header { margin-bottom: 18px; }
.eyebrow {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .09em;
  text-transform: uppercase; color: var(--muted); margin-bottom: 6px;
}
h1 { font-size: clamp(1.4rem, 1.1rem + 1.4vw, 2rem); letter-spacing: -.02em; line-height: 1.15; }
.lede { color: var(--muted); max-width: 68ch; margin-top: 7px; font-size: .95rem; }
.lede b { color: var(--ink); font-weight: 600; }

/* ---------- layout ---------- */
.grid { display: grid; gap: 14px; grid-template-columns: 1fr; align-items: start; }
@media (min-width: 900px) {
  .grid { grid-template-columns: minmax(320px, 400px) 1fr; }
}
.col { display: grid; gap: 14px; align-content: start; min-width: 0; }

.card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 14px; min-width: 0;
}
.card > h2 {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .09em;
  text-transform: uppercase; color: var(--muted); font-weight: 600;
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  margin-bottom: 10px;
}
.card > h2 .h2note { text-transform: none; letter-spacing: 0; font-weight: 400; }

/* ---------- query ---------- */
pre.sql {
  margin: 0; font-family: var(--font-mono); font-size: clamp(10px, 2.75vw, 12.5px); line-height: 1.65;
  overflow-x: auto; padding: 10px 12px; background: var(--bg);
  border: 1px solid var(--line); border-radius: 8px; tab-size: 2;
}
.kw { color: var(--muted); }
.tbl { color: var(--accent); font-weight: 600; }
.note { color: var(--muted); font-size: .85rem; margin-top: 9px; }
.note code { font-family: var(--font-mono); color: var(--ink); }

/* ---------- controls ---------- */
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; }
.chip {
  font-family: var(--font-mono); font-size: 11.5px; padding: 8px 10px;
  background: var(--surface); border: 1px solid var(--line); border-radius: 999px;
  cursor: pointer; white-space: nowrap;
}
.chip:hover { border-color: var(--accent); }
.chip[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--ink); }

.grp { display: grid; gap: 9px; }
.grp + .subhead { margin-top: 16px; }
.subhead {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .09em;
  text-transform: uppercase; color: var(--muted); margin-bottom: 9px;
  display: flex; justify-content: space-between; align-items: baseline; gap: 8px;
}
.subhead .hint { text-transform: none; letter-spacing: 0; font-size: 10.5px; }

.ctl { display: grid; grid-template-columns: 72px 1fr 74px; gap: 8px; align-items: center; }
.ctl > label { font-family: var(--font-mono); font-size: 12.5px; }
.ctl .num {
  font-family: var(--font-mono); font-size: 12px; text-align: right;
  width: 100%; padding: 4px 6px; background: var(--bg);
  border: 1px solid var(--line); border-radius: 6px;
}
.ctl .num:focus { border-color: var(--accent); }
.ctl .num.bad { border-color: var(--danger); }

.edge { display: grid; gap: 4px; }
.edge .pred { font-family: var(--font-mono); font-size: 11.5px; color: var(--ink); }
.edge .pred .op { color: var(--muted); }
.edge .erow { display: grid; grid-template-columns: 1fr auto; gap: 8px; align-items: center; }
.edge .readout {
  font-family: var(--font-mono); font-size: 11px; color: var(--muted);
  white-space: nowrap; text-align: right;
}
.edge .readout b { color: var(--ink); font-weight: 600; }

input[type="range"] {
  width: 100%; min-width: 0; accent-color: var(--accent); height: 24px; margin: 0; cursor: ew-resize;
}

/* ---------- plan card ---------- */
.planhead { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 10px; }
.planorder { font-family: var(--font-mono); font-size: 13px; font-weight: 600; }
.planorder .j { color: var(--muted); font-weight: 400; }
.badge {
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .05em;
  padding: 2px 6px; border-radius: 999px; border: 1px solid; white-space: nowrap;
}
.badge.best { color: var(--run); border-color: var(--run); background: var(--run-soft); }
.badge.rank { color: var(--muted); border-color: var(--line); background: var(--bg); }
.badge.cross { color: var(--danger); border-color: var(--danger); background: transparent; }

.jumpbest {
  margin-left: auto; font-family: var(--font-mono); font-size: 11px;
  padding: 3px 8px; background: var(--bg); border: 1px solid var(--line);
  border-radius: 999px; cursor: pointer;
}
.jumpbest:hover { border-color: var(--accent); color: var(--accent); }
.jumpbest[hidden] { display: none; }

.treewrap { background: var(--bg); border: 1px solid var(--line); border-radius: 8px; padding: 6px 4px 2px; overflow-x: auto; }
svg.tree { display: block; width: 100%; height: auto; }
svg.tree text { font-family: var(--font-mono); }
/* SVG paint via classes, not var() in presentation attributes (not honored everywhere) */
svg.tree .lnk { fill: none; stroke: var(--line); stroke-width: 1.5; }
svg.tree .lnk.x { stroke: var(--danger); stroke-dasharray: 4 3; }
svg.tree .box { fill: var(--surface); stroke: var(--line); stroke-width: 1; }
svg.tree .box.peak { fill: var(--accent-soft); stroke: var(--accent); stroke-width: 1.6; }
svg.tree .box.x { stroke: var(--danger); stroke-width: 1.6; }
svg.tree .t1 { fill: var(--ink); font-weight: 600; }
svg.tree .t2 { fill: var(--muted); }
svg.tree .t1.x, svg.tree .t2.x { fill: var(--danger); }
.tcap { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted); text-align: center; padding: 2px 0 6px; }

.stepwrap { overflow-x: auto; }
table.steps { width: 100%; min-width: 260px; border-collapse: collapse; margin-top: 12px; font-family: var(--font-mono); font-size: 11.5px; }
table.steps th {
  text-align: left; font-weight: 600; color: var(--muted); font-size: 10px;
  letter-spacing: .07em; text-transform: uppercase; padding: 0 6px 5px 0; border-bottom: 1px solid var(--line);
}
table.steps td { padding: 5px 6px 5px 0; border-bottom: 1px solid var(--line); vertical-align: top; }
table.steps tr:last-child td { border-bottom: 0; }
table.steps td.n { color: var(--muted); width: 1.4em; }
table.steps td.out { text-align: right; white-space: nowrap; }
table.steps tr.xp td { color: var(--danger); }

.why { margin-top: 12px; padding: 11px 12px; border-radius: 8px; background: var(--accent-soft); border: 1px solid var(--line); }
.why p { font-size: .93rem; }
.why p + p { margin-top: 6px; color: var(--muted); font-size: .87rem; }
.why b { font-weight: 600; }
.why .m { font-family: var(--font-mono); font-weight: 600; }

/* ---------- stats strip ---------- */
.stats { display: flex; flex-wrap: wrap; gap: 6px 18px; margin-bottom: 10px; }
.stat { display: grid; gap: 1px; }
.stat .k { font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
.stat .v { font-family: var(--font-mono); font-size: 15px; font-weight: 600; letter-spacing: -.01em; }
.stat .v.spread { color: var(--accent); }

/* ---------- ranked list ---------- */
.legend { display: flex; flex-wrap: wrap; gap: 4px 14px; font-family: var(--font-mono); font-size: 10.5px; color: var(--muted); margin-bottom: 8px; }
.legend i { display: inline-block; width: 16px; height: 5px; border-radius: 3px; margin-right: 5px; vertical-align: middle; }
.lg-best { background: var(--run); }
.lg-ok { background: var(--accent); }
.lg-x { background: var(--danger); }

.list { display: flex; flex-direction: column; max-height: min(58vh, 460px); overflow-y: auto; overscroll-behavior: contain; margin: 0 -4px; padding: 0 4px; }
.plan-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: 5px 10px; width: 100%;
  text-align: left; padding: 7px 8px; background: none; cursor: pointer;
  border: 1px solid transparent; border-left: 3px solid transparent; border-radius: 8px;
}
.plan-row:hover { background: var(--bg); }
.plan-row[aria-checked="true"] { background: var(--accent-soft); border-color: var(--line); border-left-color: var(--accent); }
.plan-row .rk { font-family: var(--font-mono); font-size: 11px; color: var(--muted); width: 26px; flex: none; }
.plan-row .ord { font-family: var(--font-mono); font-size: 12px; flex: 1 1 210px; min-width: 0; }
.plan-row .ord .j { color: var(--muted); }
.plan-row .track { flex: 1 1 70px; min-width: 56px; height: 5px; border-radius: 3px; background: var(--line); overflow: hidden; }
.plan-row .fill { display: block; height: 100%; border-radius: 3px; background: var(--accent); transition: width .25s ease; }
.plan-row.is-best .fill { background: var(--run); }
.plan-row.is-cross .fill { background: var(--danger); }
.plan-row .cost { font-family: var(--font-mono); font-size: 12px; width: 62px; flex: none; text-align: right; }
.plan-row .mult { font-family: var(--font-mono); font-size: 11px; width: 54px; flex: none; text-align: right; color: var(--muted); }
.plan-row.is-best .mult { color: var(--run); }

footer {
  margin-top: 22px; padding-top: 12px; border-top: 1px solid var(--line);
  font-family: var(--font-mono); font-size: 11px; color: var(--muted);
  display: flex; justify-content: space-between; gap: 10px; flex-wrap: wrap;
}

@media (max-width: 400px) {
  .ctl { grid-template-columns: 62px 1fr 66px; }
  .plan-row .cost { width: 56px; }
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition: none !important; animation: none !important; scroll-behavior: auto !important; }
}
</style>
</head>
<body>
<div class="wrap">

  <header>
    <div class="eyebrow">query planner · toy cost model</div>
    <h1>Join-Order Explainer</h1>
    <p class="lede">Four tables, one query, <b>24 possible join orders</b> — all returning the identical answer.
      Pick the wrong one and the database touches <b>hundreds of times more rows</b>. Drag any slider; every
      plan is re-costed and re-ranked live.</p>
  </header>

  <div class="grid">
    <div class="col">

      <section class="card">
        <h2>The query <span class="h2note">joins are unordered</span></h2>
        <pre class="sql"><span class="kw">SELECT</span>  r.name, p.name, <span class="kw">count</span>(*)
<span class="kw">FROM</span>    <span class="tbl">orders</span>   o
<span class="kw">JOIN</span>    <span class="tbl">users</span>    u <span class="kw">ON</span> o.user_id    = u.id
<span class="kw">JOIN</span>    <span class="tbl">products</span> p <span class="kw">ON</span> o.product_id = p.id
<span class="kw">JOIN</span>    <span class="tbl">regions</span>  r <span class="kw">ON</span> u.region_id  = r.id
<span class="kw">GROUP BY</span> 1, 2</pre>
        <p class="note">SQL says <em>what</em>, never <em>how</em>. The planner may join these four tables in any
          sequence. This demo enumerates every <b>left-deep</b> order (join one table at a time into a growing
          intermediate result) and costs it as <code>rows scanned</code>.</p>
      </section>

      <section class="card">
        <h2>Schema <span class="h2note">editable</span></h2>

        <div class="chips" role="group" aria-label="Preset scenarios">
          <button type="button" class="chip" data-scenario="default" aria-pressed="true" title="Classic star: one huge fact table, three dimensions, clean foreign keys">textbook FK</button>
          <button type="button" class="chip" data-scenario="bigdim" aria-pressed="false" title="regions grows to 2M rows — the smallest-first heuristic breaks">fat dimension</button>
          <button type="button" class="chip" data-scenario="sloppy" aria-pressed="false" title="orders x products matches 100x more pairs than the key implies">sloppy join key</button>
          <button type="button" class="chip" data-scenario="smallfact" aria-pressed="false" title="only 100k orders — the fact table becomes the cheap starting point">small fact table</button>
        </div>

        <div class="subhead"><span>Row counts</span><span class="hint">type 2M, 1e6, 500000</span></div>
        <div class="grp" id="sizes"></div>

        <div class="subhead">
          <span>Join selectivity</span>
          <span class="hint">x1 = one match per key</span>
        </div>
        <div class="grp" id="sels"></div>
        <p class="note">Selectivity <code>s</code> is the fraction of all row pairs that match, so
          <code>|A &#8904; B| = |A| &times; |B| &times; s</code>. At <code>x1</code> it is <code>1 / distinct keys</code> —
          a textbook foreign key. Slide up to model a duplicated or fuzzy key.</p>
      </section>
    </div>

    <div class="col">

      <section class="card">
        <h2>Selected plan <span class="h2note">click any order below</span></h2>
        <div class="planhead">
          <span class="planorder" id="planOrder"></span>
          <span id="planBadges"></span>
          <button type="button" class="jumpbest" id="jumpBest" hidden>&#8593; show best</button>
        </div>
        <div class="treewrap">
          <svg class="tree" id="tree" viewBox="0 0 500 272" role="img" aria-labelledby="treeTitle"><title id="treeTitle"></title></svg>
          <div class="tcap" id="treeCap">left-deep tree &middot; read bottom-up</div>
        </div>
        <div class="stepwrap"><table class="steps">
          <thead><tr><th class="n">#</th><th>join in</th><th>on</th><th class="out">rows out</th></tr></thead>
          <tbody id="stepBody"></tbody>
        </table></div>
        <div class="why" id="why"></div>
      </section>

      <section class="card">
        <h2>All 24 left-deep orders <span class="h2note">ranked by rows scanned</span></h2>
        <div class="stats" id="stats"></div>
        <div class="legend">
          <span><i class="lg-best"></i>best</span>
          <span><i class="lg-ok"></i>keyed join</span>
          <span><i class="lg-x"></i>cross product</span>
          <span>bar = log scale</span>
        </div>
        <div class="list" id="list" role="radiogroup" aria-label="Join orders ranked by cost, cheapest first"></div>
      </section>
    </div>
  </div>

  <footer>
    <span>demo &middot; sloprun.dev</span>
    <span>cost = every base table scanned once + every intermediate streamed onward</span>
  </footer>
</div>

<p class="sr-only" id="live" role="status" aria-live="polite"></p>

<script>
(function () {
  "use strict";

  /* ---------------- theme (platform postMessage + prefers-color-scheme) ------- */
  window.addEventListener("message", function (e) {
    var d = e && e.data;
    if (d && d.type === "sloprun:theme" && (d.theme === "light" || d.theme === "dark")) {
      document.documentElement.setAttribute("data-theme", d.theme);
    }
  });

  /* ---------------- model ---------------------------------------------------- */
  var TABLES = [
    { key: "regions",  size: 50 },
    { key: "users",    size: 1000000 },
    { key: "products", size: 10000 },
    { key: "orders",   size: 10000000 }
  ];
  var EDGES = [
    { id: "ou", a: "orders", b: "users",    ref: "users",    left: "orders.user_id",    right: "users.id",    short: "user_id" },
    { id: "op", a: "orders", b: "products", ref: "products", left: "orders.product_id", right: "products.id", short: "product_id" },
    { id: "ur", a: "users",  b: "regions",  ref: "regions",  left: "users.region_id",   right: "regions.id",  short: "region_id" }
  ];

  var DEFAULT_SIZES = { regions: 50, users: 1000000, products: 10000, orders: 10000000 };
  var DEFAULT_MULTS = { ou: 1, op: 1, ur: 1 };

  var state = {
    sizes: Object.assign({}, DEFAULT_SIZES),
    mults: Object.assign({}, DEFAULT_MULTS),
    selected: null,
    stacked: false,   /* narrow-container tree geometry (vertical spine) */
    follow: true      /* selection tracks the best plan until the user picks a row */
  };

  var MIN_ROWS = 1, MAX_ROWS = 1000000000, DECADES = 9;

  function sel(edge) { return Math.min(1, state.mults[edge.id] / Math.max(1, state.sizes[edge.ref])); }
  function edgesBetween(t, joined) {
    var out = [];
    for (var i = 0; i < EDGES.length; i++) {
      var e = EDGES[i];
      var other = e.a === t ? e.b : (e.b === t ? e.a : null);
      if (other && joined.indexOf(other) !== -1) out.push(e);
    }
    return out;
  }

  function permutations(arr) {
    if (arr.length <= 1) return [arr.slice()];
    var res = [];
    for (var i = 0; i < arr.length; i++) {
      var rest = arr.slice(0, i).concat(arr.slice(i + 1));
      var sub = permutations(rest);
      for (var j = 0; j < sub.length; j++) res.push([arr[i]].concat(sub[j]));
    }
    return res;
  }
  var PERMS = permutations(TABLES.map(function (t) { return t.key; }));

  /* cost = sum of base-table scans + every intermediate that feeds the next join */
  function evalPlan(order) {
    var baseSum = 0, i;
    for (i = 0; i < order.length; i++) baseSum += state.sizes[order[i]];

    var card = state.sizes[order[0]];
    var joined = [order[0]];
    var steps = [], intermediates = card, peak = card, cross = false;

    for (i = 1; i < order.length; i++) {
      var t = order[i];
      var es = edgesBetween(t, joined);
      var s = 1;
      for (var k = 0; k < es.length; k++) s *= sel(es[k]);
      if (!es.length) cross = true;
      var inRows = card;
      var out = Math.max(1, inRows * state.sizes[t] * s);
      steps.push({ table: t, edges: es, s: s, inRows: inRows, out: out, cross: !es.length });
      card = out;
      joined = joined.concat([t]);
      if (i < order.length - 1) { intermediates += card; peak = Math.max(peak, card); }
    }
    return {
      key: order.join(">"), order: order, steps: steps,
      cost: baseSum + intermediates, inter: intermediates, peak: peak, cross: cross, result: card
    };
  }

  /* ---------------- formatting ------------------------------------------------ */
  function fmt(n) {
    if (!isFinite(n)) return "inf";
    n = Number(n.toPrecision(3));   /* round first, so 9.995e6 reads "10.0M", not "10.00M" */
    if (n >= 1e15) return n.toExponential(1).replace("e+", "e");
    var u = [["T", 1e12], ["B", 1e9], ["M", 1e6], ["k", 1e3]];
    for (var i = 0; i < u.length; i++) {
      if (n >= u[i][1]) {
        var x = n / u[i][1];
        return (x < 10 ? x.toFixed(2) : x < 100 ? x.toFixed(1) : Math.round(x)) + u[i][0];
      }
    }
    return String(Math.round(n));
  }
  function rows(n) { return fmt(n) + (n === 1 ? " row" : " rows"); }
  /* same, but with the number in mono for prose */
  function rowsM(n) { return "<span class=\"m\">" + fmt(n) + "</span> row" + (n === 1 ? "" : "s"); }
  function ratio(n) {
    if (!isFinite(n)) return "inf";
    if (n >= 1000) return fmt(n) + "x";
    if (n >= 100) return Math.round(n) + "x";
    if (n >= 10) return n.toFixed(1) + "x";
    return n.toFixed(2) + "x";
  }
  function sci(s) {
    if (s >= 0.01) return s.toFixed(s >= 0.1 ? 2 : 3);
    var e = s.toExponential(1).split("e");
    return e[0] + "e" + Number(e[1]);
  }
  function parseCount(str) {
    var m = String(str).trim().toLowerCase().replace(/[, _]/g, "")
      .match(/^([0-9]*\.?[0-9]+(?:e[+-]?[0-9]+)?)([kmbt]?)$/);
    if (!m) return null;
    var mult = { "": 1, k: 1e3, m: 1e6, b: 1e9, t: 1e12 }[m[2]];
    var v = parseFloat(m[1]) * mult;
    if (!isFinite(v) || v <= 0) return null;
    return Math.max(MIN_ROWS, Math.min(MAX_ROWS, Math.round(v)));
  }
  function sizeToSlider(n) { return Math.round(Math.log(n) / Math.LN10 / DECADES * 100); }
  function sliderToSize(v) {
    var raw = Math.pow(10, v / 100 * DECADES);
    var mag = Math.pow(10, Math.floor(Math.log(raw) / Math.LN10) - 1);
    return Math.max(MIN_ROWS, Math.min(MAX_ROWS, Math.round(Math.round(raw / mag) * mag)));
  }
  function multToSlider(m) { return Math.round((Math.log(m) / Math.LN10 + 2) * 25); }
  function sliderToMult(v) { return Math.pow(10, v / 25 - 2); }

  var $ = function (id) { return document.getElementById(id); };
  function el(tag, cls, txt) {
    var n = document.createElement(tag);
    if (cls) n.className = cls;
    if (txt !== undefined) n.textContent = txt;
    return n;
  }
  function orderHTML(order, joiner) {
    return order.join('<span class="j"> ' + (joiner || "&#8904;") + " </span>");
  }

  /* ---------------- build controls ------------------------------------------- */
  var sizeUI = {}, selUI = {};

  (function buildSizeControls() {
    var host = $("sizes");
    TABLES.forEach(function (t) {
      var row = el("div", "ctl");
      var lab = el("label", null, t.key);
      lab.setAttribute("for", "rng-" + t.key);

      var rng = el("input");
      rng.type = "range"; rng.min = "0"; rng.max = "100"; rng.step = "1";
      rng.id = "rng-" + t.key;
      rng.setAttribute("aria-label", t.key + " row count slider");

      var num = el("input", "num");
      /* NOT inputmode="numeric": the field accepts 2M / 1e6 / 500000, and a digits-only
         mobile keypad would make two of those three impossible to type. */
      num.type = "text"; num.spellcheck = false;
      num.autocomplete = "off"; num.autocapitalize = "off"; num.setAttribute("autocorrect", "off");
      num.id = "num-" + t.key;
      num.setAttribute("aria-label", t.key + " row count");

      /* `shown` is the exact string we last painted. fmt() is lossy ("1234" -> "1.23k"),
         so re-parsing an untouched field would silently drift the value on blur. */
      var shown = "";
      function show(v) {
        shown = num.value = fmt(v);
        /* the slider's raw value is a 0-100 log position; without valuetext a screen
           reader announces "67" instead of the row count it actually sets */
        rng.setAttribute("aria-valuetext", rows(v));
        num.classList.remove("bad"); num.removeAttribute("aria-invalid");
      }

      rng.addEventListener("input", function () {
        state.sizes[t.key] = sliderToSize(Number(rng.value));
        show(state.sizes[t.key]);
        clearScenario(); recompute();
      });
      function commitNum() {
        if (num.value === shown) return true;          /* nothing typed since last paint */
        var v = parseCount(num.value);
        if (v === null) { num.classList.add("bad"); num.setAttribute("aria-invalid", "true"); return false; }
        state.sizes[t.key] = v;
        rng.value = String(sizeToSlider(v));
        show(v);
        clearScenario(); recompute();
        return true;
      }
      num.addEventListener("change", commitNum);
      /* leaving the field with unparseable text restores the value actually in use */
      num.addEventListener("blur", function () { if (!commitNum()) show(state.sizes[t.key]); });
      num.addEventListener("keydown", function (ev) { if (ev.key === "Enter") { ev.preventDefault(); commitNum(); } });

      row.appendChild(lab); row.appendChild(rng); row.appendChild(num);
      host.appendChild(row);
      sizeUI[t.key] = { rng: rng, num: num, show: show };
    });
  })();

  (function buildSelControls() {
    var host = $("sels");
    EDGES.forEach(function (e) {
      var box = el("div", "edge");
      var pred = el("div", "pred");
      pred.innerHTML = e.left + ' <span class="op">=</span> ' + e.right;

      var erow = el("div", "erow");
      var rng = el("input");
      rng.type = "range"; rng.min = "0"; rng.max = "100"; rng.step = "1";
      rng.id = "sel-" + e.id;
      rng.setAttribute("aria-label", "selectivity multiplier for " + e.left + " = " + e.right);
      var read = el("span", "readout");
      read.id = "read-" + e.id;
      rng.setAttribute("aria-describedby", read.id);

      rng.addEventListener("input", function () {
        state.mults[e.id] = sliderToMult(Number(rng.value));
        clearScenario(); recompute();
      });

      erow.appendChild(rng); erow.appendChild(read);
      box.appendChild(pred); box.appendChild(erow);
      host.appendChild(box);
      selUI[e.id] = { rng: rng, read: read };
    });
  })();

  function syncControls() {
    TABLES.forEach(function (t) {
      sizeUI[t.key].rng.value = String(sizeToSlider(state.sizes[t.key]));
      sizeUI[t.key].show(state.sizes[t.key]);
    });
    EDGES.forEach(function (e) { selUI[e.id].rng.value = String(multToSlider(state.mults[e.id])); });
  }

  function updateSelReadouts() {
    EDGES.forEach(function (e) {
      var s = sel(e);
      var pair = state.sizes[e.a] * state.sizes[e.b] * s;
      var mx = state.mults[e.id] < 10 ? state.mults[e.id].toFixed(2) : String(Math.round(state.mults[e.id]));
      selUI[e.id].read.innerHTML =
        "x" + mx + " &middot; s=" + sci(s) + " &middot; <b>" + fmt(Math.max(1, pair)) + "</b>";
      selUI[e.id].rng.setAttribute("aria-valuetext",
        "x" + mx + ", selectivity " + sci(s) + ", " + rows(Math.max(1, pair)) + " matching pairs");
    });
  }

  /* ---------------- scenarios ------------------------------------------------- */
  var SCENARIOS = {
    "default":   { sizes: DEFAULT_SIZES, mults: DEFAULT_MULTS },
    "bigdim":    { sizes: { regions: 2000000, users: 1000000, products: 10000, orders: 10000000 }, mults: DEFAULT_MULTS },
    "sloppy":    { sizes: DEFAULT_SIZES, mults: { ou: 1, op: 100, ur: 1 } },
    "smallfact": { sizes: { regions: 50, users: 1000000, products: 10000, orders: 100000 }, mults: DEFAULT_MULTS }
  };
  var chips = Array.prototype.slice.call(document.querySelectorAll(".chip"));
  function clearScenario() { chips.forEach(function (c) { c.setAttribute("aria-pressed", "false"); }); }
  chips.forEach(function (c) {
    c.addEventListener("click", function () {
      var s = SCENARIOS[c.dataset.scenario];
      state.sizes = Object.assign({}, s.sizes);
      state.mults = Object.assign({}, s.mults);
      state.follow = true;
      clearScenario();
      c.setAttribute("aria-pressed", "true");
      syncControls();
      recompute();
    });
  });

  /* ---------------- ranked list rows ------------------------------------------ */
  var rowEls = {};
  (function buildRows() {
    var host = $("list");
    PERMS.forEach(function (p) {
      var key = p.join(">");
      var b = el("button", "plan-row");
      b.type = "button";
      b.setAttribute("role", "radio");
      b.setAttribute("aria-checked", "false");
      b.tabIndex = -1;
      b.dataset.key = key;

      var rk = el("span", "rk");
      var ord = el("span", "ord"); ord.innerHTML = orderHTML(p);
      var track = el("span", "track");
      var fill = el("span", "fill"); fill.style.width = "0%";
      track.appendChild(fill);
      var cost = el("span", "cost");
      var mult = el("span", "mult");

      b.appendChild(rk); b.appendChild(ord); b.appendChild(track);
      b.appendChild(cost); b.appendChild(mult);

      b.addEventListener("click", function () { select(key, false); });
      b.addEventListener("keydown", onRowKey);

      host.appendChild(b);
      rowEls[key] = { root: b, rk: rk, fill: fill, cost: cost, mult: mult };
    });
  })();

  var sortedKeys = [];
  function onRowKey(ev) {
    var i = sortedKeys.indexOf(this.dataset.key), n = sortedKeys.length, next = null;
    if (ev.key === "ArrowDown" || ev.key === "ArrowRight") next = (i + 1) % n;
    else if (ev.key === "ArrowUp" || ev.key === "ArrowLeft") next = (i - 1 + n) % n;
    else if (ev.key === "Home") next = 0;
    else if (ev.key === "End") next = n - 1;
    else return;
    ev.preventDefault();
    select(sortedKeys[next], false);
    var t = rowEls[sortedKeys[next]].root;
    t.focus();
    t.scrollIntoView({ block: "nearest" });
  }

  function select(key, isFollow) {
    state.selected = key;
    state.follow = !!isFollow;
    render();
  }

  /* ---------------- tree diagram ---------------------------------------------- */
  var SVGNS = "http://www.w3.org/2000/svg";
  function svgEl(tag, attrs) {
    var n = document.createElementNS(SVGNS, tag);
    for (var k in attrs) if (Object.prototype.hasOwnProperty.call(attrs, k)) n.setAttribute(k, attrs[k]);
    return n;
  }
  /* wide: classic left-deep tree, leaves in a row.
     stacked: same tree turned into a vertical spine — the only shape that stays
     legible under ~430px, where the 4-across layout scales text down to ~7px. */
  var GEO = { colW: 110, x0: 62, leafY: 232, rowH: 58, LW: 100, LH: 42, JW: 120, JH: 44,
              vb: "0 0 454 272", f1: 13, f2: 11, f3: 13, f4: 10.5 };
  var STK = { spineX: 76, leafX: 218, yBase: 232, rowH: 62, BW: 140, BH: 44, LW: 112, LH: 40,
              vb: "0 0 300 268", f1: 12.5, f2: 10.5, f3: 12.5, f4: 10 };

  /* two-key joins ("on user_id + product_id") are wider than the node box — shrink to fit */
  function fitFont(str, boxW, base) {
    var w = str.length * 0.6 * base;
    return w > boxW - 8 ? Math.max(6, base * (boxW - 8) / w) : base;
  }
  function mkLabel(svg) {
    return function (x, y, cls, str, boxW, base) {
      var n = svgEl("text", { x: x, y: y, "text-anchor": "middle", "class": cls,
        "font-size": fitFont(str, boxW, base) });
      n.textContent = str;
      svg.appendChild(n);
    };
  }
  function joinCaption(st) {
    return st.cross ? "cross product"
      : "on " + st.edges.map(function (e) { return e.short; }).join(" + ");
  }
  function isPeakStep(plan, i) {
    var st = plan.steps[i - 1];
    return !st.cross && i < plan.order.length - 1 && st.out >= plan.peak;
  }

  function drawTree(plan) {
    var svg = $("tree");
    while (svg.firstChild) svg.removeChild(svg.firstChild);
    var title = svgEl("title", { id: "treeTitle" });
    title.textContent = "Left-deep join tree: " + plan.order.join(", then ") +
      ". Final result " + fmt(plan.result) + " rows.";
    svg.appendChild(title);
    (state.stacked ? drawStacked : drawWide)(svg, plan);

    /* the amber and red node outlines are meaningless without a key — name them,
       and only mention the colour that is actually on screen */
    var hasPeak = false;
    for (var i = 1; i < plan.order.length; i++) { if (isPeakStep(plan, i)) { hasPeak = true; break; } }
    $("treeCap").textContent = "left-deep tree \u00B7 read bottom-up" +
      (hasPeak ? " \u00B7 amber = biggest intermediate" : "") +
      (plan.cross ? " \u00B7 red = cross product" : "");
  }

  function drawWide(svg, plan) {
    var g = GEO, label = mkLabel(svg), n = plan.order.length, i;
    svg.setAttribute("viewBox", g.vb);
    var leafX = function (k) { return g.x0 + k * g.colW; };
    var sx = [leafX(0)], sy = [g.leafY];
    for (i = 1; i < n; i++) { sx.push((sx[i - 1] + leafX(i)) / 2); sy.push(g.leafY - i * g.rowH); }

    /* connectors first so boxes paint over them */
    for (i = 1; i < n; i++) {
      var childY = i === 1 ? g.leafY - g.LH / 2 : sy[i - 1] - g.JH / 2;
      svg.appendChild(svgEl("path", { "class": "lnk",
        d: "M" + sx[i] + " " + (sy[i] + g.JH / 2) + " L" + sx[i - 1] + " " + childY }));
      svg.appendChild(svgEl("path", { "class": "lnk" + (plan.steps[i - 1].cross ? " x" : ""),
        d: "M" + sx[i] + " " + (sy[i] + g.JH / 2) + " L" + leafX(i) + " " + (g.leafY - g.LH / 2) }));
    }
    for (i = 0; i < n; i++) {
      var t = plan.order[i];
      svg.appendChild(svgEl("rect", { "class": "box",
        x: leafX(i) - g.LW / 2, y: g.leafY - g.LH / 2, width: g.LW, height: g.LH, rx: 8 }));
      label(leafX(i), g.leafY - 4, "t1", t, g.LW, g.f1);
      label(leafX(i), g.leafY + 12, "t2", rows(state.sizes[t]), g.LW, g.f2);
    }
    for (i = 1; i < n; i++) {
      var st = plan.steps[i - 1];
      svg.appendChild(svgEl("rect", { "class": "box" + (st.cross ? " x" : (isPeakStep(plan, i) ? " peak" : "")),
        x: sx[i] - g.JW / 2, y: sy[i] - g.JH / 2, width: g.JW, height: g.JH, rx: 8 }));
      label(sx[i], sy[i] - 2, "t1" + (st.cross ? " x" : ""), "\u22C8 " + rows(st.out), g.JW, g.f3);
      label(sx[i], sy[i] + 13, "t2" + (st.cross ? " x" : ""), joinCaption(st), g.JW, g.f4);
    }
  }

  function drawStacked(svg, plan) {
    var g = STK, label = mkLabel(svg), n = plan.order.length, i;
    svg.setAttribute("viewBox", g.vb);
    var yAt = function (k) { return g.yBase - k * g.rowH; };

    for (i = 1; i < n; i++) {
      /* spine: this join sits on top of whatever the previous step produced */
      svg.appendChild(svgEl("path", { "class": "lnk",
        d: "M" + g.spineX + " " + (yAt(i) + g.BH / 2) + " L" + g.spineX + " " + (yAt(i - 1) - g.BH / 2) }));
      /* arm: the table pulled in at this step */
      svg.appendChild(svgEl("path", { "class": "lnk" + (plan.steps[i - 1].cross ? " x" : ""),
        d: "M" + (g.leafX - g.LW / 2) + " " + yAt(i) + " L" + (g.spineX + g.BW / 2) + " " + yAt(i) }));
    }

    var t0 = plan.order[0];
    svg.appendChild(svgEl("rect", { "class": "box",
      x: g.spineX - g.BW / 2, y: g.yBase - g.BH / 2, width: g.BW, height: g.BH, rx: 8 }));
    label(g.spineX, g.yBase - 4, "t1", t0, g.BW, g.f1);
    label(g.spineX, g.yBase + 12, "t2", rows(state.sizes[t0]), g.BW, g.f2);

    for (i = 1; i < n; i++) {
      var st = plan.steps[i - 1], y = yAt(i), tb = plan.order[i];
      svg.appendChild(svgEl("rect", { "class": "box",
        x: g.leafX - g.LW / 2, y: y - g.LH / 2, width: g.LW, height: g.LH, rx: 8 }));
      label(g.leafX, y - 3, "t1", tb, g.LW, g.f1);
      label(g.leafX, y + 12, "t2", rows(state.sizes[tb]), g.LW, g.f2);

      svg.appendChild(svgEl("rect", { "class": "box" + (st.cross ? " x" : (isPeakStep(plan, i) ? " peak" : "")),
        x: g.spineX - g.BW / 2, y: y - g.BH / 2, width: g.BW, height: g.BH, rx: 8 }));
      label(g.spineX, y - 2, "t1" + (st.cross ? " x" : ""), "\u22C8 " + rows(st.out), g.BW, g.f3);
      label(g.spineX, y + 13, "t2" + (st.cross ? " x" : ""), joinCaption(st), g.BW, g.f4);
    }
  }

  /* ---------------- render ----------------------------------------------------- */
  var plans = [], byKey = {}, best = null, worst = null;

  function recompute() {
    plans = PERMS.map(evalPlan);
    plans.sort(function (a, b) { return a.cost - b.cost || (a.key < b.key ? -1 : 1); });
    byKey = {};
    sortedKeys = plans.map(function (p) { byKey[p.key] = p; return p.key; });
    best = plans[0];
    worst = plans[plans.length - 1];
    if (state.follow || !byKey[state.selected]) state.selected = best.key;
    updateSelReadouts();
    render();
  }

  var liveTimer = null;
  function announce(msg) {
    if (liveTimer) clearTimeout(liveTimer);
    liveTimer = setTimeout(function () { $("live").textContent = msg; }, 450);
  }

  function render() {
    var lo = Math.log(best.cost), hi = Math.log(worst.cost);
    var span = hi - lo;

    /* Keep DOM order == visual rank order so Tab, screen readers and the
       arrow-key roving tabindex all walk the list cheapest-first. Reordering
       detaches nodes, which drops focus — restore it when a row held it. */
    var host = $("list"), needsReorder = false;
    for (var q = 0; q < plans.length; q++) {
      if (host.children[q] !== rowEls[plans[q].key].root) { needsReorder = true; break; }
    }
    if (needsReorder) {
      var active = document.activeElement;
      var hadFocus = !!(active && active.classList && active.classList.contains("plan-row"));
      var frag = document.createDocumentFragment();
      for (var z = 0; z < plans.length; z++) frag.appendChild(rowEls[plans[z].key].root);
      host.appendChild(frag);
      if (hadFocus) active.focus();
    }

    for (var i = 0; i < plans.length; i++) {
      var p = plans[i], r = rowEls[p.key];
      r.rk.textContent = "#" + (i + 1 < 10 ? "0" : "") + (i + 1);
      r.cost.textContent = fmt(p.cost);
      r.mult.textContent = i === 0 ? "best" : ratio(p.cost / best.cost);
      r.fill.style.width = (6 + 94 * (span > 0 ? (Math.log(p.cost) - lo) / span : 0)).toFixed(1) + "%";
      r.root.classList.toggle("is-best", i === 0);
      r.root.classList.toggle("is-cross", p.cross && i !== 0);
      var on = p.key === state.selected;
      r.root.setAttribute("aria-checked", on ? "true" : "false");
      r.root.tabIndex = on ? 0 : -1;
      r.root.setAttribute("aria-label",
        "Rank " + (i + 1) + ": " + p.order.join(" then ") + ", " + fmt(p.cost) + " rows scanned, " +
        (i === 0 ? "best plan" : ratio(p.cost / best.cost) + " the best") + (p.cross ? ", contains a cross product" : ""));
    }

    var cur = byKey[state.selected], rank = sortedKeys.indexOf(state.selected) + 1;
    $("planOrder").innerHTML = orderHTML(cur.order);

    var badges = $("planBadges");
    badges.innerHTML = "";
    if (rank === 1) {
      var bb = el("span", "badge best", "✓ best of 24");
      badges.appendChild(bb);
    } else {
      badges.appendChild(el("span", "badge rank", "#" + rank + " of 24 · " + ratio(cur.cost / best.cost) + " the best"));
    }
    if (cur.cross) badges.appendChild(document.createTextNode(" "));
    if (cur.cross) badges.appendChild(el("span", "badge cross", "cross product"));
    $("jumpBest").hidden = rank === 1;

    drawTree(cur);

    var tb = $("stepBody");
    tb.innerHTML = "";
    cur.steps.forEach(function (st, idx) {
      var tr = el("tr");
      if (st.cross) tr.className = "xp";
      tr.appendChild(el("td", "n", String(idx + 1)));
      tr.appendChild(el("td", null, "⋈ " + st.table));
      tr.appendChild(el("td", null, st.cross ? "(no shared key)" :
        st.edges.map(function (e) { return e.short; }).join(" + ")));
      tr.appendChild(el("td", "out", fmt(st.out)));
      tb.appendChild(tr);
    });

    renderWhy(cur, rank);
    renderStats();
    announce(rank === 1
      ? "Best plan: " + best.order.join(", then ") + " at " + fmt(best.cost) + " rows scanned."
      : "Selected rank " + rank + " of 24: " + cur.order.join(", then ") + " at " + fmt(cur.cost) +
        " rows scanned, " + ratio(cur.cost / best.cost) + " the best plan.");
  }

  function crossPairText(plan) {
    for (var i = 0; i < plan.steps.length; i++) {
      if (plan.steps[i].cross) return { table: plan.steps[i].table, rows: plan.steps[i].out, at: i + 1 };
    }
    return null;
  }

  function renderWhy(cur, rank) {
    var w = $("why"), spread = worst.cost / best.cost, tied = cur.cost <= best.cost * 1.000001;
    var first = best.order[0], second = best.order[1];
    var html;

    if (rank === 1) {
      html = "<p>Start with <b>" + first + "</b> (" + rowsM(state.sizes[first]) +
        ") and join <b>" + second + "</b> next: only <span class=\"m\">" + fmt(best.inter) +
        "</span> rows ever stream between the joins, and no intermediate grows past <span class=\"m\">" +
        fmt(best.peak) + "</span>.</p>";
      if (spread <= 1.000001) {
        /* every order costs the same — saying the worst scans "1.00x more" would be nonsense */
        html += "<p>At these row counts all 24 orders cost the same <span class=\"m\">" + fmt(best.cost) +
          "</span> rows: the tables are too small for the order to matter. Push a row count up and the spread opens.</p>";
      } else {
        var wx = crossPairText(worst);
        html += "<p>The worst of the 24 orders " +
          (wx ? "bolts <span class=\"m\">" + wx.table + "</span> on with no shared key &mdash; a cross product of " + rowsM(wx.rows) + " &mdash; and "
              : "peaks at " + rowsM(worst.peak) + " and ") +
          "scans <span class=\"m\">" + ratio(spread) + "</span> as many, for exactly the same <span class=\"m\">" +
          fmt(cur.result) + "</span>-row answer.</p>";
      }
    } else if (tied) {
      /* a genuine cost tie: "1.00x the work ... at step 3 it builds N where the best builds N"
         would compare a number to itself, so say what is actually true instead */
      html = "<p>This order costs <b>exactly as much as the best one</b> &mdash; <span class=\"m\">" +
        fmt(cur.cost) + "</span> rows scanned either way. More than one order is optimal at these numbers; " +
        "nudge any slider and the tie breaks.</p>";
      html += "<p>Ranked first is <span class=\"m\">" + best.order.join(" \u22C8 ") +
        "</span>, only because ties are broken alphabetically.</p>";
    } else {
      var cx = crossPairText(cur), because;
      if (cx) {
        because = "At step " + cx.at + " it joins <span class=\"m\">" + cx.table +
          "</span> to tables it shares no key with, so the engine must build every pair &mdash; " +
          rowsM(cx.rows) + ".";
      } else if (cur.order[0] !== best.order[0]) {
        because = "It streams <span class=\"m\">" + fmt(cur.inter) + "</span> rows through its joins against <span class=\"m\">" +
          fmt(best.inter) + "</span>, because it starts from <span class=\"m\">" + cur.order[0] + "</span> (" +
          rowsM(state.sizes[cur.order[0]]) + ") instead of <span class=\"m\">" + best.order[0] + "</span> (" +
          rowsM(state.sizes[best.order[0]]) + ").";
      } else {
        var k = 0;
        while (k < cur.steps.length - 1 && cur.steps[k].out === best.steps[k].out) k++;
        because = cur.steps[k].out === best.steps[k].out
          ? "It builds the same intermediates in a different sequence, adding up to <span class=\"m\">" +
            fmt(cur.inter) + "</span> streamed rows against <span class=\"m\">" + fmt(best.inter) + "</span>."
          : "It streams <span class=\"m\">" + fmt(cur.inter) + "</span> rows through its joins against <span class=\"m\">" +
            fmt(best.inter) + "</span>: at step " + (k + 1) + " it builds <span class=\"m\">" + fmt(cur.steps[k].out) +
            "</span> rows where the best order builds <span class=\"m\">" + fmt(best.steps[k].out) + "</span>.";
      }
      html = "<p>This order is <b>" + ratio(cur.cost / best.cost) + " the work</b> of the best one. " + because + "</p>";
      html += "<p>Best is <span class=\"m\">" + best.order.join(" \u22C8 ") + "</span> at <span class=\"m\">" +
        fmt(best.cost) + "</span> rows scanned &mdash; same answer, less work.</p>";
    }
    w.innerHTML = html;
  }

  function renderStats() {
    var host = $("stats");
    host.innerHTML = "";
    var crosses = plans.filter(function (p) { return p.cross; }).length;
    [
      ["best", fmt(best.cost), ""],
      ["worst", fmt(worst.cost), ""],
      ["spread", ratio(worst.cost / best.cost), "spread"],
      ["result rows", fmt(best.result), ""],
      ["cross-product plans", String(crosses) + "/24", ""]
    ].forEach(function (s) {
      var d = el("div", "stat");
      d.appendChild(el("span", "k", s[0]));
      d.appendChild(el("span", "v" + (s[2] ? " " + s[2] : ""), s[1]));
      host.appendChild(d);
    });
  }

  $("jumpBest").addEventListener("click", function () {
    select(best.key, true);
    var t = rowEls[best.key].root;
    t.scrollIntoView({ block: "nearest" });
    t.focus();
  });

  /* ---------------- responsive tree geometry ----------------------------------- */
  (function watchWidth() {
    var wrap = document.querySelector(".treewrap");
    function check() {
      if (!state.selected || !byKey[state.selected]) return;
      var narrow = wrap.clientWidth < 430;
      if (narrow !== state.stacked) { state.stacked = narrow; drawTree(byKey[state.selected]); }
    }
    if (typeof ResizeObserver === "function") new ResizeObserver(check).observe(wrap);
    else window.addEventListener("resize", check);
    state.stacked = wrap.clientWidth < 430;
  })();

  /* ---------------- go --------------------------------------------------------- */
  syncControls();
  recompute();
})();
</script>
</body>
</html>

source-visible by construction · nothing is published here without its code

Comments

0 total

No comments yet. If you ran it, say what happened.

Report this post

Goes straight to the moderation queue. Enough independent reports and the post is suspended automatically until a human looks.

what is wrong