diff --git a/common/components/buttons/DownloadButton.tsx b/common/components/buttons/DownloadButton.tsx index 5ff7abb80..0639da715 100644 --- a/common/components/buttons/DownloadButton.tsx +++ b/common/components/buttons/DownloadButton.tsx @@ -3,59 +3,26 @@ import React from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faFileArrowDown } from '@fortawesome/free-solid-svg-icons'; import classNames from 'classnames'; -import type { DataPoint } from '../../types/dataPoints'; -import type { AggregateDataPoint, Location } from '../../types/charts'; +import type { Location } from '../../types/charts'; import { lineColorTextHover } from '../../styles/general'; import { useDelimitatedRoute } from '../../utils/router'; - -const directionAbbrs = { - northbound: 'NB', - southbound: 'SB', - eastbound: 'EB', - westbound: 'WB', - inbound: 'IB', - outbound: 'OB', -}; - -function filename( - datasetName: string, - location: Location, - bothStops: boolean, - startDate: string, - endDate?: string -) { - // CharlesMGH-SB_dwells_20210315.csv - // CentralSquareCambridge-MelneaCassWashington_traveltimesByHour-weekday_20200101-20201231.csv - // BostonUniversityWest-EB_headways_20161226-20170328.csv - const fromStop = location.from.replace(/[^A-z]/g, ''); - const toStop = location.to.replace(/[^A-z]/g, ''); - const dir = directionAbbrs[location.direction]; - const where = `${fromStop}-${bothStops ? toStop : dir}`; - - const what = datasetName; - - const date1 = startDate.replaceAll('-', ''); - const date2 = endDate ? `-${endDate.replaceAll('-', '')}` : ''; - const when = `${date1}${date2}`; - - return `${where}_${what}_${when}.csv`; -} +import { getCsvFilename } from '../../utils/csv'; interface DownloadButtonProps { datasetName: string; - data: (DataPoint | AggregateDataPoint)[]; - location: Location; - bothStops: boolean; + data: Record[]; startDate: string; + includeBothStopsForLocation?: boolean; + location?: Location; endDate?: string; } export const DownloadButton: React.FC = ({ datasetName, data, - location, - bothStops, + includeBothStopsForLocation, startDate, + location, endDate, }) => { const { line } = useDelimitatedRoute(); @@ -65,7 +32,14 @@ export const DownloadButton: React.FC = ({ className={'csv-link'} data={data} title={'Download data as CSV'} - filename={filename(datasetName, location, bothStops, startDate, endDate)} + filename={getCsvFilename({ + datasetName, + includeBothStopsForLocation, + startDate, + line, + location, + endDate, + })} > = ({ data, location, pointField, - bothStops = false, + includeBothStopsForLocation = false, fname, timeUnit, timeFormat, @@ -179,7 +179,7 @@ export const AggregateLineChart: React.FC = ({ data={data} datasetName={fname} location={location} - bothStops={bothStops} + includeBothStopsForLocation={includeBothStopsForLocation} startDate={startDate} /> )} diff --git a/common/components/charts/SingleDayLineChart.tsx b/common/components/charts/SingleDayLineChart.tsx index c359f1cc2..3d54735f9 100644 --- a/common/components/charts/SingleDayLineChart.tsx +++ b/common/components/charts/SingleDayLineChart.tsx @@ -63,7 +63,7 @@ export const SingleDayLineChart: React.FC = ({ pointField, benchmarkField, fname, - bothStops = false, + includeBothStopsForLocation = false, location, units, showLegend = true, @@ -229,7 +229,7 @@ export const SingleDayLineChart: React.FC = ({ data={data} datasetName={fname} location={location} - bothStops={bothStops} + includeBothStopsForLocation={includeBothStopsForLocation} startDate={date} /> )} diff --git a/common/components/charts/TimeSeriesChart/TimeSeriesChart.tsx b/common/components/charts/TimeSeriesChart/TimeSeriesChart.tsx index 0a214b6df..20fb51fe1 100644 --- a/common/components/charts/TimeSeriesChart/TimeSeriesChart.tsx +++ b/common/components/charts/TimeSeriesChart/TimeSeriesChart.tsx @@ -23,7 +23,6 @@ import type { ChartData } from 'chart.js'; import { enUS } from 'date-fns/locale'; import { useBreakpoint } from '../../../hooks/useBreakpoint'; -import { ChartBorder } from '../ChartBorder'; import { ChartDiv } from '../ChartDiv'; import { CHART_COLORS, COLORS } from '../../../constants/colors'; @@ -282,9 +281,5 @@ export const TimeSeriesChart = (props: Props) => { ); }, [isMobile, chartJsData, chartJsOptions, chartJsPlugins]); - return ( - - {chart} - - ); + return {chart}; }; diff --git a/common/types/charts.ts b/common/types/charts.ts index 0c965410e..87adf4cb8 100644 --- a/common/types/charts.ts +++ b/common/types/charts.ts @@ -77,7 +77,7 @@ export interface LineProps { chartId: string; location: Location; pointField: PointField; // X value - bothStops?: boolean; + includeBothStopsForLocation?: boolean; fname: DataName; showLegend?: boolean; } @@ -111,7 +111,7 @@ export interface HeadwayHistogramProps { date: string | undefined; location: Location; isLoading: boolean; - bothStops?: boolean; + includeBothStopsForLocation?: boolean; fname: DataName; showLegend?: boolean; metricField: MetricField; diff --git a/common/types/dataPoints.ts b/common/types/dataPoints.ts index 410062a6f..55175def1 100644 --- a/common/types/dataPoints.ts +++ b/common/types/dataPoints.ts @@ -98,6 +98,7 @@ export interface DeliveredTripMetrics { miles_covered: number; total_time: number; count: number; + miles_per_hour?: string; } export type LineSegmentData = { @@ -136,6 +137,7 @@ export interface TimePrediction { weekly: string; num_accurate_predictions: number; num_predictions: number; + accuracy_percentage?: string; } export type PredictionBin = '0-3 min' | '3-6 min' | '6-12 min' | '12-30 min'; diff --git a/common/utils/csv.ts b/common/utils/csv.ts new file mode 100644 index 000000000..3e2575fc3 --- /dev/null +++ b/common/utils/csv.ts @@ -0,0 +1,61 @@ +import { flatten } from 'lodash'; +import type { Location } from '../types/charts'; +import type { DeliveredTripMetrics, TimePredictionWeek } from '../types/dataPoints'; + +const directionAbbrs = { + northbound: 'NB', + southbound: 'SB', + eastbound: 'EB', + westbound: 'WB', + inbound: 'IB', + outbound: 'OB', +}; + +type GetCsvFilenameOptions = { + datasetName: string; + startDate: string; + endDate?: string; + line?: string; + location?: Location; + includeBothStopsForLocation?: boolean | undefined; +}; + +export function getCsvFilename(options: GetCsvFilenameOptions) { + const { datasetName, startDate, endDate, line, location, includeBothStopsForLocation } = options; + // CharlesMGH-SB_dwells_20210315.csv + // CentralSquareCambridge-MelneaCassWashington_traveltimesByHour-weekday_20200101-20201231.csv + // BostonUniversityWest-EB_headways_20161226-20170328.csv + const fromStop = location?.from.replace(/[^A-z]/g, ''); + const toStop = location?.to.replace(/[^A-z]/g, ''); + const dir = location && directionAbbrs[location.direction]; + + //Location does not exist on all widgets - in that case, 'where' will just be the name of the line + const where = location ? `${fromStop}-${includeBothStopsForLocation ? toStop : dir}` : line; + const what = datasetName; + const date1 = startDate.replaceAll('-', ''); + const date2 = endDate ? `-${endDate.replaceAll('-', '')}` : ''; + const when = `${date1}${date2}`; + + return `${where}_${what}_${when}.csv`; +} + +export const addAccuracyPercentageToData = (data: TimePredictionWeek[]) => { + const predictionsList = flatten(data.map(({ prediction }) => prediction)); + + const newData = predictionsList.map((item) => { + const accuracyPercentage = (item?.num_accurate_predictions / item?.num_predictions) * 100; + return { ...item, accuracy_percentage: accuracyPercentage.toFixed(1) }; + }); + + return newData; +}; + +export const addMPHToSpeedData = (data: DeliveredTripMetrics[]) => { + const newData = data.map((item) => { + const hours = item.total_time / 3600; + const mph = item.miles_covered / hours; + return { ...item, miles_per_hour: mph.toFixed(1) }; + }); + + return newData; +}; diff --git a/modules/dwells/charts/DwellsAggregateChart.tsx b/modules/dwells/charts/DwellsAggregateChart.tsx index 094e4d85c..01a53c233 100644 --- a/modules/dwells/charts/DwellsAggregateChart.tsx +++ b/modules/dwells/charts/DwellsAggregateChart.tsx @@ -37,7 +37,7 @@ export const DwellsAggregateChart: React.FC = ({ endDate={endDate} fillColor={CHART_COLORS.FILL} location={getLocationDetails(fromStation, toStation)} - bothStops={false} + includeBothStopsForLocation={false} fname="dwells" yUnit="Minutes" /> diff --git a/modules/headways/charts/HeadwaysAggregateChart.tsx b/modules/headways/charts/HeadwaysAggregateChart.tsx index 3faf5312e..4a3d24b86 100644 --- a/modules/headways/charts/HeadwaysAggregateChart.tsx +++ b/modules/headways/charts/HeadwaysAggregateChart.tsx @@ -37,7 +37,7 @@ export const HeadwaysAggregateChart: React.FC = ({ endDate={endDate} fillColor={CHART_COLORS.FILL} location={getLocationDetails(fromStation, toStation)} - bothStops={false} + includeBothStopsForLocation={false} fname="headways" yUnit="Minutes" /> diff --git a/modules/predictions/charts/PredictionsGraph.tsx b/modules/predictions/charts/PredictionsGraph.tsx index f632f2dbc..751e707fd 100644 --- a/modules/predictions/charts/PredictionsGraph.tsx +++ b/modules/predictions/charts/PredictionsGraph.tsx @@ -18,6 +18,8 @@ import { ChartDiv } from '../../../common/components/charts/ChartDiv'; import { PEAK_SPEED } from '../../../common/constants/baselines'; import { getRemainingBlockAnnotation } from '../../service/utils/graphUtils'; import { DATE_FORMAT, TODAY } from '../../../common/constants/dates'; +import { DownloadButton } from '../../../common/components/buttons/DownloadButton'; +import { addAccuracyPercentageToData } from '../../../common/utils/csv'; interface PredictionsGraphProps { data: TimePredictionWeek[]; @@ -61,6 +63,8 @@ export const PredictionsGraph: React.FC = ({ }, 0), })); + const dataWithPercentage = addAccuracyPercentageToData(data); + return ( @@ -207,6 +211,17 @@ export const PredictionsGraph: React.FC = ({ ]} /> +
+ {startDate && ( + + )} +
); }; diff --git a/modules/ridership/RidershipGraph.tsx b/modules/ridership/RidershipGraph.tsx index f5516cc3c..4e39c317a 100644 --- a/modules/ridership/RidershipGraph.tsx +++ b/modules/ridership/RidershipGraph.tsx @@ -17,6 +17,7 @@ import { useBreakpoint } from '../../common/hooks/useBreakpoint'; import { watermarkLayout } from '../../common/constants/charts'; import { ChartBorder } from '../../common/components/charts/ChartBorder'; import { ChartDiv } from '../../common/components/charts/ChartDiv'; +import { DownloadButton } from '../../common/components/buttons/DownloadButton'; interface RidershipGraphProps { data: RidershipCount[]; @@ -198,6 +199,17 @@ export const RidershipGraph: React.FC = ({ ]} /> +
+ {startDate && ( + + )} +
); }, [ diff --git a/modules/service/ServiceGraph.tsx b/modules/service/ServiceGraph.tsx index 567e309cd..cfe123db6 100644 --- a/modules/service/ServiceGraph.tsx +++ b/modules/service/ServiceGraph.tsx @@ -4,8 +4,10 @@ import { useDelimitatedRoute } from '../../common/utils/router'; import { PEAK_SCHEDULED_SERVICE } from '../../common/constants/baselines'; import type { DeliveredTripMetrics, ScheduledService } from '../../common/types/dataPoints'; import type { ParamsType } from '../speed/constants/speeds'; -import { getShuttlingBlockAnnotations } from './utils/graphUtils'; +import { ChartBorder } from '../../common/components/charts/ChartBorder'; +import { DownloadButton } from '../../common/components/buttons/DownloadButton'; import { ScheduledAndDeliveredGraph } from './ScheduledAndDeliveredGraph'; +import { getShuttlingBlockAnnotations } from './utils/graphUtils'; interface ServiceGraphProps { config: ParamsType; @@ -68,15 +70,29 @@ export const ServiceGraph: React.FC = (props: ServiceGraphPro }, [data, peak]); return ( - + + + +
+ {startDate && ( + + )} +
+
); }; diff --git a/modules/service/ServiceHoursGraph.tsx b/modules/service/ServiceHoursGraph.tsx index 5b14856e3..b36be9bf0 100644 --- a/modules/service/ServiceHoursGraph.tsx +++ b/modules/service/ServiceHoursGraph.tsx @@ -2,6 +2,7 @@ import React, { useMemo } from 'react'; import type { FetchServiceHoursResponse } from '../../common/types/api'; import type { AggType } from '../speed/constants/speeds'; +import { ChartBorder } from '../../common/components/charts/ChartBorder'; import { ScheduledAndDeliveredGraph } from './ScheduledAndDeliveredGraph'; interface ServiceHoursGraphProps { @@ -32,13 +33,15 @@ export const ServiceHoursGraph: React.FC = ( }, [serviceHours]); return ( - + + + ); }; diff --git a/modules/speed/charts/SpeedBetweenStationsAggregateChart.tsx b/modules/speed/charts/SpeedBetweenStationsAggregateChart.tsx index bb3265d54..367efd539 100644 --- a/modules/speed/charts/SpeedBetweenStationsAggregateChart.tsx +++ b/modules/speed/charts/SpeedBetweenStationsAggregateChart.tsx @@ -48,7 +48,7 @@ export const SpeedBetweenStationsAggregateChart: React.FC< endDate={endDate} fillColor={timeUnitByDate ? CHART_COLORS.FILL : CHART_COLORS.FILL_HOURLY} location={getLocationDetails(fromStation, toStation)} - bothStops={true} + includeBothStopsForLocation={true} fname="speeds" yUnit="MPH" /> diff --git a/modules/speed/charts/SpeedBetweenStationsSingleChart.tsx b/modules/speed/charts/SpeedBetweenStationsSingleChart.tsx index ad159be12..7094061b6 100644 --- a/modules/speed/charts/SpeedBetweenStationsSingleChart.tsx +++ b/modules/speed/charts/SpeedBetweenStationsSingleChart.tsx @@ -34,7 +34,7 @@ export const SpeedBetweenStationsSingleChart: React.FC = ({ const isMobile = !useBreakpoint('md'); const labels = data.map((point) => point.date); const shuttlingBlocks = getShuttlingBlockAnnotations(data); + const dataWithMPH = addMPHToSpeedData(data); return ( @@ -192,6 +195,17 @@ export const SpeedGraph: React.FC = ({ ]} /> +
+ {startDate && ( + + )} +
); }; diff --git a/modules/traveltimes/charts/TravelTimesAggregateChart.tsx b/modules/traveltimes/charts/TravelTimesAggregateChart.tsx index bcf3bd8b6..10da57900 100644 --- a/modules/traveltimes/charts/TravelTimesAggregateChart.tsx +++ b/modules/traveltimes/charts/TravelTimesAggregateChart.tsx @@ -46,7 +46,7 @@ export const TravelTimesAggregateChart: React.FC endDate={endDate} fillColor={timeUnitByDate ? CHART_COLORS.FILL : CHART_COLORS.FILL_HOURLY} location={getLocationDetails(fromStation, toStation)} - bothStops={true} + includeBothStopsForLocation={true} fname="traveltimes" yUnit="Minutes" /> diff --git a/modules/traveltimes/charts/TravelTimesSingleChart.tsx b/modules/traveltimes/charts/TravelTimesSingleChart.tsx index 4a768d14d..5d85fa095 100644 --- a/modules/traveltimes/charts/TravelTimesSingleChart.tsx +++ b/modules/traveltimes/charts/TravelTimesSingleChart.tsx @@ -37,7 +37,7 @@ export const TravelTimesSingleChart: React.FC = ({ metricField={MetricFieldKeys.travelTimeSec} pointField={PointFieldKeys.depDt} benchmarkField={BenchmarkFieldKeys.benchmarkTravelTimeSec} - bothStops={true} + includeBothStopsForLocation={true} units="Minutes" location={getLocationDetails(fromStation, toStation)} fname={'traveltimes'}