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:
nitin
2025-10-15 15:08:48 +05:30
committed by GitHub
parent d21be90c5d
commit 436084f8f4
48 changed files with 1453 additions and 412 deletions
@@ -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);