[9.0] [Lens][Datatable] Fix color by value for Last value array mode (#213917) (#214919)

# Backport

This will backport the following commits from `main` to `9.0`:
- [[Lens][Datatable] Fix color by value for Last value array mode
(#213917)](https://github.com/elastic/kibana/pull/213917)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Marco
Liberati","email":"dej611@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-03-18T09:09:03Z","message":"[Lens][Datatable]
Fix color by value for Last value array mode (#213917)\n\n##
Summary\n\nFixes the table side of #188263 \n\nThe [fix used
for\n`Metric`](https://github.com/elastic/kibana/pull/209110) has
been\ngeneralized and re-used for the datatable
visualization.\n\n\n![color_by_value_table](https://github.com/user-attachments/assets/b347dba2-24d7-4233-8c0c-3236f5212f35)\n\n\n###
Checklist\n\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios","sha":"0b9df094d1770065f79c05272a41175396caa27b","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","Team:Visualizations","Feature:Lens","backport:version","v9.1.0","v8.19.0","v8.18.1","v9.0.1"],"title":"[Lens][Datatable]
Fix color by value for Last value array
mode","number":213917,"url":"https://github.com/elastic/kibana/pull/213917","mergeCommit":{"message":"[Lens][Datatable]
Fix color by value for Last value array mode (#213917)\n\n##
Summary\n\nFixes the table side of #188263 \n\nThe [fix used
for\n`Metric`](https://github.com/elastic/kibana/pull/209110) has
been\ngeneralized and re-used for the datatable
visualization.\n\n\n![color_by_value_table](https://github.com/user-attachments/assets/b347dba2-24d7-4233-8c0c-3236f5212f35)\n\n\n###
Checklist\n\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios","sha":"0b9df094d1770065f79c05272a41175396caa27b"}},"sourceBranch":"main","suggestedTargetBranches":["8.x","8.18","9.0"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/213917","number":213917,"mergeCommit":{"message":"[Lens][Datatable]
Fix color by value for Last value array mode (#213917)\n\n##
Summary\n\nFixes the table side of #188263 \n\nThe [fix used
for\n`Metric`](https://github.com/elastic/kibana/pull/209110) has
been\ngeneralized and re-used for the datatable
visualization.\n\n\n![color_by_value_table](https://github.com/user-attachments/assets/b347dba2-24d7-4233-8c0c-3236f5212f35)\n\n\n###
Checklist\n\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios","sha":"0b9df094d1770065f79c05272a41175396caa27b"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.18","label":"v8.18.1","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"9.0","label":"v9.0.1","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Marco Liberati <dej611@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2025-03-18 11:56:44 +01:00 committed by GitHub
parent ab3050f295
commit ce776eca54
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 60 additions and 66 deletions

View file

@ -24,7 +24,7 @@ import {
import { getOriginalId } from '@kbn/transpose-utils';
import { Datatable, DatatableColumnType } from '@kbn/expressions-plugin/common';
import { KbnPalettes } from '@kbn/palettes';
import { DataType } from '../../types';
import { DataType, DatasourcePublicAPI } from '../../types';
/**
* Returns array of colors for provided palette or colorMapping
@ -45,8 +45,33 @@ export function getPaletteDisplayColors(
.getCategoricalColors(palette?.params?.steps || 10, palette);
}
/**
* Analyze the column from the datasource prospective (formal check)
* to know whether it's a numeric type or not
* Note: to be used for Lens UI only
*/
export function getAccessorType(
datasource: DatasourcePublicAPI | undefined,
accessor: string | undefined
) {
// No accessor means it's not a numeric type by default
if (!accessor || !datasource) {
return { isNumeric: false, isCategory: false };
}
const operation = datasource.getOperationForColumnId(accessor);
const isNumericTypeFromOperation = Boolean(
!operation?.isBucketed && operation?.dataType === 'number' && !operation.hasArraySupport
);
const isBucketableTypeFromOperationType = Boolean(
operation?.isBucketed ||
(!['number', 'date'].includes(operation?.dataType || '') && !operation?.hasArraySupport)
);
return { isNumeric: isNumericTypeFromOperation, isCategory: isBucketableTypeFromOperationType };
}
/**
* Bucketed numerical columns should be treated as categorical
* Note: to be used within expression renderer scope only
*/
export function shouldColorByTerms(
dataType?: DataType | DatatableColumnType,

View file

@ -12,14 +12,13 @@ import userEvent, { type UserEvent } from '@testing-library/user-event';
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
import { EuiButtonGroupTestHarness } from '@kbn/test-eui-helpers';
import { FramePublicAPI, DatasourcePublicAPI, OperationDescriptor } from '../../../types';
import { FramePublicAPI, DatasourcePublicAPI, OperationDescriptor, DataType } from '../../../types';
import { DatatableVisualizationState } from '../visualization';
import { createMockDatasource, createMockFramePublicAPI } from '../../../mocks';
import { TableDimensionEditor, TableDimensionEditorProps } from './dimension_editor';
import { ColumnState } from '../../../../common/expressions';
import { capitalize } from 'lodash';
import { I18nProvider } from '@kbn/i18n-react';
import { DatatableColumnType } from '@kbn/expressions-plugin/common';
import { getKbnPalettes } from '@kbn/palettes';
describe('data table dimension editor', () => {
@ -127,14 +126,13 @@ describe('data table dimension editor', () => {
});
it('should render default alignment for number', () => {
frame.activeData!.first.columns[0].meta.type = 'number';
mockOperationForFirstColumn({ dataType: 'number' });
renderTableDimensionEditor();
expect(btnGroups.alignment.selected).toHaveTextContent('Right');
});
it('should render default alignment for ranges', () => {
frame.activeData!.first.columns[0].meta.type = 'number';
frame.activeData!.first.columns[0].meta.params = { id: 'range' };
mockOperationForFirstColumn({ isBucketed: true, dataType: 'number' });
renderTableDimensionEditor();
expect(btnGroups.alignment.selected).toHaveTextContent('Left');
});
@ -178,10 +176,10 @@ describe('data table dimension editor', () => {
expect(screen.queryByTestId('lns_dynamicColoring_edit')).not.toBeInTheDocument();
});
it.each<DatatableColumnType>(['date'])(
it.each<DataType>(['date'])(
'should not show the dynamic coloring option for "%s" columns',
(type) => {
frame.activeData!.first.columns[0].meta.type = type;
mockOperationForFirstColumn({ dataType: type });
renderTableDimensionEditor();
expect(screen.queryByTestId('lnsDatatable_dynamicColoring_groups')).not.toBeInTheDocument();
@ -229,7 +227,7 @@ describe('data table dimension editor', () => {
});
});
it.each<{ flyout: 'terms' | 'values'; isBucketed: boolean; type: DatatableColumnType }>([
it.each<{ flyout: 'terms' | 'values'; isBucketed: boolean; type: DataType }>([
{ flyout: 'terms', isBucketed: true, type: 'number' },
{ flyout: 'terms', isBucketed: false, type: 'string' },
{ flyout: 'values', isBucketed: false, type: 'number' },
@ -237,8 +235,7 @@ describe('data table dimension editor', () => {
'should show color by $flyout flyout when bucketing is $isBucketed with $type column',
async ({ flyout, isBucketed, type }) => {
state.columns[0].colorMode = 'cell';
frame.activeData!.first.columns[0].meta.type = type;
mockOperationForFirstColumn({ isBucketed });
mockOperationForFirstColumn({ isBucketed, dataType: type });
renderTableDimensionEditor();
await user.click(screen.getByLabelText('Edit colors'));
@ -250,8 +247,7 @@ describe('data table dimension editor', () => {
it('should show the dynamic coloring option for a bucketed operation', () => {
state.columns[0].colorMode = 'cell';
frame.activeData!.first.columns[0].meta.type = 'string';
mockOperationForFirstColumn({ isBucketed: true });
mockOperationForFirstColumn({ isBucketed: true, dataType: 'string' });
renderTableDimensionEditor();
expect(screen.queryByTestId('lnsDatatable_dynamicColoring_groups')).toBeInTheDocument();

View file

@ -26,7 +26,7 @@ import type { DatatableVisualizationState } from '../visualization';
import {
defaultPaletteParams,
findMinMaxByColumnId,
shouldColorByTerms,
getAccessorType,
} from '../../../shared_components';
import './dimension_editor.scss';
@ -34,10 +34,6 @@ import { CollapseSetting } from '../../../shared_components/collapse_setting';
import { ColorMappingByValues } from '../../../shared_components/coloring/color_mapping_by_values';
import { ColorMappingByTerms } from '../../../shared_components/coloring/color_mapping_by_terms';
import { getColumnAlignment } from '../utils';
import {
getFieldMetaFromDatatable,
isNumericField,
} from '../../../../common/expressions/datatable/utils';
import { DatatableInspectorTables } from '../../../../common/expressions/datatable/datatable_fn';
const idPrefix = htmlIdGenerator()();
@ -90,13 +86,13 @@ export function TableDimensionEditor(props: TableDimensionEditorProps) {
const currentData =
frame.activeData?.[localState.layerId] ?? frame.activeData?.[DatatableInspectorTables.Default];
const datasource = frame.datasourceLayers?.[localState.layerId];
const { isBucketed } = datasource?.getOperationForColumnId(accessor) ?? {};
const meta = getFieldMetaFromDatatable(currentData, accessor);
const showColorByTerms = shouldColorByTerms(meta?.type, isBucketed);
const currentAlignment = getColumnAlignment(column, isNumericField(meta));
const { isNumeric, isCategory: isBucketable } = getAccessorType(datasource, accessor);
const showColorByTerms = isBucketable;
const showDynamicColoringFeature = isBucketable || isNumeric;
const currentAlignment = getColumnAlignment(column, isNumeric);
const currentColorMode = column?.colorMode || 'none';
const hasDynamicColoring = currentColorMode !== 'none';
const showDynamicColoringFeature = meta?.type !== 'date';
const visibleColumnsCount = localState.columns.filter((c) => !c.hidden).length;
const hasTransposedColumn = localState.columns.some(({ isTransposed }) => isTransposed);

View file

@ -56,7 +56,7 @@ import {
defaultPaletteParams,
findMinMaxByColumnId,
getPaletteDisplayColors,
shouldColorByTerms,
getAccessorType,
} from '../../shared_components';
import { getColorMappingTelemetryEvents } from '../../lens_ui_telemetry/color_telemetry_helpers';
import { DatatableInspectorTables } from '../../../common/expressions/datatable/datatable_fn';
@ -147,11 +147,11 @@ export const getDatatableVisualization = ({
const hasTransposedColumn = state.columns.some(({ isTransposed }) => isTransposed);
const columns = state.columns.map((column) => {
if (column.palette) {
const accessor = column.columnId;
const accessor = column.columnId;
const { isNumeric, isCategory: isBucketable } = getAccessorType(datasource, accessor);
if (column.palette && (isNumeric || isBucketable)) {
const showColorByTerms = isBucketable;
const currentData = frame?.activeData?.[state.layerId];
const { dataType, isBucketed } = datasource?.getOperationForColumnId(column.columnId) ?? {};
const showColorByTerms = shouldColorByTerms(dataType, isBucketed);
const palette = paletteMap.get(column.palette?.name ?? '');
const columnsToCheck = hasTransposedColumn
? currentData?.columns
@ -163,7 +163,7 @@ export const getDatatableVisualization = ({
if (palette && !showColorByTerms && !palette?.canDynamicColoring && dataBounds) {
const newPalette: PaletteOutput<CustomPaletteParams> = {
type: 'palette',
name: showColorByTerms ? 'default' : defaultPaletteParams.name,
name: defaultPaletteParams.name,
};
return {
...column,
@ -583,6 +583,10 @@ export const getDatatableVisualization = ({
columns: columns
.filter((c) => !c.collapseFn)
.map((column) => {
const { isNumeric, isCategory: isBucketable } = getAccessorType(
datasource,
column.columnId
);
const stops = getOverridePaletteStops(paletteService, column.palette);
const paletteParams = {
...column.palette?.params,
@ -594,11 +598,11 @@ export const getDatatableVisualization = ({
: [],
reverse: false, // managed at UI level
};
const { dataType, isBucketed, sortingHint, inMetricDimension } =
const { sortingHint, inMetricDimension } =
datasource?.getOperationForColumnId(column.columnId) ?? {};
const hasNoSummaryRow = column.summaryRow == null || column.summaryRow === 'none';
const canColor = dataType !== 'date';
const colorByTerms = shouldColorByTerms(dataType, isBucketed);
const canColor = isNumeric || isBucketable;
const colorByTerms = isBucketable;
let isTransposable =
!isTextBasedLanguage &&
!datasource!.getOperationForColumnId(column.columnId)?.isBucketed;

View file

@ -31,14 +31,13 @@ import { getColumnByAccessor } from '@kbn/visualizations-plugin/common/utils';
import { css } from '@emotion/react';
import { DebouncedInput, IconSelect } from '@kbn/visualization-ui-components';
import { useDebouncedValue } from '@kbn/visualization-utils';
import { PalettePanelContainer } from '../../shared_components';
import { PalettePanelContainer, getAccessorType } from '../../shared_components';
import type { VisualizationDimensionEditorProps } from '../../types';
import { defaultNumberPaletteParams, defaultPercentagePaletteParams } from './palette_config';
import { DEFAULT_MAX_COLUMNS, getDefaultColor, showingBar } from './visualization';
import { CollapseSetting } from '../../shared_components/collapse_setting';
import { MetricVisualizationState } from './types';
import { metricIconsSet } from '../../shared_components/icon_set';
import { isMetricNumericType } from './helpers';
export type SupportingVisType = 'none' | 'bar' | 'trendline';
@ -217,7 +216,7 @@ function PrimaryMetricEditor(props: SubProps) {
return null;
}
const isMetricNumeric = isMetricNumericType(props.datasource, accessor);
const { isNumeric: isMetricNumeric } = getAccessorType(props.datasource, accessor);
const hasDynamicColoring = Boolean(isMetricNumeric && state.palette);
@ -417,7 +416,7 @@ export function DimensionEditorAdditionalSection({
}: VisualizationDimensionEditorProps<MetricVisualizationState>) {
const { euiTheme } = useEuiTheme();
const isMetricNumeric = isMetricNumericType(datasource, accessor);
const { isNumeric: isMetricNumeric } = getAccessorType(datasource, accessor);
if (accessor !== state.metricAccessor || !isMetricNumeric) {
return null;
}

View file

@ -1,26 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { DatasourcePublicAPI } from '../../types';
/**
* Infer the numeric type of a metric column purely on the configuration
*/
export function isMetricNumericType(
datasource: DatasourcePublicAPI | undefined,
accessor: string | undefined
) {
// No accessor means it's not a numeric type by default
if (!accessor || !datasource) {
return false;
}
const operation = datasource.getOperationForColumnId(accessor);
const isNumericTypeFromOperation = Boolean(
operation?.dataType === 'number' && !operation.hasArraySupport
);
return isNumericTypeFromOperation;
}

View file

@ -27,7 +27,7 @@ import { showingBar } from './metric_visualization';
import { DEFAULT_MAX_COLUMNS, getDefaultColor } from './visualization';
import { MetricVisualizationState } from './types';
import { metricStateDefaults } from './constants';
import { isMetricNumericType } from './helpers';
import { getAccessorType } from '../../shared_components';
// TODO - deduplicate with gauges?
function computePaletteParams(
@ -105,7 +105,7 @@ export const toExpression = (
const datasource = datasourceLayers[state.layerId];
const datasourceExpression = datasourceExpressionsByLayers[state.layerId];
const isMetricNumeric = isMetricNumericType(datasource, state.metricAccessor);
const { isNumeric: isMetricNumeric } = getAccessorType(datasource, state.metricAccessor);
const maxPossibleTiles =
// if there's a collapse function, no need to calculate since we're dealing with a single tile
state.breakdownByAccessor && !state.collapseFn

View file

@ -33,7 +33,7 @@ import { toExpression } from './to_expression';
import { nonNullable } from '../../utils';
import { METRIC_NUMERIC_MAX } from '../../user_messages_ids';
import { MetricVisualizationState } from './types';
import { isMetricNumericType } from './helpers';
import { getAccessorType } from '../../shared_components';
export const DEFAULT_MAX_COLUMNS = 3;
@ -658,7 +658,7 @@ export const getMetricVisualization = ({
const hasStaticColoring = !!state.color;
const hasDynamicColoring = !!state.palette;
const isMetricNumeric = isMetricNumericType(
const { isNumeric: isMetricNumeric } = getAccessorType(
frame?.datasourceLayers[state.layerId],
state.metricAccessor
);