Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getGroupByQueryResultGqlFieldName } from '@/page-layout/utils/getGroupB
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity';
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
import { fillDateGapsInBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/fillDateGapsInBarChartData';
import { fillSelectGapsInChartData } from '@/page-layout/widgets/graph/utils/fillSelectGapsInChartData';
import { transformOneDimensionalGroupByToBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/transformOneDimensionalGroupByToBarChartData';
import { transformTwoDimensionalGroupByToBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/transformTwoDimensionalGroupByToBarChartData';
import { type GraphColorMode } from '@/page-layout/widgets/graph/types/GraphColorMode';
Expand All @@ -18,6 +19,7 @@ import { type BarDatum } from '@nivo/bar';
import {
isDefined,
isFieldMetadataDateKind,
isFieldMetadataSelectKind,
type FirstDayOfTheWeek,
} from 'twenty-shared/utils';
import {
Expand Down Expand Up @@ -244,9 +246,23 @@ export const transformGroupByDataToBarChartData = ({
const filteredResultsWithDateGaps = dateGapFillResult.data;
const dateRangeWasTruncated = dateGapFillResult.wasTruncated;

const isSelectField = isFieldMetadataSelectKind(groupByFieldX.type);
const shouldApplySelectGapFill = isSelectField && !omitNullValues;

const selectGapFillResult = shouldApplySelectGapFill
? fillSelectGapsInChartData({
data: filteredResultsWithDateGaps,
selectOptions: groupByFieldX.options,
aggregateKeys: [aggregateField.name],
hasSecondDimension: isDefined(groupByFieldY),
})
: { data: filteredResultsWithDateGaps };

const resultsWithAllGapsFilled = selectGapFillResult.data;

const baseResult = isDefined(groupByFieldY)
? transformTwoDimensionalGroupByToBarChartData({
rawResults: filteredResultsWithDateGaps,
rawResults: resultsWithAllGapsFilled,
groupByFieldX,
groupByFieldY,
aggregateField,
Expand All @@ -258,7 +274,7 @@ export const transformGroupByDataToBarChartData = ({
firstDayOfTheWeek,
})
: transformOneDimensionalGroupByToBarChartData({
rawResults: filteredResultsWithDateGaps,
rawResults: resultsWithAllGapsFilled,
groupByFieldX,
aggregateField,
configuration: sanitizedConfiguration,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getGroupByQueryResultGqlFieldName } from '@/page-layout/utils/getGroupB
import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeries';
import { transformOneDimensionalGroupByToLineChartData } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/transformOneDimensionalGroupByToLineChartData';
import { transformTwoDimensionalGroupByToLineChartData } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/transformTwoDimensionalGroupByToLineChartData';
import { fillSelectGapsInChartData } from '@/page-layout/widgets/graph/utils/fillSelectGapsInChartData';
import { type GraphColorMode } from '@/page-layout/widgets/graph/types/GraphColorMode';
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
Expand All @@ -15,6 +16,7 @@ import {
type FirstDayOfTheWeek,
isDefined,
isFieldMetadataDateKind,
isFieldMetadataSelectKind,
} from 'twenty-shared/utils';
import {
AxisNameDisplay,
Expand Down Expand Up @@ -152,6 +154,21 @@ export const transformGroupByDataToLineChartData = ({
const showDataLabels = configuration.displayDataLabel ?? false;
const showLegend = configuration.displayLegend ?? true;

const omitNullValues = configuration.omitNullValues ?? false;
const isSelectField = isFieldMetadataSelectKind(groupByFieldX.type);
const shouldApplySelectGapFill = isSelectField && !omitNullValues;

const selectGapFillResult = shouldApplySelectGapFill
? fillSelectGapsInChartData({
data: filteredResults,
selectOptions: groupByFieldX.options,
aggregateKeys: [aggregateField.name],
hasSecondDimension: isDefined(groupByFieldY),
})
: { data: filteredResults };

const resultsWithSelectGaps = selectGapFillResult.data;

const isDateField = isFieldMetadataDateKind(groupByFieldX.type);
const isNestedDateField = isRelationNestedFieldDateKind({
relationField: groupByFieldX,
Expand Down Expand Up @@ -190,7 +207,7 @@ export const transformGroupByDataToLineChartData = ({

const baseResult = isDefined(groupByFieldY)
? transformTwoDimensionalGroupByToLineChartData({
rawResults: filteredResults,
rawResults: resultsWithSelectGaps,
groupByFieldX,
groupByFieldY,
aggregateField,
Expand All @@ -202,7 +219,7 @@ export const transformGroupByDataToLineChartData = ({
firstDayOfTheWeek,
})
: transformOneDimensionalGroupByToLineChartData({
rawResults: filteredResults,
rawResults: resultsWithSelectGaps,
groupByFieldX,
aggregateField,
configuration: sanitizedConfiguration,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';

export type FillSelectGapsResult = {
data: GroupByRawResult[];
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { createEmptySelectGroup } from '@/page-layout/widgets/graph/utils/createEmptySelectGroup';

describe('createEmptySelectGroup', () => {
it('creates record with correct groupByDimensionValues', () => {
const result = createEmptySelectGroup(['A'], ['count']);

expect(result.groupByDimensionValues).toEqual(['A']);
});

it('initializes all aggregateKeys to 0', () => {
const result = createEmptySelectGroup(['A'], ['count']);

expect(result.count).toBe(0);
});

it('handles empty aggregateKeys array', () => {
const result = createEmptySelectGroup(['A'], []);

expect(result).toEqual({
groupByDimensionValues: ['A'],
});
});

it('handles multiple aggregate keys', () => {
const result = createEmptySelectGroup(['A'], ['count', 'sum', 'avg']);

expect(result).toEqual({
groupByDimensionValues: ['A'],
count: 0,
sum: 0,
avg: 0,
});
});

it('handles null values in dimensionValues', () => {
const result = createEmptySelectGroup(['A', null], ['count']);

expect(result.groupByDimensionValues).toEqual(['A', null]);
expect(result.count).toBe(0);
});

it('handles multiple dimension values', () => {
const result = createEmptySelectGroup(['A', 'X', 'Y'], ['count']);

expect(result.groupByDimensionValues).toEqual(['A', 'X', 'Y']);
expect(result.count).toBe(0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { fillSelectGapsInChartData } from '@/page-layout/widgets/graph/utils/fillSelectGapsInChartData';

describe('fillSelectGapsInChartData', () => {
const selectOptions = [
{ id: '1', label: 'Option A', value: 'A', position: 0, color: 'green' },
{ id: '2', label: 'Option B', value: 'B', position: 1, color: 'blue' },
{ id: '3', label: 'Option C', value: 'C', position: 2, color: 'red' },
] as const;

describe('one-dimensional data', () => {
it('returns data unchanged when selectOptions is undefined', () => {
const data = [
{ groupByDimensionValues: ['A'], count: 5 },
{ groupByDimensionValues: ['B'], count: 3 },
];

const result = fillSelectGapsInChartData({
data,
selectOptions: undefined,
aggregateKeys: ['count'],
});

expect(result.data).toEqual(data);
});

it('returns data unchanged when selectOptions is null', () => {
const data = [
{ groupByDimensionValues: ['A'], count: 5 },
{ groupByDimensionValues: ['B'], count: 3 },
];

const result = fillSelectGapsInChartData({
data,
selectOptions: null,
aggregateKeys: ['count'],
});

expect(result.data).toEqual(data);
});

it('returns data unchanged when selectOptions is empty', () => {
const data = [
{ groupByDimensionValues: ['A'], count: 5 },
{ groupByDimensionValues: ['B'], count: 3 },
];

const result = fillSelectGapsInChartData({
data,
selectOptions: [],
aggregateKeys: ['count'],
});

expect(result.data).toEqual(data);
});

it('returns empty data unchanged', () => {
const result = fillSelectGapsInChartData({
data: [],
selectOptions: [...selectOptions],
aggregateKeys: ['count'],
});

expect(result.data).toEqual([]);
});

it('fills missing select options with zero values', () => {
const data = [
{ groupByDimensionValues: ['A'], count: 5 },
{ groupByDimensionValues: ['C'], count: 3 },
];

const result = fillSelectGapsInChartData({
data,
selectOptions: [...selectOptions],
aggregateKeys: ['count'],
});

expect(result.data).toHaveLength(3);
expect(result.data[0]).toEqual({
groupByDimensionValues: ['A'],
count: 5,
});
expect(result.data[1]).toEqual({
groupByDimensionValues: ['B'],
count: 0,
});
expect(result.data[2]).toEqual({
groupByDimensionValues: ['C'],
count: 3,
});
});
});

describe('two-dimensional data', () => {
it('fills missing primary axis options for all secondary values', () => {
const data = [
{ groupByDimensionValues: ['A', 'X'], count: 5 },
{ groupByDimensionValues: ['C', 'X'], count: 3 },
{ groupByDimensionValues: ['A', 'Y'], count: 2 },
];

const result = fillSelectGapsInChartData({
data,
selectOptions: [...selectOptions],
aggregateKeys: ['count'],
hasSecondDimension: true,
});

expect(result.data).toHaveLength(6);

expect(
result.data.filter((r) => r.groupByDimensionValues[0] === 'A'),
).toHaveLength(2);
expect(
result.data.filter((r) => r.groupByDimensionValues[0] === 'B'),
).toHaveLength(2);
expect(
result.data.filter((r) => r.groupByDimensionValues[0] === 'C'),
).toHaveLength(2);

expect(
result.data.find(
(r) =>
r.groupByDimensionValues[0] === 'B' &&
r.groupByDimensionValues[1] === 'X',
),
).toEqual({
groupByDimensionValues: ['B', 'X'],
count: 0,
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { fillSelectGapsInOneDimensionalChartData } from '@/page-layout/widgets/graph/utils/fillSelectGapsInOneDimensionalChartData';

describe('fillSelectGapsInOneDimensionalChartData', () => {
const selectOptions = [
{ id: '1', label: 'Option A', value: 'A', position: 0, color: 'green' },
{ id: '2', label: 'Option B', value: 'B', position: 1, color: 'blue' },
{ id: '3', label: 'Option C', value: 'C', position: 2, color: 'red' },
] as const;

it('fills missing select options with zero values', () => {
const data = [
{ groupByDimensionValues: ['A'], count: 5 },
{ groupByDimensionValues: ['C'], count: 3 },
];

const result = fillSelectGapsInOneDimensionalChartData({
data,
selectOptions: [...selectOptions],
aggregateKeys: ['count'],
});

expect(result.data).toHaveLength(3);
expect(result.data[0]).toEqual({
groupByDimensionValues: ['A'],
count: 5,
});
expect(result.data[1]).toEqual({
groupByDimensionValues: ['B'],
count: 0,
});
expect(result.data[2]).toEqual({
groupByDimensionValues: ['C'],
count: 3,
});
});

it('preserves existing data values', () => {
const data = [
{ groupByDimensionValues: ['A'], count: 10, sum: 100 },
{ groupByDimensionValues: ['B'], count: 20, sum: 200 },
{ groupByDimensionValues: ['C'], count: 30, sum: 300 },
];

const result = fillSelectGapsInOneDimensionalChartData({
data,
selectOptions: [...selectOptions],
aggregateKeys: ['count', 'sum'],
});

expect(result.data).toHaveLength(3);
expect(result.data).toEqual(data);
});

it('preserves selectOptions order', () => {
const data = [
{ groupByDimensionValues: ['C'], count: 3 },
{ groupByDimensionValues: ['A'], count: 5 },
];

const result = fillSelectGapsInOneDimensionalChartData({
data,
selectOptions: [...selectOptions],
aggregateKeys: ['count'],
});

expect(result.data).toHaveLength(3);
expect(result.data[0].groupByDimensionValues[0]).toBe('A');
expect(result.data[1].groupByDimensionValues[0]).toBe('B');
expect(result.data[2].groupByDimensionValues[0]).toBe('C');
});

it('works with multiple aggregate keys', () => {
const data = [{ groupByDimensionValues: ['A'], count: 5, sum: 100 }];

const result = fillSelectGapsInOneDimensionalChartData({
data,
selectOptions: [...selectOptions],
aggregateKeys: ['count', 'sum'],
});

expect(result.data).toHaveLength(3);
expect(result.data[1]).toEqual({
groupByDimensionValues: ['B'],
count: 0,
sum: 0,
});
});
});
Loading
Loading