[Dashboards] - Min Max range on secondary axis bar charts (#15118)
video QA https://github.com/user-attachments/assets/70c37188-2398-43de-bbf6-5882bb79940a --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
-6
@@ -6,14 +6,12 @@ exports[`generateGroupByQuery should generate valid GraphQL query for empty aggr
|
||||
$groupBy: [PersonGroupByInput!]
|
||||
$filter: PersonFilterInput
|
||||
$orderBy: [PersonOrderByWithGroupByInput!]
|
||||
$omitNullValues: Boolean
|
||||
$viewId: UUID
|
||||
) {
|
||||
peopleGroupBy(
|
||||
groupBy: $groupBy
|
||||
filter: $filter
|
||||
orderBy: $orderBy
|
||||
omitNullValues: $omitNullValues
|
||||
viewId: $viewId
|
||||
) {
|
||||
groupByDimensionValues
|
||||
@@ -28,14 +26,12 @@ exports[`generateGroupByQuery should generate valid GraphQL query for multiple a
|
||||
$groupBy: [OpportunityGroupByInput!]
|
||||
$filter: OpportunityFilterInput
|
||||
$orderBy: [OpportunityOrderByWithGroupByInput!]
|
||||
$omitNullValues: Boolean
|
||||
$viewId: UUID
|
||||
) {
|
||||
opportunitiesGroupBy(
|
||||
groupBy: $groupBy
|
||||
filter: $filter
|
||||
orderBy: $orderBy
|
||||
omitNullValues: $omitNullValues
|
||||
viewId: $viewId
|
||||
) {
|
||||
groupByDimensionValues
|
||||
@@ -53,14 +49,12 @@ exports[`generateGroupByQuery should generate valid GraphQL query for single agg
|
||||
$groupBy: [OpportunityGroupByInput!]
|
||||
$filter: OpportunityFilterInput
|
||||
$orderBy: [OpportunityOrderByWithGroupByInput!]
|
||||
$omitNullValues: Boolean
|
||||
$viewId: UUID
|
||||
) {
|
||||
opportunitiesGroupBy(
|
||||
groupBy: $groupBy
|
||||
filter: $filter
|
||||
orderBy: $orderBy
|
||||
omitNullValues: $omitNullValues
|
||||
viewId: $viewId
|
||||
) {
|
||||
groupByDimensionValues
|
||||
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { filterGroupByResults } from '../filterGroupByResults';
|
||||
|
||||
describe('filterGroupByResults', () => {
|
||||
const mockAggregateField = {
|
||||
id: 'field-1',
|
||||
name: 'amount',
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: 'Amount',
|
||||
createdAt: '2024-01-01',
|
||||
updatedAt: '2024-01-01',
|
||||
};
|
||||
|
||||
const mockObjectMetadataItem = {
|
||||
id: 'obj-1',
|
||||
nameSingular: 'opportunity',
|
||||
namePlural: 'opportunities',
|
||||
fields: [mockAggregateField],
|
||||
} as any;
|
||||
|
||||
const createMockResult = (value: number | null) => ({
|
||||
groupByDimensionValues: ['Group A'],
|
||||
SUM_amount: value,
|
||||
});
|
||||
|
||||
describe('rangeMin filtering', () => {
|
||||
it('should filter out results below rangeMin', () => {
|
||||
const rawResults = [
|
||||
createMockResult(500),
|
||||
createMockResult(1500),
|
||||
createMockResult(2500),
|
||||
];
|
||||
|
||||
const filtered = filterGroupByResults({
|
||||
rawResults,
|
||||
filterOptions: { rangeMin: 1000 },
|
||||
aggregateField: mockAggregateField,
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
aggregateOperationFromRawResult: 'SUM_amount',
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
});
|
||||
|
||||
expect(filtered).toHaveLength(2);
|
||||
expect(filtered[0].SUM_amount).toBe(1500);
|
||||
expect(filtered[1].SUM_amount).toBe(2500);
|
||||
});
|
||||
|
||||
it('should include values equal to rangeMin', () => {
|
||||
const rawResults = [
|
||||
createMockResult(500),
|
||||
createMockResult(1000),
|
||||
createMockResult(1500),
|
||||
];
|
||||
|
||||
const filtered = filterGroupByResults({
|
||||
rawResults,
|
||||
filterOptions: { rangeMin: 1000 },
|
||||
aggregateField: mockAggregateField,
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
aggregateOperationFromRawResult: 'SUM_amount',
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
});
|
||||
|
||||
expect(filtered).toHaveLength(2);
|
||||
expect(filtered[0].SUM_amount).toBe(1000);
|
||||
expect(filtered[1].SUM_amount).toBe(1500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rangeMax filtering', () => {
|
||||
it('should filter out results above rangeMax', () => {
|
||||
const rawResults = [
|
||||
createMockResult(500),
|
||||
createMockResult(1500),
|
||||
createMockResult(2500),
|
||||
];
|
||||
|
||||
const filtered = filterGroupByResults({
|
||||
rawResults,
|
||||
filterOptions: { rangeMax: 2000 },
|
||||
aggregateField: mockAggregateField,
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
aggregateOperationFromRawResult: 'SUM_amount',
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
});
|
||||
|
||||
expect(filtered).toHaveLength(2);
|
||||
expect(filtered[0].SUM_amount).toBe(500);
|
||||
expect(filtered[1].SUM_amount).toBe(1500);
|
||||
});
|
||||
|
||||
it('should include values equal to rangeMax', () => {
|
||||
const rawResults = [
|
||||
createMockResult(1500),
|
||||
createMockResult(2000),
|
||||
createMockResult(2500),
|
||||
];
|
||||
|
||||
const filtered = filterGroupByResults({
|
||||
rawResults,
|
||||
filterOptions: { rangeMax: 2000 },
|
||||
aggregateField: mockAggregateField,
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
aggregateOperationFromRawResult: 'SUM_amount',
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
});
|
||||
|
||||
expect(filtered).toHaveLength(2);
|
||||
expect(filtered[0].SUM_amount).toBe(1500);
|
||||
expect(filtered[1].SUM_amount).toBe(2000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('range filtering', () => {
|
||||
it('should keep only results within range', () => {
|
||||
const rawResults = [
|
||||
createMockResult(500),
|
||||
createMockResult(1500),
|
||||
createMockResult(2500),
|
||||
];
|
||||
|
||||
const filtered = filterGroupByResults({
|
||||
rawResults,
|
||||
filterOptions: { rangeMin: 1000, rangeMax: 2000 },
|
||||
aggregateField: mockAggregateField,
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
aggregateOperationFromRawResult: 'SUM_amount',
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
});
|
||||
|
||||
expect(filtered).toHaveLength(1);
|
||||
expect(filtered[0].SUM_amount).toBe(1500);
|
||||
});
|
||||
|
||||
it('should include boundary values', () => {
|
||||
const rawResults = [
|
||||
createMockResult(500),
|
||||
createMockResult(1000),
|
||||
createMockResult(1500),
|
||||
createMockResult(2000),
|
||||
createMockResult(2500),
|
||||
];
|
||||
|
||||
const filtered = filterGroupByResults({
|
||||
rawResults,
|
||||
filterOptions: { rangeMin: 1000, rangeMax: 2000 },
|
||||
aggregateField: mockAggregateField,
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
aggregateOperationFromRawResult: 'SUM_amount',
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
});
|
||||
|
||||
expect(filtered).toHaveLength(3);
|
||||
expect(filtered[0].SUM_amount).toBe(1000);
|
||||
expect(filtered[1].SUM_amount).toBe(1500);
|
||||
expect(filtered[2].SUM_amount).toBe(2000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('no filters', () => {
|
||||
it('should return all results when no filters are active', () => {
|
||||
const rawResults = [
|
||||
createMockResult(500),
|
||||
createMockResult(1500),
|
||||
createMockResult(2500),
|
||||
];
|
||||
|
||||
const filtered = filterGroupByResults({
|
||||
rawResults,
|
||||
filterOptions: {},
|
||||
aggregateField: mockAggregateField,
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
aggregateOperationFromRawResult: 'SUM_amount',
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
});
|
||||
|
||||
expect(filtered).toEqual(rawResults);
|
||||
});
|
||||
|
||||
it('should return all results when omitNullValues is false', () => {
|
||||
const rawResults = [
|
||||
createMockResult(null),
|
||||
createMockResult(0),
|
||||
createMockResult(100),
|
||||
];
|
||||
|
||||
const filtered = filterGroupByResults({
|
||||
rawResults,
|
||||
filterOptions: { omitNullValues: false },
|
||||
aggregateField: mockAggregateField,
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
aggregateOperationFromRawResult: 'SUM_amount',
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
});
|
||||
|
||||
expect(filtered).toEqual(rawResults);
|
||||
});
|
||||
});
|
||||
});
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
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 { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export type GroupByResultFilterOptions = {
|
||||
rangeMin?: number | null;
|
||||
rangeMax?: number | null;
|
||||
omitNullValues?: boolean;
|
||||
};
|
||||
|
||||
type FilterGroupByResultsParams = {
|
||||
rawResults: GroupByRawResult[];
|
||||
filterOptions: GroupByResultFilterOptions;
|
||||
aggregateField: FieldMetadataItem;
|
||||
aggregateOperation: ExtendedAggregateOperations;
|
||||
aggregateOperationFromRawResult: string;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
};
|
||||
|
||||
export const filterGroupByResults = ({
|
||||
rawResults,
|
||||
filterOptions,
|
||||
aggregateField,
|
||||
aggregateOperation,
|
||||
aggregateOperationFromRawResult,
|
||||
objectMetadataItem,
|
||||
}: FilterGroupByResultsParams): GroupByRawResult[] => {
|
||||
const { rangeMin, rangeMax, omitNullValues } = filterOptions;
|
||||
|
||||
const hasActiveFilters =
|
||||
isDefined(rangeMin) || isDefined(rangeMax) || omitNullValues === true;
|
||||
|
||||
if (!hasActiveFilters) {
|
||||
return rawResults;
|
||||
}
|
||||
|
||||
return rawResults.filter((result) => {
|
||||
const aggregateValue = computeAggregateValueFromGroupByResult({
|
||||
rawResult: result,
|
||||
aggregateField,
|
||||
aggregateOperation,
|
||||
aggregateOperationFromRawResult,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
if (omitNullValues === true && !isDefined(aggregateValue)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (omitNullValues === true && aggregateValue === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof aggregateValue === 'number') {
|
||||
if (isDefined(rangeMin) && aggregateValue < rangeMin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isDefined(rangeMax) && aggregateValue > rangeMax) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
};
|
||||
-2
@@ -20,14 +20,12 @@ export const generateGroupByQuery = ({
|
||||
$groupBy: [${capitalizedSingular}GroupByInput!]
|
||||
$filter: ${capitalizedSingular}FilterInput
|
||||
$orderBy: [${capitalizedSingular}OrderByWithGroupByInput!]
|
||||
$omitNullValues: Boolean
|
||||
$viewId: UUID
|
||||
) {
|
||||
${queryFieldName}(
|
||||
groupBy: $groupBy
|
||||
filter: $filter
|
||||
orderBy: $orderBy
|
||||
omitNullValues: $omitNullValues
|
||||
viewId: $viewId
|
||||
) {
|
||||
groupByDimensionValues${aggregateOperations.length > 0 ? `\n ${aggregateOperations.join('\n ')}` : ''}
|
||||
|
||||
-1
@@ -101,6 +101,5 @@ export const generateGroupByQueryVariablesFromBarChartConfiguration = ({
|
||||
return {
|
||||
groupBy,
|
||||
...(orderBy.length > 0 && { orderBy }),
|
||||
...(barChartConfiguration.omitNullValues ? { omitNullValues: true } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
+18
-2
@@ -1,10 +1,12 @@
|
||||
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 BarChartDataItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartDataItem';
|
||||
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { filterGroupByResults } from '@/page-layout/widgets/graph/utils/filterGroupByResults';
|
||||
import { getFieldKey } from '@/page-layout/widgets/graph/utils/getFieldKey';
|
||||
import { transformOneDimensionalGroupByToBarChartData } from '@/page-layout/widgets/graph/utils/transformOneDimensionalGroupByToBarChartData';
|
||||
import { transformTwoDimensionalGroupByToBarChartData } from '@/page-layout/widgets/graph/utils/transformTwoDimensionalGroupByToBarChartData';
|
||||
@@ -105,6 +107,20 @@ export const transformGroupByDataToBarChartData = ({
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -123,7 +139,7 @@ export const transformGroupByDataToBarChartData = ({
|
||||
|
||||
const baseResult = isDefined(groupByFieldY)
|
||||
? transformTwoDimensionalGroupByToBarChartData({
|
||||
rawResults,
|
||||
rawResults: filteredResults,
|
||||
groupByFieldX,
|
||||
groupByFieldY,
|
||||
aggregateField,
|
||||
@@ -133,7 +149,7 @@ export const transformGroupByDataToBarChartData = ({
|
||||
primaryAxisSubFieldName,
|
||||
})
|
||||
: transformOneDimensionalGroupByToBarChartData({
|
||||
rawResults,
|
||||
rawResults: filteredResults,
|
||||
groupByFieldX,
|
||||
aggregateField,
|
||||
configuration,
|
||||
|
||||
Reference in New Issue
Block a user