Keep chart palette colors stable when series order changes (#23638)
https://discord.com/channels/1130383047699738754/1522812783140540538 Chart palette colors were assigned by array position, so changing the sort order (or any reordering of the data) reshuffled every series color on line, bar and pie charts. Grouping by a select field was unaffected since options carry their own colors — this only hit groupings without intrinsic colors (relations, text fields, etc). Colors are now assigned by the alphabetical rank of the series key, so a key keeps its color no matter what order the data arrives in. Side effect: existing palette-colored charts get a one-time color reassignment. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23638?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+80
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+6
-4
@@ -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 {
|
||||
|
||||
+90
@@ -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',
|
||||
|
||||
+7
-3
@@ -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
|
||||
|
||||
+105
-4
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+8
-4
@@ -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);
|
||||
|
||||
+38
@@ -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']);
|
||||
});
|
||||
});
|
||||
+75
-21
@@ -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);
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
export const buildAlphabeticalRankByKey = (
|
||||
keys: string[],
|
||||
): Map<string, number> => {
|
||||
const uniqueSortedKeys = [...new Set(keys)].sort();
|
||||
|
||||
return new Map(uniqueSortedKeys.map((key, rank) => [key, rank]));
|
||||
};
|
||||
+19
-9
@@ -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<string, number>;
|
||||
}): 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,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user