diff --git a/.changeset/cli-duration-axis-tick-compact.md b/.changeset/cli-duration-axis-tick-compact.md new file mode 100644 index 0000000000..a665a68c13 --- /dev/null +++ b/.changeset/cli-duration-axis-tick-compact.md @@ -0,0 +1,7 @@ +--- +'@hyperdx/cli': patch +--- + +fix: use compact duration labels on terminal chart axis ticks + +`axisTickFormatter` is a documented port of the web's `formatAxisTick` (fixed in #3148) and is expected to stay behaviorally in sync with it, but it fell through to the wide `formatDurationMs` instead of the compact formatter for duration-formatted charts. `hdx chart` now renders the same short labels (e.g. "13m" instead of "13.33min") as the web app. diff --git a/packages/cli/src/__tests__/formatNumber.test.ts b/packages/cli/src/__tests__/formatNumber.test.ts index 71f64140c3..6991ffa454 100644 --- a/packages/cli/src/__tests__/formatNumber.test.ts +++ b/packages/cli/src/__tests__/formatNumber.test.ts @@ -58,4 +58,27 @@ describe('axisTickFormatter', () => { axisTickFormatter({ output: 'throughput', mantissa: 2 })?.(1234567), ).toBe('1234567'); }); + + describe('duration output', () => { + it('renders minutes with a single-character unit, not "min"', () => { + expect(axisTickFormatter({ output: 'duration' })?.(799.8)).toBe('13m'); + }); + + it('renders seconds compactly', () => { + expect(axisTickFormatter({ output: 'duration' })?.(39.51)).toBe('39.5s'); + }); + + it('respects a configured factor, matching formatNumber`s own duration math', () => { + expect( + axisTickFormatter({ output: 'duration', factor: 0.001 })?.(442_800), + ).toBe('7.4m'); + }); + + it('renders sub-millisecond and hour-scale values compactly too', () => { + expect(axisTickFormatter({ output: 'duration' })?.(0.0000005)).toBe( + '500ns', + ); + expect(axisTickFormatter({ output: 'duration' })?.(7500)).toBe('2.1h'); + }); + }); }); diff --git a/packages/cli/src/shared/formatNumber.ts b/packages/cli/src/shared/formatNumber.ts index 97e750e17b..5b309e77fa 100644 --- a/packages/cli/src/shared/formatNumber.ts +++ b/packages/cli/src/shared/formatNumber.ts @@ -289,6 +289,27 @@ function formatDurationMs(ms: number): string { return `${parseFloat((ms / 3_600_000).toFixed(2))}h`; } +/** + * Compact duration labels for axis ticks — fewer decimals, shorter units. + * + * @source packages/app/src/utils.ts (formatDurationMsCompact) + */ +function formatDurationMsCompact(ms: number): string { + if (ms < 0) return `-${formatDurationMsCompact(-ms)}`; + if (ms === 0) return '0'; + if (ms < 0.001) return `${+(ms * 1e6).toPrecision(2)}ns`; + if (ms < 1) { + const µs = ms * 1000; + return µs < 10 ? `${+µs.toPrecision(2)}µs` : `${Math.round(µs)}µs`; + } + if (ms < 1000) { + return ms < 10 ? `${+ms.toPrecision(2)}ms` : `${Math.round(ms)}ms`; + } + if (ms < 120_000) return `${+(ms / 1000).toPrecision(3)}s`; + if (ms < 3_600_000) return `${+(ms / 60_000).toPrecision(2)}m`; + return `${+(ms / 3_600_000).toPrecision(2)}h`; +} + // ---- Number-format resolution (de-hooked from packages/app/src/source.ts) -- // Aggregate functions whose output preserves the unit of the input value. @@ -506,6 +527,13 @@ export function axisTickFormatter( return undefined; } return (value: number) => { + // formatNumber returns early for 'duration' - use the compact formatter + // instead, matching the web's formatAxisTick. + if (numberFormat.output === 'duration') { + const factor = numberFormat.factor ?? 1; + return formatDurationMsCompact(value * factor * 1000); + } + const displayed = numberFormat.output === 'percent' ? value * 100 : value; return formatNumber(value, { ...numberFormat,