harshapitla/stackoverflow
0
1import React, { useMemo, useState } from "react";2import "./overview.css";3 4/* ================= KPI CARD ================= */5function StackOverflowKpiCard({6 icon,7 title,8 value,9 subtitle,10 accent,11 trend,12 progress,13 onClick,14 isActive,15}) {16 const [isHovered, setIsHovered] = useState(false);17 18 return (19 <div20 className={`kpi-card ${accent || ""} ${isActive ? "active" : ""}`}21 onClick={onClick}22 onMouseEnter={() => setIsHovered(true)}23 onMouseLeave={() => setIsHovered(false)}24 >25 <div className="kpi-header">26 <span className={`kpi-icon ${isHovered ? "bounce" : ""}`}>{icon}</span>27 <span className="kpi-title">{title}</span>28 {trend && (29 <span className={`kpi-trend ${trend > 0 ? "positive" : "negative"}`}>30 {trend > 0 ? "↑" : "↓"} {Math.abs(trend)}%31 </span>32 )}33 </div>34 <div className="kpi-value-container">35 <div className="kpi-value">{value}</div>36 {progress !== undefined && (37 <div className="kpi-progress-bar">38 <div39 className="kpi-progress-fill"40 style={{ width: `${Math.min(progress, 100)}%` }}41 />42 </div>43 )}44 </div>45 {subtitle && <div className="kpi-sub">{subtitle}</div>}46 </div>47 );48}49 50/* ================= FEATURED METRIC ================= */51function StackOverflowFeaturedMetric({52 icon,53 title,54 value,55 subtitle,56 description,57}) {58 return (59 <div className="featured-metric">60 <div className="featured-icon">{icon}</div>61 <div className="featured-content">62 <div className="featured-title">{title}</div>63 <div className="featured-value">{value}</div>64 {subtitle && <div className="featured-subtitle">{subtitle}</div>}65 {description && (66 <div className="featured-description">{description}</div>67 )}68 </div>69 </div>70 );71}72 73/* ================= MAIN OVERVIEW ================= */74export default function StackOverflowKpiOverview({ rows = [] }) {75 const [selectedMetric, setSelectedMetric] = useState(null);76 const [viewMode, setViewMode] = useState("grid");77 78 const metrics = useMemo(() => {79 if (!rows.length) return null;80 81 // Demographics82 const ageGroups = {};83 const educationLevels = {};84 const countries = {};85 const employmentTypes = {};86 const orgSizes = {};87 const remoteWorkTypes = {};88 89 // Experience90 const yearsCodeRanges = {};91 const devTypes = new Set();92 const icOrPM = {};93 94 // Technology95 const languages = new Set();96 const databases = new Set();97 const platforms = new Set();98 const aiTools = new Set();99 100 // Salary & Satisfaction101 let totalSalary = 0;102 let salaryCount = 0;103 const jobSatLevels = {};104 105 // AI Usage106 let aiUsers = 0;107 let aiSentiments = { positive: 0, negative: 0, neutral: 0 };108 let aiThreats = { yes: 0, no: 0 };109 110 rows.forEach((row) => {111 // Demographics112 if (row.Age) {113 ageGroups[row.Age] = (ageGroups[row.Age] || 0) + 1;114 }115 if (row.EdLevel) {116 educationLevels[row.EdLevel] = (educationLevels[row.EdLevel] || 0) + 1;117 }118 if (row.Country) {119 countries[row.Country] = (countries[row.Country] || 0) + 1;120 }121 if (row.Employment) {122 employmentTypes[row.Employment] =123 (employmentTypes[row.Employment] || 0) + 1;124 }125 if (row.OrgSize) {126 orgSizes[row.OrgSize] = (orgSizes[row.OrgSize] || 0) + 1;127 }128 if (row.RemoteWork) {129 remoteWorkTypes[row.RemoteWork] =130 (remoteWorkTypes[row.RemoteWork] || 0) + 1;131 }132 133 // Experience134 if (row.YearsCode) {135 yearsCodeRanges[row.YearsCode] =136 (yearsCodeRanges[row.YearsCode] || 0) + 1;137 }138 if (row.DevType) {139 row.DevType.split(";").forEach((type) => devTypes.add(type.trim()));140 }141 if (row.ICorPM) {142 icOrPM[row.ICorPM] = (icOrPM[row.ICorPM] || 0) + 1;143 }144 145 // Technology146 if (row.LanguageHaveWorkedWith) {147 row.LanguageHaveWorkedWith.split(";").forEach((lang) =>148 languages.add(lang.trim())149 );150 }151 if (row.DatabaseHaveWorkedWith) {152 row.DatabaseHaveWorkedWith.split(";").forEach((db) =>153 databases.add(db.trim())154 );155 }156 if (row.PlatformHaveWorkedWith) {157 row.PlatformHaveWorkedWith.split(";").forEach((platform) =>158 platforms.add(platform.trim())159 );160 }161 if (row.AIModelsHaveWorkedWith) {162 row.AIModelsHaveWorkedWith.split(";").forEach((ai) =>163 aiTools.add(ai.trim())164 );165 aiUsers++;166 }167 168 // Salary169 if (row.CompTotal && row.CompTotal !== "NA" && row.CompTotal !== "") {170 const salary = parseFloat(row.CompTotal.replace(/,/g, ""));171 if (!isNaN(salary)) {172 totalSalary += salary;173 salaryCount++;174 }175 }176 177 // Job Satisfaction178 if (row.JobSat) {179 jobSatLevels[row.JobSat] = (jobSatLevels[row.JobSat] || 0) + 1;180 }181 182 // AI Sentiment183 if (row.AISent) {184 if (185 row.AISent.toLowerCase().includes("agree") ||186 row.AISent.toLowerCase().includes("positive")187 ) {188 aiSentiments.positive++;189 } else if (190 row.AISent.toLowerCase().includes("disagree") ||191 row.AISent.toLowerCase().includes("negative")192 ) {193 aiSentiments.negative++;194 } else {195 aiSentiments.neutral++;196 }197 }198 199 // AI Threat Perception200 if (row.AIThreat) {201 if (202 row.AIThreat.toLowerCase().includes("agree") ||203 row.AIThreat.toLowerCase() === "yes"204 ) {205 aiThreats.yes++;206 } else {207 aiThreats.no++;208 }209 }210 });211 212 const avgSalary = salaryCount > 0 ? totalSalary / salaryCount : 0;213 const aiAdoptionRate = (aiUsers / rows.length) * 100;214 const topCountry = Object.entries(countries).sort((a, b) => b[1] - a[1])[0];215 const topLanguage = Array.from(languages).length;216 const topDevType = Array.from(devTypes).length;217 218 return {219 totalRespondents: rows.length,220 avgSalary,221 salaryCount,222 uniqueCountries: Object.keys(countries).length,223 uniqueLanguages: topLanguage,224 uniqueDevTypes: topDevType,225 aiAdoptionRate,226 aiUsers,227 aiSentiments,228 aiThreats,229 topCountry: topCountry ? topCountry[0] : "N/A",230 topCountryCount: topCountry ? topCountry[1] : 0,231 remoteWorkPercentage:232 (((remoteWorkTypes["Remote"] || 0) + (remoteWorkTypes["Hybrid"] || 0)) /233 rows.length) *234 100,235 fullyRemotePercentage:236 ((remoteWorkTypes["Remote"] || 0) / rows.length) * 100,237 educationDiversity: Object.keys(educationLevels).length,238 avgYearsCode: calculateAverageYearsCode(yearsCodeRanges),239 jobSatisfaction: calculateJobSatisfaction(jobSatLevels),240 organizationSizes: Object.keys(orgSizes).length,241 };242 }, [rows]);243 244 function calculateAverageYearsCode(yearsCodeRanges) {245 let totalYears = 0;246 let totalRespondents = 0;247 248 Object.entries(yearsCodeRanges).forEach(([range, count]) => {249 const avgYears = parseCodeRange(range);250 if (avgYears !== null) {251 totalYears += avgYears * count;252 totalRespondents += count;253 }254 });255 256 return totalRespondents > 0 ? totalYears / totalRespondents : 0;257 }258 259 function parseCodeRange(range) {260 if (range.includes("More than 50")) return 55;261 if (range.includes("30-44")) return 37;262 if (range.includes("20-29")) return 25;263 if (range.includes("15-19")) return 17;264 if (range.includes("10-14")) return 12;265 if (range.includes("5-9")) return 7;266 if (range.includes("3-5")) return 4;267 if (range.includes("1-2")) return 1.5;268 if (range.includes("Less than 1")) return 0.5;269 return null;270 }271 272 function calculateJobSatisfaction(jobSatLevels) {273 const total = Object.values(jobSatLevels).reduce(274 (sum, count) => sum + count,275 0276 );277 const satisfied =278 (jobSatLevels["Very satisfied"] || 0) +279 (jobSatLevels["Slightly satisfied"] || 0);280 return total > 0 ? (satisfied / total) * 100 : 0;281 }282 283 if (!metrics) return null;284 285 const kpiData = [286 {287 icon: "👥",288 title: "Total Respondents",289 value: metrics.totalRespondents.toLocaleString(),290 subtitle: "Developer survey participants",291 accent: "accent-primary",292 trend: 15.2,293 progress: (metrics.totalRespondents / 100000) * 100,294 },295 296 {297 icon: "🌍",298 title: "Global Reach",299 value: metrics.uniqueCountries,300 subtitle: `Top: ${metrics.topCountry} (${metrics.topCountryCount})`,301 accent: "accent-info",302 trend: 5.3,303 progress: (metrics.uniqueCountries / 200) * 100,304 },305 {306 icon: "💻",307 title: "Languages Used",308 value: metrics.uniqueLanguages,309 subtitle: "Programming languages",310 accent: "accent-secondary",311 trend: 12.1,312 progress: (metrics.uniqueLanguages / 50) * 100,313 },314 {315 icon: "🏢",316 title: "Developer Types",317 value: metrics.uniqueDevTypes,318 subtitle: "Professional roles",319 accent: "accent-warning",320 trend: 7.8,321 progress: (metrics.uniqueDevTypes / 30) * 100,322 },323 324 {325 icon: "🏠",326 title: "Remote Work",327 value: `${metrics.remoteWorkPercentage.toFixed(1)}%`,328 subtitle: `${metrics.fullyRemotePercentage.toFixed(1)}% fully remote`,329 accent: "accent-success",330 trend: 18.5,331 progress: metrics.remoteWorkPercentage,332 },333 {334 icon: "🎓",335 title: "Education Diversity",336 value: metrics.educationDiversity,337 subtitle: "Education levels",338 accent: "accent-info",339 trend: 3.2,340 progress: (metrics.educationDiversity / 10) * 100,341 },342 // {343 // icon: "📊",344 // title: "Avg Experience",345 // value: `${metrics.avgYearsCode.toFixed(1)} yrs`,346 // subtitle: "Years coding",347 // accent: "accent-primary",348 // trend: 2.1,349 // progress: (metrics.avgYearsCode / 20) * 100,350 // },351 ];352 353 const insights = [354 // {355 // type: "success",356 // icon: "🚀",357 // text: `AI adoption is booming with ${metrics.aiAdoptionRate.toFixed(358 // 1359 // )}% of developers using AI tools, showing ${(360 // (metrics.aiSentiments.positive / (metrics.aiUsers || 1)) *361 // 100362 // ).toFixed(1)}% positive sentiment`,363 // },364 {365 type: "info",366 icon: "🌐",367 text: `Global developer community spans ${metrics.uniqueCountries} countries with ${metrics.uniqueLanguages} programming languages in use`,368 },369 {370 type: "success",371 icon: "🏠",372 text: `Remote work revolution: ${metrics.remoteWorkPercentage.toFixed(373 1374 )}% work remotely with ${metrics.fullyRemotePercentage.toFixed(375 1376 )}% fully remote`,377 },378 // {379 // type: "warning",380 // icon: "💰",381 // text: `Average salary of $${metrics.avgSalary.toLocaleString()} across ${382 // metrics.salaryCount383 // } respondents, showing ${metrics.jobSatisfaction.toFixed(384 // 1385 // )}% job satisfaction`,386 // },387 ];388 389 return (390 <div className="kpi-overview-container">391 {/* ================= HEADER ================= */}392 <div className="kpi-header-section">393 <div className="kpi-title-area">394 <h1 className="kpi-main-title">395 Stack Overflow Developer Survey Insights396 </h1>397 <div className="kpi-subtitle">398 <span className="live-indicator">●</span>399 Live developer community analytics and trends400 </div>401 </div>402 {/* <div className="kpi-controls">403 <button404 className={`view-toggle ${viewMode === "grid" ? "active" : ""}`}405 onClick={() => setViewMode("grid")}406 >407 Grid View408 </button>409 <button410 className={`view-toggle ${viewMode === "list" ? "active" : ""}`}411 onClick={() => setViewMode("list")}412 >413 List View414 </button>415 </div> */}416 </div>417 418 {/* ================= FEATURED METRICS ================= */}419 <div className="featured-metrics-row">420 <StackOverflowFeaturedMetric421 icon="👥"422 title="Developer Community"423 value={metrics.totalRespondents.toLocaleString()}424 subtitle={`${metrics.uniqueCountries} countries represented`}425 description="Global developer participation in annual survey"426 />427 {/* <StackOverflowFeaturedMetric428 icon="🤖"429 title="AI Revolution"430 value={`${metrics.aiAdoptionRate.toFixed(1)}%`}431 subtitle={`${metrics.aiUsers.toLocaleString()} AI tool users`}432 description={`${(433 (metrics.aiSentiments.positive / (metrics.aiUsers || 1)) *434 100435 ).toFixed(1)}% positive AI sentiment`}436 /> */}437 <StackOverflowFeaturedMetric438 icon="🌍"439 title="Tech Diversity"440 value={metrics.uniqueLanguages}441 subtitle={`${metrics.uniqueDevTypes} developer roles`}442 description={`${metrics.uniqueCountries} countries, ${metrics.educationDiversity} education levels`}443 />444 </div>445 446 {/* ================= KPI GRID ================= */}447 <div className={`kpi-grid ${viewMode}`}>448 {kpiData.map((kpi, index) => (449 <StackOverflowKpiCard450 key={index}451 {...kpi}452 onClick={() =>453 setSelectedMetric(selectedMetric === index ? null : index)454 }455 isActive={selectedMetric === index}456 />457 ))}458 </div>459 460 {/* ================= DETAILED INSIGHTS ================= */}461 <div className="insights-section">462 <div className="insights-header">463 <h2>🎯 Developer Insights</h2>464 <div className="insights-subtitle">465 Key trends and observations from the developer community466 </div>467 </div>468 <div className="insights-grid">469 {insights.map((insight, index) => (470 <div key={index} className={`insight-card insight-${insight.type}`}>471 <div className="insight-icon">{insight.icon}</div>472 <div className="insight-text">{insight.text}</div>473 </div>474 ))}475 </div>476 </div>477 478 {/* ================= SUMMARY STATS ================= */}479 <div className="summary-stats">480 <div className="stat-item">481 <div className="stat-label">Community Growth</div>482 <div className="stat-value">+15.2%</div>483 <div className="stat-period">vs. last year</div>484 </div>485 {/* <div className="stat-item">486 <div className="stat-label">AI Adoption</div>487 <div className="stat-value">{metrics.aiAdoptionRate.toFixed(1)}%</div>488 <div className="stat-period">using AI tools</div>489 </div> */}490 <div className="stat-item">491 <div className="stat-label">Remote Work</div>492 <div className="stat-value">493 {metrics.remoteWorkPercentage.toFixed(1)}%494 </div>495 <div className="stat-period">work remotely</div>496 </div>497 {/* <div className="stat-item">498 <div className="stat-label">Job Satisfaction</div>499 <div className="stat-value">500 {metrics.jobSatisfaction.toFixed(1)}%501 </div>502 <div className="stat-period">satisfied developers</div>503 </div> */}504 </div>505 </div>506 );507}508 