[Lens] Fitting functions (#69820)
This commit is contained in:
parent
e70fcc708e
commit
bbda3f99ef
15 changed files with 379 additions and 37 deletions
|
@ -68,29 +68,33 @@ export function WorkspacePanelWrapper({
|
||||||
return (
|
return (
|
||||||
<EuiFlexGroup gutterSize="s" direction="column" alignItems="stretch" responsive={false}>
|
<EuiFlexGroup gutterSize="s" direction="column" alignItems="stretch" responsive={false}>
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<ChartSwitch
|
<EuiFlexGroup gutterSize="s" direction="row" responsive={false}>
|
||||||
data-test-subj="lnsChartSwitcher"
|
<EuiFlexItem grow={false}>
|
||||||
visualizationMap={visualizationMap}
|
<ChartSwitch
|
||||||
visualizationId={visualizationId}
|
data-test-subj="lnsChartSwitcher"
|
||||||
visualizationState={visualizationState}
|
visualizationMap={visualizationMap}
|
||||||
datasourceMap={datasourceMap}
|
visualizationId={visualizationId}
|
||||||
datasourceStates={datasourceStates}
|
visualizationState={visualizationState}
|
||||||
dispatch={dispatch}
|
datasourceMap={datasourceMap}
|
||||||
framePublicAPI={framePublicAPI}
|
datasourceStates={datasourceStates}
|
||||||
/>
|
dispatch={dispatch}
|
||||||
|
framePublicAPI={framePublicAPI}
|
||||||
|
/>
|
||||||
|
</EuiFlexItem>
|
||||||
|
{activeVisualization && activeVisualization.renderToolbar && (
|
||||||
|
<EuiFlexItem grow>
|
||||||
|
<NativeRenderer
|
||||||
|
render={activeVisualization.renderToolbar}
|
||||||
|
nativeProps={{
|
||||||
|
frame: framePublicAPI,
|
||||||
|
state: visualizationState,
|
||||||
|
setState: setVisualizationState,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</EuiFlexItem>
|
||||||
|
)}
|
||||||
|
</EuiFlexGroup>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
{activeVisualization && activeVisualization.renderToolbar && (
|
|
||||||
<EuiFlexItem grow={false}>
|
|
||||||
<NativeRenderer
|
|
||||||
render={activeVisualization.renderToolbar}
|
|
||||||
nativeProps={{
|
|
||||||
frame: framePublicAPI,
|
|
||||||
state: visualizationState,
|
|
||||||
setState: setVisualizationState,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</EuiFlexItem>
|
|
||||||
)}
|
|
||||||
<EuiFlexItem>
|
<EuiFlexItem>
|
||||||
<EuiPageContent className="lnsWorkspacePanelWrapper">
|
<EuiPageContent className="lnsWorkspacePanelWrapper">
|
||||||
{(!emptyExpression || title) && (
|
{(!emptyExpression || title) && (
|
||||||
|
|
|
@ -5,6 +5,9 @@ Object {
|
||||||
"chain": Array [
|
"chain": Array [
|
||||||
Object {
|
Object {
|
||||||
"arguments": Object {
|
"arguments": Object {
|
||||||
|
"fittingFunction": Array [
|
||||||
|
"Carry",
|
||||||
|
],
|
||||||
"layers": Array [
|
"layers": Array [
|
||||||
Object {
|
Object {
|
||||||
"chain": Array [
|
"chain": Array [
|
||||||
|
|
|
@ -54,6 +54,11 @@ exports[`xy_expression XYChart component it renders area 1`] = `
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
enableHistogramMode={false}
|
enableHistogramMode={false}
|
||||||
|
fit={
|
||||||
|
Object {
|
||||||
|
"type": "none",
|
||||||
|
}
|
||||||
|
}
|
||||||
groupId="left"
|
groupId="left"
|
||||||
id="d-a"
|
id="d-a"
|
||||||
key="0-0"
|
key="0-0"
|
||||||
|
@ -93,6 +98,11 @@ exports[`xy_expression XYChart component it renders area 1`] = `
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
enableHistogramMode={false}
|
enableHistogramMode={false}
|
||||||
|
fit={
|
||||||
|
Object {
|
||||||
|
"type": "none",
|
||||||
|
}
|
||||||
|
}
|
||||||
groupId="left"
|
groupId="left"
|
||||||
id="d-b"
|
id="d-b"
|
||||||
key="0-1"
|
key="0-1"
|
||||||
|
@ -402,6 +412,11 @@ exports[`xy_expression XYChart component it renders line 1`] = `
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
enableHistogramMode={false}
|
enableHistogramMode={false}
|
||||||
|
fit={
|
||||||
|
Object {
|
||||||
|
"type": "none",
|
||||||
|
}
|
||||||
|
}
|
||||||
groupId="left"
|
groupId="left"
|
||||||
id="d-a"
|
id="d-a"
|
||||||
key="0-0"
|
key="0-0"
|
||||||
|
@ -441,6 +456,11 @@ exports[`xy_expression XYChart component it renders line 1`] = `
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
enableHistogramMode={false}
|
enableHistogramMode={false}
|
||||||
|
fit={
|
||||||
|
Object {
|
||||||
|
"type": "none",
|
||||||
|
}
|
||||||
|
}
|
||||||
groupId="left"
|
groupId="left"
|
||||||
id="d-b"
|
id="d-b"
|
||||||
key="0-1"
|
key="0-1"
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
* 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 { Fit } from '@elastic/charts';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
|
||||||
|
export type FittingFunction = typeof fittingFunctionDefinitions[number]['id'];
|
||||||
|
|
||||||
|
export const fittingFunctionDefinitions = [
|
||||||
|
{
|
||||||
|
id: 'None',
|
||||||
|
title: i18n.translate('xpack.lens.fittingFunctionsTitle.none', {
|
||||||
|
defaultMessage: 'Hide',
|
||||||
|
}),
|
||||||
|
description: i18n.translate('xpack.lens.fittingFunctionsDescription.none', {
|
||||||
|
defaultMessage: 'Do not fill gaps',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Zero',
|
||||||
|
title: i18n.translate('xpack.lens.fittingFunctionsTitle.zero', {
|
||||||
|
defaultMessage: 'Zero',
|
||||||
|
}),
|
||||||
|
description: i18n.translate('xpack.lens.fittingFunctionsDescription.zero', {
|
||||||
|
defaultMessage: 'Fill gaps with zeros',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Linear',
|
||||||
|
title: i18n.translate('xpack.lens.fittingFunctionsTitle.linear', {
|
||||||
|
defaultMessage: 'Linear',
|
||||||
|
}),
|
||||||
|
description: i18n.translate('xpack.lens.fittingFunctionsDescription.linear', {
|
||||||
|
defaultMessage: 'Fill gaps with a line',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Carry',
|
||||||
|
title: i18n.translate('xpack.lens.fittingFunctionsTitle.carry', {
|
||||||
|
defaultMessage: 'Last',
|
||||||
|
}),
|
||||||
|
description: i18n.translate('xpack.lens.fittingFunctionsDescription.carry', {
|
||||||
|
defaultMessage: 'Fill gaps with the last value',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Lookahead',
|
||||||
|
title: i18n.translate('xpack.lens.fittingFunctionsTitle.lookahead', {
|
||||||
|
defaultMessage: 'Next',
|
||||||
|
}),
|
||||||
|
description: i18n.translate('xpack.lens.fittingFunctionsDescription.lookahead', {
|
||||||
|
defaultMessage: 'Fill gaps with the next value',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export function getFitEnum(fittingFunction?: FittingFunction) {
|
||||||
|
if (fittingFunction) {
|
||||||
|
return Fit[fittingFunction];
|
||||||
|
}
|
||||||
|
return Fit.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFitOptions(fittingFunction?: FittingFunction) {
|
||||||
|
return { type: getFitEnum(fittingFunction) };
|
||||||
|
}
|
|
@ -40,6 +40,7 @@ describe('#toExpression', () => {
|
||||||
{
|
{
|
||||||
legend: { position: Position.Bottom, isVisible: true },
|
legend: { position: Position.Bottom, isVisible: true },
|
||||||
preferredSeriesType: 'bar',
|
preferredSeriesType: 'bar',
|
||||||
|
fittingFunction: 'Carry',
|
||||||
layers: [
|
layers: [
|
||||||
{
|
{
|
||||||
layerId: 'first',
|
layerId: 'first',
|
||||||
|
@ -55,6 +56,27 @@ describe('#toExpression', () => {
|
||||||
).toMatchSnapshot();
|
).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should default the fitting function to None', () => {
|
||||||
|
expect(
|
||||||
|
(xyVisualization.toExpression(
|
||||||
|
{
|
||||||
|
legend: { position: Position.Bottom, isVisible: true },
|
||||||
|
preferredSeriesType: 'bar',
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
layerId: 'first',
|
||||||
|
seriesType: 'area',
|
||||||
|
splitAccessor: 'd',
|
||||||
|
xAccessor: 'a',
|
||||||
|
accessors: ['b', 'c'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
frame
|
||||||
|
) as Ast).chain[0].arguments.fittingFunction[0]
|
||||||
|
).toEqual('None');
|
||||||
|
});
|
||||||
|
|
||||||
it('should not generate an expression when missing x', () => {
|
it('should not generate an expression when missing x', () => {
|
||||||
expect(
|
expect(
|
||||||
xyVisualization.toExpression(
|
xyVisualization.toExpression(
|
||||||
|
|
|
@ -133,6 +133,7 @@ export const buildExpression = (
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
fittingFunction: [state.fittingFunction || 'None'],
|
||||||
layers: validLayers.map((layer) => {
|
layers: validLayers.map((layer) => {
|
||||||
const columnToLabel: Record<string, string> = {};
|
const columnToLabel: Record<string, string> = {};
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ import chartBarHorizontalStackedSVG from '../assets/chart_bar_horizontal_stacked
|
||||||
import chartLineSVG from '../assets/chart_line.svg';
|
import chartLineSVG from '../assets/chart_line.svg';
|
||||||
|
|
||||||
import { VisualizationType } from '../index';
|
import { VisualizationType } from '../index';
|
||||||
|
import { FittingFunction } from './fitting_functions';
|
||||||
|
|
||||||
export interface LegendConfig {
|
export interface LegendConfig {
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
|
@ -225,12 +226,14 @@ export interface XYArgs {
|
||||||
yTitle: string;
|
yTitle: string;
|
||||||
legend: LegendConfig & { type: 'lens_xy_legendConfig' };
|
legend: LegendConfig & { type: 'lens_xy_legendConfig' };
|
||||||
layers: LayerArgs[];
|
layers: LayerArgs[];
|
||||||
|
fittingFunction?: FittingFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persisted parts of the state
|
// Persisted parts of the state
|
||||||
export interface XYState {
|
export interface XYState {
|
||||||
preferredSeriesType: SeriesType;
|
preferredSeriesType: SeriesType;
|
||||||
legend: LegendConfig;
|
legend: LegendConfig;
|
||||||
|
fittingFunction?: FittingFunction;
|
||||||
layers: LayerConfig[];
|
layers: LayerConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
.lnsXyToolbar__popover {
|
||||||
|
width: 400px;
|
||||||
|
}
|
|
@ -5,15 +5,15 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithIntl as mount } from 'test_utils/enzyme_helpers';
|
import { mountWithIntl as mount, shallowWithIntl as shallow } from 'test_utils/enzyme_helpers';
|
||||||
import { EuiButtonGroupProps } from '@elastic/eui';
|
import { EuiButtonGroupProps, EuiSuperSelect } from '@elastic/eui';
|
||||||
import { LayerContextMenu } from './xy_config_panel';
|
import { LayerContextMenu, XyToolbar } from './xy_config_panel';
|
||||||
import { FramePublicAPI } from '../types';
|
import { FramePublicAPI } from '../types';
|
||||||
import { State } from './types';
|
import { State } from './types';
|
||||||
import { Position } from '@elastic/charts';
|
import { Position } from '@elastic/charts';
|
||||||
import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_service/mocks';
|
import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_service/mocks';
|
||||||
|
|
||||||
describe('LayerContextMenu', () => {
|
describe('XY Config panels', () => {
|
||||||
let frame: FramePublicAPI;
|
let frame: FramePublicAPI;
|
||||||
|
|
||||||
function testState(): State {
|
function testState(): State {
|
||||||
|
@ -39,11 +39,6 @@ describe('LayerContextMenu', () => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
test.skip('allows toggling of legend visibility', () => {});
|
|
||||||
test.skip('allows changing legend position', () => {});
|
|
||||||
test.skip('allows toggling the y axis gridlines', () => {});
|
|
||||||
test.skip('allows toggling the x axis gridlines', () => {});
|
|
||||||
|
|
||||||
describe('LayerContextMenu', () => {
|
describe('LayerContextMenu', () => {
|
||||||
test('enables stacked chart types even when there is no split series', () => {
|
test('enables stacked chart types even when there is no split series', () => {
|
||||||
const state = testState();
|
const state = testState();
|
||||||
|
@ -92,4 +87,45 @@ describe('LayerContextMenu', () => {
|
||||||
expect(options!.filter(({ isDisabled }) => isDisabled).map(({ id }) => id)).toEqual([]);
|
expect(options!.filter(({ isDisabled }) => isDisabled).map(({ id }) => id)).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('XyToolbar', () => {
|
||||||
|
it('should show currently selected fitting function', () => {
|
||||||
|
const state = testState();
|
||||||
|
|
||||||
|
const component = shallow(
|
||||||
|
<XyToolbar
|
||||||
|
frame={frame}
|
||||||
|
setState={jest.fn()}
|
||||||
|
state={{
|
||||||
|
...state,
|
||||||
|
layers: [{ ...state.layers[0], seriesType: 'line' }],
|
||||||
|
fittingFunction: 'Carry',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(component.find(EuiSuperSelect).prop('valueOfSelected')).toEqual('Carry');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disable the select if there is no unstacked area or line series', () => {
|
||||||
|
const state = testState();
|
||||||
|
|
||||||
|
const component = shallow(
|
||||||
|
<XyToolbar
|
||||||
|
frame={frame}
|
||||||
|
setState={jest.fn()}
|
||||||
|
state={{
|
||||||
|
...state,
|
||||||
|
layers: [
|
||||||
|
{ ...state.layers[0], seriesType: 'bar' },
|
||||||
|
{ ...state.layers[0], seriesType: 'area_stacked' },
|
||||||
|
],
|
||||||
|
fittingFunction: 'Carry',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(component.find(EuiSuperSelect).prop('disabled')).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,8 +8,14 @@ import React, { useState } from 'react';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import {
|
import {
|
||||||
|
EuiButtonEmpty,
|
||||||
EuiButtonGroup,
|
EuiButtonGroup,
|
||||||
|
EuiFlexGroup,
|
||||||
|
EuiFlexItem,
|
||||||
|
EuiSuperSelect,
|
||||||
EuiFormRow,
|
EuiFormRow,
|
||||||
|
EuiPopover,
|
||||||
|
EuiText,
|
||||||
htmlIdGenerator,
|
htmlIdGenerator,
|
||||||
EuiForm,
|
EuiForm,
|
||||||
EuiColorPicker,
|
EuiColorPicker,
|
||||||
|
@ -17,10 +23,17 @@ import {
|
||||||
EuiToolTip,
|
EuiToolTip,
|
||||||
EuiIcon,
|
EuiIcon,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
|
import {
|
||||||
|
VisualizationLayerWidgetProps,
|
||||||
|
VisualizationDimensionEditorProps,
|
||||||
|
VisualizationToolbarProps,
|
||||||
|
} from '../types';
|
||||||
import { State, SeriesType, visualizationTypes, YAxisMode } from './types';
|
import { State, SeriesType, visualizationTypes, YAxisMode } from './types';
|
||||||
import { VisualizationDimensionEditorProps, VisualizationLayerWidgetProps } from '../types';
|
|
||||||
import { isHorizontalChart, isHorizontalSeries, getSeriesColor } from './state_helpers';
|
import { isHorizontalChart, isHorizontalSeries, getSeriesColor } from './state_helpers';
|
||||||
import { trackUiEvent } from '../lens_ui_telemetry';
|
import { trackUiEvent } from '../lens_ui_telemetry';
|
||||||
|
import { fittingFunctionDefinitions } from './fitting_functions';
|
||||||
|
|
||||||
|
import './xy_config_panel.scss';
|
||||||
|
|
||||||
type UnwrapArray<T> = T extends Array<infer P> ? P : T;
|
type UnwrapArray<T> = T extends Array<infer P> ? P : T;
|
||||||
|
|
||||||
|
@ -78,6 +91,75 @@ export function LayerContextMenu(props: VisualizationLayerWidgetProps<State>) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function XyToolbar(props: VisualizationToolbarProps<State>) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const hasNonBarSeries = props.state?.layers.some(
|
||||||
|
(layer) => layer.seriesType === 'line' || layer.seriesType === 'area'
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<EuiFlexGroup justifyContent="flexEnd">
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiPopover
|
||||||
|
panelClassName="lnsXyToolbar__popover"
|
||||||
|
button={
|
||||||
|
<EuiButtonEmpty
|
||||||
|
color="text"
|
||||||
|
iconType="arrowDown"
|
||||||
|
iconSide="right"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(!open);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n.translate('xpack.lens.xyChart.settingsLabel', { defaultMessage: 'Settings' })}
|
||||||
|
</EuiButtonEmpty>
|
||||||
|
}
|
||||||
|
isOpen={open}
|
||||||
|
closePopover={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
anchorPosition="downRight"
|
||||||
|
>
|
||||||
|
<EuiFormRow
|
||||||
|
display="columnCompressed"
|
||||||
|
label={i18n.translate('xpack.lens.xyChart.fittingLabel', {
|
||||||
|
defaultMessage: 'Fill missing values',
|
||||||
|
})}
|
||||||
|
helpText={
|
||||||
|
!hasNonBarSeries &&
|
||||||
|
i18n.translate('xpack.lens.xyChart.fittingDisabledHelpText', {
|
||||||
|
defaultMessage:
|
||||||
|
'This setting only applies to line charts and unstacked area charts.',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<EuiSuperSelect
|
||||||
|
compressed
|
||||||
|
disabled={!hasNonBarSeries}
|
||||||
|
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={props.state?.fittingFunction || 'None'}
|
||||||
|
onChange={(value) => props.setState({ ...props.state, fittingFunction: value })}
|
||||||
|
itemLayoutAlign="top"
|
||||||
|
hasDividers
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
</EuiPopover>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
const idPrefix = htmlIdGenerator()();
|
const idPrefix = htmlIdGenerator()();
|
||||||
|
|
||||||
export function DimensionEditor(props: VisualizationDimensionEditorProps<State>) {
|
export function DimensionEditor(props: VisualizationDimensionEditorProps<State>) {
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
GeometryValue,
|
GeometryValue,
|
||||||
XYChartSeriesIdentifier,
|
XYChartSeriesIdentifier,
|
||||||
SeriesNameFn,
|
SeriesNameFn,
|
||||||
|
Fit,
|
||||||
} from '@elastic/charts';
|
} from '@elastic/charts';
|
||||||
import { xyChart, XYChart } from './xy_expression';
|
import { xyChart, XYChart } from './xy_expression';
|
||||||
import { LensMultiTable } from '../types';
|
import { LensMultiTable } from '../types';
|
||||||
|
@ -1554,5 +1555,66 @@ describe('xy_expression', () => {
|
||||||
|
|
||||||
expect(component.find(Settings).prop('showLegend')).toEqual(true);
|
expect(component.find(Settings).prop('showLegend')).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('it should apply the fitting function to all non-bar series', () => {
|
||||||
|
const data: LensMultiTable = {
|
||||||
|
type: 'lens_multitable',
|
||||||
|
tables: {
|
||||||
|
first: createSampleDatatableWithRows([
|
||||||
|
{ a: 1, b: 2, c: 'I', d: 'Foo' },
|
||||||
|
{ a: 1, b: 5, c: 'J', d: 'Bar' },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const args: XYArgs = createArgsWithLayers([
|
||||||
|
{ ...sampleLayer, accessors: ['a'] },
|
||||||
|
{ ...sampleLayer, seriesType: 'bar', accessors: ['a'] },
|
||||||
|
{ ...sampleLayer, seriesType: 'area', accessors: ['a'] },
|
||||||
|
{ ...sampleLayer, seriesType: 'area_stacked', accessors: ['a'] },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const component = shallow(
|
||||||
|
<XYChart
|
||||||
|
data={{ ...data }}
|
||||||
|
args={{ ...args, fittingFunction: 'Carry' }}
|
||||||
|
formatFactory={getFormatSpy}
|
||||||
|
timeZone="UTC"
|
||||||
|
chartsThemeService={chartsThemeService}
|
||||||
|
histogramBarTarget={50}
|
||||||
|
onClickValue={onClickValue}
|
||||||
|
onSelectRange={onSelectRange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(component.find(LineSeries).prop('fit')).toEqual({ type: Fit.Carry });
|
||||||
|
expect(component.find(BarSeries).prop('fit')).toEqual(undefined);
|
||||||
|
expect(component.find(AreaSeries).at(0).prop('fit')).toEqual({ type: Fit.Carry });
|
||||||
|
expect(component.find(AreaSeries).at(0).prop('stackAccessors')).toEqual([]);
|
||||||
|
// stacked area series doesn't get the fit prop
|
||||||
|
expect(component.find(AreaSeries).at(1).prop('fit')).toEqual(undefined);
|
||||||
|
expect(component.find(AreaSeries).at(1).prop('stackAccessors')).toEqual(['c']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it should apply None fitting function if not specified', () => {
|
||||||
|
const { data, args } = sampleArgs();
|
||||||
|
|
||||||
|
args.layers[0].accessors = ['a'];
|
||||||
|
|
||||||
|
const component = shallow(
|
||||||
|
<XYChart
|
||||||
|
data={{ ...data }}
|
||||||
|
args={{ ...args }}
|
||||||
|
formatFactory={getFormatSpy}
|
||||||
|
timeZone="UTC"
|
||||||
|
chartsThemeService={chartsThemeService}
|
||||||
|
histogramBarTarget={50}
|
||||||
|
onClickValue={onClickValue}
|
||||||
|
onSelectRange={onSelectRange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(component.find(LineSeries).prop('fit')).toEqual({ type: Fit.None });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -40,6 +40,7 @@ import { parseInterval } from '../../../../../src/plugins/data/common';
|
||||||
import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
|
import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
|
||||||
import { EmptyPlaceholder } from '../shared_components';
|
import { EmptyPlaceholder } from '../shared_components';
|
||||||
import { desanitizeFilterContext } from '../utils';
|
import { desanitizeFilterContext } from '../utils';
|
||||||
|
import { fittingFunctionDefinitions, getFitOptions } from './fitting_functions';
|
||||||
import { getAxesConfiguration } from './axes_configuration';
|
import { getAxesConfiguration } from './axes_configuration';
|
||||||
|
|
||||||
type InferPropType<T> = T extends React.FunctionComponent<infer P> ? P : T;
|
type InferPropType<T> = T extends React.FunctionComponent<infer P> ? P : T;
|
||||||
|
@ -94,6 +95,13 @@ export const xyChart: ExpressionFunctionDefinition<
|
||||||
defaultMessage: 'Configure the chart legend.',
|
defaultMessage: 'Configure the chart legend.',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
fittingFunction: {
|
||||||
|
types: ['string'],
|
||||||
|
options: [...fittingFunctionDefinitions.map(({ id }) => id)],
|
||||||
|
help: i18n.translate('xpack.lens.xyChart.fittingFunction.help', {
|
||||||
|
defaultMessage: 'Define how missing values are treated',
|
||||||
|
}),
|
||||||
|
},
|
||||||
layers: {
|
layers: {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
types: ['lens_xy_layer'] as any,
|
types: ['lens_xy_layer'] as any,
|
||||||
|
@ -191,7 +199,7 @@ export function XYChart({
|
||||||
onClickValue,
|
onClickValue,
|
||||||
onSelectRange,
|
onSelectRange,
|
||||||
}: XYChartRenderProps) {
|
}: XYChartRenderProps) {
|
||||||
const { legend, layers } = args;
|
const { legend, layers, fittingFunction } = args;
|
||||||
const chartTheme = chartsThemeService.useChartsTheme();
|
const chartTheme = chartsThemeService.useChartsTheme();
|
||||||
const chartBaseTheme = chartsThemeService.useChartsBaseTheme();
|
const chartBaseTheme = chartsThemeService.useChartsBaseTheme();
|
||||||
|
|
||||||
|
@ -463,7 +471,7 @@ export function XYChart({
|
||||||
}
|
}
|
||||||
// This handles both split and single-y cases:
|
// This handles both split and single-y cases:
|
||||||
// * If split series without formatting, show the value literally
|
// * If split series without formatting, show the value literally
|
||||||
// * If single Y, the seriesKey will be the acccessor, so we show the human-readable name
|
// * If single Y, the seriesKey will be the accessor, so we show the human-readable name
|
||||||
return splitAccessor ? d.seriesKeys[0] : columnToLabelMap[d.seriesKeys[0]] ?? '';
|
return splitAccessor ? d.seriesKeys[0] : columnToLabelMap[d.seriesKeys[0]] ?? '';
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -472,17 +480,29 @@ export function XYChart({
|
||||||
|
|
||||||
switch (seriesType) {
|
switch (seriesType) {
|
||||||
case 'line':
|
case 'line':
|
||||||
return <LineSeries key={index} {...seriesProps} />;
|
return (
|
||||||
|
<LineSeries key={index} {...seriesProps} fit={getFitOptions(fittingFunction)} />
|
||||||
|
);
|
||||||
case 'bar':
|
case 'bar':
|
||||||
case 'bar_stacked':
|
case 'bar_stacked':
|
||||||
case 'bar_horizontal':
|
case 'bar_horizontal':
|
||||||
case 'bar_horizontal_stacked':
|
case 'bar_horizontal_stacked':
|
||||||
return <BarSeries key={index} {...seriesProps} />;
|
return <BarSeries key={index} {...seriesProps} />;
|
||||||
default:
|
case 'area_stacked':
|
||||||
return <AreaSeries key={index} {...seriesProps} />;
|
return <AreaSeries key={index} {...seriesProps} />;
|
||||||
|
case 'area':
|
||||||
|
return (
|
||||||
|
<AreaSeries key={index} {...seriesProps} fit={getFitOptions(fittingFunction)} />
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return assertNever(seriesType);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
</Chart>
|
</Chart>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assertNever(x: never): never {
|
||||||
|
throw new Error('Unexpected series type: ' + x);
|
||||||
|
}
|
||||||
|
|
|
@ -331,6 +331,7 @@ describe('xy_suggestions', () => {
|
||||||
test('makes a visible seriesType suggestion for unchanged table without split', () => {
|
test('makes a visible seriesType suggestion for unchanged table without split', () => {
|
||||||
const currentState: XYState = {
|
const currentState: XYState = {
|
||||||
legend: { isVisible: true, position: 'bottom' },
|
legend: { isVisible: true, position: 'bottom' },
|
||||||
|
fittingFunction: 'None',
|
||||||
preferredSeriesType: 'bar',
|
preferredSeriesType: 'bar',
|
||||||
layers: [
|
layers: [
|
||||||
{
|
{
|
||||||
|
@ -368,6 +369,7 @@ describe('xy_suggestions', () => {
|
||||||
const currentState: XYState = {
|
const currentState: XYState = {
|
||||||
legend: { isVisible: true, position: 'bottom' },
|
legend: { isVisible: true, position: 'bottom' },
|
||||||
preferredSeriesType: 'bar',
|
preferredSeriesType: 'bar',
|
||||||
|
fittingFunction: 'None',
|
||||||
layers: [
|
layers: [
|
||||||
{
|
{
|
||||||
accessors: ['price', 'quantity'],
|
accessors: ['price', 'quantity'],
|
||||||
|
@ -408,6 +410,7 @@ describe('xy_suggestions', () => {
|
||||||
(generateId as jest.Mock).mockReturnValueOnce('dummyCol');
|
(generateId as jest.Mock).mockReturnValueOnce('dummyCol');
|
||||||
const currentState: XYState = {
|
const currentState: XYState = {
|
||||||
legend: { isVisible: true, position: 'bottom' },
|
legend: { isVisible: true, position: 'bottom' },
|
||||||
|
fittingFunction: 'None',
|
||||||
preferredSeriesType: 'bar',
|
preferredSeriesType: 'bar',
|
||||||
layers: [
|
layers: [
|
||||||
{
|
{
|
||||||
|
@ -440,6 +443,7 @@ describe('xy_suggestions', () => {
|
||||||
const currentState: XYState = {
|
const currentState: XYState = {
|
||||||
legend: { isVisible: true, position: 'bottom' },
|
legend: { isVisible: true, position: 'bottom' },
|
||||||
preferredSeriesType: 'bar',
|
preferredSeriesType: 'bar',
|
||||||
|
fittingFunction: 'None',
|
||||||
layers: [
|
layers: [
|
||||||
{
|
{
|
||||||
accessors: ['price'],
|
accessors: ['price'],
|
||||||
|
@ -474,6 +478,7 @@ describe('xy_suggestions', () => {
|
||||||
const currentState: XYState = {
|
const currentState: XYState = {
|
||||||
legend: { isVisible: true, position: 'bottom' },
|
legend: { isVisible: true, position: 'bottom' },
|
||||||
preferredSeriesType: 'bar',
|
preferredSeriesType: 'bar',
|
||||||
|
fittingFunction: 'None',
|
||||||
layers: [
|
layers: [
|
||||||
{
|
{
|
||||||
accessors: ['price', 'quantity'],
|
accessors: ['price', 'quantity'],
|
||||||
|
@ -512,6 +517,7 @@ describe('xy_suggestions', () => {
|
||||||
const currentState: XYState = {
|
const currentState: XYState = {
|
||||||
legend: { isVisible: true, position: 'bottom' },
|
legend: { isVisible: true, position: 'bottom' },
|
||||||
preferredSeriesType: 'bar',
|
preferredSeriesType: 'bar',
|
||||||
|
fittingFunction: 'None',
|
||||||
layers: [
|
layers: [
|
||||||
{
|
{
|
||||||
accessors: ['price'],
|
accessors: ['price'],
|
||||||
|
@ -551,6 +557,7 @@ describe('xy_suggestions', () => {
|
||||||
const currentState: XYState = {
|
const currentState: XYState = {
|
||||||
legend: { isVisible: true, position: 'bottom' },
|
legend: { isVisible: true, position: 'bottom' },
|
||||||
preferredSeriesType: 'bar',
|
preferredSeriesType: 'bar',
|
||||||
|
fittingFunction: 'None',
|
||||||
layers: [
|
layers: [
|
||||||
{
|
{
|
||||||
accessors: ['price', 'quantity'],
|
accessors: ['price', 'quantity'],
|
||||||
|
|
|
@ -402,6 +402,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 },
|
||||||
|
fittingFunction: currentState?.fittingFunction || 'None',
|
||||||
preferredSeriesType: seriesType,
|
preferredSeriesType: seriesType,
|
||||||
layers: [...keptLayers, newLayer],
|
layers: [...keptLayers, newLayer],
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { Position } from '@elastic/charts';
|
||||||
import { I18nProvider } from '@kbn/i18n/react';
|
import { I18nProvider } from '@kbn/i18n/react';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { getSuggestions } from './xy_suggestions';
|
import { getSuggestions } from './xy_suggestions';
|
||||||
import { DimensionEditor, LayerContextMenu } from './xy_config_panel';
|
import { LayerContextMenu, XyToolbar, DimensionEditor } from './xy_config_panel';
|
||||||
import { Visualization, OperationMetadata, VisualizationType } from '../types';
|
import { Visualization, OperationMetadata, VisualizationType } from '../types';
|
||||||
import { State, PersistableState, SeriesType, visualizationTypes, LayerConfig } from './types';
|
import { State, PersistableState, SeriesType, visualizationTypes, LayerConfig } from './types';
|
||||||
import chartBarStackedSVG from '../assets/chart_bar_stacked.svg';
|
import chartBarStackedSVG from '../assets/chart_bar_stacked.svg';
|
||||||
|
@ -264,6 +264,15 @@ export const xyVisualization: Visualization<State, PersistableState> = {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
renderToolbar(domElement, props) {
|
||||||
|
render(
|
||||||
|
<I18nProvider>
|
||||||
|
<XyToolbar {...props} />
|
||||||
|
</I18nProvider>,
|
||||||
|
domElement
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
renderDimensionEditor(domElement, props) {
|
renderDimensionEditor(domElement, props) {
|
||||||
render(
|
render(
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
|
|
Loading…
Reference in a new issue