- Task 1: Mark imported blocklist IP addresses
- Add BanOrigin type and _derive_origin() to ban.py model
- Populate origin field in ban_service list_bans() and bans_by_country()
- BanTable and MapPage companion table show origin badge column
- Tests: origin derivation in test_ban_service.py and test_dashboard.py
- Task 2: Add origin filter to dashboard and world map
- ban_service: _origin_sql_filter() helper; origin param on list_bans()
and bans_by_country()
- dashboard router: optional origin query param forwarded to service
- Frontend: BanOriginFilter type + BAN_ORIGIN_FILTER_LABELS in ban.ts
- fetchBans / fetchBansByCountry forward origin to API
- useBans / useMapData accept and pass origin; page resets on change
- BanTable accepts origin prop; DashboardPage adds segmented filter
- MapPage adds origin Select next to time-range picker
- Tests: origin filter assertions in test_ban_service and test_dashboard
100 lines
3.3 KiB
TypeScript
100 lines
3.3 KiB
TypeScript
/**
|
|
* 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);
|
|
}
|