Skip to content

Commit b8b4156

Browse files
opensearch-trigger-bot[bot]github-actions[bot]
authored andcommitted
[VisBuilder-Next] Pie Chart Integration for VisBuilder (opensearch-project#7752) (opensearch-project#8553)
* [VisBuilder-Next] Pie Chart Integration for VisBuilder This PR integrates pie charts into VisBuilder using Vega rendering. Issue Resolve: opensearch-project#7607 --------- (cherry picked from commit 615d7d4) Signed-off-by: Anan Zhuang <[email protected]> Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Signed-off-by: Ruchi Sharma <[email protected]>
1 parent 40a03b0 commit b8b4156

32 files changed

+1084
-53
lines changed

src/plugins/vis_builder/public/application/components/data_tab/index.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { addFieldToConfiguration } from './drag_drop/add_field_to_configuration'
2020
import { replaceFieldInConfiguration } from './drag_drop/replace_field_in_configuration';
2121
import { reorderFieldsWithinSchema } from './drag_drop/reorder_fields_within_schema';
2222
import { moveFieldBetweenSchemas } from './drag_drop/move_field_between_schemas';
23+
import { validateAggregations } from '../../utils/validations';
2324

2425
export const DATA_TAB_ID = 'data_tab';
2526

@@ -39,6 +40,7 @@ export const DataTab = () => {
3940
data: {
4041
search: { aggs: aggService },
4142
},
43+
notifications: { toasts },
4244
},
4345
} = useOpenSearchDashboards<VisBuilderServices>();
4446

@@ -77,6 +79,17 @@ export const DataTab = () => {
7779
}
7880

7981
const panelGroups = Array.from(schemas.all.map((schema) => schema.name));
82+
// Check schema order
83+
if (destinationSchemaName === 'split') {
84+
const validationResult = validateAggregations(aggProps.aggs, schemas.all);
85+
if (!validationResult.valid) {
86+
toasts.addWarning({
87+
title: 'vb_invalid_schema',
88+
text: validationResult.errorMsg,
89+
});
90+
return;
91+
}
92+
}
8093

8194
if (destinationSchemaName.startsWith(ADD_PANEL_PREFIX)) {
8295
const updatedDestinationSchemaName = destinationSchemaName.split(ADD_PANEL_PREFIX)[1];

src/plugins/vis_builder/public/application/utils/validations/validate_aggregations.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,80 @@ describe('validateAggregations', () => {
9696
expect(valid).toBe(true);
9797
expect(errorMsg).not.toBeDefined();
9898
});
99+
100+
test('Split chart should be first in the configuration', () => {
101+
const aggConfigs = dataStart.search.aggs.createAggConfigs(indexPattern as IndexPattern, [
102+
{
103+
id: '0',
104+
enabled: true,
105+
type: BUCKET_TYPES.TERMS,
106+
schema: 'group',
107+
params: {},
108+
},
109+
{
110+
id: '1',
111+
enabled: true,
112+
type: BUCKET_TYPES.TERMS,
113+
schema: 'split',
114+
params: {},
115+
},
116+
]);
117+
118+
const schemas = [{ name: 'split', mustBeFirst: true }, { name: 'group' }];
119+
120+
const { valid, errorMsg } = validateAggregations(aggConfigs.aggs, schemas);
121+
122+
expect(valid).toBe(false);
123+
expect(errorMsg).toMatchInlineSnapshot(`"Split chart must be first in the configuration."`);
124+
});
125+
126+
test('Valid configuration with split chart first', () => {
127+
const aggConfigs = dataStart.search.aggs.createAggConfigs(indexPattern as IndexPattern, [
128+
{
129+
id: '0',
130+
enabled: true,
131+
type: BUCKET_TYPES.TERMS,
132+
schema: 'split',
133+
params: {},
134+
},
135+
{
136+
id: '2',
137+
enabled: true,
138+
type: METRIC_TYPES.COUNT,
139+
schema: 'metric',
140+
params: {},
141+
},
142+
]);
143+
144+
const schemas = [{ name: 'split', mustBeFirst: true }, { name: 'metric' }];
145+
146+
const { valid, errorMsg } = validateAggregations(aggConfigs.aggs, schemas);
147+
148+
expect(valid).toBe(true);
149+
expect(errorMsg).toBeUndefined();
150+
});
151+
152+
test('Valid configuration when schemas are not provided', () => {
153+
const aggConfigs = dataStart.search.aggs.createAggConfigs(indexPattern as IndexPattern, [
154+
{
155+
id: '0',
156+
enabled: true,
157+
type: BUCKET_TYPES.TERMS,
158+
schema: 'group',
159+
params: {},
160+
},
161+
{
162+
id: '1',
163+
enabled: true,
164+
type: BUCKET_TYPES.TERMS,
165+
schema: 'split',
166+
params: {},
167+
},
168+
]);
169+
170+
const { valid, errorMsg } = validateAggregations(aggConfigs.aggs);
171+
172+
expect(valid).toBe(true);
173+
expect(errorMsg).not.toBeDefined();
174+
});
99175
});

