SCGR's picture
UI refine
413452e
import {
PageContainer, Button,
Container, Spinner, SegmentInput,
Table, PieChart, ProgressBar,
} from '@ifrc-go/ui';
import {
createStringColumn,
createNumberColumn,
numericIdSelector
} from '@ifrc-go/ui/utils';
import { useState, useEffect, useMemo, useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';
import styles from './AnalyticsPage.module.css';
interface AnalyticsData {
totalCaptions: number;
sources: { [key: string]: number };
types: { [key: string]: number };
regions: { [key: string]: number };
models: {
[key: string]: {
count: number;
avgAccuracy: number;
avgContext: number;
avgUsability: number;
totalScore: number;
deleteCount: number;
};
};
modelEditTimes: { [key: string]: number[] };
percentageModified: number;
modelPercentageData: { [key: string]: number[] };
totalDeleteCount: number;
deleteRate: number;
// Add separated image data for proper filtering
crisisMaps: MapData[];
droneImages: MapData[];
}
interface LookupData {
s_code?: string;
t_code?: string;
r_code?: string;
label: string;
}
interface RegionData {
id: number;
name: string;
count: number;
percentage: number;
}
interface TypeData {
id: number;
name: string;
count: number;
percentage: number;
}
interface SourceData {
id: number;
name: string;
count: number;
percentage: number;
}
interface ModelData {
id: number;
name: string;
count: number;
accuracy: number;
context: number;
usability: number;
totalScore: number;
}
interface EditTimeData {
id: number;
name: string;
count: number;
avgEditTime: number;
minEditTime: number;
maxEditTime: number;
}
interface PercentageModifiedData {
id: number;
name: string;
count: number;
avgPercentageModified: number;
minPercentageModified: number;
maxPercentageModified: number;
}
interface DeleteRateData {
id: number;
name: string;
count: number;
deleteCount: number;
deleteRate: number;
}
interface MapData {
source?: string;
event_type?: string;
countries?: Array<{ r_code?: string }>;
model?: string;
accuracy?: number;
context?: number;
usability?: number;
created_at?: string;
updated_at?: string;
generated?: string;
edited?: string;
image_type?: string;
}
export default function AnalyticsPage() {
const [searchParams] = useSearchParams();
const [data, setData] = useState<AnalyticsData | null>(null);
const [loading, setLoading] = useState(true);
const [view, setView] = useState<'crisis_maps' | 'drone_images'>('crisis_maps');
const [sourcesLookup, setSourcesLookup] = useState<LookupData[]>([]);
const [typesLookup, setTypesLookup] = useState<LookupData[]>([]);
const [regionsLookup, setRegionsLookup] = useState<LookupData[]>([]);
const [modelsLookup, setModelsLookup] = useState<{ m_code: string; label: string }[]>([]);
const [showEditTimeModal, setShowEditTimeModal] = useState(false);
const [showPercentageModal, setShowPercentageModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showRegionsModal, setShowRegionsModal] = useState(false);
const [showSourcesModal, setShowSourcesModal] = useState(false);
const [showTypesModal, setShowTypesModal] = useState(false);
// Function to handle opening a specific modal and closing others
const openModal = (modalType: 'editTime' | 'percentage' | 'delete' | 'regions' | 'sources' | 'types' | 'none') => {
setShowEditTimeModal(modalType === 'editTime');
setShowPercentageModal(modalType === 'percentage');
setShowDeleteModal(modalType === 'delete');
setShowRegionsModal(modalType === 'regions');
setShowSourcesModal(modalType === 'sources');
setShowTypesModal(modalType === 'types');
};
const viewOptions = [
{ key: 'crisis_maps' as const, label: 'Crisis Maps' },
{ key: 'drone_images' as const, label: 'Drone Images' }
];
// Helper function to calculate word similarity
const calculateWordSimilarity = useCallback((text1: string, text2: string): number => {
if (!text1 || !text2) return 0;
// Split into words, lowercase, and remove punctuation
const words1 = text1.toLowerCase().replace(/[^\w\s]/g, '').split(/\s+/).filter(word => word.length > 0);
const words2 = text2.toLowerCase().replace(/[^\w\s]/g, '').split(/\s+/).filter(word => word.length > 0);
if (words1.length === 0 && words2.length === 0) return 1; // Both empty = 100% similar
if (words1.length === 0 || words2.length === 0) return 0; // One empty = 0% similar
// Create sets of unique words
const set1 = new Set(words1);
const set2 = new Set(words2);
// Calculate intersection and union
const intersection = new Set([...set1].filter(word => set2.has(word)));
const union = new Set([...set1, ...set2]);
// Calculate similarity
const similarity = intersection.size / union.size;
return similarity;
}, []);
const fetchAnalytics = useCallback(async () => {
setLoading(true);
try {
const res = await fetch('/api/images');
const maps = await res.json();
// Calculate edit times for each model
const modelEditTimes: { [key: string]: number[] } = {};
// Separate images by type for proper filtering
const crisisMaps = maps.filter((map: MapData) => map.image_type === 'crisis_map');
const droneImages = maps.filter((map: MapData) => map.image_type === 'drone_image');
const analytics: AnalyticsData = {
totalCaptions: maps.length,
sources: {},
types: {},
regions: {},
models: {},
modelEditTimes: modelEditTimes,
percentageModified: 0,
modelPercentageData: {},
totalDeleteCount: 0,
deleteRate: 0,
crisisMaps: crisisMaps,
droneImages: droneImages,
};
// Process all images for global analytics
maps.forEach((map: MapData) => {
if (map.source) analytics.sources[map.source] = (analytics.sources[map.source] || 0) + 1;
if (map.event_type) analytics.types[map.event_type] = (analytics.types[map.event_type] || 0) + 1;
if (map.countries) {
map.countries.forEach((c) => {
if (c.r_code) analytics.regions[c.r_code] = (analytics.regions[c.r_code] || 0) + 1;
});
}
if (map.model) {
const m = map.model;
const ctr = analytics.models[m] ||= { count: 0, avgAccuracy: 0, avgContext: 0, avgUsability: 0, totalScore: 0, deleteCount: 0 };
ctr.count++;
if (map.accuracy != null) ctr.avgAccuracy += map.accuracy;
if (map.context != null) ctr.avgContext += map.context;
if (map.usability != null) ctr.avgUsability += map.usability;
// Calculate edit time if both timestamps exist (now from captions)
if (map.created_at && map.updated_at) {
const created = new Date(map.created_at).getTime();
const updated = new Date(map.updated_at).getTime();
const editTimeMs = updated - created;
if (editTimeMs > 0) {
if (!modelEditTimes[m]) modelEditTimes[m] = [];
modelEditTimes[m].push(editTimeMs);
}
}
}
});
sourcesLookup.forEach(source => {
if (source.s_code && !analytics.sources[source.s_code]) {
analytics.sources[source.s_code] = 0;
}
});
typesLookup.forEach(type => {
if (type.t_code && !analytics.types[type.t_code]) {
analytics.types[type.t_code] = 0;
}
});
regionsLookup.forEach(region => {
if (region.r_code && !analytics.regions[region.r_code]) {
analytics.regions[region.r_code] = 0;
}
});
const allModels = ['GPT-4', 'Claude', 'Gemini', 'Llama', 'Other'];
allModels.forEach(model => {
if (!analytics.models[model]) {
analytics.models[model] = { count: 0, avgAccuracy: 0, avgContext: 0, avgUsability: 0, totalScore: 0, deleteCount: 0 };
}
});
Object.values(analytics.models).forEach(m => {
if (m.count > 0) {
m.avgAccuracy = Math.round(m.avgAccuracy / m.count);
m.avgContext = Math.round(m.avgContext / m.count);
m.avgUsability = Math.round(m.avgUsability / m.count);
m.totalScore = Math.round((m.avgAccuracy + m.avgContext + m.avgUsability) / 3);
}
});
// Calculate percentage modified (median)
const textPairs = maps.filter((map: MapData) => map.generated && map.edited);
if (textPairs.length > 0) {
const similarities = textPairs.map((map: MapData) =>
calculateWordSimilarity(map.generated!, map.edited!)
);
const sortedSimilarities = [...similarities].sort((a, b) => a - b);
const mid = Math.floor(sortedSimilarities.length / 2);
const medianSimilarity = sortedSimilarities.length % 2 === 0
? (sortedSimilarities[mid - 1] + sortedSimilarities[mid]) / 2
: sortedSimilarities[mid];
analytics.percentageModified = Math.round((1 - medianSimilarity) * 100);
}
// Calculate percentage modified per model (median)
const modelPercentageData: { [key: string]: number[] } = {};
maps.forEach((map: MapData) => {
if (map.model && map.generated && map.edited) {
const similarity = calculateWordSimilarity(map.generated, map.edited);
const percentageModified = Math.round((1 - similarity) * 100);
if (!modelPercentageData[map.model]) {
modelPercentageData[map.model] = [];
}
modelPercentageData[map.model].push(percentageModified);
}
});
analytics.modelPercentageData = modelPercentageData;
// Fetch model data including delete counts
try {
const modelsRes = await fetch('/api/models');
if (modelsRes.ok) {
const modelsData = await modelsRes.json();
// Update delete counts for each model
if (modelsData.models) {
modelsData.models.forEach((model: { m_code: string; delete_count: number }) => {
if (analytics.models[model.m_code]) {
analytics.models[model.m_code].deleteCount = model.delete_count || 0;
}
});
// Calculate total delete count and delete rate
const totalDeleteCount = modelsData.models.reduce((sum: number, model: { delete_count: number }) => sum + (model.delete_count || 0), 0);
analytics.totalDeleteCount = totalDeleteCount;
analytics.deleteRate = totalDeleteCount > 0 ? Math.round((totalDeleteCount / (totalDeleteCount + maps.length)) * 100) : 0;
}
}
} catch (error) {
console.log('Could not fetch model delete counts:', error);
}
setData(analytics);
} catch {
setData(null);
} finally {
setLoading(false);
}
}, [sourcesLookup, typesLookup, regionsLookup, calculateWordSimilarity]);
const fetchLookupData = useCallback(async () => {
try {
const [sourcesRes, typesRes, regionsRes, modelsRes] = await Promise.all([
fetch('/api/sources'),
fetch('/api/types'),
fetch('/api/regions'),
fetch('/api/models')
]);
const sources = await sourcesRes.json();
const types = await typesRes.json();
const regions = await regionsRes.json();
const models = await modelsRes.json();
setSourcesLookup(sources);
setTypesLookup(types);
setRegionsLookup(regions);
setModelsLookup(models.models || []);
} catch (error) {
console.log('Could not fetch lookup data:', error);
}
}, []);
// Set initial view based on URL parameter
useEffect(() => {
const viewParam = searchParams.get('view');
if (viewParam === 'crisis_maps' || viewParam === 'drone_images') {
setView(viewParam);
}
}, [searchParams]);
useEffect(() => {
fetchLookupData();
}, [fetchLookupData]);
useEffect(() => {
if (sourcesLookup.length > 0 && typesLookup.length > 0 && regionsLookup.length > 0 && modelsLookup.length > 0) {
fetchAnalytics();
}
}, [sourcesLookup, typesLookup, regionsLookup, modelsLookup, fetchAnalytics]);
const getSourceLabel = useCallback((code: string) => {
const source = sourcesLookup.find(s => s.s_code === code);
return source ? source.label : code;
}, [sourcesLookup]);
const getMedianEditTime = useCallback((editTimes: number[]) => {
if (editTimes.length === 0) return 0;
const sortedTimes = [...editTimes].sort((a, b) => a - b);
const mid = Math.floor(sortedTimes.length / 2);
return sortedTimes.length % 2 === 0
? Math.round((sortedTimes[mid - 1] + sortedTimes[mid]) / 2)
: sortedTimes[mid];
}, []);
const formatEditTime = useCallback((ms: number) => {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours > 0) {
return `${hours}h ${minutes % 60}m`;
} else if (minutes > 0) {
return `${minutes}m ${seconds % 60}s`;
} else {
return `${seconds}s`;
}
}, []);
const getTypeLabel = useCallback((code: string) => {
const type = typesLookup.find(t => t.t_code === code);
return type ? type.label : code;
}, [typesLookup]);
const getModelLabel = useCallback((code: string) => {
const model = modelsLookup.find(m => m.m_code === code);
return model ? model.label : code;
}, [modelsLookup]);
const editTimeTableData = useMemo(() => {
if (!data) return [];
return Object.entries(data.modelEditTimes || {})
.filter(([, editTimes]) => editTimes.length > 0)
.sort(([, a], [, b]) => getMedianEditTime(b) - getMedianEditTime(a))
.map(([modelCode, editTimes], index) => ({
id: index + 1,
name: getModelLabel(modelCode),
count: editTimes.length,
avgEditTime: getMedianEditTime(editTimes),
minEditTime: Math.min(...editTimes),
maxEditTime: Math.max(...editTimes)
}));
}, [data, getMedianEditTime, getModelLabel]);
const percentageModifiedTableData = useMemo(() => {
if (!data) return [];
return Object.entries(data.modelPercentageData || {})
.filter(([, percentages]) => percentages.length > 0)
.sort(([, a], [, b]) => {
const sortedA = [...a].sort((x, y) => x - y);
const sortedB = [...b].sort((x, y) => x - y);
const midA = Math.floor(sortedA.length / 2);
const midB = Math.floor(sortedB.length / 2);
const medianA = sortedA.length % 2 === 0
? (sortedA[midA - 1] + sortedA[midA]) / 2
: sortedA[midA];
const medianB = sortedB.length % 2 === 0
? (sortedB[midB - 1] + sortedB[midB]) / 2
: sortedB[midB];
return medianB - medianA;
})
.map(([modelCode, percentages], index) => {
const sortedPercentages = [...percentages].sort((a, b) => a - b);
const mid = Math.floor(sortedPercentages.length / 2);
const medianPercentage = sortedPercentages.length % 2 === 0
? Math.round((sortedPercentages[mid - 1] + sortedPercentages[mid]) / 2)
: sortedPercentages[mid];
return {
id: index + 1,
name: getModelLabel(modelCode),
count: percentages.length,
avgPercentageModified: medianPercentage,
minPercentageModified: Math.min(...percentages),
maxPercentageModified: Math.max(...percentages)
};
});
}, [data, getModelLabel]);
const modelConsistencyData = useMemo(() => {
if (!data) return [];
return Object.entries(data.models)
.filter(([, model]) => model.count > 0)
.map(([modelCode, model], index) => {
// Calculate consistency based on how close accuracy, context, and usability are
const scores = [model.avgAccuracy, model.avgContext, model.avgUsability];
const mean = scores.reduce((sum, score) => sum + score, 0) / scores.length;
const variance = scores.reduce((sum, score) => sum + Math.pow(score - mean, 2), 0) / scores.length;
const consistency = Math.round(100 - Math.sqrt(variance)); // Lower variance = higher consistency
return {
id: index + 1,
name: getModelLabel(modelCode),
consistency: Math.max(0, consistency),
avgScore: Math.round(mean),
count: model.count
};
})
.sort((a, b) => b.consistency - a.consistency);
}, [data, getModelLabel]);
const regionsColumns = useMemo(() => [
createStringColumn<RegionData, number>(
'name',
'Region',
(item) => item.name,
),
createNumberColumn<RegionData, number>(
'count',
'Count',
(item) => item.count,
),
createNumberColumn<RegionData, number>(
'percentage',
'% of Total',
(item) => item.percentage,
{
suffix: '%',
maximumFractionDigits: 0,
},
),
], []);
const typesColumns = useMemo(() => [
createStringColumn<TypeData, number>(
'name',
'Type',
(item) => item.name,
),
createNumberColumn<TypeData, number>(
'count',
'Count',
(item) => item.count,
),
createNumberColumn<TypeData, number>(
'percentage',
'% of Total',
(item) => item.percentage,
{
suffix: '%',
maximumFractionDigits: 0,
},
),
], []);
const sourcesColumns = useMemo(() => [
createStringColumn<SourceData, number>(
'name',
'Source',
(item) => item.name,
),
createNumberColumn<SourceData, number>(
'count',
'Count',
(item) => item.count,
),
createNumberColumn<SourceData, number>(
'percentage',
'% of Total',
(item) => item.percentage,
{
suffix: '%',
maximumFractionDigits: 0,
},
),
], []);
const modelsColumns = useMemo(() => [
createStringColumn<ModelData, number>(
'name',
'Model',
(item) => item.name,
),
createNumberColumn<ModelData, number>(
'count',
'Count',
(item) => item.count,
),
createNumberColumn<ModelData, number>(
'accuracy',
'Accuracy',
(item) => item.accuracy,
{
suffix: '%',
maximumFractionDigits: 0,
},
),
createNumberColumn<ModelData, number>(
'context',
'Context',
(item) => item.context,
{
suffix: '%',
maximumFractionDigits: 0,
},
),
createNumberColumn<ModelData, number>(
'usability',
'Usability',
(item) => item.usability,
{
suffix: '%',
maximumFractionDigits: 0,
},
),
createNumberColumn<ModelData, number>(
'totalScore',
'Total Score',
(item) => item.totalScore,
{
suffix: '%',
maximumFractionDigits: 0,
},
),
], []);
const editTimeColumns = useMemo(() => [
createStringColumn<EditTimeData, number>(
'name',
'Model',
(item) => item.name,
),
createNumberColumn<EditTimeData, number>(
'count',
'Count',
(item) => item.count,
),
createStringColumn<EditTimeData, number>(
'avgEditTime',
'Median Edit Time',
(item) => formatEditTime(item.avgEditTime),
),
createStringColumn<EditTimeData, number>(
'minEditTime',
'Min Edit Time',
(item) => formatEditTime(item.minEditTime),
),
createStringColumn<EditTimeData, number>(
'maxEditTime',
'Max Edit Time',
(item) => formatEditTime(item.maxEditTime),
),
], []);
const percentageModifiedColumns = useMemo(() => [
createStringColumn<PercentageModifiedData, number>(
'name',
'Model',
(item) => item.name,
),
createNumberColumn<PercentageModifiedData, number>(
'count',
'Count',
(item) => item.count,
),
createNumberColumn<PercentageModifiedData, number>(
'avgPercentageModified',
'Median % Modified',
(item) => item.avgPercentageModified,
{
suffix: '%',
maximumFractionDigits: 0,
},
),
createNumberColumn<PercentageModifiedData, number>(
'minPercentageModified',
'Min % Modified',
(item) => item.minPercentageModified,
{
suffix: '%',
maximumFractionDigits: 0,
},
),
createNumberColumn<PercentageModifiedData, number>(
'maxPercentageModified',
'Max % Modified',
(item) => item.maxPercentageModified,
{
suffix: '%',
maximumFractionDigits: 0,
},
),
], []);
const deleteRateColumns = useMemo(() => [
createStringColumn<DeleteRateData, number>(
'name',
'Model',
(item) => item.name,
),
createNumberColumn<DeleteRateData, number>(
'count',
'Total Count',
(item) => item.count,
),
createNumberColumn<DeleteRateData, number>(
'deleteCount',
'Delete Count',
(item) => item.deleteCount,
),
createNumberColumn<DeleteRateData, number>(
'deleteRate',
'Delete Rate',
(item) => item.deleteRate,
{
suffix: '%',
maximumFractionDigits: 1,
},
),
], []);
const qualityBySourceColumns = useMemo(() => [
createStringColumn<{ source: string; avgQuality: number; count: number }, number>(
'source',
'Source',
(item) => item.source,
),
createNumberColumn<{ source: string; avgQuality: number; count: number }, number>(
'avgQuality',
'Average Quality',
(item) => item.avgQuality,
{
suffix: '%',
maximumFractionDigits: 0,
},
),
createNumberColumn<{ source: string; avgQuality: number; count: number }, number>(
'count',
'Count',
(item) => item.count,
),
], []);
const qualityByEventTypeColumns = useMemo(() => [
createStringColumn<{ eventType: string; avgQuality: number; count: number }, number>(
'eventType',
'Event Type',
(item) => item.eventType,
),
createNumberColumn<{ eventType: string; avgQuality: number; count: number }, number>(
'avgQuality',
'Average Quality',
(item) => item.avgQuality,
{
suffix: '%',
maximumFractionDigits: 0,
},
),
createNumberColumn<{ eventType: string; avgQuality: number; count: number }, number>(
'count',
'Count',
(item) => item.count,
),
], []);
const modelConsistencyColumns = useMemo(() => [
createStringColumn<{ name: string; consistency: number; avgScore: number; count: number }, number>(
'name',
'Model',
(item) => item.name,
),
createNumberColumn<{ name: string; consistency: number; avgScore: number; count: number }, number>(
'consistency',
'Consistency',
(item) => item.consistency,
{
suffix: '%',
maximumFractionDigits: 0,
},
),
createNumberColumn<{ name: string; consistency: number; avgScore: number; count: number }, number>(
'avgScore',
'Average Score',
(item) => item.avgScore,
{
suffix: '%',
maximumFractionDigits: 0,
},
),
createNumberColumn<{ name: string; consistency: number; avgScore: number; count: number }, number>(
'count',
'Count',
(item) => item.count,
),
], []);
// Helper functions to filter data by image type
const getImageTypeCount = useCallback((imageType: string) => {
if (!data) return 0;
if (imageType === 'crisis_map') {
return data.crisisMaps.length;
} else if (imageType === 'drone_image') {
return data.droneImages.length;
}
return 0;
}, [data]);
const getImageTypeRegionsChartData = useCallback((imageType: string) => {
if (!data) return [];
// Get the appropriate image set based on type
const images = imageType === 'crisis_map' ? data.crisisMaps : data.droneImages;
// Calculate regions for this specific image type
const regions: { [key: string]: number } = {};
images.forEach((map: MapData) => {
if (map.countries) {
map.countries.forEach((c) => {
if (c.r_code) regions[c.r_code] = (regions[c.r_code] || 0) + 1;
});
}
});
return Object.entries(regions)
.filter(([, value]) => value > 0)
.map(([code, value]) => ({
name: regionsLookup.find(r => r.r_code === code)?.label || code,
value
}));
}, [data, regionsLookup]);
const getImageTypeRegionsTableData = useCallback((imageType: string) => {
if (!data) return [];
// Get the appropriate image set based on type
const images = imageType === 'crisis_map' ? data.crisisMaps : data.droneImages;
// Calculate regions for this specific image type
const regions: { [key: string]: number } = {};
images.forEach((map: MapData) => {
if (map.countries) {
map.countries.forEach((c) => {
if (c.r_code) regions[c.r_code] = (regions[c.r_code] || 0) + 1;
});
}
});
// Convert to table data format
const allRegions = regionsLookup.reduce((acc, region) => {
if (region.r_code) {
acc[region.r_code] = {
name: region.label,
count: regions[region.r_code] || 0
};
}
return acc;
}, {} as Record<string, { name: string; count: number }>);
return Object.entries(allRegions)
.sort(([,a], [,b]) => b.count - a.count)
.map(([, { name, count }], index) => ({
id: index + 1,
name,
count,
percentage: images.length > 0 ? Math.round((count / images.length) * 100) : 0
}));
}, [data, regionsLookup]);
const getImageTypeSourcesChartData = useCallback((imageType: string) => {
if (!data) return [];
// Get the appropriate image set based on type
const images = imageType === 'crisis_map' ? data.crisisMaps : data.droneImages;
// Calculate sources for this specific image type
const sources: { [key: string]: number } = {};
images.forEach((map: MapData) => {
if (map.source) sources[map.source] = (sources[map.source] || 0) + 1;
});
return Object.entries(sources)
.filter(([, value]) => value > 0)
.map(([code, value]) => ({
name: sourcesLookup.find(s => s.s_code === code)?.label || code,
value
}));
}, [data, sourcesLookup]);
const getImageTypeSourcesTableData = useCallback((imageType: string) => {
if (!data) return [];
// Get the appropriate image set based on type
const images = imageType === 'crisis_map' ? data.crisisMaps : data.droneImages;
// Calculate sources for this specific image type
const sources: { [key: string]: number } = {};
images.forEach((map: MapData) => {
if (map.source) sources[map.source] = (sources[map.source] || 0) + 1;
});
// Convert to table data format
return Object.entries(sources)
.sort(([,a], [,b]) => b - a)
.map(([sourceKey, count], index) => ({
id: index + 1,
name: getSourceLabel(sourceKey),
count,
percentage: images.length > 0 ? Math.round((count / images.length) * 100) : 0
}));
}, [data, getSourceLabel]);
const getImageTypeTypesChartData = useCallback((imageType: string) => {
if (!data) return [];
// Get the appropriate image set based on type
const images = imageType === 'crisis_map' ? data.crisisMaps : data.droneImages;
// Calculate types for this specific image type
const types: { [key: string]: number } = {};
images.forEach((map: MapData) => {
if (map.event_type) types[map.event_type] = (types[map.event_type] || 0) + 1;
});
return Object.entries(types)
.filter(([, value]) => value > 0)
.map(([code, value]) => ({
name: typesLookup.find(t => t.t_code === code)?.label || code,
value
}));
}, [data, typesLookup]);
const getImageTypeTypesTableData = useCallback((imageType: string) => {
if (!data) return [];
// Get the appropriate image set based on type
const images = imageType === 'crisis_map' ? data.crisisMaps : data.droneImages;
// Calculate types for this specific image type
const types: { [key: string]: number } = {};
images.forEach((map: MapData) => {
if (map.event_type) types[map.event_type] = (types[map.event_type] || 0) + 1;
});
// Convert to table data format
return Object.entries(types)
.sort(([,a], [,b]) => b - a)
.map(([typeKey, count], index) => ({
id: index + 1,
name: getTypeLabel(typeKey),
count,
percentage: images.length > 0 ? Math.round((count / images.length) * 100) : 0
}));
}, [data, getTypeLabel]);
const getImageTypeMedianEditTime = useCallback((imageType: string) => {
if (!data) return 'No data available';
// Get the appropriate image set based on type
const images = imageType === 'crisis_map' ? data.crisisMaps : data.droneImages;
// Get the models actually used by images of this type
const usedModels = new Set<string>();
images.forEach((map: MapData) => {
if (map.model) {
usedModels.add(map.model);
}
});
// Debug logging
console.log(`Debug ${imageType}:`, {
totalImages: images.length,
usedModels: Array.from(usedModels),
availableEditTimes: Object.keys(data.modelEditTimes),
modelEditTimesData: data.modelEditTimes
});
// Filter edit times by models actually used by this image type
const filteredEditTimes = Object.entries(data.modelEditTimes).filter(([modelName]) => {
return usedModels.has(modelName);
});
const editTimes = filteredEditTimes.flatMap(([, times]) => times);
if (editTimes.length === 0) return 'No data available';
return formatEditTime(getMedianEditTime(editTimes));
}, [data, formatEditTime, getMedianEditTime]);
const getImageTypePercentageModified = useCallback(() => {
if (!data) return 'No data available';
const total = data.totalCaptions || 0;
const modified = data.percentageModified || 0;
return total > 0 ? Math.round((modified / total) * 100) : 0;
}, [data]);
const getImageTypeDeleteRate = useCallback(() => {
if (!data) return 'No data available';
// For now, we'll return the global delete rate since we don't have image_type filtering in the backend
// In a real implementation, you'd calculate this based on filtered data
return data.deleteRate >= 0 ? `${data.deleteRate}%` : 'No data available';
}, [data]);
const getImageTypeEditTimeTableData = useCallback((imageType: string) => {
if (!data) return [];
// Get the appropriate image set based on type
const images = imageType === 'crisis_map' ? data.crisisMaps : data.droneImages;
// Get the models actually used by images of this type
const usedModels = new Set<string>();
images.forEach((map: MapData) => {
if (map.model) {
usedModels.add(map.model);
}
});
// Filter edit time data by models actually used by this image type
const filteredData = editTimeTableData.filter(d => {
// Find the model code that matches this display name
const modelCode = modelsLookup.find(m => m.label === d.name)?.m_code;
return modelCode && usedModels.has(modelCode);
});
return filteredData;
}, [data, editTimeTableData, modelsLookup]);
const getImageTypePercentageTableData = useCallback((imageType: string) => {
if (!data) return [];
// Get the appropriate image set based on type
const images = imageType === 'crisis_map' ? data.crisisMaps : data.droneImages;
// Get the models actually used by images of this type
const usedModels = new Set<string>();
images.forEach((map: MapData) => {
if (map.model) {
usedModels.add(map.model);
}
});
// Filter percentage data by models actually used by this image type
const filteredData = percentageModifiedTableData.filter(d => {
// Find the model code that matches this display name
const modelCode = modelsLookup.find(m => m.label === d.name)?.m_code;
return modelCode && usedModels.has(modelCode);
});
return filteredData;
}, [data, percentageModifiedTableData, modelsLookup]);
const getImageTypeDeleteRateTableData = useCallback((imageType: string) => {
if (!data) return [];
// Get the appropriate image set based on type
const images = imageType === 'crisis_map' ? data.crisisMaps : data.droneImages;
// Calculate delete rates for this specific image type
const modelStats: { [key: string]: { count: number; deleteCount: number } } = {};
images.forEach((map: MapData) => {
if (map.model) {
if (!modelStats[map.model]) {
modelStats[map.model] = { count: 0, deleteCount: 0 };
}
modelStats[map.model].count++;
// Note: We don't have individual delete data per image, so we'll use the model-level delete count
// In a real implementation, you'd track this at the image level
}
});
// Convert to table data format and add delete counts from analytics.models
return Object.entries(modelStats)
.map(([modelCode, stats], index) => {
const modelData = data.models?.[modelCode];
const deleteCount = modelData?.deleteCount || 0;
const deleteRate = stats.count > 0 ? Math.round((deleteCount / stats.count) * 100 * 10) / 10 : 0;
return {
id: index + 1,
name: getModelLabel(modelCode),
count: stats.count,
deleteCount: deleteCount,
deleteRate: deleteRate,
};
})
.sort((a, b) => b.count - a.count);
}, [data, getModelLabel]);
const getImageTypeModelsTableData = useCallback((imageType: string) => {
if (!data) return [];
// Get the appropriate image set based on type
const images = imageType === 'crisis_map' ? data.crisisMaps : data.droneImages;
// Calculate models for this specific image type
const modelStats: { [key: string]: { count: number; totalAccuracy: number; totalContext: number; totalUsability: number } } = {};
images.forEach((map: MapData) => {
if (map.model) {
if (!modelStats[map.model]) {
modelStats[map.model] = { count: 0, totalAccuracy: 0, totalContext: 0, totalUsability: 0 };
}
modelStats[map.model].count++;
if (map.accuracy != null) modelStats[map.model].totalAccuracy += map.accuracy;
if (map.context != null) modelStats[map.model].totalContext += map.context;
if (map.usability != null) modelStats[map.model].totalUsability += map.usability;
}
});
// Convert to table data format
return Object.entries(modelStats)
.map(([modelCode, stats], index) => ({
id: index + 1,
name: getModelLabel(modelCode),
count: stats.count,
accuracy: stats.count > 0 ? Math.round(stats.totalAccuracy / stats.count) : 0,
context: stats.count > 0 ? Math.round(stats.totalContext / stats.count) : 0,
usability: stats.count > 0 ? Math.round(stats.totalUsability / stats.count) : 0,
totalScore: stats.count > 0 ? Math.round((stats.totalAccuracy + stats.totalContext + stats.totalUsability) / (3 * stats.count)) : 0
}))
.sort((a, b) => b.totalScore - a.totalScore);
}, [data, getModelLabel]);
const getImageTypeQualityBySourceTableData = useCallback((imageType: string) => {
if (!data) return [];
// Get the appropriate image set based on type
const images = imageType === 'crisis_map' ? data.crisisMaps : data.droneImages;
// Calculate quality by source for this specific image type
const sourceQuality: { [key: string]: { total: number; count: number; totalImages: number } } = {};
images.forEach((map: MapData) => {
if (map.source) {
if (!sourceQuality[map.source]) {
sourceQuality[map.source] = { total: 0, count: 0, totalImages: 0 };
}
sourceQuality[map.source].totalImages += 1;
if (map.accuracy != null) {
sourceQuality[map.source].total += map.accuracy;
sourceQuality[map.source].count += 1;
}
}
});
// Convert to table data format
return Object.entries(sourceQuality).map(([source, stats], index) => ({
id: index + 1,
source: getSourceLabel(source),
avgQuality: stats.count > 0 ? Math.round(stats.total / stats.count) : 0,
count: stats.totalImages
}));
}, [data, getSourceLabel]);
const getImageTypeQualityByEventTypeTableData = useCallback((imageType: string) => {
if (!data) return [];
// Get the appropriate image set based on type
const images = imageType === 'crisis_map' ? data.crisisMaps : data.droneImages;
// Calculate quality by event type for this specific image type
const eventTypeQuality: { [key: string]: { total: number; count: number; totalImages: number } } = {};
images.forEach((map: MapData) => {
if (map.event_type) {
if (!eventTypeQuality[map.event_type]) {
eventTypeQuality[map.event_type] = { total: 0, count: 0, totalImages: 0 };
}
eventTypeQuality[map.event_type].totalImages += 1;
if (map.accuracy != null) {
eventTypeQuality[map.event_type].total += map.accuracy;
eventTypeQuality[map.event_type].count += 1;
}
}
});
// Convert to table data format
return Object.entries(eventTypeQuality).map(([eventTypeCode, stats], index) => ({
id: index + 1,
eventType: getTypeLabel(eventTypeCode),
avgQuality: stats.count > 0 ? Math.round(stats.total / stats.count) : 0,
count: stats.totalImages
}));
}, [data, getTypeLabel]);
const getImageTypeModelConsistencyTableData = useCallback((imageType: string) => {
if (!data) return [];
// Get the appropriate image set based on type
const images = imageType === 'crisis_map' ? data.crisisMaps : data.droneImages;
// Get the models actually used by images of this type
const usedModels = new Set<string>();
images.forEach((map: MapData) => {
if (map.model) {
usedModels.add(map.model);
}
});
// Filter model consistency table data by models actually used by this image type
const filteredData = modelConsistencyData.filter(d => {
// Find the model code that matches this display name
const modelCode = modelsLookup.find(m => m.label === d.name)?.m_code;
return modelCode && usedModels.has(modelCode);
});
return filteredData;
}, [data, modelConsistencyData, modelsLookup]);
if (loading) {
return (
<PageContainer>
<div className={styles.loadingContainer}>
<Spinner />
</div>
</PageContainer>
);
}
if (!data) {
return (
<PageContainer>
<div className={styles.errorContainer}>
<div className="text-red-500">Failed to load analytics data. Please try again.</div>
</div>
</PageContainer>
);
}
const ifrcColors = [
'#F5333F', '#F64752', '#F75C65', '#F87079', '#F9858C', '#FA999F', '#FBADB2', '#FCC2C5'
];
return (
<PageContainer>
<div className="max-w-7xl mx-auto">
<div className={styles.tabSelector}>
<SegmentInput
name="analytics-view"
value={view}
onChange={(value) => {
if (value === 'crisis_maps' || value === 'drone_images') {
setView(value);
}
}}
options={viewOptions}
keySelector={(o) => o.key}
labelSelector={(o) => o.label}
/>
</div>
{view === 'crisis_maps' ? (
<div className={styles.chartGrid}>
<Container heading="Summary Statistics" headingLevel={3} withHeaderBorder withInternalPadding>
<div className={styles.summaryStatsCards}>
<div className={styles.summaryStatsCard}>
<div className={styles.summaryStatsCardValue}>
{getImageTypeCount('crisis_map')}
</div>
<div className={styles.summaryStatsCardLabel}>Total Crisis Maps</div>
</div>
<div className={styles.summaryStatsCard}>
<div className={styles.summaryStatsCardValue}>
2000
</div>
<div className={styles.summaryStatsCardLabel}>Target Amount</div>
</div>
</div>
<div className={styles.progressSection}>
<div className={styles.progressLabel}>
<span>Progress towards target</span>
<span>{Math.round((getImageTypeCount('crisis_map') / 2000) * 100)}%</span>
</div>
<ProgressBar value={getImageTypeCount('crisis_map')} totalValue={2000} />
</div>
</Container>
<Container heading="Distribution Analysis" headingLevel={3} withHeaderBorder withInternalPadding>
<div className={styles.userInteractionCards}>
{/* Regions Distribution Card */}
<div className={styles.userInteractionCard}>
<div className={styles.userInteractionCardLabel}>Regions Distribution</div>
<div className={styles.chartContainer}>
<PieChart
data={getImageTypeRegionsChartData('crisis_map')}
valueSelector={d => d.value}
labelSelector={d => d.name}
keySelector={d => d.name}
colors={ifrcColors}
showPercentageInLegend
/>
</div>
<Button
name="view-regions-details"
variant={showRegionsModal ? "primary" : "secondary"}
onClick={() => openModal(showRegionsModal ? 'none' : 'regions')}
className={styles.userInteractionCardButton}
>
{showRegionsModal ? 'Hide Details' : 'View Details'}
</Button>
</div>
{/* Sources Distribution Card */}
<div className={styles.userInteractionCard}>
<div className={styles.userInteractionCardLabel}>Sources Distribution</div>
<div className={styles.chartContainer}>
<PieChart
data={getImageTypeSourcesChartData('crisis_map')}
valueSelector={d => d.value}
labelSelector={d => d.name}
keySelector={d => d.name}
colors={ifrcColors}
showPercentageInLegend
/>
</div>
<Button
name="view-sources-details"
variant={showSourcesModal ? "primary" : "secondary"}
onClick={() => openModal(showSourcesModal ? 'none' : 'sources')}
className={styles.userInteractionCardButton}
>
{showSourcesModal ? 'Hide Details' : 'View Details'}
</Button>
</div>
{/* Types Distribution Card */}
<div className={styles.userInteractionCard}>
<div className={styles.userInteractionCardLabel}>Types Distribution</div>
<div className={styles.chartContainer}>
<PieChart
data={getImageTypeTypesChartData('crisis_map')}
valueSelector={d => d.value}
labelSelector={d => d.name}
keySelector={d => d.name}
colors={ifrcColors}
showPercentageInLegend
/>
</div>
<Button
name="view-types-details"
variant={showTypesModal ? "primary" : "secondary"}
onClick={() => openModal(showTypesModal ? 'none' : 'types')}
className={styles.userInteractionCardButton}
>
{showTypesModal ? 'Hide Details' : 'View Details'}
</Button>
</div>
</div>
{/* Regions Details Table */}
{showRegionsModal && (
<div className={styles.modelPerformance}>
<Table
data={getImageTypeRegionsTableData('crisis_map')}
columns={regionsColumns}
keySelector={numericIdSelector}
filtered={false}
pending={false}
/>
</div>
)}
{/* Sources Details Table */}
{showSourcesModal && (
<div className={styles.modelPerformance}>
<Table
data={getImageTypeSourcesTableData('crisis_map')}
columns={sourcesColumns}
keySelector={numericIdSelector}
filtered={false}
pending={false}
/>
</div>
)}
{/* Types Details Table */}
{showTypesModal && (
<div className={styles.modelPerformance}>
<Table
data={getImageTypeTypesTableData('crisis_map')}
columns={typesColumns}
keySelector={numericIdSelector}
filtered={false}
pending={false}
/>
</div>
)}
</Container>
{/* New Analytics Containers */}
<Container heading="User Interaction Statistics" headingLevel={3} withHeaderBorder withInternalPadding>
<div className={styles.userInteractionCards}>
{/* Median Edit Time Card */}
<div className={styles.userInteractionCard}>
<div className={styles.userInteractionCardValue}>
{getImageTypeMedianEditTime('crisis_map')}
</div>
<div className={styles.userInteractionCardLabel}>Median Edit Time</div>
<Button
name="view-edit-time-details"
variant={showEditTimeModal ? "primary" : "secondary"}
onClick={() => openModal(showEditTimeModal ? 'none' : 'editTime')}
className={styles.userInteractionCardButton}
>
{showEditTimeModal ? 'Hide Details' : 'View Details'}
</Button>
</div>
{/* Median % Modified Card */}
<div className={styles.userInteractionCard}>
<div className={styles.userInteractionCardValue}>
{getImageTypePercentageModified()}
</div>
<div className={styles.userInteractionCardLabel}>Median % Modified</div>
<Button
name="view-percentage-details"
variant={showPercentageModal ? "primary" : "secondary"}
onClick={() => openModal(showPercentageModal ? 'none' : 'percentage')}
className={styles.userInteractionCardButton}
>
{showPercentageModal ? 'Hide Details' : 'View Details'}
</Button>
</div>
{/* Delete Rate Card */}
<div className={styles.userInteractionCard}>
<div className={styles.userInteractionCardValue}>
{getImageTypeDeleteRate()}
</div>
<div className={styles.userInteractionCardLabel}>Delete Rate</div>
<Button
name="view-delete-details"
variant={showDeleteModal ? "primary" : "secondary"}
onClick={() => openModal(showDeleteModal ? 'none' : 'delete')}
className={styles.userInteractionCardButton}
>
{showDeleteModal ? 'Hide Details' : 'View Details'}
</Button>
</div>
</div>
{/* Edit Time Details Table */}
{showEditTimeModal && (
<div className={styles.modelPerformance}>
<Table
data={getImageTypeEditTimeTableData('crisis_map')}
columns={editTimeColumns}
keySelector={numericIdSelector}
filtered={false}
pending={false}
/>
</div>
)}
{/* Percentage Modified Details Table */}
{showPercentageModal && (
<div className={styles.modelPerformance}>
<Table
data={getImageTypePercentageTableData('crisis_map')}
columns={percentageModifiedColumns}
keySelector={numericIdSelector}
filtered={false}
pending={false}
/>
</div>
)}
{/* Delete Rate Details Table */}
{showDeleteModal && (
<div className={styles.modelPerformance}>
<Table
data={getImageTypeDeleteRateTableData('crisis_map')}
columns={deleteRateColumns}
keySelector={numericIdSelector}
filtered={false}
pending={false}
/>
</div>
)}
</Container>
<Container heading="Model Performance" headingLevel={3} withHeaderBorder withInternalPadding>
<div className={styles.modelPerformance}>
<Table
data={getImageTypeModelsTableData('crisis_map')}
columns={modelsColumns}
keySelector={numericIdSelector}
filtered={false}
pending={false}
/>
</div>
</Container>
<Container heading="Quality-Source Correlation" headingLevel={3} withHeaderBorder withInternalPadding>
<div className={styles.tableContainer}>
<Table
data={getImageTypeQualityBySourceTableData('crisis_map')}
columns={qualityBySourceColumns}
keySelector={numericIdSelector}
filtered={false}
pending={false}
/>
</div>
</Container>
<Container heading="Quality-Event Type Correlation" headingLevel={3} withHeaderBorder withInternalPadding>
<div className={styles.tableContainer}>
<Table
data={getImageTypeQualityByEventTypeTableData('crisis_map')}
columns={qualityByEventTypeColumns}
keySelector={numericIdSelector}
filtered={false}
pending={false}
/>
</div>
</Container>
<Container heading="Model Consistency Analysis" headingLevel={3} withHeaderBorder withInternalPadding>
<div className={styles.tableContainer}>
<Table
data={getImageTypeModelConsistencyTableData('crisis_map')}
columns={modelConsistencyColumns}
keySelector={numericIdSelector}
filtered={false}
pending={false}
/>
</div>
</Container>
</div>
) : (
<div className={styles.chartGrid}>
<Container heading="Summary Statistics" headingLevel={3} withHeaderBorder withInternalPadding>
<div className={styles.summaryStatsCards}>
<div className={styles.summaryStatsCard}>
<div className={styles.summaryStatsCardValue}>
{getImageTypeCount('drone_image')}
</div>
<div className={styles.summaryStatsCardLabel}>Total Drone Images</div>
</div>
<div className={styles.summaryStatsCard}>
<div className={styles.summaryStatsCardValue}>
2000
</div>
<div className={styles.summaryStatsCardLabel}>Target Amount</div>
</div>
</div>
<div className={styles.progressSection}>
<div className={styles.progressLabel}>
<span>Progress towards target</span>
<span>{Math.round((getImageTypeCount('drone_image') / 2000) * 100)}%</span>
</div>
<ProgressBar value={getImageTypeCount('drone_image')} totalValue={2000} />
</div>
</Container>
<Container heading="Distribution Analysis" headingLevel={3} withHeaderBorder withInternalPadding>
<div className={styles.userInteractionCards}>
{/* Regions Distribution Card */}
<div className={styles.userInteractionCard}>
<div className={styles.userInteractionCardLabel}>Regions Distribution</div>
<div className={styles.chartContainer}>
<PieChart
data={getImageTypeRegionsChartData('drone_image')}
valueSelector={d => d.value}
labelSelector={d => d.name}
keySelector={d => d.name}
colors={ifrcColors}
showPercentageInLegend
/>
</div>
<Button
name="view-regions-details"
variant={showRegionsModal ? "primary" : "secondary"}
onClick={() => openModal(showRegionsModal ? 'none' : 'regions')}
className={styles.userInteractionCardButton}
>
{showRegionsModal ? 'Hide Details' : 'View Details'}
</Button>
</div>
{/* Types Distribution Card */}
<div className={styles.userInteractionCard}>
<div className={styles.userInteractionCardLabel}>Types Distribution</div>
<div className={styles.chartContainer}>
<PieChart
data={getImageTypeTypesChartData('drone_image')}
valueSelector={d => d.value}
labelSelector={d => d.name}
keySelector={d => d.name}
colors={ifrcColors}
showPercentageInLegend
/>
</div>
<Button
name="view-types-details"
variant={showTypesModal ? "primary" : "secondary"}
onClick={() => openModal(showTypesModal ? 'none' : 'types')}
className={styles.userInteractionCardButton}
>
{showTypesModal ? 'Hide Details' : 'View Details'}
</Button>
</div>
</div>
{/* Regions Details Table */}
{showRegionsModal && (
<div className={styles.modelPerformance}>
<Table
data={getImageTypeRegionsTableData('drone_image')}
columns={regionsColumns}
keySelector={numericIdSelector}
filtered={false}
pending={false}
/>
</div>
)}
{/* Types Details Table */}
{showTypesModal && (
<div className={styles.modelPerformance}>
<Table
data={getImageTypeTypesTableData('drone_image')}
columns={typesColumns}
keySelector={numericIdSelector}
filtered={false}
pending={false}
/>
</div>
)}
</Container>
{/* User Interaction Statistics Box */}
<Container heading="User Interaction Statistics" headingLevel={3} withHeaderBorder withInternalPadding>
<div className={styles.userInteractionCards}>
{/* Median Edit Time Card */}
<div className={styles.userInteractionCard}>
<div className={styles.userInteractionCardValue}>
{getImageTypeMedianEditTime('drone_image')}
</div>
<div className={styles.userInteractionCardLabel}>Median Edit Time</div>
<Button
name="view-edit-time-details"
variant={showEditTimeModal ? "primary" : "secondary"}
onClick={() => openModal(showEditTimeModal ? 'none' : 'editTime')}
className={styles.userInteractionCardButton}
>
{showEditTimeModal ? 'Hide Details' : 'View Details'}
</Button>
</div>
{/* Median % Modified Card */}
<div className={styles.userInteractionCard}>
<div className={styles.userInteractionCardValue}>
{getImageTypePercentageModified()}
</div>
<div className={styles.userInteractionCardLabel}>Median % Modified</div>
<Button
name="view-percentage-details"
variant={showPercentageModal ? "primary" : "secondary"}
onClick={() => openModal(showPercentageModal ? 'none' : 'percentage')}
className={styles.userInteractionCardButton}
>
{showPercentageModal ? 'Hide Details' : 'View Details'}
</Button>
</div>
{/* Delete Rate Card */}
<div className={styles.userInteractionCard}>
<div className={styles.userInteractionCardValue}>
{getImageTypeDeleteRate()}
</div>
<div className={styles.userInteractionCardLabel}>Delete Rate</div>
<Button
name="view-delete-details"
variant={showDeleteModal ? "primary" : "secondary"}
onClick={() => openModal(showDeleteModal ? 'none' : 'delete')}
className={styles.userInteractionCardButton}
>
{showDeleteModal ? 'Hide Details' : 'View Details'}
</Button>
</div>
</div>
{/* Edit Time Details Table */}
{showEditTimeModal && (
<div className={styles.modelPerformance}>
<Table
data={getImageTypeEditTimeTableData('drone_image')}
columns={editTimeColumns}
keySelector={numericIdSelector}
filtered={false}
pending={false}
/>
</div>
)}
{/* Percentage Modified Details Table */}
{showPercentageModal && (
<div className={styles.modelPerformance}>
<Table
data={getImageTypePercentageTableData('drone_image')}
columns={percentageModifiedColumns}
keySelector={numericIdSelector}
filtered={false}
pending={false}
/>
</div>
)}
{/* Delete Rate Details Table */}
{showDeleteModal && (
<div className={styles.modelPerformance}>
<Table
data={getImageTypeDeleteRateTableData('drone_image')}
columns={deleteRateColumns}
keySelector={numericIdSelector}
filtered={false}
pending={false}
/>
</div>
)}
</Container>
<Container heading="Model Performance" headingLevel={3} withHeaderBorder withInternalPadding>
<div className={styles.modelPerformance}>
<Table
data={getImageTypeModelsTableData('drone_image')}
columns={modelsColumns}
keySelector={numericIdSelector}
filtered={false}
pending={false}
/>
</div>
</Container>
<Container heading="Quality-Event Type Correlation" headingLevel={3} withHeaderBorder withInternalPadding>
<div className={styles.tableContainer}>
<Table
data={getImageTypeQualityByEventTypeTableData('drone_image')}
columns={qualityByEventTypeColumns}
keySelector={numericIdSelector}
filtered={false}
pending={false}
/>
</div>
</Container>
<Container heading="Model Consistency Analysis" headingLevel={3} withHeaderBorder withInternalPadding>
<div className={styles.tableContainer}>
<Table
data={getImageTypeModelConsistencyTableData('drone_image')}
columns={modelConsistencyColumns}
keySelector={numericIdSelector}
filtered={false}
pending={false}
/>
</div>
</Container>
</div>
)}
</div>
</PageContainer>
);
}