From 551b226ab6baff912cec1e66c24957c058103d5f Mon Sep 17 00:00:00 2001 From: sohailuddinsyed Date: Sun, 17 May 2026 02:11:42 -0700 Subject: [PATCH 1/3] feat(ExperienceDonutChart): redesign chart with labels, hover effects, and validation --- .../ExperienceDonutChart.jsx | 280 ++++++++++++------ 1 file changed, 186 insertions(+), 94 deletions(-) diff --git a/src/components/ExperienceDonutChart/ExperienceDonutChart.jsx b/src/components/ExperienceDonutChart/ExperienceDonutChart.jsx index 8f342c7f02..5b6c4f4fe5 100644 --- a/src/components/ExperienceDonutChart/ExperienceDonutChart.jsx +++ b/src/components/ExperienceDonutChart/ExperienceDonutChart.jsx @@ -1,7 +1,7 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import axios from 'axios'; // Added axios import to fix network request errors -import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts'; +import { PieChart, Pie, Cell, ResponsiveContainer, Sector } from 'recharts'; import styles from './ExperienceDonutChart.module.css'; const SEGMENT_COLORS = [ @@ -16,11 +16,14 @@ const SEGMENT_COLORS = [ const EXPERIENCE_LABELS = ['0-1 years', '1-3 years', '3-5 years', '5+ years']; -// ✅ Crypto-based RNG (safer than Math.random) -function secureRandomInt(min, max) { - const array = new Uint32Array(1); - crypto.getRandomValues(array); - return min + (array[0] % (max - min + 1)); +function getContrastColor(hexColor) { + const hex = hexColor.replace('#', ''); + const bigint = parseInt(hex, 16); + const r = (bigint >> 16) & 255; + const g = (bigint >> 8) & 255; + const b = bigint & 255; + const brightness = (r * 299 + g * 587 + b * 114) / 1000; + return brightness > 150 ? '#111827' : '#ffffff'; } function Spinner() { @@ -32,6 +35,13 @@ function Spinner() { ); } +const TODAY = new Date().toISOString().split('T')[0]; + +const PREFERS_REDUCED_MOTION = + typeof window !== 'undefined' + ? window.matchMedia('(prefers-reduced-motion: reduce)').matches + : false; + export default function ExperienceDonutChart() { const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); @@ -44,7 +54,6 @@ export default function ExperienceDonutChart() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [activeIndex, setActiveIndex] = useState(null); const darkMode = useSelector(state => state.theme.darkMode); const hasFilters = useMemo( @@ -60,17 +69,14 @@ export default function ExperienceDonutChart() { const fetchData = async () => { setLoading(true); setError(null); - setActiveIndex(null); try { const token = localStorage.getItem('token'); if (!token) throw new Error('No token found. Please log in.'); - // Fixed API endpoint path to include /applicant-analytics const url = `${process.env.REACT_APP_APIENDPOINT}/applicant-analytics/experience-breakdown`; const params = {}; - // Replaced undefined filter variables with correctly scoped appliedFilters if (appliedFilters.startDate && appliedFilters.endDate) { params.startDate = appliedFilters.startDate; params.endDate = appliedFilters.endDate; @@ -88,24 +94,23 @@ export default function ExperienceDonutChart() { if (!data || data.length === 0) { setChartData(null); - setLoading(false); + setTotal(0); return; } - // Re-formatted chart data as an array of objects for Recharts compatibility const formattedData = EXPERIENCE_LABELS.map((label, index) => { const found = data.find(d => d.experience === label); return { name: label, value: found ? found.count : 0, - color: SEGMENT_COLORS[index % SEGMENT_COLORS.length], // Fixed case sensitivity for constants + color: SEGMENT_COLORS[index % SEGMENT_COLORS.length], }; }); const totalCount = formattedData.reduce((a, b) => a + b.value, 0); setChartData(formattedData); - setTotal(totalCount); // Added state update for chart center total + setTotal(totalCount); } catch (err) { setError(err.response?.data?.message || err.message || 'Error fetching data.'); setChartData(null); @@ -120,18 +125,127 @@ export default function ExperienceDonutChart() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [appliedFilters]); + const visibleChartData = useMemo(() => chartData?.filter(d => d.value > 0) ?? [], [chartData]); + + // Hide counts until the sweep animation finishes so they don't bleed through + const [animationDone, setAnimationDone] = useState(false); + useEffect(() => { + setAnimationDone(PREFERS_REDUCED_MOTION); + }, [chartData]); + + const [hoveredIndex, setHoveredIndex] = useState(null); + + // Renders the hovered segment with a slightly larger outer radius + const renderActiveShape = props => { + const { cx, cy, innerRadius, outerRadius, startAngle, endAngle, fill } = props; + return ( + + ); + }; + + // Draws the count at the visual center of each segment — only after animation completes + const renderInsideCount = ({ cx, cy, midAngle, innerRadius, outerRadius, value, index }) => { + if (!value || !animationDone) return null; + const isHovered = index === hoveredIndex; + const RADIAN = Math.PI / 180; + const expandedOuter = isHovered ? outerRadius + 10 : outerRadius; + const radius = (innerRadius - (isHovered ? 3 : 0) + expandedOuter) * 0.5; + const x = cx + radius * Math.cos(-midAngle * RADIAN); + const y = cy + radius * Math.sin(-midAngle * RADIAN); + return ( + + {value.toLocaleString()} + + ); + }; + + // Draws name on top line, percentage below — outside the segment, only after animation completes + const renderOutsideLabel = ({ cx, cy, midAngle, outerRadius, name, percent, index }) => { + if (!animationDone) return null; + const isHovered = index === hoveredIndex; + const RADIAN = Math.PI / 180; + const expandedOuter = isHovered ? outerRadius + 10 : outerRadius; + const lineStart = expandedOuter + 8; + const lineEnd = expandedOuter + 50; + const sx = cx + lineStart * Math.cos(-midAngle * RADIAN); + const sy = cy + lineStart * Math.sin(-midAngle * RADIAN); + const ex = cx + lineEnd * Math.cos(-midAngle * RADIAN); + const ey = cy + lineEnd * Math.sin(-midAngle * RADIAN); + const isRight = ex > cx; + const elbowX = ex + (isRight ? 18 : -18); + const textX = elbowX + (isRight ? 4 : -4); + const textAnchor = isRight ? 'start' : 'end'; + const pct = `${(percent * 100).toFixed(1)}%`; + const labelColor = darkMode ? '#f8fafc' : '#0f172a'; + const lineColor = darkMode ? '#94a3b8' : '#64748b'; + const nameFontSize = isHovered ? '1.05rem' : '0.95rem'; + const pctFontSize = isHovered ? '0.95rem' : '0.85rem'; + const strokeWidth = isHovered ? 2.5 : 1.5; + + return ( + + + + + {name} + + + {pct} + + + + ); + }; + const onRolesChange = e => { setSelectedRoles(Array.from(e.target.selectedOptions, o => o.value)); }; const applyFilters = () => { + if (startDate && startDate > TODAY) { + setError('Start date cannot be in the future.'); + return; + } + if (endDate && endDate > TODAY) { + setError('End date cannot be in the future.'); + return; + } if (startDate && endDate && new Date(startDate) > new Date(endDate)) { - setError(null); - setChartData(null); - setTotal(0); - setLoading(false); + setError('Start date must be before end date.'); return; } + setError(null); setAppliedFilters({ startDate, endDate, roles: selectedRoles }); }; @@ -139,51 +253,10 @@ export default function ExperienceDonutChart() { setStartDate(''); setEndDate(''); setSelectedRoles([]); + setError(null); setAppliedFilters({ startDate: '', endDate: '', roles: [] }); }; - const DetailsPanel = () => { - if (!chartData || total === 0) return null; - - return ( -
- {chartData.map((d, idx) => { - const pct = total > 0 ? ((d.value / total) * 100).toFixed(1) : 0; - return ( -
setActiveIndex(idx)} - onMouseLeave={() => setActiveIndex(null)} - > - - {d.name} - {d.value.toLocaleString()} - {pct}% -
- ); - })} -
- ); - }; - - const CustomTooltip = ({ active, payload }) => { - if (!active || !payload?.length) return null; - const d = payload[0]?.payload; - const pct = total > 0 ? ((d.value / total) * 100).toFixed(1) : 0; - - return ( -
- {/* Corrected tooltip to use name and value from payload for visibility */} - {d.name} -
- Count: {d.value} -
- {pct}% of applicants -
- ); - }; - return (
setStartDate(e.target.value)} />
@@ -218,6 +292,7 @@ export default function ExperienceDonutChart() { type="date" className={styles['filter-input']} value={endDate} + max={TODAY} onChange={e => setEndDate(e.target.value)} /> @@ -261,32 +336,51 @@ export default function ExperienceDonutChart() { {loading && } {!loading && !error && chartData && total > 0 && ( - <> -
- - - setActiveIndex(i)} - onMouseLeave={() => setActiveIndex(null)} - > - {chartData.map((d, i) => ( - - ))} - - } /> +
+ + + setAnimationDone(true)} + activeIndex={hoveredIndex} + activeShape={renderActiveShape} + onMouseEnter={(_, index) => setHoveredIndex(index)} + onMouseLeave={() => setHoveredIndex(null)} + > + {visibleChartData.map(d => ( + + ))} + + {/* Inside counts rendered as a second label pass — animation disabled to prevent double-sweep */} + + {visibleChartData.map(d => ( + + ))} + + {animationDone && ( {total.toLocaleString()} - - -
- - - + )} +
+
+
)} {!loading && !error && (!chartData || total === 0) &&

No Data Available 😢

} From ee43e4ff5ce09f573fbe478a2a853dad34980664 Mon Sep 17 00:00:00 2001 From: sohailuddinsyed Date: Sun, 17 May 2026 02:16:36 -0700 Subject: [PATCH 2/3] feat(ExperienceDonutChart): redesign chart with labels, hover effects, and validation --- .../ExperienceDonutChart.jsx | 12 +- .../ExperienceDonutChart.module.css | 481 +++--------------- 2 files changed, 69 insertions(+), 424 deletions(-) diff --git a/src/components/ExperienceDonutChart/ExperienceDonutChart.jsx b/src/components/ExperienceDonutChart/ExperienceDonutChart.jsx index 5b6c4f4fe5..ba6c17d380 100644 --- a/src/components/ExperienceDonutChart/ExperienceDonutChart.jsx +++ b/src/components/ExperienceDonutChart/ExperienceDonutChart.jsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import axios from 'axios'; // Added axios import to fix network request errors import { PieChart, Pie, Cell, ResponsiveContainer, Sector } from 'recharts'; @@ -38,7 +38,7 @@ function Spinner() { const TODAY = new Date().toISOString().split('T')[0]; const PREFERS_REDUCED_MOTION = - typeof window !== 'undefined' + typeof window !== 'undefined' && typeof window.matchMedia === 'function' ? window.matchMedia('(prefers-reduced-motion: reduce)').matches : false; @@ -74,9 +74,11 @@ export default function ExperienceDonutChart() { const token = localStorage.getItem('token'); if (!token) throw new Error('No token found. Please log in.'); + // Fixed API endpoint path to include /applicant-analytics const url = `${process.env.REACT_APP_APIENDPOINT}/applicant-analytics/experience-breakdown`; const params = {}; + // Replaced undefined filter variables with correctly scoped appliedFilters if (appliedFilters.startDate && appliedFilters.endDate) { params.startDate = appliedFilters.startDate; params.endDate = appliedFilters.endDate; @@ -98,19 +100,20 @@ export default function ExperienceDonutChart() { return; } + // Re-formatted chart data as an array of objects for Recharts compatibility const formattedData = EXPERIENCE_LABELS.map((label, index) => { const found = data.find(d => d.experience === label); return { name: label, value: found ? found.count : 0, - color: SEGMENT_COLORS[index % SEGMENT_COLORS.length], + color: SEGMENT_COLORS[index % SEGMENT_COLORS.length], // Fixed case sensitivity for constants }; }); const totalCount = formattedData.reduce((a, b) => a + b.value, 0); setChartData(formattedData); - setTotal(totalCount); + setTotal(totalCount); // Added state update for chart center total } catch (err) { setError(err.response?.data?.message || err.message || 'Error fetching data.'); setChartData(null); @@ -156,6 +159,7 @@ export default function ExperienceDonutChart() { if (!value || !animationDone) return null; const isHovered = index === hoveredIndex; const RADIAN = Math.PI / 180; + // Push centroid outward slightly when hovered to stay centered in the expanded segment const expandedOuter = isHovered ? outerRadius + 10 : outerRadius; const radius = (innerRadius - (isHovered ? 3 : 0) + expandedOuter) * 0.5; const x = cx + radius * Math.cos(-midAngle * RADIAN); diff --git a/src/components/ExperienceDonutChart/ExperienceDonutChart.module.css b/src/components/ExperienceDonutChart/ExperienceDonutChart.module.css index 7cc65e3a1d..f448bbd092 100644 --- a/src/components/ExperienceDonutChart/ExperienceDonutChart.module.css +++ b/src/components/ExperienceDonutChart/ExperienceDonutChart.module.css @@ -1,4 +1,4 @@ -/* stylelint-disable */ +/* stylelint-disable */ /* ===================== Global reset ===================== */ *, *::before, @@ -10,7 +10,7 @@ .experience-chart-container { position: relative; width: 100%; - max-width: 960px; + max-width: 1100px; margin: 2rem auto; padding: 1.75rem; background: #fff; @@ -78,7 +78,7 @@ .filter-actions { flex: 1 1 220px; /* grow/shrink; base width ~220px */ max-width: 320px; /* avoid overly wide inputs */ - min-width: 200px; /* don’t get too tiny */ + min-width: 200px; /* don't get too tiny */ text-align: left; /* labels/inputs left-aligned inside */ } @@ -200,23 +200,27 @@ .chart-area { width: 100%; min-width: 0; - display: flex; /* ← was commented out; enable flex centering */ + display: flex; flex-direction: column; - align-items: center; /* center horizontally */ - justify-content: center; /* center vertically within area */ + align-items: center; + justify-content: center; text-align: center; - padding-right: 100px !important; } .chart-canvas { display: flex; align-items: center; - justify-content: center; /* center the inner wrapper */ + justify-content: center; width: 100%; - max-width: 520px; - aspect-ratio: 1 / 1; /* keeps donut perfectly square */ - min-height: 240px; /* fallback for older browsers */ + max-width: 680px; + aspect-ratio: 1 / 1; + min-height: 400px; margin: 0 auto; + overflow: visible; +} + +.recharts-wrapper { + overflow: visible !important; } .recharts-wrapper, @@ -234,144 +238,6 @@ margin: 0 auto !important; } -/* ===================== Details (below donut) ===================== */ -.chart-details { - max-width: 960px; /* match .experience-chart-container */ - width: 100%; - margin: 1rem auto 0; - padding-inline: 0.5rem; - display: grid; - gap: 0.8rem; - - /* single strategy: fluid columns that never force overflow */ - grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); - place-items: stretch stretch; - overflow-x: hidden; /* safety net */ -} - -/* 2) Let cards fill the grid cell on desktop (remove narrow cap) */ -.detail-item { - display: grid; - grid-template-columns: 12px 1fr auto auto; /* dot | name | count | pct */ - column-gap: 8px; - align-items: center; - width: 100%; - max-width: none; /* fill grid column, don't cap */ - padding: 0.6rem 0.75rem; - - /* text-align: left; */ - - /* prevent overflow on long role names / unbroken strings */ - overflow-wrap: anywhere; - text-align: center; - justify-content: center; -} - -.detail-item:hover, -.detail-item.active { - background: #111827; - border-color: #0b1220; - box-shadow: 0 8px 18px rgb(2 6 23 / 12%); - transform: translateY(-1px); -} - -.detail-item:hover span, -.detail-item.active span { - color: #fff !important; -} - -/* Map children into the grid */ -.detail-dot { - grid-column: 1; - width: 10px; - height: 10px; - border-radius: 999px; - box-shadow: 0 0 0 2px rgb(255 255 255 / 70%); -} - -.detail-name { - grid-column: 2; - font-weight: 800; - color: #0f172a; - font-size: 0.9rem; -} - -.detail-sep { - display: none; -} /* we don’t need the bullet in a grid layout */ -.detail-count { - grid-column: 3; - font-weight: 800; - color: #0f172a; -} - -.detail-sub { - display: none; -} /* optional: hide to keep cards compact; show if you prefer */ -.detail-pct { - grid-column: 4; - margin-left: 0; - font-weight: 800; - color: #2563eb; -} - -/* .detail-item:hover, -.detail-item.active { - background: #111827; - border-color: #0b1220; - box-shadow: 0 8px 18px rgba(2,6,23,.12); - transform: translateY(-1px); -} */ - -/* .detail-item:hover span, -.detail-item.active span { color: #ffffff !important; } */ -.detail-item:hover .detail-pct, -.detail-item.active .detail-pct { - color: #93c5fd !important; -} /* keep contrast on hover */ - -@media (width <= 640px) { - .chart-details { - grid-template-columns: 1fr; - } - - .detail-item { - display: flex; /* simpler on mobile */ - flex-wrap: wrap; - justify-content: center; - text-align: center; - gap: 0.45rem 0.6rem; - } - - .detail-dot { - order: 1; - } - - .detail-name { - order: 2; - width: 100%; - font-size: 1rem; - } - - .detail-sep { - display: none; - } - - .detail-count { - order: 3; - } - - .detail-sub { - order: 4; - display: inline; - color: #475569; - } - - .detail-pct { - order: 5; - } -} - /* ===================== States ===================== */ .spinner-container { display: flex; @@ -403,200 +269,25 @@ margin: 0; } -.error-container { - padding: 2rem; - text-align: center; -} - -.error-message { - color: #dc2626; - font-weight: 700; - font-size: 0.95rem; - background: #fef2f2; - border: 1px solid #fecaca; - border-radius: 10px; - padding: 1.1rem 1.25rem; - display: inline-block; -} - -.no-data-container { - padding: 3rem 2rem; - text-align: center; -} - -.no-data-message { - font-size: 1.15rem; - font-weight: 800; - color: #6b7280; - margin: 0 0 0.4rem; -} - -.no-data-subtitle { - font-size: 0.95rem; - color: #9ca3af; - margin: 0; - font-style: italic; -} - -/* ===================== Tooltip ===================== */ - -/* Adjusted tooltip for Light Mode: Light background with dark text for contrast */ -.custom-tooltip { - background: rgb(255 255 255 / 96%) !important; - border: 1px solid #e2e8f0 !important; - border-radius: 10px !important; - padding: 0.9rem 1rem !important; - box-shadow: 0 12px 28px rgb(2 6 23 / 15%) !important; - backdrop-filter: blur(10px); - color: #0f172a !important; -} - -.tooltip-content { - color: #fff !important; - margin: 0 !important; - font-size: 0.875rem !important; - line-height: 1.5 !important; -} - -/* ===================== Slices ===================== */ -.pie-cell { - transition: opacity 0.15s ease, filter 0.15s ease, transform 0.15s ease; - cursor: pointer; -} - -.pie-cell:hover { - opacity: 0.9; - filter: brightness(1.05); -} - -/* .detail-item { text-align: center; justify-content: center; } */ - -/* ===================== Responsive tweaks ===================== */ -@media (width <= 640px) { - .filter-group, - .filter-actions { - max-width: 320px; - min-width: 220px; - } - - .chart-canvas { - max-width: 360px; - min-height: 220px; - } - - .chart-details { - grid-template-columns: 1fr; - } /* single column for clarity */ -} - -/* ===================== Print & Accessibility ===================== */ +/* ===================== Print & prefers-color-scheme ===================== */ @media print { - .experience-chart-container { + .experience-donut-chart-dark-mode .experience-chart-container { + background: #fff; + color: #0f172a; box-shadow: none; border: 1px solid #cbd5e1; } - - .filter-section { - display: none; - } -} - -@media (prefers-contrast: more) { - .experience-chart-container { - border: 2px solid #000; - } - - .filter-input, - .filter-select { - border: 2px solid #000; - } } -@media (prefers-reduced-motion: reduce) { - .spinner { - animation: none; - } - - .pie-cell, - .btn, - .filter-input, - .filter-select { - transition: none; - } -} - -@media (width <= 640px) { - .chart-details { - grid-template-columns: 1fr; - } - - .detail-item { - display: flex; - flex-wrap: wrap; - justify-content: center; - text-align: center; - gap: 0.45rem 0.6rem; - } - - .detail-name { - width: 100%; - font-size: 1rem; - } -} - -/* --- Ensure detail cards always show a visible background --- */ -.chart-details .detail-item { - background-color: #f1f5f9; /* stronger than #f8fafc, more visible */ - border: 1px solid #e2e8f0; - border-radius: 10px; - box-shadow: 0 2px 8px rgb(2 6 23 / 6%); - background-clip: padding-box; - transition: background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.2s ease, - transform 0.15s ease; -} - -/* Keep dark hover/active but ensure contrast is clear */ -.chart-details .detail-item:hover, -.chart-details .detail-item.active { - background-color: #111827; - border-color: #0b1220; - box-shadow: 0 8px 18px rgb(2 6 23 / 12%); -} - -/* If your app has global card or utility classes that might strip bg, - this raises specificity a bit further without !important. */ -.experience-chart-container .chart-details .detail-item { - background-color: #f1f5f9; -} - -/* Optional: high-contrast users still get borders */ -@media (prefers-contrast: more) { - .chart-details .detail-item { - border-color: #0f172a; +/* Keep prefers-color-scheme consistent with your chosen dark */ +@media (prefers-color-scheme: dark) { + .experience-donut-chart-dark-mode .experience-chart-container { + background: #1c2441; } } -/* --- Make chart-driven active state win over base card styles --- */ -.chart-details .detail-item.active { - background-color: #111827 !important; /* win against base bg */ - border-color: #0b1220 !important; - box-shadow: 0 8px 18px rgb(2 6 23 / 12%); - color: #fff; -} - -.chart-details .detail-item.active span { - color: #fff !important; /* keep text readable */ -} - -/* Optional: smooth the state change */ - -/* .chart-details .detail-item { - transition: background-color .15s ease, border-color .15s ease, box-shadow .2s ease, transform .15s ease; -} */ - /* ===================== DARK MODE (scoped to wrapper) ===================== */ -/* Ensure every surface flips dark */ .experience-donut-chart.experience-donut-chart-dark-mode, .experience-donut-chart-dark-mode, .experience-donut-chart-dark-mode .experience-chart-container, @@ -610,7 +301,6 @@ margin-top: -35px; } -/* Top gradient bar */ .experience-donut-chart-dark-mode .experience-chart-container::before { background: linear-gradient( 90deg, @@ -622,16 +312,15 @@ ); } -/* Title & header */ .experience-donut-chart-dark-mode .chart-title { - color: #fff !important; /* “Applicants by Experience” */ + color: #fff !important; } .experience-donut-chart-dark-mode .chart-header { color: #f1f5f9; } -/* ===================== Filters ===================== */ +/* ===================== Filters (dark) ===================== */ .experience-donut-chart-dark-mode .filter-section { background: #232d53; border-color: #3b4a75; @@ -660,12 +349,11 @@ box-shadow: 0 0 0 4px rgb(96 165 250 / 25%); } -/* Date input icons */ .experience-donut-chart-dark-mode .filter-input[type='date']::-webkit-calendar-picker-indicator { filter: invert(1) opacity(0.9); } -/* ===================== Buttons ===================== */ +/* ===================== Buttons (dark) ===================== */ .experience-donut-chart-dark-mode .btn { color: #e5e7eb; } @@ -689,70 +377,18 @@ box-shadow: 0 6px 20px rgb(0 0 0 / 35%); } -/* ===================== Chart canvas & Recharts ===================== */ +/* ===================== Chart canvas (dark) ===================== */ .experience-donut-chart-dark-mode .chart-canvas { background: transparent; } -/* Default Recharts labels/ticks/legend */ .experience-donut-chart-dark-mode .recharts-text, -.experience-donut-chart-dark-mode .recharts-label, -.experience-donut-chart-dark-mode .recharts-legend-item-text { +.experience-donut-chart-dark-mode .recharts-label { fill: #e5e7eb !important; color: #e5e7eb !important; } -/* Grid lines if any */ -.experience-donut-chart-dark-mode .recharts-cartesian-grid-horizontal line, -.experience-donut-chart-dark-mode .recharts-cartesian-grid-vertical line { - stroke: #3b4a75 !important; -} - -/* Strong contrast on hover/active (details + svg labels) */ -.experience-donut-chart-dark-mode .chart-details .detail-item:hover span, -.experience-donut-chart-dark-mode .chart-details .detail-item.active span { - color: #fff !important; -} - -.experience-donut-chart-dark-mode .recharts-sector:hover text, -.experience-donut-chart-dark-mode .recharts-pie-sector:hover text { - fill: #fff !important; -} - -/* ===================== Details (cards under chart) ===================== */ -.experience-donut-chart-dark-mode .chart-details { - color: #e5e7eb; -} - -.experience-donut-chart-dark-mode .chart-details .detail-item { - background-color: #2a355f; - border: 1px solid #3b4a75; - color: #e2e8f0; - box-shadow: 0 2px 8px rgb(0 0 0 / 35%); -} - -.experience-donut-chart-dark-mode .detail-name, -.experience-donut-chart-dark-mode .detail-count { - color: #e5e7eb; -} - -.experience-donut-chart-dark-mode .detail-item .detail-pct { - color: #93c5fd; -} - -.experience-donut-chart-dark-mode .detail-dot { - box-shadow: 0 0 0 2px rgb(28 36 65 / 70%); -} - -/* Hover/active state harmonized with palette */ -.experience-donut-chart-dark-mode .chart-details .detail-item:hover, -.experience-donut-chart-dark-mode .chart-details .detail-item.active { - background-color: #232d53; - border-color: #1c2441; - box-shadow: 0 8px 18px rgb(0 0 0 / 45%); -} - -/* ===================== States (loading, error, empty) ===================== */ +/* ===================== States (dark) ===================== */ .experience-donut-chart-dark-mode .spinner { border-color: #2a355f; border-top-color: #60a5fa; @@ -768,41 +404,46 @@ border-color: #7f2a3a; } -.experience-donut-chart-dark-mode .no-data-message { - color: #dbe3ff; -} - -.experience-donut-chart-dark-mode .no-data-subtitle { - color: #b8c2ea; +/* ===================== Slices ===================== */ +.pie-cell { + transition: opacity 0.15s ease, filter 0.15s ease; } -/* ===================== Tooltip ===================== */ +/* ===================== Responsive ===================== */ +@media (width <= 640px) { + .filter-group, + .filter-actions { + max-width: 320px; + min-width: 220px; + } -/* Adjusted tooltip for Dark Mode: Scoped to .experience-donut-chart-dark-mode */ -.experience-donut-chart-dark-mode .custom-tooltip { - background-color: rgb(42 53 95 / 96%) !important; - color: #f1f5f9 !important; - border: 1px solid #3b4a75 !important; - box-shadow: 0 12px 28px rgb(0 0 0 / 45%) !important; + .chart-canvas { + max-width: 360px; + min-height: 260px; + } } -.experience-donut-chart-dark-mode .custom-tooltip strong { - color: #fff !important; -} +/* ===================== Accessibility ===================== */ +@media (prefers-contrast: more) { + .experience-chart-container { + border: 2px solid #000; + } -/* ===================== Print & prefers-color-scheme ===================== */ -@media print { - .experience-donut-chart-dark-mode .experience-chart-container { - background: #fff; - color: #0f172a; - box-shadow: none; - border: 1px solid #cbd5e1; + .filter-input, + .filter-select { + border: 2px solid #000; } } -/* Keep prefers-color-scheme consistent with your chosen dark */ -@media (prefers-color-scheme: dark) { - .experience-donut-chart-dark-mode .experience-chart-container { - background: #1c2441; +@media (prefers-reduced-motion: reduce) { + .spinner { + animation: none; + } + + .btn, + .filter-input, + .filter-select, + .pie-cell { + transition: none; } } From f0a4435b36d3f1ebbe5873271c89764f8c039f3d Mon Sep 17 00:00:00 2001 From: sohailuddinsyed Date: Thu, 21 May 2026 18:01:44 -0700 Subject: [PATCH 3/3] fix(ExperienceDonutChart): address reviewer feedback on UI and filter bugs --- .../ExperienceDonutChart.jsx | 221 +++++++++++------- .../ExperienceDonutChart.module.css | 110 ++++++++- 2 files changed, 237 insertions(+), 94 deletions(-) diff --git a/src/components/ExperienceDonutChart/ExperienceDonutChart.jsx b/src/components/ExperienceDonutChart/ExperienceDonutChart.jsx index ba6c17d380..45a8aa0b01 100644 --- a/src/components/ExperienceDonutChart/ExperienceDonutChart.jsx +++ b/src/components/ExperienceDonutChart/ExperienceDonutChart.jsx @@ -61,9 +61,12 @@ export default function ExperienceDonutChart() { Boolean( appliedFilters.startDate || appliedFilters.endDate || - (appliedFilters.roles?.length ?? 0) > 0, + (appliedFilters.roles?.length ?? 0) > 0 || + startDate || + endDate || + selectedRoles.length > 0, ), - [appliedFilters], + [appliedFilters, startDate, endDate, selectedRoles], ); const fetchData = async () => { @@ -138,6 +141,19 @@ export default function ExperienceDonutChart() { const [hoveredIndex, setHoveredIndex] = useState(null); + const [isMobile, setIsMobile] = useState( + typeof window !== 'undefined' && window.innerWidth < 450, + ); + + useEffect(() => { + const handleResize = () => setIsMobile(window.innerWidth < 450); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + const pieMargin = isMobile + ? { top: 5, right: 5, bottom: 5, left: 5 } + : { top: 20, right: 115, bottom: 20, left: 115 }; + // Renders the hovered segment with a slightly larger outer radius const renderActiveShape = props => { const { cx, cy, innerRadius, outerRadius, startAngle, endAngle, fill } = props; @@ -172,7 +188,7 @@ export default function ExperienceDonutChart() { dominantBaseline="central" fill={getContrastColor(visibleChartData[index]?.color ?? '#000')} style={{ - fontSize: isHovered ? '1.2rem' : '1.05rem', + fontSize: isHovered ? '1.35rem' : '1.2rem', fontWeight: 800, pointerEvents: 'none', transition: 'font-size 0.15s ease', @@ -186,11 +202,13 @@ export default function ExperienceDonutChart() { // Draws name on top line, percentage below — outside the segment, only after animation completes const renderOutsideLabel = ({ cx, cy, midAngle, outerRadius, name, percent, index }) => { if (!animationDone) return null; + // On very small screens hide outside labels — inside counts are still visible + if (isMobile) return null; const isHovered = index === hoveredIndex; const RADIAN = Math.PI / 180; const expandedOuter = isHovered ? outerRadius + 10 : outerRadius; const lineStart = expandedOuter + 8; - const lineEnd = expandedOuter + 50; + const lineEnd = expandedOuter + (isMobile ? 30 : 50); const sx = cx + lineStart * Math.cos(-midAngle * RADIAN); const sy = cy + lineStart * Math.sin(-midAngle * RADIAN); const ex = cx + lineEnd * Math.cos(-midAngle * RADIAN); @@ -202,8 +220,8 @@ export default function ExperienceDonutChart() { const pct = `${(percent * 100).toFixed(1)}%`; const labelColor = darkMode ? '#f8fafc' : '#0f172a'; const lineColor = darkMode ? '#94a3b8' : '#64748b'; - const nameFontSize = isHovered ? '1.05rem' : '0.95rem'; - const pctFontSize = isHovered ? '0.95rem' : '0.85rem'; + const nameFontSize = isHovered ? '1.2rem' : '1.05rem'; + const pctFontSize = isHovered ? '1.05rem' : '0.95rem'; const strokeWidth = isHovered ? 2.5 : 1.5; return ( @@ -232,10 +250,6 @@ export default function ExperienceDonutChart() { ); }; - const onRolesChange = e => { - setSelectedRoles(Array.from(e.target.selectedOptions, o => o.value)); - }; - const applyFilters = () => { if (startDate && startDate > TODAY) { setError('Start date cannot be in the future.'); @@ -302,22 +316,32 @@ export default function ExperienceDonutChart() {
- - +
+ Roles +
+ {[ + 'Frontend Developer', + 'DevOps Engineer', + 'Project Manager', + 'Junior Developer', + 'Full Stack Developer', + ].map(role => ( + + ))} +
+
@@ -326,7 +350,9 @@ export default function ExperienceDonutChart() { Apply