|
@@ -2,6 +2,7 @@
|
|
|
const getColorInputNode = () => document.getElementById("color-input");
|
|
|
const getMetricDropdownNode = () => document.getElementById("metric");
|
|
|
const getMeanArgumentDropdownNode = () => 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");
|
|
@@ -101,7 +102,8 @@ const readColorInput = () => {
|
|
|
// State
|
|
|
const state = {
|
|
|
metric: null,
|
|
|
- meanArgument: 0, // TODO
|
|
|
+ meanArgument: null,
|
|
|
+ includeScaleInDist: null,
|
|
|
includeX: null,
|
|
|
normQY: null,
|
|
|
closeCoeff: null,
|
|
@@ -114,31 +116,41 @@ const state = {
|
|
|
// Metrics
|
|
|
const summarySelectors = [
|
|
|
// true mean
|
|
|
- stats => stats.trueMean,
|
|
|
+ stats => [stats.trueMean, 1],
|
|
|
// largest cluster
|
|
|
- stats => stats.kMeans[stats.largestCluster],
|
|
|
+ stats => [stats.kMeans[stats.largestCluster], stats.kWeights[stats.largestCluster]],
|
|
|
// smallest cluster
|
|
|
- stats => stats.kMeans[stats.smallestCluster],
|
|
|
+ stats => [stats.kMeans[stats.smallestCluster], stats.kWeights[stats.smallestCluster]],
|
|
|
// best fit cluster
|
|
|
- (stats, q) => stats.kMeans[argMin(stats.kMeans.map((z, i) => vectorSqDist(z.vector, q.vector) / stats.kWeights[i]))],
|
|
|
+ (stats, q) => {
|
|
|
+ const best = argMin(stats.kMeans.map((z, i) => vectorSqDist(z.vector, q.vector) / stats.kWeights[i]));
|
|
|
+ return [stats.kMeans[best], stats.kWeights[best]];
|
|
|
+ },
|
|
|
// worst fit cluster
|
|
|
- (stats, q) => stats.kMeans[argMax(stats.kMeans.map((z, i) => vectorSqDist(z.vector, q.vector) / stats.kWeights[i]))],
|
|
|
+ (stats, q) => {
|
|
|
+ const worst = argMax(stats.kMeans.map((z, i) => vectorSqDist(z.vector, q.vector) / stats.kWeights[i]));
|
|
|
+ return [stats.kMeans[worst], stats.kWeights[worst]];
|
|
|
+ },
|
|
|
];
|
|
|
|
|
|
const selectedSummary = (stats, q) => summarySelectors[state.meanArgument](stats, q);
|
|
|
|
|
|
const metrics = [
|
|
|
// RMS
|
|
|
- (stats, q) => stats.varFromZero - 2 * vectorDot(selectedSummary(stats, q).vector, q.vector),
|
|
|
+ (stats, q) => stats.varFromZero - 2 * vectorDot(selectedSummary(stats, q)[0].vector, q.vector),
|
|
|
// mean angle
|
|
|
- (stats, q) => -vectorDot(selectedSummary(stats, q).unit, q.unit),
|
|
|
+ (stats, q) => -vectorDot(selectedSummary(stats, q)[0].unit, q.unit),
|
|
|
// mean dist
|
|
|
- (stats, q) => vectorSqDist(selectedSummary(stats, q).vector, q.vector),
|
|
|
+ (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);
|
|
|
+ },
|
|
|
// hue angle
|
|
|
- (stats, q) => angleDiff(selectedSummary(stats, q).hue, q.hue),
|
|
|
+ (stats, q) => angleDiff(selectedSummary(stats, q)[0].hue, q.hue),
|
|
|
// custom
|
|
|
(stats, q) => (state.includeX ? stats.varFromZero : 0) - state.closeCoeff * vectorDot(
|
|
|
- selectedSummary(stats, q)[state.normQY ? "unit" : "vector"],
|
|
|
+ selectedSummary(stats, q)[0][state.normQY ? "unit" : "vector"],
|
|
|
state.normQY ? q.unit : q.vector,
|
|
|
),
|
|
|
];
|
|
@@ -216,7 +228,7 @@ const mathDefinitions = {
|
|
|
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("max", "P")}\left[\cos\left(\angle \left(\vec{q}, \vec{\mu}\left(${muArg}\right)\right)\right)\right]`,
|
|
|
- muArg => String.raw`${mathArgBest("min", "P")}\left[\left|\left| \vec{q} - \vec{\mu}\left(${muArg}\right) \right|\right|^2\right]`,
|
|
|
+ muArg => String.raw`${mathArgBest("min", "P")}\left[ ${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]`,
|
|
|
].map(s => muArg => TeXZilla.toMathML(s(muArg)));
|
|
|
|
|
@@ -428,6 +440,26 @@ const checkClusterMeanWarning = () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+const checkScaleByClusterToggle = () => {
|
|
|
+ const toggle = getClusterScaleToggleNode()?.parentNode;
|
|
|
+ const unhidden = toggle.getAttribute("class").replaceAll("hide", "");
|
|
|
+ if (state.meanArgument !== 0 && state.metric === 2) {
|
|
|
+ toggle.setAttribute("class", unhidden);
|
|
|
+ } else {
|
|
|
+ toggle.setAttribute("class", unhidden + " hide");
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const onScaleByClusterChanged = skipScore => {
|
|
|
+ state.includeScaleInDist = getClusterScaleToggleNode()?.checked ?? true;
|
|
|
+
|
|
|
+ updateObjective();
|
|
|
+
|
|
|
+ if (!skipScore) {
|
|
|
+ rescore();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
const onMeanArgumentChanged = skipScore => {
|
|
|
const meanArgument = getMeanArgumentDropdownNode()?.selectedIndex ?? 0;
|
|
|
if (meanArgument === state.meanArgument) {
|
|
@@ -435,6 +467,7 @@ const onMeanArgumentChanged = skipScore => {
|
|
|
}
|
|
|
state.meanArgument = meanArgument;
|
|
|
checkClusterMeanWarning();
|
|
|
+ checkScaleByClusterToggle();
|
|
|
updateObjective();
|
|
|
if (!skipScore) {
|
|
|
rescore();
|
|
@@ -448,6 +481,7 @@ const onMetricChanged = skipScore => {
|
|
|
}
|
|
|
state.metric = metric;
|
|
|
checkClusterMeanWarning();
|
|
|
+ checkScaleByClusterToggle();
|
|
|
if (state.metric === 4) { // Custom
|
|
|
showCustomControls();
|
|
|
onCustomControlsChanged(skipScore); // triggers rescore
|
|
@@ -499,6 +533,7 @@ const onPageLoad = () => {
|
|
|
onColorChanged(true);
|
|
|
onMetricChanged(true);
|
|
|
onMeanArgumentChanged(true);
|
|
|
+ onScaleByClusterChanged(true);
|
|
|
onLimitChanged(true);
|
|
|
// then do a rescore directly, which will do nothing unless old data was loaded
|
|
|
rescore();
|