diff --git a/common/constants/baselines.ts b/common/constants/baselines.ts index 11ca5b0ce..b810700d8 100644 --- a/common/constants/baselines.ts +++ b/common/constants/baselines.ts @@ -8,7 +8,7 @@ export const PEAK_SCHEDULED_SERVICE = { DEFAULT: 0, }; -export const PEAK_SPEED = { +export const PEAK_MPH = { 'line-red': 21.4, 'line-orange': 18, 'line-blue': 20.5, diff --git a/modules/commute/speed/utils/utils.ts b/modules/commute/speed/utils/utils.ts index 1d028b4e4..f8aa8f6d0 100644 --- a/modules/commute/speed/utils/utils.ts +++ b/modules/commute/speed/utils/utils.ts @@ -1,5 +1,6 @@ +import { PEAK_MPH } from '../../../../common/constants/baselines'; import type { SpeedDataPoint } from '../../../../common/types/dataPoints'; -import { CORE_TRACK_LENGTHS, PEAK_MPH } from '../../../speed/constants/speeds'; +import { CORE_TRACK_LENGTHS } from '../../../speed/constants/speeds'; export const calculateCommuteSpeedWidgetValues = ( weeklyData: SpeedDataPoint[], diff --git a/modules/landing/utils.ts b/modules/landing/utils.ts index e0f786a5e..f0d0ca413 100644 --- a/modules/landing/utils.ts +++ b/modules/landing/utils.ts @@ -1,10 +1,6 @@ import type { ChartDataset } from 'chart.js'; import { round } from 'lodash'; -import { - PEAK_RIDERSHIP, - PEAK_SCHEDULED_SERVICE, - PEAK_SPEED, -} from '../../common/constants/baselines'; +import { PEAK_RIDERSHIP, PEAK_SCHEDULED_SERVICE, PEAK_MPH } from '../../common/constants/baselines'; import { LINE_COLORS } from '../../common/constants/colors'; import type { RidershipCount, DeliveredTripMetrics } from '../../common/types/dataPoints'; import type { Line } from '../../common/types/lines'; @@ -31,10 +27,7 @@ export const convertToSpeedDataset = (data: DeliveredTripMetrics[]) => { label: `% of peak`, data: data.map((datapoint) => datapoint.miles_covered - ? round( - (100 * datapoint.miles_covered) / (datapoint.total_time / 3600) / PEAK_SPEED[line], - 1 - ) + ? round((100 * datapoint.miles_covered) / (datapoint.total_time / 3600) / PEAK_MPH[line], 1) : Number.NaN ), }; diff --git a/modules/service/utils/graphUtils.ts b/modules/service/utils/graphUtils.ts index 4faa88df7..b2c2afe92 100644 --- a/modules/service/utils/graphUtils.ts +++ b/modules/service/utils/graphUtils.ts @@ -9,9 +9,9 @@ const shuttlingAnnotationBlockStyle = { backgroundColor: CHART_COLORS.BLOCKS, borderWidth: 0, label: { - content: 'No data', + content: 'Service Disruption', rotation: -90, - color: 'white', + color: '#505050', display: true, }, }; diff --git a/modules/speed/DelaysChart.tsx b/modules/speed/DelaysChart.tsx new file mode 100644 index 000000000..7bf4ab75e --- /dev/null +++ b/modules/speed/DelaysChart.tsx @@ -0,0 +1,182 @@ +import React, { useRef } from 'react'; +import { round } from 'lodash'; +import { Line } from 'react-chartjs-2'; + +import 'chartjs-adapter-date-fns'; +import { enUS } from 'date-fns/locale'; + +import ChartjsPluginWatermark from 'chartjs-plugin-watermark'; +import { useDelimitatedRoute } from '../../common/utils/router'; +import { COLORS, LINE_COLORS } from '../../common/constants/colors'; +import type { DeliveredTripMetrics } from '../../common/types/dataPoints'; +import { drawSimpleTitle } from '../../common/components/charts/Title'; +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 { PEAK_MPH } from '../../common/constants/baselines'; +import { getShuttlingBlockAnnotations } from '../service/utils/graphUtils'; +import type { ParamsType } from './constants/speeds'; + +interface TripTimeIncreaseChartProps { + data: DeliveredTripMetrics[]; + config: ParamsType; + startDate: string; + endDate: string; + showTitle?: boolean; +} + +export const DelaysChart: React.FC = ({ + data, + config, + startDate, + endDate, + showTitle = false, +}) => { + const { line, linePath } = useDelimitatedRoute(); + const { tooltipFormat, unit, callbacks } = config; + const peak = PEAK_MPH[line ?? 'DEFAULT']; + const ref = useRef(); + const isMobile = !useBreakpoint('md'); + const labels = data.map((point) => point.date); + const shuttlingBlocks = getShuttlingBlockAnnotations(data); + + return ( + + + { + const mph = round(datapoint.miles_covered / (datapoint.total_time / 3600), 1); + return (100 * ((PEAK_MPH[line ?? 'DEFAULT'] - mph) / mph)).toFixed(1); + }), + }, + ], + }} + options={{ + responsive: true, + maintainAspectRatio: false, + layout: { + padding: { + top: showTitle ? 25 : 0, + }, + }, + interaction: { + intersect: false, + }, + // @ts-expect-error The watermark plugin doesn't have typescript support + watermark: watermarkLayout(isMobile), + plugins: { + tooltip: { + mode: 'index', + position: 'nearest', + callbacks: { + ...callbacks, + label: (context) => { + return `Trips are ${context.parsed.y}% ${ + context.parsed.y >= 0 ? 'longer' : 'shorter' + } than peak`; + }, + }, + }, + legend: { + position: 'bottom', + labels: { + boxWidth: 15, + }, + }, + title: { + // empty title to set font and leave room for drawTitle fn + display: showTitle, + text: '', + }, + annotation: { + // Add your annotations here + annotations: [...shuttlingBlocks], + }, + }, + scales: { + y: { + suggestedMin: 0, + suggestedMax: PEAK_MPH[line ?? 'DEFAULT'], + display: true, + ticks: { + color: COLORS.design.subtitleGrey, + }, + title: { + display: true, + text: '% delay', + color: COLORS.design.subtitleGrey, + }, + }, + x: { + min: startDate, + max: endDate, + type: 'time', + time: { + unit: unit, + tooltipFormat: tooltipFormat, + displayFormats: { + month: 'MMM', + }, + }, + ticks: { + color: COLORS.design.subtitleGrey, + }, + adapters: { + date: { + locale: enUS, + }, + }, + display: true, + title: { + display: false, + text: ``, + }, + }, + }, + }} + plugins={[ + { + id: 'customTitle', + afterDraw: (chart) => { + if (!data) { + // No data is present + const { ctx } = chart; + const { width } = chart; + const { height } = chart; + chart.clear(); + + ctx.save(); + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.font = "16px normal 'Helvetica Nueue'"; + ctx.fillText('No data to display', width / 2, height / 2); + ctx.restore(); + } + if (showTitle) drawSimpleTitle(`Median Speed`, chart); + }, + }, + ChartjsPluginWatermark, + ]} + /> + + + ); +}; diff --git a/modules/speed/DelaysChartWrapper.tsx b/modules/speed/DelaysChartWrapper.tsx new file mode 100644 index 000000000..e10d35a41 --- /dev/null +++ b/modules/speed/DelaysChartWrapper.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { CarouselGraphDiv } from '../../common/components/charts/CarouselGraphDiv'; +import type { DeliveredTripMetrics } from '../../common/types/dataPoints'; +import { NoDataNotice } from '../../common/components/notices/NoDataNotice'; +import { getDetailsSpeedWidgetValues } from './utils/utils'; +import type { ParamsType } from './constants/speeds'; +import { DelaysChart } from './DelaysChart'; + +interface DelaysChartWrapperProps { + data: DeliveredTripMetrics[]; + config: ParamsType; + startDate: string; + endDate: string; +} + +export const DelaysChartWrapper: React.FC = ({ + data, + config, + startDate, + endDate, +}) => { + const dataNoNulls = data.filter((datapoint) => datapoint.miles_covered); + if (dataNoNulls.length < 1) return ; + const { current, delta, average, peak } = getDetailsSpeedWidgetValues(dataNoNulls); + return ( + + {/* + + + + */} + + + ); +}; diff --git a/modules/speed/SpeedDetailsWrapper.tsx b/modules/speed/SpeedDetailsWrapper.tsx index cb5d9d26d..c41994c9c 100644 --- a/modules/speed/SpeedDetailsWrapper.tsx +++ b/modules/speed/SpeedDetailsWrapper.tsx @@ -4,6 +4,7 @@ import { WidgetDiv } from '../../common/components/widgets/WidgetDiv'; import { WidgetTitle } from '../dashboard/WidgetTitle'; import type { ParamsType } from './constants/speeds'; import { SpeedGraphWrapper } from './SpeedGraphWrapper'; +import { DelaysChartWrapper } from './DelaysChartWrapper'; interface SpeedDetailsWrapperProps { data: DeliveredTripMetrics[]; @@ -21,9 +22,13 @@ export const SpeedDetailsWrapper: React.FC = ({ return ( <> - + + + + + ); }; diff --git a/modules/speed/SpeedGraph.tsx b/modules/speed/SpeedGraph.tsx index 9d7dc7e96..4515d4329 100644 --- a/modules/speed/SpeedGraph.tsx +++ b/modules/speed/SpeedGraph.tsx @@ -13,9 +13,8 @@ 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 { PEAK_SPEED } from '../../common/constants/baselines'; +import { PEAK_MPH } from '../../common/constants/baselines'; import { getShuttlingBlockAnnotations } from '../service/utils/graphUtils'; -import { PEAK_MPH } from './constants/speeds'; import type { ParamsType } from './constants/speeds'; interface SpeedGraphProps { @@ -35,7 +34,7 @@ export const SpeedGraph: React.FC = ({ }) => { const { line, linePath } = useDelimitatedRoute(); const { tooltipFormat, unit, callbacks } = config; - const peak = PEAK_SPEED[line ?? 'DEFAULT']; + const peak = PEAK_MPH[line ?? 'DEFAULT']; const ref = useRef(); const isMobile = !useBreakpoint('md'); const labels = data.map((point) => point.date); @@ -94,7 +93,7 @@ export const SpeedGraph: React.FC = ({ callbacks: { ...callbacks, label: (context) => { - return `${context.parsed.y} (${((100 * context.parsed.y) / peak).toFixed( + return `${context.parsed.y} mph (${((100 * context.parsed.y) / peak).toFixed( 1 )}% of peak)`; }, @@ -186,7 +185,7 @@ export const SpeedGraph: React.FC = ({ ctx.fillText('No data to display', width / 2, height / 2); ctx.restore(); } - if (showTitle) drawSimpleTitle(`Median Speed`, chart); + if (showTitle) drawSimpleTitle(`Speed`, chart); }, }, ChartjsPluginWatermark, diff --git a/modules/speed/constants/speeds.ts b/modules/speed/constants/speeds.ts index 7d13c18c7..6a29794c0 100644 --- a/modules/speed/constants/speeds.ts +++ b/modules/speed/constants/speeds.ts @@ -2,7 +2,6 @@ import type { TooltipCallbacks, TooltipItem, TooltipModel } from 'chart.js'; import type { _DeepPartialObject } from 'chart.js/dist/types/utils'; import dayjs from 'dayjs'; import { todayOrDate } from '../../../common/constants/dates'; -import { PEAK_COMPLETE_TRIP_TIMES } from '../../../common/constants/baselines'; export type AggType = 'daily' | 'weekly' | 'monthly'; export type ParamsType = { @@ -76,12 +75,3 @@ export const CORE_TRACK_LENGTHS = { 'line-blue': 5.38 + 5.37, // Revere> + DEFAULT: 1, }; - -export const PEAK_MPH = { - 'line-red': CORE_TRACK_LENGTHS['line-red'] / (PEAK_COMPLETE_TRIP_TIMES['line-red'].value / 3600), - 'line-orange': - CORE_TRACK_LENGTHS['line-orange'] / (PEAK_COMPLETE_TRIP_TIMES['line-orange'].value / 3600), - 'line-blue': - CORE_TRACK_LENGTHS['line-blue'] / (PEAK_COMPLETE_TRIP_TIMES['line-blue'].value / 3600), - DEFAULT: 1, -};