Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,836 changes: 2,836 additions & 0 deletions note.txt

Large diffs are not rendered by default.

297 changes: 297 additions & 0 deletions note2.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
h2D from "react-force-graph-2d";
import { useEffect, useState, useRef } from "react";
import { fetchRepoContributors } from "../../services/githubService";

export default function NetworkGraph({ repos }: any) {

const [graphData, setGraphData] = useState<any>({ nodes: [], links: [] });
const [selectedRepo, setSelectedRepo] = useState<any>(null);
const [crossUsers, setCrossUsers] = useState<any[]>([]);
const fgRef = useRef<any>(null);

useEffect(() => {
const buildGraph = async () => {

const nodes: any[] = [];
const links: any[] = [];

const userMap = new Map();
const repoMap = new Map();
const userOrgMap: any = {};

// 🔥 LIMIT for performance (important)
const selectedRepos = repos.slice(0, 8);

for (let repo of selectedRepos) {

const org = repo.full_name.split("/")[0];

// ✅ REPO NODE
if (!repoMap.has(repo.full_name)) {
repoMap.set(repo.full_name, true);

nodes.push({
id: repo.full_name,
type: "repo",
org,
stars: repo.stargazers_count,
size: Math.log(repo.stargazers_count + 1) * 3 + 8
});
}

try {
const contributors = await fetchRepoContributors(
`${repo.contributors_url}?per_page=50`
);

contributors.slice(0, 20).forEach((c: any) => {

// ✅ USER NODE
if (!userMap.has(c.login)) {
userMap.set(c.login, true);

nodes.push({
id: c.login,
type: "user",
img: c.avatar_url,
contributions: c.contributions,
size: Math.log(c.contributions + 1) * 2 + 5
});
}

// 🔥 TRACK ORG RELATION
if (!userOrgMap[c.login]) {
userOrgMap[c.login] = new Set();
}
userOrgMap[c.login].add(org);

// ✅ LINK repo ↔ user
links.push({
source: c.login,
target: repo.full_name,
weight: c.contributions
});

});

} catch (e) {
console.error(e);
}
}

// 🔥 CROSS ORG USERS
const cross = Object.entries(userOrgMap)
.filter(([_, set]: any) => set.size > 1)
.map(([user, set]: any) => ({
user,
orgCount: set.size
}))
.sort((a, b) => b.orgCount - a.orgCount)
.slice(0, 5);

setCrossUsers(cross);

setGraphData({ nodes, links });
};

if (repos.length) buildGraph();

}, [repos]);

// 🔥 FORCE SETTINGS (clean network look)
useEffect(() => {
if (!fgRef.current) return;

const fg = fgRef.current;

fg.d3Force("charge").strength(-150);
fg.d3Force("link").distance(100);

}, [graphData]);

// 🔥 INSIGHTS
const totalLinks = graphData.links.length;
const totalNodes = graphData.nodes.length;

return (
<div className="mt-6">

{/* 🔥 INSIGHT */}
<p className="text-gray-400 mb-2">
🔥 {totalLinks} connections across {totalNodes} nodes
</p>

<p className="text-yellow-400 mb-4">
{totalLinks > 150
? "🚀 High collaboration across organizations"
: "⚠️ Limited collaboration detected"}
</p>

{/* 🔥 CROSS ORG USERS */}
<div className="bg-gray-900 p-4 rounded-xl mb-4">
<h3 className="text-green-400 mb-2">
🔗 Top Cross-Org Contributors
</h3>

{crossUsers.length === 0 && (
<p className="text-gray-400">No cross-org contributors</p>
)}

{crossUsers.map((u: any) => (
<div key={u.user} className="text-gray-300">
{u.user} → {u.orgCount} orgs
</div>
))}
</div>

{/* 🔥 GRAPH */}
<div className="w-full h-[80vh] bg-[#020617] rounded-xl border border-gray-800">

<ForceGraph2D
ref={fgRef}
graphData={graphData}
backgroundColor="#020617"

// 🔥 SMOOTH GRAPH (important)
cooldownTicks={100}
d3AlphaDecay={0.05}
d3VelocityDecay={0.3}

// 🔥 HOVER
nodeLabel={(node: any) =>
node.type === "repo"
? `📦 ${node.id} ⭐ ${node.stars}`
: `👤 ${node.id} (${node.contributions})`
}

// 🔥 CLICK
onNodeClick={(node: any) => {
if (node.type === "user") {
window.open(`https://github.com/${node.id}`, "_blank");
}
if (node.type === "repo") {
setSelectedRepo(node);
}
}}

// 🔥 NODE DESIGN
nodeCanvasObject={(node: any, ctx, globalScale) => {
const size = node.size || 6;

// USER IMAGE
if (node.type === "user" && node.img) {
const img = new Image();
img.src = node.img;

ctx.save();
ctx.beginPath();
ctx.arc(node.x, node.y, size, 0, 2 * Math.PI);
ctx.clip();
ctx.drawImage(img, node.x - size, node.y - size, size * 2, size * 2);
ctx.restore();
}

// REPO NODE (colored by org)
else {
ctx.beginPath();
ctx.arc(node.x, node.y, size, 0, 2 * Math.PI);

if (node.org === "AOSSIE-Org") ctx.fillStyle = "#a855f7";
else if (node.org === "StabilityNexus") ctx.fillStyle = "#22c55e";
else ctx.fillStyle = "#facc15";

ctx.fill();
}

// LABEL
ctx.font = `${10 / globalScale}px Inter`;
ctx.fillStyle = "#e2e8f0";
ctx.fillText(node.id.split("/")[1] || node.id, node.x + size + 2, node.y);
}}

// 🔥 LINKS
linkWidth={(link: any) => Math.log(link.weight + 1)}
linkColor={() => "rgba(34,197,94,0.5)"}

linkDirectionalParticles={2}
linkDirectionalParticleSpeed={0.003}

onEngineStop={() => fgRef.current?.zoomToFit(400)}
/>
</div>

{/* 🔥 REPO POPUP */}
{selectedRepo && (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<div className="bg-gray-900 p-6 rounded-xl w-[350px]">

<h2 className="text-xl text-green-400 mb-3">
📦 {selectedRepo.id}
</h2>

<p className="text-gray-300 mb-2">
⭐ Stars: {selectedRepo.stars}
</p>

<button
onClick={() =>
window.open(`https://github.com/${selectedRepo.id}`, "_blank")
}
className="bg-blue-600 px-3 py-1 rounded mt-2"
>
Open Repo
</button>

<button
onClick={() => setSelectedRepo(null)}
className="ml-2 bg-red-500 px-3 py-1 rounded"
>
Close
</button>

</div>
</div>
)}
</div>
);
}


