arctic-species-portal/src/hooks/useTradeDataVisualization.ts

229 lines
6.6 KiB
TypeScript

import { useMemo } from "react";
import { CitesTradeRecord } from "@/lib/api";
// Helper types for visualization data
type YearlyData = {
year: number;
count: number;
};
type CountryData = {
country: string;
count: number;
};
type TermData = {
term: string;
count: number;
};
type PurposeData = {
purpose: string;
count: number;
description: string;
};
type SourceData = {
source: string;
count: number;
description: string;
};
type TermYearData = {
year: number;
[term: string]: number;
};
type VisualizationData = {
recordsByYear: YearlyData[];
topImporters: CountryData[];
topExporters: CountryData[];
termsTraded: TermData[];
tradePurposes: PurposeData[];
tradeSources: SourceData[];
termQuantitiesByYear: TermYearData[];
topTerms: string[];
} | null;
// Purpose code descriptions
const PURPOSE_DESCRIPTIONS: Record<string, string> = {
'T': 'Commercial',
'Z': 'Zoo',
'G': 'Botanical garden',
'Q': 'Circus or travelling exhibition',
'S': 'Scientific',
'H': 'Hunting trophy',
'P': 'Personal',
'M': 'Medical',
'E': 'Educational',
'N': 'Reintroduction',
'B': 'Breeding',
'L': 'Law enforcement'
};
// Source code descriptions
const SOURCE_DESCRIPTIONS: Record<string, string> = {
'W': 'Wild',
'R': 'Ranched',
'D': 'Captive-bred (App. I)',
'A': 'Artificially propagated (plants)',
'C': 'Captive-bred (App. II/III)',
'F': 'Born in captivity (F1)',
'U': 'Unknown',
'I': 'Confiscated/seized',
'O': 'Pre-Convention'
};
export function useTradeDataVisualization(
tradeRecords: CitesTradeRecord[] | undefined,
filteredRecords: CitesTradeRecord[] | undefined = undefined
) {
// Use filtered records for visualization if provided, otherwise use all records
const recordsToVisualize = filteredRecords || tradeRecords;
// Compute visualization data
const visualizationData = useMemo((): VisualizationData => {
if (!recordsToVisualize || recordsToVisualize.length === 0) return null;
// Records by year
const yearCounts: Record<number, number> = {};
recordsToVisualize.forEach(record => {
yearCounts[record.year] = (yearCounts[record.year] || 0) + 1;
});
const recordsByYear: YearlyData[] = Object.entries(yearCounts)
.map(([year, count]) => ({ year: parseInt(year), count }))
.sort((a, b) => a.year - b.year);
// Top importers
const importerCounts: Record<string, number> = {};
recordsToVisualize.forEach(record => {
if (record.importer) {
importerCounts[record.importer] = (importerCounts[record.importer] || 0) + 1;
}
});
const topImporters: CountryData[] = Object.entries(importerCounts)
.map(([country, count]) => ({ country, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 10);
// Top exporters
const exporterCounts: Record<string, number> = {};
recordsToVisualize.forEach(record => {
if (record.exporter) {
exporterCounts[record.exporter] = (exporterCounts[record.exporter] || 0) + 1;
}
});
const topExporters: CountryData[] = Object.entries(exporterCounts)
.map(([country, count]) => ({ country, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 10);
// Terms traded
const termCounts: Record<string, number> = {};
recordsToVisualize.forEach(record => {
termCounts[record.term] = (termCounts[record.term] || 0) + 1;
});
const termsTraded: TermData[] = Object.entries(termCounts)
.map(([term, count]) => ({ term, count }))
.sort((a, b) => b.count - a.count);
// Trade purposes
const purposeCounts: Record<string, number> = {};
recordsToVisualize.forEach(record => {
if (record.purpose) {
purposeCounts[record.purpose] = (purposeCounts[record.purpose] || 0) + 1;
}
});
const tradePurposes: PurposeData[] = Object.entries(purposeCounts)
.map(([purpose, count]) => ({
purpose,
count,
description: PURPOSE_DESCRIPTIONS[purpose] || 'Unknown'
}))
.sort((a, b) => b.count - a.count);
// Trade sources
const sourceCounts: Record<string, number> = {};
recordsToVisualize.forEach(record => {
if (record.source) {
sourceCounts[record.source] = (sourceCounts[record.source] || 0) + 1;
}
});
const tradeSources: SourceData[] = Object.entries(sourceCounts)
.map(([source, count]) => ({
source,
count,
description: SOURCE_DESCRIPTIONS[source] || 'Unknown'
}))
.sort((a, b) => b.count - a.count);
// Top terms by year
const topTerms = termsTraded.slice(0, 5).map(t => t.term);
const termsByYear: Record<number, Record<string, number>> = {};
recordsToVisualize.forEach(record => {
if (!termsByYear[record.year]) {
termsByYear[record.year] = {};
}
if (topTerms.includes(record.term)) {
termsByYear[record.year][record.term] = (termsByYear[record.year][record.term] || 0) + (record.quantity || 1);
}
});
const termQuantitiesByYear: TermYearData[] = Object.entries(termsByYear)
.map(([year, terms]) => {
const yearData: TermYearData = { year: parseInt(year) };
topTerms.forEach(term => {
yearData[term] = terms[term] || 0;
});
return yearData;
})
.sort((a, b) => a.year - b.year);
return {
recordsByYear,
topImporters,
topExporters,
termsTraded,
tradePurposes,
tradeSources,
termQuantitiesByYear,
topTerms
};
}, [recordsToVisualize]);
// Get unique years and terms (from all records, not just filtered ones)
const years = useMemo(() => {
if (!tradeRecords) return [];
const uniqueYears = [...new Set(tradeRecords.map((r: CitesTradeRecord) => r.year))]
.filter(year => year !== null && year !== undefined)
.sort((a, b) => Number(a) - Number(b)); // Sort ascending for year ranges
return uniqueYears;
}, [tradeRecords]);
const terms = useMemo(() => {
if (!tradeRecords) return [];
const uniqueTerms = [...new Set(tradeRecords.map((r: CitesTradeRecord) => r.term))]
.filter(term => term !== null && term !== undefined && term !== '')
.sort();
return uniqueTerms;
}, [tradeRecords]);
const appendices = useMemo(() => {
if (!tradeRecords) return [];
const uniqueAppendices = [...new Set(tradeRecords.map((r: CitesTradeRecord) => r.appendix))]
.filter(appendix => appendix !== null && appendix !== undefined && appendix !== '')
.sort();
return uniqueAppendices;
}, [tradeRecords]);
return {
visualizationData,
years,
terms,
appendices,
PURPOSE_DESCRIPTIONS,
SOURCE_DESCRIPTIONS
};
}