[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:
Stratoula Kalafateli 2021-07-21 09:09:39 +03:00 committed by GitHub
parent f1539ddd29
commit 1bfdc72692
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 704 additions and 103 deletions

View file

@ -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);
});
});

View file

@ -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>
)}
</>
);
};

View file

@ -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);

View file

@ -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>

View file

@ -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",

View file

@ -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();

View file

@ -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: {

View file

@ -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)]
: [],
},
},
],

View file

@ -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 {

View file

@ -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={() => {

View file

@ -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": "トップ",

View file

@ -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": "顶部",