From c7e785383a87f7e18509c601a5c089c755ac028e Mon Sep 17 00:00:00 2001 From: Marta Bondyra <4283304+mbondyra@users.noreply.github.com> Date: Mon, 30 Oct 2023 20:52:15 +0100 Subject: [PATCH] [Lens] Allow non-numeric metrics for metric vis (#169258) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #137756 Allows to set non numeric metrics for metric visualization for primary and secondary metric. Screenshot 2023-10-19 at 13 45 47 Screenshot 2023-10-19 at 13 46 37 Doesn't include coloring by terms. When primary metric is non-numeric: 1. when maximum value is empty, we hide maximum value group 2. when maximum value has a value we set an error message on dimension 3. we don’t allow to use a palette for coloring 4. we don’t allow to set a trendline Screenshot 2023-10-19 at 13 30 16 Screenshot 2023-10-19 at 13 30 22 ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### Risk Matrix Delete this section if it is not applicable to this PR. Before closing this PR, invite QA, stakeholders, and other developers to identify risks that should be tested prior to the change/feature release. When forming the risk matrix, consider some of the following examples and how they may potentially impact the change: | Risk | Probability | Severity | Mitigation/Notes | |---------------------------|-------------|----------|-------------------------| | Multiple Spaces—unexpected behavior in non-default Kibana Space. | Low | High | Integration tests will verify that all features are still supported in non-default Kibana Space and when user switches between spaces. | | Multiple nodes—Elasticsearch polling might have race conditions when multiple Kibana nodes are polling for the same tasks. | High | Low | Tasks are idempotent, so executing them multiple times will not result in logical error, but will degrade performance. To test for this case we add plenty of unit tests around this logic and document manual testing procedure. | | Code should gracefully handle cases when feature X or plugin Y are disabled. | Medium | High | Unit tests will verify that any feature flag or plugin combination still results in our service operational. | | [See more potential risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) | ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../public/components/metric_vis.tsx | 59 ++- .../config_panel/layer_panel.test.tsx | 27 ++ .../editor_frame/config_panel/layer_panel.tsx | 447 +++++++++--------- x-pack/plugins/lens/public/mocks/index.ts | 24 + .../shared_components/collapse_setting.tsx | 1 + x-pack/plugins/lens/public/types.ts | 1 + .../__snapshots__/visualization.test.ts.snap | 13 + .../metric/dimension_editor.test.tsx | 112 +++-- .../metric/dimension_editor.tsx | 265 ++++++----- .../visualizations/metric/to_expression.ts | 21 +- .../metric/visualization.test.ts | 41 +- .../visualizations/metric/visualization.tsx | 71 ++- .../xy/reference_line_helpers.test.ts | 70 ++- 13 files changed, 671 insertions(+), 481 deletions(-) diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx index b80d8efe68e7c3..a0d02562d7623c 100644 --- a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx +++ b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx @@ -20,12 +20,14 @@ import { MetricWTrend, MetricWNumber, SettingsProps, + MetricWText, } from '@elastic/charts'; import { getColumnByAccessor, getFormatByAccessor } from '@kbn/visualizations-plugin/common/utils'; import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common'; import type { Datatable, DatatableColumn, + DatatableRow, IInterpreterRenderHandlers, RenderMode, } from '@kbn/expressions-plugin/common'; @@ -65,6 +67,28 @@ function enhanceFieldFormat(serializedFieldFormat: SerializedFieldFormat | undef return serializedFieldFormat ?? { id: formatId }; } +const renderSecondaryMetric = ( + columns: DatatableColumn[], + row: DatatableRow, + config: Pick +) => { + let secondaryMetricColumn: DatatableColumn | undefined; + let formatSecondaryMetric: ReturnType; + if (config.dimensions.secondaryMetric) { + secondaryMetricColumn = getColumnByAccessor(config.dimensions.secondaryMetric, columns); + formatSecondaryMetric = getMetricFormatter(config.dimensions.secondaryMetric, columns); + } + const secondaryPrefix = config.metric.secondaryPrefix ?? secondaryMetricColumn?.name; + return ( + + {secondaryPrefix} + {secondaryMetricColumn + ? `${secondaryPrefix ? ' ' : ''}${formatSecondaryMetric!(row[secondaryMetricColumn.id])}` + : undefined} + + ); +}; + const getMetricFormatter = ( accessor: ExpressionValueVisDimension | string, columns: Datatable['columns'] @@ -149,13 +173,6 @@ export const MetricVis = ({ const primaryMetricColumn = getColumnByAccessor(config.dimensions.metric, data.columns)!; const formatPrimaryMetric = getMetricFormatter(config.dimensions.metric, data.columns); - let secondaryMetricColumn: DatatableColumn | undefined; - let formatSecondaryMetric: ReturnType; - if (config.dimensions.secondaryMetric) { - secondaryMetricColumn = getColumnByAccessor(config.dimensions.secondaryMetric, data.columns); - formatSecondaryMetric = getMetricFormatter(config.dimensions.secondaryMetric, data.columns); - } - let breakdownByColumn: DatatableColumn | undefined; let formatBreakdownValue: FieldFormatConvertFunction; if (config.dimensions.breakdownBy) { @@ -172,28 +189,32 @@ export const MetricVis = ({ const metricConfigs: MetricSpec['data'][number] = ( breakdownByColumn ? data.rows : data.rows.slice(0, 1) ).map((row, rowIdx) => { - const value: number = row[primaryMetricColumn.id] !== null ? row[primaryMetricColumn.id] : NaN; + const value: number | string = + row[primaryMetricColumn.id] !== null ? row[primaryMetricColumn.id] : NaN; const title = breakdownByColumn ? formatBreakdownValue(row[breakdownByColumn.id]) : primaryMetricColumn.name; const subtitle = breakdownByColumn ? primaryMetricColumn.name : config.metric.subtitle; - const secondaryPrefix = config.metric.secondaryPrefix ?? secondaryMetricColumn?.name; + + if (typeof value !== 'number') { + const nonNumericMetric: MetricWText = { + value: formatPrimaryMetric(value), + title: String(title), + subtitle, + icon: config.metric?.icon ? getIcon(config.metric?.icon) : undefined, + extra: renderSecondaryMetric(data.columns, row, config), + color: config.metric.color ?? defaultColor, + }; + return nonNumericMetric; + } + const baseMetric: MetricWNumber = { value, valueFormatter: formatPrimaryMetric, title: String(title), subtitle, icon: config.metric?.icon ? getIcon(config.metric?.icon) : undefined, - extra: ( - - {secondaryPrefix} - {secondaryMetricColumn - ? `${secondaryPrefix ? ' ' : ''}${formatSecondaryMetric!( - row[secondaryMetricColumn.id] - )}` - : undefined} - - ), + extra: renderSecondaryMetric(data.columns, row, config), color: config.metric.palette && value != null ? getColor( diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index be3393d3b52e3d..cef598de31af0d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -224,6 +224,33 @@ describe('LayerPanel', () => { const group = instance.find('.lnsLayerPanel__dimensionContainer[data-test-subj="lnsGroup"]'); expect(group).toHaveLength(1); }); + it('does not render a hidden group', async () => { + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + }, + { + groupLabel: 'B', + groupId: 'b', + accessors: [], + filterOperations: () => true, + isHidden: true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + }, + ], + }); + + const { instance } = await mountWithProvider(); + const group = instance.find('.lnsLayerPanel__dimensionContainer[data-test-subj="lnsGroup"]'); + expect(group).toHaveLength(1); + }); it('should render the required warning when only one group is configured', async () => { mockVisualization.getConfiguration.mockReturnValue({ diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 47987926b039fc..78a06408902b5c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -427,239 +427,244 @@ export function LayerPanel( )} - {dimensionGroups.map((group, groupIndex) => { - let errorText: string = ''; - - if (!isEmptyLayer) { - if ( - group.requiredMinDimensionCount && - group.requiredMinDimensionCount > group.accessors.length - ) { - if (group.requiredMinDimensionCount > 1) { + {dimensionGroups + .filter((group) => !group.isHidden) + .map((group, groupIndex) => { + let errorText: string = ''; + + if (!isEmptyLayer) { + if ( + group.requiredMinDimensionCount && + group.requiredMinDimensionCount > group.accessors.length + ) { + if (group.requiredMinDimensionCount > 1) { + errorText = i18n.translate( + 'xpack.lens.editorFrame.requiresTwoOrMoreFieldsWarningLabel', + { + defaultMessage: 'Requires {requiredMinDimensionCount} fields', + values: { + requiredMinDimensionCount: group.requiredMinDimensionCount, + }, + } + ); + } else { + errorText = i18n.translate('xpack.lens.editorFrame.requiresFieldWarningLabel', { + defaultMessage: 'Requires field', + }); + } + } else if (group.dimensionsTooMany && group.dimensionsTooMany > 0) { errorText = i18n.translate( - 'xpack.lens.editorFrame.requiresTwoOrMoreFieldsWarningLabel', + 'xpack.lens.editorFrame.tooManyDimensionsSingularWarningLabel', { - defaultMessage: 'Requires {requiredMinDimensionCount} fields', + defaultMessage: + 'Please remove {dimensionsTooMany, plural, one {a dimension} other {{dimensionsTooMany} dimensions}}', values: { - requiredMinDimensionCount: group.requiredMinDimensionCount, + dimensionsTooMany: group.dimensionsTooMany, }, } ); - } else { - errorText = i18n.translate('xpack.lens.editorFrame.requiresFieldWarningLabel', { - defaultMessage: 'Requires field', - }); } - } else if (group.dimensionsTooMany && group.dimensionsTooMany > 0) { - errorText = i18n.translate( - 'xpack.lens.editorFrame.tooManyDimensionsSingularWarningLabel', - { - defaultMessage: - 'Please remove {dimensionsTooMany, plural, one {a dimension} other {{dimensionsTooMany} dimensions}}', - values: { - dimensionsTooMany: group.dimensionsTooMany, - }, - } - ); } - } - const isOptional = !group.requiredMinDimensionCount && !group.suggestedValue; - return ( - - {group.groupLabel} - {group.groupTooltip && ( - <> - - - )} - - } - labelAppend={ - isOptional ? ( - - {i18n.translate('xpack.lens.editorFrame.optionalDimensionLabel', { - defaultMessage: 'Optional', - })} - - ) : null - } - labelType="legend" - key={group.groupId} - isInvalid={Boolean(errorText)} - error={errorText} - > - <> - {group.accessors.length ? ( - - {group.accessors.map((accessorConfig, accessorIndex) => { - const { columnId } = accessorConfig; - - const messages = - props?.getUserMessages?.('dimensionButton', { - dimensionId: columnId, - }) ?? []; - - return ( - + {group.groupLabel} + {group.groupTooltip && ( + <> + setHideTooltip(true)} - onDragEnd={() => setHideTooltip(false)} - onDrop={onDrop} - indexPatterns={dataViews.indexPatterns} - > - { - setActiveDimension({ - isNew: false, - activeGroup: group, - activeId: id, - }); - }} - onRemoveClick={(id: string) => { - props.onRemoveDimension({ columnId: id, layerId }); - removeButtonRef(id); - }} - message={{ - severity: messages[0]?.severity, - content: messages[0]?.shortMessage || messages[0]?.longMessage, + position="top" + size="s" + type="questionInCircle" + /> + + )} + + } + labelAppend={ + isOptional ? ( + + {i18n.translate('xpack.lens.editorFrame.optionalDimensionLabel', { + defaultMessage: 'Optional', + })} + + ) : null + } + labelType="legend" + key={group.groupId} + isInvalid={Boolean(errorText)} + error={errorText} + > + <> + {group.accessors.length ? ( + + {group.accessors.map((accessorConfig, accessorIndex) => { + const { columnId } = accessorConfig; + + const messages = + props?.getUserMessages?.('dimensionButton', { + dimensionId: columnId, + }) ?? []; + + return ( + setHideTooltip(true)} + onDragEnd={() => setHideTooltip(false)} + onDrop={onDrop} + indexPatterns={dataViews.indexPatterns} > - {layerDatasource ? ( - <> - {layerDatasource.DimensionTriggerComponent({ - ...layerDatasourceConfigProps, - columnId: accessorConfig.columnId, - groupId: group.groupId, - filterOperations: group.filterOperations, - indexPatterns: dataViews.indexPatterns, - })} - - ) : ( - <> - {activeVisualization?.DimensionTriggerComponent?.({ - columnId, - label: columnLabelMap?.[columnId] ?? '', - hideTooltip, - })} - - )} - - - ); - })} - - ) : null} - - {group.fakeFinalAccessor && ( -
- -
- )} + { + setActiveDimension({ + isNew: false, + activeGroup: group, + activeId: id, + }); + }} + onRemoveClick={(id: string) => { + props.onRemoveDimension({ columnId: id, layerId }); + removeButtonRef(id); + }} + message={{ + severity: messages[0]?.severity, + content: messages[0]?.shortMessage || messages[0]?.longMessage, + }} + > + {layerDatasource ? ( + <> + {layerDatasource.DimensionTriggerComponent({ + ...layerDatasourceConfigProps, + columnId: accessorConfig.columnId, + groupId: group.groupId, + filterOperations: group.filterOperations, + indexPatterns: dataViews.indexPatterns, + })} + + ) : ( + <> + {activeVisualization?.DimensionTriggerComponent?.({ + columnId, + label: columnLabelMap?.[columnId] ?? '', + hideTooltip, + })} + + )} + + + ); + })} + + ) : null} + + {group.fakeFinalAccessor && ( +
+ +
+ )} - {group.supportsMoreColumns ? ( - { - props.onEmptyDimensionAdd(id, group); - setActiveDimension({ - activeGroup: group, - activeId: id, - isNew: !group.supportStaticValue && Boolean(layerDatasource), - }); - }} - onDrop={onDrop} - indexPatterns={dataViews.indexPatterns} - /> - ) : null} - -
- ); - })} + {group.supportsMoreColumns ? ( + { + props.onEmptyDimensionAdd(id, group); + setActiveDimension({ + activeGroup: group, + activeId: id, + isNew: !group.supportStaticValue && Boolean(layerDatasource), + }); + }} + onDrop={onDrop} + indexPatterns={dataViews.indexPatterns} + /> + ) : null} + + + ); + })} {(layerDatasource?.LayerSettingsComponent || activeVisualization?.LayerSettingsComponent) && ( diff --git a/x-pack/plugins/lens/public/mocks/index.ts b/x-pack/plugins/lens/public/mocks/index.ts index c71da33fda3c94..f9760c50005f4e 100644 --- a/x-pack/plugins/lens/public/mocks/index.ts +++ b/x-pack/plugins/lens/public/mocks/index.ts @@ -6,6 +6,7 @@ */ import { DragContextState, DragContextValue } from '@kbn/dom-drag-drop'; +import { DatatableColumnType } from '@kbn/expressions-plugin/common'; import { createMockDataViewsState } from '../data_views_service/mocks'; import { FramePublicAPI, FrameDatasourceAPI } from '../types'; export { mockDataPlugin } from './data_plugin_mock'; @@ -83,3 +84,26 @@ export function createMockedDragDropContext( setState ? setState : jest.fn(), ]; } + +export function generateActiveData( + json: Array<{ + id: string; + rows: Array>; + }> +) { + return json.reduce((memo, { id, rows }) => { + const columns = Object.keys(rows[0]).map((columnId) => ({ + id: columnId, + name: columnId, + meta: { + type: typeof rows[0][columnId]! as DatatableColumnType, + }, + })); + memo[id] = { + type: 'datatable' as const, + columns, + rows, + }; + return memo; + }, {} as NonNullable); +} diff --git a/x-pack/plugins/lens/public/shared_components/collapse_setting.tsx b/x-pack/plugins/lens/public/shared_components/collapse_setting.tsx index bbaf5296a4e288..9d855fa2bccfee 100644 --- a/x-pack/plugins/lens/public/shared_components/collapse_setting.tsx +++ b/x-pack/plugins/lens/public/shared_components/collapse_setting.tsx @@ -28,6 +28,7 @@ export function CollapseSetting({ return ( <> { diff --git a/x-pack/plugins/lens/public/visualizations/metric/__snapshots__/visualization.test.ts.snap b/x-pack/plugins/lens/public/visualizations/metric/__snapshots__/visualization.test.ts.snap index 8ad5a53ee57b26..2490904751e8c4 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/__snapshots__/visualization.test.ts.snap +++ b/x-pack/plugins/lens/public/visualizations/metric/__snapshots__/visualization.test.ts.snap @@ -55,6 +55,7 @@ Object { "groupId": "max", "groupLabel": "Maximum value", "groupTooltip": "If the maximum value is specified, the minimum value is fixed at zero.", + "isHidden": false, "paramEditorCustomProps": Object { "headingLabel": "Value", }, @@ -100,6 +101,10 @@ Array [ "dataType": "number", "isBucketed": false, }, + Object { + "dataType": "string", + "isBucketed": false, + }, ] `; @@ -109,6 +114,10 @@ Array [ "dataType": "number", "isBucketed": false, }, + Object { + "dataType": "string", + "isBucketed": false, + }, ] `; @@ -118,6 +127,10 @@ Array [ "dataType": "number", "isBucketed": false, }, + Object { + "dataType": "string", + "isBucketed": false, + }, ] `; diff --git a/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.test.tsx b/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.test.tsx index de0d59ef1bc4b1..5d2c6e2a50ca44 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.test.tsx @@ -24,7 +24,7 @@ import { mountWithIntl } from '@kbn/test-jest-helpers'; import { LayoutDirection } from '@elastic/charts'; import { act } from 'react-dom/test-utils'; import { EuiColorPickerOutput } from '@elastic/eui/src/components/color_picker/color_picker'; -import { createMockFramePublicAPI } from '../../mocks'; +import { createMockFramePublicAPI, generateActiveData } from '../../mocks'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { euiLightVars } from '@kbn/ui-theme'; import { DebouncedInput } from '@kbn/visualization-ui-components'; @@ -47,6 +47,18 @@ const SELECTORS = { BREAKDOWN_EDITOR: '[data-test-subj="lnsMetricDimensionEditor_breakdown"]', }; +const nonNumericMetricFrame = createMockFramePublicAPI({ + activeData: generateActiveData([ + { + id: 'first', + rows: Array(3).fill({ + 'metric-col-id': 'nonNumericData', + 'max-col-id': 1000, + }), + }, + ]), +}); + // see https://github.com/facebook/jest/issues/4402#issuecomment-534516219 const expectCalledBefore = (mock1: jest.Mock, mock2: jest.Mock) => expect(mock1.mock.invocationCallOrder[0]).toBeLessThan(mock2.mock.invocationCallOrder[0]); @@ -102,7 +114,14 @@ describe('dimension editor', () => { } as unknown as DatasourcePublicAPI, removeLayer: jest.fn(), addLayer: jest.fn(), - frame: createMockFramePublicAPI(), + frame: createMockFramePublicAPI({ + activeData: generateActiveData([ + { + id: 'first', + rows: Array(3).fill({ 'metric-col-id': 100, 'secondary-metric-col-id': 1 }), + }, + ]), + }), setState: jest.fn(), panelRef: {} as React.MutableRefObject, paletteService: chartPluginMock.createPaletteRegistry(), @@ -112,27 +131,9 @@ describe('dimension editor', () => { afterEach(() => jest.clearAllMocks()); describe('primary metric dimension', () => { - const accessor = 'primary-metric-col-id'; + const accessor = 'metric-col-id'; const metricAccessorState = { ...fullState, metricAccessor: accessor }; - beforeEach(() => { - props.frame.activeData = { - first: { - type: 'datatable', - columns: [ - { - id: accessor, - name: 'foo', - meta: { - type: 'number', - }, - }, - ], - rows: [], - }, - }; - }); - class Harness { public _wrapper; @@ -146,6 +147,10 @@ describe('dimension editor', () => { return this._wrapper.find(DimensionEditor); } + public get colorModeSwitch() { + return this._wrapper.find('EuiButtonGroup[data-test-subj="lnsMetric_color_mode_buttons"]'); + } + public get colorPicker() { return this._wrapper.find(EuiColorPicker); } @@ -163,11 +168,16 @@ describe('dimension editor', () => { const mockSetState = jest.fn(); - const getHarnessWithState = (state: MetricVisualizationState, datasource = props.datasource) => + const getHarnessWithState = ( + state: MetricVisualizationState, + datasource = props.datasource, + propsOverrides: Partial> = {} + ) => new Harness( mountWithIntl( { expect(component.exists(SELECTORS.BREAKDOWN_EDITOR)).toBeFalsy(); }); + it('Color mode switch is not shown when the primary metric is non-numeric', () => { + expect(getHarnessWithState(fullState, undefined).colorModeSwitch.exists()).toBeTruthy(); + expect( + getHarnessWithState(fullState, undefined, { + frame: nonNumericMetricFrame, + }).colorModeSwitch.exists() + ).toBeFalsy(); + }); + describe('static color controls', () => { it('is hidden when dynamic coloring is enabled', () => { const harnessWithPalette = getHarnessWithState({ ...metricAccessorState, palette }); @@ -202,6 +221,13 @@ describe('dimension editor', () => { }); expect(harnessNoPalette.colorPicker.exists()).toBeTruthy(); }); + it('is visible when metric is non-numeric even if palette is set', () => { + expect( + getHarnessWithState(fullState, undefined, { + frame: nonNumericMetricFrame, + }).colorPicker.exists() + ).toBeTruthy(); + }); it('fills with default value', () => { const localHarness = getHarnessWithState({ @@ -240,24 +266,6 @@ describe('dimension editor', () => { describe('secondary metric dimension', () => { const accessor = 'secondary-metric-col-id'; - beforeEach(() => { - props.frame.activeData = { - first: { - type: 'datatable', - columns: [ - { - id: accessor, - name: 'foo', - meta: { - type: 'number', - }, - }, - ], - rows: [], - }, - }; - }); - it('renders when the accessor matches', () => { const component = shallow( { const setState = jest.fn(); const localState = { ...fullState, - secondaryPrefix: 'foo', + secondaryPrefix: 'secondary-metric-col-id2', secondaryMetricAccessor: accessor, }; const component = mount( @@ -341,7 +349,7 @@ describe('dimension editor', () => { const buttonGroup = component.find(EuiButtonGroup); // make sure that if the user was to select the "custom" option, they would get the default value - expect(buttonGroup.props().options[1].value).toBe('foo'); + expect(buttonGroup.props().options[1].value).toBe('secondary-metric-col-id'); const newVal = 'bar'; @@ -461,7 +469,7 @@ describe('dimension editor', () => { }); describe('additional section', () => { - const accessor = 'primary-metric-col-id'; + const accessor = 'metric-col-id'; const metricAccessorState = { ...fullState, metricAccessor: accessor }; class Harness { @@ -473,6 +481,10 @@ describe('dimension editor', () => { this._wrapper = wrapper; } + public get wrapper() { + return this._wrapper; + } + private get rootComponent() { return this._wrapper.find(DimensionEditorAdditionalSection); } @@ -520,11 +532,16 @@ describe('dimension editor', () => { const mockSetState = jest.fn(); - const getHarnessWithState = (state: MetricVisualizationState, datasource = props.datasource) => + const getHarnessWithState = ( + state: MetricVisualizationState, + datasource = props.datasource, + propsOverrides: Partial> = {} + ) => new Harness( mountWithIntl( { } as DatasourcePublicAPI).isDisabled('trendline') ).toBeTruthy(); }); + it('should not show a trendline button group when primary metric dimension is non-numeric', () => { + expect( + getHarnessWithState(fullState, undefined, { + frame: nonNumericMetricFrame, + }).wrapper.isEmptyRender() + ).toBeTruthy(); + }); describe('responding to buttons', () => { it('enables trendline', () => { diff --git a/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx index 7d2561369ee6b5..c84fa45834fa0b 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx @@ -143,78 +143,77 @@ function SecondaryMetricEditor({ accessor, idPrefix, frame, layerId, setState, s const defaultPrefix = columnName || ''; return ( -
- - <> - { + + <> + { + setState({ + ...state, + secondaryPrefix, + }); + }} + /> + + {state.secondaryPrefix && ( + { setState({ ...state, - secondaryPrefix, + secondaryPrefix: newPrefix, }); }} /> - - {state.secondaryPrefix && ( - { - setState({ - ...state, - secondaryPrefix: newPrefix, - }); - }} - /> - )} - - -
+ )} + +
); } @@ -225,11 +224,13 @@ function PrimaryMetricEditor(props: SubProps) { const currentData = frame.activeData?.[state.layerId]; - if (accessor == null || !isNumericFieldForDatatable(currentData, accessor)) { + const isMetricNumeric = isNumericFieldForDatatable(currentData, accessor); + + if (accessor == null) { return null; } - const hasDynamicColoring = Boolean(state?.palette); + const hasDynamicColoring = Boolean(isMetricNumeric && state?.palette); const supportsPercentPalette = Boolean( state.maxAccessor || @@ -265,62 +266,65 @@ function PrimaryMetricEditor(props: SubProps) { return ( <> - - { - const colorMode = id.replace(idPrefix, '') as 'static' | 'dynamic'; - - const params = - colorMode === 'dynamic' - ? { - palette: { - ...activePalette, - params: { - ...activePalette.params, - stops: displayStops, + > + { + const colorMode = id.replace(idPrefix, '') as 'static' | 'dynamic'; + + const params = + colorMode === 'dynamic' + ? { + palette: { + ...activePalette, + params: { + ...activePalette.params, + stops: displayStops, + }, }, - }, - } - : { - palette: undefined, - }; - setState({ - ...state, - color: undefined, - ...params, - }); - }} - /> - + color: undefined, + } + : { + palette: undefined, + color: undefined, + }; + setState({ + ...state, + ...params, + }); + }} + /> + + )} {!hasDynamicColoring && } {hasDynamicColoring && ( ) { +function StaticColorControls({ + state, + setState, + frame, +}: Pick) { const colorLabel = i18n.translate('xpack.lens.metric.color', { defaultMessage: 'Color', }); + const currentData = frame.activeData?.[state.layerId]; + const isMetricNumeric = Boolean( + state.metricAccessor && isNumericFieldForDatatable(currentData, state.metricAccessor) + ); const setColor = useCallback( (color: string) => { @@ -420,7 +432,7 @@ function StaticColorControls({ state, setState }: Pick( { onChange: setColor, - value: state.color || getDefaultColor(state), + value: state.color || getDefaultColor(state, isMetricNumeric), }, { allowFalsyValue: true } ); @@ -448,10 +460,12 @@ export function DimensionEditorAdditionalSection({ addLayer, removeLayer, accessor, + frame, }: VisualizationDimensionEditorProps) { const { euiTheme } = useEuiTheme(); - if (accessor !== state.metricAccessor) { + const currentData = frame.activeData?.[state.layerId]; + if (accessor !== state.metricAccessor || !isNumericFieldForDatatable(currentData, accessor)) { return null; } @@ -566,7 +580,6 @@ export function DimensionEditorAdditionalSection({ }`} onChange={(id) => { const supportingVisualizationType = id.split('--')[1] as SupportingVisType; - switch (supportingVisualizationType) { case 'trendline': setState({ diff --git a/x-pack/plugins/lens/public/visualizations/metric/to_expression.ts b/x-pack/plugins/lens/public/visualizations/metric/to_expression.ts index b2367bb7c1fc86..281e758fcbf445 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/to_expression.ts +++ b/x-pack/plugins/lens/public/visualizations/metric/to_expression.ts @@ -90,6 +90,10 @@ export const toExpression = ( const datasource = datasourceLayers[state.layerId]; const datasourceExpression = datasourceExpressionsByLayers[state.layerId]; + const isMetricNumeric = Boolean( + state.metricAccessor && + datasource?.getOperationForColumnId(state.metricAccessor)?.dataType === 'number' + ); const maxPossibleTiles = // if there's a collapse function, no need to calculate since we're dealing with a single tile state.breakdownByAccessor && !state.collapseFn @@ -142,15 +146,16 @@ export const toExpression = ( trendline: trendlineExpression ? [trendlineExpression] : [], subtitle: state.subtitle ?? undefined, progressDirection: state.progressDirection as LayoutDirection, - color: state.color || getDefaultColor(state), + color: state.color || getDefaultColor(state, isMetricNumeric), icon: state.icon, - palette: state.palette?.params - ? [ - paletteService - .get(CUSTOM_PALETTE) - .toExpression(computePaletteParams(state.palette.params as CustomPaletteParams)), - ] - : [], + palette: + isMetricNumeric && state.palette?.params + ? [ + paletteService + .get(CUSTOM_PALETTE) + .toExpression(computePaletteParams(state.palette.params as CustomPaletteParams)), + ] + : [], maxCols: state.maxCols ?? DEFAULT_MAX_COLUMNS, minTiles: maxPossibleTiles ?? undefined, inspectorTableId: state.layerId, diff --git a/x-pack/plugins/lens/public/visualizations/metric/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/metric/visualization.test.ts index 61fac1418196e3..78ea5331e34cfa 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/metric/visualization.test.ts @@ -10,7 +10,7 @@ import { CustomPaletteParams, PaletteOutput } from '@kbn/coloring'; import { ExpressionAstExpression, ExpressionAstFunction } from '@kbn/expressions-plugin/common'; import { euiLightVars, euiThemeVars } from '@kbn/ui-theme'; import { LayerTypes } from '@kbn/expression-xy-plugin/public'; -import { createMockDatasource, createMockFramePublicAPI } from '../../mocks'; +import { createMockDatasource, createMockFramePublicAPI, generateActiveData } from '../../mocks'; import { DatasourceLayers, DatasourcePublicAPI, @@ -82,7 +82,14 @@ describe('metric visualization', () => { ...trendlineProps, }; - const mockFrameApi = createMockFramePublicAPI(); + const mockFrameApi = createMockFramePublicAPI({ + activeData: generateActiveData([ + { + id: 'first', + rows: Array(3).fill({ 'metric-col-id': 20, 'max-metric-col-id': 100 }), + }, + ]), + }); describe('initialization', () => { test('returns a default state', () => { @@ -268,6 +275,7 @@ describe('metric visualization', () => { mockDatasource.publicAPIMock.getMaxPossibleNumValues.mockReturnValue(maxPossibleNumValues); mockDatasource.publicAPIMock.getOperationForColumnId.mockReturnValue({ isStaticValue: false, + dataType: 'number', } as OperationDescriptor); datasourceLayers = { @@ -616,7 +624,7 @@ describe('metric visualization', () => { it('always applies max function to static max dimensions', () => { ( datasourceLayers.first as jest.Mocked - ).getOperationForColumnId.mockReturnValueOnce({ + ).getOperationForColumnId.mockReturnValue({ isStaticValue: true, } as OperationDescriptor); @@ -648,6 +656,9 @@ describe('metric visualization', () => { "type": "function", } `); + ( + datasourceLayers.first as jest.Mocked + ).getOperationForColumnId.mockClear(); }); }); @@ -1109,4 +1120,28 @@ describe('metric visualization', () => { noPadding: true, }); }); + + describe('#getUserMessages', () => { + it('returns error for non numeric primary metric if maxAccessor exists', () => { + const frame = createMockFramePublicAPI({ + activeData: generateActiveData([ + { + id: 'first', + rows: Array(3).fill({ 'metric-col-id': '100', 'max-metric-col-id': 100 }), + }, + ]), + }); + expect(visualization.getUserMessages!(fullState, { frame })).toHaveLength(1); + + const frameNoErrors = createMockFramePublicAPI({ + activeData: generateActiveData([ + { + id: 'first', + rows: Array(3).fill({ 'metric-col-id': 30, 'max-metric-col-id': 100 }), + }, + ]), + }); + expect(visualization.getUserMessages!(fullState, { frame: frameNoErrors })).toHaveLength(0); + }); + }); }); diff --git a/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx b/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx index 4dee7938da049f..21a7067f7191a7 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx @@ -14,6 +14,7 @@ import { LayoutDirection } from '@elastic/charts'; import { euiLightVars, euiThemeVars } from '@kbn/ui-theme'; import { IconChartMetric } from '@kbn/chart-icons'; import { AccessorConfig } from '@kbn/visualization-ui-components'; +import { isNumericFieldForDatatable } from '../../../common/expressions/datatable/utils'; import { CollapseFunction } from '../../../common/expressions'; import type { LayerType } from '../../../common/types'; import { layerTypes } from '../../../common/layer_types'; @@ -25,6 +26,7 @@ import { VisualizationConfigProps, VisualizationDimensionGroupConfig, Suggestion, + UserMessage, } from '../../types'; import { GROUP_ID, LENS_METRIC_ID } from './constants'; import { DimensionEditor, DimensionEditorAdditionalSection } from './dimension_editor'; @@ -40,8 +42,11 @@ export const showingBar = ( ): state is MetricVisualizationState & { showBar: true; maxAccessor: string } => Boolean(state.showBar && state.maxAccessor); -export const getDefaultColor = (state: MetricVisualizationState) => - showingBar(state) ? euiLightVars.euiColorPrimary : euiThemeVars.euiColorLightestShade; +export const getDefaultColor = (state: MetricVisualizationState, isMetricNumeric?: boolean) => { + return showingBar(state) && isMetricNumeric + ? euiLightVars.euiColorPrimary + : euiThemeVars.euiColorLightestShade; +}; export interface MetricVisualizationState { layerId: string; @@ -70,7 +75,13 @@ export interface MetricVisualizationState { trendlineBreakdownByAccessor?: string; } -export const supportedDataTypes = new Set(['number']); +export const supportedDataTypes = new Set(['string', 'boolean', 'number', 'ip', 'date']); + +const isSupportedMetric = (op: OperationMetadata) => + !op.isBucketed && supportedDataTypes.has(op.dataType); + +const isSupportedDynamicMetric = (op: OperationMetadata) => + !op.isBucketed && supportedDataTypes.has(op.dataType) && !op.isStaticValue; export const metricLabel = i18n.translate('xpack.lens.metric.label', { defaultMessage: 'Metric', @@ -84,29 +95,25 @@ const getMetricLayerConfiguration = ( ): { groups: VisualizationDimensionGroupConfig[]; } => { - const isSupportedMetric = (op: OperationMetadata) => - !op.isBucketed && supportedDataTypes.has(op.dataType); + const currentData = props.frame.activeData?.[props.state.layerId]; - const isSupportedDynamicMetric = (op: OperationMetadata) => - !op.isBucketed && supportedDataTypes.has(op.dataType) && !op.isStaticValue; + const isMetricNumeric = Boolean( + props.state.metricAccessor && + isNumericFieldForDatatable(currentData, props.state.metricAccessor) + ); const getPrimaryAccessorDisplayConfig = (): Partial => { + const hasDynamicColoring = Boolean(isMetricNumeric && props.state.palette); const stops = props.state.palette?.params?.stops || []; - const hasStaticColoring = !!props.state.color; - const hasDynamicColoring = !!props.state.palette; + return hasDynamicColoring ? { triggerIconType: 'colorBy', palette: stops.map(({ color }) => color), } - : hasStaticColoring - ? { - triggerIconType: 'color', - color: props.state.color, - } : { triggerIconType: 'color', - color: getDefaultColor(props.state), + color: props.state.color ?? getDefaultColor(props.state, isMetricNumeric), }; }; @@ -180,6 +187,7 @@ const getMetricLayerConfiguration = ( }, ] : [], + isHidden: !props.state.maxAccessor && !isMetricNumeric, supportsMoreColumns: !props.state.maxAccessor, filterOperations: isSupportedMetric, enableDimensionEditor: true, @@ -632,7 +640,7 @@ export const getMetricVisualization = ({ return suggestion; }, - getVisualizationInfo(state) { + getVisualizationInfo(state, frame) { const dimensions = []; if (state.metricAccessor) { dimensions.push({ @@ -676,6 +684,11 @@ export const getMetricVisualization = ({ const hasStaticColoring = !!state.color; const hasDynamicColoring = !!state.palette; + const currentData = frame?.activeData?.[state.layerId]; + const isMetricNumeric = Boolean( + state.metricAccessor && isNumericFieldForDatatable(currentData, state.metricAccessor) + ); + return { layers: [ { @@ -688,10 +701,34 @@ export const getMetricVisualization = ({ ? stops.map(({ color }) => color) : hasStaticColoring ? [state.color] - : [getDefaultColor(state)] + : [getDefaultColor(state, isMetricNumeric)] ).filter(nonNullable), }, ], }; }, + + getUserMessages(state, { frame }) { + const currentData = frame.activeData?.[state.layerId]; + + const errors: UserMessage[] = []; + + if (state.maxAccessor) { + const isMetricNonNumeric = Boolean( + state.metricAccessor && !isNumericFieldForDatatable(currentData, state.metricAccessor) + ); + if (isMetricNonNumeric) { + errors.push({ + severity: 'error', + fixableInEditor: true, + displayLocations: [{ id: 'dimensionButton', dimensionId: state.maxAccessor }], + shortMessage: i18n.translate('xpack.lens.lnsMetric_maxDimensionPanel.nonNumericError', { + defaultMessage: 'Primary metric must be numeric to set a maximum value.', + }), + longMessage: '', + }); + } + } + return errors; + }, }); diff --git a/x-pack/plugins/lens/public/visualizations/xy/reference_line_helpers.test.ts b/x-pack/plugins/lens/public/visualizations/xy/reference_line_helpers.test.ts index 61f32b3adfc66a..35aab159d42f67 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/reference_line_helpers.test.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/reference_line_helpers.test.ts @@ -5,25 +5,9 @@ * 2.0. */ -import { FramePublicAPI } from '../../types'; import { computeOverallDataDomain, getStaticValue } from './reference_line_helpers'; import { XYDataLayerConfig } from './types'; - -function getActiveData(json: Array<{ id: string; rows: Array> }>) { - return json.reduce((memo, { id, rows }) => { - const columns = Object.keys(rows[0]).map((columnId) => ({ - id: columnId, - name: columnId, - meta: { type: 'number' as const }, - })); - memo[id] = { - type: 'datatable' as const, - columns, - rows, - }; - return memo; - }, {} as NonNullable); -} +import { generateActiveData } from '../../mocks'; describe('reference_line helpers', () => { describe('getStaticValue', () => { @@ -41,7 +25,7 @@ describe('reference_line helpers', () => { [], 'x', { - activeData: getActiveData([ + activeData: generateActiveData([ { id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) }, ]), }, @@ -54,7 +38,7 @@ describe('reference_line helpers', () => { [{ layerId: 'id-a', seriesType: 'area' } as XYDataLayerConfig], // missing xAccessor for groupId == x 'x', { - activeData: getActiveData([ + activeData: generateActiveData([ { id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) }, ]), }, @@ -73,7 +57,7 @@ describe('reference_line helpers', () => { ], // missing hit of accessor "d" in data 'yLeft', { - activeData: getActiveData([ + activeData: generateActiveData([ { id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) }, ]), }, @@ -92,7 +76,7 @@ describe('reference_line helpers', () => { ], // missing yConfig fallbacks to left axis, but the requested group is yRight 'yRight', { - activeData: getActiveData([ + activeData: generateActiveData([ { id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) }, ]), }, @@ -111,7 +95,7 @@ describe('reference_line helpers', () => { ], // same as above with x groupId 'x', { - activeData: getActiveData([ + activeData: generateActiveData([ { id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) }, ]), }, @@ -134,7 +118,7 @@ describe('reference_line helpers', () => { ], 'yRight', { - activeData: getActiveData([ + activeData: generateActiveData([ { id: 'id-a', rows: [{ a: -30 }, { a: 10 }], @@ -159,7 +143,7 @@ describe('reference_line helpers', () => { ], 'yLeft', { - activeData: getActiveData([ + activeData: generateActiveData([ { id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) }, ]), }, @@ -182,7 +166,7 @@ describe('reference_line helpers', () => { ], 'yRight', { - activeData: getActiveData([ + activeData: generateActiveData([ { id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) }, ]), }, @@ -192,7 +176,7 @@ describe('reference_line helpers', () => { }); it('should correctly distribute axis on left and right with different formatters when in auto', () => { - const tables = getActiveData([ + const tables = generateActiveData([ { id: 'id-a', rows: Array(3).fill({ a: 100, b: 200, c: 100 }) }, ]); tables['id-a'].columns[0].meta.params = { id: 'number' }; // a: number formatter @@ -230,7 +214,7 @@ describe('reference_line helpers', () => { }); it('should ignore hasHistogram for left or right axis', () => { - const tables = getActiveData([ + const tables = generateActiveData([ { id: 'id-a', rows: Array(3).fill({ a: 100, b: 200, c: 100 }) }, ]); tables['id-a'].columns[0].meta.params = { id: 'number' }; // a: number formatter @@ -285,7 +269,7 @@ describe('reference_line helpers', () => { ], 'x', // this is influenced by the callback { - activeData: getActiveData([ + activeData: generateActiveData([ { id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) }, ]), }, @@ -312,7 +296,7 @@ describe('reference_line helpers', () => { ], 'x', { - activeData: getActiveData([ + activeData: generateActiveData([ { id: 'id-a', rows: Array(3) @@ -334,7 +318,7 @@ describe('reference_line helpers', () => { computeOverallDataDomain( [{ layerId: 'id-a', seriesType, accessors: ['a', 'b', 'c'] } as XYDataLayerConfig], ['a', 'b', 'c'], - getActiveData([ + generateActiveData([ { id: 'id-a', rows: Array(3) @@ -360,7 +344,7 @@ describe('reference_line helpers', () => { computeOverallDataDomain( [{ layerId: 'id-a', seriesType, accessors: ['a', 'b', 'c'] } as XYDataLayerConfig], ['a', 'b', 'c'], - getActiveData([ + generateActiveData([ { id: 'id-a', rows: Array(3) @@ -385,7 +369,7 @@ describe('reference_line helpers', () => { { layerId: 'id-b', seriesType, accessors: ['d', 'e', 'f'] }, ] as XYDataLayerConfig[], ['a', 'b', 'c', 'd', 'e', 'f'], - getActiveData([ + generateActiveData([ { id: 'id-a', rows: [{ a: 25, b: 100, c: 100 }] }, { id: 'id-b', rows: [{ d: 50, e: 50, f: 50 }] }, ]) @@ -399,7 +383,7 @@ describe('reference_line helpers', () => { { layerId: 'id-b', seriesType, accessors: ['d', 'e', 'f'] }, ] as XYDataLayerConfig[], ['a', 'b', 'c', 'd', 'e', 'f'], - getActiveData([ + generateActiveData([ { id: 'id-a', rows: Array(3) @@ -435,7 +419,7 @@ describe('reference_line helpers', () => { { layerId: 'id-b', seriesType, accessors: ['d', 'e', 'f'] }, ] as XYDataLayerConfig[], ['a', 'b', 'c', 'd', 'e', 'f'], - getActiveData([ + generateActiveData([ { id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) }, { id: 'id-b', rows: Array(3).fill({ d: 50, e: 50, f: 50 }) }, ]) @@ -453,7 +437,7 @@ describe('reference_line helpers', () => { { layerId: 'id-b', seriesType: stackedSeries, accessors: ['d', 'e', 'f'] }, ] as XYDataLayerConfig[], ['a', 'b', 'c', 'd', 'e', 'f'], - getActiveData([ + generateActiveData([ { id: 'id-a', rows: [{ a: 100, b: 100, c: 100 }] }, { id: 'id-b', rows: [{ d: 50, e: 50, f: 50 }] }, ]) @@ -475,7 +459,7 @@ describe('reference_line helpers', () => { { layerId: 'id-b', seriesType, xAccessor: 'f', accessors: ['d', 'e'] }, ] as XYDataLayerConfig[], ['a', 'b', 'd', 'e'], - getActiveData([ + generateActiveData([ { id: 'id-a', rows: Array(3) @@ -502,7 +486,7 @@ describe('reference_line helpers', () => { { layerId: 'id-b', seriesType, accessors: ['f'] }, ] as XYDataLayerConfig[], ['c', 'f'], - getActiveData([ + generateActiveData([ { id: 'id-a', rows: Array(3) @@ -530,7 +514,7 @@ describe('reference_line helpers', () => { { layerId: 'id-b', seriesType, xAccessor: 'f', accessors: ['d', 'e'] }, ] as XYDataLayerConfig[], ['a', 'b', 'd', 'e'], - getActiveData([ + generateActiveData([ { id: 'id-a', rows: Array(3) @@ -560,7 +544,7 @@ describe('reference_line helpers', () => { } as XYDataLayerConfig, ], ['a', 'b', 'c'], - getActiveData([ + generateActiveData([ { id: 'id-a', rows: Array(3) @@ -584,7 +568,7 @@ describe('reference_line helpers', () => { } as XYDataLayerConfig, ], ['a', 'b', 'c'], - getActiveData([ + generateActiveData([ { id: 'id-a', rows: Array(3) @@ -605,7 +589,7 @@ describe('reference_line helpers', () => { computeOverallDataDomain( [], ['a', 'b', 'c'], - getActiveData([{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) }]) + generateActiveData([{ id: 'id-a', rows: Array(3).fill({ a: 100, b: 100, c: 100 }) }]) ) ).toEqual({ min: undefined, max: undefined }); }); @@ -618,7 +602,7 @@ describe('reference_line helpers', () => { { layerId: 'id-b', seriesType: 'line', accessors: ['d', 'e', 'f'] }, ] as XYDataLayerConfig[], ['a', 'b'], - getActiveData([{ id: 'id-c', rows: [{ a: 100, b: 100 }] }]) // mind the layer id here + generateActiveData([{ id: 'id-c', rows: [{ a: 100, b: 100 }] }]) // mind the layer id here ) ).toEqual({ min: undefined, max: undefined }); @@ -629,7 +613,7 @@ describe('reference_line helpers', () => { { layerId: 'id-b', seriesType: 'bar_stacked' }, ] as XYDataLayerConfig[], ['a', 'b'], - getActiveData([]) + generateActiveData([]) ) ).toEqual({ min: undefined, max: undefined }); });