[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:
nitin
2025-10-22 14:21:01 +05:30
committed by GitHub
parent c6addc2bb4
commit 226cc9eb74
37 changed files with 1096 additions and 407 deletions
@@ -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
@@ -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);
});
});
});
@@ -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;
});
};
@@ -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 ')}` : ''}
@@ -101,6 +101,5 @@ export const generateGroupByQueryVariablesFromBarChartConfiguration = ({
return {
groupBy,
...(orderBy.length > 0 && { orderBy }),
...(barChartConfiguration.omitNullValues ? { omitNullValues: true } : {}),
};
};
@@ -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,