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.
#### 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,
}