From b5bc887925a81afb95f5c8372f61e7f976845472 Mon Sep 17 00:00:00 2001 From: Anan Zhuang Date: Thu, 1 Aug 2024 05:54:29 +0000 Subject: [PATCH] add pie to visbuilder Signed-off-by: Anan Zhuang --- .../application/components/data_tab/index.tsx | 29 ++++ .../components/data_tab/secondary_panel.tsx | 3 +- src/plugins/vis_builder/public/plugin.ts | 3 +- .../public/services/type_service/types.ts | 1 + .../public/visualizations/index.ts | 15 +- .../visualizations/vega/utils/helpers.ts | 83 ++++++++-- ... => vega_lite_spec_series_builder.test.ts} | 16 +- ...er.ts => vega_lite_spec_series_builder.ts} | 2 +- .../visualizations/vega/vega_spec_factory.ts | 22 ++- ...st.ts => vega_spec_series_builder.test.ts} | 12 +- ...builder.ts => vega_spec_series_builder.ts} | 6 +- .../vega/vega_spec_slices_builder.ts | 155 ++++++++++++++++++ .../public/visualizations/vislib/index.ts | 1 + .../vislib/pie/components/pie_vis_options.tsx | 54 ++++++ .../public/visualizations/vislib/pie/index.ts | 6 + .../visualizations/vislib/pie/pie_vis_type.ts | 93 +++++++++++ .../vislib/pie/to_expression.test.ts | 4 + .../vislib/pie/to_expression.ts | 88 ++++++++++ .../vis_default_editor/public/index.ts | 2 + 19 files changed, 559 insertions(+), 36 deletions(-) rename src/plugins/vis_builder/public/visualizations/vega/{vega_lite_spec_builder.test.ts => vega_lite_spec_series_builder.test.ts} (81%) rename src/plugins/vis_builder/public/visualizations/vega/{vega_lite_spec_builder.ts => vega_lite_spec_series_builder.ts} (98%) rename src/plugins/vis_builder/public/visualizations/vega/{vega_spec_builder.test.ts => vega_spec_series_builder.test.ts} (77%) rename src/plugins/vis_builder/public/visualizations/vega/{vega_spec_builder.ts => vega_spec_series_builder.ts} (96%) create mode 100644 src/plugins/vis_builder/public/visualizations/vega/vega_spec_slices_builder.ts create mode 100644 src/plugins/vis_builder/public/visualizations/vislib/pie/components/pie_vis_options.tsx create mode 100644 src/plugins/vis_builder/public/visualizations/vislib/pie/index.ts create mode 100644 src/plugins/vis_builder/public/visualizations/vislib/pie/pie_vis_type.ts create mode 100644 src/plugins/vis_builder/public/visualizations/vislib/pie/to_expression.test.ts create mode 100644 src/plugins/vis_builder/public/visualizations/vislib/pie/to_expression.ts diff --git a/src/plugins/vis_builder/public/application/components/data_tab/index.tsx b/src/plugins/vis_builder/public/application/components/data_tab/index.tsx index 8c79f680cb02..6e05b4926186 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/index.tsx +++ b/src/plugins/vis_builder/public/application/components/data_tab/index.tsx @@ -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 { IAggConfig } from '../../../../../data/common'; export const DATA_TAB_ID = 'data_tab'; @@ -33,12 +34,14 @@ export const DataTab = () => { const editingState = useTypedSelector( (state) => state.visualization.activeVisualization?.draftAgg ); + const configs = useTypedSelector((state) => state.visualization); const schemas = vizType.ui.containerConfig.data.schemas; const { services: { data: { search: { aggs: aggService }, }, + notifications: { toasts }, }, } = useOpenSearchDashboards(); @@ -76,6 +79,18 @@ export const DataTab = () => { const panelGroups = Array.from(schemas.all.map((schema) => schema.name)); + // Check schema order + if (destinationSchemaName === 'split') { + if (isAggTooLow(aggProps.aggs, schemas.all)) { + // Prevent the move and show an error message + toasts.addWarning({ + title: 'vb_invalid_schema', + text: 'Split chart must be first in the configuration.', + }); + return; + } + } + if (Object.values(FIELD_SELECTOR_ID).includes(sourceSchemaName as FIELD_SELECTOR_ID)) { if (panelGroups.includes(destinationSchemaName) && !combine) { addFieldToConfiguration({ @@ -148,3 +163,17 @@ export const DataTab = () => { ); }; + +const isAggTooLow = (allAggs: IAggConfig[], schemas: any[]) => { + const schema = schemas.find((s) => s.name === 'split'); + if (!schema || !schema.mustBeFirst) { + return false; + } + + const firstGroupSchemaIndex = allAggs.findIndex((item) => item.schema === 'group'); + if (firstGroupSchemaIndex !== -1) { + return true; + } + + return false; +}; diff --git a/src/plugins/vis_builder/public/application/components/data_tab/secondary_panel.tsx b/src/plugins/vis_builder/public/application/components/data_tab/secondary_panel.tsx index 18a1991f6d80..3e1c7bcc92a2 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/secondary_panel.tsx +++ b/src/plugins/vis_builder/public/application/components/data_tab/secondary_panel.tsx @@ -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 { @@ -38,6 +38,7 @@ export function SecondaryPanel() { data: { search: { aggs: aggService }, }, + notifications: { toasts }, } = services; const schemas = vizType.ui.containerConfig.data.schemas.all; diff --git a/src/plugins/vis_builder/public/plugin.ts b/src/plugins/vis_builder/public/plugin.ts index 20b13281e53b..216feafe187a 100644 --- a/src/plugins/vis_builder/public/plugin.ts +++ b/src/plugins/vis_builder/public/plugin.ts @@ -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 @@ -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 diff --git a/src/plugins/vis_builder/public/services/type_service/types.ts b/src/plugins/vis_builder/public/services/type_service/types.ts index 0c232829431c..7f2a132fa752 100644 --- a/src/plugins/vis_builder/public/services/type_service/types.ts +++ b/src/plugins/vis_builder/public/services/type_service/types.ts @@ -34,4 +34,5 @@ export interface VisualizationTypeOptions { searchContext: IExpressionLoaderParams['searchContext'], useVega: boolean ) => Promise; + readonly hierarchicalData?: boolean | ((vis: { params: T }) => boolean); } diff --git a/src/plugins/vis_builder/public/visualizations/index.ts b/src/plugins/vis_builder/public/visualizations/index.ts index c867e570143e..342e148f42dd 100644 --- a/src/plugins/vis_builder/public/visualizations/index.ts +++ b/src/plugins/vis_builder/public/visualizations/index.ts @@ -6,10 +6,15 @@ 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, @@ -17,6 +22,10 @@ export function registerDefaultTypes(typeServiceSetup: TypeServiceSetup) { createTableConfig, ]; + const visualizationTypes = useVega + ? [...defaultVisualizationTypes, createPieConfig] + : defaultVisualizationTypes; + visualizationTypes.forEach((createTypeConfig) => { typeServiceSetup.createVisualizationType(createTypeConfig()); }); diff --git a/src/plugins/vis_builder/public/visualizations/vega/utils/helpers.ts b/src/plugins/vis_builder/public/visualizations/vega/utils/helpers.ts index e15edf1ac795..ce80eee7e907 100644 --- a/src/plugins/vis_builder/public/visualizations/vega/utils/helpers.ts +++ b/src/plugins/vis_builder/public/visualizations/vega/utils/helpers.ts @@ -79,21 +79,83 @@ const flattenSeries = ( }); }; +interface FlattenedSliceItem { + [key: string]: any; + value: number; + split?: string; +} + +interface FlattenHierarchyResult { + flattenedData: FlattenedSliceItem[]; + levels: string[]; +} + +/** + * Flattens hierarchical slice data into a single array of data points + * @param {any} data - The hierarchical data to flatten + * @param {any[]} group - The group data (rows or columns) if split dimensions exist + * @returns {FlattenedSliceItem[]} Flattened array of data points + */ +const flattenHierarchy = (data, group): FlattenHierarchyResult => { + const flattenedData: FlattenedSliceItem[] = []; + const levelSet = new Set(); + + const flattenSlices = ( + slices: any, + split?: string, + level = 1, + parentLabels: { [key: string]: string } = {} + ) => { + slices.children.forEach((child: any) => { + const currentLabels = { ...parentLabels, [`level${level}`]: child.name }; + levelSet.add(`level${level}`); + + if (child.children && child.children.length > 0) { + flattenSlices(child, split, level + 1, currentLabels); + } else { + const dataPoint: FlattenedSliceItem = { + ...currentLabels, + value: child.size, + }; + if (split !== undefined) { + dataPoint.split = split; + } + flattenedData.push(dataPoint); + } + }); + }; + + if (group && group.length !== 0) { + group.forEach((splitData) => { + flattenSlices(splitData.slices, splitData.label); + }); + } else { + flattenSlices(data.slices, undefined); + } + + return { flattenedData, levels: Array.from(levelSet) }; +}; + +/** + * Handles the flattening of data for different chart types + * @param {any} context - The context object containing the data + * @param {any} dimensions - The dimensions object defining the chart structure + * @param {'series' | 'slices'} handlerType - The type of chart data to handle + * @returns {any} Converted and flattened data suitable for visualization + */ export const flattenDataHandler = (context, dimensions, handlerType = 'series') => { - // Currently, our vislib only supports 'series' or 'slices' response types. - // This will need to be updated if more types are added in the future. + // TODO: Update this func if more types are added in the future. const handler = handlerType === 'series' ? vislibSeriesResponseHandler : vislibSlicesResponseHandler; const converted = handler(context, dimensions); + const group = dimensions.splitRow + ? converted.rows + : dimensions.splitColumn + ? converted.columns + : []; if (handlerType === 'series') { // Determine the group based on split dimensions - const group = dimensions.splitRow - ? converted.rows - : dimensions.splitColumn - ? converted.columns - : []; - if (group && group.length !== 0) { converted.series = group.flatMap((split) => flattenSeries(split.series, split.label)); setAxisProperties(converted, group); @@ -101,8 +163,9 @@ export const flattenDataHandler = (context, dimensions, handlerType = 'series') converted.series = flattenSeries(converted.series); } } else if (handlerType === 'slices') { - // TODO: Handle slices data, such as pie charts - // This section should be implemented when support for slice-based charts is added + const { flattenedData, levels } = flattenHierarchy(converted, group); + converted.slices = flattenedData; + converted.levels = levels; } return converted; diff --git a/src/plugins/vis_builder/public/visualizations/vega/vega_lite_spec_builder.test.ts b/src/plugins/vis_builder/public/visualizations/vega/vega_lite_spec_series_builder.test.ts similarity index 81% rename from src/plugins/vis_builder/public/visualizations/vega/vega_lite_spec_builder.test.ts rename to src/plugins/vis_builder/public/visualizations/vega/vega_lite_spec_series_builder.test.ts index 0f1f25b1e762..e5c0a2a93fce 100644 --- a/src/plugins/vis_builder/public/visualizations/vega/vega_lite_spec_builder.test.ts +++ b/src/plugins/vis_builder/public/visualizations/vega/vega_lite_spec_series_builder.test.ts @@ -3,9 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { generateVegaLiteSpec } from './vega_lite_spec_builder'; +import { generateVegaLiteSpecForSeries } from './vega_lite_spec_series_builder'; -describe('generateVegaLiteSpec', () => { +describe('generateVegaLiteSpecForSeries', () => { it('should generate a basic Vega-Lite specification', () => { const data = { xAxisFormat: { id: 'date' }, @@ -22,7 +22,7 @@ describe('generateVegaLiteSpec', () => { }; const style = { type: 'line' }; - const result = generateVegaLiteSpec(data, visConfig, style); + const result = generateVegaLiteSpecForSeries(data, visConfig, style); expect(result.$schema).toBe('https://vega.github.io/schema/vega-lite/v5.json'); expect(result.data).toBeDefined(); @@ -45,10 +45,10 @@ describe('generateVegaLiteSpec', () => { addTooltip: true, }; - const lineResult = generateVegaLiteSpec(data, visConfig, { type: 'line' }); + const lineResult = generateVegaLiteSpecForSeries(data, visConfig, { type: 'line' }); expect(lineResult.mark).toEqual({ type: 'line', point: true, tooltip: true }); - const areaResult = generateVegaLiteSpec(data, visConfig, { type: 'area' }); + const areaResult = generateVegaLiteSpecForSeries(data, visConfig, { type: 'area' }); expect(areaResult.mark).toEqual({ type: 'area', line: true, @@ -58,7 +58,7 @@ describe('generateVegaLiteSpec', () => { baseline: 0, }); - const barResult = generateVegaLiteSpec(data, visConfig, { type: 'bar' }); + const barResult = generateVegaLiteSpecForSeries(data, visConfig, { type: 'bar' }); expect(barResult.mark).toEqual({ type: 'bar', tooltip: true }); }); @@ -78,7 +78,7 @@ describe('generateVegaLiteSpec', () => { }; const style = { type: 'line' }; - const result = generateVegaLiteSpec(data, visConfig, style); + const result = generateVegaLiteSpecForSeries(data, visConfig, style); expect(result.config).toBeDefined(); expect(result.config!.legend).toBeDefined(); @@ -101,7 +101,7 @@ describe('generateVegaLiteSpec', () => { }; const style = { type: 'line' }; - const result = generateVegaLiteSpec(data, visConfig, style); + const result = generateVegaLiteSpecForSeries(data, visConfig, style); expect(result.encoding!.tooltip).toBeDefined(); expect(result.mark).toHaveProperty('tooltip', true); diff --git a/src/plugins/vis_builder/public/visualizations/vega/vega_lite_spec_builder.ts b/src/plugins/vis_builder/public/visualizations/vega/vega_lite_spec_series_builder.ts similarity index 98% rename from src/plugins/vis_builder/public/visualizations/vega/vega_lite_spec_builder.ts rename to src/plugins/vis_builder/public/visualizations/vega/vega_lite_spec_series_builder.ts index 6110b3f75ed9..e82486facc13 100644 --- a/src/plugins/vis_builder/public/visualizations/vega/vega_lite_spec_builder.ts +++ b/src/plugins/vis_builder/public/visualizations/vega/vega_lite_spec_series_builder.ts @@ -19,7 +19,7 @@ import { mapChartTypeToVegaType } from './utils/helpers'; * @param {StyleState} style - The StyleState defined in style slice. * @returns {VegaLiteSpec} The complete Vega-Lite specification. */ -export const generateVegaLiteSpec = ( +export const generateVegaLiteSpecForSeries = ( data: any, visConfig: any, style: StyleState diff --git a/src/plugins/vis_builder/public/visualizations/vega/vega_spec_factory.ts b/src/plugins/vis_builder/public/visualizations/vega/vega_spec_factory.ts index b1ea168e8279..ba01c48e4ad5 100644 --- a/src/plugins/vis_builder/public/visualizations/vega/vega_spec_factory.ts +++ b/src/plugins/vis_builder/public/visualizations/vega/vega_spec_factory.ts @@ -5,8 +5,9 @@ import { StyleState } from '../../application/utils/state_management'; import { flattenDataHandler } from './utils/helpers'; -import { generateVegaLiteSpec } from './vega_lite_spec_builder'; -import { generateVegaSpec } from './vega_spec_builder'; +import { generateVegaLiteSpecForSeries } from './vega_lite_spec_series_builder'; +import { generateVegaSpecForSeries } from './vega_spec_series_builder'; +import { generateVegaSpecForSlices } from './vega_spec_slices_builder'; import { VegaLiteSpec, VegaSpec } from './utils/types'; /** @@ -25,15 +26,26 @@ export const createVegaSpec = ( const { dimensions } = visConfig; // Transform the data using the flattenDataHandler - const transformedData = flattenDataHandler(context, dimensions, 'series'); + const handler = style.type !== 'pie' ? 'series' : 'slices'; + const transformedData = flattenDataHandler(context, dimensions, handler); + return handler === 'series' + ? createVegaSpecForSeriesData(dimensions, transformedData, visConfig, style) + : createVegaSpecForSlicesData(dimensions, transformedData, visConfig, style); +}; + +const createVegaSpecForSeriesData = (dimensions, transformedData, visConfig, style) => { // Determine whether to use Vega or Vega-Lite based on the presence of split dimensions // TODO: Summarize the cases to use Vega. Change this to a better determine function. if (dimensions.splitRow || dimensions.splitColumn) { // Use Vega for more complex, split visualizations - return generateVegaSpec(transformedData, visConfig, style); + return generateVegaSpecForSeries(transformedData, visConfig, style); } else { // Use Vega-Lite for simpler visualizations - return generateVegaLiteSpec(transformedData, visConfig, style); + return generateVegaLiteSpecForSeries(transformedData, visConfig, style); } }; + +const createVegaSpecForSlicesData = (dimensions, transformedData, visConfig, style) => { + return generateVegaSpecForSlices(transformedData, visConfig, style); +}; diff --git a/src/plugins/vis_builder/public/visualizations/vega/vega_spec_builder.test.ts b/src/plugins/vis_builder/public/visualizations/vega/vega_spec_series_builder.test.ts similarity index 77% rename from src/plugins/vis_builder/public/visualizations/vega/vega_spec_builder.test.ts rename to src/plugins/vis_builder/public/visualizations/vega/vega_spec_series_builder.test.ts index 3197e28ae40a..9b2cc424ea84 100644 --- a/src/plugins/vis_builder/public/visualizations/vega/vega_spec_builder.test.ts +++ b/src/plugins/vis_builder/public/visualizations/vega/vega_spec_series_builder.test.ts @@ -3,9 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { generateVegaSpec } from './vega_spec_builder'; +import { generateVegaSpecForSeries } from './vega_spec_series_builder'; -describe('generateVegaSpec', () => { +describe('generateVegaSpecForSeries', () => { const baseData = { xAxisFormat: { id: 'date' }, xAxisLabel: 'Date', @@ -26,7 +26,7 @@ describe('generateVegaSpec', () => { it('should generate a basic Vega specification', () => { const style = { type: 'line' }; - const result = generateVegaSpec(baseData, baseVisConfig, style); + const result = generateVegaSpecForSeries(baseData, baseVisConfig, style); expect(result.$schema).toBe('https://vega.github.io/schema/vega/v5.json'); expect(result.data).toBeDefined(); @@ -38,7 +38,7 @@ describe('generateVegaSpec', () => { it('should handle area charts', () => { const style = { type: 'area' }; - const result = generateVegaSpec(baseData, baseVisConfig, style); + const result = generateVegaSpecForSeries(baseData, baseVisConfig, style); expect(result.data).toBeDefined(); expect(result.data?.some((d) => d.name === 'stacked')).toBe(true); @@ -48,7 +48,7 @@ describe('generateVegaSpec', () => { it('should add legend when specified', () => { const style = { type: 'line' }; - const result = generateVegaSpec(baseData, baseVisConfig, style); + const result = generateVegaSpecForSeries(baseData, baseVisConfig, style); expect(result.legends).toBeDefined(); expect(result.legends?.[0]?.orient).toBe('right'); @@ -61,7 +61,7 @@ describe('generateVegaSpec', () => { }; const style = { type: 'line' }; - const result = generateVegaSpec(baseData, visConfigNoLegend, style); + const result = generateVegaSpecForSeries(baseData, visConfigNoLegend, style); expect(result.legends).toBeUndefined(); }); diff --git a/src/plugins/vis_builder/public/visualizations/vega/vega_spec_builder.ts b/src/plugins/vis_builder/public/visualizations/vega/vega_spec_series_builder.ts similarity index 96% rename from src/plugins/vis_builder/public/visualizations/vega/vega_spec_builder.ts rename to src/plugins/vis_builder/public/visualizations/vega/vega_spec_series_builder.ts index 90181a8e5513..58b7e8abf08f 100644 --- a/src/plugins/vis_builder/public/visualizations/vega/vega_spec_builder.ts +++ b/src/plugins/vis_builder/public/visualizations/vega/vega_spec_series_builder.ts @@ -17,7 +17,11 @@ import { mapChartTypeToVegaType } from './utils/helpers'; * @param {StyleState} style - The style configuration for the visualization. * @returns {VegaSpec} The complete Vega specification. */ -export const generateVegaSpec = (data: any, visConfig: any, style: StyleState): VegaSpec => { +export const generateVegaSpecForSeries = ( + data: any, + visConfig: any, + style: StyleState +): VegaSpec => { const { dimensions, addLegend, legendPosition } = visConfig; const { type } = style; const vegaType = mapChartTypeToVegaType(type) as VegaMarkType; diff --git a/src/plugins/vis_builder/public/visualizations/vega/vega_spec_slices_builder.ts b/src/plugins/vis_builder/public/visualizations/vega/vega_spec_slices_builder.ts new file mode 100644 index 000000000000..1275a9c5f028 --- /dev/null +++ b/src/plugins/vis_builder/public/visualizations/vega/vega_spec_slices_builder.ts @@ -0,0 +1,155 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { VegaSpec } from './utils/types'; +import { buildLegend } from './components/legend'; +import { StyleState } from '../../application/utils/state_management'; + +export const generateVegaSpecForSlices = ( + data: any, + visConfig: any, + style: StyleState +): VegaSpec => { + const { dimensions, addLegend, legendPosition } = visConfig; + const { slices, levels } = data; + const hasSplit = dimensions.splitRow || dimensions.splitColumn; + const splitField = hasSplit ? 'split' : null; + + const spec: VegaSpec = { + $schema: 'https://vega.github.io/schema/vega/v5.json', + padding: 5, + + signals: [ + { name: 'splitCount', update: hasSplit ? `length(data('splits'))` : '1' }, + { name: 'chartWidth', update: hasSplit ? 'width / splitCount - 10' : 'width' }, + { name: 'chartHeight', update: 'height' }, + { name: 'radius', update: 'min(chartWidth, chartHeight) / 2' }, + { name: 'levelCount', update: levels.length }, + { + name: 'innerRadiusRatio', + update: + 'max(levelCount, splitCount) > 1 ? 0.1 + (max(levelCount, splitCount) - 1) * 0.05 : 0', + }, + { name: 'innerRadius', update: 'radius * innerRadiusRatio' }, + { name: 'thickness', update: '(radius - innerRadius) / max(levelCount, 1)' }, + ], + + data: [ + { + name: 'table', + values: slices, + transform: [{ type: 'filter', expr: 'datum.value != null' }], + }, + { + name: 'splits', + source: 'table', + transform: [ + { + type: 'aggregate', + groupby: hasSplit ? ['split'] : [], + }, + ], + }, + ], + + scales: [ + { + name: 'color', + type: 'ordinal', + domain: { data: 'table', fields: levels }, + range: { scheme: style.colorSchema || 'category20' }, + }, + ], + + layout: hasSplit + ? { + columns: { signal: 'splitCount' }, + padding: { row: 40, column: 20 }, + } + : null, + + marks: [ + { + type: 'group', + from: hasSplit ? { data: 'splits' } : null, + encode: { + enter: { + width: { signal: hasSplit ? 'chartWidth' : 'width' }, + height: { signal: 'height' }, + }, + }, + signals: [ + { name: 'facetWidth', update: hasSplit ? 'chartWidth' : 'width' }, + { name: 'facetHeight', update: 'height' }, + ], + title: hasSplit + ? { + text: { field: splitField }, + frame: 'group', + } + : null, + data: buildPieMarks(levels, hasSplit), + marks: buildArcMarks(levels), + }, + ], + }; + + // Add legend if specified + if (addLegend) { + spec.legends = [buildLegend(legendPosition, true)]; + } + + return spec; +}; + +const buildPieMarks = (levels: string[], hasSplit) => { + return levels.map((level, index) => ({ + name: `facet_${level}`, + source: 'table', + transform: [ + { + type: 'filter', + expr: hasSplit ? `datum.split === parent.split` : 'true', + }, + { + type: 'aggregate', + groupby: levels.slice(0, index + 1), + fields: ['value'], + ops: ['sum'], + as: ['sum_value'], + }, + { type: 'pie', field: 'sum_value' }, + ], + })); +}; + +const buildArcMarks = (levels: string[]) => { + return levels.map((level, index) => ({ + type: 'arc', + from: { data: `facet_${level}` }, + encode: { + enter: { + fill: { scale: 'color', field: level }, + x: { signal: 'facetWidth / 2' }, + y: { signal: 'facetHeight / 2' }, + }, + update: { + startAngle: { field: 'startAngle' }, + endAngle: { field: 'endAngle' }, + padAngle: { value: 0.01 }, + innerRadius: { signal: `innerRadius + thickness * ${index}` }, + outerRadius: { signal: `innerRadius + thickness * (${index} + 1)` }, + stroke: { value: 'white' }, + strokeWidth: { value: 2 }, + tooltip: { + signal: `{${levels + .slice(0, index + 1) + .map((l) => `'${l}': datum.${l}`) + .join(', ')}, 'Value': datum.sum_value}`, + }, + }, + }, + })); +}; diff --git a/src/plugins/vis_builder/public/visualizations/vislib/index.ts b/src/plugins/vis_builder/public/visualizations/vislib/index.ts index 84dc3e346ef5..f981ce154e73 100644 --- a/src/plugins/vis_builder/public/visualizations/vislib/index.ts +++ b/src/plugins/vis_builder/public/visualizations/vislib/index.ts @@ -6,3 +6,4 @@ export { createHistogramConfig } from './histogram'; export { createLineConfig } from './line'; export { createAreaConfig } from './area'; +export { createPieConfig } from './pie'; diff --git a/src/plugins/vis_builder/public/visualizations/vislib/pie/components/pie_vis_options.tsx b/src/plugins/vis_builder/public/visualizations/vislib/pie/components/pie_vis_options.tsx new file mode 100644 index 000000000000..38ed1078f6c6 --- /dev/null +++ b/src/plugins/vis_builder/public/visualizations/vislib/pie/components/pie_vis_options.tsx @@ -0,0 +1,54 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback } from 'react'; +import { i18n } from '@osd/i18n'; +import produce, { Draft } from 'immer'; +import { useTypedDispatch, useTypedSelector } from '../../../../application/utils/state_management'; +import { PieOptionsDefaults } from '../pie_vis_type'; +import { setState } from '../../../../application/utils/state_management/style_slice'; +import { Option } from '../../../../application/app'; +import { BasicVisOptions } from '../../common/basic_vis_options'; +import { SwitchOption } from '../../../../../../charts/public'; + +function PieVisOptions() { + const styleState = useTypedSelector((state) => state.style) as PieOptionsDefaults; + const dispatch = useTypedDispatch(); + + const setOption = useCallback( + (callback: (draft: Draft) => void) => { + const newState = produce(styleState, callback); + dispatch(setState(newState)); + }, + [dispatch, styleState] + ); + + return ( + <> + + + ); +} + +export { PieVisOptions }; diff --git a/src/plugins/vis_builder/public/visualizations/vislib/pie/index.ts b/src/plugins/vis_builder/public/visualizations/vislib/pie/index.ts new file mode 100644 index 000000000000..51bc8a1ccb9b --- /dev/null +++ b/src/plugins/vis_builder/public/visualizations/vislib/pie/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { createPieConfig } from './pie_vis_type'; diff --git a/src/plugins/vis_builder/public/visualizations/vislib/pie/pie_vis_type.ts b/src/plugins/vis_builder/public/visualizations/vislib/pie/pie_vis_type.ts new file mode 100644 index 000000000000..06d9771c407d --- /dev/null +++ b/src/plugins/vis_builder/public/visualizations/vislib/pie/pie_vis_type.ts @@ -0,0 +1,93 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { Schemas } from '../../../../../vis_default_editor/public'; +import { Positions } from '../../../../../vis_type_vislib/public'; +import { AggGroupNames } from '../../../../../data/public'; +import { PieVisOptions } from './components/pie_vis_options'; +import { VisualizationTypeOptions } from '../../../services/type_service'; +import { toExpression } from './to_expression'; +import { BasicOptionsDefaults } from '../common/types'; + +export interface PieOptionsDefaults extends BasicOptionsDefaults { + type: 'pie'; + isDonut: boolean; + showMetricsAtAllLevels: boolean; + labels: { + show: boolean; + values: boolean; + last_level: boolean; + truncate: number; + }; +} + +export const createPieConfig = (): VisualizationTypeOptions => ({ + name: 'pie', + title: 'Pie', + icon: 'visPie', + description: 'Display pie chart', + toExpression, + ui: { + containerConfig: { + data: { + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('visTypeVislib.pie.metricTitle', { + defaultMessage: 'Slice size', + }), + min: 1, + max: 1, + aggFilter: ['sum', 'count', 'cardinality', 'top_hits'], + defaults: [{ schema: 'metric', type: 'count' }], + }, + { + group: AggGroupNames.Buckets, + name: 'group', + title: i18n.translate('visTypeVislib.pie.groupTitle', { + defaultMessage: 'Split slices', + }), + min: 0, + max: Infinity, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], + defaults: { aggTypes: ['terms'] }, + }, + { + group: AggGroupNames.Buckets, + name: 'split', + title: i18n.translate('visTypeVislib.pie.splitTitle', { + defaultMessage: 'Split chart', + }), + mustBeFirst: true, + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], + defaults: { aggTypes: ['terms'] }, + }, + ]), + }, + style: { + defaults: { + addTooltip: true, + addLegend: true, + legendPosition: Positions.RIGHT, + isDonut: false, + showMetricsAtAllLevels: true, + type: 'pie', + labels: { + show: false, + values: true, + last_level: true, + truncate: 100, + }, + }, + render: PieVisOptions, + }, + }, + }, + hierarchicalData: true, +}); diff --git a/src/plugins/vis_builder/public/visualizations/vislib/pie/to_expression.test.ts b/src/plugins/vis_builder/public/visualizations/vislib/pie/to_expression.test.ts new file mode 100644 index 000000000000..a850c1690e3b --- /dev/null +++ b/src/plugins/vis_builder/public/visualizations/vislib/pie/to_expression.test.ts @@ -0,0 +1,4 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ diff --git a/src/plugins/vis_builder/public/visualizations/vislib/pie/to_expression.ts b/src/plugins/vis_builder/public/visualizations/vislib/pie/to_expression.ts new file mode 100644 index 000000000000..10ddbfcb9cc5 --- /dev/null +++ b/src/plugins/vis_builder/public/visualizations/vislib/pie/to_expression.ts @@ -0,0 +1,88 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { buildVislibDimensions, getVisSchemas } from '../../../../../visualizations/public'; +import { + buildExpression, + buildExpressionFunction, + IExpressionLoaderParams, +} from '../../../../../expressions/public'; +import { PieOptionsDefaults } from './pie_vis_type'; +import { getAggExpressionFunctions } from '../../common/expression_helpers'; +import { VislibRootState, getPipelineParams } from '../common'; +import { createVis } from '../common/create_vis'; +import { buildPipeline } from '../../../../../visualizations/public'; +import { createVegaSpec } from '../../vega/vega_spec_factory'; +import { executeExpression } from '../../vega/utils/expression_helper'; + +export const toExpression = async ( + { style: styleState, visualization }: VislibRootState, + searchContext: IExpressionLoaderParams['searchContext'] +) => { + const { expressionFns, aggConfigs, indexPattern } = await getAggExpressionFunctions( + visualization, + styleState, + true, + searchContext + ); + const { + addLegend, + addTooltip, + showMetricsAtAllLevels, + isDonut, + legendPosition, + type, + } = styleState; + const vis = await createVis(type, aggConfigs, indexPattern, searchContext); + const params = getPipelineParams(); + const schemas = getVisSchemas(vis, { + timeRange: params.timeRange, + timefilter: params.timefilter, + }); + + const dimensions = { + metric: schemas.metric[0], + buckets: schemas.group, + splitRow: schemas.split_row, + splitColumn: schemas.split_column, + }; + + const visConfig = { + addLegend, + addTooltip, + isDonut, + legendPosition, + dimensions, + showMetricsAtAllLevels, + }; + + const rawDataFn = buildExpressionFunction('rawData', {}); + const dataExpression = buildExpression([...expressionFns, rawDataFn]).toString(); + // Execute the expression to get the raw data + const rawData = await executeExpression(dataExpression, searchContext); + + const vegaSpec = createVegaSpec(rawData, visConfig, styleState); + + const visVega = await createVis('vega', aggConfigs, indexPattern, searchContext); + visVega.params = { + spec: JSON.stringify(vegaSpec), + }; + + const vegaExpression = await buildPipeline(visVega, { + timefilter: params.timefilter, + timeRange: params.timeRange, + abortSignal: undefined, + visLayers: undefined, + visAugmenterConfig: undefined, + }); + + return vegaExpression; + + // const pieVis = buildExpressionFunction('opensearch_dashboards_pie', { + // visConfig: JSON.stringify(visConfig), + // }); + + // return buildExpression([...expressionFns, pieVis]).toString(); +}; diff --git a/src/plugins/vis_default_editor/public/index.ts b/src/plugins/vis_default_editor/public/index.ts index 88769ac71f9f..47a3573335db 100644 --- a/src/plugins/vis_default_editor/public/index.ts +++ b/src/plugins/vis_default_editor/public/index.ts @@ -44,3 +44,5 @@ export function plugin() { start() {} })(); } + +export { calcAggIsTooLow } from './components/agg_group_helper';