361 lines
13 KiB
TypeScript
361 lines
13 KiB
TypeScript
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { LineChart, BarChart3, PieChart } from "lucide-react";
|
|
import {
|
|
LineChart as RechartsLineChart,
|
|
Line,
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
Tooltip,
|
|
Legend,
|
|
ResponsiveContainer,
|
|
BarChart,
|
|
Bar,
|
|
PieChart as RechartsPieChart,
|
|
Pie,
|
|
Cell,
|
|
} from "recharts";
|
|
import React from "react";
|
|
|
|
// Colors for charts
|
|
const CHART_COLORS = [
|
|
"#8884d8", "#83a6ed", "#8dd1e1", "#82ca9d", "#a4de6c",
|
|
"#d0ed57", "#ffc658", "#ff8042", "#ff6361", "#bc5090"
|
|
];
|
|
|
|
type TradeChartsProps = {
|
|
visualizationData: {
|
|
recordsByYear: { year: number; count: number }[];
|
|
topImporters: { country: string; count: number }[];
|
|
topExporters: { country: string; count: number }[];
|
|
termsTraded: { term: string; count: number }[];
|
|
tradePurposes: { purpose: string; count: number; description: string }[];
|
|
tradeSources: { source: string; count: number; description: string }[];
|
|
termQuantitiesByYear: { year: number; [term: string]: number }[];
|
|
topTerms: string[];
|
|
};
|
|
PURPOSE_DESCRIPTIONS: Record<string, string>;
|
|
SOURCE_DESCRIPTIONS: Record<string, string>;
|
|
};
|
|
|
|
export function TradeCharts({ visualizationData, PURPOSE_DESCRIPTIONS, SOURCE_DESCRIPTIONS }: TradeChartsProps) {
|
|
// Process terms data to combine small segments
|
|
const processedTermsData = React.useMemo(() => {
|
|
const threshold = 0.02; // 2% threshold
|
|
let otherCount = 0;
|
|
|
|
// Sort by count in descending order
|
|
const sortedTerms = [...visualizationData.termsTraded].sort((a, b) => b.count - a.count);
|
|
|
|
// Calculate total for percentage
|
|
const total = sortedTerms.reduce((sum, item) => sum + item.count, 0);
|
|
|
|
// Filter and combine small segments
|
|
const significantTerms = sortedTerms.filter(item => {
|
|
const percentage = item.count / total;
|
|
if (percentage < threshold) {
|
|
otherCount += item.count;
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
// Add "Other" category if there are small segments
|
|
if (otherCount > 0) {
|
|
significantTerms.push({ term: 'Other', count: otherCount });
|
|
}
|
|
|
|
return significantTerms;
|
|
}, [visualizationData.termsTraded]);
|
|
|
|
// Custom tooltip for pie chart
|
|
const renderCustomizedLabel = ({ cx, cy, midAngle, outerRadius, percent, payload }: any) => {
|
|
const RADIAN = Math.PI / 180;
|
|
const radius = outerRadius * 1.15; // Increased radius for better spacing
|
|
const x = cx + radius * Math.cos(-midAngle * RADIAN);
|
|
const y = cy + radius * Math.sin(-midAngle * RADIAN);
|
|
|
|
return (
|
|
<text
|
|
x={x}
|
|
y={y}
|
|
fill="#000000"
|
|
textAnchor={x > cx ? 'start' : 'end'}
|
|
dominantBaseline="central"
|
|
fontSize={12}
|
|
fontWeight="500"
|
|
>
|
|
{`${payload.term} (${(percent * 100).toFixed(0)}%)`}
|
|
</text>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
<h3 className="text-lg font-semibold">Trade Visualizations</h3>
|
|
|
|
{/* Records Over Time */}
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<div className="flex items-center">
|
|
<LineChart className="mr-2 h-5 w-5 text-muted-foreground" />
|
|
<CardTitle className="text-base">Records Over Time</CardTitle>
|
|
</div>
|
|
<CardDescription>Number of trade records by year</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="h-[300px] w-full">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<RechartsLineChart
|
|
data={visualizationData.recordsByYear}
|
|
margin={{ top: 5, right: 30, left: 20, bottom: 25 }}
|
|
>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis
|
|
dataKey="year"
|
|
angle={-45}
|
|
textAnchor="end"
|
|
tick={{ fontSize: 12 }}
|
|
/>
|
|
<YAxis />
|
|
<Tooltip />
|
|
<Legend />
|
|
<Line
|
|
type="monotone"
|
|
dataKey="count"
|
|
name="Records"
|
|
stroke="#8884d8"
|
|
activeDot={{ r: 8 }}
|
|
/>
|
|
</RechartsLineChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Top Importers and Exporters */}
|
|
<div className="grid gap-6 md:grid-cols-2">
|
|
{/* Top Importers */}
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<div className="flex items-center">
|
|
<BarChart3 className="mr-2 h-5 w-5 text-muted-foreground" />
|
|
<CardTitle className="text-base">Top Importers</CardTitle>
|
|
</div>
|
|
<CardDescription>Countries importing the most specimens</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="h-[300px] w-full">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<BarChart
|
|
data={visualizationData.topImporters}
|
|
margin={{ top: 5, right: 30, left: 20, bottom: 60 }}
|
|
>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis
|
|
dataKey="country"
|
|
angle={-45}
|
|
textAnchor="end"
|
|
tick={{ fontSize: 12 }}
|
|
interval={0}
|
|
/>
|
|
<YAxis />
|
|
<Tooltip />
|
|
<Bar dataKey="count" name="Records" fill="#8884d8" />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Top Exporters */}
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<div className="flex items-center">
|
|
<BarChart3 className="mr-2 h-5 w-5 text-muted-foreground" />
|
|
<CardTitle className="text-base">Top Exporters</CardTitle>
|
|
</div>
|
|
<CardDescription>Countries exporting the most specimens</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="h-[300px] w-full">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<BarChart
|
|
data={visualizationData.topExporters}
|
|
margin={{ top: 5, right: 30, left: 20, bottom: 60 }}
|
|
>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis
|
|
dataKey="country"
|
|
angle={-45}
|
|
textAnchor="end"
|
|
tick={{ fontSize: 12 }}
|
|
interval={0}
|
|
/>
|
|
<YAxis />
|
|
<Tooltip />
|
|
<Bar dataKey="count" name="Records" fill="#82ca9d" />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Terms Traded */}
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<div className="flex items-center">
|
|
<PieChart className="mr-2 h-5 w-5 text-muted-foreground" />
|
|
<CardTitle className="text-base">Terms Traded</CardTitle>
|
|
</div>
|
|
<CardDescription>Distribution of specimen types in trade</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="h-[400px] w-full">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<RechartsPieChart>
|
|
<Pie
|
|
data={processedTermsData}
|
|
cx="50%"
|
|
cy="50%"
|
|
labelLine={true}
|
|
label={renderCustomizedLabel}
|
|
outerRadius={130}
|
|
fill="#8884d8"
|
|
dataKey="count"
|
|
nameKey="term"
|
|
>
|
|
{processedTermsData.map((_, index) => (
|
|
<Cell key={`cell-${index}`} fill={CHART_COLORS[index % CHART_COLORS.length]} />
|
|
))}
|
|
</Pie>
|
|
<Tooltip formatter={(value, _, props) => [`${value} records`, props.payload.term]} />
|
|
</RechartsPieChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Trade Purposes and Sources */}
|
|
<div className="grid gap-6 md:grid-cols-2">
|
|
{/* Trade Purposes */}
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<div className="flex items-center">
|
|
<BarChart3 className="mr-2 h-5 w-5 text-muted-foreground" />
|
|
<CardTitle className="text-base">Trade Purposes</CardTitle>
|
|
</div>
|
|
<CardDescription>Reasons for trade</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="h-[300px] w-full">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<BarChart
|
|
data={visualizationData.tradePurposes}
|
|
margin={{ top: 5, right: 30, left: 20, bottom: 60 }}
|
|
layout="vertical"
|
|
>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis type="number" />
|
|
<YAxis
|
|
dataKey="purpose"
|
|
type="category"
|
|
tick={{ fontSize: 12 }}
|
|
tickFormatter={(value) => `${value} - ${PURPOSE_DESCRIPTIONS[value] || 'Unknown'}`}
|
|
width={150}
|
|
/>
|
|
<Tooltip formatter={(value, _, props) => [
|
|
`${value} records`,
|
|
`${props.payload.purpose} - ${props.payload.description}`
|
|
]} />
|
|
<Bar dataKey="count" name="Records" fill="#8884d8" />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Trade Sources */}
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<div className="flex items-center">
|
|
<BarChart3 className="mr-2 h-5 w-5 text-muted-foreground" />
|
|
<CardTitle className="text-base">Trade Sources</CardTitle>
|
|
</div>
|
|
<CardDescription>Origin of traded specimens</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="h-[300px] w-full">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<BarChart
|
|
data={visualizationData.tradeSources}
|
|
margin={{ top: 5, right: 30, left: 20, bottom: 60 }}
|
|
layout="vertical"
|
|
>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis type="number" />
|
|
<YAxis
|
|
dataKey="source"
|
|
type="category"
|
|
tick={{ fontSize: 12 }}
|
|
tickFormatter={(value) => `${value} - ${SOURCE_DESCRIPTIONS[value] || 'Unknown'}`}
|
|
width={150}
|
|
/>
|
|
<Tooltip formatter={(value, _, props) => [
|
|
`${value} records`,
|
|
`${props.payload.source} - ${props.payload.description}`
|
|
]} />
|
|
<Bar dataKey="count" name="Records" fill="#82ca9d" />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Quantity of Top Terms Over Time */}
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<div className="flex items-center">
|
|
<LineChart className="mr-2 h-5 w-5 text-muted-foreground" />
|
|
<CardTitle className="text-base">Quantity of Top Terms Over Time</CardTitle>
|
|
</div>
|
|
<CardDescription>Trends in quantities of the most traded terms</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="h-[300px] w-full">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<RechartsLineChart
|
|
data={visualizationData.termQuantitiesByYear}
|
|
margin={{ top: 5, right: 30, left: 20, bottom: 25 }}
|
|
>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis
|
|
dataKey="year"
|
|
angle={-45}
|
|
textAnchor="end"
|
|
tick={{ fontSize: 12 }}
|
|
/>
|
|
<YAxis />
|
|
<Tooltip />
|
|
<Legend />
|
|
{visualizationData.topTerms.map((term, index) => (
|
|
<Line
|
|
key={term}
|
|
type="monotone"
|
|
dataKey={term}
|
|
name={term}
|
|
stroke={CHART_COLORS[index % CHART_COLORS.length]}
|
|
activeDot={{ r: 8 }}
|
|
/>
|
|
))}
|
|
</RechartsLineChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|