Skip to content

Commit 9884a67

Browse files
author
Ibrahim Haizel
committed
feat: implement sequential color mapping for selected items to ensure unique color assignment based on selection order
1 parent 7a4bde3 commit 9884a67

File tree

1 file changed

+60
-58
lines changed

1 file changed

+60
-58
lines changed

src/lib/components/ui/MultiSelectSearchAutocomplete.svelte

Lines changed: 60 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
selectedItemCircleColor = "#1d70b8", // Default color when not using palette
7777
selectedItemCircleColorPalette = [
7878
// Complete GOV.UK Design System palette (19 colors)
79+
// Maximum selections = 19 before colors start cycling
7980
"#1d70b8", // Blue (primary)
8081
"#d4351c", // Red
8182
"#00703c", // Green
@@ -94,8 +95,7 @@
9495
"#626a6e", // Mid grey
9596
"#b1b4b6", // Light grey
9697
"#0b0c0c", // Black
97-
"#ffffff", // White (with border for visibility)
98-
], // Complete GOV.UK Design System palette
98+
], // Complete GOV.UK Design System palette (19 colors)
9999
...attributes
100100
}: {
101101
id: string;
@@ -167,81 +167,83 @@
167167
return div.innerHTML;
168168
}
169169
170-
// Extended color palette using proven data visualization algorithms
171-
function generateExtendedColorPalette(count: number): string[] {
172-
if (count === 0) return [];
173-
174-
// Start with the predefined GOV.UK palette
175-
const baseColors = [...selectedItemCircleColorPalette];
176-
177-
if (count <= baseColors.length) {
178-
return baseColors.slice(0, count);
170+
// Sequential color mapping: each selected item gets a unique color based on selection order
171+
// This ensures perfect visual distinction and predictable color assignment
172+
// 1st selection = color[0], 2nd selection = color[1], 3rd selection = color[2], etc.
173+
let selectedItemIndexMap = new Map<string, number>(); // Maps item value to selection index
174+
let nextSelectionIndex = 0; // Tracks the next available color index
175+
176+
// Get the maximum number of selections allowed (limited by palette size)
177+
// With 19 GOV.UK colors, maximum selections = 19 before cycling begins
178+
const maxSelections = selectedItemCircleColorPalette.length;
179+
180+
// Function to get color for a selected item based on its selection order
181+
function getColorForSelectedItem(itemValue: string | number): string {
182+
const valueKey = String(itemValue);
183+
184+
// If this item already has an index, use it
185+
if (selectedItemIndexMap.has(valueKey)) {
186+
const index = selectedItemIndexMap.get(valueKey)!;
187+
console.log("🎨 Color index map hit (existing item):", {
188+
itemValue: valueKey,
189+
existingColorIndex: index,
190+
existingColor: selectedItemCircleColorPalette[index],
191+
totalMappedItems: selectedItemIndexMap.size,
192+
});
193+
return selectedItemCircleColorPalette[index];
179194
}
180195
181-
// For more colors, generate using Plotly.js-style algorithm
182-
const extendedColors = [...baseColors];
183-
const needed = count - baseColors.length;
184-
185-
// Use golden ratio and HSL for optimal color distribution
186-
const goldenRatio = 0.618033988749895;
187-
let hue = 0;
188-
189-
for (let i = 0; i < needed; i++) {
190-
// Vary saturation and lightness for accessibility
191-
const saturation = 65 + (i % 3) * 10; // 65%, 75%, 85%
192-
const lightness = 45 + (i % 4) * 10; // 45%, 55%, 65%, 75%
193-
194-
// Use golden ratio for optimal hue distribution
195-
hue = (hue + goldenRatio) % 1;
196-
const hslHue = Math.floor(hue * 360);
197-
198-
const color = `hsl(${hslHue}, ${saturation}%, ${lightness}%)`;
199-
// Avoid pure white/very light colors
200-
if (lightness < 90) {
201-
extendedColors.push(color);
202-
}
196+
// If we've reached the palette limit, cycle back to the beginning
197+
// This means the 20th selection will get color[0], 21st will get color[1], etc.
198+
if (nextSelectionIndex >= maxSelections) {
199+
nextSelectionIndex = 0;
203200
}
204201
205-
return extendedColors;
206-
}
202+
// Assign the next available color index to this item
203+
const colorIndex = nextSelectionIndex;
204+
selectedItemIndexMap.set(valueKey, colorIndex);
205+
nextSelectionIndex++;
206+
207+
// Log the index map update for debugging
208+
console.log("🎨 Color index map updated:", {
209+
itemValue: valueKey,
210+
assignedColorIndex: colorIndex,
211+
assignedColor: selectedItemCircleColorPalette[colorIndex],
212+
nextSelectionIndex,
213+
totalMappedItems: selectedItemIndexMap.size,
214+
currentMap: Object.fromEntries(selectedItemIndexMap),
215+
});
207216
208-
// Helper function to get optimized color palette based on number of selected items
209-
function getOptimizedColorPalette(selectedCount: number): string[] {
210-
return generateExtendedColorPalette(selectedCount);
217+
return selectedItemCircleColorPalette[colorIndex];
211218
}
212219
213-
// Precompute stable palette once instead of regenerating per call
214-
const stablePalette = (() => {
215-
const base = [...selectedItemCircleColorPalette];
216-
// Filter out pure white to avoid visibility issues
217-
const filtered = base.filter(color => color !== "#ffffff");
218-
// extend once if needed:
219-
return filtered.length >= 64 ? filtered : generateExtendedColorPalette(64);
220-
})();
220+
// Function to reset the selection index when items are removed
221+
// Call this when you want to clear all selections and start fresh
222+
function resetSelectionIndexes() {
223+
console.log("🔄 Color index map reset:", {
224+
previousMapSize: selectedItemIndexMap.size,
225+
previousNextIndex: nextSelectionIndex,
226+
});
227+
selectedItemIndexMap.clear();
228+
nextSelectionIndex = 0;
229+
console.log("✅ Color index map reset complete");
230+
}
221231
222232
// Color cache to ensure consistent colors for the same values
223233
const colorCache = new Map<string, string>();
224234
225235
/**
226236
* Generate a consistent, deterministic color for a given value.
227-
* Uses FNV-1a hash with proper unsigned handling to avoid color duplication.
228-
* Caches results for performance and consistency.
237+
* Uses sequential index-based mapping for perfect visual distinction.
238+
* Each selected item gets a unique color based on selection order.
229239
*/
230240
function colorForValue(val: unknown): string {
231241
const key = String(val).toLowerCase().trim();
232242
const cached = colorCache.get(key);
233243
if (cached) return cached;
234244
235-
// FNV-1a (32-bit) -> force unsigned before modulo to avoid negative array indices
236-
let h = 2166136261;
237-
for (let i = 0; i < key.length; i++) {
238-
h ^= key.charCodeAt(i);
239-
h = Math.imul(h, 16777619);
240-
}
241-
h >>>= 0; // ✅ make it unsigned
242-
243-
const idx = h % stablePalette.length; // 0..palette-1
244-
const color = stablePalette[idx] || selectedItemCircleColor;
245+
// Use sequential color mapping instead of hash-based approach
246+
const color = getColorForSelectedItem(String(val));
245247
colorCache.set(key, color);
246248
return color;
247249
}

0 commit comments

Comments
 (0)