From 266c45d9c0a9684d53df0601d481954b1e488e50 Mon Sep 17 00:00:00 2001 From: JonasGilg Date: Wed, 11 Dec 2024 21:51:06 +0100 Subject: [PATCH] :wrench: Merge statistics dashboard. --- frontend/package.json | 1 - .../InspireGridComponents/BaseLayer.tsx | 134 +++++++++++------- .../StatisticsDashboard.tsx | 23 +-- frontend/src/data_sockets/PandemosContext.tsx | 28 +--- frontend/src/types/pandemos.ts | 12 +- 5 files changed, 102 insertions(+), 96 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 99ba26d4..605300c2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,7 +37,6 @@ "@amcharts/amcharts5": "^5.8.4", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", - "@mui/base": "^5.0.0-beta.40", "@mui/icons-material": "^5.15.0", "@mui/lab": "^5.0.0-alpha.170", "@mui/base": "^5.0.0-beta.40", diff --git a/frontend/src/components/InspireGridComponents/BaseLayer.tsx b/frontend/src/components/InspireGridComponents/BaseLayer.tsx index f094587c..7426b1ec 100644 --- a/frontend/src/components/InspireGridComponents/BaseLayer.tsx +++ b/frontend/src/components/InspireGridComponents/BaseLayer.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR) // SPDX-License-Identifier: Apache-2.0 -import React, {useCallback, useContext, useEffect, useMemo} from 'react'; +import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react'; import {LayerGroup, LayersControl, MapContainer, TileLayer, Rectangle, useMap, Polyline} from 'react-leaflet'; import 'leaflet/dist/leaflet.css'; import {getGridNew, getCellFromPosition} from './inspire'; @@ -31,8 +31,8 @@ function MapEventsHandler({setMapZoom, setMapBounds, setMapCenter}: BaseLayerPro useEffect(() => { const bounds: MapBounds = [ - [52.248, 10.477], - [52.273, 10.572], + [52.15, 10.2], + [52.5, 10.8], ]; map.setMaxBounds(bounds); @@ -73,9 +73,9 @@ function MapEventsHandler({setMapZoom, setMapBounds, setMapCenter}: BaseLayerPro } function getResolutionFromZoom(input: number): number { - const inputStart = 14; + const inputStart = 13; const inputEnd = 18; - const outputStart = 9; + const outputStart = 8; const outputEnd = 12; if (input < inputStart || input > inputEnd) { @@ -89,12 +89,10 @@ function getResolutionFromZoom(input: number): number { export default function BaseLayer({ mapZoom = 14, mapBounds = [ - [52.248, 10.477], - [52.273, 10.572], + [52.15, 10], + [52.5, 11], ], mapCenter = [52.26, 10.525], - inspireGrid = true, - inspireGridLevel = 10, setMapZoom, setMapBounds, setMapCenter, @@ -102,16 +100,14 @@ export default function BaseLayer({ const context = useContext(PandemosContext); const selectedTab = useAppSelector((state) => state.userPreference.selectedSidebarTab ?? '1'); const filter = useAppSelector((state) => state.pandemosFilter); + const [showTrips, setShowTrips] = useState(false); + const [showHeatMap, setShowHeatMap] = useState(false); const gridResolution = useMemo(() => getResolutionFromZoom(mapZoom), [mapZoom]); const gridData = useMemo(() => { - const bounds: MapBounds = [ - [52.248, 10.477], - [52.273, 10.572], - ]; return getGridNew(mapBounds, gridResolution); - }, [mapBounds, gridResolution]); + }, [gridResolution, mapBounds]); const getLocationPos = useCallback( (location: number) => { @@ -121,36 +117,34 @@ export default function BaseLayer({ [context.locations] ); - // Calculate infected locations from filtered trip chains const infectedLocations = useMemo(() => { + if (!showHeatMap) { + return []; + } + const infectedLocations: {pos: number[]; infectionType: number}[] = []; - context.filteredTripChains?.forEach((tripChains) => { - tripChains.forEach((tripChainId) => { - if (context.tripChains) { - const tripChain = context.tripChains.get(tripChainId); - tripChain?.forEach((trip, index) => { - infectedLocations.push({ - pos: getLocationPos(trip.start_location), - infectionType: trip.infection_state, - }); - /*if (index > 0) { - if ( - infectionStates.includes(trip.infection_state) && - trip.infection_state !== tripChain[index - 1].infection_state && - susceptibleStates.includes(tripChain[index - 1].infection_state) - ) { - infectedLocations.push({ - pos: getLocationPos(trip.start_location), - infectionType: trip.infection_state, - }); - } - }*/ - }); - } + context.tripChains?.forEach((tripChain) => { + tripChain?.forEach((trip) => { + infectedLocations.push({ + pos: getLocationPos(trip.start_location), + infectionType: trip.infection_state, + }); + /*if (index > 0) { + if ( + infectionStates.includes(trip.infection_state) && + trip.infection_state !== tripChain[index - 1].infection_state && + susceptibleStates.includes(tripChain[index - 1].infection_state) + ) { + infectedLocations.push({ + pos: getLocationPos(trip.start_location), + infectionType: trip.infection_state, + }); + } + }*/ }); }); return infectedLocations; - }, [context.filteredTripChains, context.tripChains, getLocationPos]); + }, [context.tripChains, getLocationPos, showHeatMap]); const getColorForInfection = (infectionCount: number, maxInfectionCount: number) => { const ratio = infectionCount / maxInfectionCount; @@ -164,11 +158,15 @@ export default function BaseLayer({ // Calculate infection count per grid cell const infectedCellData = useMemo(() => { + if (!showHeatMap) { + return {cellsData: [], maxInfectionCount: 0}; + } + const cellsData: {bounds: [[number, number], [number, number]]; infectionCount: number}[] = []; gridData.rectangles.forEach(([latMin, lonMin, latMax, lonMax]) => { const infectionCount = infectedLocations.filter((loc) => { - return loc.pos[0] >= latMin && loc.pos[0] <= latMax && loc.pos[1] >= lonMin && loc.pos[1] <= lonMax; + return loc.pos[1] >= latMin && loc.pos[1] <= latMax && loc.pos[0] >= lonMin && loc.pos[0] <= lonMax; }).length; cellsData.push({ @@ -183,10 +181,14 @@ export default function BaseLayer({ const maxInfectionCount = Math.max(...cellsData.map((cell) => cell.infectionCount), 0); return {cellsData, maxInfectionCount}; - }, [infectedLocations, gridData]); + }, [showHeatMap, gridData.rectangles, infectedLocations]); const trips = useMemo(() => { - const trips: {pos: [number, number]; color: string}[] = []; + if (!showTrips) { + return []; + } + + const trips: {id: number; pos: [number, number]; color: string}[] = []; if (selectedTab === '3') { context.filteredTripChains?.forEach((tripChains) => { @@ -218,6 +220,7 @@ export default function BaseLayer({ color = 'red'; // Unknown } trips.push({ + id: trip.trip_id, pos: [getLocationPos(trip.start_location), getLocationPos(trip.end_location)], color: color, }); @@ -254,9 +257,8 @@ export default function BaseLayer({ filter.destinationTypes.includes(end?.location_type ?? -1) ) { if ( - !filter.infectionStates || - filter.infectionStates.length === 0 || - filter.infectionStates.includes(trip.infection_state) + ((!filter.infectionStates || filter.infectionStates.length === 0) && trip.infection_state > 0) || + filter.infectionStates?.includes(trip.infection_state) ) { let color: string; switch (trip.transport_mode) { @@ -282,6 +284,7 @@ export default function BaseLayer({ color = 'red'; // Unknown } trips.push({ + id: trip.trip_id, pos: [getLocationPos(trip.start_location), getLocationPos(trip.end_location)], color: color, }); @@ -311,6 +314,7 @@ export default function BaseLayer({ filter.tripDurationMin, getLocationPos, selectedTab, + showTrips, ]); return ( @@ -320,6 +324,7 @@ export default function BaseLayer({ scrollWheelZoom={true} doubleClickZoom={false} dragging={true} + minZoom={13} maxBounds={mapBounds} maxBoundsViscosity={1.0} style={{height: '100%', zIndex: '1', position: 'relative'}} @@ -328,9 +333,18 @@ export default function BaseLayer({ attribution='© OpenStreetMap contributors' url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' /> - - - + + + {infectedCellData.cellsData.map((rectangle, index) => { const fillColor = getColorForInfection(rectangle.infectionCount, infectedCellData.maxInfectionCount); @@ -349,11 +363,25 @@ export default function BaseLayer({ })} - - - {trips.map((line, index) => ( - - ))} + + + {trips + .map((line) => ( + [pos[1], pos[0]])} + /> + ))} diff --git a/frontend/src/components/Sidebar/StatisicsComponent/StatisticsDashboard.tsx b/frontend/src/components/Sidebar/StatisicsComponent/StatisticsDashboard.tsx index 3cbc6a9f..d0c057a9 100644 --- a/frontend/src/components/Sidebar/StatisicsComponent/StatisticsDashboard.tsx +++ b/frontend/src/components/Sidebar/StatisicsComponent/StatisticsDashboard.tsx @@ -8,7 +8,7 @@ import { selectActivities, selectAgeGroups, selectDestinationTypes, - selectInfectionStates, + selectInfectionStates, selectOriginTypes, selectTransportationModes, selectTripDuration, } from 'store/PandemosFilterSlice'; @@ -37,7 +37,6 @@ export default function StatisticsDashboard(props: any): JSX.Element { // To reset individual chart const resetChart = (chartId: string) => { const chart = chartRefs.current[chartId]; // Retrieve the chart from chartRefs using chartId - console.log('chart', chart); if (chart) { chart.filterAll(); chart.redraw(); @@ -49,8 +48,6 @@ export default function StatisticsDashboard(props: any): JSX.Element { useLayoutEffect(() => { if (context.expandedTrips && context.expandedTrips?.size() > 0) { - console.log('+++++++', context.expandedTrips); - // **************************************** 1. INFECTION CHART ******************************************// // Create the dimension based on infection_state @@ -79,7 +76,7 @@ export default function StatisticsDashboard(props: any): JSX.Element { .keyAccessor((d: any) => KeyInfo.infection_state[d.key].icon) .colors(d3.scaleOrdinal(d3.schemeBlues[9].slice().reverse())) .on('filtered', function (_chart: any) { - const selectedFilters = _chart.filters(); + const selectedFilters = _chart.filters().map((d: any) => Object.values(KeyInfo.infection_state).findIndex((e: any) => e.icon === d)); dispatch( selectInfectionStates({ infectionStates: selectedFilters, @@ -136,9 +133,14 @@ export default function StatisticsDashboard(props: any): JSX.Element { odInfectionChart.on('filtered', function (_chart: any, filter: any) { const selectedFilters = _chart.filters(); + dispatch( + selectOriginTypes({ + originTypes: selectedFilters.map((d: any) => d[0]), + }) + ); dispatch( selectDestinationTypes({ - destinationTypes: selectedFilters, + destinationTypes: selectedFilters.map((d: any) => d[1]), }) ); }); @@ -231,7 +233,6 @@ export default function StatisticsDashboard(props: any): JSX.Element { return Math.floor((endTime - startTime) / 60); }); const tripDurationGroup = tripDurationDimension.group().reduceCount(); - console.log('tripDurationxxxx', tripDurationGroup.top(3)); if (!tripDurationDimension || !tripDurationGroup) { console.error('Trip duration dimension or group is not defined'); return; @@ -248,11 +249,13 @@ export default function StatisticsDashboard(props: any): JSX.Element { .clipPadding(10) .group(tripDurationGroup) .on('filtered', function (_chart: any, filter: any) { + /*const selectedFilters = _chart.filters(); dispatch( selectTripDuration({ - // tripDurationMax: filter // TODO: once the real data is available + start: Math.round(selectedFilters[0][0]), + end: Math.round(selectedFilters[0][1]), }) - ); + );*/ }) .title((d: any) => d.value); @@ -261,7 +264,6 @@ export default function StatisticsDashboard(props: any): JSX.Element { // **************************************** 5. AGE CHART ******************************************// const ageDimension = context.expandedTrips?.dimension((d) => d.agent_age_group); const ageGroup = ageDimension?.group().reduceCount(); - console.log('agebins', ageGroup.all()); if (!ageDimension || !ageGroup) { console.error('Age dimension or group is not defined'); return; @@ -302,7 +304,6 @@ export default function StatisticsDashboard(props: any): JSX.Element { variant='contained' startIcon={} onClick={resetFilters} - href='javascript:dashboard.filterAll();dc.redrawAll();' sx={{ width: '80px', height: '26px', diff --git a/frontend/src/data_sockets/PandemosContext.tsx b/frontend/src/data_sockets/PandemosContext.tsx index 2bdfd46f..2e24d90d 100644 --- a/frontend/src/data_sockets/PandemosContext.tsx +++ b/frontend/src/data_sockets/PandemosContext.tsx @@ -125,9 +125,9 @@ export const PandemosProvider = ({children}: {children: React.ReactNode}) => { return ( ({ ...trip, - agent_age_group: agents[trip.agent_id].age_group, - start_location_type: locations[trip.start_location].location_type, - end_location_type: locations[trip.end_location].location_type, + agent_age_group: agents.find((agent) => agent.agent_id === trip.agent_id)!.age_group, + start_location_type: locations.find((loc) => loc.location_id === trip.start_location)!.location_type, + end_location_type: locations.find((loc) => loc.location_id === trip.end_location)!.location_type, } as TripExpanded) ?? {} ); }) @@ -136,29 +136,7 @@ export const PandemosProvider = ({children}: {children: React.ReactNode}) => { return crossfilter([]); } }, [agents, locations, trips]); - /* - // Preprocess trip chains - const tripChains = useMemo>(() => { - const agentTrips = new Map>(); - - // Group trips by agent - for (const trip of trips ?? []) { - agentTrips.set(trip.agent_id, [...(agentTrips.get(trip.agent_id) ?? []), trip]); - } - let chain_id = 0; - const tripChains = new Array(); - for (const tripChain of agentTrips.values()) { - let start = 0; - tripChain.forEach((trip, index) => { - if (locations![trip.start_location].location_type === 0) start = index; - if (trip.activity === 6) - tripChains.push({agent_id: trip.agent_id, chain_id: chain_id++, trips: tripChain.slice(start, index + 1)}); - }); - } - return tripChains; - }, [trips, locations]); -*/ return ( = { - /** Ages 0 to 4 */ 1: {icon: '0-4', fullName: 'Ages 0 to 4'}, - /** Ages 5 to 14 */ 2: {icon: '5-14', fullName: 'Ages 5 to 14'}, - /** Ages 15 to 34 */ 3: {icon: '15-34', fullName: 'Ages 15 to 34'}, - /** Ages 35 to 59 */ 4: {icon: '35-59', fullName: 'Ages 35 to 59'}, - /** Ages 60 to 79 */ 5: {icon: '60-79', fullName: 'Ages 60 to 79'}, - /** Ages 80 and older */ 6: {icon: '80+', fullName: 'Ages 80 and older'}, + /** Ages 0 to 4 */ 0: {icon: '0-4', fullName: 'Ages 0 to 4'}, + /** Ages 5 to 14 */ 1: {icon: '5-14', fullName: 'Ages 5 to 14'}, + /** Ages 15 to 34 */ 2: {icon: '15-34', fullName: 'Ages 15 to 34'}, + /** Ages 35 to 59 */ 3: {icon: '35-59', fullName: 'Ages 35 to 59'}, + /** Ages 60 to 79 */ 4: {icon: '60-79', fullName: 'Ages 60 to 79'}, + /** Ages 80 and older */ 5: {icon: '80+', fullName: 'Ages 80 and older'}, }; }