ProposalsChart.tsx
1 import React, { RefObject, useEffect, useState } from "react"; 2 import { Bar } from "react-chartjs-2"; 3 import { DATA_URL } from "../../lib/chart/data-url"; 4 import { 5 Chart as ChartJS, 6 CategoryScale, 7 LinearScale, 8 BarElement, 9 Title, 10 Tooltip, 11 Legend, 12 } from "chart.js"; 13 14 ChartJS.register( 15 CategoryScale, 16 LinearScale, 17 BarElement, 18 Title, 19 Tooltip, 20 Legend 21 ); 22 23 interface ProposalsChartProps { 24 divChartRef?: RefObject<HTMLDivElement | null>; 25 } 26 27 const ProposalsChart = (props: ProposalsChartProps) => { 28 const [loading, setLoading] = useState(true); 29 const [error, setError] = useState<string | null>(null); 30 const [voteCounts, setVoteCounts] = useState<number[]>([]); 31 const [timeRange, setTimeRange] = useState<"10" | "20" | "all">("all"); 32 33 useEffect(() => { 34 const fetchVoteCounts = async () => { 35 try { 36 setLoading(true); 37 setError(null); 38 39 // Replace with your actual DATA_URL.propAddressesCounts 40 const response = await fetch(DATA_URL.propAddressesCounts); 41 42 if (!response.ok) { 43 throw new Error(`HTTP error! status: ${response.status}`); 44 } 45 46 const data: number[] = await response.json(); 47 48 if (!Array.isArray(data)) { 49 throw new Error("Invalid data format"); 50 } 51 52 setVoteCounts(data); 53 } catch (err) { 54 console.error("Fetch error:", err); 55 setError( 56 err instanceof Error ? err.message : "Failed to load vote data" 57 ); 58 } finally { 59 setLoading(false); 60 } 61 }; 62 63 fetchVoteCounts(); 64 65 // Optional: Set up polling for live data 66 const interval = setInterval(fetchVoteCounts, 300000); // 5 minutes 67 68 return () => clearInterval(interval); 69 }, []); 70 71 // Filter data based on time range 72 const filteredData = (() => { 73 if (timeRange === "all") return voteCounts; 74 const count = timeRange === "10" ? 10 : 20; 75 return voteCounts.slice(0, count); 76 })(); 77 78 // Process data for chart 79 const chartData = { 80 labels: filteredData.map((_, index) => `#${index + 1}`), 81 datasets: [ 82 { 83 label: "Number of Addresses Voted", 84 data: filteredData, 85 backgroundColor: filteredData.map((value, index) => { 86 const hue = (index * 360) / filteredData.length; 87 return `hsla(${hue}, 70%, 60%, 0.8)`; 88 }), 89 borderColor: filteredData.map((value, index) => { 90 const hue = (index * 360) / filteredData.length; 91 return `hsla(${hue}, 70%, 50%, 1)`; 92 }), 93 borderWidth: 2, 94 borderRadius: 8, 95 hoverBackgroundColor: filteredData.map((value, index) => { 96 const hue = (index * 360) / filteredData.length; 97 return `hsla(${hue}, 70%, 70%, 0.9)`; 98 }), 99 }, 100 ], 101 }; 102 103 const options: any = { 104 responsive: true, 105 maintainAspectRatio: false, 106 interaction: { 107 mode: "index" as const, 108 intersect: false, 109 }, 110 scales: { 111 x: { 112 title: { 113 display: true, 114 text: "Proposal Index", 115 font: { 116 size: 14, 117 weight: "bold", 118 }, 119 }, 120 grid: { 121 display: false, 122 }, 123 }, 124 y: { 125 type: "linear" as const, 126 display: true, 127 title: { 128 display: true, 129 text: "Number of Addresses", 130 font: { 131 size: 14, 132 weight: "bold", 133 }, 134 }, 135 beginAtZero: true, 136 grid: { 137 color: "rgba(0, 0, 0, 0.05)", 138 }, 139 }, 140 }, 141 plugins: { 142 title: { 143 display: true, 144 text: "Voting Participation by Proposal", 145 font: { 146 size: 18, 147 weight: "bold", 148 }, 149 padding: 20, 150 }, 151 legend: { 152 display: false, 153 }, 154 tooltip: { 155 backgroundColor: "rgba(0, 0, 0, 0.8)", 156 padding: 12, 157 titleFont: { 158 size: 14, 159 }, 160 bodyFont: { 161 size: 13, 162 }, 163 callbacks: { 164 label: function (context: any) { 165 return `Addresses: ${context.parsed.y.toLocaleString()}`; 166 }, 167 }, 168 }, 169 }, 170 }; 171 172 if (loading) { 173 return ( 174 <div className="flex justify-center items-center h-64"> 175 <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div> 176 </div> 177 ); 178 } 179 180 if (error) { 181 return ( 182 <div className="bg-red-50 border border-red-200 rounded p-4 text-red-700"> 183 <p>Error loading vote data:</p> 184 <p className="font-medium">{error}</p> 185 <button 186 onClick={() => window.location.reload()} 187 className="mt-2 px-3 py-1 bg-red-100 hover:bg-red-200 rounded" 188 > 189 Retry 190 </button> 191 </div> 192 ); 193 } 194 195 if (voteCounts.length === 0) { 196 return ( 197 <div className="bg-yellow-50 border border-yellow-200 rounded p-4 text-yellow-700"> 198 No vote data available 199 </div> 200 ); 201 } 202 203 const totalProposals = voteCounts.length; 204 const totalAddresses = voteCounts.reduce((sum, count) => sum + count, 0); 205 const avgAddresses = (totalAddresses / totalProposals).toFixed(0); 206 const maxVotes = Math.max(...voteCounts); 207 const maxProposal = voteCounts.indexOf(maxVotes) + 1; 208 209 return ( 210 <div 211 ref={props.divChartRef} 212 className="bg-white dark:bg-slate-900 px-4 py-6 md:px-6 rounded-lg shadow-sm border border-gray-200 dark:border-slate-700" 213 style={{ width: "100%" }} 214 > 215 <div className="flex flex-col md:flex-row gap-4 mb-6 items-center"> 216 <h2 className="flex-1 text-xl font-semibold"> 217 Proposal Voting Participation 218 </h2> 219 <div className="flex items-center gap-2"> 220 <label className="text-sm font-medium">Show:</label> 221 <select 222 value={timeRange} 223 onChange={(e) => setTimeRange(e.target.value as any)} 224 className="w-48 border dark:border-slate-700 rounded px-3 py-2 bg-white dark:bg-slate-800" 225 > 226 <option value="all">All Proposals ({totalProposals})</option> 227 <option value="10">First 10 Proposals</option> 228 <option value="20">First 20 Proposals</option> 229 </select> 230 </div> 231 </div> 232 233 <div className="w-full" style={{ height: "480px", position: "relative" }}> 234 <Bar data={chartData} options={options} /> 235 </div> 236 237 <div className="mt-6 grid grid-cols-1 md:grid-cols-3 gap-4"> 238 <div className="bg-blue-50 dark:bg-slate-800 p-4 rounded-lg"> 239 <h3 className="font-semibold text-blue-800 dark:text-blue-300"> 240 Total Proposals 241 </h3> 242 <p className="text-2xl font-bold dark:text-white">{totalProposals}</p> 243 </div> 244 <div className="bg-green-50 dark:bg-slate-800 p-4 rounded-lg"> 245 <h3 className="font-semibold text-green-800 dark:text-green-300"> 246 Average Participation 247 </h3> 248 <p className="text-2xl font-bold dark:text-white"> 249 {avgAddresses} addresses 250 </p> 251 </div> 252 <div className="bg-purple-50 dark:bg-slate-800 p-4 rounded-lg"> 253 <h3 className="font-semibold text-purple-800 dark:text-purple-300"> 254 Highest Participation 255 </h3> 256 <p className="text-2xl font-bold dark:text-white"> 257 {maxVotes.toLocaleString()} 258 </p> 259 <p className="text-sm text-gray-500 dark:text-slate-400"> 260 Proposal {maxProposal} 261 </p> 262 </div> 263 </div> 264 </div> 265 ); 266 }; 267 268 export default ProposalsChart;