Skip to content

Commit

Permalink
[VisBuilder-Next] Pie Chart Integration for VisBuilder
Browse files Browse the repository at this point in the history
This PR integrates pie charts into VisBuilder using Vega rendering.

Issue Resolve:
#7752

Signed-off-by: Anan Zhuang <[email protected]>
  • Loading branch information
ananzh committed Aug 20, 2024
1 parent 389ad1b commit b6f3345
Show file tree
Hide file tree
Showing 28 changed files with 947 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { addFieldToConfiguration } from './drag_drop/add_field_to_configuration'
import { replaceFieldInConfiguration } from './drag_drop/replace_field_in_configuration';
import { reorderFieldsWithinSchema } from './drag_drop/reorder_fields_within_schema';
import { moveFieldBetweenSchemas } from './drag_drop/move_field_between_schemas';
import { validateAggregations } from '../../utils/validations';

export const DATA_TAB_ID = 'data_tab';

Expand All @@ -39,6 +40,7 @@ export const DataTab = () => {
data: {
search: { aggs: aggService },
},
notifications: { toasts },
},
} = useOpenSearchDashboards<VisBuilderServices>();

Expand Down Expand Up @@ -76,6 +78,18 @@ export const DataTab = () => {

const panelGroups = Array.from(schemas.all.map((schema) => schema.name));

// Check schema order
if (destinationSchemaName === 'split') {
const validationResult = validateAggregations(aggProps.aggs, schemas.all);
if (!validationResult.valid) {
toasts.addWarning({
title: 'vb_invalid_schema',
text: validationResult.errorMsg,
});
return;
}
}

if (Object.values(FIELD_SELECTOR_ID).includes(sourceSchemaName as FIELD_SELECTOR_ID)) {
if (panelGroups.includes(destinationSchemaName) && !combine) {
addFieldToConfiguration({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useDebounce } from 'react-use';
import { i18n } from '@osd/i18n';
import { EuiCallOut } from '@elastic/eui';
import { useTypedDispatch, useTypedSelector } from '../../utils/state_management';
import { DefaultEditorAggParams } from '../../../../../vis_default_editor/public';
import { DefaultEditorAggParams, calcAggIsTooLow } from '../../../../../vis_default_editor/public';
import { Title } from './title';
import { useIndexPatterns, useVisualizationType } from '../../utils/use';
import {
Expand Down Expand Up @@ -38,6 +38,7 @@ export function SecondaryPanel() {
data: {
search: { aggs: aggService },
},
notifications: { toasts },
} = services;
const schemas = vizType.ui.containerConfig.data.schemas.all;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,80 @@ describe('validateAggregations', () => {
expect(valid).toBe(true);
expect(errorMsg).not.toBeDefined();
});

test('Split chart should be first in the configuration', () => {
const aggConfigs = dataStart.search.aggs.createAggConfigs(indexPattern as IndexPattern, [
{
id: '0',
enabled: true,
type: BUCKET_TYPES.TERMS,
schema: 'group',
params: {},
},
{
id: '1',
enabled: true,
type: BUCKET_TYPES.TERMS,
schema: 'split',
params: {},
},
]);

const schemas = [{ name: 'split', mustBeFirst: true }, { name: 'group' }];

const { valid, errorMsg } = validateAggregations(aggConfigs.aggs, schemas);

expect(valid).toBe(false);
expect(errorMsg).toMatchInlineSnapshot(`"Split chart must be first in the configuration."`);
});

test('Valid configuration with split chart first', () => {
const aggConfigs = dataStart.search.aggs.createAggConfigs(indexPattern as IndexPattern, [
{
id: '0',
enabled: true,
type: BUCKET_TYPES.TERMS,
schema: 'split',
params: {},
},
{
id: '2',
enabled: true,
type: METRIC_TYPES.COUNT,
schema: 'metric',
params: {},
},
]);

const schemas = [{ name: 'split', mustBeFirst: true }, { name: 'metric' }];

const { valid, errorMsg } = validateAggregations(aggConfigs.aggs, schemas);

expect(valid).toBe(true);
expect(errorMsg).toBeUndefined();
});

test('Valid configuration when schemas are not provided', () => {
const aggConfigs = dataStart.search.aggs.createAggConfigs(indexPattern as IndexPattern, [
{
id: '0',
enabled: true,
type: BUCKET_TYPES.TERMS,
schema: 'group',
params: {},
},
{
id: '1',
enabled: true,
type: BUCKET_TYPES.TERMS,
schema: 'split',
params: {},
},
]);

const { valid, errorMsg } = validateAggregations(aggConfigs.aggs);

expect(valid).toBe(true);
expect(errorMsg).not.toBeDefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import { ValidationResult } from './types';
/**
* Validate if the aggregations to perform are possible
* @param aggs Aggregations to be performed
* @param schemas Optional. All available schemas
* @returns ValidationResult
*/
export const validateAggregations = (aggs: AggConfig[]): ValidationResult => {
export const validateAggregations = (aggs: AggConfig[], schemas?: any[]): ValidationResult => {
// Pipeline aggs should have a valid bucket agg
const metricAggs = aggs.filter((agg) => agg.schema === 'metric');
const lastParentPipelineAgg = findLast(
Expand Down Expand Up @@ -50,5 +51,18 @@ export const validateAggregations = (aggs: AggConfig[]): ValidationResult => {
};
}

const splitSchema = schemas?.find((s) => s.name === 'split');
if (splitSchema && splitSchema.mustBeFirst) {
const firstGroupSchemaIndex = aggs.findIndex((item) => item.schema === 'group');
if (firstGroupSchemaIndex !== -1) {
return {
valid: false,
errorMsg: i18n.translate('visBuilder.aggregation.splitChartOrderErrorMessage', {
defaultMessage: 'Split chart must be first in the configuration.',
}),
};
}
}

return { valid: true };
};
3 changes: 2 additions & 1 deletion src/plugins/vis_builder/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import {
} from '../../opensearch_dashboards_utils/public';
import { opensearchFilters } from '../../data/public';
import { createRawDataVisFn } from './visualizations/vega/utils/expression_helper';
import { VISBUILDER_ENABLE_VEGA_SETTING } from '../common/constants';

export class VisBuilderPlugin
implements
Expand Down Expand Up @@ -107,7 +108,7 @@ export class VisBuilderPlugin

// Register Default Visualizations
const typeService = this.typeService;
registerDefaultTypes(typeService.setup());
registerDefaultTypes(typeService.setup(), core.uiSettings.get(VISBUILDER_ENABLE_VEGA_SETTING));
exp.registerFunction(createRawDataVisFn());

// Register the plugin to core
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@ export interface VisualizationTypeOptions<T = any> {
searchContext: IExpressionLoaderParams['searchContext'],
useVega: boolean
) => Promise<string | undefined>;
readonly hierarchicalData?: boolean | ((vis: { params: T }) => boolean);
}
15 changes: 12 additions & 3 deletions src/plugins/vis_builder/public/visualizations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,26 @@
import type { TypeServiceSetup } from '../services/type_service';
import { createMetricConfig } from './metric';
import { createTableConfig } from './table';
import { createHistogramConfig, createLineConfig, createAreaConfig } from './vislib';
import {
createHistogramConfig,
createLineConfig,
createAreaConfig,
createPieConfig,
} from './vislib';

export function registerDefaultTypes(typeServiceSetup: TypeServiceSetup) {
const visualizationTypes = [
export function registerDefaultTypes(typeServiceSetup: TypeServiceSetup, useVega: boolean) {
const defaultVisualizationTypes = [
createHistogramConfig,
createLineConfig,
createAreaConfig,
createMetricConfig,
createTableConfig,
];

const visualizationTypes = useVega
? [...defaultVisualizationTypes, createPieConfig]
: defaultVisualizationTypes;

visualizationTypes.forEach((createTypeConfig) => {
typeServiceSetup.createVisualizationType(createTypeConfig());
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { AxisFormats } from '../utils/types';
import { buildAxes } from './axes';
import { buildAxes } from '../axes';

export type VegaMarkType =
| 'line'
Expand Down Expand Up @@ -87,7 +87,7 @@ export const buildMarkForVegaLite = (vegaType: string): VegaLiteMark => {
};

/**
* Builds a mark configuration for Vega based on the chart type.
* Builds a mark configuration for Vega useing series data based on the chart type.
*
* @param {VegaMarkType} chartType - The type of chart to build the mark for.
* @param {any} dimensions - The dimensions of the data.
Expand All @@ -97,7 +97,7 @@ export const buildMarkForVegaLite = (vegaType: string): VegaLiteMark => {
export const buildMarkForVega = (
chartType: VegaMarkType,
dimensions: any,
formats: AxisFormats
formats: AxisFormats,

Check failure on line 100 in src/plugins/vis_builder/public/visualizations/vega/components/mark/mark.ts

View workflow job for this annotation

GitHub Actions / Build and Verify on Linux (ciGroup1)

Delete `,`
): VegaMark => {
const baseMark: VegaMark = {
type: 'group',
Expand All @@ -108,13 +108,13 @@ export const buildMarkForVega = (
groupby: 'split',
},
},
signals: [{ name: 'width', update: 'chartWidth' }],
encode: {
enter: {
width: { signal: 'chartWidth' },
height: { signal: 'height' },
},
width: { signal: "facetWidth" },

Check failure on line 114 in src/plugins/vis_builder/public/visualizations/vega/components/mark/mark.ts

View workflow job for this annotation

GitHub Actions / Build and Verify on Linux (ciGroup1)

Replace `"facetWidth"` with `'facetWidth'`
height: { signal: "facetHeight" }

Check failure on line 115 in src/plugins/vis_builder/public/visualizations/vega/components/mark/mark.ts

View workflow job for this annotation

GitHub Actions / Build and Verify on Linux (ciGroup1)

Replace `"facetHeight"·}` with `'facetHeight'·},`
}

Check failure on line 116 in src/plugins/vis_builder/public/visualizations/vega/components/mark/mark.ts

View workflow job for this annotation

GitHub Actions / Build and Verify on Linux (ciGroup1)

Insert `,`
},
signals: [{ name: 'width', update: 'chartWidth' }],
scales: [
buildXScale(chartType, dimensions),
buildYScale(chartType),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { buildSlicesMarkForVega, buildPieMarks, buildArcMarks } from './mark_slices';

describe('buildSlicesMarkForVega', () => {
it('should return a group mark with correct properties', () => {
const result = buildSlicesMarkForVega(['level1', 'level2'], true, true);
expect(result.type).toBe('group');
expect(result.from).toEqual({ data: 'splits' });
expect(result.encode.enter.width).toEqual({ signal: 'chartWidth' });
expect(result.title).toBeDefined();
expect(result.data).toBeDefined();
expect(result.marks).toBeDefined();
});

it('should handle non-split case correctly', () => {
const result = buildSlicesMarkForVega(['level1'], false, true);
expect(result.from).toBeNull();
expect(result.encode.enter.width).toEqual({ signal: 'width' });
expect(result.title).toBeNull();
});
});

describe('buildPieMarks', () => {
it('should create correct number of marks', () => {
const result = buildPieMarks(['level1', 'level2'], true);
expect(result).toHaveLength(2);
});

it('should create correct transformations', () => {
const result = buildPieMarks(['level1'], true);
expect(result[0].transform).toHaveLength(3);
expect(result[0].transform[0].type).toBe('filter');
expect(result[0].transform[1].type).toBe('aggregate');
expect(result[0].transform[2].type).toBe('pie');
});
});

describe('buildArcMarks', () => {
it('should create correct number of arc marks', () => {
const result = buildArcMarks(['level1', 'level2']);
expect(result).toHaveLength(2);
});

it('should create arc marks with correct properties', () => {
const result = buildArcMarks(['level1']);
expect(result[0].type).toBe('arc');
expect(result[0].encode.enter.fill).toBeDefined();
expect(result[0].encode.update.startAngle).toBeDefined();
expect(result[0].encode.update.tooltip).toBeDefined();
});
});
Loading

0 comments on commit b6f3345

Please sign in to comment.