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:
+9
-8
@@ -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()
|
||||
|
||||
+6
-6
@@ -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()
|
||||
|
||||
+2
-1
@@ -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',
|
||||
}
|
||||
|
||||
|
||||
+45
-13
@@ -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/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+2
-1
@@ -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, {
|
||||
|
||||
Reference in New Issue
Block a user