From 0a032ffb4bbe59cd0aa40f664ecdd445da1a0d15 Mon Sep 17 00:00:00 2001 From: Devin Matte Date: Mon, 3 Jul 2023 18:49:22 -0400 Subject: [PATCH 1/5] Updating time labels to be consistent across the app --- .../components/charts/AggregateLineChart.tsx | 6 +- .../components/charts/SingleDayLineChart.tsx | 6 +- common/utils/time.tsx | 79 +++++++------------ modules/slowzones/charts/TotalSlowTime.tsx | 11 +++ modules/slowzones/map/SlowSegmentLabel.tsx | 12 +-- 5 files changed, 51 insertions(+), 63 deletions(-) diff --git a/common/components/charts/AggregateLineChart.tsx b/common/components/charts/AggregateLineChart.tsx index 921af2681..4a8c90bec 100644 --- a/common/components/charts/AggregateLineChart.tsx +++ b/common/components/charts/AggregateLineChart.tsx @@ -12,6 +12,7 @@ import { DownloadButton } from '../general/DownloadButton'; import { useBreakpoint } from '../../hooks/useBreakpoint'; import { watermarkLayout } from '../../constants/charts'; import { writeError } from '../../utils/chartError'; +import { getFormattedTimeString } from '../../utils/time'; import { LegendLongTerm } from './Legend'; import { ChartBorder } from './ChartBorder'; import { ChartDiv } from './ChartDiv'; @@ -146,7 +147,10 @@ export const AggregateLineChart: React.FC = ({ position: 'nearest', callbacks: { label: (tooltipItem) => { - return `${tooltipItem.dataset.label}: ${tooltipItem.parsed.y} minutes`; + return `${tooltipItem.dataset.label}: ${getFormattedTimeString( + tooltipItem.parsed.y, + 'minutes' + )}`; }, }, }, diff --git a/common/components/charts/SingleDayLineChart.tsx b/common/components/charts/SingleDayLineChart.tsx index 775609047..71c8af8da 100644 --- a/common/components/charts/SingleDayLineChart.tsx +++ b/common/components/charts/SingleDayLineChart.tsx @@ -13,6 +13,7 @@ import { DownloadButton } from '../general/DownloadButton'; import { useBreakpoint } from '../../hooks/useBreakpoint'; import { watermarkLayout } from '../../constants/charts'; import { writeError } from '../../utils/chartError'; +import { getFormattedTimeString } from '../../utils/time'; import { Legend as LegendView } from './Legend'; import { ChartDiv } from './ChartDiv'; import { ChartBorder } from './ChartBorder'; @@ -128,7 +129,10 @@ export const SingleDayLineChart: React.FC = ({ ) { return ''; } - return `${tooltipItem.dataset.label}: ${tooltipItem.parsed.y} minutes`; + return `${tooltipItem.dataset.label}: ${getFormattedTimeString( + tooltipItem.parsed.y, + 'minutes' + )}`; }, afterBody: (tooltipItems) => { return departureFromNormalString( diff --git a/common/utils/time.tsx b/common/utils/time.tsx index 9cb712a6b..45eb52870 100644 --- a/common/utils/time.tsx +++ b/common/utils/time.tsx @@ -1,56 +1,9 @@ import React from 'react'; import dayjs from 'dayjs'; +import type { DurationUnitType } from 'dayjs/plugin/duration'; import { WidgetText } from '../components/widgets/internal/WidgetText'; import { UnitText } from '../components/widgets/internal/UnitText'; -type StringifyTimeOptions = { - truncateLeadingHoursZeros?: boolean; - truncateLeadingMinutesZeros?: boolean; - showSeconds?: boolean; - showHours?: boolean; - use12Hour?: boolean; -}; - -export const stringifyTime = (totalSeconds: number, options: StringifyTimeOptions = {}): string => { - const { - truncateLeadingHoursZeros = true, - truncateLeadingMinutesZeros = false, - showSeconds = false, - showHours = true, - use12Hour = false, - } = options; - let seconds = Math.round(totalSeconds), - minutes = 0, - hours = 0; - const minutesToAdd = Math.floor(seconds / 60); - seconds = seconds % 60; - minutes = minutes += minutesToAdd; - const hoursToAdd = Math.floor(minutes / 60); - minutes = minutes % 60; - hours += hoursToAdd; - const isPM = hours >= 12 && hours < 24; - hours = (use12Hour && hours > 12 ? hours - 12 : hours) % 24; - // We never reassign to secondString but it's nice to destructure this way - // eslint-disable-next-line prefer-const - let [hoursString, minutesString, secondsString] = [hours, minutes, seconds].map((num) => - num.toString().padStart(2, '0') - ); - if (truncateLeadingHoursZeros && hoursString.startsWith('0')) { - hoursString = hoursString.slice(1); - } - if (truncateLeadingMinutesZeros && minutesString.startsWith('0')) { - minutesString = minutesString.slice(1); - } - const timeString = [hoursString, minutesString, secondsString] - .slice(showHours ? 0 : 1) - .slice(0, showSeconds ? 3 : 2) - .join(':'); - if (use12Hour) { - return `${timeString} ${isPM ? 'PM' : 'AM'}`; - } - return timeString; -}; - export const getTimeUnit = (value: number) => { const secondsAbs = Math.abs(value); switch (true) { @@ -64,7 +17,7 @@ export const getTimeUnit = (value: number) => { }; export const getFormattedTimeValue = (value: number) => { - const absValue = Math.abs(value); + const absValue = Math.round(Math.abs(value)); const duration = dayjs.duration(absValue, 'seconds'); switch (true) { case absValue < 100: @@ -78,7 +31,7 @@ export const getFormattedTimeValue = (value: number) => { return (

- +

); @@ -86,9 +39,33 @@ export const getFormattedTimeValue = (value: number) => { return (

- +

); } }; + +export const getFormattedTimeString = (value: number, unit: DurationUnitType = 'seconds') => { + if (unit === 'seconds') { + const absValue = Math.round(Math.abs(value)); + const duration = dayjs.duration(absValue, unit); + switch (true) { + case absValue < 100: + return `${absValue}s`; + case absValue < 3600: + return `${duration.format('m')}m ${duration.format('s').padStart(2, '0')}s`; + default: + return `${duration.format('H')}h ${duration.format('m').padStart(2, '0')}m`; + } + } else if (unit === 'minutes') { + const absValue = Math.abs(value); + const duration = dayjs.duration(absValue, unit); + switch (true) { + case absValue < 60: + return `${duration.format('m')}m ${duration.format('s').padStart(2, '0')}s`; + default: + return `${duration.format('H')}h ${duration.format('m').padStart(2, '0')}m`; + } + } +}; diff --git a/modules/slowzones/charts/TotalSlowTime.tsx b/modules/slowzones/charts/TotalSlowTime.tsx index 9dece13de..8d3596c9d 100644 --- a/modules/slowzones/charts/TotalSlowTime.tsx +++ b/modules/slowzones/charts/TotalSlowTime.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 { getFormattedTimeString } from '../../../common/utils/time'; interface TotalSlowTimeProps { // Data is always all data. We filter it by adjusting the X axis of the graph. @@ -145,6 +146,16 @@ export const TotalSlowTime: React.FC = ({ plugins: { tooltip: { intersect: false, + mode: 'index', + position: 'nearest', + callbacks: { + label: (tooltipItem) => { + return `${tooltipItem.dataset.label}: ${getFormattedTimeString( + tooltipItem.parsed.y, + 'minutes' + )}`; + }, + }, }, title: { // empty title to set font and leave room for drawTitle fn diff --git a/modules/slowzones/map/SlowSegmentLabel.tsx b/modules/slowzones/map/SlowSegmentLabel.tsx index 69505ab81..5441f8224 100644 --- a/modules/slowzones/map/SlowSegmentLabel.tsx +++ b/modules/slowzones/map/SlowSegmentLabel.tsx @@ -2,8 +2,8 @@ import React, { useMemo } from 'react'; import type { SlowZoneResponse } from '../../../common/types/dataPoints'; import type { LineMetadata } from '../../../common/types/lines'; -import { stringifyTime } from '../../../common/utils/time'; +import { getFormattedTimeString } from '../../../common/utils/time'; import type { SlowZoneDirection, SlowZonesSegment } from './segment'; import { DIRECTIONS } from './segment'; @@ -23,15 +23,7 @@ const SlowZoneLabel: React.FC = ({ color, slowZone: { delay, baseline }, }) => { - const delayString = useMemo( - () => - stringifyTime(delay, { - showHours: false, - showSeconds: true, - truncateLeadingMinutesZeros: true, - }), - [delay] - ); + const delayString = useMemo(() => getFormattedTimeString(delay), [delay]); const fractionOverBaseline = -1 + (delay + baseline) / baseline; From ff53329ff79c2eb00b3fcee36f5e475fc2ac3029 Mon Sep 17 00:00:00 2001 From: Devin Matte Date: Mon, 3 Jul 2023 19:04:13 -0400 Subject: [PATCH 2/5] Handle less than 1 minute --- common/utils/time.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common/utils/time.tsx b/common/utils/time.tsx index 45eb52870..396bfb501 100644 --- a/common/utils/time.tsx +++ b/common/utils/time.tsx @@ -62,6 +62,8 @@ export const getFormattedTimeString = (value: number, unit: DurationUnitType = ' const absValue = Math.abs(value); const duration = dayjs.duration(absValue, unit); switch (true) { + case absValue < 1: + return `${duration.format('s').padStart(2, '0')}s`; case absValue < 60: return `${duration.format('m')}m ${duration.format('s').padStart(2, '0')}s`; default: From 0ef69031eb4fa86005efb7abffb68ff79ac4d41b Mon Sep 17 00:00:00 2001 From: Devin Matte Date: Mon, 3 Jul 2023 19:05:15 -0400 Subject: [PATCH 3/5] Remove padding --- common/utils/time.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/utils/time.tsx b/common/utils/time.tsx index 396bfb501..23fe11c73 100644 --- a/common/utils/time.tsx +++ b/common/utils/time.tsx @@ -63,7 +63,7 @@ export const getFormattedTimeString = (value: number, unit: DurationUnitType = ' const duration = dayjs.duration(absValue, unit); switch (true) { case absValue < 1: - return `${duration.format('s').padStart(2, '0')}s`; + return `${duration.format('s')}s`; case absValue < 60: return `${duration.format('m')}m ${duration.format('s').padStart(2, '0')}s`; default: From 618a18bc968c2a39b36852ccf60c5933d46efd48 Mon Sep 17 00:00:00 2001 From: Devin Matte Date: Mon, 3 Jul 2023 19:09:13 -0400 Subject: [PATCH 4/5] Fix lining wrapping with space --- modules/slowzones/map/SlowSegmentLabel.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/slowzones/map/SlowSegmentLabel.tsx b/modules/slowzones/map/SlowSegmentLabel.tsx index 5441f8224..3fef2ed95 100644 --- a/modules/slowzones/map/SlowSegmentLabel.tsx +++ b/modules/slowzones/map/SlowSegmentLabel.tsx @@ -32,6 +32,7 @@ const SlowZoneLabel: React.FC = ({ style={{ flexDirection: isHorizontal && direction === '0' ? 'row-reverse' : 'row', fontWeight: fractionOverBaseline >= 0.5 ? 'bold' : 'normal', + whiteSpace: 'nowrap', }} className={styles.slowZoneLabel} > From ca389843d60ceda47883d523ddb6590350f302d2 Mon Sep 17 00:00:00 2001 From: Patrick Cleary Date: Thu, 6 Jul 2023 11:50:25 -0400 Subject: [PATCH 5/5] rounding bug (#716) * rounding bug * whoops --- common/utils/time.tsx | 35 +++++++++++------------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/common/utils/time.tsx b/common/utils/time.tsx index 23fe11c73..868f21c26 100644 --- a/common/utils/time.tsx +++ b/common/utils/time.tsx @@ -1,6 +1,5 @@ import React from 'react'; import dayjs from 'dayjs'; -import type { DurationUnitType } from 'dayjs/plugin/duration'; import { WidgetText } from '../components/widgets/internal/WidgetText'; import { UnitText } from '../components/widgets/internal/UnitText'; @@ -46,28 +45,16 @@ export const getFormattedTimeValue = (value: number) => { } }; -export const getFormattedTimeString = (value: number, unit: DurationUnitType = 'seconds') => { - if (unit === 'seconds') { - const absValue = Math.round(Math.abs(value)); - const duration = dayjs.duration(absValue, unit); - switch (true) { - case absValue < 100: - return `${absValue}s`; - case absValue < 3600: - return `${duration.format('m')}m ${duration.format('s').padStart(2, '0')}s`; - default: - return `${duration.format('H')}h ${duration.format('m').padStart(2, '0')}m`; - } - } else if (unit === 'minutes') { - const absValue = Math.abs(value); - const duration = dayjs.duration(absValue, unit); - switch (true) { - case absValue < 1: - return `${duration.format('s')}s`; - case absValue < 60: - return `${duration.format('m')}m ${duration.format('s').padStart(2, '0')}s`; - default: - return `${duration.format('H')}h ${duration.format('m').padStart(2, '0')}m`; - } +export const getFormattedTimeString = (value: number, unit: 'minutes' | 'seconds' = 'seconds') => { + const secondsValue = unit === 'seconds' ? value : value * 60; + const absValue = Math.round(Math.abs(secondsValue)); + const duration = dayjs.duration(absValue, 'seconds'); + switch (true) { + case absValue < 100: + return `${absValue}s`; + case absValue < 3600: + return `${duration.format('m')}m ${duration.format('s').padStart(2, '0')}s`; + default: + return `${duration.format('H')}h ${duration.format('m').padStart(2, '0')}m`; } };