Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Lens] Display legend inside chart #105571

Merged
merged 14 commits into from
Jul 21, 2021
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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 { VerticalAlignment, HorizontalAlignment } from '@elastic/charts';
import { shallowWithIntl as shallow, mountWithIntl as mount } from '@kbn/test/jest';
import { LegendLocationSettings, LegendLocationSettingsProps } from './legend_location_settings';

describe('Legend Location Settings', () => {
let props: LegendLocationSettingsProps;
beforeEach(() => {
props = {
location: 'inside',
onLocationChange: jest.fn(),
};
});

it('should have selected the given location', () => {
const component = shallow(<LegendLocationSettings {...props} />);
expect(
component.find('[data-test-subj="lens-legend-location-btn"]').prop('idSelected')
).toEqual('xy_location_inside');
});

it('should have called the onLocationChange function on ButtonGroup change', () => {
const component = shallow(<LegendLocationSettings {...props} />);
component
.find('[data-test-subj="lens-legend-location-btn"]')
.simulate('change', 'xy_location_outside');
expect(props.onLocationChange).toHaveBeenCalled();
});

it('should have default the Vertical alignment to top when no value is given', () => {
const component = shallow(<LegendLocationSettings {...props} />);
expect(
component.find('[data-test-subj="lens-legend-inside-valign-btn"]').prop('idSelected')
).toEqual(VerticalAlignment.Top);
});

it('should have default the Horizontal alignment to right when no value is given', () => {
const component = shallow(<LegendLocationSettings {...props} />);
expect(
component.find('[data-test-subj="lens-legend-inside-halign-btn"]').prop('idSelected')
).toEqual(HorizontalAlignment.Right);
});

it('should have called the onAlignmentChange function on ButtonGroup change', () => {
const newProps = { ...props, onAlignmentChange: jest.fn() };
const component = shallow(<LegendLocationSettings {...newProps} />);
component.find('[data-test-subj="lens-legend-inside-halign-btn"]').simulate('change');
expect(newProps.onAlignmentChange).toHaveBeenCalled();
});

it('should have default the columns slider to 1 when no value is given', () => {
const component = mount(<LegendLocationSettings {...props} />);
expect(
component.find('[data-test-subj="lens-legend-location-columns-slider"]').at(0).prop('value')
).toEqual(1);
});

it('should disable the components when is Disabled is true', () => {
const component = shallow(<LegendLocationSettings {...props} isDisabled={true} />);
expect(
component.find('[data-test-subj="lens-legend-location-btn"]').prop('isDisabled')
).toEqual(true);
expect(
component.find('[data-test-subj="lens-legend-inside-valign-btn"]').prop('isDisabled')
).toEqual(true);
expect(
component.find('[data-test-subj="lens-legend-inside-halign-btn"]').prop('isDisabled')
).toEqual(true);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { EuiFormRow, EuiButtonGroup, EuiRange } from '@elastic/eui';
import { VerticalAlignment, HorizontalAlignment } from '@elastic/charts';
import { useDebouncedValue } from './debounced_value';

export interface LegendLocationSettingsProps {
/**
* Determines the legend location
*/
location: 'inside' | 'outside';
/**
* Callback on location option change
*/
onLocationChange: (id: string) => void;
/**
* Sets the vertical alignment for legend inside chart
*/
verticalAlignment?: VerticalAlignment;
/**
* Sets the vertical alignment for legend inside chart
*/
horizontalAlignment?: HorizontalAlignment;
/**
* Callback on horizontal alignment option change
*/
onAlignmentChange?: (id: string, type: string) => void;
/**
* Sets the number of columns for legend inside chart
*/
floatingColumns?: number;
/**
* Callback on horizontal alignment option change
*/
onFloatingColumnsChange?: (value: number) => void;
/**
* Flag to disable the location settings
*/
isDisabled?: boolean;
}

const DEFAULT_FLOATING_COLUMNS = 1;

const locationOptions: Array<{
id: string;
value: 'outside' | 'inside';
label: string;
}> = [
{
id: `xy_location_outside`,
value: 'outside',
label: i18n.translate('xpack.lens.xyChart.legendLocation.outside', {
defaultMessage: 'Outside',
}),
},
{
id: `xy_location_inside`,
value: 'inside',
label: i18n.translate('xpack.lens.xyChart.legendLocation.inside', {
defaultMessage: 'Inside',
}),
},
];

const verticalAlignButtonsIcons = [
{
id: VerticalAlignment.Bottom,
label: i18n.translate('xpack.lens.shared.legendVerticalAlignBottom', {
defaultMessage: 'Bottom',
}),
iconType: 'arrowDown',
},
{
id: VerticalAlignment.Top,
label: i18n.translate('xpack.lens.shared.legendVerticalAlignTop', {
defaultMessage: 'Top',
}),
iconType: 'arrowUp',
},
];

const horizontalAlignButtonsIcons = [
{
id: HorizontalAlignment.Left,
label: i18n.translate('xpack.lens.shared.legendHorizontalAlignLeft', {
defaultMessage: 'Left',
}),
iconType: 'arrowLeft',
},
{
id: HorizontalAlignment.Right,
label: i18n.translate('xpack.lens.shared.legendHorizontalAlignRight', {
defaultMessage: 'Right',
}),
iconType: 'arrowRight',
},
];

const FloatingColumnsSlider = ({
value,
setValue,
isDisabled,
}: {
value: number;
setValue: (value: number) => void;
isDisabled: boolean;
}) => {
const { inputValue, handleInputChange } = useDebouncedValue({ value, onChange: setValue });
stratoula marked this conversation as resolved.
Show resolved Hide resolved
return (
<EuiRange
data-test-subj="lens-legend-location-columns-slider"
value={inputValue}
min={0}
stratoula marked this conversation as resolved.
Show resolved Hide resolved
max={10}
showInput
compressed
disabled={isDisabled}
onChange={(e) => {
handleInputChange(Number(e.currentTarget.value));
}}
/>
);
};

export const LegendLocationSettings: React.FunctionComponent<LegendLocationSettingsProps> = ({
location,
onLocationChange = () => {},
verticalAlignment,
horizontalAlignment,
onAlignmentChange = () => {},
floatingColumns,
onFloatingColumnsChange = () => {},
isDisabled = false,
}) => {
return (
<>
<EuiFormRow
display="columnCompressed"
label={i18n.translate('xpack.lens.shared.legendLocationLabel', {
defaultMessage: 'Location',
})}
>
<EuiButtonGroup
isFullWidth
legend={i18n.translate('xpack.lens.shared.legendLocationLabel', {
defaultMessage: 'Location',
})}
data-test-subj="lens-legend-location-btn"
name="legendLocation"
buttonSize="compressed"
options={locationOptions}
isDisabled={isDisabled}
idSelected={locationOptions.find(({ value }) => value === location)!.id}
onChange={(optionId) => {
const newLocation = locationOptions.find(({ id }) => id === optionId)!.value;
onLocationChange(newLocation);
}}
/>
</EuiFormRow>
{location === 'inside' && (
<>
<EuiFormRow
display="columnCompressed"
label={i18n.translate('xpack.lens.shared.legendInsideAlignmentLabel', {
defaultMessage: 'Alignment',
})}
>
<>
<EuiButtonGroup
legend={i18n.translate('xpack.lens.shared.legendInsideVerticalAlignmentLabel', {
defaultMessage: 'Vertical alignment',
})}
type="single"
data-test-subj="lens-legend-inside-valign-btn"
name="legendInsideVAlign"
buttonSize="compressed"
isDisabled={isDisabled}
options={verticalAlignButtonsIcons}
idSelected={verticalAlignment || VerticalAlignment.Top}
onChange={(id) => onAlignmentChange(id, 'vertical')}
isIconOnly
/>
<EuiButtonGroup
legend={i18n.translate('xpack.lens.shared.legendInsideHorizontalAlignLabel', {
defaultMessage: 'Horizontal alignment',
})}
type="single"
data-test-subj="lens-legend-inside-halign-btn"
name="legendInsideHAlign"
buttonSize="compressed"
isDisabled={isDisabled}
options={horizontalAlignButtonsIcons}
idSelected={horizontalAlignment || HorizontalAlignment.Right}
onChange={(id) => onAlignmentChange(id, 'horizontal')}
isIconOnly
/>
</>
stratoula marked this conversation as resolved.
Show resolved Hide resolved
</EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.lens.shared.legendInsideColumnsLabel', {
defaultMessage: 'Number of columns',
})}
fullWidth
display="rowCompressed"
stratoula marked this conversation as resolved.
Show resolved Hide resolved
>
<FloatingColumnsSlider
value={floatingColumns ?? DEFAULT_FLOATING_COLUMNS}
setValue={onFloatingColumnsChange}
isDisabled={isDisabled}
/>
stratoula marked this conversation as resolved.
Show resolved Hide resolved
</EuiFormRow>
</>
)}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import React from 'react';
import { Position } from '@elastic/charts';
import { shallowWithIntl as shallow } from '@kbn/test/jest';
import { LegendSettingsPopover, LegendSettingsPopoverProps } from './legend_settings_popover';
import { LegendLocationSettings } from './legend_location_settings';

describe('Legend Settings', () => {
const legendOptions: Array<{ id: string; value: 'auto' | 'show' | 'hide'; label: string }> = [
Expand Down Expand Up @@ -71,6 +72,25 @@ describe('Legend Settings', () => {
).toEqual(true);
});

it('should hide the position button group if location inside is given', () => {
const newProps = {
...props,
location: 'inside',
} as LegendSettingsPopoverProps;
const component = shallow(<LegendSettingsPopover {...newProps} />);
expect(component.find('[data-test-subj="lens-legend-position-btn"]').length).toEqual(0);
});

it('should render the location settings if location inside is given', () => {
const newProps = {
...props,
location: 'inside',
onLocationChange: jest.fn(),
} as LegendSettingsPopoverProps;
const component = shallow(<LegendSettingsPopover {...newProps} />);
expect(component.find(LegendLocationSettings).length).toEqual(1);
});

it('should enable the Nested Legend Switch when renderNestedLegendSwitch prop is true', () => {
const component = shallow(<LegendSettingsPopover {...props} renderNestedLegendSwitch />);
expect(component.find('[data-test-subj="lens-legend-nested-switch"]')).toHaveLength(1);
Expand Down
Loading