mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Lens] Xy gap settings (#127749)
* add end value and fitting style settings * debug statement * fix tests * fix test and types * fix translation key * adjust copy Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
0dc168e086
commit
9449f1dcdd
14 changed files with 244 additions and 56 deletions
40
x-pack/plugins/lens/common/expressions/xy_chart/end_value.ts
Normal file
40
x-pack/plugins/lens/common/expressions/xy_chart/end_value.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export type EndValue = typeof endValueDefinitions[number]['id'];
|
||||
|
||||
export const endValueDefinitions = [
|
||||
{
|
||||
id: 'None',
|
||||
title: i18n.translate('xpack.lens.endValue.none', {
|
||||
defaultMessage: 'Hide',
|
||||
}),
|
||||
description: i18n.translate('xpack.lens.endValueDescription.none', {
|
||||
defaultMessage: 'Do not extend series to the edge of the chart',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'Zero',
|
||||
title: i18n.translate('xpack.lens.endValue.zero', {
|
||||
defaultMessage: 'Zero',
|
||||
}),
|
||||
description: i18n.translate('xpack.lens.endValueDescription.zero', {
|
||||
defaultMessage: 'Extend series as zero to the edge of the chart',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'Nearest',
|
||||
title: i18n.translate('xpack.lens.endValue.nearest', {
|
||||
defaultMessage: 'Nearest',
|
||||
}),
|
||||
description: i18n.translate('xpack.lens.endValueDescription.nearest', {
|
||||
defaultMessage: 'Extend series with the first/last value to the edge of the chart',
|
||||
}),
|
||||
},
|
||||
] as const;
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
export * from './axis_config';
|
||||
export * from './fitting_function';
|
||||
export * from './end_value';
|
||||
export * from './grid_lines_config';
|
||||
export * from './layer_config';
|
||||
export * from './legend_config';
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import type { AxisExtentConfigResult, AxisTitlesVisibilityConfigResult } from './axis_config';
|
||||
import type { FittingFunction } from './fitting_function';
|
||||
import type { EndValue } from './end_value';
|
||||
import type { GridlinesConfigResult } from './grid_lines_config';
|
||||
import type { DataLayerArgs } from './layer_config';
|
||||
import type { LegendConfigResult } from './legend_config';
|
||||
|
@ -29,6 +30,8 @@ export interface XYArgs {
|
|||
valueLabels: ValueLabelConfig;
|
||||
layers: DataLayerArgs[];
|
||||
fittingFunction?: FittingFunction;
|
||||
endValue?: EndValue;
|
||||
emphasizeFitting?: boolean;
|
||||
axisTitlesVisibilitySettings?: AxisTitlesVisibilityConfigResult;
|
||||
tickLabelsVisibilitySettings?: TickLabelsConfigResult;
|
||||
gridlinesVisibilitySettings?: GridlinesConfigResult;
|
||||
|
|
|
@ -10,6 +10,7 @@ import type { ExpressionValueSearchContext } from '../../../../../../src/plugins
|
|||
import type { LensMultiTable } from '../../types';
|
||||
import type { XYArgs } from './xy_args';
|
||||
import { fittingFunctionDefinitions } from './fitting_function';
|
||||
import { endValueDefinitions } from './end_value';
|
||||
import { logDataTable } from '../expressions_utils';
|
||||
|
||||
export interface XYChartProps {
|
||||
|
@ -87,6 +88,16 @@ export const xyChart: ExpressionFunctionDefinition<
|
|||
defaultMessage: 'Define how missing values are treated',
|
||||
}),
|
||||
},
|
||||
endValue: {
|
||||
types: ['string'],
|
||||
options: [...endValueDefinitions.map(({ id }) => id)],
|
||||
help: '',
|
||||
},
|
||||
emphasizeFitting: {
|
||||
types: ['boolean'],
|
||||
default: false,
|
||||
help: '',
|
||||
},
|
||||
valueLabels: {
|
||||
types: ['string'],
|
||||
options: ['hide', 'inside'],
|
||||
|
|
|
@ -138,6 +138,7 @@ exports[`xy_expression XYChart component it renders area 1`] = `
|
|||
enableHistogramMode={false}
|
||||
fit={
|
||||
Object {
|
||||
"endValue": undefined,
|
||||
"type": "none",
|
||||
}
|
||||
}
|
||||
|
@ -198,6 +199,7 @@ exports[`xy_expression XYChart component it renders area 1`] = `
|
|||
enableHistogramMode={false}
|
||||
fit={
|
||||
Object {
|
||||
"endValue": undefined,
|
||||
"type": "none",
|
||||
}
|
||||
}
|
||||
|
@ -862,6 +864,7 @@ exports[`xy_expression XYChart component it renders line 1`] = `
|
|||
enableHistogramMode={false}
|
||||
fit={
|
||||
Object {
|
||||
"endValue": undefined,
|
||||
"type": "none",
|
||||
}
|
||||
}
|
||||
|
@ -922,6 +925,7 @@ exports[`xy_expression XYChart component it renders line 1`] = `
|
|||
enableHistogramMode={false}
|
||||
fit={
|
||||
Object {
|
||||
"endValue": undefined,
|
||||
"type": "none",
|
||||
}
|
||||
}
|
||||
|
@ -1094,6 +1098,7 @@ exports[`xy_expression XYChart component it renders stacked area 1`] = `
|
|||
enableHistogramMode={false}
|
||||
fit={
|
||||
Object {
|
||||
"endValue": undefined,
|
||||
"type": "none",
|
||||
}
|
||||
}
|
||||
|
@ -1158,6 +1163,7 @@ exports[`xy_expression XYChart component it renders stacked area 1`] = `
|
|||
enableHistogramMode={false}
|
||||
fit={
|
||||
Object {
|
||||
"endValue": undefined,
|
||||
"type": "none",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,12 @@ Object {
|
|||
"description": Array [
|
||||
"",
|
||||
],
|
||||
"emphasizeFitting": Array [
|
||||
true,
|
||||
],
|
||||
"endValue": Array [
|
||||
"Nearest",
|
||||
],
|
||||
"fillOpacity": Array [
|
||||
0.3,
|
||||
],
|
||||
|
|
|
@ -36,6 +36,7 @@ import {
|
|||
AreaSeriesProps,
|
||||
BarSeriesProps,
|
||||
LineSeriesProps,
|
||||
ColorVariant,
|
||||
} from '@elastic/charts';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import type {
|
||||
|
@ -240,6 +241,8 @@ export function XYChart({
|
|||
legend,
|
||||
layers,
|
||||
fittingFunction,
|
||||
endValue,
|
||||
emphasizeFitting,
|
||||
gridlinesVisibilitySettings,
|
||||
valueLabels,
|
||||
hideEndzones,
|
||||
|
@ -857,15 +860,38 @@ export function XYChart({
|
|||
areaSeriesStyle: {
|
||||
point: {
|
||||
visible: !xAccessor,
|
||||
radius: 5,
|
||||
radius: xAccessor && !emphasizeFitting ? 5 : 0,
|
||||
},
|
||||
...(args.fillOpacity && { area: { opacity: args.fillOpacity } }),
|
||||
...(emphasizeFitting && {
|
||||
fit: {
|
||||
area: {
|
||||
opacity: args.fillOpacity || 0.5,
|
||||
},
|
||||
line: {
|
||||
visible: true,
|
||||
stroke: ColorVariant.Series,
|
||||
opacity: 1,
|
||||
dash: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
lineSeriesStyle: {
|
||||
point: {
|
||||
visible: !xAccessor,
|
||||
radius: 5,
|
||||
radius: xAccessor && !emphasizeFitting ? 5 : 0,
|
||||
},
|
||||
...(emphasizeFitting && {
|
||||
fit: {
|
||||
line: {
|
||||
visible: true,
|
||||
stroke: ColorVariant.Series,
|
||||
opacity: 1,
|
||||
dash: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
name(d) {
|
||||
// For multiple y series, the name of the operation is used on each, either:
|
||||
|
@ -913,7 +939,7 @@ export function XYChart({
|
|||
<LineSeries
|
||||
key={index}
|
||||
{...seriesProps}
|
||||
fit={getFitOptions(fittingFunction)}
|
||||
fit={getFitOptions(fittingFunction, endValue)}
|
||||
curve={curveType}
|
||||
/>
|
||||
);
|
||||
|
@ -945,7 +971,7 @@ export function XYChart({
|
|||
<AreaSeries
|
||||
key={index}
|
||||
{...seriesProps}
|
||||
fit={isPercentage ? 'zero' : getFitOptions(fittingFunction)}
|
||||
fit={isPercentage ? 'zero' : getFitOptions(fittingFunction, endValue)}
|
||||
curve={curveType}
|
||||
/>
|
||||
);
|
||||
|
@ -954,7 +980,7 @@ export function XYChart({
|
|||
<AreaSeries
|
||||
key={index}
|
||||
{...seriesProps}
|
||||
fit={getFitOptions(fittingFunction)}
|
||||
fit={getFitOptions(fittingFunction, endValue)}
|
||||
curve={curveType}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -6,15 +6,25 @@
|
|||
*/
|
||||
|
||||
import { Fit } from '@elastic/charts';
|
||||
import { FittingFunction } from '../../common/expressions';
|
||||
import { EndValue, FittingFunction } from '../../common/expressions';
|
||||
|
||||
export function getFitEnum(fittingFunction?: FittingFunction) {
|
||||
export function getFitEnum(fittingFunction?: FittingFunction | EndValue) {
|
||||
if (fittingFunction) {
|
||||
return Fit[fittingFunction];
|
||||
}
|
||||
return Fit.None;
|
||||
}
|
||||
|
||||
export function getFitOptions(fittingFunction?: FittingFunction) {
|
||||
return { type: getFitEnum(fittingFunction) };
|
||||
export function getEndValue(endValue?: EndValue) {
|
||||
if (endValue === 'Nearest') {
|
||||
return Fit[endValue];
|
||||
}
|
||||
if (endValue === 'Zero') {
|
||||
return 0;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getFitOptions(fittingFunction?: FittingFunction, endValue?: EndValue) {
|
||||
return { type: getFitEnum(fittingFunction), endValue: getEndValue(endValue) };
|
||||
}
|
||||
|
|
|
@ -54,6 +54,8 @@ describe('#toExpression', () => {
|
|||
valueLabels: 'hide',
|
||||
preferredSeriesType: 'bar',
|
||||
fittingFunction: 'Carry',
|
||||
endValue: 'Nearest',
|
||||
emphasizeFitting: true,
|
||||
tickLabelsVisibilitySettings: { x: false, yLeft: true, yRight: true },
|
||||
labelsOrientation: {
|
||||
x: 0,
|
||||
|
|
|
@ -194,6 +194,8 @@ export const buildExpression = (
|
|||
},
|
||||
],
|
||||
fittingFunction: [state.fittingFunction || 'None'],
|
||||
endValue: [state.endValue || 'None'],
|
||||
emphasizeFitting: [state.emphasizeFitting || false],
|
||||
curveType: [state.curveType || 'LINEAR'],
|
||||
fillOpacity: [state.fillOpacity || 0.3],
|
||||
yLeftExtent: [
|
||||
|
|
|
@ -27,6 +27,7 @@ import type {
|
|||
AxesSettingsConfig,
|
||||
FittingFunction,
|
||||
LabelsOrientationConfig,
|
||||
EndValue,
|
||||
} from '../../common/expressions';
|
||||
import type { ValueLabelConfig } from '../../common/types';
|
||||
|
||||
|
@ -36,6 +37,8 @@ export interface XYState {
|
|||
legend: LegendConfig;
|
||||
valueLabels?: ValueLabelConfig;
|
||||
fittingFunction?: FittingFunction;
|
||||
emphasizeFitting?: boolean;
|
||||
endValue?: EndValue;
|
||||
yLeftExtent?: AxisExtentConfig;
|
||||
yRightExtent?: AxisExtentConfig;
|
||||
layers: XYLayerConfig[];
|
||||
|
|
|
@ -115,9 +115,17 @@ export const VisualOptionsPopover: React.FC<VisualOptionsPopoverProps> = ({
|
|||
<MissingValuesOptions
|
||||
isFittingEnabled={isFittingEnabled}
|
||||
fittingFunction={state?.fittingFunction}
|
||||
emphasizeFitting={state?.emphasizeFitting}
|
||||
endValue={state?.endValue}
|
||||
onFittingFnChange={(newVal) => {
|
||||
setState({ ...state, fittingFunction: newVal });
|
||||
}}
|
||||
onEmphasizeFittingChange={(newVal) => {
|
||||
setState({ ...state, emphasizeFitting: newVal });
|
||||
}}
|
||||
onEndValueChange={(newVal) => {
|
||||
setState({ ...state, endValue: newVal });
|
||||
}}
|
||||
/>
|
||||
|
||||
<FillOpacityOption
|
||||
|
|
|
@ -13,10 +13,15 @@ import { MissingValuesOptions } from './missing_values_option';
|
|||
describe('Missing values option', () => {
|
||||
it('should show currently selected fitting function', () => {
|
||||
const component = shallow(
|
||||
<MissingValuesOptions onFittingFnChange={jest.fn()} fittingFunction={'Carry'} />
|
||||
<MissingValuesOptions
|
||||
onFittingFnChange={jest.fn()}
|
||||
fittingFunction={'Carry'}
|
||||
onEmphasizeFittingChange={jest.fn()}
|
||||
onEndValueChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find(EuiSuperSelect).prop('valueOfSelected')).toEqual('Carry');
|
||||
expect(component.find(EuiSuperSelect).first().prop('valueOfSelected')).toEqual('Carry');
|
||||
});
|
||||
|
||||
it('should show the fitting option when enabled', () => {
|
||||
|
@ -25,6 +30,8 @@ describe('Missing values option', () => {
|
|||
onFittingFnChange={jest.fn()}
|
||||
fittingFunction={'Carry'}
|
||||
isFittingEnabled={true}
|
||||
onEmphasizeFittingChange={jest.fn()}
|
||||
onEndValueChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -37,6 +44,8 @@ describe('Missing values option', () => {
|
|||
onFittingFnChange={jest.fn()}
|
||||
fittingFunction={'Carry'}
|
||||
isFittingEnabled={false}
|
||||
onEmphasizeFittingChange={jest.fn()}
|
||||
onEndValueChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -7,69 +7,130 @@
|
|||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFormRow, EuiIconTip, EuiSuperSelect, EuiText } from '@elastic/eui';
|
||||
import { fittingFunctionDefinitions } from '../../../../common/expressions';
|
||||
import type { FittingFunction } from '../../../../common/expressions';
|
||||
import { EuiFormRow, EuiIconTip, EuiSuperSelect, EuiSwitch, EuiText } from '@elastic/eui';
|
||||
import { fittingFunctionDefinitions, endValueDefinitions } from '../../../../common/expressions';
|
||||
import type { FittingFunction, EndValue } from '../../../../common/expressions';
|
||||
|
||||
export interface MissingValuesOptionProps {
|
||||
fittingFunction?: FittingFunction;
|
||||
onFittingFnChange: (newMode: FittingFunction) => void;
|
||||
emphasizeFitting?: boolean;
|
||||
onEmphasizeFittingChange: (emphasize: boolean) => void;
|
||||
endValue?: EndValue;
|
||||
onEndValueChange: (endValue: EndValue) => void;
|
||||
isFittingEnabled?: boolean;
|
||||
}
|
||||
|
||||
export const MissingValuesOptions: React.FC<MissingValuesOptionProps> = ({
|
||||
onFittingFnChange,
|
||||
fittingFunction,
|
||||
emphasizeFitting,
|
||||
onEmphasizeFittingChange,
|
||||
onEndValueChange,
|
||||
endValue,
|
||||
isFittingEnabled = true,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{isFittingEnabled && (
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
label={
|
||||
<>
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
label={
|
||||
<>
|
||||
{i18n.translate('xpack.lens.xyChart.missingValuesLabel', {
|
||||
defaultMessage: 'Missing values',
|
||||
})}{' '}
|
||||
<EuiIconTip
|
||||
color="subdued"
|
||||
content={i18n.translate('xpack.lens.xyChart.missingValuesLabelHelpText', {
|
||||
defaultMessage: `By default, area and line charts hide the gaps in the data. To fill the gap, make a selection.`,
|
||||
})}
|
||||
iconProps={{
|
||||
className: 'eui-alignTop',
|
||||
}}
|
||||
position="top"
|
||||
size="s"
|
||||
type="questionInCircle"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<EuiSuperSelect
|
||||
data-test-subj="lnsMissingValuesSelect"
|
||||
compressed
|
||||
options={fittingFunctionDefinitions.map(({ id, title, description }) => {
|
||||
return {
|
||||
value: id,
|
||||
dropdownDisplay: (
|
||||
<>
|
||||
<strong>{title}</strong>
|
||||
<EuiText size="xs" color="subdued">
|
||||
<p>{description}</p>
|
||||
</EuiText>
|
||||
</>
|
||||
),
|
||||
inputDisplay: title,
|
||||
};
|
||||
})}
|
||||
valueOfSelected={fittingFunction || 'None'}
|
||||
onChange={(value) => onFittingFnChange(value)}
|
||||
itemLayoutAlign="top"
|
||||
hasDividers
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{fittingFunction && fittingFunction !== 'None' && (
|
||||
<>
|
||||
{i18n.translate('xpack.lens.xyChart.missingValuesLabel', {
|
||||
defaultMessage: 'Missing values',
|
||||
})}{' '}
|
||||
<EuiIconTip
|
||||
color="subdued"
|
||||
content={i18n.translate('xpack.lens.xyChart.missingValuesLabelHelpText', {
|
||||
defaultMessage: `By default, Lens hides the gaps in the data. To fill the gap, make a selection.`,
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
label={i18n.translate('xpack.lens.xyChart.endValuesLabel', {
|
||||
defaultMessage: 'End values',
|
||||
})}
|
||||
iconProps={{
|
||||
className: 'eui-alignTop',
|
||||
}}
|
||||
position="top"
|
||||
size="s"
|
||||
type="questionInCircle"
|
||||
/>
|
||||
>
|
||||
<EuiSuperSelect
|
||||
data-test-subj="lnsEndValuesSelect"
|
||||
compressed
|
||||
options={endValueDefinitions.map(({ id, title, description }) => {
|
||||
return {
|
||||
value: id,
|
||||
dropdownDisplay: (
|
||||
<>
|
||||
<strong>{title}</strong>
|
||||
<EuiText size="xs" color="subdued">
|
||||
<p>{description}</p>
|
||||
</EuiText>
|
||||
</>
|
||||
),
|
||||
inputDisplay: title,
|
||||
};
|
||||
})}
|
||||
valueOfSelected={endValue || 'None'}
|
||||
onChange={(value) => onEndValueChange(value)}
|
||||
itemLayoutAlign="top"
|
||||
hasDividers
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.xyChart.missingValuesStyle', {
|
||||
defaultMessage: 'Show as dotted line',
|
||||
})}
|
||||
display="columnCompressedSwitch"
|
||||
>
|
||||
<EuiSwitch
|
||||
showLabel={false}
|
||||
label={i18n.translate('xpack.lens.xyChart.missingValuesStyle', {
|
||||
defaultMessage: 'Show as dotted line',
|
||||
})}
|
||||
checked={!emphasizeFitting}
|
||||
onChange={() => {
|
||||
onEmphasizeFittingChange(!emphasizeFitting);
|
||||
}}
|
||||
compressed
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<EuiSuperSelect
|
||||
data-test-subj="lnsMissingValuesSelect"
|
||||
compressed
|
||||
options={fittingFunctionDefinitions.map(({ id, title, description }) => {
|
||||
return {
|
||||
value: id,
|
||||
dropdownDisplay: (
|
||||
<>
|
||||
<strong>{title}</strong>
|
||||
<EuiText size="xs" color="subdued">
|
||||
<p>{description}</p>
|
||||
</EuiText>
|
||||
</>
|
||||
),
|
||||
inputDisplay: title,
|
||||
};
|
||||
})}
|
||||
valueOfSelected={fittingFunction || 'None'}
|
||||
onChange={(value) => onFittingFnChange(value)}
|
||||
itemLayoutAlign="top"
|
||||
hasDividers
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue