Prechádzať zdrojové kódy

Remove nearest.js and fix autocomplete on slider

Kirk Trombley 3 rokov pred
rodič
commit
1880146726
2 zmenil súbory, kde vykonal 2 pridanie a 632 odobranie
  1. 2 1
      nearest.html
  2. 0 631
      nearest.js

+ 2 - 1
nearest.html

@@ -72,6 +72,7 @@
           </label>
           <input
               id="num-poke" style="grid-area: limt;"
+              autocomplete="off"
               type="range" min="1" max="100" value="10"
               oninput="document.getElementById('num-poke-display').textContent = event.target.value" 
               onchange="state.number = event.target.value; onControlsChanged(state)"
@@ -176,4 +177,4 @@
   </div>
 </body>
 
-</html>
+</html>

+ 0 - 631
nearest.js

@@ -1,631 +0,0 @@
-// Selectors + DOM Manipulation
-const getColorInputNode = () => document.getElementById("color-input");
-const getMetricDropdownNode = () => document.getElementById("metric");
-const getClusterChoiceDropdownNode = () => document.getElementById("image-summary");
-const getClusterScaleToggleNode = () => document.getElementById("scale-by-cluster-size");
-const getClusterMeanWarning = () => document.getElementById("cluster-mean-warning");
-const getIncludeXToggleNode = () => document.getElementById("include-x");
-const getNormQYToggleNode = () => document.getElementById("norm-q-y");
-const getCloseCoeffSliderNode = () => document.getElementById("close-coeff");
-const getCloseCoeffDisplayNode = () => document.getElementById("close-coeff-display");
-const getLimitSliderNode = () => document.getElementById("num-poke");
-const getLimitDisplayNode = () => document.getElementById("num-poke-display");
-const getNameInputNode = () => document.getElementById("pokemon-name");
-const getScoreListJABNode = () => document.getElementById("best-list-jab");
-const getScoreListRGBNode = () => document.getElementById("best-list-rgb");
-const getSearchSpaceDisplayNode = () => document.getElementById("search-space-display");
-const getSearchListNode = () => document.getElementById("search-list");
-const getHideableControlNodes = () => document.querySelectorAll(".hideable_control");
-const getQJABDisplay = () => document.getElementById("q-vec-jab");
-const getQRGBDisplay = () => document.getElementById("q-vec-rgb");
-const getObjFnDisplay = () => document.getElementById("obj-fn");
-
-const clearNodeContents = node => { node.innerHTML = ""; };
-const hideCustomControls = () => getHideableControlNodes()
-  .forEach(n => n.setAttribute("class", "hideable_control hideable_control--hidden"));
-const showCustomControls = () => getHideableControlNodes()
-  .forEach(n => n.setAttribute("class", "hideable_control"));
-
-// Vector Math
-const vectorDot = (u, v) => u.map((x, i) => x * v[i]).reduce((x, y) => x + y);
-const vectorSqMag = v => vectorDot(v, v);
-const vectorMag = v => Math.sqrt(vectorSqMag(v));
-const vectorSqDist = (u, v) => vectorSqMag(u.map((x, i) => x - v[i]));
-const vectorDist = (u, v) => Math.sqrt(vectorSqDist(u, v));
-const vectorNorm = v => { const n = vectorMag(v); return [ n, v.map(c => c / n) ]; };
-
-// Angle Math
-const angleDiff = (a, b) => { const raw = Math.abs(a - b); return raw < 180 ? raw : (360 - raw); };
-const rad2deg = 180 / Math.PI;
-
-// Conversions
-const jab2hex = jab => d3.jab(...jab).formatHex();
-const rgb2hex = rgb => d3.rgb(...rgb).formatHex();
-const jab2hue = ([, a, b]) => rad2deg * Math.atan2(b, a);
-const rgb2hue = rgb => d3.hsl(d3.rgb(...rgb)).h || 0;
-const hex2rgb = hex => {
-  const { r, g, b } = d3.color(hex);
-  return [r, g, b];
-};
-
-// Arg Compare
-const argComp = comp => ra => ra.map((x, i) => [x, i]).reduce((a, b) => comp(a[0], b[0]) > 0 ? b : a)[1];
-const argMin = argComp((a, b) => a - b);
-const argMax = argComp((a, b) => b - a);
-
-// Pre-Compute Data
-const computeVectorData = (vector, toHex, toHue) => {
-  const [ magnitude, unit ] = vectorNorm(vector);
-  return {
-    vector,
-    magnitude,
-    magSq: magnitude * magnitude,
-    unit,
-    hex: toHex(vector),
-    hue: toHue(vector),
-  };
-};
-
-const computeStats = (inertia, trueMeanVec, kMeanStruct, toHex, toHue) => ({
-  inertia,
-  trueMean: computeVectorData(trueMeanVec, toHex, toHue),
-  kMeans: kMeanStruct.slice(0, 3).map(z => computeVectorData(z, toHex, toHue)),
-  kWeights: kMeanStruct[3],
-  largestCluster: argMax(kMeanStruct[3]),
-  smallestCluster: argMin(kMeanStruct[3]),
-});
-
-const pokemonColorData = database.map(({
-  name, xJAB, xRGB, yJAB, yRGB, zJAB, zRGB,
-}) => ({
-  name,
-  jabStats: computeStats(xJAB, yJAB, zJAB, jab2hex, jab2hue),
-  rgbStats: computeStats(xRGB, yRGB, zRGB, rgb2hex, rgb2hue),
-}));
-
-const pokemonLookup = new Fuse(pokemonColorData, { keys: [ "name" ] });
-
-// Color Calculations
-const getContrastingTextColor = rgb => vectorDot(rgb, [0.3, 0.6, 0.1]) >= 128 ? "#222" : "#ddd";
-
-const readColorInput = () => {
-  const colorInput = "#" + (getColorInputNode()?.value?.replace("#", "") ?? "FFFFFF");
-
-  if (colorInput.length !== 7) {
-    return;
-  }
-
-  const rgb = d3.color(colorInput);
-  const { J, a, b } = d3.jab(rgb);
-
-  return {
-    jabData: computeVectorData([ J, a, b ], jab2hex, jab2hue),
-    rgbData: computeVectorData([ rgb.r, rgb.g, rgb.b ], rgb2hex, rgb2hue),
-  };
-};
-
-// State
-const state = {
-  metric: null,
-  clusterChoice: null,
-  includeScale: null,
-  includeX: null,
-  normQY: null,
-  closeCoeff: null,
-  numPoke: null,
-  searchTerm: null,
-  searchSpace: null,
-  targetColor: null,
-  searchResults: null,
-  clusterToggles: {},
-  currentScores: {},
-};
-
-// Metrics
-const getBestKMean = (stats, q) => argMin(stats.kMeans.map(z => vectorDist(z.vector, q.vector)));
-const getWorstKMean = (stats, q) => argMax(stats.kMeans.map(z => vectorDist(z.vector, q.vector)));
-
-const getScale = weight => state.includeScale ? (1 / weight) : 1;
-
-const summarySelectors = [
-  // true mean
-  stats => [stats.trueMean, 1],
-  // largest cluster
-  stats => [stats.kMeans[stats.largestCluster], getScale(stats.kWeights[stats.largestCluster])],
-  // smallest cluster
-  stats => [stats.kMeans[stats.smallestCluster], getScale(stats.kWeights[stats.smallestCluster])],
-  // best fit cluster
-  (stats, q) => {
-    const best = getBestKMean(stats, q);
-    return [stats.kMeans[best], getScale(stats.kWeights[best])];
-  },
-  // worst fit cluster
-  (stats, q) => {
-    const worst = getWorstKMean(stats, q);
-    return [stats.kMeans[worst], getScale(stats.kWeights[worst])];
-  },
-];
-
-const selectedSummary = (stats, q) => summarySelectors[state.clusterChoice](stats, q);
-
-// TODO unfortunately the addition of the scaling factor means we have to compute *real*
-// metrics instead of cheaper approximations. would be nice to fix this
-const metrics = [
-  // RMS
-  (stats, q) => {
-    const [ mean, scale ] = selectedSummary(stats, q);
-    return (stats.inertia - 2 * vectorDot(mean.vector, q.vector)) * scale;
-  },
-  // mean angle
-  (stats, q) => {
-    const [ mean, scale ] = selectedSummary(stats, q);
-    return rad2deg * Math.acos(vectorDot(mean.unit, q.unit)) * scale;
-  },
-  // mean dist
-  (stats, q) => {
-    const [ mean, scale ] = selectedSummary(stats, q);
-    // TODO I know there's some way to avoid recalculation here but I'm just too lazy right now
-    return vectorDist(mean.vector, q.vector) * scale;
-  },
-  // hue angle
-  (stats, q) => {
-    const [ mean, scale ] = selectedSummary(stats, q);
-    return angleDiff(mean.hue, q.hue) * scale;
-  },
-  // max inertia
-  (stats, q) => {
-    const [ , scale ] = selectedSummary(stats, q);
-    return -stats.inertia * scale;
-  },
-  // chebyshev
-  (stats, q) => {
-    const [ mean, scale ] = selectedSummary(stats, q);
-    return Math.max(...mean.vector.map((x, i) => Math.abs(x - q.vector[i]))) * scale;
-  },
-  // custom
-  (stats, q) => {
-    const [ mean, scale ] = selectedSummary(stats, q);
-    return (
-      (state.includeX ? stats.inertia : 0)
-      -
-      state.closeCoeff * vectorDot(
-        mean[state.normQY ? "unit" : "vector"],
-        state.normQY ? q.unit : q.vector,
-      )
-    ) * scale;
-  },
-];
-
-const scorePokemon = pkmn => ({
-  jab: metrics[state.metric](pkmn.jabStats, state.targetColor.jabData),
-  rgb: metrics[state.metric](pkmn.rgbStats, state.targetColor.rgbData),
-});
-
-const calcDisplayMetrics = (meanData, q) => ({
-  theta: rad2deg * Math.acos(vectorDot(q.unit, meanData.unit)),
-  delta: vectorDist(q.vector, meanData.vector),
-  phi: angleDiff(q.hue, meanData.hue),
-});
-
-// Math Rendering
-const renderQVec = (q, node, sub) => {
-  node.innerHTML = TeXZilla.toMathMLString(String.raw`\vec{q}_{\text{${sub}}} = \left(\text{${q.join(", ")}}\right)`);
-};
-
-const mathArgBest = (mxn, arg) => `\\underset{${arg}}{\\arg\\${mxn}}`;
-
-const mathDefinitions = {
-  "main-definition": String.raw`
-    \begin{aligned}
-      \vec{\mu}\left(P\right) &= \frac{1}{\left|P\right|}\sum_{p\in P}{\vec{p}} \\
-      I\left(P\right) &= \frac{1}{\left|P\right|}\sum_{p\in P}{\left|\left|\vec{p}\right|\right|^2} \\
-      \delta\left(P\right) &= \left|\left| \vec{q} - \vec{\mu}\left(P\right) \right|\right| \\
-      \theta\left(P\right) &= \angle \left(\vec{q}, \vec{\mu}\left(P\right)\right) \\
-      \vec{x}_{\perp} &= \text{oproj}_{\left\{\vec{J}, \vec{L}\right\}}{\vec{x}} \\
-      \phi\left(P\right) &= \angle \left(\vec{q}_{\perp}, \vec{\mu}\left(P\right)_{\perp} \right) \\
-      \sigma\left(P\right) &= \sqrt{E\left[\left(\vec{q} - P\right)^2\right]} \\
-        &= \sqrt{\frac{1}{|P|}\sum_{p \in P}{\left|\left|\vec{p} - \vec{q}\right|\right|^2}}
-    \end{aligned}
-  `,
-  "cluster-definition": String.raw`
-    \begin{aligned}
-      \left\{P_1, P_2, P_3\right\} &= ${mathArgBest("max", String.raw`\left\{P_1, P_2, P_3\right\}`)} \sum_{i=1}^3 \sum_{p\inP_i} \left|\left| \vec{p} - \vec{\mu}\left(P_i\right) \right|\right|^2 \\
-      \pi_i &= \frac{\left|P_i\right|}{\left|P\right|} \\
-      M\left(P\right) &= ${mathArgBest("max", "P_i")} \left( \left|P_i\right| \right) \\
-      m\left(P\right) &= ${mathArgBest("min", "P_i")} \left( \left|P_i\right| \right) \\
-      \alpha\left(P\right) &= ${mathArgBest("min", "P_i")} \left( \left|\left| \vec{q} - \vec{\mu}\left(P_i\right) \right|\right| \right) \\
-      \omega\left(P\right) &= ${mathArgBest("max", "P_i")} \left( \left|\left| \vec{q} - \vec{\mu}\left(P_i\right) \right|\right| \right)
-    \end{aligned}
-  `,
-};
-
-const includeScaleFactor = muArg => state.clusterChoice > 0 && state.includeScale ? String.raw`\frac{\left|P\right|}{\left|${muArg}\right|}` : ""
-
-const metricText = [
-  muArg => String.raw`
-    ${mathArgBest("min", "P")}\left[
-      ${includeScaleFactor(muArg)}I\left(P\right)
-      - 2\vec{q}\cdot ${includeScaleFactor(muArg)}\vec{\mu}\left(${muArg}\right)
-    \right]`,
-  muArg => String.raw`${mathArgBest("min", "P")}\left[${includeScaleFactor(muArg)}\angle \left(\vec{q}, \vec{\mu}\left(${muArg}\right)\right)\right]`,
-  muArg => String.raw`${mathArgBest("min", "P")}\left[${includeScaleFactor(muArg)}\left|\left|\vec{q} - \vec{\mu}\left(${muArg}\right) \right|\right|\right]`,
-  muArg => String.raw`${mathArgBest("min", "P")}\left[${includeScaleFactor(muArg)}\angle \left(\vec{q}_{\perp}, \vec{\mu}\left(${muArg}\right)_{\perp}\right)\right]`,
-  muArg => String.raw`${mathArgBest("min", "P")}\left[-${includeScaleFactor(muArg)}I\left(P\right)\right]`,
-  muArg => String.raw`${mathArgBest("min", "P")}\left[${includeScaleFactor(muArg)} \max_{i} \left|\vec{\mu}\left(${muArg}\right)_i - \vec{q}_i \right|\right]`,
-].map(s => muArg => TeXZilla.toMathML(s(muArg)));
-
-const muArgs = [
-  "P",
-  String.raw`M\left(P\right)`,
-  String.raw`m\left(P\right)`,
-  String.raw`\alpha\left(P\right)`,
-  String.raw`\omega\left(P\right)`,
-];
-
-const renderVec = math => String.raw`\vec{${math.charAt(0)}}${math.substr(1)}`;
-const renderNorm = vec => String.raw`\frac{${vec}}{\left|\left|${vec}\right|\right|}`;
-const updateObjective = () => {
-  const muArg = muArgs[state.clusterChoice];
-  let tex = metricText?.[state.metric]?.(muArg);
-  if (!tex) {
-    const { includeX, normQY, closeCoeff } = state;
-    if (!includeX && closeCoeff === 0) {
-      tex = TeXZilla.toMathML(String.raw`\text{Malamar-ness}`);
-    } else {
-      const qyMod = normQY ? renderNorm : c => c;
-      tex = TeXZilla.toMathML(String.raw`
-        ${mathArgBest("min", "P")}
-        \left[
-          ${includeX ? String.raw`${includeScaleFactor(muArg)}I\left(P\right)` : ""}
-          ${closeCoeff === 0 ? "" : String.raw`
-              -
-              ${closeCoeff}
-              ${qyMod("\\vec{q}")}
-              \cdot
-              ${includeScaleFactor(muArg)}${qyMod(String.raw`\vec{\mu}\left(${muArg}\right)`)}
-          `}
-        \right]
-      `);
-    }
-  }
-  const objFnNode = getObjFnDisplay();
-  clearNodeContents(objFnNode);
-  objFnNode.appendChild(tex);
-};
-
-// Pokemon Rendering
-const stripForm = ["flabebe", "floette", "florges", "vivillon", "basculin", "furfrou", "magearna"];
-
-const getSprite = pokemon => {
-  pokemon = pokemon
-    .replace("-alola", "-alolan")
-    .replace("-galar", "-galarian")
-    .replace("darmanitan-galarian", "darmanitan-galarian-standard");
-  if (stripForm.find(s => pokemon.includes(s))) {
-    pokemon = pokemon.replace(/-.*$/, "");
-  }
-  return `https://img.pokemondb.net/sprites/sword-shield/icon/${pokemon}.png`;
-};
-
-const renderCluster = ({
-  index, big, small, best, worst, pi, theta, delta, phi, hex, vector,
-}) => `
-  <div
-    class="pkmn_tile-cluster"
-    style="grid-area: k${index + 1}; color: ${getContrastingTextColor(hex2rgb(hex))}; background-color: ${hex};"
-  >
-    <div class="pkmn_tile-cluster-top_label" style="grid-area: lbl;">
-      ${index === big ? "<span>M</span>" : ""}
-      ${index === small ? "<span>m</span>" : ""}
-      ${index === best ? "<span>α</span>" : ""}
-      ${index === worst ? "<span>ω</span>" : ""}
-    </div>
-    <div class="pkmn_tile-cluster-stat_label" style="grid-area: mu;">μ =</div>
-    <div class="pkmn_tile-cluster-stat_label" style="grid-area: pi;">π =</div>
-    <div class="pkmn_tile-cluster-stat_label" style="grid-area: th;">θ =</div>
-    <div class="pkmn_tile-cluster-stat_label" style="grid-area: dl;">δ =</div>
-    <div class="pkmn_tile-cluster-stat_label" style="grid-area: ph;">ϕ =</div>
-    <div style="grid-area: mux">${hex}</div>
-    <div style="grid-area: muv; justify-self: center;">(${vector.map(c => c.toFixed(2)).join(", ")})</div>
-    <div style="grid-area: piv">${(pi * 100).toFixed(1)}%</div>
-    <div style="grid-area: thv">${theta.toFixed(2)}°</div>
-    <div style="grid-area: dlv">${delta.toFixed(2)}</div>
-    <div style="grid-area: phv">${phi.toFixed(2)}°</div>
-  </div>
-`;
-
-const getPokemonRenderer = targetList =>  (name, stats, q, score, idPostfix) => {
-  let sigma, metrics, kMeanInfo, kMeanResults;
-  if (q) {
-    sigma = Math.sqrt(stats.inertia - 2 * vectorDot(stats.trueMean.vector, q.vector) + q.magSq)
-    metrics = calcDisplayMetrics(stats.trueMean, q)
-    kMeanInfo = {
-      big: stats.largestCluster,
-      small: stats.smallestCluster,
-      best: getBestKMean(stats, q),
-      worst: getWorstKMean(stats, q), // TODO yeah yeah this is a recalc whatever
-    };
-    kMeanResults = stats.kMeans.map(k => calcDisplayMetrics(k, q));
-  } else {
-    // no target color, just do all zeros
-    sigma = 0;
-    metrics = { theta: 0, delta: 0, phi: 0 };
-    kMeanInfo = { big: 0, small: 0, best: 0, worst: 0 };
-    kMeanResults = [ metrics, metrics, metrics ];
-  }
-  const clusterToggleId = `reveal_clusters-${name}-${idPostfix}`;
-
-  const li = document.createElement("li");
-  li.innerHTML = `
-    <div class="pkmn_tile">
-      <img class="pkmn_tile-img" src="${getSprite(name)}" />
-      <span class="pkmn_tile-name">
-        ${name.split("-").map(part => part.charAt(0).toUpperCase() + part.substr(1)).join(" ")}
-      </span>
-      <div class="pkmn_tile-fn">
-        ${score?.toFixed(3) ?? ""}
-      </div>
-      <input
-        type="checkbox"
-        ${state.clusterToggles?.[clusterToggleId] ? "checked" : ""}
-        id="${clusterToggleId}"
-        onchange="state.clusterToggles['${clusterToggleId}'] = event.target.checked"
-        class="pkmn_tile-reveal_clusters"
-        role="button"
-      >
-      <label class="pkmn_tile-reveal_clusters_label" for="${clusterToggleId}">
-        <div class="pkmn_tile-reveal_clusters_label--closed">►</div>
-        <div class="pkmn_tile-reveal_clusters_label--open">▼</div>
-      </label>
-      <div
-        class="pkmn_tile-true_mean"
-        style="color: ${getContrastingTextColor(hex2rgb(stats.trueMean.hex))}; background-color: ${stats.trueMean.hex};"
-      >
-          <div class="pkmn_tile-true_mean-value">
-              <div class="pkmn_tile-true_mean-mu_label">μ =</div>
-              <div class="pkmn_tile-true_mean-mu_hex">${stats.trueMean.hex}</div>
-              <div class="pkmn_tile-true_mean-mu_vec">
-                (${stats.trueMean.vector.map(c => c.toFixed(2)).join(", ")})
-              </div>
-          </div>
-          <div class="pkmn_tile-true_mean-stat pkmn_tile-true_mean-inertia">
-            𝖨 = ${stats.inertia.toFixed(2)}
-          </div>
-          <div class="pkmn_tile-true_mean-stat pkmn_tile-true_mean-stat-sigma">
-            σ = ${sigma.toFixed(2)}
-          </div>
-          <div class="pkmn_tile-true_mean-stat pkmn_tile-true_mean-stat-theta">
-            θ = ${metrics.theta.toFixed(2)}°
-          </div>
-          <div class="pkmn_tile-true_mean-stat pkmn_tile-true_mean-stat-delta">
-            δ = ${metrics.delta.toFixed(2)}
-          </div>
-          <div class="pkmn_tile-true_mean-stat pkmn_tile-true_mean-stat-phi">
-            ϕ = ${metrics.phi.toFixed(2)}°
-          </div>
-      </div>
-      ${stats.kMeans.map((data, index) => renderCluster({
-          index,
-          ...kMeanInfo,
-          pi: stats.kWeights[index],
-          ...kMeanResults[index],
-          hex: data.hex,
-          vector: data.vector,
-      })).join("\n")}
-    </div>
-  `;
-  targetList.appendChild(li);
-};
-
-// Update Search Results
-const renderSearch = () => {
-  const resultsNode = getSearchListNode();
-  const append = getPokemonRenderer(resultsNode);
-  clearNodeContents(resultsNode);
-  const argMapper = state.searchSpace === "RGB"
-    ? pkmn => [pkmn.rgbStats, state.targetColor?.rgbData, state.currentScores?.[pkmn.name]?.rgb ?? null]
-    : pkmn => [pkmn.jabStats, state.targetColor?.jabData, state.currentScores?.[pkmn.name]?.jab ?? null]
-  state.searchResults?.forEach(pkmn => append(
-    pkmn.name, ...argMapper(pkmn), "search"
-  ));
-};
-
-// Scoring
-const renderScored = () => {
-  const jabList = getScoreListJABNode();
-  const appendJAB = getPokemonRenderer(jabList);
-  const rgbList = getScoreListRGBNode();
-  const appendRGB = getPokemonRenderer(rgbList);
-
-  const clonedData = pokemonColorData.slice();
-
-  // extract best CIECAM02 results
-  const bestJAB = clonedData
-    .sort((a, b) => state.currentScores[a.name].jab - state.currentScores[b.name].jab)
-    .slice(0, state.numPoke);
-  clearNodeContents(jabList);
-  bestJAB.forEach(data => appendJAB(
-    data.name, data.jabStats, state.targetColor.jabData, state.currentScores[data.name].jab, "jab"
-  ));
-
-  // extract best RGB results
-  const bestRGB = clonedData
-    .sort((a, b) => state.currentScores[a.name].rgb - state.currentScores[b.name].rgb)
-    .slice(0, state.numPoke);
-  clearNodeContents(rgbList);
-  bestRGB.forEach(data => appendRGB(
-    data.name, data.rgbStats, state.targetColor.rgbData, state.currentScores[data.name].rgb, "rgb"
-  ));
-};
-
-const rescore = () => {
-  if (!state.targetColor) {
-    return;
-  }
-
-  state.currentScores = {};
-  pokemonColorData.forEach(data => {
-    state.currentScores[data.name] = scorePokemon(data);
-  });
-
-  // update displays
-  renderScored();
-
-  // update the rendered search results as well
-  renderSearch();
-};
-
-// Listeners
-const onColorChanged = skipScore => {
-  const readColor = readColorInput();
-  if (readColor) {
-    state.targetColor = readColor;
-
-    renderQVec(state.targetColor.jabData.vector.map(c => c.toFixed(3)), getQJABDisplay(), "Jab");
-    renderQVec(state.targetColor.rgbData.vector.map(c => c.toFixed()), getQRGBDisplay(), "RGB");
-
-    const rootElem = document.querySelector(":root");
-    rootElem.style.setProperty("--background", state.targetColor.rgbData.hex);
-    rootElem.style.setProperty("--highlight", getContrastingTextColor(state.targetColor.rgbData.vector));
-
-    if (!skipScore) {
-      rescore();
-    }
-  }
-};
-
-const onRandomColor = () => {
-  const color = [Math.random(), Math.random(), Math.random()].map(c => c * 255);
-  getColorInputNode().value = d3.rgb(...color).formatHex();
-  onColorChanged(); // triggers rescore
-};
-
-const onCustomControlsChanged = skipScore => {
-  state.includeX = getIncludeXToggleNode()?.checked ?? false;
-  state.normQY = getNormQYToggleNode()?.checked ?? false;
-  state.closeCoeff = parseFloat(getCloseCoeffSliderNode()?.value ?? 2);
-
-  getCloseCoeffDisplayNode().innerHTML = state.closeCoeff;
-  updateObjective();
-
-  if (!skipScore) {
-    rescore();
-  }
-}
-
-const checkClusterMeanWarning = () => {
-  const warning = getClusterMeanWarning();
-  const unhidden = warning.getAttribute("class").replaceAll("hide", "");
-  if (state.clusterChoice !== 0 && (state.metric === 0 || state.metric === 4)) {
-    warning.setAttribute("class", unhidden);
-  } else {
-    warning.setAttribute("class", unhidden + " hide");
-  }
-}
-
-const checkScaleByClusterToggle = () => {
-  const toggle = getClusterScaleToggleNode()?.parentNode;
-  const unhidden = toggle.getAttribute("class").replaceAll("hide", "");
-  if (state.clusterChoice !== 0) {
-    toggle.setAttribute("class", unhidden);
-  } else {
-    toggle.setAttribute("class", unhidden + " hide");
-  }
-}
-
-const onScaleByClusterChanged = skipScore => {
-  state.includeScale = getClusterScaleToggleNode()?.checked ?? true;
-
-  updateObjective();
-
-  if (!skipScore) {
-    rescore();
-  }
-}
-
-const onClusterChoiceChanged = skipScore => {
-  const clusterChoice = getClusterChoiceDropdownNode()?.selectedIndex ?? 0;
-  if (clusterChoice === state.clusterChoice) {
-    return;
-  }
-  state.clusterChoice = clusterChoice;
-  checkClusterMeanWarning();
-  checkScaleByClusterToggle();
-  updateObjective();
-  if (!skipScore) {
-    rescore();
-  }
-}
-
-const onMetricChanged = skipScore => {
-  const metric = getMetricDropdownNode()?.selectedIndex ?? 0;
-  if (metric === state.metric) {
-    return;
-  }
-  state.metric = metric;
-  checkClusterMeanWarning();
-  checkScaleByClusterToggle();
-  if (state.metric === 6) { // Custom
-    showCustomControls();
-    onCustomControlsChanged(skipScore); // triggers rescore
-  } else {
-    hideCustomControls();
-    updateObjective();
-    if (!skipScore) {
-      rescore();
-    }
-  }
-};
-
-const onLimitChanged = skipRenderScore => {
-  state.numPoke = parseInt(getLimitSliderNode()?.value ?? 10);
-
-  getLimitDisplayNode().textContent = state.numPoke;
-
-  if (!skipRenderScore) {
-    renderScored();
-  }
-};
-
-const onSearchChanged = () => {
-  state.searchTerm = getNameInputNode()?.value?.toLowerCase() ?? "";
-  if (state.searchTerm.length === 0) {
-    state.searchResults = [];
-  } else {
-    state.searchResults = pokemonLookup
-      .search(state.searchTerm, { limit: 10 })
-      .map(({ item }) => item);
-  }
-  renderSearch();
-};
-
-const onSearchSpaceChanged = () => {
-  const old = state.searchSpace ?? "Jab";
-  state.searchSpace = old === "RGB" ? "Jab" : "RGB";
-  getSearchSpaceDisplayNode().textContent = old;
-  renderSearch();
-};
-
-const onRandomPokemon = () => {
-  getNameInputNode().value = "";
-  state.searchResults = Array.from({ length: 10 }, () => pokemonColorData[Math.floor(Math.random() * pokemonColorData.length)]);
-  renderSearch();
-};
-
-const onPageLoad = () => {
-  // render static explanations
-  Object.entries(mathDefinitions).forEach(([id, tex]) => {
-    document.getElementById(id)?.appendChild(TeXZilla.toMathML(tex));
-  });
-
-  // fake some events but don't do any scoring
-  onColorChanged(true);
-  onMetricChanged(true);
-  onClusterChoiceChanged(true);
-  onScaleByClusterChanged(true);
-  onLimitChanged(true);
-  // then do a rescore directly, which will do nothing unless old data was loaded
-  rescore();
-  // finally render search in case rescore didn't
-  onSearchChanged();
-};