From 598314473ee9ccbc1e9f8ff0a5a86f01b220e86a Mon Sep 17 00:00:00 2001 From: Ian Reynolds Date: Sat, 15 Jul 2023 22:06:12 -0400 Subject: [PATCH] fix: SZ segment labels render properly in Safari --- common/components/maps/LineMap.stories.tsx | 4 +- common/components/maps/LineMap.tsx | 70 +++++------- .../components/maps/useDiagramCoordinates.ts | 4 +- common/utils/time.tsx | 46 ++++++++ modules/slowzones/map/DirectionIndicator.tsx | 2 +- modules/slowzones/map/SlowSegmentLabel.tsx | 102 +++++++++++++----- modules/slowzones/map/SlowZonesMap.tsx | 9 +- 7 files changed, 162 insertions(+), 75 deletions(-) diff --git a/common/components/maps/LineMap.stories.tsx b/common/components/maps/LineMap.stories.tsx index d8226dedd..3c1db63fe 100644 --- a/common/components/maps/LineMap.stories.tsx +++ b/common/components/maps/LineMap.stories.tsx @@ -26,7 +26,7 @@ const redLineSegments: SegmentRenderOptions[] = [ { mapSide: '0', boundingSize: 40, - content: ( + content: () => (
Greetings amigos thank you for inviting me into your SVG
@@ -35,7 +35,7 @@ const redLineSegments: SegmentRenderOptions[] = [ { mapSide: '1', boundingSize: 40, - content: ( + content: () => (
And on this side too! I also like being on this side!
), }, diff --git a/common/components/maps/LineMap.tsx b/common/components/maps/LineMap.tsx index 99a7258e3..a59bded2c 100644 --- a/common/components/maps/LineMap.tsx +++ b/common/components/maps/LineMap.tsx @@ -24,7 +24,7 @@ export type SegmentLabel = { mapSide: MapSide; boundingSize?: number; offset?: { x: number; y: number }; - content: React.ReactNode; + content: (size: { width: number; height: number }) => React.ReactNode; }; export type SegmentRenderOptions = { @@ -33,12 +33,12 @@ export type SegmentRenderOptions = { labels?: SegmentLabel[]; }; -export type TooltipRenderer = (props: { +type TooltipRenderer = (props: { segmentLocation: SegmentLocation; isHorizontal: boolean; }) => React.ReactNode; -export type TooltipOptions = { +type TooltipOptions = { render: TooltipRenderer; snapToSegment?: boolean; maxDistance?: number; @@ -64,7 +64,7 @@ const getPropsForStrokeOptions = (options: Partial) => { }; }; -const getLabelPositionProps = ( +const getSegmentLabelBounds = ( segmentBounds: Rect, segmentLabel: SegmentLabel, isHorizontal: boolean @@ -75,32 +75,19 @@ const getLabelPositionProps = ( if (isHorizontal) { const moveAcross = mapSide === '0'; return { - foreignObjectProps: { - x: top + offset.x, - y: 0 - left - (moveAcross ? boundingSize + width : 0) + offset.y, - width: height, - height: boundingSize, - style: { transform: 'rotate(90deg)' }, - }, - wrapperDivStyles: { - flexDirection: 'row', - alignItems: moveAcross ? 'flex-end' : 'flex-start', - }, + x: top + offset.x, + y: 0 - left - (moveAcross ? boundingSize + width : 0) + offset.y, + width: height, + height: boundingSize, } as const; } const moveAcross = mapSide === '1'; return { - foreignObjectProps: { - x: right - (moveAcross ? boundingSize + width : 0) + offset.x, - y: top + offset.y, - height, - width: boundingSize, - }, - wrapperDivStyles: { - flexDirection: 'column', - alignItems: moveAcross ? 'flex-end' : 'flex-start', - }, - } as const; + x: right - (moveAcross ? boundingSize + width : 0) + offset.x, + y: top + offset.y, + height, + width: boundingSize, + }; }; export const LineMap: React.FC = ({ @@ -173,27 +160,15 @@ export const LineMap: React.FC = ({ }); const computedLabels = labels.map((label, index) => { - const { foreignObjectProps, wrapperDivStyles } = getLabelPositionProps( - bounds, - label, - isHorizontal - ); + const { x, y, width, height } = getSegmentLabelBounds(bounds, label, isHorizontal); return ( - -
- {label.content} -
-
+ + {label.content({ width, height })} + ); }); @@ -263,7 +238,12 @@ export const LineMap: React.FC = ({ }; const renderComputedLabels = () => { - return computedSegmentExtras.map((segment) => segment.computedLabels).flat(); + const transform = isHorizontal ? 'rotate(90)' : undefined; + return ( + + {computedSegmentExtras.map((segment) => segment.computedLabels).flat()} + + ); }; const renderTooltip = () => { diff --git a/common/components/maps/useDiagramCoordinates.ts b/common/components/maps/useDiagramCoordinates.ts index be1b4c5ff..e4be4fb51 100644 --- a/common/components/maps/useDiagramCoordinates.ts +++ b/common/components/maps/useDiagramCoordinates.ts @@ -65,8 +65,8 @@ export const useDiagramCoordinates = (options: Options) => { const scaleBasis = getScaleBasis({ width: viewportWidth, height: viewportHeight }); setSvgProps({ viewBox: `${x} ${y} ${width} ${height}`, - width: width * scaleBasis, - height: height * scaleBasis, + width: Math.round(width * scaleBasis), + height: Math.round(height * scaleBasis), }); } } diff --git a/common/utils/time.tsx b/common/utils/time.tsx index 7f6bb1bbf..00be322c4 100644 --- a/common/utils/time.tsx +++ b/common/utils/time.tsx @@ -61,3 +61,49 @@ export const getFormattedTimeString = (value: number, unit: 'minutes' | 'seconds return `${duration.format('H')}h ${duration.format('m').padStart(2, '0')}m`; } }; + +interface GetClockFormattedTimeStringOptions { + truncateLeadingZeros?: boolean; + showSeconds?: boolean; + showHours?: boolean; + use12Hour?: boolean; +} + +export const getClockFormattedTimeString = ( + time: number, + options: GetClockFormattedTimeStringOptions = {} +): string => { + time = Math.round(time); + const { + truncateLeadingZeros = true, + showSeconds = false, + showHours = true, + use12Hour = false, + } = options; + let seconds = time, + 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; + // eslint-disable-next-line prefer-const + let [hoursString, minutesString, secondsString] = [hours, minutes, seconds].map((num) => + num.toString().padStart(2, '0') + ); + let timeString = [hoursString, minutesString, secondsString] + .slice(showHours ? 0 : 1) + .slice(0, showSeconds ? 3 : 2) + .join(':'); + if (truncateLeadingZeros && timeString.startsWith('0')) { + timeString = timeString.slice(1); + } + if (use12Hour) { + return `${timeString} ${isPM ? 'PM' : 'AM'}`; + } + return timeString; +}; diff --git a/modules/slowzones/map/DirectionIndicator.tsx b/modules/slowzones/map/DirectionIndicator.tsx index c749ce0ee..5c2b00430 100644 --- a/modules/slowzones/map/DirectionIndicator.tsx +++ b/modules/slowzones/map/DirectionIndicator.tsx @@ -19,7 +19,7 @@ export const DirectionIndicator: React.FC = ({ }) => { const rotation = isHorizontal ? (direction === '1' ? 90 : 270) : direction === '1' ? 180 : 0; return ( -
= ({ direction, - isHorizontal, color, + containingWidth, + offset, + isHorizontal, slowZone: { delay, baseline }, }) => { - const delayString = useMemo(() => getFormattedTimeString(delay), [delay]); - + const delayString = useMemo( + () => + getClockFormattedTimeString(delay, { + showHours: false, + showSeconds: true, + truncateLeadingZeros: true, + }), + [delay] + ); + const indicatorBeforeText = direction === '1'; + const indicatorSolidArrow = indicatorBeforeText + ? isHorizontal + ? '❮' + : '▲' + : isHorizontal + ? '❯' + : '▼'; const fractionOverBaseline = -1 + (delay + baseline) / baseline; + const isBold = fractionOverBaseline >= 0.5; + + const indicator = ( + + {indicatorSolidArrow} + + ); + + const delayText = {delayString}; return ( -
= 0.5 ? 'bold' : 'normal', - whiteSpace: 'nowrap', - }} - className={styles.slowZoneLabel} - > - - +{delayString} -
+ + {indicatorBeforeText ? <>{indicator} : null} + {delayText} + {!indicatorBeforeText ? <> {indicator} : null} + ); }; @@ -51,16 +71,48 @@ interface SlowSegmentLabelProps { segment: SlowZonesSegment; line: LineMetadata; isHorizontal: boolean; + width: number; + height: number; } +const getDirectionLabelOffsets = (slowZones: ByDirection, height: number) => { + const hasZero = slowZones['0'].length > 0; + const hasOne = slowZones['1'].length > 0; + const isBidi = hasZero && hasOne; + const midline = height / 2; + if (isBidi) { + const bidiOffset = LABEL_INNER_PADDING + LABEL_HEIGHT; + return { + '0': midline + bidiOffset / 2, + '1': midline - bidiOffset / 2, + }; + } + if (hasZero) { + return { + '0': midline, + }; + } + if (hasOne) { + return { + '1': midline, + }; + } + return {}; +}; + export const SlowSegmentLabel: React.FC = (props) => { const { isHorizontal, segment: { slowZones }, line, + width, + height, } = props; + + const offsets = getDirectionLabelOffsets(slowZones, height); + return ( -
+ {DIRECTIONS.map((direction) => { const [zone] = slowZones[direction]; if (!zone) { @@ -73,9 +125,11 @@ export const SlowSegmentLabel: React.FC = (props) => { slowZone={zone} color={line.color} isHorizontal={isHorizontal} + offset={offsets[direction]!} + containingWidth={width} /> ); })} -
+ ); }; diff --git a/modules/slowzones/map/SlowZonesMap.tsx b/modules/slowzones/map/SlowZonesMap.tsx index 98d01eb13..56c6e5cb9 100644 --- a/modules/slowzones/map/SlowZonesMap.tsx +++ b/modules/slowzones/map/SlowZonesMap.tsx @@ -94,7 +94,14 @@ export const SlowZonesMap: React.FC = ({ mapSide: '0' as const, boundingSize: isHorizontal ? 15 : 20, ...getSegmentLabelOverrides(segment.segmentLocation, isHorizontal), - content: , + content: (size) => ( + + ), }, ], strokes: Object.entries(segment.slowZones).map(([direction, zones]) => {