Skip to content

Commit 4e9d699

Browse files
authored
feat(compare): add animated Developer Skills Radar Chart using recharts (use best to best tech) (JhaSourav07#3086)
### Description This PR introduces an interactive, animated **Developer Skills Radar Chart** (Spider Web chart) to the Compare Developers page, transforming raw stat comparisons into a stunning visual "developer shape" overlay. Fixes JhaSourav07#3079 Currently, users must mentally parse 8+ individual `StatBattle` cards to form an overall opinion of two developers. This chart solves that by mapping five distinct skill dimensions onto a single **overlapping polygon chart**, letting users instantly see who dominates in which area — at a single glance. **🧠 The 5 Skill Dimensions (Normalized 0–100):** | Skill | Formula | What it Measures | |---|---|---| | **Volume** | `totalContributions + LoC additions` | Raw output and code throughput | | **Consistency** | `currentStreak × 2 + peakStreak` | Long-term coding discipline | | **Impact** | `stars × 3 + followers` | Community reach and project influence | | **Collaboration** | `PRs × 2 + issues` | Teamwork through code reviews and bug tracking | | **Versatility** | `uniqueLanguages × 20` | Breadth of tech stack knowledge | All values are normalized against `Math.max(user1, user2)` per dimension, ensuring fair visual comparison regardless of absolute numbers (a 50k-commit veteran vs. a 500-commit newcomer won't break the chart). ### Pillar - [x] 🎨 Pillar 1 — New Theme Design - [ ] 📐 Pillar 2 — Geometric SVG Improvement - [ ] 🕐 Pillar 3 — Timezone Logic Optimization - [x] 🛠️ Other (Feature Enhancement, UI Upgrades) ### 🚀 What's Changed #### New Dependency - **`recharts`** — Lightweight, React-native charting library with built-in SVG animations. Chosen over Chart.js for its declarative component API and zero-config animation support. #### `app/compare/CompareClient.tsx` - **`normalizeSkill()`** — Pure utility function that clamps any raw stat into a 0–100 range relative to the max of both users. - **`DeveloperSkillsRadar`** — New component rendering a `<RadarChart>` with: - **Two overlapping `<Radar>` polygons:** - User 1 → Violet (`#8B5CF6`, 25% fill opacity, 2px stroke) - User 2 → Cyan (`#06B6D4`, 25% fill opacity, 2px stroke) - **`<PolarGrid>`** with dashed stroke lines for a clean, modern aesthetic. - **`<PolarAngleAxis>`** with custom `11px` / `600 weight` label styling. - **`<Tooltip>`** with dark-themed glassmorphic styling (`#0a0a0a` bg, 10px border-radius). - **Dot markers** (4px radius) on each vertex for precision. - **1200ms ease-out** SVG entry animation via recharts built-in `animationDuration`. - Wrapped in **Framer Motion** `<motion.div>` for scroll-triggered fade-in (`whileInView`). - **Responsive:** Uses `<ResponsiveContainer width="100%" height={380}>` for all screen sizes. - **Placement:** Between `CodeVolumeShowdown` and `LanguageComparison` for logical visual flow. ### Screenshots > ⚠️ _Add a screenshot of the radar chart here after testing locally._ <img width="898" height="490" alt="image" src="https://github.com/user-attachments/assets/68c42acb-f930-42b9-b036-2b9e6ff8e925" /> ### 🧪 How I Tested 1. Ran `npm run dev` and navigated to `/compare?user1=torvalds&user2=github`. 2. Verified that both polygons render correctly with overlapping colors. 3. Hovered on each vertex to confirm tooltip shows correct skill name and score. 4. Tested responsiveness by resizing the browser window — chart scales proportionally. 5. Verified that edge cases work (user with 0 PRs, user with 0 languages). 6. Confirmed no console errors or hydration mismatches. ### Checklist before requesting a review: - [x] I have read the [CONTRIBUTING.md](../CONTRIBUTING.md) file. - [x] I have tested these changes locally. - [x] I have run `npm run format` and `npm run lint` locally and resolved all errors. - [x] My commits follow the [Conventional Commits](https://www.conventionalcommits.org/) format. - [x] I have updated README.md if I added a new theme or URL parameter. - [x] I have started the repo. - [x] I have made sure that I have only one commit to merge in this PR. - [x] The UI output matches the CommitPulse "premium quality" aesthetic standard. - [x] (Recommended) I joined the [CommitPulse Discord](https://discord.gg/commitpulse) community.
2 parents 786481e + 81eea11 commit 4e9d699

3 files changed

Lines changed: 443 additions & 4 deletions

File tree

app/compare/CompareClient.tsx

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
import { useState, useEffect, useCallback } from 'react';
44
import { motion, AnimatePresence } from 'framer-motion';
5+
import {
6+
Radar,
7+
RadarChart,
8+
PolarGrid,
9+
PolarAngleAxis,
10+
PolarRadiusAxis,
11+
ResponsiveContainer,
12+
Tooltip,
13+
} from 'recharts';
514
import { useSearchParams, useRouter } from 'next/navigation';
615
import {
716
Search,
@@ -624,6 +633,156 @@ function CodeVolumeShowdown({ user1, user2 }: { user1: CompareUserData; user2: C
624633
);
625634
}
626635

636+
/* ── helper: developer skills radar chart ──────────────────────────────── */
637+
638+
function normalizeSkill(value: number, max: number): number {
639+
if (max <= 0) return 0;
640+
return Math.min(Math.round((value / max) * 100), 100);
641+
}
642+
643+
function DeveloperSkillsRadar({
644+
user1,
645+
user2,
646+
}: {
647+
user1: CompareUserData;
648+
user2: CompareUserData;
649+
}) {
650+
// Compute raw values for each skill dimension
651+
const calcLoC = (activity: ActivityData[]) => {
652+
let add = 0;
653+
activity.forEach((d) => {
654+
add += d.locAdditions || 0;
655+
});
656+
return add;
657+
};
658+
659+
const raw1 = {
660+
volume: user1.stats.totalContributions + calcLoC(user1.activity),
661+
consistency: user1.stats.currentStreak * 2 + user1.stats.peakStreak,
662+
impact: user1.profile.stats.stars * 3 + user1.profile.stats.followers,
663+
collaboration: (user1.stats.totalPRs || 0) * 2 + (user1.stats.totalIssues || 0),
664+
versatility: user1.languages.length * 20,
665+
};
666+
667+
const raw2 = {
668+
volume: user2.stats.totalContributions + calcLoC(user2.activity),
669+
consistency: user2.stats.currentStreak * 2 + user2.stats.peakStreak,
670+
impact: user2.profile.stats.stars * 3 + user2.profile.stats.followers,
671+
collaboration: (user2.stats.totalPRs || 0) * 2 + (user2.stats.totalIssues || 0),
672+
versatility: user2.languages.length * 20,
673+
};
674+
675+
// Normalize against combined max for fair comparison
676+
const maxVolume = Math.max(raw1.volume, raw2.volume, 1);
677+
const maxConsistency = Math.max(raw1.consistency, raw2.consistency, 1);
678+
const maxImpact = Math.max(raw1.impact, raw2.impact, 1);
679+
const maxCollaboration = Math.max(raw1.collaboration, raw2.collaboration, 1);
680+
const maxVersatility = Math.max(raw1.versatility, raw2.versatility, 1);
681+
682+
const radarData = [
683+
{
684+
skill: 'Volume',
685+
user1: normalizeSkill(raw1.volume, maxVolume),
686+
user2: normalizeSkill(raw2.volume, maxVolume),
687+
},
688+
{
689+
skill: 'Consistency',
690+
user1: normalizeSkill(raw1.consistency, maxConsistency),
691+
user2: normalizeSkill(raw2.consistency, maxConsistency),
692+
},
693+
{
694+
skill: 'Impact',
695+
user1: normalizeSkill(raw1.impact, maxImpact),
696+
user2: normalizeSkill(raw2.impact, maxImpact),
697+
},
698+
{
699+
skill: 'Collaboration',
700+
user1: normalizeSkill(raw1.collaboration, maxCollaboration),
701+
user2: normalizeSkill(raw2.collaboration, maxCollaboration),
702+
},
703+
{
704+
skill: 'Versatility',
705+
user1: normalizeSkill(raw1.versatility, maxVersatility),
706+
user2: normalizeSkill(raw2.versatility, maxVersatility),
707+
},
708+
];
709+
710+
return (
711+
<motion.div
712+
initial={{ opacity: 0, y: 20 }}
713+
whileInView={{ opacity: 1, y: 0 }}
714+
viewport={{ once: true }}
715+
transition={{ duration: 0.6 }}
716+
>
717+
<h2 className="text-xs text-[#A1A1AA] uppercase tracking-widest font-medium mb-4 flex items-center gap-2">
718+
<Trophy size={14} className="text-amber-400" />
719+
Developer Skills Radar
720+
</h2>
721+
<div className="p-6 rounded-xl bg-white dark:bg-[#0a0a0a] border border-black/10 dark:border-[rgba(255,255,255,0.08)]">
722+
{/* Legend */}
723+
<div className="flex justify-center gap-6 mb-4">
724+
<div className="flex items-center gap-2">
725+
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: '#8B5CF6' }} />
726+
<span className="text-xs text-[#A1A1AA] font-medium">@{user1.profile.username}</span>
727+
</div>
728+
<div className="flex items-center gap-2">
729+
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: '#06B6D4' }} />
730+
<span className="text-xs text-[#A1A1AA] font-medium">@{user2.profile.username}</span>
731+
</div>
732+
</div>
733+
734+
<ResponsiveContainer width="100%" height={380}>
735+
<RadarChart cx="50%" cy="50%" outerRadius="75%" data={radarData}>
736+
<PolarGrid stroke="rgba(161,161,170,0.15)" strokeDasharray="3 3" />
737+
<PolarAngleAxis
738+
dataKey="skill"
739+
tick={{
740+
fill: '#A1A1AA',
741+
fontSize: 11,
742+
fontWeight: 600,
743+
}}
744+
/>
745+
<PolarRadiusAxis angle={90} domain={[0, 100]} tick={false} axisLine={false} />
746+
<Radar
747+
name={user1.profile.username}
748+
dataKey="user1"
749+
stroke="#8B5CF6"
750+
fill="#8B5CF6"
751+
fillOpacity={0.25}
752+
strokeWidth={2}
753+
dot={{ r: 4, fill: '#8B5CF6', strokeWidth: 0 }}
754+
animationDuration={1200}
755+
animationEasing="ease-out"
756+
/>
757+
<Radar
758+
name={user2.profile.username}
759+
dataKey="user2"
760+
stroke="#06B6D4"
761+
fill="#06B6D4"
762+
fillOpacity={0.25}
763+
strokeWidth={2}
764+
dot={{ r: 4, fill: '#06B6D4', strokeWidth: 0 }}
765+
animationDuration={1200}
766+
animationEasing="ease-out"
767+
/>
768+
<Tooltip
769+
contentStyle={{
770+
backgroundColor: '#0a0a0a',
771+
border: '1px solid rgba(255,255,255,0.1)',
772+
borderRadius: '10px',
773+
padding: '10px 14px',
774+
fontSize: '12px',
775+
color: '#fff',
776+
}}
777+
itemStyle={{ color: '#e4e4e7', fontSize: '11px' }}
778+
/>
779+
</RadarChart>
780+
</ResponsiveContainer>
781+
</div>
782+
</motion.div>
783+
);
784+
}
785+
627786
/* ── main component ───────────────────────────────────────────────────── */
628787

629788
export default function CompareClient() {
@@ -915,6 +1074,9 @@ export default function CompareClient() {
9151074
{/* Code Volume Showdown */}
9161075
<CodeVolumeShowdown user1={d1} user2={d2} />
9171076

1077+
{/* Developer Skills Radar */}
1078+
<DeveloperSkillsRadar user1={d1} user2={d2} />
1079+
9181080
{/* Language Comparison */}
9191081
<LanguageComparison
9201082
langsA={d1.languages}

0 commit comments

Comments
 (0)