Spaces:
Running
Running
| import React, { useMemo, useState } from "react"; | |
| import "./overview.css"; | |
| /* ================= KPI CARD ================= */ | |
| function StackOverflowKpiCard({ | |
| icon, | |
| title, | |
| value, | |
| subtitle, | |
| accent, | |
| trend, | |
| progress, | |
| onClick, | |
| isActive, | |
| }) { | |
| const [isHovered, setIsHovered] = useState(false); | |
| return ( | |
| <div | |
| className={`kpi-card ${accent || ""} ${isActive ? "active" : ""}`} | |
| onClick={onClick} | |
| onMouseEnter={() => setIsHovered(true)} | |
| onMouseLeave={() => setIsHovered(false)} | |
| > | |
| <div className="kpi-header"> | |
| <span className={`kpi-icon ${isHovered ? "bounce" : ""}`}>{icon}</span> | |
| <span className="kpi-title">{title}</span> | |
| {trend && ( | |
| <span className={`kpi-trend ${trend > 0 ? "positive" : "negative"}`}> | |
| {trend > 0 ? "↑" : "↓"} {Math.abs(trend)}% | |
| </span> | |
| )} | |
| </div> | |
| <div className="kpi-value-container"> | |
| <div className="kpi-value">{value}</div> | |
| {progress !== undefined && ( | |
| <div className="kpi-progress-bar"> | |
| <div | |
| className="kpi-progress-fill" | |
| style={{ width: `${Math.min(progress, 100)}%` }} | |
| /> | |
| </div> | |
| )} | |
| </div> | |
| {subtitle && <div className="kpi-sub">{subtitle}</div>} | |
| </div> | |
| ); | |
| } | |
| /* ================= FEATURED METRIC ================= */ | |
| function StackOverflowFeaturedMetric({ | |
| icon, | |
| title, | |
| value, | |
| subtitle, | |
| description, | |
| }) { | |
| return ( | |
| <div className="featured-metric"> | |
| <div className="featured-icon">{icon}</div> | |
| <div className="featured-content"> | |
| <div className="featured-title">{title}</div> | |
| <div className="featured-value">{value}</div> | |
| {subtitle && <div className="featured-subtitle">{subtitle}</div>} | |
| {description && ( | |
| <div className="featured-description">{description}</div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| /* ================= MAIN OVERVIEW ================= */ | |
| export default function StackOverflowKpiOverview({ rows = [] }) { | |
| const [selectedMetric, setSelectedMetric] = useState(null); | |
| const [viewMode, setViewMode] = useState("grid"); | |
| const metrics = useMemo(() => { | |
| if (!rows.length) return null; | |
| // Demographics | |
| const ageGroups = {}; | |
| const educationLevels = {}; | |
| const countries = {}; | |
| const employmentTypes = {}; | |
| const orgSizes = {}; | |
| const remoteWorkTypes = {}; | |
| // Experience | |
| const yearsCodeRanges = {}; | |
| const devTypes = new Set(); | |
| const icOrPM = {}; | |
| // Technology | |
| const languages = new Set(); | |
| const databases = new Set(); | |
| const platforms = new Set(); | |
| const aiTools = new Set(); | |
| // Salary & Satisfaction | |
| let totalSalary = 0; | |
| let salaryCount = 0; | |
| const jobSatLevels = {}; | |
| // AI Usage | |
| let aiUsers = 0; | |
| let aiSentiments = { positive: 0, negative: 0, neutral: 0 }; | |
| let aiThreats = { yes: 0, no: 0 }; | |
| rows.forEach((row) => { | |
| // Demographics | |
| if (row.Age) { | |
| ageGroups[row.Age] = (ageGroups[row.Age] || 0) + 1; | |
| } | |
| if (row.EdLevel) { | |
| educationLevels[row.EdLevel] = (educationLevels[row.EdLevel] || 0) + 1; | |
| } | |
| if (row.Country) { | |
| countries[row.Country] = (countries[row.Country] || 0) + 1; | |
| } | |
| if (row.Employment) { | |
| employmentTypes[row.Employment] = | |
| (employmentTypes[row.Employment] || 0) + 1; | |
| } | |
| if (row.OrgSize) { | |
| orgSizes[row.OrgSize] = (orgSizes[row.OrgSize] || 0) + 1; | |
| } | |
| if (row.RemoteWork) { | |
| remoteWorkTypes[row.RemoteWork] = | |
| (remoteWorkTypes[row.RemoteWork] || 0) + 1; | |
| } | |
| // Experience | |
| if (row.YearsCode) { | |
| yearsCodeRanges[row.YearsCode] = | |
| (yearsCodeRanges[row.YearsCode] || 0) + 1; | |
| } | |
| if (row.DevType) { | |
| row.DevType.split(";").forEach((type) => devTypes.add(type.trim())); | |
| } | |
| if (row.ICorPM) { | |
| icOrPM[row.ICorPM] = (icOrPM[row.ICorPM] || 0) + 1; | |
| } | |
| // Technology | |
| if (row.LanguageHaveWorkedWith) { | |
| row.LanguageHaveWorkedWith.split(";").forEach((lang) => | |
| languages.add(lang.trim()) | |
| ); | |
| } | |
| if (row.DatabaseHaveWorkedWith) { | |
| row.DatabaseHaveWorkedWith.split(";").forEach((db) => | |
| databases.add(db.trim()) | |
| ); | |
| } | |
| if (row.PlatformHaveWorkedWith) { | |
| row.PlatformHaveWorkedWith.split(";").forEach((platform) => | |
| platforms.add(platform.trim()) | |
| ); | |
| } | |
| if (row.AIModelsHaveWorkedWith) { | |
| row.AIModelsHaveWorkedWith.split(";").forEach((ai) => | |
| aiTools.add(ai.trim()) | |
| ); | |
| aiUsers++; | |
| } | |
| // Salary | |
| if (row.CompTotal && row.CompTotal !== "NA" && row.CompTotal !== "") { | |
| const salary = parseFloat(row.CompTotal.replace(/,/g, "")); | |
| if (!isNaN(salary)) { | |
| totalSalary += salary; | |
| salaryCount++; | |
| } | |
| } | |
| // Job Satisfaction | |
| if (row.JobSat) { | |
| jobSatLevels[row.JobSat] = (jobSatLevels[row.JobSat] || 0) + 1; | |
| } | |
| // AI Sentiment | |
| if (row.AISent) { | |
| if ( | |
| row.AISent.toLowerCase().includes("agree") || | |
| row.AISent.toLowerCase().includes("positive") | |
| ) { | |
| aiSentiments.positive++; | |
| } else if ( | |
| row.AISent.toLowerCase().includes("disagree") || | |
| row.AISent.toLowerCase().includes("negative") | |
| ) { | |
| aiSentiments.negative++; | |
| } else { | |
| aiSentiments.neutral++; | |
| } | |
| } | |
| // AI Threat Perception | |
| if (row.AIThreat) { | |
| if ( | |
| row.AIThreat.toLowerCase().includes("agree") || | |
| row.AIThreat.toLowerCase() === "yes" | |
| ) { | |
| aiThreats.yes++; | |
| } else { | |
| aiThreats.no++; | |
| } | |
| } | |
| }); | |
| const avgSalary = salaryCount > 0 ? totalSalary / salaryCount : 0; | |
| const aiAdoptionRate = (aiUsers / rows.length) * 100; | |
| const topCountry = Object.entries(countries).sort((a, b) => b[1] - a[1])[0]; | |
| const topLanguage = Array.from(languages).length; | |
| const topDevType = Array.from(devTypes).length; | |
| return { | |
| totalRespondents: rows.length, | |
| avgSalary, | |
| salaryCount, | |
| uniqueCountries: Object.keys(countries).length, | |
| uniqueLanguages: topLanguage, | |
| uniqueDevTypes: topDevType, | |
| aiAdoptionRate, | |
| aiUsers, | |
| aiSentiments, | |
| aiThreats, | |
| topCountry: topCountry ? topCountry[0] : "N/A", | |
| topCountryCount: topCountry ? topCountry[1] : 0, | |
| remoteWorkPercentage: | |
| (((remoteWorkTypes["Remote"] || 0) + (remoteWorkTypes["Hybrid"] || 0)) / | |
| rows.length) * | |
| 100, | |
| fullyRemotePercentage: | |
| ((remoteWorkTypes["Remote"] || 0) / rows.length) * 100, | |
| educationDiversity: Object.keys(educationLevels).length, | |
| avgYearsCode: calculateAverageYearsCode(yearsCodeRanges), | |
| jobSatisfaction: calculateJobSatisfaction(jobSatLevels), | |
| organizationSizes: Object.keys(orgSizes).length, | |
| }; | |
| }, [rows]); | |
| function calculateAverageYearsCode(yearsCodeRanges) { | |
| let totalYears = 0; | |
| let totalRespondents = 0; | |
| Object.entries(yearsCodeRanges).forEach(([range, count]) => { | |
| const avgYears = parseCodeRange(range); | |
| if (avgYears !== null) { | |
| totalYears += avgYears * count; | |
| totalRespondents += count; | |
| } | |
| }); | |
| return totalRespondents > 0 ? totalYears / totalRespondents : 0; | |
| } | |
| function parseCodeRange(range) { | |
| if (range.includes("More than 50")) return 55; | |
| if (range.includes("30-44")) return 37; | |
| if (range.includes("20-29")) return 25; | |
| if (range.includes("15-19")) return 17; | |
| if (range.includes("10-14")) return 12; | |
| if (range.includes("5-9")) return 7; | |
| if (range.includes("3-5")) return 4; | |
| if (range.includes("1-2")) return 1.5; | |
| if (range.includes("Less than 1")) return 0.5; | |
| return null; | |
| } | |
| function calculateJobSatisfaction(jobSatLevels) { | |
| const total = Object.values(jobSatLevels).reduce( | |
| (sum, count) => sum + count, | |
| 0 | |
| ); | |
| const satisfied = | |
| (jobSatLevels["Very satisfied"] || 0) + | |
| (jobSatLevels["Slightly satisfied"] || 0); | |
| return total > 0 ? (satisfied / total) * 100 : 0; | |
| } | |
| if (!metrics) return null; | |
| const kpiData = [ | |
| { | |
| icon: "👥", | |
| title: "Total Respondents", | |
| value: metrics.totalRespondents.toLocaleString(), | |
| subtitle: "Developer survey participants", | |
| accent: "accent-primary", | |
| trend: 15.2, | |
| progress: (metrics.totalRespondents / 100000) * 100, | |
| }, | |
| { | |
| icon: "🌍", | |
| title: "Global Reach", | |
| value: metrics.uniqueCountries, | |
| subtitle: `Top: ${metrics.topCountry} (${metrics.topCountryCount})`, | |
| accent: "accent-info", | |
| trend: 5.3, | |
| progress: (metrics.uniqueCountries / 200) * 100, | |
| }, | |
| { | |
| icon: "💻", | |
| title: "Languages Used", | |
| value: metrics.uniqueLanguages, | |
| subtitle: "Programming languages", | |
| accent: "accent-secondary", | |
| trend: 12.1, | |
| progress: (metrics.uniqueLanguages / 50) * 100, | |
| }, | |
| { | |
| icon: "🏢", | |
| title: "Developer Types", | |
| value: metrics.uniqueDevTypes, | |
| subtitle: "Professional roles", | |
| accent: "accent-warning", | |
| trend: 7.8, | |
| progress: (metrics.uniqueDevTypes / 30) * 100, | |
| }, | |
| { | |
| icon: "🏠", | |
| title: "Remote Work", | |
| value: `${metrics.remoteWorkPercentage.toFixed(1)}%`, | |
| subtitle: `${metrics.fullyRemotePercentage.toFixed(1)}% fully remote`, | |
| accent: "accent-success", | |
| trend: 18.5, | |
| progress: metrics.remoteWorkPercentage, | |
| }, | |
| { | |
| icon: "🎓", | |
| title: "Education Diversity", | |
| value: metrics.educationDiversity, | |
| subtitle: "Education levels", | |
| accent: "accent-info", | |
| trend: 3.2, | |
| progress: (metrics.educationDiversity / 10) * 100, | |
| }, | |
| // { | |
| // icon: "📊", | |
| // title: "Avg Experience", | |
| // value: `${metrics.avgYearsCode.toFixed(1)} yrs`, | |
| // subtitle: "Years coding", | |
| // accent: "accent-primary", | |
| // trend: 2.1, | |
| // progress: (metrics.avgYearsCode / 20) * 100, | |
| // }, | |
| ]; | |
| const insights = [ | |
| // { | |
| // type: "success", | |
| // icon: "🚀", | |
| // text: `AI adoption is booming with ${metrics.aiAdoptionRate.toFixed( | |
| // 1 | |
| // )}% of developers using AI tools, showing ${( | |
| // (metrics.aiSentiments.positive / (metrics.aiUsers || 1)) * | |
| // 100 | |
| // ).toFixed(1)}% positive sentiment`, | |
| // }, | |
| { | |
| type: "info", | |
| icon: "🌐", | |
| text: `Global developer community spans ${metrics.uniqueCountries} countries with ${metrics.uniqueLanguages} programming languages in use`, | |
| }, | |
| { | |
| type: "success", | |
| icon: "🏠", | |
| text: `Remote work revolution: ${metrics.remoteWorkPercentage.toFixed( | |
| 1 | |
| )}% work remotely with ${metrics.fullyRemotePercentage.toFixed( | |
| 1 | |
| )}% fully remote`, | |
| }, | |
| // { | |
| // type: "warning", | |
| // icon: "💰", | |
| // text: `Average salary of $${metrics.avgSalary.toLocaleString()} across ${ | |
| // metrics.salaryCount | |
| // } respondents, showing ${metrics.jobSatisfaction.toFixed( | |
| // 1 | |
| // )}% job satisfaction`, | |
| // }, | |
| ]; | |
| return ( | |
| <div className="kpi-overview-container"> | |
| {/* ================= HEADER ================= */} | |
| <div className="kpi-header-section"> | |
| <div className="kpi-title-area"> | |
| <h1 className="kpi-main-title"> | |
| Stack Overflow Developer Survey Insights | |
| </h1> | |
| <div className="kpi-subtitle"> | |
| <span className="live-indicator">●</span> | |
| Live developer community analytics and trends | |
| </div> | |
| </div> | |
| {/* <div className="kpi-controls"> | |
| <button | |
| className={`view-toggle ${viewMode === "grid" ? "active" : ""}`} | |
| onClick={() => setViewMode("grid")} | |
| > | |
| Grid View | |
| </button> | |
| <button | |
| className={`view-toggle ${viewMode === "list" ? "active" : ""}`} | |
| onClick={() => setViewMode("list")} | |
| > | |
| List View | |
| </button> | |
| </div> */} | |
| </div> | |
| {/* ================= FEATURED METRICS ================= */} | |
| <div className="featured-metrics-row"> | |
| <StackOverflowFeaturedMetric | |
| icon="👥" | |
| title="Developer Community" | |
| value={metrics.totalRespondents.toLocaleString()} | |
| subtitle={`${metrics.uniqueCountries} countries represented`} | |
| description="Global developer participation in annual survey" | |
| /> | |
| {/* <StackOverflowFeaturedMetric | |
| icon="🤖" | |
| title="AI Revolution" | |
| value={`${metrics.aiAdoptionRate.toFixed(1)}%`} | |
| subtitle={`${metrics.aiUsers.toLocaleString()} AI tool users`} | |
| description={`${( | |
| (metrics.aiSentiments.positive / (metrics.aiUsers || 1)) * | |
| 100 | |
| ).toFixed(1)}% positive AI sentiment`} | |
| /> */} | |
| <StackOverflowFeaturedMetric | |
| icon="🌍" | |
| title="Tech Diversity" | |
| value={metrics.uniqueLanguages} | |
| subtitle={`${metrics.uniqueDevTypes} developer roles`} | |
| description={`${metrics.uniqueCountries} countries, ${metrics.educationDiversity} education levels`} | |
| /> | |
| </div> | |
| {/* ================= KPI GRID ================= */} | |
| <div className={`kpi-grid ${viewMode}`}> | |
| {kpiData.map((kpi, index) => ( | |
| <StackOverflowKpiCard | |
| key={index} | |
| {...kpi} | |
| onClick={() => | |
| setSelectedMetric(selectedMetric === index ? null : index) | |
| } | |
| isActive={selectedMetric === index} | |
| /> | |
| ))} | |
| </div> | |
| {/* ================= DETAILED INSIGHTS ================= */} | |
| <div className="insights-section"> | |
| <div className="insights-header"> | |
| <h2>🎯 Developer Insights</h2> | |
| <div className="insights-subtitle"> | |
| Key trends and observations from the developer community | |
| </div> | |
| </div> | |
| <div className="insights-grid"> | |
| {insights.map((insight, index) => ( | |
| <div key={index} className={`insight-card insight-${insight.type}`}> | |
| <div className="insight-icon">{insight.icon}</div> | |
| <div className="insight-text">{insight.text}</div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| {/* ================= SUMMARY STATS ================= */} | |
| <div className="summary-stats"> | |
| <div className="stat-item"> | |
| <div className="stat-label">Community Growth</div> | |
| <div className="stat-value">+15.2%</div> | |
| <div className="stat-period">vs. last year</div> | |
| </div> | |
| {/* <div className="stat-item"> | |
| <div className="stat-label">AI Adoption</div> | |
| <div className="stat-value">{metrics.aiAdoptionRate.toFixed(1)}%</div> | |
| <div className="stat-period">using AI tools</div> | |
| </div> */} | |
| <div className="stat-item"> | |
| <div className="stat-label">Remote Work</div> | |
| <div className="stat-value"> | |
| {metrics.remoteWorkPercentage.toFixed(1)}% | |
| </div> | |
| <div className="stat-period">work remotely</div> | |
| </div> | |
| {/* <div className="stat-item"> | |
| <div className="stat-label">Job Satisfaction</div> | |
| <div className="stat-value"> | |
| {metrics.jobSatisfaction.toFixed(1)}% | |
| </div> | |
| <div className="stat-period">satisfied developers</div> | |
| </div> */} | |
| </div> | |
| </div> | |
| ); | |
| } | |