From 82c390e9b1a61c89196a57c16c17b77d6c67869c Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 15 Mar 2021 13:37:55 +0100 Subject: [PATCH 1/4] dynamic threshold lines --- .../config_panel/config_panel.tsx | 93 ++++++------ .../config_panel/layer_actions.ts | 22 +-- .../editor_frame/config_panel/layer_panel.tsx | 4 +- x-pack/plugins/lens/public/types.ts | 6 +- .../public/xy_visualization/expression.tsx | 42 +++++- .../public/xy_visualization/to_expression.ts | 2 + .../lens/public/xy_visualization/types.ts | 11 ++ .../public/xy_visualization/visualization.tsx | 132 +++++++++++------- .../xy_visualization/xy_config_panel.tsx | 105 ++++++++++++++ 9 files changed, 315 insertions(+), 102 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx index 393c7363dc03f..e621def2e39d3 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -133,48 +133,59 @@ export function LayerPanels( /> ) : null )} - {activeVisualization.appendLayer && visualizationState && ( - - - ( + + { - const id = generateId(); - dispatch({ - type: 'UPDATE_STATE', - subType: 'ADD_LAYER', - updater: (state) => - appendLayer({ - activeVisualization, - generateId: () => id, - trackUiEvent, - activeDatasource: datasourceMap[activeDatasourceId], - state, - }), - }); - setNextFocusedLayerId(id); - }} - iconType="plusInCircleFilled" - /> - - - )} + content={i18n.translate('xpack.lens.xyChart.addLayerTooltip', { + defaultMessage: + 'Use multiple layers to combine chart types or visualize different index patterns.', + })} + position="bottom" + > + { + const id = generateId(); + dispatch({ + type: 'UPDATE_STATE', + subType: 'ADD_LAYER', + updater: (state) => + appendLayer({ + activeVisualization, + generateId: () => id, + trackUiEvent, + activeDatasource: datasourceMap[activeDatasourceId], + state, + layerType: activeVisualization.getLayerTypes ? layerType : undefined, + dataBacked, + }), + }); + setNextFocusedLayerId(id); + }} + iconType="plusInCircleFilled" + > + {layerType} + + + + ))} ); } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts index dd95770655d1a..24ff725a550b2 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts @@ -23,6 +23,8 @@ interface AppendLayerOptions { generateId: () => string; activeDatasource: Pick; activeVisualization: Pick; + layerType?: string; + dataBacked?: boolean; } export function removeLayer(opts: RemoveLayerOptions): EditorFrameState { @@ -61,6 +63,8 @@ export function appendLayer({ state, generateId, activeDatasource, + layerType, + dataBacked, }: AppendLayerOptions): EditorFrameState { trackUiEvent('layer_added'); @@ -74,17 +78,19 @@ export function appendLayer({ ...state, datasourceStates: { ...state.datasourceStates, - [activeDatasource.id]: { - ...state.datasourceStates[activeDatasource.id], - state: activeDatasource.insertLayer( - state.datasourceStates[activeDatasource.id].state, - layerId - ), - }, + [activeDatasource.id]: dataBacked + ? { + ...state.datasourceStates[activeDatasource.id], + state: activeDatasource.insertLayer( + state.datasourceStates[activeDatasource.id].state, + layerId + ), + } + : state.datasourceStates[activeDatasource.id], }, visualization: { ...state.visualization, - state: activeVisualization.appendLayer(state.visualization.state, layerId), + state: activeVisualization.appendLayer(state.visualization.state, layerId, layerType), }, stagedPreview: undefined, }; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 14063aea02665..874f9084d92cc 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -108,7 +108,9 @@ export function LayerPanel( activeData: props.framePublicAPI.activeData, }; - const { groups } = activeVisualization.getConfiguration(layerVisualizationConfigProps); + const { groups, isConstant } = activeVisualization.getConfiguration( + layerVisualizationConfigProps + ); const isEmptyLayer = !groups.some((d) => d.accessors.length > 0); const { activeId, activeGroup } = activeDimension; diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 3b47792af4254..65bbb5b7681d2 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -568,14 +568,16 @@ export interface Visualization { /** Optional, if the visualization supports multiple layers */ removeLayer?: (state: T, layerId: string) => T; /** Track added layers in internal state */ - appendLayer?: (state: T, layerId: string) => T; + appendLayer?: (state: T, layerId: string, layerType?: string) => T; + /* if set, allows adding of multiple types of layers */ + getLayerTypes?: (state: T) => Array<{ name: string; dataBacked: boolean }>; /** * For consistency across different visualizations, the dimension configuration UI is standardized */ getConfiguration: ( props: VisualizationConfigProps - ) => { groups: VisualizationDimensionGroupConfig[] }; + ) => { groups: VisualizationDimensionGroupConfig[]; isConstant?: boolean }; /** * Popover contents that open when the user clicks the contextMenuIcon. This can be used diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 0bf5c139e2403..b93d7c0758c0f 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -24,6 +24,8 @@ import { HorizontalAlignment, ElementClickListener, BrushEndListener, + LineAnnotation, + AnnotationDomainTypes, } from '@elastic/charts'; import { I18nProvider } from '@kbn/i18n/react'; import { @@ -32,7 +34,7 @@ import { Datatable, DatatableRow, } from 'src/plugins/expressions/public'; -import { IconType } from '@elastic/eui'; +import { EuiIcon, IconType } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { RenderMode } from 'src/plugins/expressions'; import { @@ -335,6 +337,7 @@ export function XYChart({ const chartBaseTheme = chartsThemeService.useChartsBaseTheme(); const filteredLayers = getFilteredLayers(layers, data); + const thresholdLayers = layers.filter((layer) => layer.layerType === 'threshold'); if (filteredLayers.length === 0) { const icon: IconType = layers.length > 0 ? getIconForSeriesType(layers[0].seriesType) : 'bar'; @@ -815,13 +818,48 @@ export function XYChart({ } }) )} + + {thresholdLayers.map((thresholdLayer) => { + return ( + + data.tables[thresholdLayer.layerId].rows.map((row) => ({ + dataValue: row[accessor], + })) + )} + groupId={ + thresholdLayer.thresholdAxis === 'bottom' + ? undefined + : thresholdLayer.thresholdAxis === 'right' + ? 'right' + : 'left' + } + style={{ + line: { + strokeWidth: 3, + stroke: '#f00', + opacity: 1, + }, + }} + marker={} + /> + ); + })} ); } function getFilteredLayers(layers: LayerArgs[], data: LensMultiTable) { - return layers.filter(({ layerId, xAccessor, accessors, splitAccessor }) => { + return layers.filter(({ layerId, layerType, xAccessor, accessors, splitAccessor }) => { return !( + layerType === 'threshold' || !accessors.length || !data.tables[layerId] || data.tables[layerId].rows.length === 0 || diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index 331e27a8efdb0..1441d11aa6ca9 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -248,6 +248,8 @@ export const buildExpression = ( })) : [], seriesType: [layer.seriesType], + layerType: [layer.layerType || 'data'], + thresholdAxis: layer.thresholdAxis ? [layer.thresholdAxis] : [], accessors: layer.accessors, columnToLabel: [JSON.stringify(columnToLabel)], ...(layer.palette diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index 126be41e7b129..628a26b236b0c 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -302,6 +302,14 @@ export const layerConfig: ExpressionFunctionDefinition< ], help: 'The type of chart to display.', }, + layerType: { + types: ['string'], + options: ['data', 'threshold', 'threshold_const'], + }, + thresholdAxis: { + types: ['string'], + options: ['bottom', 'left', 'right'], + }, xScaleType: { options: ['ordinal', 'linear', 'time'], help: 'The scale type of the x axis', @@ -379,9 +387,12 @@ export interface XYLayerConfig { xAccessor?: string; accessors: string[]; yConfig?: YConfig[]; + thresholdAxis?: 'left' | 'right' | 'bottom'; + constantThresholdValues?: number[]; seriesType: SeriesType; splitAccessor?: string; palette?: PaletteOutput; + layerType?: 'data' | 'threshold' | 'threshold_const'; } export interface ValidLayer extends XYLayerConfig { diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index a6df995513fdf..472191895cfb1 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -104,7 +104,7 @@ export const getXyVisualization = ({ }; }, - appendLayer(state, layerId) { + appendLayer(state, layerId, layerType) { const usedSeriesTypes = _.uniq(state.layers.map((layer) => layer.seriesType)); return { ...state, @@ -112,7 +112,8 @@ export const getXyVisualization = ({ ...state.layers, newLayerState( usedSeriesTypes.length === 1 ? usedSeriesTypes[0] : state.preferredSeriesType, - layerId + layerId, + layerType as 'data' | 'threshold' | 'threshold_const' ), ], }; @@ -196,51 +197,75 @@ export const getXyVisualization = ({ ); } + const isDataLayer = !layer.layerType || layer.layerType === 'data'; + const isHorizontal = isHorizontalChart(state.layers); - return { - groups: [ - { - groupId: 'x', - groupLabel: getAxisName('x', { isHorizontal }), - accessors: layer.xAccessor ? [{ columnId: layer.xAccessor }] : [], - filterOperations: isBucketed, - supportsMoreColumns: !layer.xAccessor, - dataTestSubj: 'lnsXY_xDimensionPanel', - }, - { - groupId: 'y', - groupLabel: getAxisName('y', { isHorizontal }), - accessors: mappedAccessors, - filterOperations: isNumericMetric, - supportsMoreColumns: true, - required: true, - dataTestSubj: 'lnsXY_yDimensionPanel', - enableDimensionEditor: true, - }, - { - groupId: 'breakdown', - groupLabel: i18n.translate('xpack.lens.xyChart.splitSeries', { - defaultMessage: 'Break down by', - }), - accessors: layer.splitAccessor - ? [ - { - columnId: layer.splitAccessor, - triggerIcon: 'colorBy', - palette: paletteService - .get(layer.palette?.name || 'default') - .getColors(10, layer.palette?.params), - }, - ] - : [], - filterOperations: isBucketed, - supportsMoreColumns: !layer.splitAccessor, - dataTestSubj: 'lnsXY_splitDimensionPanel', - required: layer.seriesType.includes('percentage'), - enableDimensionEditor: true, - }, - ], - }; + return isDataLayer + ? { + groups: [ + { + groupId: 'x', + groupLabel: getAxisName('x', { isHorizontal }), + accessors: layer.xAccessor ? [{ columnId: layer.xAccessor }] : [], + filterOperations: isBucketed, + supportsMoreColumns: !layer.xAccessor, + dataTestSubj: 'lnsXY_xDimensionPanel', + }, + { + groupId: 'y', + groupLabel: getAxisName('y', { isHorizontal }), + accessors: mappedAccessors, + filterOperations: isNumericMetric, + supportsMoreColumns: true, + required: true, + dataTestSubj: 'lnsXY_yDimensionPanel', + enableDimensionEditor: true, + }, + { + groupId: 'breakdown', + groupLabel: i18n.translate('xpack.lens.xyChart.splitSeries', { + defaultMessage: 'Break down by', + }), + accessors: layer.splitAccessor + ? [ + { + columnId: layer.splitAccessor, + triggerIcon: 'colorBy', + palette: paletteService + .get(layer.palette?.name || 'default') + .getColors(10, layer.palette?.params), + }, + ] + : [], + filterOperations: isBucketed, + supportsMoreColumns: !layer.splitAccessor, + dataTestSubj: 'lnsXY_splitDimensionPanel', + required: layer.seriesType.includes('percentage'), + enableDimensionEditor: true, + }, + ], + } + : { + groups: [ + { + groupId: 'threshold', + groupLabel: 'Threshold value', + accessors: + layer.layerType === 'threshold' + ? layer.accessors.map((a) => ({ columnId: a })) + : layer.constantThresholdValues + ? layer.constantThresholdValues.map((_a, index) => ({ + columnId: String(index), + })) + : [], + filterOperations: isNumericMetric, + supportsMoreColumns: true, + required: false, + dataTestSubj: '', + enableDimensionEditor: true, + }, + ], + }; }, getMainPalette: (state) => { @@ -257,7 +282,7 @@ export const getXyVisualization = ({ if (groupId === 'x') { newLayer.xAccessor = columnId; } - if (groupId === 'y') { + if (groupId === 'y' || groupId === 'threshold') { newLayer.accessors = [...newLayer.accessors.filter((a) => a !== columnId), columnId]; } if (groupId === 'breakdown') { @@ -296,6 +321,12 @@ export const getXyVisualization = ({ }; }, + getLayerTypes: () => [ + { name: 'data', dataBacked: true }, + { name: 'threshold', dataBacked: true }, + { name: 'threshold_const', dataBacked: false }, + ], + getLayerContextMenuIcon({ state, layerId }) { const layer = state.layers.find((l) => l.layerId === layerId); const visualizationType = visualizationTypes.find((t) => t.id === layer?.seriesType); @@ -510,10 +541,15 @@ function getMessageIdsForDimension(dimension: string, layers: number[], isHorizo return { shortMessage: '', longMessage: '' }; } -function newLayerState(seriesType: SeriesType, layerId: string): XYLayerConfig { +function newLayerState( + seriesType: SeriesType, + layerId: string, + layerType: 'data' | 'threshold' | 'threshold_const' = 'data' +): XYLayerConfig { return { layerId, seriesType, accessors: [], + layerType, }; } diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx index ac08c55eeadbf..4693a841bae8b 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -23,6 +23,7 @@ import { EuiToolTip, EuiIcon, EuiIconTip, + EuiFieldNumber, } from '@elastic/eui'; import { PaletteRegistry } from 'src/plugins/charts/public'; import { @@ -518,6 +519,110 @@ export function DimensionEditor( ); } + if (layer.layerType === 'threshold' && props.groupId === 'threshold') { + return ( + + { + const newMode = id.replace(idPrefix, '') as 'left' | 'right' | 'bottom'; + setState(updateLayer(state, { ...layer, thresholdAxis: newMode }, index)); + }} + /> + + ); + } + + if (layer.layerType === 'threshold_const' && props.groupId === 'threshold') { + return ( + <> + + { + const newMode = id.replace(idPrefix, '') as 'left' | 'right' | 'bottom'; + setState(updateLayer(state, { ...layer, thresholdAxis: newMode }, index)); + }} + /> + + + { + const vals = [...(layer.constantThresholdValues || [])]; + vals[Number(props.accessor)] = Number(e.target.value); + setState(updateLayer(state, { ...layer, constantThresholdValues: vals }, index)); + }} + /> + + + ); + } + return ( <> From 6a4effabc2bafc797abdb456bda3cd1927c695ae Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Sat, 27 Mar 2021 16:26:02 +0100 Subject: [PATCH 2/4] working on all the features --- .../lens/public/xy_visualization/types.ts | 18 ++ .../xy_visualization/xy_config_panel.tsx | 200 ++++++++++++++---- 2 files changed, 181 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index bdaba6ea25f6b..211df9694bc3c 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -305,10 +305,25 @@ export const layerConfig: ExpressionFunctionDefinition< layerType: { types: ['string'], options: ['data', 'threshold'], + help: '', }, thresholdAxis: { types: ['string'], options: ['bottom', 'left', 'right'], + help: '', + }, + lineStyle: { + types: ['string'], + options: ['dashed', 'dotted', 'solid'], + help: '', + }, + lineWidth: { + types: ['number'], + help: '', + }, + icon: { + types: ['string'], + help: '', }, xScaleType: { options: ['ordinal', 'linear', 'time'], @@ -388,6 +403,9 @@ export interface XYLayerConfig { accessors: string[]; yConfig?: YConfig[]; thresholdAxis?: 'left' | 'right' | 'bottom'; + lineStyle?: 'solid' | 'dashed' | 'dotted'; + lineWidth?: number; + icon?: string; constantThresholdValues?: number[]; seriesType: SeriesType; splitAccessor?: string; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx index 5a92e4ec3570a..1253da0c3c2c4 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -20,6 +20,8 @@ import { EuiColorPickerProps, EuiToolTip, EuiIcon, + EuiFieldNumber, + EuiComboBox, } from '@elastic/eui'; import { PaletteRegistry } from 'src/plugins/charts/public'; import { @@ -315,6 +317,51 @@ export const XyToolbar = memo(function XyToolbar(props: VisualizationToolbarProp const idPrefix = htmlIdGenerator()(); +const icons = [ + { value: 'none', label: 'None' }, + { value: 'asterisk', label: 'Asterisk' }, + { value: 'bell', label: 'Bell' }, + { value: 'bolt', label: 'Bolt' }, + { value: 'bug', label: 'Bug' }, + { value: 'editorComment', label: 'Comment' }, + { value: 'alert', label: 'Alert' }, + { value: 'flag', label: 'Flag' }, + { value: 'tag', label: 'Tag' }, +]; + +const IconView = (props: { value?: string; label: string }) => { + if (!props.value) return null; + return ( + + + {` ${props.label}`} + + ); +}; + +const IconSelect = ({ + value, + onChange, +}: { + value?: string; + onChange: (newIcon: string) => void; +}) => { + const selectedIcon = icons.find((option) => value === option.value) || icons[0]; + + return ( + { + onChange(selection[0].value!); + }} + singleSelection={{ asPlainText: true }} + renderOption={IconView} + /> + ); +}; + export function DimensionEditor( props: VisualizationDimensionEditorProps & { formatFactory: FormatFactory; @@ -346,45 +393,124 @@ export function DimensionEditor( if (layer.layerType === 'threshold' && props.groupId === 'threshold') { return ( - - + { - const newMode = id.replace(idPrefix, '') as 'left' | 'right' | 'bottom'; - setState(updateLayer(state, { ...layer, thresholdAxis: newMode }, index)); - }} - /> - + > + { + const newMode = id.replace(idPrefix, '') as 'left' | 'right' | 'bottom'; + setState(updateLayer(state, { ...layer, thresholdAxis: newMode }, index)); + }} + /> + + + { + // TODO proper number input handling + setState(updateLayer(state, { ...layer, lineWidth: Number(e.target.value) }, index)); + }} + /> + + + { + const newMode = id.replace(idPrefix, '') as 'solid' | 'dashed' | 'dotted'; + setState(updateLayer(state, { ...layer, lineStyle: newMode }, index)); + }} + /> + + + + { + setState( + updateLayer( + state, + { ...layer, icon: newIcon === 'none' ? undefined : newIcon }, + index + ) + ); + }} + /> + + ); } From 415ed6b5180ca955c9924c82913d5bae9e3e9551 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 30 Mar 2021 14:58:14 +0200 Subject: [PATCH 3/4] fix some things --- .../config_panel/config_panel.tsx | 184 +++++++++++++----- x-pack/plugins/lens/public/types.ts | 2 +- .../public/xy_visualization/expression.tsx | 83 +++++--- .../public/xy_visualization/to_expression.ts | 4 +- .../lens/public/xy_visualization/types.ts | 41 ++-- .../public/xy_visualization/visualization.tsx | 41 +++- .../xy_visualization/xy_config_panel.tsx | 54 +++-- 7 files changed, 278 insertions(+), 131 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx index b3ec943dffd7a..2e19d30d3c16b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -7,8 +7,16 @@ import './config_panel.scss'; -import React, { useMemo, memo } from 'react'; -import { EuiFlexItem, EuiToolTip, EuiButton, EuiForm } from '@elastic/eui'; +import React, { useMemo, memo, useState } from 'react'; +import { + EuiFlexItem, + EuiToolTip, + EuiButton, + EuiForm, + EuiContextMenuPanel, + EuiContextMenuItem, + EuiPopover, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Visualization } from '../../../types'; import { LayerPanel } from './layer_panel'; @@ -99,6 +107,16 @@ export function LayerPanels( const datasourcePublicAPIs = props.framePublicAPI.datasourceLayers; + const appendableLayers = + activeVisualization.appendLayer && visualizationState + ? (activeVisualization.getLayerTypes && + activeVisualization?.getLayerTypes(visualizationState)) || [ + { name: 'default', label: '' }, + ] + : []; + + const [popoverOpen, setPopoverOpen] = useState(false); + return ( {layerIds.map((layerId, layerIndex) => @@ -133,56 +151,126 @@ export function LayerPanels( /> ) : null )} - {activeVisualization.appendLayer && - visualizationState && - ( - (activeVisualization.getLayerTypes && - activeVisualization?.getLayerTypes(visualizationState)) || [{ name: 'default' }] - ).map(({ name: layerType }) => ( - - + + { + const id = generateId(); + dispatch({ + type: 'UPDATE_STATE', + subType: 'ADD_LAYER', + updater: (state) => + appendLayer({ + activeVisualization, + generateId: () => id, + trackUiEvent, + activeDatasource: datasourceMap[activeDatasourceId], + state, + layerType: appendableLayers[0].name, + }), + }); + setNextFocusedLayerId(id); + }} + iconType="plusInCircleFilled" > - + + + )} + {appendableLayers.length > 1 && ( + + { + setPopoverOpen(false); + }} + button={ + { - const id = generateId(); - dispatch({ - type: 'UPDATE_STATE', - subType: 'ADD_LAYER', - updater: (state) => - appendLayer({ - activeVisualization, - generateId: () => id, - trackUiEvent, - activeDatasource: datasourceMap[activeDatasourceId], - state, - layerType: activeVisualization.getLayerTypes ? layerType : undefined, - }), - }); - setNextFocusedLayerId(id); - }} - iconType="plusInCircleFilled" + position="bottom" > - {layerType} - - - - ))} + { + setPopoverOpen(!popoverOpen); + }} + iconType="plusInCircleFilled" + > + {i18n.translate('xpack.lens.xyChart.addLayerButton', { + defaultMessage: 'Add layer', + })} + + + } + > + { + return ( + { + setPopoverOpen(false); + const id = generateId(); + dispatch({ + type: 'UPDATE_STATE', + subType: 'ADD_LAYER', + updater: (state) => + appendLayer({ + activeVisualization, + generateId: () => id, + trackUiEvent, + activeDatasource: datasourceMap[activeDatasourceId], + state, + layerType: layerType.name, + }), + }); + setNextFocusedLayerId(id); + }} + > + {layerType.label} + + ); + })} + /> + + + )} ); } diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 1200f9ef6ac8e..ec304bdd8441b 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -592,7 +592,7 @@ export interface Visualization { /** Track added layers in internal state */ appendLayer?: (state: T, layerId: string, layerType?: string) => T; /* if set, allows adding of multiple types of layers */ - getLayerTypes?: (state: T) => Array<{ name: string }>; + getLayerTypes?: (state: T) => Array<{ name: string; label: string }>; /** * For consistency across different visualizations, the dimension configuration UI is standardized diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 5f371ba73d6a9..7378940bd261a 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -840,38 +840,59 @@ export function XYChart({ }) )} - {thresholdLayers.map((thresholdLayer) => { - return ( - - data.tables[thresholdLayer.layerId].rows.map((row) => ({ - dataValue: row[accessor], - })) - )} - groupId={ - thresholdLayer.thresholdAxis === 'bottom' - ? undefined - : thresholdLayer.thresholdAxis === 'right' - ? 'right' - : 'left' + {thresholdLayers.flatMap((thresholdLayer) => { + if (!thresholdLayer.yConfig) { + return []; + } + const columnToLabelMap: Record = thresholdLayer.columnToLabel + ? JSON.parse(thresholdLayer.columnToLabel) + : {}; + return thresholdLayer.yConfig.map((yConfig) => { + const table = data.tables[thresholdLayer.layerId]; + const formatter = formatFactory( + table?.columns.find((column) => column.id === yConfig.forAccessor)?.meta?.params || { + id: 'number', } - style={{ - line: { - strokeWidth: 3, - stroke: '#f00', - opacity: 1, - }, - }} - marker={} - /> - ); + ); + return ( + ({ + dataValue: row[yConfig.forAccessor], + header: columnToLabelMap[yConfig.forAccessor], + details: formatter.convert(row[yConfig.forAccessor]), + }))} + groupId={ + yConfig.axisMode === 'bottom' + ? undefined + : yConfig.axisMode === 'right' + ? 'right' + : 'left' + } + style={{ + line: { + // TODO add line mode here + strokeWidth: yConfig.lineWidth || 1, + stroke: yConfig.color || '#f00', + dash: + yConfig.lineStyle === 'dashed' + ? [(yConfig.lineWidth || 1) * 3, yConfig.lineWidth || 1] + : yConfig.lineStyle === 'dotted' + ? [yConfig.lineWidth || 1, yConfig.lineWidth || 1] + : undefined, + opacity: 1, + }, + }} + marker={yConfig.icon ? : undefined} + /> + ); + }); })} ); diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index 524b69b45f769..049f25ed8a3dc 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -243,6 +243,9 @@ export const buildExpression = ( forAccessor: [yConfig.forAccessor], axisMode: yConfig.axisMode ? [yConfig.axisMode] : [], color: yConfig.color ? [yConfig.color] : [], + lineStyle: yConfig.lineStyle ? [yConfig.lineStyle] : [], + lineWidth: yConfig.lineWidth ? [yConfig.lineWidth] : [], + icon: yConfig.icon ? [yConfig.icon] : [], }, }, ], @@ -250,7 +253,6 @@ export const buildExpression = ( : [], seriesType: [layer.seriesType], layerType: [layer.layerType || 'data'], - thresholdAxis: layer.thresholdAxis ? [layer.thresholdAxis] : [], accessors: layer.accessors, columnToLabel: [JSON.stringify(columnToLabel)], ...(layer.palette diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index 211df9694bc3c..b36c66042fa3b 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -257,6 +257,19 @@ export const yAxisConfig: ExpressionFunctionDefinition< types: ['string'], help: 'The color of the series', }, + lineStyle: { + types: ['string'], + options: ['dashed', 'dotted', 'solid'], + help: '', + }, + lineWidth: { + types: ['number'], + help: '', + }, + icon: { + types: ['string'], + help: '', + }, }, fn: function fn(input: unknown, args: YConfig) { return { @@ -307,24 +320,6 @@ export const layerConfig: ExpressionFunctionDefinition< options: ['data', 'threshold'], help: '', }, - thresholdAxis: { - types: ['string'], - options: ['bottom', 'left', 'right'], - help: '', - }, - lineStyle: { - types: ['string'], - options: ['dashed', 'dotted', 'solid'], - help: '', - }, - lineWidth: { - types: ['number'], - help: '', - }, - icon: { - types: ['string'], - help: '', - }, xScaleType: { options: ['ordinal', 'linear', 'time'], help: 'The scale type of the x axis', @@ -386,7 +381,7 @@ export type SeriesType = | 'area_stacked' | 'area_percentage_stacked'; -export type YAxisMode = 'auto' | 'left' | 'right'; +export type YAxisMode = 'auto' | 'left' | 'right' | 'bottom'; export type ValueLabelConfig = 'hide' | 'inside' | 'outside'; @@ -394,6 +389,9 @@ export interface YConfig { forAccessor: string; axisMode?: YAxisMode; color?: string; + lineStyle?: 'solid' | 'dashed' | 'dotted'; + lineWidth?: number; + icon?: string; } export interface XYLayerConfig { @@ -402,11 +400,6 @@ export interface XYLayerConfig { xAccessor?: string; accessors: string[]; yConfig?: YConfig[]; - thresholdAxis?: 'left' | 'right' | 'bottom'; - lineStyle?: 'solid' | 'dashed' | 'dotted'; - lineWidth?: number; - icon?: string; - constantThresholdValues?: number[]; seriesType: SeriesType; splitAccessor?: string; palette?: PaletteOutput; diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index 690ead657d3ce..354d11c85bf39 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -250,14 +250,7 @@ export const getXyVisualization = ({ { groupId: 'threshold', groupLabel: 'Threshold value', - accessors: - layer.layerType === 'threshold' - ? layer.accessors.map((a) => ({ columnId: a })) - : layer.constantThresholdValues - ? layer.constantThresholdValues.map((_a, index) => ({ - columnId: String(index), - })) - : [], + accessors: layer.accessors.map((a) => ({ columnId: a })), filterOperations: isNumericMetric, supportsMoreColumns: true, required: false, @@ -284,6 +277,23 @@ export const getXyVisualization = ({ } if (groupId === 'y' || groupId === 'threshold') { newLayer.accessors = [...newLayer.accessors.filter((a) => a !== columnId), columnId]; + if (groupId === 'threshold') { + const hasYConfig = newLayer.yConfig?.some((yConfig) => yConfig.forAccessor === columnId); + if (!hasYConfig) { + newLayer.yConfig = [ + ...(newLayer.yConfig || []), + { + forAccessor: columnId, + // todo auto axis mode as well? + axisMode: 'left', + color: '#ddd', + icon: undefined, + lineStyle: 'solid', + lineWidth: 1, + }, + ]; + } + } } if (groupId === 'breakdown') { newLayer.splitAccessor = columnId; @@ -321,7 +331,20 @@ export const getXyVisualization = ({ }; }, - getLayerTypes: () => [{ name: 'data' }, { name: 'threshold' }], + getLayerTypes: () => [ + { + name: 'data', + label: i18n.translate('xpack.lens.xyChart.addLayerLabel', { + defaultMessage: 'Add chart layer', + }), + }, + { + name: 'threshold', + label: i18n.translate('xpack.lens.xyChart.addThresholdLayerLabel', { + defaultMessage: 'Add threshold layer', + }), + }, + ], getLayerContextMenuIcon({ state, layerId }) { const layer = state.layers.find((l) => l.layerId === layerId); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx index 1253da0c3c2c4..46c93fe1e3e39 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -30,7 +30,14 @@ import { VisualizationDimensionEditorProps, FormatFactory, } from '../types'; -import { State, SeriesType, visualizationTypes, YAxisMode, AxesSettingsConfig } from './types'; +import { + State, + SeriesType, + visualizationTypes, + YAxisMode, + AxesSettingsConfig, + YConfig, +} from './types'; import { isHorizontalChart, isHorizontalSeries, getSeriesColor } from './state_helpers'; import { trackUiEvent } from '../lens_ui_telemetry'; import { LegendSettingsPopover } from '../shared_components'; @@ -391,7 +398,24 @@ export function DimensionEditor( ); } + function setYConfig(yConfig: Partial) { + const newYConfigs = [...(layer.yConfig || [])]; + const existingIndex = newYConfigs.findIndex( + (yAxisConfig) => yAxisConfig.forAccessor === accessor + ); + if (existingIndex !== -1) { + newYConfigs[existingIndex] = { ...newYConfigs[existingIndex], ...yConfig }; + } else { + newYConfigs.push({ + forAccessor: accessor, + ...yConfig, + }); + } + setState(updateLayer(state, { ...layer, yConfig: newYConfigs }, index)); + } + if (layer.layerType === 'threshold' && props.groupId === 'threshold') { + const currentYConfig = layer.yConfig?.find((yConfig) => yConfig.forAccessor === props.accessor); return ( <> { const newMode = id.replace(idPrefix, '') as 'left' | 'right' | 'bottom'; - setState(updateLayer(state, { ...layer, thresholdAxis: newMode }, index)); + setYConfig({ axisMode: newMode }); }} /> @@ -443,10 +467,10 @@ export function DimensionEditor( { // TODO proper number input handling - setState(updateLayer(state, { ...layer, lineWidth: Number(e.target.value) }, index)); + setYConfig({ lineWidth: Number(e.target.value) }); }} /> @@ -482,14 +506,14 @@ export function DimensionEditor( 'data-test-subj': 'lnsXY_line_style_dotted', }, ]} - idSelected={`${idPrefix}${layer.lineStyle || 'solid'}`} + idSelected={`${idPrefix}${currentYConfig?.lineStyle || 'solid'}`} onChange={(id) => { const newMode = id.replace(idPrefix, '') as 'solid' | 'dashed' | 'dotted'; - setState(updateLayer(state, { ...layer, lineStyle: newMode }, index)); + setYConfig({ lineStyle: newMode }); }} /> - + { - setState( - updateLayer( - state, - { ...layer, icon: newIcon === 'none' ? undefined : newIcon }, - index - ) - ); + setYConfig({ icon: newIcon }); }} /> @@ -608,9 +626,11 @@ const ColorPicker = ({ frame, formatFactory, paletteService, + allowClearing = true, }: VisualizationDimensionEditorProps & { formatFactory: FormatFactory; paletteService: PaletteRegistry; + allowClearing?: boolean; }) => { const index = state.layers.findIndex((l) => l.layerId === layerId); const layer = state.layers[index]; @@ -676,7 +696,7 @@ const ColorPicker = ({ Date: Tue, 30 Mar 2021 15:00:47 +0200 Subject: [PATCH 4/4] add color indicators --- .../plugins/lens/public/xy_visualization/visualization.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index 354d11c85bf39..11842faaf9bc2 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -250,7 +250,11 @@ export const getXyVisualization = ({ { groupId: 'threshold', groupLabel: 'Threshold value', - accessors: layer.accessors.map((a) => ({ columnId: a })), + accessors: layer.accessors.map((a) => ({ + columnId: a, + color: layer.yConfig?.find((yConfig) => yConfig.forAccessor === a)?.color, + triggerIcon: 'color', + })), filterOperations: isNumericMetric, supportsMoreColumns: true, required: false,