src/plugins/vis_builder/public/application/utils/validations/validate_aggregations.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ import { ValidationResult } from './types';
1212
/**
1313
* Validate if the aggregations to perform are possible
1414
* @param aggs Aggregations to be performed
15+
* @param schemas Optional. All available schemas
1516
* @returns ValidationResult
1617
*/
17-
export const validateAggregations = (aggs: AggConfig[]): ValidationResult => {
18+
export const validateAggregations = (aggs: AggConfig[], schemas?: any[]): ValidationResult => {
1819
// Pipeline aggs should have a valid bucket agg
1920
const metricAggs = aggs.filter((agg) => agg.schema === 'metric');
2021
const lastParentPipelineAgg = findLast(
@@ -50,5 +51,18 @@ export const validateAggregations = (aggs: AggConfig[]): ValidationResult => {
5051
};
5152
}
5253

54+
const splitSchema = schemas?.find((s) => s.name === 'split');
55+
if (splitSchema?.mustBeFirst) {
56+
const firstGroupSchemaIndex = aggs.findIndex((item) => item.schema === 'group');
57+
if (firstGroupSchemaIndex !== -1) {
58+
return {
59+
valid: false,
60+
errorMsg: i18n.translate('visBuilder.aggregation.splitChartOrderErrorMessage', {
61+
defaultMessage: 'Split chart must be first in the configuration.',
62+
}),
63+
};
64+
}
65+
}
66+
5367
return { valid: true };
5468
};

src/plugins/vis_builder/public/plugin.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {
5757
} from '../../opensearch_dashboards_utils/public';
5858
import { opensearchFilters } from '../../data/public';
5959
import { createRawDataVisFn } from './visualizations/vega/utils/expression_helper';
60+
import { VISBUILDER_ENABLE_VEGA_SETTING } from '../common/constants';
6061

6162
export class VisBuilderPlugin
6263
implements
@@ -107,7 +108,7 @@ export class VisBuilderPlugin
107108

108109
// Register Default Visualizations
109110
const typeService = this.typeService;
110-
registerDefaultTypes(typeService.setup());
111+
registerDefaultTypes(typeService.setup(), core.uiSettings.get(VISBUILDER_ENABLE_VEGA_SETTING));
111112
exp.registerFunction(createRawDataVisFn());
112113

113114
// Register the plugin to core

src/plugins/vis_builder/public/services/type_service/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,5 @@ export interface VisualizationTypeOptions<T = any> {
3434
searchContext: IExpressionLoaderParams['searchContext'],
3535
useVega: boolean
3636
) => Promise<string | undefined>;
37+
readonly hierarchicalData?: boolean | ((vis: { params: T }) => boolean);
3738
}

src/plugins/vis_builder/public/visualizations/index.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,41 @@
66
import type { TypeServiceSetup } from '../services/type_service';
77
import { createMetricConfig } from './metric';
88
import { createTableConfig } from './table';
9-
import { createHistogramConfig, createLineConfig, createAreaConfig } from './vislib';
9+
import {
10+
createHistogramConfig,
11+
createLineConfig,
12+
createAreaConfig,
13+
createPieConfig,
14+
} from './vislib';
15+
import { VisualizationTypeOptions } from '../services/type_service';
16+
import {
17+
HistogramOptionsDefaults,
18+
LineOptionsDefaults,
19+
AreaOptionsDefaults,
20+
PieOptionsDefaults,
21+
} from './vislib';
22+
import { MetricOptionsDefaults } from './metric/metric_viz_type';
23+
import { TableOptionsDefaults } from './table/table_viz_type';
1024

