feat: implement data visualization charts for analytics dashboard

- Add TimeSeriesChart component for report trends over time
- Add LitterTypeChart component with pie chart for type distribution
- Add QuantityChart component with bar chart for quantity aggregation
- Update analytics service with new API methods for chart data
- Add new types for chart data structures (TimeSeriesDataPoint, LitterTypeDistribution, QuantityAggregation)
- Integrate charts into AnalyticsPage with responsive grid layout
- Add mock data fallback for development when API is not available
- Configure charts for mobile responsiveness
- Implement comparison metrics display in time series
- Add loading states and empty states for all charts

Implements task 11: Data visualization charts
Requirements: 5.2, 5.3, 5.4
This commit is contained in:
2025-11-16 18:58:01 +00:00
parent 4be50caf4c
commit 82635559db
8 changed files with 429 additions and 14 deletions

View File

@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react';
import { DateRangeSelector, StatCard } from '@/components/analytics';
import { DateRangeSelector, StatCard, TimeSeriesChart, LitterTypeChart, QuantityChart } from '@/components/analytics';
import { analyticsService } from '@/services';
import type { DateRange, AnalyticsSummary } from '@/types';
import type { DateRange, AnalyticsSummary, TimeSeriesDataPoint, LitterTypeDistribution, QuantityAggregation } from '@/types';
/**
* Analytics Dashboard Page
@@ -16,12 +16,17 @@ export function AnalyticsPage() {
});
const [summary, setSummary] = useState<AnalyticsSummary | null>(null);
const [timeSeriesData, setTimeSeriesData] = useState<TimeSeriesDataPoint[]>([]);
const [litterTypeData, setLitterTypeData] = useState<LitterTypeDistribution[]>([]);
const [quantityData, setQuantityData] = useState<QuantityAggregation[]>([]);
const [loading, setLoading] = useState(true);
const [chartsLoading, setChartsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [exporting, setExporting] = useState(false);
useEffect(() => {
loadSummary();
loadChartData();
}, [dateRange]);
const loadSummary = async () => {
@@ -48,6 +53,68 @@ export function AnalyticsPage() {
}
};
const loadChartData = async () => {
try {
setChartsLoading(true);
const dateRangeParam = {
start: dateRange.start.toISOString(),
end: dateRange.end.toISOString(),
};
// Load all chart data in parallel
const [timeSeries, litterTypes, quantities] = await Promise.all([
analyticsService.getTimeSeries(dateRangeParam),
analyticsService.getLitterTypeDistribution(dateRangeParam),
analyticsService.getQuantityAggregation(dateRangeParam),
]);
setTimeSeriesData(timeSeries);
setLitterTypeData(litterTypes);
setQuantityData(quantities);
} catch (err) {
// If API is not available yet, show mock data for development
console.warn('Chart data API not available, using mock data:', err);
// Generate mock time series data
const mockTimeSeries: TimeSeriesDataPoint[] = [];
const daysDiff = Math.ceil((dateRange.end.getTime() - dateRange.start.getTime()) / (1000 * 60 * 60 * 24));
const dataPoints = Math.min(daysDiff, 30); // Max 30 data points
for (let i = 0; i < dataPoints; i++) {
const date = new Date(dateRange.start);
date.setDate(date.getDate() + Math.floor(i * daysDiff / dataPoints));
mockTimeSeries.push({
date: date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
reports: Math.floor(Math.random() * 20) + 5,
events: Math.floor(Math.random() * 5),
});
}
setTimeSeriesData(mockTimeSeries);
// Generate mock litter type distribution
const mockLitterTypes: LitterTypeDistribution[] = [
{ type: 'plastic', count: 45, percentage: 45 },
{ type: 'metal', count: 15, percentage: 15 },
{ type: 'glass', count: 12, percentage: 12 },
{ type: 'organic', count: 10, percentage: 10 },
{ type: 'hazardous', count: 8, percentage: 8 },
{ type: 'other', count: 10, percentage: 10 },
];
setLitterTypeData(mockLitterTypes);
// Generate mock quantity aggregation
const mockQuantities: QuantityAggregation[] = [
{ quantity: 'minimal', count: 30, percentage: 30 },
{ quantity: 'moderate', count: 40, percentage: 40 },
{ quantity: 'significant', count: 20, percentage: 20 },
{ quantity: 'severe', count: 10, percentage: 10 },
];
setQuantityData(mockQuantities);
} finally {
setChartsLoading(false);
}
};
const handleExportCSV = async () => {
try {
setExporting(true);
@@ -210,17 +277,17 @@ export function AnalyticsPage() {
</div>
)}
{/* Placeholder for future charts (tasks 11-12) */}
{!loading && summary && (
<div className="mt-8 bg-white rounded-lg shadow p-8">
<h2 className="text-xl font-semibold text-gray-900 mb-4">
Data Visualizations
</h2>
<p className="text-gray-500">
Charts and visualizations will be implemented in tasks 11-12
</p>
{/* Data Visualizations */}
<div className="mt-8 space-y-6">
{/* Time Series Chart */}
<TimeSeriesChart data={timeSeriesData} loading={chartsLoading} />
{/* Litter Type and Quantity Charts - Side by Side on Desktop */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<LitterTypeChart data={litterTypeData} loading={chartsLoading} />
<QuantityChart data={quantityData} loading={chartsLoading} />
</div>
)}
</div>
</div>
);
}