"use client";
import { geoPath } from "d3";
import type {
Feature,
FeatureCollection,
GeoJsonProperties,
Geometry,
} from "geojson";
import { AlertTriangle, ChevronDown, Info, MapPin } from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { Card } from "@/components/shadcn/card/card";
import { mapProviderFiltersForFindings } from "@/lib/provider-helpers";
import { HorizontalBarChart } from "./horizontal-bar-chart";
import {
DEFAULT_MAP_COLORS,
LocationPoint,
MAP_CONFIG,
MapColorsConfig,
STATUS_FILTER_MAP,
ThreatMapProps,
} from "./threat-map.types";
import {
createProjection,
createSVGElement,
fetchWorldData,
getMapColors,
} from "./threat-map.utils";
import { BarDataPoint } from "./types";
// Sub-components
function MapTooltip({
location,
position,
}: {
location: LocationPoint;
position: { x: number; y: number };
}) {
return (
{location.name}
{location.failFindings.toLocaleString()} Fail Findings
{location.change !== undefined && (
0 ? "text-pass-primary" : "text-fail-primary"}`}
>
{location.change > 0 ? "+" : ""}
{location.change}%{" "}
since last scan
)}
);
}
function LocationDetails({
location,
onBarClick,
}: {
location: Pick;
onBarClick: (dataPoint: BarDataPoint) => void;
}) {
return (
{location.totalFindings.toLocaleString()} Total Findings
);
}
export function ThreatMap({
data,
height = MAP_CONFIG.defaultHeight,
}: ThreatMapProps) {
const router = useRouter();
const searchParams = useSearchParams();
const svgRef = useRef(null);
const containerRef = useRef(null);
const [selectedLocation, setSelectedLocation] =
useState(null);
const [hoveredLocation, setHoveredLocation] = useState(
null,
);
const [tooltipPosition, setTooltipPosition] = useState<{
x: number;
y: number;
} | null>(null);
const [selectedRegion, setSelectedRegion] = useState("All Regions");
const [worldData, setWorldData] = useState(null);
const [isLoadingMap, setIsLoadingMap] = useState(true);
const [dimensions, setDimensions] = useState<{
width: number;
height: number;
}>({
width: MAP_CONFIG.defaultWidth,
height,
});
const [mapColors, setMapColors] =
useState(DEFAULT_MAP_COLORS);
const isGlobalSelected = selectedRegion.toLowerCase() === "global";
const isAllRegions = selectedRegion === "All Regions";
// For display count only (not used in useEffect to avoid infinite loop)
const locationCount = data.locations.filter((loc) => {
if (loc.region.toLowerCase() === "global") return false;
if (isAllRegions) return true;
if (isGlobalSelected) return false;
return loc.region === selectedRegion;
}).length;
const sortedRegions = [...data.regions].sort((a, b) => {
if (a.toLowerCase() === "global") return -1;
if (b.toLowerCase() === "global") return 1;
return a.localeCompare(b);
});
// Compute global aggregated data
const globalLocations = data.locations.filter(
(loc) => loc.region.toLowerCase() === "global",
);
const globalAggregatedData =
globalLocations.length > 0
? (() => {
const aggregate = (name: string) =>
globalLocations.reduce(
(sum, loc) =>
sum +
(loc.severityData.find((d) => d.name === name)?.value || 0),
0,
);
const failValue = aggregate("Fail");
const passValue = aggregate("Pass");
const total = failValue + passValue;
return {
name: "Global Regions",
regionCode: "global",
providerType: "global",
totalFindings: total,
failFindings: failValue,
severityData: [
{
name: "Fail",
value: failValue,
percentage:
total > 0 ? Math.round((failValue / total) * 100) : 0,
color: "var(--color-bg-fail)",
},
{
name: "Pass",
value: passValue,
percentage:
total > 0 ? Math.round((passValue / total) * 100) : 0,
color: "var(--color-bg-pass)",
},
],
};
})()
: null;
// Reset selected location when region changes
useEffect(() => {
setSelectedLocation(null);
}, [selectedRegion]);
// Theme colors
useEffect(() => {
setMapColors(getMapColors());
const observer = new MutationObserver(() => setMapColors(getMapColors()));
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
return () => observer.disconnect();
}, []);
// Fetch world data
useEffect(() => {
let mounted = true;
fetchWorldData()
.then((d) => mounted && d && setWorldData(d))
.finally(() => mounted && setIsLoadingMap(false));
return () => {
mounted = false;
};
}, []);
// Resize handler
useEffect(() => {
const update = () =>
containerRef.current &&
setDimensions({ width: containerRef.current.clientWidth, height });
update();
window.addEventListener("resize", update);
return () => window.removeEventListener("resize", update);
}, [height]);
// Render map
useEffect(() => {
if (!svgRef.current || !worldData || isLoadingMap) return;
const svg = svgRef.current;
svg.innerHTML = "";
// Compute filtered locations inside useEffect to avoid infinite loop
const isGlobal = selectedRegion.toLowerCase() === "global";
const isAll = selectedRegion === "All Regions";
const locationsToRender = data.locations.filter((loc) => {
if (loc.region.toLowerCase() === "global") return false;
if (isAll) return true;
if (isGlobal) return false;
return loc.region === selectedRegion;
});
const projection = createProjection(dimensions.width, dimensions.height);
const path = geoPath().projection(projection);
// Countries
const mapGroup = createSVGElement("g", {
class: "map-countries",
});
const fillColor = isGlobal ? mapColors.pointDefault : mapColors.landFill;
worldData.features?.forEach(
(feat: Feature) => {
const pathData = path(feat);
if (pathData) {
const el = createSVGElement("path", {
d: pathData,
fill: fillColor,
stroke: mapColors.landStroke,
"stroke-width": "0.5",
});
mapGroup.appendChild(el);
}
},
);
svg.appendChild(mapGroup);
// Helper to create glow rings
const createGlowRing = (
cx: string,
cy: string,
r: number,
color: string,
opacity: string,
) =>
createSVGElement("circle", {
cx,
cy,
r: r.toString(),
fill: "none",
stroke: color,
"stroke-width": "1",
opacity,
});
// Points
const pointsGroup = createSVGElement("g", {
class: "threat-points",
});
const createPoint = (loc: LocationPoint) => {
const proj = projection(loc.coordinates);
if (
!proj ||
proj[0] < 0 ||
proj[0] > dimensions.width ||
proj[1] < 0 ||
proj[1] > dimensions.height
) {
return null;
}
const [x, y] = proj;
const isSelected = selectedLocation?.id === loc.id;
const radius = isSelected
? MAP_CONFIG.selectedPointRadius
: MAP_CONFIG.pointRadius;
const color = isSelected
? mapColors.pointSelected
: mapColors.pointDefault;
const group = createSVGElement("g", {
class: "cursor-pointer",
});
group.appendChild(
createGlowRing(x.toString(), y.toString(), radius + 4, color, "0.4"),
);
group.appendChild(
createGlowRing(x.toString(), y.toString(), radius + 8, color, "0.2"),
);
const circle = createSVGElement("circle", {
cx: x.toString(),
cy: y.toString(),
r: radius.toString(),
fill: color,
});
group.appendChild(circle);
group.addEventListener("click", () =>
setSelectedLocation(isSelected ? null : loc),
);
group.addEventListener("mouseenter", (e) => {
setHoveredLocation(loc);
const rect = svg.getBoundingClientRect();
setTooltipPosition({
x: e.clientX - rect.left,
y: e.clientY - rect.top,
});
});
group.addEventListener("mousemove", (e) => {
const rect = svg.getBoundingClientRect();
setTooltipPosition({
x: e.clientX - rect.left,
y: e.clientY - rect.top,
});
});
group.addEventListener("mouseleave", () => {
setHoveredLocation(null);
setTooltipPosition(null);
});
return group;
};
locationsToRender.forEach((loc) => {
if (selectedLocation?.id !== loc.id) {
const point = createPoint(loc);
if (point) pointsGroup.appendChild(point);
}
});
if (selectedLocation) {
const loc = locationsToRender.find((l) => l.id === selectedLocation.id);
if (loc) {
const point = createPoint(loc);
if (point) pointsGroup.appendChild(point);
}
}
svg.appendChild(pointsGroup);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
dimensions,
data.locations,
selectedRegion,
selectedLocation,
worldData,
isLoadingMap,
mapColors,
]);
const navigateToFindings = (
status: string,
regionCode: string,
providerType?: string,
) => {
const params = new URLSearchParams(searchParams.toString());
mapProviderFiltersForFindings(params);
if (providerType) params.set("filter[provider_type__in]", providerType);
params.set("filter[region__in]", regionCode);
params.set("filter[status__in]", status);
params.set("filter[muted]", "false");
router.push(`/findings?${params.toString()}`);
};
return (
Threat Map
{isLoadingMap ? (
) : (
{hoveredLocation && tooltipPosition && (
)}
{locationCount} Locations
)}
{selectedLocation ? (
{
const status = STATUS_FILTER_MAP[dp.name];
if (status && selectedLocation.providerType) {
navigateToFindings(
status,
selectedLocation.regionCode,
selectedLocation.providerType,
);
}
}}
/>
) : isGlobalSelected && globalAggregatedData ? (
{
const status = STATUS_FILTER_MAP[dp.name];
if (status) {
navigateToFindings(status, "global");
}
}}
/>
) : (
Select a location on the map to view details
)}
);
}