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:
@@ -83,7 +83,7 @@
|
|||||||
- Create export to CSV functionality
|
- Create export to CSV functionality
|
||||||
- _Requirements: 5.1, 5.5_
|
- _Requirements: 5.1, 5.5_
|
||||||
|
|
||||||
- [ ] 11. Implement data visualization charts
|
- [x] 11. Implement data visualization charts
|
||||||
- Create time series chart for report trends
|
- Create time series chart for report trends
|
||||||
- Build pie chart for litter type distribution
|
- Build pie chart for litter type distribution
|
||||||
- Implement comparison metrics with previous periods
|
- Implement comparison metrics with previous periods
|
||||||
|
|||||||
107
src/components/analytics/LitterTypeChart.tsx
Normal file
107
src/components/analytics/LitterTypeChart.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts';
|
||||||
|
import type { LitterTypeDistribution } from '@/types';
|
||||||
|
|
||||||
|
interface LitterTypeChartProps {
|
||||||
|
data: LitterTypeDistribution[];
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const COLORS: Record<string, string> = {
|
||||||
|
plastic: '#3b82f6', // blue
|
||||||
|
metal: '#6b7280', // gray
|
||||||
|
glass: '#10b981', // green
|
||||||
|
organic: '#84cc16', // lime
|
||||||
|
hazardous: '#ef4444', // red
|
||||||
|
other: '#8b5cf6', // purple
|
||||||
|
};
|
||||||
|
|
||||||
|
const LITTER_TYPE_LABELS: Record<string, string> = {
|
||||||
|
plastic: 'Plastic',
|
||||||
|
metal: 'Metal',
|
||||||
|
glass: 'Glass',
|
||||||
|
organic: 'Organic',
|
||||||
|
hazardous: 'Hazardous',
|
||||||
|
other: 'Other',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function LitterTypeChart({ data, loading }: LitterTypeChartProps) {
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<div className="animate-pulse">
|
||||||
|
<div className="h-6 bg-gray-200 rounded w-1/3 mb-4" />
|
||||||
|
<div className="h-64 bg-gray-200 rounded" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Litter Type Distribution</h3>
|
||||||
|
<div className="h-64 flex items-center justify-center text-gray-500">
|
||||||
|
No data available for the selected period
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format data for the chart
|
||||||
|
const chartData = data.map(item => ({
|
||||||
|
name: LITTER_TYPE_LABELS[item.type] || item.type,
|
||||||
|
value: item.count,
|
||||||
|
percentage: item.percentage,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const renderCustomLabel = (entry: { name: string; percentage: number }) => {
|
||||||
|
return `${entry.name}: ${entry.percentage.toFixed(1)}%`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Litter Type Distribution</h3>
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={chartData}
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
labelLine={false}
|
||||||
|
label={renderCustomLabel}
|
||||||
|
outerRadius={80}
|
||||||
|
fill="#8884d8"
|
||||||
|
dataKey="value"
|
||||||
|
>
|
||||||
|
{chartData.map((entry, index) => {
|
||||||
|
const litterType = data[index].type;
|
||||||
|
return (
|
||||||
|
<Cell
|
||||||
|
key={`cell-${index}`}
|
||||||
|
fill={COLORS[litterType] || '#8b5cf6'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip
|
||||||
|
formatter={(value: number, name: string, props: { payload: { percentage: number } }) => [
|
||||||
|
`${value} reports (${props.payload.percentage.toFixed(1)}%)`,
|
||||||
|
name
|
||||||
|
]}
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
border: '1px solid #e5e7eb',
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
padding: '0.75rem'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Legend
|
||||||
|
verticalAlign="bottom"
|
||||||
|
height={36}
|
||||||
|
wrapperStyle={{ paddingTop: '1rem' }}
|
||||||
|
/>
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
93
src/components/analytics/QuantityChart.tsx
Normal file
93
src/components/analytics/QuantityChart.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell } from 'recharts';
|
||||||
|
import type { QuantityAggregation } from '@/types';
|
||||||
|
|
||||||
|
interface QuantityChartProps {
|
||||||
|
data: QuantityAggregation[];
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QUANTITY_COLORS: Record<string, string> = {
|
||||||
|
minimal: '#10b981', // green
|
||||||
|
moderate: '#f59e0b', // amber
|
||||||
|
significant: '#f97316', // orange
|
||||||
|
severe: '#ef4444', // red
|
||||||
|
};
|
||||||
|
|
||||||
|
const QUANTITY_LABELS: Record<string, string> = {
|
||||||
|
minimal: 'Minimal',
|
||||||
|
moderate: 'Moderate',
|
||||||
|
significant: 'Significant',
|
||||||
|
severe: 'Severe',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function QuantityChart({ data, loading }: QuantityChartProps) {
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<div className="animate-pulse">
|
||||||
|
<div className="h-6 bg-gray-200 rounded w-1/3 mb-4" />
|
||||||
|
<div className="h-64 bg-gray-200 rounded" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Quantity Estimation</h3>
|
||||||
|
<div className="h-64 flex items-center justify-center text-gray-500">
|
||||||
|
No data available for the selected period
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format data for the chart
|
||||||
|
const chartData = data.map(item => ({
|
||||||
|
name: QUANTITY_LABELS[item.quantity] || item.quantity,
|
||||||
|
count: item.count,
|
||||||
|
percentage: item.percentage,
|
||||||
|
quantity: item.quantity,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Litter Quantity Distribution</h3>
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<BarChart data={chartData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="name"
|
||||||
|
stroke="#6b7280"
|
||||||
|
style={{ fontSize: '12px' }}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
stroke="#6b7280"
|
||||||
|
style={{ fontSize: '12px' }}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
formatter={(value: number, name: string, props: { payload: { percentage: number } }) => [
|
||||||
|
`${value} reports (${props.payload.percentage.toFixed(1)}%)`,
|
||||||
|
'Count'
|
||||||
|
]}
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
border: '1px solid #e5e7eb',
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
padding: '0.75rem'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Bar dataKey="count" radius={[8, 8, 0, 0]}>
|
||||||
|
{chartData.map((entry, index) => (
|
||||||
|
<Cell
|
||||||
|
key={`cell-${index}`}
|
||||||
|
fill={QUANTITY_COLORS[entry.quantity] || '#8b5cf6'}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Bar>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
82
src/components/analytics/TimeSeriesChart.tsx
Normal file
82
src/components/analytics/TimeSeriesChart.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
||||||
|
import type { TimeSeriesDataPoint } from '@/types';
|
||||||
|
|
||||||
|
interface TimeSeriesChartProps {
|
||||||
|
data: TimeSeriesDataPoint[];
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TimeSeriesChart({ data, loading }: TimeSeriesChartProps) {
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<div className="animate-pulse">
|
||||||
|
<div className="h-6 bg-gray-200 rounded w-1/3 mb-4" />
|
||||||
|
<div className="h-64 bg-gray-200 rounded" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Report Trends</h3>
|
||||||
|
<div className="h-64 flex items-center justify-center text-gray-500">
|
||||||
|
No data available for the selected period
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Report Trends Over Time</h3>
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<LineChart data={data} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="date"
|
||||||
|
stroke="#6b7280"
|
||||||
|
style={{ fontSize: '12px' }}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
stroke="#6b7280"
|
||||||
|
style={{ fontSize: '12px' }}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
border: '1px solid #e5e7eb',
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
padding: '0.75rem'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Legend
|
||||||
|
wrapperStyle={{ paddingTop: '1rem' }}
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="reports"
|
||||||
|
stroke="#3b82f6"
|
||||||
|
strokeWidth={2}
|
||||||
|
name="Reports"
|
||||||
|
dot={{ fill: '#3b82f6', r: 4 }}
|
||||||
|
activeDot={{ r: 6 }}
|
||||||
|
/>
|
||||||
|
{data.some(d => d.events !== undefined) && (
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="events"
|
||||||
|
stroke="#10b981"
|
||||||
|
strokeWidth={2}
|
||||||
|
name="Events"
|
||||||
|
dot={{ fill: '#10b981', r: 4 }}
|
||||||
|
activeDot={{ r: 6 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,2 +1,5 @@
|
|||||||
export { DateRangeSelector } from './DateRangeSelector';
|
export { DateRangeSelector } from './DateRangeSelector';
|
||||||
export { StatCard } from './StatCard';
|
export { StatCard } from './StatCard';
|
||||||
|
export { TimeSeriesChart } from './TimeSeriesChart';
|
||||||
|
export { LitterTypeChart } from './LitterTypeChart';
|
||||||
|
export { QuantityChart } from './QuantityChart';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
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 { analyticsService } from '@/services';
|
||||||
import type { DateRange, AnalyticsSummary } from '@/types';
|
import type { DateRange, AnalyticsSummary, TimeSeriesDataPoint, LitterTypeDistribution, QuantityAggregation } from '@/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Analytics Dashboard Page
|
* Analytics Dashboard Page
|
||||||
@@ -16,12 +16,17 @@ export function AnalyticsPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [summary, setSummary] = useState<AnalyticsSummary | null>(null);
|
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 [loading, setLoading] = useState(true);
|
||||||
|
const [chartsLoading, setChartsLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [exporting, setExporting] = useState(false);
|
const [exporting, setExporting] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadSummary();
|
loadSummary();
|
||||||
|
loadChartData();
|
||||||
}, [dateRange]);
|
}, [dateRange]);
|
||||||
|
|
||||||
const loadSummary = async () => {
|
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 () => {
|
const handleExportCSV = async () => {
|
||||||
try {
|
try {
|
||||||
setExporting(true);
|
setExporting(true);
|
||||||
@@ -210,17 +277,17 @@ export function AnalyticsPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Placeholder for future charts (tasks 11-12) */}
|
{/* Data Visualizations */}
|
||||||
{!loading && summary && (
|
<div className="mt-8 space-y-6">
|
||||||
<div className="mt-8 bg-white rounded-lg shadow p-8">
|
{/* Time Series Chart */}
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">
|
<TimeSeriesChart data={timeSeriesData} loading={chartsLoading} />
|
||||||
Data Visualizations
|
|
||||||
</h2>
|
{/* Litter Type and Quantity Charts - Side by Side on Desktop */}
|
||||||
<p className="text-gray-500">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
Charts and visualizations will be implemented in tasks 11-12
|
<LitterTypeChart data={litterTypeData} loading={chartsLoading} />
|
||||||
</p>
|
<QuantityChart data={quantityData} loading={chartsLoading} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Analytics service for API interactions
|
// Analytics service for API interactions
|
||||||
import { apiClient } from './api';
|
import { apiClient } from './api';
|
||||||
import type { AnalyticsSummary } from '@/types';
|
import type { AnalyticsSummary, TimeSeriesDataPoint, LitterTypeDistribution, QuantityAggregation } from '@/types';
|
||||||
|
|
||||||
class AnalyticsService {
|
class AnalyticsService {
|
||||||
/**
|
/**
|
||||||
@@ -18,6 +18,51 @@ class AnalyticsService {
|
|||||||
return apiClient.get<AnalyticsSummary>(`/analytics/summary${queryString ? `?${queryString}` : ''}`);
|
return apiClient.get<AnalyticsSummary>(`/analytics/summary${queryString ? `?${queryString}` : ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get time series data for report trends
|
||||||
|
*/
|
||||||
|
async getTimeSeries(dateRange?: { start: string; end: string }): Promise<TimeSeriesDataPoint[]> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (dateRange) {
|
||||||
|
params.append('startDate', dateRange.start);
|
||||||
|
params.append('endDate', dateRange.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryString = params.toString();
|
||||||
|
return apiClient.get<TimeSeriesDataPoint[]>(`/analytics/trends${queryString ? `?${queryString}` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get litter type distribution data
|
||||||
|
*/
|
||||||
|
async getLitterTypeDistribution(dateRange?: { start: string; end: string }): Promise<LitterTypeDistribution[]> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (dateRange) {
|
||||||
|
params.append('startDate', dateRange.start);
|
||||||
|
params.append('endDate', dateRange.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryString = params.toString();
|
||||||
|
return apiClient.get<LitterTypeDistribution[]>(`/analytics/litter-types${queryString ? `?${queryString}` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get quantity estimation aggregation data
|
||||||
|
*/
|
||||||
|
async getQuantityAggregation(dateRange?: { start: string; end: string }): Promise<QuantityAggregation[]> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (dateRange) {
|
||||||
|
params.append('startDate', dateRange.start);
|
||||||
|
params.append('endDate', dateRange.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryString = params.toString();
|
||||||
|
return apiClient.get<QuantityAggregation[]>(`/analytics/quantities${queryString ? `?${queryString}` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export analytics data to CSV
|
* Export analytics data to CSV
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -137,3 +137,21 @@ export interface DateRange {
|
|||||||
start: Date;
|
start: Date;
|
||||||
end: Date;
|
end: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TimeSeriesDataPoint {
|
||||||
|
date: string;
|
||||||
|
reports: number;
|
||||||
|
events?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LitterTypeDistribution {
|
||||||
|
type: LitterType;
|
||||||
|
count: number;
|
||||||
|
percentage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuantityAggregation {
|
||||||
|
quantity: QuantityLevel;
|
||||||
|
count: number;
|
||||||
|
percentage: number;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user