[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
This commit is contained in:
Raphaël Bosi
2025-12-02 14:00:58 +01:00
committed by GitHub
parent 00e970bdf4
commit 1038efa3dd
34 changed files with 766 additions and 19 deletions
@@ -350,6 +350,7 @@ export type BarChartConfiguration = {
firstDayOfTheWeek?: Maybe<Scalars['Int']>;
graphType: GraphType;
groupMode?: Maybe<BarChartGroupMode>;
isCumulative?: Maybe<Scalars['Boolean']>;
omitNullValues?: Maybe<Scalars['Boolean']>;
primaryAxisDateGranularity?: Maybe<ObjectRecordGroupByDateGranularity>;
primaryAxisGroupByFieldMetadataId: Scalars['UUID'];
@@ -1699,6 +1700,7 @@ export type LineChartConfiguration = {
filter?: Maybe<Scalars['JSON']>;
firstDayOfTheWeek?: Maybe<Scalars['Int']>;
graphType: GraphType;
isCumulative?: Maybe<Scalars['Boolean']>;
isStacked?: Maybe<Scalars['Boolean']>;
omitNullValues?: Maybe<Scalars['Boolean']>;
primaryAxisDateGranularity?: Maybe<ObjectRecordGroupByDateGranularity>;
@@ -350,6 +350,7 @@ export type BarChartConfiguration = {
firstDayOfTheWeek?: Maybe<Scalars['Int']>;
graphType: GraphType;
groupMode?: Maybe<BarChartGroupMode>;
isCumulative?: Maybe<Scalars['Boolean']>;
omitNullValues?: Maybe<Scalars['Boolean']>;
primaryAxisDateGranularity?: Maybe<ObjectRecordGroupByDateGranularity>;
primaryAxisGroupByFieldMetadataId: Scalars['UUID'];
@@ -1675,6 +1676,7 @@ export type LineChartConfiguration = {
filter?: Maybe<Scalars['JSON']>;
firstDayOfTheWeek?: Maybe<Scalars['Int']>;
graphType: GraphType;
isCumulative?: Maybe<Scalars['Boolean']>;
isStacked?: Maybe<Scalars['Boolean']>;
omitNullValues?: Maybe<Scalars['Boolean']>;
primaryAxisDateGranularity?: Maybe<ObjectRecordGroupByDateGranularity>;
@@ -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
}
@@ -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();
@@ -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,
],
@@ -26,4 +26,5 @@ export const CHART_CONFIGURATION_SETTING_LABELS = {
SHOW_LEGEND: msg`Legend`,
PREFIX: msg`Prefix`,
SUFFIX: msg`Suffix`,
CUMULATIVE: msg`Cumulative`,
};
@@ -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,
};
@@ -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)
@@ -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;
@@ -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', () => {
@@ -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);
});
});
@@ -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<T extends ChartConfiguration> = {
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<T>) => {
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 } : {}),
};
}
@@ -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,
];
@@ -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);
};
@@ -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;
@@ -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
}
@@ -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,
@@ -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,
@@ -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,
@@ -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,
},
]
`;
@@ -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,
},
]
`;
@@ -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,
},
]
`;
@@ -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();
},
);
});
@@ -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();
});
});
@@ -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();
},
);
});
@@ -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;
};
@@ -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;
};
@@ -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<string, number>;
}>(
(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;
};
@@ -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,
@@ -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,
},
];
@@ -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,
};
},
);
@@ -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()
@@ -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()
@@ -58,6 +58,7 @@ export {
IconCaretLeft,
IconCaretRight,
IconChartBar,
IconChartBarPopular,
IconChartCandle,
IconChartDots3,
IconChartLine,
+1
View File
@@ -123,6 +123,7 @@ export {
IconCaretLeft,
IconCaretRight,
IconChartBar,
IconChartBarPopular,
IconChartCandle,
IconChartDots3,
IconChartLine,