diff --git a/frontend/src/component/common/PrettifyLargeNumber/PrettifyLargeNumber.tsx b/frontend/src/component/common/PrettifyLargeNumber/PrettifyLargeNumber.tsx index 200ef8d87d..bd2f29e520 100644 --- a/frontend/src/component/common/PrettifyLargeNumber/PrettifyLargeNumber.tsx +++ b/frontend/src/component/common/PrettifyLargeNumber/PrettifyLargeNumber.tsx @@ -1,8 +1,8 @@ import type { FC } from 'react'; -import millify from 'millify'; import { Tooltip } from '@mui/material'; import { LARGE_NUMBER_PRETTIFIED } from 'utils/testIds'; import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender.tsx'; +import { prettifyLargeNumber } from './formatLargeNumber.js'; interface IPrettifyLargeNumberProps { /** @@ -25,15 +25,6 @@ interface IPrettifyLargeNumberProps { 'data-loading'?: string; } -export const prettifyLargeNumber = - (threshold: number = 1_000_000, precision: number = 2) => - (value: number) => { - if (value < threshold) { - return value.toLocaleString(); - } - return millify(value, { precision }); - }; - export const PrettifyLargeNumber: FC = ({ value, threshold = 1_000_000, diff --git a/frontend/src/component/common/PrettifyLargeNumber/formatLargeNumber.ts b/frontend/src/component/common/PrettifyLargeNumber/formatLargeNumber.ts new file mode 100644 index 0000000000..1efa1252b7 --- /dev/null +++ b/frontend/src/component/common/PrettifyLargeNumber/formatLargeNumber.ts @@ -0,0 +1,10 @@ +import millify from 'millify'; + +export const prettifyLargeNumber = + (threshold: number = 1_000_000, precision: number = 2) => + (value: number) => { + if (value < threshold) { + return value.toLocaleString(); + } + return millify(value, { precision }); + }; diff --git a/frontend/src/component/impact-metrics/ChartConfigModal/ImpactMetricsControls/ImpactMetricsControls.tsx b/frontend/src/component/impact-metrics/ChartConfigModal/ImpactMetricsControls/ImpactMetricsControls.tsx index 1ab8cfad6b..519bbaf231 100644 --- a/frontend/src/component/impact-metrics/ChartConfigModal/ImpactMetricsControls/ImpactMetricsControls.tsx +++ b/frontend/src/component/impact-metrics/ChartConfigModal/ImpactMetricsControls/ImpactMetricsControls.tsx @@ -5,7 +5,7 @@ import { SeriesSelector } from './SeriesSelector/SeriesSelector.tsx'; import { RangeSelector } from './RangeSelector/RangeSelector.tsx'; import { ModeSelector } from './ModeSelector/ModeSelector.tsx'; import type { ChartFormState } from '../../hooks/useChartFormState.ts'; -import { getMetricType } from '../../utils.ts'; +import { getMetricType } from '../../metricsFormatters.ts'; export type ImpactMetricsControlsProps = { formData: ChartFormState['formData']; diff --git a/frontend/src/component/impact-metrics/ImpactMetricsChart.tsx b/frontend/src/component/impact-metrics/ImpactMetricsChart.tsx index 3dcebf418f..a118f8e322 100644 --- a/frontend/src/component/impact-metrics/ImpactMetricsChart.tsx +++ b/frontend/src/component/impact-metrics/ImpactMetricsChart.tsx @@ -7,7 +7,11 @@ import { } from '../insights/components/LineChart/LineChart.tsx'; import { useImpactMetricsData } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData'; import { usePlaceholderData } from '../insights/hooks/usePlaceholderData.js'; -import { getDisplayFormat, getTimeUnit, formatLargeNumbers } from './utils.ts'; +import { + getDisplayFormat, + getTimeUnit, + formatLargeNumbers, +} from './metricsFormatters.js'; import { fromUnixTime } from 'date-fns'; import { useChartData } from './hooks/useChartData.ts'; import type { AggregationMode } from './types.ts'; diff --git a/frontend/src/component/impact-metrics/hooks/useChartData.ts b/frontend/src/component/impact-metrics/hooks/useChartData.ts index 786778f6ce..e668a9e2cc 100644 --- a/frontend/src/component/impact-metrics/hooks/useChartData.ts +++ b/frontend/src/component/impact-metrics/hooks/useChartData.ts @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { useTheme } from '@mui/material'; import type { ImpactMetricsSeries } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData'; -import { getSeriesLabel } from '../utils.ts'; +import { getSeriesLabel } from '../metricsFormatters.ts'; const getColorStartingIndex = (modulo: number, series?: string): number => { if (!series || series.length === 0 || modulo <= 0) { diff --git a/frontend/src/component/impact-metrics/hooks/useChartFormState.ts b/frontend/src/component/impact-metrics/hooks/useChartFormState.ts index 6a6ce2ce50..f3a0e67e8e 100644 --- a/frontend/src/component/impact-metrics/hooks/useChartFormState.ts +++ b/frontend/src/component/impact-metrics/hooks/useChartFormState.ts @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { useImpactMetricsData } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData'; import type { AggregationMode, ChartConfig } from '../types.ts'; import type { ImpactMetricsLabels } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData'; -import { getDefaultAggregation, getMetricType } from '../utils.ts'; +import { getDefaultAggregation, getMetricType } from '../metricsFormatters.ts'; type UseChartConfigParams = { open: boolean; diff --git a/frontend/src/component/impact-metrics/metricsFormatters.test.ts b/frontend/src/component/impact-metrics/metricsFormatters.test.ts new file mode 100644 index 0000000000..be1f04f548 --- /dev/null +++ b/frontend/src/component/impact-metrics/metricsFormatters.test.ts @@ -0,0 +1,41 @@ +import { formatLargeNumbers } from './metricsFormatters.js'; + +describe('formatLargeNumbers', () => { + it('formats small numbers with locale formatting', () => { + expect(formatLargeNumbers(0)).toBe('0'); + expect(formatLargeNumbers(999)).toBe('999'); + }); + + it('formats thousands correctly', () => { + expect(formatLargeNumbers(1000)).toBe('1k'); + expect(formatLargeNumbers(1200)).toBe('1.2k'); + expect(formatLargeNumbers(1400)).toBe('1.4k'); + expect(formatLargeNumbers(1600)).toBe('1.6k'); + expect(formatLargeNumbers(5000)).toBe('5k'); + expect(formatLargeNumbers(9500)).toBe('9.5k'); + expect(formatLargeNumbers(10000)).toBe('10k'); + expect(formatLargeNumbers(999000)).toBe('999k'); + }); + + it('formats millions correctly', () => { + expect(formatLargeNumbers(1000000)).toBe('1M'); + expect(formatLargeNumbers(1500000)).toBe('1.5M'); + expect(formatLargeNumbers(2700000)).toBe('2.7M'); + expect(formatLargeNumbers(5000000)).toBe('5M'); + expect(formatLargeNumbers(9900000)).toBe('9.9M'); + expect(formatLargeNumbers(10000000)).toBe('10M'); + }); + + it('formats billions correctly', () => { + expect(formatLargeNumbers(1000000000)).toBe('1B'); + expect(formatLargeNumbers(1500000000)).toBe('1.5B'); + expect(formatLargeNumbers(10000000000)).toBe('10B'); + expect(formatLargeNumbers(999000000000)).toBe('999B'); + }); + + it('formats trillions correctly', () => { + expect(formatLargeNumbers(1000000000000)).toBe('1T'); + expect(formatLargeNumbers(2500000000000)).toBe('2.5T'); + expect(formatLargeNumbers(10000000000000)).toBe('10T'); + }); +}); diff --git a/frontend/src/component/impact-metrics/utils.ts b/frontend/src/component/impact-metrics/metricsFormatters.ts similarity index 89% rename from frontend/src/component/impact-metrics/utils.ts rename to frontend/src/component/impact-metrics/metricsFormatters.ts index 09570739e6..3cf88b3a09 100644 --- a/frontend/src/component/impact-metrics/utils.ts +++ b/frontend/src/component/impact-metrics/metricsFormatters.ts @@ -1,3 +1,5 @@ +import { prettifyLargeNumber } from '../common/PrettifyLargeNumber/formatLargeNumber.js'; + export const getTimeUnit = (timeRange: string) => { switch (timeRange) { case 'hour': @@ -52,13 +54,9 @@ export const getSeriesLabel = (metric: Record): string => { }; export const formatLargeNumbers = (value: number): string => { - if (value >= 1000000) { - return `${(value / 1000000).toFixed(0)}M`; - } - if (value >= 1000) { - return `${(value / 1000).toFixed(0)}k`; - } - return value.toString(); + const formatter = prettifyLargeNumber(1000, 1); + const result = formatter(value); + return result.replace(/K/g, 'k'); }; export const getMetricType = (seriesName: string) => { diff --git a/frontend/src/component/insights/sections/LifecycleInsights.tsx b/frontend/src/component/insights/sections/LifecycleInsights.tsx index ab53827bb9..8933085638 100644 --- a/frontend/src/component/insights/sections/LifecycleInsights.tsx +++ b/frontend/src/component/insights/sections/LifecycleInsights.tsx @@ -4,10 +4,8 @@ import { FilterItemParam } from 'utils/serializeQueryParams'; import { InsightsSection } from 'component/insights/sections/InsightsSection'; import { LifecycleChart } from '../components/LifecycleChart/LifecycleChart.tsx'; import { styled, useTheme } from '@mui/material'; -import { - prettifyLargeNumber, - PrettifyLargeNumber, -} from 'component/common/PrettifyLargeNumber/PrettifyLargeNumber.tsx'; +import { prettifyLargeNumber } from 'component/common/PrettifyLargeNumber/formatLargeNumber.js'; +import { PrettifyLargeNumber } from 'component/common/PrettifyLargeNumber/PrettifyLargeNumber.tsx'; import { FeatureLifecycleStageIcon } from 'component/common/FeatureLifecycle/FeatureLifecycleStageIcon.tsx'; import { normalizeDays } from './normalize-days.ts'; import useLoading from 'hooks/useLoading.ts'; diff --git a/frontend/src/component/insights/sections/normalize-days.ts b/frontend/src/component/insights/sections/normalize-days.ts index b880ede392..4eb64615b8 100644 --- a/frontend/src/component/insights/sections/normalize-days.ts +++ b/frontend/src/component/insights/sections/normalize-days.ts @@ -1,4 +1,4 @@ -import { prettifyLargeNumber } from 'component/common/PrettifyLargeNumber/PrettifyLargeNumber'; +import { prettifyLargeNumber } from 'component/common/PrettifyLargeNumber/formatLargeNumber.js'; const prettifyNumber = prettifyLargeNumber(1000, 2);