From ec62e15529eba90448db7cf53173c431efc7b7cc Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 8 Jun 2023 08:09:20 -0600 Subject: [PATCH] [lens] tag cloud (#157751) Part of https://github.com/elastic/kibana/issues/154307 Closes https://github.com/elastic/kibana/issues/95542 PR adds tagcloud visualization to lens Screen Shot 2023-05-31 at 1 11 00 PM --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Stratoula Kalafateli --- packages/kbn-chart-icons/index.ts | 1 + .../src/assets/chart_tagcloud.tsx | 29 ++ packages/kbn-chart-icons/src/assets/index.ts | 1 + .../tagcloud_function.test.ts.snap | 2 + .../expression_functions/tagcloud_function.ts | 14 +- .../expression_tagcloud/common/index.ts | 2 + .../common/types/expression_functions.ts | 24 +- .../expression_tagcloud/kibana.jsonc | 3 + .../__stories__/tagcloud_renderer.stories.tsx | 1 + .../components/tagcloud_component.test.tsx | 1 + .../public/components/tagcloud_component.tsx | 23 +- .../tagcloud_renderer.tsx | 3 - .../expression_tagcloud/tsconfig.json | 1 + .../snapshots/baseline/partial_test_1.json | 2 +- .../snapshots/baseline/tagcloud_all_data.json | 2 +- .../baseline/tagcloud_empty_data.json | 2 +- .../snapshots/baseline/tagcloud_fontsize.json | 2 +- .../baseline/tagcloud_metric_data.json | 2 +- .../snapshots/baseline/tagcloud_options.json | 2 +- .../snapshots/session/tagcloud_fontsize.json | 2 +- x-pack/plugins/lens/kibana.jsonc | 1 + x-pack/plugins/lens/public/async_services.ts | 2 + x-pack/plugins/lens/public/plugin.ts | 5 + .../visualizations/tagcloud/constants.ts | 21 ++ .../public/visualizations/tagcloud/index.ts | 25 ++ .../visualizations/tagcloud/suggestions.ts | 54 ++++ .../tagcloud_toolbar/font_size_input.tsx | 56 ++++ .../tagcloud/tagcloud_toolbar/index.ts | 8 + .../tagcloud_toolbar/tagcloud_toolbar.tsx | 127 ++++++++ .../tagcloud/tagcloud_visualization.tsx | 286 ++++++++++++++++++ .../tagcloud/tags_dimension_editor.tsx | 32 ++ .../public/visualizations/tagcloud/types.ts | 32 ++ x-pack/plugins/lens/tsconfig.json | 1 + 33 files changed, 736 insertions(+), 33 deletions(-) create mode 100644 packages/kbn-chart-icons/src/assets/chart_tagcloud.tsx create mode 100644 x-pack/plugins/lens/public/visualizations/tagcloud/constants.ts create mode 100644 x-pack/plugins/lens/public/visualizations/tagcloud/index.ts create mode 100644 x-pack/plugins/lens/public/visualizations/tagcloud/suggestions.ts create mode 100644 x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_toolbar/font_size_input.tsx create mode 100644 x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_toolbar/index.ts create mode 100644 x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_toolbar/tagcloud_toolbar.tsx create mode 100644 x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_visualization.tsx create mode 100644 x-pack/plugins/lens/public/visualizations/tagcloud/tags_dimension_editor.tsx create mode 100644 x-pack/plugins/lens/public/visualizations/tagcloud/types.ts diff --git a/packages/kbn-chart-icons/index.ts b/packages/kbn-chart-icons/index.ts index 98a4e7fd323e53..f9f0a5ad8962dc 100644 --- a/packages/kbn-chart-icons/index.ts +++ b/packages/kbn-chart-icons/index.ts @@ -40,4 +40,5 @@ export { IconChartHeatmap, IconChartHorizontalBullet, IconChartVerticalBullet, + IconChartTagcloud, } from './src/assets'; diff --git a/packages/kbn-chart-icons/src/assets/chart_tagcloud.tsx b/packages/kbn-chart-icons/src/assets/chart_tagcloud.tsx new file mode 100644 index 00000000000000..3119841eb8dce0 --- /dev/null +++ b/packages/kbn-chart-icons/src/assets/chart_tagcloud.tsx @@ -0,0 +1,29 @@ +/* + * 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, { FunctionComponent } from 'react'; +import type { EuiIconProps } from '@elastic/eui'; +import { colors } from './common_styles'; + +export const IconChartTagcloud: FunctionComponent = ({ + title, + titleId, + ...props +}: Omit) => ( + + {title ? : null} + + + +); diff --git a/packages/kbn-chart-icons/src/assets/index.ts b/packages/kbn-chart-icons/src/assets/index.ts index d3d2f968cdcc54..85cc291936b18c 100644 --- a/packages/kbn-chart-icons/src/assets/index.ts +++ b/packages/kbn-chart-icons/src/assets/index.ts @@ -40,3 +40,4 @@ export { IconRegionMap } from './region_map'; export { IconChartHeatmap } from './chart_heatmap'; export { IconChartHorizontalBullet } from './chart_horizontal_bullet'; export { IconChartVerticalBullet } from './chart_vertical_bullet'; +export { IconChartTagcloud } from './chart_tagcloud'; diff --git a/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/__snapshots__/tagcloud_function.test.ts.snap b/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/__snapshots__/tagcloud_function.test.ts.snap index 6d9eff8aeeab21..d03cebd6802904 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/__snapshots__/tagcloud_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/__snapshots__/tagcloud_function.test.ts.snap @@ -66,6 +66,7 @@ Object { "bucket": Object { "accessor": 1, }, + "isPreview": false, "maxFontSize": 72, "metric": Object { "accessor": 0, @@ -125,6 +126,7 @@ Object { }, "type": "vis_dimension", }, + "isPreview": false, "maxFontSize": 72, "metric": Object { "accessor": Object { diff --git a/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.ts b/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.ts index d86f570daf5c13..ec69431cd1735d 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.ts +++ b/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.ts @@ -20,7 +20,7 @@ const strings = { help: i18n.translate('expressionTagcloud.functions.tagcloudHelpText', { defaultMessage: 'Tagcloud visualization.', }), - args: { + argHelp: { scale: i18n.translate('expressionTagcloud.functions.tagcloud.args.scaleHelpText', { defaultMessage: 'Scale to determine font size of a word', }), @@ -48,6 +48,9 @@ const strings = { ariaLabel: i18n.translate('expressionTagcloud.functions.tagcloud.args.ariaLabelHelpText', { defaultMessage: 'Specifies the aria label of the tagcloud', }), + isPreview: i18n.translate('expressionTagcloud.functions.tagcloud.args.isPreviewHelpText', { + defaultMessage: 'Set isPreview to true to avoid showing out of room warnings', + }), }, dimension: { tags: i18n.translate('expressionTagcloud.functions.tagcloud.dimension.tags', { @@ -81,7 +84,7 @@ export const errors = { }; export const tagcloudFunction: ExpressionTagcloudFunction = () => { - const { help, args: argHelp, dimension } = strings; + const { help, argHelp, dimension } = strings; return { name: EXPRESSION_NAME, @@ -137,6 +140,12 @@ export const tagcloudFunction: ExpressionTagcloudFunction = () => { help: argHelp.ariaLabel, required: false, }, + isPreview: { + types: ['boolean'], + help: argHelp.isPreview, + default: false, + required: false, + }, }, fn(input, args, handlers) { validateAccessor(args.metric, input.columns); @@ -157,6 +166,7 @@ export const tagcloudFunction: ExpressionTagcloudFunction = () => { args.ariaLabel ?? (handlers.variables?.embeddableTitle as string) ?? handlers.getExecutionContext?.()?.description, + isPreview: Boolean(args.isPreview), }; if (handlers?.inspectorAdapters?.tables) { diff --git a/src/plugins/chart_expressions/expression_tagcloud/common/index.ts b/src/plugins/chart_expressions/expression_tagcloud/common/index.ts index 899cb3723a95c1..ff7ac8f4e0945c 100755 --- a/src/plugins/chart_expressions/expression_tagcloud/common/index.ts +++ b/src/plugins/chart_expressions/expression_tagcloud/common/index.ts @@ -7,3 +7,5 @@ */ export { EXPRESSION_NAME, ScaleOptions, Orientation } from './constants'; + +export type { ExpressionTagcloudFunctionDefinition } from './types/expression_functions'; diff --git a/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts index 5ec00ee6edb382..995cfca06c4908 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts @@ -17,23 +17,23 @@ import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common'; import { EXPRESSION_NAME, ScaleOptions, Orientation } from '../constants'; interface TagCloudCommonParams { - scale: $Values; + scale?: $Values; orientation: $Values; minFontSize: number; maxFontSize: number; showLabel: boolean; ariaLabel?: string; + metric: ExpressionValueVisDimension | string; + bucket?: ExpressionValueVisDimension | string; + palette: PaletteOutput; } export interface TagCloudVisConfig extends TagCloudCommonParams { - metric: ExpressionValueVisDimension | string; - bucket?: ExpressionValueVisDimension | string; + isPreview?: boolean; } export interface TagCloudRendererParams extends TagCloudCommonParams { - palette: PaletteOutput; - metric: ExpressionValueVisDimension | string; - bucket?: ExpressionValueVisDimension | string; + isPreview: boolean; } export interface TagcloudRendererConfig { @@ -43,13 +43,11 @@ export interface TagcloudRendererConfig { syncColors: boolean; } -interface Arguments extends TagCloudVisConfig { - palette: PaletteOutput; -} - -export type ExpressionTagcloudFunction = () => ExpressionFunctionDefinition< - 'tagcloud', +export type ExpressionTagcloudFunctionDefinition = ExpressionFunctionDefinition< + typeof EXPRESSION_NAME, Datatable, - Arguments, + TagCloudVisConfig, ExpressionValueRender >; + +export type ExpressionTagcloudFunction = () => ExpressionTagcloudFunctionDefinition; diff --git a/src/plugins/chart_expressions/expression_tagcloud/kibana.jsonc b/src/plugins/chart_expressions/expression_tagcloud/kibana.jsonc index e68af8e25c99f5..6c6ce82d321edc 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/kibana.jsonc +++ b/src/plugins/chart_expressions/expression_tagcloud/kibana.jsonc @@ -20,6 +20,9 @@ "requiredBundles": [ "kibanaUtils", "kibanaReact" + ], + "extraPublicDirs": [ + "common" ] } } diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/__stories__/tagcloud_renderer.stories.tsx b/src/plugins/chart_expressions/expression_tagcloud/public/__stories__/tagcloud_renderer.stories.tsx index 322ddb55430e6f..27e1413eadd7a3 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/public/__stories__/tagcloud_renderer.stories.tsx +++ b/src/plugins/chart_expressions/expression_tagcloud/public/__stories__/tagcloud_renderer.stories.tsx @@ -57,6 +57,7 @@ const config: TagcloudRendererConfig = { format: { id: 'string', params: {} }, }, palette: { type: 'palette', name: 'default' }, + isPreview: false, }, syncColors: false, }; diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.test.tsx b/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.test.tsx index 85f77c98e0b50f..9b73646dda837e 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.test.tsx +++ b/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.test.tsx @@ -61,6 +61,7 @@ const visParams: TagCloudRendererParams = { minFontSize: 12, maxFontSize: 70, showLabel: true, + isPreview: false, }; const formattedData: WordcloudSpec['data'] = [ diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx b/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx index 75b8734e5453d1..7f1759f0b19d83 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx +++ b/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx @@ -10,14 +10,12 @@ import React, { useCallback, useState, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { throttle } from 'lodash'; import { EuiIconTip, EuiResizeObserver } from '@elastic/eui'; +import { IconChartTagcloud } from '@kbn/chart-icons'; import { Chart, Settings, Wordcloud, RenderChangeListener } from '@elastic/charts'; +import { EmptyPlaceholder } from '@kbn/charts-plugin/public'; import type { PaletteRegistry, PaletteOutput } from '@kbn/coloring'; import { IInterpreterRenderHandlers } from '@kbn/expressions-plugin/public'; -import { - getColumnByAccessor, - getAccessor, - getFormatByAccessor, -} from '@kbn/visualizations-plugin/common/utils'; +import { getColumnByAccessor, getFormatByAccessor } from '@kbn/visualizations-plugin/common/utils'; import { getFormatService } from '../format_service'; import { TagcloudRendererConfig } from '../../common/types'; import { ScaleOptions, Orientation } from '../../common/constants'; @@ -153,6 +151,11 @@ export const TagCloudChart = ({ const termsBucketId = getColumnByAccessor(bucket, visData.columns)!.id; const clickedValue = elements[0][0].text; + const columnIndex = visData.columns.findIndex((col) => col.id === termsBucketId); + if (columnIndex < 0) { + return; + } + const rowIndex = visData.rows.findIndex((row) => { const formattedValue = bucketFormatter ? bucketFormatter.convert(row[termsBucketId], 'text') @@ -170,7 +173,7 @@ export const TagCloudChart = ({ data: [ { table: visData, - column: getAccessor(bucket), + column: columnIndex, row: rowIndex, }, ], @@ -180,6 +183,10 @@ export const TagCloudChart = ({ [bucket, bucketFormatter, fireEvent, visData] ); + if (visData.rows.length === 0) { + return ; + } + return ( {(resizeRef) => ( @@ -215,7 +222,7 @@ export const TagCloudChart = ({ {label} )} - {warning && ( + {!visParams.isPreview && warning && (
)} - {tagCloudData.length > MAX_TAG_COUNT && ( + {!visParams.isPreview && tagCloudData.length > MAX_TAG_COUNT && (
@@ -81,7 +79,6 @@ export const tagcloudRenderer: ( // It is used for rendering at `Canvas`. className={cx('tagCloudContainer', css(tagCloudVisClass))} renderComplete={renderComplete} - showNoResult={showNoResult} > { editorFrameSetupInterface.registerVisualization(queuedVis); diff --git a/x-pack/plugins/lens/public/visualizations/tagcloud/constants.ts b/x-pack/plugins/lens/public/visualizations/tagcloud/constants.ts new file mode 100644 index 00000000000000..b6d54c9986e695 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/tagcloud/constants.ts @@ -0,0 +1,21 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { $Values } from '@kbn/utility-types'; +import { Orientation } from '@kbn/expression-tagcloud-plugin/common'; + +export const TAGCLOUD_LABEL = i18n.translate('xpack.lens.tagcloud.label', { + defaultMessage: 'Tag cloud', +}); + +export const DEFAULT_STATE = { + maxFontSize: 72, + minFontSize: 18, + orientation: Orientation.SINGLE as $Values, + showLabel: true, +}; diff --git a/x-pack/plugins/lens/public/visualizations/tagcloud/index.ts b/x-pack/plugins/lens/public/visualizations/tagcloud/index.ts new file mode 100644 index 00000000000000..129d8f4eb545f7 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/tagcloud/index.ts @@ -0,0 +1,25 @@ +/* + * 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 { CoreSetup } from '@kbn/core/public'; +import type { ChartsPluginSetup } from '@kbn/charts-plugin/public'; +import type { EditorFrameSetup } from '../../types'; + +export interface TagcloudVisualizationPluginSetupPlugins { + editorFrame: EditorFrameSetup; + charts: ChartsPluginSetup; +} + +export class TagcloudVisualization { + setup(core: CoreSetup, { editorFrame, charts }: TagcloudVisualizationPluginSetupPlugins) { + editorFrame.registerVisualization(async () => { + const { getTagcloudVisualization } = await import('../../async_services'); + const palettes = await charts.palettes.getPalettes(); + return getTagcloudVisualization({ paletteService: palettes, theme: core.theme }); + }); + } +} diff --git a/x-pack/plugins/lens/public/visualizations/tagcloud/suggestions.ts b/x-pack/plugins/lens/public/visualizations/tagcloud/suggestions.ts new file mode 100644 index 00000000000000..b4866bf95a4073 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/tagcloud/suggestions.ts @@ -0,0 +1,54 @@ +/* + * 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 { partition } from 'lodash'; +import { IconChartTagcloud } from '@kbn/chart-icons'; +import type { SuggestionRequest, VisualizationSuggestion } from '../../types'; +import type { TagcloudState } from './types'; +import { DEFAULT_STATE, TAGCLOUD_LABEL } from './constants'; + +export function suggestions({ + table, + state, + keptLayerIds, + mainPalette, + subVisualizationId, +}: SuggestionRequest): Array> { + const isUnchanged = state && table.changeType === 'unchanged'; + if ( + isUnchanged || + keptLayerIds.length > 1 || + (keptLayerIds.length && table.layerId !== keptLayerIds[0]) + ) { + return []; + } + + const [buckets, metrics] = partition(table.columns, (col) => col.operation.isBucketed); + + if (buckets.length !== 1 || metrics.length !== 1) { + return []; + } + + return buckets + .filter((bucket) => { + return bucket.operation.dataType !== 'date'; + }) + .map((bucket) => { + return { + previewIcon: IconChartTagcloud, + title: TAGCLOUD_LABEL, + hide: true, // hide suggestions while in tech preview + score: 0.1, + state: { + layerId: table.layerId, + tagAccessor: bucket.columnId, + valueAccessor: metrics[0].columnId, + ...DEFAULT_STATE, + }, + }; + }); +} diff --git a/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_toolbar/font_size_input.tsx b/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_toolbar/font_size_input.tsx new file mode 100644 index 00000000000000..b8463f8663080b --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_toolbar/font_size_input.tsx @@ -0,0 +1,56 @@ +/* + * 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, { useEffect, useState } from 'react'; +import useDebounce from 'react-use/lib/useDebounce'; +import { EuiDualRange } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface Props { + minFontSize: number; + maxFontSize: number; + onChange: (minFontSize: number, maxFontSize: number) => void; +} + +export function FontSizeInput(props: Props) { + const [fontSize, setFontSize] = useState<[number, number]>([ + props.minFontSize, + props.maxFontSize, + ]); + + const [, cancel] = useDebounce( + () => { + props.onChange(fontSize[0], fontSize[1]); + }, + 150, + [fontSize] + ); + + useEffect(() => { + return () => { + cancel(); + }; + }, [cancel]); + + return ( + { + setFontSize(value as [number, number]); + }} + showLabels + compressed + aria-label={i18n.translate('xpack.lens.label.tagcloud.fontSizeLabel', { + defaultMessage: 'Font size', + })} + /> + ); +} diff --git a/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_toolbar/index.ts b/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_toolbar/index.ts new file mode 100644 index 00000000000000..76f4c0f51bc40d --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_toolbar/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { TagcloudToolbar } from './tagcloud_toolbar'; diff --git a/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_toolbar/tagcloud_toolbar.tsx b/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_toolbar/tagcloud_toolbar.tsx new file mode 100644 index 00000000000000..406fe411665e4d --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_toolbar/tagcloud_toolbar.tsx @@ -0,0 +1,127 @@ +/* + * 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. + */ + +/* + * 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, { ChangeEvent } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSelect, + EuiSwitch, + EuiSwitchEvent, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { $Values } from '@kbn/utility-types'; +import { Orientation } from '@kbn/expression-tagcloud-plugin/common'; +import type { VisualizationToolbarProps } from '../../../types'; +import { ToolbarPopover } from '../../../shared_components'; +import type { TagcloudState } from '../types'; +import { FontSizeInput } from './font_size_input'; + +const ORIENTATION_OPTIONS = [ + { + value: Orientation.SINGLE, + text: i18n.translate('xpack.lens.label.tagcloud.orientation.single', { + defaultMessage: 'Single', + }), + }, + { + value: Orientation.RIGHT_ANGLED, + text: i18n.translate('xpack.lens.label.tagcloud.orientation.rightAngled', { + defaultMessage: 'Right angled', + }), + }, + { + value: Orientation.MULTIPLE, + text: i18n.translate('xpack.lens.label.tagcloud.orientation.multiple', { + defaultMessage: 'Multiple', + }), + }, +]; + +export function TagcloudToolbar(props: VisualizationToolbarProps) { + return ( + + + + + + { + props.setState({ + ...props.state, + minFontSize, + maxFontSize, + }); + }} + /> + + + ) => { + props.setState({ + ...props.state, + orientation: event.target.value as $Values, + }); + }} + compressed + /> + + + { + props.setState({ + ...props.state, + showLabel: event.target.checked, + }); + }} + compressed + /> + + + + + + ); +} diff --git a/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_visualization.tsx b/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_visualization.tsx new file mode 100644 index 00000000000000..b74351bc185d85 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_visualization.tsx @@ -0,0 +1,286 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { I18nProvider } from '@kbn/i18n-react'; +import { render } from 'react-dom'; +import { ThemeServiceStart } from '@kbn/core/public'; +import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public'; +import type { ExpressionTagcloudFunctionDefinition } from '@kbn/expression-tagcloud-plugin/common'; +import { LayerTypes } from '@kbn/expression-xy-plugin/public'; +import { + buildExpression, + buildExpressionFunction, + ExpressionFunctionTheme, +} from '@kbn/expressions-plugin/common'; +import { PaletteRegistry } from '@kbn/coloring'; +import { IconChartTagcloud } from '@kbn/chart-icons'; +import { SystemPaletteExpressionFunctionDefinition } from '@kbn/charts-plugin/common'; +import type { OperationMetadata, Visualization } from '../..'; +import type { TagcloudState } from './types'; +import { suggestions } from './suggestions'; +import { TagcloudToolbar } from './tagcloud_toolbar'; +import { TagsDimensionEditor } from './tags_dimension_editor'; +import { DEFAULT_STATE, TAGCLOUD_LABEL } from './constants'; + +const TAG_GROUP_ID = 'tags'; +const METRIC_GROUP_ID = 'metric'; + +export const getTagcloudVisualization = ({ + paletteService, + theme, +}: { + paletteService: PaletteRegistry; + theme: ThemeServiceStart; +}): Visualization => ({ + id: 'lnsTagcloud', + + visualizationTypes: [ + { + id: 'lnsTagcloud', + icon: IconChartTagcloud, + label: TAGCLOUD_LABEL, + groupLabel: i18n.translate('xpack.lens.pie.groupLabel', { + defaultMessage: 'Proportion', + }), + showExperimentalBadge: true, + }, + ], + + getVisualizationTypeId() { + return 'lnsTagcloud'; + }, + + clearLayer(state) { + const newState = { + ...state, + ...DEFAULT_STATE, + }; + delete newState.tagAccessor; + delete newState.valueAccessor; + delete newState.palette; + return newState; + }, + + getLayerIds(state) { + return [state.layerId]; + }, + + getDescription() { + return { + icon: IconChartTagcloud, + label: TAGCLOUD_LABEL, + }; + }, + + getSuggestions: suggestions, + + triggers: [VIS_EVENT_TO_TRIGGER.filter], + + initialize(addNewLayer, state) { + return ( + state || { + layerId: addNewLayer(), + layerType: LayerTypes.DATA, + ...DEFAULT_STATE, + } + ); + }, + + getConfiguration({ state }) { + return { + groups: [ + { + groupId: TAG_GROUP_ID, + groupLabel: i18n.translate('xpack.lens.tagcloud.tagLabel', { + defaultMessage: 'Tags', + }), + layerId: state.layerId, + accessors: state.tagAccessor + ? [ + { + columnId: state.tagAccessor, + triggerIconType: 'colorBy', + palette: paletteService + .get(state.palette?.name || 'default') + .getCategoricalColors(10, state.palette?.params), + }, + ] + : [], + supportsMoreColumns: !state.tagAccessor, + filterOperations: (op: OperationMetadata) => op.isBucketed, + enableDimensionEditor: true, + required: true, + requiredMinDimensionCount: 1, + dataTestSubj: 'lnsTagcloud_tagDimensionPanel', + }, + { + groupId: METRIC_GROUP_ID, + groupLabel: i18n.translate('xpack.lens.tagcloud.metricValueLabel', { + defaultMessage: 'Metric', + }), + isMetricDimension: true, + layerId: state.layerId, + accessors: state.valueAccessor ? [{ columnId: state.valueAccessor }] : [], + supportsMoreColumns: !state.valueAccessor, + filterOperations: (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number', + enableDimensionEditor: true, + required: true, + requiredMinDimensionCount: 1, + dataTestSubj: 'lnsTagcloud_valueDimensionPanel', + }, + ], + }; + }, + + getSupportedLayers() { + return [ + { + type: LayerTypes.DATA, + label: i18n.translate('xpack.lens.tagcloud.addLayer', { + defaultMessage: 'Add visualization layer', + }), + }, + ]; + }, + + getLayerType(layerId, state) { + if (state?.layerId === layerId) { + return LayerTypes.DATA; + } + }, + + toExpression: (state, datasourceLayers, attributes, datasourceExpressionsByLayers = {}) => { + if (!state.tagAccessor || !state.valueAccessor) { + return null; + } + + const datasourceExpression = datasourceExpressionsByLayers[state.layerId]; + return { + type: 'expression', + chain: [ + ...(datasourceExpression ? datasourceExpression.chain : []), + buildExpressionFunction('tagcloud', { + bucket: state.tagAccessor, + metric: state.valueAccessor, + maxFontSize: state.maxFontSize, + minFontSize: state.minFontSize, + orientation: state.orientation, + palette: buildExpression([ + state.palette + ? buildExpressionFunction('theme', { + variable: 'palette', + default: [ + paletteService.get(state.palette.name).toExpression(state.palette.params), + ], + }) + : buildExpressionFunction( + 'system_palette', + { + name: 'default', + } + ), + ]).toAst(), + showLabel: state.showLabel, + }).toAst(), + ], + }; + }, + + toPreviewExpression: (state, datasourceLayers, datasourceExpressionsByLayers = {}) => { + if (!state.tagAccessor || !state.valueAccessor) { + return null; + } + + const datasourceExpression = datasourceExpressionsByLayers[state.layerId]; + return { + type: 'expression', + chain: [ + ...(datasourceExpression ? datasourceExpression.chain : []), + buildExpressionFunction('tagcloud', { + bucket: state.tagAccessor, + isPreview: true, + metric: state.valueAccessor, + maxFontSize: 18, + minFontSize: 4, + orientation: state.orientation, + palette: buildExpression([ + state.palette + ? buildExpressionFunction('theme', { + variable: 'palette', + default: [ + paletteService.get(state.palette.name).toExpression(state.palette.params), + ], + }) + : buildExpressionFunction( + 'system_palette', + { + name: 'default', + } + ), + ]).toAst(), + showLabel: false, + }).toAst(), + ], + }; + }, + + setDimension({ columnId, groupId, prevState }) { + const update: Partial = {}; + if (groupId === TAG_GROUP_ID) { + update.tagAccessor = columnId; + } else if (groupId === METRIC_GROUP_ID) { + update.valueAccessor = columnId; + } + return { + ...prevState, + ...update, + }; + }, + + removeDimension({ prevState, layerId, columnId }) { + const update = { ...prevState }; + + if (prevState.tagAccessor === columnId) { + delete update.tagAccessor; + } else if (prevState.valueAccessor === columnId) { + delete update.valueAccessor; + } + + return update; + }, + + renderDimensionEditor(domElement, props) { + if (props.groupId === TAG_GROUP_ID) { + render( + + + + + , + domElement + ); + } + }, + + renderToolbar(domElement, props) { + render( + + + + + , + domElement + ); + }, +}); diff --git a/x-pack/plugins/lens/public/visualizations/tagcloud/tags_dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/tagcloud/tags_dimension_editor.tsx new file mode 100644 index 00000000000000..e91a73982dd388 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/tagcloud/tags_dimension_editor.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { PaletteRegistry } from '@kbn/coloring'; +import type { TagcloudState } from './types'; +import { PalettePicker } from '../../shared_components'; + +interface Props { + paletteService: PaletteRegistry; + state: TagcloudState; + setState: (state: TagcloudState) => void; +} + +export function TagsDimensionEditor(props: Props) { + return ( + { + props.setState({ + ...props.state, + palette: newPalette, + }); + }} + /> + ); +} diff --git a/x-pack/plugins/lens/public/visualizations/tagcloud/types.ts b/x-pack/plugins/lens/public/visualizations/tagcloud/types.ts new file mode 100644 index 00000000000000..c4a6ff1ddb6ad9 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/tagcloud/types.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { $Values } from '@kbn/utility-types'; +import { Datatable } from '@kbn/expressions-plugin/common'; +import type { PaletteOutput } from '@kbn/coloring'; +import { Orientation } from '@kbn/expression-tagcloud-plugin/common'; + +export interface TagcloudState { + layerId: string; + tagAccessor?: string; + valueAccessor?: string; + maxFontSize: number; + minFontSize: number; + orientation: $Values; + palette?: PaletteOutput; + showLabel: boolean; +} + +export interface TagcloudConfig extends TagcloudState { + title: string; + description: string; +} + +export interface TagcloudProps { + data: Datatable; + args: TagcloudConfig; +} diff --git a/x-pack/plugins/lens/tsconfig.json b/x-pack/plugins/lens/tsconfig.json index 05ab37120b8a59..7f0ea1f389ce79 100644 --- a/x-pack/plugins/lens/tsconfig.json +++ b/x-pack/plugins/lens/tsconfig.json @@ -34,6 +34,7 @@ "@kbn/expression-gauge-plugin", "@kbn/expression-legacy-metric-vis-plugin", "@kbn/expression-metric-vis-plugin", + "@kbn/expression-tagcloud-plugin", "@kbn/data-view-editor-plugin", "@kbn/event-annotation-plugin", "@kbn/expression-xy-plugin",