Split bar graph into two distinct horizontal and vertical bars (#15061)
### Summary Split BAR into VERTICAL_BAR and HORIZONTAL_BAR as separate chart types. ### The Problem Initially wanted a simple vertical/horizontal toggle for bar charts, but ran into a GraphQL union type constraint: union types can't have the same field name with different nullability. - Vertical bars need groupByFieldMetadataIdX as required (categories on X) - Horizontal bars need groupByFieldMetadataIdY as required (categories on Y) - GraphQL schema generation fails with this setup ### The Solution Use semantic primaryAxis and secondaryAxis naming that's orientation-agnostic: - primaryAxisGroupByFieldMetadataId = main grouping field (e.g., "Company Name") - secondaryAxisGroupByFieldMetadataId = optional secondary grouping (e.g., "Stage") These fields have consistent meaning regardless of orientation. The visual mapping happens at the UI layer: - Vertical bars: primary data renders on X-axis, secondary on Y-axis - Horizontal bars: primary data renders on Y-axis, secondary on X-axis Both chart types share the same DTO structure with consistent nullability. ### What Changed - Split GraphType.BAR → VERTICAL_BAR | HORIZONTAL_BAR - Renamed fields: primaryAxisGroupByFieldMetadataId, secondaryAxisGroupByFieldMetadataId (+ subfield variants) - useChartSettingsValues(): Maps semantic fields to setting values (no swapping) - getBarChartSettings(): Dynamically arranges settings panel based on orientation - transformGroupByDataToBarChartData(): Maps semantic fields to Nivo's layout prop video QA https://github.com/user-attachments/assets/479061b5-712e-4ca6-9858-95273d1f16c1
This commit is contained in:
+55
-48
@@ -9,6 +9,7 @@ import { getAggregateOperationLabel } from '@/object-record/record-board/record-
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { type CompositeFieldSubFieldName } from 'twenty-shared/types';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { type GraphOrderBy } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useChartSettingsValues = ({
|
||||
objectMetadataId,
|
||||
@@ -37,20 +38,43 @@ export const useChartSettingsValues = ({
|
||||
};
|
||||
}
|
||||
|
||||
const groupByFieldX =
|
||||
'groupByFieldMetadataIdX' in configuration
|
||||
? objectMetadataItem?.fields.find(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.id === configuration.groupByFieldMetadataIdX,
|
||||
)
|
||||
: undefined;
|
||||
const isBarOrLineChart =
|
||||
configuration.__typename === 'BarChartConfiguration' ||
|
||||
configuration.__typename === 'LineChartConfiguration';
|
||||
|
||||
let groupByFieldXId: string | undefined;
|
||||
let groupByFieldYId: string | undefined;
|
||||
let groupBySubFieldNameX: CompositeFieldSubFieldName | undefined;
|
||||
let groupBySubFieldNameY: CompositeFieldSubFieldName | undefined;
|
||||
let xAxisOrderBy: GraphOrderBy | undefined | null;
|
||||
let groupByOrderBy: GraphOrderBy | undefined | null;
|
||||
|
||||
if (isBarOrLineChart) {
|
||||
groupByFieldXId = configuration.primaryAxisGroupByFieldMetadataId;
|
||||
groupByFieldYId = configuration.secondaryAxisGroupByFieldMetadataId;
|
||||
groupBySubFieldNameX = configuration.primaryAxisGroupBySubFieldName as
|
||||
| CompositeFieldSubFieldName
|
||||
| undefined;
|
||||
groupBySubFieldNameY = configuration.secondaryAxisGroupBySubFieldName as
|
||||
| CompositeFieldSubFieldName
|
||||
| undefined;
|
||||
xAxisOrderBy = configuration.primaryAxisOrderBy;
|
||||
groupByOrderBy = configuration.secondaryAxisOrderBy;
|
||||
}
|
||||
|
||||
const groupByFieldX = isDefined(groupByFieldXId)
|
||||
? objectMetadataItem?.fields.find((field) => field.id === groupByFieldXId)
|
||||
: undefined;
|
||||
|
||||
const groupByFieldY = isDefined(groupByFieldYId)
|
||||
? objectMetadataItem?.fields.find((field) => field.id === groupByFieldYId)
|
||||
: undefined;
|
||||
|
||||
const groupBySubFieldNameXLabel =
|
||||
'groupBySubFieldNameX' in configuration && isDefined(groupByFieldX)
|
||||
isDefined(groupBySubFieldNameX) && isDefined(groupByFieldX)
|
||||
? getFieldLabelWithSubField({
|
||||
field: groupByFieldX,
|
||||
subFieldName:
|
||||
configuration.groupBySubFieldNameX as CompositeFieldSubFieldName,
|
||||
subFieldName: groupBySubFieldNameX,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
@@ -61,53 +85,36 @@ export const useChartSettingsValues = ({
|
||||
|
||||
const yAxisAggregateOperation = configuration.aggregateOperation;
|
||||
|
||||
const groupByFieldY =
|
||||
'groupByFieldMetadataIdY' in configuration
|
||||
? objectMetadataItem?.fields.find(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.id === configuration.groupByFieldMetadataIdY,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const xAxisOrderBy =
|
||||
'orderByX' in configuration ? configuration.orderByX : undefined;
|
||||
|
||||
const xAxisOrderByLabel =
|
||||
isDefined(xAxisOrderBy) && 'groupByFieldMetadataIdX' in configuration
|
||||
isDefined(xAxisOrderBy) && isDefined(groupByFieldXId)
|
||||
? getXSortOptionLabel({
|
||||
graphOrderBy: xAxisOrderBy,
|
||||
groupByFieldMetadataIdX: configuration.groupByFieldMetadataIdX,
|
||||
groupBySubFieldNameX:
|
||||
configuration.groupBySubFieldNameX as CompositeFieldSubFieldName,
|
||||
groupByFieldMetadataIdX: groupByFieldXId,
|
||||
groupBySubFieldNameX: groupBySubFieldNameX,
|
||||
aggregateFieldMetadataId: configuration.aggregateFieldMetadataId,
|
||||
aggregateOperation: configuration.aggregateOperation ?? undefined,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const groupByOrderBy =
|
||||
'orderByY' in configuration
|
||||
? configuration.orderByY
|
||||
: 'orderBy' in configuration
|
||||
? configuration.orderBy
|
||||
: undefined;
|
||||
if (configuration.__typename === 'PieChartConfiguration') {
|
||||
groupByOrderBy = configuration.orderBy;
|
||||
groupByFieldYId = configuration.groupByFieldMetadataId;
|
||||
groupBySubFieldNameY = configuration.groupBySubFieldName as
|
||||
| CompositeFieldSubFieldName
|
||||
| undefined;
|
||||
}
|
||||
|
||||
const finalGroupByFieldYId = groupByFieldYId;
|
||||
const finalGroupBySubFieldNameY = groupBySubFieldNameY;
|
||||
|
||||
const groupByOrderByLabel =
|
||||
isDefined(groupByOrderBy) &&
|
||||
getGroupBySortOptionLabel({
|
||||
graphOrderBy: groupByOrderBy,
|
||||
groupByFieldMetadataId:
|
||||
'groupByFieldMetadataIdY' in configuration
|
||||
? configuration.groupByFieldMetadataIdY
|
||||
: 'groupByFieldMetadataId' in configuration
|
||||
? configuration.groupByFieldMetadataId
|
||||
: undefined,
|
||||
groupBySubFieldName:
|
||||
'groupBySubFieldNameY' in configuration
|
||||
? (configuration.groupBySubFieldNameY as CompositeFieldSubFieldName)
|
||||
: 'groupBySubFieldName' in configuration
|
||||
? (configuration.groupBySubFieldName as CompositeFieldSubFieldName)
|
||||
: undefined,
|
||||
});
|
||||
isDefined(groupByOrderBy) && isDefined(finalGroupByFieldYId)
|
||||
? getGroupBySortOptionLabel({
|
||||
graphOrderBy: groupByOrderBy,
|
||||
groupByFieldMetadataId: finalGroupByFieldYId,
|
||||
groupBySubFieldName: finalGroupBySubFieldNameY,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const getChartSettingsValues = (
|
||||
itemId: CHART_CONFIGURATION_SETTING_IDS,
|
||||
@@ -119,7 +126,7 @@ export const useChartSettingsValues = ({
|
||||
return groupBySubFieldNameXLabel ?? groupByFieldX?.label;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.COLORS:
|
||||
return 'color' in configuration && isDefined(configuration.color)
|
||||
? capitalize(configuration.color as string)
|
||||
? capitalize(configuration.color)
|
||||
: undefined;
|
||||
case CHART_CONFIGURATION_SETTING_IDS.DATA_ON_DISPLAY_Y: {
|
||||
const hasAggregateLabel = isDefined(aggregateField?.label);
|
||||
|
||||
Reference in New Issue
Block a user