-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[WIP] Regional dashboards wip (#227)
* WIP Test Release oif the National Dashboards - this is restricted to only those with the forecast:view role and is being merged to test functionality in production with real data. Not ready for consumtpion by the wider user base! --------- Co-authored-by: jackson <Jackson Hyde> Co-authored-by: Rich Birch <[email protected]>
- Loading branch information
1 parent
fce189e
commit 44a5627
Showing
17 changed files
with
1,006 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
149 changes: 149 additions & 0 deletions
149
react/src/components/regionalpressure/ RegionalPressureChart.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
import * as React from 'react'; | ||
import {connect} from 'react-redux'; | ||
import { | ||
Alert, | ||
Card, | ||
CardContent, | ||
CardHeader, | ||
Button, | ||
Stack, | ||
useTheme, | ||
} from "@mui/material"; | ||
import { CheckCircle } from '@mui/icons-material'; | ||
import ErrorIcon from '@mui/icons-material/Error'; | ||
import {RootState} from '../../store/redux'; | ||
import drtTheme from '../../drtTheme'; | ||
import { | ||
Chart as ChartJS, | ||
RadialLinearScale, | ||
PointElement, | ||
LineElement, | ||
Filler, | ||
Tooltip, | ||
Legend, | ||
} from 'chart.js'; | ||
import { Radar } from 'react-chartjs-2'; | ||
|
||
ChartJS.register( | ||
RadialLinearScale, | ||
PointElement, | ||
LineElement, | ||
Filler, | ||
Tooltip, | ||
Legend | ||
); | ||
|
||
interface RegionalPressureChartProps { | ||
regionName: string; | ||
portCodes: string[]; | ||
portTotals: { | ||
[key: string]: number | ||
}; | ||
historicPortTotals: { | ||
[key: string]: number | ||
}; | ||
onMoreInfo: (region :string) => void | ||
} | ||
|
||
const doesExceed = (forecast: number, historic: number): boolean => { | ||
return forecast > historic; | ||
} | ||
|
||
const RegionalPressureChart = ({regionName, portCodes, portTotals, historicPortTotals, onMoreInfo}: RegionalPressureChartProps) => { | ||
const theme = useTheme(); | ||
|
||
const forecasts = [...portCodes].map(portCode => portTotals[portCode]); | ||
const historics = [...portCodes].map(portCode => historicPortTotals[portCode]); | ||
|
||
const chartData = { | ||
labels: portCodes, | ||
datasets: [ | ||
{ | ||
label: 'Forecasted PAX arrivals', | ||
data: forecasts, | ||
backgroundColor: 'rgba(0, 94, 165, 0.2)', | ||
borderColor: drtTheme.palette.primary.main, | ||
borderDash: [5, 5], | ||
pointStyle: 'rectRot', | ||
borderWidth: 1, | ||
}, | ||
{ | ||
label: 'Historic PAX average', | ||
data: historics, | ||
backgroundColor: 'transparent', | ||
borderColor: '#547a00', | ||
pointStyle: 'circle', | ||
pointBackgroundColor: '#547a00', | ||
borderDash: [0,0], | ||
borderWidth: 1, | ||
}, | ||
], | ||
}; | ||
|
||
const exceededForecasts = forecasts.map((forecast, index) => {return (doesExceed(forecast!, historics[index]!))}); | ||
const exceededCount = exceededForecasts.filter(forecast => forecast).length; | ||
|
||
const chartOptions = { | ||
layout: { | ||
padding: 0 | ||
}, | ||
plugins: { | ||
legend: { | ||
labels: { | ||
usePointStyle: true, | ||
} | ||
} | ||
}, | ||
scales: { | ||
r: { | ||
pointLabels: { | ||
callback: (label: string, index: number): string | number | string[] | number[] => { | ||
return doesExceed(forecasts[index]!, historics[index]!) ? `⚠ ${label}` : `${label}`; | ||
}, | ||
font: { | ||
weight: (context: any) => { | ||
return doesExceed(forecasts[context.index]!, historics[context.index]!) ? 'bold' : 'normal' | ||
} | ||
}, | ||
color: (context: any) => { | ||
return doesExceed(forecasts[context.index]!, historics[context.index]!) ? theme.palette.warning.main : 'black'; | ||
}, | ||
}, | ||
} | ||
} | ||
} | ||
|
||
return ( | ||
<Card variant='outlined'> | ||
<CardHeader title={regionName} /> | ||
<CardContent> | ||
<Stack sx={{ width: '100%' }} spacing={2}> | ||
<Radar data={chartData} options={chartOptions} /> | ||
<Button onClick={() => onMoreInfo(regionName)} fullWidth variant='contained'>More Info</Button> | ||
|
||
{ exceededCount > 0 ? | ||
<Alert icon={<ErrorIcon fontSize="inherit" />} severity="info"> | ||
{`PAX arrivals exceeds historic average across ${exceededCount} airports`} | ||
</Alert> | ||
: | ||
<Alert icon={<CheckCircle fontSize="inherit" />} severity="success"> | ||
Pax arrivals do not exceed historic average at any airport | ||
</Alert> | ||
} | ||
</Stack> | ||
</CardContent> | ||
</Card> | ||
) | ||
|
||
} | ||
|
||
|
||
const mapState = (state: RootState) => { | ||
return { | ||
portTotals: state.pressureDashboard?.portTotals, | ||
historicPortTotals: state.pressureDashboard?.historicPortTotals, | ||
}; | ||
} | ||
|
||
|
||
export default connect(mapState)(RegionalPressureChart); |
157 changes: 157 additions & 0 deletions
157
react/src/components/regionalpressure/RegionalDashboard.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
import * as React from 'react'; | ||
import {connect, MapDispatchToProps} from 'react-redux'; | ||
import { | ||
Box, | ||
Grid, | ||
FormControl, | ||
FormControlLabel, | ||
FormLabel, | ||
RadioGroup, | ||
Radio, | ||
TextField, | ||
} from "@mui/material"; | ||
import {DatePicker} from '@mui/x-date-pickers/DatePicker'; | ||
import {UserProfile} from "../../model/User"; | ||
import {ConfigValues, PortRegion} from "../../model/Config"; | ||
import moment, {Moment} from 'moment'; | ||
import {RootState} from '../../store/redux'; | ||
import { FormError } from '../../services/ValidationService'; | ||
|
||
import { requestPaxTotals } from './regionalPressureSagas'; | ||
import RegionalPressureViewSwitch from './RegionalPressureViewSwitch'; | ||
|
||
interface RegionalPressureDashboardProps { | ||
user: UserProfile; | ||
config: ConfigValues; | ||
errors: FormError[]; | ||
type?: string; | ||
start?: string; | ||
end?: string; | ||
requestRegion: (ports: string[], availablePorts: string[], searchType: string, startDate: string, endDate: string) => void; | ||
} | ||
|
||
interface ErrorFieldMapping { | ||
[key:string]: boolean | ||
} | ||
|
||
interface RegionalPressureDatesState { | ||
start: Moment; | ||
end: Moment; | ||
} | ||
|
||
|
||
const RegionalPressureDashboard = ({config, user, errors, type, start, end, requestRegion}: RegionalPressureDashboardProps) => { | ||
const [searchType, setSearchType] = React.useState<string>(type || 'single'); | ||
const [dates, setDate] = React.useState<RegionalPressureDatesState>({ | ||
start: moment(start).subtract(1, 'day'), | ||
end: moment(end), | ||
}); | ||
const errorFieldMapping: ErrorFieldMapping = {} | ||
errors.forEach((error: FormError) => errorFieldMapping[error.field] = true); | ||
|
||
let userPortsByRegion: PortRegion[] = config.portsByRegion.map(region => { | ||
const userPorts: string[] = user.ports.filter(p => region.ports.includes(p)); | ||
return {...region, ports: userPorts} as PortRegion | ||
}).filter(r => r.ports.length > 0) | ||
|
||
const availablePorts = config.ports.map(port => port.iata); | ||
|
||
React.useEffect(() => { | ||
requestRegion(user.ports, availablePorts, searchType, dates.start.format('YYYY-MM-DD'), dates.end.format('YYYY-MM-DD')); | ||
}, [user, availablePorts, requestRegion, searchType, dates]) | ||
|
||
const handleSearchTypeChange = (event: React.ChangeEvent<HTMLInputElement>) => { | ||
setSearchType(event.target.value); | ||
event.preventDefault(); | ||
}; | ||
|
||
const handleDateChange = (type: string, date: Moment | null) => { | ||
setDate({ | ||
...dates, | ||
[type]: date | ||
}); | ||
requestRegion(user.ports, availablePorts, searchType, date!.format('YYYY-MM-DD'), dates.end.format('YYYY-MM-DD')); | ||
} | ||
|
||
return ( | ||
<Box> | ||
<Box sx={{backgroundColor: '#E6E9F1', p: 2}}> | ||
<Grid container spacing={2} justifyItems={'stretch'} sx={{mb:2}}> | ||
<Grid item xs={12}> | ||
<h1>National Dashboard</h1> | ||
<h2>Compare pax arrivals vs previous year</h2> | ||
</Grid> | ||
<Grid item xs={12}> | ||
<FormControl> | ||
<FormLabel id="date-label">Select date</FormLabel> | ||
<RadioGroup | ||
row | ||
aria-labelledby="date-label" | ||
onChange={handleSearchTypeChange} | ||
> | ||
<FormControlLabel value="single" control={<Radio checked={searchType === 'single'} />} label="Single date" /> | ||
<FormControlLabel value="range" control={<Radio checked={searchType === 'range'} />} label="Date range" /> | ||
</RadioGroup> | ||
</FormControl> | ||
</Grid> | ||
</Grid> | ||
|
||
|
||
<Grid container spacing={2} justifyItems={'stretch'} sx={{mb:2}}> | ||
<Grid item> | ||
<DatePicker | ||
slots={{ | ||
textField: TextField | ||
}} | ||
slotProps={{ | ||
textField: { error: !!errorFieldMapping.startDate } | ||
}} | ||
label="From" | ||
sx={{backgroundColor: '#fff', marginRight: '10px'}} | ||
value={dates.start} | ||
onChange={(newValue: Moment | null) => handleDateChange('start', newValue)}/> | ||
|
||
</Grid> | ||
{ searchType === 'range' && | ||
<Grid item> | ||
<DatePicker | ||
slots={{ | ||
textField: TextField | ||
}} | ||
slotProps={{ | ||
textField: { error: !!errorFieldMapping.endDate } | ||
}} | ||
label="To" | ||
sx={{backgroundColor: '#fff'}} | ||
value={dates.end} | ||
onChange={(newValue: Moment | null) => handleDateChange('end', newValue)}/> | ||
</Grid> | ||
} | ||
</Grid> | ||
|
||
|
||
<RegionalPressureViewSwitch config={config} userPortsByRegion={userPortsByRegion} /> | ||
</Box> | ||
</Box> | ||
) | ||
|
||
} | ||
|
||
const mapState = (state: RootState) => { | ||
return { | ||
errors: state.pressureDashboard?.errors, | ||
type: state.pressureDashboard?.type, | ||
startDate: state.pressureDashboard?.start, | ||
endDate: state.pressureDashboard?.end, | ||
}; | ||
} | ||
|
||
const mapDispatch = (dispatch :MapDispatchToProps<any, RegionalPressureDashboardProps>) => { | ||
return { | ||
requestRegion: (userPorts: string[], availablePorts: string[], searchType: string, startDate: string, endDate: string) => { | ||
dispatch(requestPaxTotals(userPorts, availablePorts, searchType, startDate, endDate)); | ||
} | ||
}; | ||
}; | ||
|
||
export default connect(mapState, mapDispatch)(RegionalPressureDashboard); |
Oops, something went wrong.