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
@@ -3,6 +3,7 @@ import { Field, ObjectType } from '@nestjs/graphql';
import {
IsBoolean,
IsEnum,
IsIn,
IsNotEmpty,
IsNumber,
IsObject,
@@ -24,9 +25,9 @@ import { GraphType } from 'src/engine/core-modules/page-layout/enums/graph-type.
@ObjectType('BarChartConfiguration')
export class BarChartConfigurationDTO {
@Field(() => GraphType)
@IsEnum(GraphType)
@IsIn([GraphType.VERTICAL_BAR, GraphType.HORIZONTAL_BAR])
@IsNotEmpty()
graphType: GraphType.BAR;
graphType: GraphType.VERTICAL_BAR | GraphType.HORIZONTAL_BAR;
@Field(() => UUIDScalarType)
@IsUUID()
@@ -41,32 +42,32 @@ export class BarChartConfigurationDTO {
@Field(() => UUIDScalarType)
@IsUUID()
@IsNotEmpty()
groupByFieldMetadataIdX: string;
primaryAxisGroupByFieldMetadataId: string;
@Field(() => String, { nullable: true })
@IsString()
@IsOptional()
groupBySubFieldNameX?: string;
primaryAxisGroupBySubFieldName?: string;
@Field(() => GraphOrderBy, { nullable: true })
@IsEnum(GraphOrderBy)
@IsOptional()
orderByX?: GraphOrderBy;
primaryAxisOrderBy?: GraphOrderBy;
@Field(() => UUIDScalarType, { nullable: true })
@IsUUID()
@IsOptional()
groupByFieldMetadataIdY?: string;
secondaryAxisGroupByFieldMetadataId?: string;
@Field(() => String, { nullable: true })
@IsString()
@IsOptional()
groupBySubFieldNameY?: string;
secondaryAxisGroupBySubFieldName?: string;
@Field(() => GraphOrderBy, { nullable: true })
@IsEnum(GraphOrderBy)
@IsOptional()
orderByY?: GraphOrderBy;
secondaryAxisOrderBy?: GraphOrderBy;
@Field(() => Boolean, { nullable: true })
@IsBoolean()
@@ -40,12 +40,12 @@ export class LineChartConfigurationDTO {
@Field(() => UUIDScalarType)
@IsUUID()
@IsNotEmpty()
groupByFieldMetadataIdX: string;
primaryAxisGroupByFieldMetadataId: string;
@Field(() => String, { nullable: true })
@IsString()
@IsOptional()
groupBySubFieldNameX?: string;
primaryAxisGroupBySubFieldName?: string;
@Field(() => GraphOrderBy, {
nullable: true,
@@ -53,22 +53,22 @@ export class LineChartConfigurationDTO {
})
@IsEnum(GraphOrderBy)
@IsOptional()
orderByX?: GraphOrderBy;
primaryAxisOrderBy?: GraphOrderBy;
@Field(() => UUIDScalarType, { nullable: true })
@IsUUID()
@IsOptional()
groupByFieldMetadataIdY?: string;
secondaryAxisGroupByFieldMetadataId?: string;
@Field(() => String, { nullable: true })
@IsString()
@IsOptional()
groupBySubFieldNameY?: string;
secondaryAxisGroupBySubFieldName?: string;
@Field(() => GraphOrderBy, { nullable: true })
@IsEnum(GraphOrderBy)
@IsOptional()
orderByY?: GraphOrderBy;
secondaryAxisOrderBy?: GraphOrderBy;
@Field(() => Boolean, { nullable: true })
@IsBoolean()
@@ -30,7 +30,8 @@ export const WidgetConfiguration = createUnionType({
configuration.configurationType === WidgetConfigurationType.CHART_CONFIG
) {
switch (configuration.graphType) {
case GraphType.BAR:
case GraphType.VERTICAL_BAR:
case GraphType.HORIZONTAL_BAR:
return BarChartConfigurationDTO;
case GraphType.LINE:
return LineChartConfigurationDTO;
@@ -4,7 +4,8 @@ export enum GraphType {
NUMBER = 'NUMBER',
GAUGE = 'GAUGE',
PIE = 'PIE',
BAR = 'BAR',
VERTICAL_BAR = 'VERTICAL_BAR',
HORIZONTAL_BAR = 'HORIZONTAL_BAR',
LINE = 'LINE',
}
@@ -1,12 +1,15 @@
import {
INVALID_BAR_CHART_CONFIG_MISSING_GROUP_BY,
INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
INVALID_HORIZONTAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
INVALID_IFRAME_CONFIG_BAD_URL,
INVALID_IFRAME_CONFIG_EMPTY_URL,
INVALID_IFRAME_CONFIG_MISSING_URL,
INVALID_NUMBER_CHART_CONFIG_BAD_UUID,
INVALID_NUMBER_CHART_CONFIG_MISSING_FIELDS,
TEST_BAR_CHART_CONFIG,
TEST_BAR_CHART_CONFIG_MINIMAL,
TEST_VERTICAL_BAR_CHART_CONFIG,
TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL,
TEST_HORIZONTAL_BAR_CHART_CONFIG,
TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL,
TEST_GAUGE_CHART_CONFIG,
TEST_GAUGE_CHART_CONFIG_MINIMAL,
TEST_IFRAME_CONFIG,
@@ -99,32 +102,61 @@ describe('validateAndTransformWidgetConfiguration', () => {
});
});
describe('BAR graph', () => {
it('should validate full bar graph configuration', () => {
describe('VERTICAL_BAR graph', () => {
it('should validate full vertical bar graph configuration', () => {
const result = validateAndTransformWidgetConfiguration(
WidgetType.GRAPH,
TEST_BAR_CHART_CONFIG,
TEST_VERTICAL_BAR_CHART_CONFIG,
);
expect(result).toMatchObject(TEST_BAR_CHART_CONFIG);
expect(result).toMatchObject(TEST_VERTICAL_BAR_CHART_CONFIG);
});
it('should validate minimal bar graph configuration', () => {
it('should validate minimal vertical bar graph configuration', () => {
const result = validateAndTransformWidgetConfiguration(
WidgetType.GRAPH,
TEST_BAR_CHART_CONFIG_MINIMAL,
TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL,
);
expect(result).toMatchObject(TEST_BAR_CHART_CONFIG_MINIMAL);
expect(result).toMatchObject(TEST_VERTICAL_BAR_CHART_CONFIG_MINIMAL);
});
it('should throw error for partial bar graph configuration with missing required fields', () => {
it('should throw error for partial vertical bar graph configuration with missing required fields', () => {
expect(() =>
validateAndTransformWidgetConfiguration(
WidgetType.GRAPH,
INVALID_BAR_CHART_CONFIG_MISSING_GROUP_BY,
INVALID_VERTICAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
),
).toThrow(/groupByFieldMetadataIdX/);
).toThrow(/primaryAxisGroupByFieldMetadataId/);
});
});
describe('HORIZONTAL_BAR graph', () => {
it('should validate full horizontal bar graph configuration', () => {
const result = validateAndTransformWidgetConfiguration(
WidgetType.GRAPH,
TEST_HORIZONTAL_BAR_CHART_CONFIG,
);
expect(result).toMatchObject(TEST_HORIZONTAL_BAR_CHART_CONFIG);
});
it('should validate minimal horizontal bar graph configuration', () => {
const result = validateAndTransformWidgetConfiguration(
WidgetType.GRAPH,
TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL,
);
expect(result).toMatchObject(TEST_HORIZONTAL_BAR_CHART_CONFIG_MINIMAL);
});
it('should throw error for partial horizontal bar graph configuration with missing required fields', () => {
expect(() =>
validateAndTransformWidgetConfiguration(
WidgetType.GRAPH,
INVALID_HORIZONTAL_BAR_CHART_CONFIG_MISSING_GROUP_BY,
),
).toThrow(/primaryAxisGroupByFieldMetadataId/);
});
});
@@ -33,7 +33,8 @@ const validateGraphConfiguration = (
}
switch (graphType) {
case GraphType.BAR: {
case GraphType.VERTICAL_BAR:
case GraphType.HORIZONTAL_BAR: {
const instance = plainToInstance(BarChartConfigurationDTO, configuration);
const errors = validateSync(instance, {