'use client'; import React from 'react'; // import { red, yellow, green } from '@/components/badge/colors'; import { GraphInsightsPayload, ScoringSummary } from '../types'; import { green1, green2, green3, green4, green5 } from '../colors'; import { ResponsiveContainer, Tooltip, PieChart, Pie, Cell, RadarChart, Radar, PolarGrid, PolarAngleAxis, PolarRadiusAxis, // BarChart, // Bar, // XAxis, // YAxis, // CartesianGrid, // Legend, } from 'recharts'; // avoid platform based charts. // target over arching insights that can // be used to build a mental model of the user's profile // Manual overrides for the radar chart's average ring per category (0–100). // Edit this object to set a custom average for specific categories. // Keys must match the category names exactly as they appear on the radar axes. // Example: const CATEGORY_AVG_OVERRIDES: Record = { "Authenticity": 87, "Activity": 87, "Network": 50, "Sanctions and Criminal": 100, "Telemetry": 100, "Portfolio": 65, "Skills": 72, "Experience": 85, }; // const CATEGORY_AVG_OVERRIDES: Record = {}; const GraphInsights = ({ graphInsights, categories, genre, scoringSummary, headless, }: { graphInsights: GraphInsightsPayload; categories?: string[]; genre: string; scoringSummary?: ScoringSummary; headless?: boolean; }) => { const disableAnimations = React.useMemo(() => { // Disable chart animations for headless/PDF/print to avoid capturing initial frames if (headless) return true; if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') { try { return window.matchMedia('print').matches; } catch {} } return false; }, [headless]); // Nudge ResponsiveContainer to measure after mount in headless contexts React.useEffect(() => { if (typeof window !== 'undefined') { const id = window.setTimeout(() => { try { window.dispatchEvent(new Event('resize')); } catch {} }, 0); return () => window.clearTimeout(id); } }, []); const getCategoryTooltipCopy = (category: string): string => { const name = (category || '').toLowerCase(); // Specific technical facets if (/network|connection|collab|peer/.test(name)) return 'Signals from the developer’s professional connections, collaborations, and peer recognition.'; if (/project|repo|portfolio|work/.test(name)) return 'Signals from a developer’s visible projects, repositories, or published work that indicate breadth and quality of output.'; if (/skill|cert|assessment|endorse/.test(name)) return 'Signals tied to specific technical abilities, such as verified certifications, assessments, or endorsements.'; if (/experience|tenure|history/.test(name)) return 'Signals of tenure and diversity of professional or project involvement over time.'; if (/activity|recency|frequency|engage/.test(name)) return 'Signals of recency and frequency of developer engagement in professional or technical platforms.'; // Specific risk facets if (/sanction|legal|criminal|regulatory|ofac|fbi|watchlist/.test(name)) return 'Signals of legal, criminal, or regulatory red flags linked to an identity.'; if (/identity|authentic|consisten/.test(name)) return 'Signals that indicate whether a developer’s identity is genuine and consistent across platforms.'; if (/reputation|review|rating|feedback|perceive|peer/.test(name)) return 'Signals of how peers, clients, and communities perceive the developer.'; if (/geo|jurisdiction|country|region|ip|location/.test(name)) return 'Signals tied to a developer’s geographic or jurisdictional context.'; if (/security|cyber/.test(name)) return 'Signals of security posture and potential cyber-risk exposure.'; // Generic umbrellas (pick one only) if (/risk/.test(name)) return 'KYD Risk surfaces signals of authenticity, reputation, and environmental telemetry that indicate potential risks in engaging with a developer.'; if (/tech|technical/.test(name)) return 'KYD Technical surfaces signals from a developer’s portfolio, skills, experience, activity, and network to indicate the likelihood of technical capability.'; // Fallback return 'Share of overall contribution by category based on applied weights.'; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any const CategoryPieTooltip = ({ active, payload }: any) => { if (!active || !payload || !payload.length) return null; const p = payload[0]; const entry = p && p.payload ? p.payload : {}; const name = entry.name || p.name || ''; const percent = Math.round(((entry.share || entry.percent || 0) * 100)); const line = getCategoryTooltipCopy(String(name)); return (
{name}
Contributes {percent}% to overall KYD {genre!.charAt(0).toUpperCase() + genre?.slice(1)} Pillar score.
{line}
); }; // eslint-disable-next-line @typescript-eslint/no-explicit-any const RadarCategoryTooltip = ({ active, payload, label }: any) => { if (!active || !payload || !payload.length) return null; const p = payload[0]; const entry = p && p.payload ? p.payload : {}; const name = entry.axis || label || ''; const score = Number(entry.score ?? p.value ?? 0); const avg = entry.avg !== undefined ? Number(entry.avg) : undefined; const line = getCategoryTooltipCopy(String(name)); return (
{name}
Score: {Math.round(score)}{avg !== undefined ? ` • Avg: ${Math.round(avg)}` : ''}
{line}
); }; const allowedCategoriesSet = (() => { const list = Array.isArray(categories) ? categories : undefined; if (!list || list.length === 0) return undefined; const set = new Set(); for (const c of list) if (typeof c === 'string' && c) set.add(c); return set.size > 0 ? set : undefined; })(); const categoryData = (graphInsights?.categoryAggregate || []) .map((c) => ({ category: c.category, value: Number(c.applied_weight_sum || 0) })) .filter((c) => (allowedCategoriesSet ? allowedCategoriesSet.has(c.category) : true)) .filter((c) => c.value > 0) .sort((a, b) => b.value - a.value) .slice(0, 10); // Legend dataset for pie: include ALL categories (even 0%) and compute share const legendData = (() => { const list = (graphInsights?.categoryAggregate || []) .map((c) => ({ name: c.category, value: Number(c.applied_weight_sum || 0) })) .filter((c) => (allowedCategoriesSet ? allowedCategoriesSet.has(c.name) : true)); const total = list.reduce((sum, d) => sum + (d.value || 0), 0); return list .map((d) => ({ ...d, share: total > 0 ? d.value / total : 0 })) .sort((a, b) => (b.share - a.share)); })(); // Radar (spider) chart dataset — category scores out of 100 from backend const radarData = (() => { let percentList = (graphInsights?.categoryScoresPercent || []) as Array<{ category: string; percent?: number }>; // Fallback: derive from scoringSummary.category_scores if graphInsights is missing it if ((!percentList || percentList.length === 0) && scoringSummary && scoringSummary.category_scores) { percentList = Object.entries(scoringSummary.category_scores).map(([cat, entry]) => ({ category: cat, // Prefer business, else combined, else atomic percent_progress percent: Number(entry?.business?.percent_progress), })); } if (!percentList || percentList.length === 0) return [] as Array<{ axis: string; score: number; avg?: number }>; // Build map for quick lookup const percentMap = new Map(); for (const row of percentList) { percentMap.set(row.category, Math.max(0, Math.min(100, Math.round(Number(row?.percent || 0))))); } // Ensure we include all requested section categories, even if 0 const axes: string[] = allowedCategoriesSet ? Array.from(allowedCategoriesSet) : Array.from(new Set(percentList.map((c) => c.category))); const base = axes.map((cat) => ({ axis: cat, score: percentMap.has(cat) ? (percentMap.get(cat) as number) : 0, })); const values = base.map((d) => d.score); const defaultAvg = values.length ? Math.round(values.reduce((a, b) => a + b, 0) / values.length) : 0; const clamp = (n: number) => Math.max(0, Math.min(100, Math.round(Number(n ?? 0)))); return base .map((d) => ({ ...d, avg: Object.prototype.hasOwnProperty.call(CATEGORY_AVG_OVERRIDES, d.axis) ? clamp(CATEGORY_AVG_OVERRIDES[d.axis]) : defaultAvg, })) .slice(0, 8); })(); if (!graphInsights) return null; // Tooltip for axis labels (outside of Recharts' built-in hover zones) const [labelHover, setLabelHover] = React.useState< | { x: number; y: number; name: string; score: number; avg?: number; } | null >(null); const radarLookup = React.useMemo(() => { const map = new Map(); for (const d of radarData) map.set(d.axis, { score: d.score, avg: d.avg }); return map; }, [radarData]); // Custom tick renderer to attach hover handlers to category names // eslint-disable-next-line @typescript-eslint/no-explicit-any const renderAngleTick = (props: any) => { const { payload, x, y, textAnchor } = props; const name: string = payload?.value ?? ''; const info = radarLookup.get(name) || { score: 0, avg: undefined }; return ( setLabelHover({ x, y, name, score: info.score, avg: info.avg })} onMouseMove={() => setLabelHover({ x, y, name, score: info.score, avg: info.avg })} onMouseLeave={() => setLabelHover(null)} > {name} ); }; return (
{/* Spider Chart: Category Balance (genre-scoped) */} {radarData && radarData.length > 2 && (
{/* Spider Chart: Category Scores */}
{genre ? `${genre} ` : ''} Category Contributions - Percentages
The spider diagram displays the KYD {genre} score across its sub-categories, with each point representing the strength of available evidence signals
{/* Primary area */} {/* Subtle average ring (arbitrary benchmark for comparison) */} } /> {!headless && labelHover && (
{labelHover.name}
Score: {Math.round(labelHover.score)}{labelHover.avg !== undefined ? ` • Avg: ${Math.round(labelHover.avg)}` : ''}
{getCategoryTooltipCopy(labelHover.name)}
)}
{/* Custom legend below the spider chart */}
Score
Average
{/* Pie Chart: Category Distribution */}
{genre ? `${genre} ` : ''} Category Contributions - Proportions
The donut diagram illustrates the relative contribution of each {genre} category to the pillar’s composite score.
{(() => { const palette = [green1, green2, green3, green4, green5]; const total = categoryData.reduce((sum, d) => sum + (d.value || 0), 0); const pieData = categoryData.map(d => ({ name: d.category, value: d.value, share: total > 0 ? (d.value / total) : 0, })); // Smaller labels const renderLabel = (props: {x: number, y: number, textAnchor: string, percent: number, name: string, payload: {name: string}}) => { const pct = Math.round(((props?.percent ?? 0) * 100)); if (pct < 4) return null; const name = props?.name ?? props?.payload?.name; const { x, y, textAnchor } = props; return ( {`${name} ${pct}%`} ); }; return ( {pieData.map((_, idx) => ( ))} ); })()} } />
{/* Legend below the pie chart: includes 0% categories */}
{(() => { const palette = [green1, green2, green3, green4, green5]; const pieNames = categoryData.map((d) => d.category); const getColor = (name: string) => { const idx = pieNames.indexOf(name); return idx >= 0 ? palette[idx % palette.length] : 'rgba(128,128,128,0.5)'; }; return (
{legendData.map((item, idx) => { const pct = Math.round(((item?.share ?? 0) * 100)); return (
{item.name} {pct}%
); })}
); })()}
)} {/* Provider Impact: Atomic vs Business (stacked) */} {/* {providersData && providersData.length > 0 && (

Provider Impact

Which platforms most influence this profile. Stacked bars separate objective signal (Atomic) from business-context signal (Business).

)} */}
); }; export default GraphInsights;