Skip to content

Commit

Permalink
[Lens][Heatmap] Add ability to rotate X axis label (elastic#202143)
Browse files Browse the repository at this point in the history
## Summary

Closes elastic#61248

Adds the ability to rotate the X-axis labels in the heatmap chart.

<img width="2560" alt="Screenshot 2025-01-08 at 16 50 20"
src="https://github.com/user-attachments/assets/0847dd6d-747d-4a4d-bc7e-4da6c903d394"
/>

#### 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>
  • Loading branch information
mariairiartef and mbondyra authored Jan 16, 2025
1 parent c9286ec commit 2f6b9f6
Show file tree
Hide file tree
Showing 10 changed files with 261 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export interface HeatmapGridConfig {
yTitle?: string;
// X-axis
isXAxisLabelVisible: boolean;
xAxisLabelRotation?: number;
isXAxisTitleVisible: boolean;
xTitle?: string;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,8 @@ export const HeatmapComponent: FC<HeatmapRenderProps> = 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%)',
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AxisLabelOrientationSelectorProps>) => {
return render(
<AxisLabelOrientationSelector
axis="x"
selectedLabelOrientation={0}
setLabelOrientation={jest.fn()}
{...propsOverrides}
/>
);
};

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);
});
});
Original file line number Diff line number Diff line change
@@ -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 (
<EuiFormRow
display="columnCompressed"
fullWidth
label={i18n.translate('xpack.lens.shared.axisOrientation.label', {
defaultMessage: 'Orientation',
})}
>
<EuiButtonGroup
isFullWidth
legend={i18n.translate('xpack.lens.shared.axisOrientation.label', {
defaultMessage: 'Orientation',
})}
data-test-subj={`lns${axis}AxisLabelRotationSelector`}
buttonSize="compressed"
options={orientationOptions}
idSelected={
orientationOptions.find(({ value }) => value === selectedLabelOrientation)?.id ??
orientationOptions[0].id
}
onChange={(optionId: string) => {
const newOrientation =
orientationOptions.find(({ id }) => id === optionId)?.value ??
orientationOptions[0].value;
setLabelOrientation(newOrientation);
}}
/>
</EuiFormRow>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<typeof HeatmapToolbar>;

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<Props> = {}) => {
return render(<HeatmapToolbar {...defaultProps} {...props} />);
};

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(
<HeatmapToolbar
{...defaultProps}
state={{
...defaultProps.state,
gridConfig: { ...defaultProps.state.gridConfig, isXAxisLabelVisible: false },
}}
/>
);
await clickHorizontalAxisButton();

const updatedOrientationGroup = screen.queryByRole('group', { name: /orientation/i });
expect(updatedOrientationGroup).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 }> = [
Expand Down Expand Up @@ -59,6 +61,8 @@ export const HeatmapToolbar = memo(

const [hadAutoLegendSize] = useState(() => legendSize === LegendSize.AUTO);

const isXAxisLabelVisible = state?.gridConfig.isXAxisLabelVisible;

return (
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexItem grow={false}>
Expand Down Expand Up @@ -99,7 +103,7 @@ export const HeatmapToolbar = memo(
groupPosition="left"
isDisabled={!Boolean(state?.yAccessor)}
buttonDataTestSubj="lnsHeatmapVerticalAxisButton"
panelClassName="lnsVisToolbarAxis__popover"
panelStyle={PANEL_STYLE}
>
<ToolbarTitleSettings
settingId="yLeft"
Expand Down Expand Up @@ -146,6 +150,7 @@ export const HeatmapToolbar = memo(
groupPosition="center"
isDisabled={!Boolean(state?.xAccessor)}
buttonDataTestSubj="lnsHeatmapHorizontalAxisButton"
panelStyle={PANEL_STYLE}
>
<ToolbarTitleSettings
settingId="x"
Expand Down Expand Up @@ -173,8 +178,26 @@ export const HeatmapToolbar = memo(
},
});
}}
isAxisLabelVisible={state?.gridConfig.isXAxisLabelVisible}
isAxisLabelVisible={isXAxisLabelVisible}
/>
{isXAxisLabelVisible && (
<AxisLabelOrientationSelector
axis="x"
selectedLabelOrientation={
allowedOrientations.includes(
state.gridConfig.xAxisLabelRotation as Orientation
)
? (state.gridConfig.xAxisLabelRotation as Orientation)
: 0 // Default to 0 if the value is not valid
}
setLabelOrientation={(orientation) => {
setState({
...state,
gridConfig: { ...state.gridConfig, xAxisLabelRotation: orientation },
});
}}
/>
)}
</ToolbarPopover>
</TooltipWrapper>
</EuiFlexGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down

0 comments on commit 2f6b9f6

Please sign in to comment.