mirror of
https://github.com/samsonjs/spirit-tracker.git
synced 2026-03-25 09:25:51 +00:00
213 lines
4.1 KiB
JavaScript
213 lines
4.1 KiB
JavaScript
function normalizeId(s) {
|
||
return String(s || "")
|
||
.toLowerCase()
|
||
.replace(/[^a-z0-9]+/g, "");
|
||
}
|
||
|
||
// Map normalized store *labels* to canonical ids used by OVERRIDES
|
||
const ALIASES = {
|
||
strathliquor: "strath",
|
||
vesselliquor: "vessel",
|
||
tudorhouse: "tudor",
|
||
coopworldofwhisky: "coop",
|
||
|
||
kensingtonwinemarket: "kensingtonwinemarket",
|
||
gullliquor: "gullliquor",
|
||
legacyliquor: "legacyliquor",
|
||
vintagespirits: "vintagespirits",
|
||
kegncork: "kegncork",
|
||
|
||
// short forms
|
||
gull: "gullliquor",
|
||
legacy: "legacyliquor",
|
||
vintage: "vintagespirits",
|
||
kwm: "kensingtonwinemarket",
|
||
};
|
||
|
||
// Your pinned colors
|
||
const OVERRIDES = {
|
||
strath: "#76B7FF",
|
||
bsw: "#E9DF7A",
|
||
kensingtonwinemarket: "#F2C200",
|
||
vessel: "#FFFFFF",
|
||
gullliquor: "#6B0F1A",
|
||
kegncork: "#111111",
|
||
legacyliquor: "#7B4A12",
|
||
vintagespirits: "#E34A2C",
|
||
|
||
craftcellars: "#E31B23",
|
||
maltsandgrains: "#A67C52",
|
||
|
||
// aliases
|
||
gull: "#6B0F1A",
|
||
legacy: "#7B4A12",
|
||
vintage: "#E34A2C",
|
||
kwm: "#F2C200",
|
||
};
|
||
|
||
// High-contrast qualitative palette
|
||
const PALETTE = [
|
||
"#1F77B4",
|
||
"#FF7F0E",
|
||
"#2CA02C",
|
||
"#D62728",
|
||
"#9467BD",
|
||
"#8C564B",
|
||
"#E377C2",
|
||
"#7F7F7F",
|
||
"#17BECF",
|
||
"#BCBD22",
|
||
"#AEC7E8",
|
||
"#FFBB78",
|
||
"#98DF8A",
|
||
"#FF9896",
|
||
"#C5B0D5",
|
||
"#C49C94",
|
||
"#F7B6D2",
|
||
"#C7C7C7",
|
||
"#9EDAE5",
|
||
"#DBDB8D",
|
||
"#393B79",
|
||
"#637939",
|
||
"#8C6D31",
|
||
"#843C39",
|
||
"#7B4173",
|
||
"#3182BD",
|
||
"#31A354",
|
||
"#756BB1",
|
||
"#636363",
|
||
"#E6550D",
|
||
];
|
||
|
||
function uniq(arr) {
|
||
return [...new Set(arr)];
|
||
}
|
||
|
||
function canonicalId(s) {
|
||
const id = normalizeId(s);
|
||
return ALIASES[id] || id;
|
||
}
|
||
|
||
function buildUniverse(base, extra) {
|
||
const a = Array.isArray(base) ? base : [];
|
||
const b = Array.isArray(extra) ? extra : [];
|
||
return uniq([...a, ...b].map(canonicalId).filter(Boolean));
|
||
}
|
||
|
||
// Keep mapping stable even if page sees a subset
|
||
const DEFAULT_UNIVERSE = buildUniverse(Object.keys(OVERRIDES), [
|
||
"bcl",
|
||
"bsw",
|
||
"coop",
|
||
"craftcellars",
|
||
"gullliquor",
|
||
"gull",
|
||
"kegncork",
|
||
"kwm",
|
||
"kensingtonwinemarket",
|
||
"legacy",
|
||
"legacyliquor",
|
||
"maltsandgrains",
|
||
"sierrasprings",
|
||
"strath",
|
||
"tudor",
|
||
"vessel",
|
||
"vintage",
|
||
"vintagespirits",
|
||
"willowpark",
|
||
"arc",
|
||
]);
|
||
|
||
function isWhiteHex(c) {
|
||
return (
|
||
String(c || "")
|
||
.trim()
|
||
.toUpperCase() === "#FFFFFF"
|
||
);
|
||
}
|
||
|
||
export function buildStoreColorMap(extraUniverse = []) {
|
||
const universe = buildUniverse(DEFAULT_UNIVERSE, extraUniverse).sort();
|
||
|
||
const used = new Set();
|
||
const map = new Map();
|
||
|
||
// Pin overrides first
|
||
for (const id of universe) {
|
||
const c = OVERRIDES[id];
|
||
if (c) {
|
||
map.set(id, c);
|
||
used.add(String(c).toUpperCase());
|
||
}
|
||
}
|
||
|
||
// Filter palette to avoid collisions and keep white/black reserved
|
||
const palette = PALETTE.map((c) => String(c).toUpperCase()).filter(
|
||
(c) => !used.has(c) && c !== "#FFFFFF" && c !== "#111111",
|
||
);
|
||
|
||
let pi = 0;
|
||
for (const id of universe) {
|
||
if (map.has(id)) continue;
|
||
if (pi >= palette.length) pi = 0;
|
||
const c = palette[pi++];
|
||
map.set(id, c);
|
||
used.add(c);
|
||
}
|
||
|
||
return map;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
return PALETTE[(id.length + id.charCodeAt(0)) % PALETTE.length];
|
||
}
|
||
|
||
export function datasetStrokeWidth(color) {
|
||
return isWhiteHex(color) ? 2.5 : 1.5;
|
||
}
|
||
|
||
export function datasetPointRadius(color) {
|
||
return isWhiteHex(color) ? 2.8 : 2.2;
|
||
}
|
||
|
||
function clamp(v, lo, hi) {
|
||
return Math.max(lo, Math.min(hi, v));
|
||
}
|
||
|
||
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 (0–1)
|
||
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,
|
||
});
|
||
}
|