/ src / components / Charts / Namada / ProposalsChart.tsx
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;