diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/README.md b/packages/kbn-coloring/src/shared_components/color_mapping/README.md new file mode 100644 index 0000000000000..220824ca47820 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/README.md @@ -0,0 +1,87 @@ +# Color Mapping + +This shared component can be used to define a color mapping as an association of one or multiple string values to a color definition. + +This package provides: +- a React component, called `CategoricalColorMapping` that provides a simplified UI (that in general can be hosted in a flyout), that helps the user generate a `ColorMapping.Config` object that descibes the mappings configuration +- a function `getColorFactory` that given a color mapping configuration returns a function that maps a passed category to the corresponding color +- a definition scheme for the color mapping, based on the type `ColorMapping.Config`, that provides an extensible way of describing the link between colors and rules. Collects the minimal information required apply colors based on categories. Together with the `ColorMappingInputData` can be used to get colors in a deterministic way. + + +An example of the configuration is the following: +```ts +const DEFAULT_COLOR_MAPPING_CONFIG: ColorMapping.Config = { + assignmentMode: 'auto', + assignments: [ + { + rule: { + type: 'matchExactly', + values: ['']; + }, + color: { + type: 'categorical', + paletteId: 'eui', + colorIndex: 2, + } + } + ], + specialAssignments: [ + { + rule: { + type: 'other', + }, + color: { + type: 'categorical', + paletteId: 'neutral', + colorIndex: 2 + }, + touched: false, + }, + ], + paletteId: EUIPalette.id, + colorMode: { + type: 'categorical', + }, +}; +``` + +The function `getColorFactory` is a curry function where, given the model, a palette getter, the theme mode (dark/light) and a list of categories, returns a function that can be used to pick the right color based on a given category. + +```ts +function getColorFactory( + model: ColorMapping.Config, + getPaletteFn: (paletteId: string) => ColorMapping.CategoricalPalette, + isDarkMode: boolean, + data: { + type: 'categories'; + categories: Array; + } +): (category: string | string[]) => Color +``` + + + +A `category` can be in the shape of a plain string or an array of strings. Numbers, MultiFieldKey, IP etc needs to be stringified. + + +The `CategoricalColorMapping` React component has the following props: + +```tsx +function CategoricalColorMapping(props: { + /** The initial color mapping model, usually coming from a the visualization saved object */ + model: ColorMapping.Config; + /** A map of paletteId and palette configuration */ + palettes: Map; + /** A data description of what needs to be colored */ + data: ColorMappingInputData; + /** Theme dark mode */ + isDarkMode: boolean; + /** A map between original and formatted tokens used to handle special cases, like the Other bucket and the empty bucket */ + specialTokens: Map; + /** A function called at every change in the model */ + onModelUpdate: (model: ColorMapping.Config) => void; +}) + +``` + +the `onModelUpdate` callback is called everytime a change in the model is applied from within the component. Is not called when the `model` prop is updated. \ No newline at end of file diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/__stories__/color_mapping.stories.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/__stories__/color_mapping.stories.tsx new file mode 100644 index 0000000000000..95f4ff5623ea3 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/__stories__/color_mapping.stories.tsx @@ -0,0 +1,132 @@ +/* + * 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 React, { FC } from 'react'; +import { EuiFlyout, EuiForm } from '@elastic/eui'; +import { ComponentStory } from '@storybook/react'; +import { CategoricalColorMapping, ColorMappingProps } from '../categorical_color_mapping'; +import { AVAILABLE_PALETTES } from '../palettes'; +import { DEFAULT_COLOR_MAPPING_CONFIG } from '../config/default_color_mapping'; + +export default { + title: 'Color Mapping', + component: CategoricalColorMapping, + decorators: [ + (story: Function) => ( + {}} hideCloseButton> + {story()} + + ), + ], +}; + +const Template: ComponentStory> = (args) => ( + +); + +export const Default = Template.bind({}); + +Default.args = { + model: { + ...DEFAULT_COLOR_MAPPING_CONFIG, + assignmentMode: 'manual', + colorMode: { + type: 'gradient', + steps: [ + { + type: 'categorical', + colorIndex: 0, + paletteId: DEFAULT_COLOR_MAPPING_CONFIG.paletteId, + touched: false, + }, + { + type: 'categorical', + colorIndex: 1, + paletteId: DEFAULT_COLOR_MAPPING_CONFIG.paletteId, + touched: false, + }, + { + type: 'categorical', + colorIndex: 2, + paletteId: DEFAULT_COLOR_MAPPING_CONFIG.paletteId, + touched: false, + }, + ], + sort: 'asc', + }, + assignments: [ + { + rule: { + type: 'matchExactly', + values: ['this is', 'a multi-line combobox that is very long and that will be truncated'], + }, + color: { + type: 'gradient', + }, + touched: false, + }, + { + rule: { + type: 'matchExactly', + values: ['b', ['double', 'value']], + }, + color: { + type: 'gradient', + }, + touched: false, + }, + { + rule: { + type: 'matchExactly', + values: ['c'], + }, + color: { + type: 'gradient', + }, + touched: false, + }, + { + rule: { + type: 'matchExactly', + values: [ + 'this is', + 'a multi-line wrap', + 'combo box', + 'test combo', + '3 lines', + ['double', 'value'], + ], + }, + color: { + type: 'gradient', + }, + touched: false, + }, + ], + }, + isDarkMode: false, + data: { + type: 'categories', + categories: [ + 'a', + 'b', + 'c', + 'd', + 'this is', + 'a multi-line wrap', + 'combo box', + 'test combo', + '3 lines', + ], + }, + + palettes: AVAILABLE_PALETTES, + specialTokens: new Map(), + // eslint-disable-next-line no-console + onModelUpdate: (model) => console.log(model), +}; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/categorical_color_mapping.test.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/categorical_color_mapping.test.tsx new file mode 100644 index 0000000000000..fe8374d7dcdcd --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/categorical_color_mapping.test.tsx @@ -0,0 +1,115 @@ +/* + * 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 React from 'react'; +import { mount } from 'enzyme'; +import { CategoricalColorMapping, ColorMappingInputData } from './categorical_color_mapping'; +import { AVAILABLE_PALETTES } from './palettes'; +import { DEFAULT_COLOR_MAPPING_CONFIG } from './config/default_color_mapping'; +import { MULTI_FIELD_KEY_SEPARATOR } from '@kbn/data-plugin/common'; + +const AUTO_ASSIGN_SWITCH = '[data-test-subj="lns-colorMapping-autoAssignSwitch"]'; +const ASSIGNMENTS_LIST = '[data-test-subj="lns-colorMapping-assignmentsList"]'; +const ASSIGNMENT_ITEM = (i: number) => `[data-test-subj="lns-colorMapping-assignmentsItem${i}"]`; + +describe('color mapping', () => { + it('load a default color mapping', () => { + const dataInput: ColorMappingInputData = { + type: 'categories', + categories: ['categoryA', 'categoryB'], + }; + const onModelUpdateFn = jest.fn(); + const component = mount( + + ); + + expect(component.find(AUTO_ASSIGN_SWITCH).hostNodes().prop('aria-checked')).toEqual(true); + expect(component.find(ASSIGNMENTS_LIST).hostNodes().children().length).toEqual( + dataInput.categories.length + ); + dataInput.categories.forEach((category, index) => { + const assignment = component.find(ASSIGNMENT_ITEM(index)).hostNodes(); + expect(assignment.text()).toEqual(category); + expect(assignment.hasClass('euiComboBox-isDisabled')).toEqual(true); + }); + expect(onModelUpdateFn).not.toBeCalled(); + }); + + it('switch to manual assignments', () => { + const dataInput: ColorMappingInputData = { + type: 'categories', + categories: ['categoryA', 'categoryB'], + }; + const onModelUpdateFn = jest.fn(); + const component = mount( + + ); + component.find(AUTO_ASSIGN_SWITCH).hostNodes().simulate('click'); + expect(onModelUpdateFn).toBeCalledTimes(1); + expect(component.find(AUTO_ASSIGN_SWITCH).hostNodes().prop('aria-checked')).toEqual(false); + expect(component.find(ASSIGNMENTS_LIST).hostNodes().children().length).toEqual( + dataInput.categories.length + ); + dataInput.categories.forEach((category, index) => { + const assignment = component.find(ASSIGNMENT_ITEM(index)).hostNodes(); + expect(assignment.text()).toEqual(category); + expect(assignment.hasClass('euiComboBox-isDisabled')).toEqual(false); + }); + }); + + it('handle special tokens, multi-fields keys and non-trimmed whitespaces', () => { + const dataInput: ColorMappingInputData = { + type: 'categories', + categories: ['__other__', ['fieldA', 'fieldB'], '__empty__', ' with-whitespaces '], + }; + const onModelUpdateFn = jest.fn(); + const component = mount( + + ); + expect(component.find(ASSIGNMENTS_LIST).hostNodes().children().length).toEqual( + dataInput.categories.length + ); + const assignment1 = component.find(ASSIGNMENT_ITEM(0)).hostNodes(); + expect(assignment1.text()).toEqual('Other'); + + const assignment2 = component.find(ASSIGNMENT_ITEM(1)).hostNodes(); + expect(assignment2.text()).toEqual(`fieldA${MULTI_FIELD_KEY_SEPARATOR}fieldB`); + + const assignment3 = component.find(ASSIGNMENT_ITEM(2)).hostNodes(); + expect(assignment3.text()).toEqual('(Empty)'); + + const assignment4 = component.find(ASSIGNMENT_ITEM(3)).hostNodes(); + expect(assignment4.text()).toEqual(' with-whitespaces '); + }); +}); diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/categorical_color_mapping.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/categorical_color_mapping.tsx new file mode 100644 index 0000000000000..290c549684f90 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/categorical_color_mapping.tsx @@ -0,0 +1,97 @@ +/* + * 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 React from 'react'; +import { Provider } from 'react-redux'; +import { type EnhancedStore, configureStore } from '@reduxjs/toolkit'; +import { isEqual } from 'lodash'; +import { colorMappingReducer, updateModel } from './state/color_mapping'; +import { Container } from './components/container/container'; +import { ColorMapping } from './config'; +import { uiReducer } from './state/ui'; + +/** + * 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; + } + | { + type: 'ranges'; + min: number; + max: number; + bins: number; + }; + +/** + * The props of the CategoricalColorMapping component + */ +export interface ColorMappingProps { + /** The initial color mapping model, usually coming from a the visualization saved object */ + model: ColorMapping.Config; + /** A map of paletteId and palette configuration */ + palettes: Map; + /** A data description of what needs to be colored */ + data: ColorMappingInputData; + /** Theme dark mode */ + isDarkMode: boolean; + /** A map between original and formatted tokens used to handle special cases, like the Other bucket and the empty bucket */ + specialTokens: Map; + /** A function called at every change in the model */ + onModelUpdate: (model: ColorMapping.Config) => void; +} + +/** + * The React component for mapping categorical values to colors + */ +export class CategoricalColorMapping extends React.Component { + store: EnhancedStore<{ colorMapping: ColorMapping.Config }>; + unsubscribe: () => void; + constructor(props: ColorMappingProps) { + super(props); + // configure the store at mount time + this.store = configureStore({ + preloadedState: { + colorMapping: props.model, + }, + reducer: { + colorMapping: colorMappingReducer, + ui: uiReducer, + }, + }); + // subscribe to store changes to update external tools + this.unsubscribe = this.store.subscribe(() => { + this.props.onModelUpdate(this.store.getState().colorMapping); + }); + } + componentWillUnmount() { + this.unsubscribe(); + } + componentDidUpdate(prevProps: Readonly) { + if (!isEqual(prevProps.model, this.props.model)) { + this.store.dispatch(updateModel(this.props.model)); + } + } + render() { + const { palettes, data, isDarkMode, specialTokens } = this.props; + return ( + + + + ); + } +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.test.ts b/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.test.ts new file mode 100644 index 0000000000000..93896394daf41 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.test.ts @@ -0,0 +1,294 @@ +/* + * 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 { + DEFAULT_COLOR_MAPPING_CONFIG, + DEFAULT_NEUTRAL_PALETTE_INDEX, +} from '../config/default_color_mapping'; +import { getColorFactory } from './color_handling'; +import { getPalette, AVAILABLE_PALETTES } from '../palettes'; +import { + EUIAmsterdamColorBlindPalette, + EUI_AMSTERDAM_PALETTE_COLORS, +} from '../palettes/eui_amsterdam'; +import { NeutralPalette, NEUTRAL_COLOR_DARK, NEUTRAL_COLOR_LIGHT } from '../palettes/neutral'; +import { toHex } from './color_math'; + +import { ColorMapping } from '../config'; + +describe('Color mapping - color generation', () => { + const getPaletteFn = getPalette(AVAILABLE_PALETTES, NeutralPalette); + it('returns EUI light colors from default config', () => { + const colorFactory = getColorFactory(DEFAULT_COLOR_MAPPING_CONFIG, getPaletteFn, false, { + type: 'categories', + categories: ['catA', 'catB', 'catC'], + }); + expect(colorFactory('catA')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]); + expect(colorFactory('catB')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[1]); + expect(colorFactory('catC')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[2]); + // if the category is not available in the `categories` list then a default neutral color is used + expect(colorFactory('not_available')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]); + }); + + it('returns max number of colors defined in palette, use other color otherwise', () => { + const twoColorPalette: ColorMapping.CategoricalPalette = { + id: 'twoColors', + name: 'twoColors', + colorCount: 2, + type: 'categorical', + getColor(valueInRange, isDarkMode) { + return ['red', 'blue'][valueInRange]; + }, + }; + + const simplifiedGetPaletteGn = getPalette( + new Map([[twoColorPalette.id, twoColorPalette]]), + NeutralPalette + ); + const colorFactory = getColorFactory( + { + ...DEFAULT_COLOR_MAPPING_CONFIG, + paletteId: twoColorPalette.id, + }, + simplifiedGetPaletteGn, + false, + { + type: 'categories', + categories: ['cat1', 'cat2', 'cat3', 'cat4'], + } + ); + expect(colorFactory('cat1')).toBe('#ff0000'); + expect(colorFactory('cat2')).toBe('#0000ff'); + // return a palette color only up to the max number of color in the palette + expect(colorFactory('cat3')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]); + expect(colorFactory('cat4')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]); + }); + + // currently there is no difference in the two colors, but this could change in the future + // this test will catch the change + it('returns EUI dark colors from default config', () => { + const colorFactory = getColorFactory(DEFAULT_COLOR_MAPPING_CONFIG, getPaletteFn, true, { + type: 'categories', + categories: ['catA', 'catB', 'catC'], + }); + expect(colorFactory('catA')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]); + expect(colorFactory('catB')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[1]); + expect(colorFactory('catC')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[2]); + // if the category is not available in the `categories` list then a default neutral color is used + expect(colorFactory('not_available')).toBe(NEUTRAL_COLOR_DARK[DEFAULT_NEUTRAL_PALETTE_INDEX]); + }); + + it('handles special tokens, multi-field categories and non-trimmed whitespaces', () => { + const colorFactory = getColorFactory(DEFAULT_COLOR_MAPPING_CONFIG, getPaletteFn, false, { + type: 'categories', + categories: ['__other__', ['fieldA', 'fieldB'], '__empty__', ' with-whitespaces '], + }); + expect(colorFactory('__other__')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]); + expect(colorFactory(['fieldA', 'fieldB'])).toBe(EUI_AMSTERDAM_PALETTE_COLORS[1]); + expect(colorFactory('__empty__')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[2]); + expect(colorFactory(' with-whitespaces ')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[3]); + }); + + it('ignores configured assignments in auto mode', () => { + const colorFactory = getColorFactory( + { + ...DEFAULT_COLOR_MAPPING_CONFIG, + assignments: [ + { + color: { type: 'colorCode', colorCode: 'red' }, + rule: { type: 'matchExactly', values: ['assignmentToIgnore'] }, + touched: false, + }, + ], + }, + getPaletteFn, + false, + { + type: 'categories', + categories: ['catA', 'catB', 'assignmentToIgnore'], + } + ); + expect(colorFactory('catA')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]); + expect(colorFactory('catB')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[1]); + expect(colorFactory('assignmentToIgnore')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[2]); + }); + + it('color with auto rule are assigned in order of the configured data input', () => { + const colorFactory = getColorFactory( + { + ...DEFAULT_COLOR_MAPPING_CONFIG, + assignmentMode: 'manual', + assignments: [ + { + color: { type: 'colorCode', colorCode: 'red' }, + rule: { type: 'auto' }, + touched: false, + }, + { + color: { type: 'colorCode', colorCode: 'blue' }, + rule: { type: 'matchExactly', values: ['blueCat'] }, + touched: false, + }, + { + color: { type: 'colorCode', colorCode: 'green' }, + rule: { type: 'auto' }, + touched: false, + }, + ], + }, + getPaletteFn, + false, + { + type: 'categories', + categories: ['blueCat', 'redCat', 'greenCat'], + } + ); + // this matches exactly + expect(colorFactory('blueCat')).toBe('blue'); + // this matches with the first availabe "auto" rule + expect(colorFactory('redCat')).toBe('red'); + // this matches with the second availabe "auto" rule + expect(colorFactory('greenCat')).toBe('green'); + // if the category is not available in the `categories` list then a default neutral color is used + expect(colorFactory('not_available')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]); + }); + + it('returns sequential gradient colors from darker to lighter [desc, lightMode]', () => { + const colorFactory = getColorFactory( + { + ...DEFAULT_COLOR_MAPPING_CONFIG, + colorMode: { + type: 'gradient', + steps: [ + { + type: 'categorical', + paletteId: EUIAmsterdamColorBlindPalette.id, + colorIndex: 0, + touched: false, + }, + ], + sort: 'desc', + }, + }, + getPaletteFn, + false, + { + type: 'categories', + categories: ['cat1', 'cat2', 'cat3'], + } + ); + // this matches exactly with the initial step selected + expect(toHex(colorFactory('cat1'))).toBe(toHex(EUI_AMSTERDAM_PALETTE_COLORS[0])); + expect(toHex(colorFactory('cat2'))).toBe('#93cebc'); + expect(toHex(colorFactory('cat3'))).toBe('#cce8e0'); + }); + + it('returns sequential gradient colors from lighter to darker [asc, lightMode]', () => { + const colorFactory = getColorFactory( + { + ...DEFAULT_COLOR_MAPPING_CONFIG, + colorMode: { + type: 'gradient', + steps: [ + { + type: 'categorical', + paletteId: EUIAmsterdamColorBlindPalette.id, + colorIndex: 0, + touched: false, + }, + ], + sort: 'asc', + }, + }, + getPaletteFn, + false, + { + type: 'categories', + categories: ['cat1', 'cat2', 'cat3'], + } + ); + expect(toHex(colorFactory('cat1'))).toBe('#cce8e0'); + expect(toHex(colorFactory('cat2'))).toBe('#93cebc'); + // this matches exactly with the initial step selected + expect(toHex(colorFactory('cat3'))).toBe(toHex(EUI_AMSTERDAM_PALETTE_COLORS[0])); + }); + + it('returns 2 colors gradient [desc, lightMode]', () => { + const colorFactory = getColorFactory( + { + ...DEFAULT_COLOR_MAPPING_CONFIG, + colorMode: { + type: 'gradient', + steps: [ + { + type: 'categorical', + paletteId: EUIAmsterdamColorBlindPalette.id, + colorIndex: 0, + touched: false, + }, + { + type: 'categorical', + paletteId: EUIAmsterdamColorBlindPalette.id, + colorIndex: 2, + touched: false, + }, + ], + sort: 'desc', + }, + }, + getPaletteFn, + false, + { + type: 'categories', + categories: ['cat1', 'cat2', 'cat3'], + } + ); + expect(toHex(colorFactory('cat1'))).toBe(toHex(EUI_AMSTERDAM_PALETTE_COLORS[0])); // EUI green + expect(toHex(colorFactory('cat2'))).toBe('#a4908f'); // red gray green + expect(toHex(colorFactory('cat3'))).toBe(toHex(EUI_AMSTERDAM_PALETTE_COLORS[2])); // EUI pink + }); + + it('returns divergent gradient [asc, darkMode]', () => { + const colorFactory = getColorFactory( + { + ...DEFAULT_COLOR_MAPPING_CONFIG, + colorMode: { + type: 'gradient', + steps: [ + { + type: 'categorical', + paletteId: EUIAmsterdamColorBlindPalette.id, + colorIndex: 0, + touched: false, + }, + { type: 'categorical', paletteId: NeutralPalette.id, colorIndex: 0, touched: false }, + { + type: 'categorical', + paletteId: EUIAmsterdamColorBlindPalette.id, + colorIndex: 2, + touched: false, + }, + ], + sort: 'asc', // testing in ascending order + }, + }, + getPaletteFn, + true, // testing in dark mode + { + type: 'categories', + categories: ['cat1', 'cat2', 'cat3'], + } + ); + expect(toHex(colorFactory('cat1'))).toBe(toHex(EUI_AMSTERDAM_PALETTE_COLORS[2])); // EUI pink + expect(toHex(colorFactory('cat2'))).toBe(NEUTRAL_COLOR_DARK[0]); // NEUTRAL LIGHT GRAY + expect(toHex(colorFactory('cat3'))).toBe(toHex(EUI_AMSTERDAM_PALETTE_COLORS[0])); // EUI green + expect(toHex(colorFactory('not available cat'))).toBe( + toHex(NEUTRAL_COLOR_DARK[DEFAULT_NEUTRAL_PALETTE_INDEX]) + ); // check the other + }); +}); diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.ts b/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.ts new file mode 100644 index 0000000000000..795f94b740e9b --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/color/color_handling.ts @@ -0,0 +1,164 @@ +/* + * 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 chroma from 'chroma-js'; +import { ColorMapping } from '../config'; +import { changeAlpha, combineColors, getValidColor } from './color_math'; +import { generateAutoAssignmentsForCategories } from '../config/assignment_from_categories'; +import { getPalette } from '../palettes'; +import { ColorMappingInputData } from '../categorical_color_mapping'; +import { ruleMatch } from './rule_matching'; +import { GradientColorMode } from '../config/types'; + +export function getAssignmentColor( + colorMode: ColorMapping.Config['colorMode'], + color: ColorMapping.Config['assignments'][number]['color'], + getPaletteFn: ReturnType, + isDarkMode: boolean, + index: number, + total: number +) { + switch (color.type) { + case 'colorCode': + case 'categorical': + return getColor(color, getPaletteFn, isDarkMode); + case 'gradient': { + if (colorMode.type === 'categorical') { + return 'red'; + } + const colorScale = getGradientColorScale(colorMode, getPaletteFn, isDarkMode); + return total === 0 ? 'red' : total === 1 ? colorScale(0) : colorScale(index / (total - 1)); + } + } +} + +export function getColor( + color: ColorMapping.ColorCode | ColorMapping.CategoricalColor, + getPaletteFn: ReturnType, + isDarkMode: boolean +) { + return color.type === 'colorCode' + ? color.colorCode + : getValidColor(getPaletteFn(color.paletteId).getColor(color.colorIndex, isDarkMode)).hex(); +} + +export function getColorFactory( + model: ColorMapping.Config, + getPaletteFn: ReturnType, + isDarkMode: boolean, + data: ColorMappingInputData +): (category: string | string[]) => string { + const palette = getPaletteFn(model.paletteId); + // generate on-the-fly assignments in auto-mode based on current data. + // This simplify the code by always using assignments, even if there is no real static assigmnets + const assignments = + model.assignmentMode === 'auto' + ? generateAutoAssignmentsForCategories(data, palette, model.colorMode) + : model.assignments; + + // find auto-assigned colors + const autoAssignedColors = + data.type === 'categories' + ? assignments.filter((a) => { + return ( + a.rule.type === 'auto' || (a.rule.type === 'matchExactly' && a.rule.values.length === 0) + ); + }) + : []; + + // find all categories that doesn't match with an assignment + const nonAssignedCategories = + data.type === 'categories' + ? data.categories.filter((category) => { + return !assignments.some(({ rule }) => ruleMatch(rule, category)); + }) + : []; + + return (category: string | string[]) => { + if (typeof category === 'string' || Array.isArray(category)) { + const nonAssignedCategoryIndex = nonAssignedCategories.indexOf(category); + + // return color for a non assigned category + if (nonAssignedCategoryIndex > -1) { + if (nonAssignedCategoryIndex < autoAssignedColors.length) { + const autoAssignmentIndex = assignments.findIndex( + (d) => d === autoAssignedColors[nonAssignedCategoryIndex] + ); + return getAssignmentColor( + model.colorMode, + autoAssignedColors[nonAssignedCategoryIndex].color, + getPaletteFn, + isDarkMode, + autoAssignmentIndex, + assignments.length + ); + } + // if no auto-assign color rule/color is available then use the other color + // TODO: the specialAssignment[0] position is arbitrary, we should fix it better + return getColor(model.specialAssignments[0].color, getPaletteFn, isDarkMode); + } + + // find the assignment where the category matches the rule + const matchingAssignmentIndex = assignments.findIndex(({ rule }) => { + return ruleMatch(rule, category); + }); + + // return the assigned color + if (matchingAssignmentIndex > -1) { + const assignment = assignments[matchingAssignmentIndex]; + return getAssignmentColor( + model.colorMode, + assignment.color, + getPaletteFn, + isDarkMode, + matchingAssignmentIndex, + assignments.length + ); + } + // if no assign color rule/color is available then use the other color + // TODO: the specialAssignment[0] position is arbitrary, we should fix it better + return getColor(model.specialAssignments[0].color, getPaletteFn, isDarkMode); + } else { + const matchingAssignmentIndex = assignments.findIndex(({ rule }) => { + return ruleMatch(rule, category); + }); + + if (matchingAssignmentIndex > -1) { + const assignment = assignments[matchingAssignmentIndex]; + return getAssignmentColor( + model.colorMode, + assignment.color, + getPaletteFn, + isDarkMode, + matchingAssignmentIndex, + assignments.length + ); + } + return getColor(model.specialAssignments[0].color, getPaletteFn, isDarkMode); + } + }; +} + +export function getGradientColorScale( + colorMode: GradientColorMode, + getPaletteFn: ReturnType, + isDarkMode: boolean +): (value: number) => string { + const steps = + colorMode.steps.length === 1 + ? [ + getColor(colorMode.steps[0], getPaletteFn, isDarkMode), + combineColors( + changeAlpha(getColor(colorMode.steps[0], getPaletteFn, isDarkMode), 0.3), + isDarkMode ? 'black' : 'white' + ), + ] + : colorMode.steps.map((d) => getColor(d, getPaletteFn, isDarkMode)); + steps.sort(() => (colorMode.sort === 'asc' ? -1 : 1)); + const scale = chroma.scale(steps).mode('lab'); + return (value: number) => scale(value).hex(); +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/color/color_math.ts b/packages/kbn-coloring/src/shared_components/color_mapping/color/color_math.ts new file mode 100644 index 0000000000000..eb9e57d52af55 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/color/color_math.ts @@ -0,0 +1,60 @@ +/* + * 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 chroma from 'chroma-js'; + +export function getValidColor(color: string): chroma.Color { + try { + return chroma(color); + } catch { + return chroma('red'); + } +} + +export function hasEnoughContrast(color: string, isDark: boolean, threshold = 4.5) { + return chroma.contrast(getValidColor(color), isDark ? 'black' : 'white') >= threshold; +} + +export function changeAlpha(color: string, alpha: number) { + const [r, g, b] = getValidColor(color).rgb(); + return `rgba(${r},${g},${b},${alpha})`; +} + +export function toHex(color: string) { + return getValidColor(color).hex().toLowerCase(); +} + +export function isSameColor(color1: string, color2: string) { + return toHex(color1) === toHex(color2); +} + +/** + * Blend a foreground (fg) color with a background (bg) color + */ +export function combineColors(fg: string, bg: string): string { + const [fgR, fgG, fgB, fgA] = getValidColor(fg).rgba(); + const [bgR, bgG, bgB, bgA] = getValidColor(bg).rgba(); + + // combine colors only if foreground has transparency + if (fgA === 1) { + return chroma.rgb(fgR, fgG, fgB).hex(); + } + + // For reference on alpha calculations: + // https://en.wikipedia.org/wiki/Alpha_compositing + const alpha = fgA + bgA * (1 - fgA); + + if (alpha === 0) { + return '#00000000'; + } + + const r = Math.round((fgR * fgA + bgR * bgA * (1 - fgA)) / alpha); + const g = Math.round((fgG * fgA + bgG * bgA * (1 - fgA)) / alpha); + const b = Math.round((fgB * fgA + bgB * bgA * (1 - fgA)) / alpha); + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/color/rule_matching.ts b/packages/kbn-coloring/src/shared_components/color_mapping/color/rule_matching.ts new file mode 100644 index 0000000000000..7557644154a52 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/color/rule_matching.ts @@ -0,0 +1,46 @@ +/* + * 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 { ColorMapping } from '../config'; + +export function ruleMatch( + rule: ColorMapping.Config['assignments'][number]['rule'], + value: string | number | string[] +) { + switch (rule.type) { + case 'matchExactly': + if (Array.isArray(value)) { + return rule.values.some( + (v) => + Array.isArray(v) && v.length === value.length && v.every((part, i) => part === value[i]) + ); + } + return rule.values.includes(`${value}`); + case 'matchExactlyCI': + return rule.values.some((d) => d.toLowerCase() === `${value}`); + case 'range': + // TODO: color by value not yet possible in all charts in elastic-charts + return typeof value === 'number' ? rangeMatch(rule, value) : false; + default: + return false; + } +} + +export function rangeMatch(rule: ColorMapping.RuleRange, value: number) { + return ( + (rule.min === rule.max && rule.min === value) || + ((rule.minInclusive ? value >= rule.min : value > rule.min) && + (rule.maxInclusive ? value <= rule.max : value < rule.max)) + ); +} + +// TODO: move in some data/table related package +export const SPECIAL_TOKENS_STRING_CONVERTION = new Map([ + ['__other__', 'Other'], + ['', '(empty)'], +]); diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/assignment.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/assignment.tsx new file mode 100644 index 0000000000000..896f2ea392884 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/assignment.tsx @@ -0,0 +1,150 @@ +/* + * 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 React from 'react'; +import { useDispatch } from 'react-redux'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { + removeAssignment, + updateAssignmentColor, + updateAssignmentRule, +} from '../../state/color_mapping'; +import { ColorMapping } from '../../config'; +import { Range } from './range'; +import { Match } from './match'; +import { getPalette } from '../../palettes'; + +import { ColorMappingInputData } from '../../categorical_color_mapping'; +import { ColorSwatch } from '../color_picker/color_swatch'; + +export function Assignment({ + data, + assignment, + disableDelete, + index, + total, + canPickColor, + editable, + palette, + colorMode, + getPaletteFn, + isDarkMode, + specialTokens, + assignmentValuesCounter, +}: { + data: ColorMappingInputData; + index: number; + total: number; + colorMode: ColorMapping.Config['colorMode']; + assignment: ColorMapping.Config['assignments'][number]; + disableDelete: boolean; + palette: ColorMapping.CategoricalPalette; + getPaletteFn: ReturnType; + canPickColor: boolean; + editable: boolean; + isDarkMode: boolean; + specialTokens: Map; + assignmentValuesCounter: Map; +}) { + const dispatch = useDispatch(); + + return ( + + + { + dispatch(updateAssignmentColor({ assignmentIndex: index, color })); + }} + /> + + + {assignment.rule.type === 'auto' || + assignment.rule.type === 'matchExactly' || + assignment.rule.type === 'matchExactlyCI' ? ( + ) => { + dispatch( + updateAssignmentRule({ + assignmentIndex: index, + rule: values.length === 0 ? { type: 'auto' } : { type: 'matchExactly', values }, + }) + ); + }} + assignmentValuesCounter={assignmentValuesCounter} + /> + ) : assignment.rule.type === 'range' ? ( + { + const rule: ColorMapping.RuleRange = { + type: 'range', + min, + max, + minInclusive, + maxInclusive, + }; + dispatch(updateAssignmentRule({ assignmentIndex: index, rule })); + }} + /> + ) : null} + + + dispatch(removeAssignment(index))} + aria-label={i18n.translate( + 'coloring.colorMapping.assignments.deleteAssignmentButtonLabel', + { + defaultMessage: 'Delete this assignment', + } + )} + color="danger" + css={ + !disableDelete + ? css` + color: ${euiThemeVars.euiTextSubduedColor}; + transition: ${euiThemeVars.euiAnimSpeedFast} ease-in-out; + transition-property: color; + &:hover, + &:focus { + color: ${euiThemeVars.euiColorDangerText}; + } + ` + : undefined + } + /> + + + ); +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/match.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/match.tsx new file mode 100644 index 0000000000000..43c5583191cf3 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/match.tsx @@ -0,0 +1,101 @@ +/* + * 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 React from 'react'; +import { EuiComboBox, EuiFlexItem, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { MULTI_FIELD_KEY_SEPARATOR } from '@kbn/data-plugin/common'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { ColorMapping } from '../../config'; + +export const Match: React.FC<{ + index: number; + editable: boolean; + rule: + | ColorMapping.RuleAuto + | ColorMapping.RuleMatchExactly + | ColorMapping.RuleMatchExactlyCI + | ColorMapping.RuleRegExp; + updateValue: (values: Array) => void; + options: Array; + specialTokens: Map; + assignmentValuesCounter: Map; +}> = ({ index, rule, updateValue, editable, options, specialTokens, assignmentValuesCounter }) => { + const selectedOptions = + rule.type === 'auto' + ? [] + : typeof rule.values === 'string' + ? [ + { + label: rule.values, + value: rule.values, + append: + (assignmentValuesCounter.get(rule.values) ?? 0) > 1 ? ( + + ) : undefined, + }, + ] + : rule.values.map((value) => { + const ruleValues = Array.isArray(value) ? value : [value]; + return { + label: ruleValues.map((v) => specialTokens.get(v) ?? v).join(MULTI_FIELD_KEY_SEPARATOR), + value, + append: + (assignmentValuesCounter.get(value) ?? 0) > 1 ? ( + + ) : undefined, + }; + }); + + const convertedOptions = options.map((value) => { + const ruleValues = Array.isArray(value) ? value : [value]; + return { + label: ruleValues.map((v) => specialTokens.get(v) ?? v).join(MULTI_FIELD_KEY_SEPARATOR), + value, + }; + }); + + return ( + + { + updateValue( + changedOptions.reduce>((acc, option) => { + if (option.value !== undefined) { + acc.push(option.value); + } + return acc; + }, []) + ); + }} + onCreateOption={(label) => { + if (selectedOptions.findIndex((option) => option.label.toLowerCase() === label) === -1) { + updateValue([...selectedOptions, { label, value: label }].map((d) => d.value)); + } + }} + isClearable={false} + compressed + /> + + ); +}; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/range.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/range.tsx new file mode 100644 index 0000000000000..70f2cf49609e0 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/range.tsx @@ -0,0 +1,67 @@ +/* + * 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 React from 'react'; +import { EuiButtonEmpty, EuiFieldNumber, EuiFlexItem } from '@elastic/eui'; +import { ColorMapping } from '../../config'; + +export const Range: React.FC<{ + rule: ColorMapping.RuleRange; + editable: boolean; + updateValue: (min: number, max: number, minInclusive: boolean, maxInclusive: boolean) => void; +}> = ({ rule, updateValue, editable }) => { + const minValid = rule.min <= rule.max; + const maxValid = rule.max >= rule.min; + + return ( + <> + + updateValue(rule.min, rule.max, !rule.minInclusive, rule.maxInclusive)} + > + {rule.minInclusive ? 'GTE' : 'GT'} + + } + placeholder="min" + value={rule.min} + isInvalid={!minValid} + disabled={!editable} + onChange={(e) => + updateValue(+e.currentTarget.value, rule.max, rule.minInclusive, rule.maxInclusive) + } + aria-label="The min value" + /> + + + updateValue(rule.min, rule.max, rule.minInclusive, !rule.maxInclusive)} + > + {rule.maxInclusive ? 'LTE' : 'LT'} + + } + placeholder="max" + disabled={!editable} + value={rule.max} + onChange={(e) => + updateValue(rule.min, +e.currentTarget.value, rule.minInclusive, rule.maxInclusive) + } + aria-label="The max value" + /> + + + ); +}; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/special_assignment.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/special_assignment.tsx new file mode 100644 index 0000000000000..29ede59e37f41 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/assignment/special_assignment.tsx @@ -0,0 +1,79 @@ +/* + * 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 { EuiFieldText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useDispatch } from 'react-redux'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { ColorMapping } from '../../config'; +import { getPalette } from '../../palettes'; +import { ColorSwatch } from '../color_picker/color_swatch'; +import { updateSpecialAssignmentColor } from '../../state/color_mapping'; + +export function SpecialAssignment({ + assignment, + index, + palette, + getPaletteFn, + isDarkMode, + total, +}: { + isDarkMode: boolean; + index: number; + assignment: ColorMapping.Config['specialAssignments'][number]; + palette: ColorMapping.CategoricalPalette; + getPaletteFn: ReturnType; + total: number; +}) { + const dispatch = useDispatch(); + const canPickColor = true; + return ( + + + { + dispatch( + updateSpecialAssignmentColor({ + assignmentIndex: index, + color, + }) + ); + }} + /> + + + + + + ); +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_picker.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_picker.tsx new file mode 100644 index 0000000000000..e1e8a08aa6b22 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_picker.tsx @@ -0,0 +1,117 @@ +/* + * 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 React, { useState } from 'react'; +import { + EuiButtonEmpty, + EuiPopoverTitle, + EuiTab, + EuiTabs, + EuiTitle, + EuiHorizontalRule, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ColorMapping } from '../../config'; +import { getPalette } from '../../palettes'; +import { PaletteColors } from './palette_colors'; +import { RGBPicker } from './rgb_picker'; +import { NeutralPalette } from '../../palettes/neutral'; + +export function ColorPicker({ + palette, + getPaletteFn, + color, + close, + selectColor, + isDarkMode, + deleteStep, +}: { + color: ColorMapping.CategoricalColor | ColorMapping.ColorCode; + getPaletteFn: ReturnType; + palette: ColorMapping.CategoricalPalette; + isDarkMode: boolean; + close: () => void; + selectColor: (color: ColorMapping.CategoricalColor | ColorMapping.ColorCode) => void; + deleteStep?: () => void; +}) { + const [tab, setTab] = useState( + color.type === 'categorical' && + (color.paletteId === palette.id || color.paletteId === NeutralPalette.id) + ? 'palette' + : 'custom' + ); + + return ( +
+ + + setTab('palette')} isSelected={tab === 'palette'}> + + + {i18n.translate('coloring.colorMapping.colorPicker.paletteTabLabel', { + defaultMessage: 'Colors', + })} + + + + setTab('custom')} isSelected={tab === 'custom'}> + + + {i18n.translate('coloring.colorMapping.colorPicker.customTabLabel', { + defaultMessage: 'Custom', + })} + + + + + + {tab === 'palette' ? ( + + ) : ( + + )} + {deleteStep ? ( + <> + + { + close(); + deleteStep(); + }} + style={{ paddingBottom: 8 }} + > + {i18n.translate('coloring.colorMapping.colorPicker.removeGradientColorButtonLabel', { + defaultMessage: 'Remove color step', + })} + + + ) : null} +
+ ); +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_swatch.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_swatch.tsx new file mode 100644 index 0000000000000..8ddc56d2476c7 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/color_swatch.tsx @@ -0,0 +1,184 @@ +/* + * 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 { + EuiColorPickerSwatch, + EuiPopover, + euiShadowSmall, + isColorDark, + useEuiTheme, +} from '@elastic/eui'; +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; +import { ColorPicker } from './color_picker'; +import { getAssignmentColor } from '../../color/color_handling'; +import { ColorMapping } from '../../config'; +import { getPalette } from '../../palettes'; +import { removeGradientColorStep } from '../../state/color_mapping'; + +import { selectColorPickerVisibility } from '../../state/selectors'; +import { colorPickerVisibility, hideColorPickerVisibility } from '../../state/ui'; +import { getValidColor } from '../../color/color_math'; + +interface ColorPickerSwatchProps { + colorMode: ColorMapping.Config['colorMode']; + assignmentColor: + | ColorMapping.Config['assignments'][number]['color'] + | ColorMapping.Config['specialAssignments'][number]['color']; + getPaletteFn: ReturnType; + canPickColor: boolean; + index: number; + total: number; + palette: ColorMapping.CategoricalPalette; + onColorChange: (color: ColorMapping.CategoricalColor | ColorMapping.ColorCode) => void; + swatchShape: 'square' | 'round'; + isDarkMode: boolean; + forType: 'assignment' | 'specialAssignment' | 'gradient'; +} +export const ColorSwatch = ({ + colorMode, + assignmentColor, + getPaletteFn, + canPickColor, + index, + total, + palette, + onColorChange, + swatchShape, + isDarkMode, + forType, +}: ColorPickerSwatchProps) => { + const colorPickerState = useSelector(selectColorPickerVisibility); + const dispatch = useDispatch(); + const colorPickerVisible = + colorPickerState.index === index && + colorPickerState.type === forType && + colorPickerState.visibile; + const colorHex = getAssignmentColor( + colorMode, + assignmentColor, + getPaletteFn, + isDarkMode, + index, + total + ); + const colorIsDark = isColorDark(...getValidColor(colorHex).rgb()); + const euiTheme = useEuiTheme(); + return canPickColor && assignmentColor.type !== 'gradient' ? ( + dispatch(hideColorPickerVisibility())} + anchorPosition="upLeft" + button={ + swatchShape === 'round' ? ( + + ); +} + +function ColorStop({ + colorMode, + step, + index, + currentPalette, + getPaletteFn, + isDarkMode, +}: { + colorMode: ColorMapping.GradientColorMode; + step: ColorMapping.CategoricalColor | ColorMapping.ColorCode; + index: number; + currentPalette: ColorMapping.CategoricalPalette; + getPaletteFn: ReturnType; + isDarkMode: boolean; +}) { + const dispatch = useDispatch(); + return ( + { + dispatch( + updateGradientColorStep({ + index, + color, + }) + ); + }} + forType="gradient" + /> + ); +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/palette_selector.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/palette_selector.tsx new file mode 100644 index 0000000000000..a15bdca26ee1c --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/palette_selector.tsx @@ -0,0 +1,263 @@ +/* + * 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 React, { useCallback, useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { + EuiButtonGroup, + EuiColorPalettePicker, + EuiConfirmModal, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ScaleCategoricalIcon } from './scale_categorical'; +import { ScaleSequentialIcon } from './scale_sequential'; + +import { RootState, updatePalette } from '../../state/color_mapping'; +import { ColorMapping } from '../../config'; +import { updateAssignmentsPalette, updateColorModePalette } from '../../config/assignments'; +import { getPalette } from '../../palettes'; + +export function PaletteSelector({ + palettes, + getPaletteFn, + isDarkMode, +}: { + getPaletteFn: ReturnType; + palettes: Map; + isDarkMode: boolean; +}) { + const dispatch = useDispatch(); + const colorMode = useSelector((state: RootState) => state.colorMapping.colorMode); + const model = useSelector((state: RootState) => state.colorMapping); + + const { paletteId } = model; + + const switchPaletteFn = useCallback( + (selectedPaletteId: string, preserveColorChanges: boolean) => { + dispatch( + updatePalette({ + paletteId: selectedPaletteId, + assignments: updateAssignmentsPalette( + model.assignments, + model.assignmentMode, + model.colorMode, + selectedPaletteId, + getPaletteFn, + preserveColorChanges + ), + colorMode: updateColorModePalette( + model.colorMode, + selectedPaletteId, + preserveColorChanges + ), + }) + ); + }, + [getPaletteFn, model, dispatch] + ); + + const updateColorMode = useCallback( + (type: 'gradient' | 'categorical', preserveColorChanges: boolean) => { + const updatedColorMode: ColorMapping.Config['colorMode'] = + type === 'gradient' + ? { + type: 'gradient', + steps: [ + { + type: 'categorical', + paletteId, + colorIndex: 0, + touched: false, + }, + ], + sort: 'desc', + } + : { type: 'categorical' }; + + const assignments = updateAssignmentsPalette( + model.assignments, + model.assignmentMode, + updatedColorMode, + paletteId, + getPaletteFn, + preserveColorChanges + ); + dispatch(updatePalette({ paletteId, assignments, colorMode: updatedColorMode })); + }, + [getPaletteFn, model, dispatch, paletteId] + ); + + const [preserveModalPaletteId, setPreserveModalPaletteId] = useState(null); + + const preserveChangesModal = + preserveModalPaletteId !== null ? ( + { + if (preserveModalPaletteId) switchPaletteFn(preserveModalPaletteId, true); + setPreserveModalPaletteId(null); + }} + onConfirm={() => { + if (preserveModalPaletteId) switchPaletteFn(preserveModalPaletteId, false); + setPreserveModalPaletteId(null); + }} + confirmButtonText={i18n.translate('coloring.colorMapping.colorChangesModal.discardButton', { + defaultMessage: 'Discard changes', + })} + cancelButtonText={i18n.translate('coloring.colorMapping.colorChangesModal.preserveButton', { + defaultMessage: 'Preserve changes', + })} + buttonColor="danger" + defaultFocusedButton="confirm" + > +

+ {i18n.translate('coloring.colorMapping.colorChangesModal.switchPaletteDescription', { + defaultMessage: 'Switching palette will discard all your custom color changes', + })} +

+
+ ) : null; + + const [colorScaleModalId, setColorScaleModalId] = useState<'gradient' | 'categorical' | null>( + null + ); + + const colorScaleModal = + colorScaleModalId !== null ? ( + { + setColorScaleModalId(null); + }} + onConfirm={() => { + if (colorScaleModalId) updateColorMode(colorScaleModalId, false); + setColorScaleModalId(null); + }} + cancelButtonText={i18n.translate( + 'coloring.colorMapping.colorChangesModal.goBackButtonLabel', + { + defaultMessage: 'Go back', + } + )} + confirmButtonText={i18n.translate( + 'coloring.colorMapping.colorChangesModal.discardButtonLabel', + { + defaultMessage: 'Discard changes', + } + )} + defaultFocusedButton="confirm" + buttonColor="danger" + > +

+ {colorScaleModalId === 'categorical' + ? i18n.translate('coloring.colorMapping.colorChangesModal.categoricalModeDescription', { + defaultMessage: `Switching to a categorical mode will discard all your custom color changes`, + }) + : i18n.translate('coloring.colorMapping.colorChangesModal.sequentialModeDescription', { + defaultMessage: `Switching to a sequential mode will discard all your custom color changes`, + })} +

+
+ ) : null; + + return ( + <> + {preserveChangesModal} + {colorScaleModal} + + + + d.name !== 'Neutral') + .map((palette) => ({ + 'data-test-subj': `kbnColoring_ColorMapping_Palette-${palette.id}`, + value: palette.id, + title: palette.name, + palette: Array.from({ length: palette.colorCount }, (_, i) => { + return palette.getColor(i, isDarkMode); + }), + type: 'fixed', + }))} + onChange={(selectedPaletteId) => { + const hasChanges = model.assignments.some((a) => a.touched); + const hasGradientChanges = + model.colorMode.type === 'gradient' && + model.colorMode.steps.some((a) => a.touched); + if (hasChanges || hasGradientChanges) { + setPreserveModalPaletteId(selectedPaletteId); + } else { + switchPaletteFn(selectedPaletteId, false); + } + }} + valueOfSelected={model.paletteId} + selectionDisplay={'palette'} + compressed={true} + /> + + + + + { + const hasChanges = model.assignments.some((a) => a.touched); + const hasGradientChanges = + model.colorMode.type === 'gradient' && + model.colorMode.steps.some((a) => a.touched); + + if (hasChanges || hasGradientChanges) { + setColorScaleModalId(id as 'gradient' | 'categorical'); + } else { + updateColorMode(id as 'gradient' | 'categorical', false); + } + }} + isIconOnly + /> + + + + + ); +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/scale_categorical.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/scale_categorical.tsx new file mode 100644 index 0000000000000..f71ed74485365 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/scale_categorical.tsx @@ -0,0 +1,17 @@ +/* + * 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 React from 'react'; + +export function ScaleCategoricalIcon() { + return ( + + + + + ); +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/scale_sequential.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/scale_sequential.tsx new file mode 100644 index 0000000000000..ec245f471f307 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/scale_sequential.tsx @@ -0,0 +1,20 @@ +/* + * 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 React from 'react'; + +export function ScaleSequentialIcon() { + return ( + + + + ); +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/config/assignment_from_categories.ts b/packages/kbn-coloring/src/shared_components/color_mapping/config/assignment_from_categories.ts new file mode 100644 index 0000000000000..97c4d17c35e4d --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/config/assignment_from_categories.ts @@ -0,0 +1,65 @@ +/* + * 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 { ColorMapping } from '.'; +import { ColorMappingInputData } from '../categorical_color_mapping'; +import { MAX_ASSIGNABLE_COLORS } from '../components/container/container'; + +export function generateAutoAssignmentsForCategories( + data: ColorMappingInputData, + palette: ColorMapping.CategoricalPalette, + colorMode: ColorMapping.Config['colorMode'] +): ColorMapping.Config['assignments'] { + const isCategorical = colorMode.type === 'categorical'; + + const maxColorAssignable = data.type === 'categories' ? data.categories.length : data.bins; + + const assignableColors = isCategorical + ? Math.min(palette.colorCount, maxColorAssignable) + : Math.min(MAX_ASSIGNABLE_COLORS, maxColorAssignable); + + const autoRules: Array = + data.type === 'categories' + ? data.categories.map((c) => ({ type: 'matchExactly', values: [c] })) + : Array.from({ length: data.bins }, (d, i) => { + const step = (data.max - data.min) / data.bins; + return { + type: 'range', + min: data.max - i * step - step, + max: data.max - i * step, + minInclusive: true, + maxInclusive: false, + }; + }); + + const assignments = autoRules + .slice(0, assignableColors) + .map((rule, colorIndex) => { + if (isCategorical) { + return { + rule, + color: { + type: 'categorical', + paletteId: palette.id, + colorIndex, + }, + touched: false, + }; + } else { + return { + rule, + color: { + type: 'gradient', + }, + touched: false, + }; + } + }); + + return assignments; +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/config/assignments.ts b/packages/kbn-coloring/src/shared_components/color_mapping/config/assignments.ts new file mode 100644 index 0000000000000..701baa1b1710b --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/config/assignments.ts @@ -0,0 +1,91 @@ +/* + * 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 type { ColorMapping } from '.'; +import { MAX_ASSIGNABLE_COLORS } from '../components/container/container'; +import { getPalette, NeutralPalette } from '../palettes'; +import { DEFAULT_NEUTRAL_PALETTE_INDEX } from './default_color_mapping'; + +export function updateAssignmentsPalette( + assignments: ColorMapping.Config['assignments'], + assignmentMode: ColorMapping.Config['assignmentMode'], + colorMode: ColorMapping.Config['colorMode'], + paletteId: string, + getPaletteFn: ReturnType, + preserveColorChanges: boolean +): ColorMapping.Config['assignments'] { + const palette = getPaletteFn(paletteId); + const maxColors = palette.type === 'categorical' ? palette.colorCount : MAX_ASSIGNABLE_COLORS; + return assignmentMode === 'auto' + ? [] + : assignments.map(({ rule, color, touched }, index) => { + if (preserveColorChanges && touched) { + return { rule, color, touched }; + } else { + const newColor: ColorMapping.Config['assignments'][number]['color'] = + colorMode.type === 'categorical' + ? { + type: 'categorical', + paletteId: index < maxColors ? paletteId : NeutralPalette.id, + colorIndex: index < maxColors ? index : 0, + } + : { type: 'gradient' }; + return { + rule, + color: newColor, + touched: false, + }; + } + }); +} + +export function updateColorModePalette( + colorMode: ColorMapping.Config['colorMode'], + paletteId: string, + preserveColorChanges: boolean +): ColorMapping.Config['colorMode'] { + return colorMode.type === 'categorical' + ? colorMode + : { + type: 'gradient', + steps: colorMode.steps.map((step, stepIndex) => { + return preserveColorChanges + ? step + : { type: 'categorical', paletteId, colorIndex: stepIndex, touched: false }; + }), + sort: colorMode.sort, + }; +} + +export function getUnusedColorForNewAssignment( + palette: ColorMapping.CategoricalPalette, + colorMode: ColorMapping.Config['colorMode'], + assignments: ColorMapping.Config['assignments'] +): ColorMapping.Config['assignments'][number]['color'] { + if (colorMode.type === 'categorical') { + // TODO: change the type of color assignment depending on palette + // compute the next unused color index in the palette. + const maxColors = palette.type === 'categorical' ? palette.colorCount : MAX_ASSIGNABLE_COLORS; + const colorIndices = new Set(Array.from({ length: maxColors }, (d, i) => i)); + assignments.forEach(({ color }) => { + if (color.type === 'categorical' && color.paletteId === palette.id) { + colorIndices.delete(color.colorIndex); + } + }); + const paletteForNextUnusedColorIndex = colorIndices.size > 0 ? palette.id : NeutralPalette.id; + const nextUnusedColorIndex = + colorIndices.size > 0 ? [...colorIndices][0] : DEFAULT_NEUTRAL_PALETTE_INDEX; + return { + type: 'categorical', + paletteId: paletteForNextUnusedColorIndex, + colorIndex: nextUnusedColorIndex, + }; + } else { + return { type: 'gradient' }; + } +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/config/default_color_mapping.ts b/packages/kbn-coloring/src/shared_components/color_mapping/config/default_color_mapping.ts new file mode 100644 index 0000000000000..e4005770b2883 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/config/default_color_mapping.ts @@ -0,0 +1,79 @@ +/* + * 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 { ColorMapping } from '.'; +import { AVAILABLE_PALETTES, getPalette } from '../palettes'; +import { EUIAmsterdamColorBlindPalette } from '../palettes/eui_amsterdam'; +import { NeutralPalette } from '../palettes/neutral'; +import { getColor, getGradientColorScale } from '../color/color_handling'; + +export const DEFAULT_NEUTRAL_PALETTE_INDEX = 1; + +/** + * The default color mapping used in Kibana, starts with the EUI color palette + */ +export const DEFAULT_COLOR_MAPPING_CONFIG: ColorMapping.Config = { + assignmentMode: 'auto', + assignments: [], + specialAssignments: [ + { + rule: { + type: 'other', + }, + color: { + type: 'categorical', + paletteId: NeutralPalette.id, + colorIndex: DEFAULT_NEUTRAL_PALETTE_INDEX, + }, + touched: false, + }, + ], + paletteId: EUIAmsterdamColorBlindPalette.id, + colorMode: { + type: 'categorical', + }, +}; + +export function getPaletteColors( + isDarkMode: boolean, + colorMappings?: ColorMapping.Config +): string[] { + const colorMappingModel = colorMappings ?? { ...DEFAULT_COLOR_MAPPING_CONFIG }; + const palette = getPalette(AVAILABLE_PALETTES, NeutralPalette)(colorMappingModel.paletteId); + return Array.from({ length: palette.colorCount }, (d, i) => palette.getColor(i, isDarkMode)); +} + +export function getColorsFromMapping( + isDarkMode: boolean, + colorMappings?: ColorMapping.Config +): string[] { + const { colorMode, paletteId, assignmentMode, assignments, specialAssignments } = + colorMappings ?? { + ...DEFAULT_COLOR_MAPPING_CONFIG, + }; + + const getPaletteFn = getPalette(AVAILABLE_PALETTES, NeutralPalette); + if (colorMode.type === 'gradient') { + const colorScale = getGradientColorScale(colorMode, getPaletteFn, isDarkMode); + return Array.from({ length: 6 }, (d, i) => colorScale(i / 6)); + } else { + const palette = getPaletteFn(paletteId); + if (assignmentMode === 'auto') { + return Array.from({ length: palette.colorCount }, (d, i) => palette.getColor(i, isDarkMode)); + } else { + return [ + ...assignments.map((a) => { + return a.color.type === 'gradient' ? '' : getColor(a.color, getPaletteFn, isDarkMode); + }), + ...specialAssignments.map((a) => { + return getColor(a.color, getPaletteFn, isDarkMode); + }), + ].filter((color) => color !== ''); + } + } +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/config/index.ts b/packages/kbn-coloring/src/shared_components/color_mapping/config/index.ts new file mode 100644 index 0000000000000..e75687596789e --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/config/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export * as ColorMapping from './types'; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/config/types.ts b/packages/kbn-coloring/src/shared_components/color_mapping/config/types.ts new file mode 100644 index 0000000000000..59cb18435112d --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/config/types.ts @@ -0,0 +1,153 @@ +/* + * 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. + */ + +/** + * A color specified as a CSS color datatype (rgb/a,hex,keywords,lab,lch etc) + */ +export interface ColorCode { + type: 'colorCode'; + colorCode: string; +} + +/** + * An index specified categorical color, coming from paletteId + */ +export interface CategoricalColor { + type: 'categorical'; + paletteId: string; + colorIndex: number; +} + +/** + * Specify that the Color in an Assignment needs to be taken from a gradient defined in the `Config.colorMode` + */ +export interface GradientColor { + type: 'gradient'; +} + +/** + * A special rule that match automatically, in order, all the categories that are not matching a specified rule + */ +export interface RuleAuto { + /* tag */ + type: 'auto'; +} +/** + * A rule that match exactly, case sensitive, with the provided strings + */ +export interface RuleMatchExactly { + /* tag */ + type: 'matchExactly'; + values: Array; +} + +/** + * A Match rule to match the values case insensitive + * @ignore not used yet + */ +export interface RuleMatchExactlyCI { + /* tag */ + type: 'matchExactlyCI'; + values: string[]; +} + +/** + * A range rule, not used yet, but can be used for numerical data assignments + */ +export interface RuleRange { + /* tag */ + type: 'range'; + /** + * The min value of the range + */ + min: number; + /** + * The max value of the range + */ + max: number; + /** + * `true` if the range is left-closed (the `min` value is considered within the range), false otherwise (only values that are + * greater than the `min` are considered within the range) + */ + minInclusive: boolean; + /** + * `true` if the range is right-closed (the `max` value is considered within the range), false otherwise (only values less than + * the `max` are considered within the range) + */ + maxInclusive: boolean; +} +/** + * Regex rule. + * @ignore not used yet + */ +export interface RuleRegExp { + /* tag */ + type: 'regex'; + /** + * TODO: not sure how we can store a regexp + */ + values: string; +} + +/** + * A specific catch-everything-else rule + */ +export interface RuleOthers { + /* tag */ + type: 'other'; +} + +/** + * An assignment is the connection link between a rule and a color + */ +export interface Assignment { + /** + * Describe the rule used to assign the color. + */ + rule: R; + /** + * The color definition + */ + color: C; + + /** + * Specify if the color was changed from the original one + * TODO: rename + */ + touched: boolean; +} + +export interface CategoricalColorMode { + type: 'categorical'; +} +export interface GradientColorMode { + type: 'gradient'; + steps: Array<(CategoricalColor | ColorCode) & { touched: boolean }>; + sort: 'asc' | 'desc'; +} + +export interface Config { + paletteId: string; + colorMode: CategoricalColorMode | GradientColorMode; + assignmentMode: 'auto' | 'manual'; + assignments: Array< + Assignment< + RuleAuto | RuleMatchExactly | RuleMatchExactlyCI | RuleRange | RuleRegExp, + CategoricalColor | ColorCode | GradientColor + > + >; + specialAssignments: Array>; +} + +export interface CategoricalPalette { + id: string; + name: string; + type: 'categorical'; + colorCount: number; + getColor: (valueInRange: number, isDarkMode: boolean) => string; +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/index.ts b/packages/kbn-coloring/src/shared_components/color_mapping/index.ts new file mode 100644 index 0000000000000..1b49a2c6a8bf3 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/index.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +export { CategoricalColorMapping, type ColorMappingProps } 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 { + DEFAULT_COLOR_MAPPING_CONFIG, + getPaletteColors, + getColorsFromMapping, +} from './config/default_color_mapping'; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/palettes/elastic_brand.ts b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/elastic_brand.ts new file mode 100644 index 0000000000000..d93440c5ac5e4 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/elastic_brand.ts @@ -0,0 +1,28 @@ +/* + * 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 { ColorMapping } from '../config'; + +export const ELASTIC_BRAND_PALETTE_COLORS = [ + '#20377d', + '#7de2d1', + '#ff957d', + '#f04e98', + '#0077cc', + '#fec514', +]; + +export const ElasticBrandPalette: ColorMapping.CategoricalPalette = { + id: 'elastic_brand_2023', + name: 'Elastic Brand', + colorCount: ELASTIC_BRAND_PALETTE_COLORS.length, + type: 'categorical', + getColor(valueInRange) { + return ELASTIC_BRAND_PALETTE_COLORS[valueInRange]; + }, +}; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/palettes/eui_amsterdam.ts b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/eui_amsterdam.ts new file mode 100644 index 0000000000000..ec48793e12819 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/eui_amsterdam.ts @@ -0,0 +1,32 @@ +/* + * 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 { ColorMapping } from '../config'; + +export const EUI_AMSTERDAM_PALETTE_COLORS = [ + '#54b399', + '#6092c0', + '#d36086', + '#9170b8', + '#ca8eae', + '#d6bf57', + '#b9a888', + '#da8b45', + '#aa6556', + '#e7664c', +]; + +export const EUIAmsterdamColorBlindPalette: ColorMapping.CategoricalPalette = { + id: 'eui_amsterdam_color_blind', + name: 'Default', + colorCount: EUI_AMSTERDAM_PALETTE_COLORS.length, + type: 'categorical', + getColor(valueInRange) { + return EUI_AMSTERDAM_PALETTE_COLORS[valueInRange]; + }, +}; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/palettes/index.ts b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/index.ts new file mode 100644 index 0000000000000..340bbd32f0279 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/index.ts @@ -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 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 { ColorMapping } from '../config'; +import { ElasticBrandPalette } from './elastic_brand'; +import { EUIAmsterdamColorBlindPalette } from './eui_amsterdam'; +import { KibanaV7LegacyPalette } from './kibana_legacy'; +import { NeutralPalette } from './neutral'; + +export const AVAILABLE_PALETTES = new Map([ + [EUIAmsterdamColorBlindPalette.id, EUIAmsterdamColorBlindPalette], + [ElasticBrandPalette.id, ElasticBrandPalette], + [KibanaV7LegacyPalette.id, KibanaV7LegacyPalette], + [NeutralPalette.id, NeutralPalette], +]); + +/** + * This function should be instanciated once at the root of the component with the available palettes and + * a choosed default one and shared across components to keep a single point of truth of the available palettes and the default + * one. + */ +export function getPalette( + palettes: Map, + defaultPalette: ColorMapping.CategoricalPalette +): (paletteId: string) => ColorMapping.CategoricalPalette { + return (paletteId) => palettes.get(paletteId) ?? defaultPalette; +} + +export * from './eui_amsterdam'; +export * from './elastic_brand'; +export * from './kibana_legacy'; +export * from './neutral'; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/palettes/kibana_legacy.ts b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/kibana_legacy.ts new file mode 100644 index 0000000000000..9b576e0b05c66 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/kibana_legacy.ts @@ -0,0 +1,29 @@ +/* + * 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 { ColorMapping } from '../config'; + +export const KIBANA_V7_LEGACY_PALETTE_COLORS = [ + '#00a69b', + '#57c17b', + '#6f87d8', + '#663db8', + '#bc52bc', + '#9e3533', + '#daa05d', +]; + +export const KibanaV7LegacyPalette: ColorMapping.CategoricalPalette = { + id: 'kibana_v7_legacy', + name: 'Kibana Legacy', + colorCount: KIBANA_V7_LEGACY_PALETTE_COLORS.length, + type: 'categorical', + getColor(valueInRange) { + return KIBANA_V7_LEGACY_PALETTE_COLORS[valueInRange]; + }, +}; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/palettes/neutral.ts b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/neutral.ts new file mode 100644 index 0000000000000..5d3d92790843b --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/palettes/neutral.ts @@ -0,0 +1,23 @@ +/* + * 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 { ColorMapping } from '../config'; + +const schemeGreys = ['#f2f4fb', '#d4d9e5', '#98a2b3', '#696f7d', '#353642']; +export const NEUTRAL_COLOR_LIGHT = schemeGreys.slice(); +export const NEUTRAL_COLOR_DARK = schemeGreys.slice().reverse(); + +export const NeutralPalette: ColorMapping.CategoricalPalette = { + id: 'neutral', + name: 'Neutral', + colorCount: NEUTRAL_COLOR_LIGHT.length, + type: 'categorical', + getColor(valueInRange, isDarkMode) { + return isDarkMode ? NEUTRAL_COLOR_DARK[valueInRange] : NEUTRAL_COLOR_LIGHT[valueInRange]; + }, +}; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/state/color_mapping.ts b/packages/kbn-coloring/src/shared_components/color_mapping/state/color_mapping.ts new file mode 100644 index 0000000000000..27588aff2b389 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/state/color_mapping.ts @@ -0,0 +1,225 @@ +/* + * 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 { createSlice } from '@reduxjs/toolkit'; +import type { PayloadAction } from '@reduxjs/toolkit'; +import type { ColorMapping } from '../config'; + +export interface RootState { + colorMapping: ColorMapping.Config; + ui: { + colorPicker: { + index: number; + visibile: boolean; + type: 'gradient' | 'assignment' | 'specialAssignment'; + }; + }; +} + +const initialState: RootState['colorMapping'] = { + assignmentMode: 'auto', + assignments: [], + specialAssignments: [], + paletteId: 'eui', + colorMode: { type: 'categorical' }, +}; + +export const colorMappingSlice = createSlice({ + name: 'colorMapping', + initialState, + reducers: { + updateModel: (state, action: PayloadAction) => { + state.assignmentMode = action.payload.assignmentMode; + state.assignments = [...action.payload.assignments]; + state.specialAssignments = [...action.payload.specialAssignments]; + state.paletteId = action.payload.paletteId; + state.colorMode = { ...action.payload.colorMode }; + }, + updatePalette: ( + state, + action: PayloadAction<{ + assignments: ColorMapping.Config['assignments']; + paletteId: ColorMapping.Config['paletteId']; + colorMode: ColorMapping.Config['colorMode']; + }> + ) => { + state.paletteId = action.payload.paletteId; + state.assignments = [...action.payload.assignments]; + state.colorMode = { ...action.payload.colorMode }; + }, + assignStatically: (state, action: PayloadAction) => { + state.assignmentMode = 'manual'; + state.assignments = [...action.payload]; + }, + assignAutomatically: (state) => { + state.assignmentMode = 'auto'; + state.assignments = []; + }, + + addNewAssignment: ( + state, + action: PayloadAction + ) => { + state.assignments.push({ ...action.payload }); + }, + updateAssignment: ( + state, + action: PayloadAction<{ + assignmentIndex: number; + assignment: ColorMapping.Config['assignments'][number]; + }> + ) => { + state.assignments[action.payload.assignmentIndex] = { + ...action.payload.assignment, + touched: true, + }; + }, + updateAssignmentRule: ( + state, + action: PayloadAction<{ + assignmentIndex: number; + rule: ColorMapping.Config['assignments'][number]['rule']; + }> + ) => { + state.assignments[action.payload.assignmentIndex] = { + ...state.assignments[action.payload.assignmentIndex], + rule: action.payload.rule, + }; + }, + updateAssignmentColor: ( + state, + action: PayloadAction<{ + assignmentIndex: number; + color: ColorMapping.Config['assignments'][number]['color']; + }> + ) => { + state.assignments[action.payload.assignmentIndex] = { + ...state.assignments[action.payload.assignmentIndex], + color: action.payload.color, + touched: true, + }; + }, + + updateSpecialAssignmentColor: ( + state, + action: PayloadAction<{ + assignmentIndex: number; + color: ColorMapping.Config['specialAssignments'][number]['color']; + }> + ) => { + state.specialAssignments[action.payload.assignmentIndex] = { + ...state.specialAssignments[action.payload.assignmentIndex], + color: action.payload.color, + touched: true, + }; + }, + removeAssignment: (state, action: PayloadAction) => { + state.assignments.splice(action.payload, 1); + }, + changeColorMode: (state, action: PayloadAction) => { + state.colorMode = { ...action.payload }; + }, + updateGradientColorStep: ( + state, + action: PayloadAction<{ + index: number; + color: ColorMapping.CategoricalColor | ColorMapping.ColorCode; + }> + ) => { + if (state.colorMode.type !== 'gradient') { + return; + } + + state.colorMode = { + ...state.colorMode, + steps: state.colorMode.steps.map((step, index) => { + return index === action.payload.index + ? { ...action.payload.color, touched: true } + : { ...step, touched: false }; + }), + }; + }, + removeGradientColorStep: (state, action: PayloadAction) => { + if (state.colorMode.type !== 'gradient') { + return; + } + const steps = [...state.colorMode.steps]; + steps.splice(action.payload, 1); + + // this maintain the correct sort direciton depending on which step + // gets removed from the array when only 2 steps are left. + const sort = + state.colorMode.steps.length === 2 + ? state.colorMode.sort === 'desc' + ? action.payload === 0 + ? 'asc' + : 'desc' + : action.payload === 0 + ? 'desc' + : 'asc' + : state.colorMode.sort; + + state.colorMode = { + ...state.colorMode, + steps: [...steps], + sort, + }; + }, + addGradientColorStep: ( + state, + action: PayloadAction<{ + color: ColorMapping.CategoricalColor | ColorMapping.ColorCode; + at: number; + }> + ) => { + if (state.colorMode.type !== 'gradient') { + return; + } + + state.colorMode = { + ...state.colorMode, + steps: [ + ...state.colorMode.steps.slice(0, action.payload.at), + { ...action.payload.color, touched: false }, + ...state.colorMode.steps.slice(action.payload.at), + ], + }; + }, + + changeGradientSortOrder: (state, action: PayloadAction<'asc' | 'desc'>) => { + if (state.colorMode.type !== 'gradient') { + return; + } + + state.colorMode = { + ...state.colorMode, + sort: action.payload, + }; + }, + }, +}); +// Action creators are generated for each case reducer function +export const { + updatePalette, + assignStatically, + assignAutomatically, + addNewAssignment, + updateAssignment, + updateAssignmentColor, + updateSpecialAssignmentColor, + updateAssignmentRule, + removeAssignment, + changeColorMode, + updateGradientColorStep, + removeGradientColorStep, + addGradientColorStep, + changeGradientSortOrder, + updateModel, +} = colorMappingSlice.actions; + +export const colorMappingReducer = colorMappingSlice.reducer; diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/state/selectors.ts b/packages/kbn-coloring/src/shared_components/color_mapping/state/selectors.ts new file mode 100644 index 0000000000000..69bd57d2d852e --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/state/selectors.ts @@ -0,0 +1,26 @@ +/* + * 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 { getPalette } from '../palettes'; +import { RootState } from './color_mapping'; + +export function selectPalette(getPaletteFn: ReturnType) { + return (state: RootState) => getPaletteFn(state.colorMapping.paletteId); +} +export function selectColorMode(state: RootState) { + return state.colorMapping.colorMode; +} +export function selectSpecialAssignments(state: RootState) { + return state.colorMapping.specialAssignments; +} +export function selectIsAutoAssignmentMode(state: RootState) { + return state.colorMapping.assignmentMode === 'auto'; +} +export function selectColorPickerVisibility(state: RootState) { + return state.ui.colorPicker; +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/state/ui.ts b/packages/kbn-coloring/src/shared_components/color_mapping/state/ui.ts new file mode 100644 index 0000000000000..632fb31e9dcc5 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/state/ui.ts @@ -0,0 +1,55 @@ +/* + * 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 { type PayloadAction, createSlice } from '@reduxjs/toolkit'; +import { RootState } from './color_mapping'; + +const initialState: RootState['ui'] = { + colorPicker: { + index: 0, + visibile: false, + type: 'assignment', + }, +}; + +export const uiSlice = createSlice({ + name: 'colorMapping', + initialState, + reducers: { + colorPickerVisibility: ( + state, + action: PayloadAction<{ + index: number; + type: RootState['ui']['colorPicker']['type']; + visible: boolean; + }> + ) => { + state.colorPicker.visibile = action.payload.visible; + state.colorPicker.index = action.payload.index; + state.colorPicker.type = action.payload.type; + }, + switchColorPickerVisibility: (state) => { + state.colorPicker.visibile = !state.colorPicker.visibile; + }, + showColorPickerVisibility: (state) => { + state.colorPicker.visibile = true; + }, + hideColorPickerVisibility: (state) => { + state.colorPicker.visibile = false; + }, + }, +}); + +export const { + colorPickerVisibility, + switchColorPickerVisibility, + showColorPickerVisibility, + hideColorPickerVisibility, +} = uiSlice.actions; + +export const uiReducer = uiSlice.reducer; diff --git a/packages/kbn-coloring/src/shared_components/index.ts b/packages/kbn-coloring/src/shared_components/index.ts index 546224092e576..242df23b19e53 100644 --- a/packages/kbn-coloring/src/shared_components/index.ts +++ b/packages/kbn-coloring/src/shared_components/index.ts @@ -21,3 +21,5 @@ export const CustomizablePaletteLazy = React.lazy(() => import('./coloring')); * a predefined fallback and error boundary. */ export const CustomizablePalette = withSuspense(CustomizablePaletteLazy); + +export * from './color_mapping'; diff --git a/packages/kbn-coloring/tsconfig.json b/packages/kbn-coloring/tsconfig.json index 54c068f8bd3b6..315e59225601c 100644 --- a/packages/kbn-coloring/tsconfig.json +++ b/packages/kbn-coloring/tsconfig.json @@ -10,7 +10,6 @@ ] }, "include": [ - "**/*.scss", "**/*.ts", "**/*.tsx" ], @@ -21,6 +20,8 @@ "@kbn/utility-types", "@kbn/shared-ux-utility", "@kbn/test-jest-helpers", + "@kbn/data-plugin", + "@kbn/ui-theme", ], "exclude": [ "target/**/*", diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 78bba1df4220a..ec34d257eadc2 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -50,13 +50,13 @@ pageLoadAssetSize: expressionLegacyMetricVis: 23121 expressionMetric: 22238 expressionMetricVis: 23121 - expressionPartitionVis: 26338 + expressionPartitionVis: 28000 expressionRepeatImage: 22341 expressionRevealImage: 25675 expressions: 140958 expressionShape: 34008 expressionTagcloud: 27505 - expressionXY: 39500 + expressionXY: 45000 features: 21723 fieldFormats: 65209 files: 22673 diff --git a/src/plugins/chart_expressions/common/color_categories.ts b/src/plugins/chart_expressions/common/color_categories.ts new file mode 100644 index 0000000000000..0bb8811f2701a --- /dev/null +++ b/src/plugins/chart_expressions/common/color_categories.ts @@ -0,0 +1,41 @@ +/* + * 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 { isMultiFieldKey } from '@kbn/data-plugin/common'; + +/** + * Get the stringified version of all the categories that needs to be colored in the chart. + * Multifield keys will return as array of string and simple fields (numeric, string) will be returned as a plain unformatted string. + */ +export function getColorCategories( + rows: DatatableRow[], + accessor?: string +): Array { + return accessor + ? rows.reduce<{ keys: Set; categories: Array }>( + (acc, r) => { + const value = r[accessor]; + if (value === undefined) { + return acc; + } + // 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 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 + : []; +} diff --git a/src/plugins/chart_expressions/common/index.ts b/src/plugins/chart_expressions/common/index.ts index 0983b1ed28d4d..acc3b4d8c88cd 100644 --- a/src/plugins/chart_expressions/common/index.ts +++ b/src/plugins/chart_expressions/common/index.ts @@ -13,3 +13,4 @@ export { isOnAggBasedEditor, } from './utils'; export type { Simplify, MakeOverridesSerializable } from './types'; +export { getColorCategories } from './color_categories'; diff --git a/src/plugins/chart_expressions/common/tsconfig.json b/src/plugins/chart_expressions/common/tsconfig.json index f65660474561b..7ac76523fcb6c 100644 --- a/src/plugins/chart_expressions/common/tsconfig.json +++ b/src/plugins/chart_expressions/common/tsconfig.json @@ -15,5 +15,7 @@ ], "kbn_references": [ "@kbn/core-execution-context-common", + "@kbn/expressions-plugin", + "@kbn/data-plugin", ] } diff --git a/src/plugins/chart_expressions/expression_legacy_metric/kibana.jsonc b/src/plugins/chart_expressions/expression_legacy_metric/kibana.jsonc index 41a9c965a66da..a49ca80a2fcd2 100644 --- a/src/plugins/chart_expressions/expression_legacy_metric/kibana.jsonc +++ b/src/plugins/chart_expressions/expression_legacy_metric/kibana.jsonc @@ -12,7 +12,8 @@ "fieldFormats", "charts", "visualizations", - "presentationUtil" + "presentationUtil", + "data" ], "optionalPlugins": [ "usageCollection" diff --git a/src/plugins/chart_expressions/expression_metric/kibana.jsonc b/src/plugins/chart_expressions/expression_metric/kibana.jsonc index 087583e6fff6f..a53def7de36ee 100644 --- a/src/plugins/chart_expressions/expression_metric/kibana.jsonc +++ b/src/plugins/chart_expressions/expression_metric/kibana.jsonc @@ -12,7 +12,8 @@ "fieldFormats", "charts", "visualizations", - "presentationUtil" + "presentationUtil", + "data" ], "optionalPlugins": [ "usageCollection" diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/mosaic_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/mosaic_vis_function.test.ts.snap index 604368d7ab130..a3cd4f976757b 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/mosaic_vis_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/mosaic_vis_function.test.ts.snap @@ -71,6 +71,7 @@ Object { "type": "vis_dimension", }, ], + "colorMapping": undefined, "dimensions": Object { "buckets": Array [ Object { diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap index 293f86c6bf9ec..edcb2c8fd76e4 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap @@ -61,6 +61,7 @@ Object { "type": "vis_dimension", }, ], + "colorMapping": undefined, "dimensions": Object { "buckets": Array [ Object { @@ -203,6 +204,7 @@ Object { "type": "vis_dimension", }, ], + "colorMapping": undefined, "dimensions": Object { "buckets": Array [ Object { diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/treemap_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/treemap_vis_function.test.ts.snap index f6817eca439cf..17c372547ad79 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/treemap_vis_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/treemap_vis_function.test.ts.snap @@ -71,6 +71,7 @@ Object { "type": "vis_dimension", }, ], + "colorMapping": undefined, "dimensions": Object { "buckets": Array [ Object { diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap index 7c74291190a2d..cb1d724053dfe 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap @@ -53,6 +53,7 @@ Object { }, "type": "vis_dimension", }, + "colorMapping": undefined, "dimensions": Object { "buckets": Array [ Object { diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/i18n.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/i18n.ts index b312de7bf1583..c74669439b2c3 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/i18n.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/i18n.ts @@ -122,6 +122,10 @@ export const strings = { i18n.translate('expressionPartitionVis.reusable.function.dimension.splitrow', { defaultMessage: 'Row split', }), + getColorMappingHelp: () => + i18n.translate('expressionPartitionVis.layer.colorMapping.help', { + defaultMessage: 'JSON key-value pairs of the color mapping model', + }), }; export const errors = { diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts index fc863cf73c68c..4a9dff714c8da 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts @@ -110,6 +110,10 @@ export const mosaicVisFunction = (): MosaicVisExpressionFunctionDefinition => ({ help: strings.getAriaLabelHelp(), required: false, }, + colorMapping: { + types: ['string'], + help: strings.getColorMappingHelp(), + }, }, fn(context, args, handlers) { const maxSupportedBuckets = 2; @@ -146,6 +150,7 @@ export const mosaicVisFunction = (): MosaicVisExpressionFunctionDefinition => ({ splitColumn: args.splitColumn, splitRow: args.splitRow, }, + colorMapping: args.colorMapping, }; if (handlers?.inspectorAdapters?.tables) { diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts index 0cf6522456c62..30e8388f1255e 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts @@ -141,6 +141,10 @@ export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({ help: strings.getAriaLabelHelp(), required: false, }, + colorMapping: { + types: ['string'], + help: strings.getColorMappingHelp(), + }, }, fn(context, args, handlers) { if (args.splitColumn && args.splitRow) { @@ -173,6 +177,7 @@ export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({ splitColumn: args.splitColumn, splitRow: args.splitRow, }, + colorMapping: args.colorMapping, }; if (handlers?.inspectorAdapters?.tables) { diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts index 2a5d0a6af7a8a..e0804dd9b0e92 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts @@ -115,6 +115,10 @@ export const treemapVisFunction = (): TreemapVisExpressionFunctionDefinition => help: strings.getAriaLabelHelp(), required: false, }, + colorMapping: { + types: ['string'], + help: strings.getColorMappingHelp(), + }, }, fn(context, args, handlers) { const maxSupportedBuckets = 2; @@ -152,6 +156,7 @@ export const treemapVisFunction = (): TreemapVisExpressionFunctionDefinition => splitColumn: args.splitColumn, splitRow: args.splitRow, }, + colorMapping: args.colorMapping, }; if (handlers?.inspectorAdapters?.tables) { diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts index e4176cf6015c1..6e23513851b1e 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts @@ -114,6 +114,10 @@ export const waffleVisFunction = (): WaffleVisExpressionFunctionDefinition => ({ help: strings.getAriaLabelHelp(), required: false, }, + colorMapping: { + types: ['string'], + help: strings.getColorMappingHelp(), + }, }, fn(context, args, handlers) { if (args.splitColumn && args.splitRow) { @@ -147,6 +151,7 @@ export const waffleVisFunction = (): WaffleVisExpressionFunctionDefinition => ({ splitColumn: args.splitColumn, splitRow: args.splitRow, }, + colorMapping: args.colorMapping, }; if (handlers?.inspectorAdapters?.tables) { diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts index 239253f54491d..00667ace39576 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts @@ -61,6 +61,7 @@ interface VisCommonParams { maxLegendLines: number; legendSize?: LegendSize; ariaLabel?: string; + colorMapping?: string; // JSON stringified object of the color mapping } interface VisCommonConfig extends VisCommonParams { diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/colors/color_mapping_accessors.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/colors/color_mapping_accessors.ts new file mode 100644 index 0000000000000..ec11fc7605de1 --- /dev/null +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/colors/color_mapping_accessors.ts @@ -0,0 +1,77 @@ +/* + * 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 { NodeColorAccessor, PATH_KEY } from '@elastic/charts'; +import { lightenColor } from '@kbn/charts-plugin/public'; +import { MultiFieldKey } from '@kbn/data-plugin/common'; +import { getColorFactory } from '@kbn/coloring'; +import { isMultiFieldKey } from '@kbn/data-plugin/common'; +import { ChartTypes } from '../../../common/types'; + +export function getCategoryKeys(category: string | MultiFieldKey): string | string[] { + return isMultiFieldKey(category) ? category.keys.map(String) : `${category}`; +} + +/** + * Get the color of a specific slice/section in Pie,donut,waffle and treemap. + * These chart type shares the same color assignment mechanism. + */ +const getPieFillColor = + ( + layerIndex: number, + numOfLayers: number, + getColorFn: ReturnType + ): NodeColorAccessor => + (_key, _sortIndex, node) => { + const path = node[PATH_KEY]; + // the category used to color the pie/donut is at the third level of the path + // first two are: small multiple and pie whole center. + const category = getCategoryKeys(path[2].value); + const color = getColorFn(category); + // increase the lightness of the color on each layer. + return lightenColor(color, layerIndex + 1, numOfLayers); + }; + +/** + * Get the color of a section in a Mosaic chart. + * This chart has a slight variation in the way color are applied. Mosaic can represent up to 2 layers, + * described in lens as the horizontal and vertical axes. + * With a single layer the color is simply applied per each category, with 2 layer, the color is applied only + * to the category that describe a row, not by column. + */ +const getMosaicFillColor = + ( + layerIndex: number, + numOfLayers: number, + getColorFn: ReturnType + ): NodeColorAccessor => + (_key, _sortIndex, node) => { + // Special case for 2 layer mosaic where the color is per rows and the columns are not colored + if (numOfLayers === 2 && layerIndex === 0) { + // transparent color will fallback to the kibana/context background + return 'rgba(0,0,0,0)'; + } + const path = node[PATH_KEY]; + + // the category used to color the pie/donut is at the third level of the `path` when using a single layer mosaic + // and are at fourth level of `path` when using 2 layer mosaic + // first two are: small multiple and pie whole center. + const category = getCategoryKeys(numOfLayers === 2 ? path[3].value : path[2].value); + return getColorFn(category); + }; + +export const getPartitionFillColor = ( + chartType: ChartTypes, + layerIndex: number, + numOfLayers: number, + getColorFn: ReturnType +): NodeColorAccessor => { + return chartType === ChartTypes.MOSAIC + ? getMosaicFillColor(layerIndex, numOfLayers, getColorFn) + : getPieFillColor(layerIndex, numOfLayers, getColorFn); +}; diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.ts index e4f3e1687e4ad..6f40097809e18 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.ts @@ -7,16 +7,25 @@ */ import { Datum, PartitionLayer } from '@elastic/charts'; -import type { PaletteRegistry } from '@kbn/coloring'; +import { + PaletteRegistry, + getColorFactory, + getPalette, + AVAILABLE_PALETTES, + NeutralPalette, +} from '@kbn/coloring'; import { i18n } from '@kbn/i18n'; import { FieldFormat } from '@kbn/field-formats-plugin/common'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import type { Datatable, DatatableRow } from '@kbn/expressions-plugin/public'; + +import { getColorCategories } from '@kbn/chart-expressions-common'; import { getDistinctSeries } from '..'; import { BucketColumns, ChartTypes, PartitionVisParams } from '../../../common/types'; import { sortPredicateByType, sortPredicateSaveSourceOrder } from './sort_predicate'; import { byDataColorPaletteMap, getColor } from './get_color'; import { getNodeLabel } from './get_node_labels'; +import { getPartitionFillColor } from '../colors/color_mapping_accessors'; // This is particularly useful in case of a text based languages where // it's no possible to use a missingBucketLabel @@ -62,6 +71,15 @@ export const getLayers = ( const distinctSeries = getDistinctSeries(rows, columns); + // return a fn only if color mapping is available in visParams + const getColorFromMappingFn = getColorFromMappingFactory( + chartType, + columns, + rows, + isDarkMode, + visParams + ); + return columns.map((col, layerIndex) => { return { groupByRollup: (d: Datum) => (col.id ? d[col.id] ?? emptySliceLabel : col.name), @@ -75,26 +93,74 @@ export const getLayers = ( ? sortPredicateSaveSourceOrder() : sortPredicateForType, shape: { - fillColor: (key, sortIndex, node) => - getColor( - chartType, - key, - node, - layerIndex, - isSplitChart, - overwriteColors, - distinctSeries, - { columnsLength: columns.length, rowsLength: rows.length }, - visParams, - palettes, - byDataPalette, - syncColors, - isDarkMode, - formatter, - col, - formatters - ), + // this applies color mapping only if visParams.colorMapping is available + fillColor: getColorFromMappingFn + ? getPartitionFillColor(chartType, layerIndex, columns.length, getColorFromMappingFn) + : (key, sortIndex, node) => + getColor( + chartType, + key, + node, + layerIndex, + isSplitChart, + overwriteColors, + distinctSeries, + { columnsLength: columns.length, rowsLength: rows.length }, + visParams, + palettes, + byDataPalette, + syncColors, + isDarkMode, + formatter, + col, + formatters + ), }, }; }); }; + +/** + * If colorMapping is available, returns a function that accept a string or an array of strings (used in case of multi-field-key) + * and returns a color specified in the provided mapping + */ +function getColorFromMappingFactory( + chartType: ChartTypes, + columns: Array>, + rows: DatatableRow[], + isDarkMode: boolean, + visParams: PartitionVisParams +): undefined | ((category: string | string[]) => string) { + const { colorMapping, dimensions } = visParams; + + if (!colorMapping) { + // return undefined, we will use the legacy color mapping instead + return undefined; + } + // if pie/donut/treemap with no buckets use the default color mode + if ( + (chartType === ChartTypes.DONUT || + chartType === ChartTypes.PIE || + chartType === ChartTypes.TREEMAP) && + (!dimensions.buckets || dimensions.buckets?.length === 0) + ) { + return undefined; + } + // the mosaic configures the main categories in the second column, instead of the first + // as it happens in all the other partition types. + // Independentely from the bucket aggregation used, the categories will always be casted + // as string to make it nicely working with a text input field, avoiding a field + const categories = + chartType === ChartTypes.MOSAIC && columns.length === 2 + ? getColorCategories(rows, columns[1]?.id) + : getColorCategories(rows, columns[0]?.id); + return getColorFactory( + JSON.parse(colorMapping), + getPalette(AVAILABLE_PALETTES, NeutralPalette), + isDarkMode, + { + type: 'categories', + categories, + } + ); +} diff --git a/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/__snapshots__/tagcloud_function.test.ts.snap b/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/__snapshots__/tagcloud_function.test.ts.snap index d03cebd680290..15f335df82684 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/__snapshots__/tagcloud_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/__snapshots__/tagcloud_function.test.ts.snap @@ -66,6 +66,7 @@ Object { "bucket": Object { "accessor": 1, }, + "colorMapping": undefined, "isPreview": false, "maxFontSize": 72, "metric": Object { @@ -126,6 +127,7 @@ Object { }, "type": "vis_dimension", }, + "colorMapping": undefined, "isPreview": false, "maxFontSize": 72, "metric": Object { diff --git a/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.ts b/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.ts index ec69431cd1735..75148e570331c 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.ts +++ b/src/plugins/chart_expressions/expression_tagcloud/common/expression_functions/tagcloud_function.ts @@ -51,6 +51,9 @@ const strings = { isPreview: i18n.translate('expressionTagcloud.functions.tagcloud.args.isPreviewHelpText', { defaultMessage: 'Set isPreview to true to avoid showing out of room warnings', }), + colorMapping: i18n.translate('expressionTagcloud.layer.colorMapping.help', { + defaultMessage: 'JSON key-value pairs of the color mapping model', + }), }, dimension: { tags: i18n.translate('expressionTagcloud.functions.tagcloud.dimension.tags', { @@ -146,6 +149,10 @@ export const tagcloudFunction: ExpressionTagcloudFunction = () => { default: false, required: false, }, + colorMapping: { + types: ['string'], + help: argHelp.colorMapping, + }, }, fn(input, args, handlers) { validateAccessor(args.metric, input.columns); @@ -167,6 +174,7 @@ export const tagcloudFunction: ExpressionTagcloudFunction = () => { (handlers.variables?.embeddableTitle as string) ?? handlers.getExecutionContext?.()?.description, isPreview: Boolean(args.isPreview), + colorMapping: args.colorMapping, }; if (handlers?.inspectorAdapters?.tables) { diff --git a/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts index 985da788c6ffc..c59e70a5c028d 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_tagcloud/common/types/expression_functions.ts @@ -27,6 +27,7 @@ interface TagCloudCommonParams { metric: ExpressionValueVisDimension | string; bucket?: ExpressionValueVisDimension | string; palette: PaletteOutput; + colorMapping?: string; // JSON stringified object of the color mapping } export interface TagCloudVisConfig extends TagCloudCommonParams { diff --git a/src/plugins/chart_expressions/expression_tagcloud/kibana.jsonc b/src/plugins/chart_expressions/expression_tagcloud/kibana.jsonc index 6c6ce82d321ed..b6bf410e2786f 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/kibana.jsonc +++ b/src/plugins/chart_expressions/expression_tagcloud/kibana.jsonc @@ -8,6 +8,7 @@ "server": true, "browser": true, "requiredPlugins": [ + "data", "expressions", "visualizations", "charts", diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.test.tsx b/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.test.tsx index 3f9c86778e82d..86c4bc009d931 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.test.tsx +++ b/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.test.tsx @@ -105,6 +105,7 @@ describe('TagCloudChart', function () { renderComplete: jest.fn(), syncColors: false, visType: 'tagcloud', + isDarkMode: false, }; wrapperPropsWithColumnNames = { @@ -135,6 +136,7 @@ describe('TagCloudChart', function () { renderComplete: jest.fn(), syncColors: false, visType: 'tagcloud', + isDarkMode: false, }; }); diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx b/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx index adfc3df81f97f..e3532bb17f97e 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx +++ b/src/plugins/chart_expressions/expression_tagcloud/public/components/tagcloud_component.tsx @@ -13,11 +13,19 @@ import { EuiIconTip, EuiResizeObserver } from '@elastic/eui'; import { IconChartTagcloud } from '@kbn/chart-icons'; import { Chart, Settings, Wordcloud, RenderChangeListener } from '@elastic/charts'; import { EmptyPlaceholder } from '@kbn/charts-plugin/public'; -import type { PaletteRegistry, PaletteOutput } from '@kbn/coloring'; -import { IInterpreterRenderHandlers } from '@kbn/expressions-plugin/public'; -import { getOverridesFor } from '@kbn/chart-expressions-common'; +import { + PaletteRegistry, + PaletteOutput, + getColorFactory, + getPalette, + AVAILABLE_PALETTES, + NeutralPalette, +} from '@kbn/coloring'; +import { IInterpreterRenderHandlers, DatatableRow } from '@kbn/expressions-plugin/public'; +import { getColorCategories, getOverridesFor } from '@kbn/chart-expressions-common'; import type { AllowedSettingsOverrides, AllowedChartOverrides } from '@kbn/charts-plugin/common'; import { getColumnByAccessor, getFormatByAccessor } from '@kbn/visualizations-plugin/common/utils'; +import { isMultiFieldKey } from '@kbn/data-plugin/common'; import { getFormatService } from '../format_service'; import { TagcloudRendererConfig } from '../../common/types'; import { ScaleOptions, Orientation } from '../../common/constants'; @@ -31,6 +39,7 @@ export type TagCloudChartProps = TagcloudRendererConfig & { renderComplete: IInterpreterRenderHandlers['done']; palettesRegistry: PaletteRegistry; overrides?: AllowedSettingsOverrides & AllowedChartOverrides; + isDarkMode: boolean; }; const calculateWeight = (value: number, x1: number, y1: number, x2: number, y2: number) => @@ -84,9 +93,10 @@ export const TagCloudChart = ({ renderComplete, syncColors, overrides, + isDarkMode, }: TagCloudChartProps) => { const [warning, setWarning] = useState(false); - const { bucket, metric, scale, palette, showLabel, orientation } = visParams; + const { bucket, metric, scale, palette, showLabel, orientation, colorMapping } = visParams; const bucketFormatter = useMemo(() => { return bucket @@ -96,23 +106,35 @@ export const TagCloudChart = ({ const tagCloudData = useMemo(() => { const bucketColumn = bucket ? getColumnByAccessor(bucket, visData.columns)! : null; - const tagColumn = bucket ? bucketColumn!.id : null; + const tagColumn = bucket ? bucketColumn!.id : undefined; const metricColumn = getColumnByAccessor(metric, visData.columns)!.id; const metrics = visData.rows.map((row) => row[metricColumn]); - const values = bucket && tagColumn !== null ? visData.rows.map((row) => row[tagColumn]) : []; + const values = + bucket && tagColumn !== undefined ? visData.rows.map((row) => row[tagColumn]) : []; const maxValue = Math.max(...metrics); const minValue = Math.min(...metrics); + const colorFromMappingFn = getColorFromMappingFactory( + tagColumn, + visData.rows, + isDarkMode, + colorMapping + ); + return visData.rows.map((row) => { - const tag = tagColumn === null ? 'all' : row[tagColumn]; + const tag = tagColumn === undefined ? 'all' : row[tagColumn]; + + const category = isMultiFieldKey(tag) ? tag.keys.map(String) : `${tag}`; return { text: bucketFormatter ? bucketFormatter.convert(tag, 'text') : tag, weight: tag === 'all' || visData.rows.length <= 1 ? 1 : calculateWeight(row[metricColumn], minValue, maxValue, 0, 1) || 0, - color: getColor(palettesRegistry, palette, tag, values, syncColors) || 'rgba(0,0,0,0)', + color: colorFromMappingFn + ? colorFromMappingFn(category) + : getColor(palettesRegistry, palette, tag, values, syncColors) || 'rgba(0,0,0,0)', }; }); }, [ @@ -124,6 +146,8 @@ export const TagCloudChart = ({ syncColors, visData.columns, visData.rows, + colorMapping, + isDarkMode, ]); useEffect(() => { @@ -278,3 +302,28 @@ export const TagCloudChart = ({ // eslint-disable-next-line import/no-default-export export { TagCloudChart as default }; + +/** + * If colorMapping is available, returns a function that accept a string or an array of strings (used in case of multi-field-key) + * and returns a color specified in the provided mapping + */ +function getColorFromMappingFactory( + tagColumn: string | undefined, + rows: DatatableRow[], + isDarkMode: boolean, + colorMapping?: string +): undefined | ((category: string | string[]) => string) { + if (!colorMapping) { + // return undefined, we will use the legacy color mapping instead + return undefined; + } + return getColorFactory( + JSON.parse(colorMapping), + getPalette(AVAILABLE_PALETTES, NeutralPalette), + isDarkMode, + { + type: 'categories', + categories: getColorCategories(rows, tagColumn), + } + ); +} diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/expression_renderers/tagcloud_renderer.tsx b/src/plugins/chart_expressions/expression_tagcloud/public/expression_renderers/tagcloud_renderer.tsx index b3ab496447754..101c40b6b384d 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/public/expression_renderers/tagcloud_renderer.tsx +++ b/src/plugins/chart_expressions/expression_tagcloud/public/expression_renderers/tagcloud_renderer.tsx @@ -67,6 +67,12 @@ export const tagcloudRenderer: ( }; const palettesRegistry = await plugins.charts.palettes.getPalettes(); + let isDarkMode = false; + plugins.charts.theme.darkModeEnabled$ + .subscribe((val) => { + isDarkMode = val.darkMode; + }) + .unsubscribe(); render( @@ -87,6 +93,7 @@ export const tagcloudRenderer: ( fireEvent={handlers.event} syncColors={config.syncColors} overrides={config.overrides} + isDarkMode={isDarkMode} /> )} diff --git a/src/plugins/chart_expressions/expression_tagcloud/tsconfig.json b/src/plugins/chart_expressions/expression_tagcloud/tsconfig.json index 55e81302586b8..b737dfb445f09 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/tsconfig.json +++ b/src/plugins/chart_expressions/expression_tagcloud/tsconfig.json @@ -27,6 +27,7 @@ "@kbn/analytics", "@kbn/chart-expressions-common", "@kbn/chart-icons", + "@kbn/data-plugin", ], "exclude": [ "target/**/*", diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts index 10f6d5d748b23..b9e2bd6dbac67 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts @@ -94,4 +94,8 @@ export const commonDataLayerArgs: Omit< help: strings.getPaletteHelp(), default: '{palette}', }, + colorMapping: { + types: ['string'], + help: strings.getColorMappingHelp(), + }, }; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts index 94d788106acb3..03df575b3c653 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts @@ -52,6 +52,7 @@ const createDataLayer = (args: XYArgs, table: Datatable): DataLayerConfigResult layerType: LayerTypes.DATA, table: normalizedTable, showLines: args.showLines, + colorMapping: args.colorMapping, ...accessors, }; }; diff --git a/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx b/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx index d9fc015c2844c..2446a27e718ce 100644 --- a/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx +++ b/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx @@ -209,6 +209,10 @@ export const strings = { i18n.translate('expressionXY.dataLayer.palette.help', { defaultMessage: 'Palette', }), + getColorMappingHelp: () => + i18n.translate('expressionXY.layer.colorMapping.help', { + defaultMessage: 'JSON key-value pairs of the color mapping model', + }), getTableHelp: () => i18n.translate('expressionXY.layers.table.help', { defaultMessage: 'Table', diff --git a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts index 55fd63786570b..a81128f6e74a7 100644 --- a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts @@ -136,6 +136,7 @@ export interface DataLayerArgs { isStacked: boolean; isHorizontal: boolean; palette: PaletteOutput; + colorMapping?: string; // JSON stringified object of the color mapping decorations?: DataDecorationConfigResult[]; curveType?: XYCurveType; } @@ -163,6 +164,7 @@ export interface ExtendedDataLayerArgs { isStacked: boolean; isHorizontal: boolean; palette: PaletteOutput; + colorMapping?: string; // palette will always be set on the expression decorations?: DataDecorationConfigResult[]; curveType?: XYCurveType; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap index fe76259b65889..9bc59b677ed78 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap +++ b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap @@ -1099,6 +1099,7 @@ exports[`XYChart component it renders area 1`] = ` }, } } + isDarkMode={false} layers={ Array [ Object { @@ -2107,6 +2108,7 @@ exports[`XYChart component it renders bar 1`] = ` }, } } + isDarkMode={false} layers={ Array [ Object { @@ -3115,6 +3117,7 @@ exports[`XYChart component it renders horizontal bar 1`] = ` }, } } + isDarkMode={false} layers={ Array [ Object { @@ -4123,6 +4126,7 @@ exports[`XYChart component it renders line 1`] = ` }, } } + isDarkMode={false} layers={ Array [ Object { @@ -5131,6 +5135,7 @@ exports[`XYChart component it renders stacked area 1`] = ` }, } } + isDarkMode={false} layers={ Array [ Object { @@ -6139,6 +6144,7 @@ exports[`XYChart component it renders stacked bar 1`] = ` }, } } + isDarkMode={false} layers={ Array [ Object { @@ -7147,6 +7153,7 @@ exports[`XYChart component it renders stacked horizontal bar 1`] = ` }, } } + isDarkMode={false} layers={ Array [ Object { @@ -8381,6 +8388,7 @@ exports[`XYChart component split chart should render split chart if both, splitR }, } } + isDarkMode={false} layers={ Array [ Object { @@ -9622,6 +9630,7 @@ exports[`XYChart component split chart should render split chart if splitColumnA }, } } + isDarkMode={false} layers={ Array [ Object { @@ -10861,6 +10870,7 @@ exports[`XYChart component split chart should render split chart if splitRowAcce }, } } + isDarkMode={false} layers={ Array [ Object { diff --git a/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx b/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx index 5cabeaee31575..cc6e969a10af9 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx @@ -57,6 +57,7 @@ interface Props { fieldFormats: LayersFieldFormats; uiState?: PersistedState; singleTable?: boolean; + isDarkMode: boolean; } export const DataLayers: FC = ({ @@ -80,6 +81,7 @@ export const DataLayers: FC = ({ fieldFormats, uiState, singleTable, + isDarkMode, }) => { // for singleTable mode we should use y accessors from all layers for creating correct series name and getting color const allYAccessors = layers.flatMap((layer) => layer.accessors); @@ -169,6 +171,7 @@ export const DataLayers: FC = ({ allYAccessors, singleTable, multipleLayersWithSplits, + isDarkMode, }); const index = `${layer.layerId}-${accessorIndex}`; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx index b8ac9d5cd0bbb..c241e476db5de 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx @@ -964,6 +964,7 @@ export function XYChart({ fieldFormats={fieldFormats} uiState={uiState} singleTable={singleTable} + isDarkMode={darkMode} /> )} {referenceLineLayers.length ? ( diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/color/color_mapping_accessor.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/color/color_mapping_accessor.ts new file mode 100644 index 0000000000000..b57f371eab2fd --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/color/color_mapping_accessor.ts @@ -0,0 +1,49 @@ +/* + * 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 { SeriesColorAccessorFn } from '@elastic/charts'; +import { getColorFactory, type ColorMapping, type ColorMappingInputData } from '@kbn/coloring'; +import { MULTI_FIELD_KEY_SEPARATOR } from '@kbn/data-plugin/common'; + +/** + * Return a color accessor function for XY charts depending on the split accessors received. + */ +export function getColorSeriesAccessorFn( + config: ColorMapping.Config, + getPaletteFn: (paletteId: string) => ColorMapping.CategoricalPalette, + isDarkMode: boolean, + mappingData: ColorMappingInputData, + fieldId: string, + specialTokens: Map +): SeriesColorAccessorFn { + // inverse map to handle the conversion between the formatted string and their original format + // for any specified special tokens + const specialHandlingInverseMap: Map = new Map( + [...specialTokens.entries()].map((d) => [d[1], d[0]]) + ); + + const getColor = getColorFactory(config, getPaletteFn, isDarkMode, mappingData); + + return ({ splitAccessors }) => { + const splitValue = splitAccessors.get(fieldId); + // if there isn't a category associated in the split accessor, let's use the default color + if (splitValue === undefined) { + return null; + } + + // category can be also a number, range, ip, multi-field. We need to stringify it to be sure + // we can correctly match it a with user string + // if the separator exist, we de-construct it into a multifieldkey into values. + const categories = `${splitValue}`.split(MULTI_FIELD_KEY_SEPARATOR).map((category) => { + return specialHandlingInverseMap.get(category) ?? category; + }); + // we must keep the array nature of a multi-field key or just use a single string + // This is required because the rule stored are checked differently for single values or multi-values + return getColor(categories.length > 1 ? categories : categories[0]); + }; +} diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.ts index 94b187055e6dd..990d1ab93a1bc 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.ts @@ -95,6 +95,11 @@ export const getAllSeries = ( return allSeries; }; +/** + * 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[], titles: LayersAccessorsTitles, diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx b/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx index ff76ec511ffc9..1971409ab4223 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx @@ -16,6 +16,7 @@ import { SeriesName, StackMode, XYChartSeriesIdentifier, + SeriesColorAccessorFn, } from '@elastic/charts'; import { IFieldFormat } from '@kbn/field-formats-plugin/common'; import type { PersistedState } from '@kbn/visualizations-plugin/public'; @@ -23,6 +24,13 @@ import { Datatable } from '@kbn/expressions-plugin/common'; import { getAccessorByDimension } from '@kbn/visualizations-plugin/common/utils'; import type { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common/expression_functions'; import { PaletteRegistry, SeriesLayer } from '@kbn/coloring'; +import { + getPalette, + AVAILABLE_PALETTES, + NeutralPalette, + SPECIAL_TOKENS_STRING_CONVERTION, +} from '@kbn/coloring'; +import { getColorCategories } from '@kbn/chart-expressions-common'; import { isDataLayer } from '../../common/utils/layer_types_guards'; import { CommonXYDataLayerConfig, CommonXYLayerConfig, XScaleType } from '../../common'; import { AxisModes, SeriesTypes } from '../../common/constants'; @@ -32,6 +40,7 @@ import { ColorAssignments } from './color_assignment'; import { GroupsConfiguration } from './axes_configuration'; import { LayerAccessorsTitles, LayerFieldFormats, LayersFieldFormats } from './layers'; import { getFormat } from './format'; +import { getColorSeriesAccessorFn } from './color/color_mapping_accessor'; type SeriesSpec = LineSeriesProps & BarSeriesProps & AreaSeriesProps; @@ -57,6 +66,7 @@ type GetSeriesPropsFn = (config: { allYAccessors: Array; singleTable?: boolean; multipleLayersWithSplits: boolean; + isDarkMode: boolean; }) => SeriesSpec; type GetSeriesNameFn = ( @@ -399,6 +409,7 @@ export const getSeriesProps: GetSeriesPropsFn = ({ allYAccessors, singleTable, multipleLayersWithSplits, + isDarkMode, }): SeriesSpec => { const { table, isStacked, markSizeAccessor } = layer; const isPercentage = layer.isPercentage; @@ -478,6 +489,34 @@ export const getSeriesProps: GetSeriesPropsFn = ({ ); }; + const colorAccessorFn: SeriesColorAccessorFn = + // 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 + getPalette(AVAILABLE_PALETTES, NeutralPalette), + isDarkMode, + { + type: 'categories', + categories: getColorCategories(table.rows, splitColumnIds[0]), + }, + splitColumnIds[0], + SPECIAL_TOKENS_STRING_CONVERTION + ) + : (series) => + getColor( + series, + { + layer, + colorAssignments, + paletteService, + getSeriesNameFn, + syncColors, + }, + uiState, + singleTable + ); + return { splitSeriesAccessors: splitColumnIds.length ? splitColumnIds : [], stackAccessors: isStacked ? [xColumnId || 'unifiedX'] : [], @@ -497,19 +536,7 @@ export const getSeriesProps: GetSeriesPropsFn = ({ formatter?.id === 'bytes' && scaleType === ScaleType.Linear ? ScaleType.LinearBinary : scaleType, - color: (series) => - getColor( - series, - { - layer, - colorAssignments, - paletteService, - getSeriesNameFn, - syncColors, - }, - uiState, - singleTable - ), + color: colorAccessorFn, groupId: yAxis?.groupId, enableHistogramMode, stackMode, diff --git a/src/plugins/charts/kibana.jsonc b/src/plugins/charts/kibana.jsonc index 6b0e952969329..8c00cd40f4ad3 100644 --- a/src/plugins/charts/kibana.jsonc +++ b/src/plugins/charts/kibana.jsonc @@ -7,7 +7,8 @@ "server": true, "browser": true, "requiredPlugins": [ - "expressions" + "expressions", + "data" ], "extraPublicDirs": [ "common" diff --git a/src/plugins/data/common/search/aggs/buckets/index.ts b/src/plugins/data/common/search/aggs/buckets/index.ts index 31bc7cf9ca544..369e56caf1859 100644 --- a/src/plugins/data/common/search/aggs/buckets/index.ts +++ b/src/plugins/data/common/search/aggs/buckets/index.ts @@ -37,7 +37,7 @@ export * from './significant_text_fn'; export * from './significant_text'; export * from './terms_fn'; export * from './terms'; -export { MultiFieldKey } from './multi_field_key'; +export { MultiFieldKey, isMultiFieldKey, MULTI_FIELD_KEY_SEPARATOR } from './multi_field_key'; export * from './multi_terms_fn'; export * from './multi_terms'; export * from './rare_terms_fn'; diff --git a/src/plugins/data/common/search/aggs/buckets/multi_field_key.ts b/src/plugins/data/common/search/aggs/buckets/multi_field_key.ts index 89ac1f4c00a54..5b02d0d8827f2 100644 --- a/src/plugins/data/common/search/aggs/buckets/multi_field_key.ts +++ b/src/plugins/data/common/search/aggs/buckets/multi_field_key.ts @@ -38,3 +38,13 @@ export class MultiFieldKey { return this[id]; } } + +export function isMultiFieldKey(field: unknown): field is MultiFieldKey { + return field instanceof MultiFieldKey; +} + +/** + * Multi-field key separator used in Visualizations (Lens, AggBased, TSVB). + * This differs from the separator used in the toString method of the MultiFieldKey + */ +export const MULTI_FIELD_KEY_SEPARATOR = ' › '; diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_1.json b/test/interpreter_functional/snapshots/baseline/partial_test_1.json index c7bb37566b0fe..90528b3321d22 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_1.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_1.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"isPreview":false,"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json index ac809c756d2cc..4d94b530c86e2 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json index 6b52c8de57ae5..8c4e9fc5cd523 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_empty_data.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[],"type":"datatable"},"visParams":{"ariaLabel":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[],"type":"datatable"},"visParams":{"ariaLabel":null,"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json index 21e213ebb9c27..f142588711a31 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"isPreview":false,"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"colorMapping":null,"isPreview":false,"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json index afaac18bf342d..291e6b40e6bfd 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json index 03f74cc01d3e3..381d3afc54067 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_1.json b/test/interpreter_functional/snapshots/session/partial_test_1.json index 6e12a10d1e283..90528b3321d22 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_1.json +++ b/test/interpreter_functional/snapshots/session/partial_test_1.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json index cb14c6ea89407..4d94b530c86e2 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_empty_data.json b/test/interpreter_functional/snapshots/session/tagcloud_empty_data.json index 0910e67409423..8c4e9fc5cd523 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_empty_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_empty_data.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[],"type":"datatable"},"visParams":{"ariaLabel":null,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[],"type":"datatable"},"visParams":{"ariaLabel":null,"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json index 21e213ebb9c27..f142588711a31 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"isPreview":false,"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"colorMapping":null,"isPreview":false,"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json index f340c5b653e35..291e6b40e6bfd 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_options.json b/test/interpreter_functional/snapshots/session/tagcloud_options.json index ecbafbbc0afba..381d3afc54067 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_options.json @@ -1 +1 @@ -{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/x-pack/plugins/lens/common/types.ts b/x-pack/plugins/lens/common/types.ts index ff269070a18c2..34baa25120e09 100644 --- a/x-pack/plugins/lens/common/types.ts +++ b/x-pack/plugins/lens/common/types.ts @@ -8,7 +8,7 @@ import type { Filter, FilterMeta } from '@kbn/es-query'; import type { Position } from '@elastic/charts'; import type { $Values } from '@kbn/utility-types'; -import type { CustomPaletteParams, PaletteOutput } from '@kbn/coloring'; +import { CustomPaletteParams, PaletteOutput, ColorMapping } from '@kbn/coloring'; import type { ColorMode } from '@kbn/charts-plugin/common'; import type { LegendSize } from '@kbn/visualizations-plugin/common'; import { CategoryDisplay, LegendDisplay, NumberDisplay, PieChartTypes } from './constants'; @@ -71,6 +71,7 @@ export interface SharedPieLayerState { legendMaxLines?: number; legendSize?: LegendSize; truncateLegend?: boolean; + colorMapping?: ColorMapping.Config; } export type PieLayerState = SharedPieLayerState & { diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 22c5a21ad3377..d68476598ad99 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -154,7 +154,7 @@ export function App({ useExecutionContext(executionContext, { type: 'application', - id: savedObjectId || 'new', + id: savedObjectId || 'new', // TODO: this doesn't consider when lens is saved by value page: 'editor', }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts index 0e677804d5f16..d9cfa8c84c62f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -18,6 +18,7 @@ import { type EventAnnotationGroupConfig, EVENT_ANNOTATION_GROUP_TYPE, } from '@kbn/event-annotation-common'; +import { DEFAULT_COLOR_MAPPING_CONFIG } from '@kbn/coloring'; import type { Datasource, DatasourceMap, @@ -289,7 +290,8 @@ export function initializeVisualization({ visualizationMap[visualizationState.activeId]?.initialize( () => '', visualizationState.state, - undefined, + // initialize a new visualization always with the new color mapping + { type: 'colorMapping', value: { ...DEFAULT_COLOR_MAPPING_CONFIG } }, annotationGroups, references ) ?? visualizationState.state diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts index e86f602465584..235e3b34538b8 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts @@ -457,7 +457,13 @@ describe('suggestion helpers', () => { it('should pass passed in main palette if specified', () => { const mockVisualization1 = createMockVisualization(); const mockVisualization2 = createMockVisualization(); - const mainPalette: PaletteOutput = { type: 'palette', name: 'mock' }; + const mainPalette: { type: 'legacyPalette'; value: PaletteOutput } = { + type: 'legacyPalette', + value: { + type: 'palette', + name: 'mock', + }, + }; datasourceMap.mock.getDatasourceSuggestionsFromCurrentState.mockReturnValue([ generateSuggestion(0), generateSuggestion(1), @@ -490,7 +496,13 @@ describe('suggestion helpers', () => { it('should query active visualization for main palette if not specified', () => { const mockVisualization1 = createMockVisualization(); const mockVisualization2 = createMockVisualization(); - const mainPalette: PaletteOutput = { type: 'palette', name: 'mock' }; + const mainPalette: { type: 'legacyPalette'; value: PaletteOutput } = { + type: 'legacyPalette', + value: { + type: 'palette', + name: 'mock', + }, + }; mockVisualization1.getMainPalette = jest.fn(() => mainPalette); datasourceMap.mock.getDatasourceSuggestionsFromCurrentState.mockReturnValue([ generateSuggestion(0), diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts index 6679e8b042480..c1032d144ac33 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts @@ -6,12 +6,11 @@ */ import type { Datatable } from '@kbn/expressions-plugin/common'; -import type { PaletteOutput } from '@kbn/coloring'; import type { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public'; import { LayerTypes } from '@kbn/expression-xy-plugin/public'; import type { DragDropIdentifier } from '@kbn/dom-drag-drop'; import { showMemoizedErrorNotification } from '../../lens_ui_errors'; -import type { +import { Visualization, Datasource, TableSuggestion, @@ -21,6 +20,7 @@ import type { VisualizeEditorContext, Suggestion, DatasourceLayers, + SuggestionRequest, } from '../../types'; import type { LayerType } from '../../../common/types'; import { @@ -64,7 +64,7 @@ export function getSuggestions({ visualizeTriggerFieldContext?: VisualizeFieldContext | VisualizeEditorContext; activeData?: Record; dataViews: DataViewsState; - mainPalette?: PaletteOutput; + mainPalette?: SuggestionRequest['mainPalette']; allowMixed?: boolean; }): Suggestion[] { const datasources = Object.entries(datasourceMap).filter( @@ -237,7 +237,7 @@ function getVisualizationSuggestions( datasourceSuggestion: DatasourceSuggestion & { datasourceId: string }, currentVisualizationState: unknown, subVisualizationId?: string, - mainPalette?: PaletteOutput, + mainPalette?: SuggestionRequest['mainPalette'], isFromContext?: boolean, activeData?: Record, allowMixed?: boolean diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx index 5b14bb43dfb16..3e613d5a23e89 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx @@ -519,7 +519,10 @@ describe('chart_switch', () => { it('should query main palette from active chart and pass into suggestions', async () => { const visualizationMap = mockVisualizationMap(); const mockPalette: PaletteOutput = { type: 'palette', name: 'mock' }; - visualizationMap.visA.getMainPalette = jest.fn(() => mockPalette); + visualizationMap.visA.getMainPalette = jest.fn(() => ({ + type: 'legacyPalette', + value: mockPalette, + })); visualizationMap.visB.getSuggestions.mockReturnValueOnce([]); const frame = mockFrame(['a', 'b', 'c']); const currentVisState = {}; @@ -550,7 +553,7 @@ describe('chart_switch', () => { expect(visualizationMap.visB.getSuggestions).toHaveBeenCalledWith( expect.objectContaining({ keptLayerIds: ['a'], - mainPalette: mockPalette, + mainPalette: { type: 'legacyPalette', value: mockPalette }, }) ); }); diff --git a/x-pack/plugins/lens/public/shared_components/coloring/palette_panel_container.tsx b/x-pack/plugins/lens/public/shared_components/coloring/palette_panel_container.tsx index b910354f1f68d..523c98c9d6903 100644 --- a/x-pack/plugins/lens/public/shared_components/coloring/palette_panel_container.tsx +++ b/x-pack/plugins/lens/public/shared_components/coloring/palette_panel_container.tsx @@ -27,8 +27,10 @@ export function PalettePanelContainer({ handleClose, siblingRef, children, + title, }: { isOpen: boolean; + title: string; handleClose: () => void; siblingRef: MutableRefObject; children?: React.ReactElement | React.ReactElement[]; @@ -76,16 +78,12 @@ export function PalettePanelContainer({ -

- - {i18n.translate('xpack.lens.table.palettePanelTitle', { - defaultMessage: 'Color', - })} - -

+ {title} +
diff --git a/x-pack/plugins/lens/public/shared_components/palette_picker.tsx b/x-pack/plugins/lens/public/shared_components/palette_picker.tsx index efd1caba7e4da..51977e551128e 100644 --- a/x-pack/plugins/lens/public/shared_components/palette_picker.tsx +++ b/x-pack/plugins/lens/public/shared_components/palette_picker.tsx @@ -36,28 +36,24 @@ export function PalettePicker({ }); return ( - <> - { - setPalette({ - type: 'palette', - name: newPalette, - }); - }} - valueOfSelected={activePalette?.name || 'default'} - selectionDisplay={'palette'} - /> - + { + setPalette({ + type: 'palette', + name: newPalette, + }); + }} + valueOfSelected={activePalette?.name || 'default'} + selectionDisplay={'palette'} + /> ); } diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index d549fbb71bdcf..0c09d84df9adc 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -7,7 +7,7 @@ import type { Ast } from '@kbn/interpreter'; import type { IconType } from '@elastic/eui/src/components/icon/icon'; import type { CoreStart, SavedObjectReference, ResolvedSimpleSavedObject } from '@kbn/core/public'; -import type { PaletteOutput } from '@kbn/coloring'; +import type { ColorMapping, PaletteOutput } from '@kbn/coloring'; import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; import type { MutableRefObject, ReactElement } from 'react'; import type { Filter, TimeRange } from '@kbn/es-query'; @@ -863,7 +863,12 @@ export interface SuggestionRequest { * State is only passed if the visualization is active. */ state?: T; - mainPalette?: PaletteOutput; + /** + * Passing the legacy palette or the new color mapping if available + */ + mainPalette?: + | { type: 'legacyPalette'; value: PaletteOutput } + | { type: 'colorMapping'; value: ColorMapping.Config }; isFromContext?: boolean; /** * The visualization needs to know which table is being suggested @@ -1026,11 +1031,15 @@ export interface Visualization string, nonPersistedState?: T, mainPalette?: PaletteOutput): T; + ( + addNewLayer: () => string, + nonPersistedState?: T, + mainPalette?: SuggestionRequest['mainPalette'] + ): T; ( addNewLayer: () => string, persistedState: P, - mainPalette?: PaletteOutput, + mainPalette?: SuggestionRequest['mainPalette'], annotationGroups?: AnnotationGroups, references?: SavedObjectReference[] ): T; @@ -1042,7 +1051,7 @@ export interface Visualization string[]; - getMainPalette?: (state: T) => undefined | PaletteOutput; + getMainPalette?: (state: T) => undefined | SuggestionRequest['mainPalette']; /** * Supported triggers of this visualization type when embedded somewhere diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.tsx index 09f1bc93d6779..60646bdb8d054 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/dimension_editor.tsx @@ -253,6 +253,9 @@ export function TableDimensionEditor( siblingRef={props.panelRef} isOpen={isPaletteOpen} handleClose={() => setIsPaletteOpen(!isPaletteOpen)} + title={i18n.translate('xpack.lens.table.colorByRangePanelTitle', { + defaultMessage: 'Color', + })} > setIsPaletteOpen(!isPaletteOpen)} + title={i18n.translate('xpack.lens.table.colorByRangePanelTitle', { + defaultMessage: 'Color', + })} > {activePalette && ( & { paletteService: PaletteRegistry; + isDarkMode: boolean; }; export function DimensionEditor(props: DimensionEditorProps) { @@ -30,10 +52,14 @@ export function DimensionEditor(props: DimensionEditorProps) { value: props.state, onChange: props.setState, }); + const [isPaletteOpen, setIsPaletteOpen] = useState(false); const currentLayer = localState.layers.find((layer) => layer.layerId === props.layerId); - const setConfig = React.useCallback( + const canUseColorMapping = currentLayer && currentLayer.colorMapping ? true : false; + const [useNewColorMapping, setUseNewColorMapping] = useState(canUseColorMapping); + + const setConfig = useCallback( ({ color }) => { if (!currentLayer) { return; @@ -61,6 +87,23 @@ export function DimensionEditor(props: DimensionEditorProps) { [currentLayer, localState, props.accessor, setLocalState] ); + const setColorMapping = useCallback( + (colorMapping?: ColorMapping.Config) => { + setLocalState({ + ...localState, + layers: localState.layers.map((layer) => + layer.layerId === currentLayer?.layerId + ? { + ...layer, + colorMapping, + } + : layer + ), + }); + }, + [localState, currentLayer, setLocalState] + ); + if (!currentLayer) { return null; } @@ -84,17 +127,125 @@ export function DimensionEditor(props: DimensionEditorProps) { }) : undefined; + const colors = getColorsFromMapping(props.isDarkMode, currentLayer.colorMapping); + const table = props.frame.activeData?.[currentLayer.layerId]; + const splitCategories = getColorCategories(table?.rows ?? [], props.accessor); + return ( <> {props.accessor === firstNonCollapsedColumnId && ( - { - setLocalState({ ...props.state, palette: newPalette }); - }} - /> + + + + { + setIsPaletteOpen(!isPaletteOpen); + }} + /> + + + { + setIsPaletteOpen(!isPaletteOpen); + }} + size="xs" + /> + setIsPaletteOpen(!isPaletteOpen)} + title={ + useNewColorMapping + ? i18n.translate('xpack.lens.colorMapping.editColorMappingTitle', { + defaultMessage: 'Edit colors by term mapping', + }) + : i18n.translate('xpack.lens.colorMapping.editColorsTitle', { + defaultMessage: 'Edit colors', + }) + } + > +
+ + + + + {i18n.translate('xpack.lens.colorMapping.tryLabel', { + defaultMessage: 'Use the new Color Mapping feature', + })}{' '} + + {i18n.translate('xpack.lens.colorMapping.techPreviewLabel', { + defaultMessage: 'Tech preview', + })} + + + + } + 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); + }} + /> + + + {canUseColorMapping || useNewColorMapping ? ( + setColorMapping(model)} + palettes={AVAILABLE_PALETTES} + data={{ + type: 'categories', + categories: splitCategories, + }} + specialTokens={SPECIAL_TOKENS_STRING_CONVERTION} + /> + ) : ( + { + setLocalState({ ...props.state, palette: newPalette }); + }} + /> + )} + + +
+
+
+
+
)} + {/* TODO: understand how this works */} {showColorPicker && ( { }); it('should keep passed in palette', () => { - const mainPalette: PaletteOutput = { type: 'palette', name: 'mock' }; const results = suggestions({ table: { layerId: 'first', @@ -617,10 +616,13 @@ describe('suggestions', () => { }, state: undefined, keptLayerIds: ['first'], - mainPalette, + mainPalette: { + type: 'legacyPalette', + value: { type: 'palette', name: 'mock' }, + }, }); - expect(results[0].state.palette).toEqual(mainPalette); + expect(results[0].state.palette).toEqual({ type: 'palette', name: 'mock' }); }); it('should keep the layer settings and palette when switching from treemap', () => { @@ -681,6 +683,7 @@ describe('suggestions', () => { legendMaxLines: 1, truncateLegend: true, nestedLegend: true, + colorMapping: DEFAULT_COLOR_MAPPING_CONFIG, }, ], }, @@ -1060,6 +1063,7 @@ describe('suggestions', () => { Object { "allowMultipleMetrics": false, "categoryDisplay": "default", + "colorMapping": undefined, "layerId": "first", "layerType": "data", "legendDisplay": "show", @@ -1169,6 +1173,7 @@ describe('suggestions', () => { "layers": Array [ Object { "categoryDisplay": "default", + "colorMapping": undefined, "layerId": "first", "layerType": "data", "legendDisplay": "show", diff --git a/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts b/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts index f3dea7c54b989..e78c203670aec 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts +++ b/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts @@ -7,6 +7,7 @@ import { partition } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { DEFAULT_COLOR_MAPPING_CONFIG } from '@kbn/coloring'; import type { SuggestionRequest, TableSuggestionColumn, @@ -131,7 +132,7 @@ export function suggestions({ score: state && !hasCustomSuggestionsExists(state.shape) ? 0.6 : 0.4, state: { shape: newShape, - palette: mainPalette || state?.palette, + palette: mainPalette?.type === 'legacyPalette' ? mainPalette.value : state?.palette, layers: [ state?.layers[0] ? { @@ -140,6 +141,11 @@ export function suggestions({ primaryGroups: groups.map((col) => col.columnId), metrics: metricColumnIds, layerType: layerTypes.DATA, + colorMapping: !mainPalette + ? { ...DEFAULT_COLOR_MAPPING_CONFIG } + : mainPalette?.type === 'colorMapping' + ? mainPalette.value + : state.layers[0].colorMapping, } : { layerId: table.layerId, @@ -150,6 +156,11 @@ export function suggestions({ legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, layerType: layerTypes.DATA, + colorMapping: !mainPalette + ? { ...DEFAULT_COLOR_MAPPING_CONFIG } + : mainPalette?.type === 'colorMapping' + ? mainPalette.value + : undefined, }, ], }, @@ -196,7 +207,7 @@ export function suggestions({ score: state?.shape === PieChartTypes.TREEMAP ? 0.7 : 0.5, state: { shape: PieChartTypes.TREEMAP, - palette: mainPalette || state?.palette, + palette: mainPalette?.type === 'legacyPalette' ? mainPalette.value : state?.palette, layers: [ state?.layers[0] ? { @@ -209,6 +220,10 @@ export function suggestions({ ? CategoryDisplay.DEFAULT : state.layers[0].categoryDisplay, layerType: layerTypes.DATA, + colorMapping: + mainPalette?.type === 'colorMapping' + ? mainPalette.value + : state.layers[0].colorMapping, } : { layerId: table.layerId, @@ -219,6 +234,7 @@ export function suggestions({ legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, layerType: layerTypes.DATA, + colorMapping: mainPalette?.type === 'colorMapping' ? mainPalette.value : undefined, }, ], }, @@ -243,7 +259,7 @@ export function suggestions({ score: state?.shape === PieChartTypes.MOSAIC ? 0.7 : 0.5, state: { shape: PieChartTypes.MOSAIC, - palette: mainPalette || state?.palette, + palette: mainPalette?.type === 'legacyPalette' ? mainPalette.value : state?.palette, layers: [ state?.layers[0] ? { @@ -255,6 +271,10 @@ export function suggestions({ categoryDisplay: CategoryDisplay.DEFAULT, layerType: layerTypes.DATA, allowMultipleMetrics: false, + colorMapping: + mainPalette?.type === 'colorMapping' + ? mainPalette.value + : state.layers[0].colorMapping, } : { layerId: table.layerId, @@ -267,6 +287,7 @@ export function suggestions({ nestedLegend: false, layerType: layerTypes.DATA, allowMultipleMetrics: false, + colorMapping: mainPalette?.type === 'colorMapping' ? mainPalette.value : undefined, }, ], }, @@ -290,7 +311,7 @@ export function suggestions({ score: state?.shape === PieChartTypes.WAFFLE ? 0.7 : 0.4, state: { shape: PieChartTypes.WAFFLE, - palette: mainPalette || state?.palette, + palette: mainPalette?.type === 'legacyPalette' ? mainPalette.value : state?.palette, layers: [ state?.layers[0] ? { @@ -301,6 +322,10 @@ export function suggestions({ secondaryGroups: [], categoryDisplay: CategoryDisplay.DEFAULT, layerType: layerTypes.DATA, + colorMapping: + mainPalette?.type === 'colorMapping' + ? mainPalette.value + : state.layers[0].colorMapping, } : { layerId: table.layerId, @@ -311,6 +336,7 @@ export function suggestions({ legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, layerType: layerTypes.DATA, + colorMapping: mainPalette?.type === 'colorMapping' ? mainPalette.value : undefined, }, ], }, diff --git a/x-pack/plugins/lens/public/visualizations/partition/to_expression.ts b/x-pack/plugins/lens/public/visualizations/partition/to_expression.ts index c592f7d369eb3..29e5fd399d148 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/to_expression.ts +++ b/x-pack/plugins/lens/public/visualizations/partition/to_expression.ts @@ -7,7 +7,7 @@ import type { Ast } from '@kbn/interpreter'; import { Position } from '@elastic/charts'; -import type { PaletteOutput, PaletteRegistry } from '@kbn/coloring'; +import { PaletteOutput, PaletteRegistry } from '@kbn/coloring'; import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/public'; import type { @@ -175,7 +175,6 @@ const generateCommonArguments = ( const datasource = datasourceLayers[layer.layerId]; const columnToLabelMap = getColumnToLabelMap(layer.metrics, datasource); const sortedMetricAccessors = getSortedAccessorsForGroup(datasource, layer, 'metrics'); - return { labels: generateCommonLabelsAstArgs(state, attributes, layer, columnToLabelMap), buckets: operations @@ -200,6 +199,7 @@ const generateCommonArguments = ( layer.truncateLegend ?? getDefaultVisualValuesForLayer(state, datasourceLayers).truncateText, palette: generatePaletteAstArguments(paletteService, state.palette), addTooltip: true, + colorMapping: layer.colorMapping ? JSON.stringify(layer.colorMapping) : undefined, }; }; diff --git a/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx b/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx index 6b4767b9177e5..429089f743c33 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx @@ -8,13 +8,19 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { PaletteRegistry } from '@kbn/coloring'; +import { + ColorMapping, + DEFAULT_COLOR_MAPPING_CONFIG, + PaletteRegistry, + getColorsFromMapping, +} from '@kbn/coloring'; import { ThemeServiceStart } from '@kbn/core/public'; import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public'; import { EuiSpacer } from '@elastic/eui'; import { PartitionVisConfiguration } from '@kbn/visualizations-plugin/common/convert_to_lens'; import { LayerTypes } from '@kbn/expression-xy-plugin/public'; import { AccessorConfig } from '@kbn/visualization-ui-components'; +import useObservable from 'react-use/lib/useObservable'; import type { FormBasedPersistedState } from '../../datasources/form_based/types'; import type { Visualization, @@ -51,7 +57,7 @@ const metricLabel = i18n.translate('xpack.lens.pie.groupMetricLabelSingular', { defaultMessage: 'Metric', }); -function newLayerState(layerId: string): PieLayerState { +function newLayerState(layerId: string, colorMapping: ColorMapping.Config): PieLayerState { return { layerId, primaryGroups: [], @@ -62,6 +68,7 @@ function newLayerState(layerId: string): PieLayerState { legendDisplay: LegendDisplay.DEFAULT, nestedLegend: false, layerType: LayerTypes.DATA, + colorMapping, }; } @@ -137,7 +144,9 @@ export const getPieVisualization = ({ clearLayer(state) { return { shape: state.shape, - layers: state.layers.map((l) => newLayerState(l.layerId)), + layers: state.layers.map((l) => + newLayerState(l.layerId, { ...DEFAULT_COLOR_MAPPING_CONFIG }) + ), }; }, @@ -156,13 +165,29 @@ export const getPieVisualization = ({ return ( state || { shape: PieChartTypes.DONUT, - layers: [newLayerState(addNewLayer())], - palette: mainPalette, + layers: [ + newLayerState( + addNewLayer(), + mainPalette?.type === 'colorMapping' + ? mainPalette.value + : { ...DEFAULT_COLOR_MAPPING_CONFIG } + ), + ], + palette: mainPalette?.type === 'legacyPalette' ? mainPalette.value : undefined, } ); }, - getMainPalette: (state) => (state ? state.palette : undefined), + getMainPalette: (state) => { + if (!state) { + return undefined; + } + return state.layers.length > 0 && state.layers[0].colorMapping + ? { type: 'colorMapping', value: state.layers[0].colorMapping } + : state.palette + ? { type: 'legacyPalette', value: state.palette } + : undefined; + }, getSuggestions: suggestions, @@ -174,6 +199,19 @@ export const getPieVisualization = ({ const datasource = frame.datasourceLayers[layer.layerId]; + let colors: string[] = []; + kibanaTheme.theme$ + .subscribe({ + next(theme) { + colors = state.layers[0]?.colorMapping + ? getColorsFromMapping(theme.darkMode, state.layers[0].colorMapping) + : paletteService + .get(state.palette?.name || 'default') + .getCategoricalColors(10, state.palette?.params); + }, + }) + .unsubscribe(); + const getPrimaryGroupConfig = (): VisualizationDimensionGroupConfig => { const originalOrder = getSortedAccessorsForGroup(datasource, layer, 'primaryGroups'); // When we add a column it could be empty, and therefore have no order @@ -187,9 +225,7 @@ export const getPieVisualization = ({ accessors.forEach((accessorConfig) => { if (firstNonCollapsedColumnId === accessorConfig.columnId) { accessorConfig.triggerIconType = 'colorBy'; - accessorConfig.palette = paletteService - .get(state.palette?.name || 'default') - .getCategoricalColors(10, state.palette?.params); + accessorConfig.palette = colors; } }); @@ -459,7 +495,8 @@ export const getPieVisualization = ({ }; }, DimensionEditorComponent(props) { - return ; + const isDarkMode = useObservable(kibanaTheme.theme$, { darkMode: false }).darkMode; + return ; }, DimensionEditorDataExtraComponent(props) { return ; diff --git a/x-pack/plugins/lens/public/visualizations/tagcloud/index.ts b/x-pack/plugins/lens/public/visualizations/tagcloud/index.ts index 129d8f4eb545f..e58f8fe673127 100644 --- a/x-pack/plugins/lens/public/visualizations/tagcloud/index.ts +++ b/x-pack/plugins/lens/public/visualizations/tagcloud/index.ts @@ -19,7 +19,7 @@ export class TagcloudVisualization { editorFrame.registerVisualization(async () => { const { getTagcloudVisualization } = await import('../../async_services'); const palettes = await charts.palettes.getPalettes(); - return getTagcloudVisualization({ paletteService: palettes, theme: core.theme }); + return getTagcloudVisualization({ paletteService: palettes, kibanaTheme: core.theme }); }); } } diff --git a/x-pack/plugins/lens/public/visualizations/tagcloud/suggestions.ts b/x-pack/plugins/lens/public/visualizations/tagcloud/suggestions.ts index c85f7b0b28fe2..4a528c99d41ad 100644 --- a/x-pack/plugins/lens/public/visualizations/tagcloud/suggestions.ts +++ b/x-pack/plugins/lens/public/visualizations/tagcloud/suggestions.ts @@ -7,6 +7,7 @@ import { partition } from 'lodash'; import { IconChartTagcloud } from '@kbn/chart-icons'; +import { DEFAULT_COLOR_MAPPING_CONFIG } from '@kbn/coloring'; import type { SuggestionRequest, VisualizationSuggestion } from '../../types'; import type { TagcloudState } from './types'; import { DEFAULT_STATE, TAGCLOUD_LABEL } from './constants'; @@ -48,6 +49,11 @@ export function getSuggestions({ tagAccessor: bucket.columnId, valueAccessor: metrics[0].columnId, ...DEFAULT_STATE, + colorMapping: !mainPalette + ? { ...DEFAULT_COLOR_MAPPING_CONFIG } + : mainPalette?.type === 'colorMapping' + ? mainPalette.value + : undefined, }, }; }); diff --git a/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_visualization.tsx b/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_visualization.tsx index d1bd1ec337bc0..c9449f2dad178 100644 --- a/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_visualization.tsx @@ -16,9 +16,10 @@ import { buildExpressionFunction, ExpressionFunctionTheme, } from '@kbn/expressions-plugin/common'; -import { PaletteRegistry } from '@kbn/coloring'; +import { PaletteRegistry, DEFAULT_COLOR_MAPPING_CONFIG, getColorsFromMapping } from '@kbn/coloring'; import { IconChartTagcloud } from '@kbn/chart-icons'; import { SystemPaletteExpressionFunctionDefinition } from '@kbn/charts-plugin/common'; +import useObservable from 'react-use/lib/useObservable'; import type { OperationMetadata, Visualization } from '../..'; import type { TagcloudState } from './types'; import { getSuggestions } from './suggestions'; @@ -31,10 +32,10 @@ const METRIC_GROUP_ID = 'metric'; export const getTagcloudVisualization = ({ paletteService, - theme, + kibanaTheme, }: { paletteService: PaletteRegistry; - theme: ThemeServiceStart; + kibanaTheme: ThemeServiceStart; }): Visualization => ({ id: 'lnsTagcloud', @@ -89,6 +90,15 @@ export const getTagcloudVisualization = ({ }, }; }, + getMainPalette: (state) => { + if (!state) return; + + return state.colorMapping + ? { type: 'colorMapping', value: state.colorMapping } + : state.palette + ? { type: 'legacyPalette', value: state.palette } + : undefined; + }, triggers: [VIS_EVENT_TO_TRIGGER.filter], @@ -98,11 +108,27 @@ export const getTagcloudVisualization = ({ layerId: addNewLayer(), layerType: LayerTypes.DATA, ...DEFAULT_STATE, + colorMapping: { ...DEFAULT_COLOR_MAPPING_CONFIG }, } ); }, getConfiguration({ state }) { + const canUseColorMapping = state.colorMapping ? true : false; + let colors: string[] = []; + if (canUseColorMapping) { + kibanaTheme.theme$ + .subscribe({ + next(theme) { + colors = getColorsFromMapping(theme.darkMode, state.colorMapping); + }, + }) + .unsubscribe(); + } else { + colors = paletteService + .get(state.palette?.name || 'default') + .getCategoricalColors(10, state.palette?.params); + } return { groups: [ { @@ -116,9 +142,7 @@ export const getTagcloudVisualization = ({ { columnId: state.tagAccessor, triggerIconType: 'colorBy', - palette: paletteService - .get(state.palette?.name || 'default') - .getCategoricalColors(10, state.palette?.params), + palette: colors, }, ] : [], @@ -197,6 +221,7 @@ export const getTagcloudVisualization = ({ ), ]).toAst(), showLabel: state.showLabel, + colorMapping: state.colorMapping ? JSON.stringify(state.colorMapping) : undefined, }).toAst(), ], }; @@ -235,6 +260,7 @@ export const getTagcloudVisualization = ({ ), ]).toAst(), showLabel: false, + colorMapping: state.colorMapping ? JSON.stringify(state.colorMapping) : undefined, }).toAst(), ], }; @@ -266,12 +292,16 @@ export const getTagcloudVisualization = ({ }, DimensionEditorComponent(props) { + const isDarkMode: boolean = useObservable(kibanaTheme.theme$, { darkMode: false }).darkMode; if (props.groupId === TAG_GROUP_ID) { return ( ); } diff --git a/x-pack/plugins/lens/public/visualizations/tagcloud/tags_dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/tagcloud/tags_dimension_editor.tsx index e91a73982dd38..1728e6240ad9f 100644 --- a/x-pack/plugins/lens/public/visualizations/tagcloud/tags_dimension_editor.tsx +++ b/x-pack/plugins/lens/public/visualizations/tagcloud/tags_dimension_editor.tsx @@ -6,27 +6,196 @@ */ import React from 'react'; -import { PaletteRegistry } from '@kbn/coloring'; +import { + PaletteRegistry, + CategoricalColorMapping, + DEFAULT_COLOR_MAPPING_CONFIG, + ColorMapping, + SPECIAL_TOKENS_STRING_CONVERTION, + PaletteOutput, + AVAILABLE_PALETTES, + getColorsFromMapping, +} from '@kbn/coloring'; +import { i18n } from '@kbn/i18n'; +import { + EuiButtonIcon, + EuiColorPaletteDisplay, + EuiFlexGroup, + EuiFlexItem, + EuiSwitch, + EuiFormRow, + EuiText, + EuiBadge, +} from '@elastic/eui'; +import { useState, MutableRefObject, useCallback } from 'react'; +import { PalettePicker } from '@kbn/coloring/src/shared_components/coloring/palette_picker'; +import { useDebouncedValue } from '@kbn/visualization-ui-components'; +import { getColorCategories } from '@kbn/chart-expressions-common'; import type { TagcloudState } from './types'; -import { PalettePicker } from '../../shared_components'; +import { PalettePanelContainer } from '../../shared_components'; +import { FramePublicAPI } from '../../types'; +import { trackUiCounterEvents } from '../../lens_ui_telemetry'; interface Props { paletteService: PaletteRegistry; state: TagcloudState; setState: (state: TagcloudState) => void; + frame: FramePublicAPI; + panelRef: MutableRefObject; + isDarkMode: boolean; } -export function TagsDimensionEditor(props: Props) { +export function TagsDimensionEditor({ + state, + frame, + setState, + panelRef, + isDarkMode, + paletteService, +}: Props) { + const { inputValue: localState, handleInputChange: setLocalState } = + useDebouncedValue({ + value: state, + onChange: setState, + }); + const [isPaletteOpen, setIsPaletteOpen] = useState(false); + const [useNewColorMapping, setUseNewColorMapping] = useState(state.colorMapping ? true : false); + + const colors = getColorsFromMapping(isDarkMode, state.colorMapping); + const table = frame.activeData?.[state.layerId]; + const splitCategories = getColorCategories(table?.rows ?? [], state.tagAccessor); + + const setColorMapping = useCallback( + (colorMapping?: ColorMapping.Config) => { + setLocalState({ + ...localState, + colorMapping, + }); + }, + [localState, setLocalState] + ); + + const setPalette = useCallback( + (palette: PaletteOutput) => { + setLocalState({ + ...localState, + palette, + colorMapping: undefined, + }); + }, + [localState, setLocalState] + ); + + const canUseColorMapping = state.colorMapping; + return ( - { - props.setState({ - ...props.state, - palette: newPalette, - }); - }} - /> + + + + { + setIsPaletteOpen(!isPaletteOpen); + }} + /> + + + { + setIsPaletteOpen(!isPaletteOpen); + }} + size="xs" + /> + setIsPaletteOpen(!isPaletteOpen)} + title={ + useNewColorMapping + ? i18n.translate('xpack.lens.colorMapping.editColorMappingTitle', { + defaultMessage: 'Edit colors by term mapping', + }) + : i18n.translate('xpack.lens.colorMapping.editColorsTitle', { + defaultMessage: 'Edit colors', + }) + } + > +
+ + + + + {i18n.translate('xpack.lens.colorMapping.tryLabel', { + defaultMessage: 'Use the new Color Mapping feature', + })}{' '} + + {i18n.translate('xpack.lens.colorMapping.techPreviewLabel', { + defaultMessage: 'Tech preview', + })} + + + + } + 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); + }} + /> + + + {canUseColorMapping || useNewColorMapping ? ( + setColorMapping(model)} + palettes={AVAILABLE_PALETTES} + data={{ + type: 'categories', + categories: splitCategories, + }} + specialTokens={SPECIAL_TOKENS_STRING_CONVERTION} + /> + ) : ( + { + setPalette(newPalette); + }} + /> + )} + + +
+
+
+
+
); } diff --git a/x-pack/plugins/lens/public/visualizations/tagcloud/types.ts b/x-pack/plugins/lens/public/visualizations/tagcloud/types.ts index c4a6ff1ddb6ad..afc83074c1fa2 100644 --- a/x-pack/plugins/lens/public/visualizations/tagcloud/types.ts +++ b/x-pack/plugins/lens/public/visualizations/tagcloud/types.ts @@ -7,7 +7,7 @@ import { $Values } from '@kbn/utility-types'; import { Datatable } from '@kbn/expressions-plugin/common'; -import type { PaletteOutput } from '@kbn/coloring'; +import { PaletteOutput, ColorMapping } from '@kbn/coloring'; import { Orientation } from '@kbn/expression-tagcloud-plugin/common'; export interface TagcloudState { @@ -19,6 +19,7 @@ export interface TagcloudState { orientation: $Values; palette?: PaletteOutput; showLabel: boolean; + colorMapping?: ColorMapping.Config; } export interface TagcloudConfig extends TagcloudState { diff --git a/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts b/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts index 4e8264a733398..2b80a39fc3b53 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts @@ -7,7 +7,7 @@ import { Ast } from '@kbn/interpreter'; import { Position, ScaleType } from '@elastic/charts'; -import type { PaletteRegistry } from '@kbn/coloring'; +import { PaletteRegistry } from '@kbn/coloring'; import { buildExpression, buildExpressionFunction, @@ -511,6 +511,7 @@ const dataLayerToExpression = ( name: 'default', }), ]).toAst(), + colorMapping: layer.colorMapping ? JSON.stringify(layer.colorMapping) : undefined, }); return { diff --git a/x-pack/plugins/lens/public/visualizations/xy/types.ts b/x-pack/plugins/lens/public/visualizations/xy/types.ts index e863f04bcdc05..8961c38b1582a 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/types.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/types.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { $Values } from '@kbn/utility-types'; -import type { PaletteOutput } from '@kbn/coloring'; +import type { ColorMapping, PaletteOutput } from '@kbn/coloring'; import type { LegendConfig, AxisExtentConfig, @@ -103,6 +103,7 @@ export interface XYDataLayerConfig { xScaleType?: XScaleType; isHistogram?: boolean; columnToLabel?: string; + colorMapping?: ColorMapping.Config; } export interface XYReferenceLineLayerConfig { diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.tsx index e89b3a843e8ac..c9d52d43df7ef 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.tsx @@ -57,6 +57,7 @@ import { } from './visualization_helpers'; import { cloneDeep } from 'lodash'; import { DataViewsServicePublic } from '@kbn/data-views-plugin/public'; +import { EUIAmsterdamColorBlindPalette } from '@kbn/coloring'; const DATE_HISTORGRAM_COLUMN_ID = 'date_histogram_column'; const exampleAnnotation: EventAnnotationConfig = { @@ -221,8 +222,30 @@ describe('xy_visualization', () => { "layers": Array [ Object { "accessors": Array [], + "colorMapping": Object { + "assignmentMode": "auto", + "assignments": Array [], + "colorMode": Object { + "type": "categorical", + }, + "paletteId": "${EUIAmsterdamColorBlindPalette.id}", + "specialAssignments": Array [ + Object { + "color": Object { + "colorIndex": 1, + "paletteId": "neutral", + "type": "categorical", + }, + "rule": Object { + "type": "other", + }, + "touched": false, + }, + ], + }, "layerId": "l1", "layerType": "data", + "palette": undefined, "position": "top", "seriesType": "bar_stacked", "showGridlines": false, diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx index 9f5f9755d1781..812d74ddcb331 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx @@ -25,6 +25,8 @@ import type { EventAnnotationGroupConfig } from '@kbn/event-annotation-common'; import { isEqual } from 'lodash'; import { type AccessorConfig, DimensionTrigger } from '@kbn/visualization-ui-components'; import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import { DEFAULT_COLOR_MAPPING_CONFIG, getColorsFromMapping } from '@kbn/coloring'; +import useObservable from 'react-use/lib/useObservable'; import { generateId } from '../../id_generator'; import { isDraggedDataViewField, @@ -254,7 +256,7 @@ export const getXyVisualization = ({ initialize( addNewLayer, state, - _mainPalette?, + mainPalette?, annotationGroups?: AnnotationGroups, references?: SavedObjectReference[] ) { @@ -276,6 +278,11 @@ export const getXyVisualization = ({ seriesType: defaultSeriesType, showGridlines: false, layerType: LayerTypes.DATA, + palette: mainPalette?.type === 'legacyPalette' ? mainPalette.value : undefined, + colorMapping: + mainPalette?.type === 'colorMapping' + ? mainPalette.value + : { ...DEFAULT_COLOR_MAPPING_CONFIG }, }, ], } @@ -416,6 +423,22 @@ export const getXyVisualization = ({ } ).length < 2; + const canUseColorMapping = layer.colorMapping ? true : false; + let colors: string[] = []; + if (canUseColorMapping) { + kibanaTheme.theme$ + .subscribe({ + next(theme) { + colors = getColorsFromMapping(theme.darkMode, layer.colorMapping); + }, + }) + .unsubscribe(); + } else { + colors = paletteService + .get(dataLayer.palette?.name || 'default') + .getCategoricalColors(10, dataLayer.palette?.params); + } + return { groups: [ { @@ -447,11 +470,7 @@ export const getXyVisualization = ({ { columnId: dataLayer.splitAccessor, triggerIconType: dataLayer.collapseFn ? 'aggregate' : 'colorBy', - palette: dataLayer.collapseFn - ? undefined - : paletteService - .get(dataLayer.palette?.name || 'default') - .getCategoricalColors(10, dataLayer.palette?.params), + palette: dataLayer.collapseFn ? undefined : colors, }, ] : [], @@ -469,7 +488,13 @@ export const getXyVisualization = ({ getMainPalette: (state) => { if (!state || state.layers.length === 0) return; - return getFirstDataLayer(state.layers)?.palette; + const firstDataLayer = getFirstDataLayer(state.layers); + + return firstDataLayer?.colorMapping + ? { type: 'colorMapping', value: firstDataLayer.colorMapping } + : firstDataLayer?.palette + ? { type: 'legacyPalette', value: firstDataLayer.palette } + : undefined; }, getDropProps(dropProps) { @@ -641,13 +666,15 @@ export const getXyVisualization = ({ formatFactory: fieldFormats.deserialize, paletteService, }; + + const darkMode: boolean = useObservable(kibanaTheme.theme$, { darkMode: false }).darkMode; const layer = props.state.layers.find((l) => l.layerId === props.layerId)!; const dimensionEditor = isReferenceLayer(layer) ? ( ) : isAnnotationsLayer(layer) ? ( ) : ( - + ); return dimensionEditor; diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/dimension_editor.tsx index 94c7a326ccbb4..44114e8d560ed 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/dimension_editor.tsx @@ -5,21 +5,45 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiButtonGroup, EuiFormRow, htmlIdGenerator } from '@elastic/eui'; -import type { PaletteRegistry } from '@kbn/coloring'; import { useDebouncedValue } from '@kbn/visualization-ui-components'; import { ColorPicker } from '@kbn/visualization-ui-components'; + +import { + EuiBadge, + EuiButtonGroup, + EuiButtonIcon, + EuiColorPaletteDisplay, + 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 { getColorCategories } from '@kbn/chart-expressions-common'; 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 { PalettePicker } from '../../../shared_components'; +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 extends Array ? P : T; @@ -43,11 +67,16 @@ export function DataDimensionEditor( props: VisualizationDimensionEditorProps & { formatFactory: FormatFactory; paletteService: PaletteRegistry; + darkMode: boolean; } ) { - const { state, setState, layerId, accessor } = props; + const { state, layerId, accessor, darkMode } = props; const index = state.layers.findIndex((l) => l.layerId === layerId); const layer = state.layers[index] as XYDataLayerConfig; + const canUseColorMapping = layer.colorMapping ? true : false; + + const [isPaletteOpen, setIsPaletteOpen] = useState(false); + const [useNewColorMapping, setUseNewColorMapping] = useState(canUseColorMapping); const { inputValue: localState, handleInputChange: setLocalState } = useDebouncedValue({ value: props.state, @@ -79,6 +108,19 @@ export function DataDimensionEditor( [accessor, index, localState, layer, setLocalState] ); + const setColorMapping = useCallback( + (colorMapping?: ColorMapping.Config) => { + setLocalState(updateLayer(localState, { ...layer, colorMapping }, index)); + }, + [index, localState, layer, setLocalState] + ); + const setPalette = useCallback( + (palette: PaletteOutput) => { + setLocalState(updateLayer(localState, { ...layer, palette }, index)); + }, + [index, localState, layer, setLocalState] + ); + const overwriteColor = getSeriesColor(layer, accessor); const assignedColor = useMemo(() => { const sortedAccessors: string[] = getSortedAccessors( @@ -105,19 +147,128 @@ export function DataDimensionEditor( }, [props.frame, props.paletteService, state.layers, accessor, props.formatFactory, layer]); const localLayer: XYDataLayerConfig = layer; - if (props.groupId === 'breakdown') { + + 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 ( - <> - {!layer.collapseFn && ( - { - setState(updateLayer(localState, { ...localLayer, palette: newPalette }, index)); - }} - /> - )} - + + + + { + setIsPaletteOpen(!isPaletteOpen); + }} + /> + + + { + setIsPaletteOpen(!isPaletteOpen); + }} + size="xs" + /> + setIsPaletteOpen(!isPaletteOpen)} + title={ + useNewColorMapping + ? i18n.translate('xpack.lens.colorMapping.editColorMappingTitle', { + defaultMessage: 'Edit colors by term mapping', + }) + : i18n.translate('xpack.lens.colorMapping.editColorsTitle', { + defaultMessage: 'Edit colors', + }) + } + > +
+ + + + + {i18n.translate('xpack.lens.colorMapping.tryLabel', { + defaultMessage: 'Use the new Color Mapping feature', + })}{' '} + + {i18n.translate('xpack.lens.colorMapping.techPreviewLabel', { + defaultMessage: 'Tech preview', + })} + + + + } + 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); + }} + /> + + + + {canUseColorMapping || useNewColorMapping ? ( + setColorMapping(model)} + palettes={AVAILABLE_PALETTES} + data={{ + type: 'categories', + categories: splitCategories, + }} + specialTokens={SPECIAL_TOKENS_STRING_CONVERTION} + /> + ) : ( + { + setPalette(newPalette); + }} + /> + )} + + +
+
+
+
+
); } diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/xy_config_panel.test.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/xy_config_panel.test.tsx index 252c3de6b8e57..9a98f5bae168b 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/xy_config_panel.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/xy_config_panel.test.tsx @@ -272,6 +272,7 @@ describe('XY Config panels', () => { addLayer={jest.fn()} removeLayer={jest.fn()} datasource={{} as DatasourcePublicAPI} + darkMode={false} /> ); @@ -299,6 +300,7 @@ describe('XY Config panels', () => { addLayer={jest.fn()} removeLayer={jest.fn()} datasource={{} as DatasourcePublicAPI} + darkMode={false} /> ); @@ -347,6 +349,7 @@ describe('XY Config panels', () => { addLayer={jest.fn()} removeLayer={jest.fn()} datasource={{} as DatasourcePublicAPI} + darkMode={false} /> ); @@ -392,6 +395,7 @@ describe('XY Config panels', () => { addLayer={jest.fn()} removeLayer={jest.fn()} datasource={{} as DatasourcePublicAPI} + darkMode={false} /> ); @@ -437,6 +441,7 @@ describe('XY Config panels', () => { addLayer={jest.fn()} removeLayer={jest.fn()} datasource={{} as DatasourcePublicAPI} + darkMode={false} /> ); diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.test.ts b/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.test.ts index 8417d02d79995..a12afc7465579 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.test.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.test.ts @@ -18,7 +18,7 @@ import { generateId } from '../../id_generator'; import { getXyVisualization } from './xy_visualization'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { eventAnnotationServiceMock } from '@kbn/event-annotation-plugin/public/mocks'; -import type { PaletteOutput } from '@kbn/coloring'; +import { DEFAULT_COLOR_MAPPING_CONFIG, PaletteOutput } from '@kbn/coloring'; import { LayerTypes } from '@kbn/expression-xy-plugin/public'; import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks'; import { coreMock, themeServiceMock } from '@kbn/core/public/mocks'; @@ -757,7 +757,7 @@ describe('xy_suggestions', () => { changeType: 'unchanged', }, keptLayerIds: [], - mainPalette, + mainPalette: { type: 'legacyPalette', value: mainPalette }, }); expect((suggestion.state.layers as XYDataLayerConfig[])[0].palette).toEqual(mainPalette); @@ -773,7 +773,7 @@ describe('xy_suggestions', () => { changeType: 'unchanged', }, keptLayerIds: [], - mainPalette, + mainPalette: { type: 'legacyPalette', value: mainPalette }, }); expect((suggestion.state.layers as XYDataLayerConfig[])[0].palette).toEqual(undefined); @@ -913,7 +913,13 @@ describe('xy_suggestions', () => { expect(suggestions[0].state).toEqual({ ...currentState, preferredSeriesType: 'line', - layers: [{ ...currentState.layers[0], seriesType: 'line' }], + layers: [ + { + ...currentState.layers[0], + seriesType: 'line', + colorMapping: DEFAULT_COLOR_MAPPING_CONFIG, + }, + ], }); expect(suggestions[0].title).toEqual('Line chart'); }); @@ -954,12 +960,24 @@ describe('xy_suggestions', () => { expect(seriesSuggestion.state).toEqual({ ...currentState, preferredSeriesType: 'line', - layers: [{ ...currentState.layers[0], seriesType: 'line' }], + layers: [ + { + ...currentState.layers[0], + seriesType: 'line', + colorMapping: DEFAULT_COLOR_MAPPING_CONFIG, + }, + ], }); expect(stackSuggestion.state).toEqual({ ...currentState, preferredSeriesType: 'bar_stacked', - layers: [{ ...currentState.layers[0], seriesType: 'bar_stacked' }], + layers: [ + { + ...currentState.layers[0], + seriesType: 'bar_stacked', + colorMapping: DEFAULT_COLOR_MAPPING_CONFIG, + }, + ], }); expect(seriesSuggestion.title).toEqual('Line chart'); expect(stackSuggestion.title).toEqual('Stacked'); @@ -1081,6 +1099,7 @@ describe('xy_suggestions', () => { ...currentState.layers[0], xAccessor: 'product', splitAccessor: 'category', + colorMapping: DEFAULT_COLOR_MAPPING_CONFIG, }, ], }); @@ -1126,6 +1145,7 @@ describe('xy_suggestions', () => { ...currentState.layers[0], xAccessor: 'category', splitAccessor: 'product', + colorMapping: DEFAULT_COLOR_MAPPING_CONFIG, }, ], }); @@ -1172,6 +1192,7 @@ describe('xy_suggestions', () => { ...currentState.layers[0], xAccessor: 'timestamp', splitAccessor: 'product', + colorMapping: DEFAULT_COLOR_MAPPING_CONFIG, }, ], }); diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.ts b/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.ts index 33381822b6eed..b63acd9513300 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.ts @@ -8,8 +8,8 @@ import { i18n } from '@kbn/i18n'; import { partition } from 'lodash'; import { Position } from '@elastic/charts'; -import type { PaletteOutput } from '@kbn/coloring'; import { LayerTypes } from '@kbn/expression-xy-plugin/public'; +import { DEFAULT_COLOR_MAPPING_CONFIG } from '@kbn/coloring'; import type { SuggestionRequest, VisualizationSuggestion, @@ -96,7 +96,7 @@ function getSuggestionForColumns( keptLayerIds: string[], currentState?: State, seriesType?: SeriesType, - mainPalette?: PaletteOutput, + mainPalette?: SuggestionRequest['mainPalette'], allowMixed?: boolean ): VisualizationSuggestion | Array> | undefined { const [buckets, values] = partition(table.columns, (col) => col.operation.isBucketed); @@ -230,7 +230,7 @@ function getSuggestionsForLayer({ tableLabel?: string; keptLayerIds: string[]; requestedSeriesType?: SeriesType; - mainPalette?: PaletteOutput; + mainPalette?: SuggestionRequest['mainPalette']; allowMixed?: boolean; }): VisualizationSuggestion | Array> { const title = getSuggestionTitle(yValues, xValue, tableLabel); @@ -493,7 +493,7 @@ function buildSuggestion({ changeType: TableChangeType; keptLayerIds: string[]; hide?: boolean; - mainPalette?: PaletteOutput; + mainPalette?: SuggestionRequest['mainPalette']; allowMixed?: boolean; }) { if (seriesType.includes('percentage') && xValue?.operation.scale === 'ordinal' && !splitBy) { @@ -505,10 +505,11 @@ function buildSuggestion({ const newLayer: XYDataLayerConfig = { ...(existingLayer || {}), palette: - mainPalette || - (existingLayer && 'palette' in existingLayer + mainPalette?.type === 'legacyPalette' + ? mainPalette.value + : existingLayer && 'palette' in existingLayer ? (existingLayer as XYDataLayerConfig).palette - : undefined), + : undefined, layerId, seriesType, xAccessor: xValue?.columnId, @@ -519,6 +520,11 @@ function buildSuggestion({ ? existingLayer.yConfig.filter(({ forAccessor }) => accessors.indexOf(forAccessor) !== -1) : undefined, layerType: LayerTypes.DATA, + colorMapping: !mainPalette + ? { ...DEFAULT_COLOR_MAPPING_CONFIG } + : mainPalette?.type === 'colorMapping' + ? mainPalette.value + : undefined, }; const hasDateHistogramDomain = diff --git a/x-pack/plugins/lens/tsconfig.json b/x-pack/plugins/lens/tsconfig.json index ca536dc187c3f..4fb8f849d6d27 100644 --- a/x-pack/plugins/lens/tsconfig.json +++ b/x-pack/plugins/lens/tsconfig.json @@ -86,6 +86,7 @@ "@kbn/content-management-utils", "@kbn/serverless", "@kbn/ebt-tools", + "@kbn/chart-expressions-common", "@kbn/search-response-warnings", ], "exclude": [ diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 4ae31ffb358ef..8981de9b982c7 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -21322,7 +21322,6 @@ "xpack.lens.table.dynamicColoring.text": "Texte", "xpack.lens.table.hide.hideLabel": "Masquer", "xpack.lens.table.palettePanelContainer.back": "Retour", - "xpack.lens.table.palettePanelTitle": "Couleur", "xpack.lens.table.resize.reset": "Réinitialiser la largeur", "xpack.lens.table.rowHeight.auto": "Ajustement automatique", "xpack.lens.table.rowHeight.custom": "Personnalisé", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 87e23ff29cb1f..41665e626fc8c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -21337,7 +21337,6 @@ "xpack.lens.table.dynamicColoring.text": "テキスト", "xpack.lens.table.hide.hideLabel": "非表示", "xpack.lens.table.palettePanelContainer.back": "戻る", - "xpack.lens.table.palettePanelTitle": "色", "xpack.lens.table.resize.reset": "幅のリセット", "xpack.lens.table.rowHeight.auto": "自動的に合わせる", "xpack.lens.table.rowHeight.custom": "カスタム", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 6e3c128ec642a..33625a62a6356 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -21337,7 +21337,6 @@ "xpack.lens.table.dynamicColoring.text": "文本", "xpack.lens.table.hide.hideLabel": "隐藏", "xpack.lens.table.palettePanelContainer.back": "返回", - "xpack.lens.table.palettePanelTitle": "颜色", "xpack.lens.table.resize.reset": "重置宽度", "xpack.lens.table.rowHeight.auto": "自动适应", "xpack.lens.table.rowHeight.custom": "定制", diff --git a/x-pack/test/functional/apps/dashboard/group2/sync_colors.ts b/x-pack/test/functional/apps/dashboard/group2/sync_colors.ts index 5d1f590490c4d..a388a4d90af3b 100644 --- a/x-pack/test/functional/apps/dashboard/group2/sync_colors.ts +++ b/x-pack/test/functional/apps/dashboard/group2/sync_colors.ts @@ -68,6 +68,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension', operation: 'terms', field: 'geo.src', + palette: { mode: 'legacy', id: 'default' }, }); await PageObjects.lens.save('vis1', false, true); @@ -84,6 +85,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 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' }); diff --git a/x-pack/test/functional/apps/lens/group4/color_mapping.ts b/x-pack/test/functional/apps/lens/group4/color_mapping.ts new file mode 100644 index 0000000000000..0fc9f79afec79 --- /dev/null +++ b/x-pack/test/functional/apps/lens/group4/color_mapping.ts @@ -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 expect from '@kbn/expect'; + +import { + EUI_AMSTERDAM_PALETTE_COLORS, + ELASTIC_BRAND_PALETTE_COLORS, + EUIAmsterdamColorBlindPalette, + ElasticBrandPalette, +} from '@kbn/coloring/src/shared_components/color_mapping/palettes'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['visualize', 'lens', 'common']); + const elasticChart = getService('elasticChart'); + + describe('lens color mapping', () => { + before(async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + await elasticChart.setNewChartUiDebugFlag(true); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'average', + field: 'bytes', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: 'extension.raw', + palette: { mode: 'colorMapping', id: ElasticBrandPalette.id }, + keepOpen: true, + }); + }); + + it('should render correct color mapping', async () => { + const chart = await PageObjects.lens.getCurrentChartDebugState('xyVisChart'); + const legendColors = chart?.legend?.items?.map((item) => item.color.toLowerCase()) ?? []; + expect(legendColors).to.eql( + ELASTIC_BRAND_PALETTE_COLORS.slice(0, 5).map((c) => c.toLowerCase()) + ); + }); + it('should allow switching color mapping palette', async () => { + await PageObjects.lens.changeColorMappingPalette( + 'lnsXY_splitDimensionPanel > lnsLayerPanel-dimensionLink', + EUIAmsterdamColorBlindPalette.id + ); + const chart = await PageObjects.lens.getCurrentChartDebugState('xyVisChart'); + const legendColors = chart?.legend?.items?.map((item) => item.color.toLowerCase()) ?? []; + expect(legendColors).to.eql( + EUI_AMSTERDAM_PALETTE_COLORS.slice(0, 5).map((c) => c.toLowerCase()) + ); + }); + + it('should change categorical color', async () => { + await PageObjects.lens.changeColorMappingCategoricalColors( + 'lnsXY_splitDimensionPanel > lnsLayerPanel-dimensionLink', + 0, + 3 + ); + const chart = await PageObjects.lens.getCurrentChartDebugState('xyVisChart'); + const firstLegendItemColor = chart?.legend?.items?.[0]?.color?.toLowerCase() ?? 'NONE'; + expect(firstLegendItemColor).to.eql(EUI_AMSTERDAM_PALETTE_COLORS[3].toLowerCase()); + }); + }); +} diff --git a/x-pack/test/functional/apps/lens/group4/colors.ts b/x-pack/test/functional/apps/lens/group4/colors.ts index 4078b0663d7ae..20265d247cf83 100644 --- a/x-pack/test/functional/apps/lens/group4/colors.ts +++ b/x-pack/test/functional/apps/lens/group4/colors.ts @@ -4,13 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { ElasticBrandPalette } from '@kbn/coloring'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common']); describe('lens color palette tests', () => { - it('should allow to pick color palette in xy chart', async () => { + it('should allow to pick legacy color palette in xy chart', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); await PageObjects.lens.goToTimeRange(); @@ -31,11 +32,38 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension', operation: 'terms', field: '@message.raw', - palette: 'negative', + palette: { mode: 'legacy', id: 'negative' }, keepOpen: true, }); - await PageObjects.lens.assertPalette('negative'); + await PageObjects.lens.assertPalette('negative', true); + }); + it('should allow to pick color mapping palette in xy chart', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: 'geo.src', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'average', + field: 'bytes', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: '@message.raw', + palette: { mode: 'colorMapping', id: ElasticBrandPalette.id }, + keepOpen: true, + }); + + await PageObjects.lens.assertPalette(ElasticBrandPalette.id, false); }); it('should carry over palette to the pie chart', async () => { @@ -43,7 +71,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.openDimensionEditor( 'lnsPie_sliceByDimensionPanel > lns-dimensionTrigger' ); - await PageObjects.lens.assertPalette('negative'); + await PageObjects.lens.assertPalette(ElasticBrandPalette.id, false); }); it('should carry palette back to the bar chart', async () => { @@ -51,7 +79,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.openDimensionEditor( 'lnsXY_splitDimensionPanel > lns-dimensionTrigger' ); - await PageObjects.lens.assertPalette('negative'); + await PageObjects.lens.assertPalette(ElasticBrandPalette.id, false); }); }); } diff --git a/x-pack/test/functional/apps/lens/group4/index.ts b/x-pack/test/functional/apps/lens/group4/index.ts index 13cfa3fe421e1..9627b744300ba 100644 --- a/x-pack/test/functional/apps/lens/group4/index.ts +++ b/x-pack/test/functional/apps/lens/group4/index.ts @@ -73,6 +73,7 @@ export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext // total run time ~16m 30s loadTestFile(require.resolve('./colors')); // 1m 2s + loadTestFile(require.resolve('./color_mapping')); loadTestFile(require.resolve('./chart_data')); // 1m 10s loadTestFile(require.resolve('./time_shift')); // 1m loadTestFile(require.resolve('./dashboard')); // 6m 45s diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 493977e7fc735..6069ae838cea8 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -178,7 +178,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont field?: string; isPreviousIncompatible?: boolean; keepOpen?: boolean; - palette?: string; + palette?: { mode: 'legacy' | 'colorMapping'; id: string }; formula?: string; disableEmptyRows?: boolean; }) { @@ -205,7 +205,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont } if (opts.palette) { - await this.setPalette(opts.palette); + await this.setPalette(opts.palette.id, opts.palette.mode === 'legacy'); } if (opts.disableEmptyRows) { @@ -519,13 +519,26 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await PageObjects.header.waitUntilLoadingHasFinished(); }, - async assertPalette(palette: string) { + async assertPalette(paletteId: string, isLegacy: boolean) { await retry.try(async () => { - await testSubjects.click('lns-palettePicker'); + await testSubjects.click('lns_colorEditing_trigger'); + // open the palette picker + if (isLegacy) { + await testSubjects.click('lns-palettePicker'); + } else { + await testSubjects.click('kbnColoring_ColorMapping_PalettePicker'); + } const currentPalette = await ( await find.byCssSelector('[role=option][aria-selected=true]') ).getAttribute('id'); - expect(currentPalette).to.equal(palette); + // close the palette picker + if (isLegacy) { + await testSubjects.click('lns-palettePicker'); + } else { + await testSubjects.click('kbnColoring_ColorMapping_PalettePicker'); + } + expect(currentPalette).to.equal(paletteId); + await this.closePaletteEditor(); }); }, @@ -1214,9 +1227,20 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await testSubjects.click(`${paletteName}-palette`); }, - async setPalette(paletteName: string) { - await testSubjects.click('lns-palettePicker'); - await find.clickByCssSelector(`#${paletteName}`); + async setPalette(paletteId: string, isLegacy: boolean) { + await testSubjects.click('lns_colorEditing_trigger'); + await testSubjects.setEuiSwitch( + 'lns_colorMappingOrLegacyPalette_switch', + isLegacy ? 'uncheck' : 'check' + ); + if (isLegacy) { + await testSubjects.click('lns-palettePicker'); + await find.clickByCssSelector(`#${paletteId}`); + } else { + await testSubjects.click('kbnColoring_ColorMapping_PalettePicker'); + await testSubjects.click(`kbnColoring_ColorMapping_Palette-${paletteId}`); + } + await this.closePaletteEditor(); }, async closePaletteEditor() { @@ -1456,7 +1480,6 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont dimension: string; field: string; keepOpen?: boolean; - palette?: string; }) { await retry.try(async () => { if (!(await testSubjects.exists('lns-indexPattern-dimensionContainerClose'))) { @@ -1467,10 +1490,6 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await this.selectOptionFromComboBox('text-based-dimension-field', opts.field); - if (opts.palette) { - await this.setPalette(opts.palette); - } - if (!opts.keepOpen) { await this.closeDimensionEditor(); await testSubjects.click('applyFlyoutButton'); @@ -1857,5 +1876,42 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await testSubjects.click('lensSuggestionsPanelToggleButton'); } }, + + async changeColorMappingPalette(selector: string, paletteId: string) { + await retry.try(async () => { + if (!(await testSubjects.exists('lns-indexPattern-dimensionContainerClose'))) { + await testSubjects.click(selector); + } + await testSubjects.existOrFail('lns-indexPattern-dimensionContainerClose'); + }); + await this.setPalette(paletteId, false); + await this.closeDimensionEditor(); + }, + + async changeColorMappingCategoricalColors( + selector: string, + colorSwatchIndex: number, + paletteColorIndex: number + ) { + await retry.try(async () => { + if (!(await testSubjects.exists('lns-indexPattern-dimensionContainerClose'))) { + await testSubjects.click(selector); + } + await testSubjects.existOrFail('lns-indexPattern-dimensionContainerClose'); + }); + await testSubjects.click('lns_colorEditing_trigger'); + // disable autoAssign + await testSubjects.setEuiSwitch('lns-colorMapping-autoAssignSwitch', 'uncheck'); + + await testSubjects.click(`lns-colorMapping-colorSwatch-${colorSwatchIndex}`); + + await testSubjects.click(`lns-colorMapping-colorPicker-staticColor-${paletteColorIndex}`); + + await testSubjects.click(`lns-colorMapping-colorSwatch-${colorSwatchIndex}`); + + await this.closePaletteEditor(); + + await this.closeDimensionEditor(); + }, }); } diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 93d4866ef0933..bf7d721fc0ba2 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -141,6 +141,7 @@ "@kbn/aiops-utils", "@kbn/stack-alerts-plugin", "@kbn/apm-data-access-plugin", + "@kbn/coloring", "@kbn/profiling-utils", "@kbn/profiling-data-access-plugin", ]