From 1038efa3dd76532033bcc69f41e5833400e83956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Tue, 2 Dec 2025 14:00:58 +0100 Subject: [PATCH] [DASHBOARDS] Add cumulative setting for bar chart and line chart (#16248) ## Description - Add cumulative setting - This setting is only present for dates - It is preserved when switching from a date field to another date field but it is reset when switching to another field type ## Video QA https://github.com/user-attachments/assets/a070bf54-8ef6-4e35-9378-a514fe1c3848 https://github.com/user-attachments/assets/07edfe87-253c-45a5-b582-06f2df9a2dfc --- .../src/generated-metadata/graphql.ts | 2 + .../twenty-front/src/generated/graphql.ts | 10 ++- ...oupByFieldSelectionDropdownContentBase.tsx | 8 ++ .../constants/LineChartSettings.ts | 2 + .../ChartConfigurationSettingLabels.ts | 1 + .../constants/settings/CumulativeSetting.ts | 11 +++ .../hooks/useChartSettingsValues.ts | 2 + .../types/ChartConfigurationSettingIds.ts | 2 + .../__tests__/getBarChartSettings.test.ts | 15 ++-- .../isFieldOrNestedFieldDateKind.test.ts | 74 +++++++++++++++++++ .../buildChartGroupByFieldConfigUpdate.ts | 20 +++++ .../page-layout/utils/getBarChartSettings.ts | 2 + .../utils/isFieldOrNestedFieldDateKind.ts | 38 ++++++++++ .../utils/shouldHideChartSetting.ts | 15 ++++ .../fragments/pageLayoutWidgetFragment.ts | 4 + .../transformGroupByDataToBarChartData.ts | 8 +- ...formOneDimensionalGroupByToBarChartData.ts | 12 ++- ...formTwoDimensionalGroupByToBarChartData.ts | 14 +++- ...lativeTransformToBarChartData.test.ts.snap | 59 +++++++++++++++ ...ativeTransformToLineChartData.test.ts.snap | 59 +++++++++++++++ ...mToTwoDimensionalBarChartData.test.ts.snap | 60 +++++++++++++++ ...yCumulativeTransformToBarChartData.test.ts | 63 ++++++++++++++++ ...CumulativeTransformToLineChartData.test.ts | 54 ++++++++++++++ ...nsformToTwoDimensionalBarChartData.test.ts | 63 ++++++++++++++++ .../applyCumulativeTransformToBarChartData.ts | 41 ++++++++++ ...applyCumulativeTransformToLineChartData.ts | 40 ++++++++++ ...veTransformToTwoDimensionalBarChartData.ts | 56 ++++++++++++++ .../transformGroupByDataToLineChartData.ts | 8 +- ...ormOneDimensionalGroupByToLineChartData.ts | 13 +++- ...ormTwoDimensionalGroupByToLineChartData.ts | 13 +++- .../dtos/bar-chart-configuration.dto.ts | 7 ++ .../dtos/line-chart-configuration.dto.ts | 7 ++ .../display/icon/components/TablerIcons.ts | 1 + packages/twenty-ui/src/display/index.ts | 1 + 34 files changed, 766 insertions(+), 19 deletions(-) create mode 100644 packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/CumulativeSetting.ts create mode 100644 packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/__tests__/isFieldOrNestedFieldDateKind.test.ts create mode 100644 packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/isFieldOrNestedFieldDateKind.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/__snapshots__/applyCumulativeTransformToBarChartData.test.ts.snap create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/__snapshots__/applyCumulativeTransformToLineChartData.test.ts.snap create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/__snapshots__/applyCumulativeTransformToTwoDimensionalBarChartData.test.ts.snap create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/applyCumulativeTransformToBarChartData.test.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/applyCumulativeTransformToLineChartData.test.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/applyCumulativeTransformToTwoDimensionalBarChartData.test.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/applyCumulativeTransformToBarChartData.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/applyCumulativeTransformToLineChartData.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/applyCumulativeTransformToTwoDimensionalBarChartData.ts diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index e1f294b146..de06c2c92f 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -350,6 +350,7 @@ export type BarChartConfiguration = { firstDayOfTheWeek?: Maybe; graphType: GraphType; groupMode?: Maybe; + isCumulative?: Maybe; omitNullValues?: Maybe; primaryAxisDateGranularity?: Maybe; primaryAxisGroupByFieldMetadataId: Scalars['UUID']; @@ -1699,6 +1700,7 @@ export type LineChartConfiguration = { filter?: Maybe; firstDayOfTheWeek?: Maybe; graphType: GraphType; + isCumulative?: Maybe; isStacked?: Maybe; omitNullValues?: Maybe; primaryAxisDateGranularity?: Maybe; diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index bdefcc69f3..4c2044b2fb 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -350,6 +350,7 @@ export type BarChartConfiguration = { firstDayOfTheWeek?: Maybe; graphType: GraphType; groupMode?: Maybe; + isCumulative?: Maybe; omitNullValues?: Maybe; primaryAxisDateGranularity?: Maybe; primaryAxisGroupByFieldMetadataId: Scalars['UUID']; @@ -1675,6 +1676,7 @@ export type LineChartConfiguration = { filter?: Maybe; firstDayOfTheWeek?: Maybe; graphType: GraphType; + isCumulative?: Maybe; isStacked?: Maybe; omitNullValues?: Maybe; primaryAxisDateGranularity?: Maybe; @@ -4788,7 +4790,7 @@ export type SearchQueryVariables = Exact<{ export type SearchQuery = { __typename?: 'Query', search: { __typename?: 'SearchResultConnection', edges: Array<{ __typename?: 'SearchResultEdge', cursor: string, node: { __typename?: 'SearchRecord', recordId: any, objectNameSingular: string, label: string, imageUrl?: string | null, tsRankCD: number, tsRank: number } }>, pageInfo: { __typename?: 'SearchResultPageInfo', hasNextPage: boolean, endCursor?: string | null } } }; -export type PageLayoutWidgetFragmentFragment = { __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'AggregateChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', url?: string | null } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | null }; +export type PageLayoutWidgetFragmentFragment = { __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'AggregateChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, prefix?: string | null, suffix?: string | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, isCumulative?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', url?: string | null } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, isCumulative?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | null }; export type UpdatePageLayoutWithTabsAndWidgetsMutationVariables = Exact<{ id: Scalars['String']; @@ -4796,7 +4798,7 @@ export type UpdatePageLayoutWithTabsAndWidgetsMutationVariables = Exact<{ }>; -export type UpdatePageLayoutWithTabsAndWidgetsMutation = { __typename?: 'Mutation', updatePageLayoutWithTabsAndWidgets: { __typename?: 'PageLayout', id: any, name: string, type: PageLayoutType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, tabs?: Array<{ __typename?: 'PageLayoutTab', id: any, title: string, position: number, pageLayoutId: any, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'AggregateChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', url?: string | null } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | null }> | null }> | null } }; +export type UpdatePageLayoutWithTabsAndWidgetsMutation = { __typename?: 'Mutation', updatePageLayoutWithTabsAndWidgets: { __typename?: 'PageLayout', id: any, name: string, type: PageLayoutType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, tabs?: Array<{ __typename?: 'PageLayoutTab', id: any, title: string, position: number, pageLayoutId: any, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration?: { __typename?: 'AggregateChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, prefix?: string | null, suffix?: string | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'BarChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, isCumulative?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'GaugeChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', url?: string | null } | { __typename?: 'LineChartConfiguration', graphType: GraphType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, isCumulative?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'PieChartConfiguration', graphType: GraphType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | null }> | null }> | null } }; export type OnDbEventSubscriptionVariables = Exact<{ input: OnDbEventInput; @@ -5129,6 +5131,7 @@ export const PageLayoutWidgetFragmentFragmentDoc = gql` description filter groupMode + isCumulative timezone firstDayOfTheWeek } @@ -5154,6 +5157,7 @@ export const PageLayoutWidgetFragmentFragmentDoc = gql` description filter isStacked + isCumulative timezone firstDayOfTheWeek } @@ -5183,6 +5187,8 @@ export const PageLayoutWidgetFragmentFragmentDoc = gql` format description filter + prefix + suffix timezone firstDayOfTheWeek } diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionDropdownContentBase.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionDropdownContentBase.tsx index bdf2ef6ab7..77325e7805 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionDropdownContentBase.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionDropdownContentBase.tsx @@ -123,6 +123,8 @@ export const ChartGroupByFieldSelectionDropdownContentBase = < subFieldNameKey, fieldId: fieldMetadataItem.id, subFieldName: null, + objectMetadataItem: sourceObjectMetadataItem, + objectMetadataItems, }), }); closeDropdown(); @@ -136,6 +138,8 @@ export const ChartGroupByFieldSelectionDropdownContentBase = < subFieldNameKey, fieldId: null, subFieldName: null, + objectMetadataItem: sourceObjectMetadataItem, + objectMetadataItems, }), }); closeDropdown(); @@ -161,6 +165,8 @@ export const ChartGroupByFieldSelectionDropdownContentBase = < subFieldNameKey, fieldId: selectedCompositeField.id, subFieldName, + objectMetadataItem: sourceObjectMetadataItem, + objectMetadataItems, }), }); closeDropdown(); @@ -178,6 +184,8 @@ export const ChartGroupByFieldSelectionDropdownContentBase = < subFieldNameKey, fieldId: selectedRelationField.id, subFieldName, + objectMetadataItem: sourceObjectMetadataItem, + objectMetadataItems, }), }); closeDropdown(); diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/LineChartSettings.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/LineChartSettings.ts index e00fa89c64..82e84d75b6 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/LineChartSettings.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/LineChartSettings.ts @@ -2,6 +2,7 @@ import { CHART_SETTINGS_HEADINGS } from '@/command-menu/pages/page-layout/consta import { AXIS_NAME_SETTING } from '@/command-menu/pages/page-layout/constants/settings/AxisNameSetting'; import { CHART_DATA_SOURCE_SETTING } from '@/command-menu/pages/page-layout/constants/settings/ChartDataSourceSetting'; import { COLORS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/ColorsSetting'; +import { CUMULATIVE_SETTING } from '@/command-menu/pages/page-layout/constants/settings/CumulativeSetting'; import { DATA_DISPLAY_X_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataDisplayXSetting'; import { DATA_DISPLAY_Y_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataDisplayYSetting'; import { DATA_LABELS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataLabelsSetting'; @@ -39,6 +40,7 @@ export const LINE_CHART_SETTINGS: ChartSettingsGroup[] = [ GROUP_BY_SETTING, DATE_GRANULARITY_Y_SETTING, SORT_BY_GROUP_BY_FIELD_SETTING, + CUMULATIVE_SETTING, RANGE_MIN_SETTING, RANGE_MAX_SETTING, ], diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels.ts index 0995b4e3cd..6e0ddbbd98 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels.ts @@ -26,4 +26,5 @@ export const CHART_CONFIGURATION_SETTING_LABELS = { SHOW_LEGEND: msg`Legend`, PREFIX: msg`Prefix`, SUFFIX: msg`Suffix`, + CUMULATIVE: msg`Cumulative`, }; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/CumulativeSetting.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/CumulativeSetting.ts new file mode 100644 index 0000000000..f79cf32f22 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/constants/settings/CumulativeSetting.ts @@ -0,0 +1,11 @@ +import { CHART_CONFIGURATION_SETTING_LABELS } from '@/command-menu/pages/page-layout/constants/settings/ChartConfigurationSettingLabels'; +import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds'; +import { type ChartSettingsItem } from '@/command-menu/pages/page-layout/types/ChartSettingsGroup'; +import { IconChartBarPopular } from 'twenty-ui/display'; + +export const CUMULATIVE_SETTING: ChartSettingsItem = { + isBoolean: true, + Icon: IconChartBarPopular, + label: CHART_CONFIGURATION_SETTING_LABELS.CUMULATIVE, + id: CHART_CONFIGURATION_SETTING_IDS.CUMULATIVE, +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useChartSettingsValues.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useChartSettingsValues.ts index 496fdfc829..e1586d73ce 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useChartSettingsValues.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useChartSettingsValues.ts @@ -224,6 +224,8 @@ export const useChartSettingsValues = ({ return configuration.__typename === 'LineChartConfiguration' ? configuration.isStacked !== false : true; + case CHART_CONFIGURATION_SETTING_IDS.CUMULATIVE: + return isBarOrLineChart ? (configuration.isCumulative ?? false) : false; case CHART_CONFIGURATION_SETTING_IDS.OMIT_NULL_VALUES: return isBarOrLineChart ? (configuration.omitNullValues ?? false) diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/ChartConfigurationSettingIds.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/ChartConfigurationSettingIds.ts index 8e7dcb0af5..8c86cb7d44 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/ChartConfigurationSettingIds.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/ChartConfigurationSettingIds.ts @@ -25,6 +25,7 @@ export enum CHART_CONFIGURATION_SETTING_IDS { SHOW_LEGEND = 'SHOW_LEGEND', PREFIX = 'PREFIX', SUFFIX = 'SUFFIX', + CUMULATIVE = 'CUMULATIVE', } export const CHART_CONFIGURATION_SETTING_TO_CONFIG_KEY_MAP = { @@ -43,4 +44,5 @@ export const CHART_CONFIGURATION_SETTING_TO_CONFIG_KEY_MAP = { [CHART_CONFIGURATION_SETTING_IDS.SHOW_LEGEND]: 'displayLegend', [CHART_CONFIGURATION_SETTING_IDS.PREFIX]: 'prefix', [CHART_CONFIGURATION_SETTING_IDS.SUFFIX]: 'suffix', + [CHART_CONFIGURATION_SETTING_IDS.CUMULATIVE]: 'isCumulative', } as const; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/__tests__/getBarChartSettings.test.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/__tests__/getBarChartSettings.test.ts index 7a6c0be5cc..b60ecac331 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/__tests__/getBarChartSettings.test.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/__tests__/getBarChartSettings.test.ts @@ -2,6 +2,7 @@ import { CHART_SETTINGS_HEADINGS } from '@/command-menu/pages/page-layout/consta import { AXIS_NAME_SETTING } from '@/command-menu/pages/page-layout/constants/settings/AxisNameSetting'; import { CHART_DATA_SOURCE_SETTING } from '@/command-menu/pages/page-layout/constants/settings/ChartDataSourceSetting'; import { COLORS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/ColorsSetting'; +import { CUMULATIVE_SETTING } from '@/command-menu/pages/page-layout/constants/settings/CumulativeSetting'; import { DATA_DISPLAY_X_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataDisplayXSetting'; import { DATA_DISPLAY_Y_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataDisplayYSetting'; import { DATA_LABELS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataLabelsSetting'; @@ -47,15 +48,16 @@ describe('getBarChartSettings', () => { ); expect(yAxisGroup).toBeDefined(); - expect(yAxisGroup?.items).toHaveLength(6); + expect(yAxisGroup?.items).toHaveLength(7); expect(yAxisGroup?.items[0].id).toBe(DATA_DISPLAY_Y_SETTING.id); expect(yAxisGroup?.items[0].label).toBe(DATA_DISPLAY_Y_SETTING.label); expect(yAxisGroup?.items[0].Icon).toBe(IconAxisY); expect(yAxisGroup?.items[1]).toEqual(GROUP_BY_SETTING); expect(yAxisGroup?.items[2]).toEqual(DATE_GRANULARITY_Y_SETTING); expect(yAxisGroup?.items[3]).toEqual(SORT_BY_GROUP_BY_FIELD_SETTING); - expect(yAxisGroup?.items[4]).toEqual(RANGE_MIN_SETTING); - expect(yAxisGroup?.items[5]).toEqual(RANGE_MAX_SETTING); + expect(yAxisGroup?.items[4]).toEqual(CUMULATIVE_SETTING); + expect(yAxisGroup?.items[5]).toEqual(RANGE_MIN_SETTING); + expect(yAxisGroup?.items[6]).toEqual(RANGE_MAX_SETTING); }); it('should have all expected groups in correct order', () => { @@ -78,15 +80,16 @@ describe('getBarChartSettings', () => { ); expect(xAxisGroup).toBeDefined(); - expect(xAxisGroup?.items).toHaveLength(6); + expect(xAxisGroup?.items).toHaveLength(7); expect(xAxisGroup?.items[0].id).toBe(DATA_DISPLAY_Y_SETTING.id); expect(xAxisGroup?.items[0].label).toBe(DATA_DISPLAY_Y_SETTING.label); expect(xAxisGroup?.items[0].Icon).toBe(IconAxisX); expect(xAxisGroup?.items[1]).toEqual(GROUP_BY_SETTING); expect(xAxisGroup?.items[2]).toEqual(DATE_GRANULARITY_Y_SETTING); expect(xAxisGroup?.items[3]).toEqual(SORT_BY_GROUP_BY_FIELD_SETTING); - expect(xAxisGroup?.items[4]).toEqual(RANGE_MIN_SETTING); - expect(xAxisGroup?.items[5]).toEqual(RANGE_MAX_SETTING); + expect(xAxisGroup?.items[4]).toEqual(CUMULATIVE_SETTING); + expect(xAxisGroup?.items[5]).toEqual(RANGE_MIN_SETTING); + expect(xAxisGroup?.items[6]).toEqual(RANGE_MAX_SETTING); }); it('should place PRIMARY axis items under "Y axis" heading', () => { diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/__tests__/isFieldOrNestedFieldDateKind.test.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/__tests__/isFieldOrNestedFieldDateKind.test.ts new file mode 100644 index 0000000000..1ea5b69d2e --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/__tests__/isFieldOrNestedFieldDateKind.test.ts @@ -0,0 +1,74 @@ +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { isFieldOrNestedFieldDateKind } from '../isFieldOrNestedFieldDateKind'; + +describe('isFieldOrNestedFieldDateKind', () => { + it('returns false when fieldId is null', () => { + const result = isFieldOrNestedFieldDateKind({ + fieldId: null, + subFieldName: null, + objectMetadataItem: {} as ObjectMetadataItem, + }); + + expect(result).toBe(false); + }); + + it('returns true for a DATE type field', () => { + const objectMetadataItem = { + fields: [{ id: 'date-field-id', type: FieldMetadataType.DATE }], + } as ObjectMetadataItem; + + const result = isFieldOrNestedFieldDateKind({ + fieldId: 'date-field-id', + subFieldName: null, + objectMetadataItem, + }); + + expect(result).toBe(true); + }); + + it('returns false for a non-date type field', () => { + const objectMetadataItem = { + fields: [{ id: 'text-field-id', type: FieldMetadataType.TEXT }], + } as ObjectMetadataItem; + + const result = isFieldOrNestedFieldDateKind({ + fieldId: 'text-field-id', + subFieldName: null, + objectMetadataItem, + }); + + expect(result).toBe(false); + }); + + it('returns true for a relation subfield that is a date type', () => { + const objectMetadataItem = { + fields: [ + { + id: 'relation-field-id', + type: FieldMetadataType.RELATION, + relation: { targetObjectMetadata: { nameSingular: 'company' } }, + }, + ], + } as ObjectMetadataItem; + + const companyObjectMetadataItem = { + nameSingular: 'company', + fields: [ + { + name: 'createdAt', + type: FieldMetadataType.DATE, + }, + ], + } as ObjectMetadataItem; + + const result = isFieldOrNestedFieldDateKind({ + fieldId: 'relation-field-id', + subFieldName: 'createdAt', + objectMetadataItem, + objectMetadataItems: [companyObjectMetadataItem], + }); + + expect(result).toBe(true); + }); +}); diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/buildChartGroupByFieldConfigUpdate.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/buildChartGroupByFieldConfigUpdate.ts index 2898eb9060..a7d3f95c70 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/buildChartGroupByFieldConfigUpdate.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/buildChartGroupByFieldConfigUpdate.ts @@ -1,4 +1,6 @@ import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration'; +import { isFieldOrNestedFieldDateKind } from '@/command-menu/pages/page-layout/utils/isFieldOrNestedFieldDateKind'; +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { BarChartGroupMode, GraphOrderBy } from '~/generated/graphql'; @@ -9,6 +11,8 @@ type BuildChartGroupByFieldConfigUpdateArgs = { subFieldNameKey: keyof T; fieldId: string | null; subFieldName: string | null; + objectMetadataItem?: ObjectMetadataItem; + objectMetadataItems?: ObjectMetadataItem[]; }; export const buildChartGroupByFieldConfigUpdate = < @@ -19,6 +23,8 @@ export const buildChartGroupByFieldConfigUpdate = < subFieldNameKey, fieldId, subFieldName, + objectMetadataItem, + objectMetadataItems, }: BuildChartGroupByFieldConfigUpdateArgs) => { const isPrimaryAxis = fieldMetadataIdKey === 'primaryAxisGroupByFieldMetadataId'; @@ -44,6 +50,19 @@ export const buildChartGroupByFieldConfigUpdate = < ? configuration.primaryAxisDateGranularity : null; + const isNewFieldDateType = isFieldOrNestedFieldDateKind({ + fieldId, + subFieldName, + objectMetadataItem, + objectMetadataItems, + }); + + const shouldResetCumulative = + isDefined(fieldId) && + isDefined(objectMetadataItem) && + !isNewFieldDateType && + (isBarChart || isLineChart); + return { ...baseConfig, primaryAxisOrderBy: isDefined(fieldId) @@ -52,6 +71,7 @@ export const buildChartGroupByFieldConfigUpdate = < primaryAxisDateGranularity: isDefined(fieldId) ? (existingDateGranularity ?? ObjectRecordGroupByDateGranularity.DAY) : null, + ...(shouldResetCumulative ? { isCumulative: false } : {}), }; } diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getBarChartSettings.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getBarChartSettings.ts index 012578cd9d..023c2561cf 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getBarChartSettings.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getBarChartSettings.ts @@ -2,6 +2,7 @@ import { CHART_SETTINGS_HEADINGS } from '@/command-menu/pages/page-layout/consta import { AXIS_NAME_SETTING } from '@/command-menu/pages/page-layout/constants/settings/AxisNameSetting'; import { CHART_DATA_SOURCE_SETTING } from '@/command-menu/pages/page-layout/constants/settings/ChartDataSourceSetting'; import { COLORS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/ColorsSetting'; +import { CUMULATIVE_SETTING } from '@/command-menu/pages/page-layout/constants/settings/CumulativeSetting'; import { DATA_DISPLAY_X_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataDisplayXSetting'; import { DATA_DISPLAY_Y_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataDisplayYSetting'; import { DATA_LABELS_SETTING } from '@/command-menu/pages/page-layout/constants/settings/DataLabelsSetting'; @@ -40,6 +41,7 @@ export const getBarChartSettings = ( GROUP_BY_SETTING, DATE_GRANULARITY_Y_SETTING, SORT_BY_GROUP_BY_FIELD_SETTING, + CUMULATIVE_SETTING, RANGE_MIN_SETTING, RANGE_MAX_SETTING, ]; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/isFieldOrNestedFieldDateKind.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/isFieldOrNestedFieldDateKind.ts new file mode 100644 index 0000000000..6a523b4f9f --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/isFieldOrNestedFieldDateKind.ts @@ -0,0 +1,38 @@ +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation'; +import { isNestedFieldDateType } from '@/page-layout/widgets/graph/utils/isNestedFieldDateType'; +import { isDefined, isFieldMetadataDateKind } from 'twenty-shared/utils'; + +export const isFieldOrNestedFieldDateKind = ({ + fieldId, + subFieldName, + objectMetadataItem, + objectMetadataItems, +}: { + fieldId: string | null; + subFieldName: string | null; + objectMetadataItem?: ObjectMetadataItem; + objectMetadataItems?: ObjectMetadataItem[]; +}): boolean => { + if (!isDefined(fieldId) || !isDefined(objectMetadataItem)) { + return false; + } + + const field = objectMetadataItem.fields.find( + (fieldMetadataItem) => fieldMetadataItem.id === fieldId, + ); + + if (!isDefined(field)) { + return false; + } + + if (isFieldRelation(field) && isDefined(subFieldName)) { + return isNestedFieldDateType( + field, + subFieldName, + objectMetadataItems ?? [], + ); + } + + return isFieldMetadataDateKind(field.type); +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/shouldHideChartSetting.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/shouldHideChartSetting.ts index 2047bf3f91..305e03619f 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/shouldHideChartSetting.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/shouldHideChartSetting.ts @@ -90,6 +90,21 @@ export const shouldHideChartSetting = ( } } + if (item.id === CHART_CONFIGURATION_SETTING_IDS.CUMULATIVE) { + const isBarOrLineChart = + configuration.__typename === 'BarChartConfiguration' || + configuration.__typename === 'LineChartConfiguration'; + + if (isBarOrLineChart) { + return shouldHideDateGranularityBasedOnFieldType( + configuration.primaryAxisGroupByFieldMetadataId, + configuration.primaryAxisGroupBySubFieldName, + objectMetadataItem, + objectMetadataItems ?? [], + ); + } + } + if (item.id === CHART_CONFIGURATION_SETTING_IDS.SHOW_LEGEND) { if (configuration.__typename === 'PieChartConfiguration') { return false; diff --git a/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts b/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts index 68e5445d3f..45901c715f 100644 --- a/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts +++ b/packages/twenty-front/src/modules/page-layout/graphql/fragments/pageLayoutWidgetFragment.ts @@ -38,6 +38,7 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql` description filter groupMode + isCumulative timezone firstDayOfTheWeek } @@ -63,6 +64,7 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql` description filter isStacked + isCumulative timezone firstDayOfTheWeek } @@ -92,6 +94,8 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql` format description filter + prefix + suffix timezone firstDayOfTheWeek } 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 48c08ca389..965151db25 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 @@ -121,8 +121,12 @@ export const transformGroupByDataToBarChartData = ({ const filteredResults = filterGroupByResults({ rawResults, filterOptions: { - rangeMin: configuration.rangeMin ?? undefined, - rangeMax: configuration.rangeMax ?? undefined, + rangeMin: configuration.isCumulative + ? undefined + : (configuration.rangeMin ?? undefined), + rangeMax: configuration.isCumulative + ? undefined + : (configuration.rangeMax ?? undefined), omitNullValues: configuration.omitNullValues ?? false, }, aggregateField, 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 ca84f862b1..3c262e962c 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 @@ -7,6 +7,7 @@ import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarC import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor'; import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult'; import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue'; +import { applyCumulativeTransformToBarChartData } from '@/page-layout/widgets/graph/utils/applyCumulativeTransformToBarChartData'; import { buildFormattedToRawLookup } from '@/page-layout/widgets/graph/utils/buildFormattedToRawLookup'; import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult'; import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue'; @@ -104,8 +105,17 @@ export const transformOneDimensionalGroupByToBarChartData = ({ }, ]; + const finalData = configuration.isCumulative + ? applyCumulativeTransformToBarChartData({ + data, + aggregateKey: aggregateValueKey, + rangeMin: configuration.rangeMin ?? undefined, + rangeMax: configuration.rangeMax ?? undefined, + }) + : data; + return { - data, + data: finalData, indexBy: indexByKey, keys: [aggregateValueKey], series, 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 29d299b7a2..cb7d1aa4fb 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 @@ -7,6 +7,7 @@ import { sortBarChartDataBySecondaryDimensionSum } from '@/page-layout/widgets/g import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor'; import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult'; import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue'; +import { applyCumulativeTransformToTwoDimensionalBarChartData } from '@/page-layout/widgets/graph/utils/applyCumulativeTransformToTwoDimensionalBarChartData'; import { buildFormattedToRawLookup } from '@/page-layout/widgets/graph/utils/buildFormattedToRawLookup'; import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult'; import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue'; @@ -153,7 +154,7 @@ export const transformTwoDimensionalGroupByToBarChartData = ({ })); const unsortedData = Array.from(dataMap.values()); - const data = isDefined(configuration.primaryAxisOrderBy) + const sortedData = isDefined(configuration.primaryAxisOrderBy) ? sortBarChartDataBySecondaryDimensionSum({ data: unsortedData, keys, @@ -161,8 +162,17 @@ export const transformTwoDimensionalGroupByToBarChartData = ({ }) : unsortedData; + const finalData = configuration.isCumulative + ? applyCumulativeTransformToTwoDimensionalBarChartData({ + data: sortedData, + keys, + rangeMin: configuration.rangeMin ?? undefined, + rangeMax: configuration.rangeMax ?? undefined, + }) + : sortedData; + return { - data, + data: finalData, indexBy: indexByKey, keys, series, diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/__snapshots__/applyCumulativeTransformToBarChartData.test.ts.snap b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/__snapshots__/applyCumulativeTransformToBarChartData.test.ts.snap new file mode 100644 index 0000000000..414801fd43 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/__snapshots__/applyCumulativeTransformToBarChartData.test.ts.snap @@ -0,0 +1,59 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`applyCumulativeTransformToBarChartData should handle basic accumulation 1`] = ` +[ + { + "id": "a", + "value": 10, + }, + { + "id": "b", + "value": 30, + }, + { + "id": "c", + "value": 60, + }, +] +`; + +exports[`applyCumulativeTransformToBarChartData should handle empty data 1`] = `[]`; + +exports[`applyCumulativeTransformToBarChartData should handle filter above rangeMax 1`] = ` +[ + { + "id": "a", + "value": 10, + }, +] +`; + +exports[`applyCumulativeTransformToBarChartData should handle filter below rangeMin 1`] = ` +[ + { + "id": "b", + "value": 20, + }, + { + "id": "c", + "value": 30, + }, +] +`; + +exports[`applyCumulativeTransformToBarChartData should handle skip non-numeric values 1`] = ` +[ + { + "id": "a", + "value": 10, + }, + { + "id": "b", + "value": 10, + }, + { + "id": "c", + "value": 30, + }, +] +`; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/__snapshots__/applyCumulativeTransformToLineChartData.test.ts.snap b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/__snapshots__/applyCumulativeTransformToLineChartData.test.ts.snap new file mode 100644 index 0000000000..c208dd0b3a --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/__snapshots__/applyCumulativeTransformToLineChartData.test.ts.snap @@ -0,0 +1,59 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`applyCumulativeTransformToLineChartData should handle basic accumulation 1`] = ` +[ + { + "x": "Jan", + "y": 10, + }, + { + "x": "Feb", + "y": 30, + }, + { + "x": "Mar", + "y": 60, + }, +] +`; + +exports[`applyCumulativeTransformToLineChartData should handle empty data 1`] = `[]`; + +exports[`applyCumulativeTransformToLineChartData should handle filter above rangeMax 1`] = ` +[ + { + "x": "a", + "y": 10, + }, +] +`; + +exports[`applyCumulativeTransformToLineChartData should handle filter below rangeMin 1`] = ` +[ + { + "x": "b", + "y": 20, + }, + { + "x": "c", + "y": 30, + }, +] +`; + +exports[`applyCumulativeTransformToLineChartData should handle skip null y values 1`] = ` +[ + { + "x": "a", + "y": 10, + }, + { + "x": "b", + "y": 10, + }, + { + "x": "c", + "y": 30, + }, +] +`; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/__snapshots__/applyCumulativeTransformToTwoDimensionalBarChartData.test.ts.snap b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/__snapshots__/applyCumulativeTransformToTwoDimensionalBarChartData.test.ts.snap new file mode 100644 index 0000000000..ea07234352 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/__snapshots__/applyCumulativeTransformToTwoDimensionalBarChartData.test.ts.snap @@ -0,0 +1,60 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`applyCumulativeTransformToTwoDimensionalBarChartData should handle accumulate each key independently 1`] = ` +[ + { + "id": "Jan", + "revenue": 100, + "sales": 10, + }, + { + "id": "Feb", + "revenue": 300, + "sales": 30, + }, + { + "id": "Mar", + "revenue": 600, + "sales": 60, + }, +] +`; + +exports[`applyCumulativeTransformToTwoDimensionalBarChartData should handle empty data 1`] = `[]`; + +exports[`applyCumulativeTransformToTwoDimensionalBarChartData should handle filter above rangeMax based on total sum 1`] = ` +[ + { + "id": "a", + "x": 10, + "y": 10, + }, +] +`; + +exports[`applyCumulativeTransformToTwoDimensionalBarChartData should handle filter below rangeMin based on total sum 1`] = ` +[ + { + "id": "c", + "x": 30, + "y": 30, + }, +] +`; + +exports[`applyCumulativeTransformToTwoDimensionalBarChartData should handle skip non-numeric values 1`] = ` +[ + { + "id": "a", + "x": 10, + }, + { + "id": "b", + "x": 10, + }, + { + "id": "c", + "x": 30, + }, +] +`; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/applyCumulativeTransformToBarChartData.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/applyCumulativeTransformToBarChartData.test.ts new file mode 100644 index 0000000000..cceea0fd9b --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/applyCumulativeTransformToBarChartData.test.ts @@ -0,0 +1,63 @@ +import { applyCumulativeTransformToBarChartData } from '../applyCumulativeTransformToBarChartData'; + +describe('applyCumulativeTransformToBarChartData', () => { + const testCases = [ + { + name: 'basic accumulation', + data: [ + { id: 'a', value: 10 }, + { id: 'b', value: 20 }, + { id: 'c', value: 30 }, + ], + aggregateKey: 'value', + }, + { + name: 'filter below rangeMin', + data: [ + { id: 'a', value: 10 }, + { id: 'b', value: 10 }, + { id: 'c', value: 10 }, + ], + aggregateKey: 'value', + rangeMin: 15, + }, + { + name: 'filter above rangeMax', + data: [ + { id: 'a', value: 10 }, + { id: 'b', value: 20 }, + { id: 'c', value: 30 }, + ], + aggregateKey: 'value', + rangeMax: 25, + }, + { + name: 'empty data', + data: [], + aggregateKey: 'value', + }, + { + name: 'skip non-numeric values', + data: [ + { id: 'a', value: 10 }, + { id: 'b', value: 'not a number' }, + { id: 'c', value: 20 }, + ], + aggregateKey: 'value', + }, + ]; + + it.each(testCases)( + 'should handle $name', + ({ data, aggregateKey, rangeMin, rangeMax }) => { + const result = applyCumulativeTransformToBarChartData({ + data, + aggregateKey, + rangeMin, + rangeMax, + }); + + expect(result).toMatchSnapshot(); + }, + ); +}); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/applyCumulativeTransformToLineChartData.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/applyCumulativeTransformToLineChartData.test.ts new file mode 100644 index 0000000000..dfaf42a02f --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/applyCumulativeTransformToLineChartData.test.ts @@ -0,0 +1,54 @@ +import { applyCumulativeTransformToLineChartData } from '../applyCumulativeTransformToLineChartData'; + +describe('applyCumulativeTransformToLineChartData', () => { + const testCases = [ + { + name: 'basic accumulation', + data: [ + { x: 'Jan', y: 10 }, + { x: 'Feb', y: 20 }, + { x: 'Mar', y: 30 }, + ], + }, + { + name: 'filter below rangeMin', + data: [ + { x: 'a', y: 10 }, + { x: 'b', y: 10 }, + { x: 'c', y: 10 }, + ], + rangeMin: 15, + }, + { + name: 'filter above rangeMax', + data: [ + { x: 'a', y: 10 }, + { x: 'b', y: 20 }, + { x: 'c', y: 30 }, + ], + rangeMax: 25, + }, + { + name: 'empty data', + data: [], + }, + { + name: 'skip null y values', + data: [ + { x: 'a', y: 10 }, + { x: 'b', y: null }, + { x: 'c', y: 20 }, + ], + }, + ]; + + it.each(testCases)('should handle $name', ({ data, rangeMin, rangeMax }) => { + const result = applyCumulativeTransformToLineChartData({ + data, + rangeMin, + rangeMax, + }); + + expect(result).toMatchSnapshot(); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/applyCumulativeTransformToTwoDimensionalBarChartData.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/applyCumulativeTransformToTwoDimensionalBarChartData.test.ts new file mode 100644 index 0000000000..677874724f --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/__tests__/applyCumulativeTransformToTwoDimensionalBarChartData.test.ts @@ -0,0 +1,63 @@ +import { applyCumulativeTransformToTwoDimensionalBarChartData } from '../applyCumulativeTransformToTwoDimensionalBarChartData'; + +describe('applyCumulativeTransformToTwoDimensionalBarChartData', () => { + const testCases = [ + { + name: 'accumulate each key independently', + data: [ + { id: 'Jan', sales: 10, revenue: 100 }, + { id: 'Feb', sales: 20, revenue: 200 }, + { id: 'Mar', sales: 30, revenue: 300 }, + ], + keys: ['sales', 'revenue'], + }, + { + name: 'filter below rangeMin based on total sum', + data: [ + { id: 'a', x: 10, y: 10 }, + { id: 'b', x: 10, y: 10 }, + { id: 'c', x: 10, y: 10 }, + ], + keys: ['x', 'y'], + rangeMin: 50, + }, + { + name: 'filter above rangeMax based on total sum', + data: [ + { id: 'a', x: 10, y: 10 }, + { id: 'b', x: 20, y: 20 }, + { id: 'c', x: 30, y: 30 }, + ], + keys: ['x', 'y'], + rangeMax: 50, + }, + { + name: 'empty data', + data: [], + keys: ['x', 'y'], + }, + { + name: 'skip non-numeric values', + data: [ + { id: 'a', x: 10 }, + { id: 'b', x: 'not a number' }, + { id: 'c', x: 20 }, + ], + keys: ['x'], + }, + ]; + + it.each(testCases)( + 'should handle $name', + ({ data, keys, rangeMin, rangeMax }) => { + const result = applyCumulativeTransformToTwoDimensionalBarChartData({ + data, + keys, + rangeMin, + rangeMax, + }); + + expect(result).toMatchSnapshot(); + }, + ); +}); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/applyCumulativeTransformToBarChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/applyCumulativeTransformToBarChartData.ts new file mode 100644 index 0000000000..a46508bdb2 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/applyCumulativeTransformToBarChartData.ts @@ -0,0 +1,41 @@ +import { type BarDatum } from '@nivo/bar'; +import { isDefined } from 'twenty-shared/utils'; + +type ApplyCumulativeTransformToBarChartDataOptions = { + data: BarDatum[]; + aggregateKey: string; + rangeMin?: number; + rangeMax?: number; +}; + +export const applyCumulativeTransformToBarChartData = ({ + data, + aggregateKey, + rangeMin, + rangeMax, +}: ApplyCumulativeTransformToBarChartDataOptions): BarDatum[] => { + const { result } = data.reduce<{ result: BarDatum[]; runningTotal: number }>( + (accumulator, datum) => { + const value = datum[aggregateKey]; + + if (typeof value === 'number') { + accumulator.runningTotal += value; + } + + const cumulativeValue = accumulator.runningTotal; + + const isOutOfRange = + (isDefined(rangeMin) && cumulativeValue < rangeMin) || + (isDefined(rangeMax) && cumulativeValue > rangeMax); + + if (!isOutOfRange) { + accumulator.result.push({ ...datum, [aggregateKey]: cumulativeValue }); + } + + return accumulator; + }, + { result: [], runningTotal: 0 }, + ); + + return result; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/applyCumulativeTransformToLineChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/applyCumulativeTransformToLineChartData.ts new file mode 100644 index 0000000000..bcabd7cbe7 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/applyCumulativeTransformToLineChartData.ts @@ -0,0 +1,40 @@ +import { type LineChartDataPoint } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartDataPoint'; +import { isDefined } from 'twenty-shared/utils'; + +type ApplyCumulativeTransformToLineChartDataOptions = { + data: LineChartDataPoint[]; + rangeMin?: number; + rangeMax?: number; +}; + +export const applyCumulativeTransformToLineChartData = ({ + data, + rangeMin, + rangeMax, +}: ApplyCumulativeTransformToLineChartDataOptions): LineChartDataPoint[] => { + const { result } = data.reduce<{ + result: LineChartDataPoint[]; + runningTotal: number; + }>( + (accumulator, point) => { + if (point.y !== null) { + accumulator.runningTotal += point.y; + } + + const cumulativeValue = accumulator.runningTotal; + + const isOutOfRange = + (isDefined(rangeMin) && cumulativeValue < rangeMin) || + (isDefined(rangeMax) && cumulativeValue > rangeMax); + + if (!isOutOfRange) { + accumulator.result.push({ ...point, y: cumulativeValue }); + } + + return accumulator; + }, + { result: [], runningTotal: 0 }, + ); + + return result; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/applyCumulativeTransformToTwoDimensionalBarChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/applyCumulativeTransformToTwoDimensionalBarChartData.ts new file mode 100644 index 0000000000..f51ba076c0 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/applyCumulativeTransformToTwoDimensionalBarChartData.ts @@ -0,0 +1,56 @@ +import { type BarDatum } from '@nivo/bar'; +import { isDefined } from 'twenty-shared/utils'; + +type ApplyCumulativeTransformToTwoDimensionalBarChartDataOptions = { + data: BarDatum[]; + keys: string[]; + rangeMin?: number; + rangeMax?: number; +}; + +export const applyCumulativeTransformToTwoDimensionalBarChartData = ({ + data, + keys, + rangeMin, + rangeMax, +}: ApplyCumulativeTransformToTwoDimensionalBarChartDataOptions): BarDatum[] => { + const { result } = data.reduce<{ + result: BarDatum[]; + runningTotals: Record; + }>( + (accumulator, datum) => { + const newDatum = { ...datum }; + + for (const key of keys) { + const value = datum[key]; + + if (typeof value === 'number') { + accumulator.runningTotals[key] += value; + } + + newDatum[key] = accumulator.runningTotals[key]; + } + + const totalValue = keys.reduce((sum, key) => { + const value = newDatum[key]; + return sum + (typeof value === 'number' ? value : 0); + }, 0); + + const isOutOfRange = + (isDefined(rangeMin) && totalValue < rangeMin) || + (isDefined(rangeMax) && totalValue > rangeMax); + + if (!isOutOfRange) { + accumulator.result.push(newDatum); + } + + return accumulator; + }, + { + result: [], + runningTotals: Object.fromEntries(keys.map((key) => [key, 0])), + }, + ); + + return result; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformGroupByDataToLineChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformGroupByDataToLineChartData.ts index 177f0004eb..0706a75445 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformGroupByDataToLineChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformGroupByDataToLineChartData.ts @@ -89,8 +89,12 @@ export const transformGroupByDataToLineChartData = ({ const filteredResults = filterGroupByResults({ rawResults, filterOptions: { - rangeMin: configuration.rangeMin ?? undefined, - rangeMax: configuration.rangeMax ?? undefined, + rangeMin: configuration.isCumulative + ? undefined + : (configuration.rangeMin ?? undefined), + rangeMax: configuration.isCumulative + ? undefined + : (configuration.rangeMax ?? undefined), omitNullValues: configuration.omitNullValues ?? false, }, aggregateField, diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToLineChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToLineChartData.ts index b350fecbc8..66594abb24 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToLineChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToLineChartData.ts @@ -8,10 +8,11 @@ import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLin import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor'; import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult'; import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue'; +import { applyCumulativeTransformToLineChartData } from '@/page-layout/widgets/graph/utils/applyCumulativeTransformToLineChartData'; import { buildFormattedToRawLookup } from '@/page-layout/widgets/graph/utils/buildFormattedToRawLookup'; -import { formatPrimaryDimensionValues } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues'; import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult'; import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue'; +import { formatPrimaryDimensionValues } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues'; import { isDefined } from 'twenty-shared/utils'; import { type LineChartConfiguration } from '~/generated/graphql'; @@ -91,12 +92,20 @@ export const transformOneDimensionalGroupByToLineChartData = ({ }) .filter((point) => isDefined(point)); + const transformedData = configuration.isCumulative + ? applyCumulativeTransformToLineChartData({ + data, + rangeMin: configuration.rangeMin ?? undefined, + rangeMax: configuration.rangeMax ?? undefined, + }) + : data; + const series: LineChartSeries[] = [ { id: aggregateField.name, label: aggregateField.label, color: (configuration.color ?? GRAPH_DEFAULT_COLOR) as GraphColor, - data, + data: transformedData, }, ]; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToLineChartData.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToLineChartData.ts index 65d2ae24b1..5e9981a00e 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToLineChartData.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToLineChartData.ts @@ -7,10 +7,11 @@ import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLin import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor'; import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult'; import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue'; +import { applyCumulativeTransformToLineChartData } from '@/page-layout/widgets/graph/utils/applyCumulativeTransformToLineChartData'; import { buildFormattedToRawLookup } from '@/page-layout/widgets/graph/utils/buildFormattedToRawLookup'; -import { formatPrimaryDimensionValues } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues'; import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult'; import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue'; +import { formatPrimaryDimensionValues } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues'; import { sortLineChartSeries } from '@/page-layout/widgets/graph/utils/sortLineChartSeries'; import { isDefined } from 'twenty-shared/utils'; import { type LineChartConfiguration } from '~/generated/graphql'; @@ -117,11 +118,19 @@ export const transformTwoDimensionalGroupByToLineChartData = ({ y: xToYMap.get(xValue) ?? 0, })); + const transformedData = configuration.isCumulative + ? applyCumulativeTransformToLineChartData({ + data, + rangeMin: configuration.rangeMin ?? undefined, + rangeMax: configuration.rangeMax ?? undefined, + }) + : data; + return { id: seriesKey, label: seriesKey, color: configuration.color as GraphColor, - data, + data: transformedData, }; }, ); diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/bar-chart-configuration.dto.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/bar-chart-configuration.dto.ts index 104373e90a..8ac96b46ac 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/bar-chart-configuration.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/bar-chart-configuration.dto.ts @@ -145,6 +145,13 @@ export class BarChartConfigurationDTO { @IsOptional() groupMode?: BarChartGroupMode; + @Field(() => Boolean, { + nullable: true, + }) + @IsBoolean() + @IsOptional() + isCumulative?: boolean; + @Field(() => String, { nullable: true, defaultValue: 'UTC' }) @IsTimeZone() @IsOptional() diff --git a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto.ts b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto.ts index 4716974cc5..db244a5e38 100644 --- a/packages/twenty-server/src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/page-layout/dtos/line-chart-configuration.dto.ts @@ -146,6 +146,13 @@ export class LineChartConfigurationDTO { @IsOptional() isStacked?: boolean; + @Field(() => Boolean, { + nullable: true, + }) + @IsBoolean() + @IsOptional() + isCumulative?: boolean; + @Field(() => String, { nullable: true, defaultValue: 'UTC' }) @IsTimeZone() @IsOptional() diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts index 33915f7c76..bb9d2bbfa5 100644 --- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts +++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts @@ -58,6 +58,7 @@ export { IconCaretLeft, IconCaretRight, IconChartBar, + IconChartBarPopular, IconChartCandle, IconChartDots3, IconChartLine, diff --git a/packages/twenty-ui/src/display/index.ts b/packages/twenty-ui/src/display/index.ts index 9609b833a7..923d2e5e1a 100644 --- a/packages/twenty-ui/src/display/index.ts +++ b/packages/twenty-ui/src/display/index.ts @@ -123,6 +123,7 @@ export { IconCaretLeft, IconCaretRight, IconChartBar, + IconChartBarPopular, IconChartCandle, IconChartDots3, IconChartLine,