Skip to content

Commit

Permalink
feat: Gaps patterns page created (#44)
Browse files Browse the repository at this point in the history
Gaps patterns page - a page where you can see chart of buses and routes
routes they supposed to do again routed they actually did
  • Loading branch information
ShayAdler authored Oct 10, 2023
1 parent 5642449 commit f386f62
Show file tree
Hide file tree
Showing 15 changed files with 361 additions and 33 deletions.
12 changes: 9 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { ThemeProvider, createTheme } from '@mui/material'
import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'
import { LocalizationProvider } from '@mui/x-date-pickers'
import About from './pages/About'
import GapsPatternsPage from './pages/GapsPatternsPage'

const { Content } = Layout

Expand Down Expand Up @@ -58,6 +59,10 @@ const PAGES = [
key: '/gaps',
searchParamsRequired: true,
},
{
label: TEXTS.gaps_patterns_page_title,
key: '/gaps_patterns',
},
{
label: TEXTS.realtime_map_page_title,
key: '/map',
Expand Down Expand Up @@ -161,9 +166,10 @@ const App = () => {
<Route path={PAGES[0].key} element={<DashboardPage />} />
<Route path={PAGES[1].key} element={<TimelinePage />} />
<Route path={PAGES[2].key} element={<GapsPage />} />
<Route path={PAGES[3].key} element={<RealtimeMapPage />} />
<Route path={PAGES[4].key} element={<SingleLineMapPage />} />
<Route path={PAGES[5].key} element={<About />} />
<Route path={PAGES[3].key} element={<GapsPatternsPage />} />
<Route path={PAGES[4].key} element={<RealtimeMapPage />} />
<Route path={PAGES[5].key} element={<SingleLineMapPage />} />
<Route path={PAGES[6].key} element={<About />} />
<Route path="*" element={<RedirectToDashboard />} />
</Routes>
</StyledBody>
Expand Down
13 changes: 8 additions & 5 deletions src/api/gapsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,25 @@ const parseTime = (time: string): Moment | null => {
}

const USE_API = true
const LIMIT = 100
const LIMIT = 10000

export const getGapsAsync = async (
timestamp: Moment,
fromTimestamp: Moment,
toTimestamp: Moment,
operatorId: string,
lineRef: number,
): Promise<GapsList> => {
log('Searching for gaps', { operatorId, lineRef })
const startOfDay = moment(timestamp).startOf('day')
const fromDay = moment(fromTimestamp).startOf('day')
const toDay = moment(toTimestamp).startOf('day')
// const startOfDay = moment(fromTimestamp).startOf('day')
const data = USE_API
? (
await axios.get<RawGapsList>(`${BASE_PATH}/rides_execution/list`, {
params: {
limit: LIMIT,
date_from: startOfDay.format('YYYY-MM-DD'),
date_to: startOfDay.format('YYYY-MM-DD'),
date_from: fromDay.format('YYYY-MM-DD'),
date_to: toDay.format('YYYY-MM-DD'),
operator_ref: operatorId,
line_ref: lineRef,
},
Expand Down
7 changes: 4 additions & 3 deletions src/api/gtfsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@ const JOIN_SEPARATOR = ','
const SEARCH_MARGIN_HOURS = 4

export async function getRoutesAsync(
timestamp: Moment,
fromTimestamp: moment.Moment,
toTimestamp: moment.Moment,
operatorId: string,
lineNumber: string,
): Promise<BusRoute[]> {
log('looking up routes', { operatorId, lineNumber })
const gtfsRoutes = await GTFS_API.gtfsRoutesListGet({
routeShortName: lineNumber,
operatorRefs: operatorId,
dateFrom: timestamp.toDate(),
dateTo: timestamp.toDate(),
dateFrom: fromTimestamp.toDate(),
dateTo: toTimestamp.toDate(),
limit: 100,
})
const routes = Object.values(
Expand Down
4 changes: 2 additions & 2 deletions src/pages/GapsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const GapsPage = () => {
return
}
setGapsIsLoading(true)
getGapsAsync(moment(timestamp), operatorId, selectedRoute.lineRef)
getGapsAsync(moment(timestamp), moment(timestamp), operatorId, selectedRoute.lineRef)
.then(setGaps)
.finally(() => setGapsIsLoading(false))
}
Expand All @@ -81,7 +81,7 @@ const GapsPage = () => {
if (!operatorId || !lineNumber) {
return
}
getRoutesAsync(moment(timestamp), operatorId, lineNumber)
getRoutesAsync(moment(timestamp), moment(timestamp), operatorId, lineNumber)
.then((routes) =>
setSearch((current) =>
search.lineNumber === lineNumber ? { ...current, routes: routes } : current,
Expand Down
6 changes: 6 additions & 0 deletions src/pages/GapsPatternsPage.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.tooltip-style {
background: white;
padding: 5px;
border: 1px solid #ccc;
border-radius: 5px;
}
210 changes: 210 additions & 0 deletions src/pages/GapsPatternsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import React, { useContext, useEffect, useState } from 'react'
import './GapsPatternsPage.scss'
import { Moment } from 'moment'
import { DatePicker, Spin } from 'antd'
import moment from 'moment/moment'
import { useDate } from './components/DateTimePicker'
import { PageContainer } from './components/PageContainer'
import { Row } from './components/Row'
import { Label } from './components/Label'
import { TEXTS } from '../resources/texts'
import OperatorSelector from './components/OperatorSelector'
import LineNumberSelector from './components/LineSelector'
import { NotFound } from './components/NotFound'
import RouteSelector from './components/RouteSelector'
import { SearchContext } from '../model/pageState'
import { getRoutesAsync } from '../api/gtfsService'
import {
Bar,
CartesianGrid,
Legend,
Tooltip,
XAxis,
YAxis,
ComposedChart,
Cell,
TooltipProps,
} from 'recharts'
import { FormControlLabel, Radio, RadioGroup } from '@mui/material'
import { mapColorByExecution } from './components/utils'
import { useGapsList } from './useGapsList'

// Define prop types for the component
interface BusLineStatisticsProps {
lineRef: number
operatorRef: string
fromDate: Moment
toDate: Moment
}

const now = moment()

const CustomTooltip = ({ active, payload }: TooltipProps<number, string>) => {
if (active && payload && payload.length > 1) {
const actualRides = payload[0].value || 0
const plannedRides = payload[1].value || 0
const actualPercentage = ((actualRides / plannedRides) * 100).toFixed(0)
return (
<div className="custom-tooltip tooltip-style">
{` בוצעו ${actualPercentage}% מהנסיעות (${actualRides}/${plannedRides})`}
</div>
)
}

return null
}

function GapsByHour({ lineRef, operatorRef, fromDate, toDate }: BusLineStatisticsProps) {
const [sortingMode, setSortingMode] = useState<'hour' | 'severity'>('hour')

const hourlyData = useGapsList(fromDate, toDate, operatorRef, lineRef, sortingMode)

const maxHourlyRides = Math.max(
...hourlyData.map((entry) => entry.planned_rides),
...hourlyData.map((entry) => entry.actual_rides),
)

return (
<div>
<div>
<RadioGroup
row
aria-label="sorting-mode"
name="sorting-mode"
value={sortingMode}
onChange={(e) => setSortingMode(e.target.value as 'hour' | 'severity')}>
<FormControlLabel value="hour" control={<Radio />} label={TEXTS.order_by_hour} />
<FormControlLabel value="severity" control={<Radio />} label={TEXTS.order_by_severity} />
</RadioGroup>
</div>
<ComposedChart
layout="vertical"
width={500}
height={hourlyData.length * 50}
data={hourlyData}
margin={{
top: 20,
right: 20,
bottom: 20,
left: 20,
}}
barGap={-20}>
<CartesianGrid stroke="#f5f5f5" />
<XAxis
type="number"
xAxisId={0}
reversed={true}
orientation={'top'}
domain={[0, maxHourlyRides]}
/>
<XAxis
type="number"
xAxisId={1}
reversed={true}
orientation={'top'}
domain={[0, maxHourlyRides]}
hide
/>
<YAxis
dataKey="planned_hour"
type="category"
orientation={'right'}
style={{ direction: 'ltr', marginTop: '-10px' }}
/>
<Tooltip content={<CustomTooltip />} />
<Legend />
<Bar dataKey="actual_rides" barSize={20} radius={9} xAxisId={1} opacity={30}>
{hourlyData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={mapColorByExecution(entry.planned_rides, entry.actual_rides)}
/>
))}
</Bar>
<Bar dataKey="planned_rides" barSize={20} fill="#413ea055" radius={9} xAxisId={0} />
</ComposedChart>
</div>
)
}

const GapsPatternsPage = () => {
const [startDate, setStartDate] = useDate(now.clone().subtract(7, 'days'))
const [endDate, setEndDate] = useDate(now.clone().subtract(1, 'day'))
const { search, setSearch } = useContext(SearchContext)
const { operatorId, lineNumber, routes, routeKey } = search
const [routesIsLoading, setRoutesIsLoading] = useState(false)

const loadSearchData = async () => {
setRoutesIsLoading(true)
const routes = await getRoutesAsync(
moment(startDate),
moment(endDate),
operatorId as string,
lineNumber as string,
)
setSearch((current) => (search.lineNumber === lineNumber ? { ...current, routes } : current))
setRoutesIsLoading(false)
}

useEffect(() => {
if (!operatorId || !lineNumber) {
return
}
loadSearchData()
}, [operatorId, lineNumber, endDate, startDate, setSearch])

return (
<PageContainer>
<Row className="date-picker-container">
<DatePicker
defaultValue={startDate}
onChange={(data) => setStartDate(data)}
format="DD/MM/YYYY"
/>
-
<DatePicker
defaultValue={endDate}
onChange={(data) => setEndDate(data)}
format="DD/MM/YYYY"
/>
</Row>
<Row>
<OperatorSelector
operatorId={operatorId}
setOperatorId={(id) => setSearch((current) => ({ ...current, operatorId: id }))}
/>
</Row>
<Row>
<LineNumberSelector
lineNumber={lineNumber}
setLineNumber={(number) => setSearch((current) => ({ ...current, lineNumber: number }))}
/>
</Row>
{routesIsLoading && (
<Row>
<Label text={TEXTS.loading_routes} />
<Spin />
</Row>
)}
{!routesIsLoading &&
routes &&
(routes.length === 0 ? (
<NotFound>{TEXTS.line_not_found}</NotFound>
) : (
<RouteSelector
routes={routes}
routeKey={routeKey}
setRouteKey={(key) => setSearch((current) => ({ ...current, routeKey: key }))}
/>
))}
<GapsByHour
lineRef={routes?.find((route) => route.key === routeKey)?.lineRef || 0}
operatorRef={operatorId || ''}
fromDate={startDate}
toDate={endDate}
/>
</PageContainer>
)
}

export default GapsPatternsPage
2 changes: 1 addition & 1 deletion src/pages/SingleLineMapPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const SingleLineMapPage = () => {
if (!operatorId || !lineNumber) {
return
}
getRoutesAsync(moment(timestamp), operatorId, lineNumber).then((routes) =>
getRoutesAsync(moment(timestamp), moment(timestamp), operatorId, lineNumber).then((routes) =>
setSearch((current) =>
search.lineNumber === lineNumber ? { ...current, routes: routes } : current,
),
Expand Down
2 changes: 1 addition & 1 deletion src/pages/TimelinePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const TimelinePage = () => {
return
}
setRoutesIsLoading(true)
getRoutesAsync(moment(timestamp), operatorId, lineNumber)
getRoutesAsync(moment(timestamp), moment(timestamp), operatorId, lineNumber)
.then((routes) =>
setSearch((current) =>
search.lineNumber === lineNumber ? { ...current, routes: routes } : current,
Expand Down
12 changes: 11 additions & 1 deletion src/pages/components/DateTimePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import moment, { Moment } from 'moment'
import React from 'react'
import React, { useCallback } from 'react'
import { DatePicker } from 'antd'
import { TEXTS } from 'src/resources/texts'

Expand All @@ -16,6 +16,16 @@ function isDateDisabled(date: Moment) {
return date.isAfter(END_OF_TODAY) || date.isBefore(moment(JAN_FIRST_2022))
}

export function useDate(initialValue: Moment) {
const [date, setDate] = React.useState<Moment>(initialValue)
const onChange = useCallback((date: Moment | null) => {
if (date) {
setDate(date)
}
}, [])
return [date, onChange] as const
}

const DateTimePicker = ({ timestamp, setDateTime, showTime }: DateTimePicker) => (
<DatePicker
value={timestamp}
Expand Down
1 change: 1 addition & 0 deletions src/pages/components/OperatorSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const OperatorSelector = ({ operatorId, setOperatorId }: OperatorSelectorProps)
return (
<Autocomplete
disablePortal
style={{ width: '100%' }}
value={value}
onChange={(e, value) => setOperatorId(value ? value.id : '0')}
id="operator-select"
Expand Down
Loading

0 comments on commit f386f62

Please sign in to comment.