[Lens] fit line charts by default (#196184)

## Summary

Every line charts are now interpolated by default with a linear
interpolation.

Solves the second task of
[#186076](https://github.com/elastic/kibana/issues/186076)

fix [#186076](https://github.com/elastic/kibana/issues/186076)

before:
<img width="816" alt="Screenshot 2024-10-17 at 16 25 47"
src="https://github.com/user-attachments/assets/3b14c80b-deef-4d8d-9d5b-e118619e31cb">


after:
<img width="814" alt="Screenshot 2024-10-17 at 16 25 56"
src="https://github.com/user-attachments/assets/45788530-aeb6-4851-ac1e-c53efcd73068">

## Release note
Newly and default configured Lens line charts are now interpolated by
default with a straight Linear interpolation.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Marta Bondyra <4283304+mbondyra@users.noreply.github.com>
This commit is contained in:
Marco Vettorello 2024-10-21 15:05:02 +02:00 committed by GitHub
parent 3b8cf1236b
commit 5fe8aad89d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 61 additions and 68 deletions

View file

@ -46,7 +46,7 @@ Understanding `LensXYConfig` in detail
### `emphasizeFitting` ### `emphasizeFitting`
- **Type:** `boolean` - **Type:** `boolean`
- **Description:** When set to true, emphasizes the fitting of lines to the data points in line charts, making trends and patterns more apparent. - **Description:** When set to true a straight line will be used between isolated points in a line chart, a dashed line will be used otherwise.
### `fittingFunction` ### `fittingFunction`

View file

@ -143,8 +143,8 @@ test('generates xy chart config', async () => {
"yLeft": true, "yLeft": true,
"yRight": true, "yRight": true,
}, },
"emphasizeFitting": false, "emphasizeFitting": true,
"fittingFunction": "None", "fittingFunction": "Linear",
"gridlinesVisibilitySettings": Object { "gridlinesVisibilitySettings": Object {
"x": true, "x": true,
"yLeft": true, "yLeft": true,

View file

@ -51,8 +51,8 @@ function buildVisualizationState(config: LensXYConfig): XYState {
hideEndzones: true, hideEndzones: true,
preferredSeriesType: 'line', preferredSeriesType: 'line',
valueLabels: 'hide', valueLabels: 'hide',
emphasizeFitting: config?.emphasizeFitting ?? false, emphasizeFitting: config?.emphasizeFitting ?? true,
fittingFunction: config?.fittingFunction ?? 'None', fittingFunction: config?.fittingFunction ?? 'Linear',
yLeftExtent: { yLeftExtent: {
mode: config.yBounds?.mode ?? 'full', mode: config.yBounds?.mode ?? 'full',
lowerBound: config.yBounds?.lowerBound, lowerBound: config.yBounds?.lowerBound,

View file

@ -40,7 +40,7 @@ export const commonXYArgs: CommonXYFn['args'] = {
}, },
emphasizeFitting: { emphasizeFitting: {
types: ['boolean'], types: ['boolean'],
default: false, default: true,
help: '', help: '',
}, },
valueLabels: { valueLabels: {

View file

@ -10,7 +10,7 @@
export const PLUGIN_ID = 'expressionXy'; export const PLUGIN_ID = 'expressionXy';
export const PLUGIN_NAME = 'expressionXy'; export const PLUGIN_NAME = 'expressionXy';
export { LayerTypes, XYCurveTypes } from './constants'; export { LayerTypes, XYCurveTypes, FittingFunctions } from './constants';
export type { export type {
AllowedXYOverrides, AllowedXYOverrides,

View file

@ -100,7 +100,6 @@ type GetColorFn = (
type GetPointConfigFn = (config: { type GetPointConfigFn = (config: {
xAccessor: string | undefined; xAccessor: string | undefined;
markSizeAccessor: string | undefined; markSizeAccessor: string | undefined;
emphasizeFitting?: boolean;
showPoints?: boolean; showPoints?: boolean;
pointsRadius?: number; pointsRadius?: number;
}) => Partial<AreaSeriesStyle['point']>; }) => Partial<AreaSeriesStyle['point']>;
@ -297,18 +296,10 @@ export const getSeriesName: GetSeriesNameFn = (
return splitValues.length > 0 ? splitValues.join(' - ') : yAccessorTitle; return splitValues.length > 0 ? splitValues.join(' - ') : yAccessorTitle;
}; };
const getPointConfig: GetPointConfigFn = ({ const getPointConfig: GetPointConfigFn = ({ markSizeAccessor, showPoints, pointsRadius }) => {
xAccessor,
markSizeAccessor,
emphasizeFitting,
showPoints,
pointsRadius,
}) => {
return { return {
visible: (showPoints !== undefined ? showPoints : !xAccessor || markSizeAccessor !== undefined) visible: showPoints || markSizeAccessor ? 'always' : 'never',
? 'always' radius: pointsRadius,
: 'never',
radius: pointsRadius !== undefined ? pointsRadius : xAccessor && !emphasizeFitting ? 5 : 0,
fill: markSizeAccessor ? ColorVariant.Series : undefined, fill: markSizeAccessor ? ColorVariant.Series : undefined,
}; };
}; };
@ -550,7 +541,6 @@ export const getSeriesProps: GetSeriesPropsFn = ({
point: getPointConfig({ point: getPointConfig({
xAccessor: xColumnId, xAccessor: xColumnId,
markSizeAccessor: markSizeColumnId, markSizeAccessor: markSizeColumnId,
emphasizeFitting,
showPoints: layer.showPoints, showPoints: layer.showPoints,
pointsRadius: layer.pointsRadius, pointsRadius: layer.pointsRadius,
}), }),
@ -567,7 +557,6 @@ export const getSeriesProps: GetSeriesPropsFn = ({
point: getPointConfig({ point: getPointConfig({
xAccessor: xColumnId, xAccessor: xColumnId,
markSizeAccessor: markSizeColumnId, markSizeAccessor: markSizeColumnId,
emphasizeFitting,
showPoints: layer.showPoints, showPoints: layer.showPoints,
pointsRadius: layer.pointsRadius, pointsRadius: layer.pointsRadius,
}), }),

View file

@ -15,6 +15,6 @@ export function plugin() {
return new ExpressionXyPlugin(); return new ExpressionXyPlugin();
} }
export { LayerTypes, XYCurveTypes } from '../common'; export { LayerTypes, XYCurveTypes, FittingFunctions } from '../common';
export type { ExpressionXyPluginSetup, ExpressionXyPluginStart } from './types'; export type { ExpressionXyPluginSetup, ExpressionXyPluginStart } from './types';

View file

@ -121,31 +121,30 @@ describe('#toExpression', () => {
).toMatchSnapshot(); ).toMatchSnapshot();
}); });
it('should default the fitting function to None', () => { it('should default the fitting function to Linear', () => {
expect( const ast = xyVisualization.toExpression(
( {
xyVisualization.toExpression( legend: { position: Position.Bottom, isVisible: true },
valueLabels: 'hide',
preferredSeriesType: 'bar',
layers: [
{ {
legend: { position: Position.Bottom, isVisible: true }, layerId: 'first',
valueLabels: 'hide', layerType: LayerTypes.DATA,
preferredSeriesType: 'bar', seriesType: 'area',
layers: [ splitAccessor: 'd',
{ xAccessor: 'a',
layerId: 'first', accessors: ['b', 'c'],
layerType: LayerTypes.DATA,
seriesType: 'area',
splitAccessor: 'd',
xAccessor: 'a',
accessors: ['b', 'c'],
},
],
}, },
frame.datasourceLayers, ],
undefined, },
datasourceExpressionsByLayers frame.datasourceLayers,
) as Ast undefined,
).chain[0].arguments.fittingFunction[0] datasourceExpressionsByLayers
).toEqual('None'); ) as Ast;
expect(ast.chain[0].arguments.fittingFunction[0]).toEqual('Linear');
expect(ast.chain[0].arguments.emphasizeFitting[0]).toEqual(true);
}); });
it('should default the axisTitles visibility settings to true', () => { it('should default the axisTitles visibility settings to true', () => {

View file

@ -35,6 +35,8 @@ import {
XYCurveType, XYCurveType,
YAxisConfigFn, YAxisConfigFn,
} from '@kbn/expression-xy-plugin/common'; } from '@kbn/expression-xy-plugin/common';
import { FittingFunctions } from '@kbn/expression-xy-plugin/public';
import type { EventAnnotationConfig } from '@kbn/event-annotation-common'; import type { EventAnnotationConfig } from '@kbn/event-annotation-common';
import { LayerTypes } from '@kbn/expression-xy-plugin/public'; import { LayerTypes } from '@kbn/expression-xy-plugin/public';
import { SystemPaletteExpressionFunctionDefinition } from '@kbn/charts-plugin/common'; import { SystemPaletteExpressionFunctionDefinition } from '@kbn/charts-plugin/common';
@ -336,9 +338,9 @@ export const buildXYExpression = (
const layeredXyVisFn = buildExpressionFunction<LayeredXyVisFn>('layeredXyVis', { const layeredXyVisFn = buildExpressionFunction<LayeredXyVisFn>('layeredXyVis', {
legend: buildExpression([legendConfigFn]).toAst(), legend: buildExpression([legendConfigFn]).toAst(),
fittingFunction: state.fittingFunction ?? 'None', fittingFunction: state.fittingFunction ?? FittingFunctions.LINEAR,
endValue: state.endValue ?? 'None', endValue: state.endValue ?? 'None',
emphasizeFitting: state.emphasizeFitting ?? false, emphasizeFitting: state.emphasizeFitting ?? true,
minBarHeight: state.minBarHeight ?? 1, minBarHeight: state.minBarHeight ?? 1,
fillOpacity: state.fillOpacity ?? 0.3, fillOpacity: state.fillOpacity ?? 0.3,
valueLabels: state.valueLabels ?? 'hide', valueLabels: state.valueLabels ?? 'hide',

View file

@ -7,10 +7,11 @@
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import type { FittingFunction } from '@kbn/expression-xy-plugin/common'; import type { FittingFunction } from '@kbn/expression-xy-plugin/common';
import { FittingFunctions } from '@kbn/expression-xy-plugin/public';
export const fittingFunctionDefinitions: Array<{ id: FittingFunction } & Record<string, string>> = [ export const fittingFunctionDefinitions: Array<{ id: FittingFunction } & Record<string, string>> = [
{ {
id: 'None', id: FittingFunctions.NONE,
title: i18n.translate('xpack.lens.fittingFunctionsTitle.none', { title: i18n.translate('xpack.lens.fittingFunctionsTitle.none', {
defaultMessage: 'Hide', defaultMessage: 'Hide',
}), }),
@ -19,7 +20,7 @@ export const fittingFunctionDefinitions: Array<{ id: FittingFunction } & Record<
}), }),
}, },
{ {
id: 'Zero', id: FittingFunctions.ZERO,
title: i18n.translate('xpack.lens.fittingFunctionsTitle.zero', { title: i18n.translate('xpack.lens.fittingFunctionsTitle.zero', {
defaultMessage: 'Zero', defaultMessage: 'Zero',
}), }),
@ -28,7 +29,7 @@ export const fittingFunctionDefinitions: Array<{ id: FittingFunction } & Record<
}), }),
}, },
{ {
id: 'Linear', id: FittingFunctions.LINEAR,
title: i18n.translate('xpack.lens.fittingFunctionsTitle.linear', { title: i18n.translate('xpack.lens.fittingFunctionsTitle.linear', {
defaultMessage: 'Linear', defaultMessage: 'Linear',
}), }),
@ -37,7 +38,7 @@ export const fittingFunctionDefinitions: Array<{ id: FittingFunction } & Record<
}), }),
}, },
{ {
id: 'Carry', id: FittingFunctions.CARRY,
title: i18n.translate('xpack.lens.fittingFunctionsTitle.carry', { title: i18n.translate('xpack.lens.fittingFunctionsTitle.carry', {
defaultMessage: 'Last', defaultMessage: 'Last',
}), }),
@ -46,7 +47,7 @@ export const fittingFunctionDefinitions: Array<{ id: FittingFunction } & Record<
}), }),
}, },
{ {
id: 'Lookahead', id: FittingFunctions.LOOKAHEAD,
title: i18n.translate('xpack.lens.fittingFunctionsTitle.lookahead', { title: i18n.translate('xpack.lens.fittingFunctionsTitle.lookahead', {
defaultMessage: 'Next', defaultMessage: 'Next',
}), }),

View file

@ -9,6 +9,7 @@ import React from 'react';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { EuiFormRow, EuiIconTip, EuiSuperSelect, EuiSwitch, EuiText } from '@elastic/eui'; import { EuiFormRow, EuiIconTip, EuiSuperSelect, EuiSwitch, EuiText } from '@elastic/eui';
import type { FittingFunction, EndValue } from '@kbn/expression-xy-plugin/common'; import type { FittingFunction, EndValue } from '@kbn/expression-xy-plugin/common';
import { FittingFunctions } from '@kbn/expression-xy-plugin/public';
import { fittingFunctionDefinitions } from './fitting_function_definitions'; import { fittingFunctionDefinitions } from './fitting_function_definitions';
import { endValueDefinitions } from './end_value_definitions'; import { endValueDefinitions } from './end_value_definitions';
@ -25,7 +26,7 @@ export interface MissingValuesOptionProps {
export const MissingValuesOptions: React.FC<MissingValuesOptionProps> = ({ export const MissingValuesOptions: React.FC<MissingValuesOptionProps> = ({
onFittingFnChange, onFittingFnChange,
fittingFunction, fittingFunction,
emphasizeFitting, emphasizeFitting = true,
onEmphasizeFittingChange, onEmphasizeFittingChange,
onEndValueChange, onEndValueChange,
endValue, endValue,
@ -78,13 +79,13 @@ export const MissingValuesOptions: React.FC<MissingValuesOptionProps> = ({
inputDisplay: title, inputDisplay: title,
}; };
})} })}
valueOfSelected={fittingFunction || 'None'} valueOfSelected={fittingFunction || FittingFunctions.LINEAR}
onChange={(value) => onFittingFnChange(value)} onChange={(value) => onFittingFnChange(value)}
itemLayoutAlign="top" itemLayoutAlign="top"
hasDividers hasDividers
/> />
</EuiFormRow> </EuiFormRow>
{fittingFunction && fittingFunction !== 'None' && ( {fittingFunction && fittingFunction !== FittingFunctions.NONE && (
<> <>
<EuiFormRow <EuiFormRow
display="columnCompressed" display="columnCompressed"
@ -109,7 +110,7 @@ export const MissingValuesOptions: React.FC<MissingValuesOptionProps> = ({
inputDisplay: title, inputDisplay: title,
}; };
})} })}
valueOfSelected={endValue || 'None'} valueOfSelected={endValue || FittingFunctions.NONE}
onChange={(value) => onEndValueChange(value)} onChange={(value) => onEndValueChange(value)}
itemLayoutAlign="top" itemLayoutAlign="top"
hasDividers hasDividers

View file

@ -633,7 +633,7 @@ describe('xy_suggestions', () => {
legend: { isVisible: true, position: 'bottom' }, legend: { isVisible: true, position: 'bottom' },
valueLabels: 'hide', valueLabels: 'hide',
preferredSeriesType: 'bar', preferredSeriesType: 'bar',
fittingFunction: 'None', fittingFunction: 'Linear',
layers: [ layers: [
{ {
accessors: ['price'], accessors: ['price'],
@ -691,7 +691,7 @@ describe('xy_suggestions', () => {
legend: { isVisible: true, position: 'bottom' }, legend: { isVisible: true, position: 'bottom' },
valueLabels: 'hide', valueLabels: 'hide',
preferredSeriesType: 'bar', preferredSeriesType: 'bar',
fittingFunction: 'None', fittingFunction: 'Linear',
layers: [ layers: [
{ {
layerId: 'first', layerId: 'first',
@ -823,7 +823,7 @@ describe('xy_suggestions', () => {
const currentState: XYState = { const currentState: XYState = {
legend: { isVisible: true, position: 'bottom' }, legend: { isVisible: true, position: 'bottom' },
valueLabels: 'hide', valueLabels: 'hide',
fittingFunction: 'None', fittingFunction: 'Linear',
preferredSeriesType: 'line', preferredSeriesType: 'line',
layers: [ layers: [
{ {
@ -858,7 +858,7 @@ describe('xy_suggestions', () => {
const currentState: XYState = { const currentState: XYState = {
legend: { isVisible: true, position: 'bottom' }, legend: { isVisible: true, position: 'bottom' },
valueLabels: 'hide', valueLabels: 'hide',
fittingFunction: 'None', fittingFunction: 'Linear',
axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true },
gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true },
tickLabelsVisibilitySettings: { x: true, yLeft: false, yRight: false }, tickLabelsVisibilitySettings: { x: true, yLeft: false, yRight: false },
@ -908,7 +908,7 @@ describe('xy_suggestions', () => {
legend: { isVisible: true, position: 'bottom' }, legend: { isVisible: true, position: 'bottom' },
valueLabels: 'hide', valueLabels: 'hide',
preferredSeriesType: 'bar', preferredSeriesType: 'bar',
fittingFunction: 'None', fittingFunction: 'Linear',
axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true },
gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true },
tickLabelsVisibilitySettings: { x: true, yLeft: false, yRight: false }, tickLabelsVisibilitySettings: { x: true, yLeft: false, yRight: false },
@ -967,7 +967,7 @@ describe('xy_suggestions', () => {
const currentState: XYState = { const currentState: XYState = {
legend: { isVisible: true, position: 'bottom' }, legend: { isVisible: true, position: 'bottom' },
valueLabels: 'hide', valueLabels: 'hide',
fittingFunction: 'None', fittingFunction: 'Linear',
preferredSeriesType: 'bar', preferredSeriesType: 'bar',
layers: [ layers: [
{ {
@ -1006,7 +1006,7 @@ describe('xy_suggestions', () => {
legend: { isVisible: true, position: 'bottom' }, legend: { isVisible: true, position: 'bottom' },
valueLabels: 'hide', valueLabels: 'hide',
preferredSeriesType: 'bar', preferredSeriesType: 'bar',
fittingFunction: 'None', fittingFunction: 'Linear',
layers: [ layers: [
{ {
accessors: ['price'], accessors: ['price'],
@ -1043,7 +1043,7 @@ describe('xy_suggestions', () => {
legend: { isVisible: true, position: 'bottom' }, legend: { isVisible: true, position: 'bottom' },
valueLabels: 'hide', valueLabels: 'hide',
preferredSeriesType: 'bar', preferredSeriesType: 'bar',
fittingFunction: 'None', fittingFunction: 'Linear',
axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true },
gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true },
tickLabelsVisibilitySettings: { x: true, yLeft: false, yRight: false }, tickLabelsVisibilitySettings: { x: true, yLeft: false, yRight: false },
@ -1089,7 +1089,7 @@ describe('xy_suggestions', () => {
legend: { isVisible: true, position: 'bottom' }, legend: { isVisible: true, position: 'bottom' },
valueLabels: 'hide', valueLabels: 'hide',
preferredSeriesType: 'bar', preferredSeriesType: 'bar',
fittingFunction: 'None', fittingFunction: 'Linear',
axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true },
gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true },
tickLabelsVisibilitySettings: { x: true, yLeft: false, yRight: false }, tickLabelsVisibilitySettings: { x: true, yLeft: false, yRight: false },
@ -1136,7 +1136,7 @@ describe('xy_suggestions', () => {
legend: { isVisible: true, position: 'bottom' }, legend: { isVisible: true, position: 'bottom' },
valueLabels: 'hide', valueLabels: 'hide',
preferredSeriesType: 'bar', preferredSeriesType: 'bar',
fittingFunction: 'None', fittingFunction: 'Linear',
axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true },
gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true },
tickLabelsVisibilitySettings: { x: true, yLeft: false, yRight: false }, tickLabelsVisibilitySettings: { x: true, yLeft: false, yRight: false },

View file

@ -8,7 +8,8 @@
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { partition } from 'lodash'; import { partition } from 'lodash';
import { Position } from '@elastic/charts'; import { Position } from '@elastic/charts';
import { LayerTypes } from '@kbn/expression-xy-plugin/public'; import { FittingFunctions, LayerTypes } from '@kbn/expression-xy-plugin/public';
import type { import type {
SuggestionRequest, SuggestionRequest,
VisualizationSuggestion, VisualizationSuggestion,
@ -573,7 +574,7 @@ function buildSuggestion({
const state: State = { const state: State = {
legend: currentState ? currentState.legend : { isVisible: true, position: Position.Right }, legend: currentState ? currentState.legend : { isVisible: true, position: Position.Right },
valueLabels: currentState?.valueLabels || 'hide', valueLabels: currentState?.valueLabels || 'hide',
fittingFunction: currentState?.fittingFunction || 'None', fittingFunction: currentState?.fittingFunction ?? FittingFunctions.LINEAR,
curveType: currentState?.curveType, curveType: currentState?.curveType,
fillOpacity: currentState?.fillOpacity, fillOpacity: currentState?.fillOpacity,
xTitle: currentState?.xTitle, xTitle: currentState?.xTitle,