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

feature: Service hours chart #885

Merged
merged 16 commits into from
Dec 6, 2023
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
11 changes: 9 additions & 2 deletions common/api/hooks/service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { useQuery } from '@tanstack/react-query';
import type { FetchScheduledServiceOptions } from '../../types/api';
import type { FetchScheduledServiceOptions, FetchServiceHoursOptions } from '../../types/api';
import { ONE_HOUR } from '../../constants/time';
import { fetchScheduledService } from '../service';
import { fetchScheduledService, fetchServiceHours } from '../service';

export const useScheduledService = (options: FetchScheduledServiceOptions, enabled?: boolean) => {
return useQuery(['scheduledservice', options], () => fetchScheduledService(options), {
enabled: enabled,
staleTime: ONE_HOUR,
});
};

export const useServiceHours = (params: FetchServiceHoursOptions, enabled?: boolean) => {
return useQuery(['service_hours', params], () => fetchServiceHours(params), {
enabled: enabled,
staleTime: 0,
});
};
20 changes: 19 additions & 1 deletion common/api/service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { FetchScheduledServiceOptions } from '../types/api';
import type {
FetchScheduledServiceOptions,
FetchServiceHoursOptions,
FetchServiceHoursResponse,
} from '../types/api';
import { FetchScheduledServiceParams } from '../types/api';
import type { ScheduledService } from '../types/dataPoints';
import { APP_DATA_BASE_PATH } from '../utils/constants';
import { apiFetch } from './utils/fetch';

export const fetchScheduledService = async (
Expand All @@ -14,3 +19,16 @@ export const fetchScheduledService = async (
errorMessage: 'Failed to fetch trip counts',
});
};

