From aa574baceabcea67b8d847d29f63740e5be607a5 Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:06:56 +0530 Subject: [PATCH] [Dashboards] use select option colors when grouping by SELECT/MULTI_SELECT fields (#16973) closes https://github.com/twentyhq/core-team-issues/issues/2031 https://github.com/user-attachments/assets/16fbfefd-107c-45a3-979c-1f9adbb9d912 --- .../GraphWidgetBarChart.stories.tsx | 27 ++++++ .../GraphWidgetLineChart.stories.tsx | 12 +++ .../GraphWidgetPieChart.stories.tsx | 22 +++++ .../components/GraphWidgetBarChart.tsx | 4 + .../GraphWidgetBarChartRenderer.tsx | 2 + .../hooks/__tests__/useBarChartData.test.ts | 15 +++- .../hooks/useBarChartData.ts | 22 ++++- .../hooks/useGraphBarChartWidgetData.ts | 2 + .../utils/sortTwoDimensionalBarChartData.ts | 30 +++++-- .../transformGroupByDataToBarChartData.ts | 3 + ...formOneDimensionalGroupByToBarChartData.ts | 38 +++++++-- ...formTwoDimensionalGroupByToBarChartData.ts | 5 +- .../components/GraphWidgetLineChart.tsx | 4 + .../GraphWidgetLineChartRenderer.tsx | 2 + .../hooks/__tests__/useLineChartData.test.ts | 10 +++ .../hooks/useGraphLineChartWidgetData.ts | 6 +- .../hooks/useLineChartData.ts | 9 +- .../utils/sortTwoDimensionalLineChartData.ts | 25 ++++++ .../transformGroupByDataToLineChartData.ts | 7 +- ...ormOneDimensionalGroupByToLineChartData.ts | 15 +++- ...ormTwoDimensionalGroupByToLineChartData.ts | 9 +- .../components/GraphWidgetPieChart.tsx | 4 + .../GraphWidgetPieChartRenderer.tsx | 2 + .../hooks/__tests__/usePieChartData.test.ts | 10 +++ .../hooks/useGraphPieChartWidgetData.ts | 2 + .../hooks/usePieChartData.ts | 9 +- ...transformGroupByDataToPieChartData.test.ts | 5 +- .../transformGroupByDataToPieChartData.ts | 38 +++++++-- .../widgets/graph/types/GraphColor.ts | 15 +--- .../widgets/graph/types/GraphColorMode.ts | 4 + .../__tests__/determineChartItemColor.test.ts | 84 ++++++++++++++++++ .../getSelectOptionColorForValue.test.ts | 85 +++++++++++++++++++ .../utils/__tests__/parseGraphColor.test.ts | 31 +++++++ .../graph/utils/determineChartItemColor.ts | 32 +++++++ .../graph/utils/determineGraphColorMode.ts | 24 ++++++ .../utils/getSelectOptionColorForValue.ts | 19 +++++ .../widgets/graph/utils/parseGraphColor.ts | 21 +++++ 37 files changed, 599 insertions(+), 55 deletions(-) create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/types/GraphColorMode.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/determineChartItemColor.test.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/getSelectOptionColorForValue.test.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/parseGraphColor.test.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/determineChartItemColor.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/determineGraphColorMode.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getSelectOptionColorForValue.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/parseGraphColor.ts diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetBarChart.stories.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetBarChart.stories.tsx index bc4e6a7fb0..d4a6af5124 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetBarChart.stories.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetBarChart.stories.tsx @@ -76,6 +76,17 @@ const meta: Meta = { seriesLabels: { control: 'object', }, + colorMode: { + control: 'select', + options: [ + 'automaticPalette', + 'explicitSingleColor', + 'selectFieldOptionColors', + ], + }, + }, + args: { + colorMode: 'automaticPalette', }, }; @@ -146,6 +157,7 @@ export const Default: Story = { yAxisLabel={args.yAxisLabel} id={args.id} groupMode={args.groupMode} + colorMode={args.colorMode} /> ), @@ -211,6 +223,7 @@ export const Revenue: Story = { yAxisLabel={args.yAxisLabel} id={args.id} groupMode={args.groupMode} + colorMode={args.colorMode} /> ), @@ -275,6 +288,7 @@ export const Grouped: Story = { xAxisLabel={args.xAxisLabel} yAxisLabel={args.yAxisLabel} id={args.id} + colorMode={args.colorMode} /> ), @@ -312,6 +326,7 @@ export const Horizontal: Story = { yAxisLabel={args.yAxisLabel} suffix={args.suffix} id={args.id} + colorMode={args.colorMode} /> ), @@ -362,6 +377,7 @@ export const WithValues: Story = { yAxisLabel={args.yAxisLabel} id={args.id} groupMode={args.groupMode} + colorMode={args.colorMode} /> ), @@ -433,6 +449,7 @@ export const WithCustomColors: Story = { yAxisLabel={args.yAxisLabel} id={args.id} groupMode={args.groupMode} + colorMode={args.colorMode} /> ), @@ -469,6 +486,7 @@ export const SingleSeries: Story = { xAxisLabel={args.xAxisLabel} yAxisLabel={args.yAxisLabel} id={args.id} + colorMode={args.colorMode} /> ), @@ -510,6 +528,7 @@ export const Currency: Story = { yAxisLabel={args.yAxisLabel} id={args.id} groupMode={args.groupMode} + colorMode={args.colorMode} /> ), @@ -550,6 +569,7 @@ export const NegativeValues: Story = { xAxisLabel={args.xAxisLabel} yAxisLabel={args.yAxisLabel} id={args.id} + colorMode={args.colorMode} /> ), @@ -618,6 +638,7 @@ export const MixedPositiveNegative: Story = { yAxisLabel={args.yAxisLabel} groupMode={args.groupMode} id={args.id} + colorMode={args.colorMode} /> ), @@ -650,6 +671,7 @@ export const AllNegative: Story = { xAxisLabel={args.xAxisLabel} yAxisLabel={args.yAxisLabel} id={args.id} + colorMode={args.colorMode} /> ), @@ -686,6 +708,7 @@ export const TemperatureData: Story = { yAxisLabel={args.yAxisLabel} suffix={args.suffix} id={args.id} + colorMode={args.colorMode} /> ), @@ -754,6 +777,7 @@ export const StackedNegative: Story = { xAxisLabel={args.xAxisLabel} yAxisLabel={args.yAxisLabel} id={args.id} + colorMode={args.colorMode} /> ), @@ -815,6 +839,7 @@ export const GroupedWithAllBarsTooltip: Story = { yAxisLabel={args.yAxisLabel} groupMode={args.groupMode} id={args.id} + colorMode={args.colorMode} /> ), @@ -870,6 +895,7 @@ export const GroupedDefaultTooltip: Story = { yAxisLabel={args.yAxisLabel} groupMode={args.groupMode} id={args.id} + colorMode={args.colorMode} /> ), @@ -925,6 +951,7 @@ export const Catalog: Story = { id={`bar-chart-catalog-${args.keys?.length ?? 0}-${ args.groupMode ?? 'grouped' }`} + colorMode={args.colorMode} /> ), diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetLineChart.stories.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetLineChart.stories.tsx index 6f0065346f..f2aa832b38 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetLineChart.stories.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetLineChart.stories.tsx @@ -66,6 +66,17 @@ const meta: Meta = { omitNullValues: { control: 'boolean', }, + colorMode: { + control: 'select', + options: [ + 'automaticPalette', + 'explicitSingleColor', + 'selectFieldOptionColors', + ], + }, + }, + args: { + colorMode: 'automaticPalette', }, }; @@ -95,6 +106,7 @@ const renderChart = (args: ChartArgs) => ( prefix={args.prefix} suffix={args.suffix} decimals={args.decimals} + colorMode={args.colorMode} /> ); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetPieChart.stories.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetPieChart.stories.tsx index 715c2d98df..af2ecb6340 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetPieChart.stories.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetPieChart.stories.tsx @@ -75,6 +75,17 @@ const meta: Meta = { showDataLabels: { control: 'boolean', }, + colorMode: { + control: 'select', + options: [ + 'automaticPalette', + 'explicitSingleColor', + 'selectFieldOptionColors', + ], + }, + }, + args: { + colorMode: 'automaticPalette', }, }; @@ -109,6 +120,7 @@ export const Default: Story = { id={args.id} objectMetadataItemId={mockObjectMetadataItemId} configuration={mockConfiguration} + colorMode={args.colorMode} /> ), @@ -132,6 +144,7 @@ export const WithCenterMetric: Story = { id={args.id} objectMetadataItemId={mockObjectMetadataItemId} configuration={mockConfiguration} + colorMode={args.colorMode} /> ), @@ -155,6 +168,7 @@ export const WithDataLabels: Story = { id={args.id} objectMetadataItemId={mockObjectMetadataItemId} configuration={mockConfiguration} + colorMode={args.colorMode} /> ), @@ -184,6 +198,7 @@ export const Revenue: Story = { id={args.id} objectMetadataItemId={mockObjectMetadataItemId} configuration={mockConfiguration} + colorMode={args.colorMode} /> ), @@ -212,6 +227,7 @@ export const TaskStatus: Story = { id={args.id} objectMetadataItemId={mockObjectMetadataItemId} configuration={mockConfiguration} + colorMode={args.colorMode} /> ), @@ -239,6 +255,7 @@ export const TwoSlices: Story = { id={args.id} objectMetadataItemId={mockObjectMetadataItemId} configuration={mockConfiguration} + colorMode={args.colorMode} /> ), @@ -271,6 +288,7 @@ export const ManySlices: Story = { id={args.id} objectMetadataItemId={mockObjectMetadataItemId} configuration={mockConfiguration} + colorMode={args.colorMode} /> ), @@ -299,6 +317,7 @@ export const WithoutLegend: Story = { id={args.id} objectMetadataItemId={mockObjectMetadataItemId} configuration={mockConfiguration} + colorMode={args.colorMode} /> ), @@ -328,6 +347,7 @@ export const MarketShare: Story = { id={args.id} objectMetadataItemId={mockObjectMetadataItemId} configuration={mockConfiguration} + colorMode={args.colorMode} /> ), @@ -357,6 +377,7 @@ export const Storage: Story = { id={args.id} objectMetadataItemId={mockObjectMetadataItemId} configuration={mockConfiguration} + colorMode={args.colorMode} /> ), @@ -421,6 +442,7 @@ export const Catalog: Story = { id={args.id} objectMetadataItemId={mockObjectMetadataItemId} configuration={mockConfiguration} + colorMode={args.colorMode} /> ), diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChart.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChart.tsx index 62c5e4eff1..d0ff6c471c 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChart.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChart.tsx @@ -18,6 +18,7 @@ import { getBarChartAxisConfigs } from '@/page-layout/widgets/graph/graphWidgetB import { getBarChartColor } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartColor'; import { getBarChartInnerPadding } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartInnerPadding'; import { getBarChartTickConfig } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartTickConfig'; +import { type GraphColorMode } from '@/page-layout/widgets/graph/types/GraphColorMode'; import { computeEffectiveValueRange } from '@/page-layout/widgets/graph/utils/computeEffectiveValueRange'; import { computeValueTickValues } from '@/page-layout/widgets/graph/utils/computeValueTickValues'; import { createGraphColorRegistry } from '@/page-layout/widgets/graph/utils/createGraphColorRegistry'; @@ -57,6 +58,7 @@ type GraphWidgetBarChartProps = { id: string; layout?: BarChartLayout; groupMode?: 'grouped' | 'stacked'; + colorMode: GraphColorMode; seriesLabels?: Record; rangeMin?: number; rangeMax?: number; @@ -86,6 +88,7 @@ export const GraphWidgetBarChart = ({ id, layout = BarChartLayout.VERTICAL, groupMode, + colorMode, seriesLabels, rangeMin, rangeMax, @@ -130,6 +133,7 @@ export const GraphWidgetBarChart = ({ series, colorRegistry, seriesLabels, + colorMode, }); const calculatedValueRange = diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartRenderer.tsx index 8a4bb75fc8..f64abbb9f8 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartRenderer.tsx @@ -47,6 +47,7 @@ export const GraphWidgetBarChartRenderer = () => { loading, hasTooManyGroups, formattedToRawLookup, + colorMode, objectMetadataItem, } = useGraphBarChartWidgetData({ objectMetadataItemId: widget.objectMetadataId, @@ -123,6 +124,7 @@ export const GraphWidgetBarChartRenderer = () => { showLegend={showLegend} layout={layout} groupMode={groupMode} + colorMode={colorMode} id={widget.id} displayType="shortNumber" rangeMin={configuration.rangeMin ?? undefined} diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/__tests__/useBarChartData.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/__tests__/useBarChartData.test.ts index fcf97fd28a..d79d1e35e8 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/__tests__/useBarChartData.test.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/__tests__/useBarChartData.test.ts @@ -76,6 +76,7 @@ describe('useBarChartData', () => { keys: ['sales', 'costs'], series: mockSeries, colorRegistry: mockColorRegistry, + colorMode: 'automaticPalette', }), ); @@ -91,6 +92,7 @@ describe('useBarChartData', () => { keys: ['sales', 'costs'], series: mockSeries, colorRegistry: mockColorRegistry, + colorMode: 'automaticPalette', }), ); @@ -119,6 +121,7 @@ describe('useBarChartData', () => { keys: ['sales', 'costs'], series: mockSeries, colorRegistry: mockColorRegistry, + colorMode: 'automaticPalette', }), ); @@ -150,6 +153,7 @@ describe('useBarChartData', () => { series: undefined, colorRegistry: mockColorRegistry, seriesLabels: { sales: 'Revenue', costs: 'Expenses' }, + colorMode: 'automaticPalette', }), ); @@ -165,6 +169,7 @@ describe('useBarChartData', () => { keys: ['sales', 'costs'], series: mockSeries, colorRegistry: mockColorRegistry, + colorMode: 'automaticPalette', }), ); @@ -179,6 +184,7 @@ describe('useBarChartData', () => { keys: [], series: mockSeries, colorRegistry: mockColorRegistry, + colorMode: 'automaticPalette', }), ); @@ -195,6 +201,7 @@ describe('useBarChartData', () => { series: undefined, colorRegistry: mockColorRegistry, seriesLabels: undefined, + colorMode: 'automaticPalette', }), ); @@ -210,6 +217,7 @@ describe('useBarChartData', () => { keys: ['sales', 'costs'], series: mockSeries, colorRegistry: mockColorRegistry, + colorMode: 'automaticPalette', }), ); @@ -217,7 +225,7 @@ describe('useBarChartData', () => { expect(result.current.legendItems[0]).toMatchObject({ id: 'sales', label: 'Sales', - color: 'green5', + color: 'greenSolid', }); }); @@ -231,6 +239,7 @@ describe('useBarChartData', () => { keys: ['sales', 'costs'], series: mockSeries, colorRegistry: mockColorRegistry, + colorMode: 'automaticPalette', }), ); @@ -249,6 +258,7 @@ describe('useBarChartData', () => { keys: ['sales', 'costs'], series: mockSeries, colorRegistry: mockColorRegistry, + colorMode: 'automaticPalette', }), ); @@ -265,6 +275,7 @@ describe('useBarChartData', () => { keys: ['sales', 'costs'], series: mockSeries, colorRegistry: mockColorRegistry, + colorMode: 'automaticPalette', }), ); @@ -282,6 +293,7 @@ describe('useBarChartData', () => { keys: ['sales', 'costs'], series: mockSeries, colorRegistry: mockColorRegistry, + colorMode: 'automaticPalette', }), ); @@ -301,6 +313,7 @@ describe('useBarChartData', () => { keys: ['sales', 'costs'], series: mockSeries, colorRegistry: mockColorRegistry, + colorMode: 'automaticPalette', }), ); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartData.ts index 6924a48430..b6046686f4 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartData.ts @@ -3,8 +3,10 @@ import { type BarChartConfig } from '@/page-layout/widgets/graph/graphWidgetBarC import { type BarChartEnrichedKey } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartEnrichedKey'; import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries'; 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 { getColorScheme } from '@/page-layout/widgets/graph/utils/getColorScheme'; +import { parseGraphColor } from '@/page-layout/widgets/graph/utils/parseGraphColor'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { type BarDatum } from '@nivo/bar'; import { useMemo } from 'react'; @@ -18,6 +20,7 @@ type UseBarChartDataProps = { colorRegistry: GraphColorRegistry; seriesLabels?: Record; groupMode?: 'grouped' | 'stacked'; + colorMode: GraphColorMode; }; export const useBarChartData = ({ @@ -27,6 +30,7 @@ export const useBarChartData = ({ series, colorRegistry, seriesLabels, + colorMode, }: UseBarChartDataProps) => { const hiddenLegendIds = useRecoilComponentValue( graphWidgetHiddenLegendIdsComponentState, @@ -37,13 +41,15 @@ export const useBarChartData = ({ [series], ); + const shouldApplyGradient = colorMode === 'explicitSingleColor'; + const allEnrichedKeys: BarChartEnrichedKey[] = keys.map((key, index) => { const seriesConfig = seriesConfigMap.get(key); const colorScheme = getColorScheme({ registry: colorRegistry, colorName: seriesConfig?.color, fallbackIndex: index, - totalGroups: keys.length, + totalGroups: shouldApplyGradient ? keys.length : undefined, }); return { @@ -68,21 +74,31 @@ export const useBarChartData = ({ const barConfigs = useMemo((): BarChartConfig[] => { return data.flatMap((dataPoint) => { const indexValue = dataPoint[indexBy]; + const datumColor = parseGraphColor(dataPoint.color as string | undefined); + return visibleKeys.flatMap((key): BarChartConfig[] => { const enrichedKey = allEnrichedKeys.find((ek) => ek.key === key); if (!isDefined(enrichedKey)) { return []; } + + const colorScheme = isDefined(datumColor) + ? getColorScheme({ + registry: colorRegistry, + colorName: datumColor, + }) + : enrichedKey.colorScheme; + return [ { key, indexValue, - colorScheme: enrichedKey.colorScheme, + colorScheme, }, ]; }); }); - }, [data, indexBy, visibleKeys, allEnrichedKeys]); + }, [data, indexBy, visibleKeys, allEnrichedKeys, colorRegistry]); return { seriesConfigMap, diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useGraphBarChartWidgetData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useGraphBarChartWidgetData.ts index ac5610761b..616cc46b89 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useGraphBarChartWidgetData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/hooks/useGraphBarChartWidgetData.ts @@ -4,6 +4,7 @@ import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarC import { getBarChartQueryLimit } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartQueryLimit'; import { transformGroupByDataToBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/transformGroupByDataToBarChartData'; import { useGraphWidgetGroupByQuery } from '@/page-layout/widgets/graph/hooks/useGraphWidgetGroupByQuery'; +import { type GraphColorMode } from '@/page-layout/widgets/graph/types/GraphColorMode'; import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue'; import { useUserFirstDayOfTheWeek } from '@/ui/input/components/internal/date/hooks/useUserFirstDayOfTheWeek'; import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone'; @@ -33,6 +34,7 @@ type UseGraphBarChartWidgetDataResult = { error?: Error; hasTooManyGroups: boolean; formattedToRawLookup: Map; + colorMode: GraphColorMode; objectMetadataItem: ReturnType< typeof useObjectMetadataItemById >['objectMetadataItem']; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/sortTwoDimensionalBarChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/sortTwoDimensionalBarChartData.ts index 76465215b0..16fe488d26 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/sortTwoDimensionalBarChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/sortTwoDimensionalBarChartData.ts @@ -1,8 +1,11 @@ import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem'; import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries'; import { sortBarChartDataBySecondaryDimensionSum } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/sortBarChartDataBySecondaryDimensionSum'; -import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor'; +import { type GraphColorMode } from '@/page-layout/widgets/graph/types/GraphColorMode'; +import { determineGraphColorMode } from '@/page-layout/widgets/graph/utils/determineGraphColorMode'; import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue'; +import { determineChartItemColor } from '@/page-layout/widgets/graph/utils/determineChartItemColor'; +import { parseGraphColor } from '@/page-layout/widgets/graph/utils/parseGraphColor'; import { sortSecondaryAxisData } from '@/page-layout/widgets/graph/utils/sortSecondaryAxisData'; import { sortTwoDimensionalChartPrimaryAxisDataByFieldOrManuallyIfNeeded } from '@/page-layout/widgets/graph/utils/sortTwoDimensionalChartPrimaryAxisDataByFieldOrManuallyIfNeeded'; import { type BarDatum } from '@nivo/bar'; @@ -28,6 +31,7 @@ type SortTwoDimensionalBarChartDataResult = { sortedData: BarDatum[]; sortedKeys: string[]; sortedSeries: BarChartSeries[]; + colorMode: GraphColorMode; }; export const sortTwoDimensionalBarChartData = ({ @@ -59,11 +63,19 @@ export const sortTwoDimensionalBarChartData = ({ subFieldName: secondaryAxisSubFieldName, }); - const sortedSeries: BarChartSeries[] = sortedKeys.map((key) => ({ - key, - label: key, - color: color as GraphColor, - })); + const sortedSeries: BarChartSeries[] = sortedKeys.map((key) => { + const rawValue = secondaryAxisFormattedToRawLookup?.get(key); + + return { + key, + label: key, + color: determineChartItemColor({ + configurationColor: parseGraphColor(color), + selectOptions: secondaryAxisSelectFieldOptions, + rawValue: isDefined(rawValue) ? String(rawValue) : key, + }), + }; + }); let sortedData: BarDatum[] = data; @@ -90,9 +102,15 @@ export const sortTwoDimensionalBarChartData = ({ } } + const colorMode = determineGraphColorMode({ + configurationColor: color, + selectFieldOptions: secondaryAxisSelectFieldOptions, + }); + return { sortedData, sortedKeys, sortedSeries, + colorMode, }; }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/transformGroupByDataToBarChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/transformGroupByDataToBarChartData.ts index add9397d65..4abf73e0bb 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/transformGroupByDataToBarChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/transformGroupByDataToBarChartData.ts @@ -8,6 +8,7 @@ import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarC import { fillDateGapsInBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/fillDateGapsInBarChartData'; import { transformOneDimensionalGroupByToBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/transformOneDimensionalGroupByToBarChartData'; import { transformTwoDimensionalGroupByToBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/transformTwoDimensionalGroupByToBarChartData'; +import { type GraphColorMode } from '@/page-layout/widgets/graph/types/GraphColorMode'; import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult'; import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue'; import { filterGroupByResults } from '@/page-layout/widgets/graph/utils/filterGroupByResults'; @@ -47,6 +48,7 @@ type TransformGroupByDataToBarChartDataResult = { layout?: BarChartLayout; hasTooManyGroups: boolean; formattedToRawLookup: Map; + colorMode: GraphColorMode; }; const EMPTY_BAR_CHART_RESULT: Omit< @@ -62,6 +64,7 @@ const EMPTY_BAR_CHART_RESULT: Omit< layout: BarChartLayout.VERTICAL, hasTooManyGroups: false, formattedToRawLookup: new Map(), + colorMode: 'automaticPalette', }; export const transformGroupByDataToBarChartData = ({ diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/transformOneDimensionalGroupByToBarChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/transformOneDimensionalGroupByToBarChartData.ts index c650f4b2f4..9f7f7139f6 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/transformOneDimensionalGroupByToBarChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/transformOneDimensionalGroupByToBarChartData.ts @@ -1,19 +1,22 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { GRAPH_DEFAULT_COLOR } from '@/page-layout/widgets/graph/constants/GraphDefaultColor.constant'; import { BAR_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartConstants'; import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries'; import { applyCumulativeTransformToBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/applyCumulativeTransformToBarChartData'; -import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor'; +import { type GraphColorMode } from '@/page-layout/widgets/graph/types/GraphColorMode'; +import { determineGraphColorMode } from '@/page-layout/widgets/graph/utils/determineGraphColorMode'; import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult'; import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue'; +import { determineChartItemColor } from '@/page-layout/widgets/graph/utils/determineChartItemColor'; import { getFieldKey } from '@/page-layout/widgets/graph/utils/getFieldKey'; +import { parseGraphColor } from '@/page-layout/widgets/graph/utils/parseGraphColor'; import { processOneDimensionalGroupByResults } from '@/page-layout/widgets/graph/utils/processOneDimensionalGroupByResults'; import { sortChartDataIfNeeded } from '@/page-layout/widgets/graph/utils/sortChartDataIfNeeded'; import { type BarDatum } from '@nivo/bar'; import { - type FirstDayOfTheWeek, + isDefined, isFieldMetadataSelectKind, + type FirstDayOfTheWeek, } from 'twenty-shared/utils'; import { type BarChartConfiguration } from '~/generated/graphql'; @@ -36,6 +39,7 @@ type TransformOneDimensionalGroupByToBarChartDataResult = { series: BarChartSeries[]; hasTooManyGroups: boolean; formattedToRawLookup: Map; + colorMode: GraphColorMode; }; export const transformOneDimensionalGroupByToBarChartData = ({ @@ -73,10 +77,21 @@ export const transformOneDimensionalGroupByToBarChartData = ({ }); const unsortedData: BarDatum[] = processedDataPoints.map( - ({ xValue, aggregateValue }) => ({ - [indexByKey]: xValue, - [aggregateValueKey]: aggregateValue, - }), + ({ xValue, rawXValue, aggregateValue }) => { + const color = determineChartItemColor({ + configurationColor: parseGraphColor(configuration.color), + selectOptions: isFieldMetadataSelectKind(groupByFieldX.type) + ? groupByFieldX.options + : undefined, + rawValue: isDefined(rawXValue) ? String(rawXValue) : null, + }); + + return { + [indexByKey]: xValue, + [aggregateValueKey]: aggregateValue, + ...(isDefined(color) && { color }), + }; + }, ); const sortedData = sortChartDataIfNeeded({ @@ -100,7 +115,6 @@ export const transformOneDimensionalGroupByToBarChartData = ({ { key: aggregateValueKey, label: aggregateField.label, - color: (configuration.color ?? GRAPH_DEFAULT_COLOR) as GraphColor, }, ]; @@ -113,6 +127,13 @@ export const transformOneDimensionalGroupByToBarChartData = ({ }) : limitedSortedData; + const colorMode = determineGraphColorMode({ + configurationColor: configuration.color, + selectFieldOptions: isFieldMetadataSelectKind(groupByFieldX.type) + ? groupByFieldX.options + : undefined, + }); + return { data: finalData, indexBy: indexByKey, @@ -121,5 +142,6 @@ export const transformOneDimensionalGroupByToBarChartData = ({ hasTooManyGroups: rawResults.length > BAR_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_BARS, formattedToRawLookup, + colorMode, }; }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/transformTwoDimensionalGroupByToBarChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/transformTwoDimensionalGroupByToBarChartData.ts index fbbaa38038..1563e1025d 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/transformTwoDimensionalGroupByToBarChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/utils/transformTwoDimensionalGroupByToBarChartData.ts @@ -5,6 +5,7 @@ import { applyCumulativeTransformToTwoDimensionalBarChartData } from '@/page-lay import { buildTwoDimensionalBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/buildTwoDimensionalBarChartData'; import { limitTwoDimensionalBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/limitTwoDimensionalBarChartData'; import { sortTwoDimensionalBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/sortTwoDimensionalBarChartData'; +import { type GraphColorMode } from '@/page-layout/widgets/graph/types/GraphColorMode'; import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult'; import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue'; import { getFieldKey } from '@/page-layout/widgets/graph/utils/getFieldKey'; @@ -34,6 +35,7 @@ type TransformTwoDimensionalGroupByToBarChartDataResult = { series: BarChartSeries[]; hasTooManyGroups: boolean; formattedToRawLookup: Map; + colorMode: GraphColorMode; }; export const transformTwoDimensionalGroupByToBarChartData = ({ @@ -72,7 +74,7 @@ export const transformTwoDimensionalGroupByToBarChartData = ({ indexByKey, }); - const { sortedData, sortedKeys, sortedSeries } = + const { sortedData, sortedKeys, sortedSeries, colorMode } = sortTwoDimensionalBarChartData({ data: unsortedData, keys: Array.from(yValues), @@ -113,5 +115,6 @@ export const transformTwoDimensionalGroupByToBarChartData = ({ series: limitedSeries, hasTooManyGroups, formattedToRawLookup, + colorMode, }; }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChart.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChart.tsx index b1a81fe5f1..55980eaae8 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChart.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChart.tsx @@ -19,6 +19,7 @@ import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLin import { calculateValueRangeFromLineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/calculateValueRangeFromLineChartSeries'; import { getLineChartAxisBottomConfig } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/getLineChartAxisBottomConfig'; import { getLineChartAxisLeftConfig } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/getLineChartAxisLeftConfig'; +import { type GraphColorMode } from '@/page-layout/widgets/graph/types/GraphColorMode'; import { computeEffectiveValueRange } from '@/page-layout/widgets/graph/utils/computeEffectiveValueRange'; import { computeValueTickValues } from '@/page-layout/widgets/graph/utils/computeValueTickValues'; import { createGraphColorRegistry } from '@/page-layout/widgets/graph/utils/createGraphColorRegistry'; @@ -60,6 +61,7 @@ type GraphWidgetLineChartProps = { rangeMax?: number; omitNullValues?: boolean; groupMode?: 'stacked'; + colorMode: GraphColorMode; onSliceClick?: (point: Point) => void; } & GraphValueFormatOptions; @@ -86,6 +88,7 @@ export const GraphWidgetLineChart = ({ omitNullValues: _omitNullValues = false, displayType, groupMode, + colorMode, decimals, prefix, suffix, @@ -111,6 +114,7 @@ export const GraphWidgetLineChart = ({ data, colorRegistry, id, + colorMode, }); const calculatedValueRange = diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChartRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChartRenderer.tsx index 173d129e6b..dd666a918f 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChartRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChartRenderer.tsx @@ -43,6 +43,7 @@ export const GraphWidgetLineChartRenderer = () => { hasTooManyGroups, loading, formattedToRawLookup, + colorMode, objectMetadataItem, } = useGraphLineChartWidgetData({ objectMetadataItemId: widget.objectMetadataId, @@ -124,6 +125,7 @@ export const GraphWidgetLineChartRenderer = () => { rangeMax={configuration.rangeMax ?? undefined} omitNullValues={configuration.omitNullValues ?? false} groupMode={groupMode} + colorMode={colorMode} displayType="shortNumber" onSliceClick={isPageLayoutInEditMode ? undefined : handlePointClick} /> diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/__tests__/useLineChartData.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/__tests__/useLineChartData.test.ts index 044f11df9e..01158be20b 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/__tests__/useLineChartData.test.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/__tests__/useLineChartData.test.ts @@ -85,6 +85,7 @@ describe('useLineChartData', () => { data: mockData, colorRegistry: mockColorRegistry, id: 'test-chart', + colorMode: 'automaticPalette', }), ); @@ -105,6 +106,7 @@ describe('useLineChartData', () => { data: mockData, colorRegistry: mockColorRegistry, id: 'test-chart', + colorMode: 'automaticPalette', }), ); @@ -134,6 +136,7 @@ describe('useLineChartData', () => { data: mockData, colorRegistry: mockColorRegistry, id: 'test-chart', + colorMode: 'automaticPalette', }), ); @@ -157,6 +160,7 @@ describe('useLineChartData', () => { data: [], colorRegistry: mockColorRegistry, id: 'test-chart', + colorMode: 'automaticPalette', }), ); @@ -179,6 +183,7 @@ describe('useLineChartData', () => { data: dataWithoutLabel, colorRegistry: mockColorRegistry, id: 'test-chart', + colorMode: 'automaticPalette', }), ); @@ -193,6 +198,7 @@ describe('useLineChartData', () => { data: mockData, colorRegistry: mockColorRegistry, id: 'test-chart', + colorMode: 'automaticPalette', }), ); @@ -210,6 +216,7 @@ describe('useLineChartData', () => { data: mockData, colorRegistry: mockColorRegistry, id: 'test-chart', + colorMode: 'automaticPalette', }), ); @@ -224,6 +231,7 @@ describe('useLineChartData', () => { data: mockData, colorRegistry: mockColorRegistry, id: 'test-chart', + colorMode: 'automaticPalette', }), ); @@ -239,6 +247,7 @@ describe('useLineChartData', () => { data: mockData, colorRegistry: mockColorRegistry, id: 'test-chart', + colorMode: 'automaticPalette', }), ); @@ -255,6 +264,7 @@ describe('useLineChartData', () => { data: mockData, colorRegistry: mockColorRegistry, id: 'test-chart', + colorMode: 'automaticPalette', }), ); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/useGraphLineChartWidgetData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/useGraphLineChartWidgetData.ts index aa7387db62..6ead53f3d3 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/useGraphLineChartWidgetData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/useGraphLineChartWidgetData.ts @@ -2,9 +2,10 @@ import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMeta import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeries'; import { getLineChartQueryLimit } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/getLineChartQueryLimit'; -import { useGraphWidgetGroupByQuery } from '@/page-layout/widgets/graph/hooks/useGraphWidgetGroupByQuery'; -import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue'; import { transformGroupByDataToLineChartData } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/transformGroupByDataToLineChartData'; +import { useGraphWidgetGroupByQuery } from '@/page-layout/widgets/graph/hooks/useGraphWidgetGroupByQuery'; +import { type GraphColorMode } from '@/page-layout/widgets/graph/types/GraphColorMode'; +import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue'; import { useUserFirstDayOfTheWeek } from '@/ui/input/components/internal/date/hooks/useUserFirstDayOfTheWeek'; import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone'; import { useMemo } from 'react'; @@ -23,6 +24,7 @@ type UseGraphLineChartWidgetDataResult = { showLegend: boolean; hasTooManyGroups: boolean; formattedToRawLookup: Map; + colorMode: GraphColorMode; loading: boolean; error?: Error; objectMetadataItem: ReturnType< diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/useLineChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/useLineChartData.ts index 8b54ea1a09..a1942ffdab 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/useLineChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/hooks/useLineChartData.ts @@ -2,6 +2,7 @@ import { type GraphWidgetLegendItem } from '@/page-layout/widgets/graph/componen import { type LineChartEnrichedSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartEnrichedSeries'; import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeries'; 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 { getColorScheme } from '@/page-layout/widgets/graph/utils/getColorScheme'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; @@ -12,24 +13,28 @@ type UseLineChartDataProps = { data: LineChartSeries[]; colorRegistry: GraphColorRegistry; id: string; + colorMode: GraphColorMode; }; export const useLineChartData = ({ data, colorRegistry, id, + colorMode, }: UseLineChartDataProps) => { const hiddenLegendIds = useRecoilComponentValue( graphWidgetHiddenLegendIdsComponentState, ); const allEnrichedSeries = useMemo((): LineChartEnrichedSeries[] => { + const shouldApplyGradient = colorMode === 'explicitSingleColor'; + return data.map((series, index) => { const colorScheme = getColorScheme({ registry: colorRegistry, colorName: series.color, fallbackIndex: index, - totalGroups: data.length, + totalGroups: shouldApplyGradient ? data.length : undefined, }); const sanitizedSeriesId = series.id @@ -40,7 +45,7 @@ export const useLineChartData = ({ return { ...series, colorScheme, areaFillId, label }; }); - }, [data, colorRegistry, id]); + }, [data, colorRegistry, id, colorMode]); const legendItems: GraphWidgetLegendItem[] = allEnrichedSeries.map( (series) => ({ diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/utils/sortTwoDimensionalLineChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/utils/sortTwoDimensionalLineChartData.ts index 20962fc700..986a1a51fd 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/utils/sortTwoDimensionalLineChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/utils/sortTwoDimensionalLineChartData.ts @@ -2,7 +2,11 @@ import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetad import { type LineChartDataPoint } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartDataPoint'; import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeries'; import { sortLineChartDataBySecondaryDimensionSum } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/sortLineChartDataBySecondaryDimensionSum'; +import { type GraphColorMode } from '@/page-layout/widgets/graph/types/GraphColorMode'; +import { determineGraphColorMode } from '@/page-layout/widgets/graph/utils/determineGraphColorMode'; import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue'; +import { determineChartItemColor } from '@/page-layout/widgets/graph/utils/determineChartItemColor'; +import { parseGraphColor } from '@/page-layout/widgets/graph/utils/parseGraphColor'; import { sortSecondaryAxisData } from '@/page-layout/widgets/graph/utils/sortSecondaryAxisData'; import { sortTwoDimensionalChartPrimaryAxisDataByFieldOrManuallyIfNeeded } from '@/page-layout/widgets/graph/utils/sortTwoDimensionalChartPrimaryAxisDataByFieldOrManuallyIfNeeded'; import { type CompositeFieldSubFieldName } from 'twenty-shared/types'; @@ -23,6 +27,7 @@ type SortTwoDimensionalLineChartDataConfiguration = { type SortTwoDimensionalLineChartDataResult = { sortedSeries: LineChartSeries[]; + colorMode: GraphColorMode; }; export const sortTwoDimensionalLineChartData = ({ @@ -32,6 +37,7 @@ export const sortTwoDimensionalLineChartData = ({ primaryAxisManualSortOrder, secondaryAxisOrderBy, secondaryAxisManualSortOrder, + color, }, primaryAxisFormattedToRawLookup, primaryAxisSelectFieldOptions, @@ -83,7 +89,26 @@ export const sortTwoDimensionalLineChartData = ({ subFieldName: secondaryAxisSubFieldName, }); + sortedSeries = sortedSeries.map((seriesItem) => { + const rawValue = secondaryAxisFormattedToRawLookup?.get(seriesItem.id); + + return { + ...seriesItem, + color: determineChartItemColor({ + configurationColor: parseGraphColor(color), + selectOptions: secondaryAxisSelectFieldOptions, + rawValue: isDefined(rawValue) ? String(rawValue) : seriesItem.id, + }), + }; + }); + + const colorMode = determineGraphColorMode({ + configurationColor: color, + selectFieldOptions: secondaryAxisSelectFieldOptions, + }); + return { sortedSeries, + colorMode, }; }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/utils/transformGroupByDataToLineChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/utils/transformGroupByDataToLineChartData.ts index 88d40a5639..e04ce09e1b 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/utils/transformGroupByDataToLineChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/utils/transformGroupByDataToLineChartData.ts @@ -4,12 +4,13 @@ import { getAggregateOperationLabel } from '@/object-record/record-board/record- import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations'; import { getGroupByQueryResultGqlFieldName } from '@/page-layout/utils/getGroupByQueryResultGqlFieldName'; import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeries'; +import { transformOneDimensionalGroupByToLineChartData } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/transformOneDimensionalGroupByToLineChartData'; +import { transformTwoDimensionalGroupByToLineChartData } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/transformTwoDimensionalGroupByToLineChartData'; +import { type GraphColorMode } from '@/page-layout/widgets/graph/types/GraphColorMode'; import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult'; import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue'; import { filterGroupByResults } from '@/page-layout/widgets/graph/utils/filterGroupByResults'; import { isRelationNestedFieldDateKind } from '@/page-layout/widgets/graph/utils/isRelationNestedFieldDateKind'; -import { transformOneDimensionalGroupByToLineChartData } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/transformOneDimensionalGroupByToLineChartData'; -import { transformTwoDimensionalGroupByToLineChartData } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/transformTwoDimensionalGroupByToLineChartData'; import { type FirstDayOfTheWeek, isDefined, @@ -38,6 +39,7 @@ type TransformGroupByDataToLineChartDataResult = { showLegend: boolean; hasTooManyGroups: boolean; formattedToRawLookup: Map; + colorMode: GraphColorMode; }; const EMPTY_LINE_CHART_RESULT: Omit< @@ -49,6 +51,7 @@ const EMPTY_LINE_CHART_RESULT: Omit< showLegend: true, hasTooManyGroups: false, formattedToRawLookup: new Map(), + colorMode: 'automaticPalette', }; export const transformGroupByDataToLineChartData = ({ diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/utils/transformOneDimensionalGroupByToLineChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/utils/transformOneDimensionalGroupByToLineChartData.ts index a5e1d5e2ab..65d26b8f9a 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/utils/transformOneDimensionalGroupByToLineChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/utils/transformOneDimensionalGroupByToLineChartData.ts @@ -5,9 +5,11 @@ import { LINE_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetLin import { type LineChartDataPoint } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartDataPoint'; import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeries'; import { applyCumulativeTransformToLineChartData } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/applyCumulativeTransformToLineChartData'; -import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor'; +import { type GraphColorMode } from '@/page-layout/widgets/graph/types/GraphColorMode'; +import { determineGraphColorMode } from '@/page-layout/widgets/graph/utils/determineGraphColorMode'; import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult'; import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue'; +import { parseGraphColor } from '@/page-layout/widgets/graph/utils/parseGraphColor'; import { processOneDimensionalGroupByResults } from '@/page-layout/widgets/graph/utils/processOneDimensionalGroupByResults'; import { sortChartDataIfNeeded } from '@/page-layout/widgets/graph/utils/sortChartDataIfNeeded'; import { @@ -32,6 +34,7 @@ type TransformOneDimensionalGroupByToLineChartDataResult = { series: LineChartSeries[]; hasTooManyGroups: boolean; formattedToRawLookup: Map; + colorMode: GraphColorMode; }; export const transformOneDimensionalGroupByToLineChartData = ({ @@ -94,15 +97,23 @@ export const transformOneDimensionalGroupByToLineChartData = ({ { id: aggregateField.name, label: aggregateField.label, - color: (configuration.color ?? GRAPH_DEFAULT_COLOR) as GraphColor, + color: parseGraphColor(configuration.color) ?? GRAPH_DEFAULT_COLOR, data: transformedData, }, ]; + const colorMode = determineGraphColorMode({ + configurationColor: configuration.color, + selectFieldOptions: isFieldMetadataSelectKind(groupByFieldX.type) + ? groupByFieldX.options + : undefined, + }); + return { series, hasTooManyGroups: rawResults.length > LINE_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_DATA_POINTS, formattedToRawLookup, + colorMode, }; }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/utils/transformTwoDimensionalGroupByToLineChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/utils/transformTwoDimensionalGroupByToLineChartData.ts index 8a669caa76..de687f0420 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/utils/transformTwoDimensionalGroupByToLineChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/utils/transformTwoDimensionalGroupByToLineChartData.ts @@ -6,9 +6,10 @@ import { applyCumulativeTransformToLineChartData } from '@/page-layout/widgets/g import { buildTwoDimensionalLineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/buildTwoDimensionalLineChartSeries'; import { limitTwoDimensionalLineChartData } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/limitTwoDimensionalLineChartData'; import { sortTwoDimensionalLineChartData } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/sortTwoDimensionalLineChartData'; -import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor'; +import { type GraphColorMode } from '@/page-layout/widgets/graph/types/GraphColorMode'; import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult'; import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue'; +import { parseGraphColor } from '@/page-layout/widgets/graph/utils/parseGraphColor'; import { processTwoDimensionalGroupByResults } from '@/page-layout/widgets/graph/utils/processTwoDimensionalGroupByResults'; import { type CompositeFieldSubFieldName } from 'twenty-shared/types'; import { type FirstDayOfTheWeek } from 'twenty-shared/utils'; @@ -31,6 +32,7 @@ type TransformTwoDimensionalGroupByToLineChartDataResult = { series: LineChartSeries[]; hasTooManyGroups: boolean; formattedToRawLookup: Map; + colorMode: GraphColorMode; }; export const transformTwoDimensionalGroupByToLineChartData = ({ @@ -61,10 +63,10 @@ export const transformTwoDimensionalGroupByToLineChartData = ({ const { unsortedSeries } = buildTwoDimensionalLineChartSeries({ processedDataPoints, - color: configuration.color as GraphColor, + color: parseGraphColor(configuration.color), }); - const { sortedSeries } = sortTwoDimensionalLineChartData({ + const { sortedSeries, colorMode } = sortTwoDimensionalLineChartData({ series: unsortedSeries, configuration, primaryAxisFormattedToRawLookup: formattedToRawLookup, @@ -99,5 +101,6 @@ export const transformTwoDimensionalGroupByToLineChartData = ({ series: finalSeries, hasTooManyGroups, formattedToRawLookup, + colorMode, }; }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart.tsx index e4228020a7..c8c348eb01 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart.tsx @@ -9,6 +9,7 @@ import { usePieChartData } from '@/page-layout/widgets/graph/graphWidgetPieChart import { graphWidgetPieTooltipComponentState } from '@/page-layout/widgets/graph/graphWidgetPieChart/states/graphWidgetPieTooltipComponentState'; import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartDataItem'; import { getPieChartFormattedValue } from '@/page-layout/widgets/graph/graphWidgetPieChart/utils/getPieChartFormattedValue'; +import { type GraphColorMode } from '@/page-layout/widgets/graph/types/GraphColorMode'; import { createGraphColorRegistry } from '@/page-layout/widgets/graph/utils/createGraphColorRegistry'; import { type GraphValueFormatOptions } from '@/page-layout/widgets/graph/utils/graphFormatters'; import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; @@ -34,6 +35,7 @@ type GraphWidgetPieChartProps = { id: string; objectMetadataItemId: string; configuration: PieChartConfiguration; + colorMode: GraphColorMode; onSliceClick?: (datum: PieChartDataItem) => void; showDataLabels?: boolean; showCenterMetric?: boolean; @@ -72,6 +74,7 @@ export const GraphWidgetPieChart = ({ id, objectMetadataItemId, configuration, + colorMode, displayType, decimals, prefix, @@ -99,6 +102,7 @@ export const GraphWidgetPieChart = ({ const { enrichedData, legendItems } = usePieChartData({ data, colorRegistry, + colorMode, }); const handleSliceMove = useCallback( diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChartRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChartRenderer.tsx index f27608116a..6e00c46fe7 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChartRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChartRenderer.tsx @@ -40,6 +40,7 @@ export const GraphWidgetPieChartRenderer = () => { showDataLabels, showCenterMetric, formattedToRawLookup, + colorMode, } = useGraphPieChartWidgetData({ objectMetadataItemId: widget.objectMetadataId, configuration: widget.configuration, @@ -98,6 +99,7 @@ export const GraphWidgetPieChartRenderer = () => { objectMetadataItemId={widget.objectMetadataId} configuration={widget.configuration} showLegend={showLegend} + colorMode={colorMode} displayType="shortNumber" onSliceClick={isPageLayoutInEditMode ? undefined : handleSliceClick} showDataLabels={showDataLabels} diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/hooks/__tests__/usePieChartData.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/hooks/__tests__/usePieChartData.test.ts index 08e56de61d..6462925cab 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/hooks/__tests__/usePieChartData.test.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/hooks/__tests__/usePieChartData.test.ts @@ -67,6 +67,7 @@ describe('usePieChartData', () => { usePieChartData({ data: mockData, colorRegistry: mockColorRegistry, + colorMode: 'automaticPalette', }), ); @@ -86,6 +87,7 @@ describe('usePieChartData', () => { usePieChartData({ data: [], colorRegistry: mockColorRegistry, + colorMode: 'automaticPalette', }), ); @@ -99,6 +101,7 @@ describe('usePieChartData', () => { usePieChartData({ data: singleData, colorRegistry: mockColorRegistry, + colorMode: 'automaticPalette', }), ); @@ -110,6 +113,7 @@ describe('usePieChartData', () => { usePieChartData({ data: mockData, colorRegistry: mockColorRegistry, + colorMode: 'automaticPalette', }), ); @@ -122,6 +126,7 @@ describe('usePieChartData', () => { usePieChartData({ data: mockData, colorRegistry: mockColorRegistry, + colorMode: 'automaticPalette', }), ); @@ -140,6 +145,7 @@ describe('usePieChartData', () => { usePieChartData({ data: mockData, colorRegistry: mockColorRegistry, + colorMode: 'automaticPalette', }), ); @@ -157,6 +163,7 @@ describe('usePieChartData', () => { usePieChartData({ data: mockData, colorRegistry: mockColorRegistry, + colorMode: 'automaticPalette', }), ); @@ -170,6 +177,7 @@ describe('usePieChartData', () => { usePieChartData({ data: mockData, colorRegistry: mockColorRegistry, + colorMode: 'automaticPalette', }), ); @@ -184,6 +192,7 @@ describe('usePieChartData', () => { usePieChartData({ data: mockData, colorRegistry: mockColorRegistry, + colorMode: 'automaticPalette', }), ); @@ -199,6 +208,7 @@ describe('usePieChartData', () => { usePieChartData({ data: mockData, colorRegistry: mockColorRegistry, + colorMode: 'automaticPalette', }), ); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/hooks/useGraphPieChartWidgetData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/hooks/useGraphPieChartWidgetData.ts index f84743e24a..03490479ad 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/hooks/useGraphPieChartWidgetData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/hooks/useGraphPieChartWidgetData.ts @@ -5,6 +5,7 @@ import { PIE_CHART_MAXIMUM_NUMBER_OF_SLICES } from '@/page-layout/widgets/graph/ import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartDataItem'; import { transformGroupByDataToPieChartData } from '@/page-layout/widgets/graph/graphWidgetPieChart/utils/transformGroupByDataToPieChartData'; import { useGraphWidgetGroupByQuery } from '@/page-layout/widgets/graph/hooks/useGraphWidgetGroupByQuery'; +import { type GraphColorMode } from '@/page-layout/widgets/graph/types/GraphColorMode'; import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue'; import { useUserFirstDayOfTheWeek } from '@/ui/input/components/internal/date/hooks/useUserFirstDayOfTheWeek'; import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone'; @@ -24,6 +25,7 @@ type UseGraphPieChartWidgetDataResult = { hasTooManyGroups: boolean; objectMetadataItem: ObjectMetadataItem; formattedToRawLookup: Map; + colorMode: GraphColorMode; showDataLabels: boolean; showCenterMetric: boolean; }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/hooks/usePieChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/hooks/usePieChartData.ts index 3fa3b885db..edec855d09 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/hooks/usePieChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/hooks/usePieChartData.ts @@ -3,6 +3,7 @@ import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPi import { type PieChartEnrichedData } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartEnrichedData'; import { calculatePieChartPercentage } from '@/page-layout/widgets/graph/graphWidgetPieChart/utils/calculatePieChartPercentage'; 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 { getColorScheme } from '@/page-layout/widgets/graph/utils/getColorScheme'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; @@ -11,11 +12,13 @@ import { useMemo } from 'react'; type UsePieChartDataProps = { data: PieChartDataItem[]; colorRegistry: GraphColorRegistry; + colorMode: GraphColorMode; }; export const usePieChartData = ({ data, colorRegistry, + colorMode, }: UsePieChartDataProps) => { const hiddenLegendIds = useRecoilComponentValue( graphWidgetHiddenLegendIdsComponentState, @@ -24,12 +27,14 @@ export const usePieChartData = ({ const allEnrichedData = useMemo((): PieChartEnrichedData[] => { const totalValue = data.reduce((sum, item) => sum + item.value, 0); + const shouldApplyGradient = colorMode === 'explicitSingleColor'; + return data.map((item, index) => { const colorScheme = getColorScheme({ registry: colorRegistry, colorName: item.color, fallbackIndex: index, - totalGroups: data.length, + totalGroups: shouldApplyGradient ? data.length : undefined, }); const percentage = calculatePieChartPercentage(item.value, totalValue); @@ -40,7 +45,7 @@ export const usePieChartData = ({ percentage, }; }); - }, [data, colorRegistry]); + }, [data, colorRegistry, colorMode]); const legendItems: GraphWidgetLegendItem[] = allEnrichedData.map((item) => ({ id: item.id, diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/utils/__tests__/transformGroupByDataToPieChartData.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/utils/__tests__/transformGroupByDataToPieChartData.test.ts index 6375712fe9..09b780203c 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/utils/__tests__/transformGroupByDataToPieChartData.test.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/utils/__tests__/transformGroupByDataToPieChartData.test.ts @@ -1,4 +1,3 @@ -import { GRAPH_DEFAULT_COLOR } from '@/page-layout/widgets/graph/constants/GraphDefaultColor.constant'; import { PIE_CHART_MAXIMUM_NUMBER_OF_SLICES } from '@/page-layout/widgets/graph/graphWidgetPieChart/constants/PieChartMaximumNumberOfSlices.constant'; import { transformGroupByDataToPieChartData } from '@/page-layout/widgets/graph/graphWidgetPieChart/utils/transformGroupByDataToPieChartData'; import { FirstDayOfTheWeek } from 'twenty-shared/types'; @@ -83,8 +82,8 @@ describe('transformGroupByDataToPieChartData', () => { }); expect(result.data).toEqual([ - { id: 'Not Set', value: 2, color: GRAPH_DEFAULT_COLOR }, - { id: 'Active', value: 5, color: GRAPH_DEFAULT_COLOR }, + { id: 'Not Set', value: 2, color: undefined }, + { id: 'Active', value: 5, color: undefined }, ]); expect(result.formattedToRawLookup.get('Not Set')).toBeNull(); expect(result.formattedToRawLookup.get('Active')).toBe('Active'); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/utils/transformGroupByDataToPieChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/utils/transformGroupByDataToPieChartData.ts index f4a1d5181b..91da9650c7 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/utils/transformGroupByDataToPieChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/utils/transformGroupByDataToPieChartData.ts @@ -1,12 +1,14 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getGroupByQueryResultGqlFieldName } from '@/page-layout/utils/getGroupByQueryResultGqlFieldName'; -import { GRAPH_DEFAULT_COLOR } from '@/page-layout/widgets/graph/constants/GraphDefaultColor.constant'; import { PIE_CHART_MAXIMUM_NUMBER_OF_SLICES } from '@/page-layout/widgets/graph/graphWidgetPieChart/constants/PieChartMaximumNumberOfSlices.constant'; import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartDataItem'; -import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor'; +import { type GraphColorMode } from '@/page-layout/widgets/graph/types/GraphColorMode'; +import { determineGraphColorMode } from '@/page-layout/widgets/graph/utils/determineGraphColorMode'; import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult'; import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue'; +import { determineChartItemColor } from '@/page-layout/widgets/graph/utils/determineChartItemColor'; +import { parseGraphColor } from '@/page-layout/widgets/graph/utils/parseGraphColor'; import { processOneDimensionalGroupByResults } from '@/page-layout/widgets/graph/utils/processOneDimensionalGroupByResults'; import { sortChartDataIfNeeded } from '@/page-layout/widgets/graph/utils/sortChartDataIfNeeded'; import { type FirstDayOfTheWeek } from 'twenty-shared/types'; @@ -27,6 +29,7 @@ type TransformGroupByDataToPieChartDataResult = { showLegend: boolean; hasTooManyGroups: boolean; formattedToRawLookup: Map; + colorMode: GraphColorMode; }; const EMPTY_PIE_CHART_RESULT: TransformGroupByDataToPieChartDataResult = { @@ -34,6 +37,7 @@ const EMPTY_PIE_CHART_RESULT: TransformGroupByDataToPieChartDataResult = { showLegend: true, hasTooManyGroups: false, formattedToRawLookup: new Map(), + colorMode: 'automaticPalette', }; export const transformGroupByDataToPieChartData = ({ @@ -99,12 +103,22 @@ export const transformGroupByDataToPieChartData = ({ }; const unsortedDataWithRawValues: PieChartDataItemWithRawValue[] = - limitedProcessedDataPoints.map(({ xValue, rawXValue, aggregateValue }) => ({ - id: xValue, - value: aggregateValue, - color: (configuration.color ?? GRAPH_DEFAULT_COLOR) as GraphColor, - rawValue: isDefined(rawXValue) ? String(rawXValue) : null, - })); + limitedProcessedDataPoints.map(({ xValue, rawXValue, aggregateValue }) => { + const rawValueString = isDefined(rawXValue) ? String(rawXValue) : null; + + return { + id: xValue, + value: aggregateValue, + color: determineChartItemColor({ + configurationColor: parseGraphColor(configuration.color), + selectOptions: isFieldMetadataSelectKind(groupByField.type) + ? groupByField.options + : undefined, + rawValue: rawValueString, + }), + rawValue: rawValueString, + }; + }); const sortedDataWithRawValues = sortChartDataIfNeeded({ data: unsortedDataWithRawValues, @@ -124,11 +138,19 @@ export const transformGroupByDataToPieChartData = ({ const showLegend = configuration.displayLegend ?? true; + const colorMode = determineGraphColorMode({ + configurationColor: configuration.color, + selectFieldOptions: isFieldMetadataSelectKind(groupByField.type) + ? groupByField.options + : undefined, + }); + return { data, showLegend, hasTooManyGroups: filteredResults.length > PIE_CHART_MAXIMUM_NUMBER_OF_SLICES, formattedToRawLookup, + colorMode, }; }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/types/GraphColor.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/types/GraphColor.ts index 3d1b95a0a6..aad6bb2d17 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/types/GraphColor.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/types/GraphColor.ts @@ -1,12 +1,3 @@ -export type GraphColor = - | 'auto' - | 'blue' - | 'purple' - | 'turquoise' - | 'orange' - | 'pink' - | 'yellow' - | 'red' - | 'green' - | 'sky' - | 'gray'; +import { type ThemeColor } from 'twenty-ui/theme'; + +export type GraphColor = ThemeColor | 'auto'; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/types/GraphColorMode.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/types/GraphColorMode.ts new file mode 100644 index 0000000000..f65b5b3dd4 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/types/GraphColorMode.ts @@ -0,0 +1,4 @@ +export type GraphColorMode = + | 'explicitSingleColor' + | 'selectFieldOptionColors' + | 'automaticPalette'; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/determineChartItemColor.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/determineChartItemColor.test.ts new file mode 100644 index 0000000000..d43f5fd169 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/determineChartItemColor.test.ts @@ -0,0 +1,84 @@ +import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem'; +import { determineChartItemColor } from '@/page-layout/widgets/graph/utils/determineChartItemColor'; + +describe('determineChartItemColor', () => { + const mockOptions: FieldMetadataItemOption[] = [ + { id: '1', value: 'ACTIVE', label: 'Active', position: 0, color: 'green' }, + { + id: '2', + value: 'INACTIVE', + label: 'Inactive', + position: 1, + color: 'red', + }, + ]; + + describe('priority 1: explicit configuration color', () => { + it('should return configurationColor when set and not "auto"', () => { + expect( + determineChartItemColor({ + configurationColor: 'blue', + selectOptions: mockOptions, + rawValue: 'ACTIVE', // Would be 'green' if select option was used + }), + ).toBe('blue'); + }); + }); + + describe('priority 2: select option color', () => { + it('should use select option color when configurationColor is "auto"', () => { + expect( + determineChartItemColor({ + configurationColor: 'auto', + selectOptions: mockOptions, + rawValue: 'ACTIVE', + }), + ).toBe('green'); + }); + + it.each([null, undefined])( + 'should use select option color when configurationColor is %s (null and undefined handled identically)', + (configurationColor) => { + expect( + determineChartItemColor({ + configurationColor, + selectOptions: mockOptions, + rawValue: 'INACTIVE', + }), + ).toBe('red'); + }, + ); + }); + + describe('priority 3: return undefined (no fallback)', () => { + it('should return undefined when no match and no selectOptions', () => { + expect( + determineChartItemColor({ + configurationColor: 'auto', + selectOptions: undefined, + rawValue: 'ACTIVE', + }), + ).toBeUndefined(); + }); + + it('should return undefined when rawValue does not match any option', () => { + expect( + determineChartItemColor({ + configurationColor: 'auto', + selectOptions: mockOptions, + rawValue: 'UNKNOWN', + }), + ).toBeUndefined(); + }); + + it('should return undefined when rawValue is null', () => { + expect( + determineChartItemColor({ + configurationColor: 'auto', + selectOptions: mockOptions, + rawValue: null, + }), + ).toBeUndefined(); + }); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/getSelectOptionColorForValue.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/getSelectOptionColorForValue.test.ts new file mode 100644 index 0000000000..3658211690 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/getSelectOptionColorForValue.test.ts @@ -0,0 +1,85 @@ +import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem'; +import { getSelectOptionColorForValue } from '@/page-layout/widgets/graph/utils/getSelectOptionColorForValue'; + +describe('getSelectOptionColorForValue', () => { + const mockOptions: FieldMetadataItemOption[] = [ + { id: '1', value: 'ACTIVE', label: 'Active', position: 0, color: 'green' }, + { + id: '2', + value: 'INACTIVE', + label: 'Inactive', + position: 1, + color: 'red', + }, + ]; + + it('should return the color for a matching option value', () => { + expect( + getSelectOptionColorForValue({ + rawValue: 'ACTIVE', + selectOptions: mockOptions, + }), + ).toBe('green'); + }); + + it('should return undefined when rawValue is null or undefined', () => { + expect( + getSelectOptionColorForValue({ + rawValue: null, + selectOptions: mockOptions, + }), + ).toBeUndefined(); + + expect( + getSelectOptionColorForValue({ + rawValue: undefined, + selectOptions: mockOptions, + }), + ).toBeUndefined(); + }); + + it('should return undefined when selectOptions is null, undefined, or empty', () => { + expect( + getSelectOptionColorForValue({ + rawValue: 'ACTIVE', + selectOptions: null, + }), + ).toBeUndefined(); + + expect( + getSelectOptionColorForValue({ + rawValue: 'ACTIVE', + selectOptions: undefined, + }), + ).toBeUndefined(); + + expect( + getSelectOptionColorForValue({ + rawValue: 'ACTIVE', + selectOptions: [], + }), + ).toBeUndefined(); + }); + + it('should return undefined when value does not match any option', () => { + expect( + getSelectOptionColorForValue({ + rawValue: 'UNKNOWN', + selectOptions: mockOptions, + }), + ).toBeUndefined(); + }); + + it('should return undefined when matching option has no color', () => { + const optionsWithoutColor = [ + { id: '1', value: 'NO_COLOR', label: 'No Color', position: 0 }, + ] as FieldMetadataItemOption[]; + + expect( + getSelectOptionColorForValue({ + rawValue: 'NO_COLOR', + selectOptions: optionsWithoutColor, + }), + ).toBeUndefined(); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/parseGraphColor.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/parseGraphColor.test.ts new file mode 100644 index 0000000000..778bf00487 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/parseGraphColor.test.ts @@ -0,0 +1,31 @@ +import { parseGraphColor } from '@/page-layout/widgets/graph/utils/parseGraphColor'; + +describe('parseGraphColor', () => { + it('should return "auto" for "auto" string', () => { + expect(parseGraphColor('auto')).toBe('auto'); + }); + + it('should return valid ThemeColor values', () => { + expect(parseGraphColor('green')).toBe('green'); + expect(parseGraphColor('blue')).toBe('blue'); + expect(parseGraphColor('red')).toBe('red'); + expect(parseGraphColor('purple')).toBe('purple'); + }); + + it('should return undefined for null', () => { + expect(parseGraphColor(null)).toBeUndefined(); + }); + + it('should return undefined for undefined', () => { + expect(parseGraphColor(undefined)).toBeUndefined(); + }); + + it('should return undefined for invalid color strings', () => { + expect(parseGraphColor('invalid-color')).toBeUndefined(); + expect(parseGraphColor('notacolor')).toBeUndefined(); + }); + + it('should return undefined for empty string', () => { + expect(parseGraphColor('')).toBeUndefined(); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/determineChartItemColor.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/determineChartItemColor.ts new file mode 100644 index 0000000000..79d8450de4 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/determineChartItemColor.ts @@ -0,0 +1,32 @@ +import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem'; +import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor'; +import { getSelectOptionColorForValue } from '@/page-layout/widgets/graph/utils/getSelectOptionColorForValue'; +import { isDefined } from 'twenty-shared/utils'; + +type DetermineChartItemColorParams = { + configurationColor: GraphColor | null | undefined; + selectOptions: FieldMetadataItemOption[] | null | undefined; + rawValue: string | null | undefined; +}; + +export const determineChartItemColor = ({ + configurationColor, + selectOptions, + rawValue, +}: DetermineChartItemColorParams): GraphColor | undefined => { + if (isDefined(configurationColor) && configurationColor !== 'auto') { + return configurationColor; + } + + if (isDefined(selectOptions)) { + const optionColor = getSelectOptionColorForValue({ + rawValue, + selectOptions, + }); + if (isDefined(optionColor)) { + return optionColor; + } + } + + return undefined; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/determineGraphColorMode.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/determineGraphColorMode.ts new file mode 100644 index 0000000000..e2a61087c0 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/determineGraphColorMode.ts @@ -0,0 +1,24 @@ +import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem'; +import { type GraphColorMode } from '@/page-layout/widgets/graph/types/GraphColorMode'; +import { isDefined } from 'twenty-shared/utils'; + +type DetermineGraphColorModeParams = { + configurationColor: string | null | undefined; + selectFieldOptions: FieldMetadataItemOption[] | null | undefined; +}; + +export const determineGraphColorMode = ({ + configurationColor, + selectFieldOptions, +}: DetermineGraphColorModeParams): GraphColorMode => { + const hasExplicitColor = + isDefined(configurationColor) && configurationColor !== 'auto'; + const hasSelectFieldOptions = + isDefined(selectFieldOptions) && selectFieldOptions.length > 0; + + return hasExplicitColor + ? 'explicitSingleColor' + : hasSelectFieldOptions + ? 'selectFieldOptionColors' + : 'automaticPalette'; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getSelectOptionColorForValue.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getSelectOptionColorForValue.ts new file mode 100644 index 0000000000..5abcdcb3db --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getSelectOptionColorForValue.ts @@ -0,0 +1,19 @@ +import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem'; +import { isDefined } from 'twenty-shared/utils'; +import { type ThemeColor } from 'twenty-ui/theme'; + +export const getSelectOptionColorForValue = ({ + rawValue, + selectOptions, +}: { + rawValue: string | null | undefined; + selectOptions: FieldMetadataItemOption[] | null | undefined; +}): ThemeColor | undefined => { + if (!isDefined(selectOptions) || !isDefined(rawValue)) { + return undefined; + } + + const option = selectOptions.find((opt) => opt.value === rawValue); + + return option?.color; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/parseGraphColor.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/parseGraphColor.ts new file mode 100644 index 0000000000..cce8179294 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/parseGraphColor.ts @@ -0,0 +1,21 @@ +import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor'; +import { isDefined } from 'twenty-shared/utils'; +import { MAIN_COLOR_NAMES, type ThemeColor } from 'twenty-ui/theme'; + +export const parseGraphColor = ( + value: string | null | undefined, +): GraphColor | undefined => { + if (!isDefined(value)) { + return undefined; + } + + if (value === 'auto') { + return 'auto'; + } + + if (MAIN_COLOR_NAMES.includes(value as ThemeColor)) { + return value as GraphColor; + } + + return undefined; +};