Created
January 19, 2026 19:22
-
-
Save tarasyarema/c14a215b64a9c14ecce804a7902d775b to your computer and use it in GitHub Desktop.
Github Org Metrics
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React, { useState, useMemo } from 'react'; | |
| import { ComposedChart, Bar, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; | |
| const data = { | |
| "organization": "desplega-ai", | |
| "period": { "months": 6, "since": "2025-07-19" }, | |
| "generatedAt": "2026-01-19T18:52:33.908Z", | |
| "repositories": [ | |
| { | |
| "name": "desplega.ai", | |
| "fullName": "desplega-ai/desplega.ai", | |
| "authors": [ | |
| ... | |
| { "login": "vercel[bot]", "additions": 45, "deletions": 45, "commits": 1, "weeks": [{"week":"2025-12-07","additions":45,"deletions":45,"commits":1}] } | |
| ] | |
| }, | |
| ... | |
| ] | |
| }; | |
| export default function GitHubViz() { | |
| const [enabledContributors, setEnabledContributors] = useState(new Set()); | |
| const [enabledRepos, setEnabledRepos] = useState(new Set()); | |
| const [showContributorPanel, setShowContributorPanel] = useState(false); | |
| const [showRepoPanel, setShowRepoPanel] = useState(false); | |
| const [showTrendLine, setShowTrendLine] = useState(true); | |
| // Extract all unique contributors and repos | |
| const { allContributors, allRepos } = useMemo(() => { | |
| const contributors = new Set(); | |
| const repos = new Set(); | |
| data.repositories.forEach(repo => { | |
| repos.add(repo.name); | |
| repo.authors.forEach(author => { | |
| contributors.add(author.login); | |
| }); | |
| }); | |
| return { | |
| allContributors: Array.from(contributors).sort(), | |
| allRepos: Array.from(repos).sort() | |
| }; | |
| }, []); | |
| // Initialize enabled sets on first render | |
| useMemo(() => { | |
| if (enabledContributors.size === 0) { | |
| setEnabledContributors(new Set(allContributors)); | |
| } | |
| if (enabledRepos.size === 0) { | |
| setEnabledRepos(new Set(allRepos)); | |
| } | |
| }, [allContributors, allRepos]); | |
| // Process data for the chart | |
| const chartData = useMemo(() => { | |
| const weekMap = new Map(); | |
| data.repositories.forEach(repo => { | |
| if (!enabledRepos.has(repo.name)) return; | |
| repo.authors.forEach(author => { | |
| if (!enabledContributors.has(author.login)) return; | |
| author.weeks.forEach(week => { | |
| const existing = weekMap.get(week.week) || { week: week.week, additions: 0, deletions: 0 }; | |
| existing.additions += week.additions; | |
| existing.deletions += week.deletions; | |
| weekMap.set(week.week, existing); | |
| }); | |
| }); | |
| }); | |
| const sorted = Array.from(weekMap.values()) | |
| .sort((a, b) => a.week.localeCompare(b.week)) | |
| .map((d, index) => ({ | |
| ...d, | |
| index, | |
| weekLabel: new Date(d.week).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), | |
| totalChanges: d.additions + d.deletions, | |
| negDeletions: -d.deletions | |
| })); | |
| // Calculate 4-week rolling average for trend | |
| const withTrend = sorted.map((d, i) => { | |
| const windowSize = 4; | |
| const start = Math.max(0, i - windowSize + 1); | |
| const window = sorted.slice(start, i + 1); | |
| const avgChanges = window.reduce((sum, w) => sum + w.totalChanges, 0) / window.length; | |
| return { ...d, trendLine: Math.round(avgChanges) }; | |
| }); | |
| return withTrend; | |
| }, [enabledContributors, enabledRepos]); | |
| // Calculate monthly totals for comparison | |
| const monthlyStats = useMemo(() => { | |
| const months = {}; | |
| chartData.forEach(d => { | |
| const month = d.week.substring(0, 7); // YYYY-MM | |
| if (!months[month]) { | |
| months[month] = { additions: 0, deletions: 0, total: 0 }; | |
| } | |
| months[month].additions += d.additions; | |
| months[month].deletions += d.deletions; | |
| months[month].total += d.totalChanges; | |
| }); | |
| return months; | |
| }, [chartData]); | |
| // Calculate growth multiplier | |
| const growthStats = useMemo(() => { | |
| const nov = monthlyStats['2025-11'] || { total: 1 }; | |
| const jan = monthlyStats['2026-01'] || { total: 0 }; | |
| const multiplier = nov.total > 0 ? (jan.total / nov.total) : 0; | |
| return { | |
| novTotal: nov.total, | |
| janTotal: jan.total, | |
| multiplier: multiplier.toFixed(1) | |
| }; | |
| }, [monthlyStats]); | |
| // Calculate totals | |
| const totals = useMemo(() => { | |
| return chartData.reduce((acc, d) => ({ | |
| additions: acc.additions + d.additions, | |
| deletions: acc.deletions + d.deletions | |
| }), { additions: 0, deletions: 0 }); | |
| }, [chartData]); | |
| const toggleContributor = (contributor) => { | |
| const newSet = new Set(enabledContributors); | |
| if (newSet.has(contributor)) { | |
| newSet.delete(contributor); | |
| } else { | |
| newSet.add(contributor); | |
| } | |
| setEnabledContributors(newSet); | |
| }; | |
| const toggleRepo = (repo) => { | |
| const newSet = new Set(enabledRepos); | |
| if (newSet.has(repo)) { | |
| newSet.delete(repo); | |
| } else { | |
| newSet.add(repo); | |
| } | |
| setEnabledRepos(newSet); | |
| }; | |
| const selectAllContributors = () => setEnabledContributors(new Set(allContributors)); | |
| const clearAllContributors = () => setEnabledContributors(new Set()); | |
| const selectAllRepos = () => setEnabledRepos(new Set(allRepos)); | |
| const clearAllRepos = () => setEnabledRepos(new Set()); | |
| const formatNumber = (num) => { | |
| if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'; | |
| if (num >= 1000) return (num / 1000).toFixed(1) + 'K'; | |
| return num.toString(); | |
| }; | |
| return ( | |
| <div className="min-h-screen bg-gray-950 text-gray-100 p-6"> | |
| <div className="max-w-7xl mx-auto"> | |
| <div className="mb-6"> | |
| <h1 className="text-2xl font-bold text-white mb-1">{data.organization} Contributions</h1> | |
| <p className="text-gray-400 text-sm">Week over week code changes ยท {data.period.months} months since {data.period.since}</p> | |
| </div> | |
| {/* Summary Stats */} | |
| <div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6"> | |
| <div className="bg-gray-900 rounded-lg p-4 border border-gray-800"> | |
| <div className="text-green-400 text-2xl font-bold">+{formatNumber(totals.additions)}</div> | |
| <div className="text-gray-500 text-sm">Additions</div> | |
| </div> | |
| <div className="bg-gray-900 rounded-lg p-4 border border-gray-800"> | |
| <div className="text-red-400 text-2xl font-bold">-{formatNumber(totals.deletions)}</div> | |
| <div className="text-gray-500 text-sm">Deletions</div> | |
| </div> | |
| <div className="bg-gray-900 rounded-lg p-4 border border-gray-800"> | |
| <div className="text-blue-400 text-2xl font-bold">{enabledContributors.size}/{allContributors.length}</div> | |
| <div className="text-gray-500 text-sm">Contributors</div> | |
| </div> | |
| <div className="bg-gray-900 rounded-lg p-4 border border-gray-800"> | |
| <div className="text-purple-400 text-2xl font-bold">{enabledRepos.size}/{allRepos.length}</div> | |
| <div className="text-gray-500 text-sm">Repositories</div> | |
| </div> | |
| <div className="bg-gradient-to-r from-amber-900/50 to-orange-900/50 rounded-lg p-4 border border-amber-700/50"> | |
| <div className="text-amber-400 text-2xl font-bold">{growthStats.multiplier}x</div> | |
| <div className="text-amber-200/70 text-sm">Jan vs Nov</div> | |
| </div> | |
| </div> | |
| {/* Growth Callout */} | |
| <div className="bg-gradient-to-r from-amber-900/30 to-orange-900/30 rounded-lg p-4 mb-6 border border-amber-700/30"> | |
| <div className="flex items-center gap-3"> | |
| <div className="text-3xl">๐</div> | |
| <div> | |
| <div className="text-amber-200 font-semibold">Acceleration: {growthStats.multiplier}x growth in 2 months</div> | |
| <div className="text-amber-200/60 text-sm"> | |
| Nov 2025: {formatNumber(growthStats.novTotal)} changes โ Jan 2026: {formatNumber(growthStats.janTotal)} changes | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Filter Buttons */} | |
| <div className="flex flex-wrap gap-3 mb-6"> | |
| <button | |
| onClick={() => setShowContributorPanel(!showContributorPanel)} | |
| className={`px-4 py-2 rounded-lg font-medium transition-all ${ | |
| showContributorPanel | |
| ? 'bg-blue-600 text-white' | |
| : 'bg-gray-800 text-gray-300 hover:bg-gray-700' | |
| }`} | |
| > | |
| Contributors ({enabledContributors.size}) | |
| </button> | |
| <button | |
| onClick={() => setShowRepoPanel(!showRepoPanel)} | |
| className={`px-4 py-2 rounded-lg font-medium transition-all ${ | |
| showRepoPanel | |
| ? 'bg-purple-600 text-white' | |
| : 'bg-gray-800 text-gray-300 hover:bg-gray-700' | |
| }`} | |
| > | |
| Repositories ({enabledRepos.size}) | |
| </button> | |
| <button | |
| onClick={() => setShowTrendLine(!showTrendLine)} | |
| className={`px-4 py-2 rounded-lg font-medium transition-all ${ | |
| showTrendLine | |
| ? 'bg-amber-600 text-white' | |
| : 'bg-gray-800 text-gray-300 hover:bg-gray-700' | |
| }`} | |
| > | |
| {showTrendLine ? '๐ Trend On' : '๐ Trend Off'} | |
| </button> | |
| </div> | |
| {/* Contributor Filter Panel */} | |
| {showContributorPanel && ( | |
| <div className="bg-gray-900 rounded-lg p-4 mb-6 border border-gray-800"> | |
| <div className="flex justify-between items-center mb-3"> | |
| <span className="text-gray-300 font-medium">Filter by Contributor</span> | |
| <div className="flex gap-2"> | |
| <button onClick={selectAllContributors} className="text-xs px-2 py-1 bg-gray-700 hover:bg-gray-600 rounded">Select All</button> | |
| <button onClick={clearAllContributors} className="text-xs px-2 py-1 bg-gray-700 hover:bg-gray-600 rounded">Clear All</button> | |
| </div> | |
| </div> | |
| <div className="flex flex-wrap gap-2"> | |
| {allContributors.map(contributor => ( | |
| <button | |
| key={contributor} | |
| onClick={() => toggleContributor(contributor)} | |
| className={`px-3 py-1.5 rounded-full text-sm font-medium transition-all ${ | |
| enabledContributors.has(contributor) | |
| ? 'bg-blue-600 text-white' | |
| : 'bg-gray-700 text-gray-400 hover:bg-gray-600' | |
| }`} | |
| > | |
| {contributor} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {/* Repository Filter Panel */} | |
| {showRepoPanel && ( | |
| <div className="bg-gray-900 rounded-lg p-4 mb-6 border border-gray-800"> | |
| <div className="flex justify-between items-center mb-3"> | |
| <span className="text-gray-300 font-medium">Filter by Repository</span> | |
| <div className="flex gap-2"> | |
| <button onClick={selectAllRepos} className="text-xs px-2 py-1 bg-gray-700 hover:bg-gray-600 rounded">Select All</button> | |
| <button onClick={clearAllRepos} className="text-xs px-2 py-1 bg-gray-700 hover:bg-gray-600 rounded">Clear All</button> | |
| </div> | |
| </div> | |
| <div className="flex flex-wrap gap-2"> | |
| {allRepos.map(repo => ( | |
| <button | |
| key={repo} | |
| onClick={() => toggleRepo(repo)} | |
| className={`px-3 py-1.5 rounded-full text-sm font-medium transition-all ${ | |
| enabledRepos.has(repo) | |
| ? 'bg-purple-600 text-white' | |
| : 'bg-gray-700 text-gray-400 hover:bg-gray-600' | |
| }`} | |
| > | |
| {repo} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {/* Chart */} | |
| <div className="bg-gray-900 rounded-lg p-6 border border-gray-800"> | |
| <div className="flex items-center gap-2 mb-4"> | |
| <h2 className="text-gray-300 font-medium">Weekly Code Changes</h2> | |
| {showTrendLine && ( | |
| <span className="text-xs px-2 py-1 bg-amber-900/50 text-amber-300 rounded-full"> | |
| 4-week rolling average | |
| </span> | |
| )} | |
| </div> | |
| <ResponsiveContainer width="100%" height={450}> | |
| <ComposedChart data={chartData} margin={{ top: 20, right: 30, left: 20, bottom: 60 }}> | |
| <CartesianGrid strokeDasharray="3 3" stroke="#374151" /> | |
| <XAxis | |
| dataKey="weekLabel" | |
| stroke="#9CA3AF" | |
| tick={{ fill: '#9CA3AF', fontSize: 11 }} | |
| angle={-45} | |
| textAnchor="end" | |
| height={60} | |
| /> | |
| <YAxis | |
| yAxisId="left" | |
| stroke="#9CA3AF" | |
| tick={{ fill: '#9CA3AF', fontSize: 11 }} | |
| tickFormatter={formatNumber} | |
| /> | |
| <YAxis | |
| yAxisId="right" | |
| orientation="right" | |
| stroke="#F59E0B" | |
| tick={{ fill: '#F59E0B', fontSize: 11 }} | |
| tickFormatter={formatNumber} | |
| domain={[0, 'auto']} | |
| /> | |
| <Tooltip | |
| contentStyle={{ | |
| backgroundColor: '#1F2937', | |
| border: '1px solid #374151', | |
| borderRadius: '8px', | |
| color: '#F3F4F6' | |
| }} | |
| formatter={(value, name) => { | |
| const absValue = Math.abs(value); | |
| if (name === 'negDeletions') return [formatNumber(absValue), 'Deletions']; | |
| if (name === 'trendLine') return [formatNumber(absValue), 'Trend (4-wk avg)']; | |
| return [formatNumber(absValue), 'Additions']; | |
| }} | |
| labelFormatter={(label) => `Week of ${label}`} | |
| /> | |
| <Legend | |
| formatter={(value) => { | |
| if (value === 'negDeletions') return 'Deletions'; | |
| if (value === 'trendLine') return 'Trend (4-wk rolling avg)'; | |
| return 'Additions'; | |
| }} | |
| wrapperStyle={{ paddingTop: '20px' }} | |
| /> | |
| <Bar | |
| yAxisId="left" | |
| dataKey="additions" | |
| fill="#22C55E" | |
| name="Additions" | |
| radius={[4, 4, 0, 0]} | |
| /> | |
| <Bar | |
| yAxisId="left" | |
| dataKey="negDeletions" | |
| fill="#EF4444" | |
| name="negDeletions" | |
| radius={[0, 0, 4, 4]} | |
| /> | |
| {showTrendLine && ( | |
| <Line | |
| yAxisId="right" | |
| type="monotone" | |
| dataKey="trendLine" | |
| stroke="#F59E0B" | |
| strokeWidth={3} | |
| dot={false} | |
| name="trendLine" | |
| /> | |
| )} | |
| </ComposedChart> | |
| </ResponsiveContainer> | |
| </div> | |
| {/* Monthly Breakdown */} | |
| <div className="mt-6 bg-gray-900 rounded-lg p-4 border border-gray-800"> | |
| <h3 className="text-gray-300 font-medium mb-3">Monthly Breakdown</h3> | |
| <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-3"> | |
| {Object.entries(monthlyStats).sort().map(([month, stats]) => { | |
| const monthName = new Date(month + '-01').toLocaleDateString('en-US', { month: 'short', year: '2-digit' }); | |
| const isHighlight = month === '2026-01' || month === '2025-11'; | |
| return ( | |
| <div | |
| key={month} | |
| className={`rounded-lg p-3 ${ | |
| isHighlight | |
| ? 'bg-gradient-to-b from-amber-900/40 to-gray-800 border border-amber-700/30' | |
| : 'bg-gray-800' | |
| }`} | |
| > | |
| <div className="text-gray-400 text-xs mb-1">{monthName}</div> | |
| <div className="text-white font-bold">{formatNumber(stats.total)}</div> | |
| <div className="text-xs"> | |
| <span className="text-green-400">+{formatNumber(stats.additions)}</span> | |
| {' / '} | |
| <span className="text-red-400">-{formatNumber(stats.deletions)}</span> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| <p className="text-gray-500 text-xs mt-4 text-center"> | |
| Generated {new Date(data.generatedAt).toLocaleDateString()} | |
| </p> | |
| </div> | |
| </div> | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment