[Dashboards] add primary axis select gap fill for bar and line charts (#17098)
closes https://github.com/twentyhq/core-team-issues/issues/2056 https://github.com/user-attachments/assets/44eb2704-9ea9-4f2b-8941-6dd5c12d6276
This commit is contained in:
+16
-2
@@ -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';
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
isFieldMetadataDateKind,
|
||||
type FirstDayOfTheWeek,
|
||||
} from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import {
|
||||
AxisNameDisplay,
|
||||
BarChartLayout,
|
||||
@@ -244,9 +246,21 @@ export const transformGroupByDataToBarChartData = ({
|
||||
const filteredResultsWithDateGaps = dateGapFillResult.data;
|
||||
const dateRangeWasTruncated = dateGapFillResult.wasTruncated;
|
||||
|
||||
const isSingleSelectField = groupByFieldX.type === FieldMetadataType.SELECT;
|
||||
const shouldApplySelectGapFill = isSingleSelectField && !omitNullValues;
|
||||
|
||||
const resultsWithAllGapsFilled = shouldApplySelectGapFill
|
||||
? fillSelectGapsInChartData({
|
||||
data: filteredResultsWithDateGaps,
|
||||
selectOptions: groupByFieldX.options,
|
||||
aggregateKeys: [aggregateField.name],
|
||||
hasSecondDimension: isDefined(groupByFieldY),
|
||||
})
|
||||
: filteredResultsWithDateGaps;
|
||||
|
||||
const baseResult = isDefined(groupByFieldY)
|
||||
? transformTwoDimensionalGroupByToBarChartData({
|
||||
rawResults: filteredResultsWithDateGaps,
|
||||
rawResults: resultsWithAllGapsFilled,
|
||||
groupByFieldX,
|
||||
groupByFieldY,
|
||||
aggregateField,
|
||||
@@ -258,7 +272,7 @@ export const transformGroupByDataToBarChartData = ({
|
||||
firstDayOfTheWeek,
|
||||
})
|
||||
: transformOneDimensionalGroupByToBarChartData({
|
||||
rawResults: filteredResultsWithDateGaps,
|
||||
rawResults: resultsWithAllGapsFilled,
|
||||
groupByFieldX,
|
||||
aggregateField,
|
||||
configuration: sanitizedConfiguration,
|
||||
|
||||
+17
-2
@@ -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';
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
isDefined,
|
||||
isFieldMetadataDateKind,
|
||||
} from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import {
|
||||
AxisNameDisplay,
|
||||
type LineChartConfiguration,
|
||||
@@ -152,6 +154,19 @@ export const transformGroupByDataToLineChartData = ({
|
||||
const showDataLabels = configuration.displayDataLabel ?? false;
|
||||
const showLegend = configuration.displayLegend ?? true;
|
||||
|
||||
const omitNullValues = configuration.omitNullValues ?? false;
|
||||
const isSingleSelectField = groupByFieldX.type === FieldMetadataType.SELECT;
|
||||
const shouldApplySelectGapFill = isSingleSelectField && !omitNullValues;
|
||||
|
||||
const resultsWithSelectGaps = shouldApplySelectGapFill
|
||||
? fillSelectGapsInChartData({
|
||||
data: filteredResults,
|
||||
selectOptions: groupByFieldX.options,
|
||||
aggregateKeys: [aggregateField.name],
|
||||
hasSecondDimension: isDefined(groupByFieldY),
|
||||
})
|
||||
: filteredResults;
|
||||
|
||||
const isDateField = isFieldMetadataDateKind(groupByFieldX.type);
|
||||
const isNestedDateField = isRelationNestedFieldDateKind({
|
||||
relationField: groupByFieldX,
|
||||
@@ -190,7 +205,7 @@ export const transformGroupByDataToLineChartData = ({
|
||||
|
||||
const baseResult = isDefined(groupByFieldY)
|
||||
? transformTwoDimensionalGroupByToLineChartData({
|
||||
rawResults: filteredResults,
|
||||
rawResults: resultsWithSelectGaps,
|
||||
groupByFieldX,
|
||||
groupByFieldY,
|
||||
aggregateField,
|
||||
@@ -202,7 +217,7 @@ export const transformGroupByDataToLineChartData = ({
|
||||
firstDayOfTheWeek,
|
||||
})
|
||||
: transformOneDimensionalGroupByToLineChartData({
|
||||
rawResults: filteredResults,
|
||||
rawResults: resultsWithSelectGaps,
|
||||
groupByFieldX,
|
||||
aggregateField,
|
||||
configuration: sanitizedConfiguration,
|
||||
|
||||
+48
@@ -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);
|
||||
});
|
||||
});
|
||||
+133
@@ -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).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).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).toEqual(data);
|
||||
});
|
||||
|
||||
it('returns empty data unchanged', () => {
|
||||
const result = fillSelectGapsInChartData({
|
||||
data: [],
|
||||
selectOptions: [...selectOptions],
|
||||
aggregateKeys: ['count'],
|
||||
});
|
||||
|
||||
expect(result).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).toHaveLength(3);
|
||||
expect(result[0]).toEqual({
|
||||
groupByDimensionValues: ['A'],
|
||||
count: 5,
|
||||
});
|
||||
expect(result[1]).toEqual({
|
||||
groupByDimensionValues: ['B'],
|
||||
count: 0,
|
||||
});
|
||||
expect(result[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).toHaveLength(6);
|
||||
|
||||
expect(
|
||||
result.filter((r) => r.groupByDimensionValues[0] === 'A'),
|
||||
).toHaveLength(2);
|
||||
expect(
|
||||
result.filter((r) => r.groupByDimensionValues[0] === 'B'),
|
||||
).toHaveLength(2);
|
||||
expect(
|
||||
result.filter((r) => r.groupByDimensionValues[0] === 'C'),
|
||||
).toHaveLength(2);
|
||||
|
||||
expect(
|
||||
result.find(
|
||||
(r) =>
|
||||
r.groupByDimensionValues[0] === 'B' &&
|
||||
r.groupByDimensionValues[1] === 'X',
|
||||
),
|
||||
).toEqual({
|
||||
groupByDimensionValues: ['B', 'X'],
|
||||
count: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+88
@@ -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).toHaveLength(3);
|
||||
expect(result[0]).toEqual({
|
||||
groupByDimensionValues: ['A'],
|
||||
count: 5,
|
||||
});
|
||||
expect(result[1]).toEqual({
|
||||
groupByDimensionValues: ['B'],
|
||||
count: 0,
|
||||
});
|
||||
expect(result[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).toHaveLength(3);
|
||||
expect(result).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).toHaveLength(3);
|
||||
expect(result[0].groupByDimensionValues[0]).toBe('A');
|
||||
expect(result[1].groupByDimensionValues[0]).toBe('B');
|
||||
expect(result[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).toHaveLength(3);
|
||||
expect(result[1]).toEqual({
|
||||
groupByDimensionValues: ['B'],
|
||||
count: 0,
|
||||
sum: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
import { fillSelectGapsInTwoDimensionalChartData } from '@/page-layout/widgets/graph/utils/fillSelectGapsInTwoDimensionalChartData';
|
||||
|
||||
describe('fillSelectGapsInTwoDimensionalChartData', () => {
|
||||
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 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 = fillSelectGapsInTwoDimensionalChartData({
|
||||
data,
|
||||
selectOptions: [...selectOptions],
|
||||
aggregateKeys: ['count'],
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(6);
|
||||
|
||||
expect(
|
||||
result.filter((r) => r.groupByDimensionValues[0] === 'A'),
|
||||
).toHaveLength(2);
|
||||
expect(
|
||||
result.filter((r) => r.groupByDimensionValues[0] === 'B'),
|
||||
).toHaveLength(2);
|
||||
expect(
|
||||
result.filter((r) => r.groupByDimensionValues[0] === 'C'),
|
||||
).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('creates zero-value entries for missing combinations', () => {
|
||||
const data = [
|
||||
{ groupByDimensionValues: ['A', 'X'], count: 5 },
|
||||
{ groupByDimensionValues: ['C', 'Y'], count: 3 },
|
||||
];
|
||||
|
||||
const result = fillSelectGapsInTwoDimensionalChartData({
|
||||
data,
|
||||
selectOptions: [...selectOptions],
|
||||
aggregateKeys: ['count'],
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(6);
|
||||
|
||||
expect(
|
||||
result.find(
|
||||
(r) =>
|
||||
r.groupByDimensionValues[0] === 'B' &&
|
||||
r.groupByDimensionValues[1] === 'X',
|
||||
),
|
||||
).toEqual({
|
||||
groupByDimensionValues: ['B', 'X'],
|
||||
count: 0,
|
||||
});
|
||||
|
||||
expect(
|
||||
result.find(
|
||||
(r) =>
|
||||
r.groupByDimensionValues[0] === 'A' &&
|
||||
r.groupByDimensionValues[1] === 'Y',
|
||||
),
|
||||
).toEqual({
|
||||
groupByDimensionValues: ['A', 'Y'],
|
||||
count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves existing combinations', () => {
|
||||
const data = [
|
||||
{ groupByDimensionValues: ['A', 'X'], count: 5 },
|
||||
{ groupByDimensionValues: ['B', 'X'], count: 10 },
|
||||
{ groupByDimensionValues: ['C', 'X'], count: 15 },
|
||||
{ groupByDimensionValues: ['A', 'Y'], count: 20 },
|
||||
{ groupByDimensionValues: ['B', 'Y'], count: 25 },
|
||||
{ groupByDimensionValues: ['C', 'Y'], count: 30 },
|
||||
];
|
||||
|
||||
const result = fillSelectGapsInTwoDimensionalChartData({
|
||||
data,
|
||||
selectOptions: [...selectOptions],
|
||||
aggregateKeys: ['count'],
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(6);
|
||||
|
||||
expect(
|
||||
result.find(
|
||||
(r) =>
|
||||
r.groupByDimensionValues[0] === 'A' &&
|
||||
r.groupByDimensionValues[1] === 'X',
|
||||
),
|
||||
).toEqual({
|
||||
groupByDimensionValues: ['A', 'X'],
|
||||
count: 5,
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves selectOptions order for primary axis', () => {
|
||||
const data = [
|
||||
{ groupByDimensionValues: ['C', 'X'], count: 3 },
|
||||
{ groupByDimensionValues: ['A', 'X'], count: 5 },
|
||||
];
|
||||
|
||||
const result = fillSelectGapsInTwoDimensionalChartData({
|
||||
data,
|
||||
selectOptions: [...selectOptions],
|
||||
aggregateKeys: ['count'],
|
||||
});
|
||||
|
||||
const primaryValues = result.map((r) => r.groupByDimensionValues[0]);
|
||||
const uniquePrimary = [...new Set(primaryValues)];
|
||||
|
||||
expect(uniquePrimary[0]).toBe('A');
|
||||
expect(uniquePrimary[1]).toBe('B');
|
||||
expect(uniquePrimary[2]).toBe('C');
|
||||
});
|
||||
|
||||
it('handles null secondary dimension values', () => {
|
||||
const data = [
|
||||
{ groupByDimensionValues: ['A', null], count: 5 },
|
||||
{ groupByDimensionValues: ['C', null], count: 3 },
|
||||
];
|
||||
|
||||
const result = fillSelectGapsInTwoDimensionalChartData({
|
||||
data,
|
||||
selectOptions: [...selectOptions],
|
||||
aggregateKeys: ['count'],
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
|
||||
expect(
|
||||
result.find(
|
||||
(r) =>
|
||||
r.groupByDimensionValues[0] === 'B' &&
|
||||
r.groupByDimensionValues[1] === null,
|
||||
),
|
||||
).toEqual({
|
||||
groupByDimensionValues: ['B', null],
|
||||
count: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('does NOT fill missing secondary axis values', () => {
|
||||
const data = [
|
||||
{ groupByDimensionValues: ['A', 'X'], count: 5 },
|
||||
{ groupByDimensionValues: ['A', 'Y'], count: 3 },
|
||||
];
|
||||
|
||||
const result = fillSelectGapsInTwoDimensionalChartData({
|
||||
data,
|
||||
selectOptions: [...selectOptions],
|
||||
aggregateKeys: ['count'],
|
||||
});
|
||||
|
||||
const secondaryValues = result.map((r) => r.groupByDimensionValues[1]);
|
||||
const uniqueSecondary = [...new Set(secondaryValues)];
|
||||
|
||||
expect(uniqueSecondary).toHaveLength(2);
|
||||
expect(uniqueSecondary).toContain('X');
|
||||
expect(uniqueSecondary).toContain('Y');
|
||||
});
|
||||
});
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
|
||||
export const createEmptySelectGroup = (
|
||||
dimensionValues: RawDimensionValue[],
|
||||
aggregateKeys: string[],
|
||||
): GroupByRawResult => {
|
||||
const newItem: GroupByRawResult = {
|
||||
groupByDimensionValues: dimensionValues,
|
||||
};
|
||||
|
||||
for (const key of aggregateKeys) {
|
||||
newItem[key] = 0;
|
||||
}
|
||||
|
||||
return newItem;
|
||||
};
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { fillSelectGapsInOneDimensionalChartData } from '@/page-layout/widgets/graph/utils/fillSelectGapsInOneDimensionalChartData';
|
||||
import { fillSelectGapsInTwoDimensionalChartData } from '@/page-layout/widgets/graph/utils/fillSelectGapsInTwoDimensionalChartData';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type FillSelectGapsParams = {
|
||||
data: GroupByRawResult[];
|
||||
selectOptions: FieldMetadataItemOption[] | null | undefined;
|
||||
aggregateKeys: string[];
|
||||
hasSecondDimension?: boolean;
|
||||
};
|
||||
|
||||
export const fillSelectGapsInChartData = ({
|
||||
data,
|
||||
selectOptions,
|
||||
aggregateKeys,
|
||||
hasSecondDimension = false,
|
||||
}: FillSelectGapsParams): GroupByRawResult[] => {
|
||||
if (
|
||||
!isDefined(selectOptions) ||
|
||||
selectOptions.length === 0 ||
|
||||
data.length === 0
|
||||
) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return hasSecondDimension
|
||||
? fillSelectGapsInTwoDimensionalChartData({
|
||||
data,
|
||||
selectOptions,
|
||||
aggregateKeys,
|
||||
})
|
||||
: fillSelectGapsInOneDimensionalChartData({
|
||||
data,
|
||||
selectOptions,
|
||||
aggregateKeys,
|
||||
});
|
||||
};
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { createEmptySelectGroup } from '@/page-layout/widgets/graph/utils/createEmptySelectGroup';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type OneDimensionalFillSelectGapsParams = {
|
||||
data: GroupByRawResult[];
|
||||
selectOptions: FieldMetadataItemOption[];
|
||||
aggregateKeys: string[];
|
||||
};
|
||||
|
||||
export const fillSelectGapsInOneDimensionalChartData = ({
|
||||
data,
|
||||
selectOptions,
|
||||
aggregateKeys,
|
||||
}: OneDimensionalFillSelectGapsParams): GroupByRawResult[] => {
|
||||
const existingGroupsMap = new Map<string, GroupByRawResult>();
|
||||
|
||||
for (const item of data) {
|
||||
const dimensionValue = item.groupByDimensionValues?.[0];
|
||||
|
||||
if (isDefined(dimensionValue)) {
|
||||
existingGroupsMap.set(String(dimensionValue), item);
|
||||
}
|
||||
}
|
||||
|
||||
const filledData: GroupByRawResult[] = selectOptions.map((option) => {
|
||||
const existingGroup = existingGroupsMap.get(option.value);
|
||||
|
||||
return isDefined(existingGroup)
|
||||
? existingGroup
|
||||
: createEmptySelectGroup([option.value], aggregateKeys);
|
||||
});
|
||||
|
||||
return filledData;
|
||||
};
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { createEmptySelectGroup } from '@/page-layout/widgets/graph/utils/createEmptySelectGroup';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type TwoDimensionalFillSelectGapsParams = {
|
||||
data: GroupByRawResult[];
|
||||
selectOptions: FieldMetadataItemOption[];
|
||||
aggregateKeys: string[];
|
||||
};
|
||||
|
||||
export const fillSelectGapsInTwoDimensionalChartData = ({
|
||||
data,
|
||||
selectOptions,
|
||||
aggregateKeys,
|
||||
}: TwoDimensionalFillSelectGapsParams): GroupByRawResult[] => {
|
||||
const existingGroupsMap = new Map<string, GroupByRawResult>();
|
||||
const uniqueSecondDimensionValues = new Set<RawDimensionValue>();
|
||||
|
||||
for (const item of data) {
|
||||
const primaryDimensionValue = item.groupByDimensionValues?.[0];
|
||||
|
||||
if (!isDefined(primaryDimensionValue)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const secondDimensionValue = (item.groupByDimensionValues?.[1] ??
|
||||
null) as RawDimensionValue;
|
||||
uniqueSecondDimensionValues.add(secondDimensionValue);
|
||||
|
||||
const key = `${String(primaryDimensionValue)}_${String(secondDimensionValue)}`;
|
||||
existingGroupsMap.set(key, item);
|
||||
}
|
||||
|
||||
const filledData = selectOptions.flatMap((option) =>
|
||||
Array.from(uniqueSecondDimensionValues).map((secondDimensionValue) => {
|
||||
const key = `${option.value}_${String(secondDimensionValue)}`;
|
||||
const existingGroup = existingGroupsMap.get(key);
|
||||
|
||||
return isDefined(existingGroup)
|
||||
? existingGroup
|
||||
: createEmptySelectGroup(
|
||||
[option.value, secondDimensionValue],
|
||||
aggregateKeys,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
return filledData;
|
||||
};
|
||||
Reference in New Issue
Block a user