diff --git a/src/plugins/chart_expressions/expression_partition_vis/kibana.json b/src/plugins/chart_expressions/expression_partition_vis/kibana.json index 226d1681cd3fc..08a030d466eab 100755 --- a/src/plugins/chart_expressions/expression_partition_vis/kibana.json +++ b/src/plugins/chart_expressions/expression_partition_vis/kibana.json @@ -12,7 +12,7 @@ "extraPublicDirs": [ "common" ], - "requiredPlugins": ["charts", "data", "expressions", "visualizations", "fieldFormats"], + "requiredPlugins": ["charts", "data", "expressions", "visualizations", "fieldFormats", "presentationUtil"], "requiredBundles": ["kibanaReact"], "optionalPlugins": [] } diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap b/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap index b367db1af5437..2df18b5813473 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap +++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap @@ -5,33 +5,34 @@ exports[`PartitionVisComponent should render correct structure for donut 1`] = ` css={ Object { "map": undefined, - "name": "1bdmk0u", + "name": "13h2mjc", "next": undefined, "styles": " - display:flex;flex:1 1 auto;min-height:0;min-width:0;;; + + min-height: 0; + min-width: 0; + margin-left: auto; + margin-right: auto; + width: 100%; + height: 100%; +; + inset: 0; position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; padding: 8px; - margin-left: auto; - margin-right: auto; - overflow: hidden; ", "toString": [Function], } } - data-test-subj="visTypePieChart" + data-test-subj="partitionVisChart" >
css` - ${partitionVisWrapperStyle}; +export const partitionVisContainerStyle = css` + min-height: 0; + min-width: 0; + margin-left: auto; + margin-right: auto; + width: 100%; + height: 100%; +`; + +export const partitionVisContainerWithToggleStyleFactory = (theme: EuiThemeComputed) => css` + ${partitionVisContainerStyle} + inset: 0; position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; padding: ${theme.size.s}; - margin-left: auto; - margin-right: auto; - overflow: hidden; `; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.test.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.test.tsx index ddade06c2c7e0..001f2390799e6 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.test.tsx +++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.test.tsx @@ -221,7 +221,9 @@ describe('PartitionVisComponent', function () { } as unknown as Datatable; const newProps = { ...wrapperProps, visData: newVisData }; const component = mount(); - expect(findTestSubject(component, 'pieVisualizationError').text()).toEqual('No results found'); + expect(findTestSubject(component, 'partitionVisEmptyValues').text()).toEqual( + 'No results found' + ); }); it('renders the no results component if there are negative values', () => { @@ -250,8 +252,8 @@ describe('PartitionVisComponent', function () { } as unknown as Datatable; const newProps = { ...wrapperProps, visData: newVisData }; const component = mount(); - expect(findTestSubject(component, 'pieVisualizationError').text()).toEqual( - "Pie/donut charts can't render with negative values." + expect(findTestSubject(component, 'partitionVisNegativeValues').text()).toEqual( + "Pie chart can't render with negative values." ); }); }); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx index cc96baac3a8ae..42a298d00d48c 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx +++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx @@ -20,12 +20,7 @@ import { SeriesIdentifier, } from '@elastic/charts'; import { useEuiTheme } from '@elastic/eui'; -import { - LegendToggle, - ClickTriggerEvent, - ChartsPluginSetup, - PaletteRegistry, -} from '../../../../charts/public'; +import { LegendToggle, ChartsPluginSetup, PaletteRegistry } from '../../../../charts/public'; import type { PersistedState } from '../../../../visualizations/public'; import { Datatable, @@ -63,10 +58,12 @@ import { VisualizationNoResults } from './visualization_noresults'; import { VisTypePiePluginStartDependencies } from '../plugin'; import { partitionVisWrapperStyle, - partitionVisContainerStyleFactory, + partitionVisContainerStyle, + partitionVisContainerWithToggleStyleFactory, } from './partition_vis_component.styles'; import { ChartTypes } from '../../common/types'; import { filterOutConfig } from '../utils/filter_out_config'; +import { FilterEvent } from '../types'; declare global { interface Window { @@ -93,7 +90,6 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => { const { visData, visParams: preVisParams, visType, services, syncColors } = props; const visParams = useMemo(() => filterOutConfig(visType, preVisParams), [preVisParams, visType]); - const theme = useEuiTheme(); const chartTheme = props.chartsThemeService.useChartsTheme(); const chartBaseTheme = props.chartsThemeService.useChartsBaseTheme(); @@ -103,8 +99,8 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => { ); const formatters = useMemo( - () => generateFormatters(visParams, visData, services.fieldFormats.deserialize), - [services.fieldFormats.deserialize, visData, visParams] + () => generateFormatters(visData, services.fieldFormats.deserialize), + [services.fieldFormats.deserialize, visData] ); const showLegendDefault = useCallback(() => { @@ -114,6 +110,8 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => { const [showLegend, setShowLegend] = useState(() => showLegendDefault()); + const showToggleLegendElement = props.uiState !== undefined; + const [dimensions, setDimensions] = useState(); const parentRef = useRef(null); @@ -157,11 +155,7 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => { splitChartDimension, splitChartFormatter ); - const event = { - name: 'filterBucket', - data: { data }, - }; - props.fireEvent(event); + props.fireEvent({ name: 'filter', data: { data } }); }, [props] ); @@ -169,11 +163,11 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => { // handles legend action event data const getLegendActionEventData = useCallback( (vData: Datatable) => - (series: SeriesIdentifier): ClickTriggerEvent | null => { + (series: SeriesIdentifier): FilterEvent => { const data = getFilterEventData(vData, series); return { - name: 'filterBucket', + name: 'filter', data: { negate: false, data, @@ -184,7 +178,7 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => { ); const handleLegendAction = useCallback( - (event: ClickTriggerEvent, negate = false) => { + (event: FilterEvent, negate = false) => { props.fireEvent({ ...event, data: { @@ -318,6 +312,9 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => { [visData.rows, metricColumn] ); + const isEmpty = visData.rows.length === 0; + const isMetricEmpty = visData.rows.every((row) => !row[metricColumn.id]); + /** * Checks whether data have negative values. * If so, the no data container is loaded. @@ -330,14 +327,23 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => { }), [visData.rows, metricColumn] ); + const flatLegend = isLegendFlat(visType, splitChartDimension); - const canShowPieChart = !isAllZeros && !hasNegative; + + const canShowPieChart = !isEmpty && !isMetricEmpty && !isAllZeros && !hasNegative; + + const { euiTheme } = useEuiTheme(); + + const chartContainerStyle = showToggleLegendElement + ? partitionVisContainerWithToggleStyleFactory(euiTheme) + : partitionVisContainerStyle; + const partitionType = getPartitionType(visType); return ( -
+
{!canShowPieChart ? ( - + ) : (
{ distinctColors: visParams.distinctColors ?? false, }} > - + {showToggleLegendElement && ( + + )} { /> { - return ( - - {hasNegativeValues - ? i18n.translate('expressionPartitionVis.negativeValuesFound', { - defaultMessage: "Pie/donut charts can't render with negative values.", - }) - : i18n.translate('expressionPartitionVis.noResultsFoundTitle', { - defaultMessage: 'No results found', - })} - - } - /> - ); +interface Props { + hasNegativeValues?: boolean; + chartType: ChartTypes; +} + +export const VisualizationNoResults: FC = ({ hasNegativeValues = false, chartType }) => { + if (hasNegativeValues) { + const message = ( + + ); + + return ( + + ); + } + + return ; }; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/expression_renderers/partition_vis_renderer.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/expression_renderers/partition_vis_renderer.tsx index c3521c7346a81..53e729466c1d2 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/expression_renderers/partition_vis_renderer.tsx +++ b/src/plugins/chart_expressions/expression_partition_vis/public/expression_renderers/partition_vis_renderer.tsx @@ -10,35 +10,27 @@ import React, { lazy } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { I18nProvider } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { Datatable, ExpressionRenderDefinition } from '../../../../expressions/public'; -import { VisualizationContainer } from '../../../../visualizations/public'; +import { ExpressionRenderDefinition } from '../../../../expressions/public'; import type { PersistedState } from '../../../../visualizations/public'; +import { VisTypePieDependencies } from '../plugin'; +import { withSuspense } from '../../../../presentation_util/public'; import { KibanaThemeProvider } from '../../../../kibana_react/public'; - import { PARTITION_VIS_RENDERER_NAME } from '../../common/constants'; import { ChartTypes, RenderValue } from '../../common/types'; -import { VisTypePieDependencies } from '../plugin'; - export const strings = { getDisplayName: () => - i18n.translate('expressionPartitionVis.renderer.pieVis.displayName', { - defaultMessage: 'Pie visualization', + i18n.translate('expressionPartitionVis.renderer.partitionVis.pie.displayName', { + defaultMessage: 'Partition visualization', }), getHelpDescription: () => - i18n.translate('expressionPartitionVis.renderer.pieVis.helpDescription', { - defaultMessage: 'Render a pie', + i18n.translate('expressionPartitionVis.renderer.partitionVis.pie.helpDescription', { + defaultMessage: 'Render pie/donut/treemap/mosaic/waffle charts', }), }; -const PartitionVisComponent = lazy(() => import('../components/partition_vis_component')); - -function shouldShowNoResultsMessage(visData: Datatable | undefined): boolean { - const rows: object[] | undefined = visData?.rows; - const isZeroHits = !rows || !rows.length; - - return Boolean(isZeroHits); -} +const LazyPartitionVisComponent = lazy(() => import('../components/partition_vis_component')); +const PartitionVisComponent = withSuspense(LazyPartitionVisComponent); export const getPartitionVisRenderer: ( deps: VisTypePieDependencies @@ -48,8 +40,6 @@ export const getPartitionVisRenderer: ( help: strings.getHelpDescription(), reuseDomNode: true, render: async (domNode, { visConfig, visData, visType, syncColors }, handlers) => { - const showNoResult = shouldShowNoResultsMessage(visData); - handlers.onDestroy(() => { unmountComponentAtNode(domNode); }); @@ -60,7 +50,7 @@ export const getPartitionVisRenderer: ( render( - +
- +
, - domNode + domNode, + () => { + handlers.done(); + } ); }, }); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/icons/donut.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/icons/donut.tsx new file mode 100644 index 0000000000000..5846fe0e7e8ba --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/icons/donut.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const DonutIcon = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/icons/index.ts b/src/plugins/chart_expressions/expression_partition_vis/public/icons/index.ts new file mode 100644 index 0000000000000..e61bd6557d581 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/icons/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { PieIcon } from './pie'; +export { DonutIcon } from './donut'; +export { TreemapIcon } from './treemap'; +export { MosaicIcon } from './mosaic'; +export { WaffleIcon } from './waffle'; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/icons/mosaic.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/icons/mosaic.tsx new file mode 100644 index 0000000000000..f8582495f2e0c --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/icons/mosaic.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import type { EuiIconProps } from '@elastic/eui'; + +export const MosaicIcon = ({ title, titleId, ...props }: Omit) => ( + + {title ? : null} + + + +); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/icons/pie.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/icons/pie.tsx new file mode 100644 index 0000000000000..9176ac3fdd5c1 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/icons/pie.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const PieIcon = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + +); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/icons/treemap.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/icons/treemap.tsx new file mode 100644 index 0000000000000..1860132fa9ffd --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/icons/treemap.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiIconProps } from '@elastic/eui'; + +export const TreemapIcon = ({ title, titleId, ...props }: Omit) => ( + + {title ? {title} : null} + + + + +); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/icons/waffle.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/icons/waffle.tsx new file mode 100644 index 0000000000000..30f05dd57f348 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/icons/waffle.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import type { EuiIconProps } from '@elastic/eui'; + +export const WaffleIcon = ({ title, titleId, ...props }: Omit) => ( + + {title ? : null} + + + +); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/types.ts b/src/plugins/chart_expressions/expression_partition_vis/public/types.ts index 64e132d2ddadb..aa87124ed2b4b 100755 --- a/src/plugins/chart_expressions/expression_partition_vis/public/types.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/types.ts @@ -5,6 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import type { ValueClickContext } from '../../../embeddable/public'; import { ChartsPluginSetup } from '../../../charts/public'; import { ExpressionsPublicPlugin, ExpressionsServiceStart } from '../../../expressions/public'; @@ -19,3 +20,8 @@ export interface SetupDeps { export interface StartDeps { expression: ExpressionsServiceStart; } + +export interface FilterEvent { + name: 'filter'; + data: ValueClickContext['data']; +} diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_helpers.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_helpers.ts index 47641a7f270c2..5b48d68f68201 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_helpers.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/filter_helpers.ts @@ -9,13 +9,13 @@ import { LayerValue, SeriesIdentifier } from '@elastic/charts'; import { Datatable, DatatableColumn } from '../../../../expressions/public'; import { DataPublicPluginStart } from '../../../../data/public'; -import { ClickTriggerEvent } from '../../../../charts/public'; import { ValueClickContext } from '../../../../embeddable/public'; import type { FieldFormat } from '../../../../field_formats/common'; import { BucketColumns } from '../../common/types'; +import { FilterEvent } from '../types'; export const canFilter = async ( - event: ClickTriggerEvent | null, + event: FilterEvent | null, actions: DataPublicPluginStart['actions'] ): Promise => { if (!event) { diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/formatters.test.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/formatters.test.ts index 69443dcfea5fb..18f89cb5f3e4e 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/utils/formatters.test.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/formatters.test.ts @@ -8,31 +8,19 @@ import { fieldFormatsMock } from '../../../../field_formats/common/mocks'; import { Datatable } from '../../../../expressions'; -import { createMockPieParams, createMockVisData } from '../mocks'; +import { createMockVisData } from '../mocks'; import { generateFormatters, getAvailableFormatter, getFormatter } from './formatters'; import { BucketColumns } from '../../common/types'; describe('generateFormatters', () => { - const visParams = createMockPieParams(); const visData = createMockVisData(); const defaultFormatter = jest.fn((...args) => fieldFormatsMock.deserialize(...args)); beforeEach(() => { defaultFormatter.mockClear(); }); - it('returns empty object, if labels should not be should ', () => { - const formatters = generateFormatters( - { ...visParams, labels: { ...visParams.labels, show: false } }, - visData, - defaultFormatter - ); - - expect(formatters).toEqual({}); - expect(defaultFormatter).toHaveBeenCalledTimes(0); - }); - it('returns formatters, if columns have meta parameters', () => { - const formatters = generateFormatters(visParams, visData, defaultFormatter); + const formatters = generateFormatters(visData, defaultFormatter); const formattingResult = fieldFormatsMock.deserialize(); const serializedFormatters = Object.keys(formatters).reduce( @@ -62,7 +50,7 @@ describe('generateFormatters', () => { columns: visData.columns.map(({ meta, ...col }) => ({ ...col, meta: { type: 'string' } })), }; - const formatters = generateFormatters(visParams, newVisData, defaultFormatter); + const formatters = generateFormatters(newVisData, defaultFormatter); expect(formatters).toEqual({ 'col-0-2': undefined, diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/formatters.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/formatters.ts index 59574dd248518..bbb30169928d4 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/utils/formatters.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/formatters.ts @@ -8,25 +8,16 @@ import type { FieldFormat, FormatFactory } from '../../../../field_formats/common'; import type { Datatable } from '../../../../expressions/public'; -import { BucketColumns, PartitionVisParams } from '../../common/types'; +import { BucketColumns } from '../../common/types'; -export const generateFormatters = ( - visParams: PartitionVisParams, - visData: Datatable, - formatFactory: FormatFactory -) => { - if (!visParams.labels.show) { - return {}; - } - - return visData.columns.reduce | undefined>>( +export const generateFormatters = (visData: Datatable, formatFactory: FormatFactory) => + visData.columns.reduce | undefined>>( (newFormatters, column) => ({ ...newFormatters, [column.id]: column?.meta?.params ? formatFactory(column.meta.params) : undefined, }), {} ); -}; export const getAvailableFormatter = ( column: Partial, diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_icon.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_icon.ts new file mode 100644 index 0000000000000..cac282553af11 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_icon.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ChartTypes } from '../../common/types'; +import { PieIcon, DonutIcon, TreemapIcon, MosaicIcon, WaffleIcon } from '../icons'; + +export const getIcon = (chart: ChartTypes) => + ({ + [ChartTypes.PIE]: PieIcon, + [ChartTypes.DONUT]: DonutIcon, + [ChartTypes.TREEMAP]: TreemapIcon, + [ChartTypes.MOSAIC]: MosaicIcon, + [ChartTypes.WAFFLE]: WaffleIcon, + }[chart]); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_legend_actions.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_legend_actions.tsx index 28b85f6300977..72793d771a0ee 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_legend_actions.tsx +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_legend_actions.tsx @@ -13,16 +13,16 @@ import { EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover, EuiContextMenu } fr import { LegendAction, SeriesIdentifier, useLegendAction } from '@elastic/charts'; import { DataPublicPluginStart } from '../../../../data/public'; import { PartitionVisParams } from '../../common/types'; -import { ClickTriggerEvent } from '../../../../charts/public'; import { FieldFormatsStart } from '../../../../field_formats/public'; +import { FilterEvent } from '../types'; export const getLegendActions = ( canFilter: ( - data: ClickTriggerEvent | null, + data: FilterEvent | null, actions: DataPublicPluginStart['actions'] ) => Promise, - getFilterEventData: (series: SeriesIdentifier) => ClickTriggerEvent | null, - onFilter: (data: ClickTriggerEvent, negate?: any) => void, + getFilterEventData: (series: SeriesIdentifier) => FilterEvent | null, + onFilter: (data: FilterEvent, negate?: any) => void, visParams: PartitionVisParams, actions: DataPublicPluginStart['actions'], formatter: FieldFormatsStart diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/index.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/index.ts index afa0b82a87eb1..b0ce92f1205e8 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/utils/index.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/index.ts @@ -18,3 +18,4 @@ export { getColumnByAccessor } from './accessor'; export { isLegendFlat, shouldShowLegend } from './legend'; export { generateFormatters, getAvailableFormatter, getFormatter } from './formatters'; export { getPartitionType } from './get_partition_type'; +export { getIcon } from './get_icon'; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_color.test.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_color.test.ts new file mode 100644 index 0000000000000..efeb1f038232d --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_color.test.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PaletteDefinition, PaletteOutput } from '../../../../../charts/public'; +import { chartPluginMock } from '../../../../../charts/public/mocks'; +import { Datatable } from '../../../../../expressions'; +import { byDataColorPaletteMap } from './get_color'; + +describe('#byDataColorPaletteMap', () => { + let datatable: Datatable; + let paletteDefinition: PaletteDefinition; + let palette: PaletteOutput; + const columnId = 'foo'; + + beforeEach(() => { + datatable = { + rows: [ + { + [columnId]: '1', + }, + { + [columnId]: '2', + }, + ], + } as unknown as Datatable; + paletteDefinition = chartPluginMock.createPaletteRegistry().get('default'); + palette = { type: 'palette' } as PaletteOutput; + }); + + it('should create byDataColorPaletteMap', () => { + expect(byDataColorPaletteMap(datatable.rows, columnId, paletteDefinition, palette)) + .toMatchInlineSnapshot(` + Object { + "getColor": [Function], + } + `); + }); + + it('should get color', () => { + const colorPaletteMap = byDataColorPaletteMap( + datatable.rows, + columnId, + paletteDefinition, + palette + ); + + expect(colorPaletteMap.getColor('1')).toBe('black'); + }); + + it('should return undefined in case if values not in datatable', () => { + const colorPaletteMap = byDataColorPaletteMap( + datatable.rows, + columnId, + paletteDefinition, + palette + ); + + expect(colorPaletteMap.getColor('wrong')).toBeUndefined(); + }); + + it('should increase rankAtDepth for each new value', () => { + const colorPaletteMap = byDataColorPaletteMap( + datatable.rows, + columnId, + paletteDefinition, + palette + ); + colorPaletteMap.getColor('1'); + colorPaletteMap.getColor('2'); + + expect(paletteDefinition.getCategoricalColor).toHaveBeenNthCalledWith( + 1, + [{ name: '1', rankAtDepth: 0, totalSeriesAtDepth: 2 }], + { behindText: false }, + undefined + ); + + expect(paletteDefinition.getCategoricalColor).toHaveBeenNthCalledWith( + 2, + [{ name: '2', rankAtDepth: 1, totalSeriesAtDepth: 2 }], + { behindText: false }, + undefined + ); + }); +}); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/sort_predicate.test.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/sort_predicate.test.ts new file mode 100644 index 0000000000000..1ccfdb7a5b1f9 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/sort_predicate.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Datatable } from '../../../../../expressions'; +import { extractUniqTermsMap } from './sort_predicate'; + +describe('#extractUniqTermsMap', () => { + it('should extract map', () => { + const table: Datatable = { + type: 'datatable', + columns: [ + { id: 'a', name: 'A', meta: { type: 'string' } }, + { id: 'b', name: 'B', meta: { type: 'string' } }, + { id: 'c', name: 'C', meta: { type: 'number' } }, + ], + rows: [ + { a: 'Hi', b: 'Two', c: 2 }, + { a: 'Test', b: 'Two', c: 5 }, + { a: 'Foo', b: 'Three', c: 6 }, + ], + }; + expect(extractUniqTermsMap(table, 'a')).toMatchInlineSnapshot(` + Object { + "Foo": 2, + "Hi": 0, + "Test": 1, + } + `); + expect(extractUniqTermsMap(table, 'b')).toMatchInlineSnapshot(` + Object { + "Three": 1, + "Two": 0, + } + `); + }); +}); diff --git a/src/plugins/chart_expressions/expression_partition_vis/tsconfig.json b/src/plugins/chart_expressions/expression_partition_vis/tsconfig.json index d480d7d27df5a..97a0c8a9fc515 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/tsconfig.json +++ b/src/plugins/chart_expressions/expression_partition_vis/tsconfig.json @@ -15,6 +15,7 @@ "references": [ { "path": "../../../core/tsconfig.json" }, { "path": "../../expressions/tsconfig.json" }, + { "path": "../../presentation_util/tsconfig.json" }, { "path": "../../data/tsconfig.json" }, { "path": "../../field_formats/tsconfig.json" }, { "path": "../../charts/tsconfig.json" }, diff --git a/src/plugins/charts/public/static/components/empty_placeholder.tsx b/src/plugins/charts/public/static/components/empty_placeholder.tsx index e376120c9cd9e..6989ea7a7a63b 100644 --- a/src/plugins/charts/public/static/components/empty_placeholder.tsx +++ b/src/plugins/charts/public/static/components/empty_placeholder.tsx @@ -13,14 +13,24 @@ import './empty_placeholder.scss'; export const EmptyPlaceholder = ({ icon, + iconColor = 'subdued', message = , + dataTestSubj = 'emptyPlaceholder', }: { icon: IconType; + iconColor?: string; message?: JSX.Element; + dataTestSubj?: string; }) => ( <> - - + +

{message}

diff --git a/test/functional/apps/visualize/_pie_chart.ts b/test/functional/apps/visualize/_pie_chart.ts index 744ba3caa719e..48d49d3007b68 100644 --- a/test/functional/apps/visualize/_pie_chart.ts +++ b/test/functional/apps/visualize/_pie_chart.ts @@ -432,7 +432,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { '360,000', 'CN', ].sort(); - if (await PageObjects.visChart.isNewLibraryChart('visTypePieChart')) { + if (await PageObjects.visChart.isNewLibraryChart('partitionVisChart')) { await PageObjects.visEditor.clickOptionsTab(); await PageObjects.visEditor.togglePieLegend(); await PageObjects.visEditor.togglePieNestedLegend(); diff --git a/test/functional/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts index 60d7c6e7d7435..3eec4e2ce1a2b 100644 --- a/test/functional/page_objects/visualize_chart_page.ts +++ b/test/functional/page_objects/visualize_chart_page.ts @@ -11,7 +11,7 @@ import chroma from 'chroma-js'; import { FtrService } from '../ftr_provider_context'; -const pieChartSelector = 'visTypePieChart'; +const partitionVisChartSelector = 'partitionVisChart'; const heatmapChartSelector = 'heatmapChart'; export class VisualizeChartPageObject extends FtrService { @@ -149,7 +149,7 @@ export class VisualizeChartPageObject extends FtrService { } private async toggleLegend(force = false) { - const isVisTypePieChart = await this.isNewLibraryChart(pieChartSelector); + const isVisTypePieChart = await this.isNewLibraryChart(partitionVisChartSelector); const legendSelector = force || isVisTypePieChart ? '.echLegend' : '.visLegend'; await this.retry.try(async () => { @@ -182,10 +182,11 @@ export class VisualizeChartPageObject extends FtrService { } public async doesSelectedLegendColorExistForPie(matchingColor: string) { - if (await this.isNewLibraryChart(pieChartSelector)) { + if (await this.isNewLibraryChart(partitionVisChartSelector)) { const hexMatchingColor = chroma(matchingColor).hex().toUpperCase(); const slices = - (await this.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ?? []; + (await this.getEsChartDebugState(partitionVisChartSelector))?.partition?.[0]?.partitions ?? + []; return slices.some(({ color }) => { return hexMatchingColor === chroma(color).hex().toUpperCase(); }); @@ -195,7 +196,7 @@ export class VisualizeChartPageObject extends FtrService { } public async expectError() { - if (!this.isNewLibraryChart(pieChartSelector)) { + if (!this.isNewLibraryChart(partitionVisChartSelector)) { await this.testSubjects.existOrFail('vislibVisualizeError'); } } @@ -244,12 +245,13 @@ export class VisualizeChartPageObject extends FtrService { } public async getLegendEntries() { - const isVisTypePieChart = await this.isNewLibraryChart(pieChartSelector); + const isVisTypePieChart = await this.isNewLibraryChart(partitionVisChartSelector); const isVisTypeHeatmapChart = await this.isNewLibraryChart(heatmapChartSelector); if (isVisTypePieChart) { const slices = - (await this.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ?? []; + (await this.getEsChartDebugState(partitionVisChartSelector))?.partition?.[0]?.partitions ?? + []; return slices.map(({ name }) => name); } @@ -290,7 +292,7 @@ export class VisualizeChartPageObject extends FtrService { public async openLegendOptionColorsForPie(name: string, chartSelector: string) { await this.waitForVisualizationRenderingStabilized(); await this.retry.try(async () => { - if (await this.isNewLibraryChart(pieChartSelector)) { + if (await this.isNewLibraryChart(partitionVisChartSelector)) { const chart = await this.find.byCssSelector(chartSelector); const legendItemColor = await chart.findByCssSelector( `[data-ech-series-name="${name}"] .echLegendItem__color` diff --git a/test/functional/services/visualizations/pie_chart.ts b/test/functional/services/visualizations/pie_chart.ts index ff0c24e2830cf..16133140e4abf 100644 --- a/test/functional/services/visualizations/pie_chart.ts +++ b/test/functional/services/visualizations/pie_chart.ts @@ -10,7 +10,7 @@ import expect from '@kbn/expect'; import { isNil } from 'lodash'; import { FtrService } from '../../ftr_provider_context'; -const pieChartSelector = 'visTypePieChart'; +const partitionVisChartSelector = 'partitionVisChart'; export class PieChartService extends FtrService { private readonly log = this.ctx.getService('log'); @@ -27,16 +27,16 @@ export class PieChartService extends FtrService { async clickOnPieSlice(name?: string) { this.log.debug(`PieChart.clickOnPieSlice(${name})`); - if (await this.visChart.isNewLibraryChart(pieChartSelector)) { + if (await this.visChart.isNewLibraryChart(partitionVisChartSelector)) { const slices = - (await this.visChart.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ?? - []; + (await this.visChart.getEsChartDebugState(partitionVisChartSelector))?.partition?.[0] + ?.partitions ?? []; let sliceLabel = name || slices[0].name; if (name === 'Other') { sliceLabel = '__other__'; } const pieSlice = slices.find((slice) => slice.name === sliceLabel); - const pie = await this.testSubjects.find(pieChartSelector); + const pie = await this.testSubjects.find(partitionVisChartSelector); if (pieSlice) { const pieSize = await pie.getSize(); const pieHeight = pieSize.height; @@ -88,10 +88,10 @@ export class PieChartService extends FtrService { async getPieSliceStyle(name: string) { this.log.debug(`VisualizePage.getPieSliceStyle(${name})`); - if (await this.visChart.isNewLibraryChart(pieChartSelector)) { + if (await this.visChart.isNewLibraryChart(partitionVisChartSelector)) { const slices = - (await this.visChart.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ?? - []; + (await this.visChart.getEsChartDebugState(partitionVisChartSelector))?.partition?.[0] + ?.partitions ?? []; const selectedSlice = slices.filter((slice) => { return slice.name.toString() === name.replace(',', ''); }); @@ -103,10 +103,10 @@ export class PieChartService extends FtrService { async getAllPieSliceColor(name: string) { this.log.debug(`VisualizePage.getAllPieSliceColor(${name})`); - if (await this.visChart.isNewLibraryChart(pieChartSelector)) { + if (await this.visChart.isNewLibraryChart(partitionVisChartSelector)) { const slices = - (await this.visChart.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ?? - []; + (await this.visChart.getEsChartDebugState(partitionVisChartSelector))?.partition?.[0] + ?.partitions ?? []; const selectedSlice = slices.filter((slice) => { return slice.name.toString() === name.replace(',', ''); }); @@ -143,10 +143,10 @@ export class PieChartService extends FtrService { } async getPieChartLabels() { - if (await this.visChart.isNewLibraryChart(pieChartSelector)) { + if (await this.visChart.isNewLibraryChart(partitionVisChartSelector)) { const slices = - (await this.visChart.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ?? - []; + (await this.visChart.getEsChartDebugState(partitionVisChartSelector))?.partition?.[0] + ?.partitions ?? []; return slices.map((slice) => { if (slice.name === '__missing__') { return 'Missing'; @@ -169,10 +169,10 @@ export class PieChartService extends FtrService { async getPieSliceCount() { this.log.debug('PieChart.getPieSliceCount'); - if (await this.visChart.isNewLibraryChart(pieChartSelector)) { + if (await this.visChart.isNewLibraryChart(partitionVisChartSelector)) { const slices = - (await this.visChart.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ?? - []; + (await this.visChart.getEsChartDebugState(partitionVisChartSelector))?.partition?.[0] + ?.partitions ?? []; return slices?.length; } const slices = await this.find.allByCssSelector('svg > g > g.arcs > path.slice'); @@ -181,8 +181,8 @@ export class PieChartService extends FtrService { async expectPieSliceCountEsCharts(expectedCount: number) { const slices = - (await this.visChart.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ?? - []; + (await this.visChart.getEsChartDebugState(partitionVisChartSelector))?.partition?.[0] + ?.partitions ?? []; expect(slices.length).to.be(expectedCount); } diff --git a/x-pack/plugins/lens/common/constants.ts b/x-pack/plugins/lens/common/constants.ts index f0db3385cefc1..bd507be52e2ab 100644 --- a/x-pack/plugins/lens/common/constants.ts +++ b/x-pack/plugins/lens/common/constants.ts @@ -17,6 +17,32 @@ export const NOT_INTERNATIONALIZED_PRODUCT_NAME = 'Lens Visualizations'; export const BASE_API_URL = '/api/lens'; export const LENS_EDIT_BY_VALUE = 'edit_by_value'; +export const PieChartTypes = { + PIE: 'pie', + DONUT: 'donut', + TREEMAP: 'treemap', + MOSAIC: 'mosaic', + WAFFLE: 'waffle', +} as const; + +export const CategoryDisplay = { + DEFAULT: 'default', + INSIDE: 'inside', + HIDE: 'hide', +} as const; + +export const NumberDisplay = { + HIDDEN: 'hidden', + PERCENT: 'percent', + VALUE: 'value', +} as const; + +export const LegendDisplay = { + DEFAULT: 'default', + SHOW: 'show', + HIDE: 'hide', +} as const; + export const layerTypes: Record = { DATA: 'data', REFERENCELINE: 'referenceLine', diff --git a/x-pack/plugins/lens/common/expressions/index.ts b/x-pack/plugins/lens/common/expressions/index.ts index c5ee16ed4bcfd..d7c27c4436b42 100644 --- a/x-pack/plugins/lens/common/expressions/index.ts +++ b/x-pack/plugins/lens/common/expressions/index.ts @@ -12,7 +12,6 @@ export * from './merge_tables'; export * from './time_scale'; export * from './datatable'; export * from './metric_chart'; -export * from './pie_chart'; export * from './xy_chart'; export * from './expression_types'; diff --git a/x-pack/plugins/lens/common/expressions/pie_chart/index.ts b/x-pack/plugins/lens/common/expressions/pie_chart/index.ts deleted file mode 100644 index 1c1f6fdae4578..0000000000000 --- a/x-pack/plugins/lens/common/expressions/pie_chart/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { pie } from './pie_chart'; - -export type { - SharedPieLayerState, - PieLayerState, - PieVisualizationState, - PieExpressionArgs, - PieExpressionProps, -} from './types'; diff --git a/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts b/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts deleted file mode 100644 index feec2117632c0..0000000000000 --- a/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Position } from '@elastic/charts'; -import { i18n } from '@kbn/i18n'; - -import type { ExpressionFunctionDefinition } from '../../../../../../src/plugins/expressions/common'; -import type { LensMultiTable } from '../../types'; -import type { PieExpressionProps, PieExpressionArgs } from './types'; - -interface PieRender { - type: 'render'; - as: 'lens_pie_renderer'; - value: PieExpressionProps; -} - -export const pie: ExpressionFunctionDefinition< - 'lens_pie', - LensMultiTable, - PieExpressionArgs, - PieRender -> = { - name: 'lens_pie', - type: 'render', - help: i18n.translate('xpack.lens.pie.expressionHelpLabel', { - defaultMessage: 'Pie renderer', - }), - args: { - title: { - types: ['string'], - help: 'The chart title.', - }, - description: { - types: ['string'], - help: '', - }, - groups: { - types: ['string'], - multi: true, - help: '', - }, - metric: { - types: ['string'], - help: '', - }, - shape: { - types: ['string'], - options: ['pie', 'donut', 'treemap', 'mosaic'], - help: '', - }, - hideLabels: { - types: ['boolean'], - help: '', - }, - numberDisplay: { - types: ['string'], - options: ['hidden', 'percent', 'value'], - help: '', - }, - categoryDisplay: { - types: ['string'], - options: ['default', 'inside', 'hide'], - help: '', - }, - legendDisplay: { - types: ['string'], - options: ['default', 'show', 'hide'], - help: '', - }, - nestedLegend: { - types: ['boolean'], - help: '', - }, - legendMaxLines: { - types: ['number'], - help: '', - }, - truncateLegend: { - types: ['boolean'], - help: '', - }, - showValuesInLegend: { - types: ['boolean'], - help: '', - }, - legendPosition: { - types: ['string'], - options: [Position.Top, Position.Right, Position.Bottom, Position.Left], - help: '', - }, - percentDecimals: { - types: ['number'], - help: '', - }, - palette: { - default: `{theme "palette" default={system_palette name="default"} }`, - help: '', - types: ['palette'], - }, - emptySizeRatio: { - types: ['number'], - help: '', - }, - ariaLabel: { - types: ['string'], - help: '', - required: false, - }, - }, - inputTypes: ['lens_multitable'], - fn(data: LensMultiTable, args: PieExpressionArgs, handlers) { - return { - type: 'render', - as: 'lens_pie_renderer', - value: { - data, - args: { - ...args, - ariaLabel: - args.ariaLabel ?? - (handlers.variables?.embeddableTitle as string) ?? - handlers.getExecutionContext?.()?.description, - }, - }, - }; - }, -}; diff --git a/x-pack/plugins/lens/common/expressions/pie_chart/types.ts b/x-pack/plugins/lens/common/expressions/pie_chart/types.ts deleted file mode 100644 index aa84488dbc2c2..0000000000000 --- a/x-pack/plugins/lens/common/expressions/pie_chart/types.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { PaletteOutput } from '../../../../../../src/plugins/charts/common'; -import type { LensMultiTable, LayerType } from '../../types'; - -export type PieChartTypes = 'donut' | 'pie' | 'treemap' | 'mosaic' | 'waffle'; - -export interface SharedPieLayerState { - groups: string[]; - metric?: string; - numberDisplay: 'hidden' | 'percent' | 'value'; - categoryDisplay: 'default' | 'inside' | 'hide'; - legendDisplay: 'default' | 'show' | 'hide'; - legendPosition?: 'left' | 'right' | 'top' | 'bottom'; - showValuesInLegend?: boolean; - nestedLegend?: boolean; - percentDecimals?: number; - emptySizeRatio?: number; - legendMaxLines?: number; - truncateLegend?: boolean; -} - -export type PieLayerState = SharedPieLayerState & { - layerId: string; - layerType: LayerType; -}; - -export interface PieVisualizationState { - shape: PieChartTypes; - layers: PieLayerState[]; - palette?: PaletteOutput; -} - -export type PieExpressionArgs = SharedPieLayerState & { - title?: string; - description?: string; - shape: PieChartTypes; - hideLabels: boolean; - palette: PaletteOutput; - ariaLabel?: string; -}; - -export interface PieExpressionProps { - data: LensMultiTable; - args: PieExpressionArgs; -} diff --git a/x-pack/plugins/lens/common/types.ts b/x-pack/plugins/lens/common/types.ts index f3572fea90f9e..0b2b5d5d739d0 100644 --- a/x-pack/plugins/lens/common/types.ts +++ b/x-pack/plugins/lens/common/types.ts @@ -6,12 +6,16 @@ */ import type { Filter, FilterMeta } from '@kbn/es-query'; +import { Position } from '@elastic/charts'; +import { $Values } from '@kbn/utility-types'; import type { IFieldFormat, SerializedFieldFormat, } from '../../../../src/plugins/field_formats/common'; import type { Datatable } from '../../../../src/plugins/expressions/common'; import type { PaletteContinuity } from '../../../../src/plugins/charts/common'; +import type { PaletteOutput } from '../../../../src/plugins/charts/common'; +import { CategoryDisplay, LegendDisplay, NumberDisplay, PieChartTypes } from './constants'; export type FormatFactory = (mapping?: SerializedFieldFormat) => IFieldFormat; @@ -73,3 +77,41 @@ export type LayerType = 'data' | 'referenceLine'; // Shared by XY Chart and Heatmap as for now export type ValueLabelConfig = 'hide' | 'inside' | 'outside'; + +export type PieChartType = $Values; +export type CategoryDisplayType = $Values; +export type NumberDisplayType = $Values; + +export type LegendDisplayType = $Values; + +export enum EmptySizeRatios { + SMALL = 0.3, + MEDIUM = 0.54, + LARGE = 0.7, +} + +export interface SharedPieLayerState { + groups: string[]; + metric?: string; + numberDisplay: NumberDisplayType; + categoryDisplay: CategoryDisplayType; + legendDisplay: LegendDisplayType; + legendPosition?: Position; + showValuesInLegend?: boolean; + nestedLegend?: boolean; + percentDecimals?: number; + emptySizeRatio?: number; + legendMaxLines?: number; + truncateLegend?: boolean; +} + +export type PieLayerState = SharedPieLayerState & { + layerId: string; + layerType: LayerType; +}; + +export interface PieVisualizationState { + shape: $Values; + layers: PieLayerState[]; + palette?: PaletteOutput; +} diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx b/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx index b8fd06a09ebcd..482a5b931ed78 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx @@ -23,7 +23,8 @@ import type { LensByReferenceInput, LensByValueInput } from './embeddable'; import type { Document } from '../persistence'; import type { IndexPatternPersistedState } from '../indexpattern_datasource/types'; import type { XYState } from '../xy_visualization/types'; -import type { PieVisualizationState, MetricState } from '../../common/expressions'; +import type { MetricState } from '../../common/expressions'; +import type { PieVisualizationState } from '../../common'; import type { DatatableVisualizationState } from '../datatable_visualization/visualization'; import type { HeatmapVisualizationState } from '../heatmap_visualization/types'; import type { GaugeVisualizationState } from '../visualizations/gauge/constants'; diff --git a/x-pack/plugins/lens/public/expressions.ts b/x-pack/plugins/lens/public/expressions.ts index 22e43addefcdd..2e5a30345633f 100644 --- a/x-pack/plugins/lens/public/expressions.ts +++ b/x-pack/plugins/lens/public/expressions.ts @@ -24,7 +24,6 @@ import { datatableColumn } from '../common/expressions/datatable/datatable_colum import { mergeTables } from '../common/expressions/merge_tables'; import { renameColumns } from '../common/expressions/rename_columns/rename_columns'; -import { pie } from '../common/expressions/pie_chart/pie_chart'; import { formatColumn } from '../common/expressions/format_column'; import { counterRate } from '../common/expressions/counter_rate'; import { getTimeScale } from '../common/expressions/time_scale/time_scale'; @@ -39,7 +38,6 @@ export const setupExpressions = ( [lensMultitable].forEach((expressionType) => expressions.registerType(expressionType)); [ - pie, xyChart, mergeTables, counterRate, diff --git a/x-pack/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts index 1c045e63e9e26..f6ccb071075ac 100644 --- a/x-pack/plugins/lens/public/index.ts +++ b/x-pack/plugins/lens/public/index.ts @@ -14,9 +14,6 @@ export type { export type { XYState } from './xy_visualization/types'; export type { DataType, OperationMetadata, Visualization } from './types'; export type { - PieVisualizationState, - PieLayerState, - SharedPieLayerState, MetricState, AxesSettingsConfig, XYLayerConfig, @@ -26,7 +23,13 @@ export type { XYCurveType, YConfig, } from '../common/expressions'; -export type { ValueLabelConfig } from '../common/types'; +export type { + ValueLabelConfig, + PieVisualizationState, + PieLayerState, + SharedPieLayerState, +} from '../common/types'; + export type { DatatableVisualizationState } from './datatable_visualization/visualization'; export type { HeatmapVisualizationState } from './heatmap_visualization/types'; export type { GaugeVisualizationState } from './visualizations/gauge/constants'; diff --git a/x-pack/plugins/lens/public/pie_visualization/constants.ts b/x-pack/plugins/lens/public/pie_visualization/constants.ts index e32320bb75ff0..bfb263b415891 100644 --- a/x-pack/plugins/lens/public/pie_visualization/constants.ts +++ b/x-pack/plugins/lens/public/pie_visualization/constants.ts @@ -6,9 +6,3 @@ */ export const DEFAULT_PERCENT_DECIMALS = 2; - -export enum EMPTY_SIZE_RATIOS { - SMALL = 0.3, - MEDIUM = 0.54, - LARGE = 0.7, -} diff --git a/x-pack/plugins/lens/public/pie_visualization/expression.tsx b/x-pack/plugins/lens/public/pie_visualization/expression.tsx deleted file mode 100644 index bf52fb6ba5e5e..0000000000000 --- a/x-pack/plugins/lens/public/pie_visualization/expression.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; -import { i18n } from '@kbn/i18n'; -import { I18nProvider } from '@kbn/i18n-react'; -import type { - IInterpreterRenderHandlers, - ExpressionRenderDefinition, -} from 'src/plugins/expressions/public'; -import { ThemeServiceStart } from 'kibana/public'; -import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public'; -import type { LensFilterEvent } from '../types'; -import { PieComponent } from './render_function'; -import type { FormatFactory } from '../../common'; -import type { PieExpressionProps } from '../../common/expressions'; -import type { ChartsPluginSetup, PaletteRegistry } from '../../../../../src/plugins/charts/public'; - -export const getPieRenderer = (dependencies: { - formatFactory: FormatFactory; - chartsThemeService: ChartsPluginSetup['theme']; - paletteService: PaletteRegistry; - kibanaTheme: ThemeServiceStart; -}): ExpressionRenderDefinition => ({ - name: 'lens_pie_renderer', - displayName: i18n.translate('xpack.lens.pie.visualizationName', { - defaultMessage: 'Pie', - }), - help: '', - validate: () => undefined, - reuseDomNode: true, - render: (domNode: Element, config: PieExpressionProps, handlers: IInterpreterRenderHandlers) => { - const onClickValue = (data: LensFilterEvent['data']) => { - handlers.event({ name: 'filter', data }); - }; - - ReactDOM.render( - - - - - , - domNode, - () => { - handlers.done(); - } - ); - handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); - }, -}); - -const MemoizedChart = React.memo(PieComponent); diff --git a/x-pack/plugins/lens/public/pie_visualization/get_legend_action.test.tsx b/x-pack/plugins/lens/public/pie_visualization/get_legend_action.test.tsx deleted file mode 100644 index df0648aa40d74..0000000000000 --- a/x-pack/plugins/lens/public/pie_visualization/get_legend_action.test.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { LegendActionProps, SeriesIdentifier } from '@elastic/charts'; -import { EuiPopover } from '@elastic/eui'; -import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { ComponentType, ReactWrapper } from 'enzyme'; -import type { Datatable } from 'src/plugins/expressions/public'; -import { getLegendAction } from './get_legend_action'; -import { LegendActionPopover } from '../shared_components'; - -const table: Datatable = { - type: 'datatable', - columns: [ - { id: 'a', name: 'A', meta: { type: 'string' } }, - { id: 'b', name: 'B', meta: { type: 'number' } }, - ], - rows: [ - { a: 'Hi', b: 2 }, - { a: 'Test', b: 4 }, - { a: 'Foo', b: 6 }, - ], -}; - -describe('getLegendAction', function () { - let wrapperProps: LegendActionProps; - const Component: ComponentType = getLegendAction(table, jest.fn()); - let wrapper: ReactWrapper; - - beforeAll(() => { - wrapperProps = { - color: 'rgb(109, 204, 177)', - label: 'Bar', - series: [ - { - specId: 'donut', - key: 'Bar', - }, - ] as unknown as SeriesIdentifier[], - }; - }); - - it('is not rendered if row does not exist', () => { - wrapper = mountWithIntl(); - expect(wrapper).toEqual({}); - expect(wrapper.find(EuiPopover).length).toBe(0); - }); - - it('is rendered if row is detected', () => { - const newProps = { - ...wrapperProps, - label: 'Hi', - series: [ - { - specId: 'donut', - key: 'Hi', - }, - ] as unknown as SeriesIdentifier[], - }; - wrapper = mountWithIntl(); - expect(wrapper.find(EuiPopover).length).toBe(1); - expect(wrapper.find(EuiPopover).prop('title')).toEqual('Hi, filter options'); - expect(wrapper.find(LegendActionPopover).prop('context')).toEqual({ - data: [ - { - column: 0, - row: 0, - table, - value: 'Hi', - }, - ], - }); - }); -}); diff --git a/x-pack/plugins/lens/public/pie_visualization/get_legend_action.tsx b/x-pack/plugins/lens/public/pie_visualization/get_legend_action.tsx deleted file mode 100644 index 9f16ad863a415..0000000000000 --- a/x-pack/plugins/lens/public/pie_visualization/get_legend_action.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import type { LegendAction } from '@elastic/charts'; -import type { Datatable } from 'src/plugins/expressions/public'; -import type { LensFilterEvent } from '../types'; -import { LegendActionPopover } from '../shared_components'; - -export const getLegendAction = ( - table: Datatable, - onFilter: (data: LensFilterEvent['data']) => void -): LegendAction => - React.memo(({ series: [pieSeries], label }) => { - const data = table.columns.reduce((acc, { id }, column) => { - const value = pieSeries.key; - const row = table.rows.findIndex((r) => r[id] === value); - if (row > -1) { - acc.push({ - table, - column, - row, - value, - }); - } - - return acc; - }, []); - - if (data.length === 0) { - return null; - } - - const context: LensFilterEvent['data'] = { - data, - }; - - return ; - }); diff --git a/x-pack/plugins/lens/public/pie_visualization/index.ts b/x-pack/plugins/lens/public/pie_visualization/index.ts index ce54f53c1cc93..b86c2fc90e4fa 100644 --- a/x-pack/plugins/lens/public/pie_visualization/index.ts +++ b/x-pack/plugins/lens/public/pie_visualization/index.ts @@ -6,16 +6,12 @@ */ import type { CoreSetup } from 'src/core/public'; -import type { ExpressionsSetup } from 'src/plugins/expressions/public'; import type { EditorFrameSetup } from '../types'; import type { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; import type { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; -import type { FormatFactory } from '../../common'; export interface PieVisualizationPluginSetupPlugins { editorFrame: EditorFrameSetup; - expressions: ExpressionsSetup; - formatFactory: FormatFactory; charts: ChartsPluginSetup; } @@ -24,22 +20,11 @@ export interface PieVisualizationPluginStartPlugins { } export class PieVisualization { - setup( - core: CoreSetup, - { expressions, formatFactory, editorFrame, charts }: PieVisualizationPluginSetupPlugins - ) { + setup(core: CoreSetup, { editorFrame, charts }: PieVisualizationPluginSetupPlugins) { editorFrame.registerVisualization(async () => { - const { getPieVisualization, getPieRenderer } = await import('../async_services'); + const { getPieVisualization } = await import('../async_services'); const palettes = await charts.palettes.getPalettes(); - expressions.registerRenderer( - getPieRenderer({ - formatFactory, - chartsThemeService: charts.theme, - paletteService: palettes, - kibanaTheme: core.theme, - }) - ); return getPieVisualization({ paletteService: palettes, kibanaTheme: core.theme }); }); } diff --git a/x-pack/plugins/lens/public/pie_visualization/partition_charts_meta.ts b/x-pack/plugins/lens/public/pie_visualization/partition_charts_meta.ts index 3d02c0f6d513e..d77a09ae10689 100644 --- a/x-pack/plugins/lens/public/pie_visualization/partition_charts_meta.ts +++ b/x-pack/plugins/lens/public/pie_visualization/partition_charts_meta.ts @@ -6,24 +6,20 @@ */ import { i18n } from '@kbn/i18n'; -import { ArrayEntry, PartitionLayout } from '@elastic/charts'; import type { EuiIconProps } from '@elastic/eui'; +import type { DatatableColumn } from '../../../../../src/plugins/expressions'; import { LensIconChartDonut } from '../assets/chart_donut'; import { LensIconChartPie } from '../assets/chart_pie'; import { LensIconChartTreemap } from '../assets/chart_treemap'; import { LensIconChartMosaic } from '../assets/chart_mosaic'; import { LensIconChartWaffle } from '../assets/chart_waffle'; -import { EMPTY_SIZE_RATIOS } from './constants'; - -import type { SharedPieLayerState } from '../../common/expressions'; -import type { PieChartTypes } from '../../common/expressions/pie_chart/types'; -import type { DatatableColumn } from '../../../../../src/plugins/expressions'; +import { CategoryDisplay, NumberDisplay, SharedPieLayerState, EmptySizeRatios } from '../../common'; +import type { PieChartType } from '../../common/types'; interface PartitionChartMeta { icon: ({ title, titleId, ...props }: Omit) => JSX.Element; label: string; - partitionType: PartitionLayout; groupLabel: string; maxBuckets: number; isExperimental?: boolean; @@ -40,7 +36,7 @@ interface PartitionChartMeta { }>; emptySizeRatioOptions?: Array<{ id: string; - value: EMPTY_SIZE_RATIOS; + value: EmptySizeRatios; label: string; }>; }; @@ -50,10 +46,6 @@ interface PartitionChartMeta { hideNestedLegendSwitch?: boolean; getShowLegendDefault?: (bucketColumns: DatatableColumn[]) => boolean; }; - sortPredicate?: ( - bucketColumns: DatatableColumn[], - sortingMap: Record - ) => (node1: ArrayEntry, node2: ArrayEntry) => number; } const groupLabel = i18n.translate('xpack.lens.pie.groupLabel', { @@ -62,19 +54,19 @@ const groupLabel = i18n.translate('xpack.lens.pie.groupLabel', { const categoryOptions: PartitionChartMeta['toolbarPopover']['categoryOptions'] = [ { - value: 'default', + value: CategoryDisplay.DEFAULT, inputDisplay: i18n.translate('xpack.lens.pieChart.showCategoriesLabel', { defaultMessage: 'Inside or outside', }), }, { - value: 'inside', + value: CategoryDisplay.INSIDE, inputDisplay: i18n.translate('xpack.lens.pieChart.fitInsideOnlyLabel', { defaultMessage: 'Inside only', }), }, { - value: 'hide', + value: CategoryDisplay.HIDE, inputDisplay: i18n.translate('xpack.lens.pieChart.categoriesInLegendLabel', { defaultMessage: 'Hide labels', }), @@ -83,13 +75,13 @@ const categoryOptions: PartitionChartMeta['toolbarPopover']['categoryOptions'] = const categoryOptionsTreemap: PartitionChartMeta['toolbarPopover']['categoryOptions'] = [ { - value: 'default', + value: CategoryDisplay.DEFAULT, inputDisplay: i18n.translate('xpack.lens.pieChart.showTreemapCategoriesLabel', { defaultMessage: 'Show labels', }), }, { - value: 'hide', + value: CategoryDisplay.HIDE, inputDisplay: i18n.translate('xpack.lens.pieChart.categoriesInLegendLabel', { defaultMessage: 'Hide labels', }), @@ -98,19 +90,19 @@ const categoryOptionsTreemap: PartitionChartMeta['toolbarPopover']['categoryOpti const numberOptions: PartitionChartMeta['toolbarPopover']['numberOptions'] = [ { - value: 'hidden', + value: NumberDisplay.HIDDEN, inputDisplay: i18n.translate('xpack.lens.pieChart.hiddenNumbersLabel', { defaultMessage: 'Hide from chart', }), }, { - value: 'percent', + value: NumberDisplay.PERCENT, inputDisplay: i18n.translate('xpack.lens.pieChart.showPercentValuesLabel', { defaultMessage: 'Show percent', }), }, { - value: 'value', + value: NumberDisplay.VALUE, inputDisplay: i18n.translate('xpack.lens.pieChart.showFormatterValuesLabel', { defaultMessage: 'Show value', }), @@ -120,34 +112,33 @@ const numberOptions: PartitionChartMeta['toolbarPopover']['numberOptions'] = [ const emptySizeRatioOptions: PartitionChartMeta['toolbarPopover']['emptySizeRatioOptions'] = [ { id: 'emptySizeRatioOption-small', - value: EMPTY_SIZE_RATIOS.SMALL, + value: EmptySizeRatios.SMALL, label: i18n.translate('xpack.lens.pieChart.emptySizeRatioOptions.small', { defaultMessage: 'Small', }), }, { id: 'emptySizeRatioOption-medium', - value: EMPTY_SIZE_RATIOS.MEDIUM, + value: EmptySizeRatios.MEDIUM, label: i18n.translate('xpack.lens.pieChart.emptySizeRatioOptions.medium', { defaultMessage: 'Medium', }), }, { id: 'emptySizeRatioOption-large', - value: EMPTY_SIZE_RATIOS.LARGE, + value: EmptySizeRatios.LARGE, label: i18n.translate('xpack.lens.pieChart.emptySizeRatioOptions.large', { defaultMessage: 'Large', }), }, ]; -export const PartitionChartsMeta: Record = { +export const PartitionChartsMeta: Record = { donut: { icon: LensIconChartDonut, label: i18n.translate('xpack.lens.pie.donutLabel', { defaultMessage: 'Donut', }), - partitionType: PartitionLayout.sunburst, groupLabel, maxBuckets: 3, toolbarPopover: { @@ -164,7 +155,6 @@ export const PartitionChartsMeta: Record = { label: i18n.translate('xpack.lens.pie.pielabel', { defaultMessage: 'Pie', }), - partitionType: PartitionLayout.sunburst, groupLabel, maxBuckets: 3, toolbarPopover: { @@ -180,7 +170,6 @@ export const PartitionChartsMeta: Record = { label: i18n.translate('xpack.lens.pie.treemaplabel', { defaultMessage: 'Treemap', }), - partitionType: PartitionLayout.treemap, groupLabel, maxBuckets: 2, toolbarPopover: { @@ -196,7 +185,6 @@ export const PartitionChartsMeta: Record = { label: i18n.translate('xpack.lens.pie.mosaiclabel', { defaultMessage: 'Mosaic', }), - partitionType: PartitionLayout.mosaic, groupLabel, maxBuckets: 2, isExperimental: true, @@ -208,23 +196,12 @@ export const PartitionChartsMeta: Record = { getShowLegendDefault: () => false, }, requiredMinDimensionCount: 2, - sortPredicate: - (bucketColumns, sortingMap) => - ([name1, node1], [, node2]) => { - // Sorting for first group - if (bucketColumns.length === 1 || (node1.children.length && name1 in sortingMap)) { - return sortingMap[name1]; - } - // Sorting for second group - return node2.value - node1.value; - }, }, waffle: { icon: LensIconChartWaffle, label: i18n.translate('xpack.lens.pie.wafflelabel', { defaultMessage: 'Waffle', }), - partitionType: PartitionLayout.waffle, groupLabel, maxBuckets: 1, isExperimental: true, @@ -239,9 +216,5 @@ export const PartitionChartsMeta: Record = { hideNestedLegendSwitch: true, getShowLegendDefault: () => true, }, - sortPredicate: - () => - ([, node1], [, node2]) => - node2.value - node1.value, }, }; diff --git a/x-pack/plugins/lens/public/pie_visualization/pie_visualization.ts b/x-pack/plugins/lens/public/pie_visualization/pie_visualization.ts index 231b6bacbbe20..78f082b8c0e29 100644 --- a/x-pack/plugins/lens/public/pie_visualization/pie_visualization.ts +++ b/x-pack/plugins/lens/public/pie_visualization/pie_visualization.ts @@ -5,5 +5,4 @@ * 2.0. */ -export * from './expression'; export * from './visualization'; diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx deleted file mode 100644 index 8cd8e4f50d625..0000000000000 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx +++ /dev/null @@ -1,430 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { - Partition, - SeriesIdentifier, - Settings, - NodeColorAccessor, - ShapeTreeNode, - HierarchyOfArrays, - Chart, - PartialTheme, -} from '@elastic/charts'; -import { shallow } from 'enzyme'; -import type { LensMultiTable } from '../../common'; -import type { PieExpressionArgs } from '../../common/expressions'; -import { PieComponent } from './render_function'; -import { VisualizationContainer } from '../visualization_container'; -import { EmptyPlaceholder } from '../../../../../src/plugins/charts/public'; -import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; -import { LensIconChartDonut } from '../assets/chart_donut'; - -const chartsThemeService = chartPluginMock.createSetupContract().theme; - -describe('PieVisualization component', () => { - let getFormatSpy: jest.Mock; - let convertSpy: jest.Mock; - - beforeEach(() => { - convertSpy = jest.fn((x) => x); - getFormatSpy = jest.fn(); - getFormatSpy.mockReturnValue({ convert: convertSpy }); - }); - - describe('legend options', () => { - const data: LensMultiTable = { - type: 'lens_multitable', - tables: { - first: { - type: 'datatable', - columns: [ - { id: 'a', name: 'a', meta: { type: 'number' } }, - { id: 'b', name: 'b', meta: { type: 'string' } }, - { id: 'c', name: 'c', meta: { type: 'number' } }, - ], - rows: [ - { a: 6, b: 'I', c: 2, d: 'Row 1' }, - { a: 1, b: 'J', c: 5, d: 'Row 2' }, - ], - }, - }, - }; - - const args: PieExpressionArgs = { - shape: 'pie', - groups: ['a', 'b'], - metric: 'c', - numberDisplay: 'hidden', - categoryDisplay: 'default', - legendDisplay: 'default', - legendMaxLines: 1, - truncateLegend: true, - nestedLegend: false, - percentDecimals: 3, - hideLabels: false, - palette: { name: 'mock', type: 'palette' }, - }; - - function getDefaultArgs() { - return { - data, - formatFactory: getFormatSpy, - onClickValue: jest.fn(), - chartsThemeService, - paletteService: chartPluginMock.createPaletteRegistry(), - renderMode: 'view' as const, - syncColors: false, - }; - } - - test('it shows legend on correct side', () => { - const component = shallow( - - ); - expect(component.find(Settings).prop('legendPosition')).toEqual('top'); - }); - - test('it shows legend for 2 groups using default legendDisplay', () => { - const component = shallow(); - expect(component.find(Settings).prop('showLegend')).toEqual(true); - }); - - test('it hides legend for 1 group using default legendDisplay', () => { - const component = shallow( - - ); - expect(component.find(Settings).prop('showLegend')).toEqual(false); - }); - - test('it hides legend that would show otherwise in preview mode', () => { - const component = shallow( - - ); - expect(component.find(Settings).prop('showLegend')).toEqual(false); - }); - - test('it sets the correct lines per legend item', () => { - const component = shallow(); - expect(component.find(Settings).prop('theme')[0]).toMatchObject({ - background: { - color: undefined, - }, - legend: { - labelOptions: { - maxLines: 1, - }, - }, - }); - }); - - test('it calls the color function with the right series layers', () => { - const defaultArgs = getDefaultArgs(); - const component = shallow( - - ); - - (component.find(Partition).prop('layers')![1].shape!.fillColor as NodeColorAccessor)( - { - dataName: 'third', - depth: 2, - parent: { - children: [ - ['first', {}], - ['second', {}], - ['third', {}], - ], - depth: 1, - value: 200, - dataName: 'css', - parent: { - children: [ - ['empty', {}], - ['css', {}], - ['gz', {}], - ], - depth: 0, - sortIndex: 0, - value: 500, - }, - sortIndex: 1, - }, - value: 41, - sortIndex: 2, - } as unknown as ShapeTreeNode, - 0, - [] as HierarchyOfArrays - ); - - expect(defaultArgs.paletteService.get('mock').getCategoricalColor).toHaveBeenCalledWith( - [ - { - name: 'css', - rankAtDepth: 1, - totalSeriesAtDepth: 3, - }, - { - name: 'third', - rankAtDepth: 2, - totalSeriesAtDepth: 3, - }, - ], - { - maxDepth: 2, - totalSeries: 5, - syncColors: false, - behindText: true, - }, - undefined - ); - }); - - test('it hides legend with 2 groups for treemap', () => { - const component = shallow( - - ); - expect(component.find(Settings).prop('showLegend')).toEqual(false); - }); - - test('it shows treemap legend only when forced on', () => { - const component = shallow( - - ); - expect(component.find(Settings).prop('showLegend')).toEqual(true); - }); - - test('it defaults to 1-level legend depth', () => { - const component = shallow(); - expect(component.find(Settings).prop('legendMaxDepth')).toEqual(1); - }); - - test('it shows nested legend only when forced on', () => { - const component = shallow( - - ); - expect(component.find(Settings).prop('legendMaxDepth')).toBeUndefined(); - }); - - test('it calls filter callback with the given context', () => { - const defaultArgs = getDefaultArgs(); - const component = shallow(); - component.find(Settings).first().prop('onElementClick')!([ - [ - [ - { - groupByRollup: 6, - value: 6, - depth: 1, - path: [], - sortIndex: 1, - smAccessorValue: '', - }, - ], - {} as SeriesIdentifier, - ], - ]); - - expect(defaultArgs.onClickValue.mock.calls[0][0]).toMatchInlineSnapshot(` - Object { - "data": Array [ - Object { - "column": 0, - "row": 0, - "table": Object { - "columns": Array [ - Object { - "id": "a", - "meta": Object { - "type": "number", - }, - "name": "a", - }, - Object { - "id": "b", - "meta": Object { - "type": "string", - }, - "name": "b", - }, - Object { - "id": "c", - "meta": Object { - "type": "number", - }, - "name": "c", - }, - ], - "rows": Array [ - Object { - "a": 6, - "b": "I", - "c": 2, - "d": "Row 1", - }, - Object { - "a": 1, - "b": "J", - "c": 5, - "d": "Row 2", - }, - ], - "type": "datatable", - }, - "value": 6, - }, - ], - } - `); - }); - - test('does not set click listener and legend actions on non-interactive mode', () => { - const defaultArgs = getDefaultArgs(); - const component = shallow( - - ); - expect(component.find(Settings).first().prop('onElementClick')).toBeUndefined(); - expect(component.find(Settings).first().prop('legendAction')).toBeUndefined(); - }); - - test('it renders the empty placeholder when metric contains only falsy data', () => { - const defaultData = getDefaultArgs().data; - const emptyData: LensMultiTable = { - ...defaultData, - tables: { - first: { - ...defaultData.tables.first, - rows: [ - { a: 0, b: 'I', c: 0, d: 'Row 1' }, - { a: 0, b: 'J', c: null, d: 'Row 2' }, - ], - }, - }, - }; - - const component = shallow( - - ); - expect(component.find(VisualizationContainer)).toHaveLength(1); - expect(component.find(EmptyPlaceholder)).toHaveLength(1); - }); - - test('it renders the chart when metric contains truthy data and buckets contain only falsy data', () => { - const defaultData = getDefaultArgs().data; - const emptyData: LensMultiTable = { - ...defaultData, - tables: { - first: { - ...defaultData.tables.first, - // a and b are buckets, c is a metric - rows: [{ a: 0, b: undefined, c: 12 }], - }, - }, - }; - - const component = shallow( - - ); - - expect(component.find(VisualizationContainer)).toHaveLength(1); - expect(component.find(EmptyPlaceholder)).toHaveLength(0); - expect(component.find(Chart)).toHaveLength(1); - }); - - test('it shows emptyPlaceholder for undefined grouped data', () => { - const defaultData = getDefaultArgs().data; - const emptyData: LensMultiTable = { - ...defaultData, - tables: { - first: { - ...defaultData.tables.first, - rows: [ - { a: undefined, b: 'I', c: undefined, d: 'Row 1' }, - { a: undefined, b: 'J', c: undefined, d: 'Row 2' }, - ], - }, - }, - }; - - const component = shallow( - - ); - expect(component.find(VisualizationContainer)).toHaveLength(1); - expect(component.find(EmptyPlaceholder).prop('icon')).toEqual(LensIconChartDonut); - }); - - test('it should dynamically shrink the chart area to when some small slices are detected', () => { - const defaultData = getDefaultArgs().data; - const emptyData: LensMultiTable = { - ...defaultData, - tables: { - first: { - ...defaultData.tables.first, - rows: [ - { a: 60, b: 'I', c: 200, d: 'Row 1' }, - { a: 1, b: 'J', c: 0.1, d: 'Row 2' }, - ], - }, - }, - }; - - const component = shallow( - - ); - expect( - component.find(Settings).prop('theme')[0].partition?.outerSizeRatio - ).toBeCloseTo(1 / 1.05); - }); - - test('it should bound the shrink the chart area to ~20% when some small slices are detected', () => { - const defaultData = getDefaultArgs().data; - const emptyData: LensMultiTable = { - ...defaultData, - tables: { - first: { - ...defaultData.tables.first, - rows: [ - { a: 60, b: 'I', c: 200, d: 'Row 1' }, - { a: 1, b: 'J', c: 0.1, d: 'Row 2' }, - { a: 1, b: 'K', c: 0.1, d: 'Row 3' }, - { a: 1, b: 'G', c: 0.1, d: 'Row 4' }, - { a: 1, b: 'H', c: 0.1, d: 'Row 5' }, - ], - }, - }, - }; - - const component = shallow( - - ); - expect( - component.find(Settings).prop('theme')[0].partition?.outerSizeRatio - ).toBeCloseTo(1 / 1.2); - }); - }); -}); diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx deleted file mode 100644 index 15706e69d1e16..0000000000000 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ /dev/null @@ -1,354 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { uniq } from 'lodash'; -import React from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { Required } from '@kbn/utility-types'; -import { EuiText } from '@elastic/eui'; -import { - Chart, - Datum, - LayerValue, - Partition, - PartitionLayer, - Position, - Settings, - ElementClickListener, - PartialTheme, -} from '@elastic/charts'; -import { RenderMode } from 'src/plugins/expressions'; -import type { LensFilterEvent } from '../types'; -import { VisualizationContainer } from '../visualization_container'; -import { DEFAULT_PERCENT_DECIMALS } from './constants'; -import { PartitionChartsMeta } from './partition_charts_meta'; -import type { FormatFactory } from '../../common'; -import type { PieExpressionProps } from '../../common/expressions'; -import { - getSliceValue, - getFilterContext, - isTreemapOrMosaicShape, - byDataColorPaletteMap, - extractUniqTermsMap, -} from './render_helpers'; -import { EmptyPlaceholder } from '../../../../../src/plugins/charts/public'; -import './visualization.scss'; -import { - ChartsPluginSetup, - PaletteRegistry, - SeriesLayer, -} from '../../../../../src/plugins/charts/public'; -import { LensIconChartDonut } from '../assets/chart_donut'; -import { getLegendAction } from './get_legend_action'; - -declare global { - interface Window { - /** - * Flag used to enable debugState on elastic charts - */ - _echDebugStateFlag?: boolean; - } -} - -const EMPTY_SLICE = Symbol('empty_slice'); - -export function PieComponent( - props: PieExpressionProps & { - formatFactory: FormatFactory; - chartsThemeService: ChartsPluginSetup['theme']; - interactive?: boolean; - paletteService: PaletteRegistry; - onClickValue: (data: LensFilterEvent['data']) => void; - renderMode: RenderMode; - syncColors: boolean; - } -) { - const [firstTable] = Object.values(props.data.tables); - const formatters: Record> = {}; - - const { chartsThemeService, paletteService, syncColors, onClickValue } = props; - const { - shape, - groups, - metric, - numberDisplay, - categoryDisplay, - legendDisplay, - legendPosition, - nestedLegend, - percentDecimals, - emptySizeRatio, - legendMaxLines, - truncateLegend, - hideLabels, - palette, - showValuesInLegend, - } = props.args; - const chartTheme = chartsThemeService.useChartsTheme(); - const chartBaseTheme = chartsThemeService.useChartsBaseTheme(); - const isDarkMode = chartsThemeService.useDarkMode(); - - if (!hideLabels) { - firstTable.columns.forEach((column) => { - formatters[column.id] = props.formatFactory(column.meta.params); - }); - } - - const fillLabel: PartitionLayer['fillLabel'] = { - valueFont: { - fontWeight: 700, - }, - }; - - if (numberDisplay === 'hidden') { - // Hides numbers from appearing inside chart, but they still appear in linkLabel - // and tooltips. - fillLabel.valueFormatter = () => ''; - } - - const bucketColumns = firstTable.columns.filter((col) => groups.includes(col.id)); - const totalSeriesCount = uniq( - firstTable.rows.map((row) => { - return bucketColumns.map(({ id: columnId }) => row[columnId]).join(','); - }) - ).length; - - const shouldUseByDataPalette = !syncColors && ['mosaic'].includes(shape) && bucketColumns[1]?.id; - let byDataPalette: ReturnType; - if (shouldUseByDataPalette) { - byDataPalette = byDataColorPaletteMap( - firstTable, - bucketColumns[1].id, - paletteService.get(palette.name), - palette - ); - } - - let sortingMap: Record = {}; - if (shape === 'mosaic') { - sortingMap = extractUniqTermsMap(firstTable, bucketColumns[0].id); - } - - const layers: PartitionLayer[] = bucketColumns.map((col, layerIndex) => { - return { - groupByRollup: (d: Datum) => d[col.id] ?? EMPTY_SLICE, - showAccessor: (d: Datum) => d !== EMPTY_SLICE, - nodeLabel: (d: unknown) => { - if (hideLabels || d === EMPTY_SLICE) { - return ''; - } - if (col.meta.params) { - return formatters[col.id].convert(d) ?? ''; - } - return String(d); - }, - fillLabel, - sortPredicate: PartitionChartsMeta[shape].sortPredicate?.(bucketColumns, sortingMap), - shape: { - fillColor: (d) => { - const seriesLayers: SeriesLayer[] = []; - - // Mind the difference here: the contrast computation for the text ignores the alpha/opacity - // therefore change it for dask mode - const defaultColor = isDarkMode ? 'rgba(0,0,0,0)' : 'rgba(255,255,255,0)'; - - // Color is determined by round-robin on the index of the innermost slice - // This has to be done recursively until we get to the slice index - let tempParent: typeof d | typeof d['parent'] = d; - - while (tempParent.parent && tempParent.depth > 0) { - seriesLayers.unshift({ - name: String(tempParent.parent.children[tempParent.sortIndex][0]), - rankAtDepth: tempParent.sortIndex, - totalSeriesAtDepth: tempParent.parent.children.length, - }); - tempParent = tempParent.parent; - } - - if (byDataPalette && seriesLayers[1]) { - return byDataPalette.getColor(seriesLayers[1].name) || defaultColor; - } - - if (isTreemapOrMosaicShape(shape)) { - // Only highlight the innermost color of the treemap, as it accurately represents area - if (layerIndex < bucketColumns.length - 1) { - return defaultColor; - } - // only use the top level series layer for coloring - if (seriesLayers.length > 1) { - seriesLayers.pop(); - } - } - - const outputColor = paletteService.get(palette.name).getCategoricalColor( - seriesLayers, - { - behindText: categoryDisplay !== 'hide' || isTreemapOrMosaicShape(shape), - maxDepth: bucketColumns.length, - totalSeries: totalSeriesCount, - syncColors, - }, - palette.params - ); - - return outputColor || defaultColor; - }, - }, - }; - }); - - const { legend, partitionType, label: chartType } = PartitionChartsMeta[shape]; - - const themeOverrides: Required = { - chartMargins: { top: 0, bottom: 0, left: 0, right: 0 }, - background: { - color: undefined, // removes background for embeddables - }, - legend: { - labelOptions: { maxLines: truncateLegend ? legendMaxLines ?? 1 : 0 }, - }, - partition: { - fontFamily: chartTheme.barSeriesStyle?.displayValue?.fontFamily, - outerSizeRatio: 1, - minFontSize: 10, - maxFontSize: 16, - // Labels are added outside the outer ring when the slice is too small - linkLabel: { - maxCount: 5, - fontSize: 11, - // Dashboard background color is affected by dark mode, which we need - // to account for in outer labels - // This does not handle non-dashboard embeddables, which are allowed to - // have different backgrounds. - textColor: chartTheme.axes?.axisTitle?.fill, - }, - sectorLineStroke: chartTheme.lineSeriesStyle?.point?.fill, - sectorLineWidth: 1.5, - circlePadding: 4, - }, - }; - if (isTreemapOrMosaicShape(shape)) { - if (hideLabels || categoryDisplay === 'hide') { - themeOverrides.partition.fillLabel = { textColor: 'rgba(0,0,0,0)' }; - } - } else { - themeOverrides.partition.emptySizeRatio = shape === 'donut' ? emptySizeRatio : 0; - - if (hideLabels || categoryDisplay === 'hide') { - // Force all labels to be linked, then prevent links from showing - themeOverrides.partition.linkLabel = { - maxCount: 0, - maximumSection: Number.POSITIVE_INFINITY, - }; - } else if (categoryDisplay === 'inside') { - // Prevent links from showing - themeOverrides.partition.linkLabel = { maxCount: 0 }; - } else { - // if it contains any slice below 2% reduce the ratio - // first step: sum it up the overall sum - const overallSum = firstTable.rows.reduce((sum, row) => sum + row[metric!], 0); - const slices = firstTable.rows.map((row) => row[metric!] / overallSum); - const smallSlices = slices.filter((value) => value < 0.02).length; - if (smallSlices) { - // shrink up to 20% to give some room for the linked values - themeOverrides.partition.outerSizeRatio = 1 / (1 + Math.min(smallSlices * 0.05, 0.2)); - } - } - } - const metricColumn = firstTable.columns.find((c) => c.id === metric)!; - const percentFormatter = props.formatFactory({ - id: 'percent', - params: { - pattern: `0,0.[${'0'.repeat(percentDecimals ?? DEFAULT_PERCENT_DECIMALS)}]%`, - }, - }); - - const hasNegative = firstTable.rows.some((row) => { - const value = row[metricColumn.id]; - return typeof value === 'number' && value < 0; - }); - - const isMetricEmpty = firstTable.rows.every((row) => { - return !row[metricColumn.id]; - }); - - const isEmpty = - firstTable.rows.length === 0 || - firstTable.rows.every((row) => groups.every((colId) => typeof row[colId] === 'undefined')) || - isMetricEmpty; - - if (isEmpty) { - return ( - - - - ); - } - - if (hasNegative) { - return ( - - - - ); - } - - const onElementClickHandler: ElementClickListener = (args) => { - const context = getFilterContext(args[0][0] as LayerValue[], groups, firstTable); - - onClickValue(context); - }; - - return ( - - - - - ); -} diff --git a/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts b/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts index bcd9d79babbab..bf09b3f2706e5 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts +++ b/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts @@ -6,321 +6,11 @@ */ import type { Datatable } from 'src/plugins/expressions/public'; -import type { PaletteDefinition, PaletteOutput } from 'src/plugins/charts/public'; -import { - getSliceValue, - getFilterContext, - byDataColorPaletteMap, - extractUniqTermsMap, - checkTableForContainsSmallValues, - shouldShowValuesInLegend, -} from './render_helpers'; -import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; -import type { PieLayerState } from '../../common/expressions'; +import { checkTableForContainsSmallValues, shouldShowValuesInLegend } from './render_helpers'; +import { PieLayerState, PieChartTypes } from '../../common'; describe('render helpers', () => { - describe('#getSliceValue', () => { - it('returns the metric when positive number', () => { - expect( - getSliceValue( - { a: 'Cat', b: 'Home', c: 5 }, - { - id: 'c', - name: 'C', - meta: { type: 'number' }, - } - ) - ).toEqual(5); - }); - - it('returns the metric when negative number', () => { - expect( - getSliceValue( - { a: 'Cat', b: 'Home', c: -100 }, - { - id: 'c', - name: 'C', - meta: { type: 'number' }, - } - ) - ).toEqual(0); - }); - - it('returns 0 when metric value is 0', () => { - expect( - getSliceValue( - { a: 'Cat', b: 'Home', c: 0 }, - { - id: 'c', - name: 'C', - meta: { type: 'number' }, - } - ) - ).toEqual(0); - }); - - it('returns 0 when metric value is infinite', () => { - expect( - getSliceValue( - { a: 'Cat', b: 'Home', c: Number.POSITIVE_INFINITY }, - { - id: 'c', - name: 'C', - meta: { type: 'number' }, - } - ) - ).toEqual(0); - }); - }); - - describe('#getFilterContext', () => { - it('handles single slice click for single ring', () => { - const table: Datatable = { - type: 'datatable', - columns: [ - { id: 'a', name: 'A', meta: { type: 'string' } }, - { id: 'b', name: 'B', meta: { type: 'number' } }, - ], - rows: [ - { a: 'Hi', b: 2 }, - { a: 'Test', b: 4 }, - { a: 'Foo', b: 6 }, - ], - }; - expect( - getFilterContext( - [ - { - groupByRollup: 'Test', - value: 100, - depth: 1, - path: [], - sortIndex: 1, - smAccessorValue: '', - }, - ], - ['a'], - table - ) - ).toEqual({ - data: [ - { - row: 1, - column: 0, - value: 'Test', - table, - }, - ], - }); - }); - - it('handles single slice click with 2 rings', () => { - const table: Datatable = { - type: 'datatable', - columns: [ - { id: 'a', name: 'A', meta: { type: 'string' } }, - { id: 'b', name: 'B', meta: { type: 'string' } }, - { id: 'c', name: 'C', meta: { type: 'number' } }, - ], - rows: [ - { a: 'Hi', b: 'Two', c: 2 }, - { a: 'Test', b: 'Two', c: 5 }, - { a: 'Foo', b: 'Three', c: 6 }, - ], - }; - expect( - getFilterContext( - [ - { - groupByRollup: 'Test', - value: 100, - depth: 1, - path: [], - sortIndex: 1, - smAccessorValue: '', - }, - ], - ['a', 'b'], - table - ) - ).toEqual({ - data: [ - { - row: 1, - column: 0, - value: 'Test', - table, - }, - ], - }); - }); - - it('finds right row for multi slice click', () => { - const table: Datatable = { - type: 'datatable', - columns: [ - { id: 'a', name: 'A', meta: { type: 'string' } }, - { id: 'b', name: 'B', meta: { type: 'string' } }, - { id: 'c', name: 'C', meta: { type: 'number' } }, - ], - rows: [ - { a: 'Hi', b: 'Two', c: 2 }, - { a: 'Test', b: 'Two', c: 5 }, - { a: 'Foo', b: 'Three', c: 6 }, - ], - }; - expect( - getFilterContext( - [ - { - groupByRollup: 'Test', - value: 100, - depth: 1, - path: [], - sortIndex: 1, - smAccessorValue: '', - }, - { - groupByRollup: 'Two', - value: 5, - depth: 1, - path: [], - sortIndex: 1, - smAccessorValue: '', - }, - ], - ['a', 'b'], - table - ) - ).toEqual({ - data: [ - { - row: 1, - column: 0, - value: 'Test', - table, - }, - { - row: 1, - column: 1, - value: 'Two', - table, - }, - ], - }); - }); - }); - - describe('#extractUniqTermsMap', () => { - it('should extract map', () => { - const table: Datatable = { - type: 'datatable', - columns: [ - { id: 'a', name: 'A', meta: { type: 'string' } }, - { id: 'b', name: 'B', meta: { type: 'string' } }, - { id: 'c', name: 'C', meta: { type: 'number' } }, - ], - rows: [ - { a: 'Hi', b: 'Two', c: 2 }, - { a: 'Test', b: 'Two', c: 5 }, - { a: 'Foo', b: 'Three', c: 6 }, - ], - }; - expect(extractUniqTermsMap(table, 'a')).toMatchInlineSnapshot(` - Object { - "Foo": 2, - "Hi": 0, - "Test": 1, - } - `); - expect(extractUniqTermsMap(table, 'b')).toMatchInlineSnapshot(` - Object { - "Three": 1, - "Two": 0, - } - `); - }); - }); - - describe('#byDataColorPaletteMap', () => { - let datatable: Datatable; - let paletteDefinition: PaletteDefinition; - let palette: PaletteOutput; - const columnId = 'foo'; - - beforeEach(() => { - datatable = { - rows: [ - { - [columnId]: '1', - }, - { - [columnId]: '2', - }, - ], - } as unknown as Datatable; - paletteDefinition = chartPluginMock.createPaletteRegistry().get('default'); - palette = { type: 'palette' } as PaletteOutput; - }); - - it('should create byDataColorPaletteMap', () => { - expect(byDataColorPaletteMap(datatable, columnId, paletteDefinition, palette)) - .toMatchInlineSnapshot(` - Object { - "getColor": [Function], - } - `); - }); - - it('should get color', () => { - const colorPaletteMap = byDataColorPaletteMap( - datatable, - columnId, - paletteDefinition, - palette - ); - - expect(colorPaletteMap.getColor('1')).toBe('black'); - }); - - it('should return undefined in case if values not in datatable', () => { - const colorPaletteMap = byDataColorPaletteMap( - datatable, - columnId, - paletteDefinition, - palette - ); - - expect(colorPaletteMap.getColor('wrong')).toBeUndefined(); - }); - - it('should increase rankAtDepth for each new value', () => { - const colorPaletteMap = byDataColorPaletteMap( - datatable, - columnId, - paletteDefinition, - palette - ); - colorPaletteMap.getColor('1'); - colorPaletteMap.getColor('2'); - - expect(paletteDefinition.getCategoricalColor).toHaveBeenNthCalledWith( - 1, - [{ name: '1', rankAtDepth: 0, totalSeriesAtDepth: 2 }], - { behindText: false }, - undefined - ); - - expect(paletteDefinition.getCategoricalColor).toHaveBeenNthCalledWith( - 2, - [{ name: '2', rankAtDepth: 1, totalSeriesAtDepth: 2 }], - { behindText: false }, - undefined - ); - }); - }); - describe('#checkTableForContainsSmallValues', () => { let datatable: Datatable; const columnId = 'foo'; @@ -380,23 +70,35 @@ describe('render helpers', () => { describe('#shouldShowValuesInLegend', () => { it('should firstly read the state value', () => { expect( - shouldShowValuesInLegend({ showValuesInLegend: true } as PieLayerState, 'waffle') + shouldShowValuesInLegend( + { showValuesInLegend: true } as PieLayerState, + PieChartTypes.WAFFLE + ) ).toBeTruthy(); expect( - shouldShowValuesInLegend({ showValuesInLegend: false } as PieLayerState, 'waffle') + shouldShowValuesInLegend( + { showValuesInLegend: false } as PieLayerState, + PieChartTypes.WAFFLE + ) ).toBeFalsy(); }); it('should read value from meta in case of value in state is undefined', () => { expect( - shouldShowValuesInLegend({ showValuesInLegend: undefined } as PieLayerState, 'waffle') + shouldShowValuesInLegend( + { showValuesInLegend: undefined } as PieLayerState, + PieChartTypes.WAFFLE + ) ).toBeTruthy(); - expect(shouldShowValuesInLegend({} as PieLayerState, 'waffle')).toBeTruthy(); + expect(shouldShowValuesInLegend({} as PieLayerState, PieChartTypes.WAFFLE)).toBeTruthy(); expect( - shouldShowValuesInLegend({ showValuesInLegend: undefined } as PieLayerState, 'pie') + shouldShowValuesInLegend( + { showValuesInLegend: undefined } as PieLayerState, + PieChartTypes.PIE + ) ).toBeFalsy(); }); }); diff --git a/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts b/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts index a9685e13e1774..1f6d40abc32ec 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts +++ b/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts @@ -5,47 +5,14 @@ * 2.0. */ -import type { Datum, LayerValue } from '@elastic/charts'; -import type { Datatable, DatatableColumn } from 'src/plugins/expressions/public'; -import type { LensFilterEvent } from '../types'; -import type { PieChartTypes, PieLayerState } from '../../common/expressions/pie_chart/types'; -import type { PaletteDefinition, PaletteOutput } from '../../../../../src/plugins/charts/public'; +import type { Datatable } from 'src/plugins/expressions/public'; +import type { PieChartType, PieLayerState } from '../../common/types'; import { PartitionChartsMeta } from './partition_charts_meta'; -export function getSliceValue(d: Datum, metricColumn: DatatableColumn) { - const value = d[metricColumn.id]; - return Number.isFinite(value) && value >= 0 ? value : 0; -} - -export function getFilterContext( - clickedLayers: LayerValue[], - layerColumnIds: string[], - table: Datatable -): LensFilterEvent['data'] { - const matchingIndex = table.rows.findIndex((row) => - clickedLayers.every((layer, index) => { - const columnId = layerColumnIds[index]; - return row[columnId] === layer.groupByRollup; - }) - ); - - return { - data: clickedLayers.map((clickedLayer, index) => ({ - column: table.columns.findIndex((col) => col.id === layerColumnIds[index]), - row: matchingIndex, - value: clickedLayer.groupByRollup, - table, - })), - }; -} - -export const isPartitionShape = (shape: PieChartTypes | string) => +export const isPartitionShape = (shape: PieChartType | string) => ['donut', 'pie', 'treemap', 'mosaic', 'waffle'].includes(shape); -export const isTreemapOrMosaicShape = (shape: PieChartTypes | string) => - ['treemap', 'mosaic'].includes(shape); - -export const shouldShowValuesInLegend = (layer: PieLayerState, shape: PieChartTypes) => { +export const shouldShowValuesInLegend = (layer: PieLayerState, shape: PieChartType) => { if ('showValues' in PartitionChartsMeta[shape]?.legend) { return layer.showValuesInLegend ?? PartitionChartsMeta[shape]?.legend?.showValues ?? true; } @@ -53,58 +20,6 @@ export const shouldShowValuesInLegend = (layer: PieLayerState, shape: PieChartTy return false; }; -export const extractUniqTermsMap = (dataTable: Datatable, columnId: string) => - [...new Set(dataTable.rows.map((item) => item[columnId]))].reduce( - (acc, item, index) => ({ - ...acc, - [item]: index, - }), - {} - ); - -export const byDataColorPaletteMap = ( - dataTable: Datatable, - columnId: string, - paletteDefinition: PaletteDefinition, - { params }: PaletteOutput -) => { - const colorMap = new Map( - dataTable.rows.map((item) => [String(item[columnId]), undefined]) - ); - let rankAtDepth = 0; - - return { - getColor: (item: unknown) => { - const key = String(item); - - if (colorMap.has(key)) { - let color = colorMap.get(key); - - if (color) { - return color; - } - color = - paletteDefinition.getCategoricalColor( - [ - { - name: key, - totalSeriesAtDepth: colorMap.size, - rankAtDepth: rankAtDepth++, - }, - ], - { - behindText: false, - }, - params - ) || undefined; - - colorMap.set(key, color); - return color; - } - }, - }; -}; - export const checkTableForContainsSmallValues = ( dataTable: Datatable, columnId: string, diff --git a/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts b/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts index 229ef9b387ac0..f951d4f07e865 100644 --- a/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts +++ b/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts @@ -8,7 +8,14 @@ import { PaletteOutput } from 'src/plugins/charts/public'; import { suggestions } from './suggestions'; import type { DataType, SuggestionRequest } from '../types'; -import type { PieLayerState, PieVisualizationState } from '../../common/expressions'; +import { + CategoryDisplay, + LegendDisplay, + NumberDisplay, + PieChartTypes, + PieLayerState, + PieVisualizationState, +} from '../../common'; import { layerTypes } from '../../common'; describe('suggestions', () => { @@ -53,16 +60,16 @@ describe('suggestions', () => { changeType: 'unchanged', }, state: { - shape: 'pie', + shape: PieChartTypes.PIE, layers: [ { layerId: 'first', layerType: layerTypes.DATA, groups: [], metric: 'a', - numberDisplay: 'hidden', - categoryDisplay: 'default', - legendDisplay: 'default', + numberDisplay: NumberDisplay.HIDDEN, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, }, ], }, @@ -168,7 +175,7 @@ describe('suggestions', () => { changeType: 'initial', }, state: { - shape: 'mosaic', + shape: PieChartTypes.MOSAIC, layers: [{} as PieLayerState], }, keptLayerIds: ['first'], @@ -380,7 +387,7 @@ describe('suggestions', () => { expect(results).toContainEqual( expect.objectContaining({ - state: expect.objectContaining({ shape: 'donut' }), + state: expect.objectContaining({ shape: PieChartTypes.DONUT }), }) ); }); @@ -412,7 +419,7 @@ describe('suggestions', () => { expect(results).toContainEqual( expect.objectContaining({ - state: expect.objectContaining({ shape: 'pie' }), + state: expect.objectContaining({ shape: PieChartTypes.PIE }), }) ); }); @@ -542,7 +549,7 @@ describe('suggestions', () => { changeType: 'unchanged', }, state: { - shape: 'treemap', + shape: PieChartTypes.TREEMAP, palette, layers: [ { @@ -551,9 +558,9 @@ describe('suggestions', () => { groups: ['a'], metric: 'b', - numberDisplay: 'hidden', - categoryDisplay: 'inside', - legendDisplay: 'show', + numberDisplay: NumberDisplay.HIDDEN, + categoryDisplay: CategoryDisplay.INSIDE, + legendDisplay: LegendDisplay.SHOW, percentDecimals: 0, legendMaxLines: 1, truncateLegend: true, @@ -566,7 +573,7 @@ describe('suggestions', () => { ).toContainEqual( expect.objectContaining({ state: { - shape: 'donut', + shape: PieChartTypes.DONUT, palette, layers: [ { @@ -575,8 +582,8 @@ describe('suggestions', () => { groups: ['a'], metric: 'b', - numberDisplay: 'hidden', - categoryDisplay: 'inside', + numberDisplay: NumberDisplay.HIDDEN, + categoryDisplay: CategoryDisplay.INSIDE, legendDisplay: 'show', percentDecimals: 0, legendMaxLines: 1, @@ -601,7 +608,7 @@ describe('suggestions', () => { changeType: 'unchanged', }, state: { - shape: 'treemap', + shape: PieChartTypes.TREEMAP, layers: [ { layerId: 'first', @@ -609,9 +616,9 @@ describe('suggestions', () => { groups: [], metric: 'a', - numberDisplay: 'hidden', - categoryDisplay: 'default', - legendDisplay: 'default', + numberDisplay: NumberDisplay.HIDDEN, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, }, ], }, @@ -651,16 +658,16 @@ describe('suggestions', () => { changeType: 'extended', }, state: { - shape: 'treemap', + shape: PieChartTypes.TREEMAP, layers: [ { layerId: 'first', layerType: layerTypes.DATA, groups: ['a', 'b'], metric: 'e', - numberDisplay: 'value', - categoryDisplay: 'default', - legendDisplay: 'default', + numberDisplay: NumberDisplay.VALUE, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, }, ], }, @@ -700,16 +707,16 @@ describe('suggestions', () => { changeType: 'initial', }, state: { - shape: 'treemap', + shape: PieChartTypes.TREEMAP, layers: [ { layerId: 'first', layerType: layerTypes.DATA, groups: ['a', 'b'], metric: 'e', - numberDisplay: 'percent', - categoryDisplay: 'default', - legendDisplay: 'default', + numberDisplay: NumberDisplay.PERCENT, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, }, ], }, @@ -737,7 +744,7 @@ describe('suggestions', () => { changeType: 'unchanged', }, state: { - shape: 'pie', + shape: PieChartTypes.PIE, layers: [ { layerId: 'first', @@ -745,9 +752,9 @@ describe('suggestions', () => { groups: ['a'], metric: 'b', - numberDisplay: 'hidden', - categoryDisplay: 'inside', - legendDisplay: 'show', + numberDisplay: NumberDisplay.HIDDEN, + categoryDisplay: CategoryDisplay.INSIDE, + legendDisplay: LegendDisplay.SHOW, percentDecimals: 0, legendMaxLines: 1, truncateLegend: true, @@ -760,7 +767,7 @@ describe('suggestions', () => { ).toContainEqual( expect.objectContaining({ state: { - shape: 'treemap', + shape: PieChartTypes.TREEMAP, layers: [ { layerId: 'first', @@ -768,8 +775,8 @@ describe('suggestions', () => { groups: ['a'], metric: 'b', - numberDisplay: 'hidden', - categoryDisplay: 'default', // This is changed + numberDisplay: NumberDisplay.HIDDEN, + categoryDisplay: CategoryDisplay.DEFAULT, // This is changed legendDisplay: 'show', percentDecimals: 0, legendMaxLines: 1, @@ -794,7 +801,7 @@ describe('suggestions', () => { changeType: 'unchanged', }, state: { - shape: 'mosaic', + shape: PieChartTypes.MOSAIC, layers: [ { layerId: 'first', @@ -802,9 +809,9 @@ describe('suggestions', () => { groups: [], metric: 'a', - numberDisplay: 'hidden', - categoryDisplay: 'default', - legendDisplay: 'default', + numberDisplay: NumberDisplay.HIDDEN, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, }, ], }, @@ -836,7 +843,7 @@ describe('suggestions', () => { changeType: 'unchanged', }, state: { - shape: 'treemap', + shape: PieChartTypes.TREEMAP, layers: [ { layerId: 'first', @@ -844,9 +851,9 @@ describe('suggestions', () => { groups: ['a', 'b'], metric: 'c', - numberDisplay: 'hidden', - categoryDisplay: 'inside', - legendDisplay: 'show', + numberDisplay: NumberDisplay.HIDDEN, + categoryDisplay: CategoryDisplay.INSIDE, + legendDisplay: LegendDisplay.SHOW, percentDecimals: 0, legendMaxLines: 1, truncateLegend: true, @@ -871,7 +878,7 @@ describe('suggestions', () => { changeType: 'unchanged', }, state: { - shape: 'waffle', + shape: PieChartTypes.WAFFLE, layers: [ { layerId: 'first', @@ -879,9 +886,9 @@ describe('suggestions', () => { groups: [], metric: 'a', - numberDisplay: 'hidden', - categoryDisplay: 'default', - legendDisplay: 'default', + numberDisplay: NumberDisplay.HIDDEN, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, }, ], }, @@ -909,16 +916,16 @@ describe('suggestions', () => { changeType: 'unchanged', }, state: { - shape: 'pie', + shape: PieChartTypes.PIE, layers: [ { layerId: 'first', layerType: layerTypes.DATA, groups: ['a', 'b'], metric: 'c', - numberDisplay: 'hidden', - categoryDisplay: 'inside', - legendDisplay: 'show', + numberDisplay: NumberDisplay.HIDDEN, + categoryDisplay: CategoryDisplay.INSIDE, + legendDisplay: LegendDisplay.SHOW, percentDecimals: 0, legendMaxLines: 1, truncateLegend: true, diff --git a/x-pack/plugins/lens/public/pie_visualization/suggestions.ts b/x-pack/plugins/lens/public/pie_visualization/suggestions.ts index dd42dd6474e0b..0ff75ee823d42 100644 --- a/x-pack/plugins/lens/public/pie_visualization/suggestions.ts +++ b/x-pack/plugins/lens/public/pie_visualization/suggestions.ts @@ -8,11 +8,17 @@ import { partition } from 'lodash'; import { i18n } from '@kbn/i18n'; import type { SuggestionRequest, TableSuggestionColumn, VisualizationSuggestion } from '../types'; -import { layerTypes } from '../../common'; -import type { PieVisualizationState } from '../../common/expressions'; +import { + CategoryDisplay, + layerTypes, + LegendDisplay, + NumberDisplay, + PieChartTypes, + PieVisualizationState, +} from '../../common'; +import type { PieChartType } from '../../common/types'; import { PartitionChartsMeta } from './partition_charts_meta'; import { isPartitionShape } from './render_helpers'; -import { PieChartTypes } from '../../common/expressions/pie_chart/types'; function hasIntervalScale(columns: TableSuggestionColumn[]) { return columns.some((col) => col.operation.scale === 'interval'); @@ -43,14 +49,19 @@ function getNewShape( let newShape: PieVisualizationState['shape'] | undefined; if (groups.length !== 1 && !subVisualizationId) { - newShape = 'pie'; + newShape = PieChartTypes.PIE; } - return newShape ?? 'donut'; + return newShape ?? PieChartTypes.DONUT; } -function hasCustomSuggestionsExists(shape: PieChartTypes | string | undefined) { - return shape ? ['treemap', 'waffle', 'mosaic'].includes(shape) : false; +function hasCustomSuggestionsExists(shape: PieChartType | string | undefined) { + const shapes: Array = [ + PieChartTypes.TREEMAP, + PieChartTypes.WAFFLE, + PieChartTypes.MOSAIC, + ]; + return shape ? shapes.includes(shape) : false; } const maximumGroupLength = Math.max( @@ -116,9 +127,9 @@ export function suggestions({ layerId: table.layerId, groups: groups.map((col) => col.columnId), metric: metricColumnId, - numberDisplay: 'percent', - categoryDisplay: 'default', - legendDisplay: 'default', + numberDisplay: NumberDisplay.PERCENT, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, layerType: layerTypes.DATA, }, @@ -137,13 +148,18 @@ export function suggestions({ ...baseSuggestion, title: i18n.translate('xpack.lens.pie.suggestionLabel', { defaultMessage: 'As {chartName}', - values: { chartName: PartitionChartsMeta[newShape === 'pie' ? 'donut' : 'pie'].label }, + values: { + chartName: + PartitionChartsMeta[ + newShape === PieChartTypes.PIE ? PieChartTypes.DONUT : PieChartTypes.PIE + ].label, + }, description: 'chartName is already translated', }), score: 0.1, state: { ...baseSuggestion.state, - shape: newShape === 'pie' ? 'donut' : 'pie', + shape: newShape === PieChartTypes.PIE ? PieChartTypes.DONUT : PieChartTypes.PIE, }, hide: true, }); @@ -159,9 +175,9 @@ export function suggestions({ }), // Use a higher score when currently active, to prevent chart type switching // on the user unintentionally - score: state?.shape === 'treemap' ? 0.7 : 0.5, + score: state?.shape === PieChartTypes.TREEMAP ? 0.7 : 0.5, state: { - shape: 'treemap', + shape: PieChartTypes.TREEMAP, palette: mainPalette || state?.palette, layers: [ state?.layers[0] @@ -171,8 +187,8 @@ export function suggestions({ groups: groups.map((col) => col.columnId), metric: metricColumnId, categoryDisplay: - state.layers[0].categoryDisplay === 'inside' - ? 'default' + state.layers[0].categoryDisplay === CategoryDisplay.INSIDE + ? CategoryDisplay.DEFAULT : state.layers[0].categoryDisplay, layerType: layerTypes.DATA, } @@ -180,9 +196,9 @@ export function suggestions({ layerId: table.layerId, groups: groups.map((col) => col.columnId), metric: metricColumnId, - numberDisplay: 'percent', - categoryDisplay: 'default', - legendDisplay: 'default', + numberDisplay: NumberDisplay.PERCENT, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, layerType: layerTypes.DATA, }, @@ -194,21 +210,21 @@ export function suggestions({ table.changeType === 'reduced' || !state || hasIntervalScale(groups) || - (state && state.shape === 'treemap'), + (state && state.shape === PieChartTypes.TREEMAP), }); } if ( groups.length <= PartitionChartsMeta.mosaic.maxBuckets && - (!subVisualizationId || subVisualizationId === 'mosaic') + (!subVisualizationId || subVisualizationId === PieChartTypes.MOSAIC) ) { results.push({ title: i18n.translate('xpack.lens.pie.mosaicSuggestionLabel', { defaultMessage: 'As Mosaic', }), - score: state?.shape === 'mosaic' ? 0.7 : 0.5, + score: state?.shape === PieChartTypes.MOSAIC ? 0.7 : 0.5, state: { - shape: 'mosaic', + shape: PieChartTypes.MOSAIC, palette: mainPalette || state?.palette, layers: [ state?.layers[0] @@ -217,16 +233,16 @@ export function suggestions({ layerId: table.layerId, groups: groups.map((col) => col.columnId), metric: metricColumnId, - categoryDisplay: 'default', + categoryDisplay: CategoryDisplay.DEFAULT, layerType: layerTypes.DATA, } : { layerId: table.layerId, groups: groups.map((col) => col.columnId), metric: metricColumnId, - numberDisplay: 'percent', - categoryDisplay: 'default', - legendDisplay: 'default', + numberDisplay: NumberDisplay.PERCENT, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, layerType: layerTypes.DATA, }, @@ -239,15 +255,15 @@ export function suggestions({ if ( groups.length <= PartitionChartsMeta.waffle.maxBuckets && - (!subVisualizationId || subVisualizationId === 'waffle') + (!subVisualizationId || subVisualizationId === PieChartTypes.WAFFLE) ) { results.push({ title: i18n.translate('xpack.lens.pie.waffleSuggestionLabel', { defaultMessage: 'As Waffle', }), - score: state?.shape === 'waffle' ? 0.7 : 0.5, + score: state?.shape === PieChartTypes.WAFFLE ? 0.7 : 0.5, state: { - shape: 'waffle', + shape: PieChartTypes.WAFFLE, palette: mainPalette || state?.palette, layers: [ state?.layers[0] @@ -256,16 +272,16 @@ export function suggestions({ layerId: table.layerId, groups: groups.map((col) => col.columnId), metric: metricColumnId, - categoryDisplay: 'default', + categoryDisplay: CategoryDisplay.DEFAULT, layerType: layerTypes.DATA, } : { layerId: table.layerId, groups: groups.map((col) => col.columnId), metric: metricColumnId, - numberDisplay: 'percent', - categoryDisplay: 'default', - legendDisplay: 'default', + numberDisplay: NumberDisplay.PERCENT, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, layerType: layerTypes.DATA, }, diff --git a/x-pack/plugins/lens/public/pie_visualization/to_expression.ts b/x-pack/plugins/lens/public/pie_visualization/to_expression.ts index f703b1b5f419b..9ae9f4ac0cae4 100644 --- a/x-pack/plugins/lens/public/pie_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/pie_visualization/to_expression.ts @@ -6,13 +6,62 @@ */ import type { Ast } from '@kbn/interpreter'; -import type { PaletteRegistry } from 'src/plugins/charts/public'; +import { Position } from '@elastic/charts'; + +import type { PaletteOutput, PaletteRegistry } from '../../../../../src/plugins/charts/public'; +import { + buildExpression, + buildExpressionFunction, +} from '../../../../../src/plugins/expressions/public'; import type { Operation, DatasourcePublicAPI } from '../types'; -import { DEFAULT_PERCENT_DECIMALS, EMPTY_SIZE_RATIOS } from './constants'; +import { DEFAULT_PERCENT_DECIMALS } from './constants'; import { shouldShowValuesInLegend } from './render_helpers'; -import type { PieLayerState, PieVisualizationState } from '../../common/expressions'; +import { + CategoryDisplay, + NumberDisplay, + PieChartTypes, + PieLayerState, + PieVisualizationState, + EmptySizeRatios, + LegendDisplay, +} from '../../common'; import { getDefaultVisualValuesForLayer } from '../shared_components/datasource_default_values'; +interface Attributes { + isPreview: boolean; + title?: string; + description?: string; +} + +interface OperationColumnId { + columnId: string; + operation: Operation; +} + +type GenerateExpressionAstFunction = ( + state: PieVisualizationState, + attributes: Attributes, + operations: OperationColumnId[], + layer: PieLayerState, + datasourceLayers: Record, + paletteService: PaletteRegistry +) => Ast | null; + +type GenerateExpressionAstArguments = ( + state: PieVisualizationState, + attributes: Attributes, + operations: OperationColumnId[], + layer: PieLayerState, + datasourceLayers: Record, + paletteService: PaletteRegistry +) => Ast['chain'][number]['arguments']; + +type GenerateLabelsAstArguments = ( + state: PieVisualizationState, + attributes: Attributes, + layer: PieLayerState +) => [Ast]; + export const getSortedGroups = (datasource: DatasourcePublicAPI, layer: PieLayerState) => { const originalOrder = datasource .getTableSpec() @@ -22,23 +71,183 @@ export const getSortedGroups = (datasource: DatasourcePublicAPI, layer: PieLayer return Array.from(new Set(originalOrder.concat(layer.groups))); }; -export function toExpression( - state: PieVisualizationState, - datasourceLayers: Record, +const prepareDimension = (accessor: string) => { + const visdimension = buildExpressionFunction('visdimension', { accessor }); + return buildExpression([visdimension]).toAst(); +}; + +const generateCommonLabelsAstArgs: GenerateLabelsAstArguments = (state, attributes, layer) => { + const show = [!attributes.isPreview && layer.categoryDisplay !== CategoryDisplay.HIDE]; + const position = layer.categoryDisplay !== CategoryDisplay.HIDE ? [layer.categoryDisplay] : []; + const values = [layer.numberDisplay !== NumberDisplay.HIDDEN]; + const valuesFormat = layer.numberDisplay !== NumberDisplay.HIDDEN ? [layer.numberDisplay] : []; + const percentDecimals = [layer.percentDecimals ?? DEFAULT_PERCENT_DECIMALS]; + + return [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'partitionLabels', + arguments: { show, position, values, valuesFormat, percentDecimals }, + }, + ], + }, + ]; +}; + +const generateWaffleLabelsAstArguments: GenerateLabelsAstArguments = (...args) => { + const [labelsExpr] = generateCommonLabelsAstArgs(...args); + const [labels] = labelsExpr.chain; + return [ + { + ...labelsExpr, + chain: [{ ...labels, percentDecimals: DEFAULT_PERCENT_DECIMALS }], + }, + ]; +}; + +const generatePaletteAstArguments = ( paletteService: PaletteRegistry, - attributes: Partial<{ title: string; description: string }> = {} -) { - return expressionHelper(state, datasourceLayers, paletteService, { - ...attributes, - isPreview: false, - }); -} + palette?: PaletteOutput +): [Ast] => + palette + ? [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'theme', + arguments: { + variable: ['palette'], + default: [paletteService.get(palette.name).toExpression(palette.params)], + }, + }, + ], + }, + ] + : [paletteService.get('default').toExpression()]; + +const generateCommonArguments: GenerateExpressionAstArguments = ( + state, + attributes, + operations, + layer, + datasourceLayers, + paletteService +) => ({ + labels: generateCommonLabelsAstArgs(state, attributes, layer), + buckets: operations.map((o) => o.columnId).map(prepareDimension), + metric: layer.metric ? [prepareDimension(layer.metric)] : [], + legendDisplay: [attributes.isPreview ? LegendDisplay.HIDE : layer.legendDisplay], + legendPosition: [layer.legendPosition || Position.Right], + maxLegendLines: [layer.legendMaxLines ?? 1], + nestedLegend: [!!layer.nestedLegend], + truncateLegend: [ + layer.truncateLegend ?? getDefaultVisualValuesForLayer(state, datasourceLayers).truncateText, + ], + palette: generatePaletteAstArguments(paletteService, state.palette), +}); + +const generatePieVisAst: GenerateExpressionAstFunction = (...rest) => ({ + type: 'expression', + chain: [ + { + type: 'function', + function: 'pieVis', + arguments: { + ...generateCommonArguments(...rest), + respectSourceOrder: [false], + startFromSecondLargestSlice: [true], + }, + }, + ], +}); + +const generateDonutVisAst: GenerateExpressionAstFunction = (...rest) => { + const [, , , layer] = rest; + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'pieVis', + arguments: { + ...generateCommonArguments(...rest), + respectSourceOrder: [false], + isDonut: [true], + startFromSecondLargestSlice: [true], + emptySizeRatio: [layer.emptySizeRatio ?? EmptySizeRatios.SMALL], + }, + }, + ], + }; +}; + +const generateTreemapVisAst: GenerateExpressionAstFunction = (...rest) => { + const [, , , layer] = rest; + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'treemapVis', + arguments: { + ...generateCommonArguments(...rest), + nestedLegend: [!!layer.nestedLegend], + }, + }, + ], + }; +}; + +const generateMosaicVisAst: GenerateExpressionAstFunction = (...rest) => ({ + type: 'expression', + chain: [ + { + type: 'function', + function: 'mosaicVis', + arguments: generateCommonArguments(...rest), + }, + ], +}); + +const generateWaffleVisAst: GenerateExpressionAstFunction = (...rest) => { + const { buckets, nestedLegend, ...args } = generateCommonArguments(...rest); + const [state, attributes, , layer] = rest; + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'waffleVis', + arguments: { + ...args, + bucket: buckets, + labels: generateWaffleLabelsAstArguments(state, attributes, layer), + showValuesInLegend: [shouldShowValuesInLegend(layer, state.shape)], + }, + }, + ], + }; +}; + +const generateExprAst: GenerateExpressionAstFunction = (state, ...restArgs) => + ({ + [PieChartTypes.PIE]: () => generatePieVisAst(state, ...restArgs), + [PieChartTypes.DONUT]: () => generateDonutVisAst(state, ...restArgs), + [PieChartTypes.TREEMAP]: () => generateTreemapVisAst(state, ...restArgs), + [PieChartTypes.MOSAIC]: () => generateMosaicVisAst(state, ...restArgs), + [PieChartTypes.WAFFLE]: () => generateWaffleVisAst(state, ...restArgs), + }[state.shape]()); function expressionHelper( state: PieVisualizationState, datasourceLayers: Record, paletteService: PaletteRegistry, - attributes: { isPreview: boolean; title?: string; description?: string } = { isPreview: false } + attributes: Attributes = { isPreview: false } ): Ast | null { const layer = state.layers[0]; const datasource = datasourceLayers[layer.layerId]; @@ -51,63 +260,20 @@ function expressionHelper( if (!layer.metric || !operations.length) { return null; } - return { - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_pie', - arguments: { - title: [attributes.title || ''], - description: [attributes.description || ''], - shape: [state.shape], - hideLabels: [attributes.isPreview], - groups: operations.map((o) => o.columnId), - metric: [layer.metric], - numberDisplay: [layer.numberDisplay], - categoryDisplay: [layer.categoryDisplay], - legendDisplay: [layer.legendDisplay], - legendPosition: [layer.legendPosition || 'right'], - emptySizeRatio: [layer.emptySizeRatio ?? EMPTY_SIZE_RATIOS.SMALL], - showValuesInLegend: [shouldShowValuesInLegend(layer, state.shape)], - percentDecimals: [ - state.shape === 'waffle' - ? DEFAULT_PERCENT_DECIMALS - : layer.percentDecimals ?? DEFAULT_PERCENT_DECIMALS, - ], - legendMaxLines: [layer.legendMaxLines ?? 1], - truncateLegend: [ - layer.truncateLegend ?? - getDefaultVisualValuesForLayer(state, datasourceLayers).truncateText, - ], - nestedLegend: [!!layer.nestedLegend], - ...(state.palette - ? { - palette: [ - { - type: 'expression', - chain: [ - { - type: 'function', - function: 'theme', - arguments: { - variable: ['palette'], - default: [ - paletteService - .get(state.palette.name) - .toExpression(state.palette.params), - ], - }, - }, - ], - }, - ], - } - : {}), - }, - }, - ], - }; + + return generateExprAst(state, attributes, operations, layer, datasourceLayers, paletteService); +} + +export function toExpression( + state: PieVisualizationState, + datasourceLayers: Record, + paletteService: PaletteRegistry, + attributes: Partial<{ title: string; description: string }> = {} +) { + return expressionHelper(state, datasourceLayers, paletteService, { + ...attributes, + isPreview: false, + }); } export function toPreviewExpression( diff --git a/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx b/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx index cebacd5c95863..f188aa12069d7 100644 --- a/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx @@ -20,7 +20,7 @@ import type { Position } from '@elastic/charts'; import type { PaletteRegistry } from 'src/plugins/charts/public'; import { DEFAULT_PERCENT_DECIMALS } from './constants'; import { PartitionChartsMeta } from './partition_charts_meta'; -import type { PieVisualizationState, SharedPieLayerState } from '../../common/expressions'; +import { LegendDisplay, PieVisualizationState, SharedPieLayerState } from '../../common'; import { VisualizationDimensionEditorProps, VisualizationToolbarProps } from '../types'; import { ToolbarPopover, LegendSettingsPopover, useDebouncedValue } from '../shared_components'; import { PalettePicker } from '../shared_components'; @@ -34,21 +34,21 @@ const legendOptions: Array<{ }> = [ { id: 'pieLegendDisplay-default', - value: 'default', + value: LegendDisplay.DEFAULT, label: i18n.translate('xpack.lens.pieChart.legendVisibility.auto', { defaultMessage: 'Auto', }), }, { id: 'pieLegendDisplay-show', - value: 'show', + value: LegendDisplay.SHOW, label: i18n.translate('xpack.lens.pieChart.legendVisibility.show', { defaultMessage: 'Show', }), }, { id: 'pieLegendDisplay-hide', - value: 'hide', + value: LegendDisplay.HIDE, label: i18n.translate('xpack.lens.pieChart.legendVisibility.hide', { defaultMessage: 'Hide', }), diff --git a/x-pack/plugins/lens/public/pie_visualization/visualization.scss b/x-pack/plugins/lens/public/pie_visualization/visualization.scss deleted file mode 100644 index a8890208596b6..0000000000000 --- a/x-pack/plugins/lens/public/pie_visualization/visualization.scss +++ /dev/null @@ -1,7 +0,0 @@ -.lnsPieExpression__container { - height: 100%; - width: 100%; - // the FocusTrap is adding extra divs which are making the visualization redraw twice - // with a visible glitch. This make the chart library resilient to this extra reflow - overflow-x: hidden; -} diff --git a/x-pack/plugins/lens/public/pie_visualization/visualization.test.ts b/x-pack/plugins/lens/public/pie_visualization/visualization.test.ts index 86ac635e36068..c178613657947 100644 --- a/x-pack/plugins/lens/public/pie_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/pie_visualization/visualization.test.ts @@ -6,7 +6,13 @@ */ import { getPieVisualization } from './visualization'; -import type { PieVisualizationState } from '../../common/expressions'; +import { + PieVisualizationState, + PieChartTypes, + CategoryDisplay, + NumberDisplay, + LegendDisplay, +} from '../../common'; import { layerTypes } from '../../common'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; import { createMockDatasource, createMockFramePublicAPI } from '../mocks'; @@ -24,16 +30,16 @@ const pieVisualization = getPieVisualization({ function getExampleState(): PieVisualizationState { return { - shape: 'pie', + shape: PieChartTypes.PIE, layers: [ { layerId: LAYER_ID, layerType: layerTypes.DATA, groups: [], metric: undefined, - numberDisplay: 'percent', - categoryDisplay: 'default', - legendDisplay: 'default', + numberDisplay: NumberDisplay.PERCENT, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, }, ], @@ -81,14 +87,14 @@ describe('pie_visualization', () => { groups: ['a'], layerId: LAYER_ID, layerType: layerTypes.DATA, - numberDisplay: 'percent', - categoryDisplay: 'default', - legendDisplay: 'default', + numberDisplay: NumberDisplay.PERCENT, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, metric: undefined, }, ], - shape: 'donut', + shape: PieChartTypes.DONUT, }; const setDimensionResult = pieVisualization.setDimension({ prevState, @@ -100,7 +106,7 @@ describe('pie_visualization', () => { expect(setDimensionResult).toEqual( expect.objectContaining({ - shape: 'donut', + shape: PieChartTypes.DONUT, }) ); }); diff --git a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx index 8c52fc5a52fd8..0e8f05eff8920 100644 --- a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx @@ -20,21 +20,21 @@ import type { VisualizationDimensionGroupConfig, } from '../types'; import { getSortedGroups, toExpression, toPreviewExpression } from './to_expression'; -import type { PieLayerState, PieVisualizationState } from '../../common/expressions'; -import { layerTypes } from '../../common'; +import { CategoryDisplay, layerTypes, LegendDisplay, NumberDisplay } from '../../common'; import { suggestions } from './suggestions'; import { PartitionChartsMeta } from './partition_charts_meta'; import { DimensionEditor, PieToolbar } from './toolbar'; import { checkTableForContainsSmallValues } from './render_helpers'; +import { PieChartTypes, PieLayerState, PieVisualizationState } from '../../common'; function newLayerState(layerId: string): PieLayerState { return { layerId, groups: [], metric: undefined, - numberDisplay: 'percent', - categoryDisplay: 'default', - legendDisplay: 'default', + numberDisplay: NumberDisplay.PERCENT, + categoryDisplay: CategoryDisplay.DEFAULT, + legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, layerType: layerTypes.DATA, }; @@ -108,7 +108,7 @@ export const getPieVisualization = ({ initialize(addNewLayer, state, mainPalette) { return ( state || { - shape: 'donut', + shape: PieChartTypes.DONUT, layers: [newLayerState(addNewLayer())], palette: mainPalette, } diff --git a/x-pack/plugins/lens/server/expressions/expressions.ts b/x-pack/plugins/lens/server/expressions/expressions.ts index a04ad27d1a276..f258db7f9aede 100644 --- a/x-pack/plugins/lens/server/expressions/expressions.ts +++ b/x-pack/plugins/lens/server/expressions/expressions.ts @@ -7,7 +7,6 @@ import type { CoreSetup } from 'kibana/server'; import { - pie, xyChart, counterRate, metricChart, @@ -36,7 +35,6 @@ export const setupExpressions = ( [lensMultitable].forEach((expressionType) => expressions.registerType(expressionType)); [ - pie, xyChart, counterRate, metricChart, diff --git a/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx b/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx index 8b8d361611a2d..c982cdd5604d1 100644 --- a/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx +++ b/x-pack/plugins/osquery/public/packs/pack_queries_status_table.tsx @@ -24,7 +24,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedDate, FormattedTime, FormattedRelative } from '@kbn/i18n-react'; import moment from 'moment-timezone'; -import { +import type { TypedLensByValueInput, PersistedIndexPatternLayer, PieVisualizationState, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a5020c1122651..125c9ff096507 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -631,17 +631,14 @@ "xpack.lens.pie.addLayer": "ビジュアライゼーションレイヤーを追加", "xpack.lens.pie.arrayValues": "{label}には配列値が含まれます。可視化が想定通りに表示されない場合があります。", "xpack.lens.pie.donutLabel": "ドーナッツ", - "xpack.lens.pie.expressionHelpLabel": "円表示", "xpack.lens.pie.groupLabel": "比率", "xpack.lens.pie.groupsizeLabel": "サイズ単位", "xpack.lens.pie.pielabel": "円", - "xpack.lens.pie.pieWithNegativeWarningLabel": "{chartType}グラフは負の値では表示できません。", "xpack.lens.pie.sliceGroupLabel": "スライス", "xpack.lens.pie.suggestionLabel": "{chartName}として", "xpack.lens.pie.treemapGroupLabel": "グループ分けの条件", "xpack.lens.pie.treemaplabel": "ツリーマップ", "xpack.lens.pie.treemapSuggestionLabel": "ツリーマップとして", - "xpack.lens.pie.visualizationName": "円", "xpack.lens.pieChart.categoriesInLegendLabel": "ラベルを非表示", "xpack.lens.pieChart.fitInsideOnlyLabel": "内部のみ", "xpack.lens.pieChart.hiddenNumbersLabel": "グラフから非表示", @@ -2983,8 +2980,6 @@ "expressionPartitionVis.legend.filterForValueButtonAriaLabel": "値でフィルター", "expressionPartitionVis.legend.filterOptionsLegend": "{legendDataLabel}、フィルターオプション", "expressionPartitionVis.legend.filterOutValueButtonAriaLabel": "値を除外", - "expressionPartitionVis.negativeValuesFound": "円/ドーナツグラフは負の値では表示できません。", - "expressionPartitionVis.noResultsFoundTitle": "結果が見つかりませんでした", "fieldFormats.advancedSettings.format.bytesFormat.numeralFormatLinkText": "数字フォーマット", "fieldFormats.advancedSettings.format.bytesFormatText": "「バイト」フォーマットのデフォルト{numeralFormatLink}です", "fieldFormats.advancedSettings.format.bytesFormatTitle": "バイトフォーマット", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ab50cb20956a8..69e9f293f845d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -643,17 +643,14 @@ "xpack.lens.pie.addLayer": "添加可视化图层", "xpack.lens.pie.arrayValues": "{label} 包含数组值。您的可视化可能无法正常渲染。", "xpack.lens.pie.donutLabel": "圆环图", - "xpack.lens.pie.expressionHelpLabel": "饼图呈现器", "xpack.lens.pie.groupLabel": "比例", "xpack.lens.pie.groupsizeLabel": "大小调整依据", "xpack.lens.pie.pielabel": "饼图", - "xpack.lens.pie.pieWithNegativeWarningLabel": "{chartType} 图表无法使用负值进行呈现。", "xpack.lens.pie.sliceGroupLabel": "切片依据", "xpack.lens.pie.suggestionLabel": "为 {chartName}", "xpack.lens.pie.treemapGroupLabel": "分组依据", "xpack.lens.pie.treemaplabel": "树状图", "xpack.lens.pie.treemapSuggestionLabel": "为树状图", - "xpack.lens.pie.visualizationName": "饼图", "xpack.lens.pieChart.categoriesInLegendLabel": "隐藏标签", "xpack.lens.pieChart.fitInsideOnlyLabel": "仅内部", "xpack.lens.pieChart.hiddenNumbersLabel": "在图表中隐藏", @@ -2767,8 +2764,6 @@ "expressionPartitionVis.legend.filterForValueButtonAriaLabel": "筛留值", "expressionPartitionVis.legend.filterOptionsLegend": "{legendDataLabel}, 筛选选项", "expressionPartitionVis.legend.filterOutValueButtonAriaLabel": "筛除值", - "expressionPartitionVis.negativeValuesFound": "饼图/圆环图无法使用负值进行呈现。", - "expressionPartitionVis.noResultsFoundTitle": "找不到结果", "fieldFormats.advancedSettings.format.bytesFormat.numeralFormatLinkText": "数值格式", "fieldFormats.advancedSettings.format.bytesFormatText": "“字节”格式的默认{numeralFormatLink}", "fieldFormats.advancedSettings.format.bytesFormatTitle": "字节格式", diff --git a/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts b/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts index 26efa4248850b..382449e5e2586 100644 --- a/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts +++ b/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts @@ -11,7 +11,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'dashboard', 'visualize', 'lens', 'timePicker']); - const find = getService('find'); const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); const dashboardPanelActions = getService('dashboardPanelActions'); @@ -49,8 +48,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.lens.saveAndReturn(); await PageObjects.dashboard.waitForRenderComplete(); - const pieExists = await find.existsByCssSelector('.lnsPieExpression__container'); - expect(pieExists).to.be(true); + const partitionVisExists = await testSubjects.exists('partitionVisChart'); + expect(partitionVisExists).to.be(true); }); it('editing and saving a lens by value panel retains number of panels', async () => { diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts b/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts index f6692a2edb3bf..1d2d3f6862e43 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts @@ -103,8 +103,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.lens.saveAndReturn(); await PageObjects.dashboard.waitForRenderComplete(); - const pieExists = await find.existsByCssSelector('.lnsPieExpression__container'); - expect(pieExists).to.be(true); + const partitionVisExists = await testSubjects.exists('partitionVisChart'); + expect(partitionVisExists).to.be(true); }); it('disables save to library button without visualize save permissions', async () => {