浏览代码

Redesign tiles to render all information

Kirk Trombley 3 年之前
父节点
当前提交
5d53caabd9
共有 3 个文件被更改,包括 267 次插入150 次删除
  1. 131 57
      nearest.css
  2. 5 2
      nearest.html
  3. 131 91
      nearest.js

+ 131 - 57
nearest.css

@@ -178,91 +178,165 @@ label {
     min-width: 500px;
 }
 
+#search-space-button {
+    width: 3em;
+}
+
 /* Pokemon Tile */
 
-.pokemon_tile {
-    padding-top: 0.5em;
-    padding-bottom: 0.5em;
+.pkmn_tile {
+    width: 480px;
+    display: grid;
+    gap: 2px 2px;
+    margin-top: 4px;
+    margin-bottom: 4px;
+    grid:
+        "img  name name fn" 1.2em
+        "img  mu   mu   mu" 1fr
+        "tog  mu   mu   mu" 1fr
+        ".    k1   k2   k3" auto
+        /60px 1fr  1fr 1fr
+    ;
+}
+
+.pkmn_tile-true_mean {
+    grid-area: mu;
+    font-size: 12px;
+    display: grid;
+    grid:
+        ".     mu  mus mud"
+        ".     in  mut mup"
+        /0.1fr 3fr 1fr 1fr
+    ;
+    gap: 2px 2px;
+    align-items: center;
+}
 
-    width: 100%;
-    max-width: 500px;
+.pkmn_tile-true_mean-value {
+    grid-area: mu;
+    display: grid;
+    grid:
+        "mul mux muv"
+        /2em 6em auto
+    ;
+    align-items: center;
+}
 
-    display: flex;
-    flex-flow: row nowrap;
-    justify-content: stretch;
-    align-items: flex-start;
+.pkmn_tile-true_mean-inertia {
+    grid-area: in;
 }
 
-.pokemon_tile--smaller {
-    max-width: 450px;
+.pkmn_tile-true_mean-mu_label {
+    grid-area: mul;
+    justify-self: end;
+    padding-right: 2px;
 }
 
-.pokemon_tile-image-wrapper {
-    width: 60px;
-    height: 50px;
-    display: flex;
-    justify-content: center;
-    align-items: center;
+.pkmn_tile-true_mean-mu_hex {
+    grid-area: mux;
 }
 
-.pokemon_tile-info_panel {
-    flex: 1;
+.pkmn_tile-true_mean-mu_vec {
+    grid-area: muv;
+}
 
-    display: flex;
-    flex-flow: column nowrap;
-    justify-content: center;
-    align-items: stretch;
+.pkmn_tile-true_mean-stat {
+    margin-left: 8px;
+}
+
+.pkmn_tile-true_mean-stat-sigma {
+    grid-area: mus;
+}
+
+.pkmn_tile-true_mean-stat-theta {
+    grid-area: mut;
+}
+
+.pkmn_tile-true_mean-stat-delta {
+    grid-area: mud;
+}
+
+.pkmn_tile-true_mean-stat-phi {
+    grid-area: mup;
 }
 
-.pokemon_tile-pokemon_name {
+.pkmn_tile-img {
+    grid-area: img;
+}
+
+.pkmn_tile-name {
+    grid-area: name;
     font-weight: 1000;
 }
 
-.pokemon_tile-results {
-    display: flex;
-    flex-flow: row nowrap;
-    justify-content: flex-start;
-    align-items: stretch;
+.pkmn_tile-fn {
+    grid-area: fn;
+    justify-self: end;
 }
 
-.pokemon_tile-labels {
-    margin-left: 8px;
+.pkmn_tile-cluster {
+    display: none;
 
-    display: flex;
-    flex-flow: column nowrap;
-    justify-content: flex-start;
-    align-items: flex-end;
+    font-size: 12px;
+    grid:
+        "bigm litm .   alp omg" 1em
+        "mu   mux  mux mux mux"
+        "muv  muv  muv muv muv"
+        "pi   piv  .   th  thv"
+        "dl   dlv  .   ph  phv"
+        /2fr  4fr  1fr 2fr 4fr
+    ;
+    gap: 5px 0px;
+    padding-top: 4px;
+    padding-bottom: 8px;
 }
 
-.pokemon_tile-score_column {
-    margin-left: 8px;
-    flex: 1;
+.pkmn_tile-cluster-top_label {
+    font-weight: 1000;
+    justify-self: center;
+}
 
-    display: flex;
-    flex-flow: column nowrap;
-    justify-content: flex-start;
-    align-items: flex-start;
+.pkmn_tile-cluster-stat_label {
+    justify-self: end;
+    padding-right: 2px;
 }
 
-.pokemon_tile-hex_column {
-    min-width: 140px;
-    
-    margin-left: 8px;
-    display: flex;
-    flex-flow: column nowrap;
+/* Cluster Hiding Logic */
+
+.pkmn_tile-reveal_clusters {
+    display: none;
 }
 
-.pokemon_tile-hex_color {
-    flex: 1;
+.pkmn_tile-reveal_clusters:checked ~ .pkmn_tile-cluster {
+    display: grid;
+}
 
-    padding: 0 0.5em 0 0.5em;
-    font-size: 10px;
+.pkmn_tile-reveal_clusters:checked ~ .pkmn_tile-reveal_clusters_label > .pkmn_tile-reveal_clusters_label--closed {
+    display: none;
+}
 
-    display: inline-flex;
-    justify-content: space-between;
-    align-items: center;
+.pkmn_tile-reveal_clusters_label--closed {
+    display: block;
 }
 
-.pokemon_tile-vector {
-    margin-left: 8px;
+.pkmn_tile-reveal_clusters:checked ~ .pkmn_tile-reveal_clusters_label > .pkmn_tile-reveal_clusters_label--open {
+    display: block;
+}
+
+.pkmn_tile-reveal_clusters_label--open {
+    display: none;
+}
+
+.pkmn_tile-reveal_clusters_label {
+    font-size: 16px;
+    text-align: center;
+    grid-area: tog;
+}
+
+.pkmn_tile-reveal_clusters--hide {
+    display: none;
+}
+
+.pkmn_tile-reveal_clusters--show {
+    display: block;
 }

+ 5 - 2
nearest.html

@@ -92,7 +92,7 @@
             <div id="definitions" class="panel math-section">
                 <div>Statistics</div>
                 <div class="container">
-                <div id="main-definition"></div>
+                    <div id="main-definition"></div>
                     <div id="angle-definition"></div>
                 </div>
                 <div id="rms-definition" class="container center-aligned center-justified"></div>
@@ -116,8 +116,11 @@
             <div class="panel bypkmn">
                 <form class="container control" onsubmit="event.preventDefault()">
                     <label for="pokemon-name">Search By Pokemon</label>
-                    <button class="padded" type="button" onclick="onRandomPokemon()">Random Pokemon</button>
+                    <button class="padded" type="button" onclick="onRandomPokemon()">Random</button>
                     <input id="pokemon-name" size="15" oninput="onSearchChanged()">
+                    <button id="search-space-button" type="button" onclick="onSearchSpaceChanged()">
+                        <span id="search-space-display">RGB</span>
+                    </button>
                 </form>
                 <ul id="search-list" class="pkmn-list"></ul>
             </div>

+ 131 - 91
nearest.js

@@ -13,6 +13,7 @@ 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");
@@ -42,6 +43,10 @@ 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];
@@ -109,6 +114,7 @@ const state = {
   closeCoeff: null,
   numPoke: null,
   searchTerm: null,
+  searchSpace: null,
   targetColor: null,
   searchResults: null,
 };
@@ -144,7 +150,7 @@ const metrics = [
   // mean angle
   (stats, q) => -vectorDot(selectedSummary(stats, q)[0].unit, q.unit),
   // mean dist
-  (stats, q) => { 
+  (stats, q) => {
     // TODO I know there's some way to avoid recalculation here but I'm just too lazy right now
     const [data, scale] = selectedSummary(stats, q);
     return vectorSqDist(data.vector, q.vector) / (state.includeScaleInDist ? scale : 1);
@@ -153,7 +159,7 @@ const metrics = [
   (stats, q) => angleDiff(selectedSummary(stats, q)[0].hue, q.hue),
   // custom
   (stats, q) => (state.includeX ? stats.inertia : 0) - state.closeCoeff * vectorDot(
-    selectedSummary(stats, q)[0][state.normQY ? "unit" : "vector"], 
+    selectedSummary(stats, q)[0][state.normQY ? "unit" : "vector"],
     state.normQY ? q.unit : q.vector,
   ),
 ];
@@ -163,28 +169,11 @@ const scorePokemon = pkmn => ({
   rgb: metrics[state.metric](pkmn.rgbStats, state.targetColor.rgbData),
 });
 
-const calcDisplayMetrics = ({ jabStats, rgbStats }) => {
-  // TODO - case on metric and meanArgument to avoid recalculation
-  // TODO - is there ever any value to computing these around the selected summary instead?
-  // obviously that has no mathematical value, and screws up the sqrts, but maybe?
-
-  const cosAngleJAB = vectorDot(state.targetColor.jabData.unit, jabStats.trueMean.unit);
-  const yTermJAB = cosAngleJAB * jabStats.trueMean.magnitude * state.targetColor.jabData.magnitude;
-
-  const cosAngleRGB = vectorDot(state.targetColor.rgbData.unit, rgbStats.trueMean.unit);
-  const yTermRGB = cosAngleRGB * rgbStats.trueMean.magnitude * state.targetColor.rgbData.magnitude;
-
-  return {
-    stdDevJAB: Math.sqrt(jabStats.inertia - 2 * yTermJAB + state.targetColor.jabData.magSq),
-    stdDevRGB: Math.sqrt(rgbStats.inertia - 2 * yTermRGB + state.targetColor.rgbData.magSq),
-    angleJAB: rad2deg * Math.acos(cosAngleJAB),
-    angleRGB: rad2deg * Math.acos(cosAngleRGB),
-    meanDistJAB: vectorDist(state.targetColor.jabData.vector, jabStats.trueMean.vector),
-    meanDistRGB: vectorDist(state.targetColor.rgbData.vector, rgbStats.trueMean.vector),
-    hueAngleJAB: angleDiff(state.targetColor.jabData.hue, jabStats.trueMean.hue),
-    hueAngleRGB: angleDiff(state.targetColor.rgbData.hue, rgbStats.trueMean.hue),
-  };
-};
+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) => {
@@ -231,10 +220,10 @@ const metricText = [
 ].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)`, 
+  "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)`,
 ];
 
@@ -283,80 +272,120 @@ const getSprite = pokemon => {
   return `https://img.pokemondb.net/sprites/sword-shield/icon/${pokemon}.png`;
 };
 
-const renderPokemon = (data, classes = {}) => {
-  const { name, jabStats, rgbStats, scores } = data;
-  const { labelClass = "", rgbClass = "", jabClass = "", tileClass = "" } = classes;
-  let { resultsClass = "" } = classes;
-  let displayMetrics = {};
-  if (!state.targetColor) {
-    // no color selected need to skip scores
-    resultsClass = "hide";
+// TODO make the M m alpha omega labels more visible
+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: bigm;">${index === big ? "M" : ""}</div>
+    <div class="pkmn_tile-cluster-top_label" style="grid-area: litm;">${index === small ? "m" : ""}</div>
+    <div class="pkmn_tile-cluster-top_label" style="grid-area: alp;">${index === best ? "α" : ""}</div>
+    <div class="pkmn_tile-cluster-top_label " style="grid-area: omg;">${index === worst ? "ω" : ""}</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})</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, vectorDecimals, 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 {
-    displayMetrics = calcDisplayMetrics(data);
+    // 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 {
-    stdDevJAB = 0, stdDevRGB = 0,
-    angleJAB = 0, angleRGB = 0,
-    meanDistJAB = 0, meanDistRGB = 0,
-    hueAngleJAB = 0, hueAngleRGB = 0,
-  } = displayMetrics;
-
-  const titleName = name.split("-").map(part => part.charAt(0).toUpperCase() + part.substr(1)).join(" ");
-  const textHex = getContrastingTextColor(rgbStats.trueMean.vector);
-  const rgbVec = rgbStats.trueMean.vector.map(c => c.toFixed()).join(", ");
-  const jabVec = jabStats.trueMean.vector.map(c => c.toFixed(1)).join(", ");
-
-  // TODO Z dists, Z colors
-
-  const pkmn = document.createElement("div");
-  pkmn.setAttribute("class", `pokemon_tile ${tileClass}`);
-  pkmn.innerHTML = `
-    <div class="pokemon_tile-image-wrapper">
-      <img src="${getSprite(name)}" />
-    </div>
-    <div class="pokemon_tile-info_panel">
-      <span class="pokemon_tile-pokemon_name">
-        ${titleName} ${scores?.jab?.toFixed(2) ?? ""} ${scores?.rgb?.toFixed(2) ?? ""}
+  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="pokemon_tile-results">
-        <div class="pokemon_tile-labels ${labelClass}">
-          <span class="${jabClass}">Jab: </span>
-          <span class="${rgbClass}">RGB: </span>
-        </div>
-        <div class="pokemon_tile-score_column ${resultsClass}">
-          <span class="${jabClass}">
-            (${stdDevJAB.toFixed(2)}, ${angleJAB.toFixed(2)}&deg;, ${meanDistJAB.toFixed(2)}, ${hueAngleJAB.toFixed(2)}&deg;)
-          </span>
-          <span class="${rgbClass}">
-            (${stdDevRGB.toFixed(2)}, ${angleRGB.toFixed(2)}&deg;, ${meanDistRGB.toFixed(2)}, ${hueAngleRGB.toFixed(2)}&deg;)
-          </span>
-        </div>
-        <div class="pokemon_tile-hex_column">
-          <div class="pokemon_tile-hex_color ${jabClass}" style="background-color: ${jabStats.trueMean.hex}; color: ${textHex}">
-            <span>${jabStats.trueMean.hex}</span><span class="pokemon_tile-vector">(${jabVec})</span>
+      <div class="pkmn_tile-fn">
+        ${score.toFixed(3)}
+      </div>
+      <input type="checkbox" id="${clusterToggleId}" 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(vectorDecimals)).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="pokemon_tile-hex_color ${rgbClass}" style="background-color: ${rgbStats.trueMean.hex}; color: ${textHex}">
-            <span>${rgbStats.trueMean.hex}</span><span class="pokemon_tile-vector">(${rgbVec})</span>
+          <div class="pkmn_tile-true_mean-stat pkmn_tile-true_mean-stat-phi">
+            ϕ = ${metrics.phi.toFixed(2)}°
           </div>
-        </div>
       </div>
+      ${stats.kMeans.map((data, index) => renderCluster({
+          index,
+          ...kMeanInfo,
+          vectorDecimals,
+          pi: stats.kWeights[index],
+          ...kMeanResults[index],
+          hex: data.hex,
+          vector: data.vector.map(c => c.toFixed(vectorDecimals)).join(", "),
+      })).join("\n")}
     </div>
   `;
-  return pkmn;
-};
-
-const getPokemonAppender = targetList => (pokemonData, classes) => {
-  const li = document.createElement("li");
-  li.appendChild(renderPokemon(pokemonData, classes));
   targetList.appendChild(li);
 };
 
 // Update Search Results
 const renderSearch = () => {
   const resultsNode = getSearchListNode();
-  const append = getPokemonAppender(resultsNode);
+  const append = getPokemonRenderer(resultsNode);
   clearNodeContents(resultsNode);
-  state.searchResults?.forEach(pkmn => append(pkmn));
+  const argMapper = state.searchSpace === "RGB"
+    ? pkmn => [pkmn.rgbStats, state.targetColor?.rgbData, state.targetColor ? scorePokemon(pkmn).rgb : 0, 2]
+    : pkmn => [pkmn.jabStats, state.targetColor?.jabData, state.targetColor ? scorePokemon(pkmn).jab : 0, 2]
+  state.searchResults?.forEach(pkmn => append(
+    pkmn.name, ...argMapper(pkmn), "search"
+  ));
 };
 
 // Scoring
@@ -369,23 +398,27 @@ const rescore = () => {
   const scores = pokemonColorData.map(data => ({ ...data, scores: scorePokemon(data) }));
 
   const jabList = getScoreListJABNode();
-  const appendJAB = getPokemonAppender(jabList);
+  const appendJAB = getPokemonRenderer(jabList);
   const rgbList = getScoreListRGBNode();
-  const appendRGB = getPokemonAppender(rgbList);
+  const appendRGB = getPokemonRenderer(rgbList);
 
   // extract best CIECAM02 results
   const bestJAB = scores
     .sort((a, b) => a.scores.jab - b.scores.jab)
     .slice(0, state.numPoke);
   clearNodeContents(jabList);
-  bestJAB.forEach(data => appendJAB(data, { labelClass: "hide", rgbClass: "hide", tileClass: "pokemon_tile--smaller" }));
+  bestJAB.forEach(data => appendJAB(
+    data.name, data.jabStats, state.targetColor.jabData, data.scores.jab, 2, "jab"
+  ));
 
   // extract best RGB results
   const bestRGB = scores
     .sort((a, b) => a.scores.rgb - b.scores.rgb)
     .slice(0, state.numPoke);
   clearNodeContents(rgbList);
-  bestRGB.forEach(data => appendRGB(data, { labelClass: "hide", jabClass: "hide", tileClass: "pokemon_tile--smaller" }));
+  bestRGB.forEach(data => appendRGB(
+    data.name, data.rgbStats, state.targetColor.rgbData, data.scores.rgb, 2, "rgb"
+  ));
 
   // update the rendered search results as well
   renderSearch();
@@ -515,6 +548,13 @@ const onSearchChanged = () => {
   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)]);