harshapitla/github-actions-analytics
0
1import React, { useMemo, useState } from "react";2import {3 ResponsiveContainer,4 ComposedChart,5 Area,6 Line,7 XAxis,8 YAxis,9 Tooltip,10 CartesianGrid,11 ReferenceArea,12} from "recharts";13import formatSI from "../components/utils/formatSI";14 15export default function SuccessTrajectory({ rows }) {16 const [viewMode, setViewMode] = useState("median"); 17 18 const data = useMemo(() => {19 const buckets = {20 1: [],21 2: [],22 3: [],23 4: [],24 5: [],25 };26 27 rows.forEach((r) => {28 const ratingRaw = Number(r.rating);29 if (!Number.isFinite(ratingRaw)) return;30 31 let bucket = Math.floor(ratingRaw);32 if (bucket <= 1) bucket = 1;33 if (bucket > 5) bucket = 5;34 35 buckets[bucket].push(Number(r.downloads_count) || 0);36 });37 38 const out = [];39 for (let b = 1; b <= 5; b++) {40 const arr = buckets[b].slice().sort((a, b) => a - b);41 if (arr.length === 0) {42 out.push({ rating: b, median: 0, p25: 0, p75: 0, count: 0 });43 } else {44 const p25 = arr[Math.floor(arr.length * 0.25)];45 const median = arr[Math.floor(arr.length * 0.5)];46 const p75 = arr[Math.floor(arr.length * 0.75)];47 48 out.push({49 rating: b,50 median,51 p25,52 p75,53 count: arr.length,54 });55 }56 }57 return out;58 }, [rows]);59 60 const tooltipFormatter = (value, key) => {61 if (key === "median") return [`${formatSI(value)}`, "Median downloads"];62 if (key === "p25") return [`${formatSI(value)}`, "Lower range (25th pct.)"];63 if (key === "p75") return [`${formatSI(value)}`, "Upper range (75th pct.)"];64 return [value, key];65 };66 67 const tooltipLabelFormatter = (label) => {68 const entry = data.find((d) => d.rating === label);69 if (!entry) return `Rating ${label}`;70 return `Rating ${label} • ${entry.count.toLocaleString()} extensions`;71 };72 73 const rangeMax = Math.max(...data.map((d) => d.p75));74 75 return (76 <>77 <h3>Success Trajectory Map</h3>78 79 <p className="small">80 Shows how downloads change across rating levels. The shaded band81 represents the typical range for most extensions, while the line shows82 the median.83 </p>84 85 <div className="feature-controls" style={{ justifyContent: "flex-start", marginBottom: 12 }}>86 <div className="filter-group">87 <span className="filter-label">View:</span>88 {[89 { id: "median", label: "Median + range" },90 { id: "range", label: "Range only" },91 ].map((option) => (92 <button93 key={option.id}94 className={`pill-button ${viewMode === option.id ? "is-active" : ""}`}95 onClick={() => setViewMode(option.id)}96 >97 {option.label}98 </button>99 ))}100 </div>101 </div>102 103 <div className="chart-area">104 <ResponsiveContainer width="100%" height={440}>105 <ComposedChart106 data={data}107 margin={{ top: 10, right: 40, left: 80, bottom: 30 }}108 >109 <CartesianGrid strokeDasharray="3 3" vertical={false} />110 <XAxis111 dataKey="rating"112 type="number"113 domain={[1, 5]}114 ticks={[1, 2, 3, 4, 5]}115 tick={{ fill: "#1f2933", fontSize: 13 }}116 label={{ value: "Rating", position: "insideBottom", offset: -6, fill: "#1f2933", fontSize: 13 }}117 />118 <YAxis119 yAxisId="range"120 tickFormatter={(v) => formatSI(v)}121 tick={{ fill: "#1f2933", fontSize: 12 }}122 label={{ value: "Downloads (range)", angle: -90, position: "insideLeft", offset: -10, fill: "#1f2933" }}123 />124 <YAxis125 yAxisId="median"126 orientation="right"127 tickFormatter={(v) => formatSI(v)}128 tick={{ fill: "#64748b", fontSize: 11 }}129 domain={[0, Math.max(...data.map((d) => d.median)) * 1.5]}130 label={{ value: "Median scale", angle: 90, position: "insideRight", offset: 18, fill: "#64748b" }}131 />132 <Tooltip133 formatter={tooltipFormatter}134 labelFormatter={tooltipLabelFormatter}135 contentStyle={{136 borderRadius: 8,137 border: "1px solid #d1d5db",138 fontSize: 13,139 }}140 />141 <Area142 type="stepAfter"143 dataKey="p75"144 yAxisId="range"145 stroke="#96c8ff"146 strokeWidth={1.5}147 fill="#dceeff"148 fillOpacity={0.7}149 isAnimationActive={false}150 />151 <Area152 type="stepAfter"153 dataKey="p25"154 yAxisId="range"155 stroke="#cde5ff"156 strokeWidth={1}157 fill="#f5fbff"158 fillOpacity={0.65}159 isAnimationActive={false}160 />161 {viewMode !== "range" && (162 <Line163 type="stepAfter"164 dataKey="median"165 yAxisId="median"166 stroke="#1d4ed8"167 strokeWidth={3}168 dot={{ r: 7, stroke: "#1d4ed8", strokeWidth: 2, fill: "#fff" }}169 activeDot={{ r: 9 }}170 isAnimationActive={false}171 />172 )}173 {rangeMax > 0 && (174 <ReferenceArea175 x1={1.95}176 x2={2.5}177 y1={data.find((d) => d.rating === 2)?.p25}178 y2={data.find((d) => d.rating === 2)?.p75}179 yAxisId="range"180 stroke="none"181 fillOpacity={0}182 label={{183 value: "Range spike driven by a few popular extensions",184 fill: "#94a3b8",185 position: "top",186 offset: 14,187 dy: -4,188 fontSize: 12,189 }}190 />191 )}192 </ComposedChart>193 </ResponsiveContainer>194 </div>195 196 <p className="small" style={{ marginTop: 10, color: "#475569" }}>197 Higher ratings generally align with steady download levels. Extensions198 with very high downloads sometimes receive mixed ratings because they199 serve large and diverse user groups.200 </p>201 </>202 );203}204 