diff --git a/x-pack/plugins/lens/public/assets/axis_bottom.tsx b/x-pack/plugins/lens/public/assets/axis_bottom.tsx new file mode 100644 index 0000000000000..9529a93e4c1cc --- /dev/null +++ b/x-pack/plugins/lens/public/assets/axis_bottom.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; + +export const EuiIconAxisBottom = ({ + title, + titleId, + ...props +}: { + title: string; + titleId: string; +}) => ( + + {title ? {title} : null} + + + +); diff --git a/x-pack/plugins/lens/public/assets/axis_left.tsx b/x-pack/plugins/lens/public/assets/axis_left.tsx new file mode 100644 index 0000000000000..d1ec0b76a1bd5 --- /dev/null +++ b/x-pack/plugins/lens/public/assets/axis_left.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; + +export const EuiIconAxisLeft = ({ + title, + titleId, + ...props +}: { + title: string; + titleId: string; +}) => ( + + {title ? {title} : null} + + + + +); diff --git a/x-pack/plugins/lens/public/assets/axis_right.tsx b/x-pack/plugins/lens/public/assets/axis_right.tsx new file mode 100644 index 0000000000000..e61f87b963a05 --- /dev/null +++ b/x-pack/plugins/lens/public/assets/axis_right.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; + +export const EuiIconAxisRight = ({ + title, + titleId, + ...props +}: { + title: string; + titleId: string; +}) => ( + + {title ? {title} : null} + + + + +); diff --git a/x-pack/plugins/lens/public/assets/axis_top.tsx b/x-pack/plugins/lens/public/assets/axis_top.tsx new file mode 100644 index 0000000000000..90fbdc4a21552 --- /dev/null +++ b/x-pack/plugins/lens/public/assets/axis_top.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; + +export const EuiIconAxisTop = ({ + title, + titleId, + ...props +}: { + title: string; + titleId: string; +}) => ( + + {title ? {title} : null} + + + + + + + +); diff --git a/x-pack/plugins/lens/public/assets/legend.tsx b/x-pack/plugins/lens/public/assets/legend.tsx new file mode 100644 index 0000000000000..d73e68839d9fb --- /dev/null +++ b/x-pack/plugins/lens/public/assets/legend.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; + +export const EuiIconLegend = ({ title, titleId, ...props }: { title: string; titleId: string }) => ( + + {title ? {title} : null} + + + + + + + +); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss index 90cc049db96eb..a4d8288d5e600 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss @@ -38,5 +38,5 @@ } .lnsWorkspacePanelWrapper__toolbar { - margin-bottom: $euiSizeS; + margin-bottom: 0; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.tsx index 682316a586626..abbd7e0838bed 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.tsx @@ -9,7 +9,7 @@ import { EuiPopover, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { NativeRenderer } from '../../../native_renderer'; import { Visualization, VisualizationLayerWidgetProps } from '../../../types'; -import { ToolbarButton } from '../../../toolbar_button'; +import { ToolbarButton } from '../../../shared_components'; export function LayerSettings({ layerId, 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 82983862e7c03..f4526cac39c8a 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 @@ -19,7 +19,7 @@ import { Visualization, FramePublicAPI, Datasource } from '../../../types'; import { Action } from '../state_management'; import { getSuggestions, switchToSuggestion, Suggestion } from '../suggestion_helpers'; import { trackUiEvent } from '../../../lens_ui_telemetry'; -import { ToolbarButton } from '../../../toolbar_button'; +import { ToolbarButton } from '../../../shared_components'; interface VisualizationSelection { visualizationId: string; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx index 901a86bb56e1d..8e7d504ff7677 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx @@ -67,8 +67,14 @@ export function WorkspacePanelWrapper({ ); return ( <> -
- +
+ {activeVisualization && activeVisualization.renderToolbar && ( - + = [ { @@ -99,179 +93,128 @@ const legendOptions: Array<{ id: 'pieLegendDisplay-default', value: 'default', label: i18n.translate('xpack.lens.pieChart.legendVisibility.auto', { - defaultMessage: 'auto', + defaultMessage: 'Auto', }), }, { id: 'pieLegendDisplay-show', value: 'show', label: i18n.translate('xpack.lens.pieChart.legendVisibility.show', { - defaultMessage: 'show', + defaultMessage: 'Show', }), }, { id: 'pieLegendDisplay-hide', value: 'hide', label: i18n.translate('xpack.lens.pieChart.legendVisibility.hide', { - defaultMessage: 'hide', + defaultMessage: 'Hide', }), }, ]; export function PieToolbar(props: VisualizationToolbarProps) { - const [open, setOpen] = useState(false); const { state, setState } = props; const layer = state.layers[0]; if (!layer) { return null; } return ( - - - { - setOpen(!open); - }} - > - {i18n.translate('xpack.lens.pieChart.settingsLabel', { defaultMessage: 'Settings' })} - - } - isOpen={open} - closePopover={() => { - setOpen(false); - }} - anchorPosition="downRight" + + + - - { - setState({ - ...state, - layers: [{ ...layer, categoryDisplay: option }], - }); - }} - /> - - - { - setState({ - ...state, - layers: [{ ...layer, numberDisplay: option }], - }); - }} - /> - - - - - setState({ - ...state, - layers: [{ ...layer, percentDecimals: value }], - }) - } - /> - - - -
- value === layer.legendDisplay)!.id} - onChange={(optionId) => { - setState({ - ...state, - layers: [ - { - ...layer, - legendDisplay: legendOptions.find(({ id }) => id === optionId)!.value, - }, - ], - }); - }} - buttonSize="compressed" - isFullWidth - /> - - - { - setState({ ...state, layers: [{ ...layer, nestedLegend: !layer.nestedLegend }] }); - }} - /> -
-
- - { - setState({ - ...state, - layers: [{ ...layer, legendPosition: e.target.value as Position }], - }); - }} - /> - -
-
+ { + setState({ + ...state, + layers: [{ ...layer, categoryDisplay: option }], + }); + }} + /> + + + { + setState({ + ...state, + layers: [{ ...layer, numberDisplay: option }], + }); + }} + /> + + + + + setState({ + ...state, + layers: [{ ...layer, percentDecimals: value }], + }) + } + /> + + + { + setState({ + ...state, + layers: [ + { + ...layer, + legendDisplay: legendOptions.find(({ id }) => id === optionId)!.value, + }, + ], + }); + }} + position={layer.legendPosition} + onPositionChange={(id) => { + setState({ + ...state, + layers: [{ ...layer, legendPosition: id as Position }], + }); + }} + renderNestedLegendSwitch + nestedLegend={!!layer.nestedLegend} + onNestedLegendChange={() => { + setState({ + ...state, + layers: [{ ...layer, nestedLegend: !layer.nestedLegend }], + }); + }} + />
); } diff --git a/x-pack/plugins/lens/public/shared_components/index.ts b/x-pack/plugins/lens/public/shared_components/index.ts index ad662fd7a59d9..c0362a5660adb 100644 --- a/x-pack/plugins/lens/public/shared_components/index.ts +++ b/x-pack/plugins/lens/public/shared_components/index.ts @@ -5,3 +5,6 @@ */ export * from './empty_placeholder'; +export { ToolbarPopoverProps, ToolbarPopover } from './toolbar_popover'; +export { ToolbarButtonProps, ToolbarButton } from './toolbar_button'; +export { LegendSettingsPopover } from './legend_settings_popover'; diff --git a/x-pack/plugins/lens/public/shared_components/legend_settings_popover.test.tsx b/x-pack/plugins/lens/public/shared_components/legend_settings_popover.test.tsx new file mode 100644 index 0000000000000..1e0e6b33b6cd4 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/legend_settings_popover.test.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Position } from '@elastic/charts'; +import { shallowWithIntl as shallow } from 'test_utils/enzyme_helpers'; +import { LegendSettingsPopover, LegendSettingsPopoverProps } from './legend_settings_popover'; + +describe('Legend Settings', () => { + const legendOptions: Array<{ id: string; value: 'auto' | 'show' | 'hide'; label: string }> = [ + { + id: `test_legend_auto`, + value: 'auto', + label: 'Auto', + }, + { + id: `test_legend_show`, + value: 'show', + label: 'Show', + }, + { + id: `test_legend_hide`, + value: 'hide', + label: 'Hide', + }, + ]; + let props: LegendSettingsPopoverProps; + beforeEach(() => { + props = { + legendOptions, + mode: 'auto', + onDisplayChange: jest.fn(), + onPositionChange: jest.fn(), + }; + }); + + it('should have selected the given mode as Display value', () => { + const component = shallow(); + expect(component.find('[data-test-subj="lens-legend-display-btn"]').prop('idSelected')).toEqual( + 'test_legend_auto' + ); + }); + + it('should have called the onDisplayChange function on ButtonGroup change', () => { + const component = shallow(); + component.find('[data-test-subj="lens-legend-display-btn"]').simulate('change'); + expect(props.onDisplayChange).toHaveBeenCalled(); + }); + + it('should have default the Position to right when no position is given', () => { + const component = shallow(); + expect( + component.find('[data-test-subj="lens-legend-position-btn"]').prop('idSelected') + ).toEqual(Position.Right); + }); + + it('should have called the onPositionChange function on ButtonGroup change', () => { + const component = shallow(); + component.find('[data-test-subj="lens-legend-position-btn"]').simulate('change'); + expect(props.onPositionChange).toHaveBeenCalled(); + }); + + it('should disable the position button group on hide mode', () => { + const component = shallow(); + expect( + component.find('[data-test-subj="lens-legend-position-btn"]').prop('isDisabled') + ).toEqual(true); + }); + + it('should enable the Nested Legend Switch when renderNestedLegendSwitch prop is true', () => { + const component = shallow(); + expect(component.find('[data-test-subj="lens-legend-nested-switch"]')).toHaveLength(1); + }); + + it('should set the switch state on nestedLegend prop value', () => { + const component = shallow( + + ); + expect(component.find('[data-test-subj="lens-legend-nested-switch"]').prop('checked')).toEqual( + true + ); + }); + + it('should have called the onNestedLegendChange function on switch change', () => { + const nestedProps = { + ...props, + renderNestedLegendSwitch: true, + onNestedLegendChange: jest.fn(), + }; + const component = shallow(); + component.find('[data-test-subj="lens-legend-nested-switch"]').simulate('change'); + expect(nestedProps.onNestedLegendChange).toHaveBeenCalled(); + }); + + it('should disable switch group on hide mode', () => { + const component = shallow( + + ); + expect(component.find('[data-test-subj="lens-legend-nested-switch"]').prop('disabled')).toEqual( + true + ); + }); +}); diff --git a/x-pack/plugins/lens/public/shared_components/legend_settings_popover.tsx b/x-pack/plugins/lens/public/shared_components/legend_settings_popover.tsx new file mode 100644 index 0000000000000..b3df4814b85f8 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/legend_settings_popover.tsx @@ -0,0 +1,158 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFormRow, EuiButtonGroup, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; +import { Position } from '@elastic/charts'; +import { ToolbarPopover } from '../shared_components'; + +export interface LegendSettingsPopoverProps { + /** + * Determines the legend display options + */ + legendOptions: Array<{ id: string; value: 'auto' | 'show' | 'hide' | 'default'; label: string }>; + /** + * Determines the legend mode + */ + mode: 'default' | 'show' | 'hide' | 'auto'; + /** + * Callback on display option change + */ + onDisplayChange: (id: string) => void; + /** + * Sets the legend position + */ + position?: Position; + /** + * Callback on position option change + */ + onPositionChange: (id: string) => void; + /** + * If true, nested legend switch is rendered + */ + renderNestedLegendSwitch?: boolean; + /** + * nested legend switch status + */ + nestedLegend?: boolean; + /** + * Callback on nested switch status change + */ + onNestedLegendChange?: (event: EuiSwitchEvent) => void; +} + +const toggleButtonsIcons = [ + { + id: Position.Bottom, + label: i18n.translate('xpack.lens.shared.legendPositionBottom', { + defaultMessage: 'Bottom', + }), + iconType: 'arrowDown', + }, + { + id: Position.Left, + label: i18n.translate('xpack.lens.shared.legendPositionLeft', { + defaultMessage: 'Left', + }), + iconType: 'arrowLeft', + }, + { + id: Position.Right, + label: i18n.translate('xpack.lens.shared.legendPositionRight', { + defaultMessage: 'Right', + }), + iconType: 'arrowRight', + }, + { + id: Position.Top, + label: i18n.translate('xpack.lens.shared.legendPositionTop', { + defaultMessage: 'Top', + }), + iconType: 'arrowUp', + }, +]; + +export const LegendSettingsPopover: React.FunctionComponent = ({ + legendOptions, + mode, + onDisplayChange, + position, + onPositionChange, + renderNestedLegendSwitch, + nestedLegend, + onNestedLegendChange = () => {}, +}) => { + return ( + + + value === mode)!.id} + onChange={onDisplayChange} + /> + + + + + {renderNestedLegendSwitch && ( + + + + )} + + ); +}; diff --git a/x-pack/plugins/lens/public/shared_components/toolbar_button.scss b/x-pack/plugins/lens/public/shared_components/toolbar_button.scss new file mode 100644 index 0000000000000..61b02f47678c3 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/toolbar_button.scss @@ -0,0 +1,60 @@ +.lnsToolbarButton { + line-height: $euiButtonHeight; // Keeps alignment of text and chart icon + background-color: $euiColorEmptyShade; + + // Some toolbar buttons are just icons, but EuiButton comes with margin and min-width that need to be removed + min-width: 0; + + &[class*='--text'] { + // Lighten the border color for all states + border-color: $euiBorderColor !important; // sass-lint:disable-line no-important + } + + &[class*='isDisabled'] { + // There is a popover `pointer-events: none` that messes with the not-allowed cursor + pointer-events: initial; + } + + .lnsToolbarButton__text > svg { + margin-top: -1px; // Just some weird alignment issue when icon is the child not the `iconType` + } + + .lnsToolbarButton__text:empty { + margin: 0; + } + + // Toolbar buttons don't look good with centered text when fullWidth + &[class*='fullWidth'] { + text-align: left; + + .lnsToolbarButton__content { + justify-content: space-between; + } + } + +} + +.lnsToolbarButton--groupLeft { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.lnsToolbarButton--groupCenter { + border-radius: 0; + border-left: none; +} + +.lnsToolbarButton--groupRight { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-left: none; +} + +.lnsToolbarButton--bold { + font-weight: $euiFontWeightBold; +} + +.lnsToolbarButton--s { + box-shadow: none !important; // sass-lint:disable-line no-important + font-size: $euiFontSizeS; +} diff --git a/x-pack/plugins/lens/public/toolbar_button/toolbar_button.tsx b/x-pack/plugins/lens/public/shared_components/toolbar_button.tsx similarity index 71% rename from x-pack/plugins/lens/public/toolbar_button/toolbar_button.tsx rename to x-pack/plugins/lens/public/shared_components/toolbar_button.tsx index 0a63781818171..56647352750a1 100644 --- a/x-pack/plugins/lens/public/toolbar_button/toolbar_button.tsx +++ b/x-pack/plugins/lens/public/shared_components/toolbar_button.tsx @@ -9,6 +9,13 @@ import React from 'react'; import classNames from 'classnames'; import { EuiButton, PropsOf, EuiButtonProps } from '@elastic/eui'; +const groupPositionToClassMap = { + none: null, + left: 'lnsToolbarButton--groupLeft', + center: 'lnsToolbarButton--groupCenter', + right: 'lnsToolbarButton--groupRight', +}; + export type ToolbarButtonProps = PropsOf & { /** * Determines prominence @@ -18,6 +25,14 @@ export type ToolbarButtonProps = PropsOf & { * Smaller buttons also remove extra shadow for less prominence */ size?: EuiButtonProps['size']; + /** + * Determines if the button will have a down arrow or not + */ + hasArrow?: boolean; + /** + * Adjusts the borders for groupings + */ + groupPosition?: 'none' | 'left' | 'center' | 'right'; }; export const ToolbarButton: React.FunctionComponent = ({ @@ -25,10 +40,13 @@ export const ToolbarButton: React.FunctionComponent = ({ className, fontWeight = 'normal', size = 'm', + hasArrow = true, + groupPosition = 'none', ...rest }) => { const classes = classNames( 'lnsToolbarButton', + groupPositionToClassMap[groupPosition], [`lnsToolbarButton--${fontWeight}`, `lnsToolbarButton--${size}`], className ); @@ -36,7 +54,7 @@ export const ToolbarButton: React.FunctionComponent = ({ = ({ + children, + title, + type, + isDisabled = false, + groupPosition, +}) => { + const [open, setOpen] = useState(false); + + const iconType: string | IconType = typeof type === 'string' ? typeToIconMap[type] : type; + + return ( + + { + setOpen(!open); + }} + hasArrow={false} + isDisabled={isDisabled} + groupPosition={groupPosition} + > + + + } + isOpen={open} + closePopover={() => { + setOpen(false); + }} + anchorPosition="downRight" + > + {title} + {children} + + + ); +}; diff --git a/x-pack/plugins/lens/public/toolbar_button/index.tsx b/x-pack/plugins/lens/public/toolbar_button/index.tsx deleted file mode 100644 index ee6489726a0a7..0000000000000 --- a/x-pack/plugins/lens/public/toolbar_button/index.tsx +++ /dev/null @@ -1,7 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -export { ToolbarButtonProps, ToolbarButton } from './toolbar_button'; diff --git a/x-pack/plugins/lens/public/toolbar_button/toolbar_button.scss b/x-pack/plugins/lens/public/toolbar_button/toolbar_button.scss deleted file mode 100644 index f36fdfdf02aba..0000000000000 --- a/x-pack/plugins/lens/public/toolbar_button/toolbar_button.scss +++ /dev/null @@ -1,30 +0,0 @@ -.lnsToolbarButton { - line-height: $euiButtonHeight; // Keeps alignment of text and chart icon - background-color: $euiColorEmptyShade; - border-color: $euiBorderColor; - - // Some toolbar buttons are just icons, but EuiButton comes with margin and min-width that need to be removed - min-width: 0; - - .lnsToolbarButton__text:empty { - margin: 0; - } - - // Toolbar buttons don't look good with centered text when fullWidth - &[class*='fullWidth'] { - text-align: left; - - .lnsToolbarButton__content { - justify-content: space-between; - } - } -} - -.lnsToolbarButton--bold { - font-weight: $euiFontWeightBold; -} - -.lnsToolbarButton--s { - box-shadow: none !important; // sass-lint:disable-line no-important - font-size: $euiFontSizeS; -} 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 19ea75239ddb2..dd8c6377cacdc 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 @@ -5,6 +5,28 @@ Object { "chain": Array [ Object { "arguments": Object { + "axisTitlesVisibilitySettings": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "x": Array [ + true, + ], + "yLeft": Array [ + true, + ], + "yRight": Array [ + true, + ], + }, + "function": "lens_xy_axisTitlesVisibilityConfig", + "type": "function", + }, + ], + "type": "expression", + }, + ], "fittingFunction": Array [ "Carry", ], @@ -16,7 +38,10 @@ Object { "x": Array [ false, ], - "y": Array [ + "yLeft": Array [ + true, + ], + "yRight": Array [ true, ], }, @@ -92,12 +117,6 @@ Object { "type": "expression", }, ], - "showXAxisTitle": Array [ - true, - ], - "showYAxisTitle": Array [ - true, - ], "tickLabelsVisibilitySettings": Array [ Object { "chain": Array [ @@ -106,7 +125,10 @@ Object { "x": Array [ false, ], - "y": Array [ + "yLeft": Array [ + true, + ], + "yRight": Array [ true, ], }, @@ -120,6 +142,9 @@ Object { "xTitle": Array [ "", ], + "yRightTitle": Array [ + "", + ], "yTitle": Array [ "", ], diff --git a/x-pack/plugins/lens/public/xy_visualization/axes_configuration.test.ts b/x-pack/plugins/lens/public/xy_visualization/axes_configuration.test.ts index 7b0edf2b367be..15c08d17e49c6 100644 --- a/x-pack/plugins/lens/public/xy_visualization/axes_configuration.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/axes_configuration.test.ts @@ -203,7 +203,7 @@ describe('axes_configuration', () => { it('should map auto series to left axis', () => { const formatFactory = jest.fn(); - const groups = getAxesConfiguration([sampleLayer], tables, formatFactory, false); + const groups = getAxesConfiguration([sampleLayer], false, tables, formatFactory); expect(groups.length).toEqual(1); expect(groups[0].position).toEqual('left'); expect(groups[0].series[0].accessor).toEqual('yAccessorId'); @@ -213,7 +213,7 @@ describe('axes_configuration', () => { it('should map auto series to right axis if formatters do not match', () => { const formatFactory = jest.fn(); const twoSeriesLayer = { ...sampleLayer, accessors: ['yAccessorId', 'yAccessorId2'] }; - const groups = getAxesConfiguration([twoSeriesLayer], tables, formatFactory, false); + const groups = getAxesConfiguration([twoSeriesLayer], false, tables, formatFactory); expect(groups.length).toEqual(2); expect(groups[0].position).toEqual('left'); expect(groups[1].position).toEqual('right'); @@ -227,7 +227,7 @@ describe('axes_configuration', () => { ...sampleLayer, accessors: ['yAccessorId', 'yAccessorId2', 'yAccessorId3'], }; - const groups = getAxesConfiguration([threeSeriesLayer], tables, formatFactory, false); + const groups = getAxesConfiguration([threeSeriesLayer], false, tables, formatFactory); expect(groups.length).toEqual(2); expect(groups[0].position).toEqual('left'); expect(groups[1].position).toEqual('right'); @@ -240,9 +240,9 @@ describe('axes_configuration', () => { const formatFactory = jest.fn(); const groups = getAxesConfiguration( [{ ...sampleLayer, yConfig: [{ forAccessor: 'yAccessorId', axisMode: 'right' }] }], + false, tables, - formatFactory, - false + formatFactory ); expect(groups.length).toEqual(1); expect(groups[0].position).toEqual('right'); @@ -260,9 +260,9 @@ describe('axes_configuration', () => { yConfig: [{ forAccessor: 'yAccessorId', axisMode: 'right' }], }, ], + false, tables, - formatFactory, - false + formatFactory ); expect(groups.length).toEqual(2); expect(groups[0].position).toEqual('left'); @@ -284,9 +284,9 @@ describe('axes_configuration', () => { yConfig: [{ forAccessor: 'yAccessorId', axisMode: 'right' }], }, ], + false, tables, - formatFactory, - false + formatFactory ); expect(formatFactory).toHaveBeenCalledTimes(2); expect(formatFactory).toHaveBeenCalledWith({ id: 'number' }); diff --git a/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts b/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts index 006995d92a926..876baaabb57c5 100644 --- a/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts +++ b/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts @@ -20,7 +20,7 @@ interface FormattedMetric { type GroupsConfiguration = Array<{ groupId: string; position: 'left' | 'right' | 'bottom' | 'top'; - formatter: IFieldFormat; + formatter?: IFieldFormat; series: Array<{ layer: string; accessor: string }>; }>; @@ -33,9 +33,9 @@ export function isFormatterCompatible( export function getAxesConfiguration( layers: LayerConfig[], - tables: Record, - formatFactory: (mapping: SerializedFieldFormat) => IFieldFormat, - shouldRotate: boolean + shouldRotate: boolean, + tables?: Record, + formatFactory?: (mapping: SerializedFieldFormat) => IFieldFormat ): GroupsConfiguration { const series: { auto: FormattedMetric[]; left: FormattedMetric[]; right: FormattedMetric[] } = { auto: [], @@ -43,13 +43,13 @@ export function getAxesConfiguration( right: [], }; - layers.forEach((layer) => { - const table = tables[layer.layerId]; + layers?.forEach((layer) => { + const table = tables?.[layer.layerId]; layer.accessors.forEach((accessor) => { const mode = layer.yConfig?.find((yAxisConfig) => yAxisConfig.forAccessor === accessor)?.axisMode || 'auto'; - let formatter: SerializedFieldFormat = table.columns.find((column) => column.id === accessor) + let formatter: SerializedFieldFormat = table?.columns.find((column) => column.id === accessor) ?.formatHint || { id: 'number' }; if (layer.seriesType.includes('percentage') && formatter.id !== 'percent') { formatter = { @@ -70,16 +70,18 @@ export function getAxesConfiguration( series.auto.forEach((currentSeries) => { if ( series.left.length === 0 || - series.left.every((leftSeries) => - isFormatterCompatible(leftSeries.fieldFormat, currentSeries.fieldFormat) - ) + (tables && + series.left.every((leftSeries) => + isFormatterCompatible(leftSeries.fieldFormat, currentSeries.fieldFormat) + )) ) { series.left.push(currentSeries); } else if ( series.right.length === 0 || - series.right.every((rightSeries) => - isFormatterCompatible(rightSeries.fieldFormat, currentSeries.fieldFormat) - ) + (tables && + series.left.every((leftSeries) => + isFormatterCompatible(leftSeries.fieldFormat, currentSeries.fieldFormat) + )) ) { series.right.push(currentSeries); } else if (series.right.length >= series.left.length) { @@ -95,7 +97,7 @@ export function getAxesConfiguration( axisGroups.push({ groupId: 'left', position: shouldRotate ? 'bottom' : 'left', - formatter: formatFactory(series.left[0].fieldFormat), + formatter: formatFactory?.(series.left[0].fieldFormat), series: series.left.map(({ fieldFormat, ...currentSeries }) => currentSeries), }); } @@ -104,7 +106,7 @@ export function getAxesConfiguration( axisGroups.push({ groupId: 'right', position: shouldRotate ? 'top' : 'right', - formatter: formatFactory(series.right[0].fieldFormat), + formatter: formatFactory?.(series.right[0].fieldFormat), series: series.right.map(({ fieldFormat, ...currentSeries }) => currentSeries), }); } diff --git a/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.test.tsx b/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.test.tsx new file mode 100644 index 0000000000000..9e71323377c1a --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.test.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallowWithIntl as shallow } from 'test_utils/enzyme_helpers'; +import { AxisSettingsPopover, AxisSettingsPopoverProps } from './axis_settings_popover'; +import { ToolbarPopover } from '../shared_components'; + +describe('Axes Settings', () => { + let props: AxisSettingsPopoverProps; + beforeEach(() => { + props = { + layers: [ + { + seriesType: 'bar', + layerId: 'first', + splitAccessor: 'baz', + xAccessor: 'foo', + accessors: ['bar'], + }, + ], + updateTitleState: jest.fn(), + axisTitle: 'My custom X axis title', + axis: 'x', + areTickLabelsVisible: true, + areGridlinesVisible: true, + isAxisTitleVisible: true, + toggleAxisTitleVisibility: jest.fn(), + toggleTickLabelsVisibility: jest.fn(), + toggleGridlinesVisibility: jest.fn(), + }; + }); + + it('should disable the popover if the isDisabled property is true', () => { + const component = shallow(); + expect(component.find(ToolbarPopover).prop('isDisabled')).toEqual(true); + }); + + it('should show the axes title on the corresponding input text', () => { + const component = shallow(); + expect(component.find('[data-test-subj="lnsxAxisTitle"]').prop('value')).toBe( + 'My custom X axis title' + ); + }); + + it('should disable the input text if the switch is off', () => { + const component = shallow(); + expect(component.find('[data-test-subj="lnsxAxisTitle"]').prop('disabled')).toBe(true); + }); + + it('has the tickLabels switch on by default', () => { + const component = shallow(); + expect(component.find('[data-test-subj="lnsshowxAxisTickLabels"]').prop('checked')).toBe(true); + }); + + it('has the tickLabels switch off when tickLabelsVisibilitySettings for this axes are false', () => { + const component = shallow( + + ); + expect(component.find('[data-test-subj="lnsshowyLeftAxisTickLabels"]').prop('checked')).toBe( + false + ); + }); + + it('has the gridlines switch on by default', () => { + const component = shallow(); + expect(component.find('[data-test-subj="lnsshowxAxisGridlines"]').prop('checked')).toBe(true); + }); + + it('has the gridlines switch off when gridlinesVisibilitySettings for this axes are false', () => { + const component = shallow( + + ); + expect(component.find('[data-test-subj="lnsshowyRightAxisGridlines"]').prop('checked')).toBe( + false + ); + }); +}); diff --git a/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx b/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx new file mode 100644 index 0000000000000..835f3e2cde769 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx @@ -0,0 +1,205 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiSwitch, + EuiSpacer, + EuiFieldText, + IconType, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { LayerConfig, AxesSettingsConfig } from './types'; +import { ToolbarPopover, ToolbarButtonProps } from '../shared_components'; +import { isHorizontalChart } from './state_helpers'; +import { EuiIconAxisBottom } from '../assets/axis_bottom'; +import { EuiIconAxisLeft } from '../assets/axis_left'; +import { EuiIconAxisRight } from '../assets/axis_right'; +import { EuiIconAxisTop } from '../assets/axis_top'; + +type AxesSettingsConfigKeys = keyof AxesSettingsConfig; +export interface AxisSettingsPopoverProps { + /** + * Determines the axis + */ + axis: AxesSettingsConfigKeys; + /** + * Contains the chart layers + */ + layers?: LayerConfig[]; + /** + * Determines the axis title + */ + axisTitle: string | undefined; + /** + * Callback to axis title change + */ + updateTitleState: (value: string) => void; + /** + * Determines if the popover is Disabled + */ + isDisabled?: boolean; + /** + * Determines if the ticklabels of the axis are visible + */ + areTickLabelsVisible: boolean; + /** + * Toggles the axis tickLabels visibility + */ + toggleTickLabelsVisibility: (axis: AxesSettingsConfigKeys) => void; + /** + * Determines if the gridlines of the axis are visible + */ + areGridlinesVisible: boolean; + /** + * Toggles the gridlines visibility + */ + toggleGridlinesVisibility: (axis: AxesSettingsConfigKeys) => void; + /** + * Determines if the title visibility switch is on and the input text is disabled + */ + isAxisTitleVisible: boolean; + /** + * Toggles the axis title visibility + */ + toggleAxisTitleVisibility: (axis: AxesSettingsConfigKeys, checked: boolean) => void; +} +const popoverConfig = ( + axis: AxesSettingsConfigKeys, + isHorizontal: boolean +): { icon: IconType; groupPosition: ToolbarButtonProps['groupPosition']; popoverTitle: string } => { + switch (axis) { + case 'yLeft': + return { + icon: (isHorizontal ? EuiIconAxisBottom : EuiIconAxisLeft) as IconType, + groupPosition: 'left', + popoverTitle: isHorizontal + ? i18n.translate('xpack.lens.xyChart.bottomAxisLabel', { + defaultMessage: 'Bottom axis', + }) + : i18n.translate('xpack.lens.xyChart.leftAxisLabel', { + defaultMessage: 'Left axis', + }), + }; + case 'yRight': + return { + icon: (isHorizontal ? EuiIconAxisTop : EuiIconAxisRight) as IconType, + groupPosition: 'right', + popoverTitle: isHorizontal + ? i18n.translate('xpack.lens.xyChart.topAxisLabel', { + defaultMessage: 'Top axis', + }) + : i18n.translate('xpack.lens.xyChart.rightAxisLabel', { + defaultMessage: 'Right axis', + }), + }; + case 'x': + default: + return { + icon: (isHorizontal ? EuiIconAxisLeft : EuiIconAxisBottom) as IconType, + groupPosition: 'center', + popoverTitle: isHorizontal + ? i18n.translate('xpack.lens.xyChart.leftAxisLabel', { + defaultMessage: 'Left axis', + }) + : i18n.translate('xpack.lens.xyChart.bottomAxisLabel', { + defaultMessage: 'Bottom axis', + }), + }; + } +}; + +export const AxisSettingsPopover: React.FunctionComponent = ({ + layers, + axis, + axisTitle, + updateTitleState, + toggleTickLabelsVisibility, + toggleGridlinesVisibility, + isDisabled, + areTickLabelsVisible, + areGridlinesVisible, + isAxisTitleVisible, + toggleAxisTitleVisibility, +}) => { + const [title, setTitle] = useState(axisTitle); + + const isHorizontal = layers?.length ? isHorizontalChart(layers) : false; + const config = popoverConfig(axis, isHorizontal); + + const onTitleChange = (value: string): void => { + setTitle(value); + updateTitleState(value); + }; + return ( + + + + +

+ {i18n.translate('xpack.lens.xyChart.axisNameLabel', { + defaultMessage: 'Axis name', + })} +

+
+
+ + toggleAxisTitleVisibility(axis, target.checked)} + checked={isAxisTitleVisible} + /> + +
+ + onTitleChange(target.value)} + aria-label={i18n.translate('xpack.lens.xyChart.overwriteAxisTitle', { + defaultMessage: 'Overwrite axis title', + })} + /> + + toggleTickLabelsVisibility(axis)} + checked={areTickLabelsVisible} + /> + + toggleGridlinesVisibility(axis)} + checked={areGridlinesVisible} + /> +
+ ); +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/index.ts b/x-pack/plugins/lens/public/xy_visualization/index.ts index fddcad7989b25..470d197e847eb 100644 --- a/x-pack/plugins/lens/public/xy_visualization/index.ts +++ b/x-pack/plugins/lens/public/xy_visualization/index.ts @@ -10,7 +10,14 @@ import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public' import { UI_SETTINGS } from '../../../../../src/plugins/data/public'; import { xyVisualization } from './xy_visualization'; import { xyChart, getXyChartRenderer } from './xy_expression'; -import { legendConfig, layerConfig, yAxisConfig, tickLabelsConfig, gridlinesConfig } from './types'; +import { + legendConfig, + layerConfig, + yAxisConfig, + tickLabelsConfig, + gridlinesConfig, + axisTitlesVisibilityConfig, +} from './types'; import { EditorFrameSetup, FormatFactory } from '../types'; import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; @@ -41,6 +48,7 @@ export class XyVisualization { expressions.registerFunction(() => yAxisConfig); expressions.registerFunction(() => tickLabelsConfig); expressions.registerFunction(() => gridlinesConfig); + expressions.registerFunction(() => axisTitlesVisibilityConfig); expressions.registerFunction(() => layerConfig); expressions.registerFunction(() => xyChart); 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 825281d6d88c2..d09ba01b32c66 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 @@ -41,8 +41,8 @@ describe('#toExpression', () => { legend: { position: Position.Bottom, isVisible: true }, preferredSeriesType: 'bar', fittingFunction: 'Carry', - tickLabelsVisibilitySettings: { x: false, y: true }, - gridlinesVisibilitySettings: { x: false, y: true }, + tickLabelsVisibilitySettings: { x: false, yLeft: true, yRight: true }, + gridlinesVisibilitySettings: { x: false, yLeft: true, yRight: true }, layers: [ { layerId: 'first', @@ -79,7 +79,7 @@ describe('#toExpression', () => { ).toEqual('None'); }); - it('should default the showXAxisTitle and showYAxisTitle to true', () => { + it('should default the axisTitles visibility settings to true', () => { const expression = xyVisualization.toExpression( { legend: { position: Position.Bottom, isVisible: true }, @@ -96,8 +96,13 @@ describe('#toExpression', () => { }, frame.datasourceLayers ) as Ast; - expect(expression.chain[0].arguments.showXAxisTitle[0]).toBe(true); - expect(expression.chain[0].arguments.showYAxisTitle[0]).toBe(true); + expect( + (expression.chain[0].arguments.axisTitlesVisibilitySettings[0] as Ast).chain[0].arguments + ).toEqual({ + x: [true], + yLeft: [true], + yRight: [true], + }); }); it('should generate an expression without x accessor', () => { @@ -166,6 +171,7 @@ describe('#toExpression', () => { expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('d'); expect(expression.chain[0].arguments.xTitle).toEqual(['']); expect(expression.chain[0].arguments.yTitle).toEqual(['']); + expect(expression.chain[0].arguments.yRightTitle).toEqual(['']); expect( (expression.chain[0].arguments.layers[0] as Ast).chain[0].arguments.columnToLabel ).toEqual([ @@ -198,7 +204,8 @@ describe('#toExpression', () => { (expression.chain[0].arguments.tickLabelsVisibilitySettings[0] as Ast).chain[0].arguments ).toEqual({ x: [true], - y: [true], + yLeft: [true], + yRight: [true], }); }); @@ -223,7 +230,8 @@ describe('#toExpression', () => { (expression.chain[0].arguments.gridlinesVisibilitySettings[0] as Ast).chain[0].arguments ).toEqual({ x: [true], - y: [true], + yLeft: [true], + yRight: [true], }); }); }); 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 f64624776186d..df8d571a1fdf8 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -99,6 +99,7 @@ export const buildExpression = ( arguments: { xTitle: [state.xTitle || ''], yTitle: [state.yTitle || ''], + yRightTitle: [state.yRightTitle || ''], legend: [ { type: 'expression', @@ -118,8 +119,22 @@ export const buildExpression = ( }, ], fittingFunction: [state.fittingFunction || 'None'], - showXAxisTitle: [state.showXAxisTitle ?? true], - showYAxisTitle: [state.showYAxisTitle ?? true], + axisTitlesVisibilitySettings: [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_xy_axisTitlesVisibilityConfig', + arguments: { + x: [state?.axisTitlesVisibilitySettings?.x ?? true], + yLeft: [state?.axisTitlesVisibilitySettings?.yLeft ?? true], + yRight: [state?.axisTitlesVisibilitySettings?.yRight ?? true], + }, + }, + ], + }, + ], tickLabelsVisibilitySettings: [ { type: 'expression', @@ -129,7 +144,8 @@ export const buildExpression = ( function: 'lens_xy_tickLabelsConfig', arguments: { x: [state?.tickLabelsVisibilitySettings?.x ?? true], - y: [state?.tickLabelsVisibilitySettings?.y ?? true], + yLeft: [state?.tickLabelsVisibilitySettings?.yLeft ?? true], + yRight: [state?.tickLabelsVisibilitySettings?.yRight ?? true], }, }, ], @@ -144,7 +160,8 @@ export const buildExpression = ( function: 'lens_xy_gridlinesConfig', arguments: { x: [state?.gridlinesVisibilitySettings?.x ?? true], - y: [state?.gridlinesVisibilitySettings?.y ?? true], + yLeft: [state?.gridlinesVisibilitySettings?.yLeft ?? true], + yRight: [state?.gridlinesVisibilitySettings?.yRight ?? true], }, }, ], diff --git a/x-pack/plugins/lens/public/xy_visualization/tooltip_wrapper.tsx b/x-pack/plugins/lens/public/xy_visualization/tooltip_wrapper.tsx new file mode 100644 index 0000000000000..cdbec3fd5d6ae --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/tooltip_wrapper.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiToolTip } from '@elastic/eui'; + +export interface TooltipWrapperProps { + tooltipContent: string; + condition: boolean; +} + +export const TooltipWrapper: React.FunctionComponent = ({ + children, + condition, + tooltipContent, +}) => { + return ( + <> + {condition ? ( + + <>{children} + + ) : ( + children + )} + + ); +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index 8438b1f27dd0d..185fa20f169ee 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -80,7 +80,8 @@ export const legendConfig: ExpressionFunctionDefinition< export interface AxesSettingsConfig { x: boolean; - y: boolean; + yLeft: boolean; + yRight: boolean; } type TickLabelsConfigResult = AxesSettingsConfig & { type: 'lens_xy_tickLabelsConfig' }; @@ -103,10 +104,16 @@ export const tickLabelsConfig: ExpressionFunctionDefinition< defaultMessage: 'Specifies whether or not the tick labels of the x-axis are visible.', }), }, - y: { + yLeft: { types: ['boolean'], - help: i18n.translate('xpack.lens.xyChart.yAxisTickLabels.help', { - defaultMessage: 'Specifies whether or not the tick labels of the y-axis are visible.', + help: i18n.translate('xpack.lens.xyChart.yLeftAxisTickLabels.help', { + defaultMessage: 'Specifies whether or not the tick labels of the left y-axis are visible.', + }), + }, + yRight: { + types: ['boolean'], + help: i18n.translate('xpack.lens.xyChart.yRightAxisTickLabels.help', { + defaultMessage: 'Specifies whether or not the tick labels of the right y-axis are visible.', }), }, }, @@ -138,10 +145,16 @@ export const gridlinesConfig: ExpressionFunctionDefinition< defaultMessage: 'Specifies whether or not the gridlines of the x-axis are visible.', }), }, - y: { + yLeft: { + types: ['boolean'], + help: i18n.translate('xpack.lens.xyChart.yLeftAxisgridlines.help', { + defaultMessage: 'Specifies whether or not the gridlines of the left y-axis are visible.', + }), + }, + yRight: { types: ['boolean'], - help: i18n.translate('xpack.lens.xyChart.yAxisgridlines.help', { - defaultMessage: 'Specifies whether or not the gridlines of the y-axis are visible.', + help: i18n.translate('xpack.lens.xyChart.yRightAxisgridlines.help', { + defaultMessage: 'Specifies whether or not the gridlines of the right y-axis are visible.', }), }, }, @@ -153,6 +166,49 @@ export const gridlinesConfig: ExpressionFunctionDefinition< }, }; +type AxisTitlesVisibilityConfigResult = AxesSettingsConfig & { + type: 'lens_xy_axisTitlesVisibilityConfig'; +}; + +export const axisTitlesVisibilityConfig: ExpressionFunctionDefinition< + 'lens_xy_axisTitlesVisibilityConfig', + null, + AxesSettingsConfig, + AxisTitlesVisibilityConfigResult +> = { + name: 'lens_xy_axisTitlesVisibilityConfig', + aliases: [], + type: 'lens_xy_axisTitlesVisibilityConfig', + help: `Configure the xy chart's axis titles appearance`, + inputTypes: ['null'], + args: { + x: { + types: ['boolean'], + help: i18n.translate('xpack.lens.xyChart.xAxisTitle.help', { + defaultMessage: 'Specifies whether or not the title of the x-axis are visible.', + }), + }, + yLeft: { + types: ['boolean'], + help: i18n.translate('xpack.lens.xyChart.yLeftAxisTitle.help', { + defaultMessage: 'Specifies whether or not the title of the left y-axis are visible.', + }), + }, + yRight: { + types: ['boolean'], + help: i18n.translate('xpack.lens.xyChart.yRightAxisTitle.help', { + defaultMessage: 'Specifies whether or not the title of the right y-axis are visible.', + }), + }, + }, + fn: function fn(input: unknown, args: AxesSettingsConfig) { + return { + type: 'lens_xy_axisTitlesVisibilityConfig', + ...args, + }; + }, +}; + interface AxisConfig { title: string; hide?: boolean; @@ -329,11 +385,13 @@ export type LayerArgs = LayerConfig & { export interface XYArgs { xTitle: string; yTitle: string; + yRightTitle: string; legend: LegendConfig & { type: 'lens_xy_legendConfig' }; layers: LayerArgs[]; fittingFunction?: FittingFunction; - showXAxisTitle?: boolean; - showYAxisTitle?: boolean; + axisTitlesVisibilitySettings?: AxesSettingsConfig & { + type: 'lens_xy_axisTitlesVisibilityConfig'; + }; tickLabelsVisibilitySettings?: AxesSettingsConfig & { type: 'lens_xy_tickLabelsConfig' }; gridlinesVisibilitySettings?: AxesSettingsConfig & { type: 'lens_xy_gridlinesConfig' }; } @@ -346,8 +404,8 @@ export interface XYState { layers: LayerConfig[]; xTitle?: string; yTitle?: string; - showXAxisTitle?: boolean; - showYAxisTitle?: boolean; + yRightTitle?: string; + axisTitlesVisibilitySettings?: AxesSettingsConfig; tickLabelsVisibilitySettings?: AxesSettingsConfig; gridlinesVisibilitySettings?: AxesSettingsConfig; } 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 c353f3f370ee5..5b14fca78e65d 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: 400px; -} + 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 89a2574026ced..7e2e8f0453588 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 @@ -8,6 +8,8 @@ import React from 'react'; import { mountWithIntl as mount, shallowWithIntl as shallow } from 'test_utils/enzyme_helpers'; import { EuiButtonGroupProps, EuiSuperSelect, EuiButtonGroup } from '@elastic/eui'; import { LayerContextMenu, XyToolbar } from './xy_config_panel'; +import { ToolbarPopover } from '../shared_components'; +import { AxisSettingsPopover } from './axis_settings_popover'; import { FramePublicAPI } from '../types'; import { State } from './types'; import { Position } from '@elastic/charts'; @@ -113,7 +115,7 @@ describe('XY Config panels', () => { expect(component.find(EuiSuperSelect).prop('valueOfSelected')).toEqual('Carry'); }); - it('should disable the select if there is no area or line series', () => { + it('should disable the popover if there is no area or line series', () => { const state = testState(); const component = shallow( { /> ); - expect(component.find(EuiSuperSelect).prop('disabled')).toEqual(true); + expect(component.find(ToolbarPopover).at(0).prop('isDisabled')).toEqual(true); }); - it('should show the values of the X and Y axes titles on the corresponding input text', () => { + it('should disable the popover if there is no right axis', () => { + const state = testState(); + const component = shallow(); + + expect(component.find(AxisSettingsPopover).at(2).prop('isDisabled')).toEqual(true); + }); + + it('should enable the popover if there is right axis', () => { const state = testState(); const component = shallow( { setState={jest.fn()} state={{ ...state, - xTitle: 'My custom X axis title', - yTitle: 'My custom Y axis title', + layers: [{ ...state.layers[0], yConfig: [{ axisMode: 'right', forAccessor: 'bar' }] }], }} /> ); - expect(component.find('[data-test-subj="lnsXAxisTitle"]').prop('value')).toBe( - 'My custom X axis title' - ); - expect(component.find('[data-test-subj="lnsYAxisTitle"]').prop('value')).toBe( - 'My custom Y axis title' - ); + expect(component.find(AxisSettingsPopover).at(2).prop('isDisabled')).toEqual(false); }); - it('should disable the input texts if the switch is off', () => { + it('should render the AxisSettingsPopover 3 times', () => { const state = testState(); const component = shallow( { setState={jest.fn()} state={{ ...state, - showXAxisTitle: false, - showYAxisTitle: false, + layers: [{ ...state.layers[0], yConfig: [{ axisMode: 'right', forAccessor: 'foo' }] }], }} /> ); - expect(component.find('[data-test-subj="lnsXAxisTitle"]').prop('disabled')).toBe(true); - expect(component.find('[data-test-subj="lnsYAxisTitle"]').prop('disabled')).toBe(true); - }); - - it('has the tick labels buttons enabled', () => { - const state = testState(); - const component = shallow(); - - const options = component - .find('[data-test-subj="lnsTickLabelsSettings"]') - .prop('options') as EuiButtonGroupProps['options']; - - expect(options!.map(({ label }) => label)).toEqual(['X-axis', 'Y-axis']); - - const selections = component - .find('[data-test-subj="lnsTickLabelsSettings"]') - .prop('idToSelectedMap'); - - expect(selections!).toEqual({ x: true, y: true }); - }); - - it('has the gridlines buttons enabled', () => { - const state = testState(); - const component = shallow(); - - const selections = component - .find('[data-test-subj="lnsGridlinesSettings"]') - .prop('idToSelectedMap'); - - expect(selections!).toEqual({ x: true, y: true }); + expect(component.find(AxisSettingsPopover).length).toEqual(3); }); }); }); 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 62fd6e013f20d..bc98bf53d9f12 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 @@ -5,7 +5,7 @@ */ import './xy_config_panel.scss'; -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { Position } from '@elastic/charts'; import { debounce } from 'lodash'; @@ -15,19 +15,13 @@ import { EuiFlexItem, EuiSuperSelect, EuiFormRow, - EuiPopover, EuiText, - EuiSelect, htmlIdGenerator, EuiForm, EuiColorPicker, EuiColorPickerProps, EuiToolTip, EuiIcon, - EuiFieldText, - EuiSwitch, - EuiHorizontalRule, - EuiTitle, } from '@elastic/eui'; import { VisualizationLayerWidgetProps, @@ -38,9 +32,13 @@ import { State, SeriesType, visualizationTypes, YAxisMode, AxesSettingsConfig } import { isHorizontalChart, isHorizontalSeries, getSeriesColor } from './state_helpers'; import { trackUiEvent } from '../lens_ui_telemetry'; import { fittingFunctionDefinitions } from './fitting_functions'; -import { ToolbarButton } from '../toolbar_button'; +import { ToolbarPopover, LegendSettingsPopover } from '../shared_components'; +import { AxisSettingsPopover } from './axis_settings_popover'; +import { TooltipWrapper } from './tooltip_wrapper'; +import { getAxesConfiguration } from './axes_configuration'; type UnwrapArray = T extends Array ? P : T; +type AxesSettingsConfigKeys = keyof AxesSettingsConfig; function updateLayer(state: State, layer: UnwrapArray, index: number): State { const newLayers = [...state.layers]; @@ -57,21 +55,21 @@ const legendOptions: Array<{ id: string; value: 'auto' | 'show' | 'hide'; label: id: `xy_legend_auto`, value: 'auto', label: i18n.translate('xpack.lens.xyChart.legendVisibility.auto', { - defaultMessage: 'auto', + defaultMessage: 'Auto', }), }, { id: `xy_legend_show`, value: 'show', label: i18n.translate('xpack.lens.xyChart.legendVisibility.show', { - defaultMessage: 'show', + defaultMessage: 'Show', }), }, { id: `xy_legend_hide`, value: 'hide', label: i18n.translate('xpack.lens.xyChart.legendVisibility.hide', { - defaultMessage: 'hide', + defaultMessage: 'Hide', }), }, ]; @@ -120,86 +118,25 @@ export function LayerContextMenu(props: VisualizationLayerWidgetProps) { } export function XyToolbar(props: VisualizationToolbarProps) { - const axes = [ - { - id: 'x', - label: 'X-axis', - }, - { - id: 'y', - label: 'Y-axis', - }, - ]; + const { state, setState } = props; - const { frame, state, setState } = props; - - const [open, setOpen] = useState(false); const hasNonBarSeries = state?.layers.some(({ seriesType }) => ['area_stacked', 'area', 'line'].includes(seriesType) ); - const [xAxisTitle, setXAxisTitle] = useState(state?.xTitle); - const [yAxisTitle, setYAxisTitle] = useState(state?.yTitle); - - const xyTitles = useCallback(() => { - const defaults = { - xTitle: xAxisTitle, - yTitle: yAxisTitle, - }; - const layer = state?.layers[0]; - if (!layer || !layer.accessors.length) { - return defaults; - } - const datasource = frame.datasourceLayers[layer.layerId]; - if (!datasource) { - return defaults; - } - const x = layer.xAccessor ? datasource.getOperationForColumnId(layer.xAccessor) : null; - const y = layer.accessors[0] ? datasource.getOperationForColumnId(layer.accessors[0]) : null; - - return { - xTitle: defaults.xTitle || x?.label, - yTitle: defaults.yTitle || y?.label, - }; - /* We want this callback to run only if open changes its state. What we want to accomplish here is to give the user a better UX. - By default these input fields have the axis legends. If the user changes the input text, the axis legends should also change. - BUT if the user cleans up the input text, it should remain empty until the user closes and reopens the panel. - In that case, the default axes legend should appear. */ - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [open]); - - useEffect(() => { - const { - xTitle, - yTitle, - }: { xTitle: string | undefined; yTitle: string | undefined } = xyTitles(); - setXAxisTitle(xTitle); - setYAxisTitle(yTitle); - }, [xyTitles]); - - const onXTitleChange = (value: string): void => { - setXAxisTitle(value); - setState({ ...state, xTitle: value }); - }; - - const onYTitleChange = (value: string): void => { - setYAxisTitle(value); - setState({ ...state, yTitle: value }); - }; - - type AxesSettingsConfigKeys = keyof AxesSettingsConfig; + const shouldRotate = state?.layers.length ? isHorizontalChart(state.layers) : false; + const axisGroups = getAxesConfiguration(state?.layers, shouldRotate); const tickLabelsVisibilitySettings = { x: state?.tickLabelsVisibilitySettings?.x ?? true, - y: state?.tickLabelsVisibilitySettings?.y ?? true, + yLeft: state?.tickLabelsVisibilitySettings?.yLeft ?? true, + yRight: state?.tickLabelsVisibilitySettings?.yRight ?? true, }; - - const onTickLabelsVisibilitySettingsChange = (optionId: string): void => { - const id = optionId as AxesSettingsConfigKeys; + const onTickLabelsVisibilitySettingsChange = (optionId: AxesSettingsConfigKeys): void => { const newTickLabelsVisibilitySettings = { ...tickLabelsVisibilitySettings, ...{ - [id]: !tickLabelsVisibilitySettings[id], + [optionId]: !tickLabelsVisibilitySettings[optionId], }, }; setState({ @@ -210,15 +147,15 @@ export function XyToolbar(props: VisualizationToolbarProps) { const gridlinesVisibilitySettings = { x: state?.gridlinesVisibilitySettings?.x ?? true, - y: state?.gridlinesVisibilitySettings?.y ?? true, + yLeft: state?.gridlinesVisibilitySettings?.yLeft ?? true, + yRight: state?.gridlinesVisibilitySettings?.yRight ?? true, }; - const onGridlinesVisibilitySettingsChange = (optionId: string): void => { - const id = optionId as AxesSettingsConfigKeys; + const onGridlinesVisibilitySettingsChange = (optionId: AxesSettingsConfigKeys): void => { const newGridlinesVisibilitySettings = { ...gridlinesVisibilitySettings, ...{ - [id]: !gridlinesVisibilitySettings[id], + [optionId]: !gridlinesVisibilitySettings[optionId], }, }; setState({ @@ -227,6 +164,27 @@ export function XyToolbar(props: VisualizationToolbarProps) { }); }; + const axisTitlesVisibilitySettings = { + x: state?.axisTitlesVisibilitySettings?.x ?? true, + yLeft: state?.axisTitlesVisibilitySettings?.yLeft ?? true, + yRight: state?.axisTitlesVisibilitySettings?.yRight ?? true, + }; + const onAxisTitlesVisibilitySettingsChange = ( + axis: AxesSettingsConfigKeys, + checked: boolean + ): void => { + const newAxisTitlesVisibilitySettings = { + ...axisTitlesVisibilitySettings, + ...{ + [axis]: checked, + }, + }; + setState({ + ...state, + axisTitlesVisibilitySettings: newAxisTitlesVisibilitySettings, + }); + }; + const legendMode = state?.legend.isVisible && !state?.legend.showSingleSeries ? 'auto' @@ -234,243 +192,149 @@ export function XyToolbar(props: VisualizationToolbarProps) { ? 'hide' : 'show'; return ( - - - { - setOpen(!open); - }} - > - {i18n.translate('xpack.lens.xyChart.settingsLabel', { defaultMessage: 'Settings' })} - - } - isOpen={open} - closePopover={() => { - setOpen(false); - }} - anchorPosition="downRight" - > - + + + - - { - return { - value: id, - dropdownDisplay: ( - <> - {title} - -

