123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726 |
- // ---- Math and Utilities ----
- // Vector Math
- const vectorDot = (u, v) => u.map((x, i) => x * v[i]).reduce((x, y) => x + y);
- const unitVector = v => {
- const mag = Math.hypot(...v);
- return v.map(c => c / mag);
- };
- // Angle Math
- const rad2deg = 180 / Math.PI;
- const twoPi = 2 * Math.PI;
- // Color Conversion
- const hex2rgb = hex => {
- hex = hex.replace("#", "");
- const red = hex.substr(0, 2);
- const grn = hex.substr(2, 2);
- const blu = hex.substr(4, 2);
- return [red, grn, blu].map(c => parseInt(c, 16));
- };
- // calculated from analyze.py
- RGB_TO_LMS = [
- [0.4121965, 0.53627432, 0.05143268],
- [0.2119195, 0.68071831, 0.10738379],
- [0.08834911, 0.28185414, 0.63018663],
- ];
- LMS_TO_OKLAB = [
- [0.2104542553, 0.793617785, -0.0040720468],
- [1.9779984951, -2.428592205, 0.4505937099],
- [0.0259040371, 0.7827717662, -0.808675766],
- ];
- const hex2oklab = hex => {
- const lrgb = hex2rgb(hex).map(c => {
- const v = c / 255;
- return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
- });
- const lms = RGB_TO_LMS.map(row => Math.cbrt(vectorDot(row, lrgb)));
- return LMS_TO_OKLAB.map(row => vectorDot(row, lms));
- };
- // Misc
- const clamp = (min, value, max) => Math.min(Math.max(value, min), max);
- const productLift =
- (...factors) =>
- (...args) =>
- factors
- .filter(fn => !!fn)
- .map(fn => fn(...args))
- .reduce((x, y) => x * y, 1);
- // Pre-computation
- const getColorData = hex => {
- const lab = hex2oklab(hex);
- return {
- hex,
- vector: lab,
- unit: unitVector(lab),
- chroma: Math.hypot(lab[1], lab[2]),
- hue: (Math.atan2(lab[2], lab[1]) + twoPi) % twoPi,
- };
- };
- const pokemonData = database.map(({ total, clusters, ...rest }) => ({
- total: {
- ...total,
- hex: total.medianHex,
- stddevTotal: Math.hypot(...total.stddev),
- stddevL: total.stddev[0],
- stddevA: total.stddev[1],
- stddevB: total.stddev[2],
- chromaMean: total.chroma[0],
- chromaDev: total.chroma[1],
- unitCentroid: unitVector(total.centroid),
- proportionDisplay: null,
- proportion: 1,
- inverseProportion: 1,
- },
- clusters: clusters
- .sort((a, b) => b.size - a.size)
- .map(c => ({
- ...c,
- hex: c.medianHex,
- stddevTotal: Math.hypot(...c.stddev),
- stddevL: c.stddev[0],
- stddevA: c.stddev[1],
- stddevB: c.stddev[2],
- chromaMean: c.chroma[0],
- chromaDev: c.chroma[1],
- unitCentroid: unitVector(c.centroid),
- proportion: c.size / total.size,
- inverseProportion: total.size / c.size,
- }))
- .map(c => ({
- ...c,
- proportionDisplay: (100 * c.proportion).toFixed(2),
- })),
- ...rest,
- }));
- const pokemonLookup = new Fuse(pokemonData, { keys: ["name"] });
- const pokemonDisplayLookup = Object.fromEntries(
- database.map(({ name, species }) => {
- const formattedName = name
- .split("-")
- .map(part => part.charAt(0).toUpperCase() + part.substr(1))
- .join(" ")
- .replace(/ M$/, " ♂")
- .replace(/ F$/, " ♀");
- let spriteName = name.toLowerCase();
- if (
- ["alcremie", "minior", "wobbuffet", "hippopotas", "hippowdon", "unfezant"].find(s =>
- spriteName.includes(s)
- )
- ) {
- spriteName = species.toLowerCase();
- } else if (spriteName === "basculin-white-striped") {
- // pdb has the wrong filenames lol
- spriteName = "1x/basculin-red-striped";
- } else if (!spriteName.includes("nidoran-")) {
- spriteName = spriteName
- .replace("-gmax", "-gigantamax")
- .replace("-alola", "-alolan")
- .replace("-galar", "-galarian")
- .replace("-hisui", "-hisuian")
- .replace("-paldea", "-paldean")
- .replace("-paldean-combat", "-paldean") // tauros
- .replace("-paldean-blaze", "-paldean-fire") // tauros
- .replace("-paldean-aqua", "-paldean-water") // tauros
- .replace("-pokeball", "-poke-ball") // vivillon
- .replaceAll("é", "e") // flabébé
- .replace(/darmanitan-galarian$/, "darmanitan-galarian-standard")
- .replace("urshifu-gigantamax", "urshifu-gigantamax-single-strike")
- .replace("urshifu-rapid-strike-gigantamax", "urshifu-gigantamax-rapid-strike")
- .replace("calyrex-shadow", "calyrex-shadow-rider")
- .replace("calyrex-ice", "calyrex-ice-rider")
- .replace("maushold-four", "maushold-family4")
- .replace("chienpao", "chien-pao")
- .replace("tinglu", "ting-lu")
- .replace("wochien", "wo-chien")
- .replace("chiyu", "chi-yu")
- .replace(/-m$/, "")
- .replace(/-f$/, "-female")
- .replaceAll(/['%]+/g, "")
- .replaceAll(/[:.\s]+/g, "-")
- .replace(/-+$/, "");
- }
- const linkName = species.replace(/[':.]/, "").replace(" ", "-");
- const handlerChain = [
- `https://img.pokemondb.net/sprites/sword-shield/normal/${spriteName}.png`,
- `https://img.pokemondb.net/sprites/scarlet-violet/icon/${spriteName}.png`,
- `https://img.pokemondb.net/sprites/sword-shield/icon/${spriteName}.png`,
- ].map((url, i) => ({ target }) => {
- target.removeEventListener("error", handlerChain[i]);
- if (handlerChain[i + 1]) {
- target.addEventListener("error", handlerChain[i + 1]);
- }
- pokemonDisplayLookup[name].sprite = target.src = url;
- });
- return [
- name,
- {
- formattedName,
- link: `https://pokemondb.net/pokedex/${linkName}`,
- sprite: `https://img.pokemondb.net/sprites/scarlet-violet/normal/${spriteName}.png`,
- handleSpriteError: handlerChain[0],
- },
- ];
- })
- );
- const calcScores = (data, target) => {
- const { centroid, unitCentroid, tilt, stddev, stddevTotal, chromaMean, chromaDev, hue } = data;
- const deltaVec = centroid.map((c, i) => c - target.vector[i]);
- const delta = Math.hypot(...deltaVec);
- const psiVec = stddev.map((s, i) => Math.hypot(s, deltaVec[i]));
- const psi = Math.hypot(...psiVec);
- const theta = Math.acos(Math.min(1, vectorDot(tilt, target.unit))) * rad2deg;
- const deltaC = chromaMean - target.chroma;
- const deltaH = Math.abs(hue - target.hue) * rad2deg;
- const likelihood = Math.exp(-Math.pow(delta / (2 * stddevTotal), 2)) / stddevTotal;
- const arcDiff = psi * theta;
- return {
- ...data,
- probArcDiff: arcDiff / likelihood,
- arcDiff,
- likelihood,
- psi,
- psiVec,
- psiL: psiVec[0],
- psiA: psiVec[1],
- psiB: psiVec[2],
- psiC: Math.hypot(chromaDev, deltaC),
- theta,
- delta,
- deltaVec,
- deltaL: Math.abs(deltaVec[0]),
- deltaA: Math.abs(deltaVec[1]),
- deltaB: Math.abs(deltaVec[2]),
- deltaC: Math.abs(deltaC),
- deltaH: Math.min(deltaH, 360 - deltaH),
- angDiff: Math.acos(vectorDot(unitCentroid, target.unit)) * rad2deg,
- hue: hue * rad2deg,
- };
- };
- const sortOrders = {
- max: (a, b) => b - a,
- min: (a, b) => a - b,
- };
- // who needs a framework?
- const makeTemplate = (id, definition = () => ({})) => {
- const content = document.getElementById(id).content;
- return (...args) => {
- const fragment = content.cloneNode(true);
- const binds = Object.fromEntries(
- Array.from(fragment.querySelectorAll("[bind-to]")).map(element => {
- const name = element.getAttribute("bind-to");
- element.removeAttribute("bind-to");
- return [name, element];
- })
- );
- const children = Array.from(content.children);
- const update = (...args) => {
- Object.entries(definition(...args))
- .map(([name, settings]) => [binds[name], settings])
- .filter(([bind]) => !!bind)
- .forEach(([bind, settings]) =>
- Object.entries(settings).forEach(([setting, value]) => {
- if (setting.startsWith("@")) {
- bind.addEventListener(setting.slice(1), value);
- } else if (setting.startsWith("--")) {
- bind.style.setProperty(setting, value);
- } else if (setting === "dataset") {
- Object.entries(value).forEach(([key, data]) => (bind.dataset[key] = data));
- } else if (setting === "append") {
- if (Array.isArray(value)) {
- bind.append(...value);
- } else {
- bind.append(value);
- }
- } else {
- bind[setting] = value;
- }
- })
- );
- };
- update(...args);
- return {
- mount: root =>
- (typeof root === "string" ? document.querySelector(root) : root).append(fragment),
- unmount: () => children.forEach(c => c.remove()),
- update,
- children,
- firstElementChild: fragment.firstElementChild,
- fragment,
- };
- };
- };
- // ---- Selectors ----
- const rootStyle = document.querySelector(":root").style;
- const colorSearchResultsTarget = document.getElementById("color-results");
- const nameSearchResultsTarget = document.getElementById("name-results");
- const prevColorsSidebar = document.getElementById("prevColors");
- const clusterMetricSection = document.getElementById("cls-metric-mount");
- const clusterFunctionSection = document.getElementById("cls-fn");
- const colorCalculateForm = document.forms.colorCalculateForm;
- const colorSortForm = document.forms.colorSortForm;
- const targetColorElements = document.forms.targetColorForm.elements;
- const colorDisplayElements = document.forms.colorDisplayForm.elements;
- const filterElements = document.forms.filterControl.elements;
- const nameSearchFormElements = document.forms.nameSearchForm.elements;
- // ---- Add Metric Selects ----
- const createMetricSelect = makeTemplate("metric-select-template");
- const { firstElementChild: sortMetricForm, mount: mountSortMetricForm } =
- createMetricSelect();
- sortMetricForm.elements.metricKind.value = "compare";
- mountSortMetricForm("#sort-metric-mount");
- const { firstElementChild: clusterMetricForm, mount: mountClusterMetricForm } =
- createMetricSelect();
- clusterMetricForm.elements.metricKind.value = "stat";
- mountClusterMetricForm("#cls-metric-mount");
- const updateMetricSelects = form => {
- const kind = form.elements.metricKind.value;
- form.elements.compare.disabled = kind !== "compare";
- form.elements.stat.disabled = kind !== "stat";
- form.elements.metric.value = form.elements[kind].value;
- };
- // bit of a hack, but lets us control this all from the template
- const metricSymbols = Object.fromEntries(
- Array.from(document.querySelectorAll("option")).map(el => [
- el.value,
- el.textContent.split("(")[1].split(")")[0],
- ])
- );
- const updateMetricDisplays = () => {
- updateMetricSelects(sortMetricForm);
- updateMetricSelects(clusterMetricForm);
- colorCalculateForm.elements.sortMetricSymbolP.value =
- colorCalculateForm.elements.sortMetricSymbolB.value =
- metricSymbols[
- sortMetricForm.elements[sortMetricForm.elements.metricKind.value].value
- ];
- colorCalculateForm.elements.clusterMetricSymbol.value =
- metricSymbols[
- clusterMetricForm.elements[clusterMetricForm.elements.metricKind.value].value
- ];
- };
- // ---- Styling ----
- const getColorStyles = hex => {
- const rgb = hex2rgb(hex);
- const lum = vectorDot(rgb, [0.3, 0.6, 0.1]);
- const highlight = lum >= 128 ? "var(--color-dark)" : "var(--color-light)";
- return {
- "--highlight": highlight,
- "--background": hex,
- "--shadow-component": lum >= 128 ? "0" : "255",
- };
- };
- const setColorStyles = (style, hex) =>
- Object.entries(getColorStyles(hex)).forEach(([prop, value]) =>
- style.setProperty(prop, value)
- );
- // ---- Pokemon Display ----
- // pulled out bc the render uses them
- const metricScores = {};
- const bestClusterIndices = {};
- const objectiveValues = {};
- const createPokemonTooltip = makeTemplate("pkmn-data-template", data =>
- Object.fromEntries(
- Object.entries(data).map(([metric, value]) => [
- metric,
- {
- innerText: Array.isArray(value)
- ? value.map(v => v.toFixed(2)).join(", ")
- : value?.toFixed?.(3)?.replace(".000", ""),
- },
- ])
- )
- );
- const createPokemonTile = makeTemplate(
- "pkmn-tile-template",
- ({ name, color }, enableTotalFlags, enableClusterFlags) => {
- const { formattedName, link, sprite, handleSpriteError } = pokemonDisplayLookup[name];
- const image = {
- alt: formattedName,
- src: sprite,
- "@error": handleSpriteError,
- };
- const score = {
- innerText: objectiveValues[name].toFixed(2),
- };
- const { total, clusters } = metricScores[name];
- const buttonBinds = [
- [clusters[0], "cls1Btn", "cls1Data", "infoHover1"],
- [clusters[1], "cls2Btn", "cls2Data", "infoHover2"],
- [clusters[2], "cls3Btn", "cls3Data", "infoHover3"],
- [clusters[3], "cls4Btn", "cls4Data", "infoHover4"],
- [total, "totalBtn", "totalData", "infoHover"],
- ]
- .filter(([data]) => !!data)
- .map(([data, button, tooltip, infoHover], index) => ({
- [button]: {
- dataset: {
- included: enableClusterFlags && index === bestClusterIndices[name],
- },
- hidden: false,
- innerText:
- data.hex + (data.proportionDisplay ? ` - ${data.proportionDisplay}%` : ""),
- "@click"() {
- model.setTargetColor(data.hex);
- },
- ...getColorStyles(data.hex),
- },
- [tooltip]: {
- append: createPokemonTooltip(data).fragment,
- },
- [infoHover]: {
- "@mouseenter"({ target }) {
- target.closest(".pkmn-tile").dataset.info = `show-${infoHover}`;
- },
- "@mouseleave"({ target }) {
- target.closest(".pkmn-tile").dataset.info = "hide";
- },
- },
- }))
- .reduce((a, b) => ({ ...a, ...b }), {});
- buttonBinds.totalBtn.dataset.included = enableTotalFlags;
- return {
- name: {
- innerText: formattedName,
- title: formattedName,
- },
- image,
- color: { innerText: color },
- link: { href: link },
- score,
- tileRoot: { dataset: { info: "hide" } },
- ...buttonBinds,
- };
- }
- );
- const renderPokemon = (list, target) => {
- target.innerText = "";
- const {
- sortUseWholeImage,
- sortUseBestCluster,
- sortUseClusterProportion,
- sortUseInvClusterProportion,
- } = Object.fromEntries(new FormData(colorCalculateForm).entries());
- const enableClusterFlags = !!(
- sortUseBestCluster ||
- sortUseClusterProportion ||
- sortUseInvClusterProportion
- );
- target.append(
- ...list.map(
- pkmn => createPokemonTile(pkmn, !!sortUseWholeImage, enableClusterFlags).fragment
- )
- );
- };
- // ---- Calculation Logic ----
- const model = {
- setTargetColor(newColor) {
- const hex = `#${newColor?.replace("#", "")}`;
- if (hex.length !== 7) {
- return;
- }
- setColorStyles(rootStyle, hex);
- const oldColor = this.targetColor;
- this.targetColor = hex;
- targetColorElements.colorText.value = hex;
- targetColorElements.colorText.dataset.lastValid = hex;
- targetColorElements.colorPicker.value = hex;
- if (oldColor) {
- const prevButton = document.createElement("button");
- prevButton.innerText = oldColor;
- prevButton.classList = "color-select";
- setColorStyles(prevButton.style, oldColor);
- prevButton.addEventListener("click", () => this.setTargetColor(oldColor));
- prevColorsSidebar.prepend(prevButton);
- }
- const targetData = getColorData(hex);
- targetColorElements.info.value = `
- (${(targetData.vector[0] * 100).toFixed(2)}%,
- ${targetData.chroma.toFixed(4)},
- ${(targetData.hue * rad2deg).toFixed(1)}°)
- `;
- pokemonData.forEach(({ name, total, clusters }) => {
- metricScores[name] = {
- total: calcScores(total, targetData),
- clusters: clusters.map(c => calcScores(c, targetData)),
- };
- });
- this.calculateObjective();
- },
- calculateObjective() {
- const {
- clusterUseClusterSize,
- clusterUseInvClusterSize,
- clusterSortOrder,
- sortUseWholeImage,
- sortUseBestCluster,
- sortUseClusterProportion,
- sortUseInvClusterProportion,
- } = Object.fromEntries(new FormData(colorCalculateForm).entries());
- const clsMetric = clusterMetricForm.elements.metric.value;
- const getClusterScore = productLift(
- cluster => cluster[clsMetric],
- clusterUseClusterSize && (cluster => cluster.size),
- clusterUseInvClusterSize && (cluster => cluster.inverseSize)
- );
- const clsSort = sortOrders[clusterSortOrder];
- const getBestClusterIndex = ({ total, clusters }) =>
- clusters
- .map((c, i) => [getClusterScore(c, total), i])
- .reduce((a, b) => (clsSort(a[0], b[0]) > 0 ? b : a))[1];
- Object.entries(metricScores).forEach(([name, scores]) => {
- bestClusterIndices[name] = getBestClusterIndex(scores);
- });
- const metric = sortMetricForm.elements.metric.value;
- const getSortScoreClusterFactors = productLift(
- sortUseBestCluster && (({ clusters }, i) => clusters[i][metric]),
- sortUseClusterProportion && (({ clusters }, i) => clusters[i].proportion),
- sortUseInvClusterProportion && (({ clusters }, i) => clusters[i].inverseProportion)
- );
- const usingCluster =
- sortUseBestCluster || sortUseClusterProportion || sortUseInvClusterProportion;
- const getSortScore = (scores, i) =>
- (sortUseWholeImage ? scores.total[metric] : 0) +
- (usingCluster ? getSortScoreClusterFactors(scores, i) : 0);
- Object.entries(metricScores).forEach(([name, scores]) => {
- objectiveValues[name] = getSortScore(scores, bestClusterIndices[name]);
- });
- this.renderNameSearchResults();
- this.rank();
- },
- rank() {
- const { sortOrder } = Object.fromEntries(new FormData(colorSortForm).entries());
- const compare = sortOrders[sortOrder];
- const sortFn = (a, b) => compare(objectiveValues[a], objectiveValues[b]);
- this.ranked = pokemonData
- .slice(0)
- .sort((a, b) => sortFn(a.name, b.name) || a.name.localeCompare(b.name));
- this.renderColorSearchResults();
- },
- setNameSearchResults(newNameResults) {
- this.nameSearchResults = newNameResults;
- this.renderNameSearchResults();
- },
- renderNameSearchResults() {
- renderPokemon(this.nameSearchResults ?? [], nameSearchResultsTarget);
- },
- resultsToDisplay: 6,
- setResultsToDisplay(count) {
- colorDisplayElements.output.value = this.resultsToDisplay = clamp(0, count, 100);
- this.renderColorSearchResults();
- },
- renderColorSearchResults() {
- const dexNums = new Set();
- const toRender = [];
- for (const pkmn of this.ranked) {
- if (toRender.length >= this.resultsToDisplay) {
- break;
- }
- if (filterElements.hideNoStart.checked && pkmn.traits.includes("nostart")) {
- continue;
- }
- if (!filterElements.allowNFE.checked && pkmn.traits.includes("nfe")) {
- continue;
- }
- if (!filterElements.allowRepeatDexNum.checked) {
- if (dexNums.has(pkmn.num)) {
- continue;
- }
- dexNums.add(pkmn.num);
- }
- toRender.push(pkmn);
- }
- renderPokemon(toRender, colorSearchResultsTarget);
- },
- getRandomTeam(p=0.05, n=6, m=1000) {
- const selected = new Set();
- const selectedSpecies = new Set();
- let iter = 0;
- while (selected.size < n && iter < m) {
- iter++;
- for (let i = 0;i < this.ranked.length; i++) {
- const pkmn = this.ranked[i];
- if (selected.has(i) || selectedSpecies.has(pkmn.num)) {
- continue;
- }
- if (filterElements.hideNoStart.checked && pkmn.traits.includes("nostart")) {
- continue;
- }
- if (!filterElements.allowNFE.checked && pkmn.traits.includes("nfe")) {
- continue;
- }
- if (Math.random() < p) {
- selected.add(i);
- selectedSpecies.add(pkmn.num);
- break;
- }
- }
- }
- return Array.from(selected).map(i => this.ranked[i]);
- }
- };
- // ---- Form Controls ----
- nameSearchFormElements.input.addEventListener("input", ({ target: { value } }) => {
- model.setNameSearchResults(
- pokemonLookup.search(value, { limit: 36 }).map(({ item }) => item)
- );
- });
- nameSearchFormElements.all.addEventListener("click", () => {
- nameSearchFormElements.input.value = "";
- model.setNameSearchResults(pokemonData.slice(0));
- });
- nameSearchFormElements.clear.addEventListener("click", () => {
- nameSearchFormElements.input.value = "";
- model.setNameSearchResults([]);
- });
- nameSearchFormElements.random.addEventListener("click", () => {
- model.setNameSearchResults(
- Array.from(
- { length: 36 },
- () => pokemonData[Math.floor(Math.random() * pokemonData.length)]
- )
- );
- });
- nameSearchFormElements.team.addEventListener("click", () => {
- model.setNameSearchResults(model.getRandomTeam());
- });
- targetColorElements.colorText.addEventListener("input", ({ target }) => {
- if (target.willValidate && !target.validity.valid) {
- target.value = target.dataset.lastValid || "";
- } else {
- model.setTargetColor(target.value);
- }
- });
- targetColorElements.colorPicker.addEventListener("change", ({ target }) =>
- model.setTargetColor(target.value)
- );
- const randomizeTargetColor = () =>
- model.setTargetColor(
- [Math.random(), Math.random(), Math.random()]
- .map(component =>
- Math.floor(component * 256)
- .toString(16)
- .padStart(2, "0")
- )
- .reduce((x, y) => x + y)
- );
- targetColorElements.randomColor.addEventListener("click", randomizeTargetColor);
- colorDisplayElements.lessResults.addEventListener("click", () => {
- model.setResultsToDisplay(model.resultsToDisplay - 6);
- });
- colorDisplayElements.moreResults.addEventListener("click", () => {
- model.setResultsToDisplay(model.resultsToDisplay + 6);
- });
- Array.from(filterElements).forEach(el =>
- el.addEventListener("change", () => model.renderColorSearchResults())
- );
- Array.from(colorSortForm.elements).forEach(el =>
- el.addEventListener("change", () => model.rank())
- );
- Array.from(colorCalculateForm.elements).forEach(el =>
- el.addEventListener("change", () => {
- const { sortUseBestCluster, sortUseClusterProportion, sortUseInvClusterProportion } =
- Object.fromEntries(new FormData(colorCalculateForm).entries());
- clusterFunctionSection.dataset.faded = !(
- sortUseBestCluster ||
- sortUseClusterProportion ||
- sortUseInvClusterProportion
- );
- model.calculateObjective();
- })
- );
- sortMetricForm.addEventListener("change", () => {
- updateMetricDisplays();
- model.calculateObjective();
- });
- clusterMetricForm.addEventListener("change", () => {
- updateMetricDisplays();
- model.calculateObjective();
- });
- // ---- Initial Setup ----
- updateMetricDisplays();
- randomizeTargetColor();
|