11-
export function registerDefaultTypes(typeServiceSetup: TypeServiceSetup) {
12-
const visualizationTypes = [
25+
type VisualizationConfigFunction =
26+
| (() => VisualizationTypeOptions<HistogramOptionsDefaults>)
27+
| (() => VisualizationTypeOptions<LineOptionsDefaults>)
28+
| (() => VisualizationTypeOptions<AreaOptionsDefaults>)
29+
| (() => VisualizationTypeOptions<MetricOptionsDefaults>)
30+
| (() => VisualizationTypeOptions<TableOptionsDefaults>)
31+
| (() => VisualizationTypeOptions<PieOptionsDefaults>);
32+
33+
export function registerDefaultTypes(typeServiceSetup: TypeServiceSetup, useVega: boolean) {
34+
const visualizationTypes: VisualizationConfigFunction[] = [
1335
createHistogramConfig,
1436
createLineConfig,
1537
createAreaConfig,
1638
createMetricConfig,
1739
createTableConfig,
1840
];
1941

42+
if (useVega) visualizationTypes.push(createPieConfig);
43+
2044
visualizationTypes.forEach((createTypeConfig) => {
2145
typeServiceSetup.createVisualizationType(createTypeConfig());
2246
});

src/plugins/vis_builder/public/visualizations/vega/components/mark.test.ts renamed to src/plugins/vis_builder/public/visualizations/vega/components/mark/mark.test.ts

File renamed without changes.

src/plugins/vis_builder/public/visualizations/vega/components/mark.ts renamed to src/plugins/vis_builder/public/visualizations/vega/components/mark/mark.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import { AxisFormats } from '../utils/types';
7-
import { buildAxes } from './axes';
7+
import { buildAxes } from '../axes';
88

99
export type VegaMarkType =
1010
| 'line'
@@ -87,7 +87,7 @@ export const buildMarkForVegaLite = (vegaType: string): VegaLiteMark => {
8787
};
8888

8989
/**
90-
* Builds a mark configuration for Vega based on the chart type.
90+
* Builds a mark configuration for Vega useing series data based on the chart type.
9191
*
9292
* @param {VegaMarkType} chartType - The type of chart to build the mark for.
9393
* @param {any} dimensions - The dimensions of the data.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { buildSlicesMarkForVega, buildPieMarks, buildArcMarks } from './mark_slices';
7+
8+
describe('buildSlicesMarkForVega', () => {
9+
it('should return a group mark with correct properties', () => {
10+
const result = buildSlicesMarkForVega(['level1', 'level2'], true, true);
11+
expect(result.type).toBe('group');
12+
expect(result.from).toEqual({ data: 'splits' });
13+
expect(result.encode.enter.width).toEqual({ signal: 'chartWidth' });
14+
expect(result.title).toBeDefined();
15+
expect(result.data).toBeDefined();
16+
expect(result.marks).toBeDefined();
17+
});
18+
19+
it('should handle non-split case correctly', () => {
20+
const result = buildSlicesMarkForVega(['level1'], false, true);
21+
expect(result.from).toBeNull();
22+
expect(result.encode.enter.width).toEqual({ signal: 'width' });
23+
expect(result.title).toBeNull();
24+
});
25+
});
26+
27+
describe('buildPieMarks', () => {
28+
it('should create correct number of marks', () => {
29+
const result = buildPieMarks(['level1', 'level2'], true);
30+
expect(result).toHaveLength(2);
31+
});
32+
33+
it('should create correct transformations', () => {
34+
const result = buildPieMarks(['level1'], true);
35+
expect(result[0].transform).toHaveLength(3);
36+
expect(result[0].transform[0].type).toBe('filter');
37+
expect(result[0].transform[1].type).toBe('aggregate');
38+
expect(result[0].transform[2].type).toBe('pie');
39+
});
40+
});
41+
42+
describe('buildArcMarks', () => {
43+
it('should create correct number of arc marks', () => {
44+
const result = buildArcMarks(['level1', 'level2'], false);
45+
expect(result).toHaveLength(2);
46+
expect(result[0].encode.update.tooltip).toBeUndefined();
47+
});
48+
49+
it('should create arc marks with correct properties', () => {
50+
const result = buildArcMarks(['level1'], true);
51+
expect(result[0].type).toBe('arc');
52+
expect(result[0].encode.enter.fill).toBeDefined();
53+
expect(result[0].encode.update.startAngle).toBeDefined();
54+
expect(result[0].encode.update.tooltip).toBeDefined();
55+
});
56+
});

0 commit comments

Comments
 (0)