Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a delays page for alert analysis #997

Merged
merged 16 commits into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions common/api/delays.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { FetchAlertDelaysByLineParams, type FetchAlertDelaysByLineOptions } from '../types/api';
import type { LineDelays } from '../types/delays';
import { apiFetch } from './utils/fetch';

export const fetchLineDelaysByLine = async (
options: FetchAlertDelaysByLineOptions
): Promise<LineDelays[]> => {
if (!options[FetchAlertDelaysByLineParams.line]) return [];

return await apiFetch({
path: '/api/linedelays',
options,
errorMessage: 'Failed to fetch delay metrics',
});
};
13 changes: 13 additions & 0 deletions common/api/hooks/delays.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useQuery } from '@tanstack/react-query';
import { ONE_HOUR } from '../../constants/time';
import { fetchLineDelaysByLine } from '../delays';
import type { FetchAlertDelaysByLineOptions } from '../../types/api';

export const useAlertDelays = (options: FetchAlertDelaysByLineOptions, enabled?: boolean) => {
return useQuery({
queryKey: ['lineDelays', options],
queryFn: () => fetchLineDelaysByLine(options),
enabled: enabled,
staleTime: ONE_HOUR,
});
};
2 changes: 1 addition & 1 deletion common/components/charts/ChartDiv.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ interface ChartDivProps {

export const ChartDiv: React.FC<ChartDivProps> = ({ children, isMobile = false }) => {
return (
<div className={classNames(isMobile ? 'h-50' : 'h-60', 'flex w-full flex-row')}>{children}</div>
<div className={classNames(isMobile ? 'h-48' : 'h-60', 'flex w-full flex-row')}>{children}</div>
);
};
34 changes: 34 additions & 0 deletions common/components/inputs/BranchSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';
import { ButtonGroup } from '../general/ButtonGroup';
import type { LineRouteId } from '../../types/lines';

interface BranchSelectorProps {
routeId: LineRouteId;
setRouteId: (routeId: LineRouteId) => void;
}

enum GreenLineBranchOptions {
'Green-B' = 'B Branch',
'Green-C' = 'C Branch',
'Green-D' = 'D Branch',
'Green-E' = 'E Branch',
}

export const BranchSelector: React.FunctionComponent<BranchSelectorProps> = ({
routeId,
setRouteId,
}) => {
const selectedIndex = Object.keys(GreenLineBranchOptions).findIndex((route) => route === routeId);

return (
<div className={'flex w-full justify-center pt-2'}>
<ButtonGroup
selectedIndex={selectedIndex}
pressFunction={setRouteId}
options={Object.entries(GreenLineBranchOptions)}
additionalDivClass="md:w-auto"
additionalButtonClass="md:w-fit"
/>
</div>
);
};
2 changes: 1 addition & 1 deletion common/constants/dates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const ONE_YEAR_AGO_STRING = ONE_YEAR_AGO.format(DATE_FORMAT);
export const THREE_MONTHS_AGO = TODAY.subtract(90, 'days');
export const THREE_MONTHS_AGO_STRING = TODAY.subtract(90, 'days').format(DATE_FORMAT);

const OVERVIEW_TRAIN_MIN_DATE = '2016-02-01';
export const OVERVIEW_TRAIN_MIN_DATE = '2016-02-01';
const TRAIN_MIN_DATE = '2016-01-15';
const BUS_MIN_DATE = '2018-08-01';
export const BUS_MAX_DATE = '2024-06-30';
Expand Down
15 changes: 13 additions & 2 deletions common/constants/pages.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import type { IconDefinition } from '@fortawesome/free-solid-svg-icons';
import {
faMapLocationDot,
faCalendar,
faHouse,
faUsers,
faWarning,
faClockFour,
faGaugeHigh,
faTableColumns,
faStopwatch20,
faCalendarDays,
faCalendarXmark,
} from '@fortawesome/free-solid-svg-icons';
import type { Line } from '../types/lines';

Expand All @@ -19,6 +20,7 @@ export enum PAGES {
overview = 'overview',
speed = 'speed',
predictions = 'predictions',
delays = 'delays',
service = 'service',
slowzones = 'slowzones',
systemSlowzones = 'systemSlowzones',
Expand Down Expand Up @@ -77,7 +79,7 @@ export const ALL_PAGES: PageMap = {
name: 'Multi-day trips',
title: 'Multi-day trips',
lines: ['line-red', 'line-blue', 'line-green', 'line-orange', 'line-bus'],
icon: faCalendar,
icon: faCalendarDays,
dateStoreSection: 'multiTrips',
hasStationStore: true,
},
Expand Down Expand Up @@ -113,6 +115,14 @@ export const ALL_PAGES: PageMap = {
dateStoreSection: 'line',
icon: faClockFour,
},
delays: {
key: 'delays',
path: '/delays',
name: 'Delays',
lines: ['line-red', 'line-orange', 'line-blue', 'line-green'],
icon: faCalendarXmark,
dateStoreSection: 'line',
},
slowzones: {
key: 'slowzones',
path: '/slowzones',
Expand Down Expand Up @@ -161,6 +171,7 @@ export const LINE_PAGES = [
ALL_PAGES.slowzones,
ALL_PAGES.speed,
ALL_PAGES.predictions,
ALL_PAGES.delays,
ALL_PAGES.ridership,
];

Expand Down
12 changes: 12 additions & 0 deletions common/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,25 @@ export type FetchDeliveredTripMetricsOptions = {
line?: Line;
};

export type FetchAlertDelaysByLineOptions = {
start_date?: string;
end_date?: string;
line?: LineRouteId;
};

export enum FetchDeliveredTripMetricsParams {
startDate = 'start_date',
endDate = 'end_date',
agg = 'agg',
line = 'line',
}

export enum FetchAlertDelaysByLineParams {
startDate = 'start_date',
endDate = 'end_date',
line = 'line',
}

export enum FetchSpeedsParams {
startDate = 'start_date',
endDate = 'end_date',
Expand Down
20 changes: 20 additions & 0 deletions common/types/delays.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Line } from './lines';

export interface LineDelays {
date: string;
disabled_vehicle: number;
door_problem: number;
flooding: number;
fire: number;
line: Line;
medical_emergency: number;
other: number;
police_activity: number;
power_problem: number;
signal_problem: number;
mechanical_problem: number;
brake_problem: number;
switch_problem: number;
total_delay_time: number;
track_issue: number;
}
3 changes: 3 additions & 0 deletions common/utils/time.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,14 @@ export const getFormattedTimeString = (value: number, unit: 'minutes' | 'seconds
const secondsValue = unit === 'seconds' ? value : value * 60;
const absValue = Math.round(Math.abs(secondsValue));
const duration = dayjs.duration(absValue, 'seconds');
const hoursDuration = duration.asHours();
switch (true) {
case absValue < 100:
return `${absValue}s`;
case absValue < 3600:
return `${duration.format('m')}m ${duration.format('s').padStart(2, '0')}s`;
case absValue > 86400:
return `${hoursDuration.toFixed(0)}h ${duration.format('m').padStart(2, '0')}m`;
default:
return `${duration.format('H')}h ${duration.format('m').padStart(2, '0')}m`;
}
Expand Down
2 changes: 1 addition & 1 deletion deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ pushd server/ > /dev/null
poetry export --without-hashes --output requirements.txt
poetry run chalice package --stage $CHALICE_STAGE --merge-template cloudformation.json cfn/
aws cloudformation package --template-file cfn/sam.json --s3-bucket $BACKEND_BUCKET --output-template-file cfn/packaged.yaml
aws cloudformation deploy --template-file cfn/packaged.yaml --stack-name $CF_STACK_NAME --capabilities CAPABILITY_IAM --no-fail-on-empty-changeset --parameter-overrides \
aws cloudformation deploy --template-file cfn/packaged.yaml --s3-bucket $BACKEND_BUCKET --stack-name $CF_STACK_NAME --capabilities CAPABILITY_IAM --no-fail-on-empty-changeset --parameter-overrides \
TMFrontendHostname=$FRONTEND_HOSTNAME \
TMFrontendZone=$FRONTEND_ZONE \
TMFrontendCertArn=$FRONTEND_CERT_ARN \
Expand Down
153 changes: 153 additions & 0 deletions modules/delays/DelaysDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
'use client';

import React from 'react';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import { useDelimitatedRoute } from '../../common/utils/router';
import { ChartPlaceHolder } from '../../common/components/graphics/ChartPlaceHolder';
import { Layout } from '../../common/layouts/layoutTypes';
import { PageWrapper } from '../../common/layouts/PageWrapper';
import { ChartPageDiv } from '../../common/components/charts/ChartPageDiv';
import { useAlertDelays } from '../../common/api/hooks/delays';
import { Widget, WidgetDiv } from '../../common/components/widgets';
import { BranchSelector } from '../../common/components/inputs/BranchSelector';
import { lineToDefaultRouteId } from '../predictions/utils/utils';
import type { LineRouteId } from '../../common/types/lines';
import { Accordion } from '../../common/components/accordion/Accordion';
import { DelayByCategoryGraph } from './charts/DelayByCategoryGraph';
import { DelayBreakdownGraph } from './charts/DelayBreakdownGraph';
import { TotalDelayGraph } from './charts/TotalDelayGraph';

dayjs.extend(utc);

export function DelaysDetails() {
const {
line,
query: { startDate, endDate },
} = useDelimitatedRoute();

const [routeId, setRouteId] = React.useState<LineRouteId>(lineToDefaultRouteId(line));
const greenBranchToggle = React.useMemo(() => {
return line === 'line-green' && <BranchSelector routeId={routeId} setRouteId={setRouteId} />;
}, [line, routeId]);

React.useEffect(() => {
setRouteId(lineToDefaultRouteId(line));
}, [line]);

const enabled = Boolean(startDate && endDate && line);
const alertDelays = useAlertDelays(
{
start_date: startDate,
end_date: endDate,
line: routeId,
},
enabled
);
const delaysReady = alertDelays && line && !alertDelays.isError && alertDelays.data;
if (!startDate || !endDate) {
return <p>Select a date range to load graphs.</p>;
}

return (
<PageWrapper pageTitle={'Delays'}>
<ChartPageDiv>
<Widget title="Total Time Delayed" ready={[alertDelays]}>
{delaysReady ? (
<TotalDelayGraph data={alertDelays.data} startDate={startDate} endDate={endDate} />
) : (
<div className="relative flex h-full">
<ChartPlaceHolder query={alertDelays} />
</div>
)}
{greenBranchToggle}
</Widget>
<Widget title="Delay Time by Reason" ready={[alertDelays]}>
{delaysReady ? (
<DelayBreakdownGraph data={alertDelays.data} startDate={startDate} endDate={endDate} />
) : (
<div className="relative flex h-full">
<ChartPlaceHolder query={alertDelays} />
</div>
)}
{greenBranchToggle}
</Widget>
<Widget title="Delay Time by Reason" ready={[alertDelays]}>
{delaysReady ? (
<DelayByCategoryGraph data={alertDelays.data} />
) : (
<div className="relative flex h-full">
<ChartPlaceHolder query={alertDelays} />
</div>
)}
{greenBranchToggle}
</Widget>
<WidgetDiv>
<Accordion
contentList={[
{
title: 'About this data',
content: (
<div>
<p>
When there's a delay on the T, the MBTA sends out an alert to riders. These
alerts almost always include a reason for the delay, and an estimate of how
long trains may be delayed. We collect these alerts and group them by general
matching categories, and add up the total delay time to riders in a week.
</p>
<h4 className="mt-2 font-semibold">Example Alerts</h4>
<blockquote className="my-2 border-s-4 border-gray-300 bg-gray-50 p-4 dark:border-gray-500 dark:bg-gray-800">
<span className="animate-text bg-clip-text text-mbta-blue shadow-none transition-shadow duration-300">
Blue Line
</span>
: Delays of about{' '}
<span className="animate-text bg-gradient-to-r from-red-500 to-blue-500 bg-clip-text font-semibold text-transparent shadow-none transition-shadow duration-300">
20 minutes
</span>{' '}
due to a{' '}
<span className="animate-text bg-yellow-400 bg-clip-text font-semibold text-transparent shadow-none transition-shadow duration-300">
power issue
</span>{' '}
near wood island. Some trains may hold at stations.
</blockquote>
<blockquote className="my-2 border-s-4 border-gray-300 bg-gray-50 p-4 dark:border-gray-500 dark:bg-gray-800">
<span className="animate-text bg-clip-text text-mbta-orange shadow-none transition-shadow duration-300">
Orange Line
</span>
: Delays of about{' '}
<span className="animate-text bg-gradient-to-r from-red-500 to-blue-500 bg-clip-text font-semibold text-transparent shadow-none transition-shadow duration-300">
10 minutes
</span>{' '}
due to a{' '}
<span className="animate-text bg-red-500 bg-clip-text font-semibold text-transparent shadow-none transition-shadow duration-300">
disabled train
</span>{' '}
at Northeastern.
</blockquote>
<blockquote className="my-2 border-s-4 border-gray-300 bg-gray-50 p-4 dark:border-gray-500 dark:bg-gray-800">
<span className="animate-text bg-clip-text text-mbta-green shadow-none transition-shadow duration-300">
Green Line D Branch
</span>
: Delays of about{' '}
<span className="animate-text bg-gradient-to-r from-red-500 to-blue-500 bg-clip-text font-semibold text-transparent shadow-none transition-shadow duration-300">
10 minutes
</span>{' '}
eastbound due to a maintenance train{' '}
<span className="animate-text bg-yellow-400 bg-clip-text font-semibold text-transparent shadow-none transition-shadow duration-300">
inspecting the overhead
</span>{' '}
between riverside and kenmore.
</blockquote>
</div>
),
},
]}
size={'lg'}
/>
</WidgetDiv>
</ChartPageDiv>
</PageWrapper>
);
}

DelaysDetails.Layout = Layout.Dashboard;
Loading
Loading