{description}

-
- - ), - inputDisplay: title, - }; + setState({ ...state, fittingFunction: value })} - itemLayoutAlign="top" - hasDividers - /> - -
- - - value === legendMode)!.id} - onChange={(optionId) => { - const newMode = legendOptions.find(({ id }) => id === optionId)!.value; - if (newMode === 'auto') { - setState({ - ...state, - legend: { ...state.legend, isVisible: true, showSingleSeries: false }, - }); - } else if (newMode === 'show') { - setState({ - ...state, - legend: { ...state.legend, isVisible: true, showSingleSeries: true }, - }); - } else if (newMode === 'hide') { - setState({ - ...state, - legend: { ...state.legend, isVisible: false, showSingleSeries: false }, - }); - } - }} - /> - - - { + > + { + return { + value: id, + dropdownDisplay: ( + <> + {title} + +

{description}

+
+ + ), + inputDisplay: title, + }; + })} + valueOfSelected={state?.fittingFunction || 'None'} + onChange={(value) => setState({ ...state, fittingFunction: value })} + itemLayoutAlign="top" + hasDividers + /> +
+ + + { + const newMode = legendOptions.find(({ id }) => id === optionId)!.value; + if (newMode === 'auto') { setState({ ...state, - legend: { ...state.legend, position: e.target.value as Position }, + legend: { ...state.legend, isVisible: true, showSingleSeries: false }, }); - }} - /> - - - - onTickLabelsVisibilitySettingsChange(id)} - buttonSize="compressed" - isFullWidth - type="multi" - /> - - { + setState({ + ...state, + legend: { ...state.legend, position: id as Position }, + }); + }} + /> +
+
+ + + - onGridlinesVisibilitySettingsChange(id)} - buttonSize="compressed" - isFullWidth - type="multi" - /> - - - - - {i18n.translate('xpack.lens.xyChart.axisTitles', { defaultMessage: 'Axis titles' })} - - - - X-axis - - - setState({ ...state, showXAxisTitle: target.checked }) - } - checked={state?.showXAxisTitle ?? true} - /> - - + condition={ + Object.keys(axisGroups.find((group) => group.groupId === 'left') || {}).length === 0 } > - onXTitleChange(target.value)} - aria-label={i18n.translate('xpack.lens.xyChart.overwriteXaxis', { - defaultMessage: 'Overwrite X-axis title', - })} + setState({ ...state, yTitle: value })} + areTickLabelsVisible={tickLabelsVisibilitySettings.yLeft} + toggleTickLabelsVisibility={onTickLabelsVisibilitySettingsChange} + areGridlinesVisible={gridlinesVisibilitySettings.yLeft} + toggleGridlinesVisibility={onGridlinesVisibilitySettingsChange} + isDisabled={ + Object.keys(axisGroups.find((group) => group.groupId === 'left') || {}).length === 0 + } + isAxisTitleVisible={axisTitlesVisibilitySettings.yLeft} + toggleAxisTitleVisibility={onAxisTitlesVisibilitySettingsChange} /> - - - Y-axis - - - setState({ ...state, showYAxisTitle: target.checked }) - } - checked={state?.showYAxisTitle ?? true} - /> - -
+ + setState({ ...state, xTitle: value })} + areTickLabelsVisible={tickLabelsVisibilitySettings.x} + toggleTickLabelsVisibility={onTickLabelsVisibilitySettingsChange} + areGridlinesVisible={gridlinesVisibilitySettings.x} + toggleGridlinesVisibility={onGridlinesVisibilitySettingsChange} + isAxisTitleVisible={axisTitlesVisibilitySettings.x} + toggleAxisTitleVisibility={onAxisTitlesVisibilitySettingsChange} + /> + group.groupId === 'right') || {}).length === 0 } > - onYTitleChange(target.value)} - aria-label={i18n.translate('xpack.lens.xyChart.overwriteYaxis', { - defaultMessage: 'Overwrite Y-axis title', - })} + setState({ ...state, yRightTitle: value })} + areTickLabelsVisible={tickLabelsVisibilitySettings.yRight} + toggleTickLabelsVisibility={onTickLabelsVisibilitySettingsChange} + areGridlinesVisible={gridlinesVisibilitySettings.yRight} + toggleGridlinesVisibility={onGridlinesVisibilitySettingsChange} + isDisabled={ + Object.keys(axisGroups.find((group) => group.groupId === 'right') || {}).length === + 0 + } + isAxisTitleVisible={axisTitlesVisibilitySettings.yRight} + toggleAxisTitleVisibility={onAxisTitlesVisibilitySettingsChange} /> - - + + ); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx index c9c27193c437e..1d809f222eb00 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx @@ -215,22 +215,29 @@ const sampleLayer: LayerArgs = { const createArgsWithLayers = (layers: LayerArgs[] = [sampleLayer]): XYArgs => ({ xTitle: '', yTitle: '', + yRightTitle: '', legend: { type: 'lens_xy_legendConfig', isVisible: false, position: Position.Top, }, - showXAxisTitle: true, - showYAxisTitle: true, + axisTitlesVisibilitySettings: { + type: 'lens_xy_axisTitlesVisibilityConfig', + x: true, + yLeft: true, + yRight: true, + }, tickLabelsVisibilitySettings: { type: 'lens_xy_tickLabelsConfig', x: true, - y: false, + yLeft: false, + yRight: false, }, gridlinesVisibilitySettings: { type: 'lens_xy_gridlinesConfig', x: true, - y: false, + yLeft: false, + yRight: false, }, layers, }); @@ -291,7 +298,8 @@ describe('xy_expression', () => { test('tickLabelsConfig produces the correct arguments', () => { const args: AxesSettingsConfig = { x: true, - y: false, + yLeft: false, + yRight: false, }; const result = tickLabelsConfig.fn(null, args, createMockExecutionContext()); @@ -305,7 +313,8 @@ describe('xy_expression', () => { test('gridlinesConfig produces the correct arguments', () => { const args: AxesSettingsConfig = { x: true, - y: false, + yLeft: false, + yRight: false, }; const result = gridlinesConfig.fn(null, args, createMockExecutionContext()); @@ -1417,7 +1426,12 @@ describe('xy_expression', () => { test('it should set the tickLabel visibility on the x axis if the tick labels is hidden', () => { const { data, args } = sampleArgs(); - args.tickLabelsVisibilitySettings = { x: false, y: true, type: 'lens_xy_tickLabelsConfig' }; + args.tickLabelsVisibilitySettings = { + x: false, + yLeft: true, + yRight: true, + type: 'lens_xy_tickLabelsConfig', + }; const instance = shallow( { test('it should set the tickLabel visibility on the y axis if the tick labels is hidden', () => { const { data, args } = sampleArgs(); - args.tickLabelsVisibilitySettings = { x: true, y: false, type: 'lens_xy_tickLabelsConfig' }; + args.tickLabelsVisibilitySettings = { + x: true, + yLeft: false, + yRight: false, + type: 'lens_xy_tickLabelsConfig', + }; const instance = shallow( { test('it should set the tickLabel visibility on the x axis if the tick labels is shown', () => { const { data, args } = sampleArgs(); - args.tickLabelsVisibilitySettings = { x: true, y: true, type: 'lens_xy_tickLabelsConfig' }; + args.tickLabelsVisibilitySettings = { + x: true, + yLeft: true, + yRight: true, + type: 'lens_xy_tickLabelsConfig', + }; const instance = shallow( { test('it should set the tickLabel visibility on the y axis if the tick labels is shown', () => { const { data, args } = sampleArgs(); - args.tickLabelsVisibilitySettings = { x: false, y: true, type: 'lens_xy_tickLabelsConfig' }; + args.tickLabelsVisibilitySettings = { + x: false, + yLeft: true, + yRight: true, + type: 'lens_xy_tickLabelsConfig', + }; const instance = shallow( { const args: XYArgs = { xTitle: '', yTitle: '', + yRightTitle: '', legend: { type: 'lens_xy_legendConfig', isVisible: false, position: Position.Top }, tickLabelsVisibilitySettings: { type: 'lens_xy_tickLabelsConfig', x: true, - y: true, + yLeft: true, + yRight: true, }, gridlinesVisibilitySettings: { type: 'lens_xy_gridlinesConfig', x: true, - y: false, + yLeft: false, + yRight: false, }, layers: [ { @@ -1635,16 +1667,19 @@ describe('xy_expression', () => { const args: XYArgs = { xTitle: '', yTitle: '', + yRightTitle: '', legend: { type: 'lens_xy_legendConfig', isVisible: false, position: Position.Top }, tickLabelsVisibilitySettings: { type: 'lens_xy_tickLabelsConfig', x: true, - y: false, + yLeft: false, + yRight: false, }, gridlinesVisibilitySettings: { type: 'lens_xy_gridlinesConfig', x: true, - y: false, + yLeft: false, + yRight: false, }, layers: [ { @@ -1701,16 +1736,19 @@ describe('xy_expression', () => { const args: XYArgs = { xTitle: '', yTitle: '', + yRightTitle: '', legend: { type: 'lens_xy_legendConfig', isVisible: true, position: Position.Top }, tickLabelsVisibilitySettings: { type: 'lens_xy_tickLabelsConfig', x: true, - y: false, + yLeft: false, + yRight: false, }, gridlinesVisibilitySettings: { type: 'lens_xy_gridlinesConfig', x: true, - y: false, + yLeft: false, + yRight: false, }, layers: [ { @@ -1894,7 +1932,12 @@ describe('xy_expression', () => { test('it should hide the X axis title if the corresponding switch is off', () => { const { data, args } = sampleArgs(); - args.showXAxisTitle = false; + args.axisTitlesVisibilitySettings = { + x: false, + yLeft: true, + yRight: true, + type: 'lens_xy_axisTitlesVisibilityConfig', + }; const component = shallow( { test('it should show the X axis gridlines if the setting is on', () => { const { data, args } = sampleArgs(); - args.gridlinesVisibilitySettings = { x: true, y: false, type: 'lens_xy_gridlinesConfig' }; + args.gridlinesVisibilitySettings = { + x: true, + yLeft: false, + yRight: false, + type: 'lens_xy_gridlinesConfig', + }; const component = shallow( , - index: number + groupId: string ) => { - if (index > 0 && args.yTitle) return; + const yTitle = groupId === 'right' ? args.yRightTitle : args.yTitle; return ( - args.yTitle || + yTitle || axisSeries .map( (series) => @@ -322,6 +333,24 @@ export function XYChart({ ); }; + const getYAxesStyle = (groupId: string) => { + const style = { + tickLabel: { + visible: + groupId === 'right' + ? tickLabelsVisibilitySettings?.yRight + : tickLabelsVisibilitySettings?.yLeft, + }, + axisTitle: { + visible: + groupId === 'right' + ? axisTitlesVisibilitySettings?.yRight + : axisTitlesVisibilitySettings?.yLeft, + }, + }; + return style; + }; + return ( - {yAxesConfiguration.map((axis, index) => ( + {yAxesConfiguration.map((axis) => ( axis.formatter.convert(d)} - style={{ - tickLabel: { - visible: tickLabelsVisibilitySettings?.y, - }, - axisTitle: { - visible: showYAxisTitle, - }, - }} + tickFormat={(d) => axis.formatter?.convert(d) || ''} + style={getYAxesStyle(axis.groupId)} /> ))} 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 ea5cff80695a3..09a2cc652a9b3 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 @@ -555,10 +555,9 @@ describe('xy_suggestions', () => { const currentState: XYState = { legend: { isVisible: true, position: 'bottom' }, fittingFunction: 'None', - showXAxisTitle: true, - showYAxisTitle: true, - gridlinesVisibilitySettings: { x: true, y: true }, - tickLabelsVisibilitySettings: { x: true, y: false }, + axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + tickLabelsVisibilitySettings: { x: true, yLeft: false, yRight: false }, preferredSeriesType: 'bar', layers: [ { @@ -597,10 +596,9 @@ describe('xy_suggestions', () => { legend: { isVisible: true, position: 'bottom' }, preferredSeriesType: 'bar', fittingFunction: 'None', - showXAxisTitle: true, - showYAxisTitle: true, - gridlinesVisibilitySettings: { x: true, y: true }, - tickLabelsVisibilitySettings: { x: true, y: false }, + axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + tickLabelsVisibilitySettings: { x: true, yLeft: false, yRight: false }, layers: [ { accessors: ['price', 'quantity'], @@ -710,10 +708,9 @@ describe('xy_suggestions', () => { legend: { isVisible: true, position: 'bottom' }, preferredSeriesType: 'bar', fittingFunction: 'None', - showXAxisTitle: true, - showYAxisTitle: true, - gridlinesVisibilitySettings: { x: true, y: true }, - tickLabelsVisibilitySettings: { x: true, y: false }, + axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + tickLabelsVisibilitySettings: { x: true, yLeft: false, yRight: false }, layers: [ { accessors: ['price', 'quantity'], @@ -753,10 +750,9 @@ describe('xy_suggestions', () => { legend: { isVisible: true, position: 'bottom' }, preferredSeriesType: 'bar', fittingFunction: 'None', - showXAxisTitle: true, - showYAxisTitle: true, - gridlinesVisibilitySettings: { x: true, y: true }, - tickLabelsVisibilitySettings: { x: true, y: false }, + axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + tickLabelsVisibilitySettings: { x: true, yLeft: false, yRight: false }, layers: [ { accessors: ['price'], @@ -797,10 +793,9 @@ describe('xy_suggestions', () => { legend: { isVisible: true, position: 'bottom' }, preferredSeriesType: 'bar', fittingFunction: 'None', - showXAxisTitle: true, - showYAxisTitle: true, - gridlinesVisibilitySettings: { x: true, y: true }, - tickLabelsVisibilitySettings: { x: true, y: false }, + axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + tickLabelsVisibilitySettings: { x: true, yLeft: false, yRight: false }, layers: [ { accessors: ['price', 'quantity'], 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 42fc538874b93..e6286523d8e2e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -489,15 +489,21 @@ function buildSuggestion({ fittingFunction: currentState?.fittingFunction || 'None', xTitle: currentState?.xTitle, yTitle: currentState?.yTitle, - showXAxisTitle: currentState?.showXAxisTitle ?? true, - showYAxisTitle: currentState?.showYAxisTitle ?? true, + yRightTitle: currentState?.yRightTitle, + axisTitlesVisibilitySettings: currentState?.axisTitlesVisibilitySettings || { + x: true, + yLeft: true, + yRight: true, + }, tickLabelsVisibilitySettings: currentState?.tickLabelsVisibilitySettings || { x: true, - y: true, + yLeft: true, + yRight: true, }, gridlinesVisibilitySettings: currentState?.gridlinesVisibilitySettings || { x: true, - y: true, + yLeft: true, + yRight: true, }, preferredSeriesType: seriesType, layers: Object.keys(existingLayer).length ? keptLayers : [...keptLayers, newLayer], diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 68f6bc166cd1d..e54d6739c0600 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9684,8 +9684,6 @@ "xpack.lens.pieChart.fitInsideOnlyLabel": "内部のみ", "xpack.lens.pieChart.hiddenNumbersLabel": "グラフから非表示", "xpack.lens.pieChart.labelPositionLabel": "ラベル位置", - "xpack.lens.pieChart.legendDisplayLabel": "凡例表示", - "xpack.lens.pieChart.legendDisplayLegend": "凡例表示", "xpack.lens.pieChart.nestedLegendLabel": "ネストされた凡例", "xpack.lens.pieChart.numberLabels": "ラベル値", "xpack.lens.pieChart.showCategoriesLabel": "内部または外部", @@ -9711,7 +9709,6 @@ "xpack.lens.xyChart.chartTypeLegend": "チャートタイプ", "xpack.lens.xyChart.fittingDisabledHelpText": "この設定は折れ線グラフとエリアグラフでのみ適用されます。", "xpack.lens.xyChart.fittingFunction.help": "欠測値の処理方法を定義", - "xpack.lens.xyChart.fittingLabel": "欠測値を埋める", "xpack.lens.xyChart.help": "X/Y チャート", "xpack.lens.xyChart.isVisible.help": "判例の表示・非表示を指定します。", "xpack.lens.xyChart.legend.help": "チャートの凡例を構成します。", @@ -9720,7 +9717,6 @@ "xpack.lens.xyChart.renderer.help": "X/Y チャートを再レンダリング", "xpack.lens.xyChart.seriesColor.auto": "自動", "xpack.lens.xyChart.seriesColor.label": "系列色", - "xpack.lens.xyChart.settingsLabel": "設定", "xpack.lens.xyChart.splitSeries": "系列を分割", "xpack.lens.xyChart.title.help": "軸のタイトル", "xpack.lens.xyChart.xAxisLabel": "X 軸", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index cb43cefdc3655..4c8ccd56c1c01 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9690,8 +9690,6 @@ "xpack.lens.pieChart.fitInsideOnlyLabel": "仅内部", "xpack.lens.pieChart.hiddenNumbersLabel": "在图表中隐藏", "xpack.lens.pieChart.labelPositionLabel": "标签位置", - "xpack.lens.pieChart.legendDisplayLabel": "图例显示", - "xpack.lens.pieChart.legendDisplayLegend": "图例显示", "xpack.lens.pieChart.nestedLegendLabel": "嵌套图例", "xpack.lens.pieChart.numberLabels": "标签值", "xpack.lens.pieChart.showCategoriesLabel": "内部或外部", @@ -9717,7 +9715,6 @@ "xpack.lens.xyChart.chartTypeLegend": "图表类型", "xpack.lens.xyChart.fittingDisabledHelpText": "此设置仅适用于折线图和非堆叠面积图。", "xpack.lens.xyChart.fittingFunction.help": "定义处理缺失值的方式", - "xpack.lens.xyChart.fittingLabel": "填充缺失值", "xpack.lens.xyChart.help": "X/Y 图表", "xpack.lens.xyChart.isVisible.help": "指定图例是否可见。", "xpack.lens.xyChart.legend.help": "配置图表图例。", @@ -9726,7 +9723,6 @@ "xpack.lens.xyChart.renderer.help": "X/Y 图表呈现器", "xpack.lens.xyChart.seriesColor.auto": "自动", "xpack.lens.xyChart.seriesColor.label": "系列颜色", - "xpack.lens.xyChart.settingsLabel": "设置", "xpack.lens.xyChart.splitSeries": "拆分序列", "xpack.lens.xyChart.title.help": "轴标题", "xpack.lens.xyChart.xAxisLabel": "X 轴",