diff --git a/src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx b/src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx index 78fb43772504c..9c6443e8a254d 100644 --- a/src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx +++ b/src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx @@ -37,6 +37,7 @@ export type ExpressionRendererProps = Pick< * this callback is called with the given result. */ onRenderFailure?: (result: Result) => void; + className?: string; }; export type ExpressionRenderer = React.FC; @@ -44,6 +45,7 @@ export type ExpressionRenderer = React.FC; export const createRenderer = (run: ExpressionRunner): ExpressionRenderer => ({ expression, onRenderFailure, + className, ...options }: ExpressionRendererProps) => { const mountpoint: React.MutableRefObject = useRef(null); @@ -63,6 +65,7 @@ export const createRenderer = (run: ExpressionRunner): ExpressionRenderer => ({ return (
{ mountpoint.current = el; }} diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx index 1cd7993922f6d..d4d9476240e68 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx @@ -158,7 +158,7 @@ export const datatableVisualization: Visualization< > { return tables.map(table => { const title = i18n.translate('xpack.lens.datatable.visualizationOf', { - defaultMessage: 'Table: ${operations}', + defaultMessage: 'Table: {operations}', values: { operations: table.columns.map(col => col.operation.label).join(' & '), }, @@ -174,6 +174,7 @@ export const datatableVisualization: Visualization< label: col.operation.label, })), }, + previewIcon: 'visTable', }; }); }, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/__mocks__/suggestion_helpers.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/__mocks__/suggestion_helpers.ts new file mode 100644 index 0000000000000..94d162aa5f1b0 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/__mocks__/suggestion_helpers.ts @@ -0,0 +1,11 @@ +/* + * 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. + */ + +const actual = jest.requireActual('../suggestion_helpers'); + +jest.spyOn(actual, 'getSuggestions'); + +export const { getSuggestions, toSwitchAction } = actual; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx index 677b37beab190..1341266ba56a5 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx @@ -23,18 +23,21 @@ function getSuggestedVisualizationState( visualization: Visualization, datasource: DatasourcePublicAPI ) { - const suggestions = visualization.getSuggestions({ - tables: [ - { - datasourceSuggestionId: 0, - isMultiRow: true, - columns: datasource.getTableSpec().map(col => ({ - ...col, - operation: datasource.getOperationForColumnId(col.columnId)!, - })), - }, - ], - }); + const suggestions = visualization.getSuggestions( + { + tables: [ + { + datasourceSuggestionId: 0, + isMultiRow: true, + columns: datasource.getTableSpec().map(col => ({ + ...col, + operation: datasource.getOperationForColumnId(col.columnId)!, + })), + }, + ], + }, + datasource + ); if (!suggestions.length) { return visualization.initialize(datasource); 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 8d67c632ea5c9..90e66336e2880 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 @@ -529,6 +529,7 @@ Object { score: 1, datasourceSuggestionId: 0, state: initialState, + previewIcon: 'empty', }, ]); @@ -627,12 +628,14 @@ Object { score: 0.5, state: {}, title: 'Suggestion2', + previewIcon: 'empty', }, { datasourceSuggestionId: 0, score: 0.8, state: {}, title: 'Suggestion1', + previewIcon: 'empty', }, ], }, @@ -644,12 +647,14 @@ Object { score: 0.4, state: {}, title: 'Suggestion4', + previewIcon: 'empty', }, { datasourceSuggestionId: 0, score: 0.45, state: {}, title: 'Suggestion3', + previewIcon: 'empty', }, ], }, @@ -670,7 +675,7 @@ Object { // TODO why is this necessary? instance.update(); - const suggestions = instance.find('[data-test-subj="suggestion"]'); + const suggestions = instance.find('[data-test-subj="suggestion-title"]'); expect(suggestions.map(el => el.text())).toEqual([ 'Suggestion1', 'Suggestion2', @@ -693,6 +698,7 @@ Object { score: 0.8, state: suggestionVisState, title: 'Suggestion1', + previewIcon: 'empty', }, ], }, @@ -716,7 +722,7 @@ Object { instance.update(); act(() => { - instance.find('[data-test-subj="suggestion"]').simulate('click'); + instance.find('[data-test-subj="suggestion-title"]').simulate('click'); }); expect(mockVisualization.renderConfigPanel).toHaveBeenCalledTimes(1); @@ -747,12 +753,14 @@ Object { score: 0.2, state: {}, title: 'Suggestion1', + previewIcon: 'empty', }, { datasourceSuggestionId: 0, score: 0.8, state: suggestionVisState, title: 'Suggestion2', + previewIcon: 'empty', }, ], }, @@ -801,12 +809,14 @@ Object { score: 0.2, state: {}, title: 'Suggestion1', + previewIcon: 'empty', }, { datasourceSuggestionId: 0, score: 0.6, state: {}, title: 'Suggestion2', + previewIcon: 'empty', }, ], }, @@ -818,6 +828,7 @@ Object { score: 0.8, state: suggestionVisState, title: 'Suggestion3', + previewIcon: 'empty', }, ], }, 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 696949ca41c81..e1353530215b1 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 @@ -129,10 +129,12 @@ export function EditorFrame(props: EditorFrameProps) { } /> diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts index ad6cdb0d7e4ee..8566035c29bf2 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts @@ -7,21 +7,12 @@ import { Ast, fromExpression } from '@kbn/interpreter/common'; import { Visualization, Datasource, DatasourcePublicAPI } from '../../types'; -export function buildExpression( - visualization: Visualization | null, - visualizationState: unknown, +export function prependDatasourceExpression( + visualizationExpression: Ast | string | null, datasource: Datasource, - datasourceState: unknown, - datasourcePublicAPI: DatasourcePublicAPI + datasourceState: unknown ): Ast | null { - if (visualization === null) { - return null; - } const datasourceExpression = datasource.toExpression(datasourceState); - const visualizationExpression = visualization.toExpression( - visualizationState, - datasourcePublicAPI - ); if (datasourceExpression === null || visualizationExpression === null) { return null; @@ -40,3 +31,21 @@ export function buildExpression( chain: [...parsedDatasourceExpression.chain, ...parsedVisualizationExpression.chain], }; } + +export function buildExpression( + visualization: Visualization | null, + visualizationState: unknown, + datasource: Datasource, + datasourceState: unknown, + datasourcePublicAPI: DatasourcePublicAPI +): Ast | null { + if (visualization === null) { + return null; + } + const visualizationExpression = visualization.toExpression( + visualizationState, + datasourcePublicAPI + ); + + return prependDatasourceExpression(visualizationExpression, datasource, datasourceState); +} diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.scss b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.scss new file mode 100644 index 0000000000000..182e36df61797 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.scss @@ -0,0 +1,2 @@ +@import './workspace_panel.scss'; +@import './suggestion_panel.scss'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.test.ts index 850cdfc2b3c0f..7acc626bcc918 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.test.ts @@ -5,7 +5,7 @@ */ import { getSuggestions } from './suggestion_helpers'; -import { createMockVisualization } from '../mocks'; +import { createMockVisualization, createMockDatasource } from '../mocks'; import { TableSuggestion } from '../../types'; const generateSuggestion = (datasourceSuggestionId: number = 1, state = {}) => ({ @@ -23,12 +23,19 @@ describe('suggestion helpers', () => { vis1: { ...mockVisualization, getSuggestions: () => [ - { datasourceSuggestionId: 0, score: 0.5, title: 'Test', state: suggestedState }, + { + datasourceSuggestionId: 0, + score: 0.5, + title: 'Test', + state: suggestedState, + previewIcon: 'empty', + }, ], }, }, 'vis1', - {} + {}, + createMockDatasource().publicAPIMock ); expect(suggestions).toHaveLength(1); expect(suggestions[0].state).toBe(suggestedState); @@ -43,19 +50,38 @@ describe('suggestion helpers', () => { vis1: { ...mockVisualization1, getSuggestions: () => [ - { datasourceSuggestionId: 0, score: 0.5, title: 'Test', state: {} }, - { datasourceSuggestionId: 0, score: 0.5, title: 'Test2', state: {} }, + { + datasourceSuggestionId: 0, + score: 0.5, + title: 'Test', + state: {}, + previewIcon: 'empty', + }, + { + datasourceSuggestionId: 0, + score: 0.5, + title: 'Test2', + state: {}, + previewIcon: 'empty', + }, ], }, vis2: { ...mockVisualization2, getSuggestions: () => [ - { datasourceSuggestionId: 0, score: 0.5, title: 'Test3', state: {} }, + { + datasourceSuggestionId: 0, + score: 0.5, + title: 'Test3', + state: {}, + previewIcon: 'empty', + }, ], }, }, 'vis1', - {} + {}, + createMockDatasource().publicAPIMock ); expect(suggestions).toHaveLength(3); }); @@ -69,19 +95,38 @@ describe('suggestion helpers', () => { vis1: { ...mockVisualization1, getSuggestions: () => [ - { datasourceSuggestionId: 0, score: 0.2, title: 'Test', state: {} }, - { datasourceSuggestionId: 0, score: 0.8, title: 'Test2', state: {} }, + { + datasourceSuggestionId: 0, + score: 0.2, + title: 'Test', + state: {}, + previewIcon: 'empty', + }, + { + datasourceSuggestionId: 0, + score: 0.8, + title: 'Test2', + state: {}, + previewIcon: 'empty', + }, ], }, vis2: { ...mockVisualization2, getSuggestions: () => [ - { datasourceSuggestionId: 0, score: 0.6, title: 'Test3', state: {} }, + { + datasourceSuggestionId: 0, + score: 0.6, + title: 'Test3', + state: {}, + previewIcon: 'empty', + }, ], }, }, 'vis1', - {} + {}, + createMockDatasource().publicAPIMock ); expect(suggestions[0].score).toBe(0.8); expect(suggestions[1].score).toBe(0.6); @@ -100,7 +145,8 @@ describe('suggestion helpers', () => { vis2: mockVisualization2, }, 'vis1', - {} + {}, + createMockDatasource().publicAPIMock ); expect(mockVisualization1.getSuggestions.mock.calls[0][0].tables[0]).toBe(table1); expect(mockVisualization1.getSuggestions.mock.calls[0][0].tables[1]).toBe(table2); @@ -119,19 +165,38 @@ describe('suggestion helpers', () => { vis1: { ...mockVisualization1, getSuggestions: () => [ - { datasourceSuggestionId: 0, score: 0.3, title: 'Test', state: {} }, - { datasourceSuggestionId: 1, score: 0.2, title: 'Test2', state: {} }, + { + datasourceSuggestionId: 0, + score: 0.3, + title: 'Test', + state: {}, + previewIcon: 'empty', + }, + { + datasourceSuggestionId: 1, + score: 0.2, + title: 'Test2', + state: {}, + previewIcon: 'empty', + }, ], }, vis2: { ...mockVisualization2, getSuggestions: () => [ - { datasourceSuggestionId: 1, score: 0.1, title: 'Test3', state: {} }, + { + datasourceSuggestionId: 1, + score: 0.1, + title: 'Test3', + state: {}, + previewIcon: 'empty', + }, ], }, }, 'vis1', - {} + {}, + createMockDatasource().publicAPIMock ); expect(suggestions[0].datasourceState).toBe(tableState1); expect(suggestions[1].datasourceState).toBe(tableState2); @@ -141,6 +206,7 @@ describe('suggestion helpers', () => { it('should pass the state of the currently active visualization to getSuggestions', () => { const mockVisualization1 = createMockVisualization(); const mockVisualization2 = createMockVisualization(); + const datasourcePublicAPI = createMockDatasource().publicAPIMock; const currentState = {}; getSuggestions( [generateSuggestion(1), generateSuggestion(2)], @@ -149,17 +215,20 @@ describe('suggestion helpers', () => { vis2: mockVisualization2, }, 'vis1', - currentState + currentState, + datasourcePublicAPI ); expect(mockVisualization1.getSuggestions).toHaveBeenCalledWith( expect.objectContaining({ state: currentState, - }) + }), + datasourcePublicAPI ); expect(mockVisualization2.getSuggestions).not.toHaveBeenCalledWith( expect.objectContaining({ state: currentState, - }) + }), + datasourcePublicAPI ); }); }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.ts index 459f5d89fb9c3..891f492bbcce1 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Visualization, DatasourceSuggestion } from '../../types'; +import { Ast } from '@kbn/interpreter/common'; +import { Visualization, DatasourceSuggestion, DatasourcePublicAPI } from '../../types'; import { Action } from './state_management'; export interface Suggestion { @@ -13,6 +14,8 @@ export interface Suggestion { score: number; title: string; state: unknown; + previewExpression?: Ast | string; + previewIcon: string; } /** @@ -27,7 +30,8 @@ export function getSuggestions( datasourceTableSuggestions: DatasourceSuggestion[], visualizationMap: Record, activeVisualizationId: string | null, - visualizationState: unknown + visualizationState: unknown, + datasourcePublicAPI: DatasourcePublicAPI ): Suggestion[] { const datasourceTables = datasourceTableSuggestions.map(({ table }) => table); @@ -35,10 +39,13 @@ export function getSuggestions( Object.entries(visualizationMap) .map(([visualizationId, visualization]) => { return visualization - .getSuggestions({ - tables: datasourceTables, - state: visualizationId === activeVisualizationId ? visualizationState : undefined, - }) + .getSuggestions( + { + tables: datasourceTables, + state: visualizationId === activeVisualizationId ? visualizationState : undefined, + }, + datasourcePublicAPI + ) .map(({ datasourceSuggestionId, ...suggestion }) => ({ ...suggestion, visualizationId, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.scss b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.scss new file mode 100644 index 0000000000000..2139b314ae314 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.scss @@ -0,0 +1,21 @@ +.lnsSidebar__suggestions { + > * { + margin-top: $euiSizeS; + } +} + +$suggestionHeight: 120px; + +.lnsSidebar__suggestionIcon { + width: 100%; + height: $suggestionHeight; + display: flex; + align-items: center; + justify-content: center; + padding: $euiSize; +} + + .lnsSuggestionChartWrapper { + height: $suggestionHeight; + pointer-events: none; + } \ No newline at end of file diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx new file mode 100644 index 0000000000000..70e2366d38cc8 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx @@ -0,0 +1,181 @@ +/* + * 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 React from 'react'; +import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; +import { Visualization } from '../../types'; +import { + createMockVisualization, + createMockDatasource, + createExpressionRendererMock, + DatasourceMock, +} from '../mocks'; +import { ExpressionRenderer } from 'src/legacy/core_plugins/data/public'; +import { SuggestionPanel, SuggestionPanelProps } from './suggestion_panel'; +import { getSuggestions, Suggestion } from './suggestion_helpers'; +import { fromExpression } from '@kbn/interpreter/target/common'; +import { EuiIcon } from '@elastic/eui'; + +jest.mock('./suggestion_helpers'); + +describe('suggestion_panel', () => { + let mockVisualization: Visualization; + let mockDatasource: DatasourceMock; + + let expressionRendererMock: ExpressionRenderer; + let dispatchMock: jest.Mock; + + const suggestion1State = { suggestion1: true }; + const suggestion2State = { suggestion2: true }; + + let defaultProps: SuggestionPanelProps; + + beforeEach(() => { + mockVisualization = createMockVisualization(); + mockDatasource = createMockDatasource(); + expressionRendererMock = createExpressionRendererMock(); + dispatchMock = jest.fn(); + + (getSuggestions as jest.Mock).mockReturnValue([ + { + datasourceState: {}, + previewIcon: 'empty', + score: 0.5, + state: suggestion1State, + visualizationId: 'vis', + title: 'Suggestion1', + }, + { + datasourceState: {}, + previewIcon: 'empty', + score: 0.5, + state: suggestion2State, + visualizationId: 'vis', + title: 'Suggestion2', + }, + ] as Suggestion[]); + + defaultProps = { + activeDatasource: mockDatasource, + datasourceState: {}, + activeVisualizationId: 'vis', + visualizationMap: { + vis: mockVisualization, + }, + visualizationState: {}, + dispatch: dispatchMock, + ExpressionRenderer: expressionRendererMock, + datasourcePublicAPI: mockDatasource.publicAPIMock, + }; + }); + + it('should list passed in suggestions', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="suggestion-title"]').map(el => el.text())).toEqual([ + 'Suggestion1', + 'Suggestion2', + ]); + }); + + it('should dispatch visualization switch action if suggestion is clicked', () => { + const wrapper = mount(); + + wrapper + .find('[data-test-subj="suggestion-title"]') + .first() + .simulate('click'); + + expect(dispatchMock).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'SWITCH_VISUALIZATION', + initialState: suggestion1State, + }) + ); + }); + + it('should render preview expression if there is one', () => { + (getSuggestions as jest.Mock).mockReturnValue([ + { + datasourceState: {}, + previewIcon: 'empty', + score: 0.5, + state: suggestion1State, + visualizationId: 'vis', + title: 'Suggestion1', + }, + { + datasourceState: {}, + previewIcon: 'empty', + score: 0.5, + state: suggestion2State, + visualizationId: 'vis', + title: 'Suggestion2', + previewExpression: 'test | expression', + }, + ] as Suggestion[]); + + mockDatasource.toExpression.mockReturnValue('datasource_expression'); + + mount(); + + expect(expressionRendererMock).toHaveBeenCalledTimes(1); + const passedExpression = fromExpression( + (expressionRendererMock as jest.Mock).mock.calls[0][0].expression + ); + expect(passedExpression).toMatchInlineSnapshot(` +Object { + "chain": Array [ + Object { + "arguments": Object {}, + "function": "datasource_expression", + "type": "function", + }, + Object { + "arguments": Object {}, + "function": "test", + "type": "function", + }, + Object { + "arguments": Object {}, + "function": "expression", + "type": "function", + }, + ], + "type": "expression", +} +`); + }); + + it('should render render icon if there is no preview expression', () => { + (getSuggestions as jest.Mock).mockReturnValue([ + { + datasourceState: {}, + previewIcon: 'visTable', + score: 0.5, + state: suggestion1State, + visualizationId: 'vis', + title: 'Suggestion1', + }, + { + datasourceState: {}, + previewIcon: 'empty', + score: 0.5, + state: suggestion2State, + visualizationId: 'vis', + title: 'Suggestion2', + previewExpression: 'test | expression', + }, + ] as Suggestion[]); + + mockDatasource.toExpression.mockReturnValue('datasource_expression'); + + const wrapper = mount(); + + expect(wrapper.find(EuiIcon)).toHaveLength(1); + expect(wrapper.find(EuiIcon).prop('type')).toEqual('visTable'); + }); +}); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx index 9d9730db37651..7c86b8e1522df 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx @@ -4,12 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiIcon, EuiTitle, EuiPanel, EuiIconTip } from '@elastic/eui'; +import { toExpression } from '@kbn/interpreter/common'; +import { i18n } from '@kbn/i18n'; import { Action } from './state_management'; -import { Datasource, Visualization } from '../../types'; -import { getSuggestions, toSwitchAction } from './suggestion_helpers'; +import { Datasource, Visualization, DatasourcePublicAPI } from '../../types'; +import { getSuggestions, toSwitchAction, Suggestion } from './suggestion_helpers'; +import { ExpressionRenderer } from '../../../../../../../src/legacy/core_plugins/data/public'; +import { prependDatasourceExpression } from './expression_helpers'; export interface SuggestionPanelProps { activeDatasource: Datasource; @@ -17,9 +22,75 @@ export interface SuggestionPanelProps { activeVisualizationId: string | null; visualizationMap: Record; visualizationState: unknown; + datasourcePublicAPI: DatasourcePublicAPI; dispatch: (action: Action) => void; + ExpressionRenderer: ExpressionRenderer; } +const SuggestionPreview = ({ + suggestion, + dispatch, + previewExpression, + ExpressionRenderer: ExpressionRendererComponent, +}: { + suggestion: Suggestion; + dispatch: (action: Action) => void; + ExpressionRenderer: ExpressionRenderer; + previewExpression?: string; +}) => { + const [expressionError, setExpressionError] = useState(false); + + useEffect( + () => { + setExpressionError(false); + }, + [previewExpression] + ); + + return ( + { + dispatch(toSwitchAction(suggestion)); + }} + > + +

{suggestion.title}

+
+ {expressionError ? ( +
+ +
+ ) : previewExpression ? ( + { + // eslint-disable-next-line no-console + console.error(`Failed to render preview: `, e); + setExpressionError(true); + }} + /> + ) : ( +
+ +
+ )} +
+ ); +}; + export function SuggestionPanel({ activeDatasource, datasourceState, @@ -27,6 +98,8 @@ export function SuggestionPanel({ visualizationMap, visualizationState, dispatch, + datasourcePublicAPI, + ExpressionRenderer: ExpressionRendererComponent, }: SuggestionPanelProps) { const datasourceSuggestions = activeDatasource.getDatasourceSuggestionsFromCurrentState( datasourceState @@ -36,30 +109,38 @@ export function SuggestionPanel({ datasourceSuggestions, visualizationMap, activeVisualizationId, - visualizationState + visualizationState, + datasourcePublicAPI ); return ( - <> -

- -

+
+ +

+ +

+
{suggestions.map((suggestion, index) => { + const previewExpression = suggestion.previewExpression + ? prependDatasourceExpression( + suggestion.previewExpression, + activeDatasource, + suggestion.datasourceState + ) + : null; return ( - + ); })} - +
); } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.scss b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.scss new file mode 100644 index 0000000000000..03c3534e1e12b --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.scss @@ -0,0 +1,3 @@ + .lnsChartWrapper { + height: 500px; + } \ No newline at end of file diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx index 1294cbb2aa498..1b5d89f50bdd4 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx @@ -314,6 +314,7 @@ Object { title: 'my title', state: {}, datasourceSuggestionId: 0, + previewIcon: 'empty', }, ]); @@ -328,7 +329,8 @@ Object { expect(mockVisualization.getSuggestions).toHaveBeenCalledWith( expect.objectContaining({ tables: [expectedTable], - }) + }), + expect.anything() ); expect(mockDispatch).toHaveBeenCalledWith({ type: 'SWITCH_VISUALIZATION', @@ -365,12 +367,14 @@ Object { isFirst: true, }, datasourceSuggestionId: 1, + previewIcon: 'empty', }, { score: 0.5, title: 'second suggestion', state: {}, datasourceSuggestionId: 0, + previewIcon: 'empty', }, ]); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx index dc9b5ffce6b49..b65941e1838c2 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx @@ -47,7 +47,8 @@ export function WorkspacePanel({ datasourceSuggestions, visualizationMap, activeVisualizationId, - visualizationState + visualizationState, + datasourcePublicAPI ); if (suggestions.length === 0) { @@ -137,6 +138,7 @@ export function WorkspacePanel({ } else { return ( { setExpressionError(e); diff --git a/x-pack/legacy/plugins/lens/public/index.scss b/x-pack/legacy/plugins/lens/public/index.scss index 6e5372e233a0d..12f384081fe7a 100644 --- a/x-pack/legacy/plugins/lens/public/index.scss +++ b/x-pack/legacy/plugins/lens/public/index.scss @@ -3,4 +3,5 @@ @import './drag_drop/drag_drop.scss'; @import './xy_visualization_plugin/xy_expression.scss'; -@import './indexpattern_plugin/indexpattern'; \ No newline at end of file +@import './indexpattern_plugin/indexpattern'; +@import './editor_frame_plugin/editor_frame/index'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 367d1bdd99c79..0e38cc3c32a12 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -154,6 +154,8 @@ export interface VisualizationSuggestion { title: string; state: T; datasourceSuggestionId: number; + previewExpression?: Ast | string; + previewIcon: string; } export interface Visualization { @@ -168,5 +170,8 @@ export interface Visualization { // The frame will call this function on all visualizations when the table changes, or when // rendering additional ways of using the data - getSuggestions: (options: SuggestionRequest) => Array>; + getSuggestions: ( + options: SuggestionRequest, + datasource: DatasourcePublicAPI + ) => Array>; } diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap index c1f13d5db63ab..2cf89a4a58196 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap @@ -40,6 +40,9 @@ Object { "accessor": Array [ "a", ], + "hide": Array [ + false, + ], "position": Array [ "bottom", ], @@ -66,6 +69,9 @@ Object { "b", "c", ], + "hide": Array [ + false, + ], "labels": Array [ "b", "c", diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts new file mode 100644 index 0000000000000..1807145996126 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts @@ -0,0 +1,80 @@ +/* + * 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 { Ast } from '@kbn/interpreter/common'; +import { State } from './types'; +import { DatasourcePublicAPI } from '../types'; + +export const toExpression = (state: State, datasource: DatasourcePublicAPI): Ast => ({ + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_xy_chart', + arguments: { + seriesType: [state.seriesType], + title: [state.title], + legend: [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_xy_legendConfig', + arguments: { + isVisible: [state.legend.isVisible], + position: [state.legend.position], + }, + }, + ], + }, + ], + x: [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_xy_xConfig', + arguments: { + title: [state.x.title], + showGridlines: [state.x.showGridlines], + position: [state.x.position], + accessor: [state.x.accessor], + hide: [Boolean(state.x.hide)], + }, + }, + ], + }, + ], + y: [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_xy_yConfig', + arguments: { + title: [state.y.title], + showGridlines: [state.y.showGridlines], + position: [state.y.position], + accessors: state.y.accessors, + hide: [Boolean(state.y.hide)], + labels: state.y.accessors.map(accessor => { + const operation = datasource.getOperationForColumnId(accessor); + return operation ? operation.label : accessor; + }), + }, + }, + ], + }, + ], + splitSeriesAccessors: state.splitSeriesAccessors, + stackAccessors: state.stackAccessors, + }, + }, + ], +}); 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 71e83943616ac..57a5bcd4966c8 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 @@ -53,6 +53,7 @@ interface AxisConfig { title: string; showGridlines: boolean; position: Position; + hide?: boolean; } const axisConfig: { [key in keyof AxisConfig]: ArgumentType } = { @@ -69,6 +70,11 @@ const axisConfig: { [key in keyof AxisConfig]: ArgumentType } = options: [Position.Top, Position.Right, Position.Bottom, Position.Left], help: 'The position of the axis', }, + hide: { + types: ['boolean'], + default: false, + help: 'Show / hide axis', + }, }; export interface YState extends AxisConfig { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.scss b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.scss index 93986078f68b1..9ba7326af6a56 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.scss +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.scss @@ -1,4 +1,3 @@ .lnsChart { - // TODO style this dependent on the screen height (see POC) - height: 500px; + height: 100%; } \ No newline at end of file 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 ea3555631f126..ad97576902462 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 @@ -140,6 +140,7 @@ export function XYChart({ data, args }: XYChartProps) { position={x.position} title={x.title} showGridLines={x.showGridlines} + hide={x.hide} /> {seriesType === 'line' ? ( diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts index b034c9fe78b27..82c3b91e3246f 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts @@ -7,6 +7,8 @@ import { getSuggestions } from './xy_suggestions'; import { TableColumn, VisualizationSuggestion } from '../types'; import { State } from './types'; +import { Ast } from '@kbn/interpreter/target/common'; +import { createMockDatasource } from '../editor_frame_plugin/mocks'; describe('xy_suggestions', () => { function numCol(columnId: string): TableColumn { @@ -67,27 +69,41 @@ describe('xy_suggestions', () => { }; expect( - getSuggestions({ - tables: [ - { datasourceSuggestionId: 0, isMultiRow: true, columns: [dateCol('a')] }, - { datasourceSuggestionId: 1, isMultiRow: true, columns: [strCol('foo'), strCol('bar')] }, - { datasourceSuggestionId: 2, isMultiRow: false, columns: [strCol('foo'), numCol('bar')] }, - { datasourceSuggestionId: 3, isMultiRow: true, columns: [unknownCol(), numCol('bar')] }, - ], - }) + getSuggestions( + { + tables: [ + { datasourceSuggestionId: 0, isMultiRow: true, columns: [dateCol('a')] }, + { + datasourceSuggestionId: 1, + isMultiRow: true, + columns: [strCol('foo'), strCol('bar')], + }, + { + datasourceSuggestionId: 2, + isMultiRow: false, + columns: [strCol('foo'), numCol('bar')], + }, + { datasourceSuggestionId: 3, isMultiRow: true, columns: [unknownCol(), numCol('bar')] }, + ], + }, + createMockDatasource().publicAPIMock + ) ).toEqual([]); }); test('suggests a basic x y chart with date on x', () => { - const [suggestion, ...rest] = getSuggestions({ - tables: [ - { - datasourceSuggestionId: 0, - isMultiRow: true, - columns: [numCol('bytes'), dateCol('date')], - }, - ], - }); + const [suggestion, ...rest] = getSuggestions( + { + tables: [ + { + datasourceSuggestionId: 0, + isMultiRow: true, + columns: [numCol('bytes'), dateCol('date')], + }, + ], + }, + createMockDatasource().publicAPIMock + ); expect(rest).toHaveLength(0); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` @@ -104,15 +120,18 @@ Object { }); test('suggests a split x y chart with date on x', () => { - const [suggestion, ...rest] = getSuggestions({ - tables: [ - { - datasourceSuggestionId: 1, - isMultiRow: true, - columns: [numCol('price'), numCol('quantity'), dateCol('date'), strCol('product')], - }, - ], - }); + const [suggestion, ...rest] = getSuggestions( + { + tables: [ + { + datasourceSuggestionId: 1, + isMultiRow: true, + columns: [numCol('price'), numCol('quantity'), dateCol('date'), strCol('product')], + }, + ], + }, + createMockDatasource().publicAPIMock + ); expect(rest).toHaveLength(0); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` @@ -132,20 +151,23 @@ Object { }); test('supports multiple suggestions', () => { - const [s1, s2, ...rest] = getSuggestions({ - tables: [ - { - datasourceSuggestionId: 0, - isMultiRow: true, - columns: [numCol('price'), dateCol('date')], - }, - { - datasourceSuggestionId: 1, - isMultiRow: true, - columns: [numCol('count'), strCol('country')], - }, - ], - }); + const [s1, s2, ...rest] = getSuggestions( + { + tables: [ + { + datasourceSuggestionId: 0, + isMultiRow: true, + columns: [numCol('price'), dateCol('date')], + }, + { + datasourceSuggestionId: 1, + isMultiRow: true, + columns: [numCol('count'), strCol('country')], + }, + ], + }, + createMockDatasource().publicAPIMock + ); expect(rest).toHaveLength(0); expect([suggestionSubset(s1), suggestionSubset(s2)]).toMatchInlineSnapshot(` @@ -173,15 +195,18 @@ Array [ }); test('handles two numeric values', () => { - const [suggestion] = getSuggestions({ - tables: [ - { - datasourceSuggestionId: 1, - isMultiRow: true, - columns: [numCol('quantity'), numCol('price')], - }, - ], - }); + const [suggestion] = getSuggestions( + { + tables: [ + { + datasourceSuggestionId: 1, + isMultiRow: true, + columns: [numCol('quantity'), numCol('price')], + }, + ], + }, + createMockDatasource().publicAPIMock + ); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` Object { @@ -197,26 +222,29 @@ Object { }); test('handles unbucketed suggestions', () => { - const [suggestion] = getSuggestions({ - tables: [ - { - datasourceSuggestionId: 1, - isMultiRow: true, - columns: [ - numCol('num votes'), - { - columnId: 'mybool', - operation: { - dataType: 'boolean', - id: 'mybool', - isBucketed: false, - label: 'Yes / No', + const [suggestion] = getSuggestions( + { + tables: [ + { + datasourceSuggestionId: 1, + isMultiRow: true, + columns: [ + numCol('num votes'), + { + columnId: 'mybool', + operation: { + dataType: 'boolean', + id: 'mybool', + isBucketed: false, + label: 'Yes / No', + }, }, - }, - ], - }, - ], - }); + ], + }, + ], + }, + createMockDatasource().publicAPIMock + ); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` Object { @@ -230,4 +258,27 @@ Object { } `); }); + + test('adds a preview expression with disabled axes and legend', () => { + const [suggestion] = getSuggestions( + { + tables: [ + { + datasourceSuggestionId: 0, + isMultiRow: true, + columns: [numCol('bytes'), dateCol('date')], + }, + ], + }, + createMockDatasource().publicAPIMock + ); + + const expression = suggestion.previewExpression! as Ast; + + expect( + (expression.chain[0].arguments.legend[0] as Ast).chain[0].arguments.isVisible[0] + ).toBeFalsy(); + expect((expression.chain[0].arguments.x[0] as Ast).chain[0].arguments.hide[0]).toBeTruthy(); + expect((expression.chain[0].arguments.y[0] as Ast).chain[0].arguments.hide[0]).toBeTruthy(); + }); }); 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 28ef677e49644..a9207d409eb84 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 @@ -6,8 +6,15 @@ import { partition } from 'lodash'; import { Position } from '@elastic/charts'; -import { SuggestionRequest, VisualizationSuggestion, TableColumn, TableSuggestion } from '../types'; +import { + SuggestionRequest, + VisualizationSuggestion, + TableColumn, + TableSuggestion, + DatasourcePublicAPI, +} from '../types'; import { State } from './types'; +import { toExpression } from './to_expression'; const columnSortOrder = { date: 0, @@ -22,7 +29,8 @@ const columnSortOrder = { * @param opts */ export function getSuggestions( - opts: SuggestionRequest + opts: SuggestionRequest, + datasource: DatasourcePublicAPI ): Array> { return opts.tables .filter( @@ -35,10 +43,13 @@ export function getSuggestions( columns.some(col => col.operation.dataType === 'number') && !columns.some(col => !columnSortOrder.hasOwnProperty(col.operation.dataType)) ) - .map(table => getSuggestionForColumns(table)); + .map(table => getSuggestionForColumns(datasource, table)); } -function getSuggestionForColumns(table: TableSuggestion): VisualizationSuggestion { +function getSuggestionForColumns( + datasource: DatasourcePublicAPI, + table: TableSuggestion +): VisualizationSuggestion { const [buckets, values] = partition( prioritizeColumns(table.columns), col => col.operation.isBucketed @@ -46,10 +57,10 @@ function getSuggestionForColumns(table: TableSuggestion): VisualizationSuggestio if (buckets.length >= 1) { const [x, splitBy] = buckets; - return getSuggestion(table.datasourceSuggestionId, x, values, splitBy); + return getSuggestion(datasource, table.datasourceSuggestionId, x, values, splitBy); } else { const [x, ...yValues] = values; - return getSuggestion(table.datasourceSuggestionId, x, yValues); + return getSuggestion(datasource, table.datasourceSuggestionId, x, yValues); } } @@ -63,6 +74,7 @@ function prioritizeColumns(columns: TableColumn[]) { } function getSuggestion( + datasource: DatasourcePublicAPI, datasourceSuggestionId: number, xValue: TableColumn, yValues: TableColumn[], @@ -75,28 +87,49 @@ function getSuggestion( // TODO: Localize the title, label, etc const preposition = isDate ? 'over' : 'of'; const title = `${yTitle} ${preposition} ${xTitle}`; + const state: State = { + title, + legend: { isVisible: true, position: Position.Right }, + seriesType: isDate ? 'line' : 'bar', + splitSeriesAccessors: splitBy && isDate ? [splitBy.columnId] : [], + stackAccessors: splitBy && !isDate ? [splitBy.columnId] : [], + x: { + accessor: xValue.columnId, + position: Position.Bottom, + showGridlines: false, + title: xTitle, + }, + y: { + accessors: yValues.map(col => col.columnId), + position: Position.Left, + showGridlines: false, + title: yTitle, + }, + }; + return { title, score: 1, datasourceSuggestionId, - state: { - title, - legend: { isVisible: true, position: Position.Right }, - seriesType: isDate ? 'line' : 'bar', - splitSeriesAccessors: splitBy && isDate ? [splitBy.columnId] : [], - stackAccessors: splitBy && !isDate ? [splitBy.columnId] : [], - x: { - accessor: xValue.columnId, - position: Position.Bottom, - showGridlines: false, - title: xTitle, + state, + previewIcon: isDate ? 'visLine' : 'visBar', + previewExpression: toExpression( + { + ...state, + x: { + ...state.x, + hide: true, + }, + y: { + ...state.y, + hide: true, + }, + legend: { + ...state.legend, + isVisible: false, + }, }, - y: { - accessors: yValues.map(col => col.columnId), - position: Position.Left, - showGridlines: false, - title: yTitle, - }, - }, + datasource + ), }; } 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 494b653c6d944..efb9cc7fc1518 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 @@ -12,6 +12,7 @@ import { getSuggestions } from './xy_suggestions'; import { XYConfigPanel } from './xy_config_panel'; import { Visualization } from '../types'; import { State, PersistableState } from './types'; +import { toExpression } from './to_expression'; export const xyVisualization: Visualization = { getSuggestions, @@ -50,72 +51,5 @@ export const xyVisualization: Visualization = { domElement ), - toExpression: (state, datasource) => ({ - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_xy_chart', - arguments: { - seriesType: [state.seriesType], - title: [state.title], - legend: [ - { - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_xy_legendConfig', - arguments: { - isVisible: [state.legend.isVisible], - position: [state.legend.position], - }, - }, - ], - }, - ], - x: [ - { - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_xy_xConfig', - arguments: { - title: [state.x.title], - showGridlines: [state.x.showGridlines], - position: [state.x.position], - accessor: [state.x.accessor], - }, - }, - ], - }, - ], - y: [ - { - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_xy_yConfig', - arguments: { - title: [state.y.title], - showGridlines: [state.y.showGridlines], - position: [state.y.position], - accessors: state.y.accessors, - labels: state.y.accessors.map(accessor => { - const operation = datasource.getOperationForColumnId(accessor); - return operation ? operation.label : accessor; - }), - }, - }, - ], - }, - ], - splitSeriesAccessors: state.splitSeriesAccessors, - stackAccessors: state.stackAccessors, - }, - }, - ], - }), + toExpression, };