diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-bar-chart/hooks/__tests__/useBarChartData.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-bar-chart/hooks/__tests__/useBarChartData.test.ts index d9656ae73c..5315384d6d 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-bar-chart/hooks/__tests__/useBarChartData.test.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-bar-chart/hooks/__tests__/useBarChartData.test.ts @@ -1,4 +1,5 @@ import { useBarChartData } from '@/page-layout/widgets/graph/graph-widget-bar-chart/hooks/useBarChartData'; +import { type BarChartEnrichedKey } from '@/page-layout/widgets/graph/graph-widget-bar-chart/types/BarChartEnrichedKey'; import { type BarChartSeriesWithColor } from '@/page-layout/widgets/graph/graph-widget-bar-chart/types/BarChartSeries'; import { type GraphColorRegistry } from '@/page-layout/widgets/graph/types/GraphColorRegistry'; import { renderHook } from '@testing-library/react'; @@ -259,4 +260,83 @@ describe('useBarChartData', () => { expect(result.current.visibleKeys).toEqual(['sales', 'costs']); expect(result.current.enrichedKeys).toHaveLength(2); }); + + const colorsByKey = (enrichedKeys: BarChartEnrichedKey[]) => + Object.fromEntries( + enrichedKeys.map((item) => [item.key, item.colorScheme.name]), + ); + + const shadesByKey = (enrichedKeys: BarChartEnrichedKey[]) => + Object.fromEntries( + enrichedKeys.map((item) => [item.key, item.colorScheme.solid]), + ); + + it('should keep the same automatic palette color per key when key order changes', () => { + const { result: firstOrderResult } = renderHook(() => + useBarChartData({ + keys: ['sales', 'revenue', 'expenses'], + series: undefined, + colorRegistry: mockColorRegistry, + colorMode: 'automaticPalette', + }), + ); + + const { result: reorderedResult } = renderHook(() => + useBarChartData({ + keys: ['expenses', 'sales', 'revenue'], + series: undefined, + colorRegistry: mockColorRegistry, + colorMode: 'automaticPalette', + }), + ); + + const expectedColorsByKey = { + sales: 'green', + revenue: 'purple', + expenses: 'green', + }; + + expect(colorsByKey(firstOrderResult.current.enrichedKeys)).toEqual( + expectedColorsByKey, + ); + expect(colorsByKey(reorderedResult.current.enrichedKeys)).toEqual( + expectedColorsByKey, + ); + }); + + it('should keep the same gradient shade per key when key order changes in explicitSingleColor mode', () => { + const gradientSeries = (keys: string[]): BarChartSeriesWithColor[] => + keys.map((key) => ({ key, label: key, color: 'green' })); + + const { result: firstOrderResult } = renderHook(() => + useBarChartData({ + keys: ['sales', 'costs', 'revenue'], + series: gradientSeries(['sales', 'costs', 'revenue']), + colorRegistry: mockColorRegistry, + colorMode: 'explicitSingleColor', + }), + ); + + const { result: reorderedResult } = renderHook(() => + useBarChartData({ + keys: ['revenue', 'sales', 'costs'], + series: gradientSeries(['revenue', 'sales', 'costs']), + colorRegistry: mockColorRegistry, + colorMode: 'explicitSingleColor', + }), + ); + + const expectedShadesByKey = { + costs: 'green4', + revenue: 'green6', + sales: 'green8', + }; + + expect(shadesByKey(firstOrderResult.current.enrichedKeys)).toEqual( + expectedShadesByKey, + ); + expect(shadesByKey(reorderedResult.current.enrichedKeys)).toEqual( + expectedShadesByKey, + ); + }); }); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-bar-chart/hooks/useBarChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-bar-chart/hooks/useBarChartData.ts index 2dc30798d2..3545177f7e 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-bar-chart/hooks/useBarChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-bar-chart/hooks/useBarChartData.ts @@ -4,6 +4,7 @@ import { type BarChartSeriesWithColor } from '@/page-layout/widgets/graph/graph- import { graphWidgetHiddenLegendIdsComponentState } from '@/page-layout/widgets/graph/states/graphWidgetHiddenLegendIdsComponentState'; import { type GraphColorMode } from '@/page-layout/widgets/graph/types/GraphColorMode'; import { type GraphColorRegistry } from '@/page-layout/widgets/graph/types/GraphColorRegistry'; +import { buildAlphabeticalRankByKey } from '@/page-layout/widgets/graph/utils/buildAlphabeticalRankByKey'; import { getColorScheme } from '@/page-layout/widgets/graph/utils/getColorScheme'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; import { useMemo } from 'react'; @@ -37,15 +38,16 @@ export const useBarChartData = ({ ); const allEnrichedKeys = useMemo((): BarChartEnrichedKey[] => { - const shouldApplyGradient = colorMode === 'explicitSingleColor'; + const alphabeticalRankByKey = buildAlphabeticalRankByKey(keys); - return keys.map((key, index) => { + return keys.map((key) => { const seriesConfig = seriesConfigMap.get(key); const colorScheme = getColorScheme({ registry: colorRegistry, colorName: seriesConfig?.color, - fallbackIndex: index, - totalGroups: shouldApplyGradient ? keys.length : undefined, + colorKey: key, + colorMode, + alphabeticalRankByKey, }); return { diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-line-chart/hooks/__tests__/useLineChartData.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-line-chart/hooks/__tests__/useLineChartData.test.ts index a5dadd2edb..f5d9bc68f7 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-line-chart/hooks/__tests__/useLineChartData.test.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-line-chart/hooks/__tests__/useLineChartData.test.ts @@ -1,3 +1,4 @@ +import { type LineChartEnrichedSeries } from '@/page-layout/widgets/graph/graph-widget-line-chart/types/LineChartEnrichedSeries'; import { type LineChartSeriesWithColor } from '@/page-layout/widgets/graph/graph-widget-line-chart/types/LineChartSeriesWithColor'; import { type GraphColorRegistry } from '@/page-layout/widgets/graph/types/GraphColorRegistry'; import { renderHook } from '@testing-library/react'; @@ -257,6 +258,95 @@ describe('useLineChartData', () => { expect(result.current.nivoData[0].id).toBe('series2'); }); + it('should keep the same automatic palette color per key when series order changes', () => { + const series = (key: string): LineChartSeriesWithColor => ({ + key, + label: key, + data: [{ x: 'Jan', y: 100 }], + }); + + const { result: firstOrderResult } = renderHook(() => + useLineChartData({ + data: [series('gamma'), series('alpha'), series('beta')], + colorRegistry: mockColorRegistry, + id: 'test-chart', + colorMode: 'automaticPalette', + }), + ); + + const { result: reorderedResult } = renderHook(() => + useLineChartData({ + data: [series('beta'), series('gamma'), series('alpha')], + colorRegistry: mockColorRegistry, + id: 'test-chart', + colorMode: 'automaticPalette', + }), + ); + + const colorsByKey = (enrichedSeries: LineChartEnrichedSeries[]) => + Object.fromEntries( + enrichedSeries.map((item) => [item.key, item.colorScheme.name]), + ); + + const expectedColorsByKey = { + alpha: 'red', + beta: 'blue', + gamma: 'red', + }; + + expect(colorsByKey(firstOrderResult.current.enrichedSeries)).toEqual( + expectedColorsByKey, + ); + expect(colorsByKey(reorderedResult.current.enrichedSeries)).toEqual( + expectedColorsByKey, + ); + }); + + it('should keep the same gradient shade per key when series order changes in explicitSingleColor mode', () => { + const series = (key: string): LineChartSeriesWithColor => ({ + key, + label: key, + data: [{ x: 'Jan', y: 100 }], + color: 'red', + }); + + const { result: firstOrderResult } = renderHook(() => + useLineChartData({ + data: [series('won'), series('open'), series('lost')], + colorRegistry: mockColorRegistry, + id: 'test-chart', + colorMode: 'explicitSingleColor', + }), + ); + + const { result: reorderedResult } = renderHook(() => + useLineChartData({ + data: [series('lost'), series('won'), series('open')], + colorRegistry: mockColorRegistry, + id: 'test-chart', + colorMode: 'explicitSingleColor', + }), + ); + + const shadesByKey = (enrichedSeries: LineChartEnrichedSeries[]) => + Object.fromEntries( + enrichedSeries.map((item) => [item.key, item.colorScheme.solid]), + ); + + const expectedShadesByKey = { + lost: 'red4', + open: 'red6', + won: 'red8', + }; + + expect(shadesByKey(firstOrderResult.current.enrichedSeries)).toEqual( + expectedShadesByKey, + ); + expect(shadesByKey(reorderedResult.current.enrichedSeries)).toEqual( + expectedShadesByKey, + ); + }); + it('should handle hidden ids that do not exist in data', () => { mockUseAtomComponentStateValue.mockReturnValue([ 'nonexistent', diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-line-chart/hooks/useLineChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-line-chart/hooks/useLineChartData.ts index 6a41ea7dd7..667580fb09 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-line-chart/hooks/useLineChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-line-chart/hooks/useLineChartData.ts @@ -4,6 +4,7 @@ import { type LineChartSeriesWithColor } from '@/page-layout/widgets/graph/graph import { graphWidgetHiddenLegendIdsComponentState } from '@/page-layout/widgets/graph/states/graphWidgetHiddenLegendIdsComponentState'; import { type GraphColorMode } from '@/page-layout/widgets/graph/types/GraphColorMode'; import { type GraphColorRegistry } from '@/page-layout/widgets/graph/types/GraphColorRegistry'; +import { buildAlphabeticalRankByKey } from '@/page-layout/widgets/graph/utils/buildAlphabeticalRankByKey'; import { getColorScheme } from '@/page-layout/widgets/graph/utils/getColorScheme'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; import { type LineSeries } from '@nivo/line'; @@ -27,14 +28,17 @@ export const useLineChartData = ({ ); const allEnrichedSeries = useMemo((): LineChartEnrichedSeries[] => { - const shouldApplyGradient = colorMode === 'explicitSingleColor'; + const alphabeticalRankByKey = buildAlphabeticalRankByKey( + data.map((series) => series.key), + ); return data.map((series, index) => { const colorScheme = getColorScheme({ registry: colorRegistry, colorName: series.color, - fallbackIndex: index, - totalGroups: shouldApplyGradient ? data.length : undefined, + colorKey: series.key, + colorMode, + alphabeticalRankByKey, }); const sanitizedSeriesKey = series.key diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-pie-chart/hooks/__tests__/usePieChartData.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-pie-chart/hooks/__tests__/usePieChartData.test.ts index 7064fd0716..55752ae6de 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-pie-chart/hooks/__tests__/usePieChartData.test.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-pie-chart/hooks/__tests__/usePieChartData.test.ts @@ -1,5 +1,6 @@ import { usePieChartData } from '@/page-layout/widgets/graph/graph-widget-pie-chart/hooks/usePieChartData'; import { type PieChartDataItemWithColor } from '@/page-layout/widgets/graph/graph-widget-pie-chart/types/PieChartDataItem'; +import { type PieChartEnrichedData } from '@/page-layout/widgets/graph/graph-widget-pie-chart/types/PieChartEnrichedData'; import { type GraphColorRegistry } from '@/page-layout/widgets/graph/types/GraphColorRegistry'; import { renderHook } from '@testing-library/react'; @@ -62,6 +63,16 @@ describe('usePieChartData', () => { { key: 'item3', value: 20 }, ]; + const colorsByKey = (enrichedData: PieChartEnrichedData[]) => + Object.fromEntries( + enrichedData.map((item) => [item.key, item.colorScheme.name]), + ); + + const shadesByKey = (enrichedData: PieChartEnrichedData[]) => + Object.fromEntries( + enrichedData.map((item) => [item.key, item.colorScheme.solid]), + ); + it('should enrich data with color schemes and percentages', () => { const { result } = renderHook(() => usePieChartData({ @@ -110,17 +121,24 @@ describe('usePieChartData', () => { expect(result.current.enrichedData[0].percentage).toBe(100); }); - it('should assign colors based on index', () => { + it('should assign automatic palette colors by alphabetical rank', () => { const { result } = renderHook(() => usePieChartData({ - data: mockData, + data: [ + { key: 'beta', value: 20 }, + { key: 'gamma', value: 30 }, + { key: 'delta', value: 50 }, + ], colorRegistry: mockColorRegistry, colorMode: 'automaticPalette', }), ); - expect(result.current.enrichedData[0].colorScheme.name).toBe('red'); - expect(result.current.enrichedData[1].colorScheme.name).toBe('blue'); + expect(colorsByKey(result.current.enrichedData)).toEqual({ + gamma: 'red', + delta: 'blue', + beta: 'red', + }); }); it('should return legend items from all data', () => { @@ -219,4 +237,87 @@ describe('usePieChartData', () => { expect(result.current.enrichedData).toHaveLength(3); }); + + it('should keep the same automatic palette color per key when item order changes', () => { + const { result: firstOrderResult } = renderHook(() => + usePieChartData({ + data: [ + { key: 'gamma', value: 30 }, + { key: 'alpha', value: 50 }, + { key: 'beta', value: 20 }, + ], + colorRegistry: mockColorRegistry, + colorMode: 'automaticPalette', + }), + ); + + const { result: reorderedResult } = renderHook(() => + usePieChartData({ + data: [ + { key: 'beta', value: 20 }, + { key: 'gamma', value: 30 }, + { key: 'alpha', value: 50 }, + ], + colorRegistry: mockColorRegistry, + colorMode: 'automaticPalette', + }), + ); + + const expectedColorsByKey = { + alpha: 'red', + beta: 'blue', + gamma: 'red', + }; + + expect(colorsByKey(firstOrderResult.current.enrichedData)).toEqual( + expectedColorsByKey, + ); + expect(colorsByKey(reorderedResult.current.enrichedData)).toEqual( + expectedColorsByKey, + ); + }); + + it('should keep the same gradient shade per key when item order changes in explicitSingleColor mode', () => { + const gradientItem = ( + key: string, + value: number, + ): PieChartDataItemWithColor => ({ key, value, color: 'blue' }); + + const { result: firstOrderResult } = renderHook(() => + usePieChartData({ + data: [ + gradientItem('won', 30), + gradientItem('open', 50), + gradientItem('lost', 20), + ], + colorRegistry: mockColorRegistry, + colorMode: 'explicitSingleColor', + }), + ); + + const { result: reorderedResult } = renderHook(() => + usePieChartData({ + data: [ + gradientItem('lost', 20), + gradientItem('won', 30), + gradientItem('open', 50), + ], + colorRegistry: mockColorRegistry, + colorMode: 'explicitSingleColor', + }), + ); + + const expectedShadesByKey = { + lost: 'blue4', + open: 'blue6', + won: 'blue8', + }; + + expect(shadesByKey(firstOrderResult.current.enrichedData)).toEqual( + expectedShadesByKey, + ); + expect(shadesByKey(reorderedResult.current.enrichedData)).toEqual( + expectedShadesByKey, + ); + }); }); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-pie-chart/hooks/usePieChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-pie-chart/hooks/usePieChartData.ts index 6b96d2afd5..9fa3a8c0d4 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-pie-chart/hooks/usePieChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graph-widget-pie-chart/hooks/usePieChartData.ts @@ -5,6 +5,7 @@ import { calculatePieChartPercentage } from '@/page-layout/widgets/graph/graph-w import { graphWidgetHiddenLegendIdsComponentState } from '@/page-layout/widgets/graph/states/graphWidgetHiddenLegendIdsComponentState'; import { type GraphColorMode } from '@/page-layout/widgets/graph/types/GraphColorMode'; import { type GraphColorRegistry } from '@/page-layout/widgets/graph/types/GraphColorRegistry'; +import { buildAlphabeticalRankByKey } from '@/page-layout/widgets/graph/utils/buildAlphabeticalRankByKey'; import { getColorScheme } from '@/page-layout/widgets/graph/utils/getColorScheme'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; import { useMemo } from 'react'; @@ -27,14 +28,17 @@ export const usePieChartData = ({ const allEnrichedData = useMemo((): PieChartEnrichedData[] => { const totalValue = data.reduce((sum, item) => sum + item.value, 0); - const shouldApplyGradient = colorMode === 'explicitSingleColor'; + const alphabeticalRankByKey = buildAlphabeticalRankByKey( + data.map((item) => item.key), + ); - return data.map((item, index) => { + return data.map((item) => { const colorScheme = getColorScheme({ registry: colorRegistry, colorName: item.color, - fallbackIndex: index, - totalGroups: shouldApplyGradient ? data.length : undefined, + colorKey: item.key, + colorMode, + alphabeticalRankByKey, }); const percentage = calculatePieChartPercentage(item.value, totalValue); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/buildAlphabeticalRankByKey.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/buildAlphabeticalRankByKey.test.ts new file mode 100644 index 0000000000..4d0c9a1061 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/buildAlphabeticalRankByKey.test.ts @@ -0,0 +1,38 @@ +import { buildAlphabeticalRankByKey } from '@/page-layout/widgets/graph/utils/buildAlphabeticalRankByKey'; + +describe('buildAlphabeticalRankByKey', () => { + it('should assign ranks by alphabetical order of keys', () => { + const result = buildAlphabeticalRankByKey(['charlie', 'alpha', 'bravo']); + + expect(result.get('alpha')).toBe(0); + expect(result.get('bravo')).toBe(1); + expect(result.get('charlie')).toBe(2); + }); + + it('should return the same ranks regardless of input order', () => { + const firstOrder = buildAlphabeticalRankByKey(['open', 'won', 'lost']); + const secondOrder = buildAlphabeticalRankByKey(['lost', 'open', 'won']); + + expect(firstOrder).toEqual(secondOrder); + }); + + it('should deduplicate keys so ranks stay dense', () => { + const result = buildAlphabeticalRankByKey(['bravo', 'alpha', 'bravo']); + + expect(result.size).toBe(2); + expect(result.get('alpha')).toBe(0); + expect(result.get('bravo')).toBe(1); + }); + + it('should handle an empty key list', () => { + expect(buildAlphabeticalRankByKey([]).size).toBe(0); + }); + + it('should not mutate the input array', () => { + const keys = ['bravo', 'alpha']; + + buildAlphabeticalRankByKey(keys); + + expect(keys).toEqual(['bravo', 'alpha']); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/getColorScheme.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/getColorScheme.test.ts index ccce05a16a..962207a383 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/getColorScheme.test.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/getColorScheme.test.ts @@ -59,11 +59,20 @@ describe('getColorScheme', () => { }, } as unknown as GraphColorRegistry; + const alphabeticalRankByKey = new Map([ + ['alpha', 0], + ['beta', 1], + ['gamma', 2], + ]); + describe('with valid color name', () => { it('should return the color scheme for a valid color name', () => { const result = getColorScheme({ registry: mockRegistry, colorName: 'blue', + colorKey: 'alpha', + colorMode: 'selectFieldOptionColors', + alphabeticalRankByKey, }); expect(result).toEqual(mockRegistry.blue); @@ -73,6 +82,9 @@ describe('getColorScheme', () => { const result = getColorScheme({ registry: mockRegistry, colorName: 'BLUE' as 'blue', + colorKey: 'alpha', + colorMode: 'selectFieldOptionColors', + alphabeticalRankByKey, }); expect(result).toEqual(mockRegistry.blue); @@ -82,6 +94,9 @@ describe('getColorScheme', () => { const result = getColorScheme({ registry: mockRegistry, colorName: 'Blue' as 'blue', + colorKey: 'alpha', + colorMode: 'selectFieldOptionColors', + alphabeticalRankByKey, }); expect(result).toEqual(mockRegistry.blue); @@ -89,62 +104,101 @@ describe('getColorScheme', () => { }); describe('with invalid or missing color name', () => { - it('should return color scheme by fallback index when color name is undefined', () => { - const result = getColorScheme({ + it('should return the color scheme at the alphabetical rank', () => { + const firstResult = getColorScheme({ registry: mockRegistry, - colorName: undefined, - fallbackIndex: 0, + colorKey: 'alpha', + colorMode: 'automaticPalette', + alphabeticalRankByKey, + }); + const secondResult = getColorScheme({ + registry: mockRegistry, + colorKey: 'beta', + colorMode: 'automaticPalette', + alphabeticalRankByKey, + }); + const thirdResult = getColorScheme({ + registry: mockRegistry, + colorKey: 'gamma', + colorMode: 'automaticPalette', + alphabeticalRankByKey, }); - expect(result.name).toBeDefined(); + expect(firstResult.name).toBe('blue'); + expect(secondResult.name).toBe('green'); + expect(thirdResult.name).toBe('red'); }); - it('should return color scheme by fallback index when color name is not in registry', () => { + it('should use the alphabetical rank when color name is not in registry', () => { const result = getColorScheme({ registry: mockRegistry, colorName: 'invalidColor' as 'blue', - fallbackIndex: 1, + colorKey: 'beta', + colorMode: 'automaticPalette', + alphabeticalRankByKey, }); - expect(result.name).toBeDefined(); + expect(result.name).toBe('green'); }); - it('should use fallback index 0 when not provided', () => { - const result = getColorScheme({ + it('should return the same color for the same key on every call', () => { + const first = getColorScheme({ registry: mockRegistry, - colorName: undefined, + colorKey: 'beta', + colorMode: 'automaticPalette', + alphabeticalRankByKey, + }); + const second = getColorScheme({ + registry: mockRegistry, + colorKey: 'beta', + colorMode: 'automaticPalette', + alphabeticalRankByKey, }); - expect(result.name).toBeDefined(); + expect(first).toEqual(second); + }); + + it('should throw when the color key has no alphabetical rank', () => { + expect(() => + getColorScheme({ + registry: mockRegistry, + colorKey: 'missing', + colorMode: 'automaticPalette', + alphabeticalRankByKey, + }), + ).toThrow('Missing alphabetical rank for color key "missing"'); }); }); - describe('with totalGroups parameter', () => { - it('should generate a group color when totalGroups is provided', () => { + describe('with explicit single color mode', () => { + it('should generate a group color', () => { const result = getColorScheme({ registry: mockRegistry, colorName: 'blue', - fallbackIndex: 0, - totalGroups: 5, + colorKey: 'alpha', + colorMode: 'explicitSingleColor', + alphabeticalRankByKey, }); expect(result.name).toBe('blue'); expect(result.variations).toEqual(mockRegistry.blue.variations); }); - it('should use fallbackIndex for group color generation', () => { + it('should use alphabetical rank for group color generation', () => { const result1 = getColorScheme({ registry: mockRegistry, colorName: 'green', - fallbackIndex: 0, - totalGroups: 3, + colorKey: 'alpha', + colorMode: 'explicitSingleColor', + alphabeticalRankByKey, }); const result2 = getColorScheme({ registry: mockRegistry, colorName: 'green', - fallbackIndex: 2, - totalGroups: 3, + colorKey: 'gamma', + colorMode: 'explicitSingleColor', + alphabeticalRankByKey, }); expect(result1.solid).not.toBe(result2.solid); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/buildAlphabeticalRankByKey.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/buildAlphabeticalRankByKey.ts new file mode 100644 index 0000000000..de6bb9324b --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/buildAlphabeticalRankByKey.ts @@ -0,0 +1,7 @@ +export const buildAlphabeticalRankByKey = ( + keys: string[], +): Map => { + const uniqueSortedKeys = [...new Set(keys)].sort(); + + return new Map(uniqueSortedKeys.map((key, rank) => [key, rank])); +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getColorScheme.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getColorScheme.ts index 0d5a722ca5..c7240b5bd2 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getColorScheme.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getColorScheme.ts @@ -1,21 +1,31 @@ import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor'; +import { type GraphColorMode } from '@/page-layout/widgets/graph/types/GraphColorMode'; import { type GraphColorRegistry } from '@/page-layout/widgets/graph/types/GraphColorRegistry'; import { type GraphColorScheme } from '@/page-layout/widgets/graph/types/GraphColorScheme'; import { generateGroupColor } from '@/page-layout/widgets/graph/utils/generateGroupColor'; import { getColorSchemeByIndex } from '@/page-layout/widgets/graph/utils/getColorSchemeByIndex'; -import { isDefined } from 'twenty-shared/utils'; +import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils'; export const getColorScheme = ({ registry, colorName, - fallbackIndex, - totalGroups, + colorKey, + colorMode, + alphabeticalRankByKey, }: { registry: GraphColorRegistry; colorName?: GraphColor; - fallbackIndex?: number; - totalGroups?: number; + colorKey: string; + colorMode: GraphColorMode; + alphabeticalRankByKey: ReadonlyMap; }): GraphColorScheme => { + const alphabeticalRank = alphabeticalRankByKey.get(colorKey); + + assertIsDefinedOrThrow( + alphabeticalRank, + new Error(`Missing alphabetical rank for color key "${colorKey}"`), + ); + const normalizedColorName = isDefined(colorName) ? (colorName.toLowerCase() as GraphColor) : undefined; @@ -24,10 +34,10 @@ export const getColorScheme = ({ !isDefined(normalizedColorName) || !isDefined(registry[normalizedColorName]) ) { - return getColorSchemeByIndex(registry, fallbackIndex ?? 0); + return getColorSchemeByIndex(registry, alphabeticalRank); } - if (!isDefined(totalGroups)) { + if (colorMode !== 'explicitSingleColor') { return registry[normalizedColorName]; } @@ -35,8 +45,8 @@ export const getColorScheme = ({ ...registry[normalizedColorName], solid: generateGroupColor({ colorScheme: registry[normalizedColorName], - groupIndex: fallbackIndex ?? 0, - totalGroups, + groupIndex: alphabeticalRank, + totalGroups: alphabeticalRankByKey.size, }), }; };