mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Lens] Display legend inside chart (#105571)
* [Lens] Legend inside chart * Apply design feedback * Add unit tests * Update snapshot * Add disabled state and unit tests * revert css change * Address PR comments * Reduce the max columns to 5 * Address last comments * minor * Add a comment Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
f1539ddd29
commit
1bfdc72692
12 changed files with 704 additions and 103 deletions
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* 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 { Position } 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 = {
|
||||
onLocationChange: jest.fn(),
|
||||
onPositionChange: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
it('should have default the Position to right when no position is given', () => {
|
||||
const component = shallow(<LegendLocationSettings {...props} />);
|
||||
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(<LegendLocationSettings {...props} />);
|
||||
component.find('[data-test-subj="lens-legend-position-btn"]').simulate('change');
|
||||
expect(props.onPositionChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should disable the position group if isDisabled prop is true', () => {
|
||||
const component = shallow(<LegendLocationSettings {...props} isDisabled />);
|
||||
expect(
|
||||
component.find('[data-test-subj="lens-legend-position-btn"]').prop('isDisabled')
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
it('should hide the position button group if location inside is given', () => {
|
||||
const newProps = {
|
||||
...props,
|
||||
location: 'inside',
|
||||
} as LegendLocationSettingsProps;
|
||||
const component = shallow(<LegendLocationSettings {...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',
|
||||
} as LegendLocationSettingsProps;
|
||||
const component = shallow(<LegendLocationSettings {...newProps} />);
|
||||
expect(component.find('[data-test-subj="lens-legend-location-btn"]').length).toEqual(1);
|
||||
});
|
||||
|
||||
it('should have selected the given location', () => {
|
||||
const newProps = {
|
||||
...props,
|
||||
location: 'inside',
|
||||
} as LegendLocationSettingsProps;
|
||||
const component = shallow(<LegendLocationSettings {...newProps} />);
|
||||
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 newProps = {
|
||||
...props,
|
||||
location: 'inside',
|
||||
} as LegendLocationSettingsProps;
|
||||
const component = shallow(<LegendLocationSettings {...newProps} />);
|
||||
component
|
||||
.find('[data-test-subj="lens-legend-location-btn"]')
|
||||
.simulate('change', 'xy_location_outside');
|
||||
expect(props.onLocationChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should default the alignment to top right when no value is given', () => {
|
||||
const newProps = {
|
||||
...props,
|
||||
location: 'inside',
|
||||
} as LegendLocationSettingsProps;
|
||||
const component = shallow(<LegendLocationSettings {...newProps} />);
|
||||
expect(
|
||||
component.find('[data-test-subj="lens-legend-inside-alignment-btn"]').prop('idSelected')
|
||||
).toEqual('xy_location_alignment_top_right');
|
||||
});
|
||||
|
||||
it('should have called the onAlignmentChange function on ButtonGroup change', () => {
|
||||
const newProps = {
|
||||
...props,
|
||||
onAlignmentChange: jest.fn(),
|
||||
location: 'inside',
|
||||
} as LegendLocationSettingsProps;
|
||||
const component = shallow(<LegendLocationSettings {...newProps} />);
|
||||
component
|
||||
.find('[data-test-subj="lens-legend-inside-alignment-btn"]')
|
||||
.simulate('change', 'xy_location_alignment_top_left');
|
||||
expect(newProps.onAlignmentChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should have default the columns input to 1 when no value is given', () => {
|
||||
const newProps = {
|
||||
...props,
|
||||
location: 'inside',
|
||||
} as LegendLocationSettingsProps;
|
||||
const component = mount(<LegendLocationSettings {...newProps} />);
|
||||
expect(
|
||||
component.find('[data-test-subj="lens-legend-location-columns-input"]').at(0).prop('value')
|
||||
).toEqual(1);
|
||||
});
|
||||
|
||||
it('should disable the components when is Disabled is true', () => {
|
||||
const newProps = {
|
||||
...props,
|
||||
location: 'inside',
|
||||
isDisabled: true,
|
||||
} as LegendLocationSettingsProps;
|
||||
const component = shallow(<LegendLocationSettings {...newProps} />);
|
||||
expect(
|
||||
component.find('[data-test-subj="lens-legend-location-btn"]').prop('isDisabled')
|
||||
).toEqual(true);
|
||||
expect(
|
||||
component.find('[data-test-subj="lens-legend-inside-alignment-btn"]').prop('isDisabled')
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,329 @@
|
|||
/*
|
||||
* 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, EuiFieldNumber } from '@elastic/eui';
|
||||
import { VerticalAlignment, HorizontalAlignment, Position } from '@elastic/charts';
|
||||
import { useDebouncedValue } from './debounced_value';
|
||||
import { TooltipWrapper } from './tooltip_wrapper';
|
||||
|
||||
export interface LegendLocationSettingsProps {
|
||||
/**
|
||||
* Sets the legend position
|
||||
*/
|
||||
position?: Position;
|
||||
/**
|
||||
* Callback on position option change
|
||||
*/
|
||||
onPositionChange: (id: string) => void;
|
||||
/**
|
||||
* 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) => 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 toggleButtonsIcons = [
|
||||
{
|
||||
id: Position.Top,
|
||||
label: i18n.translate('xpack.lens.shared.legendPositionTop', {
|
||||
defaultMessage: 'Top',
|
||||
}),
|
||||
iconType: 'arrowUp',
|
||||
},
|
||||
{
|
||||
id: Position.Right,
|
||||
label: i18n.translate('xpack.lens.shared.legendPositionRight', {
|
||||
defaultMessage: 'Right',
|
||||
}),
|
||||
iconType: 'arrowRight',
|
||||
},
|
||||
{
|
||||
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',
|
||||
},
|
||||
];
|
||||
|
||||
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 locationAlignmentButtonsIcons: Array<{
|
||||
id: string;
|
||||
value: 'bottom_left' | 'bottom_right' | 'top_left' | 'top_right';
|
||||
label: string;
|
||||
iconType: string;
|
||||
}> = [
|
||||
{
|
||||
id: 'xy_location_alignment_top_right',
|
||||
value: 'top_right',
|
||||
label: i18n.translate('xpack.lens.shared.legendLocationTopRight', {
|
||||
defaultMessage: 'Top right',
|
||||
}),
|
||||
iconType: 'editorPositionTopRight',
|
||||
},
|
||||
{
|
||||
id: 'xy_location_alignment_top_left',
|
||||
value: 'top_left',
|
||||
label: i18n.translate('xpack.lens.shared.legendLocationTopLeft', {
|
||||
defaultMessage: 'Top left',
|
||||
}),
|
||||
iconType: 'editorPositionTopLeft',
|
||||
},
|
||||
{
|
||||
id: 'xy_location_alignment_bottom_right',
|
||||
value: 'bottom_right',
|
||||
label: i18n.translate('xpack.lens.shared.legendLocationBottomRight', {
|
||||
defaultMessage: 'Bottom right',
|
||||
}),
|
||||
iconType: 'editorPositionBottomRight',
|
||||
},
|
||||
{
|
||||
id: 'xy_location_alignment_bottom_left',
|
||||
value: 'bottom_left',
|
||||
label: i18n.translate('xpack.lens.shared.legendLocationBottomLeft', {
|
||||
defaultMessage: 'Bottom left',
|
||||
}),
|
||||
iconType: 'editorPositionBottomLeft',
|
||||
},
|
||||
];
|
||||
|
||||
const FloatingColumnsInput = ({
|
||||
value,
|
||||
setValue,
|
||||
isDisabled,
|
||||
}: {
|
||||
value: number;
|
||||
setValue: (value: number) => void;
|
||||
isDisabled: boolean;
|
||||
}) => {
|
||||
const { inputValue, handleInputChange } = useDebouncedValue({ value, onChange: setValue });
|
||||
return (
|
||||
<EuiFieldNumber
|
||||
data-test-subj="lens-legend-location-columns-input"
|
||||
value={inputValue}
|
||||
min={1}
|
||||
max={5}
|
||||
compressed
|
||||
disabled={isDisabled}
|
||||
onChange={(e) => {
|
||||
handleInputChange(Number(e.target.value));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const LegendLocationSettings: React.FunctionComponent<LegendLocationSettingsProps> = ({
|
||||
location,
|
||||
onLocationChange = () => {},
|
||||
position,
|
||||
onPositionChange,
|
||||
verticalAlignment,
|
||||
horizontalAlignment,
|
||||
onAlignmentChange = () => {},
|
||||
floatingColumns,
|
||||
onFloatingColumnsChange = () => {},
|
||||
isDisabled = false,
|
||||
}) => {
|
||||
const alignment = `${verticalAlignment || VerticalAlignment.Top}_${
|
||||
horizontalAlignment || HorizontalAlignment.Right
|
||||
}`;
|
||||
return (
|
||||
<>
|
||||
{location && (
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
label={i18n.translate('xpack.lens.shared.legendLocationLabel', {
|
||||
defaultMessage: 'Location',
|
||||
})}
|
||||
>
|
||||
<TooltipWrapper
|
||||
tooltipContent={i18n.translate('xpack.lens.shared.legendVisibleTooltip', {
|
||||
defaultMessage: 'Requires legend to be shown',
|
||||
})}
|
||||
condition={isDisabled}
|
||||
position="top"
|
||||
delay="regular"
|
||||
display="block"
|
||||
>
|
||||
<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);
|
||||
}}
|
||||
/>
|
||||
</TooltipWrapper>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
label={i18n.translate('xpack.lens.shared.legendInsideAlignmentLabel', {
|
||||
defaultMessage: 'Alignment',
|
||||
})}
|
||||
>
|
||||
<>
|
||||
{(!location || location === 'outside') && (
|
||||
<TooltipWrapper
|
||||
tooltipContent={i18n.translate('xpack.lens.shared.legendVisibleTooltip', {
|
||||
defaultMessage: 'Requires legend to be shown',
|
||||
})}
|
||||
condition={isDisabled}
|
||||
position="top"
|
||||
delay="regular"
|
||||
display="block"
|
||||
>
|
||||
<EuiButtonGroup
|
||||
legend={i18n.translate('xpack.lens.shared.legendAlignmentLabel', {
|
||||
defaultMessage: 'Alignment',
|
||||
})}
|
||||
isDisabled={isDisabled}
|
||||
data-test-subj="lens-legend-position-btn"
|
||||
name="legendPosition"
|
||||
buttonSize="compressed"
|
||||
options={toggleButtonsIcons}
|
||||
idSelected={position || Position.Right}
|
||||
onChange={onPositionChange}
|
||||
isIconOnly
|
||||
/>
|
||||
</TooltipWrapper>
|
||||
)}
|
||||
{location === 'inside' && (
|
||||
<TooltipWrapper
|
||||
tooltipContent={i18n.translate('xpack.lens.shared.legendVisibleTooltip', {
|
||||
defaultMessage: 'Requires legend to be shown',
|
||||
})}
|
||||
condition={isDisabled}
|
||||
position="top"
|
||||
delay="regular"
|
||||
display="block"
|
||||
>
|
||||
<EuiButtonGroup
|
||||
legend={i18n.translate('xpack.lens.shared.legendInsideLocationAlignmentLabel', {
|
||||
defaultMessage: 'Alignment',
|
||||
})}
|
||||
type="single"
|
||||
data-test-subj="lens-legend-inside-alignment-btn"
|
||||
name="legendInsideAlignment"
|
||||
buttonSize="compressed"
|
||||
isDisabled={isDisabled}
|
||||
options={locationAlignmentButtonsIcons}
|
||||
idSelected={
|
||||
locationAlignmentButtonsIcons.find(({ value }) => value === alignment)!.id
|
||||
}
|
||||
onChange={(optionId) => {
|
||||
const newAlignment = locationAlignmentButtonsIcons.find(
|
||||
({ id }) => id === optionId
|
||||
)!.value;
|
||||
onAlignmentChange(newAlignment);
|
||||
}}
|
||||
isIconOnly
|
||||
/>
|
||||
</TooltipWrapper>
|
||||
)}
|
||||
</>
|
||||
</EuiFormRow>
|
||||
{location && (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.shared.legendInsideColumnsLabel', {
|
||||
defaultMessage: 'Number of columns',
|
||||
})}
|
||||
fullWidth
|
||||
display="columnCompressed"
|
||||
>
|
||||
<TooltipWrapper
|
||||
tooltipContent={
|
||||
isDisabled
|
||||
? i18n.translate('xpack.lens.shared.legendVisibleTooltip', {
|
||||
defaultMessage: 'Requires legend to be shown',
|
||||
})
|
||||
: i18n.translate('xpack.lens.shared.legendInsideTooltip', {
|
||||
defaultMessage: 'Requires legend to be located inside visualization',
|
||||
})
|
||||
}
|
||||
condition={isDisabled || location === 'outside'}
|
||||
position="top"
|
||||
delay="regular"
|
||||
display="block"
|
||||
>
|
||||
<FloatingColumnsInput
|
||||
value={floatingColumns ?? DEFAULT_FLOATING_COLUMNS}
|
||||
setValue={onFloatingColumnsChange}
|
||||
isDisabled={isDisabled || location === 'outside'}
|
||||
/>
|
||||
</TooltipWrapper>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Position } from '@elastic/charts';
|
||||
import { shallowWithIntl as shallow } from '@kbn/test/jest';
|
||||
import { LegendSettingsPopover, LegendSettingsPopoverProps } from './legend_settings_popover';
|
||||
|
||||
|
@ -51,26 +50,6 @@ describe('Legend Settings', () => {
|
|||
expect(props.onDisplayChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should have default the Position to right when no position is given', () => {
|
||||
const component = shallow(<LegendSettingsPopover {...props} />);
|
||||
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(<LegendSettingsPopover {...props} />);
|
||||
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(<LegendSettingsPopover {...props} mode="hide" />);
|
||||
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(<LegendSettingsPopover {...props} renderNestedLegendSwitch />);
|
||||
expect(component.find('[data-test-subj="lens-legend-nested-switch"]')).toHaveLength(1);
|
||||
|
|
|
@ -8,15 +8,21 @@
|
|||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFormRow, EuiButtonGroup, EuiSwitch, EuiSwitchEvent } from '@elastic/eui';
|
||||
import { Position } from '@elastic/charts';
|
||||
import { Position, VerticalAlignment, HorizontalAlignment } from '@elastic/charts';
|
||||
import { ToolbarPopover } from '../shared_components';
|
||||
import { LegendLocationSettings } from './legend_location_settings';
|
||||
import { ToolbarButtonProps } from '../../../../../src/plugins/kibana_react/public';
|
||||
import { TooltipWrapper } from './tooltip_wrapper';
|
||||
|
||||
export interface LegendSettingsPopoverProps {
|
||||
/**
|
||||
* Determines the legend display options
|
||||
*/
|
||||
legendOptions: Array<{ id: string; value: 'auto' | 'show' | 'hide' | 'default'; label: string }>;
|
||||
legendOptions: Array<{
|
||||
id: string;
|
||||
value: 'auto' | 'show' | 'hide' | 'default';
|
||||
label: string;
|
||||
}>;
|
||||
/**
|
||||
* Determines the legend mode
|
||||
*/
|
||||
|
@ -33,6 +39,34 @@ export interface LegendSettingsPopoverProps {
|
|||
* Callback on position option change
|
||||
*/
|
||||
onPositionChange: (id: string) => void;
|
||||
/**
|
||||
* 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) => void;
|
||||
/**
|
||||
* Sets the number of columns for legend inside chart
|
||||
*/
|
||||
floatingColumns?: number;
|
||||
/**
|
||||
* Callback on horizontal alignment option change
|
||||
*/
|
||||
onFloatingColumnsChange?: (value: number) => void;
|
||||
/**
|
||||
* If true, nested legend switch is rendered
|
||||
*/
|
||||
|
@ -63,42 +97,18 @@ export interface LegendSettingsPopoverProps {
|
|||
groupPosition?: ToolbarButtonProps['groupPosition'];
|
||||
}
|
||||
|
||||
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<LegendSettingsPopoverProps> = ({
|
||||
legendOptions,
|
||||
mode,
|
||||
onDisplayChange,
|
||||
position,
|
||||
location,
|
||||
onLocationChange = () => {},
|
||||
verticalAlignment,
|
||||
horizontalAlignment,
|
||||
floatingColumns,
|
||||
onAlignmentChange = () => {},
|
||||
onFloatingColumnsChange = () => {},
|
||||
onPositionChange,
|
||||
renderNestedLegendSwitch,
|
||||
nestedLegend,
|
||||
|
@ -136,26 +146,18 @@ export const LegendSettingsPopover: React.FunctionComponent<LegendSettingsPopove
|
|||
onChange={onDisplayChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
label={i18n.translate('xpack.lens.shared.legendPositionLabel', {
|
||||
defaultMessage: 'Position',
|
||||
})}
|
||||
>
|
||||
<EuiButtonGroup
|
||||
legend={i18n.translate('xpack.lens.shared.legendPositionLabel', {
|
||||
defaultMessage: 'Position',
|
||||
})}
|
||||
isDisabled={mode === 'hide'}
|
||||
data-test-subj="lens-legend-position-btn"
|
||||
name="legendPosition"
|
||||
buttonSize="compressed"
|
||||
options={toggleButtonsIcons}
|
||||
idSelected={position || Position.Right}
|
||||
onChange={onPositionChange}
|
||||
isIconOnly
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<LegendLocationSettings
|
||||
location={location}
|
||||
onLocationChange={onLocationChange}
|
||||
verticalAlignment={verticalAlignment}
|
||||
horizontalAlignment={horizontalAlignment}
|
||||
onAlignmentChange={onAlignmentChange}
|
||||
floatingColumns={floatingColumns}
|
||||
onFloatingColumnsChange={onFloatingColumnsChange}
|
||||
isDisabled={mode === 'hide'}
|
||||
position={position}
|
||||
onPositionChange={onPositionChange}
|
||||
/>
|
||||
{renderNestedLegendSwitch && (
|
||||
<EuiFormRow
|
||||
display="columnCompressedSwitch"
|
||||
|
@ -163,17 +165,27 @@ export const LegendSettingsPopover: React.FunctionComponent<LegendSettingsPopove
|
|||
defaultMessage: 'Nested',
|
||||
})}
|
||||
>
|
||||
<EuiSwitch
|
||||
compressed
|
||||
label={i18n.translate('xpack.lens.pieChart.nestedLegendLabel', {
|
||||
defaultMessage: 'Nested',
|
||||
<TooltipWrapper
|
||||
tooltipContent={i18n.translate('xpack.lens.shared.legendVisibleTooltip', {
|
||||
defaultMessage: 'Requires legend to be shown',
|
||||
})}
|
||||
data-test-subj="lens-legend-nested-switch"
|
||||
showLabel={false}
|
||||
disabled={mode === 'hide'}
|
||||
checked={!!nestedLegend}
|
||||
onChange={onNestedLegendChange}
|
||||
/>
|
||||
condition={mode === 'hide'}
|
||||
position="top"
|
||||
delay="regular"
|
||||
display="block"
|
||||
>
|
||||
<EuiSwitch
|
||||
compressed
|
||||
label={i18n.translate('xpack.lens.pieChart.nestedLegendLabel', {
|
||||
defaultMessage: 'Nested',
|
||||
})}
|
||||
data-test-subj="lens-legend-nested-switch"
|
||||
showLabel={false}
|
||||
disabled={mode === 'hide'}
|
||||
checked={!!nestedLegend}
|
||||
onChange={onNestedLegendChange}
|
||||
/>
|
||||
</TooltipWrapper>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
{renderValueInLegendSwitch && (
|
||||
|
@ -183,17 +195,27 @@ export const LegendSettingsPopover: React.FunctionComponent<LegendSettingsPopove
|
|||
defaultMessage: 'Show value',
|
||||
})}
|
||||
>
|
||||
<EuiSwitch
|
||||
compressed
|
||||
label={i18n.translate('xpack.lens.shared.valueInLegendLabel', {
|
||||
defaultMessage: 'Show value',
|
||||
<TooltipWrapper
|
||||
tooltipContent={i18n.translate('xpack.lens.shared.legendVisibleTooltip', {
|
||||
defaultMessage: 'Requires legend to be shown',
|
||||
})}
|
||||
data-test-subj="lens-legend-show-value"
|
||||
showLabel={false}
|
||||
disabled={mode === 'hide'}
|
||||
checked={!!valueInLegend}
|
||||
onChange={onValueInLegendChange}
|
||||
/>
|
||||
condition={mode === 'hide'}
|
||||
position="top"
|
||||
delay="regular"
|
||||
display="block"
|
||||
>
|
||||
<EuiSwitch
|
||||
compressed
|
||||
label={i18n.translate('xpack.lens.shared.valueInLegendLabel', {
|
||||
defaultMessage: 'Show value',
|
||||
})}
|
||||
data-test-subj="lens-legend-show-value"
|
||||
showLabel={false}
|
||||
disabled={mode === 'hide'}
|
||||
checked={!!valueInLegend}
|
||||
onChange={onValueInLegendChange}
|
||||
/>
|
||||
</TooltipWrapper>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
</ToolbarPopover>
|
||||
|
|
|
@ -114,6 +114,9 @@ Object {
|
|||
"chain": Array [
|
||||
Object {
|
||||
"arguments": Object {
|
||||
"floatingColumns": Array [],
|
||||
"horizontalAlignment": Array [],
|
||||
"isInside": Array [],
|
||||
"isVisible": Array [
|
||||
true,
|
||||
],
|
||||
|
@ -121,6 +124,7 @@ Object {
|
|||
"bottom",
|
||||
],
|
||||
"showSingleSeries": Array [],
|
||||
"verticalAlignment": Array [],
|
||||
},
|
||||
"function": "lens_xy_legendConfig",
|
||||
"type": "function",
|
||||
|
|
|
@ -17,6 +17,9 @@ import {
|
|||
XYChartSeriesIdentifier,
|
||||
SeriesNameFn,
|
||||
Fit,
|
||||
HorizontalAlignment,
|
||||
VerticalAlignment,
|
||||
LayoutDirection,
|
||||
} from '@elastic/charts';
|
||||
import { PaletteOutput } from 'src/plugins/charts/public';
|
||||
import {
|
||||
|
@ -2251,6 +2254,30 @@ describe('xy_expression', () => {
|
|||
expect(component.find(Settings).prop('showLegend')).toEqual(true);
|
||||
});
|
||||
|
||||
test('it should populate the correct legendPosition if isInside is set', () => {
|
||||
const { data, args } = sampleArgs();
|
||||
|
||||
const component = shallow(
|
||||
<XYChart
|
||||
{...defaultProps}
|
||||
data={{ ...data }}
|
||||
args={{
|
||||
...args,
|
||||
layers: [{ ...args.layers[0], accessors: ['a'], splitAccessor: undefined }],
|
||||
legend: { ...args.legend, isVisible: true, isInside: true },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find(Settings).prop('legendPosition')).toEqual({
|
||||
vAlign: VerticalAlignment.Top,
|
||||
hAlign: HorizontalAlignment.Right,
|
||||
direction: LayoutDirection.Vertical,
|
||||
floating: true,
|
||||
floatingColumns: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('it not show legend if isVisible is set to false', () => {
|
||||
const { data, args } = sampleArgs();
|
||||
|
||||
|
|
|
@ -22,9 +22,11 @@ import {
|
|||
StackMode,
|
||||
VerticalAlignment,
|
||||
HorizontalAlignment,
|
||||
LayoutDirection,
|
||||
ElementClickListener,
|
||||
BrushEndListener,
|
||||
CurveType,
|
||||
LegendPositionConfig,
|
||||
LabelOverflowConstraint,
|
||||
} from '@elastic/charts';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
|
@ -602,6 +604,14 @@ export function XYChart({
|
|||
onSelectRange(context);
|
||||
};
|
||||
|
||||
const legendInsideParams = {
|
||||
vAlign: legend.verticalAlignment ?? VerticalAlignment.Top,
|
||||
hAlign: legend?.horizontalAlignment ?? HorizontalAlignment.Right,
|
||||
direction: LayoutDirection.Vertical,
|
||||
floating: true,
|
||||
floatingColumns: legend?.floatingColumns ?? 1,
|
||||
} as LegendPositionConfig;
|
||||
|
||||
return (
|
||||
<Chart>
|
||||
<Settings
|
||||
|
@ -611,7 +621,7 @@ export function XYChart({
|
|||
? chartHasMoreThanOneSeries
|
||||
: legend.isVisible
|
||||
}
|
||||
legendPosition={legend.position}
|
||||
legendPosition={legend?.isInside ? legendInsideParams : legend.position}
|
||||
theme={{
|
||||
...chartTheme,
|
||||
barSeriesStyle: {
|
||||
|
|
|
@ -142,6 +142,18 @@ export const buildExpression = (
|
|||
? [state.legend.showSingleSeries]
|
||||
: [],
|
||||
position: [state.legend.position],
|
||||
isInside: state.legend.isInside ? [state.legend.isInside] : [],
|
||||
horizontalAlignment: state.legend.horizontalAlignment
|
||||
? [state.legend.horizontalAlignment]
|
||||
: [],
|
||||
verticalAlignment: state.legend.verticalAlignment
|
||||
? [state.legend.verticalAlignment]
|
||||
: [],
|
||||
// ensure that even if the user types more than 5 columns
|
||||
// we will only show 5
|
||||
floatingColumns: state.legend.floatingColumns
|
||||
? [Math.min(5, state.legend.floatingColumns)]
|
||||
: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Position } from '@elastic/charts';
|
||||
import { Position, VerticalAlignment, HorizontalAlignment } from '@elastic/charts';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { PaletteOutput } from 'src/plugins/charts/public';
|
||||
import { ArgumentType, ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
|
||||
|
@ -36,6 +36,22 @@ export interface LegendConfig {
|
|||
* Flag whether the legend should be shown even with just a single series
|
||||
*/
|
||||
showSingleSeries?: boolean;
|
||||
/**
|
||||
* Flag whether the legend is inside the chart
|
||||
*/
|
||||
isInside?: boolean;
|
||||
/**
|
||||
* Horizontal Alignment of the legend when it is set inside chart
|
||||
*/
|
||||
horizontalAlignment?: HorizontalAlignment;
|
||||
/**
|
||||
* Vertical Alignment of the legend when it is set inside chart
|
||||
*/
|
||||
verticalAlignment?: VerticalAlignment;
|
||||
/**
|
||||
* Number of columns when legend is set inside chart
|
||||
*/
|
||||
floatingColumns?: number;
|
||||
}
|
||||
|
||||
type LegendConfigResult = LegendConfig & { type: 'lens_xy_legendConfig' };
|
||||
|
@ -71,6 +87,34 @@ export const legendConfig: ExpressionFunctionDefinition<
|
|||
defaultMessage: 'Specifies whether a legend with just a single entry should be shown',
|
||||
}),
|
||||
},
|
||||
isInside: {
|
||||
types: ['boolean'],
|
||||
help: i18n.translate('xpack.lens.xyChart.isInside.help', {
|
||||
defaultMessage: 'Specifies whether a legend is inside the chart',
|
||||
}),
|
||||
},
|
||||
horizontalAlignment: {
|
||||
types: ['string'],
|
||||
options: [HorizontalAlignment.Right, HorizontalAlignment.Left],
|
||||
help: i18n.translate('xpack.lens.xyChart.horizontalAlignment.help', {
|
||||
defaultMessage:
|
||||
'Specifies the horizontal alignment of the legend when it is displayed inside chart.',
|
||||
}),
|
||||
},
|
||||
verticalAlignment: {
|
||||
types: ['string'],
|
||||
options: [VerticalAlignment.Top, VerticalAlignment.Bottom],
|
||||
help: i18n.translate('xpack.lens.xyChart.verticalAlignment.help', {
|
||||
defaultMessage:
|
||||
'Specifies the vertical alignment of the legend when it is displayed inside chart.',
|
||||
}),
|
||||
},
|
||||
floatingColumns: {
|
||||
types: ['number'],
|
||||
help: i18n.translate('xpack.lens.xyChart.floatingColumns.help', {
|
||||
defaultMessage: 'Specifies the number of columns when legend is displayed inside chart.',
|
||||
}),
|
||||
},
|
||||
},
|
||||
fn: function fn(input: unknown, args: LegendConfig) {
|
||||
return {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import './xy_config_panel.scss';
|
||||
import React, { useMemo, useState, memo, useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Position, ScaleType } from '@elastic/charts';
|
||||
import { Position, ScaleType, VerticalAlignment, HorizontalAlignment } from '@elastic/charts';
|
||||
import { debounce } from 'lodash';
|
||||
import {
|
||||
EuiButtonGroup,
|
||||
|
@ -61,7 +61,11 @@ function updateLayer(state: State, layer: UnwrapArray<State['layers']>, index: n
|
|||
};
|
||||
}
|
||||
|
||||
const legendOptions: Array<{ id: string; value: 'auto' | 'show' | 'hide'; label: string }> = [
|
||||
const legendOptions: Array<{
|
||||
id: string;
|
||||
value: 'auto' | 'show' | 'hide';
|
||||
label: string;
|
||||
}> = [
|
||||
{
|
||||
id: `xy_legend_auto`,
|
||||
value: 'auto',
|
||||
|
@ -319,32 +323,72 @@ export const XyToolbar = memo(function XyToolbar(props: VisualizationToolbarProp
|
|||
<LegendSettingsPopover
|
||||
legendOptions={legendOptions}
|
||||
mode={legendMode}
|
||||
location={state?.legend.isInside ? 'inside' : 'outside'}
|
||||
onLocationChange={(location) => {
|
||||
setState({
|
||||
...state,
|
||||
legend: {
|
||||
...state.legend,
|
||||
isInside: location === 'inside',
|
||||
},
|
||||
});
|
||||
}}
|
||||
onDisplayChange={(optionId) => {
|
||||
const newMode = legendOptions.find(({ id }) => id === optionId)!.value;
|
||||
if (newMode === 'auto') {
|
||||
setState({
|
||||
...state,
|
||||
legend: { ...state.legend, isVisible: true, showSingleSeries: false },
|
||||
legend: {
|
||||
...state.legend,
|
||||
isVisible: true,
|
||||
showSingleSeries: false,
|
||||
},
|
||||
});
|
||||
} else if (newMode === 'show') {
|
||||
setState({
|
||||
...state,
|
||||
legend: { ...state.legend, isVisible: true, showSingleSeries: true },
|
||||
legend: {
|
||||
...state.legend,
|
||||
isVisible: true,
|
||||
showSingleSeries: true,
|
||||
},
|
||||
});
|
||||
} else if (newMode === 'hide') {
|
||||
setState({
|
||||
...state,
|
||||
legend: { ...state.legend, isVisible: false, showSingleSeries: false },
|
||||
legend: {
|
||||
...state.legend,
|
||||
isVisible: false,
|
||||
showSingleSeries: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
position={state?.legend.position}
|
||||
horizontalAlignment={state?.legend.horizontalAlignment}
|
||||
verticalAlignment={state?.legend.verticalAlignment}
|
||||
floatingColumns={state?.legend.floatingColumns}
|
||||
onFloatingColumnsChange={(val) => {
|
||||
setState({
|
||||
...state,
|
||||
legend: { ...state.legend, floatingColumns: val },
|
||||
});
|
||||
}}
|
||||
onPositionChange={(id) => {
|
||||
setState({
|
||||
...state,
|
||||
legend: { ...state.legend, position: id as Position },
|
||||
});
|
||||
}}
|
||||
onAlignmentChange={(value) => {
|
||||
const [vertical, horizontal] = value.split('_');
|
||||
const verticalAlignment = vertical as VerticalAlignment;
|
||||
const horizontalAlignment = horizontal as HorizontalAlignment;
|
||||
setState({
|
||||
...state,
|
||||
legend: { ...state.legend, verticalAlignment, horizontalAlignment },
|
||||
});
|
||||
}}
|
||||
renderValueInLegendSwitch={nonOrdinalXAxis}
|
||||
valueInLegend={state?.valuesInLegend}
|
||||
onValueInLegendChange={() => {
|
||||
|
|
|
@ -12712,7 +12712,6 @@
|
|||
"xpack.lens.shared.curveLabel": "",
|
||||
"xpack.lens.shared.legendLabel": "凡例",
|
||||
"xpack.lens.shared.legendPositionBottom": "一番下",
|
||||
"xpack.lens.shared.legendPositionLabel": "位置",
|
||||
"xpack.lens.shared.legendPositionLeft": "左",
|
||||
"xpack.lens.shared.legendPositionRight": "右",
|
||||
"xpack.lens.shared.legendPositionTop": "トップ",
|
||||
|
|
|
@ -12884,7 +12884,6 @@
|
|||
"xpack.lens.shared.curveLabel": "视觉选项",
|
||||
"xpack.lens.shared.legendLabel": "图例",
|
||||
"xpack.lens.shared.legendPositionBottom": "底部",
|
||||
"xpack.lens.shared.legendPositionLabel": "位置",
|
||||
"xpack.lens.shared.legendPositionLeft": "左",
|
||||
"xpack.lens.shared.legendPositionRight": "右",
|
||||
"xpack.lens.shared.legendPositionTop": "顶部",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue