mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Lens][Table] Add color by terms with color mappings (#189895)
Adds support for coloring Table cells by terms with color mapping assignments. Supported for both `Rows` and `Metric` dimensions.
This commit is contained in:
parent
7944c1963d
commit
a994629331
77 changed files with 1933 additions and 842 deletions
|
@ -15,22 +15,26 @@ import { Container } from './components/container/container';
|
|||
import { ColorMapping } from './config';
|
||||
import { uiReducer } from './state/ui';
|
||||
|
||||
export interface ColorMappingInputCategoricalData {
|
||||
type: 'categories';
|
||||
/** an ORDERED array of categories rendered in the visualization */
|
||||
categories: Array<string | string[]>;
|
||||
}
|
||||
|
||||
export interface ColorMappingInputContinuousData {
|
||||
type: 'ranges';
|
||||
min: number;
|
||||
max: number;
|
||||
bins: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A configuration object that is required to populate correctly the visible categories
|
||||
* or the ranges in the CategoricalColorMapping component
|
||||
*/
|
||||
export type ColorMappingInputData =
|
||||
| {
|
||||
type: 'categories';
|
||||
/** an ORDERED array of categories rendered in the visualization */
|
||||
categories: Array<string | string[]>;
|
||||
}
|
||||
| {
|
||||
type: 'ranges';
|
||||
min: number;
|
||||
max: number;
|
||||
bins: number;
|
||||
};
|
||||
| ColorMappingInputCategoricalData
|
||||
| ColorMappingInputContinuousData;
|
||||
|
||||
/**
|
||||
* The props of the CategoricalColorMapping component
|
||||
|
|
|
@ -74,7 +74,7 @@ export function getColorFactory(
|
|||
})
|
||||
: [];
|
||||
|
||||
// find all categories that doesn't match with an assignment
|
||||
// find all categories that don't match with an assignment
|
||||
const notAssignedCategories =
|
||||
data.type === 'categories'
|
||||
? data.categories.filter((category) => {
|
||||
|
|
|
@ -41,7 +41,7 @@ export function rangeMatch(rule: ColorMapping.RuleRange, value: number) {
|
|||
}
|
||||
|
||||
// TODO: move in some data/table related package
|
||||
export const SPECIAL_TOKENS_STRING_CONVERTION = new Map([
|
||||
export const SPECIAL_TOKENS_STRING_CONVERSION = new Map([
|
||||
[
|
||||
'__other__',
|
||||
i18n.translate('coloring.colorMapping.terms.otherBucketLabel', {
|
||||
|
@ -55,3 +55,9 @@ export const SPECIAL_TOKENS_STRING_CONVERTION = new Map([
|
|||
}),
|
||||
],
|
||||
]);
|
||||
|
||||
/**
|
||||
* Returns special string for sake of color mapping/syncing
|
||||
*/
|
||||
export const getSpecialString = (value: string) =>
|
||||
SPECIAL_TOKENS_STRING_CONVERSION.get(value) ?? value;
|
||||
|
|
|
@ -121,7 +121,7 @@ export function Assignment({
|
|||
css={
|
||||
!disableDelete
|
||||
? css`
|
||||
color: ${euiThemeVars.euiTextSubduedColor};
|
||||
color: ${euiThemeVars.euiTextColor};
|
||||
transition: ${euiThemeVars.euiAnimSpeedFast} ease-in-out;
|
||||
transition-property: color;
|
||||
&:hover,
|
||||
|
|
|
@ -73,6 +73,7 @@ export const Match: React.FC<{
|
|||
return (
|
||||
<EuiFlexItem style={{ minWidth: 1, width: 1 }}>
|
||||
<EuiComboBox
|
||||
isClearable
|
||||
data-test-subj={`lns-colorMapping-assignmentsItem${index}`}
|
||||
fullWidth={true}
|
||||
aria-label={i18n.translate('coloring.colorMapping.assignments.autoAssignedTermAriaLabel', {
|
||||
|
@ -82,7 +83,7 @@ export const Match: React.FC<{
|
|||
placeholder={i18n.translate(
|
||||
'coloring.colorMapping.assignments.autoAssignedTermPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Auto assigned',
|
||||
defaultMessage: 'Auto assigning term',
|
||||
}
|
||||
)}
|
||||
options={convertedOptions}
|
||||
|
@ -103,7 +104,6 @@ export const Match: React.FC<{
|
|||
}
|
||||
}}
|
||||
isCaseSensitive
|
||||
isClearable={false}
|
||||
compressed
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -7,14 +7,7 @@
|
|||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiPopoverTitle,
|
||||
EuiTab,
|
||||
EuiTabs,
|
||||
EuiTitle,
|
||||
EuiHorizontalRule,
|
||||
} from '@elastic/eui';
|
||||
import { EuiButtonEmpty, EuiPopoverTitle, EuiTab, EuiTabs, EuiHorizontalRule } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ColorMapping } from '../../config';
|
||||
import { getPalette } from '../../palettes';
|
||||
|
@ -56,22 +49,14 @@ export function ColorPicker({
|
|||
>
|
||||
<EuiTabs size="m" expand>
|
||||
<EuiTab onClick={() => setTab('palette')} isSelected={tab === 'palette'}>
|
||||
<EuiTitle size="xxxs">
|
||||
<span>
|
||||
{i18n.translate('coloring.colorMapping.colorPicker.paletteTabLabel', {
|
||||
defaultMessage: 'Colors',
|
||||
})}
|
||||
</span>
|
||||
</EuiTitle>
|
||||
{i18n.translate('coloring.colorMapping.colorPicker.paletteTabLabel', {
|
||||
defaultMessage: 'Colors',
|
||||
})}
|
||||
</EuiTab>
|
||||
<EuiTab onClick={() => setTab('custom')} isSelected={tab === 'custom'}>
|
||||
<EuiTitle size="xxxs">
|
||||
<span>
|
||||
{i18n.translate('coloring.colorMapping.colorPicker.customTabLabel', {
|
||||
defaultMessage: 'Custom',
|
||||
})}
|
||||
</span>
|
||||
</EuiTitle>
|
||||
{i18n.translate('coloring.colorMapping.colorPicker.customTabLabel', {
|
||||
defaultMessage: 'Custom',
|
||||
})}
|
||||
</EuiTab>
|
||||
</EuiTabs>
|
||||
</EuiPopoverTitle>
|
||||
|
|
|
@ -124,15 +124,14 @@ export const ColorSwatch = ({
|
|||
css={css`
|
||||
&::after {
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 3px solid transparent;
|
||||
border-right: 3px solid transparent;
|
||||
border-top: 4px solid ${colorIsDark ? 'white' : 'black'};
|
||||
margin: 0;
|
||||
bottom: 2px;
|
||||
background-color: ${colorIsDark ? 'white' : 'black'};
|
||||
// custom arrowDown svg
|
||||
mask-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI3IiBoZWlnaHQ9IjQiIHZpZXdCb3g9IjAgMCA3IDQiPgogIDxwYXRoIGQ9Ik0uMTQ2LjE0N2EuNS41IDAgMCAxIC43MDggMEwzLjUgMi43OTQgNi4xNDYuMTQ3YS41LjUgMCAxIDEgLjcwOC43MDhsLTMgM2EuNS41IDAgMCAxLS43MDggMGwtMy0zYS41LjUgMCAwIDEgMC0uNzA4WiIvPgo8L3N2Zz4K');
|
||||
height: 4px;
|
||||
width: 7px;
|
||||
bottom: 6px;
|
||||
right: 4px;
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
|
|
|
@ -12,8 +12,9 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
EuiIcon,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
EuiToolTip,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ColorMapping } from '../../config';
|
||||
|
@ -50,13 +51,14 @@ export function PaletteColors({
|
|||
<>
|
||||
<EuiFlexGroup direction="column" style={{ padding: 8 }}>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<strong>
|
||||
<EuiTitle size="xxxs">
|
||||
<h6>
|
||||
{i18n.translate('coloring.colorMapping.colorPicker.paletteColorsLabel', {
|
||||
defaultMessage: 'Palette colors',
|
||||
})}
|
||||
</strong>
|
||||
</EuiText>
|
||||
</h6>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
|
@ -84,22 +86,26 @@ export function PaletteColors({
|
|||
<EuiHorizontalRule margin="xs" />
|
||||
<EuiFlexGroup style={{ padding: 8, paddingTop: 0 }}>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<strong>
|
||||
<EuiTitle size="xxxs">
|
||||
<h6>
|
||||
{i18n.translate('coloring.colorMapping.colorPicker.themeAwareColorsLabel', {
|
||||
defaultMessage: 'Neutral colors',
|
||||
})}
|
||||
</strong>
|
||||
<EuiToolTip
|
||||
position="bottom"
|
||||
content={i18n.translate('coloring.colorMapping.colorPicker.themeAwareColorsTooltip', {
|
||||
defaultMessage:
|
||||
'The provided neutral colors are theme-aware and will change appropriately when switching between light and dark themes.',
|
||||
})}
|
||||
>
|
||||
<EuiIcon tabIndex={0} type="questionInCircle" />
|
||||
</EuiToolTip>
|
||||
</EuiText>
|
||||
<EuiToolTip
|
||||
position="bottom"
|
||||
content={i18n.translate(
|
||||
'coloring.colorMapping.colorPicker.themeAwareColorsTooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
'The provided neutral colors are theme-aware and will change appropriately when switching between light and dark themes.',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiIcon tabIndex={0} type="questionInCircle" />
|
||||
</EuiToolTip>
|
||||
</h6>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
|
|
|
@ -149,7 +149,7 @@ export function AssignmentsConfig({
|
|||
return (
|
||||
<EuiPanel
|
||||
color="subdued"
|
||||
borderRadius="none"
|
||||
borderRadius="m"
|
||||
hasShadow={false}
|
||||
paddingSize="none"
|
||||
css={css`
|
||||
|
@ -195,7 +195,7 @@ export function AssignmentsConfig({
|
|||
'coloring.colorMapping.container.mapValuesPromptDescription.mapValuesPromptDetail',
|
||||
{
|
||||
defaultMessage:
|
||||
'Add new assignments to begin associating terms in your data with specified colors.',
|
||||
'Add a new assignment to manually associate terms with specified colors.',
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
|
@ -214,7 +214,6 @@ export function AssignmentsConfig({
|
|||
</EuiButton>,
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="lns-colorMapping-assignmentsPromptAddAll"
|
||||
color="primary"
|
||||
size="xs"
|
||||
onClick={onClickAddAllCurrentCategories}
|
||||
>
|
||||
|
@ -228,13 +227,14 @@ export function AssignmentsConfig({
|
|||
</EuiFlexGroup>
|
||||
</div>
|
||||
{assignments.length > 0 && <EuiHorizontalRule margin="none" />}
|
||||
<div
|
||||
css={css`
|
||||
padding: ${euiThemeVars.euiPanelPaddingModifiers.paddingSmall};
|
||||
overflow: hidden;
|
||||
`}
|
||||
>
|
||||
{assignments.length > 0 && (
|
||||
|
||||
{assignments.length > 0 && (
|
||||
<div
|
||||
css={css`
|
||||
padding: ${euiThemeVars.euiPanelPaddingModifiers.paddingSmall};
|
||||
overflow: hidden;
|
||||
`}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
|
@ -251,6 +251,7 @@ export function AssignmentsConfig({
|
|||
button={
|
||||
<EuiButtonIcon
|
||||
iconType="boxesVertical"
|
||||
color="text"
|
||||
aria-label={i18n.translate(
|
||||
'coloring.colorMapping.container.OpenAdditionalActionsButtonLabel',
|
||||
{
|
||||
|
@ -308,7 +309,9 @@ export function AssignmentsConfig({
|
|||
setShowOtherActions(false);
|
||||
dispatch(removeAllAssignments());
|
||||
}}
|
||||
color="danger"
|
||||
css={css`
|
||||
color: ${euiThemeVars.euiColorDanger};
|
||||
`}
|
||||
>
|
||||
{i18n.translate(
|
||||
'coloring.colorMapping.container.clearAllAssignmentsButtonLabel',
|
||||
|
@ -322,8 +325,8 @@ export function AssignmentsConfig({
|
|||
</EuiPopover>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -81,6 +81,7 @@ export function Container({
|
|||
>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="lns-colorMapping-invertGradient"
|
||||
color="text"
|
||||
iconType="merge"
|
||||
size="xs"
|
||||
aria-label={i18n.translate(
|
||||
|
|
|
@ -107,6 +107,7 @@ export function ScaleMode({ getPaletteFn }: { getPaletteFn: ReturnType<typeof ge
|
|||
>
|
||||
<EuiButtonGroup
|
||||
legend="Mode"
|
||||
buttonSize="compressed"
|
||||
data-test-subj="lns_colorMapping_scaleSwitch"
|
||||
options={[
|
||||
{
|
||||
|
|
|
@ -6,12 +6,17 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { CategoricalColorMapping, type ColorMappingProps } from './categorical_color_mapping';
|
||||
export {
|
||||
CategoricalColorMapping,
|
||||
type ColorMappingProps,
|
||||
type ColorMappingInputCategoricalData,
|
||||
type ColorMappingInputContinuousData,
|
||||
} from './categorical_color_mapping';
|
||||
export type { ColorMappingInputData } from './categorical_color_mapping';
|
||||
export type { ColorMapping } from './config';
|
||||
export * from './palettes';
|
||||
export * from './color/color_handling';
|
||||
export { SPECIAL_TOKENS_STRING_CONVERTION } from './color/rule_matching';
|
||||
export { SPECIAL_TOKENS_STRING_CONVERSION, getSpecialString } from './color/rule_matching';
|
||||
export {
|
||||
DEFAULT_COLOR_MAPPING_CONFIG,
|
||||
DEFAULT_OTHER_ASSIGNMENT_INDEX,
|
||||
|
|
|
@ -77,9 +77,9 @@ export function PalettePicker({
|
|||
}) {
|
||||
const palettesToShow: EuiColorPalettePickerPaletteProps[] = palettes
|
||||
.getAll()
|
||||
.filter(({ internal, canDynamicColoring }) =>
|
||||
showDynamicColorOnly ? canDynamicColoring : !internal
|
||||
)
|
||||
.filter(({ internal, canDynamicColoring }) => {
|
||||
return showDynamicColorOnly ? canDynamicColoring && !internal : !internal;
|
||||
})
|
||||
.map(({ id, title, getCategoricalColors }) => {
|
||||
const colors = getCategoricalColors(
|
||||
activePalette?.params?.steps || DEFAULT_COLOR_STEPS,
|
||||
|
|
|
@ -55,7 +55,7 @@ pageLoadAssetSize:
|
|||
expressionLegacyMetricVis: 23121
|
||||
expressionMetric: 22238
|
||||
expressionMetricVis: 23121
|
||||
expressionPartitionVis: 29700
|
||||
expressionPartitionVis: 44888
|
||||
expressionRepeatImage: 22341
|
||||
expressionRevealImage: 25675
|
||||
expressions: 140958
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { DatatableRow } from '@kbn/expressions-plugin/common';
|
||||
import { getColorCategories } from './color_categories';
|
||||
|
||||
const extensions = ['gz', 'css', '', 'rpm', 'deb', 'zip', null];
|
||||
const getExtension = (i: number) => extensions[i % extensions.length];
|
||||
|
||||
const basicDatatableRows: DatatableRow[] = Array.from({ length: 30 }).map((_, i) => ({
|
||||
count: i,
|
||||
extension: getExtension(i),
|
||||
}));
|
||||
|
||||
const isTransposedDatatableRows: DatatableRow[] = Array.from({ length: 30 }).map((_, i) => ({
|
||||
count: i,
|
||||
['safari---extension']: getExtension(i),
|
||||
['chrome---extension']: getExtension(i + 1),
|
||||
['firefox---extension']: getExtension(i + 2),
|
||||
}));
|
||||
|
||||
describe('getColorCategories', () => {
|
||||
it('should return all categories from datatable rows', () => {
|
||||
expect(getColorCategories(basicDatatableRows, 'extension')).toEqual([
|
||||
'gz',
|
||||
'css',
|
||||
'',
|
||||
'rpm',
|
||||
'deb',
|
||||
'zip',
|
||||
'null',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should exclude selected categories from datatable rows', () => {
|
||||
expect(getColorCategories(basicDatatableRows, 'extension', false, ['', null])).toEqual([
|
||||
'gz',
|
||||
'css',
|
||||
'rpm',
|
||||
'deb',
|
||||
'zip',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return categories across all transpose columns of datatable rows', () => {
|
||||
expect(getColorCategories(isTransposedDatatableRows, 'extension', true)).toEqual([
|
||||
'gz',
|
||||
'css',
|
||||
'',
|
||||
'rpm',
|
||||
'deb',
|
||||
'zip',
|
||||
'null',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should exclude selected categories across all transpose columns of datatable rows', () => {
|
||||
expect(getColorCategories(isTransposedDatatableRows, 'extension', true, ['', null])).toEqual([
|
||||
'gz',
|
||||
'css',
|
||||
'rpm',
|
||||
'deb',
|
||||
'zip',
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -15,27 +15,38 @@ import { isMultiFieldKey } from '@kbn/data-plugin/common';
|
|||
*/
|
||||
export function getColorCategories(
|
||||
rows: DatatableRow[],
|
||||
accessor?: string
|
||||
accessor?: string,
|
||||
isTransposed?: boolean,
|
||||
exclude?: any[]
|
||||
): Array<string | string[]> {
|
||||
return accessor
|
||||
? rows.reduce<{ keys: Set<string>; categories: Array<string | string[]> }>(
|
||||
(acc, r) => {
|
||||
const value = r[accessor];
|
||||
if (value === undefined) {
|
||||
return acc;
|
||||
}
|
||||
const ids = isTransposed
|
||||
? Object.keys(rows[0]).filter((key) => accessor && key.endsWith(accessor))
|
||||
: accessor
|
||||
? [accessor]
|
||||
: [];
|
||||
|
||||
return rows
|
||||
.flatMap((r) =>
|
||||
ids
|
||||
.map((id) => r[id])
|
||||
.filter((v) => !(v === undefined || exclude?.includes(v)))
|
||||
.map((v) => {
|
||||
// The categories needs to be stringified in their unformatted version.
|
||||
// We can't distinguish between a number and a string from a text input and the match should
|
||||
// work with both numeric field values and string values.
|
||||
const key = (isMultiFieldKey(value) ? [...value.keys] : [value]).map(String);
|
||||
const key = (isMultiFieldKey(v) ? v.keys : [v]).map(String);
|
||||
const stringifiedKeys = key.join(',');
|
||||
if (!acc.keys.has(stringifiedKeys)) {
|
||||
acc.keys.add(stringifiedKeys);
|
||||
acc.categories.push(key.length === 1 ? key[0] : key);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{ keys: new Set(), categories: [] }
|
||||
).categories
|
||||
: [];
|
||||
return { key, stringifiedKeys };
|
||||
})
|
||||
)
|
||||
.reduce<{ keys: Set<string>; categories: Array<string | string[]> }>(
|
||||
(acc, { key, stringifiedKeys }) => {
|
||||
if (!acc.keys.has(stringifiedKeys)) {
|
||||
acc.keys.add(stringifiedKeys);
|
||||
acc.categories.push(key.length === 1 ? key[0] : key);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{ keys: new Set(), categories: [] }
|
||||
).categories;
|
||||
}
|
||||
|
|
|
@ -1747,8 +1747,8 @@ exports[`XYChart component it renders area 1`] = `
|
|||
minBarHeight={1}
|
||||
paletteService={
|
||||
Object {
|
||||
"get": [Function],
|
||||
"getAll": [Function],
|
||||
"get": [MockFunction],
|
||||
"getAll": [MockFunction],
|
||||
}
|
||||
}
|
||||
shouldShowValueLabels={true}
|
||||
|
@ -3302,8 +3302,8 @@ exports[`XYChart component it renders bar 1`] = `
|
|||
minBarHeight={1}
|
||||
paletteService={
|
||||
Object {
|
||||
"get": [Function],
|
||||
"getAll": [Function],
|
||||
"get": [MockFunction],
|
||||
"getAll": [MockFunction],
|
||||
}
|
||||
}
|
||||
shouldShowValueLabels={true}
|
||||
|
@ -4857,8 +4857,8 @@ exports[`XYChart component it renders horizontal bar 1`] = `
|
|||
minBarHeight={1}
|
||||
paletteService={
|
||||
Object {
|
||||
"get": [Function],
|
||||
"getAll": [Function],
|
||||
"get": [MockFunction],
|
||||
"getAll": [MockFunction],
|
||||
}
|
||||
}
|
||||
shouldShowValueLabels={true}
|
||||
|
@ -6412,8 +6412,8 @@ exports[`XYChart component it renders line 1`] = `
|
|||
minBarHeight={1}
|
||||
paletteService={
|
||||
Object {
|
||||
"get": [Function],
|
||||
"getAll": [Function],
|
||||
"get": [MockFunction],
|
||||
"getAll": [MockFunction],
|
||||
}
|
||||
}
|
||||
shouldShowValueLabels={true}
|
||||
|
@ -7967,8 +7967,8 @@ exports[`XYChart component it renders stacked area 1`] = `
|
|||
minBarHeight={1}
|
||||
paletteService={
|
||||
Object {
|
||||
"get": [Function],
|
||||
"getAll": [Function],
|
||||
"get": [MockFunction],
|
||||
"getAll": [MockFunction],
|
||||
}
|
||||
}
|
||||
shouldShowValueLabels={true}
|
||||
|
@ -9522,8 +9522,8 @@ exports[`XYChart component it renders stacked bar 1`] = `
|
|||
minBarHeight={1}
|
||||
paletteService={
|
||||
Object {
|
||||
"get": [Function],
|
||||
"getAll": [Function],
|
||||
"get": [MockFunction],
|
||||
"getAll": [MockFunction],
|
||||
}
|
||||
}
|
||||
shouldShowValueLabels={true}
|
||||
|
@ -11077,8 +11077,8 @@ exports[`XYChart component it renders stacked horizontal bar 1`] = `
|
|||
minBarHeight={1}
|
||||
paletteService={
|
||||
Object {
|
||||
"get": [Function],
|
||||
"getAll": [Function],
|
||||
"get": [MockFunction],
|
||||
"getAll": [MockFunction],
|
||||
}
|
||||
}
|
||||
shouldShowValueLabels={true}
|
||||
|
@ -12858,8 +12858,8 @@ exports[`XYChart component split chart should render split chart if both, splitR
|
|||
minBarHeight={1}
|
||||
paletteService={
|
||||
Object {
|
||||
"get": [Function],
|
||||
"getAll": [Function],
|
||||
"get": [MockFunction],
|
||||
"getAll": [MockFunction],
|
||||
}
|
||||
}
|
||||
shouldShowValueLabels={true}
|
||||
|
@ -14646,8 +14646,8 @@ exports[`XYChart component split chart should render split chart if splitColumnA
|
|||
minBarHeight={1}
|
||||
paletteService={
|
||||
Object {
|
||||
"get": [Function],
|
||||
"getAll": [Function],
|
||||
"get": [MockFunction],
|
||||
"getAll": [MockFunction],
|
||||
}
|
||||
}
|
||||
shouldShowValueLabels={true}
|
||||
|
@ -16432,8 +16432,8 @@ exports[`XYChart component split chart should render split chart if splitRowAcce
|
|||
minBarHeight={1}
|
||||
paletteService={
|
||||
Object {
|
||||
"get": [Function],
|
||||
"getAll": [Function],
|
||||
"get": [MockFunction],
|
||||
"getAll": [MockFunction],
|
||||
}
|
||||
}
|
||||
shouldShowValueLabels={true}
|
||||
|
|
|
@ -48,8 +48,8 @@ interface Props {
|
|||
endValue?: EndValue | undefined;
|
||||
paletteService: PaletteRegistry;
|
||||
formattedDatatables: DatatablesWithFormatInfo;
|
||||
syncColors?: boolean;
|
||||
timeZone?: string;
|
||||
syncColors: boolean;
|
||||
timeZone: string;
|
||||
emphasizeFitting?: boolean;
|
||||
fillOpacity?: number;
|
||||
minBarHeight: number;
|
||||
|
|
|
@ -98,7 +98,6 @@ export const getAllSeries = (
|
|||
/**
|
||||
* This function joins every data series name available on each layer by the same color palette.
|
||||
* The returned function `getRank` should return the position of a series name in this unified list by palette.
|
||||
*
|
||||
*/
|
||||
export function getColorAssignments(
|
||||
layers: CommonXYLayerConfig[],
|
||||
|
|
|
@ -28,7 +28,7 @@ import {
|
|||
getPalette,
|
||||
AVAILABLE_PALETTES,
|
||||
NeutralPalette,
|
||||
SPECIAL_TOKENS_STRING_CONVERTION,
|
||||
SPECIAL_TOKENS_STRING_CONVERSION,
|
||||
} from '@kbn/coloring';
|
||||
import { getColorCategories } from '@kbn/chart-expressions-common';
|
||||
import { isDataLayer } from '../../common/utils/layer_types_guards';
|
||||
|
@ -53,10 +53,10 @@ type GetSeriesPropsFn = (config: {
|
|||
colorAssignments: ColorAssignments;
|
||||
columnToLabelMap: Record<string, string>;
|
||||
paletteService: PaletteRegistry;
|
||||
syncColors?: boolean;
|
||||
yAxis?: GroupsConfiguration[number];
|
||||
xAxis?: GroupsConfiguration[number];
|
||||
timeZone?: string;
|
||||
syncColors: boolean;
|
||||
timeZone: string;
|
||||
emphasizeFitting?: boolean;
|
||||
fillOpacity?: number;
|
||||
formattedDatatableInfo: DatatableWithFormatInfo;
|
||||
|
@ -324,7 +324,7 @@ const getColor: GetColorFn = (
|
|||
series,
|
||||
{ layer, colorAssignments, paletteService, syncColors, getSeriesNameFn },
|
||||
uiState,
|
||||
singleTable
|
||||
isSingleTable
|
||||
) => {
|
||||
const overwriteColor = getSeriesColor(layer, series.yAccessor as string);
|
||||
if (overwriteColor !== null) {
|
||||
|
@ -333,7 +333,7 @@ const getColor: GetColorFn = (
|
|||
|
||||
const name = getSeriesNameFn(series)?.toString() || '';
|
||||
|
||||
const overwriteColors: Record<string, string> = uiState?.get ? uiState.get('vis.colors', {}) : {};
|
||||
const overwriteColors: Record<string, string> = uiState?.get?.('vis.colors', {}) ?? {};
|
||||
|
||||
if (Object.keys(overwriteColors).includes(name)) {
|
||||
return overwriteColors[name];
|
||||
|
@ -344,7 +344,7 @@ const getColor: GetColorFn = (
|
|||
{
|
||||
name,
|
||||
totalSeriesAtDepth: colorAssignment.totalSeriesCount,
|
||||
rankAtDepth: colorAssignment.getRank(singleTable ? 'commonLayerId' : layer.layerId, name),
|
||||
rankAtDepth: colorAssignment.getRank(isSingleTable ? 'commonLayerId' : layer.layerId, name),
|
||||
},
|
||||
];
|
||||
return paletteService.get(layer.palette.name).getCategoricalColor(
|
||||
|
@ -493,7 +493,7 @@ export const getSeriesProps: GetSeriesPropsFn = ({
|
|||
// if colorMapping exist then we can apply it, if not let's use the legacy coloring method
|
||||
layer.colorMapping && splitColumnIds.length > 0
|
||||
? getColorSeriesAccessorFn(
|
||||
JSON.parse(layer.colorMapping), // the color mapping is at this point just a strinfigied JSON
|
||||
JSON.parse(layer.colorMapping), // the color mapping is at this point just a stringified JSON
|
||||
getPalette(AVAILABLE_PALETTES, NeutralPalette),
|
||||
isDarkMode,
|
||||
{
|
||||
|
@ -501,7 +501,7 @@ export const getSeriesProps: GetSeriesPropsFn = ({
|
|||
categories: getColorCategories(table.rows, splitColumnIds[0]),
|
||||
},
|
||||
splitColumnIds[0],
|
||||
SPECIAL_TOKENS_STRING_CONVERTION
|
||||
SPECIAL_TOKENS_STRING_CONVERSION
|
||||
)
|
||||
: (series) =>
|
||||
getColor(
|
||||
|
|
|
@ -74,9 +74,10 @@ export const getPaletteRegistry = () => {
|
|||
};
|
||||
|
||||
return {
|
||||
get: (name: string) =>
|
||||
name === 'custom' ? mockPalette3 : name !== 'default' ? mockPalette2 : mockPalette1,
|
||||
getAll: () => [mockPalette1, mockPalette2, mockPalette3],
|
||||
get: jest.fn((name: string) =>
|
||||
name === 'custom' ? mockPalette3 : name !== 'default' ? mockPalette2 : mockPalette1
|
||||
),
|
||||
getAll: jest.fn(() => [mockPalette1, mockPalette2, mockPalette3]),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -24,6 +24,8 @@ import {
|
|||
EuiTitle,
|
||||
EuiCallOut,
|
||||
EuiSwitch,
|
||||
EuiText,
|
||||
EuiIconTip,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { DashboardContainerInput } from '../../../../common';
|
||||
|
@ -269,12 +271,47 @@ export const DashboardSettings = ({ onClose }: DashboardSettingsProps) => {
|
|||
<>
|
||||
<EuiFormRow>
|
||||
<EuiSwitch
|
||||
label={i18n.translate(
|
||||
'dashboard.embeddableApi.showSettings.flyout.form.syncColorsBetweenPanelsSwitchLabel',
|
||||
{
|
||||
defaultMessage: 'Sync color palettes across panels',
|
||||
}
|
||||
)}
|
||||
label={
|
||||
<EuiText size="s">
|
||||
{i18n.translate(
|
||||
'dashboard.embeddableApi.showSettings.flyout.form.syncColorsBetweenPanelsSwitchLabel',
|
||||
{
|
||||
defaultMessage: 'Sync color palettes across panels',
|
||||
}
|
||||
)}{' '}
|
||||
<EuiIconTip
|
||||
color="subdued"
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="dashboard.embeddableApi.showSettings.flyout.form.syncColorsBetweenPanelsSwitchHelp"
|
||||
defaultMessage="Only valid for {default} and {compatibility} palettes"
|
||||
values={{
|
||||
default: (
|
||||
<strong>
|
||||
{i18n.translate('dashboard.palettes.defaultPaletteLabel', {
|
||||
defaultMessage: 'Default',
|
||||
})}
|
||||
</strong>
|
||||
),
|
||||
compatibility: (
|
||||
<strong>
|
||||
{i18n.translate('dashboard.palettes.kibanaPaletteLabel', {
|
||||
defaultMessage: 'Compatibility',
|
||||
})}
|
||||
</strong>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
iconProps={{
|
||||
className: 'eui-alignTop',
|
||||
}}
|
||||
position="top"
|
||||
size="s"
|
||||
type="questionInCircle"
|
||||
/>
|
||||
</EuiText>
|
||||
}
|
||||
checked={dashboardSettingsState.syncColors}
|
||||
onChange={(event) => updateDashboardSetting({ syncColors: event.target.checked })}
|
||||
data-test-subj="dashboardSyncColorsCheckbox"
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
.ffString__emptyValue {
|
||||
color: $euiColorDarkShade;
|
||||
}
|
||||
|
||||
.lnsTableCell--colored .ffString__emptyValue {
|
||||
color: unset;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import type { ExecutionContext } from '@kbn/expressions-plugin/common';
|
||||
import type { FormatFactory, RowHeightMode } from '../../types';
|
||||
import type { ColumnConfigArg } from './datatable_column';
|
||||
import type { DatatableColumnResult } from './datatable_column';
|
||||
import type { DatatableExpressionFunction } from './types';
|
||||
|
||||
export interface SortingState {
|
||||
|
@ -24,7 +24,7 @@ export interface PagingState {
|
|||
export interface DatatableArgs {
|
||||
title: string;
|
||||
description?: string;
|
||||
columns: ColumnConfigArg[];
|
||||
columns: DatatableColumnResult[];
|
||||
sortingColumnId: SortingState['columnId'];
|
||||
sortingDirection: SortingState['direction'];
|
||||
fitRowToContent?: boolean;
|
||||
|
|
|
@ -6,23 +6,25 @@
|
|||
*/
|
||||
|
||||
import type { Direction } from '@elastic/eui';
|
||||
import type { PaletteOutput, CustomPaletteParams } from '@kbn/coloring';
|
||||
import type { PaletteOutput, CustomPaletteParams, ColorMapping } from '@kbn/coloring';
|
||||
import type { CustomPaletteState } from '@kbn/charts-plugin/common';
|
||||
import type { ExpressionFunctionDefinition, DatatableColumn } from '@kbn/expressions-plugin/common';
|
||||
import type { SortingHint } from '../../types';
|
||||
import { CollapseFunction } from '../collapse';
|
||||
|
||||
const LENS_DATATABLE_COLUMN = 'lens_datatable_column';
|
||||
|
||||
export type LensGridDirection = 'none' | Direction;
|
||||
|
||||
export interface ColumnConfig {
|
||||
columns: ColumnConfigArg[];
|
||||
export interface DatatableColumnConfig {
|
||||
columns: DatatableColumnResult[];
|
||||
sortingColumnId: string | undefined;
|
||||
sortingDirection: LensGridDirection;
|
||||
}
|
||||
|
||||
export type ColumnConfigArg = Omit<ColumnState, 'palette'> & {
|
||||
type: 'lens_datatable_column';
|
||||
export type DatatableColumnArgs = Omit<ColumnState, 'palette' | 'colorMapping'> & {
|
||||
palette?: PaletteOutput<CustomPaletteState>;
|
||||
colorMapping?: string;
|
||||
summaryRowValue?: unknown;
|
||||
sortingHint?: SortingHint;
|
||||
};
|
||||
|
@ -41,6 +43,7 @@ export interface ColumnState {
|
|||
bucketValues?: Array<{ originalBucketColumn: DatatableColumn; value: unknown }>;
|
||||
alignment?: 'left' | 'right' | 'center';
|
||||
palette?: PaletteOutput<CustomPaletteParams>;
|
||||
colorMapping?: ColorMapping.Config;
|
||||
colorMode?: 'none' | 'cell' | 'text';
|
||||
summaryRow?: 'none' | 'sum' | 'avg' | 'count' | 'min' | 'max';
|
||||
summaryLabel?: string;
|
||||
|
@ -48,18 +51,21 @@ export interface ColumnState {
|
|||
isMetric?: boolean;
|
||||
}
|
||||
|
||||
export type DatatableColumnResult = ColumnState & { type: 'lens_datatable_column' };
|
||||
export type DatatableColumnFunction = ExpressionFunctionDefinition<
|
||||
'lens_datatable_column',
|
||||
export type DatatableColumnResult = DatatableColumnArgs & {
|
||||
type: typeof LENS_DATATABLE_COLUMN;
|
||||
};
|
||||
|
||||
export type DatatableColumnFn = ExpressionFunctionDefinition<
|
||||
typeof LENS_DATATABLE_COLUMN,
|
||||
null,
|
||||
ColumnState & { sortingHint?: SortingHint },
|
||||
DatatableColumnArgs,
|
||||
DatatableColumnResult
|
||||
>;
|
||||
|
||||
export const datatableColumn: DatatableColumnFunction = {
|
||||
name: 'lens_datatable_column',
|
||||
export const datatableColumn: DatatableColumnFn = {
|
||||
name: LENS_DATATABLE_COLUMN,
|
||||
aliases: [],
|
||||
type: 'lens_datatable_column',
|
||||
type: LENS_DATATABLE_COLUMN,
|
||||
help: '',
|
||||
inputTypes: ['null'],
|
||||
args: {
|
||||
|
@ -76,12 +82,16 @@ export const datatableColumn: DatatableColumnFunction = {
|
|||
types: ['palette'],
|
||||
help: '',
|
||||
},
|
||||
colorMapping: {
|
||||
types: ['string'],
|
||||
help: '',
|
||||
},
|
||||
summaryRow: { types: ['string'], help: '' },
|
||||
summaryLabel: { types: ['string'], help: '' },
|
||||
},
|
||||
fn: function fn(input: unknown, args: ColumnState) {
|
||||
fn: function fn(input, args) {
|
||||
return {
|
||||
type: 'lens_datatable_column',
|
||||
type: LENS_DATATABLE_COLUMN,
|
||||
...args,
|
||||
};
|
||||
},
|
||||
|
|
|
@ -72,6 +72,7 @@ export const datatableFn =
|
|||
value: {
|
||||
data: table,
|
||||
untransposedData,
|
||||
syncColors: context.isSyncColorsEnabled?.() ?? false,
|
||||
args: {
|
||||
...args,
|
||||
title: (context.variables.embeddableTitle as string) ?? args.title,
|
||||
|
|
|
@ -7,5 +7,6 @@
|
|||
|
||||
export * from './datatable_column';
|
||||
export * from './datatable';
|
||||
export { isTransposeId, getOriginalId } from './transpose_helpers';
|
||||
|
||||
export type { DatatableProps, DatatableExpressionFunction } from './types';
|
||||
|
|
|
@ -72,7 +72,7 @@ describe('Summary row helpers', () => {
|
|||
it(`should return formatted value for a ${op} summary function`, () => {
|
||||
expect(
|
||||
computeSummaryRowForColumn(
|
||||
{ summaryRow: op, columnId: 'myColumn', type: 'lens_datatable_column' },
|
||||
{ summaryRow: op, columnId: 'myColumn' },
|
||||
mockNumericTable,
|
||||
{
|
||||
myColumn: customNumericFormatter,
|
||||
|
@ -86,7 +86,7 @@ describe('Summary row helpers', () => {
|
|||
it('should ignore the column formatter, rather return the raw value for count operation', () => {
|
||||
expect(
|
||||
computeSummaryRowForColumn(
|
||||
{ summaryRow: 'count', columnId: 'myColumn', type: 'lens_datatable_column' },
|
||||
{ summaryRow: 'count', columnId: 'myColumn' },
|
||||
mockNumericTable,
|
||||
{
|
||||
myColumn: customNumericFormatter,
|
||||
|
@ -99,7 +99,7 @@ describe('Summary row helpers', () => {
|
|||
it('should only count non-null/empty values', () => {
|
||||
expect(
|
||||
computeSummaryRowForColumn(
|
||||
{ summaryRow: 'count', columnId: 'myColumn', type: 'lens_datatable_column' },
|
||||
{ summaryRow: 'count', columnId: 'myColumn' },
|
||||
{ ...mockNumericTable, rows: [...mockNumericTable.rows, { myColumn: null }] },
|
||||
{
|
||||
myColumn: customNumericFormatter,
|
||||
|
@ -112,7 +112,7 @@ describe('Summary row helpers', () => {
|
|||
it('should count numeric arrays as valid and distinct values', () => {
|
||||
expect(
|
||||
computeSummaryRowForColumn(
|
||||
{ summaryRow: 'count', columnId: 'myColumn', type: 'lens_datatable_column' },
|
||||
{ summaryRow: 'count', columnId: 'myColumn' },
|
||||
mockNumericTableWithArray,
|
||||
{
|
||||
myColumn: defaultFormatter,
|
||||
|
|
|
@ -8,15 +8,15 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import type { FieldFormat } from '@kbn/field-formats-plugin/common';
|
||||
import type { Datatable } from '@kbn/expressions-plugin/common';
|
||||
import { ColumnConfigArg } from './datatable_column';
|
||||
import { DatatableColumnArgs } from './datatable_column';
|
||||
import { getOriginalId } from './transpose_helpers';
|
||||
import { isNumericFieldForDatatable } from './utils';
|
||||
|
||||
type SummaryRowType = Extract<ColumnConfigArg['summaryRow'], string>;
|
||||
type SummaryRowType = Extract<DatatableColumnArgs['summaryRow'], string>;
|
||||
|
||||
export function getFinalSummaryConfiguration(
|
||||
columnId: string,
|
||||
columnArgs: Pick<ColumnConfigArg, 'summaryRow' | 'summaryLabel'> | undefined,
|
||||
columnArgs: Pick<DatatableColumnArgs, 'summaryRow' | 'summaryLabel'> | undefined,
|
||||
table: Datatable | undefined
|
||||
) {
|
||||
const isNumeric = isNumericFieldForDatatable(table, columnId);
|
||||
|
@ -87,13 +87,13 @@ export function getSummaryRowOptions(): Array<{
|
|||
|
||||
/** @internal **/
|
||||
export function computeSummaryRowForColumn(
|
||||
columnArgs: ColumnConfigArg,
|
||||
columnArgs: DatatableColumnArgs,
|
||||
table: Datatable,
|
||||
formatters: Record<string, FieldFormat>,
|
||||
defaultFormatter: FieldFormat
|
||||
) {
|
||||
const summaryValue = computeFinalValue(columnArgs.summaryRow, columnArgs.columnId, table.rows);
|
||||
// ignore the coluymn formatter for the count case
|
||||
// ignore the column formatter for the count case
|
||||
if (columnArgs.summaryRow === 'count') {
|
||||
return defaultFormatter.convert(summaryValue);
|
||||
}
|
||||
|
@ -101,7 +101,7 @@ export function computeSummaryRowForColumn(
|
|||
}
|
||||
|
||||
function computeFinalValue(
|
||||
type: ColumnConfigArg['summaryRow'],
|
||||
type: DatatableColumnArgs['summaryRow'],
|
||||
columnId: string,
|
||||
rows: Datatable['rows']
|
||||
) {
|
||||
|
|
|
@ -8,16 +8,20 @@
|
|||
import type { Datatable, DatatableColumn, DatatableRow } from '@kbn/expressions-plugin/common';
|
||||
import type { FieldFormat } from '@kbn/field-formats-plugin/common';
|
||||
import type { DatatableArgs } from './datatable';
|
||||
import type { ColumnConfig, ColumnConfigArg } from './datatable_column';
|
||||
import type { DatatableColumnConfig, DatatableColumnArgs } from './datatable_column';
|
||||
|
||||
const TRANSPOSE_SEPARATOR = '---';
|
||||
|
||||
const TRANSPOSE_VISUAL_SEPARATOR = '›';
|
||||
|
||||
function getTransposeId(value: string, columnId: string) {
|
||||
export function getTransposeId(value: string, columnId: string) {
|
||||
return `${value}${TRANSPOSE_SEPARATOR}${columnId}`;
|
||||
}
|
||||
|
||||
export function isTransposeId(id: string): boolean {
|
||||
return id.split(TRANSPOSE_SEPARATOR).length > 1;
|
||||
}
|
||||
|
||||
export function getOriginalId(id: string) {
|
||||
if (id.includes(TRANSPOSE_SEPARATOR)) {
|
||||
const idParts = id.split(TRANSPOSE_SEPARATOR);
|
||||
|
@ -87,11 +91,11 @@ export function transposeTable(
|
|||
|
||||
function transposeRows(
|
||||
firstTable: Datatable,
|
||||
bucketsColumnArgs: ColumnConfigArg[],
|
||||
bucketsColumnArgs: DatatableColumnArgs[],
|
||||
formatters: Record<string, FieldFormat>,
|
||||
transposedColumnFormatter: FieldFormat,
|
||||
transposedColumnId: string,
|
||||
metricsColumnArgs: ColumnConfigArg[]
|
||||
metricsColumnArgs: DatatableColumnArgs[]
|
||||
) {
|
||||
const rowsByBucketColumns: Record<string, DatatableRow[]> = groupRowsByBucketColumns(
|
||||
firstTable,
|
||||
|
@ -113,8 +117,8 @@ function transposeRows(
|
|||
*/
|
||||
function updateColumnArgs(
|
||||
args: DatatableArgs,
|
||||
bucketsColumnArgs: ColumnConfig['columns'],
|
||||
transposedColumnGroups: Array<ColumnConfig['columns']>
|
||||
bucketsColumnArgs: DatatableColumnConfig['columns'],
|
||||
transposedColumnGroups: Array<DatatableColumnConfig['columns']>
|
||||
) {
|
||||
args.columns = [...bucketsColumnArgs];
|
||||
// add first column from each group, then add second column for each group, ...
|
||||
|
@ -151,8 +155,8 @@ function getUniqueValues(table: Datatable, formatter: FieldFormat, columnId: str
|
|||
*/
|
||||
function transposeColumns(
|
||||
args: DatatableArgs,
|
||||
bucketsColumnArgs: ColumnConfig['columns'],
|
||||
metricColumns: ColumnConfig['columns'],
|
||||
bucketsColumnArgs: DatatableColumnConfig['columns'],
|
||||
metricColumns: DatatableColumnConfig['columns'],
|
||||
firstTable: Datatable,
|
||||
uniqueValues: string[],
|
||||
uniqueRawValues: unknown[],
|
||||
|
@ -196,10 +200,10 @@ function transposeColumns(
|
|||
*/
|
||||
function mergeRowGroups(
|
||||
rowsByBucketColumns: Record<string, DatatableRow[]>,
|
||||
bucketColumns: ColumnConfigArg[],
|
||||
bucketColumns: DatatableColumnArgs[],
|
||||
formatter: FieldFormat,
|
||||
transposedColumnId: string,
|
||||
metricColumns: ColumnConfigArg[]
|
||||
metricColumns: DatatableColumnArgs[]
|
||||
) {
|
||||
return Object.values(rowsByBucketColumns).map((rows) => {
|
||||
const mergedRow: DatatableRow = {};
|
||||
|
@ -222,7 +226,7 @@ function mergeRowGroups(
|
|||
*/
|
||||
function groupRowsByBucketColumns(
|
||||
firstTable: Datatable,
|
||||
bucketColumns: ColumnConfigArg[],
|
||||
bucketColumns: DatatableColumnArgs[],
|
||||
formatters: Record<string, FieldFormat>
|
||||
) {
|
||||
const rowsByBucketColumns: Record<string, DatatableRow[]> = {};
|
||||
|
|
|
@ -10,6 +10,7 @@ import type { DatatableArgs } from './datatable';
|
|||
|
||||
export interface DatatableProps {
|
||||
data: Datatable;
|
||||
syncColors: boolean;
|
||||
untransposedData?: Datatable;
|
||||
args: DatatableArgs;
|
||||
}
|
||||
|
|
|
@ -8,10 +8,11 @@
|
|||
import type { Datatable } from '@kbn/expressions-plugin/common';
|
||||
import { getOriginalId } from './transpose_helpers';
|
||||
|
||||
export function isNumericFieldForDatatable(currentData: Datatable | undefined, accessor: string) {
|
||||
const column = currentData?.columns.find(
|
||||
(col) => col.id === accessor || getOriginalId(col.id) === accessor
|
||||
);
|
||||
|
||||
return column?.meta.type === 'number';
|
||||
export function isNumericFieldForDatatable(table: Datatable | undefined, accessor: string) {
|
||||
return getFieldTypeFromDatatable(table, accessor) === 'number';
|
||||
}
|
||||
|
||||
export function getFieldTypeFromDatatable(table: Datatable | undefined, accessor: string) {
|
||||
return table?.columns.find((col) => col.id === accessor || getOriginalId(col.id) === accessor)
|
||||
?.meta.type;
|
||||
}
|
||||
|
|
|
@ -499,7 +499,7 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
position="left"
|
||||
size="s"
|
||||
type="dot"
|
||||
color="warning"
|
||||
color={euiTheme.colors.warning}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { getColorAccessorFn } from './color_mapping_accessor';
|
||||
|
||||
jest.mock('@kbn/coloring', () => ({
|
||||
...jest.requireActual('@kbn/coloring'),
|
||||
getColorFactory: jest
|
||||
.fn()
|
||||
.mockReturnValue((v: string | number) => (v === '123' ? 'blue' : 'red')),
|
||||
}));
|
||||
|
||||
describe('getColorAccessorFn', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const getColorAccessor = getColorAccessorFn('{}', {} as any, false);
|
||||
|
||||
it('should return null for null values', () => {
|
||||
expect(getColorAccessor(null)).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null for undefined values', () => {
|
||||
expect(getColorAccessor(undefined)).toBe(null);
|
||||
});
|
||||
|
||||
it('should return stringified value for numbers', () => {
|
||||
expect(getColorAccessor(123)).toBe('blue');
|
||||
});
|
||||
|
||||
it('should return color for string values', () => {
|
||||
expect(getColorAccessor('testing')).toBe('red');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 {
|
||||
AVAILABLE_PALETTES,
|
||||
getColorFactory,
|
||||
getPalette,
|
||||
NeutralPalette,
|
||||
ColorMappingInputCategoricalData,
|
||||
} from '@kbn/coloring';
|
||||
import { CellColorFn } from './get_cell_color_fn';
|
||||
|
||||
/**
|
||||
* Return a color accessor function for XY charts depending on the split accessors received.
|
||||
*/
|
||||
export function getColorAccessorFn(
|
||||
colorMapping: string,
|
||||
data: ColorMappingInputCategoricalData,
|
||||
isDarkMode: boolean
|
||||
): CellColorFn {
|
||||
const getColor = getColorFactory(
|
||||
JSON.parse(colorMapping),
|
||||
getPalette(AVAILABLE_PALETTES, NeutralPalette),
|
||||
isDarkMode,
|
||||
data
|
||||
);
|
||||
|
||||
return (value) => {
|
||||
if (value === undefined || value === null) return null;
|
||||
|
||||
return getColor(String(value));
|
||||
};
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* 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 React, { MutableRefObject, useState } from 'react';
|
||||
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
ColorMapping,
|
||||
DEFAULT_COLOR_MAPPING_CONFIG,
|
||||
CategoricalColorMapping,
|
||||
SPECIAL_TOKENS_STRING_CONVERSION,
|
||||
AVAILABLE_PALETTES,
|
||||
PaletteOutput,
|
||||
PaletteRegistry,
|
||||
CustomPaletteParams,
|
||||
} from '@kbn/coloring';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { trackUiCounterEvents } from '../../lens_ui_telemetry';
|
||||
import { PalettePicker } from '../palette_picker';
|
||||
import { PalettePanelContainer } from './palette_panel_container';
|
||||
import { getColorStops } from './utils';
|
||||
|
||||
interface ColorMappingByTermsProps {
|
||||
isDarkMode: boolean;
|
||||
colorMapping?: ColorMapping.Config;
|
||||
palette?: PaletteOutput<CustomPaletteParams>;
|
||||
isInlineEditing?: boolean;
|
||||
setPalette: (palette: PaletteOutput) => void;
|
||||
setColorMapping: (colorMapping?: ColorMapping.Config) => void;
|
||||
paletteService: PaletteRegistry;
|
||||
panelRef: MutableRefObject<HTMLDivElement | null>;
|
||||
categories: Array<string | string[]>;
|
||||
}
|
||||
|
||||
export function ColorMappingByTerms({
|
||||
isDarkMode,
|
||||
colorMapping,
|
||||
palette,
|
||||
isInlineEditing,
|
||||
setPalette,
|
||||
setColorMapping,
|
||||
paletteService,
|
||||
panelRef,
|
||||
categories,
|
||||
}: ColorMappingByTermsProps) {
|
||||
const [useNewColorMapping, setUseNewColorMapping] = useState(Boolean(colorMapping));
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
label={i18n.translate('xpack.lens.colorMapping.editColorMappingSectionLabel', {
|
||||
defaultMessage: 'Color mapping',
|
||||
})}
|
||||
style={{ alignItems: 'center' }}
|
||||
fullWidth
|
||||
>
|
||||
<PalettePanelContainer
|
||||
palette={getColorStops(paletteService, isDarkMode, palette, colorMapping)}
|
||||
siblingRef={panelRef}
|
||||
title={
|
||||
useNewColorMapping
|
||||
? i18n.translate('xpack.lens.colorMapping.editColorMappingTitle', {
|
||||
defaultMessage: 'Assign colors to terms',
|
||||
})
|
||||
: i18n.translate('xpack.lens.colorMapping.editColorsTitle', {
|
||||
defaultMessage: 'Edit colors',
|
||||
})
|
||||
}
|
||||
isInlineEditing={isInlineEditing}
|
||||
>
|
||||
<div
|
||||
data-test-subj="lns-palettePanel-terms"
|
||||
className="lnsPalettePanel__section lnsPalettePanel__section--shaded lnsIndexPatternDimensionEditor--padded"
|
||||
>
|
||||
<EuiFlexGroup direction="column" gutterSize="s" justifyContent="flexStart">
|
||||
<EuiFlexItem>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<EuiText size="xs">
|
||||
<span>
|
||||
{i18n.translate('xpack.lens.colorMapping.tryLabel', {
|
||||
defaultMessage: 'Use the new Color Mapping feature',
|
||||
})}{' '}
|
||||
<EuiBadge color="hollow">
|
||||
{i18n.translate('xpack.lens.colorMapping.techPreviewLabel', {
|
||||
defaultMessage: 'Tech preview',
|
||||
})}
|
||||
</EuiBadge>
|
||||
</span>
|
||||
</EuiText>
|
||||
}
|
||||
data-test-subj="lns_colorMappingOrLegacyPalette_switch"
|
||||
compressed
|
||||
checked={useNewColorMapping}
|
||||
onChange={({ target: { checked } }) => {
|
||||
trackUiCounterEvents(`color_mapping_switch_${checked ? 'enabled' : 'disabled'}`);
|
||||
setColorMapping(checked ? { ...DEFAULT_COLOR_MAPPING_CONFIG } : undefined);
|
||||
setUseNewColorMapping(checked);
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{useNewColorMapping ? (
|
||||
<CategoricalColorMapping
|
||||
isDarkMode={isDarkMode}
|
||||
model={colorMapping ?? { ...DEFAULT_COLOR_MAPPING_CONFIG }}
|
||||
onModelUpdate={setColorMapping}
|
||||
specialTokens={SPECIAL_TOKENS_STRING_CONVERSION}
|
||||
palettes={AVAILABLE_PALETTES}
|
||||
data={{
|
||||
type: 'categories',
|
||||
categories,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<PalettePicker
|
||||
palettes={paletteService}
|
||||
activePalette={palette}
|
||||
setPalette={setPalette}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</PalettePanelContainer>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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 React, { MutableRefObject } from 'react';
|
||||
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import {
|
||||
PaletteOutput,
|
||||
PaletteRegistry,
|
||||
CustomizablePalette,
|
||||
DataBounds,
|
||||
CustomPaletteParams,
|
||||
} from '@kbn/coloring';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { PalettePanelContainer } from './palette_panel_container';
|
||||
|
||||
interface ColorMappingByValuesProps {
|
||||
palette: PaletteOutput<CustomPaletteParams>;
|
||||
isInlineEditing?: boolean;
|
||||
setPalette: (palette: PaletteOutput<CustomPaletteParams>) => void;
|
||||
paletteService: PaletteRegistry;
|
||||
panelRef: MutableRefObject<HTMLDivElement | null>;
|
||||
dataBounds?: DataBounds;
|
||||
}
|
||||
|
||||
export function ColorMappingByValues<T>({
|
||||
palette,
|
||||
isInlineEditing,
|
||||
setPalette,
|
||||
paletteService,
|
||||
panelRef,
|
||||
dataBounds,
|
||||
}: ColorMappingByValuesProps) {
|
||||
const colors = palette.params?.stops?.map(({ color }) => color) ?? [];
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
label={i18n.translate('xpack.lens.colorMapping.editColorMappingSectionLabel', {
|
||||
defaultMessage: 'Color mapping',
|
||||
})}
|
||||
style={{ alignItems: 'center' }}
|
||||
fullWidth
|
||||
>
|
||||
<PalettePanelContainer
|
||||
palette={colors}
|
||||
siblingRef={panelRef}
|
||||
title={i18n.translate('xpack.lens.colorMapping.editColorsTitle', {
|
||||
defaultMessage: 'Edit colors',
|
||||
})}
|
||||
isInlineEditing={isInlineEditing}
|
||||
>
|
||||
<div
|
||||
data-test-subj="lns-palettePanel-values"
|
||||
className="lnsPalettePanel__section lnsPalettePanel__section--shaded lnsIndexPatternDimensionEditor--padded"
|
||||
>
|
||||
<CustomizablePalette
|
||||
palettes={paletteService}
|
||||
dataBounds={dataBounds}
|
||||
activePalette={palette}
|
||||
setPalette={(p) => {
|
||||
setPalette(p);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</PalettePanelContainer>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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 {
|
||||
ColorMappingInputData,
|
||||
PaletteOutput,
|
||||
PaletteRegistry,
|
||||
getSpecialString,
|
||||
} from '@kbn/coloring';
|
||||
import { CustomPaletteState } from '@kbn/charts-plugin/common';
|
||||
import { getColorAccessorFn } from './color_mapping_accessor';
|
||||
|
||||
export type CellColorFn = (value?: number | string | null) => string | null;
|
||||
|
||||
export function getCellColorFn(
|
||||
paletteService: PaletteRegistry,
|
||||
data: ColorMappingInputData,
|
||||
colorByTerms: boolean,
|
||||
isDarkMode: boolean,
|
||||
syncColors: boolean,
|
||||
palette?: PaletteOutput<CustomPaletteState>,
|
||||
colorMapping?: string
|
||||
): CellColorFn {
|
||||
if (!colorByTerms && palette && data.type === 'ranges') {
|
||||
return (value) => {
|
||||
if (value === null || value === undefined || typeof value !== 'number') return null;
|
||||
|
||||
return (
|
||||
paletteService.get(palette.name).getColorForValue?.(value, palette.params, data) ?? null
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
if (colorByTerms && data.type === 'categories') {
|
||||
if (colorMapping) {
|
||||
return getColorAccessorFn(colorMapping, data, isDarkMode);
|
||||
} else if (palette) {
|
||||
return (category) => {
|
||||
if (category === undefined || category === null) return null;
|
||||
|
||||
const strCategory = String(category); // can be a number as a string
|
||||
|
||||
return paletteService.get(palette.name).getCategoricalColor(
|
||||
[
|
||||
{
|
||||
name: getSpecialString(strCategory), // needed to sync special categories (i.e. '')
|
||||
rankAtDepth: Math.max(
|
||||
data.categories.findIndex((v) => v === strCategory),
|
||||
0
|
||||
),
|
||||
totalSeriesAtDepth: data.categories.length || 1,
|
||||
},
|
||||
],
|
||||
{
|
||||
maxDepth: 1,
|
||||
totalSeries: data.categories.length || 1,
|
||||
behindText: false,
|
||||
syncColors,
|
||||
},
|
||||
palette?.params ?? { colors: [] }
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return () => null;
|
||||
}
|
|
@ -6,7 +6,12 @@
|
|||
*/
|
||||
|
||||
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
|
||||
import { applyPaletteParams, findMinMaxByColumnId, getContrastColor } from './utils';
|
||||
import {
|
||||
applyPaletteParams,
|
||||
findMinMaxByColumnId,
|
||||
getContrastColor,
|
||||
shouldColorByTerms,
|
||||
} from './utils';
|
||||
|
||||
describe('applyPaletteParams', () => {
|
||||
const paletteRegistry = chartPluginMock.createPaletteRegistry();
|
||||
|
@ -108,3 +113,17 @@ describe('findMinMaxByColumnId', () => {
|
|||
).toEqual({ b: { min: 2, max: 53 } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldColorByTerms', () => {
|
||||
it('should return true if bucketed regardless of value', () => {
|
||||
expect(shouldColorByTerms('number', true)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if not bucketed and numeric', () => {
|
||||
expect(shouldColorByTerms('number', false)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if not bucketed and non-numeric', () => {
|
||||
expect(shouldColorByTerms('string', false)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,8 +17,40 @@ import {
|
|||
getPaletteStops,
|
||||
CUSTOM_PALETTE,
|
||||
enforceColorContrast,
|
||||
ColorMapping,
|
||||
getColorsFromMapping,
|
||||
DEFAULT_FALLBACK_PALETTE,
|
||||
} from '@kbn/coloring';
|
||||
import { Datatable } from '@kbn/expressions-plugin/common';
|
||||
import { Datatable, DatatableColumnType } from '@kbn/expressions-plugin/common';
|
||||
import { DataType } from '../../types';
|
||||
|
||||
/**
|
||||
* Returns array of colors for provided palette or colorMapping
|
||||
*/
|
||||
export function getColorStops(
|
||||
paletteService: PaletteRegistry,
|
||||
isDarkMode: boolean,
|
||||
palette?: PaletteOutput<CustomPaletteParams>,
|
||||
colorMapping?: ColorMapping.Config
|
||||
): string[] {
|
||||
return colorMapping
|
||||
? getColorsFromMapping(isDarkMode, colorMapping)
|
||||
: palette?.name === CUSTOM_PALETTE
|
||||
? palette?.params?.stops?.map(({ color }) => color) ?? []
|
||||
: paletteService
|
||||
.get(palette?.name || DEFAULT_FALLBACK_PALETTE)
|
||||
.getCategoricalColors(10, palette);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bucketed numerical columns should be treated as categorical
|
||||
*/
|
||||
export function shouldColorByTerms(
|
||||
dataType?: DataType | DatatableColumnType,
|
||||
isBucketed?: boolean
|
||||
) {
|
||||
return isBucketed || dataType !== 'number';
|
||||
}
|
||||
|
||||
export function getContrastColor(
|
||||
color: string,
|
||||
|
@ -37,11 +69,8 @@ export function getContrastColor(
|
|||
return enforceColorContrast(color, backgroundColor) ? lightColor : darkColor;
|
||||
}
|
||||
|
||||
export function getNumericValue(rowValue: number | number[] | undefined) {
|
||||
if (rowValue == null || Array.isArray(rowValue)) {
|
||||
return;
|
||||
}
|
||||
return rowValue;
|
||||
export function getNumericValue(rowValue?: unknown) {
|
||||
return typeof rowValue === 'number' ? rowValue : undefined;
|
||||
}
|
||||
|
||||
export function applyPaletteParams<T extends PaletteOutput<CustomPaletteParams>>(
|
||||
|
|
|
@ -12,17 +12,14 @@ import { EuiColorPalettePicker, EuiColorPalettePickerPaletteProps } from '@elast
|
|||
import { EuiFormRow } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export function PalettePicker({
|
||||
palettes,
|
||||
activePalette,
|
||||
setPalette,
|
||||
}: {
|
||||
interface PalettePickerProps<T> {
|
||||
palettes: PaletteRegistry;
|
||||
activePalette?: PaletteOutput;
|
||||
activePalette?: PaletteOutput<T>;
|
||||
setPalette: (palette: PaletteOutput) => void;
|
||||
}) {
|
||||
const paletteName = getActivePaletteName(activePalette?.name);
|
||||
}
|
||||
|
||||
export function PalettePicker<T>({ palettes, activePalette, setPalette }: PalettePickerProps<T>) {
|
||||
const paletteName = getActivePaletteName(activePalette?.name);
|
||||
const palettesToShow: EuiColorPalettePickerPaletteProps[] = palettes
|
||||
.getAll()
|
||||
.filter(({ internal }) => !internal)
|
||||
|
@ -34,6 +31,7 @@ export function PalettePicker({
|
|||
palette: getCategoricalColors(10, id === paletteName ? activePalette?.params : undefined),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
|
|
|
@ -620,6 +620,7 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
|||
const {
|
||||
datasourceState: syncedDatasourceState,
|
||||
visualizationState: syncedVisualizationState,
|
||||
frame,
|
||||
} = syncLinkedDimensions(
|
||||
currentState,
|
||||
visualizationMap,
|
||||
|
@ -627,7 +628,11 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
|
|||
payload.datasourceId
|
||||
);
|
||||
|
||||
state.visualization.state = syncedVisualizationState;
|
||||
const visualization = visualizationMap[state.visualization.activeId!];
|
||||
|
||||
state.visualization.state =
|
||||
visualization.onDatasourceUpdate?.(syncedVisualizationState, frame) ??
|
||||
syncedVisualizationState;
|
||||
state.datasourceStates[payload.datasourceId].state = syncedDatasourceState;
|
||||
})
|
||||
.addCase(updateVisualizationState, (state, { payload }) => {
|
||||
|
@ -1227,14 +1232,14 @@ function syncLinkedDimensions(
|
|||
const linkedDimensions = activeVisualization.getLinkedDimensions?.(visualizationState);
|
||||
const frame = selectFramePublicAPI({ lens: state }, datasourceMap);
|
||||
|
||||
const getDimensionGroups = (layerId: string) =>
|
||||
activeVisualization.getConfiguration({
|
||||
state: visualizationState,
|
||||
layerId,
|
||||
frame,
|
||||
}).groups;
|
||||
|
||||
if (linkedDimensions) {
|
||||
const getDimensionGroups = (layerId: string) =>
|
||||
activeVisualization.getConfiguration({
|
||||
state: visualizationState,
|
||||
layerId,
|
||||
frame,
|
||||
}).groups;
|
||||
|
||||
const idAssuredLinks = linkedDimensions.map((link) => ({
|
||||
...link,
|
||||
to: { ...link.to, columnId: link.to.columnId ?? generateId() },
|
||||
|
@ -1276,5 +1281,5 @@ function syncLinkedDimensions(
|
|||
});
|
||||
}
|
||||
|
||||
return { datasourceState, visualizationState };
|
||||
return { datasourceState, visualizationState, frame };
|
||||
}
|
||||
|
|
|
@ -1317,6 +1317,8 @@ export interface Visualization<T = unknown, P = T, ExtraAppendLayerArg = unknown
|
|||
*/
|
||||
onEditAction?: (state: T, event: LensEditEvent<LensEditSupportedActions>) => T;
|
||||
|
||||
onDatasourceUpdate?: (state: T, frame?: FramePublicAPI) => T;
|
||||
|
||||
/**
|
||||
* Some visualization track indexPattern changes (i.e. annotations)
|
||||
* This method makes it aware of the change and produces a new updated state
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { createDatatableUtilitiesMock } from '@kbn/data-plugin/common/mocks';
|
||||
import { Datatable } from '@kbn/expressions-plugin/public';
|
||||
import { getUniqueLabelGenerator, inferTimeField, renewIDs } from './utils';
|
||||
import { getUniqueLabelGenerator, inferTimeField, isLensRange, renewIDs } from './utils';
|
||||
|
||||
const datatableUtilities = createDatatableUtilitiesMock();
|
||||
|
||||
|
@ -187,4 +187,24 @@ describe('utils', () => {
|
|||
expect([' ', ' '].map(labelGenerator)).toEqual(['[Untitled]', '[Untitled] [1]']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isRange', () => {
|
||||
it.each<[expected: boolean, input: unknown]>([
|
||||
[true, { from: 0, to: 100, label: '' }],
|
||||
[true, { from: 0, to: null, label: '' }],
|
||||
[true, { from: null, to: 100, label: '' }],
|
||||
[false, { from: 0, to: 100 }],
|
||||
[false, { from: 0, to: null }],
|
||||
[false, { from: null, to: 100 }],
|
||||
[false, { from: 0 }],
|
||||
[false, { to: 100 }],
|
||||
[false, null],
|
||||
[false, undefined],
|
||||
[false, 123],
|
||||
[false, 'string'],
|
||||
[false, {}],
|
||||
])('should return %s for %j', (expected, input) => {
|
||||
expect(isLensRange(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -39,6 +39,7 @@ import {
|
|||
import type { DatasourceStates, VisualizationState } from './state_management';
|
||||
import type { IndexPatternServiceAPI } from './data_views_service/service';
|
||||
import { COLOR_MAPPING_OFF_BY_DEFAULT } from '../common/constants';
|
||||
import type { RangeTypeLens } from './datasources/form_based/operations/definitions/ranges';
|
||||
|
||||
export function getVisualizeGeoFieldMessage(fieldType: string) {
|
||||
return i18n.translate('xpack.lens.visualizeGeoFieldMessage', {
|
||||
|
@ -47,6 +48,17 @@ export function getVisualizeGeoFieldMessage(fieldType: string) {
|
|||
});
|
||||
}
|
||||
|
||||
export const isLensRange = (range: unknown = {}): range is RangeTypeLens => {
|
||||
if (!range || typeof range !== 'object') return false;
|
||||
const { from, to, label } = range as RangeTypeLens;
|
||||
|
||||
return (
|
||||
label !== undefined &&
|
||||
(typeof from === 'number' || from === null) &&
|
||||
(typeof to === 'number' || to === null)
|
||||
);
|
||||
};
|
||||
|
||||
export const getResolvedDateRange = function (timefilter: TimefilterContract) {
|
||||
const { from, to } = timefilter.getTime();
|
||||
return { fromDate: from, toDate: to };
|
||||
|
|
|
@ -10,13 +10,16 @@ import { DataContext } from './table_basic';
|
|||
import { createGridCell } from './cell_value';
|
||||
import type { FieldFormat } from '@kbn/field-formats-plugin/common';
|
||||
import { Datatable } from '@kbn/expressions-plugin/public';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { DatatableArgs, ColumnConfigArg } from '../../../../common/expressions';
|
||||
import { DatatableArgs } from '../../../../common/expressions';
|
||||
import { DataContextType } from './types';
|
||||
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { getTransposeId } from '../../../../common/expressions/datatable/transpose_helpers';
|
||||
|
||||
describe('datatable cell renderer', () => {
|
||||
const innerCellColorFnMock = jest.fn().mockReturnValue('blue');
|
||||
const cellColorFnMock = jest.fn().mockReturnValue(innerCellColorFnMock);
|
||||
const setCellProps = jest.fn();
|
||||
|
||||
const table: Datatable = {
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -30,18 +33,16 @@ describe('datatable cell renderer', () => {
|
|||
],
|
||||
rows: [{ a: 123 }],
|
||||
};
|
||||
const { theme: setUpMockTheme } = coreMock.createSetup();
|
||||
const CellRenderer = createGridCell(
|
||||
{
|
||||
a: { convert: (x) => `formatted ${x}` } as FieldFormat,
|
||||
},
|
||||
{ columns: [], sortingColumnId: '', sortingDirection: 'none' },
|
||||
DataContext,
|
||||
setUpMockTheme
|
||||
false,
|
||||
cellColorFnMock
|
||||
);
|
||||
|
||||
const setCellProps = jest.fn();
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
@ -101,7 +102,8 @@ describe('datatable cell renderer', () => {
|
|||
},
|
||||
{ columns: [], sortingColumnId: '', sortingDirection: 'none' },
|
||||
DataContext,
|
||||
setUpMockTheme,
|
||||
false,
|
||||
cellColorFnMock,
|
||||
true
|
||||
);
|
||||
render(
|
||||
|
@ -137,7 +139,8 @@ describe('datatable cell renderer', () => {
|
|||
sortingDirection: 'none',
|
||||
},
|
||||
DataContext,
|
||||
setUpMockTheme,
|
||||
false,
|
||||
cellColorFnMock,
|
||||
true
|
||||
);
|
||||
render(
|
||||
|
@ -156,9 +159,6 @@ describe('datatable cell renderer', () => {
|
|||
});
|
||||
|
||||
describe('dynamic coloring', () => {
|
||||
const paletteRegistry = chartPluginMock.createPaletteRegistry();
|
||||
const customPalette = paletteRegistry.get('custom');
|
||||
|
||||
function getCellRenderer(columnConfig: DatatableArgs) {
|
||||
return createGridCell(
|
||||
{
|
||||
|
@ -166,7 +166,8 @@ describe('datatable cell renderer', () => {
|
|||
},
|
||||
columnConfig,
|
||||
DataContext,
|
||||
setUpMockTheme
|
||||
false,
|
||||
cellColorFnMock
|
||||
);
|
||||
}
|
||||
function getColumnConfiguration(): DatatableArgs {
|
||||
|
@ -189,7 +190,7 @@ describe('datatable cell renderer', () => {
|
|||
},
|
||||
},
|
||||
type: 'lens_datatable_column',
|
||||
} as ColumnConfigArg,
|
||||
},
|
||||
],
|
||||
sortingColumnId: '',
|
||||
sortingDirection: 'none',
|
||||
|
@ -207,7 +208,7 @@ describe('datatable cell renderer', () => {
|
|||
<CellRendererWithPalette
|
||||
rowIndex={0}
|
||||
colIndex={0}
|
||||
columnId="a"
|
||||
columnId={columnConfig.columns[0].columnId}
|
||||
setCellProps={setCellProps}
|
||||
isExpandable={false}
|
||||
isDetails={false}
|
||||
|
@ -216,8 +217,7 @@ describe('datatable cell renderer', () => {
|
|||
{
|
||||
wrapper: DataContextProviderWrapper({
|
||||
table,
|
||||
minMaxByColumnId: { a: { min: 12, max: 155 /* > 123 */ } },
|
||||
getColorForValue: customPalette.getColorForValue,
|
||||
minMaxByColumnId: { a: { min: 12, max: 155 } },
|
||||
...context,
|
||||
}),
|
||||
}
|
||||
|
@ -241,6 +241,27 @@ describe('datatable cell renderer', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should call getCellColor with full columnId of transpose column', () => {
|
||||
const columnId = getTransposeId('test', 'a');
|
||||
const columnConfig = getColumnConfiguration();
|
||||
columnConfig.columns[0].colorMode = 'cell';
|
||||
columnConfig.columns[0].columnId = columnId;
|
||||
|
||||
renderCellComponent(columnConfig, {
|
||||
table: {
|
||||
...table,
|
||||
columns: [
|
||||
{
|
||||
...table.columns[0],
|
||||
id: columnId,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(cellColorFnMock.mock.calls[0][0]).toBe(columnId);
|
||||
});
|
||||
|
||||
it('should set the coloring of the text when enabled', () => {
|
||||
const columnConfig = getColumnConfiguration();
|
||||
columnConfig.columns[0].colorMode = 'text';
|
||||
|
@ -252,14 +273,23 @@ describe('datatable cell renderer', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should not color the cell when the value is an array', () => {
|
||||
it('should not color the cell when color function returns null', () => {
|
||||
setCellProps.mockClear();
|
||||
innerCellColorFnMock.mockReturnValueOnce(null);
|
||||
const columnConfig = getColumnConfiguration();
|
||||
columnConfig.columns[0].colorMode = 'cell';
|
||||
|
||||
renderCellComponent(columnConfig, {
|
||||
table: { ...table, rows: [{ a: [10, 123] }] },
|
||||
});
|
||||
renderCellComponent(columnConfig, {});
|
||||
|
||||
expect(setCellProps).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not color the cell when color function returns empty string', () => {
|
||||
innerCellColorFnMock.mockReturnValueOnce('');
|
||||
const columnConfig = getColumnConfiguration();
|
||||
columnConfig.columns[0].colorMode = 'cell';
|
||||
|
||||
renderCellComponent(columnConfig, {});
|
||||
|
||||
expect(setCellProps).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
@ -6,63 +6,73 @@
|
|||
*/
|
||||
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { EuiDataGridCellValueElementProps, EuiLink } from '@elastic/eui';
|
||||
import type { CoreSetup } from '@kbn/core/public';
|
||||
import classNames from 'classnames';
|
||||
import { PaletteOutput } from '@kbn/coloring';
|
||||
import { CustomPaletteState } from '@kbn/charts-plugin/common';
|
||||
import type { FormatFactory } from '../../../../common/types';
|
||||
import { getOriginalId } from '../../../../common/expressions/datatable/transpose_helpers';
|
||||
import type { ColumnConfig } from '../../../../common/expressions';
|
||||
import type { DatatableColumnConfig } from '../../../../common/expressions';
|
||||
import type { DataContextType } from './types';
|
||||
import { getContrastColor, getNumericValue } from '../../../shared_components/coloring/utils';
|
||||
import { getContrastColor } from '../../../shared_components/coloring/utils';
|
||||
import { CellColorFn } from '../../../shared_components/coloring/get_cell_color_fn';
|
||||
|
||||
import { isLensRange } from '../../../utils';
|
||||
|
||||
const getParsedValue = (v: unknown) => {
|
||||
if (v == null || typeof v === 'number') {
|
||||
return v;
|
||||
}
|
||||
if (isLensRange(v)) {
|
||||
return v.toString();
|
||||
}
|
||||
return String(v);
|
||||
};
|
||||
|
||||
export const createGridCell = (
|
||||
formatters: Record<string, ReturnType<FormatFactory>>,
|
||||
columnConfig: ColumnConfig,
|
||||
columnConfig: DatatableColumnConfig,
|
||||
DataContext: React.Context<DataContextType>,
|
||||
theme: CoreSetup['theme'],
|
||||
isDarkMode: boolean,
|
||||
getCellColor: (
|
||||
originalId: string,
|
||||
palette?: PaletteOutput<CustomPaletteState>,
|
||||
colorMapping?: string
|
||||
) => CellColorFn,
|
||||
fitRowToContent?: boolean
|
||||
) => {
|
||||
return ({ rowIndex, columnId, setCellProps, isExpanded }: EuiDataGridCellValueElementProps) => {
|
||||
const { table, alignments, minMaxByColumnId, getColorForValue, handleFilterClick } =
|
||||
useContext(DataContext);
|
||||
const IS_DARK_THEME: boolean = useObservable(theme.theme$, { darkMode: false }).darkMode;
|
||||
|
||||
const rowValue = table?.rows[rowIndex]?.[columnId];
|
||||
|
||||
const { table, alignments, handleFilterClick } = useContext(DataContext);
|
||||
const rawRowValue = table?.rows[rowIndex]?.[columnId];
|
||||
const rowValue = getParsedValue(rawRowValue);
|
||||
const colIndex = columnConfig.columns.findIndex(({ columnId: id }) => id === columnId);
|
||||
const { colorMode = 'none', palette, oneClickFilter } = columnConfig.columns[colIndex] || {};
|
||||
const {
|
||||
oneClickFilter,
|
||||
colorMode = 'none',
|
||||
palette,
|
||||
colorMapping,
|
||||
} = columnConfig.columns[colIndex] ?? {};
|
||||
const filterOnClick = oneClickFilter && handleFilterClick;
|
||||
|
||||
const content = formatters[columnId]?.convert(rowValue, filterOnClick ? 'text' : 'html');
|
||||
const content = formatters[columnId]?.convert(rawRowValue, filterOnClick ? 'text' : 'html');
|
||||
const currentAlignment = alignments && alignments[columnId];
|
||||
|
||||
useEffect(() => {
|
||||
let colorSet = false;
|
||||
const originalId = getOriginalId(columnId);
|
||||
if (minMaxByColumnId?.[originalId]) {
|
||||
if (colorMode !== 'none' && palette?.params && getColorForValue) {
|
||||
// workout the bucket the value belongs to
|
||||
const color = getColorForValue(
|
||||
getNumericValue(rowValue),
|
||||
palette.params,
|
||||
minMaxByColumnId[originalId]
|
||||
);
|
||||
if (color) {
|
||||
const style = { [colorMode === 'cell' ? 'backgroundColor' : 'color']: color };
|
||||
if (colorMode === 'cell' && color) {
|
||||
style.color = getContrastColor(color, IS_DARK_THEME);
|
||||
}
|
||||
colorSet = true;
|
||||
setCellProps({
|
||||
style,
|
||||
});
|
||||
if (colorMode !== 'none' && (palette || colorMapping)) {
|
||||
const color = getCellColor(columnId, palette, colorMapping)(rowValue);
|
||||
|
||||
if (color) {
|
||||
const style = { [colorMode === 'cell' ? 'backgroundColor' : 'color']: color };
|
||||
if (colorMode === 'cell' && color) {
|
||||
style.color = getContrastColor(color, isDarkMode);
|
||||
}
|
||||
colorSet = true;
|
||||
setCellProps({ style });
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up styles when something changes, this avoids cell's styling to stick forever
|
||||
// Checks isExpanded to prevent clearing style after expanding cell
|
||||
if (colorMode !== 'none' && minMaxByColumnId?.[originalId] && colorSet && !isExpanded) {
|
||||
if (colorSet && !isExpanded) {
|
||||
return () => {
|
||||
setCellProps({
|
||||
style: {
|
||||
|
@ -72,17 +82,7 @@ export const createGridCell = (
|
|||
});
|
||||
};
|
||||
}
|
||||
}, [
|
||||
rowValue,
|
||||
columnId,
|
||||
setCellProps,
|
||||
colorMode,
|
||||
palette,
|
||||
minMaxByColumnId,
|
||||
getColorForValue,
|
||||
IS_DARK_THEME,
|
||||
isExpanded,
|
||||
]);
|
||||
}, [rowValue, columnId, setCellProps, colorMode, palette, colorMapping, isExpanded]);
|
||||
|
||||
if (filterOnClick) {
|
||||
return (
|
||||
|
@ -95,7 +95,7 @@ export const createGridCell = (
|
|||
>
|
||||
<EuiLink
|
||||
onClick={() => {
|
||||
handleFilterClick?.(columnId, rowValue, colIndex, rowIndex);
|
||||
handleFilterClick?.(columnId, rawRowValue, colIndex, rowIndex);
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
|
@ -103,6 +103,7 @@ export const createGridCell = (
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
/*
|
||||
|
@ -113,6 +114,7 @@ export const createGridCell = (
|
|||
data-test-subj="lnsTableCellContent"
|
||||
className={classNames({
|
||||
'lnsTableCell--multiline': fitRowToContent,
|
||||
'lnsTableCell--colored': colorMode !== 'none',
|
||||
[`lnsTableCell--${currentAlignment}`]: true,
|
||||
})}
|
||||
/>
|
||||
|
|
|
@ -18,7 +18,7 @@ import { EuiDataGridColumnCellAction } from '@elastic/eui/src/components/datagri
|
|||
import { FILTER_CELL_ACTION_TYPE } from '@kbn/cell-actions/constants';
|
||||
import type { FormatFactory } from '../../../../common/types';
|
||||
import { RowHeightMode } from '../../../../common/types';
|
||||
import type { ColumnConfig } from '../../../../common/expressions';
|
||||
import type { DatatableColumnConfig } from '../../../../common/expressions';
|
||||
import { LensCellValueAction } from '../../../types';
|
||||
import { buildColumnsMetaLookup } from './helpers';
|
||||
import { DEFAULT_HEADER_ROW_HEIGHT } from './constants';
|
||||
|
@ -46,7 +46,7 @@ export const createGridColumns = (
|
|||
) => void)
|
||||
| undefined,
|
||||
isReadOnly: boolean,
|
||||
columnConfig: ColumnConfig,
|
||||
columnConfig: DatatableColumnConfig,
|
||||
visibleColumns: string[],
|
||||
formatFactory: FormatFactory,
|
||||
onColumnResize: (eventData: { columnId: string; width: number | undefined }) => void,
|
||||
|
|
|
@ -6,28 +6,36 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type { PaletteRegistry } from '@kbn/coloring';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { DEFAULT_COLOR_MAPPING_CONFIG, type PaletteRegistry } from '@kbn/coloring';
|
||||
import { act, render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
|
||||
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
|
||||
import { getSelectedButtonInGroup } from '@kbn/test-eui-helpers';
|
||||
import { EuiButtonGroupTestHarness } from '@kbn/test-eui-helpers';
|
||||
import {
|
||||
FramePublicAPI,
|
||||
OperationDescriptor,
|
||||
VisualizationDimensionEditorProps,
|
||||
DatasourcePublicAPI,
|
||||
DataType,
|
||||
} from '../../../types';
|
||||
import { DatatableVisualizationState } from '../visualization';
|
||||
import { createMockDatasource, createMockFramePublicAPI } from '../../../mocks';
|
||||
import { TableDimensionEditor } from './dimension_editor';
|
||||
import { ColumnState } from '../../../../common/expressions';
|
||||
import { capitalize } from 'lodash';
|
||||
|
||||
describe('data table dimension editor', () => {
|
||||
let frame: FramePublicAPI;
|
||||
let state: DatatableVisualizationState;
|
||||
let setState: (newState: DatatableVisualizationState) => void;
|
||||
let btnGroups: {
|
||||
colorMode: EuiButtonGroupTestHarness;
|
||||
alignment: EuiButtonGroupTestHarness;
|
||||
};
|
||||
let mockOperationForFirstColumn: (overrides?: Partial<OperationDescriptor>) => void;
|
||||
let props: VisualizationDimensionEditorProps<DatatableVisualizationState> & {
|
||||
paletteService: PaletteRegistry;
|
||||
isDarkMode: boolean;
|
||||
};
|
||||
|
||||
function testState(): DatatableVisualizationState {
|
||||
|
@ -42,7 +50,19 @@ describe('data table dimension editor', () => {
|
|||
};
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
btnGroups = {
|
||||
colorMode: new EuiButtonGroupTestHarness('lnsDatatable_dynamicColoring_groups'),
|
||||
alignment: new EuiButtonGroupTestHarness('lnsDatatable_alignment_groups'),
|
||||
};
|
||||
state = testState();
|
||||
frame = createMockFramePublicAPI();
|
||||
frame.datasourceLayers = {
|
||||
|
@ -63,21 +83,32 @@ describe('data table dimension editor', () => {
|
|||
rows: [],
|
||||
},
|
||||
};
|
||||
setState = jest.fn();
|
||||
|
||||
props = {
|
||||
accessor: 'foo',
|
||||
frame,
|
||||
groupId: 'columns',
|
||||
layerId: 'first',
|
||||
state,
|
||||
setState,
|
||||
setState: jest.fn(),
|
||||
isDarkMode: false,
|
||||
paletteService: chartPluginMock.createPaletteRegistry(),
|
||||
panelRef: React.createRef(),
|
||||
addLayer: jest.fn(),
|
||||
removeLayer: jest.fn(),
|
||||
datasource: {} as DatasourcePublicAPI,
|
||||
};
|
||||
|
||||
mockOperationForFirstColumn = (overrides: Partial<OperationDescriptor> = {}) => {
|
||||
frame!.datasourceLayers!.first!.getOperationForColumnId = jest.fn().mockReturnValue({
|
||||
label: 'label',
|
||||
isBucketed: false,
|
||||
dataType: 'string',
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
...overrides,
|
||||
} satisfies OperationDescriptor);
|
||||
};
|
||||
mockOperationForFirstColumn();
|
||||
});
|
||||
|
||||
const renderTableDimensionEditor = (
|
||||
|
@ -99,19 +130,19 @@ describe('data table dimension editor', () => {
|
|||
|
||||
it('should render default alignment', () => {
|
||||
renderTableDimensionEditor();
|
||||
expect(getSelectedButtonInGroup('lnsDatatable_alignment_groups')()).toHaveTextContent('Left');
|
||||
expect(btnGroups.alignment.selected).toHaveTextContent('Left');
|
||||
});
|
||||
|
||||
it('should render default alignment for number', () => {
|
||||
frame.activeData!.first.columns[0].meta.type = 'number';
|
||||
mockOperationForFirstColumn({ dataType: 'number' });
|
||||
renderTableDimensionEditor();
|
||||
expect(getSelectedButtonInGroup('lnsDatatable_alignment_groups')()).toHaveTextContent('Right');
|
||||
expect(btnGroups.alignment.selected).toHaveTextContent('Right');
|
||||
});
|
||||
|
||||
it('should render specific alignment', () => {
|
||||
state.columns[0].alignment = 'center';
|
||||
renderTableDimensionEditor();
|
||||
expect(getSelectedButtonInGroup('lnsDatatable_alignment_groups')()).toHaveTextContent('Center');
|
||||
expect(btnGroups.alignment.selected).toHaveTextContent('Center');
|
||||
});
|
||||
|
||||
it('should set state for the right column', () => {
|
||||
|
@ -125,7 +156,8 @@ describe('data table dimension editor', () => {
|
|||
];
|
||||
renderTableDimensionEditor();
|
||||
userEvent.click(screen.getByRole('button', { name: 'Center' }));
|
||||
expect(setState).toHaveBeenCalledWith({
|
||||
jest.advanceTimersByTime(256);
|
||||
expect(props.setState).toHaveBeenCalledWith({
|
||||
...state,
|
||||
columns: [
|
||||
{
|
||||
|
@ -139,44 +171,49 @@ describe('data table dimension editor', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should not show the dynamic coloring option for non numeric columns', () => {
|
||||
renderTableDimensionEditor();
|
||||
expect(screen.queryByTestId('lnsDatatable_dynamicColoring_groups')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('lns_dynamicColoring_edit')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should set the dynamic coloring default to "none"', () => {
|
||||
frame.activeData!.first.columns[0].meta.type = 'number';
|
||||
state.columns[0].colorMode = undefined;
|
||||
renderTableDimensionEditor();
|
||||
expect(getSelectedButtonInGroup('lnsDatatable_dynamicColoring_groups')()).toHaveTextContent(
|
||||
'None'
|
||||
);
|
||||
expect(btnGroups.colorMode.selected).toHaveTextContent('None');
|
||||
expect(screen.queryByTestId('lns_dynamicColoring_edit')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show the dynamic palette display ony when colorMode is different from "none"', () => {
|
||||
frame.activeData!.first.columns[0].meta.type = 'number';
|
||||
state.columns[0].colorMode = 'text';
|
||||
renderTableDimensionEditor();
|
||||
expect(getSelectedButtonInGroup('lnsDatatable_dynamicColoring_groups')()).toHaveTextContent(
|
||||
'Text'
|
||||
);
|
||||
expect(screen.getByTestId('lns_dynamicColoring_edit')).toBeInTheDocument();
|
||||
});
|
||||
it.each<DataType>(['date'])(
|
||||
'should not show the dynamic coloring option for "%s" columns',
|
||||
(dataType) => {
|
||||
mockOperationForFirstColumn({ dataType });
|
||||
renderTableDimensionEditor();
|
||||
expect(screen.queryByTestId('lnsDatatable_dynamicColoring_groups')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('lns_dynamicColoring_edit')).not.toBeInTheDocument();
|
||||
}
|
||||
);
|
||||
|
||||
it.each<ColumnState['colorMode']>(['cell', 'text'])(
|
||||
'should show the palette options ony when colorMode is "%s"',
|
||||
(colorMode) => {
|
||||
state.columns[0].colorMode = colorMode;
|
||||
renderTableDimensionEditor();
|
||||
expect(btnGroups.colorMode.selected).toHaveTextContent(capitalize(colorMode));
|
||||
expect(screen.getByTestId('lns_dynamicColoring_edit')).toBeInTheDocument();
|
||||
}
|
||||
);
|
||||
|
||||
it.each<ColumnState['colorMode']>(['none', undefined])(
|
||||
'should not show the palette options when colorMode is "%s"',
|
||||
(colorMode) => {
|
||||
state.columns[0].colorMode = colorMode;
|
||||
renderTableDimensionEditor();
|
||||
expect(btnGroups.colorMode.selected).toHaveTextContent(capitalize(colorMode ?? 'none'));
|
||||
expect(screen.queryByTestId('lns_dynamicColoring_edit')).not.toBeInTheDocument();
|
||||
}
|
||||
);
|
||||
|
||||
it('should set the coloring mode to the right column', () => {
|
||||
frame.activeData!.first.columns[0].meta.type = 'number';
|
||||
state.columns = [
|
||||
{
|
||||
columnId: 'foo',
|
||||
},
|
||||
{
|
||||
columnId: 'bar',
|
||||
},
|
||||
];
|
||||
state.columns = [{ columnId: 'foo' }, { columnId: 'bar' }];
|
||||
renderTableDimensionEditor();
|
||||
userEvent.click(screen.getByRole('button', { name: 'Cell' }));
|
||||
expect(setState).toHaveBeenCalledWith({
|
||||
jest.advanceTimersByTime(256);
|
||||
expect(props.setState).toHaveBeenCalledWith({
|
||||
...state,
|
||||
columns: [
|
||||
{
|
||||
|
@ -191,31 +228,73 @@ describe('data table dimension editor', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should open the palette panel when "Settings" link is clicked in the palette input', () => {
|
||||
frame.activeData!.first.columns[0].meta.type = 'number';
|
||||
it.each<{ flyout: 'terms' | 'values'; isBucketed: boolean; dataType: DataType }>([
|
||||
{ flyout: 'terms', isBucketed: true, dataType: 'number' },
|
||||
{ flyout: 'terms', isBucketed: false, dataType: 'string' },
|
||||
{ flyout: 'values', isBucketed: false, dataType: 'number' },
|
||||
])(
|
||||
'should show color by $flyout flyout when bucketing is $isBucketed with $dataType column',
|
||||
({ flyout, isBucketed, dataType }) => {
|
||||
state.columns[0].colorMode = 'cell';
|
||||
mockOperationForFirstColumn({ isBucketed, dataType });
|
||||
renderTableDimensionEditor();
|
||||
|
||||
userEvent.click(screen.getByLabelText('Edit colors'));
|
||||
|
||||
expect(screen.getByTestId(`lns-palettePanel-${flyout}`)).toBeInTheDocument();
|
||||
}
|
||||
);
|
||||
|
||||
it('should show the dynamic coloring option for a bucketed operation', () => {
|
||||
state.columns[0].colorMode = 'cell';
|
||||
mockOperationForFirstColumn({ isBucketed: true });
|
||||
|
||||
renderTableDimensionEditor();
|
||||
userEvent.click(screen.getByLabelText('Edit colors'));
|
||||
expect(screen.getByTestId('lns-palettePanelFlyout')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('lnsDatatable_dynamicColoring_groups')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('lns_dynamicColoring_edit')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show the dynamic coloring option for a bucketed operation', () => {
|
||||
frame.activeData!.first.columns[0].meta.type = 'number';
|
||||
const datasourceLayers = frame.datasourceLayers as Record<string, DatasourcePublicAPI>;
|
||||
datasourceLayers.first.getOperationForColumnId = jest.fn(
|
||||
() => ({ isBucketed: true } as OperationDescriptor)
|
||||
);
|
||||
it('should clear palette and colorMapping when colorMode is set to "none"', () => {
|
||||
state.columns[0].colorMode = 'cell';
|
||||
state.columns[0].palette = {
|
||||
type: 'palette',
|
||||
name: 'default',
|
||||
};
|
||||
state.columns[0].colorMapping = DEFAULT_COLOR_MAPPING_CONFIG;
|
||||
|
||||
renderTableDimensionEditor();
|
||||
expect(screen.queryByTestId('lnsDatatable_dynamicColoring_groups')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('lns_dynamicColoring_edit')).not.toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
// this throws an error about state update even in act()
|
||||
btnGroups.colorMode.select('None');
|
||||
});
|
||||
|
||||
jest.advanceTimersByTime(256);
|
||||
expect(props.setState).toBeCalledWith({
|
||||
...state,
|
||||
columns: [
|
||||
expect.objectContaining({
|
||||
colorMode: 'none',
|
||||
palette: undefined,
|
||||
colorMapping: undefined,
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should not show the summary field for non numeric columns', () => {
|
||||
renderTableDimensionEditor();
|
||||
expect(screen.queryByTestId('lnsDatatable_summaryrow_function')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('lnsDatatable_summaryrow_label')).not.toBeInTheDocument();
|
||||
[true, false].forEach((isTransposed) => {
|
||||
it(`should${isTransposed ? ' not' : ''} show hidden switch when column is${
|
||||
!isTransposed ? ' not' : ''
|
||||
} transposed`, () => {
|
||||
state.columns[0].isTransposed = isTransposed;
|
||||
renderTableDimensionEditor();
|
||||
|
||||
const hiddenSwitch = screen.queryByTestId('lns-table-column-hidden');
|
||||
if (isTransposed) {
|
||||
expect(hiddenSwitch).not.toBeInTheDocument();
|
||||
} else {
|
||||
expect(hiddenSwitch).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,37 +5,40 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFormRow, EuiSwitch, EuiButtonGroup, htmlIdGenerator } from '@elastic/eui';
|
||||
import { CustomizablePalette, PaletteRegistry } from '@kbn/coloring';
|
||||
import { PaletteRegistry } from '@kbn/coloring';
|
||||
import { getColorCategories } from '@kbn/chart-expressions-common';
|
||||
import { useDebouncedValue } from '@kbn/visualization-utils';
|
||||
import type { VisualizationDimensionEditorProps } from '../../../types';
|
||||
import type { DatatableVisualizationState } from '../visualization';
|
||||
|
||||
import {
|
||||
applyPaletteParams,
|
||||
defaultPaletteParams,
|
||||
PalettePanelContainer,
|
||||
findMinMaxByColumnId,
|
||||
shouldColorByTerms,
|
||||
} from '../../../shared_components';
|
||||
import { isNumericFieldForDatatable } from '../../../../common/expressions/datatable/utils';
|
||||
import { getOriginalId } from '../../../../common/expressions/datatable/transpose_helpers';
|
||||
|
||||
import './dimension_editor.scss';
|
||||
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';
|
||||
|
||||
const idPrefix = htmlIdGenerator()();
|
||||
|
||||
type ColumnType = DatatableVisualizationState['columns'][number];
|
||||
|
||||
function updateColumnWith(
|
||||
function updateColumn(
|
||||
state: DatatableVisualizationState,
|
||||
columnId: string,
|
||||
newColumnProps: Partial<ColumnType>
|
||||
newColumn: Partial<ColumnType>
|
||||
) {
|
||||
return state.columns.map((currentColumn) => {
|
||||
if (currentColumn.columnId === columnId) {
|
||||
return { ...currentColumn, ...newColumnProps };
|
||||
return { ...currentColumn, ...newColumn };
|
||||
} else {
|
||||
return currentColumn;
|
||||
}
|
||||
|
@ -45,30 +48,41 @@ function updateColumnWith(
|
|||
export function TableDimensionEditor(
|
||||
props: VisualizationDimensionEditorProps<DatatableVisualizationState> & {
|
||||
paletteService: PaletteRegistry;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
) {
|
||||
const { state, setState, frame, accessor, isInlineEditing } = props;
|
||||
const column = state.columns.find(({ columnId }) => accessor === columnId);
|
||||
const { frame, accessor, isInlineEditing, isDarkMode } = props;
|
||||
const column = props.state.columns.find(({ columnId }) => accessor === columnId);
|
||||
const { inputValue: localState, handleInputChange: setLocalState } =
|
||||
useDebouncedValue<DatatableVisualizationState>({
|
||||
value: props.state,
|
||||
onChange: props.setState,
|
||||
});
|
||||
|
||||
const updateColumnState = useCallback(
|
||||
(columnId: string, newColumn: Partial<ColumnType>) => {
|
||||
setLocalState({
|
||||
...localState,
|
||||
columns: updateColumn(localState, columnId, newColumn),
|
||||
});
|
||||
},
|
||||
[setLocalState, localState]
|
||||
);
|
||||
|
||||
if (!column) return null;
|
||||
if (column.isTransposed) return null;
|
||||
|
||||
const currentData = frame.activeData?.[state.layerId];
|
||||
|
||||
// either read config state or use same logic as chart itself
|
||||
const isNumeric = isNumericFieldForDatatable(currentData, accessor);
|
||||
const currentAlignment = column?.alignment || (isNumeric ? 'right' : 'left');
|
||||
const currentData = frame.activeData?.[localState.layerId];
|
||||
const datasource = frame.datasourceLayers?.[localState.layerId];
|
||||
const { dataType, isBucketed } = datasource?.getOperationForColumnId(accessor) ?? {};
|
||||
const showColorByTerms = shouldColorByTerms(dataType, isBucketed);
|
||||
const currentAlignment = column?.alignment || (dataType === 'number' ? 'right' : 'left');
|
||||
const currentColorMode = column?.colorMode || 'none';
|
||||
const hasDynamicColoring = currentColorMode !== 'none';
|
||||
const showDynamicColoringFeature = dataType !== 'date';
|
||||
const visibleColumnsCount = localState.columns.filter((c) => !c.hidden).length;
|
||||
|
||||
const datasource = frame.datasourceLayers[state.layerId];
|
||||
const showDynamicColoringFeature = Boolean(
|
||||
isNumeric && !datasource?.getOperationForColumnId(accessor)?.isBucketed
|
||||
);
|
||||
|
||||
const visibleColumnsCount = state.columns.filter((c) => !c.hidden).length;
|
||||
|
||||
const hasTransposedColumn = state.columns.some(({ isTransposed }) => isTransposed);
|
||||
const hasTransposedColumn = localState.columns.some(({ isTransposed }) => isTransposed);
|
||||
const columnsToCheck = hasTransposedColumn
|
||||
? currentData?.columns.filter(({ id }) => getOriginalId(id) === accessor).map(({ id }) => id) ||
|
||||
[]
|
||||
|
@ -76,12 +90,13 @@ export function TableDimensionEditor(
|
|||
const minMaxByColumnId = findMinMaxByColumnId(columnsToCheck, currentData, getOriginalId);
|
||||
const currentMinMax = minMaxByColumnId[accessor];
|
||||
|
||||
const activePalette = column?.palette || {
|
||||
const activePalette = column?.palette ?? {
|
||||
type: 'palette',
|
||||
name: defaultPaletteParams.name,
|
||||
name: showColorByTerms ? 'default' : defaultPaletteParams.name,
|
||||
};
|
||||
// need to tell the helper that the colorStops are required to display
|
||||
const displayStops = applyPaletteParams(props.paletteService, activePalette, currentMinMax);
|
||||
const categories = getColorCategories(currentData?.rows ?? [], accessor, false, [null]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -125,10 +140,7 @@ export function TableDimensionEditor(
|
|||
idSelected={`${idPrefix}${currentAlignment}`}
|
||||
onChange={(id) => {
|
||||
const newMode = id.replace(idPrefix, '') as ColumnType['alignment'];
|
||||
setState({
|
||||
...state,
|
||||
columns: updateColumnWith(state, accessor, { alignment: newMode }),
|
||||
});
|
||||
updateColumnState(accessor, { alignment: newMode });
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
@ -187,45 +199,46 @@ export function TableDimensionEditor(
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
// clear up when switching to no coloring
|
||||
if (column?.palette && newMode === 'none') {
|
||||
if (newMode === 'none') {
|
||||
params.palette = undefined;
|
||||
params.colorMapping = undefined;
|
||||
}
|
||||
setState({
|
||||
...state,
|
||||
columns: updateColumnWith(state, accessor, params),
|
||||
});
|
||||
updateColumnState(accessor, params);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{hasDynamicColoring && (
|
||||
<EuiFormRow
|
||||
className="lnsDynamicColoringRow"
|
||||
display="columnCompressed"
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.lens.paletteTableGradient.label', {
|
||||
defaultMessage: 'Color',
|
||||
})}
|
||||
>
|
||||
<PalettePanelContainer
|
||||
palette={displayStops.map(({ color }) => color)}
|
||||
siblingRef={props.panelRef}
|
||||
|
||||
{hasDynamicColoring &&
|
||||
(showColorByTerms ? (
|
||||
<ColorMappingByTerms
|
||||
isDarkMode={isDarkMode}
|
||||
colorMapping={column.colorMapping}
|
||||
palette={activePalette}
|
||||
isInlineEditing={isInlineEditing}
|
||||
>
|
||||
<CustomizablePalette
|
||||
palettes={props.paletteService}
|
||||
activePalette={activePalette}
|
||||
dataBounds={currentMinMax}
|
||||
setPalette={(newPalette) => {
|
||||
setState({
|
||||
...state,
|
||||
columns: updateColumnWith(state, accessor, { palette: newPalette }),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</PalettePanelContainer>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
setPalette={(palette) => {
|
||||
updateColumnState(accessor, { palette });
|
||||
}}
|
||||
setColorMapping={(colorMapping) => {
|
||||
updateColumnState(accessor, { colorMapping });
|
||||
}}
|
||||
paletteService={props.paletteService}
|
||||
panelRef={props.panelRef}
|
||||
categories={categories}
|
||||
/>
|
||||
) : (
|
||||
<ColorMappingByValues
|
||||
palette={activePalette}
|
||||
isInlineEditing={isInlineEditing}
|
||||
setPalette={(newPalette) => {
|
||||
updateColumnState(accessor, { palette: newPalette });
|
||||
}}
|
||||
paletteService={props.paletteService}
|
||||
panelRef={props.panelRef}
|
||||
dataBounds={currentMinMax}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{!column.isTransposed && (
|
||||
|
@ -247,8 +260,8 @@ export function TableDimensionEditor(
|
|||
disabled={!column.hidden && visibleColumnsCount <= 1}
|
||||
onChange={() => {
|
||||
const newState = {
|
||||
...state,
|
||||
columns: state.columns.map((currentColumn) => {
|
||||
...localState,
|
||||
columns: localState.columns.map((currentColumn) => {
|
||||
if (currentColumn.columnId === accessor) {
|
||||
return {
|
||||
...currentColumn,
|
||||
|
@ -259,7 +272,7 @@ export function TableDimensionEditor(
|
|||
}
|
||||
}),
|
||||
};
|
||||
setState(newState);
|
||||
setLocalState(newState);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
@ -283,8 +296,8 @@ export function TableDimensionEditor(
|
|||
disabled={column.hidden}
|
||||
onChange={() => {
|
||||
const newState = {
|
||||
...state,
|
||||
columns: state.columns.map((currentColumn) => {
|
||||
...localState,
|
||||
columns: localState.columns.map((currentColumn) => {
|
||||
if (currentColumn.columnId === accessor) {
|
||||
return {
|
||||
...currentColumn,
|
||||
|
@ -295,7 +308,7 @@ export function TableDimensionEditor(
|
|||
}
|
||||
}),
|
||||
};
|
||||
setState(newState);
|
||||
setLocalState(newState);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
@ -323,7 +336,7 @@ export function TableDimensionDataExtraEditor(
|
|||
onChange={(collapseFn) => {
|
||||
setState({
|
||||
...state,
|
||||
columns: updateColumnWith(state, accessor, { collapseFn }),
|
||||
columns: updateColumn(state, accessor, { collapseFn }),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -18,11 +18,11 @@ import {
|
|||
import { DatatableVisualizationState } from '../visualization';
|
||||
import { createMockDatasource, createMockFramePublicAPI } from '../../../mocks';
|
||||
import { TableDimensionEditorAdditionalSection } from './dimension_editor_addtional_section';
|
||||
import { ColumnState } from '../../../../common/expressions';
|
||||
|
||||
describe('data table dimension editor additional section', () => {
|
||||
let frame: FramePublicAPI;
|
||||
let state: DatatableVisualizationState;
|
||||
let setState: (newState: DatatableVisualizationState) => void;
|
||||
let props: VisualizationDimensionEditorProps<DatatableVisualizationState> & {
|
||||
paletteService: PaletteRegistry;
|
||||
};
|
||||
|
@ -34,6 +34,7 @@ describe('data table dimension editor additional section', () => {
|
|||
columns: [
|
||||
{
|
||||
columnId: 'foo',
|
||||
summaryRow: undefined,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -53,21 +54,20 @@ describe('data table dimension editor additional section', () => {
|
|||
id: 'foo',
|
||||
name: 'foo',
|
||||
meta: {
|
||||
type: 'string',
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
],
|
||||
rows: [],
|
||||
},
|
||||
};
|
||||
setState = jest.fn();
|
||||
props = {
|
||||
accessor: 'foo',
|
||||
frame,
|
||||
groupId: 'columns',
|
||||
layerId: 'first',
|
||||
state,
|
||||
setState,
|
||||
setState: jest.fn(),
|
||||
paletteService: chartPluginMock.createPaletteRegistry(),
|
||||
panelRef: React.createRef(),
|
||||
addLayer: jest.fn(),
|
||||
|
@ -76,27 +76,50 @@ describe('data table dimension editor additional section', () => {
|
|||
};
|
||||
});
|
||||
|
||||
it('should set the summary row function default to "none"', () => {
|
||||
frame.activeData!.first.columns[0].meta.type = 'number';
|
||||
render(<TableDimensionEditorAdditionalSection {...props} />);
|
||||
const renderComponent = (
|
||||
overrideProps?: Partial<
|
||||
VisualizationDimensionEditorProps<DatatableVisualizationState> & {
|
||||
paletteService: PaletteRegistry;
|
||||
}
|
||||
>
|
||||
) => {
|
||||
return render(<TableDimensionEditorAdditionalSection {...props} {...overrideProps} />);
|
||||
};
|
||||
|
||||
it('should set the summary row fn default to "none"', () => {
|
||||
state.columns[0].summaryRow = undefined;
|
||||
renderComponent();
|
||||
expect(screen.getByRole('combobox')).toHaveValue('None');
|
||||
expect(screen.queryByTestId('lnsDatatable_summaryrow_label')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show the summary row label input ony when summary row is different from "none"', () => {
|
||||
frame.activeData!.first.columns[0].meta.type = 'number';
|
||||
state.columns[0].summaryRow = 'sum';
|
||||
render(<TableDimensionEditorAdditionalSection {...props} />);
|
||||
expect(screen.getByRole('combobox')).toHaveValue('Sum');
|
||||
expect(screen.getByTestId('lnsDatatable_summaryrow_label')).toHaveValue('Sum');
|
||||
});
|
||||
it.each<[summaryRow: ColumnState['summaryRow'], label: string]>([
|
||||
['sum', 'Sum'],
|
||||
['avg', 'Average'],
|
||||
['count', 'Value count'],
|
||||
['min', 'Minimum'],
|
||||
['max', 'Maximum'],
|
||||
])(
|
||||
'should show the summary row label input ony when summary row fn is "%s"',
|
||||
(summaryRow, label) => {
|
||||
state.columns[0].summaryRow = summaryRow;
|
||||
renderComponent();
|
||||
expect(screen.getByRole('combobox')).toHaveValue(label);
|
||||
expect(screen.getByTestId('lnsDatatable_summaryrow_label')).toHaveValue(label);
|
||||
}
|
||||
);
|
||||
|
||||
it("should show the correct summary row name when user's changes summary label", () => {
|
||||
frame.activeData!.first.columns[0].meta.type = 'number';
|
||||
state.columns[0].summaryRow = 'sum';
|
||||
state.columns[0].summaryLabel = 'MySum';
|
||||
render(<TableDimensionEditorAdditionalSection {...props} />);
|
||||
renderComponent();
|
||||
expect(screen.getByRole('combobox')).toHaveValue('Sum');
|
||||
expect(screen.getByTestId('lnsDatatable_summaryrow_label')).toHaveValue('MySum');
|
||||
});
|
||||
|
||||
it('should not show the summary field for non numeric columns', () => {
|
||||
frame.activeData!.first.columns[0].meta.type = 'string';
|
||||
expect(screen.queryByTestId('lnsDatatable_summaryrow_function')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('lnsDatatable_summaryrow_label')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,7 +21,6 @@ import {
|
|||
getFinalSummaryConfiguration,
|
||||
getSummaryRowOptions,
|
||||
} from '../../../../common/expressions/datatable/summary';
|
||||
|
||||
import { isNumericFieldForDatatable } from '../../../../common/expressions/datatable/utils';
|
||||
|
||||
import './dimension_editor.scss';
|
||||
|
@ -76,7 +75,6 @@ export function TableDimensionEditorAdditionalSection(
|
|||
|
||||
const currentData = frame.activeData?.[state.layerId];
|
||||
|
||||
// either read config state or use same logic as chart itself
|
||||
const isNumeric = isNumericFieldForDatatable(currentData, accessor);
|
||||
// when switching from one operation to another, make sure to keep the configuration consistent
|
||||
const { summaryRow, summaryLabel: fallbackSummaryLabel } = getFinalSummaryConfiguration(
|
||||
|
|
|
@ -17,9 +17,9 @@ import {
|
|||
createGridHideHandler,
|
||||
createTransposeColumnFilterHandler,
|
||||
} from './table_actions';
|
||||
import type { LensGridDirection, ColumnConfig } from '../../../../common/expressions';
|
||||
import type { LensGridDirection, DatatableColumnConfig } from '../../../../common/expressions';
|
||||
|
||||
function getDefaultConfig(): ColumnConfig {
|
||||
function getDefaultConfig(): DatatableColumnConfig {
|
||||
return {
|
||||
columns: [
|
||||
{ columnId: 'a', type: 'lens_datatable_column' },
|
||||
|
|
|
@ -19,19 +19,15 @@ import { ClickTriggerEvent } from '@kbn/charts-plugin/public';
|
|||
import { getSortingCriteria } from '@kbn/sort-predicates';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { LensResizeAction, LensSortAction, LensToggleAction } from './types';
|
||||
import type {
|
||||
ColumnConfig,
|
||||
ColumnConfigArg,
|
||||
LensGridDirection,
|
||||
} from '../../../../common/expressions';
|
||||
import type { DatatableColumnConfig, LensGridDirection } from '../../../../common/expressions';
|
||||
import { getOriginalId } from '../../../../common/expressions/datatable/transpose_helpers';
|
||||
import type { FormatFactory } from '../../../../common/types';
|
||||
import { buildColumnsMetaLookup } from './helpers';
|
||||
|
||||
export const createGridResizeHandler =
|
||||
(
|
||||
columnConfig: ColumnConfig,
|
||||
setColumnConfig: React.Dispatch<React.SetStateAction<ColumnConfig>>,
|
||||
columnConfig: DatatableColumnConfig,
|
||||
setColumnConfig: React.Dispatch<React.SetStateAction<DatatableColumnConfig>>,
|
||||
onEditAction: (data: LensResizeAction['data']) => void
|
||||
) =>
|
||||
(eventData: { columnId: string; width: number | undefined }) => {
|
||||
|
@ -59,8 +55,8 @@ export const createGridResizeHandler =
|
|||
|
||||
export const createGridHideHandler =
|
||||
(
|
||||
columnConfig: ColumnConfig,
|
||||
setColumnConfig: React.Dispatch<React.SetStateAction<ColumnConfig>>,
|
||||
columnConfig: DatatableColumnConfig,
|
||||
setColumnConfig: React.Dispatch<React.SetStateAction<DatatableColumnConfig>>,
|
||||
onEditAction: (data: LensToggleAction['data']) => void
|
||||
) =>
|
||||
(eventData: { columnId: string }) => {
|
||||
|
@ -177,7 +173,7 @@ function getColumnType({
|
|||
columnId,
|
||||
lookup,
|
||||
}: {
|
||||
columnConfig: ColumnConfig;
|
||||
columnConfig: DatatableColumnConfig;
|
||||
columnId: string;
|
||||
lookup: Record<
|
||||
string,
|
||||
|
@ -194,11 +190,7 @@ function getColumnType({
|
|||
|
||||
export const buildSchemaDetectors = (
|
||||
columns: EuiDataGridColumn[],
|
||||
columnConfig: {
|
||||
columns: ColumnConfigArg[];
|
||||
sortingColumnId: string | undefined;
|
||||
sortingDirection: 'none' | 'asc' | 'desc';
|
||||
},
|
||||
columnConfig: DatatableColumnConfig,
|
||||
table: Datatable,
|
||||
formatters: Record<string, ReturnType<FormatFactory>>
|
||||
): EuiDataGridSchemaDetector[] => {
|
||||
|
|
|
@ -20,6 +20,18 @@ import { DatatableComponent } from './table_basic';
|
|||
import type { DatatableProps } from '../../../../common/expressions';
|
||||
import { LENS_EDIT_PAGESIZE_ACTION } from './constants';
|
||||
import { DatatableRenderProps } from './types';
|
||||
import { PaletteOutput } from '@kbn/coloring';
|
||||
import { CustomPaletteState } from '@kbn/charts-plugin/common';
|
||||
import { getCellColorFn } from '../../../shared_components/coloring/get_cell_color_fn';
|
||||
import { getTransposeId } from '../../../../common/expressions/datatable/transpose_helpers';
|
||||
|
||||
jest.mock('../../../shared_components/coloring/get_cell_color_fn', () => {
|
||||
const mod = jest.requireActual('../../../shared_components/coloring/get_cell_color_fn');
|
||||
return {
|
||||
...mod,
|
||||
getCellColorFn: jest.fn(mod.getCellColorFn),
|
||||
};
|
||||
});
|
||||
|
||||
const { theme: setUpMockTheme } = coreMock.createSetup();
|
||||
|
||||
|
@ -97,8 +109,12 @@ describe('DatatableComponent', () => {
|
|||
args = sample.args;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const renderDatatableComponent = (propsOverrides: Partial<DatatableRenderProps> = {}) => {
|
||||
const props = {
|
||||
const props: DatatableRenderProps = {
|
||||
data,
|
||||
args,
|
||||
formatFactory: () => ({ convert: (x) => x } as IFieldFormat),
|
||||
|
@ -108,6 +124,7 @@ describe('DatatableComponent', () => {
|
|||
theme: setUpMockTheme,
|
||||
renderMode: 'edit' as const,
|
||||
interactive: true,
|
||||
syncColors: false,
|
||||
renderComplete,
|
||||
...propsOverrides,
|
||||
};
|
||||
|
@ -345,9 +362,9 @@ describe('DatatableComponent', () => {
|
|||
args: {
|
||||
...args,
|
||||
columns: [
|
||||
{ columnId: 'a', alignment: 'center', type: 'lens_datatable_column' },
|
||||
{ columnId: 'b', type: 'lens_datatable_column' },
|
||||
{ columnId: 'c', type: 'lens_datatable_column' },
|
||||
{ columnId: 'a', alignment: 'center', type: 'lens_datatable_column', colorMode: 'none' },
|
||||
{ columnId: 'b', type: 'lens_datatable_column', colorMode: 'none' },
|
||||
{ columnId: 'c', type: 'lens_datatable_column', colorMode: 'none' },
|
||||
],
|
||||
sortingColumnId: 'b',
|
||||
sortingDirection: 'desc',
|
||||
|
@ -358,44 +375,10 @@ describe('DatatableComponent', () => {
|
|||
.map((cell) => cell.className);
|
||||
|
||||
expect(alignmentsClassNames).toEqual([
|
||||
// set via args
|
||||
'lnsTableCell--center',
|
||||
// default for date
|
||||
'lnsTableCell--left',
|
||||
// default for number
|
||||
'lnsTableCell--right',
|
||||
'lnsTableCell--center', // set via args
|
||||
'lnsTableCell--left', // default for date
|
||||
'lnsTableCell--right', // default for number
|
||||
]);
|
||||
// <DatatableComponent
|
||||
// data={data}
|
||||
// args={{
|
||||
// ...args,
|
||||
// columns: [
|
||||
// { columnId: 'a', alignment: 'center', type: 'lens_datatable_column' },
|
||||
// { columnId: 'b', type: 'lens_datatable_column' },
|
||||
// { columnId: 'c', type: 'lens_datatable_column' },
|
||||
// ],
|
||||
// sortingColumnId: 'b',
|
||||
// sortingDirection: 'desc',
|
||||
// }}
|
||||
// formatFactory={() => ({ convert: (x) => x } as IFieldFormat)}
|
||||
// dispatchEvent={onDispatchEvent}
|
||||
// getType={jest.fn()}
|
||||
// renderMode="view"
|
||||
// paletteService={chartPluginMock.createPaletteRegistry()}
|
||||
// theme={setUpMockTheme}
|
||||
// interactive
|
||||
// renderComplete={renderComplete}
|
||||
// />
|
||||
// );
|
||||
|
||||
// expect(wrapper.find(DataContext.Provider).prop('value').alignments).toEqual({
|
||||
// // set via args
|
||||
// a: 'center',
|
||||
// // default for date
|
||||
// b: 'left',
|
||||
// // default for number
|
||||
// c: 'right',
|
||||
// });
|
||||
});
|
||||
|
||||
test('it should refresh the table header when the datatable data changes', () => {
|
||||
|
@ -633,4 +616,106 @@ describe('DatatableComponent', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderCellValue', () => {
|
||||
describe('getCellColor', () => {
|
||||
const palette: PaletteOutput<CustomPaletteState> = {
|
||||
type: 'palette',
|
||||
name: 'default',
|
||||
params: {
|
||||
colors: [],
|
||||
gradient: false,
|
||||
stops: [],
|
||||
range: 'number',
|
||||
rangeMin: 0,
|
||||
rangeMax: 100,
|
||||
},
|
||||
};
|
||||
|
||||
describe('caching', () => {
|
||||
test('caches getCellColorFn by columnId', () => {
|
||||
args.columns[0].palette = palette;
|
||||
args.columns[0].colorMode = 'cell';
|
||||
data.rows.push(
|
||||
...[
|
||||
{ a: 'pants', b: 1588024800000, c: 4 },
|
||||
{ a: 'hat', b: 1588024800000, c: 5 },
|
||||
{ a: 'bag', b: 1588024800000, c: 6 },
|
||||
]
|
||||
);
|
||||
|
||||
renderDatatableComponent();
|
||||
|
||||
expect(getCellColorFn).toBeCalledTimes(2); // 2 initial renders of table
|
||||
});
|
||||
|
||||
test('caches getCellColorFn by columnId with transpose columns', () => {
|
||||
const columnId1 = getTransposeId('a', 'test');
|
||||
const columnId2 = getTransposeId('b', 'test');
|
||||
|
||||
renderDatatableComponent({
|
||||
data: {
|
||||
...data,
|
||||
rows: [{ [columnId1]: 'shoe', [columnId2]: 'hat' }],
|
||||
columns: [columnId1, columnId2].map((id) => ({
|
||||
...data.columns[0],
|
||||
id,
|
||||
})),
|
||||
},
|
||||
args: {
|
||||
...args,
|
||||
columns: [columnId1, columnId2].map((columnId) => ({
|
||||
...args.columns[0],
|
||||
palette,
|
||||
colorMode: 'cell',
|
||||
columnId,
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
expect(getCellColorFn).toBeCalledTimes(2); // 2 initial renders of table
|
||||
});
|
||||
});
|
||||
|
||||
const color = 'red';
|
||||
|
||||
test('should correctly color numerical values', () => {
|
||||
args.columns[0].palette = palette;
|
||||
args.columns[0].colorMode = 'cell';
|
||||
|
||||
(getCellColorFn as jest.Mock).mockReturnValue(() => color);
|
||||
|
||||
renderDatatableComponent();
|
||||
|
||||
const cellColors = screen
|
||||
.queryAllByRole('gridcell')
|
||||
.map((cell) => [cell.textContent, cell.style.backgroundColor]);
|
||||
|
||||
expect(cellColors).toEqual([
|
||||
['shoes- a, column 1, row 1', 'red'],
|
||||
['1588024800000- b, column 2, row 1', ''],
|
||||
['3- c, column 3, row 1', ''],
|
||||
]);
|
||||
});
|
||||
|
||||
test('should correctly color string values', () => {
|
||||
args.columns[2].palette = palette;
|
||||
args.columns[2].colorMode = 'cell';
|
||||
|
||||
(getCellColorFn as jest.Mock).mockReturnValue(() => color);
|
||||
|
||||
renderDatatableComponent();
|
||||
|
||||
const cellColors = screen
|
||||
.queryAllByRole('gridcell')
|
||||
.map((cell) => [cell.textContent, cell.style.backgroundColor]);
|
||||
|
||||
expect(cellColors).toEqual([
|
||||
['shoes- a, column 1, row 1', ''],
|
||||
['1588024800000- b, column 2, row 1', ''],
|
||||
['3- c, column 3, row 1', 'red'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import './table_basic.scss';
|
||||
import { CUSTOM_PALETTE } from '@kbn/coloring';
|
||||
import { ColorMappingInputData, PaletteOutput } from '@kbn/coloring';
|
||||
import React, {
|
||||
useLayoutEffect,
|
||||
useCallback,
|
||||
|
@ -27,15 +27,17 @@ import {
|
|||
EuiDataGridSorting,
|
||||
EuiDataGridStyle,
|
||||
} from '@elastic/eui';
|
||||
import { EmptyPlaceholder } from '@kbn/charts-plugin/public';
|
||||
import { CustomPaletteState, EmptyPlaceholder } from '@kbn/charts-plugin/public';
|
||||
import { ClickTriggerEvent } from '@kbn/charts-plugin/public';
|
||||
import { IconChartDatatable } from '@kbn/chart-icons';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { getColorCategories } from '@kbn/chart-expressions-common';
|
||||
import type { LensTableRowContextMenuEvent } from '../../../types';
|
||||
import type { FormatFactory } from '../../../../common/types';
|
||||
import { RowHeightMode } from '../../../../common/types';
|
||||
import type { LensGridDirection } from '../../../../common/expressions';
|
||||
import { getOriginalId, isTransposeId, LensGridDirection } from '../../../../common/expressions';
|
||||
import { VisualizationContainer } from '../../../visualization_container';
|
||||
import { findMinMaxByColumnId } from '../../../shared_components';
|
||||
import { findMinMaxByColumnId, shouldColorByTerms } from '../../../shared_components';
|
||||
import type {
|
||||
DataContextType,
|
||||
DatatableRenderProps,
|
||||
|
@ -55,8 +57,9 @@ import {
|
|||
createTransposeColumnFilterHandler,
|
||||
} from './table_actions';
|
||||
import { getFinalSummaryConfiguration } from '../../../../common/expressions/datatable/summary';
|
||||
import { getOriginalId } from '../../../../common/expressions/datatable/transpose_helpers';
|
||||
import { DEFAULT_HEADER_ROW_HEIGHT, DEFAULT_HEADER_ROW_HEIGHT_LINES } from './constants';
|
||||
import { getFieldTypeFromDatatable } from '../../../../common/expressions/datatable/utils';
|
||||
import { CellColorFn, getCellColorFn } from '../../../shared_components/coloring/get_cell_color_fn';
|
||||
|
||||
export const DataContext = React.createContext<DataContextType>({});
|
||||
|
||||
|
@ -72,6 +75,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => {
|
|||
const dataGridRef = useRef<EuiDataGridRefProps>(null);
|
||||
|
||||
const isInteractive = props.interactive;
|
||||
const isDarkMode = useObservable(props.theme.theme$, { darkMode: false }).darkMode;
|
||||
|
||||
const [columnConfig, setColumnConfig] = useState({
|
||||
columns: props.args.columns,
|
||||
|
@ -144,7 +148,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => {
|
|||
|
||||
const hasAtLeastOneRowClickAction = props.rowHasRowClickTriggerActions?.some((x) => x);
|
||||
|
||||
const { getType, dispatchEvent, renderMode, formatFactory } = props;
|
||||
const { getType, dispatchEvent, renderMode, formatFactory, syncColors } = props;
|
||||
|
||||
const formatters: Record<string, ReturnType<FormatFactory>> = useMemo(
|
||||
() =>
|
||||
|
@ -220,7 +224,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => {
|
|||
[onClickValue, untransposedDataRef, isInteractive]
|
||||
);
|
||||
|
||||
const bucketColumns = useMemo(
|
||||
const bucketedColumns = useMemo(
|
||||
() =>
|
||||
columnConfig.columns
|
||||
.filter((_col, index) => {
|
||||
|
@ -236,8 +240,8 @@ export const DatatableComponent = (props: DatatableRenderProps) => {
|
|||
|
||||
const isEmpty =
|
||||
firstLocalTable.rows.length === 0 ||
|
||||
(bucketColumns.length &&
|
||||
props.data.rows.every((row) => bucketColumns.every((col) => row[col] == null)));
|
||||
(bucketedColumns.length &&
|
||||
props.data.rows.every((row) => bucketedColumns.every((col) => row[col] == null)));
|
||||
|
||||
const visibleColumns = useMemo(
|
||||
() =>
|
||||
|
@ -302,7 +306,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => {
|
|||
const columns: EuiDataGridColumn[] = useMemo(
|
||||
() =>
|
||||
createGridColumns(
|
||||
bucketColumns,
|
||||
bucketedColumns,
|
||||
firstLocalTable,
|
||||
handleFilterClick,
|
||||
handleTransposedColumnClick,
|
||||
|
@ -320,7 +324,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => {
|
|||
props.columnFilterable
|
||||
),
|
||||
[
|
||||
bucketColumns,
|
||||
bucketedColumns,
|
||||
firstLocalTable,
|
||||
handleFilterClick,
|
||||
handleTransposedColumnClick,
|
||||
|
@ -385,17 +389,71 @@ export const DatatableComponent = (props: DatatableRenderProps) => {
|
|||
isInteractive,
|
||||
]);
|
||||
|
||||
const renderCellValue = useMemo(
|
||||
() =>
|
||||
createGridCell(
|
||||
formatters,
|
||||
columnConfig,
|
||||
DataContext,
|
||||
props.theme,
|
||||
props.args.fitRowToContent
|
||||
),
|
||||
[formatters, columnConfig, props.theme, props.args.fitRowToContent]
|
||||
);
|
||||
const renderCellValue = useMemo(() => {
|
||||
const cellColorFnMap = new Map<string, CellColorFn>();
|
||||
const getCellColor = (
|
||||
columnId: string,
|
||||
palette?: PaletteOutput<CustomPaletteState>,
|
||||
colorMapping?: string
|
||||
): CellColorFn => {
|
||||
const originalId = getOriginalId(columnId); // workout what bucket the value belongs to
|
||||
|
||||
if (cellColorFnMap.has(originalId)) {
|
||||
return cellColorFnMap.get(originalId)!;
|
||||
}
|
||||
|
||||
const dataType = getFieldTypeFromDatatable(firstLocalTable, originalId);
|
||||
const isBucketed = bucketedColumns.some((id) => id === columnId);
|
||||
const colorByTerms = shouldColorByTerms(dataType, isBucketed);
|
||||
|
||||
const data: ColorMappingInputData = colorByTerms
|
||||
? {
|
||||
type: 'categories',
|
||||
categories: getColorCategories(
|
||||
firstLocalTable.rows,
|
||||
originalId,
|
||||
isTransposeId(columnId),
|
||||
[null]
|
||||
),
|
||||
}
|
||||
: {
|
||||
type: 'ranges',
|
||||
bins: 0,
|
||||
...minMaxByColumnId[originalId],
|
||||
};
|
||||
const colorFn = getCellColorFn(
|
||||
props.paletteService,
|
||||
data,
|
||||
colorByTerms,
|
||||
isDarkMode,
|
||||
syncColors,
|
||||
palette,
|
||||
colorMapping
|
||||
);
|
||||
cellColorFnMap.set(originalId, colorFn);
|
||||
|
||||
return colorFn;
|
||||
};
|
||||
|
||||
return createGridCell(
|
||||
formatters,
|
||||
columnConfig,
|
||||
DataContext,
|
||||
isDarkMode,
|
||||
getCellColor,
|
||||
props.args.fitRowToContent
|
||||
);
|
||||
}, [
|
||||
formatters,
|
||||
columnConfig,
|
||||
isDarkMode,
|
||||
props.args.fitRowToContent,
|
||||
props.paletteService,
|
||||
firstLocalTable,
|
||||
bucketedColumns,
|
||||
minMaxByColumnId,
|
||||
syncColors,
|
||||
]);
|
||||
|
||||
const columnVisibility = useMemo(
|
||||
() => ({
|
||||
|
@ -471,7 +529,6 @@ export const DatatableComponent = (props: DatatableRenderProps) => {
|
|||
rowHasRowClickTriggerActions: props.rowHasRowClickTriggerActions,
|
||||
alignments,
|
||||
minMaxByColumnId,
|
||||
getColorForValue: props.paletteService.get(CUSTOM_PALETTE).getColorForValue!,
|
||||
handleFilterClick,
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import { CoreSetup } from '@kbn/core/public';
|
||||
import type { PaletteRegistry } from '@kbn/coloring';
|
||||
import { CustomPaletteState } from '@kbn/charts-plugin/public';
|
||||
import type { IAggType } from '@kbn/data-plugin/public';
|
||||
import type { Datatable, RenderMode } from '@kbn/expressions-plugin/common';
|
||||
import type {
|
||||
|
@ -82,9 +81,4 @@ export interface DataContextType {
|
|||
rowIndex: number,
|
||||
negate?: boolean
|
||||
) => void;
|
||||
getColorForValue?: (
|
||||
value: number | undefined,
|
||||
state: CustomPaletteState,
|
||||
minMax: { min: number; max: number }
|
||||
) => string | undefined;
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ const cellValueAction: LensCellValueAction = {
|
|||
iconType: 'test-icon',
|
||||
execute: () => {},
|
||||
};
|
||||
function sampleArgs() {
|
||||
function sampleArgs(): DatatableProps {
|
||||
const indexPatternId = 'indexPatternId';
|
||||
const data: Datatable = {
|
||||
type: 'datatable',
|
||||
|
@ -80,13 +80,13 @@ function sampleArgs() {
|
|||
sortingDirection: 'none',
|
||||
};
|
||||
|
||||
return { data, args };
|
||||
return { data, args, syncColors: false };
|
||||
}
|
||||
|
||||
describe('datatable_expression', () => {
|
||||
describe('datatable renders', () => {
|
||||
test('it renders with the specified data and args', async () => {
|
||||
const { data, args } = sampleArgs();
|
||||
const { data, args, ...rest } = sampleArgs();
|
||||
const result = await getDatatable(() => Promise.resolve((() => {}) as FormatFactory)).fn(
|
||||
data,
|
||||
args,
|
||||
|
@ -96,7 +96,7 @@ describe('datatable_expression', () => {
|
|||
expect(result).toEqual({
|
||||
type: 'render',
|
||||
as: 'lens_datatable_renderer',
|
||||
value: { data, args },
|
||||
value: { data, args, ...rest },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -168,6 +168,7 @@ export const getDatatableRenderer = (dependencies: {
|
|||
interactive={isInteractive()}
|
||||
theme={dependencies.core.theme}
|
||||
renderComplete={renderComplete}
|
||||
syncColors={config.syncColors}
|
||||
/>
|
||||
</KibanaRenderContextProvider>,
|
||||
domNode
|
||||
|
|
|
@ -44,7 +44,7 @@ export class DatatableVisualization {
|
|||
})
|
||||
);
|
||||
|
||||
return getDatatableVisualization({ paletteService: palettes, theme: core.theme });
|
||||
return getDatatableVisualization({ paletteService: palettes, kibanaTheme: core.theme });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,13 +7,7 @@
|
|||
|
||||
import { Ast } from '@kbn/interpreter';
|
||||
import { buildExpression } from '@kbn/expressions-plugin/public';
|
||||
import {
|
||||
createMockDatasource,
|
||||
createMockFramePublicAPI,
|
||||
DatasourceMock,
|
||||
generateActiveData,
|
||||
} from '../../mocks';
|
||||
import faker from 'faker';
|
||||
import { createMockDatasource, createMockFramePublicAPI, DatasourceMock } from '../../mocks';
|
||||
import { DatatableVisualizationState, getDatatableVisualization } from './visualization';
|
||||
import {
|
||||
Operation,
|
||||
|
@ -27,6 +21,20 @@ import { RowHeightMode } from '../../../common/types';
|
|||
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
|
||||
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
|
||||
import { themeServiceMock } from '@kbn/core/public/mocks';
|
||||
import { ColorMapping, CUSTOM_PALETTE, CustomPaletteParams, PaletteOutput } from '@kbn/coloring';
|
||||
import {
|
||||
ColumnState,
|
||||
DatatableColumnFn,
|
||||
DatatableExpressionFunction,
|
||||
} from '../../../common/expressions';
|
||||
import { getColorStops } from '../../shared_components/coloring';
|
||||
|
||||
jest.mock('../../shared_components/coloring', () => {
|
||||
return {
|
||||
...jest.requireActual('../../shared_components/coloring'),
|
||||
getColorStops: jest.fn().mockReturnValue([]),
|
||||
};
|
||||
});
|
||||
|
||||
function mockFrame(): FramePublicAPI {
|
||||
return {
|
||||
|
@ -35,12 +43,18 @@ function mockFrame(): FramePublicAPI {
|
|||
};
|
||||
}
|
||||
|
||||
const datatableVisualization = getDatatableVisualization({
|
||||
const mockServices = {
|
||||
paletteService: chartPluginMock.createPaletteRegistry(),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
});
|
||||
kibanaTheme: themeServiceMock.createStartContract(),
|
||||
};
|
||||
|
||||
const datatableVisualization = getDatatableVisualization(mockServices);
|
||||
|
||||
describe('Datatable Visualization', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('#initialize', () => {
|
||||
it('should initialize from the empty state', () => {
|
||||
expect(datatableVisualization.initialize(() => 'aaa', undefined)).toEqual({
|
||||
|
@ -417,64 +431,122 @@ describe('Datatable Visualization', () => {
|
|||
});
|
||||
|
||||
describe('with palette', () => {
|
||||
const mockStops = ['red', 'white', 'blue'];
|
||||
const datasource = createMockDatasource('test');
|
||||
let params: VisualizationConfigProps<DatatableVisualizationState>;
|
||||
|
||||
beforeEach(() => {
|
||||
const datasource = createMockDatasource('test');
|
||||
datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'b', fields: [] }]);
|
||||
params = {
|
||||
layerId: 'a',
|
||||
state: {
|
||||
layerId: 'a',
|
||||
layerType: LayerTypes.DATA,
|
||||
columns: [
|
||||
{
|
||||
columnId: 'b',
|
||||
palette: {
|
||||
type: 'palette' as const,
|
||||
name: '',
|
||||
params: { stops: [{ color: 'blue', stop: 0 }] },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
frame: {
|
||||
...mockFrame(),
|
||||
activeData: generateActiveData([
|
||||
{
|
||||
id: 'a',
|
||||
rows: Array(3).fill({
|
||||
b: faker.random.number(),
|
||||
}),
|
||||
},
|
||||
]),
|
||||
datasourceLayers: { a: datasource.publicAPIMock },
|
||||
},
|
||||
};
|
||||
(getColorStops as jest.Mock).mockReturnValue(mockStops);
|
||||
});
|
||||
|
||||
it('does include palette for accessor config if the values are numeric and palette exists', () => {
|
||||
expect(datatableVisualization.getConfiguration(params).groups[2].accessors).toEqual([
|
||||
{ columnId: 'b', palette: ['blue'], triggerIconType: 'colorBy' },
|
||||
]);
|
||||
describe('rows', () => {
|
||||
beforeEach(() => {
|
||||
datasource.publicAPIMock.getOperationForColumnId.mockReturnValueOnce({
|
||||
dataType: 'string',
|
||||
isBucketed: true,
|
||||
label: 'label',
|
||||
isStaticValue: false,
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
});
|
||||
datasource.publicAPIMock.getTableSpec.mockReturnValue([
|
||||
{ columnId: 'b', fields: [] },
|
||||
{ columnId: 'c', fields: [] },
|
||||
]);
|
||||
|
||||
params = {
|
||||
layerId: 'a',
|
||||
state: {
|
||||
layerId: 'a',
|
||||
layerType: LayerTypes.DATA,
|
||||
columns: [{ columnId: 'b' }, { columnId: 'c' }],
|
||||
},
|
||||
frame: {
|
||||
...mockFrame(),
|
||||
datasourceLayers: { a: datasource.publicAPIMock },
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it.each<ColumnState['colorMode']>(['cell', 'text'])(
|
||||
'should include palette if colorMode is %s and has stops',
|
||||
(colorMode) => {
|
||||
params.state.columns[0].colorMode = colorMode;
|
||||
expect(datatableVisualization.getConfiguration(params).groups[0].accessors).toEqual([
|
||||
{ columnId: 'b', palette: mockStops, triggerIconType: 'colorBy' },
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
it.each<ColumnState['colorMode']>(['cell', 'text'])(
|
||||
'should not include palette if colorMode is %s but stops is empty',
|
||||
(colorMode) => {
|
||||
(getColorStops as jest.Mock).mockReturnValue([]);
|
||||
params.state.columns[0].colorMode = colorMode;
|
||||
expect(datatableVisualization.getConfiguration(params).groups[0].accessors).toEqual([
|
||||
{ columnId: 'b' },
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
it.each<ColumnState['colorMode']>(['none', undefined])(
|
||||
'should not include palette if colorMode is %s even if stops exist',
|
||||
(colorMode) => {
|
||||
params.state.columns[0].colorMode = colorMode;
|
||||
expect(datatableVisualization.getConfiguration(params).groups[0].accessors).toEqual([
|
||||
{ columnId: 'b' },
|
||||
]);
|
||||
}
|
||||
);
|
||||
});
|
||||
it('does not include palette for accessor config if the values are not numeric and palette exists', () => {
|
||||
params.frame.activeData = generateActiveData([
|
||||
{
|
||||
id: 'a',
|
||||
rows: Array(3).fill({
|
||||
b: faker.random.word(),
|
||||
}),
|
||||
},
|
||||
]);
|
||||
expect(datatableVisualization.getConfiguration(params).groups[2].accessors).toEqual([
|
||||
{ columnId: 'b' },
|
||||
]);
|
||||
});
|
||||
it('does not include palette for accessor config if the values are numeric but palette exists', () => {
|
||||
params.state.columns[0].palette = undefined;
|
||||
expect(datatableVisualization.getConfiguration(params).groups[2].accessors).toEqual([
|
||||
{ columnId: 'b' },
|
||||
]);
|
||||
|
||||
describe('metrics', () => {
|
||||
beforeEach(() => {
|
||||
datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'b', fields: [] }]);
|
||||
params = {
|
||||
layerId: 'a',
|
||||
state: {
|
||||
layerId: 'a',
|
||||
layerType: LayerTypes.DATA,
|
||||
columns: [{ columnId: 'b' }],
|
||||
},
|
||||
frame: {
|
||||
...mockFrame(),
|
||||
datasourceLayers: { a: datasource.publicAPIMock },
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it.each<ColumnState['colorMode']>(['cell', 'text'])(
|
||||
'should include palette if colorMode is %s and has stops',
|
||||
(colorMode) => {
|
||||
params.state.columns[0].colorMode = colorMode;
|
||||
expect(datatableVisualization.getConfiguration(params).groups[2].accessors).toEqual([
|
||||
{ columnId: 'b', palette: mockStops, triggerIconType: 'colorBy' },
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
it.each<ColumnState['colorMode']>(['cell', 'text'])(
|
||||
'should not include palette if colorMode is %s but stops is empty',
|
||||
(colorMode) => {
|
||||
(getColorStops as jest.Mock).mockReturnValue([]);
|
||||
params.state.columns[0].colorMode = colorMode;
|
||||
expect(datatableVisualization.getConfiguration(params).groups[2].accessors).toEqual([
|
||||
{ columnId: 'b' },
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
it.each<ColumnState['colorMode']>(['none', undefined])(
|
||||
'should not include palette if colorMode is %s even if stops exist',
|
||||
(colorMode) => {
|
||||
params.state.columns[0].colorMode = colorMode;
|
||||
expect(datatableVisualization.getConfiguration(params).groups[2].accessors).toEqual([
|
||||
{ columnId: 'b' },
|
||||
]);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -629,13 +701,12 @@ describe('Datatable Visualization', () => {
|
|||
datatableVisualization.toExpression(
|
||||
state,
|
||||
frame.datasourceLayers,
|
||||
|
||||
{},
|
||||
{ '1': { type: 'expression', chain: [] } }
|
||||
) as Ast
|
||||
).findFunction('lens_datatable')[0].arguments;
|
||||
).findFunction<DatatableExpressionFunction>('lens_datatable')[0].arguments;
|
||||
|
||||
const defaultExpressionTableState = {
|
||||
const defaultExpressionTableState: DatatableVisualizationState = {
|
||||
layerId: 'a',
|
||||
layerType: LayerTypes.DATA,
|
||||
columns: [{ columnId: 'b' }, { columnId: 'c' }],
|
||||
|
@ -881,6 +952,104 @@ describe('Datatable Visualization', () => {
|
|||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('palette/colorMapping/colorMode', () => {
|
||||
const colorMapping: ColorMapping.Config = {
|
||||
paletteId: 'default',
|
||||
colorMode: { type: 'categorical' },
|
||||
assignments: [],
|
||||
specialAssignments: [],
|
||||
};
|
||||
const palette: PaletteOutput<CustomPaletteParams> = {
|
||||
type: 'palette',
|
||||
name: 'default',
|
||||
};
|
||||
const colorExpressionTableState = (
|
||||
colorMode?: 'cell' | 'text' | 'none'
|
||||
): DatatableVisualizationState => ({
|
||||
...defaultExpressionTableState,
|
||||
columns: [{ columnId: 'b', colorMapping, palette, colorMode }],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'b', fields: [] }]);
|
||||
});
|
||||
|
||||
it.each<[DataType, string]>([
|
||||
['string', palette.name],
|
||||
['number', CUSTOM_PALETTE], // required to property handle toExpression
|
||||
])(
|
||||
'should call paletteService.get with correct palette name for %s dataType',
|
||||
(dataType, paletteName) => {
|
||||
datasource.publicAPIMock.getOperationForColumnId.mockReturnValue({
|
||||
dataType,
|
||||
isBucketed: false,
|
||||
label: 'label',
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
});
|
||||
|
||||
getDatatableExpressionArgs(colorExpressionTableState());
|
||||
|
||||
expect(mockServices.paletteService.get).toBeCalledWith(paletteName);
|
||||
}
|
||||
);
|
||||
|
||||
describe.each<'cell' | 'text' | 'none' | undefined>(['cell', 'text', 'none', undefined])(
|
||||
'colorMode - %s',
|
||||
(colorMode) => {
|
||||
it.each<{ dataType: DataType; disallowed?: boolean }>([
|
||||
// allowed types
|
||||
{ dataType: 'document' },
|
||||
{ dataType: 'ip' },
|
||||
{ dataType: 'histogram' },
|
||||
{ dataType: 'geo_point' },
|
||||
{ dataType: 'geo_shape' },
|
||||
{ dataType: 'counter' },
|
||||
{ dataType: 'gauge' },
|
||||
{ dataType: 'murmur3' },
|
||||
{ dataType: 'string' },
|
||||
{ dataType: 'number' },
|
||||
{ dataType: 'boolean' },
|
||||
// disallowed types
|
||||
{ dataType: 'date', disallowed: true },
|
||||
])(
|
||||
'should apply correct palette, colorMapping & colorMode for $dataType',
|
||||
({ dataType, disallowed = false }) => {
|
||||
datasource.publicAPIMock.getOperationForColumnId.mockReturnValue({
|
||||
dataType,
|
||||
isBucketed: false,
|
||||
label: 'label',
|
||||
hasTimeShift: false,
|
||||
hasReducedTimeRange: false,
|
||||
});
|
||||
|
||||
const expression = datatableVisualization.toExpression(
|
||||
colorExpressionTableState(colorMode),
|
||||
frame.datasourceLayers,
|
||||
{},
|
||||
{ '1': { type: 'expression', chain: [] } }
|
||||
) as Ast;
|
||||
|
||||
const columnArgs =
|
||||
buildExpression(expression).findFunction<DatatableColumnFn>(
|
||||
'lens_datatable_column'
|
||||
)[0].arguments;
|
||||
|
||||
if (disallowed) {
|
||||
expect(columnArgs.colorMode).toEqual(['none']);
|
||||
expect(columnArgs.palette).toBeUndefined();
|
||||
expect(columnArgs.colorMapping).toBeUndefined();
|
||||
} else {
|
||||
expect(columnArgs.colorMode).toEqual([colorMode ?? 'none']);
|
||||
expect(columnArgs.palette).toEqual([expect.any(Object)]);
|
||||
expect(columnArgs.colorMapping).toEqual([expect.any(String)]);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#onEditAction', () => {
|
||||
|
|
|
@ -8,13 +8,13 @@
|
|||
import React from 'react';
|
||||
import { Ast } from '@kbn/interpreter';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { PaletteRegistry, CUSTOM_PALETTE } from '@kbn/coloring';
|
||||
import { PaletteRegistry, CUSTOM_PALETTE, PaletteOutput, CustomPaletteParams } from '@kbn/coloring';
|
||||
import { ThemeServiceStart } from '@kbn/core/public';
|
||||
import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public';
|
||||
import { IconChartDatatable } from '@kbn/chart-icons';
|
||||
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
|
||||
import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/common';
|
||||
import { isNumericFieldForDatatable } from '../../../common/expressions/datatable/utils';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import type { FormBasedPersistedState } from '../../datasources/form_based/types';
|
||||
import type {
|
||||
SuggestionRequest,
|
||||
|
@ -28,13 +28,14 @@ import { TableDimensionEditorAdditionalSection } from './components/dimension_ed
|
|||
import type { LayerType } from '../../../common/types';
|
||||
import { RowHeightMode } from '../../../common/types';
|
||||
import { getDefaultSummaryLabel } from '../../../common/expressions/datatable/summary';
|
||||
import type {
|
||||
ColumnState,
|
||||
SortingState,
|
||||
PagingState,
|
||||
CollapseExpressionFunction,
|
||||
DatatableColumnFunction,
|
||||
DatatableExpressionFunction,
|
||||
import {
|
||||
type ColumnState,
|
||||
type SortingState,
|
||||
type PagingState,
|
||||
type CollapseExpressionFunction,
|
||||
type DatatableColumnFn,
|
||||
type DatatableExpressionFunction,
|
||||
getOriginalId,
|
||||
} from '../../../common/expressions';
|
||||
import { DataTableToolbar } from './components/toolbar';
|
||||
import {
|
||||
|
@ -42,6 +43,14 @@ import {
|
|||
DEFAULT_HEADER_ROW_HEIGHT_LINES,
|
||||
DEFAULT_ROW_HEIGHT,
|
||||
} from './components/constants';
|
||||
import {
|
||||
applyPaletteParams,
|
||||
defaultPaletteParams,
|
||||
findMinMaxByColumnId,
|
||||
getColorStops,
|
||||
shouldColorByTerms,
|
||||
} from '../../shared_components';
|
||||
import { getColorMappingTelemetryEvents } from '../../lens_ui_telemetry/color_telemetry_helpers';
|
||||
export interface DatatableVisualizationState {
|
||||
columns: ColumnState[];
|
||||
layerId: string;
|
||||
|
@ -60,10 +69,10 @@ const visualizationLabel = i18n.translate('xpack.lens.datatable.label', {
|
|||
|
||||
export const getDatatableVisualization = ({
|
||||
paletteService,
|
||||
theme,
|
||||
kibanaTheme,
|
||||
}: {
|
||||
paletteService: PaletteRegistry;
|
||||
theme: ThemeServiceStart;
|
||||
kibanaTheme: ThemeServiceStart;
|
||||
}): Visualization<DatatableVisualizationState> => ({
|
||||
id: 'lnsDatatable',
|
||||
|
||||
|
@ -115,6 +124,56 @@ export const getDatatableVisualization = ({
|
|||
);
|
||||
},
|
||||
|
||||
onDatasourceUpdate(state, frame) {
|
||||
const datasource = frame?.datasourceLayers?.[state.layerId];
|
||||
const paletteMap = new Map(
|
||||
paletteService
|
||||
.getAll()
|
||||
.filter((p) => !p.internal)
|
||||
.map((p) => [p.id, p])
|
||||
);
|
||||
|
||||
const hasTransposedColumn = state.columns.some(({ isTransposed }) => isTransposed);
|
||||
const columns = state.columns.map((column) => {
|
||||
if (column.palette) {
|
||||
const accessor = column.columnId;
|
||||
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
|
||||
.filter(({ id }) => getOriginalId(id) === accessor)
|
||||
.map(({ id }) => id) || []
|
||||
: [accessor];
|
||||
const minMaxByColumnId = findMinMaxByColumnId(columnsToCheck, currentData, getOriginalId);
|
||||
|
||||
if (palette && !showColorByTerms && !palette?.canDynamicColoring) {
|
||||
const newPalette: PaletteOutput<CustomPaletteParams> = {
|
||||
type: 'palette',
|
||||
name: showColorByTerms ? 'default' : defaultPaletteParams.name,
|
||||
};
|
||||
return {
|
||||
...column,
|
||||
palette: {
|
||||
...newPalette,
|
||||
params: {
|
||||
stops: applyPaletteParams(paletteService, newPalette, minMaxByColumnId[accessor]),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return column;
|
||||
});
|
||||
|
||||
return {
|
||||
...state,
|
||||
columns,
|
||||
};
|
||||
},
|
||||
|
||||
getSuggestions({
|
||||
table,
|
||||
state,
|
||||
|
@ -197,15 +256,16 @@ export const getDatatableVisualization = ({
|
|||
},
|
||||
|
||||
/*
|
||||
Datatable works differently on text based datasource and form based
|
||||
- Form based: It relies on the isBucketed flag to identify groups. It allows only numeric fields
|
||||
Datatable works differently on text-based datasource and form-based
|
||||
- Form-based: It relies on the isBucketed flag to identify groups. It allows only numeric fields
|
||||
on the Metrics dimension
|
||||
- Text based: It relies on the isMetric flag to identify groups. It allows all type of fields
|
||||
- Text-based: It relies on the isMetric flag to identify groups. It allows all type of fields
|
||||
on the Metric dimension in cases where there are no numeric columns
|
||||
**/
|
||||
getConfiguration({ state, frame, layerId }) {
|
||||
getConfiguration({ state, frame }) {
|
||||
const isDarkMode = kibanaTheme.getTheme().darkMode;
|
||||
const { sortedColumns, datasource } =
|
||||
getDataSourceAndSortedColumns(state, frame.datasourceLayers, layerId) || {};
|
||||
getDataSourceAndSortedColumns(state, frame.datasourceLayers) || {};
|
||||
|
||||
const columnMap: Record<string, ColumnState> = {};
|
||||
state.columns.forEach((column) => {
|
||||
|
@ -245,14 +305,29 @@ export const getDatatableVisualization = ({
|
|||
}
|
||||
return datasource!.getOperationForColumnId(c)?.isBucketed && !column?.isTransposed;
|
||||
})
|
||||
.map((accessor) => ({
|
||||
columnId: accessor,
|
||||
triggerIconType: columnMap[accessor].hidden
|
||||
? 'invisible'
|
||||
: columnMap[accessor].collapseFn
|
||||
? 'aggregate'
|
||||
: undefined,
|
||||
})),
|
||||
.map((accessor) => {
|
||||
const {
|
||||
colorMode = 'none',
|
||||
palette,
|
||||
colorMapping,
|
||||
hidden,
|
||||
collapseFn,
|
||||
} = columnMap[accessor] ?? {};
|
||||
const stops = getColorStops(paletteService, isDarkMode, palette, colorMapping);
|
||||
const hasColoring = colorMode !== 'none' && stops.length > 0;
|
||||
|
||||
return {
|
||||
columnId: accessor,
|
||||
triggerIconType: hidden
|
||||
? 'invisible'
|
||||
: hasColoring
|
||||
? 'colorBy'
|
||||
: collapseFn
|
||||
? 'aggregate'
|
||||
: undefined,
|
||||
palette: hasColoring ? stops : undefined,
|
||||
};
|
||||
}),
|
||||
supportsMoreColumns: true,
|
||||
filterOperations: (op) => op.isBucketed,
|
||||
dataTestSubj: 'lnsDatatable_rows',
|
||||
|
@ -319,22 +394,19 @@ export const getDatatableVisualization = ({
|
|||
return !operation?.isBucketed;
|
||||
})
|
||||
.map((accessor) => {
|
||||
const columnConfig = columnMap[accessor];
|
||||
const stops = columnConfig?.palette?.params?.stops;
|
||||
const isNumeric = Boolean(
|
||||
accessor && isNumericFieldForDatatable(frame.activeData?.[state.layerId], accessor)
|
||||
);
|
||||
const hasColoring = Boolean(columnConfig?.colorMode !== 'none' && stops);
|
||||
const {
|
||||
colorMode = 'none',
|
||||
palette,
|
||||
colorMapping,
|
||||
hidden,
|
||||
} = columnMap[accessor] ?? {};
|
||||
const stops = getColorStops(paletteService, isDarkMode, palette, colorMapping);
|
||||
const hasColoring = colorMode !== 'none' && stops.length > 0;
|
||||
|
||||
return {
|
||||
columnId: accessor,
|
||||
triggerIconType: columnConfig?.hidden
|
||||
? 'invisible'
|
||||
: hasColoring && isNumeric
|
||||
? 'colorBy'
|
||||
: undefined,
|
||||
palette:
|
||||
hasColoring && isNumeric && stops ? stops.map(({ color }) => color) : undefined,
|
||||
triggerIconType: hidden ? 'invisible' : hasColoring ? 'colorBy' : undefined,
|
||||
palette: hasColoring ? stops : undefined,
|
||||
};
|
||||
}),
|
||||
supportsMoreColumns: true,
|
||||
|
@ -386,7 +458,11 @@ export const getDatatableVisualization = ({
|
|||
};
|
||||
},
|
||||
DimensionEditorComponent(props) {
|
||||
return <TableDimensionEditor {...props} paletteService={paletteService} />;
|
||||
const isDarkMode = useObservable(kibanaTheme.theme$, { darkMode: false }).darkMode;
|
||||
|
||||
return (
|
||||
<TableDimensionEditor {...props} isDarkMode={isDarkMode} paletteService={paletteService} />
|
||||
);
|
||||
},
|
||||
|
||||
DimensionEditorAdditionalSectionComponent(props) {
|
||||
|
@ -421,7 +497,7 @@ export const getDatatableVisualization = ({
|
|||
datasourceExpressionsByLayers = {}
|
||||
): Ast | null {
|
||||
const { sortedColumns, datasource } =
|
||||
getDataSourceAndSortedColumns(state, datasourceLayers, state.layerId) || {};
|
||||
getDataSourceAndSortedColumns(state, datasourceLayers) || {};
|
||||
const isTextBasedLanguage = datasource?.isTextBasedLanguage();
|
||||
|
||||
if (
|
||||
|
@ -481,23 +557,20 @@ export const getDatatableVisualization = ({
|
|||
: [],
|
||||
reverse: false, // managed at UI level
|
||||
};
|
||||
const sortingHint = datasource!.getOperationForColumnId(column.columnId)!.sortingHint;
|
||||
|
||||
const { dataType, isBucketed, sortingHint, inMetricDimension } =
|
||||
datasource?.getOperationForColumnId(column.columnId) ?? {};
|
||||
const hasNoSummaryRow = column.summaryRow == null || column.summaryRow === 'none';
|
||||
|
||||
const canColor =
|
||||
datasource!.getOperationForColumnId(column.columnId)?.dataType === 'number';
|
||||
|
||||
const canColor = dataType !== 'date';
|
||||
const colorByTerms = shouldColorByTerms(dataType, isBucketed);
|
||||
let isTransposable =
|
||||
!isTextBasedLanguage &&
|
||||
!datasource!.getOperationForColumnId(column.columnId)?.isBucketed;
|
||||
|
||||
if (isTextBasedLanguage) {
|
||||
const operation = datasource!.getOperationForColumnId(column.columnId);
|
||||
isTransposable = Boolean(column?.isMetric || operation?.inMetricDimension);
|
||||
isTransposable = Boolean(column?.isMetric || inMetricDimension);
|
||||
}
|
||||
|
||||
const datatableColumnFn = buildExpressionFunction<DatatableColumnFunction>(
|
||||
const datatableColumnFn = buildExpressionFunction<DatatableColumnFn>(
|
||||
'lens_datatable_column',
|
||||
{
|
||||
columnId: column.columnId,
|
||||
|
@ -507,8 +580,15 @@ export const getDatatableVisualization = ({
|
|||
isTransposed: column.isTransposed,
|
||||
transposable: isTransposable,
|
||||
alignment: column.alignment,
|
||||
colorMode: canColor && column.colorMode ? column.colorMode : 'none',
|
||||
palette: paletteService.get(CUSTOM_PALETTE).toExpression(paletteParams),
|
||||
colorMode: canColor ? column.colorMode ?? 'none' : 'none',
|
||||
palette: !canColor
|
||||
? undefined
|
||||
: paletteService
|
||||
// The by value palette is a pseudo custom palette that is only custom from params level
|
||||
.get(colorByTerms ? column.palette?.name || CUSTOM_PALETTE : CUSTOM_PALETTE)
|
||||
.toExpression(paletteParams),
|
||||
colorMapping:
|
||||
canColor && column.colorMapping ? JSON.stringify(column.colorMapping) : undefined,
|
||||
summaryRow: hasNoSummaryRow ? undefined : column.summaryRow!,
|
||||
summaryLabel: hasNoSummaryRow
|
||||
? undefined
|
||||
|
@ -537,6 +617,15 @@ export const getDatatableVisualization = ({
|
|||
};
|
||||
},
|
||||
|
||||
getTelemetryEventsOnSave(state, prevState) {
|
||||
const colorMappingEvents = state.columns.flatMap((col) => {
|
||||
const prevColumn = prevState?.columns?.find((prevCol) => prevCol.columnId === col.columnId);
|
||||
return getColorMappingTelemetryEvents(col.colorMapping, prevColumn?.colorMapping);
|
||||
});
|
||||
|
||||
return colorMappingEvents;
|
||||
},
|
||||
|
||||
getRenderEventCounters(state) {
|
||||
const events = {
|
||||
color_by_value: false,
|
||||
|
@ -642,8 +731,7 @@ export const getDatatableVisualization = ({
|
|||
},
|
||||
|
||||
getSortedColumns(state, datasourceLayers) {
|
||||
const { sortedColumns } =
|
||||
getDataSourceAndSortedColumns(state, datasourceLayers || {}, state.layerId) || {};
|
||||
const { sortedColumns } = getDataSourceAndSortedColumns(state, datasourceLayers || {}) || {};
|
||||
return sortedColumns;
|
||||
},
|
||||
|
||||
|
@ -696,8 +784,7 @@ export const getDatatableVisualization = ({
|
|||
|
||||
function getDataSourceAndSortedColumns(
|
||||
state: DatatableVisualizationState,
|
||||
datasourceLayers: DatasourceLayers,
|
||||
layerId: string
|
||||
datasourceLayers: DatasourceLayers
|
||||
) {
|
||||
const datasource = datasourceLayers[state.layerId];
|
||||
const originalOrder = datasource?.getTableSpec().map(({ columnId }) => columnId);
|
||||
|
|
|
@ -110,13 +110,16 @@ export function GaugeDimensionEditor(
|
|||
display="columnCompressed"
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.lens.paletteMetricGradient.label', {
|
||||
defaultMessage: 'Color',
|
||||
defaultMessage: 'Color mapping',
|
||||
})}
|
||||
>
|
||||
<PalettePanelContainer
|
||||
palette={displayStops.map(({ color }) => color)}
|
||||
siblingRef={props.panelRef}
|
||||
isInlineEditing={isInlineEditing}
|
||||
title={i18n.translate('xpack.lens.paletteMetricGradient.label', {
|
||||
defaultMessage: 'Color mapping',
|
||||
})}
|
||||
>
|
||||
<CustomizablePalette
|
||||
palettes={props.paletteService}
|
||||
|
|
|
@ -133,13 +133,16 @@ export function MetricDimensionEditor(
|
|||
display="columnCompressed"
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.lens.paletteMetricGradient.label', {
|
||||
defaultMessage: 'Color',
|
||||
defaultMessage: 'Color mapping',
|
||||
})}
|
||||
>
|
||||
<PalettePanelContainer
|
||||
palette={displayStops.map(({ color }) => color)}
|
||||
siblingRef={props.panelRef}
|
||||
isInlineEditing={isInlineEditing}
|
||||
title={i18n.translate('xpack.lens.paletteMetricGradient.label', {
|
||||
defaultMessage: 'Color mapping',
|
||||
})}
|
||||
>
|
||||
<CustomizablePalette
|
||||
palettes={props.paletteService}
|
||||
|
|
|
@ -136,7 +136,7 @@ describe('dimension editor', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
const colorModeGroup = screen.queryByRole('group', { name: /color mode/i });
|
||||
const colorModeGroup = screen.queryByRole('group', { name: /Color by value/i });
|
||||
const staticColorPicker = screen.queryByTestId(SELECTORS.COLOR_PICKER);
|
||||
|
||||
const typeColor = (color: string) => {
|
||||
|
@ -170,6 +170,7 @@ describe('dimension editor', () => {
|
|||
expect(screen.queryByTestId(SELECTORS.MAX_EDITOR)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(SELECTORS.BREAKDOWN_EDITOR)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Color mode switch is shown when the primary metric is numeric', () => {
|
||||
const { colorModeGroup } = renderPrimaryMetricEditor();
|
||||
expect(colorModeGroup).toBeInTheDocument();
|
||||
|
|
|
@ -258,15 +258,15 @@ function PrimaryMetricEditor(props: SubProps) {
|
|||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.lens.metric.dynamicColoring.label', {
|
||||
defaultMessage: 'Color mode',
|
||||
label={i18n.translate('xpack.lens.metric.colorByValue.label', {
|
||||
defaultMessage: 'Color by value',
|
||||
})}
|
||||
>
|
||||
<EuiButtonGroup
|
||||
isFullWidth
|
||||
buttonSize="compressed"
|
||||
legend={i18n.translate('xpack.lens.metric.colorMode.label', {
|
||||
defaultMessage: 'Color mode',
|
||||
legend={i18n.translate('xpack.lens.metric.colorByValue.label', {
|
||||
defaultMessage: 'Color by value',
|
||||
})}
|
||||
data-test-subj="lnsMetric_color_mode_buttons"
|
||||
options={[
|
||||
|
@ -319,7 +319,7 @@ function PrimaryMetricEditor(props: SubProps) {
|
|||
display="columnCompressed"
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.lens.paletteMetricGradient.label', {
|
||||
defaultMessage: 'Color',
|
||||
defaultMessage: 'Color mapping',
|
||||
})}
|
||||
>
|
||||
<PalettePanelContainer
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
DEFAULT_COLOR_MAPPING_CONFIG,
|
||||
PaletteRegistry,
|
||||
ColorMapping,
|
||||
SPECIAL_TOKENS_STRING_CONVERTION,
|
||||
SPECIAL_TOKENS_STRING_CONVERSION,
|
||||
AVAILABLE_PALETTES,
|
||||
getColorsFromMapping,
|
||||
} from '@kbn/coloring';
|
||||
|
@ -127,7 +127,7 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
{props.accessor === firstNonCollapsedColumnId && (
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
label={i18n.translate('xpack.lens.colorMapping.editColorMappingSectionlabel', {
|
||||
label={i18n.translate('xpack.lens.colorMapping.editColorMappingSectionLabel', {
|
||||
defaultMessage: 'Color mapping',
|
||||
})}
|
||||
style={{ alignItems: 'center' }}
|
||||
|
@ -139,7 +139,7 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
title={
|
||||
useNewColorMapping
|
||||
? i18n.translate('xpack.lens.colorMapping.editColorMappingTitle', {
|
||||
defaultMessage: 'Edit colors by term mapping',
|
||||
defaultMessage: 'Assign colors to terms',
|
||||
})
|
||||
: i18n.translate('xpack.lens.colorMapping.editColorsTitle', {
|
||||
defaultMessage: 'Edit colors',
|
||||
|
@ -188,7 +188,7 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
type: 'categories',
|
||||
categories: splitCategories,
|
||||
}}
|
||||
specialTokens={SPECIAL_TOKENS_STRING_CONVERTION}
|
||||
specialTokens={SPECIAL_TOKENS_STRING_CONVERSION}
|
||||
/>
|
||||
) : (
|
||||
<PalettePicker
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
CategoricalColorMapping,
|
||||
DEFAULT_COLOR_MAPPING_CONFIG,
|
||||
ColorMapping,
|
||||
SPECIAL_TOKENS_STRING_CONVERTION,
|
||||
SPECIAL_TOKENS_STRING_CONVERSION,
|
||||
PaletteOutput,
|
||||
AVAILABLE_PALETTES,
|
||||
getColorsFromMapping,
|
||||
|
@ -82,7 +82,7 @@ export function TagsDimensionEditor({
|
|||
return (
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
label={i18n.translate('xpack.lens.colorMapping.editColorMappingSectionlabel', {
|
||||
label={i18n.translate('xpack.lens.colorMapping.editColorMappingSectionLabel', {
|
||||
defaultMessage: 'Color mapping',
|
||||
})}
|
||||
style={{ alignItems: 'center' }}
|
||||
|
@ -94,7 +94,7 @@ export function TagsDimensionEditor({
|
|||
title={
|
||||
useNewColorMapping
|
||||
? i18n.translate('xpack.lens.colorMapping.editColorMappingTitle', {
|
||||
defaultMessage: 'Edit colors by term mapping',
|
||||
defaultMessage: 'Assign colors to terms',
|
||||
})
|
||||
: i18n.translate('xpack.lens.colorMapping.editColorsTitle', {
|
||||
defaultMessage: 'Edit colors',
|
||||
|
@ -141,7 +141,7 @@ export function TagsDimensionEditor({
|
|||
type: 'categories',
|
||||
categories: splitCategories,
|
||||
}}
|
||||
specialTokens={SPECIAL_TOKENS_STRING_CONVERTION}
|
||||
specialTokens={SPECIAL_TOKENS_STRING_CONVERSION}
|
||||
/>
|
||||
) : (
|
||||
<PalettePicker
|
||||
|
|
|
@ -704,14 +704,14 @@ export const getXyVisualization = ({
|
|||
paletteService,
|
||||
};
|
||||
|
||||
const darkMode: boolean = useObservable(kibanaTheme.theme$, { darkMode: false }).darkMode;
|
||||
const isDarkMode: boolean = useObservable(kibanaTheme.theme$, { darkMode: false }).darkMode;
|
||||
const layer = props.state.layers.find((l) => l.layerId === props.layerId)!;
|
||||
const dimensionEditor = isReferenceLayer(layer) ? (
|
||||
<ReferenceLinePanel {...allProps} />
|
||||
) : isAnnotationsLayer(layer) ? (
|
||||
<AnnotationsPanel {...allProps} dataViewsService={dataViewsService} />
|
||||
) : (
|
||||
<DataDimensionEditor {...allProps} darkMode={darkMode} />
|
||||
<DataDimensionEditor {...allProps} isDarkMode={isDarkMode} />
|
||||
);
|
||||
|
||||
return dimensionEditor;
|
||||
|
|
|
@ -5,81 +5,68 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useDebouncedValue } from '@kbn/visualization-utils';
|
||||
import { ColorPicker } from '@kbn/visualization-ui-components';
|
||||
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiButtonGroup,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiText,
|
||||
htmlIdGenerator,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
PaletteRegistry,
|
||||
ColorMapping,
|
||||
DEFAULT_COLOR_MAPPING_CONFIG,
|
||||
CategoricalColorMapping,
|
||||
PaletteOutput,
|
||||
SPECIAL_TOKENS_STRING_CONVERTION,
|
||||
AVAILABLE_PALETTES,
|
||||
getColorsFromMapping,
|
||||
} from '@kbn/coloring';
|
||||
import { EuiButtonGroup, EuiFormRow, htmlIdGenerator } from '@elastic/eui';
|
||||
import { PaletteRegistry, ColorMapping, PaletteOutput } from '@kbn/coloring';
|
||||
import { getColorCategories } from '@kbn/chart-expressions-common';
|
||||
import type { ValuesType } from 'utility-types';
|
||||
import type { VisualizationDimensionEditorProps } from '../../../types';
|
||||
import { State, XYState, XYDataLayerConfig, YConfig, YAxisMode } from '../types';
|
||||
import { FormatFactory } from '../../../../common/types';
|
||||
import { getSeriesColor, isHorizontalChart } from '../state_helpers';
|
||||
import { PalettePanelContainer, PalettePicker } from '../../../shared_components';
|
||||
import { getDataLayers } from '../visualization_helpers';
|
||||
import { CollapseSetting } from '../../../shared_components/collapse_setting';
|
||||
import { getSortedAccessors } from '../to_expression';
|
||||
import { getColorAssignments, getAssignedColorConfig } from '../color_assignment';
|
||||
import { trackUiCounterEvents } from '../../../lens_ui_telemetry';
|
||||
|
||||
type UnwrapArray<T> = T extends Array<infer P> ? P : T;
|
||||
|
||||
export function updateLayer(
|
||||
state: State,
|
||||
layer: UnwrapArray<State['layers']>,
|
||||
index: number
|
||||
): State {
|
||||
const newLayers = [...state.layers];
|
||||
newLayers[index] = layer;
|
||||
|
||||
return {
|
||||
...state,
|
||||
layers: newLayers,
|
||||
};
|
||||
}
|
||||
import { ColorMappingByTerms } from '../../../shared_components/coloring/color_mapping_by_terms';
|
||||
|
||||
export const idPrefix = htmlIdGenerator()();
|
||||
|
||||
function updateLayer(
|
||||
state: State,
|
||||
index: number,
|
||||
layer: ValuesType<State['layers']>,
|
||||
newLayer: Partial<ValuesType<State['layers']>>
|
||||
): State['layers'] {
|
||||
const newLayers = [...state.layers];
|
||||
newLayers[index] = {
|
||||
...layer,
|
||||
...newLayer,
|
||||
} as ValuesType<State['layers']>;
|
||||
|
||||
return newLayers;
|
||||
}
|
||||
|
||||
export function DataDimensionEditor(
|
||||
props: VisualizationDimensionEditorProps<State> & {
|
||||
formatFactory: FormatFactory;
|
||||
paletteService: PaletteRegistry;
|
||||
darkMode: boolean;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
) {
|
||||
const { state, layerId, accessor, darkMode, isInlineEditing } = props;
|
||||
const { state, layerId, accessor, isDarkMode, isInlineEditing } = props;
|
||||
const index = state.layers.findIndex((l) => l.layerId === layerId);
|
||||
const layer = state.layers[index] as XYDataLayerConfig;
|
||||
const canUseColorMapping = layer.colorMapping ? true : false;
|
||||
|
||||
const [useNewColorMapping, setUseNewColorMapping] = useState(canUseColorMapping);
|
||||
|
||||
const { inputValue: localState, handleInputChange: setLocalState } = useDebouncedValue<XYState>({
|
||||
value: props.state,
|
||||
onChange: props.setState,
|
||||
});
|
||||
|
||||
const updateLayerState = useCallback(
|
||||
(layerIndex: number, newLayer: Partial<ValuesType<State['layers']>>) => {
|
||||
setLocalState({
|
||||
...localState,
|
||||
layers: updateLayer(localState, layerIndex, layer, newLayer),
|
||||
});
|
||||
},
|
||||
[layer, setLocalState, localState]
|
||||
);
|
||||
|
||||
const localYConfig = layer?.yConfig?.find((yAxisConfig) => yAxisConfig.forAccessor === accessor);
|
||||
const axisMode = localYConfig?.axisMode || 'auto';
|
||||
|
||||
|
@ -100,22 +87,22 @@ export function DataDimensionEditor(
|
|||
...yConfig,
|
||||
});
|
||||
}
|
||||
setLocalState(updateLayer(localState, { ...layer, yConfig: newYConfigs }, index));
|
||||
updateLayerState(index, { yConfig: newYConfigs });
|
||||
},
|
||||
[accessor, index, localState, layer, setLocalState]
|
||||
[layer.yConfig, updateLayerState, index, accessor]
|
||||
);
|
||||
|
||||
const setColorMapping = useCallback(
|
||||
(colorMapping?: ColorMapping.Config) => {
|
||||
setLocalState(updateLayer(localState, { ...layer, colorMapping }, index));
|
||||
updateLayerState(index, { colorMapping });
|
||||
},
|
||||
[index, localState, layer, setLocalState]
|
||||
[updateLayerState, index]
|
||||
);
|
||||
const setPalette = useCallback(
|
||||
(palette: PaletteOutput) => {
|
||||
setLocalState(updateLayer(localState, { ...layer, palette }, index));
|
||||
updateLayerState(index, { palette });
|
||||
},
|
||||
[index, localState, layer, setLocalState]
|
||||
[updateLayerState, index]
|
||||
);
|
||||
|
||||
const overwriteColor = getSeriesColor(layer, accessor);
|
||||
|
@ -143,105 +130,28 @@ export function DataDimensionEditor(
|
|||
).color;
|
||||
}, [props.frame, props.paletteService, state.layers, accessor, props.formatFactory, layer]);
|
||||
|
||||
const localLayer: XYDataLayerConfig = layer;
|
||||
|
||||
const colors = layer.colorMapping
|
||||
? getColorsFromMapping(props.darkMode, layer.colorMapping)
|
||||
: props.paletteService
|
||||
.get(layer.palette?.name || 'default')
|
||||
.getCategoricalColors(10, layer.palette);
|
||||
|
||||
const table = props.frame.activeData?.[layer.layerId];
|
||||
const { splitAccessor } = layer;
|
||||
const splitCategories = getColorCategories(table?.rows ?? [], splitAccessor);
|
||||
|
||||
if (props.groupId === 'breakdown' && !layer.collapseFn) {
|
||||
return (
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
label={i18n.translate('xpack.lens.colorMapping.editColorMappingSectionlabel', {
|
||||
defaultMessage: 'Color mapping',
|
||||
})}
|
||||
style={{ alignItems: 'center' }}
|
||||
fullWidth
|
||||
>
|
||||
<PalettePanelContainer
|
||||
palette={colors}
|
||||
siblingRef={props.panelRef}
|
||||
title={
|
||||
useNewColorMapping
|
||||
? i18n.translate('xpack.lens.colorMapping.editColorMappingTitle', {
|
||||
defaultMessage: 'Edit colors by term mapping',
|
||||
})
|
||||
: i18n.translate('xpack.lens.colorMapping.editColorsTitle', {
|
||||
defaultMessage: 'Edit colors',
|
||||
})
|
||||
}
|
||||
isInlineEditing={isInlineEditing}
|
||||
>
|
||||
<div className="lnsPalettePanel__section lnsPalettePanel__section--shaded lnsIndexPatternDimensionEditor--padded">
|
||||
<EuiFlexGroup direction="column" gutterSize="s" justifyContent="flexStart">
|
||||
<EuiFlexItem>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<EuiText size="xs">
|
||||
<span>
|
||||
{i18n.translate('xpack.lens.colorMapping.tryLabel', {
|
||||
defaultMessage: 'Use the new Color Mapping feature',
|
||||
})}{' '}
|
||||
<EuiBadge color="hollow">
|
||||
{i18n.translate('xpack.lens.colorMapping.techPreviewLabel', {
|
||||
defaultMessage: 'Tech preview',
|
||||
})}
|
||||
</EuiBadge>
|
||||
</span>
|
||||
</EuiText>
|
||||
}
|
||||
data-test-subj="lns_colorMappingOrLegacyPalette_switch"
|
||||
compressed
|
||||
checked={useNewColorMapping}
|
||||
onChange={({ target: { checked } }) => {
|
||||
trackUiCounterEvents(
|
||||
`color_mapping_switch_${checked ? 'enabled' : 'disabled'}`
|
||||
);
|
||||
setColorMapping(checked ? { ...DEFAULT_COLOR_MAPPING_CONFIG } : undefined);
|
||||
setUseNewColorMapping(checked);
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{canUseColorMapping || useNewColorMapping ? (
|
||||
<CategoricalColorMapping
|
||||
isDarkMode={darkMode}
|
||||
model={layer.colorMapping ?? { ...DEFAULT_COLOR_MAPPING_CONFIG }}
|
||||
onModelUpdate={(model: ColorMapping.Config) => setColorMapping(model)}
|
||||
palettes={AVAILABLE_PALETTES}
|
||||
data={{
|
||||
type: 'categories',
|
||||
categories: splitCategories,
|
||||
}}
|
||||
specialTokens={SPECIAL_TOKENS_STRING_CONVERTION}
|
||||
/>
|
||||
) : (
|
||||
<PalettePicker
|
||||
palettes={props.paletteService}
|
||||
activePalette={localLayer?.palette}
|
||||
setPalette={(newPalette) => {
|
||||
setPalette(newPalette);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</PalettePanelContainer>
|
||||
</EuiFormRow>
|
||||
<ColorMappingByTerms
|
||||
isDarkMode={isDarkMode}
|
||||
colorMapping={layer.colorMapping}
|
||||
palette={layer.palette}
|
||||
isInlineEditing={isInlineEditing}
|
||||
setPalette={setPalette}
|
||||
setColorMapping={setColorMapping}
|
||||
paletteService={props.paletteService}
|
||||
panelRef={props.panelRef}
|
||||
categories={splitCategories}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const isHorizontal = isHorizontalChart(state.layers);
|
||||
const disabledMessage = Boolean(!localLayer.collapseFn && localLayer.splitAccessor)
|
||||
const disabledMessage = Boolean(!layer.collapseFn && layer.splitAccessor)
|
||||
? i18n.translate('xpack.lens.xyChart.colorPicker.tooltip.disabled', {
|
||||
defaultMessage:
|
||||
'You are unable to apply custom colors to individual series when the layer includes a "Break down by" field.',
|
||||
|
@ -329,13 +239,23 @@ export function DataDimensionEditorDataSectionExtra(
|
|||
onChange: props.setState,
|
||||
});
|
||||
|
||||
const updateLayerState = useCallback(
|
||||
(layerIndex: number, newLayer: Partial<ValuesType<State['layers']>>) => {
|
||||
setLocalState({
|
||||
...localState,
|
||||
layers: updateLayer(localState, layerIndex, layer, newLayer),
|
||||
});
|
||||
},
|
||||
[layer, setLocalState, localState]
|
||||
);
|
||||
|
||||
if (props.groupId === 'breakdown') {
|
||||
return (
|
||||
<>
|
||||
<CollapseSetting
|
||||
value={layer.collapseFn || ''}
|
||||
onChange={(collapseFn) => {
|
||||
setLocalState(updateLayer(localState, { ...layer, collapseFn }, index));
|
||||
updateLayerState(index, { collapseFn });
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -272,7 +272,7 @@ describe('XY Config panels', () => {
|
|||
addLayer={jest.fn()}
|
||||
removeLayer={jest.fn()}
|
||||
datasource={{} as DatasourcePublicAPI}
|
||||
darkMode={false}
|
||||
isDarkMode={false}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -300,7 +300,7 @@ describe('XY Config panels', () => {
|
|||
addLayer={jest.fn()}
|
||||
removeLayer={jest.fn()}
|
||||
datasource={{} as DatasourcePublicAPI}
|
||||
darkMode={false}
|
||||
isDarkMode={false}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -349,7 +349,7 @@ describe('XY Config panels', () => {
|
|||
addLayer={jest.fn()}
|
||||
removeLayer={jest.fn()}
|
||||
datasource={{} as DatasourcePublicAPI}
|
||||
darkMode={false}
|
||||
isDarkMode={false}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -395,7 +395,7 @@ describe('XY Config panels', () => {
|
|||
addLayer={jest.fn()}
|
||||
removeLayer={jest.fn()}
|
||||
datasource={{} as DatasourcePublicAPI}
|
||||
darkMode={false}
|
||||
isDarkMode={false}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -441,7 +441,7 @@ describe('XY Config panels', () => {
|
|||
addLayer={jest.fn()}
|
||||
removeLayer={jest.fn()}
|
||||
datasource={{} as DatasourcePublicAPI}
|
||||
darkMode={false}
|
||||
isDarkMode={false}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -25109,7 +25109,7 @@
|
|||
"xpack.lens.collapse.min": "Min.",
|
||||
"xpack.lens.collapse.none": "Aucun",
|
||||
"xpack.lens.collapse.sum": "Somme",
|
||||
"xpack.lens.colorMapping.editColorMappingSectionlabel": "Mapping des couleurs",
|
||||
"xpack.lens.colorMapping.editColorMappingSectionLabel": "Mapping des couleurs",
|
||||
"xpack.lens.colorMapping.editColorMappingTitle": "Modifier les couleurs par mapping de terme",
|
||||
"xpack.lens.colorMapping.editColors": "Modifier les couleurs",
|
||||
"xpack.lens.colorMapping.editColorsTitle": "Modifier les couleurs",
|
||||
|
@ -25737,9 +25737,7 @@
|
|||
"xpack.lens.metric.breakdownBy": "Répartir par",
|
||||
"xpack.lens.metric.color": "Couleur",
|
||||
"xpack.lens.metric.colorMode.dynamic": "Dynamique",
|
||||
"xpack.lens.metric.colorMode.label": "Mode couleur",
|
||||
"xpack.lens.metric.colorMode.static": "Statique",
|
||||
"xpack.lens.metric.dynamicColoring.label": "Mode couleur",
|
||||
"xpack.lens.metric.groupLabel": "Valeur d’objectif et unique",
|
||||
"xpack.lens.metric.headingLabel": "Valeur",
|
||||
"xpack.lens.metric.icon": "Décoration de l’icône",
|
||||
|
@ -25795,7 +25793,6 @@
|
|||
"xpack.lens.paletteHeatmapGradient.label": "Couleur",
|
||||
"xpack.lens.paletteMetricGradient.label": "Couleur",
|
||||
"xpack.lens.palettePicker.label": "Palette",
|
||||
"xpack.lens.paletteTableGradient.label": "Couleur",
|
||||
"xpack.lens.pie.addLayer": "Visualisation",
|
||||
"xpack.lens.pie.arrayValues": "Les dimensions suivantes contiennent des valeurs de tableau : {label}. Le rendu de votre visualisation peut ne pas se présenter comme attendu.",
|
||||
"xpack.lens.pie.collapsedDimensionsDontCount": "(Les dimensions réduites ne sont pas concernées par cette limite.)",
|
||||
|
|
|
@ -25098,7 +25098,7 @@
|
|||
"xpack.lens.collapse.min": "最低",
|
||||
"xpack.lens.collapse.none": "なし",
|
||||
"xpack.lens.collapse.sum": "合計",
|
||||
"xpack.lens.colorMapping.editColorMappingSectionlabel": "カラーマッピング",
|
||||
"xpack.lens.colorMapping.editColorMappingSectionLabel": "カラーマッピング",
|
||||
"xpack.lens.colorMapping.editColorMappingTitle": "用語マッピングで色を編集",
|
||||
"xpack.lens.colorMapping.editColors": "色を編集",
|
||||
"xpack.lens.colorMapping.editColorsTitle": "色を編集",
|
||||
|
@ -25725,9 +25725,7 @@
|
|||
"xpack.lens.metric.breakdownBy": "分類する",
|
||||
"xpack.lens.metric.color": "色",
|
||||
"xpack.lens.metric.colorMode.dynamic": "動的",
|
||||
"xpack.lens.metric.colorMode.label": "色モード",
|
||||
"xpack.lens.metric.colorMode.static": "静的",
|
||||
"xpack.lens.metric.dynamicColoring.label": "色モード",
|
||||
"xpack.lens.metric.groupLabel": "目標値と単一の値",
|
||||
"xpack.lens.metric.headingLabel": "値",
|
||||
"xpack.lens.metric.icon": "アイコン装飾",
|
||||
|
@ -25783,7 +25781,6 @@
|
|||
"xpack.lens.paletteHeatmapGradient.label": "色",
|
||||
"xpack.lens.paletteMetricGradient.label": "色",
|
||||
"xpack.lens.palettePicker.label": "パレット",
|
||||
"xpack.lens.paletteTableGradient.label": "色",
|
||||
"xpack.lens.pie.addLayer": "ビジュアライゼーション",
|
||||
"xpack.lens.pie.arrayValues": "次のディメンションには配列値があります:{label}。可視化が想定通りに表示されない場合があります。",
|
||||
"xpack.lens.pie.collapsedDimensionsDontCount": "(折りたたまれたディメンションはこの制限に対してカウントされません。)",
|
||||
|
|
|
@ -25129,7 +25129,7 @@
|
|||
"xpack.lens.collapse.min": "最小值",
|
||||
"xpack.lens.collapse.none": "无",
|
||||
"xpack.lens.collapse.sum": "求和",
|
||||
"xpack.lens.colorMapping.editColorMappingSectionlabel": "颜色映射",
|
||||
"xpack.lens.colorMapping.editColorMappingSectionLabel": "颜色映射",
|
||||
"xpack.lens.colorMapping.editColorMappingTitle": "按词映射编辑颜色",
|
||||
"xpack.lens.colorMapping.editColors": "编辑颜色",
|
||||
"xpack.lens.colorMapping.editColorsTitle": "编辑颜色",
|
||||
|
@ -25757,9 +25757,7 @@
|
|||
"xpack.lens.metric.breakdownBy": "细分方式",
|
||||
"xpack.lens.metric.color": "颜色",
|
||||
"xpack.lens.metric.colorMode.dynamic": "动态",
|
||||
"xpack.lens.metric.colorMode.label": "颜色模式",
|
||||
"xpack.lens.metric.colorMode.static": "静态",
|
||||
"xpack.lens.metric.dynamicColoring.label": "颜色模式",
|
||||
"xpack.lens.metric.groupLabel": "目标值和单值",
|
||||
"xpack.lens.metric.headingLabel": "值",
|
||||
"xpack.lens.metric.icon": "图标装饰",
|
||||
|
@ -25815,7 +25813,6 @@
|
|||
"xpack.lens.paletteHeatmapGradient.label": "颜色",
|
||||
"xpack.lens.paletteMetricGradient.label": "颜色",
|
||||
"xpack.lens.palettePicker.label": "调色板",
|
||||
"xpack.lens.paletteTableGradient.label": "颜色",
|
||||
"xpack.lens.pie.addLayer": "可视化",
|
||||
"xpack.lens.pie.arrayValues": "以下维度包含数组值:{label}。您的可视化可能无法正常渲染。",
|
||||
"xpack.lens.pie.collapsedDimensionsDontCount": "(折叠的维度不计入此限制。)",
|
||||
|
|
|
@ -88,7 +88,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
|
||||
describe('nick by reference', () => {
|
||||
describe('by reference', () => {
|
||||
const VIS_LIBRARY_DESCRIPTION = 'Vis library description';
|
||||
|
||||
let count = 0;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { DebugState } from '@elastic/charts';
|
||||
import expect from '@kbn/expect';
|
||||
import chroma from 'chroma-js';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
|
@ -51,73 +52,104 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
});
|
||||
|
||||
it('should sync colors on dashboard by default', async function () {
|
||||
it('should sync colors on dashboard for legacy default palette', async function () {
|
||||
await PageObjects.dashboard.navigateToApp();
|
||||
await elasticChart.setNewChartUiDebugFlag(true);
|
||||
await PageObjects.dashboard.clickCreateDashboardPrompt();
|
||||
|
||||
// create non-filtered xy chart
|
||||
await dashboardAddPanel.clickCreateNewLink();
|
||||
await PageObjects.lens.goToTimeRange();
|
||||
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
|
||||
operation: 'count',
|
||||
field: 'Records',
|
||||
});
|
||||
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension',
|
||||
operation: 'terms',
|
||||
field: 'geo.src',
|
||||
palette: { mode: 'legacy', id: 'default' },
|
||||
});
|
||||
|
||||
await PageObjects.lens.save('vis1', false, true);
|
||||
await PageObjects.lens.saveAndReturn();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await dashboardAddPanel.clickCreateNewLink();
|
||||
|
||||
// create filtered xy chart
|
||||
await dashboardAddPanel.clickCreateNewLink();
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
|
||||
operation: 'count',
|
||||
field: 'Records',
|
||||
});
|
||||
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension',
|
||||
operation: 'terms',
|
||||
field: 'geo.src',
|
||||
palette: { mode: 'legacy', id: 'default' },
|
||||
});
|
||||
|
||||
await filterBar.addFilter({ field: 'geo.src', operation: 'is not', value: 'CN' });
|
||||
await PageObjects.lens.saveAndReturn();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
await PageObjects.lens.save('vis2', false, true);
|
||||
// create datatable vis
|
||||
await dashboardAddPanel.clickCreateNewLink();
|
||||
await PageObjects.lens.switchToVisualization('lnsDatatable');
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsDatatable_rows > lns-empty-dimension',
|
||||
operation: 'terms',
|
||||
field: 'geo.src',
|
||||
keepOpen: true,
|
||||
});
|
||||
await PageObjects.lens.setTermsNumberOfValues(5);
|
||||
await PageObjects.lens.setTableDynamicColoring('cell');
|
||||
await PageObjects.lens.setPalette('default', true);
|
||||
await PageObjects.lens.closeDimensionEditor();
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsDatatable_metrics > lns-empty-dimension',
|
||||
operation: 'count',
|
||||
field: 'Records',
|
||||
});
|
||||
await PageObjects.lens.saveAndReturn();
|
||||
|
||||
// Set dashboard to sync colors
|
||||
await PageObjects.dashboard.openSettingsFlyout();
|
||||
await dashboardSettings.toggleSyncColors(true);
|
||||
await dashboardSettings.clickApplyButton();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
|
||||
const colorMapping1 = getColorMapping(await PageObjects.dashboard.getPanelChartDebugState(0));
|
||||
const colorMapping2 = getColorMapping(await PageObjects.dashboard.getPanelChartDebugState(1));
|
||||
const colorMappings1 = Object.entries(
|
||||
getColorMapping(await PageObjects.dashboard.getPanelChartDebugState(0))
|
||||
);
|
||||
const colorMappings2 = Object.entries(
|
||||
getColorMapping(await PageObjects.dashboard.getPanelChartDebugState(1))
|
||||
);
|
||||
|
||||
expect(Object.keys(colorMapping1)).to.have.length(6);
|
||||
expect(Object.keys(colorMapping1)).to.have.length(6);
|
||||
const panel1Keys = ['CN'];
|
||||
const panel2Keys = ['PK'];
|
||||
const sharedKeys = ['IN', 'US', 'ID', 'BR', 'Other'];
|
||||
// colors for keys exclusive to panel 1 should not occur in panel 2
|
||||
panel1Keys.forEach((panel1Key) => {
|
||||
const assignedColor = colorMapping1[panel1Key];
|
||||
expect(Object.values(colorMapping2)).not.to.contain(assignedColor);
|
||||
const els = await PageObjects.lens.getDatatableCellsByColumn(0);
|
||||
const colorMappings3 = await Promise.all(
|
||||
els.map(async (el) => [
|
||||
await el.getVisibleText(),
|
||||
chroma((await PageObjects.lens.getStylesFromCell(el))['background-color']).hex(), // eui converts hex to rgb
|
||||
])
|
||||
);
|
||||
|
||||
expect(colorMappings1).to.have.length(6);
|
||||
expect(colorMappings2).to.have.length(6);
|
||||
expect(colorMappings3).to.have.length(6);
|
||||
|
||||
const mergedColorAssignments = new Map<string, Set<string>>();
|
||||
|
||||
[...colorMappings1, ...colorMappings2, ...colorMappings3].forEach(([key, color]) => {
|
||||
if (!mergedColorAssignments.has(key)) mergedColorAssignments.set(key, new Set());
|
||||
mergedColorAssignments.get(key)?.add(color);
|
||||
});
|
||||
// colors for keys exclusive to panel 2 should not occur in panel 1
|
||||
panel2Keys.forEach((panel2Key) => {
|
||||
const assignedColor = colorMapping2[panel2Key];
|
||||
expect(Object.values(colorMapping1)).not.to.contain(assignedColor);
|
||||
});
|
||||
// colors for keys used in both panels should be synced
|
||||
sharedKeys.forEach((sharedKey) => {
|
||||
expect(colorMapping1[sharedKey]).to.eql(colorMapping2[sharedKey]);
|
||||
|
||||
// Each key should have only been assigned one color across all 3 visualizations
|
||||
mergedColorAssignments.forEach((colors, key) => {
|
||||
expect(colors.size).eql(
|
||||
1,
|
||||
`Key "${key}" was assigned multiple colors: ${JSON.stringify([...colors])}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -173,7 +173,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
},
|
||||
|
||||
/**
|
||||
* Changes the specified dimension to the specified operation and (optinally) field.
|
||||
* Changes the specified dimension to the specified operation and optionally the field.
|
||||
*
|
||||
* @param opts.dimension - the selector of the dimension being changed
|
||||
* @param opts.operation - the desired operation ID for the dimension
|
||||
|
@ -1129,8 +1129,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
return el.getVisibleText();
|
||||
},
|
||||
|
||||
async getDatatableCellStyle(rowIndex = 0, colIndex = 0) {
|
||||
const el = await this.getDatatableCell(rowIndex, colIndex);
|
||||
async getStylesFromCell(el: WebElementWrapper) {
|
||||
const styleString = (await el.getAttribute('style')) ?? '';
|
||||
return styleString.split(';').reduce<Record<string, string>>((memo, cssLine) => {
|
||||
const [prop, value] = cssLine.split(':');
|
||||
|
@ -1141,6 +1140,11 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
}, {});
|
||||
},
|
||||
|
||||
async getDatatableCellStyle(rowIndex = 0, colIndex = 0) {
|
||||
const el = await this.getDatatableCell(rowIndex, colIndex);
|
||||
return this.getStylesFromCell(el);
|
||||
},
|
||||
|
||||
async getDatatableCellSpanStyle(rowIndex = 0, colIndex = 0) {
|
||||
const el = await (await this.getDatatableCell(rowIndex, colIndex)).findByCssSelector('span');
|
||||
const styleString = (await el.getAttribute('style')) ?? '';
|
||||
|
@ -1174,6 +1178,12 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
);
|
||||
},
|
||||
|
||||
async getDatatableCellsByColumn(colIndex = 0) {
|
||||
return await find.allByCssSelector(
|
||||
`[data-test-subj="lnsDataTable"] [data-test-subj="dataGridRowCell"][data-gridcell-column-index="${colIndex}"]`
|
||||
);
|
||||
},
|
||||
|
||||
async isDatatableHeaderSorted(index = 0) {
|
||||
return find.existsByCssSelector(
|
||||
`[data-test-subj="lnsDataTable"] [data-test-subj="dataGridHeader"] [role=columnheader]:nth-child(${
|
||||
|
@ -1240,10 +1250,15 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
|
||||
async setPalette(paletteId: string, isLegacy: boolean) {
|
||||
await testSubjects.click('lns_colorEditing_trigger');
|
||||
// This action needs to be slowed WAY down, otherwise it will not correctly set the palette
|
||||
await PageObjects.common.sleep(200);
|
||||
await testSubjects.setEuiSwitch(
|
||||
'lns_colorMappingOrLegacyPalette_switch',
|
||||
isLegacy ? 'uncheck' : 'check'
|
||||
);
|
||||
|
||||
await PageObjects.common.sleep(200);
|
||||
|
||||
if (isLegacy) {
|
||||
await testSubjects.click('lns-palettePicker');
|
||||
await find.clickByCssSelector(`#${paletteId}`);
|
||||
|
@ -1251,6 +1266,8 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
await testSubjects.click('kbnColoring_ColorMapping_PalettePicker');
|
||||
await testSubjects.click(`kbnColoring_ColorMapping_Palette-${paletteId}`);
|
||||
}
|
||||
await PageObjects.common.sleep(200);
|
||||
|
||||
await this.closePaletteEditor();
|
||||
},
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue