[Lens] Legend config (#70619)
This commit is contained in:
parent
78ea171a80
commit
3709de64d6
|
@ -63,7 +63,7 @@ export function WorkspacePanelWrapper({
|
|||
clearStagedPreview: false,
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
[dispatch, activeVisualization]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -13,7 +13,7 @@ import { toExpression, toPreviewExpression } from './to_expression';
|
|||
import { LayerState, PieVisualizationState } from './types';
|
||||
import { suggestions } from './suggestions';
|
||||
import { CHART_NAMES, MAX_PIE_BUCKETS, MAX_TREEMAP_BUCKETS } from './constants';
|
||||
import { SettingsWidget } from './settings_widget';
|
||||
import { PieToolbar } from './toolbar';
|
||||
|
||||
function newLayerState(layerId: string): LayerState {
|
||||
return {
|
||||
|
@ -204,10 +204,10 @@ export const pieVisualization: Visualization<PieVisualizationState, PieVisualiza
|
|||
toExpression,
|
||||
toPreviewExpression,
|
||||
|
||||
renderLayerContextMenu(domElement, props) {
|
||||
renderToolbar(domElement, props) {
|
||||
render(
|
||||
<I18nProvider>
|
||||
<SettingsWidget {...props} />
|
||||
<PieToolbar {...props} />
|
||||
</I18nProvider>,
|
||||
domElement
|
||||
);
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Position } from '@elastic/charts';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import {
|
||||
IInterpreterRenderHandlers,
|
||||
|
@ -73,6 +74,11 @@ export const pie: ExpressionFunctionDefinition<
|
|||
types: ['boolean'],
|
||||
help: '',
|
||||
},
|
||||
legendPosition: {
|
||||
types: ['string'],
|
||||
options: [Position.Top, Position.Right, Position.Bottom, Position.Left],
|
||||
help: '',
|
||||
},
|
||||
percentDecimals: {
|
||||
types: ['number'],
|
||||
help: '',
|
||||
|
|
|
@ -65,6 +65,13 @@ describe('PieVisualization component', () => {
|
|||
};
|
||||
}
|
||||
|
||||
test('it shows legend on correct side', () => {
|
||||
const component = shallow(
|
||||
<PieComponent args={{ ...args, legendPosition: 'top' }} {...getDefaultArgs()} />
|
||||
);
|
||||
expect(component.find(Settings).prop('legendPosition')).toEqual('top');
|
||||
});
|
||||
|
||||
test('it shows legend for 2 groups using default legendDisplay', () => {
|
||||
const component = shallow(<PieComponent args={args} {...getDefaultArgs()} />);
|
||||
expect(component.find(Settings).prop('showLegend')).toEqual(true);
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
PartitionFillLabel,
|
||||
RecursivePartial,
|
||||
LayerValue,
|
||||
Position,
|
||||
} from '@elastic/charts';
|
||||
import { FormatFactory, LensFilterEvent } from '../types';
|
||||
import { VisualizationContainer } from '../visualization_container';
|
||||
|
@ -55,6 +56,7 @@ export function PieComponent(
|
|||
numberDisplay,
|
||||
categoryDisplay,
|
||||
legendDisplay,
|
||||
legendPosition,
|
||||
nestedLegend,
|
||||
percentDecimals,
|
||||
hideLabels,
|
||||
|
@ -237,6 +239,7 @@ export function PieComponent(
|
|||
(legendDisplay === 'show' ||
|
||||
(legendDisplay === 'default' && columnGroups.length > 1 && shape !== 'treemap'))
|
||||
}
|
||||
legendPosition={legendPosition || Position.Right}
|
||||
legendMaxDepth={nestedLegend ? undefined : 1 /* Color is based only on first layer */}
|
||||
onElementClick={(args) => {
|
||||
const context = getFilterContext(
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
.lnsPieSettingsWidget {
|
||||
min-width: $euiSizeXL * 10;
|
||||
}
|
|
@ -1,230 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiSuperSelect,
|
||||
EuiRange,
|
||||
EuiSwitch,
|
||||
EuiHorizontalRule,
|
||||
EuiSpacer,
|
||||
EuiButtonGroup,
|
||||
} from '@elastic/eui';
|
||||
import { DEFAULT_PERCENT_DECIMALS } from './constants';
|
||||
import { PieVisualizationState, SharedLayerState } from './types';
|
||||
import { VisualizationLayerWidgetProps } from '../types';
|
||||
import './settings_widget.scss';
|
||||
|
||||
const numberOptions: Array<{ value: SharedLayerState['numberDisplay']; inputDisplay: string }> = [
|
||||
{
|
||||
value: 'hidden',
|
||||
inputDisplay: i18n.translate('xpack.lens.pieChart.hiddenNumbersLabel', {
|
||||
defaultMessage: 'Hide from chart',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'percent',
|
||||
inputDisplay: i18n.translate('xpack.lens.pieChart.showPercentValuesLabel', {
|
||||
defaultMessage: 'Show percent',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'value',
|
||||
inputDisplay: i18n.translate('xpack.lens.pieChart.showFormatterValuesLabel', {
|
||||
defaultMessage: 'Show value',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const categoryOptions: Array<{
|
||||
value: SharedLayerState['categoryDisplay'];
|
||||
inputDisplay: string;
|
||||
}> = [
|
||||
{
|
||||
value: 'default',
|
||||
inputDisplay: i18n.translate('xpack.lens.pieChart.showCategoriesLabel', {
|
||||
defaultMessage: 'Inside or outside',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'inside',
|
||||
inputDisplay: i18n.translate('xpack.lens.pieChart.fitInsideOnlyLabel', {
|
||||
defaultMessage: 'Inside only',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'hide',
|
||||
inputDisplay: i18n.translate('xpack.lens.pieChart.categoriesInLegendLabel', {
|
||||
defaultMessage: 'Hide labels',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const categoryOptionsTreemap: Array<{
|
||||
value: SharedLayerState['categoryDisplay'];
|
||||
inputDisplay: string;
|
||||
}> = [
|
||||
{
|
||||
value: 'default',
|
||||
inputDisplay: i18n.translate('xpack.lens.pieChart.showTreemapCategoriesLabel', {
|
||||
defaultMessage: 'Show labels',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'hide',
|
||||
inputDisplay: i18n.translate('xpack.lens.pieChart.categoriesInLegendLabel', {
|
||||
defaultMessage: 'Hide labels',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const legendOptions: Array<{
|
||||
value: SharedLayerState['legendDisplay'];
|
||||
label: string;
|
||||
id: string;
|
||||
}> = [
|
||||
{
|
||||
id: 'pieLegendDisplay-default',
|
||||
value: 'default',
|
||||
label: i18n.translate('xpack.lens.pieChart.defaultLegendLabel', {
|
||||
defaultMessage: 'auto',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'pieLegendDisplay-show',
|
||||
value: 'show',
|
||||
label: i18n.translate('xpack.lens.pieChart.alwaysShowLegendLabel', {
|
||||
defaultMessage: 'show',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'pieLegendDisplay-hide',
|
||||
value: 'hide',
|
||||
label: i18n.translate('xpack.lens.pieChart.hideLegendLabel', {
|
||||
defaultMessage: 'hide',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
export function SettingsWidget(props: VisualizationLayerWidgetProps<PieVisualizationState>) {
|
||||
const { state, setState } = props;
|
||||
const layer = state.layers[0];
|
||||
if (!layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiForm className="lnsPieSettingsWidget">
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.pieChart.labelPositionLabel', {
|
||||
defaultMessage: 'Label position',
|
||||
})}
|
||||
fullWidth
|
||||
display="columnCompressed"
|
||||
>
|
||||
<EuiSuperSelect
|
||||
compressed
|
||||
valueOfSelected={layer.categoryDisplay}
|
||||
options={state.shape === 'treemap' ? categoryOptionsTreemap : categoryOptions}
|
||||
onChange={(option) => {
|
||||
setState({
|
||||
...state,
|
||||
layers: [{ ...layer, categoryDisplay: option }],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.pieChart.numberLabels', {
|
||||
defaultMessage: 'Label values',
|
||||
})}
|
||||
fullWidth
|
||||
display="columnCompressed"
|
||||
>
|
||||
<EuiSuperSelect
|
||||
compressed
|
||||
disabled={layer.categoryDisplay === 'hide'}
|
||||
valueOfSelected={layer.categoryDisplay === 'hide' ? 'hidden' : layer.numberDisplay}
|
||||
options={numberOptions}
|
||||
onChange={(option) => {
|
||||
setState({
|
||||
...state,
|
||||
layers: [{ ...layer, numberDisplay: option }],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiHorizontalRule margin="s" />
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.pieChart.percentDecimalsLabel', {
|
||||
defaultMessage: 'Decimal places for percent',
|
||||
})}
|
||||
fullWidth
|
||||
display="columnCompressed"
|
||||
>
|
||||
<EuiRange
|
||||
data-test-subj="indexPattern-dimension-formatDecimals"
|
||||
value={layer.percentDecimals ?? DEFAULT_PERCENT_DECIMALS}
|
||||
min={0}
|
||||
max={10}
|
||||
showInput
|
||||
compressed
|
||||
onChange={(e) => {
|
||||
setState({
|
||||
...state,
|
||||
layers: [{ ...layer, percentDecimals: Number(e.currentTarget.value) }],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiHorizontalRule margin="s" />
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.pieChart.legendDisplayLabel', {
|
||||
defaultMessage: 'Legend display',
|
||||
})}
|
||||
display="columnCompressed"
|
||||
>
|
||||
<div>
|
||||
<EuiButtonGroup
|
||||
legend={i18n.translate('xpack.lens.pieChart.legendDisplayLegend', {
|
||||
defaultMessage: 'Legend display',
|
||||
})}
|
||||
options={legendOptions}
|
||||
idSelected={legendOptions.find(({ value }) => value === layer.legendDisplay)!.id}
|
||||
onChange={(optionId) => {
|
||||
setState({
|
||||
...state,
|
||||
layers: [
|
||||
{
|
||||
...layer,
|
||||
legendDisplay: legendOptions.find(({ id }) => id === optionId)!.value,
|
||||
},
|
||||
],
|
||||
});
|
||||
}}
|
||||
buttonSize="compressed"
|
||||
isFullWidth
|
||||
/>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
<EuiSwitch
|
||||
compressed
|
||||
label={i18n.translate('xpack.lens.pieChart.nestedLegendLabel', {
|
||||
defaultMessage: 'Nested legend',
|
||||
})}
|
||||
checked={!!layer.nestedLegend}
|
||||
onChange={() => {
|
||||
setState({ ...state, layers: [{ ...layer, nestedLegend: !layer.nestedLegend }] });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
);
|
||||
}
|
|
@ -41,6 +41,7 @@ function expressionHelper(
|
|||
numberDisplay: [layer.numberDisplay],
|
||||
categoryDisplay: [layer.categoryDisplay],
|
||||
legendDisplay: [layer.legendDisplay],
|
||||
legendPosition: [layer.legendPosition || 'right'],
|
||||
percentDecimals: [layer.percentDecimals ?? DEFAULT_PERCENT_DECIMALS],
|
||||
nestedLegend: [!!layer.nestedLegend],
|
||||
},
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.lnsPieToolbar__popover {
|
||||
width: $euiFormMaxWidth;
|
||||
}
|
281
x-pack/plugins/lens/public/pie_visualization/toolbar.tsx
Normal file
281
x-pack/plugins/lens/public/pie_visualization/toolbar.tsx
Normal file
|
@ -0,0 +1,281 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import './toolbar.scss';
|
||||
import React, { useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPopover,
|
||||
EuiSelect,
|
||||
EuiFormRow,
|
||||
EuiSuperSelect,
|
||||
EuiRange,
|
||||
EuiSwitch,
|
||||
EuiHorizontalRule,
|
||||
EuiSpacer,
|
||||
EuiButtonGroup,
|
||||
} from '@elastic/eui';
|
||||
import { Position } from '@elastic/charts';
|
||||
import { DEFAULT_PERCENT_DECIMALS } from './constants';
|
||||
import { PieVisualizationState, SharedLayerState } from './types';
|
||||
import { VisualizationToolbarProps } from '../types';
|
||||
import { ToolbarButton } from '../toolbar_button';
|
||||
|
||||
const numberOptions: Array<{ value: SharedLayerState['numberDisplay']; inputDisplay: string }> = [
|
||||
{
|
||||
value: 'hidden',
|
||||
inputDisplay: i18n.translate('xpack.lens.pieChart.hiddenNumbersLabel', {
|
||||
defaultMessage: 'Hide from chart',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'percent',
|
||||
inputDisplay: i18n.translate('xpack.lens.pieChart.showPercentValuesLabel', {
|
||||
defaultMessage: 'Show percent',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'value',
|
||||
inputDisplay: i18n.translate('xpack.lens.pieChart.showFormatterValuesLabel', {
|
||||
defaultMessage: 'Show value',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const categoryOptions: Array<{
|
||||
value: SharedLayerState['categoryDisplay'];
|
||||
inputDisplay: string;
|
||||
}> = [
|
||||
{
|
||||
value: 'default',
|
||||
inputDisplay: i18n.translate('xpack.lens.pieChart.showCategoriesLabel', {
|
||||
defaultMessage: 'Inside or outside',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'inside',
|
||||
inputDisplay: i18n.translate('xpack.lens.pieChart.fitInsideOnlyLabel', {
|
||||
defaultMessage: 'Inside only',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'hide',
|
||||
inputDisplay: i18n.translate('xpack.lens.pieChart.categoriesInLegendLabel', {
|
||||
defaultMessage: 'Hide labels',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const categoryOptionsTreemap: Array<{
|
||||
value: SharedLayerState['categoryDisplay'];
|
||||
inputDisplay: string;
|
||||
}> = [
|
||||
{
|
||||
value: 'default',
|
||||
inputDisplay: i18n.translate('xpack.lens.pieChart.showTreemapCategoriesLabel', {
|
||||
defaultMessage: 'Show labels',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'hide',
|
||||
inputDisplay: i18n.translate('xpack.lens.pieChart.categoriesInLegendLabel', {
|
||||
defaultMessage: 'Hide labels',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const legendOptions: Array<{
|
||||
value: SharedLayerState['legendDisplay'];
|
||||
label: string;
|
||||
id: string;
|
||||
}> = [
|
||||
{
|
||||
id: 'pieLegendDisplay-default',
|
||||
value: 'default',
|
||||
label: i18n.translate('xpack.lens.pieChart.legendVisibility.auto', {
|
||||
defaultMessage: 'auto',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'pieLegendDisplay-show',
|
||||
value: 'show',
|
||||
label: i18n.translate('xpack.lens.pieChart.legendVisibility.show', {
|
||||
defaultMessage: 'show',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'pieLegendDisplay-hide',
|
||||
value: 'hide',
|
||||
label: i18n.translate('xpack.lens.pieChart.legendVisibility.hide', {
|
||||
defaultMessage: 'hide',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
export function PieToolbar(props: VisualizationToolbarProps<PieVisualizationState>) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { state, setState } = props;
|
||||
const layer = state.layers[0];
|
||||
if (!layer) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
panelClassName="lnsPieToolbar__popover"
|
||||
button={
|
||||
<ToolbarButton
|
||||
fontWeight="normal"
|
||||
onClick={() => {
|
||||
setOpen(!open);
|
||||
}}
|
||||
>
|
||||
{i18n.translate('xpack.lens.pieChart.settingsLabel', { defaultMessage: 'Settings' })}
|
||||
</ToolbarButton>
|
||||
}
|
||||
isOpen={open}
|
||||
closePopover={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
anchorPosition="downRight"
|
||||
>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.pieChart.labelPositionLabel', {
|
||||
defaultMessage: 'Label position',
|
||||
})}
|
||||
fullWidth
|
||||
display="columnCompressed"
|
||||
>
|
||||
<EuiSuperSelect
|
||||
compressed
|
||||
valueOfSelected={layer.categoryDisplay}
|
||||
options={state.shape === 'treemap' ? categoryOptionsTreemap : categoryOptions}
|
||||
onChange={(option) => {
|
||||
setState({
|
||||
...state,
|
||||
layers: [{ ...layer, categoryDisplay: option }],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.pieChart.numberLabels', {
|
||||
defaultMessage: 'Label values',
|
||||
})}
|
||||
fullWidth
|
||||
display="columnCompressed"
|
||||
>
|
||||
<EuiSuperSelect
|
||||
compressed
|
||||
disabled={layer.categoryDisplay === 'hide'}
|
||||
valueOfSelected={layer.categoryDisplay === 'hide' ? 'hidden' : layer.numberDisplay}
|
||||
options={numberOptions}
|
||||
onChange={(option) => {
|
||||
setState({
|
||||
...state,
|
||||
layers: [{ ...layer, numberDisplay: option }],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiHorizontalRule margin="s" />
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.pieChart.percentDecimalsLabel', {
|
||||
defaultMessage: 'Decimal places for percent',
|
||||
})}
|
||||
fullWidth
|
||||
display="columnCompressed"
|
||||
>
|
||||
<EuiRange
|
||||
data-test-subj="indexPattern-dimension-formatDecimals"
|
||||
value={layer.percentDecimals ?? DEFAULT_PERCENT_DECIMALS}
|
||||
min={0}
|
||||
max={10}
|
||||
showInput
|
||||
compressed
|
||||
onChange={(e) => {
|
||||
setState({
|
||||
...state,
|
||||
layers: [{ ...layer, percentDecimals: Number(e.currentTarget.value) }],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiHorizontalRule margin="s" />
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.pieChart.legendDisplayLabel', {
|
||||
defaultMessage: 'Legend display',
|
||||
})}
|
||||
display="columnCompressed"
|
||||
>
|
||||
<div>
|
||||
<EuiButtonGroup
|
||||
legend={i18n.translate('xpack.lens.pieChart.legendDisplayLegend', {
|
||||
defaultMessage: 'Legend display',
|
||||
})}
|
||||
options={legendOptions}
|
||||
idSelected={legendOptions.find(({ value }) => value === layer.legendDisplay)!.id}
|
||||
onChange={(optionId) => {
|
||||
setState({
|
||||
...state,
|
||||
layers: [
|
||||
{
|
||||
...layer,
|
||||
legendDisplay: legendOptions.find(({ id }) => id === optionId)!.value,
|
||||
},
|
||||
],
|
||||
});
|
||||
}}
|
||||
buttonSize="compressed"
|
||||
isFullWidth
|
||||
/>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
<EuiSwitch
|
||||
compressed
|
||||
label={i18n.translate('xpack.lens.pieChart.nestedLegendLabel', {
|
||||
defaultMessage: 'Nested legend',
|
||||
})}
|
||||
disabled={layer.legendDisplay === 'hide'}
|
||||
checked={!!layer.nestedLegend}
|
||||
onChange={() => {
|
||||
setState({ ...state, layers: [{ ...layer, nestedLegend: !layer.nestedLegend }] });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
label={i18n.translate('xpack.lens.xyChart.legendPositionLabel', {
|
||||
defaultMessage: 'Legend position',
|
||||
})}
|
||||
>
|
||||
<EuiSelect
|
||||
compressed
|
||||
disabled={layer.legendDisplay === 'hide'}
|
||||
options={[
|
||||
{ value: Position.Top, text: 'Top' },
|
||||
{ value: Position.Left, text: 'Left' },
|
||||
{ value: Position.Right, text: 'Right' },
|
||||
{ value: Position.Bottom, text: 'Bottom' },
|
||||
]}
|
||||
value={layer.legendPosition || Position.Right}
|
||||
onChange={(e) => {
|
||||
setState({
|
||||
...state,
|
||||
layers: [{ ...layer, legendPosition: e.target.value as Position }],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -13,6 +13,7 @@ export interface SharedLayerState {
|
|||
numberDisplay: 'hidden' | 'percent' | 'value';
|
||||
categoryDisplay: 'default' | 'inside' | 'hide';
|
||||
legendDisplay: 'default' | 'show' | 'hide';
|
||||
legendPosition?: 'left' | 'right' | 'top' | 'bottom';
|
||||
nestedLegend?: boolean;
|
||||
percentDecimals?: number;
|
||||
}
|
||||
|
|
|
@ -64,6 +64,7 @@ Object {
|
|||
"position": Array [
|
||||
"bottom",
|
||||
],
|
||||
"showSingleSeries": Array [],
|
||||
},
|
||||
"function": "lens_xy_legendConfig",
|
||||
"type": "function",
|
||||
|
|
|
@ -127,6 +127,9 @@ export const buildExpression = (
|
|||
function: 'lens_xy_legendConfig',
|
||||
arguments: {
|
||||
isVisible: [state.legend.isVisible],
|
||||
showSingleSeries: state.legend.showSingleSeries
|
||||
? [state.legend.showSingleSeries]
|
||||
: [],
|
||||
position: [state.legend.position],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -19,8 +19,18 @@ import { VisualizationType } from '../index';
|
|||
import { FittingFunction } from './fitting_functions';
|
||||
|
||||
export interface LegendConfig {
|
||||
/**
|
||||
* Flag whether the legend should be shown. If there is just a single series, it will be hidden
|
||||
*/
|
||||
isVisible: boolean;
|
||||
/**
|
||||
* Position of the legend relative to the chart
|
||||
*/
|
||||
position: Position;
|
||||
/**
|
||||
* Flag whether the legend should be shown even with just a single series
|
||||
*/
|
||||
showSingleSeries?: boolean;
|
||||
}
|
||||
|
||||
type LegendConfigResult = LegendConfig & { type: 'lens_xy_legendConfig' };
|
||||
|
@ -50,6 +60,12 @@ export const legendConfig: ExpressionFunctionDefinition<
|
|||
defaultMessage: 'Specifies the legend position.',
|
||||
}),
|
||||
},
|
||||
showSingleSeries: {
|
||||
types: ['boolean'],
|
||||
help: i18n.translate('xpack.lens.xyChart.showSingleSeries.help', {
|
||||
defaultMessage: 'Specifies whether a legend with just a single entry should be shown',
|
||||
}),
|
||||
},
|
||||
},
|
||||
fn: function fn(input: unknown, args: LegendConfig) {
|
||||
return {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import './xy_config_panel.scss';
|
||||
import React, { useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Position } from '@elastic/charts';
|
||||
import { debounce } from 'lodash';
|
||||
import {
|
||||
EuiButtonGroup,
|
||||
|
@ -16,12 +17,14 @@ import {
|
|||
EuiFormRow,
|
||||
EuiPopover,
|
||||
EuiText,
|
||||
EuiSelect,
|
||||
htmlIdGenerator,
|
||||
EuiForm,
|
||||
EuiColorPicker,
|
||||
EuiColorPickerProps,
|
||||
EuiToolTip,
|
||||
EuiIcon,
|
||||
EuiHorizontalRule,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
VisualizationLayerWidgetProps,
|
||||
|
@ -46,6 +49,30 @@ function updateLayer(state: State, layer: UnwrapArray<State['layers']>, index: n
|
|||
};
|
||||
}
|
||||
|
||||
const legendOptions: Array<{ id: string; value: 'auto' | 'show' | 'hide'; label: string }> = [
|
||||
{
|
||||
id: `xy_legend_auto`,
|
||||
value: 'auto',
|
||||
label: i18n.translate('xpack.lens.xyChart.legendVisibility.auto', {
|
||||
defaultMessage: 'auto',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: `xy_legend_show`,
|
||||
value: 'show',
|
||||
label: i18n.translate('xpack.lens.xyChart.legendVisibility.show', {
|
||||
defaultMessage: 'show',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: `xy_legend_hide`,
|
||||
value: 'hide',
|
||||
label: i18n.translate('xpack.lens.xyChart.legendVisibility.hide', {
|
||||
defaultMessage: 'hide',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
export function LayerContextMenu(props: VisualizationLayerWidgetProps<State>) {
|
||||
const { state, layerId } = props;
|
||||
const horizontalOnly = isHorizontalChart(state.layers);
|
||||
|
@ -95,6 +122,12 @@ export function XyToolbar(props: VisualizationToolbarProps<State>) {
|
|||
const hasNonBarSeries = props.state?.layers.some(
|
||||
(layer) => layer.seriesType === 'line' || layer.seriesType === 'area'
|
||||
);
|
||||
const legendMode =
|
||||
props.state?.legend.isVisible && !props.state?.legend.showSingleSeries
|
||||
? 'auto'
|
||||
: !props.state?.legend.isVisible
|
||||
? 'hide'
|
||||
: 'show';
|
||||
return (
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -157,6 +190,67 @@ export function XyToolbar(props: VisualizationToolbarProps<State>) {
|
|||
/>
|
||||
</EuiFormRow>
|
||||
</EuiToolTip>
|
||||
<EuiHorizontalRule margin="s" />
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
label={i18n.translate('xpack.lens.xyChart.legendVisibilityLabel', {
|
||||
defaultMessage: 'Legend display',
|
||||
})}
|
||||
>
|
||||
<EuiButtonGroup
|
||||
isFullWidth
|
||||
legend={i18n.translate('xpack.lens.xyChart.legendVisibilityLabel', {
|
||||
defaultMessage: 'Legend display',
|
||||
})}
|
||||
name="legendDisplay"
|
||||
buttonSize="compressed"
|
||||
options={legendOptions}
|
||||
idSelected={legendOptions.find(({ value }) => value === legendMode)!.id}
|
||||
onChange={(optionId) => {
|
||||
const newMode = legendOptions.find(({ id }) => id === optionId)!.value;
|
||||
if (newMode === 'auto') {
|
||||
props.setState({
|
||||
...props.state,
|
||||
legend: { ...props.state.legend, isVisible: true, showSingleSeries: false },
|
||||
});
|
||||
} else if (newMode === 'show') {
|
||||
props.setState({
|
||||
...props.state,
|
||||
legend: { ...props.state.legend, isVisible: true, showSingleSeries: true },
|
||||
});
|
||||
} else if (newMode === 'hide') {
|
||||
props.setState({
|
||||
...props.state,
|
||||
legend: { ...props.state.legend, isVisible: false, showSingleSeries: false },
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
label={i18n.translate('xpack.lens.xyChart.legendPositionLabel', {
|
||||
defaultMessage: 'Legend position',
|
||||
})}
|
||||
>
|
||||
<EuiSelect
|
||||
disabled={legendMode === 'hide'}
|
||||
compressed
|
||||
options={[
|
||||
{ value: Position.Top, text: 'Top' },
|
||||
{ value: Position.Left, text: 'Left' },
|
||||
{ value: Position.Right, text: 'Right' },
|
||||
{ value: Position.Bottom, text: 'Bottom' },
|
||||
]}
|
||||
value={props.state?.legend.position}
|
||||
onChange={(e) => {
|
||||
props.setState({
|
||||
...props.state,
|
||||
legend: { ...props.state.legend, position: e.target.value as Position },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -1556,6 +1556,73 @@ describe('xy_expression', () => {
|
|||
expect(component.find(Settings).prop('showLegend')).toEqual(true);
|
||||
});
|
||||
|
||||
test('it should always show legend if showSingleSeries is set', () => {
|
||||
const { data, args } = sampleArgs();
|
||||
|
||||
const component = shallow(
|
||||
<XYChart
|
||||
data={{ ...data }}
|
||||
args={{
|
||||
...args,
|
||||
layers: [{ ...args.layers[0], accessors: ['a'], splitAccessor: undefined }],
|
||||
legend: { ...args.legend, isVisible: true, showSingleSeries: true },
|
||||
}}
|
||||
formatFactory={getFormatSpy}
|
||||
timeZone="UTC"
|
||||
chartsThemeService={chartsThemeService}
|
||||
histogramBarTarget={50}
|
||||
onClickValue={onClickValue}
|
||||
onSelectRange={onSelectRange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find(Settings).prop('showLegend')).toEqual(true);
|
||||
});
|
||||
|
||||
test('it not show legend if isVisible is set to false', () => {
|
||||
const { data, args } = sampleArgs();
|
||||
|
||||
const component = shallow(
|
||||
<XYChart
|
||||
data={{ ...data }}
|
||||
args={{
|
||||
...args,
|
||||
legend: { ...args.legend, isVisible: false },
|
||||
}}
|
||||
formatFactory={getFormatSpy}
|
||||
timeZone="UTC"
|
||||
chartsThemeService={chartsThemeService}
|
||||
histogramBarTarget={50}
|
||||
onClickValue={onClickValue}
|
||||
onSelectRange={onSelectRange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find(Settings).prop('showLegend')).toEqual(false);
|
||||
});
|
||||
|
||||
test('it should show legend on right side', () => {
|
||||
const { data, args } = sampleArgs();
|
||||
|
||||
const component = shallow(
|
||||
<XYChart
|
||||
data={{ ...data }}
|
||||
args={{
|
||||
...args,
|
||||
legend: { ...args.legend, position: 'top' },
|
||||
}}
|
||||
formatFactory={getFormatSpy}
|
||||
timeZone="UTC"
|
||||
chartsThemeService={chartsThemeService}
|
||||
histogramBarTarget={50}
|
||||
onClickValue={onClickValue}
|
||||
onSelectRange={onSelectRange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find(Settings).prop('legendPosition')).toEqual('top');
|
||||
});
|
||||
|
||||
test('it should apply the fitting function to all non-bar series', () => {
|
||||
const data: LensMultiTable = {
|
||||
type: 'lens_multitable',
|
||||
|
|
|
@ -282,7 +282,11 @@ export function XYChart({
|
|||
return (
|
||||
<Chart>
|
||||
<Settings
|
||||
showLegend={legend.isVisible ? chartHasMoreThanOneSeries : legend.isVisible}
|
||||
showLegend={
|
||||
legend.isVisible && !legend.showSingleSeries
|
||||
? chartHasMoreThanOneSeries
|
||||
: legend.isVisible
|
||||
}
|
||||
legendPosition={legend.position}
|
||||
showLegendExtra={false}
|
||||
theme={chartTheme}
|
||||
|
|
|
@ -8569,15 +8569,15 @@
|
|||
"xpack.lens.pie.treemaplabel": "ツリーマップ",
|
||||
"xpack.lens.pie.treemapSuggestionLabel": "ツリーマップとして",
|
||||
"xpack.lens.pie.visualizationName": "パイ",
|
||||
"xpack.lens.pieChart.alwaysShowLegendLabel": "表示",
|
||||
"xpack.lens.pieChart.categoriesInLegendLabel": "ラベルを非表示",
|
||||
"xpack.lens.pieChart.defaultLegendLabel": "自動",
|
||||
"xpack.lens.pieChart.fitInsideOnlyLabel": "内部のみ",
|
||||
"xpack.lens.pieChart.hiddenNumbersLabel": "グラフから非表示",
|
||||
"xpack.lens.pieChart.hideLegendLabel": "非表示",
|
||||
"xpack.lens.pieChart.labelPositionLabel": "ラベル位置",
|
||||
"xpack.lens.pieChart.legendDisplayLabel": "凡例表示",
|
||||
"xpack.lens.pieChart.legendDisplayLegend": "凡例表示",
|
||||
"xpack.lens.pieChart.legendVisibility.auto": "自動",
|
||||
"xpack.lens.pieChart.legendVisibility.hide": "非表示",
|
||||
"xpack.lens.pieChart.legendVisibility.show": "表示",
|
||||
"xpack.lens.pieChart.nestedLegendLabel": "ネストされた凡例",
|
||||
"xpack.lens.pieChart.numberLabels": "ラベル値",
|
||||
"xpack.lens.pieChart.percentDecimalsLabel": "割合の小数点桁数",
|
||||
|
|
|
@ -8574,15 +8574,15 @@
|
|||
"xpack.lens.pie.treemaplabel": "树状图",
|
||||
"xpack.lens.pie.treemapSuggestionLabel": "为树状图",
|
||||
"xpack.lens.pie.visualizationName": "饼图",
|
||||
"xpack.lens.pieChart.alwaysShowLegendLabel": "显示",
|
||||
"xpack.lens.pieChart.categoriesInLegendLabel": "隐藏标签",
|
||||
"xpack.lens.pieChart.defaultLegendLabel": "自动",
|
||||
"xpack.lens.pieChart.fitInsideOnlyLabel": "仅内部",
|
||||
"xpack.lens.pieChart.hiddenNumbersLabel": "在图表中隐藏",
|
||||
"xpack.lens.pieChart.hideLegendLabel": "隐藏",
|
||||
"xpack.lens.pieChart.labelPositionLabel": "标签位置",
|
||||
"xpack.lens.pieChart.legendDisplayLabel": "图例显示",
|
||||
"xpack.lens.pieChart.legendDisplayLegend": "图例显示",
|
||||
"xpack.lens.pieChart.legendVisibility.auto": "自动",
|
||||
"xpack.lens.pieChart.legendVisibility.hide": "隐藏",
|
||||
"xpack.lens.pieChart.legendVisibility.show": "显示",
|
||||
"xpack.lens.pieChart.nestedLegendLabel": "嵌套图例",
|
||||
"xpack.lens.pieChart.numberLabels": "标签值",
|
||||
"xpack.lens.pieChart.percentDecimalsLabel": "百分比的小数位数",
|
||||
|
|
Loading…
Reference in a new issue