diff --git a/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx b/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx index 679f3a44bb60..20837424dc7b 100644 --- a/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx +++ b/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx @@ -11,7 +11,7 @@ import { EuiIconLegend } from '../assets/legend'; const typeToIconMap: { [type: string]: string | IconType } = { legend: EuiIconLegend as IconType, - values: 'visText', + values: 'number', }; export interface ToolbarPopoverProps { diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap index ca6ca9b2722f..365bf8f4d632 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap @@ -279,6 +279,15 @@ exports[`xy_expression XYChart component it renders bar 1`] = ` }, ] } + displayValueSettings={ + Object { + "hideClippedValue": true, + "isAlternatingValueLabel": false, + "isValueContainedInElement": true, + "showValueLabel": false, + "valueFormatter": [Function], + } + } enableHistogramMode={false} groupId="left" id="d-a" @@ -334,6 +343,15 @@ exports[`xy_expression XYChart component it renders bar 1`] = ` }, ] } + displayValueSettings={ + Object { + "hideClippedValue": true, + "isAlternatingValueLabel": false, + "isValueContainedInElement": true, + "showValueLabel": false, + "valueFormatter": [Function], + } + } enableHistogramMode={false} groupId="left" id="d-b" @@ -457,6 +475,15 @@ exports[`xy_expression XYChart component it renders horizontal bar 1`] = ` }, ] } + displayValueSettings={ + Object { + "hideClippedValue": true, + "isAlternatingValueLabel": false, + "isValueContainedInElement": true, + "showValueLabel": false, + "valueFormatter": [Function], + } + } enableHistogramMode={false} groupId="left" id="d-a" @@ -512,6 +539,15 @@ exports[`xy_expression XYChart component it renders horizontal bar 1`] = ` }, ] } + displayValueSettings={ + Object { + "hideClippedValue": true, + "isAlternatingValueLabel": false, + "isValueContainedInElement": true, + "showValueLabel": false, + "valueFormatter": [Function], + } + } enableHistogramMode={false} groupId="left" id="d-b" @@ -1019,6 +1055,15 @@ exports[`xy_expression XYChart component it renders stacked bar 1`] = ` }, ] } + displayValueSettings={ + Object { + "hideClippedValue": true, + "isAlternatingValueLabel": false, + "isValueContainedInElement": true, + "showValueLabel": false, + "valueFormatter": [Function], + } + } enableHistogramMode={false} groupId="left" id="d-a" @@ -1078,6 +1123,15 @@ exports[`xy_expression XYChart component it renders stacked bar 1`] = ` }, ] } + displayValueSettings={ + Object { + "hideClippedValue": true, + "isAlternatingValueLabel": false, + "isValueContainedInElement": true, + "showValueLabel": false, + "valueFormatter": [Function], + } + } enableHistogramMode={false} groupId="left" id="d-b" @@ -1205,6 +1259,15 @@ exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] = }, ] } + displayValueSettings={ + Object { + "hideClippedValue": true, + "isAlternatingValueLabel": false, + "isValueContainedInElement": true, + "showValueLabel": false, + "valueFormatter": [Function], + } + } enableHistogramMode={false} groupId="left" id="d-a" @@ -1264,6 +1327,15 @@ exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] = }, ] } + displayValueSettings={ + Object { + "hideClippedValue": true, + "isAlternatingValueLabel": false, + "isValueContainedInElement": true, + "showValueLabel": false, + "valueFormatter": [Function], + } + } enableHistogramMode={false} groupId="left" id="d-b" diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap index b35f915336ee..982f513ae101 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap @@ -145,6 +145,9 @@ Object { "title": Array [ "", ], + "valueLabels": Array [ + "hide", + ], "xTitle": Array [ "", ], 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 6c9669dc239e..a5d292fdf265 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx @@ -256,6 +256,7 @@ const createArgsWithLayers = (layers: LayerArgs[] = [sampleLayer]): XYArgs => ({ isVisible: false, position: Position.Top, }, + valueLabels: 'hide', axisTitlesVisibilitySettings: { type: 'lens_xy_axisTitlesVisibilityConfig', x: true, @@ -1867,6 +1868,7 @@ describe('xy_expression', () => { yTitle: '', yRightTitle: '', legend: { type: 'lens_xy_legendConfig', isVisible: false, position: Position.Top }, + valueLabels: 'hide', tickLabelsVisibilitySettings: { type: 'lens_xy_tickLabelsConfig', x: true, @@ -1952,6 +1954,7 @@ describe('xy_expression', () => { yTitle: '', yRightTitle: '', legend: { type: 'lens_xy_legendConfig', isVisible: false, position: Position.Top }, + valueLabels: 'hide', tickLabelsVisibilitySettings: { type: 'lens_xy_tickLabelsConfig', x: true, @@ -2023,6 +2026,7 @@ describe('xy_expression', () => { yTitle: '', yRightTitle: '', legend: { type: 'lens_xy_legendConfig', isVisible: true, position: Position.Top }, + valueLabels: 'hide', tickLabelsVisibilitySettings: { type: 'lens_xy_tickLabelsConfig', x: true, diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 877ddd3c0f27..d238e052a7c7 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -20,6 +20,10 @@ import { GeometryValue, XYChartSeriesIdentifier, StackMode, + RecursivePartial, + Theme, + VerticalAlignment, + HorizontalAlignment, } from '@elastic/charts'; import { I18nProvider } from '@kbn/i18n/react'; import { @@ -131,6 +135,11 @@ export const xyChart: ExpressionFunctionDefinition< defaultMessage: 'Define how missing values are treated', }), }, + valueLabels: { + types: ['string'], + options: ['hide', 'inside'], + help: '', + }, tickLabelsVisibilitySettings: { types: ['lens_xy_tickLabelsConfig'], help: i18n.translate('xpack.lens.xyChart.tickLabelsSettings.help', { @@ -214,6 +223,40 @@ export const getXyChartRenderer = (dependencies: { }, }); +function mergeThemeWithValueLabelsStyling( + theme: RecursivePartial, + valuesLabelMode: string = 'hide', + isHorizontal: boolean +) { + const VALUE_LABELS_MAX_FONTSIZE = 15; + const VALUE_LABELS_MIN_FONTSIZE = 10; + const VALUE_LABELS_VERTICAL_OFFSET = -10; + const VALUE_LABELS_HORIZONTAL_OFFSET = 10; + + if (valuesLabelMode === 'hide') { + return theme; + } + return { + ...theme, + ...{ + barSeriesStyle: { + ...theme.barSeriesStyle, + displayValue: { + fontSize: { min: VALUE_LABELS_MIN_FONTSIZE, max: VALUE_LABELS_MAX_FONTSIZE }, + fill: { textInverted: true, textBorder: 2 }, + alignment: isHorizontal + ? { + vertical: VerticalAlignment.Middle, + } + : { horizontal: HorizontalAlignment.Center }, + offsetX: isHorizontal ? VALUE_LABELS_HORIZONTAL_OFFSET : 0, + offsetY: isHorizontal ? 0 : VALUE_LABELS_VERTICAL_OFFSET, + }, + }, + }, + }; +} + function getIconForSeriesType(seriesType: SeriesType): IconType { return visualizationTypes.find((c) => c.id === seriesType)!.icon || 'empty'; } @@ -254,7 +297,7 @@ export function XYChart({ onClickValue, onSelectRange, }: XYChartRenderProps) { - const { legend, layers, fittingFunction, gridlinesVisibilitySettings } = args; + const { legend, layers, fittingFunction, gridlinesVisibilitySettings, valueLabels } = args; const chartTheme = chartsThemeService.useChartsTheme(); const chartBaseTheme = chartsThemeService.useChartsBaseTheme(); @@ -396,6 +439,16 @@ export function XYChart({ return style; }; + const shouldShowValueLabels = + // No stacked bar charts + filteredLayers.every((layer) => !layer.seriesType.includes('stacked')) && + // No histogram charts + !isHistogramViz; + + const baseThemeWithMaybeValueLabels = !shouldShowValueLabels + ? chartTheme + : mergeThemeWithValueLabelsStyling(chartTheme, valueLabels, shouldRotate); + const colorAssignments = getColorAssignments(args.layers, data, formatFactory); return ( @@ -408,7 +461,7 @@ export function XYChart({ } legendPosition={legend.position} showLegendExtra={false} - theme={chartTheme} + theme={baseThemeWithMaybeValueLabels} baseTheme={chartBaseTheme} tooltip={{ headerFormatter: (d) => safeXAccessorLabelRenderer(d.value), @@ -613,6 +666,10 @@ export function XYChart({ }); } + const yAxis = yAxesConfiguration.find((axisConfiguration) => + axisConfiguration.series.find((currentSeries) => currentSeries.accessor === accessor) + ); + const seriesProps: SeriesSpec = { splitSeriesAccessors: splitAccessor ? [splitAccessor] : [], stackAccessors: seriesType.includes('stacked') ? [xAccessor as string] : [], @@ -649,9 +706,7 @@ export function XYChart({ palette.params ); }, - groupId: yAxesConfiguration.find((axisConfiguration) => - axisConfiguration.series.find((currentSeries) => currentSeries.accessor === accessor) - )?.groupId, + groupId: yAxis?.groupId, enableHistogramMode: isHistogram && (seriesType.includes('stacked') || !splitAccessor) && @@ -723,7 +778,19 @@ export function XYChart({ case 'bar_horizontal': case 'bar_horizontal_stacked': case 'bar_horizontal_percentage_stacked': - return ; + const valueLabelsSettings = { + displayValueSettings: { + // This format double fixes two issues in elastic-chart + // * when rotating the chart, the formatter is not correctly picked + // * in some scenarios value labels are not strings, and this breaks the elastic-chart lib + valueFormatter: (d: unknown) => yAxis?.formatter?.convert(d) || '', + showValueLabel: shouldShowValueLabels && valueLabels !== 'hide', + isAlternatingValueLabel: false, + isValueContainedInElement: true, + hideClippedValue: true, + }, + }; + return ; case 'area_stacked': case 'area_percentage_stacked': return ( diff --git a/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts b/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts index 41d18e5199e4..bf4ffaa36a87 100644 --- a/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts +++ b/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts @@ -5,7 +5,8 @@ */ import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; -import { SeriesType, visualizationTypes, LayerConfig, YConfig } from './types'; +import { FramePublicAPI } from '../types'; +import { SeriesType, visualizationTypes, LayerConfig, YConfig, ValidLayer } from './types'; export function isHorizontalSeries(seriesType: SeriesType) { return ( @@ -37,3 +38,23 @@ export const getSeriesColor = (layer: LayerConfig, accessor: string) => { layer?.yConfig?.find((yConfig: YConfig) => yConfig.forAccessor === accessor)?.color || null ); }; + +export function hasHistogramSeries( + layers: ValidLayer[] = [], + datasourceLayers?: FramePublicAPI['datasourceLayers'] +) { + if (!datasourceLayers) { + return false; + } + const validLayers = layers.filter(({ accessors }) => accessors.length); + + return validLayers.some(({ layerId, xAccessor }: ValidLayer) => { + const xAxisOperation = datasourceLayers[layerId].getOperationForColumnId(xAccessor); + return ( + xAxisOperation && + xAxisOperation.isBucketed && + xAxisOperation.scale && + xAxisOperation.scale !== 'ordinal' + ); + }); +} diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts index 6148824bfec2..05a4b7f460ad 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts @@ -43,6 +43,7 @@ describe('#toExpression', () => { xyVisualization.toExpression( { legend: { position: Position.Bottom, isVisible: true }, + valueLabels: 'hide', preferredSeriesType: 'bar', fittingFunction: 'Carry', tickLabelsVisibilitySettings: { x: false, yLeft: true, yRight: true }, @@ -67,6 +68,7 @@ describe('#toExpression', () => { (xyVisualization.toExpression( { legend: { position: Position.Bottom, isVisible: true }, + valueLabels: 'hide', preferredSeriesType: 'bar', layers: [ { @@ -87,6 +89,7 @@ describe('#toExpression', () => { const expression = xyVisualization.toExpression( { legend: { position: Position.Bottom, isVisible: true }, + valueLabels: 'hide', preferredSeriesType: 'bar', layers: [ { @@ -113,6 +116,7 @@ describe('#toExpression', () => { const expression = xyVisualization.toExpression( { legend: { position: Position.Bottom, isVisible: true }, + valueLabels: 'hide', preferredSeriesType: 'bar', layers: [ { @@ -136,6 +140,7 @@ describe('#toExpression', () => { xyVisualization.toExpression( { legend: { position: Position.Bottom, isVisible: true }, + valueLabels: 'hide', preferredSeriesType: 'bar', layers: [ { @@ -156,6 +161,7 @@ describe('#toExpression', () => { const expression = xyVisualization.toExpression( { legend: { position: Position.Bottom, isVisible: true }, + valueLabels: 'hide', preferredSeriesType: 'bar', layers: [ { @@ -191,6 +197,7 @@ describe('#toExpression', () => { const expression = xyVisualization.toExpression( { legend: { position: Position.Bottom, isVisible: true }, + valueLabels: 'hide', preferredSeriesType: 'bar', layers: [ { @@ -217,6 +224,7 @@ describe('#toExpression', () => { const expression = xyVisualization.toExpression( { legend: { position: Position.Bottom, isVisible: true }, + valueLabels: 'hide', preferredSeriesType: 'bar', layers: [ { @@ -238,4 +246,25 @@ describe('#toExpression', () => { yRight: [true], }); }); + + it('should correctly report the valueLabels visibility settings', () => { + const expression = xyVisualization.toExpression( + { + legend: { position: Position.Bottom, isVisible: true }, + valueLabels: 'inside', + preferredSeriesType: 'bar', + layers: [ + { + layerId: 'first', + seriesType: 'area', + splitAccessor: 'd', + xAccessor: 'a', + accessors: ['b', 'c'], + }, + ], + }, + frame.datasourceLayers + ) as Ast; + expect(expression.chain[0].arguments.valueLabels[0] as Ast).toEqual('inside'); + }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index 904d1541a85e..df773146cde4 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -7,13 +7,9 @@ import { Ast } from '@kbn/interpreter/common'; import { ScaleType } from '@elastic/charts'; import { PaletteRegistry } from 'src/plugins/charts/public'; -import { State, LayerConfig } from './types'; +import { State, ValidLayer, LayerConfig } from './types'; import { OperationMetadata, DatasourcePublicAPI } from '../types'; -interface ValidLayer extends LayerConfig { - xAccessor: NonNullable; -} - export const getSortedAccessors = (datasource: DatasourcePublicAPI, layer: LayerConfig) => { const originalOrder = datasource .getTableSpec() @@ -60,6 +56,7 @@ export function toPreviewExpression( ...state.legend, isVisible: false, }, + valueLabels: 'hide', }, datasourceLayers, paletteService, @@ -197,6 +194,7 @@ export const buildExpression = ( ], }, ], + valueLabels: [state?.valueLabels || 'hide'], layers: validLayers.map((layer) => { const columnToLabel: Record = {}; diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index d1e78aec5799..d21ac675d074 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -364,6 +364,8 @@ export type SeriesType = export type YAxisMode = 'auto' | 'left' | 'right'; +export type ValueLabelConfig = 'hide' | 'inside' | 'outside'; + export interface YConfig { forAccessor: string; axisMode?: YAxisMode; @@ -381,6 +383,10 @@ export interface LayerConfig { palette?: PaletteOutput; } +export interface ValidLayer extends LayerConfig { + xAccessor: NonNullable; +} + export type LayerArgs = LayerConfig & { columnToLabel?: string; // Actually a JSON key-value pair yScaleType: 'time' | 'linear' | 'log' | 'sqrt'; @@ -398,6 +404,7 @@ export interface XYArgs { yTitle: string; yRightTitle: string; legend: LegendConfig & { type: 'lens_xy_legendConfig' }; + valueLabels: ValueLabelConfig; layers: LayerArgs[]; fittingFunction?: FittingFunction; axisTitlesVisibilitySettings?: AxesSettingsConfig & { @@ -411,6 +418,7 @@ export interface XYArgs { export interface XYState { preferredSeriesType: SeriesType; legend: LegendConfig; + valueLabels?: ValueLabelConfig; fittingFunction?: FittingFunction; layers: LayerConfig[]; xTitle?: string; diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index 7c49afa53af3..5127e5c2c259 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -15,6 +15,7 @@ import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks' function exampleState(): State { return { legend: { position: Position.Bottom, isVisible: true }, + valueLabels: 'hide', preferredSeriesType: 'bar', layers: [ { @@ -150,6 +151,7 @@ describe('xy_visualization', () => { }, "preferredSeriesType": "bar_stacked", "title": "Empty XY chart", + "valueLabels": "hide", } `); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index c7f775586ca0..7e155de14a39 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -145,6 +145,7 @@ export const getXyVisualization = ({ state || { title: 'Empty XY chart', legend: { isVisible: true, position: Position.Right }, + valueLabels: 'hide', preferredSeriesType: defaultSeriesType, layers: [ { diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.scss b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.scss index 5b14fca78e65..b9ff6a56d8e3 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.scss +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.scss @@ -1,3 +1,3 @@ .lnsXyToolbar__popover { width: 320px; -} \ No newline at end of file +} diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx index 2114d63fcfac..721bff8684a1 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx @@ -21,6 +21,7 @@ describe('XY Config panels', () => { function testState(): State { return { legend: { isVisible: true, position: Position.Right }, + valueLabels: 'hide', preferredSeriesType: 'bar', layers: [ { @@ -115,8 +116,9 @@ describe('XY Config panels', () => { expect(component.find(EuiSuperSelect).prop('valueOfSelected')).toEqual('Carry'); }); - it('should disable the popover if there is no area or line series', () => { + it('should show currently selected value labels display setting', () => { const state = testState(); + const component = shallow( { ...state, layers: [{ ...state.layers[0], seriesType: 'bar' }], fittingFunction: 'Carry', + valueLabels: 'inside', }} /> ); - expect(component.find(ToolbarPopover).at(0).prop('isDisabled')).toEqual(true); + expect(component.find(EuiButtonGroup).prop('idSelected')).toEqual('value_labels_inside'); + }); + + it('should disable the popover for stacked bar charts', () => { + const state = testState(); + const component = shallow( + + ); + + expect(component.find(ToolbarPopover).prop('isDisabled')).toEqual(true); + }); + + it('should disable the popover for percentage area charts', () => { + const state = testState(); + const component = shallow( + + ); + + expect(component.find(ToolbarPopover).prop('isDisabled')).toEqual(true); + }); + + it('should disabled the popover if there is histogram series', () => { + // make it detect an histogram series + frame.datasourceLayers.first.getOperationForColumnId = jest.fn().mockReturnValueOnce({ + isBucketed: true, + scale: 'interval', + }); + const state = testState(); + const component = shallow( + + ); + + expect(component.find(ToolbarPopover).prop('isDisabled')).toEqual(true); + }); + + it('should show the popover and display field enabled for bar and horizontal_bar series', () => { + const state = testState(); + + const component = shallow( + + ); + + expect(component.exists('[data-test-subj="lnsValueLabelsDisplay"]')).toEqual(true); + }); + + it('should hide the fitting option for bar series', () => { + const state = testState(); + const component = shallow( + + ); + + expect(component.exists('[data-test-subj="lnsMissingValuesSelect"]')).toEqual(false); + }); + + it('should hide in the popover the display option for area and line series', () => { + const state = testState(); + const component = shallow( + + ); + + expect(component.exists('[data-test-subj="lnsValueLabelsDisplay"]')).toEqual(false); + }); + + it('should keep the display option for bar series with multiple layers', () => { + frame.datasourceLayers = { + ...frame.datasourceLayers, + second: createMockDatasource('test').publicAPIMock, + }; + + const state = testState(); + const component = shallow( + + ); + + expect(component.exists('[data-test-subj="lnsValueLabelsDisplay"]')).toEqual(true); }); it('should disable the popover if there is no right axis', () => { diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx index 97e42113fc18..a22530c5743b 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -27,8 +27,20 @@ import { VisualizationToolbarProps, VisualizationDimensionEditorProps, } from '../types'; -import { State, SeriesType, visualizationTypes, YAxisMode, AxesSettingsConfig } from './types'; -import { isHorizontalChart, isHorizontalSeries, getSeriesColor } from './state_helpers'; +import { + State, + SeriesType, + visualizationTypes, + YAxisMode, + AxesSettingsConfig, + ValidLayer, +} from './types'; +import { + isHorizontalChart, + isHorizontalSeries, + getSeriesColor, + hasHistogramSeries, +} from './state_helpers'; import { trackUiEvent } from '../lens_ui_telemetry'; import { fittingFunctionDefinitions } from './fitting_functions'; import { ToolbarPopover, LegendSettingsPopover } from '../shared_components'; @@ -74,6 +86,27 @@ const legendOptions: Array<{ id: string; value: 'auto' | 'show' | 'hide'; label: }, ]; +const valueLabelsOptions: Array<{ + id: string; + value: 'hide' | 'inside' | 'outside'; + label: string; +}> = [ + { + id: `value_labels_hide`, + value: 'hide', + label: i18n.translate('xpack.lens.xyChart.valueLabelsVisibility.auto', { + defaultMessage: 'Hide', + }), + }, + { + id: `value_labels_inside`, + value: 'inside', + label: i18n.translate('xpack.lens.xyChart.valueLabelsVisibility.inside', { + defaultMessage: 'Show', + }), + }, +]; + export function LayerContextMenu(props: VisualizationLayerWidgetProps) { const { state, layerId } = props; const horizontalOnly = isHorizontalChart(state.layers); @@ -118,12 +151,24 @@ export function LayerContextMenu(props: VisualizationLayerWidgetProps) { } export function XyToolbar(props: VisualizationToolbarProps) { - const { state, setState } = props; + const { state, setState, frame } = props; const hasNonBarSeries = state?.layers.some(({ seriesType }) => ['area_stacked', 'area', 'line'].includes(seriesType) ); + const hasBarNotStacked = state?.layers.some(({ seriesType }) => + ['bar', 'bar_horizontal'].includes(seriesType) + ); + + const isAreaPercentage = state?.layers.some( + ({ seriesType }) => seriesType === 'area_percentage_stacked' + ); + + const isHistogramSeries = Boolean( + hasHistogramSeries(state?.layers as ValidLayer[], frame.datasourceLayers) + ); + const shouldRotate = state?.layers.length ? isHorizontalChart(state.layers) : false; const axisGroups = getAxesConfiguration(state?.layers, shouldRotate); @@ -191,54 +236,99 @@ export function XyToolbar(props: VisualizationToolbarProps) { : !state?.legend.isVisible ? 'hide' : 'show'; + + const valueLabelsVisibilityMode = state?.valueLabels || 'hide'; + + const isValueLabelsEnabled = !hasNonBarSeries && hasBarNotStacked && !isHistogramSeries; + const isFittingEnabled = hasNonBarSeries; + return ( - - { - return { - value: id, - dropdownDisplay: ( - <> - {title} - -

{description}

-
- - ), - inputDisplay: title, - }; + {isValueLabelsEnabled ? ( + + {i18n.translate('xpack.lens.shared.chartValueLabelVisibilityLabel', { + defaultMessage: 'Labels', + })} + + } + > + value === valueLabelsVisibilityMode)! + .id + } + onChange={(modeId) => { + const newMode = valueLabelsOptions.find(({ id }) => id === modeId)!.value; + setState({ ...state, valueLabels: newMode }); + }} + /> + + ) : null} + {isFittingEnabled ? ( + setState({ ...state, fittingFunction: value })} - itemLayoutAlign="top" - hasDividers - /> - + > + { + return { + value: id, + dropdownDisplay: ( + <> + {title} + +

{description}

+
+ + ), + inputDisplay: title, + }; + })} + valueOfSelected={state?.fittingFunction || 'None'} + onChange={(value) => setState({ ...state, fittingFunction: value })} + itemLayoutAlign="top" + hasDividers + /> +
+ ) : null}
{ keptLayerIds: [], state: { legend: { isVisible: true, position: 'bottom' }, + valueLabels: 'hide', preferredSeriesType: 'bar', layers: [ { @@ -249,6 +250,7 @@ describe('xy_suggestions', () => { keptLayerIds: ['first'], state: { legend: { isVisible: true, position: 'bottom' }, + valueLabels: 'hide', preferredSeriesType: 'bar', layers: [ { @@ -289,6 +291,7 @@ describe('xy_suggestions', () => { keptLayerIds: ['first', 'second'], state: { legend: { isVisible: true, position: 'bottom' }, + valueLabels: 'hide', preferredSeriesType: 'bar', layers: [ { @@ -523,6 +526,7 @@ describe('xy_suggestions', () => { }, state: { legend: { isVisible: true, position: 'bottom' }, + valueLabels: 'hide', preferredSeriesType: 'bar', layers: [ { @@ -575,6 +579,7 @@ describe('xy_suggestions', () => { test('keeps existing seriesType for initial tables', () => { const currentState: XYState = { legend: { isVisible: true, position: 'bottom' }, + valueLabels: 'hide', fittingFunction: 'None', preferredSeriesType: 'line', layers: [ @@ -608,6 +613,7 @@ describe('xy_suggestions', () => { test('makes a visible seriesType suggestion for unchanged table without split', () => { const currentState: XYState = { legend: { isVisible: true, position: 'bottom' }, + valueLabels: 'hide', fittingFunction: 'None', axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, @@ -648,6 +654,7 @@ describe('xy_suggestions', () => { test('suggests seriesType and stacking when there is a split', () => { const currentState: XYState = { legend: { isVisible: true, position: 'bottom' }, + valueLabels: 'hide', preferredSeriesType: 'bar', fittingFunction: 'None', axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, @@ -693,6 +700,7 @@ describe('xy_suggestions', () => { (generateId as jest.Mock).mockReturnValueOnce('dummyCol'); const currentState: XYState = { legend: { isVisible: true, position: 'bottom' }, + valueLabels: 'hide', fittingFunction: 'None', preferredSeriesType: 'bar', layers: [ @@ -725,6 +733,7 @@ describe('xy_suggestions', () => { test('suggests stacking for unchanged table that has a split', () => { const currentState: XYState = { legend: { isVisible: true, position: 'bottom' }, + valueLabels: 'hide', preferredSeriesType: 'bar', fittingFunction: 'None', layers: [ @@ -760,6 +769,7 @@ describe('xy_suggestions', () => { test('keeps column to dimension mappings on extended tables', () => { const currentState: XYState = { legend: { isVisible: true, position: 'bottom' }, + valueLabels: 'hide', preferredSeriesType: 'bar', fittingFunction: 'None', axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, @@ -802,6 +812,7 @@ describe('xy_suggestions', () => { test('changes column mappings when suggestion is reorder', () => { const currentState: XYState = { legend: { isVisible: true, position: 'bottom' }, + valueLabels: 'hide', preferredSeriesType: 'bar', fittingFunction: 'None', axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, @@ -845,6 +856,7 @@ describe('xy_suggestions', () => { (generateId as jest.Mock).mockReturnValueOnce('dummyCol'); const currentState: XYState = { legend: { isVisible: true, position: 'bottom' }, + valueLabels: 'hide', preferredSeriesType: 'bar', fittingFunction: 'None', axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, 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 edb7c4ed5224..7bbb03957730 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -509,6 +509,7 @@ function buildSuggestion({ const state: State = { legend: currentState ? currentState.legend : { isVisible: true, position: Position.Right }, + valueLabels: currentState?.valueLabels || 'hide', fittingFunction: currentState?.fittingFunction || 'None', xTitle: currentState?.xTitle, yTitle: currentState?.yTitle, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6548fb9752d4..b70420e4338a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10937,7 +10937,6 @@ "xpack.lens.xyChart.chartTypeLabel": "チャートタイプ", "xpack.lens.xyChart.chartTypeLegend": "チャートタイプ", "xpack.lens.xyChart.emptyXLabel": "(空)", - "xpack.lens.xyChart.fittingDisabledHelpText": "この設定は折れ線グラフとエリアグラフでのみ適用されます。", "xpack.lens.xyChart.fittingFunction.help": "欠測値の処理方法を定義", "xpack.lens.xyChart.Gridlines": "グリッド線", "xpack.lens.xyChart.gridlinesSettings.help": "xおよびy軸のグリッド線を表示", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 1cb1496cd9a0..14b29468d843 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10950,7 +10950,6 @@ "xpack.lens.xyChart.chartTypeLabel": "图表类型", "xpack.lens.xyChart.chartTypeLegend": "图表类型", "xpack.lens.xyChart.emptyXLabel": "(空)", - "xpack.lens.xyChart.fittingDisabledHelpText": "此设置仅适用于折线图和面积图。", "xpack.lens.xyChart.fittingFunction.help": "定义处理缺失值的方式", "xpack.lens.xyChart.Gridlines": "网格线", "xpack.lens.xyChart.gridlinesSettings.help": "显示 x 和 y 轴网格线", diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index ffb74837e9fd..3eed3fc0f26a 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -222,8 +222,8 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }, async editMissingValues(option: string) { await retry.try(async () => { - await testSubjects.click('lnsMissingValuesButton'); - await testSubjects.exists('lnsMissingValuesSelect'); + await testSubjects.click('lnsValuesButton'); + await testSubjects.exists('lnsValuesButton'); }); await testSubjects.click('lnsMissingValuesSelect'); const optionSelector = await find.byCssSelector(`#${option}`);