connect line chart to backend (#15657)
https://github.com/user-attachments/assets/b797c5a8-32a8-4d3e-9fb1-7541d107a19f optimizations: before: https://github.com/user-attachments/assets/bf45a3f1-5f89-40c1-9ce4-c2d912b77d63 after: https://github.com/user-attachments/assets/61bfd603-7b63-4695-8ae8-68fe7d3bfea0
This commit is contained in:
-59
@@ -1,59 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`generateGroupByQueryVariablesFromBarChartConfiguration Horizontal bar configuration should generate variables with secondary axis 1`] = `
|
||||
{
|
||||
"groupBy": [
|
||||
{
|
||||
"stage": true,
|
||||
},
|
||||
{
|
||||
"ownerId": true,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`generateGroupByQueryVariablesFromBarChartConfiguration Horizontal bar configuration should generate variables with single groupBy field 1`] = `
|
||||
{
|
||||
"groupBy": [
|
||||
{
|
||||
"stage": true,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`generateGroupByQueryVariablesFromBarChartConfiguration Vertical bar configuration should generate variables with composite field 1`] = `
|
||||
{
|
||||
"groupBy": [
|
||||
{
|
||||
"name": {
|
||||
"firstName": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`generateGroupByQueryVariablesFromBarChartConfiguration Vertical bar configuration should generate variables with single groupBy field 1`] = `
|
||||
{
|
||||
"groupBy": [
|
||||
{
|
||||
"stage": true,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`generateGroupByQueryVariablesFromBarChartConfiguration Vertical bar configuration should generate variables with two groupBy fields 1`] = `
|
||||
{
|
||||
"groupBy": [
|
||||
{
|
||||
"stage": true,
|
||||
},
|
||||
{
|
||||
"ownerId": true,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`generateGroupByQueryVariablesFromChartConfiguration Bar Chart Configuration Horizontal bar configuration should generate variables with secondary axis 1`] = `
|
||||
{
|
||||
"groupBy": [
|
||||
{
|
||||
"stage": true,
|
||||
},
|
||||
{
|
||||
"ownerId": true,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`generateGroupByQueryVariablesFromChartConfiguration Bar Chart Configuration Horizontal bar configuration should generate variables with single groupBy field 1`] = `
|
||||
{
|
||||
"groupBy": [
|
||||
{
|
||||
"stage": true,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`generateGroupByQueryVariablesFromChartConfiguration Bar Chart Configuration Vertical bar configuration should generate variables with composite field 1`] = `
|
||||
{
|
||||
"groupBy": [
|
||||
{
|
||||
"name": {
|
||||
"firstName": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`generateGroupByQueryVariablesFromChartConfiguration Bar Chart Configuration Vertical bar configuration should generate variables with date field and granularity 1`] = `
|
||||
{
|
||||
"groupBy": [
|
||||
{
|
||||
"createdAt": {
|
||||
"granularity": "MONTH",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`generateGroupByQueryVariablesFromChartConfiguration Bar Chart Configuration Vertical bar configuration should generate variables with single groupBy field 1`] = `
|
||||
{
|
||||
"groupBy": [
|
||||
{
|
||||
"stage": true,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`generateGroupByQueryVariablesFromChartConfiguration Bar Chart Configuration Vertical bar configuration should generate variables with two groupBy fields 1`] = `
|
||||
{
|
||||
"groupBy": [
|
||||
{
|
||||
"stage": true,
|
||||
},
|
||||
{
|
||||
"ownerId": true,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`generateGroupByQueryVariablesFromChartConfiguration Line Chart Configuration should generate variables with composite field 1`] = `
|
||||
{
|
||||
"groupBy": [
|
||||
{
|
||||
"name": {
|
||||
"firstName": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`generateGroupByQueryVariablesFromChartConfiguration Line Chart Configuration should generate variables with date field and granularity 1`] = `
|
||||
{
|
||||
"groupBy": [
|
||||
{
|
||||
"createdAt": {
|
||||
"granularity": "MONTH",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`generateGroupByQueryVariablesFromChartConfiguration Line Chart Configuration should generate variables with secondary axis 1`] = `
|
||||
{
|
||||
"groupBy": [
|
||||
{
|
||||
"stage": true,
|
||||
},
|
||||
{
|
||||
"ownerId": true,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`generateGroupByQueryVariablesFromChartConfiguration Line Chart Configuration should generate variables with single groupBy field 1`] = `
|
||||
{
|
||||
"groupBy": [
|
||||
{
|
||||
"stage": true,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`generateGroupByQueryVariablesFromChartConfiguration Line Chart Configuration should generate variables with two groupBy fields (multi-series) 1`] = `
|
||||
{
|
||||
"groupBy": [
|
||||
{
|
||||
"stage": true,
|
||||
},
|
||||
{
|
||||
"ownerId": true,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import { calculateValueRangeFromValues } from '../calculateValueRangeFromValues';
|
||||
|
||||
describe('calculateValueRangeFromValues', () => {
|
||||
it('should return minimum=0 and maximum=highest value for all positive values', () => {
|
||||
const values = [5, 10, 15, 20];
|
||||
|
||||
const result = calculateValueRangeFromValues(values);
|
||||
|
||||
expect(result).toEqual({ minimum: 0, maximum: 20 });
|
||||
});
|
||||
|
||||
it('should return minimum=lowest value and maximum=0 for all negative values', () => {
|
||||
const values = [-5, -10, -15, -20];
|
||||
|
||||
const result = calculateValueRangeFromValues(values);
|
||||
|
||||
expect(result).toEqual({ minimum: -20, maximum: 0 });
|
||||
});
|
||||
|
||||
it('should include zero in range when values cross zero', () => {
|
||||
const values = [-10, -5, 5, 10];
|
||||
|
||||
const result = calculateValueRangeFromValues(values);
|
||||
|
||||
expect(result).toEqual({ minimum: -10, maximum: 10 });
|
||||
});
|
||||
|
||||
it('should handle empty array', () => {
|
||||
const values: number[] = [];
|
||||
|
||||
const result = calculateValueRangeFromValues(values);
|
||||
|
||||
expect(result).toEqual({ minimum: 0, maximum: 0 });
|
||||
});
|
||||
|
||||
it('should ignore NaN values', () => {
|
||||
const values = [5, NaN, 10, NaN, 15];
|
||||
|
||||
const result = calculateValueRangeFromValues(values);
|
||||
|
||||
expect(result).toEqual({ minimum: 0, maximum: 15 });
|
||||
});
|
||||
|
||||
it('should handle all NaN values', () => {
|
||||
const values = [NaN, NaN, NaN];
|
||||
|
||||
const result = calculateValueRangeFromValues(values);
|
||||
|
||||
expect(result).toEqual({ minimum: 0, maximum: 0 });
|
||||
});
|
||||
|
||||
it('should handle single value', () => {
|
||||
const values = [42];
|
||||
|
||||
const result = calculateValueRangeFromValues(values);
|
||||
|
||||
expect(result).toEqual({ minimum: 0, maximum: 42 });
|
||||
});
|
||||
|
||||
it('should handle single negative value', () => {
|
||||
const values = [-42];
|
||||
|
||||
const result = calculateValueRangeFromValues(values);
|
||||
|
||||
expect(result).toEqual({ minimum: -42, maximum: 0 });
|
||||
});
|
||||
|
||||
it('should handle zero value', () => {
|
||||
const values = [0];
|
||||
|
||||
const result = calculateValueRangeFromValues(values);
|
||||
|
||||
expect(result).toEqual({ minimum: 0, maximum: 0 });
|
||||
});
|
||||
});
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import { computeChartCategoryTickValues } from '../computeChartCategoryTickValues';
|
||||
|
||||
describe('computeChartCategoryTickValues', () => {
|
||||
it('should return all values when available size allows', () => {
|
||||
const values = ['A', 'B', 'C', 'D', 'E'];
|
||||
const result = computeChartCategoryTickValues({
|
||||
availableSize: 500,
|
||||
minimumSizePerTick: 100,
|
||||
values,
|
||||
});
|
||||
|
||||
expect(result).toEqual(['A', 'B', 'C', 'D', 'E']);
|
||||
});
|
||||
|
||||
it('should return subset of values when space is limited', () => {
|
||||
const values = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'];
|
||||
const result = computeChartCategoryTickValues({
|
||||
availableSize: 400,
|
||||
minimumSizePerTick: 100,
|
||||
values,
|
||||
});
|
||||
|
||||
expect(result.length).toBe(4);
|
||||
expect(result).toEqual(['A', 'C', 'F', 'H']);
|
||||
});
|
||||
|
||||
it('should return empty array when available size is zero', () => {
|
||||
const values = ['A', 'B', 'C'];
|
||||
const result = computeChartCategoryTickValues({
|
||||
availableSize: 0,
|
||||
minimumSizePerTick: 100,
|
||||
values,
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array when available size is negative', () => {
|
||||
const values = ['A', 'B', 'C'];
|
||||
const result = computeChartCategoryTickValues({
|
||||
availableSize: -100,
|
||||
minimumSizePerTick: 100,
|
||||
values,
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array when values array is empty', () => {
|
||||
const result = computeChartCategoryTickValues({
|
||||
availableSize: 500,
|
||||
minimumSizePerTick: 100,
|
||||
values: [],
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle single value', () => {
|
||||
const values = ['A'];
|
||||
const result = computeChartCategoryTickValues({
|
||||
availableSize: 500,
|
||||
minimumSizePerTick: 100,
|
||||
values,
|
||||
});
|
||||
|
||||
expect(result).toEqual(['A']);
|
||||
});
|
||||
|
||||
it('should work with numeric values', () => {
|
||||
const values = [1, 2, 3, 4, 5, 6, 7, 8];
|
||||
const result = computeChartCategoryTickValues({
|
||||
availableSize: 300,
|
||||
minimumSizePerTick: 100,
|
||||
values,
|
||||
});
|
||||
|
||||
expect(result.length).toBe(3);
|
||||
expect(result).toEqual([1, 5, 8]);
|
||||
});
|
||||
|
||||
it('should handle very small available size', () => {
|
||||
const values = ['A', 'B', 'C', 'D', 'E'];
|
||||
const result = computeChartCategoryTickValues({
|
||||
availableSize: 50,
|
||||
minimumSizePerTick: 100,
|
||||
values,
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { generateChartAggregateFilterKey } from '../generateChartAggregateFilterKey';
|
||||
|
||||
describe('generateChartAggregateFilterKey', () => {
|
||||
it('should generate key with all values provided', () => {
|
||||
const result = generateChartAggregateFilterKey(0, 100, true);
|
||||
|
||||
expect(result).toBe('0-100-true');
|
||||
});
|
||||
|
||||
it('should generate key with negative values', () => {
|
||||
const result = generateChartAggregateFilterKey(-50, 50, false);
|
||||
|
||||
expect(result).toBe('-50-50-false');
|
||||
});
|
||||
|
||||
it('should handle undefined values as empty strings', () => {
|
||||
const result = generateChartAggregateFilterKey(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(result).toBe('--');
|
||||
});
|
||||
|
||||
it('should handle null values as empty strings', () => {
|
||||
const result = generateChartAggregateFilterKey(null, null, null);
|
||||
|
||||
expect(result).toBe('--');
|
||||
});
|
||||
|
||||
it('should handle mixed defined and undefined values', () => {
|
||||
const result = generateChartAggregateFilterKey(10, undefined, true);
|
||||
|
||||
expect(result).toBe('10--true');
|
||||
});
|
||||
|
||||
it('should handle zero as valid value', () => {
|
||||
const result = generateChartAggregateFilterKey(0, 0, false);
|
||||
|
||||
expect(result).toBe('0-0-false');
|
||||
});
|
||||
|
||||
it('should handle no arguments', () => {
|
||||
const result = generateChartAggregateFilterKey();
|
||||
|
||||
expect(result).toBe('--');
|
||||
});
|
||||
|
||||
it('should handle decimal values', () => {
|
||||
const result = generateChartAggregateFilterKey(10.5, 99.9, true);
|
||||
|
||||
expect(result).toBe('10.5-99.9-true');
|
||||
});
|
||||
});
|
||||
-132
@@ -1,132 +0,0 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import {
|
||||
AggregateOperations,
|
||||
type BarChartConfiguration,
|
||||
FieldMetadataType,
|
||||
GraphType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { generateGroupByQueryVariablesFromBarChartConfiguration } from '../generateGroupByQueryVariablesFromBarChartConfiguration';
|
||||
|
||||
describe('generateGroupByQueryVariablesFromBarChartConfiguration', () => {
|
||||
const mockObjectMetadataItem: ObjectMetadataItem = {
|
||||
id: 'obj-1',
|
||||
nameSingular: 'opportunity',
|
||||
namePlural: 'opportunities',
|
||||
fields: [
|
||||
{
|
||||
id: 'field-1',
|
||||
name: 'stage',
|
||||
type: FieldMetadataType.TEXT,
|
||||
},
|
||||
{
|
||||
id: 'field-2',
|
||||
name: 'owner',
|
||||
type: FieldMetadataType.RELATION,
|
||||
},
|
||||
{
|
||||
id: 'field-3',
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
},
|
||||
{
|
||||
id: 'field-4',
|
||||
name: 'name',
|
||||
type: FieldMetadataType.FULL_NAME,
|
||||
},
|
||||
],
|
||||
} as ObjectMetadataItem;
|
||||
|
||||
const buildConfiguration = (
|
||||
overrides: Partial<BarChartConfiguration>,
|
||||
): BarChartConfiguration =>
|
||||
({
|
||||
__typename: 'BarChartConfiguration',
|
||||
aggregateFieldMetadataId: 'aggregate-field',
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
graphType: GraphType.VERTICAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-1',
|
||||
...overrides,
|
||||
}) as BarChartConfiguration;
|
||||
|
||||
describe('Vertical bar configuration', () => {
|
||||
it('should generate variables with single groupBy field', () => {
|
||||
const result = generateGroupByQueryVariablesFromBarChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
barChartConfiguration: buildConfiguration({
|
||||
graphType: GraphType.VERTICAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-1',
|
||||
primaryAxisGroupBySubFieldName: null,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should generate variables with two groupBy fields', () => {
|
||||
const result = generateGroupByQueryVariablesFromBarChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
barChartConfiguration: buildConfiguration({
|
||||
graphType: GraphType.VERTICAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-1',
|
||||
primaryAxisGroupBySubFieldName: null,
|
||||
secondaryAxisGroupByFieldMetadataId: 'field-2',
|
||||
secondaryAxisGroupBySubFieldName: null,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should generate variables with composite field', () => {
|
||||
const result = generateGroupByQueryVariablesFromBarChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
barChartConfiguration: buildConfiguration({
|
||||
graphType: GraphType.VERTICAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-4',
|
||||
primaryAxisGroupBySubFieldName: 'firstName',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Horizontal bar configuration', () => {
|
||||
it('should generate variables with single groupBy field', () => {
|
||||
const result = generateGroupByQueryVariablesFromBarChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
barChartConfiguration: buildConfiguration({
|
||||
graphType: GraphType.HORIZONTAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-1',
|
||||
primaryAxisGroupBySubFieldName: null,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should generate variables with secondary axis', () => {
|
||||
const result = generateGroupByQueryVariablesFromBarChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
barChartConfiguration: buildConfiguration({
|
||||
graphType: GraphType.HORIZONTAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-1',
|
||||
secondaryAxisGroupByFieldMetadataId: 'field-2',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error when primary axis field not found', () => {
|
||||
expect(() =>
|
||||
generateGroupByQueryVariablesFromBarChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
barChartConfiguration: buildConfiguration({
|
||||
primaryAxisGroupByFieldMetadataId: 'invalid-field',
|
||||
}),
|
||||
}),
|
||||
).toThrow('Field with id invalid-field not found in object metadata');
|
||||
});
|
||||
});
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import {
|
||||
AggregateOperations,
|
||||
type BarChartConfiguration,
|
||||
FieldMetadataType,
|
||||
GraphType,
|
||||
type LineChartConfiguration,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { generateGroupByQueryVariablesFromChartConfiguration } from '../generateGroupByQueryVariablesFromChartConfiguration';
|
||||
|
||||
describe('generateGroupByQueryVariablesFromChartConfiguration', () => {
|
||||
const mockObjectMetadataItem: ObjectMetadataItem = {
|
||||
id: 'obj-1',
|
||||
nameSingular: 'opportunity',
|
||||
namePlural: 'opportunities',
|
||||
fields: [
|
||||
{
|
||||
id: 'field-1',
|
||||
name: 'stage',
|
||||
type: FieldMetadataType.TEXT,
|
||||
},
|
||||
{
|
||||
id: 'field-2',
|
||||
name: 'owner',
|
||||
type: FieldMetadataType.RELATION,
|
||||
},
|
||||
{
|
||||
id: 'field-3',
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
},
|
||||
{
|
||||
id: 'field-4',
|
||||
name: 'name',
|
||||
type: FieldMetadataType.FULL_NAME,
|
||||
},
|
||||
],
|
||||
} as ObjectMetadataItem;
|
||||
|
||||
const buildBarChartConfiguration = (
|
||||
overrides: Partial<BarChartConfiguration>,
|
||||
): BarChartConfiguration =>
|
||||
({
|
||||
__typename: 'BarChartConfiguration',
|
||||
aggregateFieldMetadataId: 'aggregate-field',
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
graphType: GraphType.VERTICAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-1',
|
||||
...overrides,
|
||||
}) as BarChartConfiguration;
|
||||
|
||||
const buildLineChartConfiguration = (
|
||||
overrides: Partial<LineChartConfiguration>,
|
||||
): LineChartConfiguration =>
|
||||
({
|
||||
__typename: 'LineChartConfiguration',
|
||||
aggregateFieldMetadataId: 'aggregate-field',
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
graphType: GraphType.LINE,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-1',
|
||||
...overrides,
|
||||
}) as LineChartConfiguration;
|
||||
|
||||
describe('Bar Chart Configuration', () => {
|
||||
describe('Vertical bar configuration', () => {
|
||||
it('should generate variables with single groupBy field', () => {
|
||||
const result = generateGroupByQueryVariablesFromChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
chartConfiguration: buildBarChartConfiguration({
|
||||
graphType: GraphType.VERTICAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-1',
|
||||
primaryAxisGroupBySubFieldName: null,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should generate variables with two groupBy fields', () => {
|
||||
const result = generateGroupByQueryVariablesFromChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
chartConfiguration: buildBarChartConfiguration({
|
||||
graphType: GraphType.VERTICAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-1',
|
||||
primaryAxisGroupBySubFieldName: null,
|
||||
secondaryAxisGroupByFieldMetadataId: 'field-2',
|
||||
secondaryAxisGroupBySubFieldName: null,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should generate variables with composite field', () => {
|
||||
const result = generateGroupByQueryVariablesFromChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
chartConfiguration: buildBarChartConfiguration({
|
||||
graphType: GraphType.VERTICAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-4',
|
||||
primaryAxisGroupBySubFieldName: 'firstName',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should generate variables with date field and granularity', () => {
|
||||
const result = generateGroupByQueryVariablesFromChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
chartConfiguration: buildBarChartConfiguration({
|
||||
graphType: GraphType.VERTICAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-3',
|
||||
primaryAxisGroupBySubFieldName: null,
|
||||
primaryAxisDateGranularity: 'MONTH' as any,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Horizontal bar configuration', () => {
|
||||
it('should generate variables with single groupBy field', () => {
|
||||
const result = generateGroupByQueryVariablesFromChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
chartConfiguration: buildBarChartConfiguration({
|
||||
graphType: GraphType.HORIZONTAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-1',
|
||||
primaryAxisGroupBySubFieldName: null,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should generate variables with secondary axis', () => {
|
||||
const result = generateGroupByQueryVariablesFromChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
chartConfiguration: buildBarChartConfiguration({
|
||||
graphType: GraphType.HORIZONTAL_BAR,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-1',
|
||||
secondaryAxisGroupByFieldMetadataId: 'field-2',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Line Chart Configuration', () => {
|
||||
it('should generate variables with single groupBy field', () => {
|
||||
const result = generateGroupByQueryVariablesFromChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
chartConfiguration: buildLineChartConfiguration({
|
||||
graphType: GraphType.LINE,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-1',
|
||||
primaryAxisGroupBySubFieldName: null,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should generate variables with two groupBy fields (multi-series)', () => {
|
||||
const result = generateGroupByQueryVariablesFromChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
chartConfiguration: buildLineChartConfiguration({
|
||||
graphType: GraphType.LINE,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-1',
|
||||
primaryAxisGroupBySubFieldName: null,
|
||||
secondaryAxisGroupByFieldMetadataId: 'field-2',
|
||||
secondaryAxisGroupBySubFieldName: null,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should generate variables with date field and granularity', () => {
|
||||
const result = generateGroupByQueryVariablesFromChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
chartConfiguration: buildLineChartConfiguration({
|
||||
graphType: GraphType.LINE,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-3',
|
||||
primaryAxisGroupBySubFieldName: null,
|
||||
primaryAxisDateGranularity: 'MONTH' as any,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should generate variables with composite field', () => {
|
||||
const result = generateGroupByQueryVariablesFromChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
chartConfiguration: buildLineChartConfiguration({
|
||||
graphType: GraphType.LINE,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-4',
|
||||
primaryAxisGroupBySubFieldName: 'firstName',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should generate variables with secondary axis', () => {
|
||||
const result = generateGroupByQueryVariablesFromChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
chartConfiguration: buildLineChartConfiguration({
|
||||
graphType: GraphType.LINE,
|
||||
primaryAxisGroupByFieldMetadataId: 'field-1',
|
||||
secondaryAxisGroupByFieldMetadataId: 'field-2',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should throw error when primary axis field not found (bar chart)', () => {
|
||||
expect(() =>
|
||||
generateGroupByQueryVariablesFromChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
chartConfiguration: buildBarChartConfiguration({
|
||||
primaryAxisGroupByFieldMetadataId: 'invalid-field',
|
||||
}),
|
||||
}),
|
||||
).toThrow('Field with id invalid-field not found in object metadata');
|
||||
});
|
||||
|
||||
it('should throw error when primary axis field not found (line chart)', () => {
|
||||
expect(() =>
|
||||
generateGroupByQueryVariablesFromChartConfiguration({
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
chartConfiguration: buildLineChartConfiguration({
|
||||
primaryAxisGroupByFieldMetadataId: 'invalid-field',
|
||||
}),
|
||||
}),
|
||||
).toThrow('Field with id invalid-field not found in object metadata');
|
||||
});
|
||||
});
|
||||
});
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import {
|
||||
AggregateOperations,
|
||||
FieldMetadataType,
|
||||
GraphType,
|
||||
type LineChartConfiguration,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { transformOneDimensionalGroupByToLineChartData } from '../transformOneDimensionalGroupByToLineChartData';
|
||||
|
||||
describe('transformOneDimensionalGroupByToLineChartData', () => {
|
||||
const mockAggregateField: FieldMetadataItem = {
|
||||
id: 'amount-field',
|
||||
name: 'amount',
|
||||
label: 'Amount',
|
||||
type: FieldMetadataType.NUMBER,
|
||||
} as FieldMetadataItem;
|
||||
|
||||
const mockGroupByFieldX: FieldMetadataItem = {
|
||||
id: 'stage-field',
|
||||
name: 'stage',
|
||||
label: 'Stage',
|
||||
type: FieldMetadataType.TEXT,
|
||||
} as FieldMetadataItem;
|
||||
|
||||
const mockDateGroupByField: FieldMetadataItem = {
|
||||
id: 'created-at-field',
|
||||
name: 'createdAt',
|
||||
label: 'Created At',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
} as FieldMetadataItem;
|
||||
|
||||
const mockObjectMetadataItem = {
|
||||
id: 'opportunity-object',
|
||||
nameSingular: 'opportunity',
|
||||
namePlural: 'opportunities',
|
||||
fields: [mockAggregateField, mockGroupByFieldX, mockDateGroupByField],
|
||||
} as ObjectMetadataItem;
|
||||
|
||||
const buildConfiguration = (
|
||||
overrides: Partial<LineChartConfiguration> = {},
|
||||
): LineChartConfiguration =>
|
||||
({
|
||||
__typename: 'LineChartConfiguration',
|
||||
graphType: GraphType.LINE,
|
||||
aggregateFieldMetadataId: 'amount-field',
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
primaryAxisGroupByFieldMetadataId: 'stage-field',
|
||||
color: 'blue',
|
||||
...overrides,
|
||||
}) as LineChartConfiguration;
|
||||
|
||||
describe('Categorical X-axis', () => {
|
||||
it('should transform simple categorical groupBy results', () => {
|
||||
const rawResults: GroupByRawResult[] = [
|
||||
{
|
||||
groupByDimensionValues: ['Qualification'],
|
||||
sumAmount: 150000,
|
||||
},
|
||||
{
|
||||
groupByDimensionValues: ['Proposal'],
|
||||
sumAmount: 280000,
|
||||
},
|
||||
{
|
||||
groupByDimensionValues: ['Closed Won'],
|
||||
sumAmount: 450000,
|
||||
},
|
||||
];
|
||||
|
||||
const result = transformOneDimensionalGroupByToLineChartData({
|
||||
rawResults,
|
||||
groupByFieldX: mockGroupByFieldX,
|
||||
aggregateField: mockAggregateField,
|
||||
configuration: buildConfiguration(),
|
||||
aggregateOperation: 'sumAmount',
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
primaryAxisSubFieldName: null,
|
||||
});
|
||||
|
||||
expect(result.series).toHaveLength(1);
|
||||
expect(result.series[0]).toMatchObject({
|
||||
id: 'amount',
|
||||
label: 'Amount',
|
||||
color: 'blue',
|
||||
});
|
||||
expect(result.series[0].data).toEqual([
|
||||
{ x: 'Qualification', y: 150000 },
|
||||
{ x: 'Proposal', y: 280000 },
|
||||
{ x: 'Closed Won', y: 450000 },
|
||||
]);
|
||||
expect(result.hasTooManyGroups).toBe(false);
|
||||
});
|
||||
|
||||
it('should filter out null aggregate values', () => {
|
||||
const rawResults: GroupByRawResult[] = [
|
||||
{
|
||||
groupByDimensionValues: ['Stage A'],
|
||||
sumAmount: 100,
|
||||
},
|
||||
{
|
||||
groupByDimensionValues: ['Stage B'],
|
||||
sumAmount: null,
|
||||
},
|
||||
{
|
||||
groupByDimensionValues: ['Stage C'],
|
||||
sumAmount: 200,
|
||||
},
|
||||
];
|
||||
|
||||
const result = transformOneDimensionalGroupByToLineChartData({
|
||||
rawResults,
|
||||
groupByFieldX: mockGroupByFieldX,
|
||||
aggregateField: mockAggregateField,
|
||||
configuration: buildConfiguration(),
|
||||
aggregateOperation: 'sumAmount',
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
primaryAxisSubFieldName: null,
|
||||
});
|
||||
|
||||
expect(result.series[0].data).toEqual([
|
||||
{ x: 'Stage A', y: 100 },
|
||||
{ x: 'Stage C', y: 200 },
|
||||
]);
|
||||
expect(result.hasTooManyGroups).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Time-series X-axis', () => {
|
||||
it('should transform date-based groupBy results', () => {
|
||||
const rawResults: GroupByRawResult[] = [
|
||||
{
|
||||
groupByDimensionValues: ['2024-01'],
|
||||
sumAmount: 50000,
|
||||
},
|
||||
{
|
||||
groupByDimensionValues: ['2024-02'],
|
||||
sumAmount: 75000,
|
||||
},
|
||||
{
|
||||
groupByDimensionValues: ['2024-03'],
|
||||
sumAmount: 60000,
|
||||
},
|
||||
];
|
||||
|
||||
const result = transformOneDimensionalGroupByToLineChartData({
|
||||
rawResults,
|
||||
groupByFieldX: mockDateGroupByField,
|
||||
aggregateField: mockAggregateField,
|
||||
configuration: buildConfiguration({
|
||||
primaryAxisGroupByFieldMetadataId: 'created-at-field',
|
||||
primaryAxisDateGranularity: 'MONTH' as any,
|
||||
}),
|
||||
aggregateOperation: 'sumAmount',
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
primaryAxisSubFieldName: null,
|
||||
});
|
||||
|
||||
expect(result.series).toHaveLength(1);
|
||||
expect(result.series[0].data).toHaveLength(3);
|
||||
expect(result.series[0].data[0]).toHaveProperty('x');
|
||||
expect(result.series[0].data[0]).toHaveProperty('y', 50000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle empty results', () => {
|
||||
const result = transformOneDimensionalGroupByToLineChartData({
|
||||
rawResults: [],
|
||||
groupByFieldX: mockGroupByFieldX,
|
||||
aggregateField: mockAggregateField,
|
||||
configuration: buildConfiguration(),
|
||||
aggregateOperation: 'sumAmount',
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
primaryAxisSubFieldName: null,
|
||||
});
|
||||
|
||||
expect(result.series).toHaveLength(1);
|
||||
expect(result.series[0].data).toEqual([]);
|
||||
expect(result.hasTooManyGroups).toBe(false);
|
||||
});
|
||||
|
||||
it('should use default color when not specified', () => {
|
||||
const result = transformOneDimensionalGroupByToLineChartData({
|
||||
rawResults: [
|
||||
{
|
||||
groupByDimensionValues: ['Test'],
|
||||
sumAmount: 100,
|
||||
},
|
||||
],
|
||||
groupByFieldX: mockGroupByFieldX,
|
||||
aggregateField: mockAggregateField,
|
||||
configuration: buildConfiguration({ color: undefined }),
|
||||
aggregateOperation: 'sumAmount',
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
primaryAxisSubFieldName: null,
|
||||
});
|
||||
|
||||
expect(result.series[0].color).toBeDefined();
|
||||
expect(result.hasTooManyGroups).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle COUNT operation', () => {
|
||||
const rawResults: GroupByRawResult[] = [
|
||||
{
|
||||
groupByDimensionValues: ['Stage A'],
|
||||
_count: 5,
|
||||
},
|
||||
{
|
||||
groupByDimensionValues: ['Stage B'],
|
||||
_count: 10,
|
||||
},
|
||||
];
|
||||
|
||||
const result = transformOneDimensionalGroupByToLineChartData({
|
||||
rawResults,
|
||||
groupByFieldX: mockGroupByFieldX,
|
||||
aggregateField: mockAggregateField,
|
||||
configuration: buildConfiguration({
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
}),
|
||||
aggregateOperation: '_count',
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
primaryAxisSubFieldName: null,
|
||||
});
|
||||
|
||||
expect(result.series[0].data).toEqual([
|
||||
{ x: 'Stage A', y: 5 },
|
||||
{ x: 'Stage B', y: 10 },
|
||||
]);
|
||||
expect(result.hasTooManyGroups).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
+310
@@ -0,0 +1,310 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import {
|
||||
AggregateOperations,
|
||||
FieldMetadataType,
|
||||
GraphType,
|
||||
type LineChartConfiguration,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { transformTwoDimensionalGroupByToLineChartData } from '../transformTwoDimensionalGroupByToLineChartData';
|
||||
|
||||
describe('transformTwoDimensionalGroupByToLineChartData', () => {
|
||||
const mockAggregateField: FieldMetadataItem = {
|
||||
id: 'amount-field',
|
||||
name: 'amount',
|
||||
label: 'Amount',
|
||||
type: FieldMetadataType.NUMBER,
|
||||
} as FieldMetadataItem;
|
||||
|
||||
const mockGroupByFieldX: FieldMetadataItem = {
|
||||
id: 'created-at-field',
|
||||
name: 'createdAt',
|
||||
label: 'Created At',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
} as FieldMetadataItem;
|
||||
|
||||
const mockGroupByFieldY: FieldMetadataItem = {
|
||||
id: 'stage-field',
|
||||
name: 'stage',
|
||||
label: 'Stage',
|
||||
type: FieldMetadataType.TEXT,
|
||||
} as FieldMetadataItem;
|
||||
|
||||
const mockObjectMetadataItem = {
|
||||
id: 'opportunity-object',
|
||||
nameSingular: 'opportunity',
|
||||
namePlural: 'opportunities',
|
||||
fields: [mockAggregateField, mockGroupByFieldX, mockGroupByFieldY],
|
||||
} as ObjectMetadataItem;
|
||||
|
||||
const buildConfiguration = (
|
||||
overrides: Partial<LineChartConfiguration> = {},
|
||||
): LineChartConfiguration =>
|
||||
({
|
||||
__typename: 'LineChartConfiguration',
|
||||
graphType: GraphType.LINE,
|
||||
aggregateFieldMetadataId: 'amount-field',
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
primaryAxisGroupByFieldMetadataId: 'created-at-field',
|
||||
secondaryAxisGroupByFieldMetadataId: 'stage-field',
|
||||
color: 'blue',
|
||||
...overrides,
|
||||
}) as LineChartConfiguration;
|
||||
|
||||
describe('Multi-series transformation', () => {
|
||||
it('should create multiple series from 2D groupBy results', () => {
|
||||
const rawResults: GroupByRawResult[] = [
|
||||
{
|
||||
groupByDimensionValues: ['2024-01', 'Qualification'],
|
||||
sumAmount: 50000,
|
||||
},
|
||||
{
|
||||
groupByDimensionValues: ['2024-01', 'Proposal'],
|
||||
sumAmount: 75000,
|
||||
},
|
||||
{
|
||||
groupByDimensionValues: ['2024-02', 'Qualification'],
|
||||
sumAmount: 60000,
|
||||
},
|
||||
{
|
||||
groupByDimensionValues: ['2024-02', 'Proposal'],
|
||||
sumAmount: 90000,
|
||||
},
|
||||
{
|
||||
groupByDimensionValues: ['2024-03', 'Qualification'],
|
||||
sumAmount: 55000,
|
||||
},
|
||||
{
|
||||
groupByDimensionValues: ['2024-03', 'Proposal'],
|
||||
sumAmount: 80000,
|
||||
},
|
||||
];
|
||||
|
||||
const result = transformTwoDimensionalGroupByToLineChartData({
|
||||
rawResults,
|
||||
groupByFieldX: mockGroupByFieldX,
|
||||
groupByFieldY: mockGroupByFieldY,
|
||||
aggregateField: mockAggregateField,
|
||||
configuration: buildConfiguration(),
|
||||
aggregateOperation: 'sumAmount',
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
primaryAxisSubFieldName: null,
|
||||
});
|
||||
|
||||
expect(result.series).toHaveLength(2);
|
||||
|
||||
expect(result.series[0]).toMatchObject({
|
||||
id: expect.any(String),
|
||||
label: expect.any(String),
|
||||
color: 'blue',
|
||||
});
|
||||
|
||||
expect(result.series[0].data).toHaveLength(3);
|
||||
expect(result.series[1].data).toHaveLength(3);
|
||||
|
||||
expect(result.series[0].data[0]).toHaveProperty('x');
|
||||
expect(result.series[0].data[0]).toHaveProperty('y');
|
||||
|
||||
result.series.forEach((series) => {
|
||||
series.data.forEach((point) => {
|
||||
expect(point).toHaveProperty('x');
|
||||
expect(point).toHaveProperty('y');
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.hasTooManyGroups).toBe(false);
|
||||
});
|
||||
|
||||
it('should preserve backend ordering of data points', () => {
|
||||
const rawResults: GroupByRawResult[] = [
|
||||
{
|
||||
groupByDimensionValues: ['2024-01', 'Stage A'],
|
||||
sumAmount: 100,
|
||||
},
|
||||
{
|
||||
groupByDimensionValues: ['2024-02', 'Stage A'],
|
||||
sumAmount: 200,
|
||||
},
|
||||
{
|
||||
groupByDimensionValues: ['2024-03', 'Stage A'],
|
||||
sumAmount: 300,
|
||||
},
|
||||
];
|
||||
|
||||
const result = transformTwoDimensionalGroupByToLineChartData({
|
||||
rawResults,
|
||||
groupByFieldX: mockGroupByFieldX,
|
||||
groupByFieldY: mockGroupByFieldY,
|
||||
aggregateField: mockAggregateField,
|
||||
configuration: buildConfiguration(),
|
||||
aggregateOperation: 'sumAmount',
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
primaryAxisSubFieldName: null,
|
||||
});
|
||||
|
||||
const series = result.series[0];
|
||||
expect(series.data[0].y).toBe(100);
|
||||
expect(series.data[1].y).toBe(200);
|
||||
expect(series.data[2].y).toBe(300);
|
||||
|
||||
expect(result.hasTooManyGroups).toBe(false);
|
||||
});
|
||||
|
||||
it('should normalize sparse data (all series share same X values, with 0 for missing)', () => {
|
||||
const rawResults: GroupByRawResult[] = [
|
||||
{
|
||||
groupByDimensionValues: ['2024-01', 'Stage A'],
|
||||
sumAmount: 100,
|
||||
},
|
||||
{
|
||||
groupByDimensionValues: ['2024-02', 'Stage A'],
|
||||
sumAmount: 200,
|
||||
},
|
||||
{
|
||||
groupByDimensionValues: ['2024-01', 'Stage B'],
|
||||
sumAmount: 150,
|
||||
},
|
||||
{
|
||||
groupByDimensionValues: ['2024-03', 'Stage B'],
|
||||
sumAmount: 250,
|
||||
},
|
||||
];
|
||||
|
||||
const result = transformTwoDimensionalGroupByToLineChartData({
|
||||
rawResults,
|
||||
groupByFieldX: mockGroupByFieldX,
|
||||
groupByFieldY: mockGroupByFieldY,
|
||||
aggregateField: mockAggregateField,
|
||||
configuration: buildConfiguration(),
|
||||
aggregateOperation: 'sumAmount',
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
primaryAxisSubFieldName: null,
|
||||
});
|
||||
|
||||
const stageA = result.series.find((s) => s.id === 'Stage A');
|
||||
expect(stageA?.data).toHaveLength(3);
|
||||
expect(stageA?.data[0].y).toBe(100);
|
||||
expect(stageA?.data[1].y).toBe(200);
|
||||
expect(stageA?.data[2].y).toBe(0);
|
||||
|
||||
const stageB = result.series.find((s) => s.id === 'Stage B');
|
||||
expect(stageB?.data).toHaveLength(3);
|
||||
expect(stageB?.data[0].y).toBe(150);
|
||||
expect(stageB?.data[1].y).toBe(0);
|
||||
expect(stageB?.data[2].y).toBe(250);
|
||||
|
||||
expect(result.hasTooManyGroups).toBe(false);
|
||||
});
|
||||
|
||||
it('should filter out null aggregate values', () => {
|
||||
const rawResults: GroupByRawResult[] = [
|
||||
{
|
||||
groupByDimensionValues: ['2024-01', 'Stage A'],
|
||||
sumAmount: 100,
|
||||
},
|
||||
{
|
||||
groupByDimensionValues: ['2024-02', 'Stage A'],
|
||||
sumAmount: null,
|
||||
},
|
||||
{
|
||||
groupByDimensionValues: ['2024-03', 'Stage A'],
|
||||
sumAmount: 200,
|
||||
},
|
||||
];
|
||||
|
||||
const result = transformTwoDimensionalGroupByToLineChartData({
|
||||
rawResults,
|
||||
groupByFieldX: mockGroupByFieldX,
|
||||
groupByFieldY: mockGroupByFieldY,
|
||||
aggregateField: mockAggregateField,
|
||||
configuration: buildConfiguration(),
|
||||
aggregateOperation: 'sumAmount',
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
primaryAxisSubFieldName: null,
|
||||
});
|
||||
|
||||
expect(result.series[0].data).toHaveLength(2);
|
||||
expect(result.series[0].data.map((d) => d.y)).toEqual([100, 200]);
|
||||
|
||||
expect(result.hasTooManyGroups).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle empty results', () => {
|
||||
const result = transformTwoDimensionalGroupByToLineChartData({
|
||||
rawResults: [],
|
||||
groupByFieldX: mockGroupByFieldX,
|
||||
groupByFieldY: mockGroupByFieldY,
|
||||
aggregateField: mockAggregateField,
|
||||
configuration: buildConfiguration(),
|
||||
aggregateOperation: 'sumAmount',
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
primaryAxisSubFieldName: null,
|
||||
});
|
||||
|
||||
expect(result.series).toEqual([]);
|
||||
expect(result.hasTooManyGroups).toBe(false);
|
||||
});
|
||||
|
||||
it('should skip results with missing dimension values', () => {
|
||||
const rawResults: GroupByRawResult[] = [
|
||||
{
|
||||
groupByDimensionValues: ['2024-01'],
|
||||
sumAmount: 100,
|
||||
},
|
||||
{
|
||||
groupByDimensionValues: ['2024-02', 'Stage A'],
|
||||
sumAmount: 200,
|
||||
},
|
||||
];
|
||||
|
||||
const result = transformTwoDimensionalGroupByToLineChartData({
|
||||
rawResults,
|
||||
groupByFieldX: mockGroupByFieldX,
|
||||
groupByFieldY: mockGroupByFieldY,
|
||||
aggregateField: mockAggregateField,
|
||||
configuration: buildConfiguration(),
|
||||
aggregateOperation: 'sumAmount',
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
primaryAxisSubFieldName: null,
|
||||
});
|
||||
|
||||
expect(result.series).toHaveLength(1);
|
||||
expect(result.series[0].data).toHaveLength(1);
|
||||
|
||||
expect(result.hasTooManyGroups).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle COUNT operation', () => {
|
||||
const rawResults: GroupByRawResult[] = [
|
||||
{
|
||||
groupByDimensionValues: ['2024-01', 'Stage A'],
|
||||
_count: 5,
|
||||
},
|
||||
{
|
||||
groupByDimensionValues: ['2024-02', 'Stage A'],
|
||||
_count: 10,
|
||||
},
|
||||
];
|
||||
|
||||
const result = transformTwoDimensionalGroupByToLineChartData({
|
||||
rawResults,
|
||||
groupByFieldX: mockGroupByFieldX,
|
||||
groupByFieldY: mockGroupByFieldY,
|
||||
aggregateField: mockAggregateField,
|
||||
configuration: buildConfiguration({
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
}),
|
||||
aggregateOperation: '_count',
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
primaryAxisSubFieldName: null,
|
||||
});
|
||||
|
||||
expect(result.series[0].data.map((d) => d.y)).toEqual([5, 10]);
|
||||
|
||||
expect(result.hasTooManyGroups).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { type ChartValueRange } from '@/page-layout/widgets/graph/types/ChartValueRange';
|
||||
|
||||
export const calculateValueRangeFromValues = (
|
||||
values: number[],
|
||||
): ChartValueRange => {
|
||||
let minimumValue = 0;
|
||||
let maximumValue = 0;
|
||||
|
||||
for (const value of values) {
|
||||
if (isNaN(value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
minimumValue = Math.min(minimumValue, value);
|
||||
maximumValue = Math.max(maximumValue, value);
|
||||
}
|
||||
|
||||
minimumValue = Math.min(minimumValue, 0);
|
||||
maximumValue = Math.max(maximumValue, 0);
|
||||
|
||||
return { minimum: minimumValue, maximum: maximumValue };
|
||||
};
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
export const computeCategoryTickValues = (
|
||||
numberOfTicks: number,
|
||||
totalValues: number,
|
||||
): number[] => {
|
||||
if (numberOfTicks <= 0) return [];
|
||||
if (numberOfTicks === 1) return [0];
|
||||
if (numberOfTicks >= totalValues)
|
||||
return Array.from({ length: totalValues }, (_, i) => i);
|
||||
|
||||
const step = (totalValues - 1) / (numberOfTicks - 1);
|
||||
|
||||
return Array.from({ length: numberOfTicks }, (_, i) => {
|
||||
const index = Math.min(Math.round(i * step), totalValues - 1);
|
||||
return index;
|
||||
});
|
||||
};
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { computeCategoryTickValues } from '@/page-layout/widgets/graph/utils/computeCategoryTickValues';
|
||||
|
||||
export const computeChartCategoryTickValues = ({
|
||||
availableSize,
|
||||
minimumSizePerTick,
|
||||
values,
|
||||
}: {
|
||||
availableSize: number;
|
||||
minimumSizePerTick: number;
|
||||
values: (string | number)[];
|
||||
}): (string | number)[] => {
|
||||
if (availableSize <= 0 || values.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const numberOfTicks = Math.floor(availableSize / minimumSizePerTick);
|
||||
const tickIndices = computeCategoryTickValues(numberOfTicks, values.length);
|
||||
|
||||
return tickIndices.map((index) => values[index]);
|
||||
};
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
export const generateChartAggregateFilterKey = (
|
||||
rangeMin?: number | null,
|
||||
rangeMax?: number | null,
|
||||
omitNullValues?: boolean | null,
|
||||
): string => {
|
||||
return `${rangeMin ?? ''}-${rangeMax ?? ''}-${omitNullValues ?? ''}`;
|
||||
};
|
||||
+18
-22
@@ -1,4 +1,5 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type GroupByChartConfiguration } from '@/page-layout/widgets/graph/types/GroupByChartConfiguration';
|
||||
import { getGroupByOrderBy } from '@/page-layout/widgets/graph/utils/getGroupByOrderBy';
|
||||
import {
|
||||
type AggregateOrderByWithGroupByField,
|
||||
@@ -7,29 +8,27 @@ import {
|
||||
type ObjectRecordOrderByWithGroupByDateField,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type BarChartConfiguration } from '~/generated/graphql';
|
||||
import { buildGroupByFieldObject } from './buildGroupByFieldObject';
|
||||
|
||||
export const generateGroupByQueryVariablesFromBarChartConfiguration = ({
|
||||
export const generateGroupByQueryVariablesFromChartConfiguration = ({
|
||||
objectMetadataItem,
|
||||
barChartConfiguration,
|
||||
chartConfiguration,
|
||||
aggregateOperation,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
barChartConfiguration: BarChartConfiguration;
|
||||
chartConfiguration: GroupByChartConfiguration;
|
||||
aggregateOperation?: string;
|
||||
}) => {
|
||||
const groupByFieldXId =
|
||||
barChartConfiguration.primaryAxisGroupByFieldMetadataId;
|
||||
const groupByFieldXId = chartConfiguration.primaryAxisGroupByFieldMetadataId;
|
||||
|
||||
const groupByFieldYId =
|
||||
barChartConfiguration.secondaryAxisGroupByFieldMetadataId;
|
||||
chartConfiguration.secondaryAxisGroupByFieldMetadataId;
|
||||
|
||||
const groupBySubFieldNameX =
|
||||
barChartConfiguration.primaryAxisGroupBySubFieldName ?? undefined;
|
||||
chartConfiguration.primaryAxisGroupBySubFieldName ?? undefined;
|
||||
|
||||
const groupBySubFieldNameY =
|
||||
barChartConfiguration.secondaryAxisGroupBySubFieldName ?? undefined;
|
||||
chartConfiguration.secondaryAxisGroupBySubFieldName ?? undefined;
|
||||
|
||||
const groupByFieldX = objectMetadataItem.fields.find(
|
||||
(field) => field.id === groupByFieldXId,
|
||||
@@ -54,7 +53,7 @@ export const generateGroupByQueryVariablesFromBarChartConfiguration = ({
|
||||
field: groupByFieldX,
|
||||
subFieldName: groupBySubFieldNameX,
|
||||
dateGranularity:
|
||||
barChartConfiguration.primaryAxisDateGranularity ?? undefined,
|
||||
chartConfiguration.primaryAxisDateGranularity ?? undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -64,8 +63,7 @@ export const generateGroupByQueryVariablesFromBarChartConfiguration = ({
|
||||
field: groupByFieldY,
|
||||
subFieldName: groupBySubFieldNameY,
|
||||
dateGranularity:
|
||||
barChartConfiguration.secondaryAxisGroupByDateGranularity ??
|
||||
undefined,
|
||||
chartConfiguration.secondaryAxisGroupByDateGranularity ?? undefined,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -77,33 +75,31 @@ export const generateGroupByQueryVariablesFromBarChartConfiguration = ({
|
||||
| ObjectRecordOrderByForCompositeField
|
||||
> = [];
|
||||
|
||||
if (isDefined(barChartConfiguration.primaryAxisOrderBy)) {
|
||||
if (isDefined(chartConfiguration.primaryAxisOrderBy)) {
|
||||
orderBy.push(
|
||||
getGroupByOrderBy({
|
||||
graphOrderBy: barChartConfiguration.primaryAxisOrderBy,
|
||||
graphOrderBy: chartConfiguration.primaryAxisOrderBy,
|
||||
groupByField: groupByFieldX,
|
||||
groupBySubFieldName:
|
||||
barChartConfiguration.primaryAxisGroupBySubFieldName,
|
||||
groupBySubFieldName: chartConfiguration.primaryAxisGroupBySubFieldName,
|
||||
aggregateOperation,
|
||||
dateGranularity:
|
||||
barChartConfiguration.primaryAxisDateGranularity ?? undefined,
|
||||
chartConfiguration.primaryAxisDateGranularity ?? undefined,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (
|
||||
isDefined(groupByFieldY) &&
|
||||
isDefined(barChartConfiguration.secondaryAxisOrderBy)
|
||||
isDefined(chartConfiguration.secondaryAxisOrderBy)
|
||||
) {
|
||||
orderBy.push(
|
||||
getGroupByOrderBy({
|
||||
graphOrderBy: barChartConfiguration.secondaryAxisOrderBy,
|
||||
graphOrderBy: chartConfiguration.secondaryAxisOrderBy,
|
||||
groupByField: groupByFieldY,
|
||||
groupBySubFieldName:
|
||||
barChartConfiguration.secondaryAxisGroupBySubFieldName,
|
||||
chartConfiguration.secondaryAxisGroupBySubFieldName,
|
||||
aggregateOperation,
|
||||
dateGranularity:
|
||||
barChartConfiguration.secondaryAxisGroupByDateGranularity ??
|
||||
undefined,
|
||||
chartConfiguration.secondaryAxisGroupByDateGranularity ?? undefined,
|
||||
}),
|
||||
);
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { type LineChartDataPoint } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartDataPoint';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
const normalizeXValue = (x: number | string | Date): string => {
|
||||
if (x instanceof Date) {
|
||||
return x.toISOString();
|
||||
}
|
||||
return String(x);
|
||||
};
|
||||
|
||||
export const sortLineChartDataPoints = ({
|
||||
dataPoints,
|
||||
orderBy,
|
||||
}: {
|
||||
dataPoints: LineChartDataPoint[];
|
||||
orderBy?: GraphOrderBy | null;
|
||||
}): LineChartDataPoint[] => {
|
||||
switch (orderBy) {
|
||||
case GraphOrderBy.FIELD_ASC:
|
||||
return [...dataPoints].sort((a, b) =>
|
||||
normalizeXValue(a.x).localeCompare(normalizeXValue(b.x)),
|
||||
);
|
||||
case GraphOrderBy.FIELD_DESC:
|
||||
return [...dataPoints].sort((a, b) =>
|
||||
normalizeXValue(b.x).localeCompare(normalizeXValue(a.x)),
|
||||
);
|
||||
default:
|
||||
return dataPoints;
|
||||
}
|
||||
};
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeries';
|
||||
import { GraphOrderBy } from '~/generated/graphql';
|
||||
|
||||
export const sortLineChartSeries = ({
|
||||
series,
|
||||
orderByY,
|
||||
}: {
|
||||
series: LineChartSeries[];
|
||||
orderByY?: GraphOrderBy | null;
|
||||
}): LineChartSeries[] => {
|
||||
switch (orderByY) {
|
||||
case GraphOrderBy.FIELD_ASC:
|
||||
return [...series].sort((a, b) => b.id.localeCompare(a.id));
|
||||
case GraphOrderBy.FIELD_DESC:
|
||||
return [...series].sort((a, b) => a.id.localeCompare(b.id));
|
||||
default:
|
||||
return series;
|
||||
}
|
||||
};
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
|
||||
import { getGroupByQueryName } from '@/page-layout/utils/getGroupByQueryName';
|
||||
import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeries';
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { filterGroupByResults } from '@/page-layout/widgets/graph/utils/filterGroupByResults';
|
||||
import { transformOneDimensionalGroupByToLineChartData } from '@/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToLineChartData';
|
||||
import { transformTwoDimensionalGroupByToLineChartData } from '@/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToLineChartData';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
AxisNameDisplay,
|
||||
type LineChartConfiguration,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
type TransformGroupByDataToLineChartDataParams = {
|
||||
groupByData: Record<string, GroupByRawResult[]> | null | undefined;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
configuration: LineChartConfiguration;
|
||||
aggregateOperation: string;
|
||||
};
|
||||
|
||||
type TransformGroupByDataToLineChartDataResult = {
|
||||
series: LineChartSeries[];
|
||||
xAxisLabel?: string;
|
||||
yAxisLabel?: string;
|
||||
showDataLabels: boolean;
|
||||
hasTooManyGroups: boolean;
|
||||
};
|
||||
|
||||
const EMPTY_LINE_CHART_RESULT: TransformGroupByDataToLineChartDataResult = {
|
||||
series: [],
|
||||
xAxisLabel: undefined,
|
||||
yAxisLabel: undefined,
|
||||
showDataLabels: false,
|
||||
hasTooManyGroups: false,
|
||||
};
|
||||
|
||||
export const transformGroupByDataToLineChartData = ({
|
||||
groupByData,
|
||||
objectMetadataItem,
|
||||
configuration,
|
||||
aggregateOperation,
|
||||
}: TransformGroupByDataToLineChartDataParams): TransformGroupByDataToLineChartDataResult => {
|
||||
if (!isDefined(groupByData)) {
|
||||
return EMPTY_LINE_CHART_RESULT;
|
||||
}
|
||||
|
||||
const groupByFieldX = objectMetadataItem.fields.find(
|
||||
(field: FieldMetadataItem) =>
|
||||
field.id === configuration.primaryAxisGroupByFieldMetadataId,
|
||||
);
|
||||
|
||||
const groupByFieldY = isDefined(
|
||||
configuration.secondaryAxisGroupByFieldMetadataId,
|
||||
)
|
||||
? objectMetadataItem.fields.find(
|
||||
(field: FieldMetadataItem) =>
|
||||
field.id === configuration.secondaryAxisGroupByFieldMetadataId,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const aggregateField = objectMetadataItem.fields.find(
|
||||
(field: FieldMetadataItem) =>
|
||||
field.id === configuration.aggregateFieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(groupByFieldX) || !isDefined(aggregateField)) {
|
||||
return EMPTY_LINE_CHART_RESULT;
|
||||
}
|
||||
|
||||
const primaryAxisSubFieldName =
|
||||
configuration.primaryAxisGroupBySubFieldName ?? undefined;
|
||||
|
||||
const queryName = getGroupByQueryName(objectMetadataItem);
|
||||
const rawResults = groupByData[queryName];
|
||||
|
||||
if (!isDefined(rawResults) || !Array.isArray(rawResults)) {
|
||||
return EMPTY_LINE_CHART_RESULT;
|
||||
}
|
||||
|
||||
const filteredResults = filterGroupByResults({
|
||||
rawResults,
|
||||
filterOptions: {
|
||||
rangeMin: configuration.rangeMin ?? undefined,
|
||||
rangeMax: configuration.rangeMax ?? undefined,
|
||||
omitNullValues: configuration.omitNullValues ?? false,
|
||||
},
|
||||
aggregateField,
|
||||
aggregateOperation:
|
||||
configuration.aggregateOperation as unknown as ExtendedAggregateOperations,
|
||||
aggregateOperationFromRawResult: aggregateOperation,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const showXAxis =
|
||||
configuration.axisNameDisplay === AxisNameDisplay.X ||
|
||||
configuration.axisNameDisplay === AxisNameDisplay.BOTH;
|
||||
|
||||
const showYAxis =
|
||||
configuration.axisNameDisplay === AxisNameDisplay.Y ||
|
||||
configuration.axisNameDisplay === AxisNameDisplay.BOTH;
|
||||
|
||||
const xAxisLabel = showXAxis ? groupByFieldX.label : undefined;
|
||||
|
||||
const yAxisLabel = showYAxis
|
||||
? `${getAggregateOperationLabel(configuration.aggregateOperation)} of ${aggregateField.label}`
|
||||
: undefined;
|
||||
|
||||
const showDataLabels = configuration.displayDataLabel ?? false;
|
||||
|
||||
const baseResult = isDefined(groupByFieldY)
|
||||
? transformTwoDimensionalGroupByToLineChartData({
|
||||
rawResults: filteredResults,
|
||||
groupByFieldX,
|
||||
groupByFieldY,
|
||||
aggregateField,
|
||||
configuration,
|
||||
aggregateOperation,
|
||||
objectMetadataItem,
|
||||
primaryAxisSubFieldName,
|
||||
})
|
||||
: transformOneDimensionalGroupByToLineChartData({
|
||||
rawResults: filteredResults,
|
||||
groupByFieldX,
|
||||
aggregateField,
|
||||
configuration,
|
||||
aggregateOperation,
|
||||
objectMetadataItem,
|
||||
primaryAxisSubFieldName,
|
||||
});
|
||||
|
||||
return {
|
||||
...baseResult,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
showDataLabels,
|
||||
};
|
||||
};
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||
import { GRAPH_DEFAULT_COLOR } from '@/page-layout/widgets/graph/constants/GraphDefaultColor.constant';
|
||||
import { LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartMaximumNumberOfDataPoints.constant';
|
||||
import { type LineChartDataPoint } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartDataPoint';
|
||||
import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeries';
|
||||
import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor';
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult';
|
||||
import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue';
|
||||
import { sortLineChartDataPoints } from '@/page-layout/widgets/graph/utils/sortLineChartDataPoints';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type LineChartConfiguration } from '~/generated/graphql';
|
||||
|
||||
type TransformOneDimensionalGroupByToLineChartDataParams = {
|
||||
rawResults: GroupByRawResult[];
|
||||
groupByFieldX: FieldMetadataItem;
|
||||
aggregateField: FieldMetadataItem;
|
||||
configuration: LineChartConfiguration;
|
||||
aggregateOperation: string;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
primaryAxisSubFieldName?: string | null;
|
||||
};
|
||||
|
||||
type TransformOneDimensionalGroupByToLineChartDataResult = {
|
||||
series: LineChartSeries[];
|
||||
hasTooManyGroups: boolean;
|
||||
};
|
||||
|
||||
export const transformOneDimensionalGroupByToLineChartData = ({
|
||||
rawResults,
|
||||
groupByFieldX,
|
||||
aggregateField,
|
||||
configuration,
|
||||
aggregateOperation,
|
||||
objectMetadataItem,
|
||||
primaryAxisSubFieldName,
|
||||
}: TransformOneDimensionalGroupByToLineChartDataParams): TransformOneDimensionalGroupByToLineChartDataResult => {
|
||||
// TODO: Add a limit to the query instead of slicing here (issue: twentyhq/core-team-issues#1600)
|
||||
const limitedResults = rawResults.slice(
|
||||
0,
|
||||
LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS,
|
||||
);
|
||||
|
||||
const unsortedData: LineChartDataPoint[] = limitedResults
|
||||
.map((result) => {
|
||||
const dimensionValues = result.groupByDimensionValues;
|
||||
|
||||
const rawAggregateValue = result[aggregateOperation];
|
||||
if (!isDefined(rawAggregateValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const xValue = isDefined(dimensionValues?.[0])
|
||||
? formatDimensionValue({
|
||||
value: dimensionValues[0],
|
||||
fieldMetadata: groupByFieldX,
|
||||
dateGranularity:
|
||||
configuration.primaryAxisDateGranularity ?? undefined,
|
||||
subFieldName: primaryAxisSubFieldName ?? undefined,
|
||||
})
|
||||
: '';
|
||||
|
||||
const aggregateValue = computeAggregateValueFromGroupByResult({
|
||||
rawResult: result,
|
||||
aggregateField,
|
||||
aggregateOperation:
|
||||
configuration.aggregateOperation as unknown as ExtendedAggregateOperations,
|
||||
aggregateOperationFromRawResult: aggregateOperation,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
return {
|
||||
x: xValue,
|
||||
y: aggregateValue,
|
||||
};
|
||||
})
|
||||
.filter((point) => isDefined(point));
|
||||
|
||||
const data = sortLineChartDataPoints({
|
||||
dataPoints: unsortedData,
|
||||
orderBy: configuration.primaryAxisOrderBy,
|
||||
});
|
||||
|
||||
const series: LineChartSeries[] = [
|
||||
{
|
||||
id: aggregateField.name,
|
||||
label: aggregateField.label,
|
||||
color: (configuration.color ?? GRAPH_DEFAULT_COLOR) as GraphColor,
|
||||
data,
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
series,
|
||||
hasTooManyGroups:
|
||||
rawResults.length > LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS,
|
||||
};
|
||||
};
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||
import { LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartMaximumNumberOfDataPoints.constant';
|
||||
import { type LineChartDataPoint } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartDataPoint';
|
||||
import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeries';
|
||||
import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor';
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult';
|
||||
import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue';
|
||||
import { sortLineChartDataPoints } from '@/page-layout/widgets/graph/utils/sortLineChartDataPoints';
|
||||
import { sortLineChartSeries } from '@/page-layout/widgets/graph/utils/sortLineChartSeries';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type LineChartConfiguration } from '~/generated/graphql';
|
||||
|
||||
type TransformTwoDimensionalGroupByToLineChartDataParams = {
|
||||
rawResults: GroupByRawResult[];
|
||||
groupByFieldX: FieldMetadataItem;
|
||||
groupByFieldY: FieldMetadataItem;
|
||||
aggregateField: FieldMetadataItem;
|
||||
configuration: LineChartConfiguration;
|
||||
aggregateOperation: string;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
primaryAxisSubFieldName?: string | null;
|
||||
};
|
||||
|
||||
type TransformTwoDimensionalGroupByToLineChartDataResult = {
|
||||
series: LineChartSeries[];
|
||||
hasTooManyGroups: boolean;
|
||||
};
|
||||
|
||||
export const transformTwoDimensionalGroupByToLineChartData = ({
|
||||
rawResults,
|
||||
groupByFieldX,
|
||||
groupByFieldY,
|
||||
aggregateField,
|
||||
configuration,
|
||||
aggregateOperation,
|
||||
objectMetadataItem,
|
||||
primaryAxisSubFieldName,
|
||||
}: TransformTwoDimensionalGroupByToLineChartDataParams): TransformTwoDimensionalGroupByToLineChartDataResult => {
|
||||
const seriesMap = new Map<string, Map<string, number>>();
|
||||
const allXValues: string[] = [];
|
||||
const xValueSet = new Set<string>();
|
||||
let hasTooManyGroups = false;
|
||||
|
||||
rawResults.forEach((result) => {
|
||||
const dimensionValues = result.groupByDimensionValues;
|
||||
if (!isDefined(dimensionValues) || dimensionValues.length < 2) return;
|
||||
|
||||
const rawAggregateValue = result[aggregateOperation];
|
||||
if (!isDefined(rawAggregateValue)) return;
|
||||
|
||||
const xValue = formatDimensionValue({
|
||||
value: dimensionValues[0],
|
||||
fieldMetadata: groupByFieldX,
|
||||
dateGranularity: configuration.primaryAxisDateGranularity ?? undefined,
|
||||
subFieldName: primaryAxisSubFieldName ?? undefined,
|
||||
});
|
||||
|
||||
// TODO: Add a limit to the query instead of checking here (issue: twentyhq/core-team-issues#1600)
|
||||
const isNewX = !xValueSet.has(xValue);
|
||||
|
||||
if (isNewX && xValueSet.size >= LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS) {
|
||||
hasTooManyGroups = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNewX) {
|
||||
xValueSet.add(xValue);
|
||||
allXValues.push(xValue);
|
||||
}
|
||||
|
||||
const seriesKey = formatDimensionValue({
|
||||
value: dimensionValues[1],
|
||||
fieldMetadata: groupByFieldY,
|
||||
dateGranularity:
|
||||
configuration.secondaryAxisGroupByDateGranularity ?? undefined,
|
||||
subFieldName: configuration.secondaryAxisGroupBySubFieldName ?? undefined,
|
||||
});
|
||||
|
||||
const aggregateValue = computeAggregateValueFromGroupByResult({
|
||||
rawResult: result,
|
||||
aggregateField,
|
||||
aggregateOperation:
|
||||
configuration.aggregateOperation as unknown as ExtendedAggregateOperations,
|
||||
aggregateOperationFromRawResult: aggregateOperation,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
if (!isDefined(aggregateValue)) return;
|
||||
|
||||
if (!seriesMap.has(seriesKey)) {
|
||||
seriesMap.set(seriesKey, new Map());
|
||||
}
|
||||
|
||||
seriesMap.get(seriesKey)!.set(xValue, aggregateValue);
|
||||
});
|
||||
|
||||
const unsortedSeries: LineChartSeries[] = Array.from(seriesMap.entries()).map(
|
||||
([seriesKey, xToYMap]) => {
|
||||
const unsortedData: LineChartDataPoint[] = allXValues.map((xValue) => ({
|
||||
x: xValue,
|
||||
y: xToYMap.get(xValue) ?? 0,
|
||||
}));
|
||||
|
||||
const data = sortLineChartDataPoints({
|
||||
dataPoints: unsortedData,
|
||||
orderBy: configuration.primaryAxisOrderBy,
|
||||
});
|
||||
|
||||
return {
|
||||
id: seriesKey,
|
||||
label: seriesKey,
|
||||
color: configuration.color as GraphColor,
|
||||
data,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const series = sortLineChartSeries({
|
||||
series: unsortedSeries,
|
||||
orderByY: configuration.secondaryAxisOrderBy,
|
||||
});
|
||||
|
||||
return {
|
||||
series,
|
||||
hasTooManyGroups,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user