<div className="absolute top-4 left-4 z-50
flex flex-wrap gap-3 bg-[#020617]/80 backdrop-blur-md
border border-gray-700 px-4 py-2 rounded-xl shadow-lg text-xs">

{/* 🟢 Contributors */}
<div className="flex items-center gap-2 bg-gray-900 px-3 py-1 rounded border border-gray-700">
🟢 {stats.users}
<span className="text-gray-400">Contributors</span>
</div>

{/* 🟡 Repos */}
<div className="flex items-center gap-2 bg-gray-900 px-3 py-1 rounded border border-gray-700">
🟡 {stats.repos}
<span className="text-gray-400">Repos</span>
</div>

{/* ⚪ Edges */}
<div className="flex items-center gap-2 bg-gray-900 px-3 py-1 rounded border border-gray-700">
⚪ {stats.edges}
<span className="text-gray-400">Links</span>
</div>

{/* 🔥 Top Contributor */}
<div className="flex items-center gap-2 bg-green-900/30 px-3 py-1 rounded border border-green-500/30">
Most Active Contributor : {topContributor?.label}
</div>

{/* 🚀 Most Active Repo */}
<div className="flex items-center gap-2 bg-yellow-900/30 px-3 py-1 rounded border border-yellow-500/30">
Most Active Repo : {mostActiveRepo?.label}
</div>

{/* 🔗 Strongest Link */}
<div className="flex items-center gap-2 bg-blue-900/30 px-3 py-1 rounded border border-blue-500/30">
Strongest : {getId(strongestLink?.source)} → {getId(strongestLink?.target)}
</div>

</div>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Loading
Loading