From 68997c31333190a7c36af50754cc58f158aa7077 Mon Sep 17 00:00:00 2001 From: prestoncraw Date: Fri, 25 Sep 2026 15:08:16 -0400 Subject: [PATCH 1/2] Show resolved auto time unit and full timestamps in tooltips - Time unit selector shows the unit auto resolves to (e.g. "seconds (auto)"), matching the axis unit selectors - Tooltip and Tooltip w/ Delta show the time unit under the truncated value and the full timestamp below it - Centralize the auto ms/s rule in getAutoTimeUnit/resolveTimeUnit and use it in the x-axis ticks, axis label, time deltas, and selector - Remove unused time variables from AccumulatedPoints --- .../TSX/Graphs/LineChart/Renderers/XAxes.ts | 17 +---- .../Scripts/TSX/Graphs/Utils/Utilities.tsx | 74 +++++++++---------- .../Scripts/TSX/Widgets/AccumulatedPoints.tsx | 9 +-- .../Widgets/PlotSettings/SettingWindow.tsx | 1 + .../Widgets/PlotSettings/TimeUnitSelector.tsx | 28 +++++-- .../wwwroot/Scripts/TSX/Widgets/Tooltip.tsx | 6 +- .../Scripts/TSX/Widgets/TooltipWithDelta.tsx | 10 ++- 7 files changed, 72 insertions(+), 73 deletions(-) diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart/Renderers/XAxes.ts b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart/Renderers/XAxes.ts index 7965ce28..b4c5c8c4 100644 --- a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart/Renderers/XAxes.ts +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/LineChart/Renderers/XAxes.ts @@ -24,7 +24,7 @@ import * as d3 from "d3"; import { OpenSee } from "../../../global"; -import { formatTimeTick } from "../../Utils/Utilities"; +import { formatTimeTick, getAutoTimeUnit, resolveTimeUnit } from "../../Utils/Utilities"; import { IFormatTimeContext } from "../../Utils/Types"; export function createXAxis( @@ -66,22 +66,9 @@ export const updateXAxisLabel = ( const h = xScale != null ? xScale.domain()[1] - xScale.domain()[0] : 100; - let label: string; - if ((timeUnit as OpenSee.IUnitSetting).options?.[timeUnit.current]?.short !== "auto" && !isOverlappingWaveform) { - label = (timeUnit as OpenSee.IUnitSetting).options?.[timeUnit.current]?.short ?? ""; - } else if (isOverlappingWaveform) { - label = h < 100 ? "ms" : "s"; - } else if ((timeUnit as OpenSee.IUnitSetting).options?.[timeUnit.current]?.short === "ms since event") { - label = "ms"; - } else if ((timeUnit as OpenSee.IUnitSetting).options?.[timeUnit.current]?.short === "cycles") { - label = "cycle"; - } else { - label = h < 100 ? "ms" : "s"; - } - d3.select(container) .select(".xAxisLabel") - .text(`Time (${label})`); + .text(`Time (${isOverlappingWaveform ? getAutoTimeUnit(h) : resolveTimeUnit(timeUnit, h)})`); } export const updateXAxisPositionOnResize = (container: HTMLDivElement | null, height: number, width: number) => { diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Utils/Utilities.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Utils/Utilities.tsx index 094fbb70..08baa21a 100644 --- a/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Utils/Utilities.tsx +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Graphs/Utils/Utilities.tsx @@ -83,9 +83,19 @@ export const formatValueTick = (d: number, unit: OpenSee.Unit, yScaleCollection: return d.toFixed(2) }; +// The only place that decides what the "auto" time unit means: ms while less than 100 ms is visible, otherwise s. +export const getAutoTimeUnit = (timeSpanMs: number): 'ms' | 's' => timeSpanMs < 100 ? 'ms' : 's'; + +// Short name of the time unit actually displayed, with "auto" replaced by the unit it resolves to. +export const resolveTimeUnit = (timeUnit: OpenSee.IUnitSetting, timeSpanMs: number): string => { + const short = timeUnit.options?.[timeUnit.current]?.short ?? 'auto'; + return short === 'auto' ? getAutoTimeUnit(timeSpanMs) : short; +}; + export const formatTimeTick = (d: number, ctx: IFormatTimeContext, extraPrecision = false): string => { const TS = moment(d); - let h = ctx.xDomainWidth; + const timeSpanMs = ctx.xDomainWidth; + const short = resolveTimeUnit(ctx.timeUnit, timeSpanMs); // moment/JS Date only resolve to whole milliseconds, so when extra precision is requested the // sub-second portion is taken from the raw value (which keeps sub-ms detail) instead of moment. const extra = extraPrecision ? 1 : 0; @@ -93,52 +103,44 @@ export const formatTimeTick = (d: number, ctx: IFormatTimeContext, extraPrecisio if (ctx.isOverlappingWaveform) { if (defaultSettings.OverlappingWaveTimeUnit.options?.[ctx.overlappingWaveTimeUnit]?.short === "ms") { - if (h < 2) + if (timeSpanMs < 2) return d.toFixed(3 + extra) - if (h < 5) + if (timeSpanMs < 5) return d.toFixed(2 + extra) else return d.toFixed(1 + extra) } else if (defaultSettings.OverlappingWaveTimeUnit.options?.[ctx.overlappingWaveTimeUnit]?.short === "cycles") { const cyc = d * 60.0 / 1000.0; - h = h * 60.0 / 1000.0; - if (h < 2) + const timeSpanCycles = timeSpanMs * 60.0 / 1000.0; + if (timeSpanCycles < 2) return cyc.toFixed(3 + extra) - if (h < 5) + if (timeSpanCycles < 5) return cyc.toFixed(2 + extra) else return cyc.toFixed(1 + extra) } } - else if (ctx.timeUnit.options?.[ctx.timeUnit.current]?.short == 'auto') { - if (h < 100) - return extraPrecision ? subSecondMs.toFixed(3) : TS.format("SSS.S") - else if (h < 1000) - return TS.format("ss.SS") - else - return TS.format("ss.S") - } - else if (ctx.timeUnit.options?.[ctx.timeUnit.current]?.short == 's') { - if (h < 100) + else if (short == 's') { + if (timeSpanMs < 100) return extraPrecision ? ((d % 60000) / 1000).toFixed(5) : TS.format("ss.SSS") - else if (h < 1000) + else if (timeSpanMs < 1000) return TS.format("ss.SS") else return TS.format("ss.S") } - else if (ctx.timeUnit.options?.[ctx.timeUnit.current]?.short == 'ms') + else if (short == 'ms') if (extraPrecision) return subSecondMs.toFixed(3) - else if (h < 100) + else if (timeSpanMs < 100) return TS.format("SSS.S") else return TS.format("SSS") - else if (ctx.timeUnit.options?.[ctx.timeUnit.current]?.short == 'min') + else if (short == 'min') return TS.format("mm:ss") - else if (ctx.timeUnit.options?.[ctx.timeUnit.current]?.short == 'ms since record') { + else if (short == 'ms since record') { let ms = d - ctx.originalStartTime; if (ctx.useRelevantTime && !ctx.isOriginalEvt) { @@ -147,15 +149,15 @@ export const formatTimeTick = (d: number, ctx: IFormatTimeContext, extraPrecisio ms = d - evt?.StartTime } - if (h < 2) + if (timeSpanMs < 2) return ms.toFixed(3 + extra) - if (h < 5) + if (timeSpanMs < 5) return ms.toFixed(2 + extra) else return ms.toFixed(1 + extra) } - else if (ctx.timeUnit.options?.[ctx.timeUnit.current]?.short == 'ms since inception') { + else if (short == 'ms since inception') { let ms = d - ctx.inceptionTime; if (ctx.useRelevantTime && !ctx.isOriginalEvt) { @@ -164,32 +166,32 @@ export const formatTimeTick = (d: number, ctx: IFormatTimeContext, extraPrecisio ms = d - evt?.Inception } - if (h < 2) + if (timeSpanMs < 2) return ms.toFixed(3 + extra) - if (h < 5) + if (timeSpanMs < 5) return ms.toFixed(2 + extra) else return ms.toFixed(1 + extra) } - else if (ctx.timeUnit.options?.[ctx.timeUnit.current]?.short == 'cycles since record') { + else if (short == 'cycles since record') { const cyc = (d - ctx.startTime) * 60.0 / 1000.0; - h = h * 60.0 / 1000.0; - if (h < 2) + const timeSpanCycles = timeSpanMs * 60.0 / 1000.0; + if (timeSpanCycles < 2) return cyc.toFixed(3 + extra) - if (h < 5) + if (timeSpanCycles < 5) return cyc.toFixed(2 + extra) else return cyc.toFixed(1 + extra) } - else if (ctx.timeUnit.options?.[ctx.timeUnit.current]?.short == 'cycles since inception') { + else if (short == 'cycles since inception') { const cyc = (d - ctx.inceptionTime) * 60.0 / 1000.0; - h = h * 60.0 / 1000.0; - if (h < 2) + const timeSpanCycles = timeSpanMs * 60.0 / 1000.0; + if (timeSpanCycles < 2) return cyc.toFixed(3 + extra) - if (h < 5) + if (timeSpanCycles < 5) return cyc.toFixed(2 + extra) else return cyc.toFixed(1 + extra) @@ -227,9 +229,7 @@ export const formatTimeDelta = (deltaMs: number, timeUnit: OpenSee.IUnitSetting, if (isNaN(deltaMs)) return ''; - let short = timeUnit.options?.[timeUnit.current]?.short ?? 'auto'; - if (short === 'auto') - short = domainWidthMS < 100 ? 'ms' : 's'; + const short = resolveTimeUnit(timeUnit, domainWidthMS); if (short === 's') return (deltaMs / 1000).toFixed(7) + ' (s)'; diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/AccumulatedPoints.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/AccumulatedPoints.tsx index 93911118..4189d8c0 100644 --- a/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/AccumulatedPoints.tsx +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/AccumulatedPoints.tsx @@ -30,7 +30,7 @@ import { PlotDataStateContext } from '../Context/PlotDataContext'; import { PlotStateStateContext, PlotStateActionContext } from '../Context/PlotStateContext'; import EventContext from '../Context/EventContext'; import { selectSelectedPoints } from '../PlotSelectors'; -import { formatMainEventTimeTick, formatTimeDelta } from '../Graphs/Utils/Utilities'; +import { formatTimeDelta } from '../Graphs/Utils/Utilities'; import { useGetContainerPosition } from '@gpa-gemstone/helper-functions'; import { Alert } from '@gpa-gemstone/react-interactive'; @@ -42,13 +42,6 @@ const PointWidget = () => { const colors = useAppSelector(SelectColor); const timeUnit = useAppSelector(SelectTimeUnit); - const originalStartTime = new Date(evt.Context.EventInfo?.EventDate + "Z").getTime(); - const inceptionTime = new Date(evt.Context.EventInfo?.InceptionDate + "Z").getTime(); - const timeFormatOpts = { timeUnit, domainWidth: plotState.endTime - plotState.startTime, startTime: plotState.startTime, originalStartTime, inceptionTime }; - - const timeUnitShort = timeUnit.options?.[timeUnit.current]?.short ?? 'auto'; - const displayTimeUnitShort = timeUnitShort === 'auto' ? (plotState.endTime - plotState.startTime < 100 ? 'ms' : 's') : timeUnitShort; - const points = React.useMemo( () => selectSelectedPoints(evt.Context.EventID, plots, plotState.meta), [evt.Context.EventID, plots, plotState.meta] diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/PlotSettings/SettingWindow.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/PlotSettings/SettingWindow.tsx index 4633061a..0a9ed012 100644 --- a/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/PlotSettings/SettingWindow.tsx +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/PlotSettings/SettingWindow.tsx @@ -217,6 +217,7 @@ const SettingsWidget = () => { diff --git a/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/PlotSettings/TimeUnitSelector.tsx b/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/PlotSettings/TimeUnitSelector.tsx index d91bae2f..81dedf25 100644 --- a/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/PlotSettings/TimeUnitSelector.tsx +++ b/src/OpenSEE/wwwroot/Scripts/TSX/Widgets/PlotSettings/TimeUnitSelector.tsx @@ -22,26 +22,38 @@ //****************************************************************************************************** import * as React from 'react'; import { defaultSettings } from '../../defaults'; +import { getAutoTimeUnit } from '../../Graphs/Utils/Utilities'; import { Select } from '@gpa-gemstone/react-forms'; interface IProps { setter: (index: number) => void, timeUnitIndex: number, - overlappingWave?: boolean + overlappingWave?: boolean, + timeSpanMs?: number } const TimeUnitSelector = React.memo((props: IProps) => { - const options = React.useMemo(() => { - const src = props.overlappingWave - ? (defaultSettings.OverlappingWaveTimeUnit.options ?? []) - : (defaultSettings.TimeUnit.options ?? []); - return src.map((option, index) => ({ Label: option.label, Value: index })); - }, [props.overlappingWave]); + const src = React.useMemo(() => props.overlappingWave + ? (defaultSettings.OverlappingWaveTimeUnit.options ?? []) + : (defaultSettings.TimeUnit.options ?? []), [props.overlappingWave]); + + // Like AxisUnitSelector, when auto is selected show the unit it currently resolves to + let autoIndex = -1; + if (src[props.timeUnitIndex]?.short === 'auto' && props.timeSpanMs != null) { + const autoShort = getAutoTimeUnit(props.timeSpanMs); + autoIndex = src.findIndex(option => option.short === autoShort); + } + + const options = React.useMemo(() => + src.map((option, index) => ({ + Label: index === autoIndex ? `${option.label} (auto)` : option.label, + Value: index + })), [src, autoIndex]); return (