/** * Map color utilities for World Map visualization. * * Provides color interpolation logic that maps ban counts to colors based on * configurable thresholds. Countries with zero bans remain transparent; * non-zero counts are interpolated through green → yellow → red color stops. */ /** * Interpolate a value between two numbers. * * @param start - Start value * @param end - End value * @param t - Interpolation factor in [0, 1] * @returns The interpolated value */ function lerp(start: number, end: number, t: number): number { return start + (end - start) * t; } /** * Convert RGB values to hex color string. * * @param r - Red component (0-255) * @param g - Green component (0-255) * @param b - Blue component (0-255) * @returns Hex color string in format "#RRGGBB" */ function rgbToHex(r: number, g: number, b: number): string { const toHex = (n: number): string => { const hex = Math.round(Math.max(0, Math.min(255, n))).toString(16); return hex.length === 1 ? "0" + hex : hex; }; return `#${toHex(r)}${toHex(g)}${toHex(b)}`; } /** * Compute the fill color for a country based on its ban count. * * Returns "transparent" for zero bans. For non-zero counts, interpolates * through color stops: * - 1 to threshold_low: light green → full green * - threshold_low to threshold_medium: green → yellow * - threshold_medium to threshold_high: yellow → red * - threshold_high or more: solid red * * @param banCount - Number of bans for the country * @param thresholdLow - Ban count for green coloring (default: 20) * @param thresholdMedium - Ban count for yellow coloring (default: 50) * @param thresholdHigh - Ban count for red coloring (default: 100) * @returns Hex color string or "transparent" */ export function getBanCountColor( banCount: number, thresholdLow: number = 20, thresholdMedium: number = 50, thresholdHigh: number = 100, ): string { // Zero bans → transparent (no fill) if (banCount === 0) { return "transparent"; } // Color stops const lightGreen = { r: 144, g: 238, b: 144 }; // #90EE90 const green = { r: 0, g: 128, b: 0 }; // #008000 const yellow = { r: 255, g: 255, b: 0 }; // #FFFF00 const red = { r: 220, g: 20, b: 60 }; // #DC143C (crimson) // 1 to threshold_low: interpolate light green → green if (banCount <= thresholdLow) { const t = (banCount - 1) / (thresholdLow - 1); const r = lerp(lightGreen.r, green.r, t); const g = lerp(lightGreen.g, green.g, t); const b = lerp(lightGreen.b, green.b, t); return rgbToHex(r, g, b); } // threshold_low to threshold_medium: interpolate green → yellow if (banCount <= thresholdMedium) { const t = (banCount - thresholdLow) / (thresholdMedium - thresholdLow); const r = lerp(green.r, yellow.r, t); const g = lerp(green.g, yellow.g, t); const b = lerp(green.b, yellow.b, t); return rgbToHex(r, g, b); } // threshold_medium to threshold_high: interpolate yellow → red if (banCount <= thresholdHigh) { const t = (banCount - thresholdMedium) / (thresholdHigh - thresholdMedium); const r = lerp(yellow.r, red.r, t); const g = lerp(yellow.g, red.g, t); const b = lerp(yellow.b, red.b, t); return rgbToHex(r, g, b); } // threshold_high or more: solid red return rgbToHex(red.r, red.g, red.b); }