UX Improvements

This commit is contained in:
Brennan Wilkes (Text Groove) 2026-02-05 17:24:19 -08:00
parent 8531eba4e3
commit 1216ec7fe0

View file

@ -1,166 +1,183 @@
function normalizeId(s) { function normalizeId(s) {
return String(s || "").toLowerCase().replace(/[^a-z0-9]+/g, ""); return String(s || "").toLowerCase().replace(/[^a-z0-9]+/g, "");
} }
// Your pinned colors (exact / “roughly right”) // Map normalized store *labels* to canonical ids used by OVERRIDES
const OVERRIDES = { const ALIASES = {
strath: "#76B7FF", strathliquor: "strath",
bsw: "#E9DF7A", vesselliquor: "vessel",
kensingtonwinemarket: "#F2C200", tudorhouse: "tudor",
vessel: "#FFFFFF", coopworldofwhisky: "coop",
gullliquor: "#6B0F1A",
kegncork: "#111111",
legacyliquor: "#7B4A12",
vintagespirits: "#E34A2C",
craftcellars: "#E31B23", // bright red kensingtonwinemarket: "kensingtonwinemarket",
maltsandgrains: "#A67C52", // faded brown gullliquor: "gullliquor",
legacyliquor: "legacyliquor",
vintagespirits: "vintagespirits",
kegncork: "kegncork",
// aliases // short forms
gull: "#6B0F1A", gull: "gullliquor",
legacy: "#7B4A12", legacy: "legacyliquor",
vintage: "#E34A2C", vintage: "vintagespirits",
kwm: "#F2C200", kwm: "kensingtonwinemarket",
}; };
// High-contrast qualitative palette (distinct hues). // Your pinned colors
// (Avoids whites/blacks/yellows that clash w/ your overrides by filtering below.) const OVERRIDES = {
const PALETTE = [ strath: "#76B7FF",
"#1F77B4", "#FF7F0E", "#2CA02C", "#D62728", "#9467BD", bsw: "#E9DF7A",
"#8C564B", "#E377C2", "#7F7F7F", "#17BECF", "#BCBD22", kensingtonwinemarket: "#F2C200",
"#AEC7E8", "#FFBB78", "#98DF8A", "#FF9896", "#C5B0D5", vessel: "#FFFFFF",
"#C49C94", "#F7B6D2", "#C7C7C7", "#9EDAE5", "#DBDB8D", gullliquor: "#6B0F1A",
// extras to reduce wrap risk kegncork: "#111111",
"#393B79", "#637939", "#8C6D31", "#843C39", "#7B4173", legacyliquor: "#7B4A12",
"#3182BD", "#31A354", "#756BB1", "#636363", "#E6550D", vintagespirits: "#E34A2C",
];
function uniq(arr) { craftcellars: "#E31B23",
return [...new Set(arr)]; maltsandgrains: "#A67C52",
}
function buildUniverse(base, extra) { // aliases
const a = Array.isArray(base) ? base : []; gull: "#6B0F1A",
const b = Array.isArray(extra) ? extra : []; legacy: "#7B4A12",
return uniq([...a, ...b].map(normalizeId).filter(Boolean)); vintage: "#E34A2C",
} kwm: "#F2C200",
};
// Default known ids (keeps mapping stable even if a page only sees a subset) // High-contrast qualitative palette
const DEFAULT_UNIVERSE = buildUniverse(Object.keys(OVERRIDES), [ const PALETTE = [
"bcl", "#1F77B4", "#FF7F0E", "#2CA02C", "#D62728", "#9467BD",
"bsw", "#8C564B", "#E377C2", "#7F7F7F", "#17BECF", "#BCBD22",
"coop", "#AEC7E8", "#FFBB78", "#98DF8A", "#FF9896", "#C5B0D5",
"craftcellars", "#C49C94", "#F7B6D2", "#C7C7C7", "#9EDAE5", "#DBDB8D",
"gullliquor", "#393B79", "#637939", "#8C6D31", "#843C39", "#7B4173",
"gull", "#3182BD", "#31A354", "#756BB1", "#636363", "#E6550D",
"kegncork", ];
"kwm",
"kensingtonwinemarket",
"legacy",
"legacyliquor",
"maltsandgrains",
"sierrasprings",
"strath",
"tudor",
"vessel",
"vintage",
"vintagespirits",
"willowpark",
]);
function isWhiteHex(c) { function uniq(arr) {
return String(c || "").trim().toUpperCase() === "#FFFFFF"; return [...new Set(arr)];
} }
export function buildStoreColorMap(extraUniverse = []) { function canonicalId(s) {
const universe = buildUniverse(DEFAULT_UNIVERSE, extraUniverse).sort(); const id = normalizeId(s);
return ALIASES[id] || id;
}
const used = new Set(); function buildUniverse(base, extra) {
const map = new Map(); const a = Array.isArray(base) ? base : [];
const b = Array.isArray(extra) ? extra : [];
return uniq([...a, ...b].map(canonicalId).filter(Boolean));
}
// pin overrides first // Keep mapping stable even if page sees a subset
for (const id of universe) { const DEFAULT_UNIVERSE = buildUniverse(Object.keys(OVERRIDES), [
const c = OVERRIDES[id]; "bcl",
if (c) { "bsw",
map.set(id, c); "coop",
used.add(String(c).toUpperCase()); "craftcellars",
} "gullliquor",
} "gull",
"kegncork",
"kwm",
"kensingtonwinemarket",
"legacy",
"legacyliquor",
"maltsandgrains",
"sierrasprings",
"strath",
"tudor",
"vessel",
"vintage",
"vintagespirits",
"willowpark",
]);
// filter palette to avoid exact collisions with overrides (and keep white reserved for Vessel) function isWhiteHex(c) {
const palette = PALETTE return String(c || "").trim().toUpperCase() === "#FFFFFF";
.map((c) => String(c).toUpperCase()) }
.filter((c) => !used.has(c) && c !== "#FFFFFF" && c !== "#111111");
let pi = 0; export function buildStoreColorMap(extraUniverse = []) {
for (const id of universe) { const universe = buildUniverse(DEFAULT_UNIVERSE, extraUniverse).sort();
if (map.has(id)) continue;
if (pi >= palette.length) { const used = new Set();
// If you ever exceed palette size, just reuse (rare). Still deterministic. const map = new Map();
pi = 0;
} // Pin overrides first
const c = palette[pi++]; for (const id of universe) {
const c = OVERRIDES[id];
if (c) {
map.set(id, c); map.set(id, c);
used.add(c); used.add(String(c).toUpperCase());
} }
return map;
} }
export function storeColor(storeKeyOrLabel, colorMap) { // Filter palette to avoid collisions and keep white/black reserved
const id = normalizeId(storeKeyOrLabel); const palette = PALETTE
if (!id) return "#7F7F7F"; .map((c) => String(c).toUpperCase())
.filter((c) => !used.has(c) && c !== "#FFFFFF" && c !== "#111111");
const forced = OVERRIDES[id]; let pi = 0;
if (forced) return forced; for (const id of universe) {
if (map.has(id)) continue;
if (colorMap && typeof colorMap.get === "function") { if (pi >= palette.length) pi = 0;
const c = colorMap.get(id); const c = palette[pi++];
if (c) return c; map.set(id, c);
} used.add(c);
// fallback: deterministic but not “no conflicts”
return PALETTE[(id.length + id.charCodeAt(0)) % PALETTE.length];
} }
export function datasetStrokeWidth(color) { return map;
return isWhiteHex(color) ? 2.5 : 1.5; }
export function storeColor(storeKeyOrLabel, colorMap) {
const id = canonicalId(storeKeyOrLabel);
if (!id) return "#7F7F7F";
const forced = OVERRIDES[id];
if (forced) return forced;
if (colorMap && typeof colorMap.get === "function") {
const c = colorMap.get(id);
if (c) return c;
} }
export function datasetPointRadius(color) { return PALETTE[(id.length + id.charCodeAt(0)) % PALETTE.length];
return isWhiteHex(color) ? 2.8 : 2.2; }
}
function clamp(v, lo, hi) { export function datasetStrokeWidth(color) {
return Math.max(lo, Math.min(hi, v)); return isWhiteHex(color) ? 2.5 : 1.5;
} }
function hexToRgb(hex) { export function datasetPointRadius(color) {
const m = String(hex).replace("#", ""); return isWhiteHex(color) ? 2.8 : 2.2;
if (m.length !== 6) return null; }
const n = parseInt(m, 16);
return {
r: (n >> 16) & 255,
g: (n >> 8) & 255,
b: n & 255,
};
}
function rgbToHex({ r, g, b }) { function clamp(v, lo, hi) {
const h = (x) => clamp(Math.round(x), 0, 255).toString(16).padStart(2, "0"); return Math.max(lo, Math.min(hi, v));
return `#${h(r)}${h(g)}${h(b)}`; }
}
// lighten by mixing with white (amount 01)
export function lighten(hex, amount = 0.25) {
const rgb = hexToRgb(hex);
if (!rgb) return hex;
return rgbToHex({
r: rgb.r + (255 - rgb.r) * amount,
g: rgb.g + (255 - rgb.g) * amount,
b: rgb.b + (255 - rgb.b) * amount,
});
}
function hexToRgb(hex) {
const m = String(hex).replace("#", "");
if (m.length !== 6) return null;
const n = parseInt(m, 16);
return {
r: (n >> 16) & 255,
g: (n >> 8) & 255,
b: n & 255,
};
}
function rgbToHex({ r, g, b }) {
const h = (x) =>
clamp(Math.round(x), 0, 255).toString(16).padStart(2, "0");
return `#${h(r)}${h(g)}${h(b)}`;
}
// Lighten by mixing with white (01)
export function lighten(hex, amount = 0.25) {
const rgb = hexToRgb(hex);
if (!rgb) return hex;
return rgbToHex({
r: rgb.r + (255 - rgb.r) * amount,
g: rgb.g + (255 - rgb.g) * amount,
b: rgb.b + (255 - rgb.b) * amount,
});
}