"use client"; import { Bell } from "lucide-react"; import { useState } from "react"; import { CartesianGrid, Line, LineChart as RechartsLine, TooltipProps, XAxis, YAxis, } from "recharts"; import { ChartConfig, ChartContainer, ChartTooltip, } from "@/components/ui/chart/Chart"; import { AlertPill } from "./shared/alert-pill"; import { ChartLegend } from "./shared/chart-legend"; import { CustomActiveDot, PointClickData } from "./shared/custom-active-dot"; import { AXIS_FONT_SIZE, CustomXAxisTickWithToday, } from "./shared/custom-axis-tick"; import { CustomDot } from "./shared/custom-dot"; import { LineConfig, LineDataPoint } from "./types"; interface LineChartProps { data: LineDataPoint[]; lines: LineConfig[]; height?: number; xAxisInterval?: number | "preserveStart" | "preserveEnd" | "preserveStartEnd"; onPointClick?: (data: PointClickData) => void; } interface TooltipPayloadItem { dataKey: string; value: number; stroke: string; name: string; payload: LineDataPoint; } const formatTooltipDate = (dateStr: string) => { const date = new Date(dateStr); return date.toLocaleDateString("en-US", { month: "short", day: "numeric", }); }; interface CustomLineTooltipProps extends TooltipProps { filterLine?: string | null; } const CustomLineTooltip = ({ active, payload, label, filterLine, }: CustomLineTooltipProps) => { if (!active || !payload || payload.length === 0) { return null; } const typedPayload = payload as unknown as TooltipPayloadItem[]; // Filter payload if a line is selected or hovered const filteredPayload = filterLine ? typedPayload.filter((item) => item.dataKey === filterLine) : typedPayload; // Sort by severity order: critical, high, medium, low, informational const severityOrder = [ "critical", "high", "medium", "low", "informational", ] as const; const displayPayload = [...filteredPayload].sort((a, b) => { const aIndex = severityOrder.indexOf( a.dataKey as (typeof severityOrder)[number], ); const bIndex = severityOrder.indexOf( b.dataKey as (typeof severityOrder)[number], ); // Items not in severityOrder go to the end if (aIndex === -1) return 1; if (bIndex === -1) return -1; return aIndex - bIndex; }); if (displayPayload.length === 0) { return null; } const totalValue = displayPayload.reduce((sum, item) => sum + item.value, 0); const formattedDate = formatTooltipDate(String(label)); return (

{formattedDate}

{displayPayload.map((item) => { const newFindings = item.payload[`${item.dataKey}_newFindings`]; const change = item.payload[`${item.dataKey}_change`]; return (
{item.name}
{item.value}
{newFindings !== undefined && (
{newFindings} New Findings
)} {change !== undefined && typeof change === "number" && (

{(change as number) > 0 ? "+" : ""} {change}% {" "} Since Last Scan

)}
); })}
); }; const chartConfig = { default: { color: "var(--color-bg-data-azure)", }, } satisfies ChartConfig; export function LineChart({ data, lines, height = 400, xAxisInterval = "preserveStartEnd", onPointClick, }: LineChartProps) { const [hoveredLine, setHoveredLine] = useState(null); const [selectedLine, setSelectedLine] = useState(null); // Active line is either selected (persistent) or hovered (temporary) const activeLine = selectedLine ?? hoveredLine; const legendItems = lines.map((line) => ({ label: line.label, color: line.color, dataKey: line.dataKey, })); const handleLegendClick = (dataKey: string) => { // Toggle selection: if already selected, deselect; otherwise select setSelectedLine((current) => (current === dataKey ? null : dataKey)); }; return (
( )} /> } /> {lines.map((line) => { const isActive = activeLine === line.dataKey; const isFaded = activeLine !== null && !isActive; return ( ( )} activeDot={(props: { cx?: number; cy?: number; payload?: LineDataPoint; }) => ( setHoveredLine(line.dataKey)} onMouseLeave={() => setHoveredLine(null)} /> )} style={{ transition: "stroke-opacity 0.2s" }} /> ); })}

Click to filter by severity

); }