From 0d81686c436bfa23c7cb70215e3fc1abe568f57f Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 23 Nov 2021 20:04:52 +0100 Subject: [PATCH 01/43] add gauge chart expression --- .../expressions/gauge_chart/gauge_chart.ts | 134 ++++++++++++++++++ .../common/expressions/gauge_chart/index.ts | 9 ++ .../common/expressions/gauge_chart/types.ts | 68 +++++++++ .../plugins/lens/common/expressions/index.ts | 1 + 4 files changed, 212 insertions(+) create mode 100644 x-pack/plugins/lens/common/expressions/gauge_chart/gauge_chart.ts create mode 100644 x-pack/plugins/lens/common/expressions/gauge_chart/index.ts create mode 100644 x-pack/plugins/lens/common/expressions/gauge_chart/types.ts diff --git a/x-pack/plugins/lens/common/expressions/gauge_chart/gauge_chart.ts b/x-pack/plugins/lens/common/expressions/gauge_chart/gauge_chart.ts new file mode 100644 index 0000000000000..b8ca6d066de00 --- /dev/null +++ b/x-pack/plugins/lens/common/expressions/gauge_chart/gauge_chart.ts @@ -0,0 +1,134 @@ +/* + * 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 type { ExpressionFunctionDefinition } from '../../../../../../src/plugins/expressions/common'; +import type { LensMultiTable } from '../../types'; +import { GaugeExpressionArgs, GAUGE_FUNCTION, GAUGE_FUNCTION_RENDERER } from './types'; + +export interface GaugeExpressionProps { + data: LensMultiTable; + args: GaugeExpressionArgs; +} +export interface GaugeRender { + type: 'render'; + as: typeof GAUGE_FUNCTION_RENDERER; + value: GaugeExpressionProps; +} + +export const gauge: ExpressionFunctionDefinition< + typeof GAUGE_FUNCTION, + LensMultiTable, + GaugeExpressionArgs, + GaugeRender +> = { + name: GAUGE_FUNCTION, + type: 'render', + help: i18n.translate('xpack.lens.gauge.expressionHelpLabel', { + defaultMessage: 'Gauge renderer', + }), + args: { + title: { + types: ['string'], + help: i18n.translate('xpack.lens.gauge.title.help', { + defaultMessage: 'Saved gauge title', + }), + }, + shape: { + types: ['string'], + options: ['horizontalBullet', 'verticalBullet'], + help: i18n.translate('xpack.lens.gauge.shape.help', { + defaultMessage: 'Type of gauge chart', + }), + }, + description: { + types: ['string'], + help: i18n.translate('xpack.lens.gauge.description.help', { + defaultMessage: 'Saved gauge description', + }), + }, + metricAccessor: { + types: ['string'], + help: i18n.translate('xpack.lens.gauge.metricAccessor.help', { + defaultMessage: 'Current value', + }), + }, + minAccessor: { + types: ['string'], + help: i18n.translate('xpack.lens.gauge.minAccessor.help', { + defaultMessage: 'Minimum value', + }), + }, + maxAccessor: { + types: ['string'], + help: i18n.translate('xpack.lens.gauge.maxAccessor.help', { + defaultMessage: 'Maximum value', + }), + }, + goalAccessor: { + types: ['string'], + help: i18n.translate('xpack.lens.gauge.goalAccessor.help', { + defaultMessage: 'Goal value', + }), + }, + colorMode: { + types: ['string'], + default: 'none', + options: ['none', 'palette'], + help: i18n.translate('xpack.lens.gauge.colorMode.help', { + defaultMessage: 'Which part of gauge to color', + }), + }, + palette: { + types: ['palette'], + help: i18n.translate('xpack.lens.metric.palette.help', { + defaultMessage: 'Provides colors for the values', + }), + }, + ticksPosition: { + types: ['string'], + options: ['auto', 'bands'], + help: i18n.translate('xpack.lens.gaugeChart.config.ticksPosition.help', { + defaultMessage: 'Specifies the placement of ticks', + }), + required: true, + }, + visTitle: { + types: ['string'], + help: i18n.translate('xpack.lens.gaugeChart.config.title.help', { + defaultMessage: 'Specifies the title of the gauge chart displayed inside the chart.', + }), + required: false, + }, + visTitleMode: { + types: ['string'], + options: ['none', 'auto', 'custom'], + help: i18n.translate('xpack.lens.gaugeChart.config.visTitleMode.help', { + defaultMessage: 'Specifies the mode of title', + }), + required: true, + }, + subtitle: { + types: ['string'], + help: i18n.translate('xpack.lens.gaugeChart.config.subtitle.help', { + defaultMessage: 'Specifies the Subtitle of the gauge chart', + }), + required: false, + }, + }, + inputTypes: ['lens_multitable'], + fn(data: LensMultiTable, args: GaugeExpressionArgs) { + return { + type: 'render', + as: GAUGE_FUNCTION_RENDERER, + value: { + data, + args, + }, + }; + }, +}; diff --git a/x-pack/plugins/lens/common/expressions/gauge_chart/index.ts b/x-pack/plugins/lens/common/expressions/gauge_chart/index.ts new file mode 100644 index 0000000000000..10bb76b079417 --- /dev/null +++ b/x-pack/plugins/lens/common/expressions/gauge_chart/index.ts @@ -0,0 +1,9 @@ +/* + * 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 * from './gauge_chart'; +export * from './types'; diff --git a/x-pack/plugins/lens/common/expressions/gauge_chart/types.ts b/x-pack/plugins/lens/common/expressions/gauge_chart/types.ts new file mode 100644 index 0000000000000..571489bd20907 --- /dev/null +++ b/x-pack/plugins/lens/common/expressions/gauge_chart/types.ts @@ -0,0 +1,68 @@ +/* + * 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, + CustomPaletteState, +} from '../../../../../../src/plugins/charts/common'; +import type { CustomPaletteParams, LayerType } from '../../types'; + +export const GAUGE_FUNCTION = 'lens_gauge'; +export const GAUGE_FUNCTION_RENDERER = 'lens_gauge_renderer'; + +export const GaugeShapes = { + horizontalBullet: 'horizontalBullet', + verticalBullet: 'verticalBullet', +} as const; + +export const GaugeTicksPositions = { + auto: 'auto', + bands: 'bands', +} as const; + +export const GaugeTitleModes = { + auto: 'auto', + custom: 'custom', + none: 'none', +} as const; + +export type GaugeType = 'gauge'; +export type GaugeColorMode = 'none' | 'palette'; +export type GaugeShape = keyof typeof GaugeShapes; +export type GaugeTitleMode = keyof typeof GaugeTitleModes; +export type GaugeTicksPosition = keyof typeof GaugeTicksPositions; + +export interface SharedGaugeLayerState { + metricAccessor?: string; + minAccessor?: string; + maxAccessor?: string; + goalAccessor?: string; + ticksPosition: GaugeTicksPosition; + visTitleMode: GaugeTitleMode; + visTitle?: string; + subtitle?: string; + colorMode?: GaugeColorMode; + palette?: PaletteOutput; + shape: GaugeShape; +} + +export type GaugeLayerState = SharedGaugeLayerState & { + layerId: string; + layerType: LayerType; +}; + +export type GaugeVisualizationState = GaugeLayerState & { + shape: GaugeShape; +}; + +export type GaugeExpressionArgs = SharedGaugeLayerState & { + title?: string; + description?: string; + shape: GaugeShape; + colorMode: GaugeColorMode; + palette: PaletteOutput; +}; diff --git a/x-pack/plugins/lens/common/expressions/index.ts b/x-pack/plugins/lens/common/expressions/index.ts index 344d22de6461b..76e18352b4cbc 100644 --- a/x-pack/plugins/lens/common/expressions/index.ts +++ b/x-pack/plugins/lens/common/expressions/index.ts @@ -12,6 +12,7 @@ export * from './merge_tables'; export * from './time_scale'; export * from './datatable'; export * from './heatmap_chart'; +export * from './gauge_chart'; export * from './metric_chart'; export * from './pie_chart'; export * from './xy_chart'; From ff06e33c5f9c4e3e3fd0d906f219e6d791f6680d Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 23 Nov 2021 20:10:51 +0100 Subject: [PATCH 02/43] excluding staticValue from suggestion for other visualizations --- .../lens/public/datatable_visualization/visualization.tsx | 3 ++- .../lens/public/heatmap_visualization/suggestions.ts | 7 ++++--- .../lens/public/heatmap_visualization/visualization.tsx | 2 +- .../lens/public/indexpattern_datasource/indexpattern.tsx | 3 ++- .../operations/definitions/static_value.tsx | 1 + .../lens/public/metric_visualization/metric_suggestions.ts | 3 ++- .../plugins/lens/public/pie_visualization/suggestions.ts | 3 ++- .../lens/public/pie_visualization/visualization.tsx | 2 +- x-pack/plugins/lens/public/types.ts | 2 ++ .../plugins/lens/public/xy_visualization/visualization.tsx | 3 ++- .../plugins/lens/public/xy_visualization/xy_suggestions.ts | 5 ++++- 11 files changed, 23 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index 139d85b51cee7..ca8cbbf067b06 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -105,7 +105,8 @@ export const getDatatableVisualization = ({ if ( keptLayerIds.length > 1 || (keptLayerIds.length && table.layerId !== keptLayerIds[0]) || - (state && table.changeType === 'unchanged') + (state && table.changeType === 'unchanged') || + table.columns.some((col) => col.operation.isStaticValue) ) { return []; } diff --git a/x-pack/plugins/lens/public/heatmap_visualization/suggestions.ts b/x-pack/plugins/lens/public/heatmap_visualization/suggestions.ts index ebe93419edce6..aeddb8473fa98 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/suggestions.ts +++ b/x-pack/plugins/lens/public/heatmap_visualization/suggestions.ts @@ -19,9 +19,10 @@ export const getSuggestions: Visualization['getSugges keptLayerIds, }) => { if ( - state?.shape === CHART_SHAPES.HEATMAP && - (state.xAccessor || state.yAccessor || state.valueAccessor) && - table.changeType !== 'extended' + (state?.shape === CHART_SHAPES.HEATMAP && + (state.xAccessor || state.yAccessor || state.valueAccessor) && + table.changeType !== 'extended') || + table.columns.some((col) => col.operation.isStaticValue) ) { return []; } diff --git a/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx b/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx index 626c9975141c4..fb8516babfe74 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx @@ -58,7 +58,7 @@ function getAxisName(axis: 'x' | 'y') { } export const isBucketed = (op: OperationMetadata) => op.isBucketed && op.scale === 'ordinal'; -const isNumericMetric = (op: OperationMetadata) => op.dataType === 'number'; +const isNumericMetric = (op: OperationMetadata) => op.dataType === 'number' && !op.isStaticValue; export const filterOperationsAxis = (op: OperationMetadata) => isBucketed(op) || op.scale === 'interval'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 402371930b93e..6179f34226125 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -68,12 +68,13 @@ export function columnToOperation( column: GenericIndexPatternColumn, uniqueLabel?: string ): Operation { - const { dataType, label, isBucketed, scale } = column; + const { dataType, label, isBucketed, scale, operationType } = column; return { dataType: normalizeOperationDataType(dataType), isBucketed, scale, label: uniqueLabel || label, + isStaticValue: operationType === 'static_value', }; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx index 45a35d18873fc..d10be2cae6a83 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx @@ -81,6 +81,7 @@ export const staticValueOperation: OperationDefinition< dataType: 'number', isBucketed: false, scale: 'ratio', + isStaticValue: true, }; }, toExpression: (layer, columnId) => { diff --git a/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.ts b/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.ts index 3d6b2683b4ad2..e8a377169bb97 100644 --- a/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.ts +++ b/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.ts @@ -26,7 +26,8 @@ export function getSuggestions({ keptLayerIds.length > 1 || (keptLayerIds.length && table.layerId !== keptLayerIds[0]) || table.columns.length !== 1 || - table.columns[0].operation.dataType !== 'number' + table.columns[0].operation.dataType !== 'number' || + table.columns[0].operation.isStaticValue ) { return []; } diff --git a/x-pack/plugins/lens/public/pie_visualization/suggestions.ts b/x-pack/plugins/lens/public/pie_visualization/suggestions.ts index 30cd63752f420..ccee4f9ede6bc 100644 --- a/x-pack/plugins/lens/public/pie_visualization/suggestions.ts +++ b/x-pack/plugins/lens/public/pie_visualization/suggestions.ts @@ -26,7 +26,8 @@ function shouldReject({ table, keptLayerIds, state }: SuggestionRequest 1 || (keptLayerIds.length && table.layerId !== keptLayerIds[0]) || table.changeType === 'reorder' || - shouldRejectIntervals + shouldRejectIntervals || + table.columns.some((col) => col.operation.isStaticValue) ); } diff --git a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx index 2a930e527d72e..9d3026c84366f 100644 --- a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx @@ -40,7 +40,7 @@ function newLayerState(layerId: string): PieLayerState { const bucketedOperations = (op: OperationMetadata) => op.isBucketed; const numberMetricOperations = (op: OperationMetadata) => - !op.isBucketed && op.dataType === 'number'; + !op.isBucketed && op.dataType === 'number' && !op.isStaticValue; const applyPaletteToColumnConfig = ( columns: AccessorConfig[], diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 35620982447a5..356d589070cb7 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -430,6 +430,8 @@ export interface OperationMetadata { // TODO currently it's not possible to differentiate between a field from a raw // document and an aggregated metric which might be handy in some cases. Once we // introduce a raw document datasource, this should be considered here. + + isStaticValue?: boolean; } export interface VisualizationConfigProps { diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index d536a18b6ab79..5e160e5a492ec 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -46,7 +46,8 @@ import { groupAxesByType } from './axes_configuration'; const defaultIcon = LensIconChartBarStacked; const defaultSeriesType = 'bar_stacked'; -const isNumericMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; +const isNumericMetric = (op: OperationMetadata) => + !op.isBucketed && op.dataType === 'number' && !op.isStaticValue; const isBucketed = (op: OperationMetadata) => op.isBucketed; function getVisualizationType(state: State): VisualizationType | 'mixed' { diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts index 2e275c455a4d0..0fa07f4f9ebb6 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -69,7 +69,10 @@ export function getSuggestions({ }); } - if (incompleteTable && state && !subVisualizationId) { + if ( + (incompleteTable && state && !subVisualizationId) || + table.columns.some((col) => col.operation.isStaticValue) + ) { // reject incomplete configurations if the sub visualization isn't specifically requested // this allows to switch chart types via switcher with incomplete configurations, but won't // cause incomplete suggestions getting auto applied on dropped fields From 48b0b02eae9c649b04052ea53268c64f6cad6f83 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 23 Nov 2021 20:12:45 +0100 Subject: [PATCH 03/43] adding prioritized operation for visualization groups (when dropping, it should be chosen over the default) --- .../dimension_panel/droppable/get_drop_props.ts | 10 ++++++++-- .../dimension_panel/droppable/on_drop_handler.ts | 11 ++++++++++- x-pack/plugins/lens/public/types.ts | 2 ++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts index 08361490cdc2c..0882b243c6b3d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts @@ -36,7 +36,8 @@ const operationLabels = getOperationDisplay(); export function getNewOperation( field: IndexPatternField | undefined | false, filterOperations: (meta: OperationMetadata) => boolean, - targetColumn: GenericIndexPatternColumn + targetColumn: GenericIndexPatternColumn, + prioritizedOperation?: GenericIndexPatternColumn['operationType'] ) { if (!field) { return; @@ -47,7 +48,12 @@ export function getNewOperation( } // Detects if we can change the field only, otherwise change field + operation const shouldOperationPersist = targetColumn && newOperations.includes(targetColumn.operationType); - return shouldOperationPersist ? targetColumn.operationType : newOperations[0]; + if (shouldOperationPersist) { + return targetColumn.operationType; + } + const existsPrioritizedOperation = + prioritizedOperation && newOperations.includes(prioritizedOperation); + return existsPrioritizedOperation ? prioritizedOperation : newOperations[0]; } export function getField( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts index b518f667a0bfb..0c538d0fc9486 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts @@ -70,10 +70,19 @@ function onFieldDrop(props: DropHandlerProps) { dimensionGroups, } = props; + const prioritizedOperation = dimensionGroups.find( + (g) => g.groupId === groupId + )?.prioritizedOperation; + const layer = state.layers[layerId]; const indexPattern = state.indexPatterns[layer.indexPatternId]; const targetColumn = layer.columns[columnId]; - const newOperation = getNewOperation(droppedItem.field, filterOperations, targetColumn); + const newOperation = getNewOperation( + droppedItem.field, + filterOperations, + targetColumn, + prioritizedOperation + ); if (!isDraggedField(droppedItem) || !newOperation) { return false; diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 356d589070cb7..31359054a8913 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -216,6 +216,7 @@ export interface Datasource { props: DatasourceDimensionDropProps & { groupId: string; dragging: DragContextState['dragging']; + prioritizedOperation?: string; } ) => { dropTypes: DropType[]; nextLabel?: string } | undefined; onDrop: (props: DatasourceDimensionDropHandlerProps) => false | true | { deleted: string }; @@ -476,6 +477,7 @@ export type VisualizationDimensionGroupConfig = SharedDimensionProps & { required?: boolean; requiredMinDimensionCount?: number; dataTestSubj?: string; + prioritizedOperation?: string; /** * When the dimension editor is enabled for this group, all dimensions in the group From 63eea38a39a9141aaa19ef71d7fa6beadfa99018 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 23 Nov 2021 20:13:42 +0100 Subject: [PATCH 04/43] changing the group for metric in chart switcher --- .../plugins/lens/public/metric_visualization/visualization.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx index 857bfa676faf4..87e51378377aa 100644 --- a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx @@ -90,7 +90,7 @@ export const getMetricVisualization = ({ defaultMessage: 'Metric', }), groupLabel: i18n.translate('xpack.lens.metric.groupLabel', { - defaultMessage: 'Single value', + defaultMessage: 'Goal and single value', }), sortPriority: 3, }, From 9599cd13197e30cf9963a1f3bc2f386facd9c0be Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 23 Nov 2021 20:15:08 +0100 Subject: [PATCH 05/43] only access the activeData if exists (avoids error in console) --- x-pack/plugins/lens/public/state_management/lens_slice.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index af9897581fcf4..67b7ccac97478 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -616,7 +616,9 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { visualizationState, framePublicAPI: { // any better idea to avoid `as`? - activeData: state.activeData as TableInspectorAdapter, + activeData: state.activeData + ? (current(state.activeData) as TableInspectorAdapter) + : undefined, datasourceLayers: getDatasourceLayers(state.datasourceStates, datasourceMap), }, activeVisualization, @@ -653,7 +655,9 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { visualizationState: state.visualization.state, framePublicAPI: { // any better idea to avoid `as`? - activeData: state.activeData as TableInspectorAdapter, + activeData: state.activeData + ? (current(state.activeData) as TableInspectorAdapter) + : undefined, datasourceLayers: getDatasourceLayers(state.datasourceStates, datasourceMap), }, activeVisualization, From 1dda8b59232a28f9ed8b301f4584b1fab469eed6 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 23 Nov 2021 20:36:21 +0100 Subject: [PATCH 06/43] Adding gauge chart --- .../lens/public/assets/chart_gauge.tsx | 66 +++ x-pack/plugins/lens/public/async_services.ts | 2 + x-pack/plugins/lens/public/expressions.ts | 2 + x-pack/plugins/lens/public/plugin.ts | 5 + .../lens/public/shared_components/index.ts | 1 + .../public/shared_components/vis_label.tsx | 99 ++++ .../visualizations/gauge/chart_component.tsx | 245 +++++++++ .../public/visualizations/gauge/constants.ts | 16 + .../gauge/dimension_editor.scss | 3 + .../visualizations/gauge/dimension_editor.tsx | 189 +++++++ .../visualizations/gauge/expression.tsx | 60 +++ .../gauge/gauge_visualization.ts | 9 + .../public/visualizations/gauge/index.scss | 13 + .../lens/public/visualizations/gauge/index.ts | 65 +++ .../visualizations/gauge/palette_config.tsx | 23 + .../visualizations/gauge/suggestions.ts | 100 ++++ .../toolbar_component/gauge_config_panel.scss | 3 + .../gauge/toolbar_component/index.tsx | 128 +++++ .../lens/public/visualizations/gauge/utils.ts | 81 +++ .../visualizations/gauge/visualization.tsx | 470 ++++++++++++++++++ 20 files changed, 1580 insertions(+) create mode 100644 x-pack/plugins/lens/public/assets/chart_gauge.tsx create mode 100644 x-pack/plugins/lens/public/shared_components/vis_label.tsx create mode 100644 x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx create mode 100644 x-pack/plugins/lens/public/visualizations/gauge/constants.ts create mode 100644 x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.scss create mode 100644 x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx create mode 100644 x-pack/plugins/lens/public/visualizations/gauge/expression.tsx create mode 100644 x-pack/plugins/lens/public/visualizations/gauge/gauge_visualization.ts create mode 100644 x-pack/plugins/lens/public/visualizations/gauge/index.scss create mode 100644 x-pack/plugins/lens/public/visualizations/gauge/index.ts create mode 100644 x-pack/plugins/lens/public/visualizations/gauge/palette_config.tsx create mode 100644 x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts create mode 100644 x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/gauge_config_panel.scss create mode 100644 x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx create mode 100644 x-pack/plugins/lens/public/visualizations/gauge/utils.ts create mode 100644 x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx diff --git a/x-pack/plugins/lens/public/assets/chart_gauge.tsx b/x-pack/plugins/lens/public/assets/chart_gauge.tsx new file mode 100644 index 0000000000000..f9272d4a77961 --- /dev/null +++ b/x-pack/plugins/lens/public/assets/chart_gauge.tsx @@ -0,0 +1,66 @@ +/* + * 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 { EuiIconProps } from '@elastic/eui'; +import React from 'react'; + +export const LensIconChartGaugeHorizontal = ({ + title, + titleId, + ...props +}: Omit) => ( + + {title ? {title} : null} + + + +); + +export const LensIconChartGaugeVertical = ({ + title, + titleId, + ...props +}: Omit) => ( + + {title ? {title} : null} + + + + + + + + +); diff --git a/x-pack/plugins/lens/public/async_services.ts b/x-pack/plugins/lens/public/async_services.ts index 9fac9a143c3b3..bbb4faf55e1e9 100644 --- a/x-pack/plugins/lens/public/async_services.ts +++ b/x-pack/plugins/lens/public/async_services.ts @@ -24,6 +24,8 @@ export * from './xy_visualization/xy_visualization'; export * from './xy_visualization'; export * from './heatmap_visualization/heatmap_visualization'; export * from './heatmap_visualization'; +export * from './visualizations/gauge/gauge_visualization'; +export * from './visualizations/gauge'; export * from './indexpattern_datasource/indexpattern'; export * from './indexpattern_datasource'; diff --git a/x-pack/plugins/lens/public/expressions.ts b/x-pack/plugins/lens/public/expressions.ts index 9d972d8ed6941..93175eae9a6fe 100644 --- a/x-pack/plugins/lens/public/expressions.ts +++ b/x-pack/plugins/lens/public/expressions.ts @@ -26,6 +26,7 @@ import { heatmap } from '../common/expressions/heatmap_chart/heatmap_chart'; import { heatmapGridConfig } from '../common/expressions/heatmap_chart/heatmap_grid'; import { heatmapLegendConfig } from '../common/expressions/heatmap_chart/heatmap_legend'; +import { gauge } from '../common/expressions/gauge_chart/gauge_chart'; 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'; @@ -60,6 +61,7 @@ export const setupExpressions = ( heatmap, heatmapLegendConfig, heatmapGridConfig, + gauge, axisExtentConfig, labelsOrientationConfig, getDatatable(formatFactory), diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index b89492d7e7588..ecf237ac1327d 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -57,6 +57,7 @@ import type { PieVisualizationPluginSetupPlugins, } from './pie_visualization'; import type { HeatmapVisualization as HeatmapVisualizationType } from './heatmap_visualization'; +import type { GaugeVisualization as GaugeVisualizationType } from './visualizations/gauge'; import type { SavedObjectTaggingPluginStart } from '../../saved_objects_tagging/public'; import { AppNavLinkStatus } from '../../../../src/core/public'; @@ -169,6 +170,7 @@ export class LensPlugin { private metricVisualization: MetricVisualizationType | undefined; private pieVisualization: PieVisualizationType | undefined; private heatmapVisualization: HeatmapVisualizationType | undefined; + private gaugeVisualization: GaugeVisualizationType | undefined; private stopReportManager?: () => void; @@ -308,6 +310,7 @@ export class LensPlugin { MetricVisualization, PieVisualization, HeatmapVisualization, + GaugeVisualization, } = await import('./async_services'); this.datatableVisualization = new DatatableVisualization(); this.editorFrameService = new EditorFrameService(); @@ -316,6 +319,7 @@ export class LensPlugin { this.metricVisualization = new MetricVisualization(); this.pieVisualization = new PieVisualization(); this.heatmapVisualization = new HeatmapVisualization(); + this.gaugeVisualization = new GaugeVisualization(); const editorFrameSetupInterface = this.editorFrameService.setup(); @@ -337,6 +341,7 @@ export class LensPlugin { this.metricVisualization.setup(core, dependencies); this.pieVisualization.setup(core, dependencies); this.heatmapVisualization.setup(core, dependencies); + this.gaugeVisualization.setup(core, dependencies); } start(core: CoreStart, startDependencies: LensPluginStartDependencies): LensPublicStart { diff --git a/x-pack/plugins/lens/public/shared_components/index.ts b/x-pack/plugins/lens/public/shared_components/index.ts index 9ffddaa1a135b..878c695bc8b9e 100644 --- a/x-pack/plugins/lens/public/shared_components/index.ts +++ b/x-pack/plugins/lens/public/shared_components/index.ts @@ -17,3 +17,4 @@ export * from './helpers'; export { LegendActionPopover } from './legend_action_popover'; export { ValueLabelsSettings } from './value_labels_settings'; export * from './static_header'; +export * from './vis_label'; diff --git a/x-pack/plugins/lens/public/shared_components/vis_label.tsx b/x-pack/plugins/lens/public/shared_components/vis_label.tsx new file mode 100644 index 0000000000000..1818119501ebf --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/vis_label.tsx @@ -0,0 +1,99 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiFieldText, EuiSelect } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +type LabelMode = 'auto' | 'custom' | 'none'; + +interface Label { + mode: LabelMode; + label: string; +} + +export interface VisLabelProps { + label: string; + mode: LabelMode; + handleChange: (label: Label) => void; + placeholder?: string; + hasAutoOption?: boolean; + header?: string; +} + +const defaultHeader = i18n.translate('xpack.lens.label.header', { + defaultMessage: 'Label', +}); + +const MODE_NONE = { + id: `lns_title_none`, + value: 'none', + text: i18n.translate('xpack.lens.chart.labelVisibility.none', { + defaultMessage: 'None', + }), +}; + +const MODE_CUSTOM = { + id: `lns_title_custom`, + value: 'custom', + text: i18n.translate('xpack.lens.chart.labelVisibility.custom', { + defaultMessage: 'Custom', + }), +}; + +const MODE_AUTO = { + id: `lns_title_auto`, + value: 'auto', + text: i18n.translate('xpack.lens.chart.labelVisibility.auto', { + defaultMessage: 'Auto', + }), +}; + +const modeDefaultOptions = [MODE_NONE, MODE_CUSTOM]; + +const modeEnhancedOptions = [MODE_NONE, MODE_AUTO, MODE_CUSTOM]; + +export function VisLabel({ + label, + mode, + handleChange, + hasAutoOption = false, + placeholder = '', + header = defaultHeader, +}: VisLabelProps) { + return ( + + + { + if (target.value === 'custom') { + handleChange({ label: '', mode: target.value as LabelMode }); + return; + } + handleChange({ label: '', mode: target.value as LabelMode }); + }} + options={hasAutoOption ? modeEnhancedOptions : modeDefaultOptions} + value={mode} + /> + + + handleChange({ mode: 'custom', label: target.value })} + aria-label={header} + /> + + + ); +} diff --git a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx new file mode 100644 index 0000000000000..2d2762794470c --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx @@ -0,0 +1,245 @@ +/* + * 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, { FC } from 'react'; +import { Chart, Goal, Settings } from '@elastic/charts'; +import { FormattedMessage } from '@kbn/i18n/react'; +import type { + CustomPaletteState, + ChartsPluginSetup, + PaletteRegistry, +} from 'src/plugins/charts/public'; +import { VisualizationContainer } from '../../visualization_container'; +import './index.scss'; +import { EmptyPlaceholder } from '../../shared_components'; +import { LensIconChartGaugeHorizontal, LensIconChartGaugeVertical } from '../../assets/chart_gauge'; +import { getMaxValue, getMinValue, getValueFromAccessor } from './utils'; +import { + GaugeExpressionProps, + GaugeShapes, + GaugeTicksPosition, + GaugeTicksPositions, + GaugeTitleMode, +} from '../../../common/expressions/gauge_chart'; +import type { FormatFactory } from '../../../common'; + +type GaugeRenderProps = GaugeExpressionProps & { + formatFactory: FormatFactory; + chartsThemeService: ChartsPluginSetup['theme']; + paletteService: PaletteRegistry; +}; + +declare global { + interface Window { + /** + * Flag used to enable debugState on elastic charts + */ + _echDebugStateFlag?: boolean; + } +} + +function getStops( + { colors, stops, range }: CustomPaletteState, + { min, max }: { min: number; max: number } +) { + if (stops.length) { + return stops; + } + const step = (max - min) / colors.length; + return colors.map((_, i) => min + i * step); +} + +function shiftAndNormalizeStops( + params: CustomPaletteState, + { min, max }: { min: number; max: number } +) { + const baseStops = [ + ...getStops(params, { min, max }).map((value) => { + let result = value; + if (params.range === 'percent' && params.stops.length) { + result = min + value * ((max - min) / 100); + } + // for a range of 1 value the formulas above will divide by 0, so here's a safety guard + if (Number.isNaN(result)) { + return 1; + } + return result; + }), + ]; + if (params.range === 'percent') { + const convertedMax = min + params.rangeMin * ((max - min) / 100); + baseStops.push(Math.max(max, convertedMax)); + } else { + baseStops.push(Math.max(max, ...params.stops)); + } + + if (params.stops.length) { + if (params.range === 'percent') { + baseStops.unshift(min + params.rangeMin * ((max - min) / 100)); + } else { + baseStops.unshift(params.rangeMin); + } + } + return baseStops; +} + +function getTitle(visTitleMode: GaugeTitleMode, visTitle?: string, fallbackTitle?: string) { + if (visTitleMode === 'none') { + return ''; + } else if (visTitleMode === 'auto') { + return `${fallbackTitle || ''} `; + } + return `${visTitle || fallbackTitle || ''} `; +} + +// TODO: once charts handle not displaying labels when there's no space for them, it's safe to remove this +function getTicksLabels(baseStops: number[]) { + const tenPercentRange = (Math.max(...baseStops) - Math.min(...baseStops)) * 0.1; + const lastIndex = baseStops.length - 1; + return baseStops.filter((stop, i) => { + if (i === 0 || i === lastIndex) { + return true; + } + + return ( + stop - baseStops[i - 1] < tenPercentRange || baseStops[lastIndex] - stop < tenPercentRange + ); + }); +} + +function getTicks( + ticksPosition: GaugeTicksPosition, + range: [number, number], + colorBands?: number[] +) { + if (ticksPosition === GaugeTicksPositions.auto) { + const TICKS_NO = 3; + const [min, max] = range; + const step = (max - min) / TICKS_NO; + return [ + ...Array(TICKS_NO) + .fill(null) + .map((_, i) => min + step * i), + max, + ]; + } else { + return colorBands && getTicksLabels(colorBands); + } +} + +export const GaugeComponent: FC = ({ + data, + args, + formatFactory, + chartsThemeService, + paletteService, +}) => { + const { + shape: subtype, + goalAccessor, + maxAccessor, + minAccessor, + metricAccessor, + palette, + colorMode, + subtitle, + visTitle, + visTitleMode, + ticksPosition, + } = args; + + const chartTheme = chartsThemeService.useChartsTheme(); + const table = Object.values(data.tables)[0]; + const chartData = table.rows.filter((v) => typeof v[metricAccessor!] === 'number'); + + if (!metricAccessor) { + return ; + } + const accessors = { maxAccessor, minAccessor, goalAccessor, metricAccessor }; + + const row = chartData?.[0]; + const metricValue = getValueFromAccessor('metricAccessor', row, accessors); + + const icon = + subtype === GaugeShapes.horizontalBullet + ? LensIconChartGaugeHorizontal + : LensIconChartGaugeVertical; + + if (typeof metricValue !== 'number') { + return ; + } + + const goal = getValueFromAccessor('goalAccessor', row, accessors); + const min = getMinValue(row, accessors); + const max = getMaxValue(row, accessors); + + if (min === max) { + return ( + + } + /> + ); + } + + const colors = (palette?.params as CustomPaletteState)?.colors ?? undefined; + const ranges = (palette?.params as CustomPaletteState) + ? shiftAndNormalizeStops(args.palette?.params as CustomPaletteState, { min, max }) + : [min, max]; + + const metricColumn = table.columns.find((col) => col.id === metricAccessor); + const formatter = formatFactory( + metricColumn?.meta?.params?.params + ? metricColumn?.meta?.params + : { + id: 'number', + params: { + pattern: max - min > 10 ? `0,0` : `0,0.0`, + }, + } + ); + + return ( + + + formatter.convert(tickValue)} + bands={ranges} + ticks={getTicks(ticksPosition, [min, max], ranges)} + bandFillColor={(val) => { + if (colorMode === 'none') { + return `rgb(255,255,255, 0)`; + } + const index = ranges && ranges.indexOf(val.value) - 1; + return index !== undefined && colors && index >= 0 + ? colors[index] + : 'rgb(255,255,255, 0)'; + }} + labelMajor={getTitle(visTitleMode, visTitle, metricColumn?.name)} + labelMinor={subtitle ? subtitle + ' ' : ''} + /> + + ); +}; + +export function GaugeChartReportable(props: GaugeRenderProps) { + return ( + + + + ); +} diff --git a/x-pack/plugins/lens/public/visualizations/gauge/constants.ts b/x-pack/plugins/lens/public/visualizations/gauge/constants.ts new file mode 100644 index 0000000000000..1a801dc942652 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/constants.ts @@ -0,0 +1,16 @@ +/* + * 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 const LENS_GAUGE_RENDERER = 'lens_gauge_renderer'; +export const LENS_GAUGE_ID = 'lnsGauge'; + +export const GROUP_ID = { + METRIC: 'metric', + MIN: 'min', + MAX: 'max', + GOAL: 'goal', +} as const; diff --git a/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.scss b/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.scss new file mode 100644 index 0000000000000..d7664b9d2da16 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.scss @@ -0,0 +1,3 @@ +.lnsDynamicColoringRow { + align-items: center; +} \ No newline at end of file diff --git a/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx new file mode 100644 index 0000000000000..72b25db584407 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx @@ -0,0 +1,189 @@ +/* + * 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 { + EuiButtonEmpty, + EuiFlexGroup, + EuiColorPaletteDisplay, + EuiFormRow, + EuiFlexItem, + EuiSwitchEvent, + EuiSwitch, +} from '@elastic/eui'; +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import type { PaletteRegistry } from 'src/plugins/charts/public'; +import { isNumericFieldForDatatable, GaugeVisualizationState } from '../../../common/expressions'; +import { + applyPaletteParams, + CustomizablePalette, + CUSTOM_PALETTE, + FIXED_PROGRESSION, + getStopsForFixedMode, + PalettePanelContainer, +} from '../../shared_components/'; +import type { VisualizationDimensionEditorProps } from '../../types'; +import { defaultPaletteParams } from './palette_config'; + +import './dimension_editor.scss'; +import { getMaxValue, getMinValue } from './utils'; + +export function GaugeDimensionEditor( + props: VisualizationDimensionEditorProps & { + paletteService: PaletteRegistry; + } +) { + const { state, setState, frame, accessor } = props; + const [isPaletteOpen, setIsPaletteOpen] = useState(false); + + if (state?.metricAccessor !== accessor) return null; + + const currentData = frame.activeData?.[state.layerId]; + const [firstRow] = currentData?.rows || []; + + if (accessor == null || firstRow == null || !isNumericFieldForDatatable(currentData, accessor)) { + return null; + } + + const hasDynamicColoring = state?.colorMode === 'palette'; + + const currentMinMax = { + min: getMinValue(firstRow, state), + max: getMaxValue(firstRow, state), + }; + + const activePalette = state?.palette || { + type: 'palette', + name: defaultPaletteParams.name, + params: { + ...defaultPaletteParams, + colorStops: undefined, + stops: undefined, + rangeMin: currentMinMax.min, + rangeMax: (currentMinMax.max * 3) / 4, + }, + }; + + const displayStops = applyPaletteParams(props.paletteService, activePalette, currentMinMax); + const togglePalette = () => setIsPaletteOpen(!isPaletteOpen); + return ( + <> + + { + const { checked } = e.target; + const params: Partial = { + colorMode: checked ? 'palette' : 'none', + }; + if (checked) { + params.palette = { + ...activePalette, + params: { + ...activePalette.params, + stops: displayStops, + }, + }; + } + if (!checked) { + if (state.ticksPosition === 'bands') { + params.ticksPosition = 'auto'; + } + params.palette = undefined; + } + setState({ + ...state, + ...params, + }); + }} + /> + + {hasDynamicColoring && ( + + + + color) + } + type={FIXED_PROGRESSION} + onClick={togglePalette} + /> + + + + {i18n.translate('xpack.lens.paletteTableGradient.customize', { + defaultMessage: 'Edit', + })} + + + { + // if the new palette is not custom, replace the rangeMin with the artificial one + if ( + newPalette.name !== CUSTOM_PALETTE && + newPalette.params && + newPalette.params.rangeMin !== currentMinMax.min + ) { + newPalette.params.rangeMin = currentMinMax.min; + } + setState({ + ...state, + palette: newPalette, + }); + }} + /> + + + + + )} + + ); +} diff --git a/x-pack/plugins/lens/public/visualizations/gauge/expression.tsx b/x-pack/plugins/lens/public/visualizations/gauge/expression.tsx new file mode 100644 index 0000000000000..9d7dc9ae91b83 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/expression.tsx @@ -0,0 +1,60 @@ +/* + * 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 { I18nProvider } from '@kbn/i18n/react'; +import ReactDOM from 'react-dom'; +import React from 'react'; +import type { IInterpreterRenderHandlers } from '../../../../../../src/plugins/expressions'; +import type { FormatFactory } from '../../../common'; +import { LENS_GAUGE_RENDERER } from './constants'; +import type { + ChartsPluginSetup, + PaletteRegistry, +} from '../../../../../../src/plugins/charts/public'; +import { GaugeChartReportable } from './chart_component'; +import type { GaugeExpressionProps } from '../../../common/expressions'; + +export const getGaugeRenderer = (dependencies: { + formatFactory: FormatFactory; + chartsThemeService: ChartsPluginSetup['theme']; + paletteService: PaletteRegistry; +}) => ({ + name: LENS_GAUGE_RENDERER, + displayName: i18n.translate('xpack.lens.gauge.visualizationName', { + defaultMessage: 'Gauge', + }), + help: '', + validate: () => undefined, + reuseDomNode: true, + render: async ( + domNode: Element, + config: GaugeExpressionProps, + handlers: IInterpreterRenderHandlers + ) => { + ReactDOM.render( + + { + + } + , + domNode, + () => { + handlers.done(); + } + ); + + handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); + }, +}); + +const MemoizedChart = React.memo(GaugeChartReportable); diff --git a/x-pack/plugins/lens/public/visualizations/gauge/gauge_visualization.ts b/x-pack/plugins/lens/public/visualizations/gauge/gauge_visualization.ts new file mode 100644 index 0000000000000..231b6bacbbe20 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/gauge_visualization.ts @@ -0,0 +1,9 @@ +/* + * 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 * from './expression'; +export * from './visualization'; diff --git a/x-pack/plugins/lens/public/visualizations/gauge/index.scss b/x-pack/plugins/lens/public/visualizations/gauge/index.scss new file mode 100644 index 0000000000000..2a26c17df416f --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/index.scss @@ -0,0 +1,13 @@ +.lnsGaugeExpression__container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + text-align: center; + + .echChart { + width: 100%; + } +} diff --git a/x-pack/plugins/lens/public/visualizations/gauge/index.ts b/x-pack/plugins/lens/public/visualizations/gauge/index.ts new file mode 100644 index 0000000000000..a9e431cde579f --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/index.ts @@ -0,0 +1,65 @@ +/* + * 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 'kibana/public'; +import type { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; +import type { EditorFrameSetup } from '../../types'; +import type { + ChartsPluginSetup, + ChartColorConfiguration, + PaletteDefinition, + SeriesLayer, +} from '../../../../../../src/plugins/charts/public'; +import type { FormatFactory } from '../../../common'; + +export interface GaugeVisualizationPluginSetupPlugins { + expressions: ExpressionsSetup; + formatFactory: FormatFactory; + editorFrame: EditorFrameSetup; + charts: ChartsPluginSetup; +} + +const transparentize = (color: string | null) => (color ? color + `80` : `000000`); + +const paletteModifier = (palette: PaletteDefinition) => ({ + ...palette, + getCategoricalColor: ( + series: SeriesLayer[], + chartConfiguration?: ChartColorConfiguration, + state?: unknown + ) => transparentize(palette.getCategoricalColor(series, chartConfiguration, state)), + getCategoricalColors: (size: number, state: unknown): string[] => + palette.getCategoricalColors(size, state).map(transparentize), +}); + +export class GaugeVisualization { + setup( + core: CoreSetup, + { expressions, formatFactory, editorFrame, charts }: GaugeVisualizationPluginSetupPlugins + ) { + editorFrame.registerVisualization(async () => { + const { getGaugeVisualization, getGaugeRenderer } = await import('../../async_services'); + const initialPalettes = await charts.palettes.getPalettes(); + + const palettes = { + ...initialPalettes, + get: (name: string) => paletteModifier(initialPalettes.get(name)), + getAll: () => + initialPalettes.getAll().map((singlePalette) => paletteModifier(singlePalette)), + }; + + expressions.registerRenderer( + getGaugeRenderer({ + formatFactory, + chartsThemeService: charts.theme, + paletteService: palettes, + }) + ); + return getGaugeVisualization({ paletteService: palettes }); + }); + } +} diff --git a/x-pack/plugins/lens/public/visualizations/gauge/palette_config.tsx b/x-pack/plugins/lens/public/visualizations/gauge/palette_config.tsx new file mode 100644 index 0000000000000..20e026a3f5719 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/palette_config.tsx @@ -0,0 +1,23 @@ +/* + * 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 { RequiredPaletteParamTypes } from '../../../common'; +import { defaultPaletteParams as sharedDefaultParams } from '../../shared_components/'; + +export const DEFAULT_PALETTE_NAME = 'gray'; +export const DEFAULT_COLOR_STEPS = 3; +export const DEFAULT_MIN_STOP = 0; +export const DEFAULT_MAX_STOP = 100; + +export const defaultPaletteParams: RequiredPaletteParamTypes = { + ...sharedDefaultParams, + rangeMin: DEFAULT_MIN_STOP, + rangeMax: DEFAULT_MAX_STOP, + name: DEFAULT_PALETTE_NAME, + steps: DEFAULT_COLOR_STEPS, + maxSteps: 5, +}; diff --git a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts new file mode 100644 index 0000000000000..77625c04c03c1 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts @@ -0,0 +1,100 @@ +/* + * 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 type { Visualization } from '../../types'; +import { layerTypes } from '../../../common'; +import { LensIconChartGaugeHorizontal } from '../../assets/chart_gauge'; +import { + GaugeShape, + GaugeShapes, + GaugeTicksPositions, + GaugeTitleModes, + GaugeVisualizationState, +} from '../../../common/expressions/gauge_chart'; + +export const getSuggestions: Visualization['getSuggestions'] = ({ + table, + state, + keptLayerIds, + subVisualizationId, +}) => { + const isCurrentVisGauge = + state && (state.minAccessor || state.maxAccessor || state.goalAccessor || state.metricAccessor); + + const isShapeChange = + (subVisualizationId === GaugeShapes.horizontalBullet || + subVisualizationId === GaugeShapes.verticalBullet) && + isCurrentVisGauge && + subVisualizationId !== state?.shape; + + if ( + keptLayerIds.length > 1 || + (keptLayerIds.length && table.layerId !== keptLayerIds[0]) || + (!isShapeChange && table.columns.length > 1) || + table.columns?.[0]?.operation.dataType !== 'number' || + (isCurrentVisGauge && table.changeType !== 'extended' && table.changeType !== 'unchanged') || + table.columns.some((col) => col.operation.isBucketed) + ) { + return []; + } + + const shape: GaugeShape = + state?.shape === GaugeShapes.verticalBullet + ? GaugeShapes.verticalBullet + : GaugeShapes.horizontalBullet; + + const baseSuggestion = { + state: { + ...state, + metricAccessor: table.columns[0].columnId, + shape, + layerId: table.layerId, + layerType: layerTypes.DATA, + ticksPosition: GaugeTicksPositions.auto, + visTitleMode: GaugeTitleModes.auto, + }, + title: i18n.translate('xpack.lens.gauge.gaugeLabel', { + defaultMessage: 'Gauge', + }), + previewIcon: LensIconChartGaugeHorizontal, + score: 0.1, + hide: !isShapeChange, // only display for gauges for beta + }; + const suggestions = isShapeChange + ? [ + { + ...baseSuggestion, + state: { + ...baseSuggestion.state, + ...state, + shape: + subVisualizationId === GaugeShapes.verticalBullet + ? GaugeShapes.verticalBullet + : GaugeShapes.horizontalBullet, + }, + }, + ] + : [ + { + ...baseSuggestion, + state: { + ...baseSuggestion.state, + shape, + }, + }, + { + ...baseSuggestion, + state: { + ...baseSuggestion.state, + shape: GaugeShapes.verticalBullet, + }, + }, + ]; + + return suggestions; +}; diff --git a/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/gauge_config_panel.scss b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/gauge_config_panel.scss new file mode 100644 index 0000000000000..f3d09715193ea --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/gauge_config_panel.scss @@ -0,0 +1,3 @@ +.lnsGaugeToolbar__popover { + width: 400px; +} \ No newline at end of file diff --git a/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx new file mode 100644 index 0000000000000..6cf1eb7038a1d --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx @@ -0,0 +1,128 @@ +/* + * 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, { memo, useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { VisualizationToolbarProps } from '../../../types'; +import { ToolbarPopover, useDebouncedValue, VisLabel } from '../../../shared_components'; +import './gauge_config_panel.scss'; +import { + GaugeTicksPositions, + GaugeTitleMode, + GaugeVisualizationState, +} from '../../../../common/expressions'; + +export const GaugeToolbar = memo((props: VisualizationToolbarProps) => { + const { state, setState, frame } = props; + const metricDimensionTitle = + state.layerId && + frame.activeData?.[state.layerId]?.columns.find((col) => col.id === state.metricAccessor)?.name; + + const [subtitleMode, setSubtitleMode] = useState(() => + state.subtitle ? 'custom' : 'none' + ); + + const { inputValue, handleInputChange } = useDebouncedValue({ + onChange: setState, + value: state, + }); + + return ( + + + + { + setSubtitleMode(inputValue.subtitle ? 'custom' : 'none'); + }} + title={i18n.translate('xpack.lens.gauge.appearanceLabel', { + defaultMessage: 'Appearance', + })} + type="visualOptions" + buttonDataTestSubj="lnsVisualOptionsButton" + panelClassName="lnsGaugeToolbar__popover" + > + + { + handleInputChange({ + ...inputValue, + visTitle: value.label, + visTitleMode: value.mode, + }); + }} + /> + + + {/*
*/} + { + handleInputChange({ + ...inputValue, + subtitle: value.label, + }); + setSubtitleMode(value.mode); + }} + /> + + + { + handleInputChange({ + ...inputValue, + ticksPosition: e.target.checked + ? GaugeTicksPositions.bands + : GaugeTicksPositions.auto, + }); + }} + /> + + + + + + ); +}); diff --git a/x-pack/plugins/lens/public/visualizations/gauge/utils.ts b/x-pack/plugins/lens/public/visualizations/gauge/utils.ts new file mode 100644 index 0000000000000..dc6319151783a --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/utils.ts @@ -0,0 +1,81 @@ +/* + * 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 { DatatableRow } from 'src/plugins/expressions'; +import type { GaugeVisualizationState } from '../../../common/expressions/gauge_chart'; + +type GaugeAccessors = 'maxAccessor' | 'minAccessor' | 'goalAccessor' | 'metricAccessor'; + +type GaugeAccessorsType = Pick; + +export const getValueFromAccessor = ( + accessorName: GaugeAccessors, + row?: DatatableRow, + state?: GaugeAccessorsType +) => { + if (row && state) { + const accessor = state[accessorName]; + const value = accessor && row[accessor]; + if (typeof value === 'number') { + return value; + } + } +}; + +export const getMaxValue = (row?: DatatableRow, state?: GaugeAccessorsType) => { + const FALLBACK_VALUE = 100; + const MAX_FACTOR = 1.66; + const currentValue = getValueFromAccessor('maxAccessor', row, state); + if (currentValue != null) { + return currentValue; + } + if (row && state) { + const { metricAccessor, goalAccessor } = state; + const metricValue = metricAccessor && row[metricAccessor]; + const goalValue = goalAccessor && row[goalAccessor]; + if (metricValue != null) { + return Math.round(Math.max(goalValue ?? 0, metricValue) * MAX_FACTOR) ?? FALLBACK_VALUE; + } + } + return FALLBACK_VALUE; +}; + +export const getMinValue = (row?: DatatableRow, state?: GaugeAccessorsType) => { + const currentValue = getValueFromAccessor('minAccessor', row, state); + if (currentValue != null) { + return currentValue; + } + const FALLBACK_VALUE = 0; + if (row && state) { + const { metricAccessor } = state; + const metricValue = metricAccessor && row[metricAccessor]; + if (metricValue < 0) { + return metricValue - 10; // TODO: TO THINK THROUGH + } + } + return FALLBACK_VALUE; +}; + +export const getMetricValue = (row?: DatatableRow, state?: GaugeAccessorsType) => { + const currentValue = getValueFromAccessor('metricAccessor', row, state); + if (currentValue != null) { + return currentValue; + } + const minValue = getMinValue(row, state); + const maxValue = getMaxValue(row, state); + return Math.round((minValue + maxValue) * 0.6); +}; + +export const getGoalValue = (row?: DatatableRow, state?: GaugeVisualizationState) => { + const currentValue = getValueFromAccessor('goalAccessor', row, state); + if (currentValue != null) { + return currentValue; + } + const minValue = getMinValue(row, state); + const maxValue = getMaxValue(row, state); + return Math.round((minValue + maxValue) * 0.8); +}; diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx new file mode 100644 index 0000000000000..3d1dfd32ff7ba --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx @@ -0,0 +1,470 @@ +/* + * 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 { render } from 'react-dom'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { Ast } from '@kbn/interpreter/common'; +import { PaletteRegistry } from '../../../../../../src/plugins/charts/public'; +import type { DatasourcePublicAPI, OperationMetadata, Visualization } from '../../types'; +import { getSuggestions } from './suggestions'; +import { GROUP_ID, LENS_GAUGE_ID } from './constants'; +import { GaugeToolbar } from './toolbar_component'; +import { LensIconChartGaugeHorizontal, LensIconChartGaugeVertical } from '../../assets/chart_gauge'; +import { applyPaletteParams, CUSTOM_PALETTE, getStopsForFixedMode } from '../../shared_components'; +import { GaugeDimensionEditor } from './dimension_editor'; +import { CustomPaletteParams, layerTypes } from '../../../common'; +import { generateId } from '../../id_generator'; +import { getGoalValue, getMaxValue, getMetricValue, getMinValue } from './utils'; +import { + GaugeExpressionArgs, + GaugeShapes, + GAUGE_FUNCTION, + GaugeVisualizationState, +} from '../../../common/expressions/gauge_chart'; + +const groupLabelForGauge = i18n.translate('xpack.lens.metric.groupLabel', { + defaultMessage: 'Goal and single value', +}); + +interface GaugeVisualizationDeps { + paletteService: PaletteRegistry; +} + +export const isNumericMetric = (op: OperationMetadata) => + !op.isBucketed && op.dataType === 'number'; + +export const CHART_NAMES = { + horizontalBullet: { + icon: LensIconChartGaugeHorizontal, + label: i18n.translate('xpack.lens.gaugeHorizontal.gaugeLabel', { + defaultMessage: 'Gauge Horizontal', + }), + groupLabel: groupLabelForGauge, + }, + verticalBullet: { + icon: LensIconChartGaugeVertical, + label: i18n.translate('xpack.lens.gaugeVertical.gaugeLabel', { + defaultMessage: 'Gauge Vertical', + }), + groupLabel: groupLabelForGauge, + }, +}; + +function computePaletteParams(params: CustomPaletteParams) { + return { + ...params, + // rewrite colors and stops as two distinct arguments + colors: (params?.stops || []).map(({ color }) => color), + stops: params?.name === 'custom' ? (params?.stops || []).map(({ stop }) => stop) : [], + reverse: false, // managed at UI level + }; +} + +const toExpression = ( + paletteService: PaletteRegistry, + state: GaugeVisualizationState, + datasourceLayers: Record, + attributes?: Partial> +): Ast | null => { + const datasource = datasourceLayers[state.layerId]; + + const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId); + if (!originalOrder || !state.metricAccessor) { + return null; + } + + return { + type: 'expression', + chain: [ + { + type: 'function', + function: GAUGE_FUNCTION, + arguments: { + title: [attributes?.title ?? ''], + description: [attributes?.description ?? ''], + metricAccessor: [state.metricAccessor ?? ''], + minAccessor: [state.minAccessor ?? ''], + maxAccessor: [state.maxAccessor ?? ''], + goalAccessor: [state.goalAccessor ?? ''], + shape: [state.shape ?? GaugeShapes.horizontalBullet], + colorMode: [state?.colorMode ?? 'none'], + palette: state.palette?.params + ? [ + paletteService + .get(CUSTOM_PALETTE) + .toExpression( + computePaletteParams((state.palette?.params || {}) as CustomPaletteParams) + ), + ] + : [], + ticksPosition: state.ticksPosition ? [state.ticksPosition] : ['auto'], + subtitle: state.subtitle ? [state.subtitle] : [], + visTitle: state.visTitle ? [state.visTitle] : [], + visTitleMode: state.visTitleMode ? [state.visTitleMode] : ['auto'], + }, + }, + ], + }; +}; + +export const getGaugeVisualization = ({ + paletteService, +}: GaugeVisualizationDeps): Visualization => ({ + id: LENS_GAUGE_ID, + + visualizationTypes: [ + { + ...CHART_NAMES.horizontalBullet, + id: GaugeShapes.horizontalBullet, + showExperimentalBadge: true, + }, + { + ...CHART_NAMES.verticalBullet, + id: GaugeShapes.verticalBullet, + showExperimentalBadge: true, + }, + ], + getVisualizationTypeId(state) { + return state.shape; + }, + getLayerIds(state) { + return [state.layerId]; + }, + clearLayer(state) { + const newState = { ...state }; + delete newState.metricAccessor; + delete newState.minAccessor; + delete newState.maxAccessor; + delete newState.goalAccessor; + delete newState.palette; + delete newState.colorMode; + return newState; + }, + + getDescription(state) { + if (state.shape === GaugeShapes.horizontalBullet) { + return CHART_NAMES.horizontalBullet; + } + return CHART_NAMES.verticalBullet; + }, + + switchVisualizationType: (visualizationTypeId, state) => { + return { + ...state, + shape: + visualizationTypeId === GaugeShapes.horizontalBullet + ? GaugeShapes.horizontalBullet + : GaugeShapes.verticalBullet, + }; + }, + + initialize(addNewLayer, state, mainPalette) { + return ( + state || { + layerId: addNewLayer(), + layerType: layerTypes.DATA, + title: 'Empty Gauge chart', + shape: GaugeShapes.horizontalBullet, + palette: mainPalette, + ticksPosition: 'auto', + visTitleMode: 'auto', + } + ); + }, + getSuggestions, + + getConfiguration({ state, frame }) { + const hasColoring = Boolean(state.colorMode !== 'none' && state.palette?.params?.stops); + + const row = state?.layerId ? frame?.activeData?.[state?.layerId]?.rows?.[0] : undefined; + let palette; + if (!(row == null || state?.metricAccessor == null || state?.palette == null || !hasColoring)) { + const currentMinMax = { min: getMinValue(row, state), max: getMaxValue(row, state) }; + + const displayStops = applyPaletteParams(paletteService, state?.palette, currentMinMax); + palette = getStopsForFixedMode(displayStops, state?.palette?.params?.colorStops); + } + + return { + groups: [ + { + supportStaticValue: true, + supportFieldFormat: true, + layerId: state.layerId, + groupId: GROUP_ID.METRIC, + groupLabel: i18n.translate('xpack.lens.gauge.metricLabel', { + defaultMessage: 'Metric', + }), + accessors: state.metricAccessor + ? [ + palette + ? { + columnId: state.metricAccessor, + triggerIcon: 'colorBy', + palette, + } + : { + columnId: state.metricAccessor, + triggerIcon: 'none', + }, + ] + : [], + filterOperations: isNumericMetric, + supportsMoreColumns: !state.metricAccessor, + required: true, + dataTestSubj: 'lnsGauge_minDimensionPanel', + enableDimensionEditor: true, + }, + { + supportStaticValue: true, + supportFieldFormat: false, + layerId: state.layerId, + groupId: GROUP_ID.MIN, + groupLabel: i18n.translate('xpack.lens.gauge.minValueLabel', { + defaultMessage: 'Minimum Value', + }), + accessors: state.minAccessor ? [{ columnId: state.minAccessor }] : [], + filterOperations: isNumericMetric, + supportsMoreColumns: !state.minAccessor, + required: true, + dataTestSubj: 'lnsGauge_minDimensionPanel', + prioritizedOperation: 'min', + }, + { + supportStaticValue: true, + supportFieldFormat: false, + layerId: state.layerId, + groupId: GROUP_ID.MAX, + groupLabel: i18n.translate('xpack.lens.gauge.maxValueLabel', { + defaultMessage: 'Maximum Value', + }), + accessors: state.maxAccessor ? [{ columnId: state.maxAccessor }] : [], + filterOperations: isNumericMetric, + supportsMoreColumns: !state.maxAccessor, + required: true, + dataTestSubj: 'lnsGauge_maxDimensionPanel', + prioritizedOperation: 'max', + }, + { + supportStaticValue: true, + supportFieldFormat: false, + layerId: state.layerId, + groupId: GROUP_ID.GOAL, + groupLabel: i18n.translate('xpack.lens.gauge.goalValueLabel', { + defaultMessage: 'Goal value', + }), + accessors: state.goalAccessor ? [{ columnId: state.goalAccessor }] : [], + filterOperations: isNumericMetric, + supportsMoreColumns: !state.goalAccessor, + required: false, + dataTestSubj: 'lnsGauge_goalDimensionPanel', + }, + ], + }; + }, + + setDimension({ prevState, layerId, columnId, groupId, previousColumn }) { + const update: Partial = {}; + if (groupId === GROUP_ID.MIN) { + update.minAccessor = columnId; + } + if (groupId === GROUP_ID.MAX) { + update.maxAccessor = columnId; + } + if (groupId === GROUP_ID.GOAL) { + update.goalAccessor = columnId; + } + if (groupId === GROUP_ID.METRIC) { + update.metricAccessor = columnId; + } + return { + ...prevState, + ...update, + }; + }, + + removeDimension({ prevState, layerId, columnId }) { + const update = { ...prevState }; + + if (prevState.goalAccessor === columnId) { + delete update.goalAccessor; + } + if (prevState.minAccessor === columnId) { + delete update.minAccessor; + } + if (prevState.maxAccessor === columnId) { + delete update.maxAccessor; + } + if (prevState.metricAccessor === columnId) { + delete update.metricAccessor; + delete update.palette; + delete update.colorMode; + } + + return update; + }, + + renderDimensionEditor(domElement, props) { + render( + + + , + domElement + ); + }, + + renderToolbar(domElement, props) { + render( + + + , + domElement + ); + }, + + getSupportedLayers(state, frame) { + const row = state?.layerId ? frame?.activeData?.[state?.layerId]?.rows?.[0] : undefined; + + const minAccessorValue = getMinValue(row, state); + const maxAccessorValue = getMaxValue(row, state); + const metricAccessorValue = getMetricValue(row, state); + const goalAccessorValue = getGoalValue(row, state); + + return [ + { + type: layerTypes.DATA, + label: i18n.translate('xpack.lens.gauge.addLayer', { + defaultMessage: 'Add visualization layer', + }), + initialDimensions: state + ? [ + { + groupId: 'metric', + columnId: generateId(), + dataType: 'number', + label: 'metricAccessor', + staticValue: metricAccessorValue, + }, + { + groupId: 'min', + columnId: generateId(), + dataType: 'number', + label: 'minAccessor', + staticValue: minAccessorValue, + }, + { + groupId: 'max', + columnId: generateId(), + dataType: 'number', + label: 'maxAccessor', + staticValue: maxAccessorValue, + }, + { + groupId: 'goal', + columnId: generateId(), + dataType: 'number', + label: 'goalAccessor', + staticValue: goalAccessorValue, + }, + ] + : undefined, + }, + ]; + }, + + getLayerType(layerId, state) { + if (state?.layerId === layerId) { + return state.layerType; + } + }, + + toExpression: (state, datasourceLayers, attributes) => + toExpression(paletteService, state, datasourceLayers, { ...attributes }), + toPreviewExpression: (state, datasourceLayers) => + toExpression(paletteService, state, datasourceLayers), + + getErrorMessages(state) { + // not possible to break it? + return undefined; + }, + + getWarningMessages(state, frame) { + const { maxAccessor, minAccessor, goalAccessor, metricAccessor } = state; + if (!maxAccessor && !minAccessor && !goalAccessor && !metricAccessor) { + // nothing configured yet + return; + } + if (!metricAccessor) { + return []; + } + + const warnings = []; + // if (!minAccessor) { + // warnings.push([ + // , + // ]); + // } + // if (!maxAccessor) { + // warnings.push([ + // , + // ]); + // } + + const row = frame?.activeData?.[state.layerId]?.rows?.[0]; + if (!row) { + return []; + } + const metricValue = row[metricAccessor]; + const maxValue = maxAccessor && row[maxAccessor]; + const minValue = minAccessor && row[minAccessor]; + const goalValue = goalAccessor && row[goalAccessor]; + + if (minValue != null && minValue === maxValue) { + return [ + , + ]; + } + if (minValue > metricValue) { + warnings.push([ + , + ]); + } + if (maxValue && minValue > maxValue) { + warnings.push([ + , + ]); + } + + if (maxValue && goalValue && goalValue > maxValue) { + warnings.push([ + , + ]); + } + + return warnings; + }, +}); From 1eb65d88f3c22e3d724de94ec7427ea911cdaa9e Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 23 Nov 2021 21:16:03 +0100 Subject: [PATCH 07/43] round nicely the maximum value --- .../lens/public/visualizations/gauge/chart_component.tsx | 2 +- x-pack/plugins/lens/public/visualizations/gauge/utils.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx index 2d2762794470c..0ab1fa279b726 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx @@ -105,7 +105,7 @@ function getTicksLabels(baseStops: number[]) { return true; } - return ( + return !( stop - baseStops[i - 1] < tenPercentRange || baseStops[lastIndex] - stop < tenPercentRange ); }); diff --git a/x-pack/plugins/lens/public/visualizations/gauge/utils.ts b/x-pack/plugins/lens/public/visualizations/gauge/utils.ts index dc6319151783a..760bfbf7ac97d 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/utils.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/utils.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { scaleLinear } from 'd3-scale'; import { DatatableRow } from 'src/plugins/expressions'; import type { GaugeVisualizationState } from '../../../common/expressions/gauge_chart'; @@ -28,7 +29,6 @@ export const getValueFromAccessor = ( export const getMaxValue = (row?: DatatableRow, state?: GaugeAccessorsType) => { const FALLBACK_VALUE = 100; - const MAX_FACTOR = 1.66; const currentValue = getValueFromAccessor('maxAccessor', row, state); if (currentValue != null) { return currentValue; @@ -38,7 +38,11 @@ export const getMaxValue = (row?: DatatableRow, state?: GaugeAccessorsType) => { const metricValue = metricAccessor && row[metricAccessor]; const goalValue = goalAccessor && row[goalAccessor]; if (metricValue != null) { - return Math.round(Math.max(goalValue ?? 0, metricValue) * MAX_FACTOR) ?? FALLBACK_VALUE; + const minValue = getMinValue(row, state); + const biggerValue = Math.max(goalValue ?? 0, metricValue, 1); + const nicelyRoundedNumbers = scaleLinear().domain([minValue, biggerValue]).nice().ticks(4); + const lastNumber = nicelyRoundedNumbers[nicelyRoundedNumbers.length - 1]; + return lastNumber + nicelyRoundedNumbers[1]; } } return FALLBACK_VALUE; From 3fcfd9108cb6f97b577967cb066d04992ac315bd Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 23 Nov 2021 21:27:32 +0100 Subject: [PATCH 08/43] fix warnings --- .../visualizations/gauge/visualization.tsx | 87 ++++++++++--------- 1 file changed, 46 insertions(+), 41 deletions(-) diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx index 3d1dfd32ff7ba..88a31ed19abea 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx @@ -404,24 +404,6 @@ export const getGaugeVisualization = ({ return []; } - const warnings = []; - // if (!minAccessor) { - // warnings.push([ - // , - // ]); - // } - // if (!maxAccessor) { - // warnings.push([ - // , - // ]); - // } - const row = frame?.activeData?.[state.layerId]?.rows?.[0]; if (!row) { return []; @@ -435,34 +417,57 @@ export const getGaugeVisualization = ({ return [ , ]; } - if (minValue > metricValue) { - warnings.push([ - , - ]); - } - if (maxValue && minValue > maxValue) { - warnings.push([ - , - ]); + + const warnings = []; + if (minValue) { + if (minValue > metricValue) { + warnings.push([ + , + ]); + } + if (minValue > goalValue) { + warnings.push([ + , + ]); + } } - if (maxValue && goalValue && goalValue > maxValue) { - warnings.push([ - , - ]); + if (maxValue) { + if (metricValue > maxValue) { + warnings.push([ + , + ]); + } + if (minValue > maxValue) { + warnings.push([ + , + ]); + } + + if (goalValue && goalValue > maxValue) { + warnings.push([ + , + ]); + } } return warnings; From 1b6e4d5424147c8ee2dc45d9e1be526d4da45a10 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 23 Nov 2021 21:48:44 +0100 Subject: [PATCH 09/43] fixing rounding --- .../buttons/empty_dimension_button.tsx | 86 ++++++++++++++----- x-pack/plugins/lens/public/types.ts | 1 + .../lens/public/visualizations/gauge/utils.ts | 7 +- .../visualizations/gauge/visualization.tsx | 4 +- 4 files changed, 70 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx index ac79a78ff7d6d..39eb72ad8a0a5 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx @@ -20,6 +20,63 @@ const label = i18n.translate('xpack.lens.indexPattern.emptyDimensionButton', { defaultMessage: 'Empty dimension', }); +interface EmptyButtonProps { + columnId: string; + onClick: (id: string) => void; + group: VisualizationDimensionGroupConfig; +} + +const DefaultEmptyButton = ({ columnId, group, onClick }: EmptyButtonProps) => ( + { + onClick(columnId); + }} + > + + +); + +const SuggestedValueButton = ({ columnId, group, onClick }: EmptyButtonProps) => ( + { + onClick(columnId); + }} + > + + +); + export function EmptyDimensionButton({ group, groups, @@ -34,12 +91,12 @@ export function EmptyDimensionButton({ layerId: string; groupIndex: number; layerIndex: number; - onClick: (id: string) => void; onDrop: ( droppedItem: DragDropIdentifier, dropTarget: DragDropIdentifier, dropType?: DropType ) => void; + onClick: (id: string) => void; group: VisualizationDimensionGroupConfig; groups: VisualizationDimensionGroupConfig[]; @@ -105,28 +162,11 @@ export function EmptyDimensionButton({ getCustomDropTarget={getCustomDropTarget} >
- { - onClick(value.columnId); - }} - > - - + {typeof group?.suggestedValue === 'number' ? ( + + ) : ( + + )}
diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 31359054a8913..7ca60e2aeb9ec 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -478,6 +478,7 @@ export type VisualizationDimensionGroupConfig = SharedDimensionProps & { requiredMinDimensionCount?: number; dataTestSubj?: string; prioritizedOperation?: string; + suggestedValue?: number; /** * When the dimension editor is enabled for this group, all dimensions in the group diff --git a/x-pack/plugins/lens/public/visualizations/gauge/utils.ts b/x-pack/plugins/lens/public/visualizations/gauge/utils.ts index 760bfbf7ac97d..e629697ab8e90 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/utils.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/utils.ts @@ -40,9 +40,10 @@ export const getMaxValue = (row?: DatatableRow, state?: GaugeAccessorsType) => { if (metricValue != null) { const minValue = getMinValue(row, state); const biggerValue = Math.max(goalValue ?? 0, metricValue, 1); - const nicelyRoundedNumbers = scaleLinear().domain([minValue, biggerValue]).nice().ticks(4); - const lastNumber = nicelyRoundedNumbers[nicelyRoundedNumbers.length - 1]; - return lastNumber + nicelyRoundedNumbers[1]; + const nicelyRounded = scaleLinear().domain([minValue, biggerValue]).nice().ticks(4); + if (nicelyRounded.length > 2) + return nicelyRounded[nicelyRounded.length - 1] + nicelyRounded[1]; + return minValue === biggerValue ? biggerValue + 1 : biggerValue; } } return FALLBACK_VALUE; diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx index 88a31ed19abea..779ca17104e91 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx @@ -232,9 +232,9 @@ export const getGaugeVisualization = ({ accessors: state.minAccessor ? [{ columnId: state.minAccessor }] : [], filterOperations: isNumericMetric, supportsMoreColumns: !state.minAccessor, - required: true, dataTestSubj: 'lnsGauge_minDimensionPanel', prioritizedOperation: 'min', + suggestedValue: getMinValue(row, state), }, { supportStaticValue: true, @@ -247,9 +247,9 @@ export const getGaugeVisualization = ({ accessors: state.maxAccessor ? [{ columnId: state.maxAccessor }] : [], filterOperations: isNumericMetric, supportsMoreColumns: !state.maxAccessor, - required: true, dataTestSubj: 'lnsGauge_maxDimensionPanel', prioritizedOperation: 'max', + suggestedValue: getMaxValue(row, state), }, { supportStaticValue: true, From fbdd2fa8a3f3e6678aff863430f3bb643b34df8f Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Wed, 24 Nov 2021 09:00:06 +0100 Subject: [PATCH 10/43] fixing tests --- .../indexpattern.test.ts | 1 + .../indexpattern_suggestions.test.tsx | 25 +++++++++++++++++++ .../operations/operations.test.ts | 18 ++++++++++--- .../gauge/toolbar_component/index.tsx | 2 +- .../visualizations/gauge/visualization.tsx | 2 +- 5 files changed, 42 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index da5e39c907d07..ba5564df84439 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -1250,6 +1250,7 @@ describe('IndexPattern Data Source', () => { label: 'My Op', dataType: 'string', isBucketed: true, + isStaticValue: false, } as Operation); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index a821dcee29d6d..783314968633f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -1188,6 +1188,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: false, label: '', scale: undefined, + isStaticValue: false, }, }, { @@ -1197,6 +1198,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: false, label: 'Count of records', scale: 'ratio', + isStaticValue: false, }, }, ], @@ -1273,6 +1275,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: false, label: '', scale: undefined, + isStaticValue: false, }, }, { @@ -1282,6 +1285,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: false, label: 'Count of records', scale: 'ratio', + isStaticValue: false, }, }, ], @@ -1546,6 +1550,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'string', isBucketed: true, scale: undefined, + isStaticValue: false, }, }, ], @@ -1568,6 +1573,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'string', isBucketed: true, scale: undefined, + isStaticValue: false, }, }, ], @@ -1614,6 +1620,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'date', isBucketed: true, scale: 'interval', + isStaticValue: false, }, }, { @@ -1623,6 +1630,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'number', isBucketed: false, scale: 'ratio', + isStaticValue: false, }, }, ], @@ -1683,6 +1691,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'string', isBucketed: true, scale: 'ordinal', + isStaticValue: false, }, }, { @@ -1692,6 +1701,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'date', isBucketed: true, scale: 'interval', + isStaticValue: false, }, }, { @@ -1701,6 +1711,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'number', isBucketed: false, scale: 'ratio', + isStaticValue: false, }, }, ], @@ -1780,6 +1791,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'string', isBucketed: true, scale: 'ordinal', + isStaticValue: false, }, }, { @@ -1789,6 +1801,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'date', isBucketed: true, scale: 'interval', + isStaticValue: false, }, }, { @@ -1798,6 +1811,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'number', isBucketed: false, scale: 'ratio', + isStaticValue: false, }, }, ], @@ -1900,6 +1914,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: true, label: 'My Custom Range', scale: 'ordinal', + isStaticValue: false, }, }, { @@ -1909,6 +1924,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: true, label: 'timestampLabel', scale: 'interval', + isStaticValue: false, }, }, { @@ -1918,6 +1934,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: false, label: 'Unique count of dest', scale: undefined, + isStaticValue: false, }, }, ], @@ -2429,6 +2446,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: true, label: 'My Op', scale: undefined, + isStaticValue: false, }, }, { @@ -2438,6 +2456,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: true, label: 'Top 5', scale: undefined, + isStaticValue: false, }, }, ], @@ -2501,6 +2520,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: true, label: 'timestampLabel', scale: 'interval', + isStaticValue: false, }, }, { @@ -2510,6 +2530,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: false, label: 'Cumulative sum of Records label', scale: undefined, + isStaticValue: false, }, }, { @@ -2519,6 +2540,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: false, label: 'Cumulative sum of (incomplete)', scale: undefined, + isStaticValue: false, }, }, ], @@ -2580,6 +2602,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: true, label: '', scale: undefined, + isStaticValue: false, }, }, { @@ -2589,6 +2612,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: false, label: '', scale: undefined, + isStaticValue: false, }, }, { @@ -2598,6 +2622,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: false, label: '', scale: undefined, + isStaticValue: false, }, }, ], diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index 08136ed501cfc..b2cae54b0f8ba 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -378,10 +378,6 @@ describe('getOperationTypesForField', () => { "operationType": "formula", "type": "managedReference", }, - Object { - "operationType": "static_value", - "type": "managedReference", - }, ], }, Object { @@ -398,6 +394,20 @@ describe('getOperationTypesForField', () => { }, ], }, + Object { + "operationMetaData": Object { + "dataType": "number", + "isBucketed": false, + "isStaticValue": true, + "scale": "ratio", + }, + "operations": Array [ + Object { + "operationType": "static_value", + "type": "managedReference", + }, + ], + }, ] `); }); diff --git a/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx index 6cf1eb7038a1d..3849a27a1b1a9 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx @@ -56,7 +56,7 @@ export const GaugeToolbar = memo((props: VisualizationToolbarProps , ]; From a00d0ad81b93149dc670675121012b8f3298a4be Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Wed, 24 Nov 2021 10:42:54 +0100 Subject: [PATCH 11/43] suggestions tests --- .../visualizations/gauge/suggestions.test.ts | 220 ++++++++++++++++++ .../visualizations/gauge/suggestions.ts | 2 +- 2 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/lens/public/visualizations/gauge/suggestions.test.ts diff --git a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.test.ts b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.test.ts new file mode 100644 index 0000000000000..ecfa13ca9e877 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.test.ts @@ -0,0 +1,220 @@ +/* + * 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 { getSuggestions } from './suggestions'; +import { GaugeShapes, GaugeVisualizationState } from '../../../common/expressions'; +import { layerTypes } from '../../../common'; + +const metricColumn = { + columnId: 'metric-column', + operation: { + isBucketed: false, + dataType: 'number' as const, + scale: 'ratio' as const, + label: 'Metric', + }, +}; + +const bucketColumn = { + columnId: 'date-column-01', + operation: { + isBucketed: true, + dataType: 'date' as const, + scale: 'interval' as const, + label: 'Date', + }, +}; + +describe('gauge suggestions', () => { + describe('rejects suggestions', () => { + test('when currently active and unchanged data', () => { + const unchangedSuggestion = { + table: { + layerId: 'first', + isMultiRow: true, + columns: [], + changeType: 'unchanged' as const, + }, + state: { + shape: GaugeShapes.horizontalBullet, + layerId: 'first', + layerType: layerTypes.DATA, + } as GaugeVisualizationState, + keptLayerIds: ['first'], + }; + expect(getSuggestions(unchangedSuggestion)).toHaveLength(0); + }); + test('when there are buckets', () => { + const bucketAndMetricSuggestion = { + table: { + layerId: 'first', + isMultiRow: true, + columns: [bucketColumn, metricColumn], + changeType: 'initial' as const, + }, + state: { + layerId: 'first', + layerType: layerTypes.DATA, + } as GaugeVisualizationState, + keptLayerIds: ['first'], + }; + expect(getSuggestions(bucketAndMetricSuggestion)).toEqual([]); + }); + test('when currently active with partial configuration', () => { + expect( + getSuggestions({ + table: { + layerId: 'first', + isMultiRow: true, + columns: [], // TODO + changeType: 'extended', + }, + state: { + shape: GaugeShapes.horizontalBullet, + layerId: 'first', + layerType: layerTypes.DATA, + minAccessor: 'some-field', + visTitleMode: 'auto', + ticksPosition: 'auto', + } as GaugeVisualizationState, + keptLayerIds: ['first'], + }) + ).toHaveLength(0); + }); + test('for tables with a single bucket dimension', () => { + expect( + getSuggestions({ + table: { + layerId: 'first', + isMultiRow: true, + columns: [bucketColumn], + changeType: 'reduced', + }, + state: { + layerId: 'first', + layerType: layerTypes.DATA, + } as GaugeVisualizationState, + keptLayerIds: ['first'], + }) + ).toEqual([]); + }); + test('when two metric accessor are available', () => { + expect( + getSuggestions({ + table: { + layerId: 'first', + isMultiRow: true, + columns: [ + metricColumn, + { + ...metricColumn, + columnId: 'metric-column2', + }, + ], + changeType: 'initial', + }, + state: { + layerId: 'first', + layerType: layerTypes.DATA, + } as GaugeVisualizationState, + keptLayerIds: ['first'], + }) + ).toEqual([]); + }); + }); +}); + +describe('shows suggestions', () => { + test('when complete configuration has been resolved', () => { + expect( + getSuggestions({ + table: { + layerId: 'first', + isMultiRow: true, + columns: [metricColumn], + changeType: 'initial', + }, + state: { + layerId: 'first', + layerType: layerTypes.DATA, + } as GaugeVisualizationState, + keptLayerIds: ['first'], + }) + ).toEqual([ + { + state: { + layerId: 'first', + layerType: layerTypes.DATA, + shape: GaugeShapes.horizontalBullet, + metricAccessor: 'metric-column', + visTitleMode: 'auto', + ticksPosition: 'auto', + }, + title: 'Gauge', + hide: true, + previewIcon: 'empty', + score: 0.1, + }, + { + hide: true, + previewIcon: 'empty', + title: 'Gauge', + score: 0.1, + state: { + layerId: 'first', + layerType: 'data', + metricAccessor: 'metric-column', + shape: GaugeShapes.verticalBullet, + ticksPosition: 'auto', + visTitleMode: 'auto', + }, + }, + ]); + }); + test('passes the state when change is shape change', () => { + expect( + getSuggestions({ + table: { + layerId: 'first', + isMultiRow: true, + columns: [metricColumn], + changeType: 'extended', + }, + state: { + layerId: 'first', + layerType: layerTypes.DATA, + shape: GaugeShapes.horizontalBullet, + metricAccessor: 'metric-column', + } as GaugeVisualizationState, + keptLayerIds: ['first'], + subVisualizationId: GaugeShapes.verticalBullet, + }) + ).toEqual([ + { + state: { + layerType: layerTypes.DATA, + shape: GaugeShapes.verticalBullet, + metricAccessor: 'metric-column', + visTitleMode: 'auto', + ticksPosition: 'auto', + layerId: 'first', + }, + previewIcon: 'empty', + title: 'Gauge', + hide: false, + score: 0.1, + }, + ]); + }); +}); diff --git a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts index 77625c04c03c1..6f5d294de8250 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts @@ -61,7 +61,7 @@ export const getSuggestions: Visualization['getSuggesti title: i18n.translate('xpack.lens.gauge.gaugeLabel', { defaultMessage: 'Gauge', }), - previewIcon: LensIconChartGaugeHorizontal, + previewIcon: 'empty', score: 0.1, hide: !isShapeChange, // only display for gauges for beta }; From 651a93ef76745dc16693120d9ac5cb6361d337d7 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Wed, 24 Nov 2021 12:02:53 +0100 Subject: [PATCH 12/43] suggestions tbc --- .../visualizations/gauge/suggestions.test.ts | 19 +++----- .../visualizations/gauge/suggestions.ts | 45 ++++++++++--------- 2 files changed, 30 insertions(+), 34 deletions(-) diff --git a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.test.ts b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.test.ts index ecfa13ca9e877..7c6f28a44e1a3 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.test.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.test.ts @@ -5,13 +5,6 @@ * 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 { getSuggestions } from './suggestions'; import { GaugeShapes, GaugeVisualizationState } from '../../../common/expressions'; import { layerTypes } from '../../../common'; @@ -77,8 +70,8 @@ describe('gauge suggestions', () => { table: { layerId: 'first', isMultiRow: true, - columns: [], // TODO - changeType: 'extended', + columns: [metricColumn], + changeType: 'initial', }, state: { shape: GaugeShapes.horizontalBullet, @@ -164,13 +157,13 @@ describe('shows suggestions', () => { title: 'Gauge', hide: true, previewIcon: 'empty', - score: 0.1, + score: 0.5, }, { hide: true, previewIcon: 'empty', title: 'Gauge', - score: 0.1, + score: 0.5, state: { layerId: 'first', layerType: 'data', @@ -212,8 +205,8 @@ describe('shows suggestions', () => { }, previewIcon: 'empty', title: 'Gauge', - hide: false, - score: 0.1, + hide: false, // shows suggestion when current is gauge + score: 0.5, }, ]); }); diff --git a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts index 6f5d294de8250..7ad3df93745cb 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts @@ -6,9 +6,8 @@ */ import { i18n } from '@kbn/i18n'; -import type { Visualization } from '../../types'; +import type { TableSuggestion, Visualization } from '../../types'; import { layerTypes } from '../../../common'; -import { LensIconChartGaugeHorizontal } from '../../assets/chart_gauge'; import { GaugeShape, GaugeShapes, @@ -17,28 +16,34 @@ import { GaugeVisualizationState, } from '../../../common/expressions/gauge_chart'; +const isNotNumericMetric = (table: TableSuggestion) => + table.columns?.[0]?.operation.dataType !== 'number' || + table.columns.some((col) => col.operation.isBucketed); + +const hasLayerMismatch = (keptLayerIds: string[], table: TableSuggestion) => + keptLayerIds.length > 1 || (keptLayerIds.length && table.layerId !== keptLayerIds[0]); + export const getSuggestions: Visualization['getSuggestions'] = ({ table, state, keptLayerIds, subVisualizationId, }) => { - const isCurrentVisGauge = - state && (state.minAccessor || state.maxAccessor || state.goalAccessor || state.metricAccessor); + const isGauge = Boolean( + state && (state.minAccessor || state.maxAccessor || state.goalAccessor || state.metricAccessor) + ); const isShapeChange = (subVisualizationId === GaugeShapes.horizontalBullet || subVisualizationId === GaugeShapes.verticalBullet) && - isCurrentVisGauge && + isGauge && subVisualizationId !== state?.shape; if ( - keptLayerIds.length > 1 || - (keptLayerIds.length && table.layerId !== keptLayerIds[0]) || - (!isShapeChange && table.columns.length > 1) || - table.columns?.[0]?.operation.dataType !== 'number' || - (isCurrentVisGauge && table.changeType !== 'extended' && table.changeType !== 'unchanged') || - table.columns.some((col) => col.operation.isBucketed) + hasLayerMismatch(keptLayerIds, table) || + isNotNumericMetric(table) || + (!isGauge && table.columns.length > 1) || + (isGauge && table.changeType === 'initial') ) { return []; } @@ -62,9 +67,10 @@ export const getSuggestions: Visualization['getSuggesti defaultMessage: 'Gauge', }), previewIcon: 'empty', - score: 0.1, - hide: !isShapeChange, // only display for gauges for beta + score: 0.5, + hide: !isGauge, // only display for gauges for beta }; + const suggestions = isShapeChange ? [ { @@ -80,18 +86,15 @@ export const getSuggestions: Visualization['getSuggesti }, ] : [ + { ...baseSuggestion, hide: true }, { ...baseSuggestion, state: { ...baseSuggestion.state, - shape, - }, - }, - { - ...baseSuggestion, - state: { - ...baseSuggestion.state, - shape: GaugeShapes.verticalBullet, + shape: + state?.shape === GaugeShapes.verticalBullet + ? GaugeShapes.horizontalBullet + : GaugeShapes.verticalBullet, }, }, ]; From 577ee78afdaef0ab7cf7a1a1787447cf7249c961 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Wed, 24 Nov 2021 12:32:58 +0100 Subject: [PATCH 13/43] tests for staticValue limitations --- .../visualization.test.tsx | 30 ++++++++++++ .../heatmap_visualization/suggestions.test.ts | 47 +++++++++++++++++++ .../metric_suggestions.test.ts | 24 ++++++++++ .../pie_visualization/suggestions.test.ts | 29 ++++++++++++ .../xy_visualization/xy_suggestions.test.ts | 27 +++++++++++ 5 files changed, 157 insertions(+) diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx index b3aabea46b874..3e8a31fa53915 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -138,6 +138,36 @@ describe('Datatable Visualization', () => { expect(suggestions.length).toBeGreaterThan(0); }); + it('should reject suggestion with static value', () => { + function staticValueCol(columnId: string): TableSuggestionColumn { + return { + columnId, + operation: { + dataType: 'number', + label: `Static value: ${columnId}`, + isBucketed: false, + isStaticValue: true, + }, + }; + } + const suggestions = datatableVisualization.getSuggestions({ + state: { + layerId: 'first', + layerType: layerTypes.DATA, + columns: [{ columnId: 'col1' }], + }, + table: { + isMultiRow: true, + layerId: 'first', + changeType: 'initial', + columns: [staticValueCol('col1'), strCol('col2')], + }, + keptLayerIds: [], + }); + + expect(suggestions).toHaveLength(0); + }); + it('should retain width and hidden config from existing state', () => { const suggestions = datatableVisualization.getSuggestions({ state: { diff --git a/x-pack/plugins/lens/public/heatmap_visualization/suggestions.test.ts b/x-pack/plugins/lens/public/heatmap_visualization/suggestions.test.ts index e9f8acad7f82d..1a1d7578a01e3 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/suggestions.test.ts +++ b/x-pack/plugins/lens/public/heatmap_visualization/suggestions.test.ts @@ -32,6 +32,53 @@ describe('heatmap suggestions', () => { ).toHaveLength(0); }); + test('when metric value isStaticValue', () => { + expect( + getSuggestions({ + table: { + layerId: 'first', + isMultiRow: true, + columns: [ + { + columnId: 'date-column', + operation: { + isBucketed: true, + dataType: 'date', + scale: 'interval', + label: 'Date', + }, + }, + { + columnId: 'metric-column', + operation: { + isBucketed: false, + dataType: 'number', + scale: 'ratio', + label: 'Metric', + isStaticValue: true, + }, + }, + { + columnId: 'group-column', + operation: { + isBucketed: true, + dataType: 'string', + scale: 'ratio', + label: 'Group', + }, + }, + ], + changeType: 'initial', + }, + state: { + layerId: 'first', + layerType: layerTypes.DATA, + } as HeatmapVisualizationState, + keptLayerIds: ['first'], + }) + ).toEqual([]); + }); + test('when there are 3 or more buckets', () => { expect( getSuggestions({ diff --git a/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.test.ts b/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.test.ts index 8fceffa0db1fe..7c0f8dd073674 100644 --- a/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.test.ts +++ b/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.test.ts @@ -31,6 +31,18 @@ describe('metric_suggestions', () => { }; } + function staticValueCol(columnId: string): TableSuggestionColumn { + return { + columnId, + operation: { + dataType: 'number', + label: `Static value: ${columnId}`, + isBucketed: false, + isStaticValue: true, + }, + }; + } + function dateCol(columnId: string): TableSuggestionColumn { return { columnId, @@ -86,7 +98,19 @@ describe('metric_suggestions', () => { ).map((table) => expect(getSuggestions({ table, keptLayerIds: ['l1'] })).toEqual([])) ); }); + test('does not suggest for a static value', () => { + const suggestion = getSuggestions({ + table: { + columns: [staticValueCol('id')], + isMultiRow: false, + layerId: 'l1', + changeType: 'unchanged', + }, + keptLayerIds: [], + }); + expect(suggestion).toHaveLength(0); + }); test('suggests a basic metric chart', () => { const [suggestion, ...rest] = getSuggestions({ table: { 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 656d00960766e..afc31132918f0 100644 --- a/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts +++ b/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts @@ -288,6 +288,35 @@ describe('suggestions', () => { ).toHaveLength(0); }); + it('should reject when metric value isStaticValue', () => { + const results = suggestions({ + table: { + layerId: 'first', + isMultiRow: true, + columns: [ + { + columnId: 'a', + operation: { label: 'Top 5', dataType: 'string' as DataType, isBucketed: true }, + }, + { + columnId: 'e', + operation: { + label: 'Count', + dataType: 'number' as DataType, + isBucketed: false, + isStaticValue: true, + }, + }, + ], + changeType: 'initial', + }, + state: undefined, + keptLayerIds: ['first'], + }); + + expect(results.length).toEqual(0); + }); + it('should hide suggestions when there are no buckets', () => { const currentSuggestions = suggestions({ table: { diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts index d7b48553ce73a..89714dff04a62 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts @@ -38,6 +38,18 @@ describe('xy_suggestions', () => { }; } + function staticValueCol(columnId: string): TableSuggestionColumn { + return { + columnId, + operation: { + dataType: 'number', + label: `Static value: ${columnId}`, + isBucketed: false, + isStaticValue: true, + }, + }; + } + function strCol(columnId: string): TableSuggestionColumn { return { columnId, @@ -120,6 +132,21 @@ describe('xy_suggestions', () => { ); }); + test('rejects the configuration when metric isStaticValue', () => { + (generateId as jest.Mock).mockReturnValueOnce('aaa'); + const suggestions = getSuggestions({ + table: { + isMultiRow: true, + columns: [staticValueCol('value'), dateCol('date')], + layerId: 'first', + changeType: 'unchanged', + }, + keptLayerIds: [], + }); + + expect(suggestions).toHaveLength(0); + }); + test('rejects incomplete configurations if there is a state already but no sub visualization id', () => { expect( ( From 594f93ed5ecaf5f427a02189573814ec50ef6f85 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Wed, 24 Nov 2021 16:09:13 +0100 Subject: [PATCH 14/43] added tests to visualization.ts --- .../expressions/gauge_chart/gauge_chart.ts | 2 +- .../lens/public/visualizations/gauge/index.ts | 30 +- .../public/visualizations/gauge/utils.test.ts | 41 ++ .../lens/public/visualizations/gauge/utils.ts | 27 + .../gauge/visualization.test.ts | 609 ++++++++++++++++++ .../visualizations/gauge/visualization.tsx | 22 +- 6 files changed, 692 insertions(+), 39 deletions(-) create mode 100644 x-pack/plugins/lens/public/visualizations/gauge/utils.test.ts create mode 100644 x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts diff --git a/x-pack/plugins/lens/common/expressions/gauge_chart/gauge_chart.ts b/x-pack/plugins/lens/common/expressions/gauge_chart/gauge_chart.ts index b8ca6d066de00..dd419bb53ca9a 100644 --- a/x-pack/plugins/lens/common/expressions/gauge_chart/gauge_chart.ts +++ b/x-pack/plugins/lens/common/expressions/gauge_chart/gauge_chart.ts @@ -72,7 +72,7 @@ export const gauge: ExpressionFunctionDefinition< goalAccessor: { types: ['string'], help: i18n.translate('xpack.lens.gauge.goalAccessor.help', { - defaultMessage: 'Goal value', + defaultMessage: 'Goal Value', }), }, colorMode: { diff --git a/x-pack/plugins/lens/public/visualizations/gauge/index.ts b/x-pack/plugins/lens/public/visualizations/gauge/index.ts index a9e431cde579f..b0a4f26f2d675 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/index.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/index.ts @@ -8,13 +8,9 @@ import type { CoreSetup } from 'kibana/public'; import type { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; import type { EditorFrameSetup } from '../../types'; -import type { - ChartsPluginSetup, - ChartColorConfiguration, - PaletteDefinition, - SeriesLayer, -} from '../../../../../../src/plugins/charts/public'; +import type { ChartsPluginSetup } from '../../../../../../src/plugins/charts/public'; import type { FormatFactory } from '../../../common'; +import { transparentizePalettes } from './utils'; export interface GaugeVisualizationPluginSetupPlugins { expressions: ExpressionsSetup; @@ -23,19 +19,6 @@ export interface GaugeVisualizationPluginSetupPlugins { charts: ChartsPluginSetup; } -const transparentize = (color: string | null) => (color ? color + `80` : `000000`); - -const paletteModifier = (palette: PaletteDefinition) => ({ - ...palette, - getCategoricalColor: ( - series: SeriesLayer[], - chartConfiguration?: ChartColorConfiguration, - state?: unknown - ) => transparentize(palette.getCategoricalColor(series, chartConfiguration, state)), - getCategoricalColors: (size: number, state: unknown): string[] => - palette.getCategoricalColors(size, state).map(transparentize), -}); - export class GaugeVisualization { setup( core: CoreSetup, @@ -43,14 +26,7 @@ export class GaugeVisualization { ) { editorFrame.registerVisualization(async () => { const { getGaugeVisualization, getGaugeRenderer } = await import('../../async_services'); - const initialPalettes = await charts.palettes.getPalettes(); - - const palettes = { - ...initialPalettes, - get: (name: string) => paletteModifier(initialPalettes.get(name)), - getAll: () => - initialPalettes.getAll().map((singlePalette) => paletteModifier(singlePalette)), - }; + const palettes = transparentizePalettes(await charts.palettes.getPalettes()); expressions.registerRenderer( getGaugeRenderer({ diff --git a/x-pack/plugins/lens/public/visualizations/gauge/utils.test.ts b/x-pack/plugins/lens/public/visualizations/gauge/utils.test.ts new file mode 100644 index 0000000000000..8989c5fa46934 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/utils.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { transparentizePalettes } from './utils'; +import { chartPluginMock } from '../../../../../../src/plugins/charts/public/mocks'; + +const paletteServiceMock = chartPluginMock.createPaletteRegistry(); + +describe('transparentizePalettes', () => { + it('converts all colors to half-transparent', () => { + const newPalettes = transparentizePalettes(paletteServiceMock); + + const singlePalette = newPalettes.get('mocked'); + expect(singlePalette.getCategoricalColors(2)).toEqual(['#0000FF80', '#FFFF0080']); + expect( + singlePalette.getCategoricalColor([ + { + name: 'abc', + rankAtDepth: 0, + totalSeriesAtDepth: 5, + }, + ]) + ).toEqual('#0000FF80'); + + const firstPalette = newPalettes.getAll()[0]; + expect(firstPalette.getCategoricalColors(2)).toEqual(['#FF000080', '#00000080']); + expect( + firstPalette.getCategoricalColor([ + { + name: 'abc', + rankAtDepth: 0, + totalSeriesAtDepth: 5, + }, + ]) + ).toEqual('#00000080'); + }); +}); diff --git a/x-pack/plugins/lens/public/visualizations/gauge/utils.ts b/x-pack/plugins/lens/public/visualizations/gauge/utils.ts index e629697ab8e90..7f34c510488af 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/utils.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/utils.ts @@ -6,7 +6,14 @@ */ import { scaleLinear } from 'd3-scale'; +import { + ChartColorConfiguration, + PaletteDefinition, + PaletteRegistry, + SeriesLayer, +} from 'src/plugins/charts/public'; import { DatatableRow } from 'src/plugins/expressions'; +import Color from 'color'; import type { GaugeVisualizationState } from '../../../common/expressions/gauge_chart'; type GaugeAccessors = 'maxAccessor' | 'minAccessor' | 'goalAccessor' | 'metricAccessor'; @@ -84,3 +91,23 @@ export const getGoalValue = (row?: DatatableRow, state?: GaugeVisualizationState const maxValue = getMaxValue(row, state); return Math.round((minValue + maxValue) * 0.8); }; + +export const transparentizePalettes = (palettes: PaletteRegistry) => { + const addAlpha = (c: string | null) => (c ? new Color(c).hex() + `80` : `000000`); + const transparentizePalette = (palette: PaletteDefinition) => ({ + ...palette, + getCategoricalColor: ( + series: SeriesLayer[], + chartConfiguration?: ChartColorConfiguration, + state?: unknown + ) => addAlpha(palette.getCategoricalColor(series, chartConfiguration, state)), + getCategoricalColors: (size: number, state?: unknown): string[] => + palette.getCategoricalColors(size, state).map(addAlpha), + }); + + return { + ...palettes, + get: (name: string) => transparentizePalette(palettes.get(name)), + getAll: () => palettes.getAll().map((singlePalette) => transparentizePalette(singlePalette)), + }; +}; diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts new file mode 100644 index 0000000000000..fc24a066fcd54 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts @@ -0,0 +1,609 @@ +/* + * 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 { getGaugeVisualization, isNumericMetric } from './visualization'; +import { createMockDatasource, createMockFramePublicAPI } from '../../mocks'; +import { GROUP_ID } from './constants'; +import type { DatasourcePublicAPI, Operation } from '../../types'; +import { chartPluginMock } from 'src/plugins/charts/public/mocks'; +import { CustomPaletteParams, layerTypes } from '../../../common'; +import { GAUGE_FUNCTION, GaugeVisualizationState } from '../../../common/expressions/gauge_chart'; +import { PaletteOutput } from 'src/plugins/charts/common'; +import { GaugeVisualization } from '.'; + +function exampleState(): GaugeVisualizationState { + return { + layerId: 'test-layer', + layerType: layerTypes.DATA, + visTitleMode: 'auto', + ticksPosition: 'auto', + shape: 'horizontalBullet', + }; +} + +const paletteService = chartPluginMock.createPaletteRegistry(); + +describe('gauge', () => { + let frame: ReturnType; + + beforeEach(() => { + frame = createMockFramePublicAPI(); + }); + + describe('#intialize', () => { + test('returns a default state', () => { + expect(getGaugeVisualization({ paletteService }).initialize(() => 'l1')).toEqual({ + layerId: 'l1', + layerType: layerTypes.DATA, + title: 'Empty Gauge chart', + shape: 'horizontalBullet', + visTitleMode: 'auto', + ticksPosition: 'auto', + }); + }); + + test('returns persisted state', () => { + expect( + getGaugeVisualization({ paletteService }).initialize(() => 'test-layer', exampleState()) + ).toEqual(exampleState()); + }); + }); + + describe('#getConfiguration', () => { + beforeEach(() => { + const mockDatasource = createMockDatasource('testDatasource'); + + mockDatasource.publicAPIMock.getOperationForColumnId.mockReturnValue({ + dataType: 'string', + label: 'MyOperation', + } as Operation); + + frame.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + }); + + afterEach(() => { + // some tests manipulate it, so restore a pristine version + frame = createMockFramePublicAPI(); + }); + + test('resolves configuration from complete state and available data', () => { + const state: GaugeVisualizationState = { + ...exampleState(), + layerId: 'first', + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + goalAccessor: 'goal-accessor', + }; + frame.activeData = { + first: { type: 'datatable', columns: [], rows: [{ 'metric-accessor': 200 }] }, + }; + + expect( + getGaugeVisualization({ + paletteService, + }).getConfiguration({ state, frame, layerId: 'first' }) + ).toEqual({ + groups: [ + { + layerId: 'first', + groupId: GROUP_ID.METRIC, + groupLabel: 'Metric', + accessors: [{ columnId: 'metric-accessor', triggerIcon: 'none' }], + filterOperations: isNumericMetric, + supportsMoreColumns: false, + required: true, + dataTestSubj: 'lnsGauge_maxDimensionPanel', + enableDimensionEditor: true, + supportFieldFormat: true, + supportStaticValue: true, + }, + { + layerId: 'first', + groupId: GROUP_ID.MIN, + groupLabel: 'Minimum Value', + accessors: [{ columnId: 'min-accessor' }], + filterOperations: isNumericMetric, + supportsMoreColumns: false, + dataTestSubj: 'lnsGauge_minDimensionPanel', + prioritizedOperation: 'min', + suggestedValue: 0, + supportFieldFormat: false, + supportStaticValue: true, + }, + { + layerId: 'first', + groupId: GROUP_ID.MAX, + groupLabel: 'Maximum Value', + accessors: [{ columnId: 'max-accessor' }], + filterOperations: isNumericMetric, + supportsMoreColumns: false, + dataTestSubj: 'lnsGauge_maxDimensionPanel', + prioritizedOperation: 'max', + suggestedValue: 250, + supportFieldFormat: false, + supportStaticValue: true, + }, + { + layerId: 'first', + groupId: GROUP_ID.GOAL, + groupLabel: 'Goal Value', + accessors: [{ columnId: 'goal-accessor' }], + filterOperations: isNumericMetric, + supportsMoreColumns: false, + required: false, + dataTestSubj: 'lnsGauge_goalDimensionPanel', + supportFieldFormat: false, + supportStaticValue: true, + }, + ], + }); + }); + + test('resolves configuration from partial state', () => { + const state: GaugeVisualizationState = { + ...exampleState(), + layerId: 'first', + minAccessor: 'min-accessor', + }; + expect( + getGaugeVisualization({ + paletteService, + }).getConfiguration({ state, frame, layerId: 'first' }) + ).toEqual({ + groups: [ + { + layerId: 'first', + groupId: GROUP_ID.METRIC, + groupLabel: 'Metric', + accessors: [], + filterOperations: isNumericMetric, + supportsMoreColumns: true, + required: true, + dataTestSubj: 'lnsGauge_maxDimensionPanel', + enableDimensionEditor: true, + supportFieldFormat: true, + supportStaticValue: true, + }, + { + layerId: 'first', + groupId: GROUP_ID.MIN, + groupLabel: 'Minimum Value', + accessors: [{ columnId: 'min-accessor' }], + filterOperations: isNumericMetric, + supportsMoreColumns: false, + dataTestSubj: 'lnsGauge_minDimensionPanel', + prioritizedOperation: 'min', + suggestedValue: 0, + supportFieldFormat: false, + supportStaticValue: true, + }, + { + layerId: 'first', + groupId: GROUP_ID.MAX, + groupLabel: 'Maximum Value', + accessors: [], + filterOperations: isNumericMetric, + supportsMoreColumns: true, + dataTestSubj: 'lnsGauge_maxDimensionPanel', + prioritizedOperation: 'max', + suggestedValue: 100, + supportFieldFormat: false, + supportStaticValue: true, + }, + { + layerId: 'first', + groupId: GROUP_ID.GOAL, + groupLabel: 'Goal Value', + accessors: [], + filterOperations: isNumericMetric, + supportsMoreColumns: true, + required: false, + dataTestSubj: 'lnsGauge_goalDimensionPanel', + supportFieldFormat: false, + supportStaticValue: true, + }, + ], + }); + }); + + test("resolves configuration when there's no access to active data in frame", () => { + const state: GaugeVisualizationState = { + ...exampleState(), + layerId: 'first', + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + goalAccessor: 'goal-accessor', + }; + + frame.activeData = undefined; + + expect( + getGaugeVisualization({ + paletteService, + }).getConfiguration({ state, frame, layerId: 'first' }) + ).toEqual({ + groups: [ + { + layerId: 'first', + groupId: GROUP_ID.METRIC, + groupLabel: 'Metric', + accessors: [{ columnId: 'metric-accessor', triggerIcon: 'none' }], + filterOperations: isNumericMetric, + supportsMoreColumns: false, + required: true, + dataTestSubj: 'lnsGauge_maxDimensionPanel', + enableDimensionEditor: true, + supportFieldFormat: true, + supportStaticValue: true, + }, + { + layerId: 'first', + groupId: GROUP_ID.MIN, + groupLabel: 'Minimum Value', + accessors: [{ columnId: 'min-accessor' }], + filterOperations: isNumericMetric, + supportsMoreColumns: false, + dataTestSubj: 'lnsGauge_minDimensionPanel', + prioritizedOperation: 'min', + suggestedValue: 0, + supportFieldFormat: false, + supportStaticValue: true, + }, + { + layerId: 'first', + groupId: GROUP_ID.MAX, + groupLabel: 'Maximum Value', + accessors: [{ columnId: 'max-accessor' }], + filterOperations: isNumericMetric, + supportsMoreColumns: false, + dataTestSubj: 'lnsGauge_maxDimensionPanel', + prioritizedOperation: 'max', + suggestedValue: 100, + supportFieldFormat: false, + supportStaticValue: true, + }, + { + layerId: 'first', + groupId: GROUP_ID.GOAL, + groupLabel: 'Goal Value', + accessors: [{ columnId: 'goal-accessor' }], + filterOperations: isNumericMetric, + supportsMoreColumns: false, + required: false, + dataTestSubj: 'lnsGauge_goalDimensionPanel', + supportFieldFormat: false, + supportStaticValue: true, + }, + ], + }); + }); + }); + + describe('#setDimension', () => { + test('set dimension correctly', () => { + const prevState: GaugeVisualizationState = { + ...exampleState(), + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + }; + expect( + getGaugeVisualization({ + paletteService, + }).setDimension({ + prevState, + layerId: 'first', + columnId: 'new-min-accessor', + groupId: 'min', + frame, + }) + ).toEqual({ + ...prevState, + minAccessor: 'new-min-accessor', + }); + }); + }); + + describe('#removeDimension', () => { + const prevState: GaugeVisualizationState = { + ...exampleState(), + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + palette: [] as unknown as PaletteOutput, + colorMode: 'palette', + }; + test('removes metricAccessor correctly', () => { + expect( + getGaugeVisualization({ + paletteService, + }).removeDimension({ + prevState, + layerId: 'first', + columnId: 'metric-accessor', + frame, + }) + ).toEqual({ + ...exampleState(), + minAccessor: 'min-accessor', + }); + }); + test('removes minAccessor correctly', () => { + expect( + getGaugeVisualization({ + paletteService, + }).removeDimension({ + prevState, + layerId: 'first', + columnId: 'min-accessor', + frame, + }) + ).toEqual({ + ...exampleState(), + metricAccessor: 'metric-accessor', + palette: [] as unknown as PaletteOutput, + colorMode: 'palette', + }); + }); + }); + describe('#getSupportedLayers', () => { + it('should return a single layer type', () => { + expect( + getGaugeVisualization({ + paletteService, + }).getSupportedLayers() + ).toHaveLength(1); + }); + }); + describe('#getLayerType', () => { + it('should return the type only if the layer is in the state', () => { + const state: GaugeVisualizationState = { + ...exampleState(), + minAccessor: 'minAccessor', + goalAccessor: 'value-accessor', + }; + const instance = getGaugeVisualization({ + paletteService, + }); + expect(instance.getLayerType('test-layer', state)).toEqual(layerTypes.DATA); + expect(instance.getLayerType('foo', state)).toBeUndefined(); + }); + }); + + describe('#toExpression', () => { + let datasourceLayers: Record; + beforeEach(() => { + const mockDatasource = createMockDatasource('testDatasource'); + mockDatasource.publicAPIMock.getOperationForColumnId.mockReturnValue({ + dataType: 'string', + label: 'MyOperation', + } as Operation); + datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + }); + test('creates an expression based on state and attributes', () => { + const state: GaugeVisualizationState = { + ...exampleState(), + layerId: 'first', + minAccessor: 'min-accessor', + goalAccessor: 'goal-accessor', + metricAccessor: 'metric-accessor', + maxAccessor: 'max-accessor', + subtitle: 'Subtitle', + }; + const attributes = { + title: 'Test', + }; + expect( + getGaugeVisualization({ + paletteService, + }).toExpression(state, datasourceLayers, attributes) + ).toEqual({ + type: 'expression', + chain: [ + { + type: 'function', + function: GAUGE_FUNCTION, + arguments: { + title: ['Test'], + description: [''], + metricAccessor: ['metric-accessor'], + minAccessor: ['min-accessor'], + maxAccessor: ['max-accessor'], + goalAccessor: ['goal-accessor'], + colorMode: ['none'], + ticksPosition: ['auto'], + visTitleMode: ['auto'], + subtitle: ['Subtitle'], + visTitle: [], + palette: [], + shape: ['horizontalBullet'], + }, + }, + ], + }); + }); + test('returns null with a missing metric accessor', () => { + const state: GaugeVisualizationState = { + ...exampleState(), + layerId: 'first', + minAccessor: 'minAccessor', + }; + const attributes = { + title: 'Test', + }; + expect( + getGaugeVisualization({ + paletteService, + }).toExpression(state, datasourceLayers, attributes) + ).toEqual(null); + }); + }); + + describe('#getErrorMessages', () => { + it('returns undefined if no error is raised', () => { + const error = getGaugeVisualization({ + paletteService, + }).getErrorMessages(exampleState()); + expect(error).not.toBeDefined(); + }); + }); + + describe('#getWarningMessages', () => { + beforeEach(() => { + const mockDatasource = createMockDatasource('testDatasource'); + mockDatasource.publicAPIMock.getOperationForColumnId.mockReturnValue({ + dataType: 'string', + label: 'MyOperation', + } as Operation); + frame.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + }); + const state: GaugeVisualizationState = { + ...exampleState(), + layerId: 'first', + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + goalAccessor: 'goal-accessor', + }; + it('should not warn for data in bounds', () => { + frame.activeData = { + first: { + type: 'datatable', + columns: [], + rows: [ + { + 'min-accessor': 0, + 'metric-accessor': 5, + 'max-accessor': 10, + 'goal-accessor': 8, + }, + ], + }, + }; + + expect( + getGaugeVisualization({ + paletteService, + }).getWarningMessages!(state, frame) + ).toHaveLength(0); + }); + it('should warn when minimum value is greater than metric value', () => { + frame.activeData = { + first: { + type: 'datatable', + columns: [], + rows: [ + { + 'metric-accessor': -1, + 'min-accessor': 1, + 'max-accessor': 3, + 'goal-accessor': 2, + }, + ], + }, + }; + + expect( + getGaugeVisualization({ + paletteService, + }).getWarningMessages!(state, frame) + ).toHaveLength(1); + }); + it('should warn when minimum value is greater than maximum value', () => { + frame.activeData = { + first: { + type: 'datatable', + columns: [], + rows: [ + { + 'min-accessor': 5, + 'metric-accessor': 2, + 'max-accessor': 3, + }, + ], + }, + }; + + expect( + getGaugeVisualization({ + paletteService, + }).getWarningMessages!(state, frame) + ).toHaveLength(2); + }); + it('should warn when metric value is greater than maximum value', () => { + frame.activeData = { + first: { + type: 'datatable', + columns: [], + rows: [ + { + 'metric-accessor': 10, + 'min-accessor': -10, + 'max-accessor': 0, + }, + ], + }, + }; + + expect( + getGaugeVisualization({ + paletteService, + }).getWarningMessages!(state, frame) + ).toHaveLength(1); + }); + it('should warn when goal value is greater than maximum value', () => { + frame.activeData = { + first: { + type: 'datatable', + columns: [], + rows: [ + { + 'metric-accessor': 5, + 'min-accessor': 0, + 'max-accessor': 10, + 'goal-accessor': 15, + }, + ], + }, + }; + + expect( + getGaugeVisualization({ + paletteService, + }).getWarningMessages!(state, frame) + ).toHaveLength(1); + }); + it('should warn when minimum value is greater than goal value', () => { + frame.activeData = { + first: { + type: 'datatable', + columns: [], + rows: [ + { + 'metric-accessor': 5, + 'min-accessor': 0, + 'max-accessor': 10, + 'goal-accessor': -5, + }, + ], + }, + }; + + expect( + getGaugeVisualization({ + paletteService, + }).getWarningMessages!(state, frame) + ).toHaveLength(1); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx index 578056d731ab2..31a72b273e874 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx @@ -218,7 +218,7 @@ export const getGaugeVisualization = ({ filterOperations: isNumericMetric, supportsMoreColumns: !state.metricAccessor, required: true, - dataTestSubj: 'lnsGauge_minDimensionPanel', + dataTestSubj: 'lnsGauge_maxDimensionPanel', enableDimensionEditor: true, }, { @@ -257,7 +257,7 @@ export const getGaugeVisualization = ({ layerId: state.layerId, groupId: GROUP_ID.GOAL, groupLabel: i18n.translate('xpack.lens.gauge.goalValueLabel', { - defaultMessage: 'Goal value', + defaultMessage: 'Goal Value', }), accessors: state.goalAccessor ? [{ columnId: state.goalAccessor }] : [], filterOperations: isNumericMetric, @@ -413,7 +413,7 @@ export const getGaugeVisualization = ({ const minValue = minAccessor && row[minAccessor]; const goalValue = goalAccessor && row[goalAccessor]; - if (minValue != null && minValue === maxValue) { + if (typeof minValue === 'number' && minValue === maxValue) { return [ metricValue) { warnings.push([ , ]); @@ -435,18 +435,18 @@ export const getGaugeVisualization = ({ if (minValue > goalValue) { warnings.push([ , ]); } } - if (maxValue) { + if (typeof maxValue === 'number') { if (metricValue > maxValue) { warnings.push([ , ]); @@ -454,16 +454,16 @@ export const getGaugeVisualization = ({ if (minValue > maxValue) { warnings.push([ , ]); } - if (goalValue && goalValue > maxValue) { + if (typeof goalValue === 'number' && goalValue > maxValue) { warnings.push([ , ]); From 7e966eca36917120b097faabb1c182b2f3a4ee14 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Thu, 25 Nov 2021 10:01:58 +0100 Subject: [PATCH 15/43] correct bands --- .../lens/public/visualizations/gauge/chart_component.tsx | 3 ++- .../lens/public/visualizations/gauge/visualization.test.ts | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx index 0ab1fa279b726..508f01b134908 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx @@ -118,7 +118,8 @@ function getTicks( ) { if (ticksPosition === GaugeTicksPositions.auto) { const TICKS_NO = 3; - const [min, max] = range; + const min = Math.min(...(colorBands || []), ...range); + const max = Math.max(...(colorBands || []), ...range); const step = (max - min) / TICKS_NO; return [ ...Array(TICKS_NO) diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts index fc24a066fcd54..c8a35bb05185e 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts @@ -13,7 +13,6 @@ import { chartPluginMock } from 'src/plugins/charts/public/mocks'; import { CustomPaletteParams, layerTypes } from '../../../common'; import { GAUGE_FUNCTION, GaugeVisualizationState } from '../../../common/expressions/gauge_chart'; import { PaletteOutput } from 'src/plugins/charts/common'; -import { GaugeVisualization } from '.'; function exampleState(): GaugeVisualizationState { return { From b52b1b2f82ae57730d514d2a42d4826a5fcc08dc Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Thu, 25 Nov 2021 14:04:52 +0100 Subject: [PATCH 16/43] wip --- .../gauge/chart_component.test.tsx | 132 ++++++++++++++++++ .../visualizations/gauge/chart_component.tsx | 24 ++-- 2 files changed, 144 insertions(+), 12 deletions(-) create mode 100644 x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx diff --git a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx new file mode 100644 index 0000000000000..d355dadc7d84a --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx @@ -0,0 +1,132 @@ +/* + * 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 { Settings } from '@elastic/charts'; +import { findTestSubject } from '@elastic/eui/lib/test'; +import { act } from 'react-dom/test-utils'; +import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest'; +import { chartPluginMock } from 'src/plugins/charts/public/mocks'; +import type { LensMultiTable } from '../../common'; +import { fieldFormatsServiceMock } from '../../../../../../src/plugins/field_formats/public/mocks'; +import { EmptyPlaceholder } from '../../shared_components'; +import { GaugeExpressionArgs, GaugeExpressionProps } from '../../../common/expressions/gauge_chart'; +import { GaugeComponent, GaugeRenderProps } from './chart_component'; + +jest.mock('@elastic/charts', () => { + const original = jest.requireActual('@elastic/charts'); + + return { + ...original, + getSpecId: jest.fn(() => {}), + }; +}); + +const chartsThemeService = chartPluginMock.createSetupContract().theme; +const palettesRegistry = chartPluginMock.createPaletteRegistry(); +const formatService = fieldFormatsServiceMock.createStartContract(); +const args: GaugeExpressionProps = { + percentageMode: false, + legend: { + isVisible: true, + position: 'top', + type: 'heatmap_legend', + }, + gridConfig: { + isCellLabelVisible: true, + isYAxisLabelVisible: true, + isXAxisLabelVisible: true, + type: 'heatmap_grid', + }, + palette: { + type: 'palette', + name: '', + params: { + colors: ['rgb(0, 0, 0)', 'rgb(112, 38, 231)'], + stops: [0, 150], + gradient: false, + rangeMin: 0, + rangeMax: 150, + range: 'number', + }, + }, + showTooltip: true, + highlightInHover: false, + xAccessor: 'col-1-2', + valueAccessor: 'col-0-1', +}; +const data: LensMultiTable = { + type: 'datatable', + rows: [ + { 'col-0-1': 0, 'col-1-2': 'a' }, + { 'col-0-1': 148, 'col-1-2': 'b' }, + ], + columns: [ + { id: 'col-0-1', name: 'Count', meta: { type: 'number' } }, + { id: 'col-1-2', name: 'Dest', meta: { type: 'string' } }, + ], +}; + +const mockState = new Map(); +const uiState = { + get: jest + .fn() + .mockImplementation((key, fallback) => (mockState.has(key) ? mockState.get(key) : fallback)), + set: jest.fn().mockImplementation((key, value) => mockState.set(key, value)), + emit: jest.fn(), + setSilent: jest.fn(), +} as any; + +describe('GaugeComponent', function () { + let wrapperProps: GaugeRenderProps; + + beforeAll(() => { + wrapperProps = { + data, + chartsThemeService, + args, + uiState, + onClickValue: jest.fn(), + onSelectRange: jest.fn(), + paletteService: palettesRegistry, + formatFactory: formatService.deserialize, + }; + }); + + it('renders the chart', () => { + const component = shallowWithIntl(); + expect(component.find(Settings).prop('legendPosition')).toEqual('top'); + }); + + it('shows empty placeholder when metricAccessor is not provided', async () => { + const component = mountWithIntl(); + await act(async () => { + expect(findTestSubject(component, 'vislibToggleLegend').length).toBe(1); + }); + }); + + it('shows empty placeholder when minimum accessor equals maximum accessor', async () => { + const newProps = { ...wrapperProps, uiState: undefined } as unknown as GaugeExpressionProps; + const component = mountWithIntl(); + await act(async () => { + expect(findTestSubject(component, 'vislibToggleLegend').length).toBe(0); + }); + }); + + it('rounds to 0,0.0 for range smaller than 10', async () => { + const component = mountWithIntl(); + await act(async () => { + expect(component.find(Settings).prop('legendColorPicker')).toBeDefined(); + }); + }); + + describe('title and subtitle settings', () => {}); + + describe('colors ranges', () => {}); + describe('ticks', () => {}); +}); diff --git a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx index 508f01b134908..a37adc7ca4fae 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx @@ -27,7 +27,7 @@ import { } from '../../../common/expressions/gauge_chart'; import type { FormatFactory } from '../../../common'; -type GaugeRenderProps = GaugeExpressionProps & { +export type GaugeRenderProps = GaugeExpressionProps & { formatFactory: FormatFactory; chartsThemeService: ChartsPluginSetup['theme']; paletteService: PaletteRegistry; @@ -137,7 +137,6 @@ export const GaugeComponent: FC = ({ args, formatFactory, chartsThemeService, - paletteService, }) => { const { shape: subtype, @@ -153,17 +152,19 @@ export const GaugeComponent: FC = ({ ticksPosition, } = args; - const chartTheme = chartsThemeService.useChartsTheme(); - const table = Object.values(data.tables)[0]; - const chartData = table.rows.filter((v) => typeof v[metricAccessor!] === 'number'); - if (!metricAccessor) { return ; } - const accessors = { maxAccessor, minAccessor, goalAccessor, metricAccessor }; + const chartTheme = chartsThemeService.useChartsTheme(); + + const table = Object.values(data.tables)[0]; + const metricColumn = table.columns.find((col) => col.id === metricAccessor); + + const chartData = table.rows.filter((v) => typeof v[metricAccessor!] === 'number'); const row = chartData?.[0]; - const metricValue = getValueFromAccessor('metricAccessor', row, accessors); + + const metricValue = getValueFromAccessor('metricAccessor', row, args); const icon = subtype === GaugeShapes.horizontalBullet @@ -174,9 +175,9 @@ export const GaugeComponent: FC = ({ return ; } - const goal = getValueFromAccessor('goalAccessor', row, accessors); - const min = getMinValue(row, accessors); - const max = getMaxValue(row, accessors); + const goal = getValueFromAccessor('goalAccessor', row, args); + const min = getMinValue(row, args); + const max = getMaxValue(row, args); if (min === max) { return ( @@ -197,7 +198,6 @@ export const GaugeComponent: FC = ({ ? shiftAndNormalizeStops(args.palette?.params as CustomPaletteState, { min, max }) : [min, max]; - const metricColumn = table.columns.find((col) => col.id === metricAccessor); const formatter = formatFactory( metricColumn?.meta?.params?.params ? metricColumn?.meta?.params From 96f363c5ead91d74d4ea12ec27d8dd5666319dd1 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Thu, 25 Nov 2021 16:51:40 +0100 Subject: [PATCH 17/43] added tests --- .../common/expressions/gauge_chart/types.ts | 2 +- .../chart_component.test.tsx.snap | 36 +++ .../gauge/chart_component.test.tsx | 279 +++++++++++++----- .../visualizations/gauge/chart_component.tsx | 18 +- .../visualizations/gauge/expression.tsx | 2 +- .../visualizations/gauge/suggestions.ts | 7 +- .../visualizations/gauge/visualization.tsx | 2 +- 7 files changed, 249 insertions(+), 97 deletions(-) create mode 100644 x-pack/plugins/lens/public/visualizations/gauge/__snapshots__/chart_component.test.tsx.snap diff --git a/x-pack/plugins/lens/common/expressions/gauge_chart/types.ts b/x-pack/plugins/lens/common/expressions/gauge_chart/types.ts index 571489bd20907..dd27e7ac3593f 100644 --- a/x-pack/plugins/lens/common/expressions/gauge_chart/types.ts +++ b/x-pack/plugins/lens/common/expressions/gauge_chart/types.ts @@ -64,5 +64,5 @@ export type GaugeExpressionArgs = SharedGaugeLayerState & { description?: string; shape: GaugeShape; colorMode: GaugeColorMode; - palette: PaletteOutput; + palette?: PaletteOutput; }; diff --git a/x-pack/plugins/lens/public/visualizations/gauge/__snapshots__/chart_component.test.tsx.snap b/x-pack/plugins/lens/public/visualizations/gauge/__snapshots__/chart_component.test.tsx.snap new file mode 100644 index 0000000000000..61028c7108b0a --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/__snapshots__/chart_component.test.tsx.snap @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GaugeComponent renders the chart 1`] = ` + + + + +`; diff --git a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx index d355dadc7d84a..7d703452c3d44 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx @@ -1,22 +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 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. + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ import React from 'react'; -import { Settings } from '@elastic/charts'; -import { findTestSubject } from '@elastic/eui/lib/test'; -import { act } from 'react-dom/test-utils'; -import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest'; +import { Chart, Goal } from '@elastic/charts'; +import { shallowWithIntl } from '@kbn/test/jest'; import { chartPluginMock } from 'src/plugins/charts/public/mocks'; -import type { LensMultiTable } from '../../common'; +import type { ColorStop, CustomPaletteParams, LensMultiTable } from '../../../common'; import { fieldFormatsServiceMock } from '../../../../../../src/plugins/field_formats/public/mocks'; -import { EmptyPlaceholder } from '../../shared_components'; -import { GaugeExpressionArgs, GaugeExpressionProps } from '../../../common/expressions/gauge_chart'; +import { GaugeExpressionArgs, GaugeTitleMode } from '../../../common/expressions/gauge_chart'; import { GaugeComponent, GaugeRenderProps } from './chart_component'; +import { DatatableColumn, DatatableRow } from 'src/plugins/expressions/common'; +import { VisualizationContainer } from '../../visualization_container'; +import { PaletteOutput } from 'src/plugins/charts/common'; jest.mock('@elastic/charts', () => { const original = jest.requireActual('@elastic/charts'); @@ -27,72 +26,57 @@ jest.mock('@elastic/charts', () => { }; }); -const chartsThemeService = chartPluginMock.createSetupContract().theme; -const palettesRegistry = chartPluginMock.createPaletteRegistry(); -const formatService = fieldFormatsServiceMock.createStartContract(); -const args: GaugeExpressionProps = { - percentageMode: false, - legend: { - isVisible: true, - position: 'top', - type: 'heatmap_legend', - }, - gridConfig: { - isCellLabelVisible: true, - isYAxisLabelVisible: true, - isXAxisLabelVisible: true, - type: 'heatmap_grid', - }, - palette: { - type: 'palette', - name: '', +const numberColumn = (id = 'metric-accessor'): DatatableColumn => ({ + id, + name: 'Count of records', + meta: { + type: 'number', + index: 'kibana_sample_data_ecommerce', params: { - colors: ['rgb(0, 0, 0)', 'rgb(112, 38, 231)'], - stops: [0, 150], - gradient: false, - rangeMin: 0, - rangeMax: 150, - range: 'number', + id: 'number', }, }, - showTooltip: true, - highlightInHover: false, - xAccessor: 'col-1-2', - valueAccessor: 'col-0-1', -}; -const data: LensMultiTable = { - type: 'datatable', - rows: [ - { 'col-0-1': 0, 'col-1-2': 'a' }, - { 'col-0-1': 148, 'col-1-2': 'b' }, - ], - columns: [ - { id: 'col-0-1', name: 'Count', meta: { type: 'number' } }, - { id: 'col-1-2', name: 'Dest', meta: { type: 'string' } }, - ], +}); + +const createData = ( + row: DatatableRow = { 'metric-accessor': 3, 'min-accessor': 0, 'max-accessor': 10 } +): LensMultiTable => { + return { + type: 'lens_multitable', + tables: { + layerId: { + type: 'datatable', + rows: [row], + columns: Object.keys(row).map((key) => numberColumn(key)), + }, + }, + }; }; -const mockState = new Map(); -const uiState = { - get: jest - .fn() - .mockImplementation((key, fallback) => (mockState.has(key) ? mockState.get(key) : fallback)), - set: jest.fn().mockImplementation((key, value) => mockState.set(key, value)), - emit: jest.fn(), - setSilent: jest.fn(), -} as any; +const chartsThemeService = chartPluginMock.createSetupContract().theme; +const palettesRegistry = chartPluginMock.createPaletteRegistry(); +const formatService = fieldFormatsServiceMock.createStartContract(); +const args: GaugeExpressionArgs = { + title: 'Gauge', + description: 'vis description', + metricAccessor: 'metric-accessor', + minAccessor: '', + maxAccessor: '', + goalAccessor: '', + shape: 'verticalBullet', + colorMode: 'none', + ticksPosition: 'auto', + visTitleMode: 'auto', +}; describe('GaugeComponent', function () { let wrapperProps: GaugeRenderProps; beforeAll(() => { wrapperProps = { - data, + data: createData(), chartsThemeService, args, - uiState, - onClickValue: jest.fn(), - onSelectRange: jest.fn(), paletteService: palettesRegistry, formatFactory: formatService.deserialize, }; @@ -100,33 +84,166 @@ describe('GaugeComponent', function () { it('renders the chart', () => { const component = shallowWithIntl(); - expect(component.find(Settings).prop('legendPosition')).toEqual('top'); + expect(component.find(Chart)).toMatchSnapshot(); }); it('shows empty placeholder when metricAccessor is not provided', async () => { - const component = mountWithIntl(); - await act(async () => { - expect(findTestSubject(component, 'vislibToggleLegend').length).toBe(1); - }); + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + metricAccessor: undefined, + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + }, + data: createData({ 'min-accessor': 0, 'max-accessor': 10 }), + }; + const component = shallowWithIntl(); + expect(component.find(VisualizationContainer)).toHaveLength(1); }); it('shows empty placeholder when minimum accessor equals maximum accessor', async () => { - const newProps = { ...wrapperProps, uiState: undefined } as unknown as GaugeExpressionProps; - const component = mountWithIntl(); - await act(async () => { - expect(findTestSubject(component, 'vislibToggleLegend').length).toBe(0); - }); + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + }, + data: createData({ 'metric-accessor': 0, 'min-accessor': 0, 'max-accessor': 0 }), + }; + const component = shallowWithIntl(); + expect(component.find('EmptyPlaceholder')).toHaveLength(1); }); - it('rounds to 0,0.0 for range smaller than 10', async () => { - const component = mountWithIntl(); - await act(async () => { - expect(component.find(Settings).prop('legendColorPicker')).toBeDefined(); + describe('title and subtitle settings', () => { + it('displays no title and no subtitle when no passed', () => { + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + visTitleMode: 'none' as GaugeTitleMode, + subtitle: '', + }, + }; + const goal = shallowWithIntl().find(Goal); + expect(goal.prop('labelMajor')).toEqual(''); + expect(goal.prop('labelMinor')).toEqual(''); + }); + it('displays custom title and subtitle when passed', () => { + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + visTitleMode: 'custom' as GaugeTitleMode, + visTitle: 'custom title', + subtitle: 'custom subtitle', + }, + }; + const goal = shallowWithIntl().find(Goal); + expect(goal.prop('labelMajor')).toEqual('custom title '); + expect(goal.prop('labelMinor')).toEqual('custom subtitle '); + }); + it('displays auto title', () => { + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + visTitleMode: 'auto' as GaugeTitleMode, + visTitle: '', + }, + }; + const goal = shallowWithIntl().find(Goal); + expect(goal.prop('labelMajor')).toEqual('Count of records '); }); }); - describe('title and subtitle settings', () => {}); - - describe('colors ranges', () => {}); - describe('ticks', () => {}); + describe('ticks', () => { + it('displays auto ticks', () => { + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + }, + }; + const goal = shallowWithIntl().find(Goal); + expect(goal.prop('ticks')).toEqual([0, 1.33, 2.67, 4]); + }); + it('spreads auto ticks over the color domain if bigger than min/max domain', () => { + const palette = { + type: 'palette' as const, + name: 'custom', + params: { + colors: ['#aaa', '#bbb', '#ccc', '#ddd', '#eee'], + gradient: false, + stops: [20, 40, 60, 80, 100] as unknown as ColorStop[], + range: 'number', + rangeMin: 0, + rangeMax: 4, + }, + }; + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + palette, + }, + } as GaugeRenderProps; + const goal = shallowWithIntl().find(Goal); + expect(goal.prop('ticks')).toEqual([0, 33.33, 66.67, 100]); + }); + it('passes number bands from color palette', () => { + const palette = { + type: 'palette' as const, + name: 'custom', + params: { + colors: ['#aaa', '#bbb', '#ccc', '#ddd', '#eee'], + gradient: false, + stops: [20, 60, 80, 100], + range: 'number', + rangeMin: 0, + rangeMax: 120, + }, + }; + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + colorMode: 'palette', + palette, + ticksPosition: 'bands', + }, + } as GaugeRenderProps; + const goal = shallowWithIntl().find(Goal); + expect(goal.prop('ticks')).toEqual([0, 20, 60, 80, 100]); + expect(goal.prop('bands')).toEqual([0, 20, 60, 80, 100, 100]); + }); + it('passes percent bands from color palette', () => { + const palette = { + type: 'palette' as const, + name: 'custom', + params: { + colors: ['#aaa', '#bbb', '#ccc', '#ddd', '#eee'], + gradient: false, + stops: [20, 60, 80], + range: 'percent', + rangeMin: 0, + rangeMax: 4, + }, + }; + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + colorMode: 'palette', + palette, + ticksPosition: 'bands', + }, + } as GaugeRenderProps; + const goal = shallowWithIntl().find(Goal); + expect(goal.prop('ticks')).toEqual([0, 0.8, 2.4, 3.2, 4]); + expect(goal.prop('bands')).toEqual([0, 0.8, 2.4, 3.2, 4]); + }); + }); }); diff --git a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx index a37adc7ca4fae..38772ccf1f1de 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx @@ -7,7 +7,7 @@ import React, { FC } from 'react'; import { Chart, Goal, Settings } from '@elastic/charts'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n-react'; import type { CustomPaletteState, ChartsPluginSetup, @@ -124,7 +124,7 @@ function getTicks( return [ ...Array(TICKS_NO) .fill(null) - .map((_, i) => min + step * i), + .map((_, i) => Number((min + step * i).toFixed(2))), max, ]; } else { @@ -140,9 +140,6 @@ export const GaugeComponent: FC = ({ }) => { const { shape: subtype, - goalAccessor, - maxAccessor, - minAccessor, metricAccessor, palette, colorMode, @@ -151,7 +148,6 @@ export const GaugeComponent: FC = ({ visTitleMode, ticksPosition, } = args; - if (!metricAccessor) { return ; } @@ -178,7 +174,6 @@ export const GaugeComponent: FC = ({ const goal = getValueFromAccessor('goalAccessor', row, args); const min = getMinValue(row, args); const max = getMaxValue(row, args); - if (min === max) { return ( = ({ ); } - const colors = (palette?.params as CustomPaletteState)?.colors ?? undefined; - const ranges = (palette?.params as CustomPaletteState) - ? shiftAndNormalizeStops(args.palette?.params as CustomPaletteState, { min, max }) - : [min, max]; - const formatter = formatFactory( metricColumn?.meta?.params?.params ? metricColumn?.meta?.params @@ -208,6 +198,10 @@ export const GaugeComponent: FC = ({ }, } ); + const colors = (palette?.params as CustomPaletteState)?.colors ?? undefined; + const ranges = (palette?.params as CustomPaletteState) + ? shiftAndNormalizeStops(args.palette?.params as CustomPaletteState, { min, max }) + : [min, max]; return ( diff --git a/x-pack/plugins/lens/public/visualizations/gauge/expression.tsx b/x-pack/plugins/lens/public/visualizations/gauge/expression.tsx index 9d7dc9ae91b83..b8852f22691ed 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/expression.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/expression.tsx @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { I18nProvider } from '@kbn/i18n/react'; +import { I18nProvider } from '@kbn/i18n-react'; import ReactDOM from 'react-dom'; import React from 'react'; import type { IInterpreterRenderHandlers } from '../../../../../../src/plugins/expressions'; diff --git a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts index 7ad3df93745cb..656e8aa4e9197 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts @@ -33,6 +33,11 @@ export const getSuggestions: Visualization['getSuggesti state && (state.minAccessor || state.maxAccessor || state.goalAccessor || state.metricAccessor) ); + const numberOfAccessors = + state && + [state.minAccessor, state.maxAccessor, state.goalAccessor, state.metricAccessor].filter(Boolean) + .length; + const isShapeChange = (subVisualizationId === GaugeShapes.horizontalBullet || subVisualizationId === GaugeShapes.verticalBullet) && @@ -43,7 +48,7 @@ export const getSuggestions: Visualization['getSuggesti hasLayerMismatch(keptLayerIds, table) || isNotNumericMetric(table) || (!isGauge && table.columns.length > 1) || - (isGauge && table.changeType === 'initial') + (isGauge && numberOfAccessors !== table.columns.length) ) { return []; } diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx index 31a72b273e874..056f045219b48 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { render } from 'react-dom'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n-react'; import { Ast } from '@kbn/interpreter/common'; import { PaletteRegistry } from '../../../../../../src/plugins/charts/public'; import type { DatasourcePublicAPI, OperationMetadata, Visualization } from '../../types'; From bc94d662b0f1e13a8616b535056fe14d14cc4f1e Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Thu, 25 Nov 2021 18:01:36 +0100 Subject: [PATCH 18/43] suggestions --- .../visualizations/gauge/chart_component.tsx | 19 ++++++++++--------- .../visualizations/gauge/suggestions.ts | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx index 38772ccf1f1de..bd62143a0e61a 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx @@ -215,15 +215,16 @@ export const GaugeComponent: FC = ({ tickValueFormatter={({ value: tickValue }) => formatter.convert(tickValue)} bands={ranges} ticks={getTicks(ticksPosition, [min, max], ranges)} - bandFillColor={(val) => { - if (colorMode === 'none') { - return `rgb(255,255,255, 0)`; - } - const index = ranges && ranges.indexOf(val.value) - 1; - return index !== undefined && colors && index >= 0 - ? colors[index] - : 'rgb(255,255,255, 0)'; - }} + bandFillColor={ + colorMode === 'palette' + ? (val) => { + const index = ranges && ranges.indexOf(val.value) - 1; + return index !== undefined && colors && index >= 0 + ? colors[index] + : 'rgb(255,255,255, 0)'; + } + : undefined + } labelMajor={getTitle(visTitleMode, visTitle, metricColumn?.name)} labelMinor={subtitle ? subtitle + ' ' : ''} /> diff --git a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts index 656e8aa4e9197..bb8951d60312e 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts @@ -48,7 +48,7 @@ export const getSuggestions: Visualization['getSuggesti hasLayerMismatch(keptLayerIds, table) || isNotNumericMetric(table) || (!isGauge && table.columns.length > 1) || - (isGauge && numberOfAccessors !== table.columns.length) + (isGauge && (numberOfAccessors !== table.columns.length || table.changeType === 'initial')) ) { return []; } From 81088a9389206d7f8f7bd409012760cdf49a4394 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Fri, 26 Nov 2021 15:31:33 +0100 Subject: [PATCH 19/43] palete fixes & tests --- .../chart_component.test.tsx.snap | 1 - .../gauge/chart_component.test.tsx | 129 +++++++++++++++--- .../visualizations/gauge/chart_component.tsx | 63 +++------ .../visualizations/gauge/dimension_editor.tsx | 1 + .../visualizations/gauge/visualization.tsx | 2 +- 5 files changed, 134 insertions(+), 62 deletions(-) diff --git a/x-pack/plugins/lens/public/visualizations/gauge/__snapshots__/chart_component.test.tsx.snap b/x-pack/plugins/lens/public/visualizations/gauge/__snapshots__/chart_component.test.tsx.snap index 61028c7108b0a..79f2ee8c072e3 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/__snapshots__/chart_component.test.tsx.snap +++ b/x-pack/plugins/lens/public/visualizations/gauge/__snapshots__/chart_component.test.tsx.snap @@ -10,7 +10,6 @@ exports[`GaugeComponent renders the chart 1`] = ` /> { const original = jest.requireActual('@elastic/charts'); @@ -159,25 +158,54 @@ describe('GaugeComponent', function () { }); }); - describe('ticks', () => { + describe('ticks and color bands', () => { it('displays auto ticks', () => { const customProps = { ...wrapperProps, args: { ...wrapperProps.args, + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', }, }; const goal = shallowWithIntl().find(Goal); - expect(goal.prop('ticks')).toEqual([0, 1.33, 2.67, 4]); + expect(goal.prop('ticks')).toEqual([0, 3.33, 6.67, 10]); }); it('spreads auto ticks over the color domain if bigger than min/max domain', () => { const palette = { type: 'palette' as const, name: 'custom', params: { - colors: ['#aaa', '#bbb', '#ccc', '#ddd', '#eee'], + colors: ['#aaa', '#bbb', '#ccc'], gradient: false, - stops: [20, 40, 60, 80, 100] as unknown as ColorStop[], + stops: [10, 20, 30] as unknown as ColorStop[], + range: 'number', + rangeMin: 0, + rangeMax: 20, + }, + }; + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + palette, + }, + } as GaugeRenderProps; + const goal = shallowWithIntl().find(Goal); + expect(goal.prop('ticks')).toEqual([0, 6.67, 13.33, 20]); + }); + it('sets proper color bands and ticks on color bands for values smaller than maximum', () => { + const palette = { + type: 'palette' as const, + name: 'custom', + params: { + colors: ['#aaa', '#bbb', '#ccc'], + gradient: false, + stops: [1, 2, 3] as unknown as ColorStop[], range: 'number', rangeMin: 0, rangeMax: 4, @@ -187,23 +215,84 @@ describe('GaugeComponent', function () { ...wrapperProps, args: { ...wrapperProps.args, + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', palette, + ticksPosition: 'bands', }, } as GaugeRenderProps; const goal = shallowWithIntl().find(Goal); - expect(goal.prop('ticks')).toEqual([0, 33.33, 66.67, 100]); + expect(goal.prop('ticks')).toEqual([0, 1, 2, 10]); + expect(goal.prop('bands')).toEqual([0, 1, 2, 10]); }); - it('passes number bands from color palette', () => { + it('doesnt set ticks for values differing <10%', () => { const palette = { type: 'palette' as const, name: 'custom', params: { - colors: ['#aaa', '#bbb', '#ccc', '#ddd', '#eee'], + colors: ['#aaa', '#bbb', '#ccc'], gradient: false, - stops: [20, 60, 80, 100], + stops: [1, 1.5, 3] as unknown as ColorStop[], range: 'number', rangeMin: 0, - rangeMax: 120, + rangeMax: 10, + }, + }; + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + palette, + ticksPosition: 'bands', + }, + } as GaugeRenderProps; + const goal = shallowWithIntl().find(Goal); + expect(goal.prop('ticks')).toEqual([0, 1, 10]); + expect(goal.prop('bands')).toEqual([0, 1, 1.5, 10]); + }); + it('sets proper color bands and ticks on color bands for values greater than maximum', () => { + const palette = { + type: 'palette' as const, + name: 'custom', + params: { + colors: ['#aaa', '#bbb', '#ccc'], + gradient: false, + stops: [10, 20, 30, 31] as unknown as ColorStop[], + range: 'number', + rangeMin: 0, + rangeMax: 30, + }, + }; + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + palette, + ticksPosition: 'bands', + }, + } as GaugeRenderProps; + const goal = shallowWithIntl().find(Goal); + expect(goal.prop('ticks')).toEqual([0, 10, 20, 30]); + expect(goal.prop('bands')).toEqual([0, 10, 20, 30]); + }); + it('passes number bands from color palette with no stops defined', () => { + const palette = { + type: 'palette' as const, + name: 'gray', + params: { + colors: ['#aaa', '#bbb'], + gradient: false, + stops: [], + range: 'number', + rangeMin: 0, + rangeMax: 10, }, }; const customProps = { @@ -213,23 +302,26 @@ describe('GaugeComponent', function () { colorMode: 'palette', palette, ticksPosition: 'bands', + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', }, } as GaugeRenderProps; const goal = shallowWithIntl().find(Goal); - expect(goal.prop('ticks')).toEqual([0, 20, 60, 80, 100]); - expect(goal.prop('bands')).toEqual([0, 20, 60, 80, 100, 100]); + expect(goal.prop('ticks')).toEqual([0, 5, 10]); + expect(goal.prop('bands')).toEqual([0, 5, 10]); }); it('passes percent bands from color palette', () => { const palette = { type: 'palette' as const, name: 'custom', params: { - colors: ['#aaa', '#bbb', '#ccc', '#ddd', '#eee'], + colors: ['#aaa', '#bbb', '#ccc'], gradient: false, stops: [20, 60, 80], range: 'percent', rangeMin: 0, - rangeMax: 4, + rangeMax: 10, }, }; const customProps = { @@ -239,11 +331,14 @@ describe('GaugeComponent', function () { colorMode: 'palette', palette, ticksPosition: 'bands', + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', }, } as GaugeRenderProps; const goal = shallowWithIntl().find(Goal); - expect(goal.prop('ticks')).toEqual([0, 0.8, 2.4, 3.2, 4]); - expect(goal.prop('bands')).toEqual([0, 0.8, 2.4, 3.2, 4]); + expect(goal.prop('ticks')).toEqual([0, 2, 6, 8]); + expect(goal.prop('bands')).toEqual([0, 2, 6, 8]); }); }); }); diff --git a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx index bd62143a0e61a..541f5e58ffb53 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx @@ -42,49 +42,25 @@ declare global { } } -function getStops( +function normalizeBands( { colors, stops, range }: CustomPaletteState, { min, max }: { min: number; max: number } ) { - if (stops.length) { - return stops; + if (!stops.length) { + const step = (max - min) / colors.length; + return [min, ...colors.map((_, i) => min + (i + 1) * step)]; } - const step = (max - min) / colors.length; - return colors.map((_, i) => min + i * step); -} - -function shiftAndNormalizeStops( - params: CustomPaletteState, - { min, max }: { min: number; max: number } -) { - const baseStops = [ - ...getStops(params, { min, max }).map((value) => { - let result = value; - if (params.range === 'percent' && params.stops.length) { - result = min + value * ((max - min) / 100); - } - // for a range of 1 value the formulas above will divide by 0, so here's a safety guard - if (Number.isNaN(result)) { - return 1; - } - return result; - }), - ]; - if (params.range === 'percent') { - const convertedMax = min + params.rangeMin * ((max - min) / 100); - baseStops.push(Math.max(max, convertedMax)); - } else { - baseStops.push(Math.max(max, ...params.stops)); + if (range === 'percent') { + return [min, ...stops.map((step) => min + step * ((max - min) / 100))]; } - if (params.stops.length) { - if (params.range === 'percent') { - baseStops.unshift(min + params.rangeMin * ((max - min) / 100)); - } else { - baseStops.unshift(params.rangeMin); - } + if (max >= Math.max(...stops)) { + // the max value has changed but the palette has outdated information + const updatedStops = [...stops.slice(0, -1), max]; + return [min, ...updatedStops]; + } else { + return [min, ...stops.slice(0, -1)]; } - return baseStops; } function getTitle(visTitleMode: GaugeTitleMode, visTitle?: string, fallbackTitle?: string) { @@ -174,6 +150,7 @@ export const GaugeComponent: FC = ({ const goal = getValueFromAccessor('goalAccessor', row, args); const min = getMinValue(row, args); const max = getMaxValue(row, args); + if (min === max) { return ( = ({ } ); const colors = (palette?.params as CustomPaletteState)?.colors ?? undefined; - const ranges = (palette?.params as CustomPaletteState) - ? shiftAndNormalizeStops(args.palette?.params as CustomPaletteState, { min, max }) + const bands = (palette?.params as CustomPaletteState) + ? normalizeBands(args.palette?.params as CustomPaletteState, { min, max }) : [min, max]; return ( @@ -213,15 +190,15 @@ export const GaugeComponent: FC = ({ target={goal} actual={metricValue} tickValueFormatter={({ value: tickValue }) => formatter.convert(tickValue)} - bands={ranges} - ticks={getTicks(ticksPosition, [min, max], ranges)} + bands={bands} + ticks={getTicks(ticksPosition, [min, max], bands)} bandFillColor={ colorMode === 'palette' ? (val) => { - const index = ranges && ranges.indexOf(val.value) - 1; - return index !== undefined && colors && index >= 0 + const index = bands && bands.indexOf(val.value) - 1; + return colors && index >= 0 && colors[index] ? colors[index] - : 'rgb(255,255,255, 0)'; + : colors[colors.length - 1]; } : undefined } diff --git a/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx index 72b25db584407..7fec884ed4585 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx @@ -69,6 +69,7 @@ export function GaugeDimensionEditor( }; const displayStops = applyPaletteParams(props.paletteService, activePalette, currentMinMax); + const togglePalette = () => setIsPaletteOpen(!isPaletteOpen); return ( <> diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx index 056f045219b48..af789b59fb658 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx @@ -435,7 +435,7 @@ export const getGaugeVisualization = ({ if (minValue > goalValue) { warnings.push([ , ]); From 909a7202b35de54c86a893452c6cbc067d322014 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Mon, 29 Nov 2021 17:00:53 +0100 Subject: [PATCH 20/43] fix magic max --- .../lens/public/visualizations/gauge/utils.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/lens/public/visualizations/gauge/utils.ts b/x-pack/plugins/lens/public/visualizations/gauge/utils.ts index 7f34c510488af..a543b94c6fe14 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/utils.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/utils.ts @@ -46,10 +46,12 @@ export const getMaxValue = (row?: DatatableRow, state?: GaugeAccessorsType) => { const goalValue = goalAccessor && row[goalAccessor]; if (metricValue != null) { const minValue = getMinValue(row, state); - const biggerValue = Math.max(goalValue ?? 0, metricValue, 1); + const biggerValue = goalValue ? Math.max(goalValue, metricValue) : metricValue; const nicelyRounded = scaleLinear().domain([minValue, biggerValue]).nice().ticks(4); - if (nicelyRounded.length > 2) - return nicelyRounded[nicelyRounded.length - 1] + nicelyRounded[1]; + if (nicelyRounded.length > 2) { + const ticksDifference = Math.abs(nicelyRounded[0] - nicelyRounded[1]); + return nicelyRounded[nicelyRounded.length - 1] + ticksDifference; + } return minValue === biggerValue ? biggerValue + 1 : biggerValue; } } @@ -79,7 +81,7 @@ export const getMetricValue = (row?: DatatableRow, state?: GaugeAccessorsType) = } const minValue = getMinValue(row, state); const maxValue = getMaxValue(row, state); - return Math.round((minValue + maxValue) * 0.6); + return Math.round((maxValue - minValue) * 0.6 + minValue); }; export const getGoalValue = (row?: DatatableRow, state?: GaugeVisualizationState) => { @@ -89,7 +91,7 @@ export const getGoalValue = (row?: DatatableRow, state?: GaugeVisualizationState } const minValue = getMinValue(row, state); const maxValue = getMaxValue(row, state); - return Math.round((minValue + maxValue) * 0.8); + return Math.round((maxValue - minValue) * 0.8 + minValue); }; export const transparentizePalettes = (palettes: PaletteRegistry) => { From b9ec54de6832a582162bf6214796289805064d6f Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Mon, 29 Nov 2021 17:27:52 +0100 Subject: [PATCH 21/43] metric should not overflow --- .../lens/public/visualizations/gauge/chart_component.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx index 541f5e58ffb53..32f0a7cccc788 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx @@ -176,10 +176,13 @@ export const GaugeComponent: FC = ({ } ); const colors = (palette?.params as CustomPaletteState)?.colors ?? undefined; - const bands = (palette?.params as CustomPaletteState) + const bands: number[] = (palette?.params as CustomPaletteState) ? normalizeBands(args.palette?.params as CustomPaletteState, { min, max }) : [min, max]; + const maxBand = Math.max(...bands); + const minBand = Math.min(...bands); + return ( @@ -187,8 +190,8 @@ export const GaugeComponent: FC = ({ id="spec_1" subtype={subtype} base={min} - target={goal} - actual={metricValue} + target={goal ? Math.min(goal, maxBand) : undefined} + actual={Math.min(Math.max(metricValue, minBand), maxBand)} tickValueFormatter={({ value: tickValue }) => formatter.convert(tickValue)} bands={bands} ticks={getTicks(ticksPosition, [min, max], bands)} From 940b5bd402c87ecd9ae03590751228effa111d21 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Mon, 29 Nov 2021 18:49:28 +0100 Subject: [PATCH 22/43] address review --- .../chart_component.test.tsx.snap | 1 + .../gauge/chart_component.test.tsx | 29 +++++++++++ .../visualizations/gauge/chart_component.tsx | 16 +++++- .../gauge/toolbar_component/index.tsx | 51 ++++++++++++------- .../lens/public/visualizations/gauge/utils.ts | 9 ++-- 5 files changed, 82 insertions(+), 24 deletions(-) diff --git a/x-pack/plugins/lens/public/visualizations/gauge/__snapshots__/chart_component.test.tsx.snap b/x-pack/plugins/lens/public/visualizations/gauge/__snapshots__/chart_component.test.tsx.snap index 79f2ee8c072e3..61028c7108b0a 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/__snapshots__/chart_component.test.tsx.snap +++ b/x-pack/plugins/lens/public/visualizations/gauge/__snapshots__/chart_component.test.tsx.snap @@ -10,6 +10,7 @@ exports[`GaugeComponent renders the chart 1`] = ` /> ); expect(component.find('EmptyPlaceholder')).toHaveLength(1); }); + it('shows empty placeholder when minimum accessor value is greater maximum accessor value', async () => { + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + }, + data: createData({ 'metric-accessor': 0, 'min-accessor': 0, 'max-accessor': -10 }), + }; + const component = shallowWithIntl(); + expect(component.find('EmptyPlaceholder')).toHaveLength(1); + }); + it('when metric value is bigger than max, it takes maximum value', () => { + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + ticksPosition: 'bands', + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + }, + data: createData({ 'metric-accessor': 12, 'min-accessor': 0, 'max-accessor': 10 }), + } as GaugeRenderProps; + const goal = shallowWithIntl().find(Goal); + expect(goal.prop('actual')).toEqual(10); + }); describe('title and subtitle settings', () => { it('displays no title and no subtitle when no passed', () => { diff --git a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx index 32f0a7cccc788..4246a971ea9d4 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx @@ -163,6 +163,18 @@ export const GaugeComponent: FC = ({ } /> ); + } else if (min > max) { + return ( + + } + /> + ); } const formatter = formatFactory( @@ -171,7 +183,7 @@ export const GaugeComponent: FC = ({ : { id: 'number', params: { - pattern: max - min > 10 ? `0,0` : `0,0.0`, + pattern: max - min > 5 ? `0,0` : `0,0.0`, }, } ); @@ -203,7 +215,7 @@ export const GaugeComponent: FC = ({ ? colors[index] : colors[colors.length - 1]; } - : undefined + : () => `rgba(255,255,255,0)` } labelMajor={getTitle(visTitleMode, visTitle, metricColumn?.name)} labelMinor={subtitle ? subtitle + ' ' : ''} diff --git a/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx index 3849a27a1b1a9..7b661828d8aff 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx @@ -9,7 +9,12 @@ import React, { memo, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { VisualizationToolbarProps } from '../../../types'; -import { ToolbarPopover, useDebouncedValue, VisLabel } from '../../../shared_components'; +import { + ToolbarPopover, + TooltipWrapper, + useDebouncedValue, + VisLabel, +} from '../../../shared_components'; import './gauge_config_panel.scss'; import { GaugeTicksPositions, @@ -101,24 +106,34 @@ export const GaugeToolbar = memo((props: VisualizationToolbarProps - { - handleInputChange({ - ...inputValue, - ticksPosition: e.target.checked - ? GaugeTicksPositions.bands - : GaugeTicksPositions.auto, - }); - }} - /> + condition={state.colorMode !== 'palette'} + position="top" + delay="regular" + display="block" + > + { + handleInputChange({ + ...inputValue, + ticksPosition: e.target.checked + ? GaugeTicksPositions.bands + : GaugeTicksPositions.auto, + }); + }} + /> +
diff --git a/x-pack/plugins/lens/public/visualizations/gauge/utils.ts b/x-pack/plugins/lens/public/visualizations/gauge/utils.ts index a543b94c6fe14..d49b70769cc82 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/utils.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/utils.ts @@ -34,7 +34,7 @@ export const getValueFromAccessor = ( } }; -export const getMaxValue = (row?: DatatableRow, state?: GaugeAccessorsType) => { +export const getMaxValue = (row?: DatatableRow, state?: GaugeAccessorsType): number => { const FALLBACK_VALUE = 100; const currentValue = getValueFromAccessor('maxAccessor', row, state); if (currentValue != null) { @@ -65,10 +65,11 @@ export const getMinValue = (row?: DatatableRow, state?: GaugeAccessorsType) => { } const FALLBACK_VALUE = 0; if (row && state) { - const { metricAccessor } = state; + const { metricAccessor, maxAccessor } = state; const metricValue = metricAccessor && row[metricAccessor]; - if (metricValue < 0) { - return metricValue - 10; // TODO: TO THINK THROUGH + const maxValue = maxAccessor && row[maxAccessor]; + if (Math.min(metricValue, maxValue) < 0) { + return Math.min(metricValue, maxValue) - 10; // TODO: TO THINK THROUGH } } return FALLBACK_VALUE; From 9c74621b7db8550a570cf13415e1a61f4d1d44ca Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 30 Nov 2021 11:51:03 +0100 Subject: [PATCH 23/43] [Lens] adding toolbar tests --- .../public/shared_components/vis_label.tsx | 6 +- .../toolbar_component/gauge_toolbar.test.tsx | 227 ++++++++++++++++++ .../gauge/toolbar_component/index.tsx | 14 +- 3 files changed, 239 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/gauge_toolbar.test.tsx diff --git a/x-pack/plugins/lens/public/shared_components/vis_label.tsx b/x-pack/plugins/lens/public/shared_components/vis_label.tsx index 1818119501ebf..03afe36399e10 100644 --- a/x-pack/plugins/lens/public/shared_components/vis_label.tsx +++ b/x-pack/plugins/lens/public/shared_components/vis_label.tsx @@ -23,6 +23,7 @@ export interface VisLabelProps { placeholder?: string; hasAutoOption?: boolean; header?: string; + dataTestSubj?: string; } const defaultHeader = i18n.translate('xpack.lens.label.header', { @@ -64,13 +65,14 @@ export function VisLabel({ hasAutoOption = false, placeholder = '', header = defaultHeader, + dataTestSubj, }: VisLabelProps) { return ( { if (target.value === 'custom') { @@ -85,7 +87,7 @@ export function VisLabel({ { + const original = jest.requireActual('lodash'); + + return { + ...original, + debounce: (fn: unknown) => fn, + }; +}); + +class Harness { + wrapper: ReactWrapper; + + constructor(wrapper: ReactWrapper) { + this.wrapper = wrapper; + } + + togglePopover() { + this.wrapper.find(ToolbarButton).simulate('click'); + } + + public get titleLabel() { + return this.wrapper.find('EuiFieldText[data-test-subj="lens-toolbar-gauge-title"]'); + } + public get titleSelect() { + return this.wrapper.find('EuiSelect[data-test-subj="lens-toolbar-gauge-title-select"]'); + } + + modifyTitle(e: FormEvent) { + act(() => { + this.titleLabel.prop('onChange')!(e); + }); + } + + public get subtitleSelect() { + return this.wrapper.find('EuiSelect[data-test-subj="lens-toolbar-gauge-subtitle-select"]'); + } + + public get subtitleLabel() { + return this.wrapper.find('EuiFieldText[data-test-subj="lens-toolbar-gauge-subtitle"]'); + } + + modifySubtitle(e: FormEvent) { + act(() => { + this.subtitleLabel.prop('onChange')!(e); + }); + } + public get ticksOnColorBandsSwitch() { + return this.wrapper.find( + 'EuiSwitch[data-test-subj="lens-toolbar-gauge-ticks-position-switch"]' + ); + } + + toggleTicksPositionSwitch() { + act(() => { + this.ticksOnColorBandsSwitch.prop('onChange')!({} as FormEvent); + }); + } +} + +describe('gauge toolbar', () => { + let harness: Harness; + let defaultProps: VisualizationToolbarProps; + + beforeEach(() => { + defaultProps = { + setState: jest.fn(), + frame: {} as FramePublicAPI, + state: { + layerId: 'layerId', + layerType: 'data', + metricAccessor: 'metric-accessor', + minAccessor: '', + maxAccessor: '', + goalAccessor: '', + shape: 'verticalBullet', + colorMode: 'none', + ticksPosition: 'auto', + visTitleMode: 'auto', + }, + }; + }); + + it('should reflect state in the UI for default props', async () => { + harness = new Harness(mountWithIntl()); + harness.togglePopover(); + + expect(harness.ticksOnColorBandsSwitch.prop('checked')).toBe(false); + expect(harness.titleLabel.prop('value')).toBe(''); + expect(harness.titleSelect.prop('value')).toBe('auto'); + expect(harness.subtitleLabel.prop('value')).toBe(''); + expect(harness.subtitleSelect.prop('value')).toBe('none'); + }); + it('should reflect state in the UI for non-default props', async () => { + const props = { + ...defaultProps, + state: { + ...defaultProps.state, + colorMode: 'palette' as const, + ticksPosition: 'bands' as const, + visTitleMode: 'custom' as const, + visTitle: 'new title', + subtitle: 'new subtitle', + }, + }; + + harness = new Harness(mountWithIntl()); + harness.togglePopover(); + + expect(harness.ticksOnColorBandsSwitch.prop('checked')).toBe(true); + expect(harness.titleLabel.prop('value')).toBe('new title'); + expect(harness.titleSelect.prop('value')).toBe('custom'); + expect(harness.subtitleLabel.prop('value')).toBe('new subtitle'); + expect(harness.subtitleSelect.prop('value')).toBe('custom'); + }); + describe('Ticks position switch', () => { + it('switch is disabled if colorMode is none', () => { + defaultProps.state.colorMode = 'none' as const; + + harness = new Harness(mountWithIntl()); + harness.togglePopover(); + + expect(harness.ticksOnColorBandsSwitch.prop('disabled')).toBe(true); + expect(harness.ticksOnColorBandsSwitch.prop('checked')).toBe(false); + }); + it('switch is enabled if colorMode is not none', () => { + defaultProps.state.colorMode = 'palette' as const; + + harness = new Harness(mountWithIntl()); + harness.togglePopover(); + + expect(harness.ticksOnColorBandsSwitch.prop('disabled')).toBe(false); + }); + it('Ticks position switch updates the state when clicked', () => { + defaultProps.state.colorMode = 'palette' as const; + harness = new Harness(mountWithIntl()); + harness.togglePopover(); + + harness.toggleTicksPositionSwitch(); + + expect(defaultProps.setState).toHaveBeenCalledTimes(1); + expect(defaultProps.setState).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + ticksPosition: 'bands', + }) + ); + }); + }); + + describe('title', () => { + it('title label is disabled if title is selected to be none', () => { + defaultProps.state.visTitleMode = 'none' as const; + + harness = new Harness(mountWithIntl()); + harness.togglePopover(); + + expect(harness.titleSelect.prop('value')).toBe('none'); + expect(harness.titleLabel.prop('disabled')).toBe(true); + expect(harness.titleLabel.prop('value')).toBe(''); + }); + it('title mode switches to custom when user starts typing', () => { + defaultProps.state.visTitleMode = 'auto' as const; + + harness = new Harness(mountWithIntl()); + harness.togglePopover(); + + expect(harness.titleSelect.prop('value')).toBe('auto'); + expect(harness.titleLabel.prop('disabled')).toBe(false); + expect(harness.titleLabel.prop('value')).toBe(''); + harness.modifyTitle({ target: { value: 'title' } } as unknown as FormEvent); + expect(defaultProps.setState).toHaveBeenCalledTimes(1); + expect(defaultProps.setState).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + visTitleMode: 'custom', + visTitle: 'title', + }) + ); + }); + }); + describe('subtitle', () => { + it('subtitle label is enabled if subtitle is string', () => { + defaultProps.state.subtitle = 'subtitle label'; + + harness = new Harness(mountWithIntl()); + harness.togglePopover(); + + expect(harness.subtitleSelect.prop('value')).toBe('custom'); + expect(harness.subtitleLabel.prop('disabled')).toBe(false); + expect(harness.subtitleLabel.prop('value')).toBe('subtitle label'); + }); + it('title mode can switch to custom', () => { + defaultProps.state.subtitle = ''; + + harness = new Harness(mountWithIntl()); + harness.togglePopover(); + + expect(harness.subtitleSelect.prop('value')).toBe('none'); + expect(harness.subtitleLabel.prop('disabled')).toBe(true); + expect(harness.subtitleLabel.prop('value')).toBe(''); + harness.modifySubtitle({ target: { value: 'subtitle label' } } as unknown as FormEvent); + expect(defaultProps.setState).toHaveBeenCalledTimes(1); + expect(defaultProps.setState).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + subtitle: 'subtitle label', + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx index 7b661828d8aff..6e3243ac9abef 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx @@ -63,6 +63,7 @@ export const GaugeToolbar = memo((props: VisualizationToolbarProps - {/*
*/} { @@ -120,16 +121,17 @@ export const GaugeToolbar = memo((props: VisualizationToolbarProps { + onChange={() => { handleInputChange({ ...inputValue, - ticksPosition: e.target.checked - ? GaugeTicksPositions.bands - : GaugeTicksPositions.auto, + ticksPosition: + state.ticksPosition === GaugeTicksPositions.bands + ? GaugeTicksPositions.auto + : GaugeTicksPositions.bands, }); }} /> From ba83d441af72a6a9d46d53ef9a10e48216752121 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 30 Nov 2021 12:22:58 +0100 Subject: [PATCH 24/43] limit domain to --- .../gauge/chart_component.test.tsx | 20 +++++++++---------- .../visualizations/gauge/chart_component.tsx | 20 ++++++------------- .../gauge/toolbar_component/index.tsx | 2 +- .../gauge/visualization.test.ts | 20 ------------------- .../visualizations/gauge/visualization.tsx | 17 ---------------- 5 files changed, 17 insertions(+), 62 deletions(-) diff --git a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx index 42f3e5f0cf1e6..c98d7bb5c1ca7 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx @@ -201,7 +201,7 @@ describe('GaugeComponent', function () { const goal = shallowWithIntl().find(Goal); expect(goal.prop('ticks')).toEqual([0, 3.33, 6.67, 10]); }); - it('spreads auto ticks over the color domain if bigger than min/max domain', () => { + it('spreads auto ticks only over the [min, max] domain if color bands defined bigger domain', () => { const palette = { type: 'palette' as const, name: 'custom', @@ -225,7 +225,7 @@ describe('GaugeComponent', function () { }, } as GaugeRenderProps; const goal = shallowWithIntl().find(Goal); - expect(goal.prop('ticks')).toEqual([0, 6.67, 13.33, 20]); + expect(goal.prop('ticks')).toEqual([0, 3.33, 6.67, 10]); }); it('sets proper color bands and ticks on color bands for values smaller than maximum', () => { const palette = { @@ -252,8 +252,8 @@ describe('GaugeComponent', function () { }, } as GaugeRenderProps; const goal = shallowWithIntl().find(Goal); - expect(goal.prop('ticks')).toEqual([0, 1, 2, 10]); - expect(goal.prop('bands')).toEqual([0, 1, 2, 10]); + expect(goal.prop('ticks')).toEqual([0, 1, 2, 3, 10]); + expect(goal.prop('bands')).toEqual([0, 1, 2, 3, 10]); }); it('doesnt set ticks for values differing <10%', () => { const palette = { @@ -280,8 +280,8 @@ describe('GaugeComponent', function () { }, } as GaugeRenderProps; const goal = shallowWithIntl().find(Goal); - expect(goal.prop('ticks')).toEqual([0, 1, 10]); - expect(goal.prop('bands')).toEqual([0, 1, 1.5, 10]); + expect(goal.prop('ticks')).toEqual([0, 1, 3, 10]); + expect(goal.prop('bands')).toEqual([0, 1, 1.5, 3, 10]); }); it('sets proper color bands and ticks on color bands for values greater than maximum', () => { const palette = { @@ -308,8 +308,8 @@ describe('GaugeComponent', function () { }, } as GaugeRenderProps; const goal = shallowWithIntl().find(Goal); - expect(goal.prop('ticks')).toEqual([0, 10, 20, 30]); - expect(goal.prop('bands')).toEqual([0, 10, 20, 30]); + expect(goal.prop('ticks')).toEqual([0, 10]); + expect(goal.prop('bands')).toEqual([0, 10]); }); it('passes number bands from color palette with no stops defined', () => { const palette = { @@ -366,8 +366,8 @@ describe('GaugeComponent', function () { }, } as GaugeRenderProps; const goal = shallowWithIntl().find(Goal); - expect(goal.prop('ticks')).toEqual([0, 2, 6, 8]); - expect(goal.prop('bands')).toEqual([0, 2, 6, 8]); + expect(goal.prop('ticks')).toEqual([0, 2, 6, 8, 10]); + expect(goal.prop('bands')).toEqual([0, 2, 6, 8, 10]); }); }); }); diff --git a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx index 4246a971ea9d4..36db82a5bec91 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx @@ -51,16 +51,11 @@ function normalizeBands( return [min, ...colors.map((_, i) => min + (i + 1) * step)]; } if (range === 'percent') { - return [min, ...stops.map((step) => min + step * ((max - min) / 100))]; - } - - if (max >= Math.max(...stops)) { - // the max value has changed but the palette has outdated information - const updatedStops = [...stops.slice(0, -1), max]; - return [min, ...updatedStops]; - } else { - return [min, ...stops.slice(0, -1)]; + const filteredStops = stops.filter((stop) => stop >= 0 && stop <= 100); + return [min, ...filteredStops.map((step) => min + step * ((max - min) / 100)), max]; } + const orderedStops = stops.filter((stop, i) => stop < max); + return [min, ...orderedStops, max]; } function getTitle(visTitleMode: GaugeTitleMode, visTitle?: string, fallbackTitle?: string) { @@ -192,9 +187,6 @@ export const GaugeComponent: FC = ({ ? normalizeBands(args.palette?.params as CustomPaletteState, { min, max }) : [min, max]; - const maxBand = Math.max(...bands); - const minBand = Math.min(...bands); - return ( @@ -202,8 +194,8 @@ export const GaugeComponent: FC = ({ id="spec_1" subtype={subtype} base={min} - target={goal ? Math.min(goal, maxBand) : undefined} - actual={Math.min(Math.max(metricValue, minBand), maxBand)} + target={goal ? Math.min(Math.max(goal, min), max) : undefined} + actual={Math.min(Math.max(metricValue, min), max)} tickValueFormatter={({ value: tickValue }) => formatter.convert(tickValue)} bands={bands} ticks={getTicks(ticksPosition, [min, max], bands)} diff --git a/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx index 6e3243ac9abef..81be5ddddde73 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx @@ -6,7 +6,7 @@ */ import React, { memo, useState } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSwitch } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { VisualizationToolbarProps } from '../../../types'; import { diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts index c8a35bb05185e..b2746018a07e3 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts @@ -518,27 +518,7 @@ describe('gauge', () => { }).getWarningMessages!(state, frame) ).toHaveLength(1); }); - it('should warn when minimum value is greater than maximum value', () => { - frame.activeData = { - first: { - type: 'datatable', - columns: [], - rows: [ - { - 'min-accessor': 5, - 'metric-accessor': 2, - 'max-accessor': 3, - }, - ], - }, - }; - expect( - getGaugeVisualization({ - paletteService, - }).getWarningMessages!(state, frame) - ).toHaveLength(2); - }); it('should warn when metric value is greater than maximum value', () => { frame.activeData = { first: { diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx index af789b59fb658..fbc52c21067f1 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx @@ -413,15 +413,6 @@ export const getGaugeVisualization = ({ const minValue = minAccessor && row[minAccessor]; const goalValue = goalAccessor && row[goalAccessor]; - if (typeof minValue === 'number' && minValue === maxValue) { - return [ - , - ]; - } - const warnings = []; if (typeof minValue === 'number') { if (minValue > metricValue) { @@ -451,14 +442,6 @@ export const getGaugeVisualization = ({ />, ]); } - if (minValue > maxValue) { - warnings.push([ - , - ]); - } if (typeof goalValue === 'number' && goalValue > maxValue) { warnings.push([ From 730ce3f50681ea2770c19a90b1d4bef3df7be26e Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 30 Nov 2021 13:18:43 +0100 Subject: [PATCH 25/43] changes the order of experimental badge and dataLoss indicator --- .../workspace_panel/chart_switch.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx index fe054edfb2917..df6f8e9112a49 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx @@ -339,6 +339,16 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) { alignItems="center" className="lnsChartSwitch__append" > + {v.showExperimentalBadge ? ( + + + + + + ) : null} {v.selection.dataLoss !== 'nothing' ? ( ) : null} - {v.showExperimentalBadge ? ( - - - - - - ) : null} ) : null, // Apparently checked: null is not valid for TS From f386f6fbf24bef2e4fd82ebea763d592915e8175 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 30 Nov 2021 14:23:29 +0100 Subject: [PATCH 26/43] fix i18n --- .../lens/public/visualizations/gauge/chart_component.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx index 36db82a5bec91..a4645e6fa3d85 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx @@ -152,7 +152,7 @@ export const GaugeComponent: FC = ({ icon={icon} message={ } @@ -164,7 +164,7 @@ export const GaugeComponent: FC = ({ icon={icon} message={ } From 41f60626522f7693249d741d61d365bc61215044 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Wed, 1 Dec 2021 13:17:50 +0100 Subject: [PATCH 27/43] address feedback p1 --- .../lens/public/assets/chart_gauge.tsx | 37 ++-- .../public/shared_components/vis_label.tsx | 1 + .../visualizations/gauge/chart_component.tsx | 18 +- .../visualizations/gauge/dimension_editor.tsx | 205 ++++++++++-------- .../toolbar_component/gauge_config_panel.scss | 2 +- .../gauge/toolbar_component/index.tsx | 52 +---- .../lens/public/visualizations/gauge/utils.ts | 10 +- .../visualizations/gauge/visualization.tsx | 16 +- 8 files changed, 167 insertions(+), 174 deletions(-) diff --git a/x-pack/plugins/lens/public/assets/chart_gauge.tsx b/x-pack/plugins/lens/public/assets/chart_gauge.tsx index f9272d4a77961..afece311e5fae 100644 --- a/x-pack/plugins/lens/public/assets/chart_gauge.tsx +++ b/x-pack/plugins/lens/public/assets/chart_gauge.tsx @@ -14,9 +14,9 @@ export const LensIconChartGaugeHorizontal = ({ ...props }: Omit) => ( {title ? {title} : null} ); @@ -40,27 +40,22 @@ export const LensIconChartGaugeVertical = ({ ...props }: Omit) => ( {title ? {title} : null} - - - - - - - + + ); diff --git a/x-pack/plugins/lens/public/shared_components/vis_label.tsx b/x-pack/plugins/lens/public/shared_components/vis_label.tsx index 03afe36399e10..2fec7f56561a9 100644 --- a/x-pack/plugins/lens/public/shared_components/vis_label.tsx +++ b/x-pack/plugins/lens/public/shared_components/vis_label.tsx @@ -71,6 +71,7 @@ export function VisLabel({ stop < min).length, 0); + return colors.slice(colorsOutOfRangeSmaller); +} + function normalizeBands( { colors, stops, range }: CustomPaletteState, { min, max }: { min: number; max: number } @@ -54,7 +62,7 @@ function normalizeBands( const filteredStops = stops.filter((stop) => stop >= 0 && stop <= 100); return [min, ...filteredStops.map((step) => min + step * ((max - min) / 100)), max]; } - const orderedStops = stops.filter((stop, i) => stop < max); + const orderedStops = stops.filter((stop, i) => stop < max && stop > min); return [min, ...orderedStops, max]; } @@ -153,7 +161,7 @@ export const GaugeComponent: FC = ({ message={ } /> @@ -165,7 +173,7 @@ export const GaugeComponent: FC = ({ message={ } /> @@ -182,7 +190,7 @@ export const GaugeComponent: FC = ({ }, } ); - const colors = (palette?.params as CustomPaletteState)?.colors ?? undefined; + const colors = palette?.params?.colors ? normalizeColors(palette.params, min) : undefined; const bands: number[] = (palette?.params as CustomPaletteState) ? normalizeBands(args.palette?.params as CustomPaletteState, { min, max }) : [min, max]; @@ -194,7 +202,7 @@ export const GaugeComponent: FC = ({ id="spec_1" subtype={subtype} base={min} - target={goal ? Math.min(Math.max(goal, min), max) : undefined} + target={goal && goal >= min && goal <= max ? goal : undefined} actual={Math.min(Math.max(metricValue, min), max)} tickValueFormatter={({ value: tickValue }) => formatter.convert(tickValue)} bands={bands} diff --git a/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx index 7fec884ed4585..f57a789b339ca 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx @@ -17,7 +17,11 @@ import { import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import type { PaletteRegistry } from 'src/plugins/charts/public'; -import { isNumericFieldForDatatable, GaugeVisualizationState } from '../../../common/expressions'; +import { + isNumericFieldForDatatable, + GaugeVisualizationState, + GaugeTicksPositions, +} from '../../../common/expressions'; import { applyPaletteParams, CustomizablePalette, @@ -88,24 +92,23 @@ export function GaugeDimensionEditor( checked={hasDynamicColoring} onChange={(e: EuiSwitchEvent) => { const { checked } = e.target; - const params: Partial = { - colorMode: checked ? 'palette' : 'none', - }; - if (checked) { - params.palette = { - ...activePalette, - params: { - ...activePalette.params, - stops: displayStops, - }, - }; - } - if (!checked) { - if (state.ticksPosition === 'bands') { - params.ticksPosition = 'auto'; - } - params.palette = undefined; - } + const params = checked + ? { + palette: { + ...activePalette, + params: { + ...activePalette.params, + stops: displayStops, + }, + }, + ticksPosition: 'bands', + colorMode: 'palette', + } + : { + ticksPosition: 'bands', + colorMode: 'none', + }; + setState({ ...state, ...params, @@ -114,76 +117,104 @@ export function GaugeDimensionEditor( /> {hasDynamicColoring && ( - - + - - color) - } - type={FIXED_PROGRESSION} - onClick={togglePalette} - /> - - - - {i18n.translate('xpack.lens.paletteTableGradient.customize', { - defaultMessage: 'Edit', - })} - - - { - // if the new palette is not custom, replace the rangeMin with the artificial one - if ( - newPalette.name !== CUSTOM_PALETTE && - newPalette.params && - newPalette.params.rangeMin !== currentMinMax.min - ) { - newPalette.params.rangeMin = currentMinMax.min; - } - setState({ - ...state, - palette: newPalette, - }); - }} + { + setState({ + ...state, + ticksPosition: + state.ticksPosition === GaugeTicksPositions.bands + ? GaugeTicksPositions.auto + : GaugeTicksPositions.bands, + }); + }} + /> + + + + + color) + } + type={FIXED_PROGRESSION} + onClick={togglePalette} /> - - - - + + + + {i18n.translate('xpack.lens.paletteTableGradient.customize', { + defaultMessage: 'Edit', + })} + + + { + // if the new palette is not custom, replace the rangeMin with the artificial one + if ( + newPalette.name !== CUSTOM_PALETTE && + newPalette.params && + newPalette.params.rangeMin !== currentMinMax.min + ) { + newPalette.params.rangeMin = currentMinMax.min; + } + setState({ + ...state, + palette: newPalette, + }); + }} + /> + + + + + )} ); diff --git a/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/gauge_config_panel.scss b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/gauge_config_panel.scss index f3d09715193ea..893ed71235881 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/gauge_config_panel.scss +++ b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/gauge_config_panel.scss @@ -1,3 +1,3 @@ .lnsGaugeToolbar__popover { - width: 400px; + width: 500px; } \ No newline at end of file diff --git a/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx index 81be5ddddde73..2d40c288a2510 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx @@ -6,21 +6,12 @@ */ import React, { memo, useState } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSwitch } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { VisualizationToolbarProps } from '../../../types'; -import { - ToolbarPopover, - TooltipWrapper, - useDebouncedValue, - VisLabel, -} from '../../../shared_components'; +import { ToolbarPopover, useDebouncedValue, VisLabel } from '../../../shared_components'; import './gauge_config_panel.scss'; -import { - GaugeTicksPositions, - GaugeTitleMode, - GaugeVisualizationState, -} from '../../../../common/expressions'; +import { GaugeTitleMode, GaugeVisualizationState } from '../../../../common/expressions'; export const GaugeToolbar = memo((props: VisualizationToolbarProps) => { const { state, setState, frame } = props; @@ -100,43 +91,6 @@ export const GaugeToolbar = memo((props: VisualizationToolbarProps - - - { - handleInputChange({ - ...inputValue, - ticksPosition: - state.ticksPosition === GaugeTicksPositions.bands - ? GaugeTicksPositions.auto - : GaugeTicksPositions.bands, - }); - }} - /> - - diff --git a/x-pack/plugins/lens/public/visualizations/gauge/utils.ts b/x-pack/plugins/lens/public/visualizations/gauge/utils.ts index d49b70769cc82..7cc28d0bcea3b 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/utils.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/utils.ts @@ -44,9 +44,10 @@ export const getMaxValue = (row?: DatatableRow, state?: GaugeAccessorsType): num const { metricAccessor, goalAccessor } = state; const metricValue = metricAccessor && row[metricAccessor]; const goalValue = goalAccessor && row[goalAccessor]; + const minValue = getMinValue(row, state); if (metricValue != null) { - const minValue = getMinValue(row, state); - const biggerValue = goalValue ? Math.max(goalValue, metricValue) : metricValue; + const numberValues = [minValue, goalValue, metricValue].filter((v) => typeof v === 'number'); + const biggerValue = Math.max(...numberValues); const nicelyRounded = scaleLinear().domain([minValue, biggerValue]).nice().ticks(4); if (nicelyRounded.length > 2) { const ticksDifference = Math.abs(nicelyRounded[0] - nicelyRounded[1]); @@ -68,8 +69,9 @@ export const getMinValue = (row?: DatatableRow, state?: GaugeAccessorsType) => { const { metricAccessor, maxAccessor } = state; const metricValue = metricAccessor && row[metricAccessor]; const maxValue = maxAccessor && row[maxAccessor]; - if (Math.min(metricValue, maxValue) < 0) { - return Math.min(metricValue, maxValue) - 10; // TODO: TO THINK THROUGH + const numberValues = [metricValue, maxValue].filter((v) => typeof v === 'number'); + if (Math.min(...numberValues) <= 0) { + return Math.min(...numberValues) - 10; // TODO: TO THINK THROUGH } } return FALLBACK_VALUE; diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx index fbc52c21067f1..89ee4789c0a5e 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx @@ -43,14 +43,14 @@ export const CHART_NAMES = { horizontalBullet: { icon: LensIconChartGaugeHorizontal, label: i18n.translate('xpack.lens.gaugeHorizontal.gaugeLabel', { - defaultMessage: 'Gauge Horizontal', + defaultMessage: 'Gauge horizontal', }), groupLabel: groupLabelForGauge, }, verticalBullet: { icon: LensIconChartGaugeVertical, label: i18n.translate('xpack.lens.gaugeVertical.gaugeLabel', { - defaultMessage: 'Gauge Vertical', + defaultMessage: 'Gauge vertical', }), groupLabel: groupLabelForGauge, }, @@ -227,14 +227,15 @@ export const getGaugeVisualization = ({ layerId: state.layerId, groupId: GROUP_ID.MIN, groupLabel: i18n.translate('xpack.lens.gauge.minValueLabel', { - defaultMessage: 'Minimum Value', + defaultMessage: 'Minimum value', }), accessors: state.minAccessor ? [{ columnId: state.minAccessor }] : [], filterOperations: isNumericMetric, supportsMoreColumns: !state.minAccessor, + required: true, dataTestSubj: 'lnsGauge_minDimensionPanel', prioritizedOperation: 'min', - suggestedValue: getMinValue(row, state), + suggestedValue: state.metricAccessor ? getMinValue(row, state) : undefined, }, { supportStaticValue: true, @@ -242,14 +243,15 @@ export const getGaugeVisualization = ({ layerId: state.layerId, groupId: GROUP_ID.MAX, groupLabel: i18n.translate('xpack.lens.gauge.maxValueLabel', { - defaultMessage: 'Maximum Value', + defaultMessage: 'Maximum value', }), accessors: state.maxAccessor ? [{ columnId: state.maxAccessor }] : [], filterOperations: isNumericMetric, supportsMoreColumns: !state.maxAccessor, dataTestSubj: 'lnsGauge_maxDimensionPanel', + required: true, prioritizedOperation: 'max', - suggestedValue: getMaxValue(row, state), + suggestedValue: state.metricAccessor ? getMaxValue(row, state) : undefined, }, { supportStaticValue: true, @@ -257,7 +259,7 @@ export const getGaugeVisualization = ({ layerId: state.layerId, groupId: GROUP_ID.GOAL, groupLabel: i18n.translate('xpack.lens.gauge.goalValueLabel', { - defaultMessage: 'Goal Value', + defaultMessage: 'Goal value', }), accessors: state.goalAccessor ? [{ columnId: state.goalAccessor }] : [], filterOperations: isNumericMetric, From 41a7133cffc190391bbaef40365d573b9459c08d Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Wed, 1 Dec 2021 13:41:26 +0100 Subject: [PATCH 28/43] don't show required nor optional for max/min dimensions --- .../plugins/lens/common/expressions/gauge_chart/types.ts | 7 ++++++- .../config_panel/buttons/empty_dimension_button.tsx | 4 ++-- .../editor_frame/config_panel/layer_panel.tsx | 3 +-- x-pack/plugins/lens/public/types.ts | 4 ++-- .../public/visualizations/gauge/dimension_editor.tsx | 9 +++++---- .../lens/public/visualizations/gauge/visualization.tsx | 6 ++---- 6 files changed, 18 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/lens/common/expressions/gauge_chart/types.ts b/x-pack/plugins/lens/common/expressions/gauge_chart/types.ts index dd27e7ac3593f..62c56b90c93ba 100644 --- a/x-pack/plugins/lens/common/expressions/gauge_chart/types.ts +++ b/x-pack/plugins/lens/common/expressions/gauge_chart/types.ts @@ -30,8 +30,13 @@ export const GaugeTitleModes = { none: 'none', } as const; +export const GaugeColorModes = { + palette: 'palette', + none: 'none', +} as const; + export type GaugeType = 'gauge'; -export type GaugeColorMode = 'none' | 'palette'; +export type GaugeColorMode = keyof typeof GaugeColorModes; export type GaugeShape = keyof typeof GaugeShapes; export type GaugeTitleMode = keyof typeof GaugeTitleModes; export type GaugeTicksPosition = keyof typeof GaugeTicksPositions; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx index 39eb72ad8a0a5..1ba3ff8f6ac34 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx @@ -72,7 +72,7 @@ const SuggestedValueButton = ({ columnId, group, onClick }: EmptyButtonProps) => ); @@ -162,7 +162,7 @@ export function EmptyDimensionButton({ getCustomDropTarget={getCustomDropTarget} >
- {typeof group?.suggestedValue === 'number' ? ( + {typeof group.suggestedValue?.() === 'number' ? ( ) : ( 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 84c7722ca1b88..13f9df1739005 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 @@ -406,7 +406,7 @@ export function LayerPanel( defaultMessage: 'Requires field', }); - const isOptional = !group.required; + const isOptional = !group.required && !group.suggestedValue; return ( - {' '} number; /** * When the dimension editor is enabled for this group, all dimensions in the group @@ -756,7 +756,7 @@ export interface Visualization { */ getErrorMessages: ( state: T, - datasourceLayers?: Record + frame?: FramePublicAPI ) => | Array<{ shortMessage: string; diff --git a/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx index f57a789b339ca..5e97d71c96442 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx @@ -21,6 +21,7 @@ import { isNumericFieldForDatatable, GaugeVisualizationState, GaugeTicksPositions, + GaugeColorModes, } from '../../../common/expressions'; import { applyPaletteParams, @@ -101,12 +102,12 @@ export function GaugeDimensionEditor( stops: displayStops, }, }, - ticksPosition: 'bands', - colorMode: 'palette', + ticksPosition: GaugeTicksPositions.bands, + colorMode: GaugeColorModes.palette, } : { - ticksPosition: 'bands', - colorMode: 'none', + ticksPosition: GaugeTicksPositions.auto, + colorMode: GaugeColorModes.none, }; setState({ diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx index 89ee4789c0a5e..4f78d1c50f3db 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx @@ -232,10 +232,9 @@ export const getGaugeVisualization = ({ accessors: state.minAccessor ? [{ columnId: state.minAccessor }] : [], filterOperations: isNumericMetric, supportsMoreColumns: !state.minAccessor, - required: true, dataTestSubj: 'lnsGauge_minDimensionPanel', prioritizedOperation: 'min', - suggestedValue: state.metricAccessor ? getMinValue(row, state) : undefined, + suggestedValue: () => (state.metricAccessor ? getMinValue(row, state) : undefined), }, { supportStaticValue: true, @@ -249,9 +248,8 @@ export const getGaugeVisualization = ({ filterOperations: isNumericMetric, supportsMoreColumns: !state.maxAccessor, dataTestSubj: 'lnsGauge_maxDimensionPanel', - required: true, prioritizedOperation: 'max', - suggestedValue: state.metricAccessor ? getMaxValue(row, state) : undefined, + suggestedValue: () => (state.metricAccessor ? getMaxValue(row, state) : undefined), }, { supportStaticValue: true, From d0d3b44ebf014988869c9f28a9f46069b1101917 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Wed, 1 Dec 2021 14:37:58 +0100 Subject: [PATCH 29/43] fix types --- x-pack/plugins/lens/public/types.ts | 2 +- .../lens/public/visualizations/gauge/chart_component.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 70837ffeefad5..ef94a395f552d 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -478,7 +478,7 @@ export type VisualizationDimensionGroupConfig = SharedDimensionProps & { requiredMinDimensionCount?: number; dataTestSubj?: string; prioritizedOperation?: string; - suggestedValue?: () => number; + suggestedValue?: () => number | undefined; /** * When the dimension editor is enabled for this group, all dimensions in the group diff --git a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx index 2e5bcadfc3df3..50c40f14cca8b 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx @@ -208,7 +208,7 @@ export const GaugeComponent: FC = ({ bands={bands} ticks={getTicks(ticksPosition, [min, max], bands)} bandFillColor={ - colorMode === 'palette' + colorMode === 'palette' && colors ? (val) => { const index = bands && bands.indexOf(val.value) - 1; return colors && index >= 0 && colors[index] From 6c6cb04be1d31e9b903dfb73d8ae1daff9088166 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Wed, 1 Dec 2021 16:09:42 +0100 Subject: [PATCH 30/43] tests fixed --- .../toolbar_component/gauge_toolbar.test.tsx | 48 ------------------- .../gauge/visualization.test.ts | 30 ++++++------ 2 files changed, 15 insertions(+), 63 deletions(-) diff --git a/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/gauge_toolbar.test.tsx b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/gauge_toolbar.test.tsx index 873aed966c740..5973611031b9f 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/gauge_toolbar.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/gauge_toolbar.test.tsx @@ -60,17 +60,6 @@ class Harness { this.subtitleLabel.prop('onChange')!(e); }); } - public get ticksOnColorBandsSwitch() { - return this.wrapper.find( - 'EuiSwitch[data-test-subj="lens-toolbar-gauge-ticks-position-switch"]' - ); - } - - toggleTicksPositionSwitch() { - act(() => { - this.ticksOnColorBandsSwitch.prop('onChange')!({} as FormEvent); - }); - } } describe('gauge toolbar', () => { @@ -100,7 +89,6 @@ describe('gauge toolbar', () => { harness = new Harness(mountWithIntl()); harness.togglePopover(); - expect(harness.ticksOnColorBandsSwitch.prop('checked')).toBe(false); expect(harness.titleLabel.prop('value')).toBe(''); expect(harness.titleSelect.prop('value')).toBe('auto'); expect(harness.subtitleLabel.prop('value')).toBe(''); @@ -111,7 +99,6 @@ describe('gauge toolbar', () => { ...defaultProps, state: { ...defaultProps.state, - colorMode: 'palette' as const, ticksPosition: 'bands' as const, visTitleMode: 'custom' as const, visTitle: 'new title', @@ -122,46 +109,11 @@ describe('gauge toolbar', () => { harness = new Harness(mountWithIntl()); harness.togglePopover(); - expect(harness.ticksOnColorBandsSwitch.prop('checked')).toBe(true); expect(harness.titleLabel.prop('value')).toBe('new title'); expect(harness.titleSelect.prop('value')).toBe('custom'); expect(harness.subtitleLabel.prop('value')).toBe('new subtitle'); expect(harness.subtitleSelect.prop('value')).toBe('custom'); }); - describe('Ticks position switch', () => { - it('switch is disabled if colorMode is none', () => { - defaultProps.state.colorMode = 'none' as const; - - harness = new Harness(mountWithIntl()); - harness.togglePopover(); - - expect(harness.ticksOnColorBandsSwitch.prop('disabled')).toBe(true); - expect(harness.ticksOnColorBandsSwitch.prop('checked')).toBe(false); - }); - it('switch is enabled if colorMode is not none', () => { - defaultProps.state.colorMode = 'palette' as const; - - harness = new Harness(mountWithIntl()); - harness.togglePopover(); - - expect(harness.ticksOnColorBandsSwitch.prop('disabled')).toBe(false); - }); - it('Ticks position switch updates the state when clicked', () => { - defaultProps.state.colorMode = 'palette' as const; - harness = new Harness(mountWithIntl()); - harness.togglePopover(); - - harness.toggleTicksPositionSwitch(); - - expect(defaultProps.setState).toHaveBeenCalledTimes(1); - expect(defaultProps.setState).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ - ticksPosition: 'bands', - }) - ); - }); - }); describe('title', () => { it('title label is disabled if title is selected to be none', () => { diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts index b2746018a07e3..85d6a5d3ddec9 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts @@ -106,33 +106,33 @@ describe('gauge', () => { { layerId: 'first', groupId: GROUP_ID.MIN, - groupLabel: 'Minimum Value', + groupLabel: 'Minimum value', accessors: [{ columnId: 'min-accessor' }], filterOperations: isNumericMetric, supportsMoreColumns: false, dataTestSubj: 'lnsGauge_minDimensionPanel', prioritizedOperation: 'min', - suggestedValue: 0, + suggestedValue: expect.any(Function), supportFieldFormat: false, supportStaticValue: true, }, { layerId: 'first', groupId: GROUP_ID.MAX, - groupLabel: 'Maximum Value', + groupLabel: 'Maximum value', accessors: [{ columnId: 'max-accessor' }], filterOperations: isNumericMetric, supportsMoreColumns: false, dataTestSubj: 'lnsGauge_maxDimensionPanel', prioritizedOperation: 'max', - suggestedValue: 250, + suggestedValue: expect.any(Function), supportFieldFormat: false, supportStaticValue: true, }, { layerId: 'first', groupId: GROUP_ID.GOAL, - groupLabel: 'Goal Value', + groupLabel: 'Goal value', accessors: [{ columnId: 'goal-accessor' }], filterOperations: isNumericMetric, supportsMoreColumns: false, @@ -173,33 +173,33 @@ describe('gauge', () => { { layerId: 'first', groupId: GROUP_ID.MIN, - groupLabel: 'Minimum Value', + groupLabel: 'Minimum value', accessors: [{ columnId: 'min-accessor' }], filterOperations: isNumericMetric, supportsMoreColumns: false, dataTestSubj: 'lnsGauge_minDimensionPanel', prioritizedOperation: 'min', - suggestedValue: 0, + suggestedValue: expect.any(Function), supportFieldFormat: false, supportStaticValue: true, }, { layerId: 'first', groupId: GROUP_ID.MAX, - groupLabel: 'Maximum Value', + groupLabel: 'Maximum value', accessors: [], filterOperations: isNumericMetric, supportsMoreColumns: true, dataTestSubj: 'lnsGauge_maxDimensionPanel', prioritizedOperation: 'max', - suggestedValue: 100, + suggestedValue: expect.any(Function), supportFieldFormat: false, supportStaticValue: true, }, { layerId: 'first', groupId: GROUP_ID.GOAL, - groupLabel: 'Goal Value', + groupLabel: 'Goal value', accessors: [], filterOperations: isNumericMetric, supportsMoreColumns: true, @@ -246,33 +246,33 @@ describe('gauge', () => { { layerId: 'first', groupId: GROUP_ID.MIN, - groupLabel: 'Minimum Value', + groupLabel: 'Minimum value', accessors: [{ columnId: 'min-accessor' }], filterOperations: isNumericMetric, supportsMoreColumns: false, dataTestSubj: 'lnsGauge_minDimensionPanel', prioritizedOperation: 'min', - suggestedValue: 0, + suggestedValue: expect.any(Function), supportFieldFormat: false, supportStaticValue: true, }, { layerId: 'first', groupId: GROUP_ID.MAX, - groupLabel: 'Maximum Value', + groupLabel: 'Maximum value', accessors: [{ columnId: 'max-accessor' }], filterOperations: isNumericMetric, supportsMoreColumns: false, dataTestSubj: 'lnsGauge_maxDimensionPanel', prioritizedOperation: 'max', - suggestedValue: 100, + suggestedValue: expect.any(Function), supportFieldFormat: false, supportStaticValue: true, }, { layerId: 'first', groupId: GROUP_ID.GOAL, - groupLabel: 'Goal Value', + groupLabel: 'Goal value', accessors: [{ columnId: 'goal-accessor' }], filterOperations: isNumericMetric, supportsMoreColumns: false, From 6e94edef57a3b12758f60c6168309d54783e2dd4 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Wed, 1 Dec 2021 16:23:50 +0100 Subject: [PATCH 31/43] fix types --- x-pack/plugins/lens/public/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 668d2e442aa4d..8c5331100e903 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -757,7 +757,7 @@ export interface Visualization { */ getErrorMessages: ( state: T, - frame?: FramePublicAPI + datasourceLayers?: Record ) => | Array<{ shortMessage: string; From 49baac943385bc673d10a1fed08e434f0340f29d Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Wed, 1 Dec 2021 21:42:45 +0100 Subject: [PATCH 32/43] last piece of gauge feedback --- .../workspace_panel/chart_switch.tsx | 20 +++---- .../visualizations/gauge/dimension_editor.tsx | 52 +++++++++---------- .../lens/public/visualizations/gauge/utils.ts | 4 +- 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx index df6f8e9112a49..fe054edfb2917 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx @@ -339,16 +339,6 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) { alignItems="center" className="lnsChartSwitch__append" > - {v.showExperimentalBadge ? ( - - - - - - ) : null} {v.selection.dataLoss !== 'nothing' ? ( ) : null} + {v.showExperimentalBadge ? ( + + + + + + ) : null} ) : null, // Apparently checked: null is not valid for TS diff --git a/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx index 5e97d71c96442..e12329e6d39e8 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx @@ -119,32 +119,6 @@ export function GaugeDimensionEditor( {hasDynamicColoring && ( <> - - { - setState({ - ...state, - ticksPosition: - state.ticksPosition === GaugeTicksPositions.bands - ? GaugeTicksPositions.auto - : GaugeTicksPositions.bands, - }); - }} - /> - + + { + setState({ + ...state, + ticksPosition: + state.ticksPosition === GaugeTicksPositions.bands + ? GaugeTicksPositions.auto + : GaugeTicksPositions.bands, + }); + }} + /> + )} diff --git a/x-pack/plugins/lens/public/visualizations/gauge/utils.ts b/x-pack/plugins/lens/public/visualizations/gauge/utils.ts index 7cc28d0bcea3b..fb11ecb9cb51f 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/utils.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/utils.ts @@ -84,7 +84,7 @@ export const getMetricValue = (row?: DatatableRow, state?: GaugeAccessorsType) = } const minValue = getMinValue(row, state); const maxValue = getMaxValue(row, state); - return Math.round((maxValue - minValue) * 0.6 + minValue); + return Math.round((maxValue - minValue) * 0.5 + minValue); }; export const getGoalValue = (row?: DatatableRow, state?: GaugeVisualizationState) => { @@ -94,7 +94,7 @@ export const getGoalValue = (row?: DatatableRow, state?: GaugeVisualizationState } const minValue = getMinValue(row, state); const maxValue = getMaxValue(row, state); - return Math.round((maxValue - minValue) * 0.8 + minValue); + return Math.round((maxValue - minValue) * 0.75 + minValue); }; export const transparentizePalettes = (palettes: PaletteRegistry) => { From 85b7ecbf9f533274bbc31aab88056947aa5cfe6d Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Thu, 2 Dec 2021 13:50:16 +0100 Subject: [PATCH 33/43] change naming from title and subtitle to labelMajor and labelMinor --- .../expressions/gauge_chart/gauge_chart.ts | 18 +++---- .../common/expressions/gauge_chart/types.ts | 10 ++-- .../chart_component.test.tsx.snap | 2 +- .../gauge/chart_component.test.tsx | 32 ++++++------ .../visualizations/gauge/chart_component.tsx | 26 ++++++---- .../visualizations/gauge/suggestions.test.ts | 8 +-- .../visualizations/gauge/suggestions.ts | 4 +- .../toolbar_component/gauge_toolbar.test.tsx | 52 +++++++++---------- .../gauge/toolbar_component/index.tsx | 32 ++++++------ .../gauge/visualization.test.ts | 12 ++--- .../visualizations/gauge/visualization.tsx | 8 +-- 11 files changed, 104 insertions(+), 100 deletions(-) diff --git a/x-pack/plugins/lens/common/expressions/gauge_chart/gauge_chart.ts b/x-pack/plugins/lens/common/expressions/gauge_chart/gauge_chart.ts index dd419bb53ca9a..5fa6804f974a6 100644 --- a/x-pack/plugins/lens/common/expressions/gauge_chart/gauge_chart.ts +++ b/x-pack/plugins/lens/common/expressions/gauge_chart/gauge_chart.ts @@ -97,25 +97,25 @@ export const gauge: ExpressionFunctionDefinition< }), required: true, }, - visTitle: { + labelMajor: { types: ['string'], - help: i18n.translate('xpack.lens.gaugeChart.config.title.help', { - defaultMessage: 'Specifies the title of the gauge chart displayed inside the chart.', + help: i18n.translate('xpack.lens.gaugeChart.config.labelMajor.help', { + defaultMessage: 'Specifies the labelMajor of the gauge chart displayed inside the chart.', }), required: false, }, - visTitleMode: { + labelMajorMode: { types: ['string'], options: ['none', 'auto', 'custom'], - help: i18n.translate('xpack.lens.gaugeChart.config.visTitleMode.help', { - defaultMessage: 'Specifies the mode of title', + help: i18n.translate('xpack.lens.gaugeChart.config.labelMajorMode.help', { + defaultMessage: 'Specifies the mode of labelMajor', }), required: true, }, - subtitle: { + labelMinor: { types: ['string'], - help: i18n.translate('xpack.lens.gaugeChart.config.subtitle.help', { - defaultMessage: 'Specifies the Subtitle of the gauge chart', + help: i18n.translate('xpack.lens.gaugeChart.config.labelMinor.help', { + defaultMessage: 'Specifies the labelMinor of the gauge chart', }), required: false, }, diff --git a/x-pack/plugins/lens/common/expressions/gauge_chart/types.ts b/x-pack/plugins/lens/common/expressions/gauge_chart/types.ts index 62c56b90c93ba..52e6f335bbc07 100644 --- a/x-pack/plugins/lens/common/expressions/gauge_chart/types.ts +++ b/x-pack/plugins/lens/common/expressions/gauge_chart/types.ts @@ -24,7 +24,7 @@ export const GaugeTicksPositions = { bands: 'bands', } as const; -export const GaugeTitleModes = { +export const GaugeLabelMajorModes = { auto: 'auto', custom: 'custom', none: 'none', @@ -38,7 +38,7 @@ export const GaugeColorModes = { export type GaugeType = 'gauge'; export type GaugeColorMode = keyof typeof GaugeColorModes; export type GaugeShape = keyof typeof GaugeShapes; -export type GaugeTitleMode = keyof typeof GaugeTitleModes; +export type GaugeLabelMajorMode = keyof typeof GaugeLabelMajorModes; export type GaugeTicksPosition = keyof typeof GaugeTicksPositions; export interface SharedGaugeLayerState { @@ -47,9 +47,9 @@ export interface SharedGaugeLayerState { maxAccessor?: string; goalAccessor?: string; ticksPosition: GaugeTicksPosition; - visTitleMode: GaugeTitleMode; - visTitle?: string; - subtitle?: string; + labelMajorMode: GaugeLabelMajorMode; + labelMajor?: string; + labelMinor?: string; colorMode?: GaugeColorMode; palette?: PaletteOutput; shape: GaugeShape; diff --git a/x-pack/plugins/lens/public/visualizations/gauge/__snapshots__/chart_component.test.tsx.snap b/x-pack/plugins/lens/public/visualizations/gauge/__snapshots__/chart_component.test.tsx.snap index 61028c7108b0a..b588c1d341a75 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/__snapshots__/chart_component.test.tsx.snap +++ b/x-pack/plugins/lens/public/visualizations/gauge/__snapshots__/chart_component.test.tsx.snap @@ -18,7 +18,7 @@ exports[`GaugeComponent renders the chart 1`] = ` ] } base={0} - id="spec_1" + id="goal" labelMajor="Count of records " labelMinor="" subtype="verticalBullet" diff --git a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx index c98d7bb5c1ca7..5b038191fba79 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx @@ -11,7 +11,7 @@ import { shallowWithIntl } from '@kbn/test/jest'; import { chartPluginMock } from 'src/plugins/charts/public/mocks'; import type { ColorStop, LensMultiTable } from '../../../common'; import { fieldFormatsServiceMock } from '../../../../../../src/plugins/field_formats/public/mocks'; -import { GaugeExpressionArgs, GaugeTitleMode } from '../../../common/expressions/gauge_chart'; +import { GaugeExpressionArgs, GaugeLabelMajorMode } from '../../../common/expressions/gauge_chart'; import { GaugeComponent, GaugeRenderProps } from './chart_component'; import { DatatableColumn, DatatableRow } from 'src/plugins/expressions/common'; import { VisualizationContainer } from '../../visualization_container'; @@ -56,7 +56,7 @@ const chartsThemeService = chartPluginMock.createSetupContract().theme; const palettesRegistry = chartPluginMock.createPaletteRegistry(); const formatService = fieldFormatsServiceMock.createStartContract(); const args: GaugeExpressionArgs = { - title: 'Gauge', + labelMajor: 'Gauge', description: 'vis description', metricAccessor: 'metric-accessor', minAccessor: '', @@ -65,7 +65,7 @@ const args: GaugeExpressionArgs = { shape: 'verticalBullet', colorMode: 'none', ticksPosition: 'auto', - visTitleMode: 'auto', + labelMajorMode: 'auto', }; describe('GaugeComponent', function () { @@ -145,41 +145,41 @@ describe('GaugeComponent', function () { expect(goal.prop('actual')).toEqual(10); }); - describe('title and subtitle settings', () => { - it('displays no title and no subtitle when no passed', () => { + describe('labelMajor and labelMinor settings', () => { + it('displays no labelMajor and no labelMinor when no passed', () => { const customProps = { ...wrapperProps, args: { ...wrapperProps.args, - visTitleMode: 'none' as GaugeTitleMode, - subtitle: '', + labelMajorMode: 'none' as GaugeLabelMajorMode, + labelMinor: '', }, }; const goal = shallowWithIntl().find(Goal); expect(goal.prop('labelMajor')).toEqual(''); expect(goal.prop('labelMinor')).toEqual(''); }); - it('displays custom title and subtitle when passed', () => { + it('displays custom labelMajor and labelMinor when passed', () => { const customProps = { ...wrapperProps, args: { ...wrapperProps.args, - visTitleMode: 'custom' as GaugeTitleMode, - visTitle: 'custom title', - subtitle: 'custom subtitle', + labelMajorMode: 'custom' as GaugeLabelMajorMode, + labelMajor: 'custom labelMajor', + labelMinor: 'custom labelMinor', }, }; const goal = shallowWithIntl().find(Goal); - expect(goal.prop('labelMajor')).toEqual('custom title '); - expect(goal.prop('labelMinor')).toEqual('custom subtitle '); + expect(goal.prop('labelMajor')).toEqual('custom labelMajor '); + expect(goal.prop('labelMinor')).toEqual('custom labelMinor '); }); - it('displays auto title', () => { + it('displays auto labelMajor', () => { const customProps = { ...wrapperProps, args: { ...wrapperProps.args, - visTitleMode: 'auto' as GaugeTitleMode, - visTitle: '', + labelMajorMode: 'auto' as GaugeLabelMajorMode, + labelMajor: '', }, }; const goal = shallowWithIntl().find(Goal); diff --git a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx index 50c40f14cca8b..a8999cb887d9e 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx @@ -23,7 +23,7 @@ import { GaugeShapes, GaugeTicksPosition, GaugeTicksPositions, - GaugeTitleMode, + GaugeLabelMajorMode, } from '../../../common/expressions/gauge_chart'; import type { FormatFactory } from '../../../common'; @@ -66,13 +66,17 @@ function normalizeBands( return [min, ...orderedStops, max]; } -function getTitle(visTitleMode: GaugeTitleMode, visTitle?: string, fallbackTitle?: string) { - if (visTitleMode === 'none') { +function getTitle( + labelMajorMode: GaugeLabelMajorMode, + labelMajor?: string, + fallbackTitle?: string +) { + if (labelMajorMode === 'none') { return ''; - } else if (visTitleMode === 'auto') { + } else if (labelMajorMode === 'auto') { return `${fallbackTitle || ''} `; } - return `${visTitle || fallbackTitle || ''} `; + return `${labelMajor || fallbackTitle || ''} `; } // TODO: once charts handle not displaying labels when there's no space for them, it's safe to remove this @@ -122,9 +126,9 @@ export const GaugeComponent: FC = ({ metricAccessor, palette, colorMode, - subtitle, - visTitle, - visTitleMode, + labelMinor, + labelMajor, + labelMajorMode, ticksPosition, } = args; if (!metricAccessor) { @@ -199,7 +203,7 @@ export const GaugeComponent: FC = ({ = min && goal <= max ? goal : undefined} @@ -217,8 +221,8 @@ export const GaugeComponent: FC = ({ } : () => `rgba(255,255,255,0)` } - labelMajor={getTitle(visTitleMode, visTitle, metricColumn?.name)} - labelMinor={subtitle ? subtitle + ' ' : ''} + labelMajor={getTitle(labelMajorMode, labelMajor, metricColumn?.name)} + labelMinor={labelMinor ? labelMinor + ' ' : ''} /> ); diff --git a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.test.ts b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.test.ts index 7c6f28a44e1a3..cced4bb2c309b 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.test.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.test.ts @@ -78,7 +78,7 @@ describe('gauge suggestions', () => { layerId: 'first', layerType: layerTypes.DATA, minAccessor: 'some-field', - visTitleMode: 'auto', + labelMajorMode: 'auto', ticksPosition: 'auto', } as GaugeVisualizationState, keptLayerIds: ['first'], @@ -151,7 +151,7 @@ describe('shows suggestions', () => { layerType: layerTypes.DATA, shape: GaugeShapes.horizontalBullet, metricAccessor: 'metric-column', - visTitleMode: 'auto', + labelMajorMode: 'auto', ticksPosition: 'auto', }, title: 'Gauge', @@ -170,7 +170,7 @@ describe('shows suggestions', () => { metricAccessor: 'metric-column', shape: GaugeShapes.verticalBullet, ticksPosition: 'auto', - visTitleMode: 'auto', + labelMajorMode: 'auto', }, }, ]); @@ -199,7 +199,7 @@ describe('shows suggestions', () => { layerType: layerTypes.DATA, shape: GaugeShapes.verticalBullet, metricAccessor: 'metric-column', - visTitleMode: 'auto', + labelMajorMode: 'auto', ticksPosition: 'auto', layerId: 'first', }, diff --git a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts index bb8951d60312e..8d9e9c7138e47 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts @@ -12,7 +12,7 @@ import { GaugeShape, GaugeShapes, GaugeTicksPositions, - GaugeTitleModes, + GaugeLabelMajorModes, GaugeVisualizationState, } from '../../../common/expressions/gauge_chart'; @@ -66,7 +66,7 @@ export const getSuggestions: Visualization['getSuggesti layerId: table.layerId, layerType: layerTypes.DATA, ticksPosition: GaugeTicksPositions.auto, - visTitleMode: GaugeTitleModes.auto, + labelMajorMode: GaugeLabelMajorModes.auto, }, title: i18n.translate('xpack.lens.gauge.gaugeLabel', { defaultMessage: 'Gauge', diff --git a/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/gauge_toolbar.test.tsx b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/gauge_toolbar.test.tsx index 5973611031b9f..9e3820c191345 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/gauge_toolbar.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/gauge_toolbar.test.tsx @@ -35,10 +35,10 @@ class Harness { } public get titleLabel() { - return this.wrapper.find('EuiFieldText[data-test-subj="lens-toolbar-gauge-title"]'); + return this.wrapper.find('EuiFieldText[data-test-subj="lens-toolbar-gauge-labelMajor"]'); } public get titleSelect() { - return this.wrapper.find('EuiSelect[data-test-subj="lens-toolbar-gauge-title-select"]'); + return this.wrapper.find('EuiSelect[data-test-subj="lens-toolbar-gauge-labelMajor-select"]'); } modifyTitle(e: FormEvent) { @@ -48,11 +48,11 @@ class Harness { } public get subtitleSelect() { - return this.wrapper.find('EuiSelect[data-test-subj="lens-toolbar-gauge-subtitle-select"]'); + return this.wrapper.find('EuiSelect[data-test-subj="lens-toolbar-gauge-labelMinor-select"]'); } public get subtitleLabel() { - return this.wrapper.find('EuiFieldText[data-test-subj="lens-toolbar-gauge-subtitle"]'); + return this.wrapper.find('EuiFieldText[data-test-subj="lens-toolbar-gauge-labelMinor"]'); } modifySubtitle(e: FormEvent) { @@ -80,7 +80,7 @@ describe('gauge toolbar', () => { shape: 'verticalBullet', colorMode: 'none', ticksPosition: 'auto', - visTitleMode: 'auto', + labelMajorMode: 'auto', }, }; }); @@ -100,24 +100,24 @@ describe('gauge toolbar', () => { state: { ...defaultProps.state, ticksPosition: 'bands' as const, - visTitleMode: 'custom' as const, - visTitle: 'new title', - subtitle: 'new subtitle', + labelMajorMode: 'custom' as const, + labelMajor: 'new labelMajor', + labelMinor: 'new labelMinor', }, }; harness = new Harness(mountWithIntl()); harness.togglePopover(); - expect(harness.titleLabel.prop('value')).toBe('new title'); + expect(harness.titleLabel.prop('value')).toBe('new labelMajor'); expect(harness.titleSelect.prop('value')).toBe('custom'); - expect(harness.subtitleLabel.prop('value')).toBe('new subtitle'); + expect(harness.subtitleLabel.prop('value')).toBe('new labelMinor'); expect(harness.subtitleSelect.prop('value')).toBe('custom'); }); - describe('title', () => { - it('title label is disabled if title is selected to be none', () => { - defaultProps.state.visTitleMode = 'none' as const; + describe('labelMajor', () => { + it('labelMajor label is disabled if labelMajor is selected to be none', () => { + defaultProps.state.labelMajorMode = 'none' as const; harness = new Harness(mountWithIntl()); harness.togglePopover(); @@ -126,8 +126,8 @@ describe('gauge toolbar', () => { expect(harness.titleLabel.prop('disabled')).toBe(true); expect(harness.titleLabel.prop('value')).toBe(''); }); - it('title mode switches to custom when user starts typing', () => { - defaultProps.state.visTitleMode = 'auto' as const; + it('labelMajor mode switches to custom when user starts typing', () => { + defaultProps.state.labelMajorMode = 'auto' as const; harness = new Harness(mountWithIntl()); harness.togglePopover(); @@ -135,30 +135,30 @@ describe('gauge toolbar', () => { expect(harness.titleSelect.prop('value')).toBe('auto'); expect(harness.titleLabel.prop('disabled')).toBe(false); expect(harness.titleLabel.prop('value')).toBe(''); - harness.modifyTitle({ target: { value: 'title' } } as unknown as FormEvent); + harness.modifyTitle({ target: { value: 'labelMajor' } } as unknown as FormEvent); expect(defaultProps.setState).toHaveBeenCalledTimes(1); expect(defaultProps.setState).toHaveBeenNthCalledWith( 1, expect.objectContaining({ - visTitleMode: 'custom', - visTitle: 'title', + labelMajorMode: 'custom', + labelMajor: 'labelMajor', }) ); }); }); - describe('subtitle', () => { - it('subtitle label is enabled if subtitle is string', () => { - defaultProps.state.subtitle = 'subtitle label'; + describe('labelMinor', () => { + it('labelMinor label is enabled if labelMinor is string', () => { + defaultProps.state.labelMinor = 'labelMinor label'; harness = new Harness(mountWithIntl()); harness.togglePopover(); expect(harness.subtitleSelect.prop('value')).toBe('custom'); expect(harness.subtitleLabel.prop('disabled')).toBe(false); - expect(harness.subtitleLabel.prop('value')).toBe('subtitle label'); + expect(harness.subtitleLabel.prop('value')).toBe('labelMinor label'); }); - it('title mode can switch to custom', () => { - defaultProps.state.subtitle = ''; + it('labelMajor mode can switch to custom', () => { + defaultProps.state.labelMinor = ''; harness = new Harness(mountWithIntl()); harness.togglePopover(); @@ -166,12 +166,12 @@ describe('gauge toolbar', () => { expect(harness.subtitleSelect.prop('value')).toBe('none'); expect(harness.subtitleLabel.prop('disabled')).toBe(true); expect(harness.subtitleLabel.prop('value')).toBe(''); - harness.modifySubtitle({ target: { value: 'subtitle label' } } as unknown as FormEvent); + harness.modifySubtitle({ target: { value: 'labelMinor label' } } as unknown as FormEvent); expect(defaultProps.setState).toHaveBeenCalledTimes(1); expect(defaultProps.setState).toHaveBeenNthCalledWith( 1, expect.objectContaining({ - subtitle: 'subtitle label', + labelMinor: 'labelMinor label', }) ); }); diff --git a/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx index 2d40c288a2510..4d83da5644fcd 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import type { VisualizationToolbarProps } from '../../../types'; import { ToolbarPopover, useDebouncedValue, VisLabel } from '../../../shared_components'; import './gauge_config_panel.scss'; -import { GaugeTitleMode, GaugeVisualizationState } from '../../../../common/expressions'; +import { GaugeLabelMajorMode, GaugeVisualizationState } from '../../../../common/expressions'; export const GaugeToolbar = memo((props: VisualizationToolbarProps) => { const { state, setState, frame } = props; @@ -19,8 +19,8 @@ export const GaugeToolbar = memo((props: VisualizationToolbarProps col.id === state.metricAccessor)?.name; - const [subtitleMode, setSubtitleMode] = useState(() => - state.subtitle ? 'custom' : 'none' + const [subtitleMode, setSubtitleMode] = useState(() => + state.labelMinor ? 'custom' : 'none' ); const { inputValue, handleInputChange } = useDebouncedValue({ @@ -34,7 +34,7 @@ export const GaugeToolbar = memo((props: VisualizationToolbarProps { - setSubtitleMode(inputValue.subtitle ? 'custom' : 'none'); + setSubtitleMode(inputValue.labelMinor ? 'custom' : 'none'); }} title={i18n.translate('xpack.lens.gauge.appearanceLabel', { defaultMessage: 'Appearance', @@ -45,25 +45,25 @@ export const GaugeToolbar = memo((props: VisualizationToolbarProps { handleInputChange({ ...inputValue, - visTitle: value.label, - visTitleMode: value.mode, + labelMajor: value.label, + labelMajorMode: value.mode, }); }} /> @@ -71,21 +71,21 @@ export const GaugeToolbar = memo((props: VisualizationToolbarProps { handleInputChange({ ...inputValue, - subtitle: value.label, + labelMinor: value.label, }); setSubtitleMode(value.mode); }} diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts index 85d6a5d3ddec9..506c03107033b 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts @@ -18,7 +18,7 @@ function exampleState(): GaugeVisualizationState { return { layerId: 'test-layer', layerType: layerTypes.DATA, - visTitleMode: 'auto', + labelMajorMode: 'auto', ticksPosition: 'auto', shape: 'horizontalBullet', }; @@ -40,7 +40,7 @@ describe('gauge', () => { layerType: layerTypes.DATA, title: 'Empty Gauge chart', shape: 'horizontalBullet', - visTitleMode: 'auto', + labelMajorMode: 'auto', ticksPosition: 'auto', }); }); @@ -395,7 +395,7 @@ describe('gauge', () => { goalAccessor: 'goal-accessor', metricAccessor: 'metric-accessor', maxAccessor: 'max-accessor', - subtitle: 'Subtitle', + labelMinor: 'Subtitle', }; const attributes = { title: 'Test', @@ -419,9 +419,9 @@ describe('gauge', () => { goalAccessor: ['goal-accessor'], colorMode: ['none'], ticksPosition: ['auto'], - visTitleMode: ['auto'], - subtitle: ['Subtitle'], - visTitle: [], + labelMajorMode: ['auto'], + labelMinor: ['Subtitle'], + labelMajor: [], palette: [], shape: ['horizontalBullet'], }, diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx index 4f78d1c50f3db..30fb9daca16c5 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx @@ -104,9 +104,9 @@ const toExpression = ( ] : [], ticksPosition: state.ticksPosition ? [state.ticksPosition] : ['auto'], - subtitle: state.subtitle ? [state.subtitle] : [], - visTitle: state.visTitle ? [state.visTitle] : [], - visTitleMode: state.visTitleMode ? [state.visTitleMode] : ['auto'], + labelMinor: state.labelMinor ? [state.labelMinor] : [], + labelMajor: state.labelMajor ? [state.labelMajor] : [], + labelMajorMode: state.labelMajorMode ? [state.labelMajorMode] : ['auto'], }, }, ], @@ -173,7 +173,7 @@ export const getGaugeVisualization = ({ shape: GaugeShapes.horizontalBullet, palette: mainPalette, ticksPosition: 'auto', - visTitleMode: 'auto', + labelMajorMode: 'auto', } ); }, From ddddfbae1e35871afa8f7fd21869cfc5f813f654 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Thu, 2 Dec 2021 14:21:07 +0100 Subject: [PATCH 34/43] fix bug with percent color bands --- .../gauge/chart_component.test.tsx | 56 +++++++++++++++++++ .../visualizations/gauge/chart_component.tsx | 30 +++++----- .../gauge/visualization.test.ts | 2 + .../visualizations/gauge/visualization.tsx | 1 + 4 files changed, 75 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx index 5b038191fba79..18197a00aa1d7 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx @@ -255,6 +255,62 @@ describe('GaugeComponent', function () { expect(goal.prop('ticks')).toEqual([0, 1, 2, 3, 10]); expect(goal.prop('bands')).toEqual([0, 1, 2, 3, 10]); }); + it('sets proper color bands and ticks on color bands if palette steps are smaller than minimum', () => { + const palette = { + type: 'palette' as const, + name: 'custom', + params: { + colors: ['#aaa', '#bbb', '#ccc'], + gradient: false, + stops: [-10, -5, 0] as unknown as ColorStop[], + range: 'number', + rangeMin: 0, + rangeMax: 4, + }, + }; + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + palette, + ticksPosition: 'bands', + }, + } as GaugeRenderProps; + const goal = shallowWithIntl().find(Goal); + expect(goal.prop('ticks')).toEqual([0, 10]); + expect(goal.prop('bands')).toEqual([0, 10]); + }); + it('sets proper color bands and ticks on color bands if percent palette steps are smaller than 0', () => { + const palette = { + type: 'palette' as const, + name: 'custom', + params: { + colors: ['#aaa', '#bbb', '#ccc'], + gradient: false, + stops: [-20, -60, 80], + range: 'percent', + rangeMin: 0, + rangeMax: 4, + }, + }; + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + palette, + ticksPosition: 'bands', + }, + } as GaugeRenderProps; + const goal = shallowWithIntl().find(Goal); + expect(goal.prop('ticks')).toEqual([0, 8, 10]); + expect(goal.prop('bands')).toEqual([0, 8, 10]); + }); it('doesnt set ticks for values differing <10%', () => { const palette = { type: 'palette' as const, diff --git a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx index a8999cb887d9e..c9ba372441418 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx @@ -42,11 +42,14 @@ declare global { } } -function normalizeColors({ colors, stops }: CustomPaletteState, min: number) { +function normalizeColors({ colors, stops, range }: CustomPaletteState, min: number) { if (!colors) { return; } - const colorsOutOfRangeSmaller = Math.max(stops.filter((stop, i) => stop < min).length, 0); + const colorsOutOfRangeSmaller = Math.max( + stops.filter((stop, i) => (range === 'percent' ? stop < 0 : stop < min)).length, + 0 + ); return colors.slice(colorsOutOfRangeSmaller); } @@ -99,20 +102,19 @@ function getTicks( range: [number, number], colorBands?: number[] ) { - if (ticksPosition === GaugeTicksPositions.auto) { - const TICKS_NO = 3; - const min = Math.min(...(colorBands || []), ...range); - const max = Math.max(...(colorBands || []), ...range); - const step = (max - min) / TICKS_NO; - return [ - ...Array(TICKS_NO) - .fill(null) - .map((_, i) => Number((min + step * i).toFixed(2))), - max, - ]; - } else { + if (ticksPosition === GaugeTicksPositions.bands && colorBands) { return colorBands && getTicksLabels(colorBands); } + const TICKS_NO = 3; + const min = Math.min(...(colorBands || []), ...range); + const max = Math.max(...(colorBands || []), ...range); + const step = (max - min) / TICKS_NO; + return [ + ...Array(TICKS_NO) + .fill(null) + .map((_, i) => Number((min + step * i).toFixed(2))), + max, + ]; } export const GaugeComponent: FC = ({ diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts index 506c03107033b..4f009fbf53df1 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts @@ -317,6 +317,7 @@ describe('gauge', () => { minAccessor: 'min-accessor', palette: [] as unknown as PaletteOutput, colorMode: 'palette', + ticksPosition: 'bands', }; test('removes metricAccessor correctly', () => { expect( @@ -348,6 +349,7 @@ describe('gauge', () => { metricAccessor: 'metric-accessor', palette: [] as unknown as PaletteOutput, colorMode: 'palette', + ticksPosition: 'bands', }); }); }); diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx index 30fb9daca16c5..ce416553f9408 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx @@ -305,6 +305,7 @@ export const getGaugeVisualization = ({ delete update.metricAccessor; delete update.palette; delete update.colorMode; + update.ticksPosition = 'auto'; } return update; From a9c8a652831159526df31f38220b07b65aae9ada Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Fri, 3 Dec 2021 13:50:00 +0100 Subject: [PATCH 35/43] functional tests --- .../visualizations/gauge/dimension_editor.tsx | 1 + .../toolbar_component/gauge_toolbar.test.tsx | 8 +- .../gauge/toolbar_component/index.tsx | 4 +- .../gauge/visualization.test.ts | 6 +- .../visualizations/gauge/visualization.tsx | 2 +- x-pack/test/functional/apps/lens/gauge.ts | 122 ++++++++++++++++++ x-pack/test/functional/apps/lens/index.ts | 1 + .../test/functional/page_objects/lens_page.ts | 10 +- 8 files changed, 141 insertions(+), 13 deletions(-) create mode 100644 x-pack/test/functional/apps/lens/gauge.ts diff --git a/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx index e12329e6d39e8..89a4be3300e2e 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx @@ -87,6 +87,7 @@ export function GaugeDimensionEditor( className="lnsDynamicColoringRow" > { diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts index 4f009fbf53df1..3c0020dc54e74 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts @@ -98,7 +98,7 @@ describe('gauge', () => { filterOperations: isNumericMetric, supportsMoreColumns: false, required: true, - dataTestSubj: 'lnsGauge_maxDimensionPanel', + dataTestSubj: 'lnsGauge_metricDimensionPanel', enableDimensionEditor: true, supportFieldFormat: true, supportStaticValue: true, @@ -165,7 +165,7 @@ describe('gauge', () => { filterOperations: isNumericMetric, supportsMoreColumns: true, required: true, - dataTestSubj: 'lnsGauge_maxDimensionPanel', + dataTestSubj: 'lnsGauge_metricDimensionPanel', enableDimensionEditor: true, supportFieldFormat: true, supportStaticValue: true, @@ -238,7 +238,7 @@ describe('gauge', () => { filterOperations: isNumericMetric, supportsMoreColumns: false, required: true, - dataTestSubj: 'lnsGauge_maxDimensionPanel', + dataTestSubj: 'lnsGauge_metricDimensionPanel', enableDimensionEditor: true, supportFieldFormat: true, supportStaticValue: true, diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx index ce416553f9408..58629a42ed234 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx @@ -218,7 +218,7 @@ export const getGaugeVisualization = ({ filterOperations: isNumericMetric, supportsMoreColumns: !state.metricAccessor, required: true, - dataTestSubj: 'lnsGauge_maxDimensionPanel', + dataTestSubj: 'lnsGauge_metricDimensionPanel', enableDimensionEditor: true, }, { diff --git a/x-pack/test/functional/apps/lens/gauge.ts b/x-pack/test/functional/apps/lens/gauge.ts new file mode 100644 index 0000000000000..39ee223ee68e3 --- /dev/null +++ b/x-pack/test/functional/apps/lens/gauge.ts @@ -0,0 +1,122 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); + const elasticChart = getService('elasticChart'); + const testSubjects = getService('testSubjects'); + const find = getService('find'); + + describe('lens gauge', () => { + before(async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await elasticChart.setNewChartUiDebugFlag(true); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: 'ip', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'average', + field: 'bytes', + }); + + await PageObjects.lens.waitForVisualization(); + }); + + it('should switch to gauge and render a gauge with default values', async () => { + await PageObjects.lens.switchToVisualization('horizontalBullet', 'gauge'); + await PageObjects.lens.waitForVisualization(); + const elementWithInfo = await find.byCssSelector('.echScreenReaderOnly'); + const textContent = await elementWithInfo.getAttribute('textContent'); + expect(textContent).to.contain('Average of bytes'); // it gets default title + expect(textContent).to.contain('horizontalBullet chart'); + expect(textContent).to.contain('Minimum:0'); // it gets default minimum static value + expect(textContent).to.contain('Maximum:8000'); // it gets default maximum static value + expect(textContent).to.contain('Value:5727.32'); + }); + + it('should reflect edits for gauge', async () => { + await PageObjects.lens.openVisualOptions(); + await testSubjects.setValue('lnsToolbarGaugeLabelMajor', 'custom title'); + await testSubjects.setValue('lnsToolbarGaugeLabelMinor-select', 'custom'); + await testSubjects.setValue('lnsToolbarGaugeLabelMinor', 'custom subtitle'); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsGauge_metricDimensionPanel > lns-dimensionTrigger', + operation: 'count', + isPreviousIncompatible: true, + keepOpen: true, + }); + + await testSubjects.setEuiSwitch('lnsDynamicColoringGaugeSwitch', 'check'); + await PageObjects.lens.closeDimensionEditor(); + + await PageObjects.lens.openDimensionEditor( + 'lnsGauge_goalDimensionPanel > lns-empty-dimension' + ); + + await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.closeDimensionEditor(); + await PageObjects.lens.openDimensionEditor( + 'lnsGauge_minDimensionPanel > lns-empty-dimension-suggested-value' + ); + + await testSubjects.setValue('lns-indexPattern-static_value-input', '1000', { + clearWithKeyboard: true, + }); + await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.closeDimensionEditor(); + + await PageObjects.lens.openDimensionEditor( + 'lnsGauge_maxDimensionPanel > lns-empty-dimension-suggested-value' + ); + + await testSubjects.setValue('lns-indexPattern-static_value-input', '25000', { + clearWithKeyboard: true, + }); + await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.closeDimensionEditor(); + + const elementWithInfo = await find.byCssSelector('.echScreenReaderOnly'); + const textContent = await elementWithInfo.getAttribute('textContent'); + expect(textContent).to.contain('custom title'); + expect(textContent).to.contain('custom subtitle'); + expect(textContent).to.contain('horizontalBullet chart'); + expect(textContent).to.contain('Minimum:1000'); + expect(textContent).to.contain('Maximum:25000'); + expect(textContent).to.contain('Target:15000'); + expect(textContent).to.contain('Value:14005'); + }); + it('should seamlessly switch to vertical chart without losing configuration', async () => { + await PageObjects.lens.switchToVisualization('verticalBullet', 'gauge'); + const elementWithInfo = await find.byCssSelector('.echScreenReaderOnly'); + const textContent = await elementWithInfo.getAttribute('textContent'); + expect(textContent).to.contain('custom title'); + expect(textContent).to.contain('custom subtitle'); + expect(textContent).to.contain('verticalBullet chart'); + expect(textContent).to.contain('Minimum:1000'); + expect(textContent).to.contain('Maximum:25000'); + expect(textContent).to.contain('Target:15000'); + expect(textContent).to.contain('Value:14005'); + }); + it('should switch to table chart and filter not supported static values', async () => { + await PageObjects.lens.switchToVisualization('lnsDatatable'); + const columnsCount = await PageObjects.lens.getCountOfDatatableColumns(); + expect(columnsCount).to.eql(1); + expect(await PageObjects.lens.getDatatableHeaderText(0)).to.equal('Count of records'); + }); + }); +} diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts index 72442be7645fa..dc1056918eda2 100644 --- a/x-pack/test/functional/apps/lens/index.ts +++ b/x-pack/test/functional/apps/lens/index.ts @@ -57,6 +57,7 @@ export default function ({ getService, loadTestFile, getPageObjects }: FtrProvid loadTestFile(require.resolve('./geo_field')); loadTestFile(require.resolve('./formula')); loadTestFile(require.resolve('./heatmap')); + loadTestFile(require.resolve('./gauge')); loadTestFile(require.resolve('./metrics')); loadTestFile(require.resolve('./reference_lines')); loadTestFile(require.resolve('./inspector')); diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 78b9762e3889a..77b420804737e 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -807,6 +807,12 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }, {}); }, + async getCountOfDatatableColumns() { + const table = await find.byCssSelector('.euiDataGrid'); + const $ = await table.parseDomContent(); + return (await $('.euiDataGridHeaderCell__content')).length; + }, + async getDatatableHeader(index = 0) { log.debug(`All headers ${await testSubjects.getVisibleText('dataGridHeader')}`); return find.byCssSelector( @@ -817,9 +823,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }, async getDatatableCell(rowIndex = 0, colIndex = 0) { - const table = await find.byCssSelector('.euiDataGrid'); - const $ = await table.parseDomContent(); - const columnNumber = $('.euiDataGridHeaderCell__content').length; + const columnNumber = await this.getCountOfDatatableColumns(); return await find.byCssSelector( `[data-test-subj="lnsDataTable"] [data-test-subj="dataGridRowCell"]:nth-child(${ rowIndex * columnNumber + colIndex + 2 From c10c882b3ad7301467ffa2d055a46bac28a65816 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Fri, 3 Dec 2021 14:40:12 +0100 Subject: [PATCH 36/43] metric shouldn't have static value --- .../plugins/lens/public/visualizations/gauge/utils.ts | 10 ---------- .../public/visualizations/gauge/visualization.test.ts | 3 --- .../public/visualizations/gauge/visualization.tsx | 11 +---------- 3 files changed, 1 insertion(+), 23 deletions(-) diff --git a/x-pack/plugins/lens/public/visualizations/gauge/utils.ts b/x-pack/plugins/lens/public/visualizations/gauge/utils.ts index fb11ecb9cb51f..34cd05775c292 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/utils.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/utils.ts @@ -77,16 +77,6 @@ export const getMinValue = (row?: DatatableRow, state?: GaugeAccessorsType) => { return FALLBACK_VALUE; }; -export const getMetricValue = (row?: DatatableRow, state?: GaugeAccessorsType) => { - const currentValue = getValueFromAccessor('metricAccessor', row, state); - if (currentValue != null) { - return currentValue; - } - const minValue = getMinValue(row, state); - const maxValue = getMaxValue(row, state); - return Math.round((maxValue - minValue) * 0.5 + minValue); -}; - export const getGoalValue = (row?: DatatableRow, state?: GaugeVisualizationState) => { const currentValue = getValueFromAccessor('goalAccessor', row, state); if (currentValue != null) { diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts index 3c0020dc54e74..82812347b29c2 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts @@ -101,7 +101,6 @@ describe('gauge', () => { dataTestSubj: 'lnsGauge_metricDimensionPanel', enableDimensionEditor: true, supportFieldFormat: true, - supportStaticValue: true, }, { layerId: 'first', @@ -168,7 +167,6 @@ describe('gauge', () => { dataTestSubj: 'lnsGauge_metricDimensionPanel', enableDimensionEditor: true, supportFieldFormat: true, - supportStaticValue: true, }, { layerId: 'first', @@ -241,7 +239,6 @@ describe('gauge', () => { dataTestSubj: 'lnsGauge_metricDimensionPanel', enableDimensionEditor: true, supportFieldFormat: true, - supportStaticValue: true, }, { layerId: 'first', diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx index 58629a42ed234..23aafbcc1bd71 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx @@ -20,7 +20,7 @@ import { applyPaletteParams, CUSTOM_PALETTE, getStopsForFixedMode } from '../../ import { GaugeDimensionEditor } from './dimension_editor'; import { CustomPaletteParams, layerTypes } from '../../../common'; import { generateId } from '../../id_generator'; -import { getGoalValue, getMaxValue, getMetricValue, getMinValue } from './utils'; +import { getGoalValue, getMaxValue, getMinValue } from './utils'; import { GaugeExpressionArgs, GaugeShapes, @@ -194,7 +194,6 @@ export const getGaugeVisualization = ({ return { groups: [ { - supportStaticValue: true, supportFieldFormat: true, layerId: state.layerId, groupId: GROUP_ID.METRIC, @@ -334,7 +333,6 @@ export const getGaugeVisualization = ({ const minAccessorValue = getMinValue(row, state); const maxAccessorValue = getMaxValue(row, state); - const metricAccessorValue = getMetricValue(row, state); const goalAccessorValue = getGoalValue(row, state); return [ @@ -345,13 +343,6 @@ export const getGaugeVisualization = ({ }), initialDimensions: state ? [ - { - groupId: 'metric', - columnId: generateId(), - dataType: 'number', - label: 'metricAccessor', - staticValue: metricAccessorValue, - }, { groupId: 'min', columnId: generateId(), From a356ce98a3f4d03a2bf13c1ca6db54550f8070d8 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Fri, 3 Dec 2021 15:56:34 +0100 Subject: [PATCH 37/43] pass formatter to metric --- .../visualizations/gauge/chart_component.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx index c9ba372441418..8160fdbe3d8fe 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx @@ -186,7 +186,18 @@ export const GaugeComponent: FC = ({ ); } - const formatter = formatFactory( + const metricFormatter = formatFactory( + metricColumn?.meta?.params?.id === 'number' + ? metricColumn?.meta?.params + : { + id: 'number', + params: { + pattern: `0,0.000`, + }, + } + ); + + const tickFormatter = formatFactory( metricColumn?.meta?.params?.params ? metricColumn?.meta?.params : { @@ -196,11 +207,13 @@ export const GaugeComponent: FC = ({ }, } ); + const colors = palette?.params?.colors ? normalizeColors(palette.params, min) : undefined; const bands: number[] = (palette?.params as CustomPaletteState) ? normalizeBands(args.palette?.params as CustomPaletteState, { min, max }) : [min, max]; + const target = Number(metricFormatter.convert(Math.min(Math.max(metricValue, min), max))); return ( @@ -209,8 +222,8 @@ export const GaugeComponent: FC = ({ subtype={subtype} base={min} target={goal && goal >= min && goal <= max ? goal : undefined} - actual={Math.min(Math.max(metricValue, min), max)} - tickValueFormatter={({ value: tickValue }) => formatter.convert(tickValue)} + actual={target} + tickValueFormatter={({ value: tickValue }) => tickFormatter.convert(tickValue)} bands={bands} ticks={getTicks(ticksPosition, [min, max], bands)} bandFillColor={ From bd7962562c1fb940c8763813e0e63327f322c715 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Fri, 3 Dec 2021 16:17:27 +0100 Subject: [PATCH 38/43] fake formatter --- .../visualizations/gauge/chart_component.tsx | 22 ++++++------------- .../lens/public/visualizations/gauge/utils.ts | 5 +++++ 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx index 8160fdbe3d8fe..1259a3de46d92 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx @@ -142,7 +142,9 @@ export const GaugeComponent: FC = ({ const table = Object.values(data.tables)[0]; const metricColumn = table.columns.find((col) => col.id === metricAccessor); - const chartData = table.rows.filter((v) => typeof v[metricAccessor!] === 'number'); + const chartData = table.rows.filter( + (v) => typeof v[metricAccessor!] === 'number' || Array.isArray(v[metricAccessor!]) + ); const row = chartData?.[0]; const metricValue = getValueFromAccessor('metricAccessor', row, args); @@ -186,17 +188,6 @@ export const GaugeComponent: FC = ({ ); } - const metricFormatter = formatFactory( - metricColumn?.meta?.params?.id === 'number' - ? metricColumn?.meta?.params - : { - id: 'number', - params: { - pattern: `0,0.000`, - }, - } - ); - const tickFormatter = formatFactory( metricColumn?.meta?.params?.params ? metricColumn?.meta?.params @@ -207,13 +198,14 @@ export const GaugeComponent: FC = ({ }, } ); - const colors = palette?.params?.colors ? normalizeColors(palette.params, min) : undefined; const bands: number[] = (palette?.params as CustomPaletteState) ? normalizeBands(args.palette?.params as CustomPaletteState, { min, max }) : [min, max]; - const target = Number(metricFormatter.convert(Math.min(Math.max(metricValue, min), max))); + // TODO: format in charts + const formattedActual = Math.round(Math.min(Math.max(metricValue, min), max) * 1000) / 1000; + return ( @@ -222,7 +214,7 @@ export const GaugeComponent: FC = ({ subtype={subtype} base={min} target={goal && goal >= min && goal <= max ? goal : undefined} - actual={target} + actual={formattedActual} tickValueFormatter={({ value: tickValue }) => tickFormatter.convert(tickValue)} bands={bands} ticks={getTicks(ticksPosition, [min, max], bands)} diff --git a/x-pack/plugins/lens/public/visualizations/gauge/utils.ts b/x-pack/plugins/lens/public/visualizations/gauge/utils.ts index 34cd05775c292..ec6e52b01864b 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/utils.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/utils.ts @@ -31,6 +31,11 @@ export const getValueFromAccessor = ( if (typeof value === 'number') { return value; } + if (value?.length) { + if (typeof value[value.length - 1] === 'number') { + return value[value.length - 1]; + } + } } }; From bbfefa0309e83f8a31f1eb3b1a36c9fdcf405425 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Mon, 6 Dec 2021 16:14:19 +0100 Subject: [PATCH 39/43] fix css and replace emptyPlaceholder --- .../public/expression_renderers/index.scss | 8 ----- .../static/components/empty_placeholder.scss | 7 +++++ .../static/components/empty_placeholder.tsx | 17 +++++++---- .../components/table_basic.test.tsx | 2 +- .../components/table_basic.tsx | 3 +- .../droppable/get_drop_props.ts | 23 +++++++++++--- .../operations/definitions/static_value.tsx | 1 + .../metric_visualization/expression.tsx | 3 +- .../render_function.test.tsx | 2 +- .../pie_visualization/render_function.tsx | 2 +- .../shared_components/empty_placeholder.tsx | 30 ------------------- .../lens/public/shared_components/index.ts | 1 - .../visualizations/gauge/chart_component.tsx | 2 +- .../public/visualizations/gauge/index.scss | 1 + .../visualizations/gauge/suggestions.ts | 26 ++++++++-------- .../visualizations/gauge/visualization.tsx | 3 +- .../xy_visualization/expression.test.tsx | 2 +- .../public/xy_visualization/expression.tsx | 2 +- 18 files changed, 64 insertions(+), 71 deletions(-) create mode 100644 src/plugins/charts/public/static/components/empty_placeholder.scss delete mode 100644 x-pack/plugins/lens/public/shared_components/empty_placeholder.tsx diff --git a/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/index.scss b/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/index.scss index 6e1afd91c476d..fb004dfce4ec0 100644 --- a/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/index.scss +++ b/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/index.scss @@ -9,14 +9,6 @@ padding: $euiSizeS; } -.heatmap-chart__empty { - height: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - .heatmap-chart-icon__subdued { fill: $euiTextSubduedColor; } diff --git a/src/plugins/charts/public/static/components/empty_placeholder.scss b/src/plugins/charts/public/static/components/empty_placeholder.scss new file mode 100644 index 0000000000000..3f98da9eecb6a --- /dev/null +++ b/src/plugins/charts/public/static/components/empty_placeholder.scss @@ -0,0 +1,7 @@ +.chart__empty-placeholder { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} \ No newline at end of file diff --git a/src/plugins/charts/public/static/components/empty_placeholder.tsx b/src/plugins/charts/public/static/components/empty_placeholder.tsx index db3f3fb6739d5..e376120c9cd9e 100644 --- a/src/plugins/charts/public/static/components/empty_placeholder.tsx +++ b/src/plugins/charts/public/static/components/empty_placeholder.tsx @@ -9,15 +9,20 @@ import React from 'react'; import { EuiIcon, EuiText, IconType, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import './empty_placeholder.scss'; -export const EmptyPlaceholder = (props: { icon: IconType }) => ( +export const EmptyPlaceholder = ({ + icon, + message = , +}: { + icon: IconType; + message?: JSX.Element; +}) => ( <> - - + + -

- -

+

{message}

); diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx index 19315b5835d5f..46ca179e7cdb4 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx @@ -17,7 +17,7 @@ import { SerializedFieldFormat, } from 'src/plugins/field_formats/common'; import { VisualizationContainer } from '../../visualization_container'; -import { EmptyPlaceholder } from '../../shared_components'; +import { EmptyPlaceholder } from '../../../../../../src/plugins/charts/public'; import { LensIconChartDatatable } from '../../assets/chart_datatable'; import { DataContext, DatatableComponent } from './table_basic'; import { LensMultiTable } from '../../../common'; diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx index 7ceffcaaff5db..ba33ce77fc72d 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx @@ -18,11 +18,12 @@ import { EuiDataGridSorting, EuiDataGridStyle, } from '@elastic/eui'; +import { EmptyPlaceholder } from '../../../../../../src/plugins/charts/public'; import type { LensFilterEvent, LensTableRowContextMenuEvent } from '../../types'; import type { FormatFactory } from '../../../common'; import type { LensGridDirection } from '../../../common/expressions'; import { VisualizationContainer } from '../../visualization_container'; -import { EmptyPlaceholder, findMinMaxByColumnId } from '../../shared_components'; +import { findMinMaxByColumnId } from '../../shared_components'; import { LensIconChartDatatable } from '../../assets/chart_datatable'; import type { DataContextType, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts index 0882b243c6b3d..fcc9a57285ba6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts @@ -10,6 +10,7 @@ import { isDraggedOperation, DraggedOperation, DropType, + VisualizationDimensionGroupConfig, } from '../../../types'; import { getOperationDisplay } from '../../operations'; import { hasField, isDraggedField } from '../../utils'; @@ -91,7 +92,7 @@ export function getDropProps(props: GetDropProps) { } else if (hasTheSameField(sourceColumn, targetColumn)) { return; } else if (filterOperations(sourceColumn)) { - return getDropPropsForCompatibleGroup(targetColumn); + return getDropPropsForCompatibleGroup(props.dimensionGroups, dragging.columnId, targetColumn); } else { return getDropPropsFromIncompatibleGroup({ ...props, dragging }); } @@ -143,12 +144,26 @@ function getDropPropsForSameGroup(targetColumn?: GenericIndexPatternColumn): Dro return targetColumn ? { dropTypes: ['reorder'] } : { dropTypes: ['duplicate_compatible'] }; } -function getDropPropsForCompatibleGroup(targetColumn?: GenericIndexPatternColumn): DropProps { - return { +function getDropPropsForCompatibleGroup( + dimensionGroups: VisualizationDimensionGroupConfig[], + sourceId: string, + targetColumn?: GenericIndexPatternColumn +): DropProps { + const canSwap = + targetColumn && + dimensionGroups + .find((group) => group.accessors.some((accessor) => accessor.columnId === sourceId)) + ?.filterOperations(targetColumn); + + const dropTypes: DropProps = { dropTypes: targetColumn - ? ['replace_compatible', 'replace_duplicate_compatible', 'swap_compatible'] + ? ['replace_compatible', 'replace_duplicate_compatible'] : ['move_compatible', 'duplicate_compatible'], }; + if (canSwap) { + dropTypes.dropTypes.push('swap_compatible'); + } + return dropTypes; } function getDropPropsFromIncompatibleGroup({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx index d10be2cae6a83..0adaf8ea00640 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx @@ -123,6 +123,7 @@ export const staticValueOperation: OperationDefinition< label: ofName(previousParams.value), dataType: 'number', operationType: 'static_value', + isStaticValue: true, isBucketed: false, scale: 'ratio', params: { ...previousParams, value: String(previousParams.value ?? defaultValue) }, diff --git a/x-pack/plugins/lens/public/metric_visualization/expression.tsx b/x-pack/plugins/lens/public/metric_visualization/expression.tsx index 4c92864776045..b9a79963510ed 100644 --- a/x-pack/plugins/lens/public/metric_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/expression.tsx @@ -22,7 +22,8 @@ import { } from '../../../../../src/plugins/charts/public'; import { AutoScale } from './auto_scale'; import { VisualizationContainer } from '../visualization_container'; -import { EmptyPlaceholder, getContrastColor } from '../shared_components'; +import { getContrastColor } from '../shared_components'; +import { EmptyPlaceholder } from '../../../../../src/plugins/charts/public'; import { LensIconChartMetric } from '../assets/chart_metric'; import type { FormatFactory } from '../../common'; import type { MetricChartProps } from '../../common/expressions'; 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 index 55b621498bb10..ef160b1dd682b 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx @@ -20,7 +20,7 @@ import type { LensMultiTable } from '../../common'; import type { PieExpressionArgs } from '../../common/expressions'; import { PieComponent } from './render_function'; import { VisualizationContainer } from '../visualization_container'; -import { EmptyPlaceholder } from '../shared_components'; +import { EmptyPlaceholder } from '../../../../../src/plugins/charts/public'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; import { LensIconChartDonut } from '../assets/chart_donut'; diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index 3b9fdaf094822..5841732fb08d1 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -36,7 +36,7 @@ import { byDataColorPaletteMap, extractUniqTermsMap, } from './render_helpers'; -import { EmptyPlaceholder } from '../shared_components'; +import { EmptyPlaceholder } from '../../../../../src/plugins/charts/public'; import './visualization.scss'; import { ChartsPluginSetup, diff --git a/x-pack/plugins/lens/public/shared_components/empty_placeholder.tsx b/x-pack/plugins/lens/public/shared_components/empty_placeholder.tsx deleted file mode 100644 index ab69c6cc3139d..0000000000000 --- a/x-pack/plugins/lens/public/shared_components/empty_placeholder.tsx +++ /dev/null @@ -1,30 +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 { EuiIcon, EuiText, IconType, EuiSpacer } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; - -const noResultsMessage = ( - -); - -export const EmptyPlaceholder = ({ - icon, - message = noResultsMessage, -}: { - icon: IconType; - message?: JSX.Element; -}) => ( - <> - - - -

{message}

-
- -); diff --git a/x-pack/plugins/lens/public/shared_components/index.ts b/x-pack/plugins/lens/public/shared_components/index.ts index 878c695bc8b9e..dd2d5aa7c8558 100644 --- a/x-pack/plugins/lens/public/shared_components/index.ts +++ b/x-pack/plugins/lens/public/shared_components/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -export * from './empty_placeholder'; export type { ToolbarPopoverProps } from './toolbar_popover'; export { ToolbarPopover } from './toolbar_popover'; export { LegendSettingsPopover } from './legend_settings_popover'; diff --git a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx index 1259a3de46d92..a8f2b0e1c204c 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx @@ -15,8 +15,8 @@ import type { } from 'src/plugins/charts/public'; import { VisualizationContainer } from '../../visualization_container'; import './index.scss'; -import { EmptyPlaceholder } from '../../shared_components'; import { LensIconChartGaugeHorizontal, LensIconChartGaugeVertical } from '../../assets/chart_gauge'; +import { EmptyPlaceholder } from '../../../../../../src/plugins/charts/public'; import { getMaxValue, getMinValue, getValueFromAccessor } from './utils'; import { GaugeExpressionProps, diff --git a/x-pack/plugins/lens/public/visualizations/gauge/index.scss b/x-pack/plugins/lens/public/visualizations/gauge/index.scss index 2a26c17df416f..c999fe7e218a2 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/index.scss +++ b/x-pack/plugins/lens/public/visualizations/gauge/index.scss @@ -6,6 +6,7 @@ width: 100%; height: 100%; text-align: center; + overflow-x: hidden; .echChart { width: 100%; diff --git a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts index 8d9e9c7138e47..184ce08a7f956 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts @@ -38,12 +38,6 @@ export const getSuggestions: Visualization['getSuggesti [state.minAccessor, state.maxAccessor, state.goalAccessor, state.metricAccessor].filter(Boolean) .length; - const isShapeChange = - (subVisualizationId === GaugeShapes.horizontalBullet || - subVisualizationId === GaugeShapes.verticalBullet) && - isGauge && - subVisualizationId !== state?.shape; - if ( hasLayerMismatch(keptLayerIds, table) || isNotNumericMetric(table) || @@ -61,7 +55,6 @@ export const getSuggestions: Visualization['getSuggesti const baseSuggestion = { state: { ...state, - metricAccessor: table.columns[0].columnId, shape, layerId: table.layerId, layerType: layerTypes.DATA, @@ -73,10 +66,10 @@ export const getSuggestions: Visualization['getSuggesti }), previewIcon: 'empty', score: 0.5, - hide: !isGauge, // only display for gauges for beta + hide: !isGauge && state?.metricAccessor === undefined, // only display for gauges for beta }; - const suggestions = isShapeChange + const suggestions = isGauge ? [ { ...baseSuggestion, @@ -84,18 +77,25 @@ export const getSuggestions: Visualization['getSuggesti ...baseSuggestion.state, ...state, shape: - subVisualizationId === GaugeShapes.verticalBullet - ? GaugeShapes.verticalBullet - : GaugeShapes.horizontalBullet, + state?.shape === GaugeShapes.verticalBullet + ? GaugeShapes.horizontalBullet + : GaugeShapes.verticalBullet, }, }, ] : [ - { ...baseSuggestion, hide: true }, { ...baseSuggestion, state: { ...baseSuggestion.state, + metricAccessor: table.columns[0].columnId, + }, + }, + { + ...baseSuggestion, + state: { + ...baseSuggestion.state, + metricAccessor: table.columns[0].columnId, shape: state?.shape === GaugeShapes.verticalBullet ? GaugeShapes.horizontalBullet diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx index 23aafbcc1bd71..fa39da6cbce73 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx @@ -214,7 +214,8 @@ export const getGaugeVisualization = ({ }, ] : [], - filterOperations: isNumericMetric, + filterOperations: (op: OperationMetadata) => + !op.isBucketed && op.dataType === 'number' && !op.isStaticValue, supportsMoreColumns: !state.metricAccessor, required: true, dataTestSubj: 'lnsGauge_metricDimensionPanel', diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx index bb3b5bfcbfec6..65425b04129d3 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx @@ -45,7 +45,7 @@ import { shallow } from 'enzyme'; import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks'; import { mountWithIntl } from '@kbn/test/jest'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; -import { EmptyPlaceholder } from '../shared_components/empty_placeholder'; +import { EmptyPlaceholder } from '../../../../../src/plugins/charts/public'; import { XyEndzones } from './x_domain'; const onClickValue = jest.fn(); diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index d22a8034cdf2b..01359c68c6da3 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -44,6 +44,7 @@ import { IconType } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { RenderMode } from 'src/plugins/expressions'; import { ThemeServiceStart } from 'kibana/public'; +import { EmptyPlaceholder } from '../../../../../src/plugins/charts/public'; import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public'; import type { ILensInterpreterRenderHandlers, LensFilterEvent, LensBrushEvent } from '../types'; import type { LensMultiTable, FormatFactory } from '../../common'; @@ -61,7 +62,6 @@ import { useActiveCursor, } from '../../../../../src/plugins/charts/public'; import { MULTILAYER_TIME_AXIS_STYLE } from '../../../../../src/plugins/charts/common'; -import { EmptyPlaceholder } from '../shared_components'; import { getFitOptions } from './fitting_functions'; import { getAxesConfiguration, GroupsConfiguration, validateExtent } from './axes_configuration'; import { getColorAssignments } from './color_assignment'; From 062aa5ab533070f6cddd1d35f0deb3a1cfed117f Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Mon, 6 Dec 2021 17:42:52 +0100 Subject: [PATCH 40/43] fix tests --- .../public/indexpattern_datasource/indexpattern.test.ts | 1 + .../operations/definitions/static_value.test.tsx | 8 ++++++++ .../public/visualizations/gauge/visualization.test.ts | 8 ++++---- .../lens/public/visualizations/gauge/visualization.tsx | 6 ++++-- x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 6 files changed, 17 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index ba5564df84439..d7ea174718813 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -1724,6 +1724,7 @@ describe('IndexPattern Data Source', () => { ...state.layers.first.columns, newStatic: { dataType: 'number', + isStaticValue: true, isBucketed: false, label: 'Static value: 0', operationType: 'static_value', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx index 3e56565b2e13e..6d9a39887b940 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx @@ -72,6 +72,7 @@ describe('static_value', () => { dataType: 'number', isBucketed: false, operationType: 'static_value', + isStaticValue: true, references: [], params: { value: '23', @@ -106,6 +107,7 @@ describe('static_value', () => { dataType: 'number', isBucketed: false, operationType: 'static_value', + isStaticValue: true, references: [], params: { value: '23', @@ -237,6 +239,7 @@ describe('static_value', () => { label: 'Static value', dataType: 'number', operationType: 'static_value', + isStaticValue: true, isBucketed: false, scale: 'ratio', params: { value: '100' }, @@ -253,6 +256,7 @@ describe('static_value', () => { label: 'Static value', dataType: 'number', operationType: 'static_value', + isStaticValue: true, isBucketed: false, scale: 'ratio', params: { value: '23' }, @@ -263,6 +267,7 @@ describe('static_value', () => { label: 'Static value: 23', dataType: 'number', operationType: 'static_value', + isStaticValue: true, isBucketed: false, scale: 'ratio', params: { value: '23' }, @@ -283,6 +288,7 @@ describe('static_value', () => { label: 'Static value: 23', dataType: 'number', operationType: 'static_value', + isStaticValue: true, isBucketed: false, scale: 'ratio', params: { value: '23' }, @@ -300,6 +306,7 @@ describe('static_value', () => { label: 'Static value', dataType: 'number', operationType: 'static_value', + isStaticValue: true, isBucketed: false, scale: 'ratio', params: { value: '23' }, @@ -312,6 +319,7 @@ describe('static_value', () => { label: 'Static value: 53', dataType: 'number', operationType: 'static_value', + isStaticValue: true, isBucketed: false, scale: 'ratio', params: { value: '53' }, diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts index 82812347b29c2..1d7bfdb36e0fd 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { getGaugeVisualization, isNumericMetric } from './visualization'; +import { getGaugeVisualization, isNumericDynamicMetric, isNumericMetric } from './visualization'; import { createMockDatasource, createMockFramePublicAPI } from '../../mocks'; import { GROUP_ID } from './constants'; import type { DatasourcePublicAPI, Operation } from '../../types'; @@ -95,7 +95,7 @@ describe('gauge', () => { groupId: GROUP_ID.METRIC, groupLabel: 'Metric', accessors: [{ columnId: 'metric-accessor', triggerIcon: 'none' }], - filterOperations: isNumericMetric, + filterOperations: isNumericDynamicMetric, supportsMoreColumns: false, required: true, dataTestSubj: 'lnsGauge_metricDimensionPanel', @@ -161,7 +161,7 @@ describe('gauge', () => { groupId: GROUP_ID.METRIC, groupLabel: 'Metric', accessors: [], - filterOperations: isNumericMetric, + filterOperations: isNumericDynamicMetric, supportsMoreColumns: true, required: true, dataTestSubj: 'lnsGauge_metricDimensionPanel', @@ -233,7 +233,7 @@ describe('gauge', () => { groupId: GROUP_ID.METRIC, groupLabel: 'Metric', accessors: [{ columnId: 'metric-accessor', triggerIcon: 'none' }], - filterOperations: isNumericMetric, + filterOperations: isNumericDynamicMetric, supportsMoreColumns: false, required: true, dataTestSubj: 'lnsGauge_metricDimensionPanel', diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx index fa39da6cbce73..7d9cefb429bd4 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx @@ -39,6 +39,9 @@ interface GaugeVisualizationDeps { export const isNumericMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; +export const isNumericDynamicMetric = (op: OperationMetadata) => + isNumericMetric(op) && !op.isStaticValue; + export const CHART_NAMES = { horizontalBullet: { icon: LensIconChartGaugeHorizontal, @@ -214,8 +217,7 @@ export const getGaugeVisualization = ({ }, ] : [], - filterOperations: (op: OperationMetadata) => - !op.isBucketed && op.dataType === 'number' && !op.isStaticValue, + filterOperations: isNumericDynamicMetric, supportsMoreColumns: !state.metricAccessor, required: true, dataTestSubj: 'lnsGauge_metricDimensionPanel', diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4694b8c63d7e0..aea73578fcb94 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -14729,7 +14729,6 @@ "xpack.lens.xyVisualization.lineLabel": "折れ線", "xpack.lens.xyVisualization.mixedBarHorizontalLabel": "混在した横棒", "xpack.lens.xyVisualization.mixedLabel": "ミックスされた XY", - "xpack.lens.xyVisualization.noDataLabel": "結果が見つかりませんでした", "xpack.lens.xyVisualization.stackedAreaLabel": "面積み上げ", "xpack.lens.xyVisualization.stackedBarHorizontalFullLabel": "積み上げ横棒", "xpack.lens.xyVisualization.stackedBarHorizontalLabel": "H.積み上げ棒", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ba523a4236b7d..9e006e3449745 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -14923,7 +14923,6 @@ "xpack.lens.xyVisualization.lineLabel": "折线图", "xpack.lens.xyVisualization.mixedBarHorizontalLabel": "水平混合条形图", "xpack.lens.xyVisualization.mixedLabel": "混合 XY", - "xpack.lens.xyVisualization.noDataLabel": "找不到结果", "xpack.lens.xyVisualization.stackedAreaLabel": "堆积面积图", "xpack.lens.xyVisualization.stackedBarHorizontalFullLabel": "水平堆叠条形图", "xpack.lens.xyVisualization.stackedBarHorizontalLabel": "水平堆叠条形图", From f5955503070411d2e64f3bc74ebba08bd16502f2 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 7 Dec 2021 09:50:00 +0100 Subject: [PATCH 41/43] fix static values --- .../plugins/lens/public/xy_visualization/visualization.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index 5e160e5a492ec..8330acf28264c 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -46,8 +46,9 @@ import { groupAxesByType } from './axes_configuration'; const defaultIcon = LensIconChartBarStacked; const defaultSeriesType = 'bar_stacked'; -const isNumericMetric = (op: OperationMetadata) => - !op.isBucketed && op.dataType === 'number' && !op.isStaticValue; + +const isNumericMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; +const isNumericDynamicMetric = (op: OperationMetadata) => isNumericMetric(op) && !op.isStaticValue; const isBucketed = (op: OperationMetadata) => op.isBucketed; function getVisualizationType(state: State): VisualizationType | 'mixed' { @@ -439,7 +440,7 @@ export const getXyVisualization = ({ groupId: 'y', groupLabel: getAxisName('y', { isHorizontal }), accessors: mappedAccessors, - filterOperations: isNumericMetric, + filterOperations: isNumericDynamicMetric, supportsMoreColumns: true, required: true, dataTestSubj: 'lnsXY_yDimensionPanel', From 7949d0f6a4ffde4f5ad46a6ace75ec2a4e0335cd Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 7 Dec 2021 10:08:41 +0100 Subject: [PATCH 42/43] name changes --- .../expressions/gauge_chart/gauge_chart.ts | 16 ++++++++-------- .../lens/common/expressions/gauge_chart/types.ts | 7 +++---- .../gauge/chart_component.test.tsx | 4 ++-- .../public/visualizations/gauge/suggestions.ts | 2 +- .../visualizations/gauge/visualization.test.ts | 7 +++++-- .../visualizations/gauge/visualization.tsx | 9 +++++---- 6 files changed, 24 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/lens/common/expressions/gauge_chart/gauge_chart.ts b/x-pack/plugins/lens/common/expressions/gauge_chart/gauge_chart.ts index 5fa6804f974a6..33bd9a802dde2 100644 --- a/x-pack/plugins/lens/common/expressions/gauge_chart/gauge_chart.ts +++ b/x-pack/plugins/lens/common/expressions/gauge_chart/gauge_chart.ts @@ -8,25 +8,25 @@ import { i18n } from '@kbn/i18n'; import type { ExpressionFunctionDefinition } from '../../../../../../src/plugins/expressions/common'; import type { LensMultiTable } from '../../types'; -import { GaugeExpressionArgs, GAUGE_FUNCTION, GAUGE_FUNCTION_RENDERER } from './types'; +import { GaugeArguments, EXPRESSION_GAUGE_NAME, GAUGE_FUNCTION_RENDERER_NAME } from './types'; export interface GaugeExpressionProps { data: LensMultiTable; - args: GaugeExpressionArgs; + args: GaugeArguments; } export interface GaugeRender { type: 'render'; - as: typeof GAUGE_FUNCTION_RENDERER; + as: typeof GAUGE_FUNCTION_RENDERER_NAME; value: GaugeExpressionProps; } export const gauge: ExpressionFunctionDefinition< - typeof GAUGE_FUNCTION, + typeof EXPRESSION_GAUGE_NAME, LensMultiTable, - GaugeExpressionArgs, + GaugeArguments, GaugeRender > = { - name: GAUGE_FUNCTION, + name: EXPRESSION_GAUGE_NAME, type: 'render', help: i18n.translate('xpack.lens.gauge.expressionHelpLabel', { defaultMessage: 'Gauge renderer', @@ -121,10 +121,10 @@ export const gauge: ExpressionFunctionDefinition< }, }, inputTypes: ['lens_multitable'], - fn(data: LensMultiTable, args: GaugeExpressionArgs) { + fn(data: LensMultiTable, args: GaugeArguments) { return { type: 'render', - as: GAUGE_FUNCTION_RENDERER, + as: GAUGE_FUNCTION_RENDERER_NAME, value: { data, args, diff --git a/x-pack/plugins/lens/common/expressions/gauge_chart/types.ts b/x-pack/plugins/lens/common/expressions/gauge_chart/types.ts index 52e6f335bbc07..0a58eec4fe0bb 100644 --- a/x-pack/plugins/lens/common/expressions/gauge_chart/types.ts +++ b/x-pack/plugins/lens/common/expressions/gauge_chart/types.ts @@ -11,8 +11,8 @@ import type { } from '../../../../../../src/plugins/charts/common'; import type { CustomPaletteParams, LayerType } from '../../types'; -export const GAUGE_FUNCTION = 'lens_gauge'; -export const GAUGE_FUNCTION_RENDERER = 'lens_gauge_renderer'; +export const EXPRESSION_GAUGE_NAME = 'lens_gauge'; +export const GAUGE_FUNCTION_RENDERER_NAME = 'lens_gauge_renderer'; export const GaugeShapes = { horizontalBullet: 'horizontalBullet', @@ -35,7 +35,6 @@ export const GaugeColorModes = { none: 'none', } as const; -export type GaugeType = 'gauge'; export type GaugeColorMode = keyof typeof GaugeColorModes; export type GaugeShape = keyof typeof GaugeShapes; export type GaugeLabelMajorMode = keyof typeof GaugeLabelMajorModes; @@ -64,7 +63,7 @@ export type GaugeVisualizationState = GaugeLayerState & { shape: GaugeShape; }; -export type GaugeExpressionArgs = SharedGaugeLayerState & { +export type GaugeArguments = SharedGaugeLayerState & { title?: string; description?: string; shape: GaugeShape; diff --git a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx index 18197a00aa1d7..517c5718f613f 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx @@ -11,7 +11,7 @@ import { shallowWithIntl } from '@kbn/test/jest'; import { chartPluginMock } from 'src/plugins/charts/public/mocks'; import type { ColorStop, LensMultiTable } from '../../../common'; import { fieldFormatsServiceMock } from '../../../../../../src/plugins/field_formats/public/mocks'; -import { GaugeExpressionArgs, GaugeLabelMajorMode } from '../../../common/expressions/gauge_chart'; +import { GaugeArguments, GaugeLabelMajorMode } from '../../../common/expressions/gauge_chart'; import { GaugeComponent, GaugeRenderProps } from './chart_component'; import { DatatableColumn, DatatableRow } from 'src/plugins/expressions/common'; import { VisualizationContainer } from '../../visualization_container'; @@ -55,7 +55,7 @@ const createData = ( const chartsThemeService = chartPluginMock.createSetupContract().theme; const palettesRegistry = chartPluginMock.createPaletteRegistry(); const formatService = fieldFormatsServiceMock.createStartContract(); -const args: GaugeExpressionArgs = { +const args: GaugeArguments = { labelMajor: 'Gauge', description: 'vis description', metricAccessor: 'metric-accessor', diff --git a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts index 184ce08a7f956..03198411581c1 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts @@ -66,7 +66,7 @@ export const getSuggestions: Visualization['getSuggesti }), previewIcon: 'empty', score: 0.5, - hide: !isGauge && state?.metricAccessor === undefined, // only display for gauges for beta + hide: !isGauge || state?.metricAccessor === undefined, // only display for gauges for beta }; const suggestions = isGauge diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts index 1d7bfdb36e0fd..e5e7f092db9ce 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts @@ -11,7 +11,10 @@ import { GROUP_ID } from './constants'; import type { DatasourcePublicAPI, Operation } from '../../types'; import { chartPluginMock } from 'src/plugins/charts/public/mocks'; import { CustomPaletteParams, layerTypes } from '../../../common'; -import { GAUGE_FUNCTION, GaugeVisualizationState } from '../../../common/expressions/gauge_chart'; +import { + EXPRESSION_GAUGE_NAME, + GaugeVisualizationState, +} from '../../../common/expressions/gauge_chart'; import { PaletteOutput } from 'src/plugins/charts/common'; function exampleState(): GaugeVisualizationState { @@ -408,7 +411,7 @@ describe('gauge', () => { chain: [ { type: 'function', - function: GAUGE_FUNCTION, + function: EXPRESSION_GAUGE_NAME, arguments: { title: ['Test'], description: [''], diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx index 7d9cefb429bd4..59dd2ac161a8a 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx @@ -21,10 +21,11 @@ import { GaugeDimensionEditor } from './dimension_editor'; import { CustomPaletteParams, layerTypes } from '../../../common'; import { generateId } from '../../id_generator'; import { getGoalValue, getMaxValue, getMinValue } from './utils'; + import { - GaugeExpressionArgs, GaugeShapes, - GAUGE_FUNCTION, + GaugeArguments, + EXPRESSION_GAUGE_NAME, GaugeVisualizationState, } from '../../../common/expressions/gauge_chart'; @@ -73,7 +74,7 @@ const toExpression = ( paletteService: PaletteRegistry, state: GaugeVisualizationState, datasourceLayers: Record, - attributes?: Partial> + attributes?: Partial> ): Ast | null => { const datasource = datasourceLayers[state.layerId]; @@ -87,7 +88,7 @@ const toExpression = ( chain: [ { type: 'function', - function: GAUGE_FUNCTION, + function: EXPRESSION_GAUGE_NAME, arguments: { title: [attributes?.title ?? ''], description: [attributes?.description ?? ''], From d702bb9419824ea0ab98527824185332d2e344a5 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 7 Dec 2021 13:54:31 +0100 Subject: [PATCH 43/43] breaking functional --- x-pack/test/functional/apps/lens/gauge.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/x-pack/test/functional/apps/lens/gauge.ts b/x-pack/test/functional/apps/lens/gauge.ts index 39ee223ee68e3..fd81bad258280 100644 --- a/x-pack/test/functional/apps/lens/gauge.ts +++ b/x-pack/test/functional/apps/lens/gauge.ts @@ -13,6 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const elasticChart = getService('elasticChart'); const testSubjects = getService('testSubjects'); const find = getService('find'); + const retry = getService('retry'); describe('lens gauge', () => { before(async () => { @@ -50,10 +51,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should reflect edits for gauge', async () => { await PageObjects.lens.openVisualOptions(); - await testSubjects.setValue('lnsToolbarGaugeLabelMajor', 'custom title'); - await testSubjects.setValue('lnsToolbarGaugeLabelMinor-select', 'custom'); - await testSubjects.setValue('lnsToolbarGaugeLabelMinor', 'custom subtitle'); + await retry.try(async () => { + await testSubjects.setValue('lnsToolbarGaugeLabelMajor', 'custom title'); + }); + await retry.try(async () => { + await testSubjects.setValue('lnsToolbarGaugeLabelMinor-select', 'custom'); + }); + await retry.try(async () => { + await testSubjects.setValue('lnsToolbarGaugeLabelMinor', 'custom subtitle'); + }); + await PageObjects.lens.waitForVisualization(); await PageObjects.lens.configureDimension({ dimension: 'lnsGauge_metricDimensionPanel > lns-dimensionTrigger', operation: 'count',