'use client'; import React from 'react'; import { green, red } from '../colors'; type CategoryBarsProps = { title: string; categories: string[]; // eslint-disable-next-line @typescript-eslint/no-explicit-any categoryScores: Record; getCategoryTooltipCopy: (category: string) => string; description?: string; barHeight?: number; // px height for the filled bar categoryTargets?: Record; // desired target lengths (0-100) linkToAppendix?: boolean; /** Extra classes for the flex column that lays out rows with justify-between (taller column = more space between bars). */ tracksClassName?: string; }; const CategoryBars: React.FC = ({ title, categories, categoryScores, getCategoryTooltipCopy, description, barHeight = 6, categoryTargets, linkToAppendix = true, tracksClassName, }) => { return (
{title}
{description || "Each bar represents a category's net evidence: positive values extend right in green, negative values extend left, and bar length denotes contribution magnitude."} {categoryTargets && Object.keys(categoryTargets).length > 0 ? ( <> {' '}Target overlays indicate desired evidence lengths for this role. ) : null}
{categories.map((category) => { const score = categoryScores[category] || {}; const signed = Math.round( Number( score?.business?.percent_progress_signed ?? score?.combined?.percent_progress_signed ?? score?.business?.percent_progress ?? score?.combined?.percent_progress ?? 0 ) ); const candidatePercentRaw = Math.round( Number( score?.business?.percent_progress ?? score?.combined?.percent_progress ?? 0 ) ); const candidateClamp = Math.max(0, Math.min(100, candidatePercentRaw)); const absPercent = Math.max(0, Math.min(Math.abs(signed), 100)); const hasData = !!(score?.business || score?.combined) && signed !== 0; const isNegative = signed < 0; const label = !hasData ? 'No Data' : signed <= -61 ? 'Strong Evidence' : signed <= -21 ? 'Consistent Evidence' : signed <= 19 ? 'Mixed Signals' : signed <= 59 ? 'Consistent Evidence' : 'Strong Evidence'; const fillWidth = absPercent / 2; // half-bar represents 100% const left = isNegative ? `calc(50% - ${fillWidth}%)` : '50%'; const targetRaw = categoryTargets?.[category]; const hasTarget = typeof targetRaw === 'number' && isFinite(targetRaw); const targetClamp = hasTarget ? Math.max(0, Math.min(100, Math.round(targetRaw as number))) : 0; const targetWidth = targetClamp / 2; // same scale as fillWidth const handleClick = (e: React.MouseEvent) => { if (!linkToAppendix) return; // Navigate to Appendix -> Skills category anchor try { if (typeof window !== 'undefined') { const anchor = `#appendix-skills-cat-${encodeURIComponent(category.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''))}`; const url = `#appendix${anchor}`; window.location.hash = url; } } catch {} }; return (
{/* signed fill originating from center, or full grey fill when no data */} {!hasData ? (
) : (
)} {/* role target overlay: positive side only, dashed outline (always above fill) */} {hasTarget && (
{category}
{label}
{getCategoryTooltipCopy(category)}
{hasTarget && (
Target: {targetClamp}%
Candidate Match: {candidateClamp}%
)}
); })}
); }; export default CategoryBars;