({
- value: visualizationId,
- text: visualizationId,
- }))}
- value={props.activeVisualizationId || undefined}
- onChange={e => {
- const newState = getSuggestedVisualizationState(
- props.framePublicAPI,
- props.visualizationMap[e.target.value]
- );
- props.dispatch({
- type: 'SWITCH_VISUALIZATION',
- newVisualizationId: e.target.value,
- initialState: newState,
- });
- }}
+
{props.activeVisualizationId && props.visualizationState !== null && (
diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx
index e95e6c982a50c..24cb07e6d62f7 100644
--- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx
+++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx
@@ -53,8 +53,28 @@ describe('editor_frame', () => {
};
beforeEach(() => {
- mockVisualization = createMockVisualization();
- mockVisualization2 = createMockVisualization();
+ mockVisualization = {
+ ...createMockVisualization(),
+ id: 'testVis',
+ visualizationTypes: [
+ {
+ icon: 'empty',
+ id: 'testVis',
+ label: 'TEST1',
+ },
+ ],
+ };
+ mockVisualization2 = {
+ ...createMockVisualization(),
+ id: 'testVis2',
+ visualizationTypes: [
+ {
+ icon: 'empty',
+ id: 'testVis2',
+ label: 'TEST2',
+ },
+ ],
+ };
mockDatasource = createMockDatasource();
mockDatasource2 = createMockDatasource();
@@ -223,7 +243,7 @@ describe('editor_frame', () => {
expect(mockVisualization.initialize).toHaveBeenCalledWith({
datasourceLayers: {},
addNewLayer: expect.any(Function),
- removeLayer: expect.any(Function),
+ removeLayers: expect.any(Function),
});
});
@@ -255,6 +275,37 @@ describe('editor_frame', () => {
expect(mockDatasource2.insertLayer).toHaveBeenCalledWith(initialState, expect.anything());
});
+ it('should remove layer on active datasource on frame api call', async () => {
+ const initialState = { datasource2: '' };
+ mockDatasource2.initialize.mockReturnValue(Promise.resolve(initialState));
+ mockDatasource2.getLayers.mockReturnValue(['abc', 'def']);
+ mockDatasource2.removeLayer.mockReturnValue({ removed: true });
+ act(() => {
+ mount(
+
+ );
+ });
+
+ await waitForPromises();
+
+ mockVisualization.initialize.mock.calls[0][0].removeLayers(['abc', 'def']);
+
+ expect(mockDatasource2.removeLayer).toHaveBeenCalledWith(initialState, 'abc');
+ expect(mockDatasource2.removeLayer).toHaveBeenCalledWith({ removed: true }, 'def');
+ });
+
it('should render data panel after initialization is complete', async () => {
const initialState = {};
let databaseInitialized: ({}) => void;
@@ -795,8 +846,27 @@ describe('editor_frame', () => {
describe('switching', () => {
let instance: ReactWrapper;
+ function switchTo(subType: string) {
+ act(() => {
+ instance
+ .find('[data-test-subj="lnsChartSwitchPopover"]')
+ .last()
+ .simulate('click');
+ });
+
+ instance.update();
+
+ act(() => {
+ instance
+ .find(`[data-test-subj="lnsChartSwitchPopover_${subType}"]`)
+ .last()
+ .simulate('click');
+ });
+ }
+
beforeEach(async () => {
mockDatasource.getLayers.mockReturnValue(['first']);
+
instance = mount(
{
});
it('should initialize other visualization on switch', async () => {
- act(() => {
- instance
- .find('select[data-test-subj="visualization-switch"]')
- .simulate('change', { target: { value: 'testVis2' } });
- });
+ switchTo('testVis2');
expect(mockVisualization2.initialize).toHaveBeenCalled();
});
@@ -883,11 +949,7 @@ describe('editor_frame', () => {
},
]);
- act(() => {
- instance
- .find('select[data-test-subj="visualization-switch"]')
- .simulate('change', { target: { value: 'testVis2' } });
- });
+ switchTo('testVis2');
expect(mockVisualization2.getSuggestions).toHaveBeenCalled();
expect(mockVisualization2.initialize).toHaveBeenCalledWith(expect.anything(), initialState);
@@ -900,11 +962,7 @@ describe('editor_frame', () => {
it('should fall back when switching visualizations if the visualization has no suggested use', async () => {
mockVisualization2.initialize.mockReturnValueOnce({ initial: true });
- act(() => {
- instance
- .find('select[data-test-subj="visualization-switch"]')
- .simulate('change', { target: { value: 'testVis2' } });
- });
+ switchTo('testVis2');
expect(mockDatasource.publicAPIMock.getTableSpec).toHaveBeenCalled();
expect(mockVisualization2.getSuggestions).toHaveBeenCalled();
diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx
index 12ba3ff6e3192..09b2884b29b92 100644
--- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx
+++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx
@@ -90,32 +90,32 @@ export function EditorFrame(props: EditorFrameProps) {
const framePublicAPI: FramePublicAPI = {
datasourceLayers,
- addNewLayer: () => {
- const newLayerId = generateId();
- const newState = props.datasourceMap[state.activeDatasourceId!].insertLayer(
- state.datasourceStates[state.activeDatasourceId!].state,
- newLayerId
- );
+ addNewLayer() {
+ const newLayerId = generateId();
dispatch({
- type: 'UPDATE_DATASOURCE_STATE',
+ type: 'UPDATE_LAYER',
datasourceId: state.activeDatasourceId!,
- newState,
+ layerId: newLayerId,
+ updater: props.datasourceMap[state.activeDatasourceId!].insertLayer,
});
return newLayerId;
},
- removeLayer: (layerId: string) => {
- const newState = props.datasourceMap[state.activeDatasourceId!].removeLayer(
- state.datasourceStates[state.activeDatasourceId!].state,
- layerId
- );
-
- dispatch({
- type: 'UPDATE_DATASOURCE_STATE',
- datasourceId: state.activeDatasourceId!,
- newState,
+ removeLayers: (layerIds: string[]) => {
+ layerIds.forEach(layerId => {
+ const layerDatasourceId = Object.entries(props.datasourceMap).find(
+ ([datasourceId, datasource]) =>
+ state.datasourceStates[datasourceId] &&
+ datasource.getLayers(state.datasourceStates[datasourceId].state).includes(layerId)
+ )![0];
+ dispatch({
+ type: 'UPDATE_LAYER',
+ layerId,
+ datasourceId: layerDatasourceId,
+ updater: props.datasourceMap[layerDatasourceId].removeLayer,
+ });
});
},
};
diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts
index fe829c70cc063..67e5ef1f192ed 100644
--- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts
+++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts
@@ -35,7 +35,7 @@ describe('save editor frame state', () => {
activeDatasourceId: 'indexpattern',
framePublicAPI: {
addNewLayer: jest.fn(),
- removeLayer: jest.fn(),
+ removeLayers: jest.fn(),
datasourceLayers: {
first: mockDatasource.publicAPIMock,
},
diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts
index e02614ffb4be5..eccca61814303 100644
--- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts
+++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts
@@ -153,6 +153,36 @@ describe('editor_frame state management', () => {
expect(newState.datasourceStates.testDatasource.state).toBe(newDatasourceState);
});
+ it('should update the datasource state with passed in reducer', () => {
+ const layerReducer = jest.fn((_state, layerId) => ({ inserted: layerId }));
+ const newState = reducer(
+ {
+ datasourceStates: {
+ testDatasource: {
+ state: {},
+ isLoading: false,
+ },
+ },
+ activeDatasourceId: 'testDatasource',
+ saving: false,
+ title: 'bbb',
+ visualization: {
+ activeId: 'testVis',
+ state: {},
+ },
+ },
+ {
+ type: 'UPDATE_LAYER',
+ layerId: 'abc',
+ updater: layerReducer,
+ datasourceId: 'testDatasource',
+ }
+ );
+
+ expect(newState.datasourceStates.testDatasource.state).toEqual({ inserted: 'abc' });
+ expect(layerReducer).toHaveBeenCalledTimes(1);
+ });
+
it('should should switch active visualization', () => {
const testVisState = {};
const newVisState = {};
diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts
index 004bbc45194a1..bfa52341b9495 100644
--- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts
+++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts
@@ -46,6 +46,12 @@ export type Action =
type: 'UPDATE_VISUALIZATION_STATE';
newState: unknown;
}
+ | {
+ type: 'UPDATE_LAYER';
+ layerId: string;
+ datasourceId: string;
+ updater: (state: unknown, layerId: string) => unknown;
+ }
| {
type: 'VISUALIZATION_LOADED';
doc: Document;
@@ -97,6 +103,20 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta
return { ...state, persistedId: action.id };
case 'UPDATE_TITLE':
return { ...state, title: action.title };
+ case 'UPDATE_LAYER':
+ return {
+ ...state,
+ datasourceStates: {
+ ...state.datasourceStates,
+ [action.datasourceId]: {
+ ...state.datasourceStates[action.datasourceId],
+ state: action.updater(
+ state.datasourceStates[action.datasourceId].state,
+ action.layerId
+ ),
+ },
+ },
+ };
case 'VISUALIZATION_LOADED':
return {
...state,
diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx
index ad30f84165d2c..cdd4f1bf07898 100644
--- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx
+++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx
@@ -11,6 +11,16 @@ import { EditorFrameSetupPlugins } from './plugin';
export function createMockVisualization(): jest.Mocked {
return {
+ id: 'TEST_VIS',
+ visualizationTypes: [
+ {
+ icon: 'empty',
+ id: 'TEST_VIS',
+ label: 'TEST',
+ },
+ ],
+ getDescription: jest.fn(_state => ({ label: '' })),
+ switchVisualizationType: jest.fn((_, x) => x),
getPersistableState: jest.fn(_state => _state),
getSuggestions: jest.fn(_options => []),
initialize: jest.fn((_frame, _state?) => ({})),
@@ -59,7 +69,7 @@ export function createMockFramePublicAPI(): FrameMock {
return {
datasourceLayers: {},
addNewLayer: jest.fn(() => ''),
- removeLayer: jest.fn(),
+ removeLayers: jest.fn(),
};
}
diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx
index fac75fe421d59..99beee31463d5 100644
--- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx
+++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx
@@ -146,8 +146,8 @@ export class EditorFramePlugin {
registerDatasource: (name, datasource) => {
this.datasources[name] = datasource as Datasource;
},
- registerVisualization: (name, visualization) => {
- this.visualizations[name] = visualization as Visualization;
+ registerVisualization: visualization => {
+ this.visualizations[visualization.id] = visualization as Visualization;
},
};
}
diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx
index 5a43ebe8918a8..779d06a0a0bdb 100644
--- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx
+++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx
@@ -249,6 +249,7 @@ export function getIndexPatternDatasource({
removeLayer(state: IndexPatternPrivateState, layerId: string) {
const newLayers = { ...state.layers };
delete newLayers[layerId];
+
return {
...state,
layers: newLayers,
@@ -287,11 +288,9 @@ export function getIndexPatternDatasource({
return state.layers[layerId].columnOrder.map(colId => ({ columnId: colId }));
},
getOperationForColumnId: (columnId: string) => {
- const layer = Object.values(state.layers).find(l =>
- l.columnOrder.find(id => id === columnId)
- );
+ const layer = state.layers[layerId];
- if (layer) {
+ if (layer && layer.columns[columnId]) {
return columnToOperation(layer.columns[columnId]);
}
return null;
diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts
index 39c0db880a8f3..387acd6612810 100644
--- a/x-pack/legacy/plugins/lens/public/types.ts
+++ b/x-pack/legacy/plugins/lens/public/types.ts
@@ -5,6 +5,7 @@
*/
import { Ast } from '@kbn/interpreter/common';
+import { EuiIconType } from '@elastic/eui/src/components/icon/icon';
import { DragContextState } from './drag_drop';
// eslint-disable-next-line
@@ -21,7 +22,7 @@ export interface EditorFrameSetup {
createInstance: (options: EditorFrameOptions) => EditorFrameInstance;
// generic type on the API functions to pull the "unknown vs. specific type" error into the implementation
registerDatasource: (name: string, datasource: Datasource) => void;
- registerVisualization: (name: string, visualization: Visualization) => void;
+ registerVisualization: (visualization: Visualization) => void;
}
// Hints the default nesting to the data source. 0 is the highest priority
@@ -186,10 +187,29 @@ export interface FramePublicAPI {
datasourceLayers: Record;
// Adds a new layer. This has a side effect of updating the datasource state
addNewLayer: () => string;
- removeLayer: (layerId: string) => void;
+ removeLayers: (layerIds: string[]) => void;
+}
+
+export interface VisualizationType {
+ id: string;
+ icon?: EuiIconType | string;
+ label: string;
}
export interface Visualization {
+ id: string;
+
+ visualizationTypes: VisualizationType[];
+
+ getDescription: (
+ state: T
+ ) => {
+ icon?: EuiIconType | string;
+ label: string;
+ };
+
+ switchVisualizationType?: (visualizationTypeId: string, state: T) => T;
+
// For initializing from saved object
initialize: (frame: FramePublicAPI, state?: P) => T;
diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts
index c5650fde3ddac..2287ec3c3d1ef 100644
--- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts
+++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts
@@ -5,10 +5,12 @@
*/
import { Position } from '@elastic/charts';
+import { i18n } from '@kbn/i18n';
import {
ExpressionFunction,
ArgumentType,
} from '../../../../../../src/legacy/core_plugins/interpreter/public';
+import { VisualizationType } from '..';
export interface LegendConfig {
isVisible: boolean;
@@ -177,6 +179,7 @@ export interface XYArgs {
// Persisted parts of the state
export interface XYState {
+ preferredSeriesType: SeriesType;
legend: LegendConfig;
layers: LayerConfig[];
isHorizontal: boolean;
@@ -184,3 +187,41 @@ export interface XYState {
export type State = XYState;
export type PersistableState = XYState;
+
+export const visualizationTypes: VisualizationType[] = [
+ {
+ id: 'bar',
+ icon: 'visBarVertical',
+ label: i18n.translate('xpack.lens.xyVisualization.barLabel', {
+ defaultMessage: 'Bar',
+ }),
+ },
+ {
+ id: 'bar_stacked',
+ icon: 'visBarVertical',
+ label: i18n.translate('xpack.lens.xyVisualization.stackedBarLabel', {
+ defaultMessage: 'Stacked Bar',
+ }),
+ },
+ {
+ id: 'line',
+ icon: 'visLine',
+ label: i18n.translate('xpack.lens.xyVisualization.lineLabel', {
+ defaultMessage: 'Line',
+ }),
+ },
+ {
+ id: 'area',
+ icon: 'visArea',
+ label: i18n.translate('xpack.lens.xyVisualization.areaLabel', {
+ defaultMessage: 'Area',
+ }),
+ },
+ {
+ id: 'area_stacked',
+ icon: 'visArea',
+ label: i18n.translate('xpack.lens.xyVisualization.stackedAreaLabel', {
+ defaultMessage: 'Stacked Area',
+ }),
+ },
+];
diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx
index 4d772ca9a48ab..64ceddac2021d 100644
--- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx
+++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx
@@ -10,7 +10,7 @@ import { mountWithIntl as mount } from 'test_utils/enzyme_helpers';
import { EuiButtonGroupProps } from '@elastic/eui';
import { XYConfigPanel } from './xy_config_panel';
import { DatasourceDimensionPanelProps, Operation, FramePublicAPI } from '../types';
-import { State } from './types';
+import { State, XYState } from './types';
import { Position } from '@elastic/charts';
import { NativeRendererProps } from '../native_renderer';
import { generateId } from '../id_generator';
@@ -27,6 +27,7 @@ describe('XYConfigPanel', () => {
function testState(): State {
return {
legend: { isVisible: true, position: Position.Right },
+ preferredSeriesType: 'bar',
isHorizontal: false,
layers: [
{
@@ -119,11 +120,11 @@ describe('XYConfigPanel', () => {
.prop('options') as EuiButtonGroupProps['options'];
expect(options.map(({ id }) => id)).toEqual([
+ 'bar',
+ 'bar_stacked',
'line',
'area',
- 'bar',
'area_stacked',
- 'bar_stacked',
]);
expect(options.filter(({ isDisabled }) => isDisabled).map(({ id }) => id)).toEqual([]);
@@ -294,6 +295,109 @@ describe('XYConfigPanel', () => {
],
});
});
+
+ it('should use series type of existing layers if they all have the same', () => {
+ frame.addNewLayer = jest.fn().mockReturnValue('newLayerId');
+ frame.datasourceLayers.second = createMockDatasource().publicAPIMock;
+ (generateId as jest.Mock).mockReturnValue('accessor');
+ const setState = jest.fn();
+ const state: XYState = {
+ ...testState(),
+ preferredSeriesType: 'bar',
+ layers: [
+ {
+ seriesType: 'line',
+ layerId: 'first',
+ splitAccessor: 'baz',
+ xAccessor: 'foo',
+ title: 'X',
+ accessors: ['bar'],
+ },
+ {
+ seriesType: 'line',
+ layerId: 'second',
+ splitAccessor: 'baz',
+ xAccessor: 'foo',
+ title: 'Y',
+ accessors: ['bar'],
+ },
+ ],
+ };
+ const component = mount(
+
+ );
+
+ component
+ .find('[data-test-subj="lnsXY_layer_add"]')
+ .first()
+ .simulate('click');
+
+ expect(setState.mock.calls[0][0]).toMatchObject({
+ layers: [
+ ...state.layers,
+ expect.objectContaining({
+ seriesType: 'line',
+ }),
+ ],
+ });
+ });
+
+ it('should use preffered series type if there are already various different layers', () => {
+ frame.addNewLayer = jest.fn().mockReturnValue('newLayerId');
+ frame.datasourceLayers.second = createMockDatasource().publicAPIMock;
+ (generateId as jest.Mock).mockReturnValue('accessor');
+ const setState = jest.fn();
+ const state: XYState = {
+ ...testState(),
+ preferredSeriesType: 'bar',
+ layers: [
+ {
+ seriesType: 'area',
+ layerId: 'first',
+ splitAccessor: 'baz',
+ xAccessor: 'foo',
+ title: 'X',
+ accessors: ['bar'],
+ },
+ {
+ seriesType: 'line',
+ layerId: 'second',
+ splitAccessor: 'baz',
+ xAccessor: 'foo',
+ title: 'Y',
+ accessors: ['bar'],
+ },
+ ],
+ };
+ const component = mount(
+
+ );
+
+ component
+ .find('[data-test-subj="lnsXY_layer_add"]')
+ .first()
+ .simulate('click');
+
+ expect(setState.mock.calls[0][0]).toMatchObject({
+ layers: [
+ ...state.layers,
+ expect.objectContaining({
+ seriesType: 'bar',
+ }),
+ ],
+ });
+ });
+
it('removes layers', () => {
const setState = jest.fn();
const state = testState();
@@ -313,7 +417,7 @@ describe('XYConfigPanel', () => {
.first()
.simulate('click');
- expect(frame.removeLayer).toHaveBeenCalled();
+ expect(frame.removeLayers).toHaveBeenCalled();
expect(setState).toHaveBeenCalledTimes(1);
expect(setState.mock.calls[0][0]).toMatchObject({
layers: [],
diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx
index f9d6cc01a29ca..03892abc84c3e 100644
--- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx
+++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import _ from 'lodash';
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -15,56 +16,16 @@ import {
EuiForm,
EuiFormRow,
EuiPanel,
- EuiPopover,
- IconType,
EuiButtonIcon,
- EuiIcon,
+ EuiPopover,
EuiSwitch,
} from '@elastic/eui';
-import { State, SeriesType, LayerConfig } from './types';
+import { State, SeriesType, LayerConfig, visualizationTypes } from './types';
import { VisualizationProps, OperationMetadata } from '../types';
import { NativeRenderer } from '../native_renderer';
import { MultiColumnEditor } from '../multi_column_editor';
import { generateId } from '../id_generator';
-export const chartTypeIcons: Array<{ id: SeriesType; label: string; iconType: IconType }> = [
- {
- id: 'line',
- label: i18n.translate('xpack.lens.xyVisualization.lineChartLabel', {
- defaultMessage: 'Line',
- }),
- iconType: 'visLine',
- },
- {
- id: 'area',
- label: i18n.translate('xpack.lens.xyVisualization.areaChartLabel', {
- defaultMessage: 'Area',
- }),
- iconType: 'visArea',
- },
- {
- id: 'bar',
- label: i18n.translate('xpack.lens.xyVisualization.barChartLabel', {
- defaultMessage: 'Bar',
- }),
- iconType: 'visBarVertical',
- },
- {
- id: 'area_stacked',
- label: i18n.translate('xpack.lens.xyVisualization.stackedAreaChartLabel', {
- defaultMessage: 'Stacked Area',
- }),
- iconType: 'visArea',
- },
- {
- id: 'bar_stacked',
- label: i18n.translate('xpack.lens.xyVisualization.stackedBarChartLabel', {
- defaultMessage: 'Stacked Bar',
- }),
- iconType: 'visBarVertical',
- },
-];
-
const isNumericMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number';
const isBucketed = (op: OperationMetadata) => op.isBucketed;
@@ -80,39 +41,95 @@ function updateLayer(state: State, layer: UnwrapArray, index: n
};
}
-function newLayerState(layerId: string): LayerConfig {
+function newLayerState(seriesType: SeriesType, layerId: string): LayerConfig {
return {
layerId,
+ seriesType,
xAccessor: generateId(),
- seriesType: 'bar_stacked',
accessors: [generateId()],
title: '',
splitAccessor: generateId(),
};
}
-interface LocalState {
- isChartOptionsOpen: boolean;
- openLayerId: string | null;
+function LayerSettings({
+ layer,
+ setSeriesType,
+ removeLayer,
+}: {
+ layer: LayerConfig;
+ setSeriesType: (seriesType: SeriesType) => void;
+ removeLayer: () => void;
+}) {
+ const [isOpen, setIsOpen] = useState(false);
+ const { icon } = visualizationTypes.find(c => c.id === layer.seriesType)!;
+
+ return (
+ setIsOpen(!isOpen)}
+ data-test-subj="lnsXY_layer_advanced"
+ />
+ }
+ isOpen={isOpen}
+ closePopover={() => setIsOpen(false)}
+ anchorPosition="leftUp"
+ >
+
+ ({
+ ...t,
+ iconType: t.icon || 'empty',
+ }))}
+ idSelected={layer.seriesType}
+ onChange={seriesType => setSeriesType(seriesType as SeriesType)}
+ isIconOnly
+ />
+
+
+
+ {i18n.translate('xpack.lens.xyChart.removeLayer', {
+ defaultMessage: 'Remove layer',
+ })}
+
+
+
+ );
}
export function XYConfigPanel(props: VisualizationProps) {
const { state, setState, frame } = props;
-
- const [localState, setLocalState] = useState({
- isChartOptionsOpen: false,
- openLayerId: null,
- } as LocalState);
+ const [isChartOptionsOpen, setIsChartOptionsOpen] = useState(false);
return (
{
- setLocalState({ ...localState, isChartOptionsOpen: false });
- }}
+ isOpen={isChartOptionsOpen}
+ closePopover={() => setIsChartOptionsOpen(false)}
button={
<>
@@ -120,9 +137,7 @@ export function XYConfigPanel(props: VisualizationProps) {
iconType="gear"
size="s"
data-test-subj="lnsXY_chart_settings"
- onClick={() => {
- setLocalState({ ...localState, isChartOptionsOpen: true });
- }}
+ onClick={() => setIsChartOptionsOpen(true)}
>
) {
- {
- setLocalState({ ...localState, openLayerId: null });
- }}
- button={
- {
- setLocalState({ ...localState, openLayerId: layer.layerId });
- }}
- />
- }
- >
- <>
-
- {
- setState(
- updateLayer(
- state,
- { ...layer, seriesType: seriesType as SeriesType },
- index
- )
- );
- }}
- isIconOnly
- />
-
-
- {
- frame.removeLayer(layer.layerId);
- setState({ ...state, layers: state.layers.filter(l => l !== layer) });
- }}
- >
-
-
- >
-
-
-
-
- icon.id === layer.seriesType)!.iconType}
- />
+
+
+ setState(updateLayer(state, { ...layer, seriesType }, index))
+ }
+ removeLayer={() => {
+ frame.removeLayers([layer.layerId]);
+ setState({ ...state, layers: state.layers.filter(l => l !== layer) });
+ }}
+ />
+
@@ -238,7 +195,6 @@ export function XYConfigPanel(props: VisualizationProps) {
/>
-
) {
}}
/>
-
) {
}}
/>
-
) {
size="s"
data-test-subj={`lnsXY_layer_add`}
onClick={() => {
+ const usedSeriesTypes = _.uniq(state.layers.map(layer => layer.seriesType));
setState({
...state,
- layers: [...state.layers, newLayerState(frame.addNewLayer())],
+ layers: [
+ ...state.layers,
+ newLayerState(
+ usedSeriesTypes.length === 1 ? usedSeriesTypes[0] : state.preferredSeriesType,
+ frame.addNewLayer()
+ ),
+ ],
});
}}
iconType="plusInCircle"
diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx
index de122bcb0dd24..e3b0232382bd8 100644
--- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx
+++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx
@@ -22,9 +22,8 @@ import { ExpressionFunction } from 'src/legacy/core_plugins/interpreter/types';
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, IconType } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { LensMultiTable } from '../types';
-import { XYArgs, SeriesType } from './types';
+import { XYArgs, SeriesType, visualizationTypes } from './types';
import { RenderFunction } from '../interpreter_types';
-import { chartTypeIcons } from './xy_config_panel';
export interface XYChartProps {
data: LensMultiTable;
@@ -97,7 +96,7 @@ export const xyChartRenderer: RenderFunction = {
};
function getIconForSeriesType(seriesType: SeriesType): IconType {
- return chartTypeIcons.find(chartTypeIcon => chartTypeIcon.id === seriesType)!.iconType;
+ return visualizationTypes.find(c => c.id === seriesType)!.icon || 'empty';
}
export function XYChart({ data, args }: XYChartProps) {
diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts
index b997745eb8fff..a7eb5f30b7128 100644
--- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts
+++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts
@@ -12,7 +12,7 @@ import {
TableSuggestionColumn,
TableSuggestion,
} from '../types';
-import { State } from './types';
+import { State, SeriesType } from './types';
import { generateId } from '../id_generator';
import { buildExpression } from './to_expression';
@@ -102,15 +102,18 @@ function getSuggestion(
// TODO: Localize the title, label, etc
const preposition = isDate ? 'over' : 'of';
const title = `${yTitle} ${preposition} ${xTitle}`;
+ const seriesType: SeriesType =
+ (currentState && currentState.preferredSeriesType) || (splitBy && isDate ? 'line' : 'bar');
const state: State = {
isHorizontal: false,
legend: currentState ? currentState.legend : { isVisible: true, position: Position.Right },
+ preferredSeriesType: seriesType,
layers: [
...(currentState ? currentState.layers.filter(layer => layer.layerId !== layerId) : []),
{
layerId,
+ seriesType,
xAccessor: xValue.columnId,
- seriesType: splitBy && isDate ? 'line' : 'bar',
splitAccessor: splitBy ? splitBy.columnId : generateId(),
accessors: yValues.map(col => col.columnId),
title: yTitle,
diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts
index d8854764ff898..1ee0e86f5b6b9 100644
--- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts
+++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts
@@ -18,6 +18,7 @@ function exampleState(): State {
return {
isHorizontal: false,
legend: { position: Position.Bottom, isVisible: true },
+ preferredSeriesType: 'bar',
layers: [
{
layerId: 'first',
@@ -67,6 +68,7 @@ describe('xy_visualization', () => {
"isVisible": true,
"position": "right",
},
+ "preferredSeriesType": "bar",
"title": "Empty XY Chart",
}
`);
diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx
index 437c993a5dae4..f0b3c1a79ed2b 100644
--- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx
+++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx
@@ -5,17 +5,70 @@
*/
import React from 'react';
+import _ from 'lodash';
import { render } from 'react-dom';
import { Position } from '@elastic/charts';
import { I18nProvider } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
import { getSuggestions } from './xy_suggestions';
import { XYConfigPanel } from './xy_config_panel';
import { Visualization } from '../types';
-import { State, PersistableState } from './types';
+import { State, PersistableState, SeriesType, visualizationTypes } from './types';
import { toExpression } from './to_expression';
import { generateId } from '../id_generator';
+const defaultIcon = 'visBarVertical';
+const defaultSeriesType = 'bar';
+
+function getDescription(state?: State) {
+ if (!state) {
+ return {
+ icon: defaultIcon,
+ label: i18n.translate('xpack.lens.xyVisualization.xyLabel', {
+ defaultMessage: 'XY Chart',
+ }),
+ };
+ }
+
+ if (!state.layers.length) {
+ return visualizationTypes.find(v => v.id === state.preferredSeriesType)!;
+ }
+
+ const visualizationType = visualizationTypes.find(t => t.id === state.layers[0].seriesType)!;
+ const seriesTypes = _.unique(state.layers.map(l => l.seriesType));
+
+ return {
+ icon: visualizationType.icon,
+ label:
+ seriesTypes.length === 1
+ ? visualizationType.label
+ : i18n.translate('xpack.lens.xyVisualization.mixedLabel', {
+ defaultMessage: 'Mixed XY Chart',
+ }),
+ };
+}
+
export const xyVisualization: Visualization = {
+ id: 'lnsXY',
+
+ visualizationTypes,
+
+ getDescription(state) {
+ const { icon, label } = getDescription(state);
+ return {
+ icon: icon || defaultIcon,
+ label,
+ };
+ },
+
+ switchVisualizationType(seriesType: string, state: State) {
+ return {
+ ...state,
+ preferredSeriesType: seriesType as SeriesType,
+ layers: state.layers.map(layer => ({ ...layer, seriesType: seriesType as SeriesType })),
+ };
+ },
+
getSuggestions,
initialize(frame, state) {
@@ -24,12 +77,13 @@ export const xyVisualization: Visualization = {
title: 'Empty XY Chart',
isHorizontal: false,
legend: { isVisible: true, position: Position.Right },
+ preferredSeriesType: defaultSeriesType,
layers: [
{
layerId: frame.addNewLayer(),
accessors: [generateId()],
position: Position.Top,
- seriesType: 'bar',
+ seriesType: defaultSeriesType,
showGridlines: false,
splitAccessor: generateId(),
title: '',