Spaces:
Sleeping
Sleeping
Download TruthScan AI_frontend/components/EmotionalPieChart.tsx from Bjornpool/truthscan-ai-backend: direct link, hf CLI and curl.
- Browser
- Download file 6.29 kB
-
https://huggingface.co/spaces/Bjornpool/truthscan-ai-backend/resolve/a7e77af2424a1608cd0f9013ccdf15d6e8e46c05/TruthScan%20AI_frontend/components/EmotionalPieChart.tsx
- Command line
-
hf download 'hf://spaces/Bjornpool/truthscan-ai-backend@a7e77af2424a1608cd0f9013ccdf15d6e8e46c05/TruthScan AI_frontend/components/EmotionalPieChart.tsx'
-
curl -L -o EmotionalPieChart.tsx https://huggingface.co/spaces/Bjornpool/truthscan-ai-backend/resolve/a7e77af2424a1608cd0f9013ccdf15d6e8e46c05/TruthScan%20AI_frontend/components/EmotionalPieChart.tsx
6.29 kB
| /** | |
| * Komponent wizualizujący rozkład emocji w artykułach w formie wykresu kołowego. | |
| * | |
| * Odpowiada za: | |
| * - normalizację i agregację danych sentymentu, | |
| * - tłumaczenie etykiet w zależności od języka interfejsu, | |
| * - prezentację danych z wykorzystaniem biblioteki Recharts, | |
| * - obsługę tooltipów i etykiet procentowych. | |
| */ | |
| "use client"; | |
| import { useMemo } from "react"; | |
| import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from "recharts"; | |
| import { useLanguage } from "../app/hooks/useLanguage"; | |
| import locales from "../lib/locales"; | |
| interface EmotionalPieChartProps { | |
| data: { name: string; value: number }[]; | |
| title?: string; | |
| } | |
| type Canonical = "POSITIVE" | "NEGATIVE" | "NEUTRAL"; | |
| const EMOTION_COLORS = { | |
| POSITIVE: "#22c55e", // green | |
| NEGATIVE: "#ef4444", // red | |
| NEUTRAL: "#3b82f6", // blue | |
| } as const; | |
| const EMOTION_LABELS = { | |
| pl: { | |
| POSITIVE: "Pozytywne", | |
| NEGATIVE: "Negatywne", | |
| NEUTRAL: "Neutralne", | |
| }, | |
| en: { | |
| POSITIVE: "Positive", | |
| NEGATIVE: "Negative", | |
| NEUTRAL: "Neutral", | |
| }, | |
| no: { | |
| POSITIVE: "Positivt", | |
| NEGATIVE: "Negativt", | |
| NEUTRAL: "Nøytralt", | |
| }, | |
| } as const; | |
| export default function EmotionalPieChart({ data, title }: EmotionalPieChartProps) { | |
| const { language } = useLanguage(); | |
| const t = locales[language]; | |
| const toCanonical = (raw: string): Canonical | null => { | |
| const u = (raw || "").trim().toUpperCase(); | |
| if (u === "POSITIVE" || u === "POZYTYWNE") return "POSITIVE"; | |
| if (u === "NEGATIVE" || u === "NEGATYWNE") return "NEGATIVE"; | |
| if (u === "NEUTRAL" || u === "NEUTRALNE") return "NEUTRAL"; | |
| return null; | |
| }; | |
| const translatedData = useMemo(() => { | |
| const initialData: Record<Canonical, number> = { | |
| POSITIVE: 0, NEGATIVE: 0, NEUTRAL: 0 | |
| }; | |
| for (const item of data) { | |
| const c = toCanonical(item.name); | |
| if (!c) continue; | |
| initialData[c] = (initialData[c] ?? 0) + (item.value ?? 0); | |
| } | |
| return (["POSITIVE", "NEUTRAL", "NEGATIVE"] as Canonical[]) | |
| .map(canonical => ({ | |
| canonical, | |
| name: EMOTION_LABELS[language][canonical], | |
| value: initialData[canonical], | |
| color: EMOTION_COLORS[canonical] | |
| })) | |
| .filter(item => item.value > 0); | |
| }, [data, language]); | |
| const totalValue = useMemo(() => | |
| translatedData.reduce((sum, item) => sum + item.value, 0), | |
| [translatedData] | |
| ); | |
| const RAD = Math.PI / 180; | |
| const renderLabel = ({ cx, cy, midAngle, outerRadius, name, value, payload }: any) => { | |
| const r = (outerRadius ?? 0) + 14; | |
| const x = cx + r * Math.cos(-midAngle * RAD); | |
| const y = cy + r * Math.sin(-midAngle * RAD); | |
| const anchor: "start" | "end" = x > cx ? "start" : "end"; | |
| const fill = EMOTION_COLORS[(payload?.canonical as Canonical) ?? "NEUTRAL"]; | |
| return ( | |
| <text x={x} y={y} fill={fill} textAnchor={anchor} dominantBaseline="central" | |
| className="text-[12px] select-none font-medium"> | |
| {`${name}: ${value}`} | |
| </text> | |
| ); | |
| }; | |
| const CustomTooltip = ({ active, payload }: any) => { | |
| if (active && payload && payload.length) { | |
| const data = payload[0].payload; | |
| const percentage = totalValue > 0 ? (data.value / totalValue) * 100 : 0; | |
| return ( | |
| <div className="bg-gray-900 dark:bg-gray-800 p-3 rounded-lg border border-gray-700 shadow-lg"> | |
| <p className="text-white font-medium">{data.name}</p> | |
| <p className="text-gray-300"> | |
| {language === "pl" ? "Liczba artykułów: " : language === "no" ? "Antall artikler: " : "Number of articles: "} | |
| <span className="text-white font-semibold">{data.value}</span> | |
| </p> | |
| <p className="text-gray-300"> | |
| {language === "pl" ? "Procent: " : language === "no" ? "Prosent: " : "Percentage: "} | |
| <span className="text-white font-semibold">{percentage.toFixed(1)}%</span> | |
| </p> | |
| </div> | |
| ); | |
| } | |
| return null; | |
| }; | |
| if (translatedData.length === 0) { | |
| return ( | |
| <div className="w-full"> | |
| <h3 className="text-m font-semibold mb-2 text-gray-800 dark:text-gray-100"> | |
| {title || t.emotionsInArticles} | |
| </h3> | |
| <div className="h-64 flex items-center justify-center border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg"> | |
| <p className="text-gray-500 dark:text-gray-400 text-center"> | |
| {language === "pl" ? "Brak danych do wyświetlenia" : language === "no" ? "Ingen data å vise" : "No data to display"} | |
| </p> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="w-full"> | |
| <h3 className="text-m font-semibold mb-2 text-gray-800 dark:text-gray-100"> | |
| {title || t.emotionsInArticles} | |
| </h3> | |
| <div className="h-64"> | |
| <ResponsiveContainer width="100%" height="100%"> | |
| <PieChart margin={{ top: 8, right: 28, left: 28, bottom: 0 }}> | |
| <Pie | |
| data={translatedData} | |
| dataKey="value" | |
| nameKey="name" | |
| cx="50%" | |
| cy="50%" | |
| outerRadius="70%" | |
| label={renderLabel} | |
| labelLine={false} | |
| isAnimationActive={true} | |
| animationDuration={500} | |
| > | |
| {translatedData.map((item, idx) => ( | |
| <Cell key={idx} fill={item.color} stroke="#1f2937" strokeWidth={1} /> | |
| ))} | |
| </Pie> | |
| <Tooltip content={<CustomTooltip />} /> | |
| <Legend | |
| verticalAlign="bottom" | |
| height={36} | |
| formatter={(value, entry: any) => ( | |
| <span style={{ color: entry.color, fontSize: '12px' }}>{value}</span> | |
| )} | |
| wrapperStyle={{ paddingTop: '10px' }} | |
| /> | |
| </PieChart> | |
| </ResponsiveContainer> | |
| </div> | |
| <p className="text-s text-center text-gray-500 dark:text-gray-400 mt-4"> | |
| {language === "pl" | |
| ? "Wykres pokazuje rozkład emocji w analizowanych artykułach." | |
| : language === "no" | |
| ? "Diagrammet viser fordelingen av følelser i analyserte artikler." | |
| : "This chart shows the distribution of detected emotions in articles."} | |
| </p> | |
| </div> | |
| ); | |
| } |