fix(dashboards): keep DST fall-back range endpoints and resolve threshold axis from first series by TheodoreSpeaks · Pull Request #8541 · simstudioai/sim · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions apps/sim/lib/charts/annotations.test.ts
38 changes: 32 additions & 6 deletions apps/sim/lib/charts/annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,43 @@ export interface ChartAnnotations {

const BAND_OPACITY = 0.08

function firstAxis(axis: unknown): Record<string, unknown> {
return toRecord(Array.isArray(axis) ? axis[0] : axis)
function firstSeries(option: Record<string, unknown>): Record<string, unknown> {
return toRecord(Array.isArray(option.series) ? option.series[0] : option.series)
}

/** The axis a threshold is measured on; ECharts defaults an unspecified yAxis to a value axis. */
/**
* The axis the first series is plotted on. Like ECharts, `*AxisIndex` wins over `*AxisId`, and ids
* match across string and number.
*/
function seriesAxis(
option: Record<string, unknown>,
key: 'xAxis' | 'yAxis'
): Record<string, unknown> {
const axes = Array.isArray(option[key]) ? (option[key] as unknown[]) : [option[key]]
const series = firstSeries(option)
const id = series[`${key}Id`]
if (series[`${key}Index`] === undefined && id !== undefined) {
const axis = axes.find((candidate) => String(toRecord(candidate).id) === String(id))
if (axis === undefined) throw new Error(`The first series references a missing ${key} "${id}"`)
return toRecord(axis)
Comment thread
TheodoreSpeaks marked this conversation as resolved.
}
const index = series[`${key}Index`] ?? 0
if (option[key] === undefined && index === 0) return {}
if (!Number.isInteger(index) || axes[index as number] === undefined)
throw new Error(`The first series references a missing ${key} at index ${String(index)}`)
return toRecord(axes[index as number])
}

/**
* The axis a threshold is measured on, among the axes the first series is plotted on; ECharts
* defaults an unspecified yAxis to a value axis.
*/
export function valueAxisKey(option: Record<string, unknown>): 'xAxis' | 'yAxis' {
if (option.xAxis === undefined && option.yAxis === undefined)
throw new Error('Thresholds require a value axis')
const y = firstAxis(option.yAxis)
const y = seriesAxis(option, 'yAxis')
if (y.type === undefined || y.type === 'value' || y.type === 'log') return 'yAxis'
const x = firstAxis(option.xAxis)
const x = seriesAxis(option, 'xAxis')
if (x.type === 'value' || x.type === 'log') return 'xAxis'
throw new Error('Thresholds require a value axis')
}
Expand All @@ -47,7 +73,7 @@ export function valueAxisKey(option: Record<string, unknown>): 'xAxis' | 'yAxis'
* y axis is inverted (as horizontal bar charts usually are).
*/
function verticalTop(option: Record<string, unknown>): 'start' | 'end' {
return firstAxis(option.yAxis).inverse === true ? 'start' : 'end'
return seriesAxis(option, 'yAxis').inverse === true ? 'start' : 'end'
}

/** A chart annotations can draw on: a first series with no hand-written marks to collide with. */
Expand Down
18 changes: 18 additions & 0 deletions apps/sim/lib/dashboards/time.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import {
dashboardAxisFormatter,
dashboardRangeFromCalendar,
dashboardRangeText,
dashboardTimeLabel,
dashboardZoomRange,
parseDashboardCustomRange,
Expand Down Expand Up @@ -47,4 +48,21 @@ describe('dashboard time interactions', () => {
expect(dashboardTimeLabel(stamp, 'America/Los_Angeles')).toContain('PDT')
expect(dashboardTimeLabel('2026-12-20T02:30:00Z', 'America/Los_Angeles')).toContain('PST')
})

it('keeps both ends of a range that repeats the same wall-clock minute across a DST fall-back', () => {
const text = dashboardRangeText(
{ from: '2026-11-01T05:30:00.000Z', to: '2026-11-01T06:31:00.000Z' },
'America/New_York'
)
expect(text).toBe('Nov 1, 01:30:00 EDT – Nov 1, 01:30:59 EST')
})

it('shows distinct endpoints for a sub-minute zoom range', () => {
expect(
dashboardRangeText(
{ from: '2026-09-20T14:30:00.000Z', to: '2026-09-20T14:30:01.000Z' },
'UTC'
)
).toBe('Sep 20, 14:30:00.000 UTC – Sep 20, 14:30:00.999 UTC')
})
})
19 changes: 19 additions & 0 deletions apps/sim/lib/dashboards/time.ts
Loading