|
@@ -1,7 +1,7 @@
|
|
|
// Selectors + DOM Manipulation
|
|
|
const getColorInputNode = () => document.getElementById("color-input");
|
|
|
const getMetricDropdownNode = () => document.getElementById("metric");
|
|
|
-const getMeanArgumentDropdownNode = () => document.getElementById("image-summary");
|
|
|
+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");
|
|
@@ -107,8 +107,8 @@ const readColorInput = () => {
|
|
|
// State
|
|
|
const state = {
|
|
|
metric: null,
|
|
|
- meanArgument: null,
|
|
|
- includeScaleInDist: null,
|
|
|
+ clusterChoice: null,
|
|
|
+ includeScale: null,
|
|
|
includeX: null,
|
|
|
normQY: null,
|
|
|
closeCoeff: null,
|
|
@@ -124,47 +124,68 @@ const state = {
|
|
|
const getBestKMean = (stats, q) => argMin(stats.kMeans.map((z, i) => vectorSqDist(z.vector, q.vector) / stats.kWeights[i]));
|
|
|
const getWorstKMean = (stats, q) => argMax(stats.kMeans.map((z, i) => vectorSqDist(z.vector, q.vector) / stats.kWeights[i]));
|
|
|
|
|
|
+const getScale = weight => state.includeScale ? (1 / weight) : 1;
|
|
|
+
|
|
|
const summarySelectors = [
|
|
|
// true mean
|
|
|
stats => [stats.trueMean, 1],
|
|
|
// largest cluster
|
|
|
- stats => [stats.kMeans[stats.largestCluster], stats.kWeights[stats.largestCluster]],
|
|
|
+ stats => [stats.kMeans[stats.largestCluster], getScale(stats.kWeights[stats.largestCluster])],
|
|
|
// smallest cluster
|
|
|
- stats => [stats.kMeans[stats.smallestCluster], stats.kWeights[stats.smallestCluster]],
|
|
|
+ stats => [stats.kMeans[stats.smallestCluster], getScale(stats.kWeights[stats.smallestCluster])],
|
|
|
// best fit cluster
|
|
|
(stats, q) => {
|
|
|
const best = getBestKMean(stats, q);
|
|
|
- return [stats.kMeans[best], stats.kWeights[best]];
|
|
|
+ return [stats.kMeans[best], getScale(stats.kWeights[best])];
|
|
|
},
|
|
|
// worst fit cluster
|
|
|
(stats, q) => {
|
|
|
const worst = getWorstKMean(stats, q);
|
|
|
- return [stats.kMeans[worst], stats.kWeights[worst]];
|
|
|
+ return [stats.kMeans[worst], getScale(stats.kWeights[worst])];
|
|
|
},
|
|
|
];
|
|
|
|
|
|
-const selectedSummary = (stats, q) => summarySelectors[state.meanArgument](stats, q);
|
|
|
+const selectedSummary = (stats, q) => summarySelectors[state.clusterChoice](stats, q);
|
|
|
|
|
|
const metrics = [
|
|
|
// RMS
|
|
|
- (stats, q) => stats.inertia - 2 * vectorDot(selectedSummary(stats, q)[0].vector, q.vector),
|
|
|
+ (stats, q) => {
|
|
|
+ const [ mean, scale ] = selectedSummary(stats, q);
|
|
|
+ return (stats.inertia - 2 * vectorDot(mean.vector, q.vector)) * scale;
|
|
|
+ },
|
|
|
// mean angle
|
|
|
- (stats, q) => -vectorDot(selectedSummary(stats, q)[0].unit, q.unit),
|
|
|
+ (stats, q) => {
|
|
|
+ const [ mean, scale ] = selectedSummary(stats, q);
|
|
|
+ return -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
|
|
|
- const [data, scale] = selectedSummary(stats, q);
|
|
|
- return vectorSqDist(data.vector, q.vector) / (state.includeScaleInDist ? scale : 1);
|
|
|
+ return vectorSqDist(mean.vector, q.vector) * scale;
|
|
|
},
|
|
|
// hue angle
|
|
|
- (stats, q) => angleDiff(selectedSummary(stats, q)[0].hue, q.hue),
|
|
|
+ (stats, q) => {
|
|
|
+ const [ mean, scale ] = selectedSummary(stats, q);
|
|
|
+ return angleDiff(mean.hue, q.hue) * scale;
|
|
|
+ },
|
|
|
// max inertia
|
|
|
- (stats) => -stats.inertia,
|
|
|
+ (stats, q) => {
|
|
|
+ const [ , scale ] = selectedSummary(stats, q);
|
|
|
+ return -stats.inertia * scale;
|
|
|
+ },
|
|
|
// custom
|
|
|
- (stats, q) => (state.includeX ? stats.inertia : 0) - state.closeCoeff * vectorDot(
|
|
|
- selectedSummary(stats, q)[0][state.normQY ? "unit" : "vector"],
|
|
|
- state.normQY ? q.unit : q.vector,
|
|
|
- ),
|
|
|
+ (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 => ({
|
|
@@ -215,12 +236,14 @@ const mathDefinitions = {
|
|
|
`,
|
|
|
};
|
|
|
|
|
|
+const clusterScaleText = muArg => (state.clusterChoice > 0 && state.includeScale ? String.raw`\frac{\left|P\right|}{\left|${muArg}\right|}` : "")
|
|
|
+
|
|
|
const metricText = [
|
|
|
- muArg => String.raw`${mathArgBest("min", "P")}\left[I\left(P\right) - 2\vec{q}\cdot \vec{\mu}\left(${muArg}\right)\right]`,
|
|
|
- muArg => String.raw`${mathArgBest("min", "P")}\left[-\cos\left(\angle \left(\vec{q}, \vec{\mu}\left(${muArg}\right)\right)\right)\right]`,
|
|
|
- muArg => String.raw`${mathArgBest("min", "P")}\left[${state.meanArgument > 0 && state.includeScaleInDist ? String.raw`\frac{\left|P\right|}{\left|${muArg}\right|}` : ""} \left|\left| \vec{q} - \vec{\mu}\left(${muArg}\right) \right|\right|^2\right]`,
|
|
|
- muArg => String.raw`${mathArgBest("min", "P")}\left[\angle \left(\vec{q}_{\perp}, \vec{\mu}\left(${muArg}\right)_{\perp} \right)\right]`,
|
|
|
- muArg => String.raw`${mathArgBest("min", "P")}\left[-I\left(P\right)\right]`,
|
|
|
+ muArg => String.raw`${mathArgBest("min", "P")}\left[${clusterScaleText(muArg)}I\left(P\right) - 2\vec{q}\cdot ${clusterScaleText(muArg)}\vec{\mu}\left(${muArg}\right)\right]`,
|
|
|
+ muArg => String.raw`${mathArgBest("min", "P")}\left[-${clusterScaleText(muArg)}\cos\left(\angle \left(\vec{q}, \vec{\mu}\left(${muArg}\right)\right)\right)\right]`,
|
|
|
+ muArg => String.raw`${mathArgBest("min", "P")}\left[${clusterScaleText(muArg)}\left|\left| \vec{q} - \vec{\mu}\left(${muArg}\right) \right|\right|^2\right]`,
|
|
|
+ muArg => String.raw`${mathArgBest("min", "P")}\left[${clusterScaleText(muArg)}\angle \left(\vec{q}_{\perp}, \vec{\mu}\left(${muArg}\right)_{\perp} \right)\right]`,
|
|
|
+ muArg => String.raw`${mathArgBest("min", "P")}\left[-${clusterScaleText(muArg)}I\left(P\right)\right]`,
|
|
|
].map(s => muArg => TeXZilla.toMathML(s(muArg)));
|
|
|
|
|
|
const muArgs = [
|
|
@@ -234,7 +257,7 @@ const muArgs = [
|
|
|
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.meanArgument];
|
|
|
+ const muArg = muArgs[state.clusterChoice];
|
|
|
let tex = metricText?.[state.metric]?.(muArg);
|
|
|
if (!tex) {
|
|
|
const { includeX, normQY, closeCoeff } = state;
|
|
@@ -475,7 +498,7 @@ const onCustomControlsChanged = skipScore => {
|
|
|
const checkClusterMeanWarning = () => {
|
|
|
const warning = getClusterMeanWarning();
|
|
|
const unhidden = warning.getAttribute("class").replaceAll("hide", "");
|
|
|
- if (state.meanArgument !== 0 && state.metric === 0) {
|
|
|
+ if (state.clusterChoice !== 0 && state.metric === 0) {
|
|
|
warning.setAttribute("class", unhidden);
|
|
|
} else {
|
|
|
warning.setAttribute("class", unhidden + " hide");
|
|
@@ -485,7 +508,7 @@ const checkClusterMeanWarning = () => {
|
|
|
const checkScaleByClusterToggle = () => {
|
|
|
const toggle = getClusterScaleToggleNode()?.parentNode;
|
|
|
const unhidden = toggle.getAttribute("class").replaceAll("hide", "");
|
|
|
- if (state.meanArgument !== 0 && state.metric === 2) {
|
|
|
+ if (state.clusterChoice !== 0) {
|
|
|
toggle.setAttribute("class", unhidden);
|
|
|
} else {
|
|
|
toggle.setAttribute("class", unhidden + " hide");
|
|
@@ -493,7 +516,7 @@ const checkScaleByClusterToggle = () => {
|
|
|
}
|
|
|
|
|
|
const onScaleByClusterChanged = skipScore => {
|
|
|
- state.includeScaleInDist = getClusterScaleToggleNode()?.checked ?? true;
|
|
|
+ state.includeScale = getClusterScaleToggleNode()?.checked ?? true;
|
|
|
|
|
|
updateObjective();
|
|
|
|
|
@@ -502,12 +525,12 @@ const onScaleByClusterChanged = skipScore => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-const onMeanArgumentChanged = skipScore => {
|
|
|
- const meanArgument = getMeanArgumentDropdownNode()?.selectedIndex ?? 0;
|
|
|
- if (meanArgument === state.meanArgument) {
|
|
|
+const onClusterChoiceChanged = skipScore => {
|
|
|
+ const clusterChoice = getClusterChoiceDropdownNode()?.selectedIndex ?? 0;
|
|
|
+ if (clusterChoice === state.clusterChoice) {
|
|
|
return;
|
|
|
}
|
|
|
- state.meanArgument = meanArgument;
|
|
|
+ state.clusterChoice = clusterChoice;
|
|
|
checkClusterMeanWarning();
|
|
|
checkScaleByClusterToggle();
|
|
|
updateObjective();
|
|
@@ -581,7 +604,7 @@ const onPageLoad = () => {
|
|
|
// fake some events but don't do any scoring
|
|
|
onColorChanged(true);
|
|
|
onMetricChanged(true);
|
|
|
- onMeanArgumentChanged(true);
|
|
|
+ onClusterChoiceChanged(true);
|
|
|
onScaleByClusterChanged(true);
|
|
|
onLimitChanged(true);
|
|
|
// then do a rescore directly, which will do nothing unless old data was loaded
|