[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:
Joe Reuter 2022-03-22 20:11:26 +01:00 committed by GitHub
parent 0dc168e086
commit 9449f1dcdd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 244 additions and 56 deletions

View 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;

View file

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

View file

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

View file

@ -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'],

View file

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

View file

@ -33,6 +33,12 @@ Object {
"description": Array [
"",
],
"emphasizeFitting": Array [
true,
],
"endValue": Array [
"Nearest",
],
"fillOpacity": Array [
0.3,
],

View file

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

View file

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

View file

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

View file

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

View file

@ -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[];

View file

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

View file

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

View file

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