From 2f6b9f67d8351a5688e9c3753a4a7234e466dc6a Mon Sep 17 00:00:00 2001 From: Maria Iriarte <106958839+mariairiartef@users.noreply.github.com> Date: Thu, 16 Jan 2025 15:36:37 +0100 Subject: [PATCH] [Lens][Heatmap] Add ability to rotate X axis label (#202143) ## Summary Closes https://github.com/elastic/kibana/issues/61248 Adds the ability to rotate the X-axis labels in the heatmap chart. Screenshot 2025-01-08 at 16 50 20 #### Screen recording https://github.com/user-attachments/assets/f4834722-b296-4239-a9d4-25c5fd8c738b ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: Marta Bondyra <4283304+mbondyra@users.noreply.github.com> --- .../expression_functions/heatmap_grid.ts | 6 ++ .../common/types/expression_functions.ts | 1 + .../public/components/heatmap_component.tsx | 2 + .../axis_label_orientation_selector.test.tsx | 46 ++++++++++ .../axis_label_orientation_selector.tsx | 83 +++++++++++++++++ .../lens/public/shared_components/index.ts | 3 + .../heatmap/toolbar_component.scss | 3 - .../heatmap/toolbar_component.test.tsx | 92 +++++++++++++++++++ .../heatmap/toolbar_component.tsx | 31 ++++++- .../visualizations/heatmap/visualization.tsx | 1 + 10 files changed, 261 insertions(+), 7 deletions(-) create mode 100644 x-pack/platform/plugins/shared/lens/public/shared_components/axis/orientation/axis_label_orientation_selector.test.tsx create mode 100644 x-pack/platform/plugins/shared/lens/public/shared_components/axis/orientation/axis_label_orientation_selector.tsx delete mode 100644 x-pack/platform/plugins/shared/lens/public/visualizations/heatmap/toolbar_component.scss create mode 100644 x-pack/platform/plugins/shared/lens/public/visualizations/heatmap/toolbar_component.test.tsx diff --git a/src/platform/plugins/shared/chart_expressions/expression_heatmap/common/expression_functions/heatmap_grid.ts b/src/platform/plugins/shared/chart_expressions/expression_heatmap/common/expression_functions/heatmap_grid.ts index 9128289708aa5..29f7d9a4cda65 100644 --- a/src/platform/plugins/shared/chart_expressions/expression_heatmap/common/expression_functions/heatmap_grid.ts +++ b/src/platform/plugins/shared/chart_expressions/expression_heatmap/common/expression_functions/heatmap_grid.ts @@ -73,6 +73,12 @@ export const heatmapGridConfig: ExpressionFunctionDefinition< defaultMessage: 'Specifies whether or not the X-axis labels are visible.', }), }, + xAxisLabelRotation: { + types: ['number'], + help: i18n.translate('expressionHeatmap.function.args.grid.xAxisLabelRotation.help', { + defaultMessage: 'Specifies the rotation of the X-axis labels.', + }), + }, isXAxisTitleVisible: { types: ['boolean'], help: i18n.translate('expressionHeatmap.function.args.grid.isXAxisTitleVisible.help', { diff --git a/src/platform/plugins/shared/chart_expressions/expression_heatmap/common/types/expression_functions.ts b/src/platform/plugins/shared/chart_expressions/expression_heatmap/common/types/expression_functions.ts index f63a0a56b63ac..a36d9096501b0 100644 --- a/src/platform/plugins/shared/chart_expressions/expression_heatmap/common/types/expression_functions.ts +++ b/src/platform/plugins/shared/chart_expressions/expression_heatmap/common/types/expression_functions.ts @@ -71,6 +71,7 @@ export interface HeatmapGridConfig { yTitle?: string; // X-axis isXAxisLabelVisible: boolean; + xAxisLabelRotation?: number; isXAxisTitleVisible: boolean; xTitle?: string; } diff --git a/src/platform/plugins/shared/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx b/src/platform/plugins/shared/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx index fb870e3605b02..ddd42f62fdaa7 100644 --- a/src/platform/plugins/shared/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx +++ b/src/platform/plugins/shared/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx @@ -572,6 +572,8 @@ export const HeatmapComponent: FC = memo( // eui color subdued textColor: chartBaseTheme.axes.tickLabel.fill, padding: xAxisColumn?.name ? 8 : 0, + rotation: + args.gridConfig.xAxisLabelRotation && Math.abs(args.gridConfig.xAxisLabelRotation), // rotation is a positive value }, brushMask: { fill: isDarkTheme ? 'rgb(30,31,35,80%)' : 'rgb(247,247,247,50%)', diff --git a/x-pack/platform/plugins/shared/lens/public/shared_components/axis/orientation/axis_label_orientation_selector.test.tsx b/x-pack/platform/plugins/shared/lens/public/shared_components/axis/orientation/axis_label_orientation_selector.test.tsx new file mode 100644 index 0000000000000..3ce41ee32928e --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/public/shared_components/axis/orientation/axis_label_orientation_selector.test.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { + AxisLabelOrientationSelector, + AxisLabelOrientationSelectorProps, +} from './axis_label_orientation_selector'; +import userEvent from '@testing-library/user-event'; + +const renderComponent = (propsOverrides?: Partial) => { + return render( + + ); +}; + +describe('AxisLabelOrientationSelector', () => { + it('should render all buttons', () => { + renderComponent(); + + expect(screen.getByRole('button', { pressed: true })).toHaveTextContent(/horizontal/i); + expect(screen.getByRole('button', { name: /vertical/i })).toBeEnabled(); + expect(screen.getByRole('button', { name: /angled/i })).toBeEnabled(); + }); + + it('should call setOrientation when changing the orientation', async () => { + const setLabelOrientation = jest.fn(); + renderComponent({ setLabelOrientation }); + + const button = screen.getByRole('button', { name: /vertical/i }); + await userEvent.click(button); + + expect(setLabelOrientation).toBeCalledTimes(1); + expect(setLabelOrientation).toBeCalledWith(-90); + }); +}); diff --git a/x-pack/platform/plugins/shared/lens/public/shared_components/axis/orientation/axis_label_orientation_selector.tsx b/x-pack/platform/plugins/shared/lens/public/shared_components/axis/orientation/axis_label_orientation_selector.tsx new file mode 100644 index 0000000000000..c3f224635bd19 --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/public/shared_components/axis/orientation/axis_label_orientation_selector.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFormRow, EuiButtonGroup } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +type AxesSettingsConfigKeys = 'x' | 'yRight' | 'yLeft'; + +export const allowedOrientations = [0, -45, -90] as const; +export type Orientation = (typeof allowedOrientations)[number]; + +const orientationOptions: Array<{ + id: string; + value: Orientation; + label: string; +}> = [ + { + id: 'axis_orientation_horizontal', + value: 0, + label: i18n.translate('xpack.lens.shared.axisOrientation.horizontal', { + defaultMessage: 'Horizontal', + }), + }, + { + id: 'axis_orientation_vertical', + value: -90, + label: i18n.translate('xpack.lens.shared.axisOrientation.vertical', { + defaultMessage: 'Vertical', + }), + }, + { + id: 'axis_orientation_angled', + value: -45, + label: i18n.translate('xpack.lens.shared.axisOrientation.angled', { + defaultMessage: 'Angled', + }), + }, +]; + +export interface AxisLabelOrientationSelectorProps { + axis: AxesSettingsConfigKeys; + selectedLabelOrientation: Orientation; + setLabelOrientation: (orientation: Orientation) => void; +} + +export const AxisLabelOrientationSelector: React.FunctionComponent< + AxisLabelOrientationSelectorProps +> = ({ axis = 'x', selectedLabelOrientation, setLabelOrientation }) => { + return ( + + value === selectedLabelOrientation)?.id ?? + orientationOptions[0].id + } + onChange={(optionId: string) => { + const newOrientation = + orientationOptions.find(({ id }) => id === optionId)?.value ?? + orientationOptions[0].value; + setLabelOrientation(newOrientation); + }} + /> + + ); +}; diff --git a/x-pack/platform/plugins/shared/lens/public/shared_components/index.ts b/x-pack/platform/plugins/shared/lens/public/shared_components/index.ts index 1e3f84dd2accb..cd5c6b83ef5d4 100644 --- a/x-pack/platform/plugins/shared/lens/public/shared_components/index.ts +++ b/x-pack/platform/plugins/shared/lens/public/shared_components/index.ts @@ -24,6 +24,9 @@ export * from './helpers'; export { ValueLabelsSettings } from './value_labels_settings'; export { ToolbarTitleSettings } from './axis/title/toolbar_title_settings'; export { AxisTicksSettings } from './axis/ticks/axis_ticks_settings'; +export type { Orientation } from './axis/orientation/axis_label_orientation_selector'; +export { allowedOrientations } from './axis/orientation/axis_label_orientation_selector'; +export { AxisLabelOrientationSelector } from './axis/orientation/axis_label_orientation_selector'; export * from './static_header'; export * from './vis_label'; export { ExperimentalBadge } from './experimental_badge'; diff --git a/x-pack/platform/plugins/shared/lens/public/visualizations/heatmap/toolbar_component.scss b/x-pack/platform/plugins/shared/lens/public/visualizations/heatmap/toolbar_component.scss deleted file mode 100644 index 360a76274416d..0000000000000 --- a/x-pack/platform/plugins/shared/lens/public/visualizations/heatmap/toolbar_component.scss +++ /dev/null @@ -1,3 +0,0 @@ -.lnsVisToolbarAxis__popover { - width: 500px; -} \ No newline at end of file diff --git a/x-pack/platform/plugins/shared/lens/public/visualizations/heatmap/toolbar_component.test.tsx b/x-pack/platform/plugins/shared/lens/public/visualizations/heatmap/toolbar_component.test.tsx new file mode 100644 index 0000000000000..c075e55dc455f --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/public/visualizations/heatmap/toolbar_component.test.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { ComponentProps } from 'react'; +import { HeatmapToolbar } from './toolbar_component'; +import { render, screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { LegendSize } from '@kbn/visualizations-plugin/public'; +import { FramePublicAPI } from '../../types'; +import { HeatmapVisualizationState } from './types'; +import { HeatmapGridConfigResult } from '@kbn/expression-heatmap-plugin/common'; + +type Props = ComponentProps; + +const defaultProps: Props = { + state: { + layerId: '1', + layerType: 'data', + shape: 'heatmap', + xAccessor: 'x', + legend: { + isVisible: true, + legendSize: LegendSize.AUTO, + }, + gridConfig: { + isXAxisLabelVisible: true, + isXAxisTitleVisible: false, + } as HeatmapGridConfigResult, + } as HeatmapVisualizationState, + setState: jest.fn(), + frame: { + datasourceLayers: {}, + } as FramePublicAPI, +}; + +const renderComponent = (props: Partial = {}) => { + return render(); +}; + +const clickButtonByName = async (name: string | RegExp, container?: HTMLElement) => { + const query = container ? within(container) : screen; + await userEvent.click(query.getByRole('button', { name })); +}; + +const clickHorizontalAxisButton = async () => { + await clickButtonByName(/horizontal axis/i); +}; + +describe('HeatmapToolbar', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should have called setState with the proper value of xAxisLabelRotation', async () => { + renderComponent(); + await clickHorizontalAxisButton(); + + const orientationGroup = screen.getByRole('group', { name: /orientation/i }); + await clickButtonByName(/vertical/i, orientationGroup); + expect(defaultProps.setState).toBeCalledTimes(1); + expect(defaultProps.setState).toBeCalledWith({ + ...defaultProps.state, + gridConfig: { ...defaultProps.state.gridConfig, xAxisLabelRotation: -90 }, + }); + }); + + it('should hide the orientation group if isXAxisLabelVisible it set to not visible', async () => { + const { rerender } = renderComponent(); + await clickHorizontalAxisButton(); + + const orientationGroup = screen.getByRole('group', { name: /orientation/i }); + expect(orientationGroup).toBeInTheDocument(); + + rerender( + + ); + await clickHorizontalAxisButton(); + + const updatedOrientationGroup = screen.queryByRole('group', { name: /orientation/i }); + expect(updatedOrientationGroup).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/platform/plugins/shared/lens/public/visualizations/heatmap/toolbar_component.tsx b/x-pack/platform/plugins/shared/lens/public/visualizations/heatmap/toolbar_component.tsx index cd1dd560954c8..c3112dadf4689 100644 --- a/x-pack/platform/plugins/shared/lens/public/visualizations/heatmap/toolbar_component.tsx +++ b/x-pack/platform/plugins/shared/lens/public/visualizations/heatmap/toolbar_component.tsx @@ -19,13 +19,15 @@ import { ValueLabelsSettings, ToolbarTitleSettings, AxisTicksSettings, + AxisLabelOrientationSelector, + allowedOrientations, } from '../../shared_components'; +import type { Orientation } from '../../shared_components'; import type { HeatmapVisualizationState } from './types'; import { getDefaultVisualValuesForLayer } from '../../shared_components/datasource_default_values'; -import './toolbar_component.scss'; const PANEL_STYLE = { - width: '460px', + width: '500px', }; const legendOptions: Array<{ id: string; value: 'auto' | 'show' | 'hide'; label: string }> = [ @@ -59,6 +61,8 @@ export const HeatmapToolbar = memo( const [hadAutoLegendSize] = useState(() => legendSize === LegendSize.AUTO); + const isXAxisLabelVisible = state?.gridConfig.isXAxisLabelVisible; + return ( @@ -99,7 +103,7 @@ export const HeatmapToolbar = memo( groupPosition="left" isDisabled={!Boolean(state?.yAccessor)} buttonDataTestSubj="lnsHeatmapVerticalAxisButton" - panelClassName="lnsVisToolbarAxis__popover" + panelStyle={PANEL_STYLE} > + {isXAxisLabelVisible && ( + { + setState({ + ...state, + gridConfig: { ...state.gridConfig, xAxisLabelRotation: orientation }, + }); + }} + /> + )} diff --git a/x-pack/platform/plugins/shared/lens/public/visualizations/heatmap/visualization.tsx b/x-pack/platform/plugins/shared/lens/public/visualizations/heatmap/visualization.tsx index 4dfb17241866f..a563d18abaec4 100644 --- a/x-pack/platform/plugins/shared/lens/public/visualizations/heatmap/visualization.tsx +++ b/x-pack/platform/plugins/shared/lens/public/visualizations/heatmap/visualization.tsx @@ -345,6 +345,7 @@ export const getHeatmapVisualization = ({ yTitle: state.gridConfig.yTitle, // X-axis isXAxisLabelVisible: state.gridConfig.isXAxisLabelVisible, + xAxisLabelRotation: state.gridConfig.xAxisLabelRotation, isXAxisTitleVisible: state.gridConfig.isXAxisTitleVisible ?? false, xTitle: state.gridConfig.xTitle, }