export const fetchServiceHours = async (
params: FetchServiceHoursOptions
): Promise<FetchServiceHoursResponse | undefined> => {
if (!params.start_date) return undefined;
const url = new URL(`${APP_DATA_BASE_PATH}/api/service_hours`, window.location.origin);
Object.keys(params).forEach((paramKey) => {
url.searchParams.append(paramKey, params[paramKey]);
});
const response = await fetch(url.toString());
if (!response.ok) throw new Error('Failed to fetch service hours');
return await response.json();
};
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ export const TimeSeriesChart = <Data extends Dataset[]>(props: Props<Data>) => {

return {
x: {
min: timeAxis.from,
max: timeAxis.to,
display: true,
type: 'time' as const,
adapters: {
Expand Down
2 changes: 1 addition & 1 deletion common/components/charts/TimeSeriesChart/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const defaultWeekAxis: ResolvedTimeAxis = {

export const defaultMonthAxis: ResolvedTimeAxis = {
granularity: 'month',
axisUnit: 'month',
axisUnit: 'year',
label: 'Month',
format: 'yyyy',
tooltipFormat: 'MMM yyyy',
Expand Down
6 changes: 5 additions & 1 deletion common/components/charts/TimeSeriesChart/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,15 @@ export type DisplayStyle<Point extends DataPoint, Applied extends boolean = fals
export type AppliedDisplayStyle<Point extends DataPoint> = DisplayStyle<Point, true>;

export type Granularity = 'time' | 'day' | 'week' | 'month';
export type AxisUnit = 'day' | 'month';
export type AxisUnit = 'day' | 'month' | 'year';

export type ProvidedTimeAxis = {
label: string;
format?: string;
tooltipFormat?: string;
axisUnit?: AxisUnit;
from?: string;
to?: string;
} & ({ granularity: Granularity } | { agg: AggType });

export type ResolvedTimeAxis = {
Expand All @@ -50,6 +52,8 @@ export type ResolvedTimeAxis = {
axisUnit?: AxisUnit;
format?: string;
tooltipFormat?: string;
from?: string;
to?: string;
};

export type ValueAxis = {
Expand Down
11 changes: 10 additions & 1 deletion common/types/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { AggType } from '../../modules/speed/constants/speeds';
import type { SpeedRestriction } from './dataPoints';
import type { ServiceHours, SpeedRestriction } from './dataPoints';
import type { Line, LineRouteId } from './lines';

export enum QueryNameKeys {
Expand Down Expand Up @@ -99,3 +99,12 @@ export type FetchSpeedRestrictionsResponse = {
date: string;
zones: SpeedRestriction[];
};

export type FetchServiceHoursOptions = Partial<{
start_date: string;
end_date: string;
line_id: string;
agg: AggType;
}>;

export type FetchServiceHoursResponse = ServiceHours[];
8 changes: 7 additions & 1 deletion common/types/dataPoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,17 @@ export type ServiceLevels = {
};

export type ScheduledService = {
counts: number[];
start_date: string;
end_date: string;
start_date_service_levels: ServiceLevels;
end_date_service_levels: ServiceLevels;
counts: { date: string; count: number }[];
};

export type ServiceHours = {
date: string;
scheduled: number;
delivered: number;
};

export type RidershipCount = {
Expand Down
17 changes: 13 additions & 4 deletions modules/service/PercentageServiceGraphWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import type { SetStateAction } from 'react';
import type { DeliveredTripMetrics, ScheduledService } from '../../common/types/dataPoints';
import { WidgetCarousel } from '../../common/components/general/WidgetCarousel';
Expand Down Expand Up @@ -33,11 +33,20 @@ export const PercentageServiceGraphWrapper: React.FC<PercentageServiceGraphWrapp
}) => {
// TODO: Add 1 or 2 widgets to percentage service graph.
const { line } = useDelimitatedRoute();

const { scheduled, peak } = useMemo(
() => getPercentageData(data, predictedData, line),
[data, predictedData, line]
);

const { scheduledAverage, peakAverage } = useMemo(() => {
const scheduledAverage = getAverageWithNaNs(scheduled);
const peakAverage = getAverageWithNaNs(peak);
return { scheduledAverage, peakAverage };
}, [scheduled, peak]);

if (!data.some((datapoint) => datapoint.miles_covered)) return <NoDataNotice isLineMetric />;

const { scheduled, peak } = getPercentageData(data, predictedData, line);
const scheduledAverage = getAverageWithNaNs(scheduled);
const peakAverage = getAverageWithNaNs(peak);
return (
<>
<CarouselGraphDiv>
Expand Down
40 changes: 40 additions & 0 deletions modules/service/ScheduledAndDeliveredGraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* eslint-disable import/no-unused-modules */
import React, { useMemo } from 'react';

import type { Benchmark, Block, Dataset } from '../../common/components/charts/TimeSeriesChart';
import { TimeSeriesChart } from '../../common/components/charts/TimeSeriesChart';
import { useDelimitatedRoute } from '../../common/utils/router';
import { LINE_COLORS } from '../../common/constants/colors';
import type { AggType } from '../speed/constants/speeds';

interface ScheduledAndDeliveredGraphProps {
scheduled: Dataset;
delivered: Dataset;
startDate: string;
endDate: string;
valueAxisLabel: string;
agg: AggType;
benchmarks?: Benchmark[];
blocks?: Block[];
}

export const ScheduledAndDeliveredGraph: React.FC<ScheduledAndDeliveredGraphProps> = (
props: ScheduledAndDeliveredGraphProps
) => {
const { scheduled, delivered, valueAxisLabel, agg, startDate, endDate, benchmarks, blocks } =
props;
const data = useMemo(() => [scheduled, delivered], [scheduled, delivered]);
const { line } = useDelimitatedRoute();
const color = LINE_COLORS[line ?? 'default'];

return (
<TimeSeriesChart
data={data}
valueAxis={{ min: 0, label: valueAxisLabel }}
timeAxis={{ agg, from: startDate, to: endDate }}
style={{ color, stepped: true, fill: true }}
benchmarks={benchmarks}
blocks={blocks}
/>
);
};
31 changes: 29 additions & 2 deletions modules/service/ServiceDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import { useDelimitatedRoute } from '../../common/utils/router';
import { ChartPlaceHolder } from '../../common/components/graphics/ChartPlaceHolder';
import { useScheduledService } from '../../common/api/hooks/service';
import { useScheduledService, useServiceHours } from '../../common/api/hooks/service';
import { Layout } from '../../common/layouts/layoutTypes';
import { PageWrapper } from '../../common/layouts/PageWrapper';
import { getSpeedGraphConfig } from '../speed/constants/speeds';
Expand All @@ -14,6 +14,7 @@ import { useDeliveredTripMetrics } from '../../common/api/hooks/tripmetrics';
import { WidgetTitle } from '../dashboard/WidgetTitle';
import { ServiceGraphWrapper } from './ServiceGraphWrapper';
import { PercentageServiceGraphWrapper } from './PercentageServiceGraphWrapper';
import { ServiceHoursGraph } from './ServiceHoursGraph';
dayjs.extend(utc);

export function ServiceDetails() {
Expand Down Expand Up @@ -45,7 +46,18 @@ export function ServiceDetails() {
enabled
).data;

const serviceHoursData = useServiceHours(
{
start_date: startDate,
end_date: endDate,
line_id: line,
agg: config.agg,
},
enabled
);

const serviceDataReady = !tripsData.isError && tripsData.data && line && config && predictedData;
const serviceHoursDataReady = !serviceHoursData.isError && serviceHoursData.data;

if (!startDate || !endDate) {
return <p>Select a date range to load graphs.</p>;
Expand Down Expand Up @@ -88,7 +100,22 @@ export function ServiceDetails() {
</div>
)}
</WidgetDiv>
</ChartPageDiv>{' '}
<WidgetDiv>
<WidgetTitle title="Hours of service" subtitle="Across all trains" />
{serviceHoursDataReady ? (
<ServiceHoursGraph
serviceHours={serviceHoursData.data!}
agg={config.agg}
startDate={startDate}
endDate={endDate}
/>
) : (
<div className="relative flex h-full">
<ChartPlaceHolder query={serviceHoursData} />
</div>
)}
</WidgetDiv>
</ChartPageDiv>
</PageWrapper>
);
}
Expand Down
Loading
Loading