diff --git a/src/utils/__tests__/visualizationUtils.test.ts b/src/utils/__tests__/visualizationUtils.test.ts index 47f8841a..07f7403f 100644 --- a/src/utils/__tests__/visualizationUtils.test.ts +++ b/src/utils/__tests__/visualizationUtils.test.ts @@ -152,6 +152,50 @@ describe('visualizationUtils', () => { expect(result.direction).toBe('neutral'); expect(result.percentage).toBe(0); }); + + it('should handle zero baseline with positive growth without Infinity', () => { + const data = [0, 10, 250]; + const result = calculateTrend(data); + + expect(result.direction).toBe('up'); + expect(result.percentage).toBe(100); + expect(Number.isFinite(result.percentage)).toBe(true); + }); + + it('should handle zero baseline with negative movement without Infinity', () => { + const data = [0, 2, -5]; + const result = calculateTrend(data); + + expect(result.direction).toBe('down'); + expect(result.percentage).toBe(100); + expect(Number.isFinite(result.percentage)).toBe(true); + }); + + it('should handle zero baseline with no movement without NaN', () => { + const data = [0, 0, 0]; + const result = calculateTrend(data); + + expect(result.direction).toBe('neutral'); + expect(result.percentage).toBe(0); + expect(Number.isNaN(result.percentage)).toBe(false); + }); + + it('should handle near-zero baseline without absurd percentages', () => { + const data = [Number.EPSILON / 2, 3, 5]; + const result = calculateTrend(data); + + expect(result.direction).toBe('up'); + expect(result.percentage).toBe(100); + expect(Number.isFinite(result.percentage)).toBe(true); + }); + + it('should treat near-zero values at both endpoints as neutral', () => { + const data = [Number.EPSILON / 2, 0, Number.EPSILON / 4]; + const result = calculateTrend(data); + + expect(result.direction).toBe('neutral'); + expect(result.percentage).toBe(0); + }); }); describe('calculateStatistics', () => { diff --git a/src/utils/visualizationUtils.ts b/src/utils/visualizationUtils.ts index d04f9442..1a768712 100644 --- a/src/utils/visualizationUtils.ts +++ b/src/utils/visualizationUtils.ts @@ -247,6 +247,24 @@ export const calculateTrend = ( const first = data[0]; const last = data[data.length - 1]; + + // Guard against a zero/near-zero baseline before dividing, otherwise the + // percentage change becomes Infinity or NaN (common for new metrics whose + // first data point is 0) and dashboards render a nonsensical trend. + // A percentage change from a zero baseline is undefined, so report the + // direction of movement with a full 100% change as a finite, sane fallback. + if (Math.abs(first) < Number.EPSILON) { + if (Math.abs(last) < Number.EPSILON) { + // Both endpoints are effectively zero: nothing changed. + return { direction: 'neutral', percentage: 0 }; + } + + return { + direction: last > first ? 'up' : 'down', + percentage: 100, + }; + } + const change = ((last - first) / first) * 100; if (Math.abs(change) < 1) {