diff --git a/common/components/charts/Legend.tsx b/common/components/charts/Legend.tsx index 0f812b6be..c966722cb 100644 --- a/common/components/charts/Legend.tsx +++ b/common/components/charts/Legend.tsx @@ -2,15 +2,8 @@ import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Disclosure } from '@headlessui/react'; import React from 'react'; -import { useBreakpoint } from '../../hooks/useBreakpoint'; export const LegendSingleDay: React.FC = () => { - const isMobile = !useBreakpoint('md'); - if (isMobile) return ; - return ; -}; - -const LegendMobile: React.FC = () => { return ( {({ open }) => ( @@ -37,18 +30,6 @@ const LegendMobile: React.FC = () => { ); }; -const LegendDesktop: React.FC = () => { - return ( -
- -
- ); -}; - const LegendSingle: React.FC = () => { return ( <> diff --git a/common/components/general/DataPair.tsx b/common/components/general/DataPair.tsx new file mode 100644 index 000000000..2530cd276 --- /dev/null +++ b/common/components/general/DataPair.tsx @@ -0,0 +1,20 @@ +import classNames from 'classnames'; +import React from 'react'; + +interface DataPairProps { + children: React.ReactNode; + last?: boolean; +} + +export const DataPair: React.FC = ({ children, last }) => { + return ( +
+ {children} +
+ ); +}; diff --git a/common/components/widgets/MiniWidgetCreator.tsx b/common/components/widgets/MiniWidgetCreator.tsx new file mode 100644 index 000000000..5fd30a330 --- /dev/null +++ b/common/components/widgets/MiniWidgetCreator.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import type { WidgetValueInterface } from '../../types/basicWidgets'; +import { DataPair } from '../general/DataPair'; +import { SmallDelta } from './internal/SmallDelta'; +import { SmallData } from './internal/SmallData'; + +interface MiniWidgetObject { + type: 'delta' | 'data' | string; + widgetValue: WidgetValueInterface; + text: string; +} +[]; + +interface MiniWidgetCreatorProps { + widgetObjects: MiniWidgetObject[]; +} + +const getDeltaOrDataComponent = (widgetObject: MiniWidgetObject) => { + if (widgetObject.type === 'delta') { + return ; + } + return ; +}; + +const getWidgets = (widgetObject: MiniWidgetObject[]) => { + const widgets: React.ReactNode[] = []; + for (let x = 0; x < widgetObject.length; x += 2) { + widgets.push( + widgetObject.length}> + {getDeltaOrDataComponent(widgetObject[x])} + {x + 1 < widgetObject.length ? getDeltaOrDataComponent(widgetObject[x + 1]) : null} + + ); + } + return widgets; +}; + +export const MiniWidgetCreator: React.FC = ({ widgetObjects }) => { + return ( +
+ {getWidgets(widgetObjects)} +
+ ); +}; diff --git a/common/components/widgets/internal/SmallData.tsx b/common/components/widgets/internal/SmallData.tsx new file mode 100644 index 000000000..9c91b5a36 --- /dev/null +++ b/common/components/widgets/internal/SmallData.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import classNames from 'classnames'; +import type { WidgetValueInterface } from '../../../types/basicWidgets'; + +type SmallDataProps = { + analysis: React.ReactNode; + widgetValue: WidgetValueInterface; +}; + +export const SmallData: React.FC = ({ analysis, widgetValue }) => { + return ( +
+

+ {analysis} +

+
{widgetValue.getFormattedValue()}
+
+ ); +}; diff --git a/common/components/widgets/internal/SmallDelta.tsx b/common/components/widgets/internal/SmallDelta.tsx new file mode 100644 index 000000000..237c47565 --- /dev/null +++ b/common/components/widgets/internal/SmallDelta.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import classNames from 'classnames'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faArrowTrendDown, faArrowTrendUp } from '@fortawesome/free-solid-svg-icons'; +import type { WidgetValueInterface } from '../../../types/basicWidgets'; + +type SmallDeltaProps = { + analysis: React.ReactNode; + widgetValue: WidgetValueInterface; +}; + +export const SmallDelta: React.FC = ({ analysis, widgetValue }) => { + return ( +
+

+ {analysis} +

+ {widgetValue.value !== undefined && ( +
+ {widgetValue.value !== 0 && ( + 0 ? faArrowTrendUp : faArrowTrendDown} + className={widgetValue.value > 0 ? 'text-[#e84e3b]' : 'text-[#35c759]'} + /> + )} +

+ + {widgetValue.value < 0 ? '-' : '+'} + + {widgetValue.getFormattedValue(false)} +

+
+ )} +
+ ); +}; diff --git a/common/components/widgets/internal/UnitText.tsx b/common/components/widgets/internal/UnitText.tsx index 6165e828b..75aae40c9 100644 --- a/common/components/widgets/internal/UnitText.tsx +++ b/common/components/widgets/internal/UnitText.tsx @@ -1,7 +1,13 @@ +import classNames from 'classnames'; import React from 'react'; export interface UnitTextProps { text: string; + light?: boolean; } -export const UnitText: React.FC = ({ text }) => { - return {text}; +export const UnitText: React.FC = ({ text, light }) => { + return ( + + {text} + + ); }; diff --git a/common/components/widgets/internal/WidgetText.tsx b/common/components/widgets/internal/WidgetText.tsx index 9434943f8..e7c97a2a4 100644 --- a/common/components/widgets/internal/WidgetText.tsx +++ b/common/components/widgets/internal/WidgetText.tsx @@ -1,8 +1,16 @@ +import classNames from 'classnames'; import React from 'react'; export interface WidgetTextProps { text: string; + light?: boolean; } -export const WidgetText: React.FC = ({ text }) => { - return {text}; +export const WidgetText: React.FC = ({ text, light }) => { + return ( + + {text} + + ); }; diff --git a/common/types/basicWidgets.tsx b/common/types/basicWidgets.tsx index 91995b2f9..9868052d3 100644 --- a/common/types/basicWidgets.tsx +++ b/common/types/basicWidgets.tsx @@ -12,7 +12,7 @@ export interface WidgetValueInterface { readonly percentChange?: number; getUnits: () => string; - getFormattedValue: () => React.ReactNode; + getFormattedValue: (light?: boolean) => React.ReactNode; getFormattedDelta: () => string; getFormattedPercentChange: () => string; } @@ -43,9 +43,9 @@ export class DeltaTimeWidgetValue extends BaseWidgetValue implements WidgetValue if (this.delta === undefined) return '...'; return getTimeUnit(this.delta); } - getFormattedValue() { + getFormattedValue(light) { if (this.delta === undefined) return '...'; - return getFormattedTimeValue(this.delta); + return getFormattedTimeValue(this.delta, light); } getFormattedDelta() { new Error('DeltaWidgets should use `getFormattedValue`'); @@ -57,12 +57,12 @@ export class DeltaZonesWidgetValue extends BaseWidgetValue implements WidgetValu getUnits() { return 'zones'; } - getFormattedValue() { + getFormattedValue(light) { if (this.delta === undefined) return '...'; return (

- = 0 ? '+' : '-'}${Math.abs(this.delta)}`} />{' '} - + = 0 ? '+' : '-'}${Math.abs(this.delta)}`} />{' '} +

); } @@ -79,9 +79,9 @@ export class TimeWidgetValue extends BaseWidgetValue implements WidgetValueInter return getTimeUnit(this.value); } - getFormattedValue() { + getFormattedValue(light?: boolean) { if (this.value === undefined) return '...'; - return getFormattedTimeValue(this.value); + return getFormattedTimeValue(this.value, light); } getFormattedDelta() { @@ -104,12 +104,12 @@ export class SZWidgetValue extends BaseWidgetValue implements WidgetValueInterfa getUnits() { return 'zones'; } - getFormattedValue() { + getFormattedValue(light) { if (typeof this.value === 'undefined') return '...'; return (

- {' '} - + {' '} +

); } @@ -124,12 +124,12 @@ export class PercentageWidgetValue extends BaseWidgetValue implements WidgetValu return '%'; } - getFormattedValue() { + getFormattedValue(light) { if (this.value === undefined) return '...'; return (

- {' '} - + {' '} +

); } @@ -145,11 +145,12 @@ export class TripsWidgetValue extends BaseWidgetValue implements WidgetValueInte return 'Trips'; } - getFormattedValue() { + getFormattedValue(light) { if (this.value === undefined) return '...'; return (

- + {' '} +

); } @@ -165,11 +166,12 @@ export class MPHWidgetValue extends BaseWidgetValue implements WidgetValueInterf return 'MPH'; } - getFormattedValue() { + getFormattedValue(light) { if (typeof this.value === 'undefined') return '...'; return (

- + {' '} +

); } @@ -186,12 +188,12 @@ export class RidersWidgetValue extends BaseWidgetValue implements WidgetValueInt return 'Riders'; } - getFormattedValue() { + getFormattedValue(light) { if (this.value === undefined) return '...'; return (

- {' '} - + {' '} +

); } diff --git a/common/utils/dwells.ts b/common/utils/dwells.ts deleted file mode 100644 index bffd6994d..000000000 --- a/common/utils/dwells.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { AggregateDataPoint, SingleDayDataPoint } from '../types/charts'; - -export const averageDwells = (dwells: SingleDayDataPoint[] | AggregateDataPoint[]) => { - if (!(dwells.length >= 1)) { - return 0; - } - const totalDwell = dwells - .map((trip: SingleDayDataPoint | AggregateDataPoint) => - 'mean' in trip ? trip.mean : trip.dwell_time_sec - ) - .reduce((total: number, dwell) => { - if (dwell) return total + dwell; - }, 0); - return (totalDwell || 0) / dwells.length; -}; - -export const longestDwells = (dwells: SingleDayDataPoint[] | AggregateDataPoint[]) => { - if (!(dwells.length >= 1)) { - return 0; - } - const allDwells = dwells - .map((trip: SingleDayDataPoint | AggregateDataPoint) => - 'max' in trip ? trip.max : trip.dwell_time_sec - ) - .filter((dwell) => dwell !== undefined) as number[]; - return Math.max(...allDwells); -}; - -export const getLongestDwell = (dwells: AggregateDataPoint[]) => { - return dwells.reduce( - (current, datapoint) => (datapoint.max > current.max ? datapoint : current), - dwells[0] - ); -}; - -const getLongestDwellSingle = (dwells: SingleDayDataPoint[]) => { - return dwells.reduce((current, datapoint) => { - if (datapoint.dwell_time_sec && current.dwell_time_sec) - return datapoint.dwell_time_sec > current.dwell_time_sec ? datapoint : current; - return current; - }, dwells[0]); -}; - -const getShortestDwellSingle = (dwells: SingleDayDataPoint[]) => { - return dwells.reduce((current, datapoint) => { - if (datapoint.dwell_time_sec && current.dwell_time_sec) - return datapoint.dwell_time_sec < current.dwell_time_sec ? datapoint : current; - return current; - }, dwells[0]); -}; - -export const getDwellsAggregateWidgetData = (dwells: AggregateDataPoint[]) => { - return { average: averageDwells(dwells), max: getLongestDwell(dwells) }; -}; - -export const getDwellsSingleWidgetData = (dwells: SingleDayDataPoint[]) => { - return { - average: averageDwells(dwells), - longest: getLongestDwellSingle(dwells), - shortest: getShortestDwellSingle(dwells), - }; -}; diff --git a/common/utils/headways.ts b/common/utils/headways.ts deleted file mode 100644 index 7e2212fc3..000000000 --- a/common/utils/headways.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { AggregateDataPoint, SingleDayDataPoint } from '../types/charts'; - -export const averageHeadway = (headways: SingleDayDataPoint[] | AggregateDataPoint[]) => { - if (headways && headways.length >= 1) { - const totalSum = headways - .map((trip: SingleDayDataPoint | AggregateDataPoint) => - 'mean' in trip ? trip.mean : trip.headway_time_sec - ) - .reduce((a, b) => { - if (a && b) { - return a + b; - } else { - return 0; - } - }); - return (totalSum || 0) / headways.length; - } else { - return 0; - } -}; - -export const longestHeadway = (headways: SingleDayDataPoint[] | AggregateDataPoint[]) => { - if (headways && headways.length >= 1) { - const allHeadways = headways - .map((trip: SingleDayDataPoint | AggregateDataPoint) => - 'max' in trip ? trip.max : trip.headway_time_sec - ) - .filter((headway) => headway !== undefined) as number[]; - return Math.max(...allHeadways); - } else { - return 0; - } -}; - -export const longestAggregateHeadway = (headways: AggregateDataPoint[]) => { - return headways.reduce( - (current, datapoint) => (datapoint.min < current.min ? datapoint : current), - headways[0] - ); -}; - -export const longestSingleHeadway = (headways: SingleDayDataPoint[]) => { - return headways.reduce((current, datapoint) => { - if (datapoint.headway_time_sec && current.headway_time_sec) - return datapoint.headway_time_sec > current.headway_time_sec ? datapoint : current; - return current; - }, headways[0]); -}; - -export const shortestSingleHeadway = (headways: SingleDayDataPoint[]) => { - return headways.reduce((current, datapoint) => { - if (datapoint.headway_time_sec && current.headway_time_sec) - return datapoint.headway_time_sec < current.headway_time_sec ? datapoint : current; - return current; - }, headways[0]); -}; - -export const getHeadwaysAggregateWidgetData = (headways: AggregateDataPoint[]) => { - return { average: averageHeadway(headways), max: longestAggregateHeadway(headways) }; -}; - -export const getHeadwaysSingleWidgetData = (headways: SingleDayDataPoint[]) => { - return { - average: averageHeadway(headways), - shortest: shortestSingleHeadway(headways), - longest: longestSingleHeadway(headways), - }; -}; diff --git a/common/utils/time.tsx b/common/utils/time.tsx index 7f6bb1bbf..cd723e7df 100644 --- a/common/utils/time.tsx +++ b/common/utils/time.tsx @@ -18,31 +18,33 @@ export const getTimeUnit = (value: number) => { } }; -export const getFormattedTimeValue = (value: number) => { +export const getFormattedTimeValue = (value: number, light?: boolean) => { const absValue = Math.round(Math.abs(value)); const duration = dayjs.duration(absValue, 'seconds'); switch (true) { case absValue < 100: return (

- - + +

); case absValue < 3600: return (

- - - + + {' '} + +

); default: return (

- - - + + {' '} + +

); } diff --git a/common/utils/traveltimes.ts b/common/utils/traveltimes.ts deleted file mode 100644 index a026694a3..000000000 --- a/common/utils/traveltimes.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { DeltaTimeWidgetValue } from '../types/basicWidgets'; -import type { AggregateDataPoint, SingleDayDataPoint } from '../types/charts'; - -const averageTravelTime = (traveltimes: (number | undefined)[]) => { - if (traveltimes && traveltimes.length >= 1) { - const totalSum = traveltimes.reduce((a, b) => { - if (a !== undefined && b !== undefined) { - return a + b; - } else { - return 0; - } - }, 0); - return (totalSum || 0) / traveltimes.length; - } else { - return 0; - } -}; - -const fastestAggregateTravelTime = (traveltimes: AggregateDataPoint[]) => { - return traveltimes.reduce( - (currentFastest, datapoint) => - datapoint.min < currentFastest.min ? datapoint : currentFastest, - traveltimes[0] - ); -}; - -const slowestSingleTravelTime = (traveltimes: SingleDayDataPoint[]) => { - return traveltimes.reduce((currentSlowest, datapoint) => { - if (datapoint.travel_time_sec && currentSlowest.travel_time_sec) { - return datapoint?.travel_time_sec > currentSlowest?.travel_time_sec - ? datapoint - : currentSlowest; - } - return currentSlowest; - }, traveltimes[0]); -}; - -const fastestSingleTravelTime = (traveltimes: SingleDayDataPoint[]) => { - return traveltimes.reduce((currentFastest, datapoint) => { - if (datapoint.travel_time_sec && currentFastest.travel_time_sec) { - return datapoint?.travel_time_sec < currentFastest?.travel_time_sec - ? datapoint - : currentFastest; - } - return currentFastest; - }, traveltimes[0]); -}; - -const deltaTravelTimesAggregate = (traveltimes: AggregateDataPoint[]) => { - return new DeltaTimeWidgetValue( - traveltimes[traveltimes.length - 1].mean, - traveltimes[traveltimes.length - 1].mean - traveltimes[0].mean - ); -}; - -export const getTravelTimesAggregateWidgetData = (traveltimes: AggregateDataPoint[]) => { - const average = averageTravelTime(traveltimes.map((datapoint) => datapoint.mean)); - const fastestDataPoint = fastestAggregateTravelTime(traveltimes); - const deltaWidgetValue = deltaTravelTimesAggregate(traveltimes); - return { average: average, fastest: fastestDataPoint, deltaWidgetValue: deltaWidgetValue }; -}; - -export const getTravelTimesSingleWidgetData = (traveltimes: SingleDayDataPoint[]) => { - const average = averageTravelTime(traveltimes.map((datapoint) => datapoint.travel_time_sec)); - const fastestDataPoint = fastestSingleTravelTime(traveltimes); - const slowestDataPoint = slowestSingleTravelTime(traveltimes); - return { average: average, fastest: fastestDataPoint, slowest: slowestDataPoint }; -}; diff --git a/common/utils/widgets.ts b/common/utils/widgets.ts new file mode 100644 index 000000000..a4d512df5 --- /dev/null +++ b/common/utils/widgets.ts @@ -0,0 +1,103 @@ +import { TimeWidgetValue } from '../types/basicWidgets'; +import type { AggregateDataPoint, SingleDayDataPoint } from '../types/charts'; + +const getAverage = (data: (number | undefined)[]) => { + const { length } = data; + if (data && length >= 1) { + const totalSum = data.reduce((a, b) => { + if (a !== undefined && b !== undefined) { + return a + b; + } else { + return a; + } + }, 0); + return (totalSum || 0) / length; + } else { + return 0; + } +}; + +const getPeaks = (data: (number | undefined)[]) => { + const first = data[0]; + const last = data[data.length - 1]; + data.sort(); + return { + low: data[0], + high: data[data.length - 1], + median: data[Math.round(data.length / 2)], + first: first, + last: last, + }; +}; + +const getAggDataPointsOfInterest = (aggData: AggregateDataPoint[]) => { + const medianData = aggData.map((tt) => tt['50%']); + const { low, high, median, first, last } = getPeaks(medianData); + const average = getAverage(medianData); + return { average, low, high, median, first, last }; +}; + +export const getAggDataWidgets = (aggData: AggregateDataPoint[]) => { + const { average, low, high, median, first, last } = getAggDataPointsOfInterest(aggData); + return [ + { text: 'Avg', widgetValue: new TimeWidgetValue(average), type: 'data' }, + { + text: 'From start', + widgetValue: new TimeWidgetValue( + last !== undefined && first !== undefined ? last - first : undefined + ), + type: 'delta', + }, + + { text: 'Low', widgetValue: new TimeWidgetValue(low), type: 'data' }, + { + text: 'From Low', + widgetValue: new TimeWidgetValue( + last !== undefined && low !== undefined ? last - low : undefined + ), + type: 'delta', + }, + + { text: 'High', widgetValue: new TimeWidgetValue(high), type: 'data' }, + { + text: 'From High', + widgetValue: new TimeWidgetValue( + last !== undefined && high !== undefined ? last - high : undefined + ), + type: 'delta', + }, + ]; +}; + +const getSingleDayNumberArray = ( + data: SingleDayDataPoint[], + type: 'traveltimes' | 'dwells' | 'headways' +) => { + if (type === 'traveltimes') return data.map((data) => data.travel_time_sec); + if (type === 'dwells') return data.map((data) => data.dwell_time_sec); + if (type === 'headways') return data.map((data) => data.headway_time_sec); + return []; +}; + +const getSingleDayPointsOfInterest = ( + data: SingleDayDataPoint[], + type: 'traveltimes' | 'dwells' | 'headways' +) => { + const _data = getSingleDayNumberArray(data, type); + const { high, low, median } = getPeaks(_data); + const average = getAverage(_data); + return { high, low, median, average }; +}; + +export const getSingleDayWidgets = ( + data: SingleDayDataPoint[], + type: 'traveltimes' | 'dwells' | 'headways' +) => { + const { high, low, median, average } = getSingleDayPointsOfInterest(data, type); + return [ + { text: 'Avg', widgetValue: new TimeWidgetValue(average), type: 'data' }, + { text: 'Median', widgetValue: new TimeWidgetValue(median), type: 'data' }, + { text: 'Low', widgetValue: new TimeWidgetValue(low), type: 'data' }, + { text: 'High', widgetValue: new TimeWidgetValue(high), type: 'data' }, + ]; +}; diff --git a/modules/dwells/DwellsAggregateWrapper.tsx b/modules/dwells/DwellsAggregateWrapper.tsx index ba5b78c7e..580d6038e 100644 --- a/modules/dwells/DwellsAggregateWrapper.tsx +++ b/modules/dwells/DwellsAggregateWrapper.tsx @@ -1,15 +1,12 @@ import React from 'react'; import type { UseQueryResult } from '@tanstack/react-query'; -import dayjs from 'dayjs'; -import { WidgetForCarousel } from '../../common/components/widgets/internal/WidgetForCarousel'; -import { TimeWidgetValue } from '../../common/types/basicWidgets'; import type { AggregateDataResponse } from '../../common/types/charts'; import type { Station } from '../../common/types/stations'; import { ChartPlaceHolder } from '../../common/components/graphics/ChartPlaceHolder'; -import { WidgetCarousel } from '../../common/components/general/WidgetCarousel'; -import { SMALL_DATE_FORMAT } from '../../common/constants/dates'; -import { getDwellsAggregateWidgetData } from '../../common/utils/dwells'; import { NoDataNotice } from '../../common/components/notices/NoDataNotice'; +import { MiniWidgetCreator } from '../../common/components/widgets/MiniWidgetCreator'; +import { CarouselGraphDiv } from '../../common/components/charts/CarouselGraphDiv'; +import { getAggDataWidgets } from '../../common/utils/widgets'; import { DwellsAggregateChart } from './charts/DwellsAggregateChart'; interface DwellsAggregateWrapperProps { @@ -25,25 +22,13 @@ export const DwellsAggregateWrapper: React.FC = ({ }) => { const dataReady = !query.isError && query.data && toStation && fromStation; if (!dataReady) return ; - const headwaysData = query.data.by_date.filter((datapoint) => datapoint.peak === 'all'); - if (headwaysData.length < 1) return ; - - const { average, max } = getDwellsAggregateWidgetData(headwaysData); + const dwellsData = query.data.by_date.filter((datapoint) => datapoint.peak === 'all'); + if (dwellsData.length < 1) return ; + const widgetObjects = getAggDataWidgets(dwellsData); return ( -
- - - - + -
+ + ); }; diff --git a/modules/dwells/DwellsDetails.tsx b/modules/dwells/DwellsDetails.tsx deleted file mode 100644 index 25f11237c..000000000 --- a/modules/dwells/DwellsDetails.tsx +++ /dev/null @@ -1,101 +0,0 @@ -'use client'; -import React from 'react'; -import dayjs from 'dayjs'; -import type { AggregateAPIOptions, SingleDayAPIOptions } from '../../common/types/api'; -import { AggregateAPIParams, SingleDayAPIParams } from '../../common/types/api'; -import { - getParentStationForStopId, - getLocationDetails, - stopIdsForStations, -} from '../../common/utils/stations'; -import { useDelimitatedRoute } from '../../common/utils/router'; -import { BasicDataWidgetPair } from '../../common/components/widgets/BasicDataWidgetPair'; -import { BasicDataWidgetItem } from '../../common/components/widgets/BasicDataWidgetItem'; -import { averageDwells, longestDwells } from '../../common/utils/dwells'; -import { TimeWidgetValue } from '../../common/types/basicWidgets'; -import { ErrorNotice } from '../../common/components/notices/ErrorNotice'; -import { TerminusNotice } from '../../common/components/notices/TerminusNotice'; -import { useDwellsAggregateData, useDwellsSingleDayData } from '../../common/api/hooks/dwells'; -import { WidgetDiv } from '../../common/components/widgets/WidgetDiv'; -import { SingleChartWrapper } from '../../common/components/charts/SingleChartWrapper'; -import { AggregateChartWrapper } from '../../common/components/charts/AggregateChartWrapper'; -import { PageWrapper } from '../../common/layouts/PageWrapper'; -import { WidgetTitle } from '../dashboard/WidgetTitle'; -import { Layout } from '../../common/layouts/layoutTypes'; - -export function DwellsDetails() { - const { - line, - query: { startDate, endDate, to, from }, - } = useDelimitatedRoute(); - - const fromStation = from ? getParentStationForStopId(from) : undefined; - const toStation = to ? getParentStationForStopId(to) : undefined; - const { fromStopIds } = stopIdsForStations(fromStation, toStation); - - const aggregate = Boolean(startDate && endDate); - const enabled = Boolean(fromStopIds && startDate); - const parameters: SingleDayAPIOptions | AggregateAPIOptions = aggregate - ? { - [AggregateAPIParams.stop]: fromStopIds, - [AggregateAPIParams.startDate]: startDate, - [AggregateAPIParams.endDate]: endDate, - } - : { - [SingleDayAPIParams.stop]: fromStopIds, - [SingleDayAPIParams.date]: startDate, - }; - - const dwells = useDwellsSingleDayData(parameters, !aggregate && enabled); - const dwellsAggregate = useDwellsAggregateData(parameters, aggregate && enabled); - - const dwellsData = aggregate ? dwellsAggregate?.data?.by_date : dwells?.data; - - if (dwells.isError) { - return ; - } - - return ( - - - - - - - - {aggregate ? ( - - ) : ( - - )} - - - - ); -} - -DwellsDetails.Layout = Layout.Dashboard; diff --git a/modules/dwells/DwellsSingleWrapper.tsx b/modules/dwells/DwellsSingleWrapper.tsx index b4bf5a975..31e01c477 100644 --- a/modules/dwells/DwellsSingleWrapper.tsx +++ b/modules/dwells/DwellsSingleWrapper.tsx @@ -1,15 +1,12 @@ import React from 'react'; import type { UseQueryResult } from '@tanstack/react-query'; -import dayjs from 'dayjs'; -import { WidgetForCarousel } from '../../common/components/widgets/internal/WidgetForCarousel'; -import { TimeWidgetValue } from '../../common/types/basicWidgets'; import type { Station } from '../../common/types/stations'; import { ChartPlaceHolder } from '../../common/components/graphics/ChartPlaceHolder'; -import { WidgetCarousel } from '../../common/components/general/WidgetCarousel'; import { CarouselGraphDiv } from '../../common/components/charts/CarouselGraphDiv'; import type { SingleDayDataPoint } from '../../common/types/charts'; -import { getDwellsSingleWidgetData } from '../../common/utils/dwells'; import { NoDataNotice } from '../../common/components/notices/NoDataNotice'; +import { MiniWidgetCreator } from '../../common/components/widgets/MiniWidgetCreator'; +import { getSingleDayWidgets } from '../../common/utils/widgets'; import { DwellsSingleChart } from './charts/DwellsSingleChart'; interface DwellsSingleWrapperProps { @@ -27,27 +24,12 @@ export const DwellsSingleWrapper: React.FC = ({ !query.isError && query.data && toStation && fromStation && query.data.length > 0; if (!dataReady) return ; if (query.data.length < 1) return ; - const { average, longest, shortest } = getDwellsSingleWidgetData(query.data); + const widgetObjects = getSingleDayWidgets(query.data, 'dwells'); + return ( - - - - - + ); }; diff --git a/modules/headways/HeadwaysAggregateWrapper.tsx b/modules/headways/HeadwaysAggregateWrapper.tsx index 430496a54..bdde6bf07 100644 --- a/modules/headways/HeadwaysAggregateWrapper.tsx +++ b/modules/headways/HeadwaysAggregateWrapper.tsx @@ -1,16 +1,12 @@ import React from 'react'; import type { UseQueryResult } from '@tanstack/react-query'; -import dayjs from 'dayjs'; -import { WidgetForCarousel } from '../../common/components/widgets/internal/WidgetForCarousel'; -import { TimeWidgetValue } from '../../common/types/basicWidgets'; import type { AggregateDataResponse } from '../../common/types/charts'; import type { Station } from '../../common/types/stations'; import { ChartPlaceHolder } from '../../common/components/graphics/ChartPlaceHolder'; -import { getHeadwaysAggregateWidgetData } from '../../common/utils/headways'; -import { WidgetCarousel } from '../../common/components/general/WidgetCarousel'; -import { SMALL_DATE_FORMAT } from '../../common/constants/dates'; import { CarouselGraphDiv } from '../../common/components/charts/CarouselGraphDiv'; import { NoDataNotice } from '../../common/components/notices/NoDataNotice'; +import { MiniWidgetCreator } from '../../common/components/widgets/MiniWidgetCreator'; +import { getAggDataWidgets } from '../../common/utils/widgets'; import { HeadwaysAggregateChart } from './charts/HeadwaysAggregateChart'; interface HeadwaysAggregateWrapperProps { @@ -28,27 +24,16 @@ export const HeadwaysAggregateWrapper: React.FC = if (!dataReady) return ; const headwaysData = query.data.by_date.filter((datapoint) => datapoint.peak === 'all'); if (headwaysData.length < 1) return ; + const widgetObjects = getAggDataWidgets(headwaysData); - const { average, max } = getHeadwaysAggregateWidgetData(headwaysData); return ( - - - - + ); }; diff --git a/modules/headways/HeadwaysDetails.tsx b/modules/headways/HeadwaysDetails.tsx deleted file mode 100644 index 5471dc1eb..000000000 --- a/modules/headways/HeadwaysDetails.tsx +++ /dev/null @@ -1,122 +0,0 @@ -'use client'; - -import React from 'react'; -import dayjs from 'dayjs'; -import type { AggregateAPIOptions, SingleDayAPIOptions } from '../../common/types/api'; -import { AggregateAPIParams, SingleDayAPIParams } from '../../common/types/api'; -import { - getLocationDetails, - getParentStationForStopId, - stopIdsForStations, -} from '../../common/utils/stations'; -import { useDelimitatedRoute } from '../../common/utils/router'; -import { BasicDataWidgetPair } from '../../common/components/widgets/BasicDataWidgetPair'; -import { BasicDataWidgetItem } from '../../common/components/widgets/BasicDataWidgetItem'; -import { averageHeadway, longestHeadway } from '../../common/utils/headways'; -import { TimeWidgetValue } from '../../common/types/basicWidgets'; -import { TerminusNotice } from '../../common/components/notices/TerminusNotice'; -import { - useHeadwaysAggregateData, - useHeadwaysSingleDayData, -} from '../../common/api/hooks/headways'; -import { WidgetDiv } from '../../common/components/widgets/WidgetDiv'; -import { SingleChartWrapper } from '../../common/components/charts/SingleChartWrapper'; -import { AggregateChartWrapper } from '../../common/components/charts/AggregateChartWrapper'; -import { PageWrapper } from '../../common/layouts/PageWrapper'; -import { WidgetTitle } from '../dashboard/WidgetTitle'; -import { Layout } from '../../common/layouts/layoutTypes'; -import { HeadwaysHistogramWrapper } from './charts/HeadwaysHistogramWrapper'; - -export function HeadwaysDetails() { - const { - line, - query: { startDate, endDate, to, from }, - } = useDelimitatedRoute(); - - const fromStation = from ? getParentStationForStopId(from) : undefined; - const toStation = to ? getParentStationForStopId(to) : undefined; - const { fromStopIds } = stopIdsForStations(fromStation, toStation); - - const aggregate = Boolean(startDate && endDate); - const enabled = Boolean(fromStopIds && startDate); - const parameters: SingleDayAPIOptions | AggregateAPIOptions = aggregate - ? { - [AggregateAPIParams.stop]: fromStopIds, - [AggregateAPIParams.startDate]: startDate, - [AggregateAPIParams.endDate]: endDate, - } - : { - [SingleDayAPIParams.stop]: fromStopIds, - [SingleDayAPIParams.date]: startDate, - }; - - const headways = useHeadwaysSingleDayData(parameters, !aggregate && enabled); - const headwaysAggregate = useHeadwaysAggregateData(parameters, aggregate && enabled); - - const headwaysData = aggregate ? headwaysAggregate?.data?.by_date : headways?.data; - - return ( - - - - - - - - - {aggregate ? ( - - ) : ( - - )} - - {!aggregate && ( - - - - - - )} - - - ); -} - -HeadwaysDetails.Layout = Layout.Dashboard; diff --git a/modules/headways/HeadwaysSingleWrapper.tsx b/modules/headways/HeadwaysSingleWrapper.tsx index 1d00adb8f..1a4c287d0 100644 --- a/modules/headways/HeadwaysSingleWrapper.tsx +++ b/modules/headways/HeadwaysSingleWrapper.tsx @@ -1,15 +1,12 @@ import React from 'react'; import type { UseQueryResult } from '@tanstack/react-query'; -import dayjs from 'dayjs'; -import { WidgetForCarousel } from '../../common/components/widgets/internal/WidgetForCarousel'; -import { TimeWidgetValue } from '../../common/types/basicWidgets'; import type { Station } from '../../common/types/stations'; import { ChartPlaceHolder } from '../../common/components/graphics/ChartPlaceHolder'; -import { WidgetCarousel } from '../../common/components/general/WidgetCarousel'; import { CarouselGraphDiv } from '../../common/components/charts/CarouselGraphDiv'; import type { SingleDayDataPoint } from '../../common/types/charts'; -import { getHeadwaysSingleWidgetData } from '../../common/utils/headways'; import { NoDataNotice } from '../../common/components/notices/NoDataNotice'; +import { MiniWidgetCreator } from '../../common/components/widgets/MiniWidgetCreator'; +import { getSingleDayWidgets } from '../../common/utils/widgets'; import { HeadwaysSingleChart } from './charts/HeadwaysSingleChart'; interface HeadwaysSingleWrapperProps { @@ -26,27 +23,12 @@ export const HeadwaysSingleWrapper: React.FC = ({ const dataReady = !query.isError && query.data && toStation && fromStation; if (!dataReady) return ; if (query.data.length < 1) return ; - const { average, longest, shortest } = getHeadwaysSingleWidgetData(query.data); + const widgetObjects = getSingleDayWidgets(query.data, 'headways'); + return ( - - - - - + ); }; diff --git a/modules/traveltimes/TravelTimesAggregateWrapper.tsx b/modules/traveltimes/TravelTimesAggregateWrapper.tsx index 0ed899ed8..7e03329f9 100644 --- a/modules/traveltimes/TravelTimesAggregateWrapper.tsx +++ b/modules/traveltimes/TravelTimesAggregateWrapper.tsx @@ -1,15 +1,12 @@ import React from 'react'; import type { UseQueryResult } from '@tanstack/react-query'; -import dayjs from 'dayjs'; -import { WidgetForCarousel } from '../../common/components/widgets/internal/WidgetForCarousel'; -import { TimeWidgetValue } from '../../common/types/basicWidgets'; import type { AggregateDataResponse } from '../../common/types/charts'; -import { getTravelTimesAggregateWidgetData } from '../../common/utils/traveltimes'; import type { Station } from '../../common/types/stations'; import { ChartPlaceHolder } from '../../common/components/graphics/ChartPlaceHolder'; -import { WidgetCarousel } from '../../common/components/general/WidgetCarousel'; import { CarouselGraphDiv } from '../../common/components/charts/CarouselGraphDiv'; import { NoDataNotice } from '../../common/components/notices/NoDataNotice'; +import { MiniWidgetCreator } from '../../common/components/widgets/MiniWidgetCreator'; +import { getAggDataWidgets } from '../../common/utils/widgets'; import { TravelTimesAggregateChart } from './charts/TravelTimesAggregateChart'; interface TravelTimesAggregateWrapperProps { @@ -27,32 +24,16 @@ export const TravelTimesAggregateWrapper: React.FC; const traveltimesData = query.data.by_date.filter((datapoint) => datapoint.peak === 'all'); if (traveltimesData.length < 1) return ; - const { average, fastest, deltaWidgetValue } = getTravelTimesAggregateWidgetData(traveltimesData); + const widgetObjects = getAggDataWidgets(traveltimesData); return ( - - - - - + ); }; diff --git a/modules/traveltimes/TravelTimesDetails.tsx b/modules/traveltimes/TravelTimesDetails.tsx deleted file mode 100644 index bf25768c1..000000000 --- a/modules/traveltimes/TravelTimesDetails.tsx +++ /dev/null @@ -1,107 +0,0 @@ -'use client'; - -import React from 'react'; -import type { AggregateAPIOptions, SingleDayAPIOptions } from '../../common/types/api'; -import { AggregateAPIParams, SingleDayAPIParams } from '../../common/types/api'; -import { - getLocationDetails, - getParentStationForStopId, - stopIdsForStations, -} from '../../common/utils/stations'; -import { useDelimitatedRoute } from '../../common/utils/router'; -import { TerminusNotice } from '../../common/components/notices/TerminusNotice'; -import { - useTravelTimesAggregateData, - useTravelTimesSingleDayData, -} from '../../common/api/hooks/traveltimes'; -import { WidgetDiv } from '../../common/components/widgets/WidgetDiv'; -import { SingleChartWrapper } from '../../common/components/charts/SingleChartWrapper'; -import { AggregateChartWrapper } from '../../common/components/charts/AggregateChartWrapper'; -import { PageWrapper } from '../../common/layouts/PageWrapper'; -import { ButtonGroup } from '../../common/components/general/ButtonGroup'; -import { WidgetTitle } from '../dashboard/WidgetTitle'; -import { Layout } from '../../common/layouts/layoutTypes'; - -export function TravelTimesDetails() { - const { - line, - query: { startDate, endDate, to, from }, - } = useDelimitatedRoute(); - - const fromStation = from ? getParentStationForStopId(from) : undefined; - const toStation = to ? getParentStationForStopId(to) : undefined; - const { fromStopIds, toStopIds } = stopIdsForStations(fromStation, toStation); - const location = getLocationDetails(fromStation, toStation); - - const aggregate = Boolean(startDate && endDate); - const enabled = Boolean(fromStopIds && toStopIds && startDate); - const parameters: SingleDayAPIOptions | AggregateAPIOptions = aggregate - ? { - [AggregateAPIParams.fromStop]: fromStopIds, - [AggregateAPIParams.toStop]: toStopIds, - [AggregateAPIParams.startDate]: startDate, - [AggregateAPIParams.endDate]: endDate, - } - : { - [SingleDayAPIParams.fromStop]: fromStopIds, - [SingleDayAPIParams.toStop]: toStopIds, - [SingleDayAPIParams.stop]: fromStopIds, - [SingleDayAPIParams.date]: startDate, - }; - - const travelTimes = useTravelTimesSingleDayData(parameters, !aggregate && enabled); - const travelTimesAggregate = useTravelTimesAggregateData(parameters, aggregate && enabled); - - const [peakTime, setPeakTime] = React.useState<'weekday' | 'weekend'>('weekday'); - - return ( - - - - {aggregate ? ( - - ) : ( - - )} - - {aggregate && ( - - - -
- -
-
- )} - -
- ); -} - -TravelTimesDetails.Layout = Layout.Dashboard; diff --git a/modules/traveltimes/TravelTimesSingleWrapper.tsx b/modules/traveltimes/TravelTimesSingleWrapper.tsx index d2b3188b2..28e641a10 100644 --- a/modules/traveltimes/TravelTimesSingleWrapper.tsx +++ b/modules/traveltimes/TravelTimesSingleWrapper.tsx @@ -1,15 +1,12 @@ import React from 'react'; import type { UseQueryResult } from '@tanstack/react-query'; -import dayjs from 'dayjs'; -import { WidgetForCarousel } from '../../common/components/widgets/internal/WidgetForCarousel'; -import { TimeWidgetValue } from '../../common/types/basicWidgets'; -import { getTravelTimesSingleWidgetData } from '../../common/utils/traveltimes'; import type { Station } from '../../common/types/stations'; import { ChartPlaceHolder } from '../../common/components/graphics/ChartPlaceHolder'; -import { WidgetCarousel } from '../../common/components/general/WidgetCarousel'; import { CarouselGraphDiv } from '../../common/components/charts/CarouselGraphDiv'; import type { SingleDayDataPoint } from '../../common/types/charts'; import { NoDataNotice } from '../../common/components/notices/NoDataNotice'; +import { MiniWidgetCreator } from '../../common/components/widgets/MiniWidgetCreator'; +import { getSingleDayWidgets } from '../../common/utils/widgets'; import { TravelTimesSingleChart } from './charts/TravelTimesSingleChart'; interface TravelTimesSingleWrapperProps { @@ -26,32 +23,15 @@ export const TravelTimesSingleWrapper: React.FC = const dataReady = !query.isError && query.data && toStation && fromStation; if (!dataReady) return ; if (query.data.length < 1) return ; - - const { average, fastest, slowest } = getTravelTimesSingleWidgetData(query.data); + const widgetObjects = getSingleDayWidgets(query.data, 'traveltimes'); return ( - - - - - + ); }; diff --git a/pages/[line]/trips/dwells.tsx b/pages/[line]/trips/dwells.tsx deleted file mode 100644 index 12c8a982c..000000000 --- a/pages/[line]/trips/dwells.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { ALL_LINE_PATHS } from '../../../common/types/lines'; -import { DwellsDetails } from '../../../modules/dwells/DwellsDetails'; - -export async function getStaticProps() { - return { props: {} }; -} - -export async function getStaticPaths() { - return { - paths: ALL_LINE_PATHS, - fallback: false, - }; -} - -export default DwellsDetails; diff --git a/pages/[line]/trips/headways.tsx b/pages/[line]/trips/headways.tsx deleted file mode 100644 index 838d0aeee..000000000 --- a/pages/[line]/trips/headways.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { ALL_LINE_PATHS, BUS_PATH } from '../../../common/types/lines'; -import { HeadwaysDetails } from '../../../modules/headways/HeadwaysDetails'; - -export async function getStaticProps() { - return { props: {} }; -} - -export async function getStaticPaths() { - return { - paths: [...ALL_LINE_PATHS, BUS_PATH], - fallback: false, - }; -} - -export default HeadwaysDetails; diff --git a/pages/[line]/trips/traveltimes.tsx b/pages/[line]/trips/traveltimes.tsx deleted file mode 100644 index f79cb23f7..000000000 --- a/pages/[line]/trips/traveltimes.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { ALL_LINE_PATHS, BUS_PATH } from '../../../common/types/lines'; -import { TravelTimesDetails } from '../../../modules/traveltimes/TravelTimesDetails'; - -export async function getStaticProps() { - return { props: {} }; -} - -export async function getStaticPaths() { - return { - paths: [...ALL_LINE_PATHS, BUS_PATH], - fallback: false, - }; -} - -export default TravelTimesDetails;