From 8e1a2e974a94c3e5ae57f9e28e1610e6c13f3e43 Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Thu, 8 Feb 2024 14:24:10 +0100 Subject: [PATCH] [Lens] Color mapping UX refactoring (#175144) This commit revisits the UX for the Lens color mapping applying a slightly different UX behaviour to allow looping of colors from a chosen palette. --- .../__stories__/color_mapping.stories.tsx | 165 ++++----- .../categorical_color_mapping.test.tsx | 20 +- .../color/color_handling.test.ts | 179 +++++++-- .../color_mapping/color/color_handling.ts | 137 ++++--- .../color_mapping/color/rule_matching.ts | 2 +- .../components/assignment/assignment.tsx | 14 +- .../components/assignment/match.tsx | 7 +- .../components/assignment/range.tsx | 5 +- .../assignment/special_assignment.tsx | 69 ++-- .../components/color_picker/color_picker.tsx | 2 +- .../components/color_picker/color_swatch.tsx | 14 +- .../color_picker/palette_colors.tsx | 8 +- .../components/color_picker/rgb_picker.tsx | 5 +- .../components/container/assigments.tsx | 329 +++++++++++++++++ .../components/container/container.tsx | 282 +++++--------- .../container/unassigned_terms_config.tsx | 150 ++++++++ .../components/palette_selector/gradient.tsx | 345 +++++------------- .../palette_selector/gradient_add_stop.tsx | 110 ++++++ .../palette_selector/palette_selector.tsx | 206 ++--------- .../components/palette_selector/scale.tsx | 144 ++++++++ .../config/assignment_from_categories.ts | 65 ---- .../color_mapping/config/assignments.ts | 74 +--- .../config/default_color_mapping.ts | 55 +-- .../color_mapping/config/types.ts | 12 +- .../shared_components/color_mapping/index.ts | 1 + .../color_mapping/palettes/elastic_brand.ts | 6 +- .../color_mapping/palettes/eui_amsterdam.ts | 6 +- .../color_mapping/palettes/kibana_legacy.ts | 6 +- .../color_mapping/state/color_mapping.ts | 25 +- .../color_mapping/state/selectors.ts | 6 +- .../color_telemetry_helpers.test.ts | 60 +-- .../color_telemetry_helpers.ts | 33 +- .../translations/translations/fr-FR.json | 7 +- .../translations/translations/ja-JP.json | 7 +- .../translations/translations/zh-CN.json | 7 +- .../test/functional/page_objects/lens_page.ts | 5 +- 36 files changed, 1434 insertions(+), 1134 deletions(-) create mode 100644 packages/kbn-coloring/src/shared_components/color_mapping/components/container/assigments.tsx create mode 100644 packages/kbn-coloring/src/shared_components/color_mapping/components/container/unassigned_terms_config.tsx create mode 100644 packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/gradient_add_stop.tsx create mode 100644 packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/scale.tsx delete mode 100644 packages/kbn-coloring/src/shared_components/color_mapping/config/assignment_from_categories.ts 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 index 95f4ff5623ea3..f1d9add2c0f09 100644 --- 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 @@ -6,122 +6,115 @@ * Side Public License, v 1. */ -import React, { FC } from 'react'; -import { EuiFlyout, EuiForm } from '@elastic/eui'; +import React, { FC, useState } from 'react'; +import { EuiFlyout, EuiForm, EuiPage, isColorDark } from '@elastic/eui'; import { ComponentStory } from '@storybook/react'; +import { css } from '@emotion/react'; import { CategoricalColorMapping, ColorMappingProps } from '../categorical_color_mapping'; -import { AVAILABLE_PALETTES } from '../palettes'; +import { AVAILABLE_PALETTES, getPalette, NeutralPalette } from '../palettes'; import { DEFAULT_COLOR_MAPPING_CONFIG } from '../config/default_color_mapping'; +import { ColorMapping } from '../config'; +import { getColorFactory } from '../color/color_handling'; +import { ruleMatch } from '../color/rule_matching'; +import { getValidColor } from '../color/color_math'; export default { title: 'Color Mapping', component: CategoricalColorMapping, - decorators: [ - (story: Function) => ( - {}} hideCloseButton> - {story()} - - ), - ], + decorators: [(story: Function) => story()], }; -const Template: ComponentStory> = (args) => ( - -); +const Template: ComponentStory> = (args) => { + const [updatedModel, setUpdateModel] = useState( + DEFAULT_COLOR_MAPPING_CONFIG + ); + + const getPaletteFn = getPalette(AVAILABLE_PALETTES, NeutralPalette); + + const colorFactory = getColorFactory(updatedModel, getPaletteFn, false, args.data); + + return ( + +
    + {args.data.type === 'categories' && + args.data.categories.map((c, i) => { + const match = updatedModel.assignments.some(({ rule }) => { + return ruleMatch(rule, c); + }); + const color = colorFactory(c); + const isDark = isColorDark(...getValidColor(color).rgb()); + return ( +
  1. + {c} +
  2. + ); + })} +
+ {}} + hideCloseButton + ownFocus={false} + > + + + + +
+ ); +}; 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', + type: 'categorical', }, - 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, - }, + specialAssignments: [ { rule: { - type: 'matchExactly', - values: [ - 'this is', - 'a multi-line wrap', - 'combo box', - 'test combo', - '3 lines', - ['double', 'value'], - ], + type: 'other', }, color: { - type: 'gradient', + type: 'loop', }, touched: false, }, ], + assignments: [], }, isDarkMode: false, data: { type: 'categories', categories: [ - 'a', - 'b', - 'c', - 'd', - 'this is', - 'a multi-line wrap', - 'combo box', - 'test combo', - '3 lines', + 'US', + 'Mexico', + 'Brasil', + 'Canada', + 'Italy', + 'Germany', + 'France', + 'Spain', + 'UK', + 'Portugal', + 'Greece', + 'Sweden', + 'Finland', ], }, 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 index fe8374d7dcdcd..ccc955d2b8947 100644 --- 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 @@ -13,8 +13,9 @@ 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 ASSIGNMENTS_PROMPT = '[data-test-subj="lns-colorMapping-assignmentsPrompt"]'; +const ASSIGNMENTS_PROMPT_ADD_ALL = '[data-test-subj="lns-colorMapping-assignmentsPromptAddAll"]'; const ASSIGNMENT_ITEM = (i: number) => `[data-test-subj="lns-colorMapping-assignmentsItem${i}"]`; describe('color mapping', () => { @@ -35,19 +36,12 @@ describe('color mapping', () => { /> ); - 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); - }); + // empty list prompt visible + expect(component.find(ASSIGNMENTS_PROMPT)).toBeTruthy(); expect(onModelUpdateFn).not.toBeCalled(); }); - it('switch to manual assignments', () => { + it('Add all terms to assignments', () => { const dataInput: ColorMappingInputData = { type: 'categories', categories: ['categoryA', 'categoryB'], @@ -63,9 +57,8 @@ describe('color mapping', () => { specialTokens={new Map()} /> ); - component.find(AUTO_ASSIGN_SWITCH).hostNodes().simulate('click'); + component.find(ASSIGNMENTS_PROMPT_ADD_ALL).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 ); @@ -97,6 +90,7 @@ describe('color mapping', () => { } /> ); + component.find(ASSIGNMENTS_PROMPT_ADD_ALL).hostNodes().simulate('click'); expect(component.find(ASSIGNMENTS_LIST).hostNodes().children().length).toEqual( dataInput.categories.length ); 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 index 93896394daf41..f8631ed5768da 100644 --- 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 @@ -23,6 +23,7 @@ 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', @@ -31,18 +32,36 @@ describe('Color mapping - color generation', () => { 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]); + // if the category is not available in the `categories` list then a default netural is used + // this is an edge case and ideally never happen + expect(colorFactory('not_available_1')).toBe( + NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX] + ); }); - it('returns max number of colors defined in palette, use other color otherwise', () => { + // 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 netural is used + // this is an edge case and ideally never happen + expect(colorFactory('not_available')).toBe(NEUTRAL_COLOR_DARK[DEFAULT_NEUTRAL_PALETTE_INDEX]); + }); + + it('by default loops colors defined in palette', () => { const twoColorPalette: ColorMapping.CategoricalPalette = { id: 'twoColors', name: 'twoColors', colorCount: 2, type: 'categorical', - getColor(valueInRange, isDarkMode) { - return ['red', 'blue'][valueInRange]; + getColor(indexInRange, isDarkMode, loop) { + return ['red', 'blue'][loop ? indexInRange % 2 : indexInRange]; }, }; @@ -53,6 +72,17 @@ describe('Color mapping - color generation', () => { const colorFactory = getColorFactory( { ...DEFAULT_COLOR_MAPPING_CONFIG, + specialAssignments: [ + { + color: { + type: 'loop', + }, + rule: { + type: 'other', + }, + touched: false, + }, + ], paletteId: twoColorPalette.id, }, simplifiedGetPaletteGn, @@ -64,23 +94,58 @@ describe('Color mapping - color generation', () => { ); 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]); + // the palette will loop depending on the number of colors available + expect(colorFactory('cat3')).toBe('#ff0000'); + expect(colorFactory('cat4')).toBe('#0000ff'); }); - // 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('returns the unassigned color if configured statically', () => { + const twoColorPalette: ColorMapping.CategoricalPalette = { + id: 'twoColors', + name: 'twoColors', + colorCount: 2, + type: 'categorical', + getColor(indexInRange, darkMode, loop) { + return ['red', 'blue'][loop ? indexInRange % 2 : indexInRange]; + }, + }; + + const simplifiedGetPaletteGn = getPalette( + new Map([[twoColorPalette.id, twoColorPalette]]), + NeutralPalette + ); + const colorFactory = getColorFactory( + { + ...DEFAULT_COLOR_MAPPING_CONFIG, + specialAssignments: [ + { + color: { + type: 'categorical', + paletteId: NeutralPalette.id, + colorIndex: DEFAULT_NEUTRAL_PALETTE_INDEX, + }, + rule: { + type: 'other', + }, + touched: false, + }, + ], + paletteId: twoColorPalette.id, + }, + simplifiedGetPaletteGn, + false, + { + type: 'categories', + categories: ['cat1', 'cat2', 'cat3', 'cat4'], + } + ); + expect(colorFactory('cat1')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]); + expect(colorFactory('cat2')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]); + expect(colorFactory('cat3')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]); + expect(colorFactory('cat4')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]); + // if the category is not available in the `categories` list then a default netural is used + // this is an edge case and ideally never happen + expect(colorFactory('not_available')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]); }); it('handles special tokens, multi-field categories and non-trimmed whitespaces', () => { @@ -89,19 +154,19 @@ describe('Color mapping - color generation', () => { 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(['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', () => { + it('ignores configured assignments in loop mode', () => { const colorFactory = getColorFactory( { ...DEFAULT_COLOR_MAPPING_CONFIG, assignments: [ { color: { type: 'colorCode', colorCode: 'red' }, - rule: { type: 'matchExactly', values: ['assignmentToIgnore'] }, + rule: { type: 'matchExactly', values: ['configuredAssignment'] }, touched: false, }, ], @@ -110,19 +175,19 @@ describe('Color mapping - color generation', () => { false, { type: 'categories', - categories: ['catA', 'catB', 'assignmentToIgnore'], + categories: ['catA', 'catB', 'configuredAssignment', 'nextCat'], } ); 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]); + expect(colorFactory('configuredAssignment')).toBe('red'); + expect(colorFactory('nextCat')).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' }, @@ -154,7 +219,8 @@ describe('Color mapping - color generation', () => { 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 + // if the category is not available in the `categories` list then a default netural is used + // this is an edge case and ideally never happen expect(colorFactory('not_available')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]); }); @@ -188,7 +254,7 @@ describe('Color mapping - color generation', () => { expect(toHex(colorFactory('cat3'))).toBe('#cce8e0'); }); - it('returns sequential gradient colors from lighter to darker [asc, lightMode]', () => { + it('sequential gradient colors from lighter to darker [asc, lightMode]', () => { const colorFactory = getColorFactory( { ...DEFAULT_COLOR_MAPPING_CONFIG, @@ -212,10 +278,59 @@ describe('Color mapping - color generation', () => { categories: ['cat1', 'cat2', 'cat3'], } ); + // light green expect(toHex(colorFactory('cat1'))).toBe('#cce8e0'); + // mid green point expect(toHex(colorFactory('cat2'))).toBe('#93cebc'); + // initial gradient color + expect(toHex(colorFactory('cat3'))).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]); + }); + it('sequential gradients and static color from lighter to darker [asc, lightMode]', () => { + const colorFactory = getColorFactory( + { + ...DEFAULT_COLOR_MAPPING_CONFIG, + assignments: [ + { color: { type: 'gradient' }, rule: { type: 'auto' }, touched: false }, + { color: { type: 'gradient' }, rule: { type: 'auto' }, touched: false }, + ], + + colorMode: { + type: 'gradient', + steps: [ + { + type: 'categorical', + paletteId: EUIAmsterdamColorBlindPalette.id, + colorIndex: 0, + touched: false, + }, + ], + sort: 'asc', + }, + specialAssignments: [ + { + color: { + type: 'categorical', + colorIndex: DEFAULT_NEUTRAL_PALETTE_INDEX, + paletteId: NeutralPalette.id, + }, + rule: { + type: 'other', + }, + touched: false, + }, + ], + }, + getPaletteFn, + false, + { + type: 'categories', + categories: ['cat1', 'cat2', 'cat3'], + } + ); + expect(toHex(colorFactory('cat1'))).toBe('#cce8e0'); + expect(toHex(colorFactory('cat2'))).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]); // this matches exactly with the initial step selected - expect(toHex(colorFactory('cat3'))).toBe(toHex(EUI_AMSTERDAM_PALETTE_COLORS[0])); + expect(toHex(colorFactory('cat3'))).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]); }); it('returns 2 colors gradient [desc, lightMode]', () => { @@ -287,8 +402,8 @@ describe('Color mapping - color generation', () => { 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 + // if the category is not available in the `categories` list then a default netural is used + // this is an edge case and ideally never happen + expect(colorFactory('not_available')).toBe(NEUTRAL_COLOR_DARK[DEFAULT_NEUTRAL_PALETTE_INDEX]); }); }); 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 index 795f94b740e9b..8867b07572308 100644 --- 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 @@ -6,25 +6,32 @@ * Side Public License, v 1. */ import chroma from 'chroma-js'; +import { findLast } from 'lodash'; import { ColorMapping } from '../config'; import { changeAlpha, combineColors, getValidColor } from './color_math'; -import { generateAutoAssignmentsForCategories } from '../config/assignment_from_categories'; -import { getPalette } from '../palettes'; +import { getPalette, NeutralPalette } from '../palettes'; import { ColorMappingInputData } from '../categorical_color_mapping'; import { ruleMatch } from './rule_matching'; import { GradientColorMode } from '../config/types'; +import { + DEFAULT_NEUTRAL_PALETTE_INDEX, + DEFAULT_OTHER_ASSIGNMENT_INDEX, +} from '../config/default_color_mapping'; export function getAssignmentColor( colorMode: ColorMapping.Config['colorMode'], - color: ColorMapping.Config['assignments'][number]['color'], + color: + | ColorMapping.Config['assignments'][number]['color'] + | (ColorMapping.LoopColor & { paletteId: string; colorIndex: number }), getPaletteFn: ReturnType, isDarkMode: boolean, index: number, total: number -) { +): string { switch (color.type) { case 'colorCode': case 'categorical': + case 'loop': return getColor(color, getPaletteFn, isDarkMode); case 'gradient': { if (colorMode.type === 'categorical') { @@ -37,31 +44,28 @@ export function getAssignmentColor( } export function getColor( - color: ColorMapping.ColorCode | ColorMapping.CategoricalColor, + color: + | ColorMapping.ColorCode + | ColorMapping.CategoricalColor + | (ColorMapping.LoopColor & { paletteId: string; colorIndex: number }), getPaletteFn: ReturnType, isDarkMode: boolean -) { +): string { return color.type === 'colorCode' ? color.colorCode - : getValidColor(getPaletteFn(color.paletteId).getColor(color.colorIndex, isDarkMode)).hex(); + : getValidColor( + getPaletteFn(color.paletteId).getColor(color.colorIndex, isDarkMode, true) + ).hex(); } export function getColorFactory( - model: ColorMapping.Config, + { assignments, specialAssignments, colorMode, paletteId }: 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 = + const autoByOrderAssignments = data.type === 'categories' ? assignments.filter((a) => { return ( @@ -71,75 +75,90 @@ export function getColorFactory( : []; // find all categories that doesn't match with an assignment - const nonAssignedCategories = + const notAssignedCategories = data.type === 'categories' ? data.categories.filter((category) => { return !assignments.some(({ rule }) => ruleMatch(rule, category)); }) : []; + const lastCategorical = findLast(assignments, (d) => { + return d.color.type === 'categorical'; + }); + const nextCategoricalIndex = + lastCategorical?.color.type === 'categorical' ? lastCategorical.color.colorIndex + 1 : 0; + return (category: string | string[]) => { if (typeof category === 'string' || Array.isArray(category)) { - const nonAssignedCategoryIndex = nonAssignedCategories.indexOf(category); + const nonAssignedCategoryIndex = notAssignedCategories.indexOf(category); - // return color for a non assigned category + // this category is not assigned to a specific color if (nonAssignedCategoryIndex > -1) { - if (nonAssignedCategoryIndex < autoAssignedColors.length) { + // if the category order is within current number of auto-assigned items pick the defined color + if (nonAssignedCategoryIndex < autoByOrderAssignments.length) { const autoAssignmentIndex = assignments.findIndex( - (d) => d === autoAssignedColors[nonAssignedCategoryIndex] + (d) => d === autoByOrderAssignments[nonAssignedCategoryIndex] ); return getAssignmentColor( - model.colorMode, - autoAssignedColors[nonAssignedCategoryIndex].color, + colorMode, + autoByOrderAssignments[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); - } + const totalColorsIfGradient = assignments.length || notAssignedCategories.length; + const indexIfGradient = + (nonAssignedCategoryIndex - autoByOrderAssignments.length) % totalColorsIfGradient; - // 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]; + // if no auto-assign color rule/color is available then use the color looping palette return getAssignmentColor( - model.colorMode, - assignment.color, + colorMode, + // TODO: the specialAssignment[0] position is arbitrary, we should fix it better + specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX].color.type === 'loop' + ? colorMode.type === 'gradient' + ? { type: 'gradient' } + : { + type: 'loop', + // those are applied here and depends on the current non-assigned category - auto-assignment list + colorIndex: + nonAssignedCategoryIndex - autoByOrderAssignments.length + nextCategoricalIndex, + paletteId, + } + : specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX].color, getPaletteFn, isDarkMode, - matchingAssignmentIndex, - assignments.length + indexIfGradient, + totalColorsIfGradient ); } - // 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); - }); + } + // find the assignment where the category matches the rule + 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); + if (matchingAssignmentIndex > -1) { + const assignment = assignments[matchingAssignmentIndex]; + return getAssignmentColor( + colorMode, + assignment.color, + getPaletteFn, + isDarkMode, + matchingAssignmentIndex, + assignments.length + ); } + return getColor( + { + type: 'categorical', + paletteId: NeutralPalette.id, + colorIndex: DEFAULT_NEUTRAL_PALETTE_INDEX, + }, + getPaletteFn, + isDarkMode + ); }; } 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 index 0d844ca26e27e..a157c7927747c 100644 --- 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 @@ -23,7 +23,7 @@ export function ruleMatch( } return rule.values.includes(`${value}`); case 'matchExactlyCI': - return rule.values.some((d) => d.toLowerCase() === `${value}`); + return rule.values.some((d) => d.toLowerCase() === `${value}`.toLowerCase()); case 'range': // TODO: color by value not yet possible in all charts in elastic-charts return typeof value === 'number' ? rangeMatch(rule, value) : false; 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 index 896f2ea392884..89c4375d4bc10 100644 --- 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 @@ -31,8 +31,6 @@ export function Assignment({ disableDelete, index, total, - canPickColor, - editable, palette, colorMode, getPaletteFn, @@ -48,8 +46,6 @@ export function Assignment({ disableDelete: boolean; palette: ColorMapping.CategoricalPalette; getPaletteFn: ReturnType; - canPickColor: boolean; - editable: boolean; isDarkMode: boolean; specialTokens: Map; assignmentValuesCounter: Map; @@ -57,18 +53,12 @@ export function Assignment({ const dispatch = useDispatch(); return ( - + { const rule: ColorMapping.RuleRange = { type: 'range', 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 index 1f57e731e84c0..bfef7a270e1a0 100644 --- 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 @@ -15,7 +15,6 @@ import { ColorMapping } from '../../config'; export const Match: React.FC<{ index: number; - editable: boolean; rule: | ColorMapping.RuleAuto | ColorMapping.RuleMatchExactly @@ -25,7 +24,7 @@ export const Match: React.FC<{ options: Array; specialTokens: Map; assignmentValuesCounter: Map; -}> = ({ index, rule, updateValue, editable, options, specialTokens, assignmentValuesCounter }) => { +}> = ({ index, rule, updateValue, options, specialTokens, assignmentValuesCounter }) => { const duplicateWarning = i18n.translate( 'coloring.colorMapping.assignments.duplicateCategoryWarning', { @@ -75,7 +74,6 @@ export const Match: React.FC<{ { - if (selectedOptions.findIndex((option) => option.label.toLowerCase() === label) === -1) { + if (selectedOptions.findIndex((option) => option.label === label) === -1) { updateValue([...selectedOptions, { label, value: label }].map((d) => d.value)); } }} + isCaseSensitive 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 index 70f2cf49609e0..e006a7b23543f 100644 --- 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 @@ -12,9 +12,8 @@ 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 }) => { +}> = ({ rule, updateValue }) => { const minValid = rule.min <= rule.max; const maxValid = rule.max >= rule.min; @@ -34,7 +33,6 @@ export const Range: React.FC<{ placeholder="min" value={rule.min} isInvalid={!minValid} - disabled={!editable} onChange={(e) => updateValue(+e.currentTarget.value, rule.max, rule.minInclusive, rule.maxInclusive) } @@ -54,7 +52,6 @@ export const Range: React.FC<{ } placeholder="max" - disabled={!editable} value={rule.max} onChange={(e) => updateValue(rule.min, +e.currentTarget.value, rule.minInclusive, rule.maxInclusive) 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 index 29ede59e37f41..fff892fc9cc7b 100644 --- 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 @@ -6,17 +6,16 @@ * 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'; +import { ColorCode, CategoricalColor } from '../../config/types'; export function SpecialAssignment({ - assignment, + assignmentColor, index, palette, getPaletteFn, @@ -25,55 +24,31 @@ export function SpecialAssignment({ }: { isDarkMode: boolean; index: number; - assignment: ColorMapping.Config['specialAssignments'][number]; + assignmentColor: CategoricalColor | ColorCode; palette: ColorMapping.CategoricalPalette; getPaletteFn: ReturnType; total: number; }) { const dispatch = useDispatch(); - const canPickColor = true; return ( - - - { - dispatch( - updateSpecialAssignmentColor({ - assignmentIndex: index, - color, - }) - ); - }} - /> - - - - - + { + 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 index e1e8a08aa6b22..f576daa2096cc 100644 --- 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 @@ -107,7 +107,7 @@ export function ColorPicker({ style={{ paddingBottom: 8 }} > {i18n.translate('coloring.colorMapping.colorPicker.removeGradientColorButtonLabel', { - defaultMessage: 'Remove color step', + defaultMessage: 'Remove color stop', })} 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 index 8ddc56d2476c7..34ffbefeca30f 100644 --- 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 @@ -29,11 +29,8 @@ import { getValidColor } from '../../color/color_math'; interface ColorPickerSwatchProps { colorMode: ColorMapping.Config['colorMode']; - assignmentColor: - | ColorMapping.Config['assignments'][number]['color'] - | ColorMapping.Config['specialAssignments'][number]['color']; + assignmentColor: ColorMapping.Config['assignments'][number]['color']; getPaletteFn: ReturnType; - canPickColor: boolean; index: number; total: number; palette: ColorMapping.CategoricalPalette; @@ -46,7 +43,6 @@ export const ColorSwatch = ({ colorMode, assignmentColor, getPaletteFn, - canPickColor, index, total, palette, @@ -71,7 +67,7 @@ export const ColorSwatch = ({ ); const colorIsDark = isColorDark(...getValidColor(colorHex).rgb()); const euiTheme = useEuiTheme(); - return canPickColor && assignmentColor.type !== 'gradient' ? ( + return assignmentColor.type !== 'gradient' ? ( ) : ( @@ -121,7 +117,7 @@ export const ColorSwatch = ({ style={{ // the color swatch can't pickup colors written in rgb/css standard backgroundColor: colorHex, - cursor: canPickColor ? 'pointer' : 'not-allowed', + cursor: 'pointer', width: 32, height: 32, }} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/palette_colors.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/palette_colors.tsx index 21aa18a49f9dc..77a8273654b89 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/palette_colors.tsx +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/palette_colors.tsx @@ -35,16 +35,16 @@ export function PaletteColors({ selectColor: (color: ColorMapping.CategoricalColor | ColorMapping.ColorCode) => void; }) { const colors = Array.from({ length: palette.colorCount }, (d, i) => { - return palette.getColor(i, isDarkMode); + return palette.getColor(i, isDarkMode, false); }); const neutralColors = Array.from({ length: NeutralPalette.colorCount }, (d, i) => { - return NeutralPalette.getColor(i, isDarkMode); + return NeutralPalette.getColor(i, isDarkMode, false); }); const originalColor = color.type === 'categorical' ? color.paletteId === NeutralPalette.id - ? NeutralPalette.getColor(color.colorIndex, isDarkMode) - : getPaletteFn(color.paletteId).getColor(color.colorIndex, isDarkMode) + ? NeutralPalette.getColor(color.colorIndex, isDarkMode, false) + : getPaletteFn(color.paletteId).getColor(color.colorIndex, isDarkMode, false) : color.colorCode; return ( <> diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/rgb_picker.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/rgb_picker.tsx index 84f6786922f44..28f155b85fe9b 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/rgb_picker.tsx +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/color_picker/rgb_picker.tsx @@ -48,7 +48,8 @@ export function RGBPicker({ customColorMappingColor.type === 'categorical' ? getPaletteFn(customColorMappingColor.paletteId).getColor( customColorMappingColor.colorIndex, - isDarkMode + isDarkMode, + false ) : customColorMappingColor.colorCode; @@ -142,7 +143,7 @@ export function RGBPicker({ if (chromajs.valid(textColor)) { setCustomColorMappingColor({ type: 'colorCode', - colorCode: chromajs(textColor).hex(), + colorCode: textColor, }); } }} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/container/assigments.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/container/assigments.tsx new file mode 100644 index 0000000000000..f525311b8afb3 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/container/assigments.tsx @@ -0,0 +1,329 @@ +/* + * 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 { + EuiButton, + EuiButtonEmpty, + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiIcon, + EuiNotificationBadge, + EuiPanel, + EuiPopover, + EuiText, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import React, { useCallback, useMemo, useState } from 'react'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { i18n } from '@kbn/i18n'; +import { useDispatch, useSelector } from 'react-redux'; +import { findLast } from 'lodash'; +import { Assignment } from '../assignment/assignment'; +import { + addNewAssignment, + addNewAssignments, + removeAllAssignments, +} from '../../state/color_mapping'; +import { selectColorMode, selectComputedAssignments, selectPalette } from '../../state/selectors'; +import { ColorMappingInputData } from '../../categorical_color_mapping'; +import { ColorMapping } from '../../config'; +import { getPalette, NeutralPalette } from '../../palettes'; +import { ruleMatch } from '../../color/rule_matching'; + +export function AssignmentsConfig({ + data, + palettes, + isDarkMode, + specialTokens, +}: { + palettes: Map; + data: ColorMappingInputData; + isDarkMode: boolean; + /** map between original and formatted tokens used to handle special cases, like the Other bucket and the empty bucket */ + specialTokens: Map; +}) { + const [showOtherActions, setShowOtherActions] = useState(false); + + const dispatch = useDispatch(); + const getPaletteFn = getPalette(palettes, NeutralPalette); + const palette = useSelector(selectPalette(getPaletteFn)); + const colorMode = useSelector(selectColorMode); + const assignments = useSelector(selectComputedAssignments); + + const unmatchingCategories = useMemo(() => { + return data.type === 'categories' + ? data.categories.filter((category) => { + return !assignments.some(({ rule }) => ruleMatch(rule, category)); + }) + : []; + }, [data, assignments]); + + const assignmentValuesCounter = assignments.reduce>( + (acc, assignment) => { + const values = assignment.rule.type === 'matchExactly' ? assignment.rule.values : []; + values.forEach((value) => { + acc.set(value, (acc.get(value) ?? 0) + 1); + }); + return acc; + }, + new Map() + ); + + const onClickAddNewAssignment = useCallback(() => { + const lastCategorical = findLast(assignments, (d) => { + return d.color.type === 'categorical'; + }); + const nextCategoricalIndex = + lastCategorical?.color.type === 'categorical' ? lastCategorical.color.colorIndex + 1 : 0; + dispatch( + addNewAssignment({ + rule: + data.type === 'categories' + ? { + type: 'matchExactly', + values: [], + } + : { type: 'range', min: 0, max: 0, minInclusive: true, maxInclusive: true }, + color: + colorMode.type === 'categorical' + ? { + type: 'categorical', + paletteId: palette.id, + colorIndex: nextCategoricalIndex % palette.colorCount, + } + : { type: 'gradient' }, + touched: false, + }) + ); + }, [assignments, colorMode.type, data.type, dispatch, palette.colorCount, palette.id]); + + const onClickAddAllCurrentCategories = useCallback(() => { + if (data.type === 'categories') { + const lastCategorical = findLast(assignments, (d) => { + return d.color.type === 'categorical'; + }); + const nextCategoricalIndex = + lastCategorical?.color.type === 'categorical' ? lastCategorical.color.colorIndex + 1 : 0; + + const newAssignments: ColorMapping.Config['assignments'] = unmatchingCategories.map( + (c, i) => { + return { + rule: { + type: 'matchExactly', + values: [c], + }, + color: + colorMode.type === 'categorical' + ? { + type: 'categorical', + paletteId: palette.id, + colorIndex: (nextCategoricalIndex + i) % palette.colorCount, + } + : { type: 'gradient' }, + touched: false, + }; + } + ); + dispatch(addNewAssignments(newAssignments)); + } + }, [ + dispatch, + assignments, + colorMode.type, + data.type, + palette.colorCount, + palette.id, + unmatchingCategories, + ]); + + return ( + +
+ + {assignments.map((assignment, i) => { + return ( + + ); + })} + {assignments.length === 0 && ( + +

+ {i18n.translate( + 'coloring.colorMapping.container.mapValuesPromptDescription.mapValuesPromptDetail', + { + defaultMessage: + 'Add new assignments to begin associating terms in your data with specified colors.', + } + )} +

+ + } + actions={[ + + {i18n.translate('coloring.colorMapping.container.AddAssignmentButtonLabel', { + defaultMessage: 'Add assignment', + })} + , + + {i18n.translate('coloring.colorMapping.container.mapValueButtonLabel', { + defaultMessage: 'Add all unassigned terms', + })} + , + ]} + /> + )} +
+
+ {assignments.length > 0 && } +
+ {assignments.length > 0 && ( + + + {i18n.translate('coloring.colorMapping.container.AddAssignmentButtonLabel', { + defaultMessage: 'Add assignment', + })} + + {data.type === 'categories' && ( + setShowOtherActions(true)} + /> + } + isOpen={showOtherActions} + closePopover={() => setShowOtherActions(false)} + panelPaddingSize="xs" + anchorPosition="downRight" + ownFocus + > + { + setShowOtherActions(false); + requestAnimationFrame(() => { + onClickAddAllCurrentCategories(); + }); + }} + disabled={unmatchingCategories.length === 0} + > + + + {i18n.translate( + 'coloring.colorMapping.container.mapCurrentValuesButtonLabel', + { + defaultMessage: 'Add all unsassigned terms', + } + )} + + {unmatchingCategories.length > 0 && ( + + + {unmatchingCategories.length} + + + )} + + , + } + onClick={() => { + setShowOtherActions(false); + dispatch(removeAllAssignments()); + }} + color="danger" + > + {i18n.translate( + 'coloring.colorMapping.container.clearAllAssignmentsButtonLabel', + { + defaultMessage: 'Clear all assignments', + } + )} + , + ]} + /> + + )} + + )} +
+
+ ); +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/container/container.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/container/container.tsx index 748f17fa45842..e3de3c0a24261 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/components/container/container.tsx +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/container/container.tsx @@ -8,56 +8,28 @@ import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiFormLabel, - EuiHorizontalRule, - EuiPanel, - EuiSwitch, - EuiText, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { css } from '@emotion/react'; -import { Assignment } from '../assignment/assignment'; -import { SpecialAssignment } from '../assignment/special_assignment'; import { PaletteSelector } from '../palette_selector/palette_selector'; -import { - RootState, - addNewAssignment, - assignAutomatically, - assignStatically, - changeGradientSortOrder, -} from '../../state/color_mapping'; -import { generateAutoAssignmentsForCategories } from '../../config/assignment_from_categories'; +import { changeGradientSortOrder } from '../../state/color_mapping'; import { ColorMapping } from '../../config'; import { getPalette } from '../../palettes'; -import { getUnusedColorForNewAssignment } from '../../config/assignments'; -import { - selectColorMode, - selectPalette, - selectSpecialAssignments, - selectIsAutoAssignmentMode, -} from '../../state/selectors'; +import { selectColorMode, selectComputedAssignments, selectPalette } from '../../state/selectors'; import { ColorMappingInputData } from '../../categorical_color_mapping'; import { Gradient } from '../palette_selector/gradient'; import { NeutralPalette } from '../../palettes/neutral'; +import { ScaleMode } from '../palette_selector/scale'; +import { UnassignedTermsConfig } from './unassigned_terms_config'; +import { AssignmentsConfig } from './assigments'; -export const MAX_ASSIGNABLE_COLORS = 10; - -function selectComputedAssignments( - data: ColorMappingInputData, - palette: ColorMapping.CategoricalPalette, - colorMode: ColorMapping.Config['colorMode'] -) { - return (state: RootState) => - state.colorMapping.assignmentMode === 'auto' - ? generateAutoAssignmentsForCategories(data, palette, colorMode) - : state.colorMapping.assignments; -} -export function Container(props: { +export function Container({ + data, + palettes, + isDarkMode, + specialTokens, +}: { palettes: Map; data: ColorMappingInputData; isDarkMode: boolean; @@ -66,187 +38,99 @@ export function Container(props: { }) { const dispatch = useDispatch(); - const getPaletteFn = getPalette(props.palettes, NeutralPalette); + const getPaletteFn = getPalette(palettes, NeutralPalette); const palette = useSelector(selectPalette(getPaletteFn)); const colorMode = useSelector(selectColorMode); - const autoAssignmentMode = useSelector(selectIsAutoAssignmentMode); - const assignments = useSelector(selectComputedAssignments(props.data, palette, colorMode)); - const specialAssignments = useSelector(selectSpecialAssignments); - - const canAddNewAssignment = !autoAssignmentMode && assignments.length < MAX_ASSIGNABLE_COLORS; - - const assignmentValuesCounter = assignments.reduce>( - (acc, assignment) => { - const values = assignment.rule.type === 'matchExactly' ? assignment.rule.values : []; - values.forEach((value) => { - acc.set(value, (acc.get(value) ?? 0) + 1); - }); - return acc; - }, - new Map() - ); + const assignments = useSelector(selectComputedAssignments); return ( - - - - + - + - - {i18n.translate('coloring.colorMapping.container.mappingAssignmentHeader', { - defaultMessage: 'Mapping assignments', - })} - + - - {i18n.translate('coloring.colorMapping.container.autoAssignLabel', { - defaultMessage: 'Auto assign', - })} - - } - checked={autoAssignmentMode} - compressed - onChange={() => { - if (autoAssignmentMode) { - dispatch(assignStatically(assignments)); - } else { - dispatch(assignAutomatically()); - } - }} - /> + - - + {colorMode.type === 'gradient' && ( +
- {colorMode.type !== 'gradient' ? null : ( - - )} - {assignments.map((assignment, i) => { - return ( -
- -
- ); - })} -
- - - - - {props.data.type === 'categories' && - specialAssignments.map((assignment, i) => { - return ( - - ); - })} - - -
- - - { - dispatch( - addNewAssignment({ - rule: - props.data.type === 'categories' - ? { - type: 'matchExactly', - values: [], - } - : { type: 'range', min: 0, max: 0, minInclusive: true, maxInclusive: true }, - color: getUnusedColorForNewAssignment(palette, colorMode, assignments), - touched: false, - }) - ); - }} - disabled={!canAddNewAssignment} - css={css` - margin-right: 8px; - `} - > - {i18n.translate('coloring.colorMapping.container.addAssignmentButtonLabel', { - defaultMessage: 'Add assignment', + - {colorMode.type === 'gradient' && ( - + { dispatch(changeGradientSortOrder(colorMode.sort === 'asc' ? 'desc' : 'asc')); }} - > - {i18n.translate('coloring.colorMapping.container.invertGradientButtonLabel', { - defaultMessage: 'Invert gradient', - })} - - )} - - + /> + + + + + +
+ )} + + + + + + {assignments.length > 0 && ( + + + + )}
); } diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/container/unassigned_terms_config.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/container/unassigned_terms_config.tsx new file mode 100644 index 0000000000000..8e90bcc38d119 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/container/unassigned_terms_config.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 { + EuiButtonGroup, + EuiButtonGroupOptionProps, + EuiColorPickerSwatch, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useDispatch, useSelector } from 'react-redux'; +import { css } from '@emotion/react'; +import { updateSpecialAssignmentColor } from '../../state/color_mapping'; +import { getPalette, NeutralPalette } from '../../palettes'; +import { + DEFAULT_NEUTRAL_PALETTE_INDEX, + DEFAULT_OTHER_ASSIGNMENT_INDEX, +} from '../../config/default_color_mapping'; +import { SpecialAssignment } from '../assignment/special_assignment'; +import { ColorMapping } from '../../config'; +import { selectColorMode, selectPalette, selectSpecialAssignments } from '../../state/selectors'; +import { ColorMappingInputData } from '../../categorical_color_mapping'; + +export function UnassignedTermsConfig({ + palettes, + data, + isDarkMode, +}: { + palettes: Map; + data: ColorMappingInputData; + isDarkMode: boolean; +}) { + const dispatch = useDispatch(); + + const getPaletteFn = getPalette(palettes, NeutralPalette); + + const palette = useSelector(selectPalette(getPaletteFn)); + const colorMode = useSelector(selectColorMode); + const specialAssignments = useSelector(selectSpecialAssignments); + const otherAssignment = specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX]; + + const colorModes: EuiButtonGroupOptionProps[] = [ + { + id: 'loop', + label: + colorMode.type === 'gradient' + ? i18n.translate( + 'coloring.colorMapping.container.unassignedTermsMode.ReuseGradientLabel', + { + defaultMessage: 'Gradient', + } + ) + : i18n.translate('coloring.colorMapping.container.unassignedTermsMode.ReuseColorsLabel', { + defaultMessage: 'Color palette', + }), + }, + { + id: 'static', + label: i18n.translate( + 'coloring.colorMapping.container.unassignedTermsMode.SingleColorLabel', + { + defaultMessage: 'Single color', + } + ), + }, + ]; + + return ( + + + + { + dispatch( + updateSpecialAssignmentColor({ + assignmentIndex: 0, + color: + optionId === 'loop' + ? { + type: 'loop', + } + : { + type: 'categorical', + colorIndex: DEFAULT_NEUTRAL_PALETTE_INDEX, + paletteId: NeutralPalette.id, + }, + }) + ); + }} + buttonSize="compressed" + isFullWidth + /> + + + + {data.type === 'categories' && otherAssignment.color.type !== 'loop' ? ( + + ) : ( + + )} + + + + ); +} diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/gradient.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/gradient.tsx index c4e22f797deaa..99eda60166ac4 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/gradient.tsx +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/gradient.tsx @@ -6,331 +6,174 @@ * Side Public License, v 1. */ -import { euiFocusRing, EuiIcon, euiShadowSmall, useEuiTheme } from '@elastic/eui'; import React from 'react'; -import { useDispatch } from 'react-redux'; - import { euiThemeVars } from '@kbn/ui-theme'; import { css } from '@emotion/react'; +import { useDispatch } from 'react-redux'; import { changeAlpha } from '../../color/color_math'; - import { ColorMapping } from '../../config'; -import { ColorSwatch } from '../color_picker/color_swatch'; import { getPalette } from '../../palettes'; - -import { addGradientColorStep, updateGradientColorStep } from '../../state/color_mapping'; -import { colorPickerVisibility } from '../../state/ui'; import { getGradientColorScale } from '../../color/color_handling'; +import { AddStop } from './gradient_add_stop'; +import { ColorSwatch } from '../color_picker/color_swatch'; +import { updateGradientColorStep } from '../../state/color_mapping'; export function Gradient({ paletteId, colorMode, getPaletteFn, isDarkMode, - assignmentsSize, }: { paletteId: string; isDarkMode: boolean; colorMode: ColorMapping.Config['colorMode']; getPaletteFn: ReturnType; - assignmentsSize: number; }) { + const dispatch = useDispatch(); if (colorMode.type === 'categorical') { return null; } + const currentPalette = getPaletteFn(paletteId); const gradientColorScale = getGradientColorScale(colorMode, getPaletteFn, isDarkMode); - const topMostColorStop = + const startStepColor = colorMode.sort === 'asc' ? colorMode.steps.length === 1 ? undefined : colorMode.steps.at(-1) : colorMode.steps.at(0); - const topMostColorStopIndex = + const startStepIndex = colorMode.sort === 'asc' ? colorMode.steps.length === 1 ? NaN : colorMode.steps.length - 1 : 0; - const bottomMostColorStop = + const endStepColor = colorMode.sort === 'asc' ? colorMode.steps.at(0) : colorMode.steps.length === 1 ? undefined : colorMode.steps.at(-1); - const bottomMostColorStopIndex = + const endStepIndex = colorMode.sort === 'asc' ? 0 : colorMode.steps.length === 1 ? NaN : colorMode.steps.length - 1; - const middleMostColorSep = colorMode.steps.length === 3 ? colorMode.steps[1] : undefined; - const middleMostColorStopIndex = colorMode.steps.length === 3 ? 1 : NaN; + const middleStepColor = colorMode.steps.length === 3 ? colorMode.steps[1] : undefined; + const middleStepIndex = colorMode.steps.length === 3 ? 1 : NaN; return ( - <> - {assignmentsSize > 1 && ( -
- )} +
+ +
- {topMostColorStop ? ( - { + dispatch(updateGradientColorStep({ index: startStepIndex, color })); + }} /> ) : ( )}
- {assignmentsSize > 1 && ( -
-
- {middleMostColorSep ? ( - - ) : colorMode.steps.length === 2 ? ( - - ) : undefined} -
-
- )} - {assignmentsSize > 1 && ( -
- )} -
- {bottomMostColorStop ? ( - { + dispatch(updateGradientColorStep({ index: middleStepIndex, color })); + }} /> - ) : ( + ) : colorMode.steps.length === 2 ? ( - )} + ) : undefined}
- - ); -} - -function AddStop({ - colorMode, - currentPalette, - at, -}: { - colorMode: { - type: 'gradient'; - steps: Array<(ColorMapping.CategoricalColor | ColorMapping.ColorCode) & { touched: boolean }>; - }; - currentPalette: ColorMapping.CategoricalPalette; - at: number; -}) { - const euiTheme = useEuiTheme(); - const dispatch = useDispatch(); - return ( - - ); -} - -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/gradient_add_stop.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/gradient_add_stop.tsx new file mode 100644 index 0000000000000..713e780bc32da --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/gradient_add_stop.tsx @@ -0,0 +1,110 @@ +/* + * 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 { + euiCanAnimate, + euiFocusRing, + EuiIcon, + euiShadowSmall, + EuiToolTip, + useEuiTheme, +} from '@elastic/eui'; +import { useDispatch } from 'react-redux'; +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; +import { ColorMapping } from '../../config'; +import { addGradientColorStep } from '../../state/color_mapping'; +import { colorPickerVisibility } from '../../state/ui'; + +export function AddStop({ + colorMode, + currentPalette, + at, +}: { + colorMode: ColorMapping.GradientColorMode; + currentPalette: ColorMapping.CategoricalPalette; + at: number; +}) { + const euiTheme = useEuiTheme(); + const dispatch = useDispatch(); + return ( + <> + + + + + ); +} 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 index c9fab3526a786..3db54cea6b108 100644 --- 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 @@ -8,14 +8,7 @@ import React, { useCallback, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { - EuiButtonGroup, - EuiColorPalettePicker, - EuiConfirmModal, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, -} from '@elastic/eui'; +import { EuiColorPalettePicker, EuiConfirmModal, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { RootState, updatePalette } from '../../state/color_mapping'; @@ -33,11 +26,8 @@ export function PaletteSelector({ 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( @@ -45,7 +35,6 @@ export function PaletteSelector({ paletteId: selectedPaletteId, assignments: updateAssignmentsPalette( model.assignments, - model.assignmentMode, model.colorMode, selectedPaletteId, getPaletteFn, @@ -62,37 +51,6 @@ export function PaletteSelector({ [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 = @@ -126,136 +84,44 @@ export function PaletteSelector({ ) : 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 - /> - - - + + 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, false); + }), + 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} + /> + ); } diff --git a/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/scale.tsx b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/scale.tsx new file mode 100644 index 0000000000000..056db47157c60 --- /dev/null +++ b/packages/kbn-coloring/src/shared_components/color_mapping/components/palette_selector/scale.tsx @@ -0,0 +1,144 @@ +/* + * 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, EuiConfirmModal, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { RootState, updatePalette } from '../../state/color_mapping'; +import { ColorMapping } from '../../config'; +import { updateAssignmentsPalette } from '../../config/assignments'; +import { getPalette } from '../../palettes'; + +export function ScaleMode({ getPaletteFn }: { getPaletteFn: ReturnType }) { + const dispatch = useDispatch(); + const colorMode = useSelector((state: RootState) => state.colorMapping.colorMode); + const model = useSelector((state: RootState) => state.colorMapping); + + const { paletteId } = model; + + 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, + updatedColorMode, + paletteId, + getPaletteFn, + preserveColorChanges + ); + dispatch(updatePalette({ paletteId, assignments, colorMode: updatedColorMode })); + }, + [getPaletteFn, model, dispatch, paletteId] + ); + + 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 gradient mode will discard all your custom color changes`, + })} +

+
+ ) : null; + + return ( + <> + {colorScaleModal} + + { + 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); + } + }} + /> + + + ); +} 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 deleted file mode 100644 index 97c4d17c35e4d..0000000000000 --- a/packages/kbn-coloring/src/shared_components/color_mapping/config/assignment_from_categories.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 index 701baa1b1710b..ce21732122150 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/config/assignments.ts +++ b/packages/kbn-coloring/src/shared_components/color_mapping/config/assignments.ts @@ -7,41 +7,35 @@ */ 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'; +import { getPalette } from '../palettes'; 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, - }; - } - }); + return 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, + colorIndex: index % palette.colorCount, + } + : { type: 'gradient' }; + return { + rule, + color: newColor, + touched: false, + }; + } + }); } export function updateColorModePalette( @@ -61,31 +55,3 @@ export function updateColorModePalette( 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 index e4005770b2883..8a6ae646b7b6b 100644 --- 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 @@ -13,12 +13,12 @@ import { NeutralPalette } from '../palettes/neutral'; import { getColor, getGradientColorScale } from '../color/color_handling'; export const DEFAULT_NEUTRAL_PALETTE_INDEX = 1; +export const DEFAULT_OTHER_ASSIGNMENT_INDEX = 0; /** * 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: [ { @@ -26,9 +26,7 @@ export const DEFAULT_COLOR_MAPPING_CONFIG: ColorMapping.Config = { type: 'other', }, color: { - type: 'categorical', - paletteId: NeutralPalette.id, - colorIndex: DEFAULT_NEUTRAL_PALETTE_INDEX, + type: 'loop', }, touched: false, }, @@ -45,17 +43,26 @@ export function getPaletteColors( ): 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)); + return getPaletteColorsFromPaletteId(isDarkMode, palette.id); +} + +export function getPaletteColorsFromPaletteId( + isDarkMode: boolean, + paletteId: ColorMapping.Config['paletteId'] +): string[] { + const palette = getPalette(AVAILABLE_PALETTES, NeutralPalette)(paletteId); + return Array.from({ length: palette.colorCount }, (d, i) => + palette.getColor(i, isDarkMode, true) + ); } export function getColorsFromMapping( isDarkMode: boolean, colorMappings?: ColorMapping.Config ): string[] { - const { colorMode, paletteId, assignmentMode, assignments, specialAssignments } = - colorMappings ?? { - ...DEFAULT_COLOR_MAPPING_CONFIG, - }; + const { colorMode, paletteId, assignments, specialAssignments } = colorMappings ?? { + ...DEFAULT_COLOR_MAPPING_CONFIG, + }; const getPaletteFn = getPalette(AVAILABLE_PALETTES, NeutralPalette); if (colorMode.type === 'gradient') { @@ -63,17 +70,23 @@ export function getColorsFromMapping( 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 !== ''); - } + const otherColors = + specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX].color.type === 'loop' + ? Array.from({ length: palette.colorCount }, (d, i) => + palette.getColor(i, isDarkMode, true) + ) + : [ + getColor( + specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX].color, + getPaletteFn, + isDarkMode + ), + ]; + return [ + ...assignments.map((a) => { + return a.color.type === 'gradient' ? '' : getColor(a.color, getPaletteFn, isDarkMode); + }), + ...otherColors, + ].filter((color) => color !== ''); } } 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 index 59cb18435112d..4c62044be9242 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/config/types.ts +++ b/packages/kbn-coloring/src/shared_components/color_mapping/config/types.ts @@ -30,6 +30,13 @@ export interface GradientColor { type: 'gradient'; } +/** + * An index specified categorical color, coming from paletteId + */ +export interface LoopColor { + type: 'loop'; +} + /** * A special rule that match automatically, in order, all the categories that are not matching a specified rule */ @@ -134,14 +141,13 @@ export interface GradientColorMode { export interface Config { paletteId: string; colorMode: CategoricalColorMode | GradientColorMode; - assignmentMode: 'auto' | 'manual'; assignments: Array< Assignment< RuleAuto | RuleMatchExactly | RuleMatchExactlyCI | RuleRange | RuleRegExp, CategoricalColor | ColorCode | GradientColor > >; - specialAssignments: Array>; + specialAssignments: Array>; } export interface CategoricalPalette { @@ -149,5 +155,5 @@ export interface CategoricalPalette { name: string; type: 'categorical'; colorCount: number; - getColor: (valueInRange: number, isDarkMode: boolean) => string; + getColor: (valueInRange: number, isDarkMode: boolean, loop: 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 index 1b49a2c6a8bf3..7484eabe816ab 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/index.ts +++ b/packages/kbn-coloring/src/shared_components/color_mapping/index.ts @@ -14,6 +14,7 @@ export * from './color/color_handling'; export { SPECIAL_TOKENS_STRING_CONVERTION } from './color/rule_matching'; export { DEFAULT_COLOR_MAPPING_CONFIG, + DEFAULT_OTHER_ASSIGNMENT_INDEX, 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 index d93440c5ac5e4..ce13184ff062a 100644 --- 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 @@ -22,7 +22,9 @@ export const ElasticBrandPalette: ColorMapping.CategoricalPalette = { name: 'Elastic Brand', colorCount: ELASTIC_BRAND_PALETTE_COLORS.length, type: 'categorical', - getColor(valueInRange) { - return ELASTIC_BRAND_PALETTE_COLORS[valueInRange]; + getColor(indexInRange, isDarkMode, loop) { + return ELASTIC_BRAND_PALETTE_COLORS[ + loop ? indexInRange % ELASTIC_BRAND_PALETTE_COLORS.length : indexInRange + ]; }, }; 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 index ec48793e12819..f9836a400b877 100644 --- 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 @@ -26,7 +26,9 @@ export const EUIAmsterdamColorBlindPalette: ColorMapping.CategoricalPalette = { name: 'Default', colorCount: EUI_AMSTERDAM_PALETTE_COLORS.length, type: 'categorical', - getColor(valueInRange) { - return EUI_AMSTERDAM_PALETTE_COLORS[valueInRange]; + getColor(indexInRange, isDarkMode, loop) { + return EUI_AMSTERDAM_PALETTE_COLORS[ + loop ? indexInRange % EUI_AMSTERDAM_PALETTE_COLORS.length : indexInRange + ]; }, }; 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 index 9b576e0b05c66..bb90130a817fe 100644 --- 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 @@ -23,7 +23,9 @@ export const KibanaV7LegacyPalette: ColorMapping.CategoricalPalette = { name: 'Kibana Legacy', colorCount: KIBANA_V7_LEGACY_PALETTE_COLORS.length, type: 'categorical', - getColor(valueInRange) { - return KIBANA_V7_LEGACY_PALETTE_COLORS[valueInRange]; + getColor(indexInRange, isDarkMode, loop) { + return KIBANA_V7_LEGACY_PALETTE_COLORS[ + loop ? indexInRange % KIBANA_V7_LEGACY_PALETTE_COLORS.length : indexInRange + ]; }, }; 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 index 27588aff2b389..704dbedcfec23 100644 --- 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 @@ -9,6 +9,7 @@ import { createSlice } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit'; import type { ColorMapping } from '../config'; +import { DEFAULT_OTHER_ASSIGNMENT_INDEX } from '../config/default_color_mapping'; export interface RootState { colorMapping: ColorMapping.Config; @@ -22,7 +23,6 @@ export interface RootState { } const initialState: RootState['colorMapping'] = { - assignmentMode: 'auto', assignments: [], specialAssignments: [], paletteId: 'eui', @@ -34,7 +34,6 @@ export const colorMappingSlice = createSlice({ 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; @@ -53,11 +52,9 @@ export const colorMappingSlice = createSlice({ state.colorMode = { ...action.payload.colorMode }; }, assignStatically: (state, action: PayloadAction) => { - state.assignmentMode = 'manual'; state.assignments = [...action.payload]; }, assignAutomatically: (state) => { - state.assignmentMode = 'auto'; state.assignments = []; }, @@ -67,6 +64,9 @@ export const colorMappingSlice = createSlice({ ) => { state.assignments.push({ ...action.payload }); }, + addNewAssignments: (state, action: PayloadAction) => { + state.assignments.push(...action.payload); + }, updateAssignment: ( state, action: PayloadAction<{ @@ -120,6 +120,21 @@ export const colorMappingSlice = createSlice({ }, removeAssignment: (state, action: PayloadAction) => { state.assignments.splice(action.payload, 1); + if (state.assignments.length === 0) { + state.specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX] = { + ...state.specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX], + color: { type: 'loop' }, + touched: true, + }; + } + }, + removeAllAssignments: (state) => { + state.assignments = []; + state.specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX] = { + ...state.specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX], + color: { type: 'loop' }, + touched: true, + }; }, changeColorMode: (state, action: PayloadAction) => { state.colorMode = { ...action.payload }; @@ -209,11 +224,13 @@ export const { assignStatically, assignAutomatically, addNewAssignment, + addNewAssignments, updateAssignment, updateAssignmentColor, updateSpecialAssignmentColor, updateAssignmentRule, removeAssignment, + removeAllAssignments, changeColorMode, updateGradientColorStep, removeGradientColorStep, 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 index 69bd57d2d852e..07cfdb9af0a79 100644 --- a/packages/kbn-coloring/src/shared_components/color_mapping/state/selectors.ts +++ b/packages/kbn-coloring/src/shared_components/color_mapping/state/selectors.ts @@ -18,9 +18,9 @@ export function selectColorMode(state: RootState) { 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; } +export function selectComputedAssignments(state: RootState) { + return state.colorMapping.assignments; +} diff --git a/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.test.ts b/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.test.ts index d6a6701aa658a..7998ba5bda3c0 100644 --- a/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.test.ts +++ b/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.test.ts @@ -10,35 +10,17 @@ import { ColorMapping, EUIAmsterdamColorBlindPalette, ElasticBrandPalette, - NeutralPalette, + DEFAULT_COLOR_MAPPING_CONFIG, + DEFAULT_OTHER_ASSIGNMENT_INDEX, } from '@kbn/coloring'; import faker from 'faker'; -import { DEFAULT_NEUTRAL_PALETTE_INDEX } from '@kbn/coloring/src/shared_components/color_mapping/config/default_color_mapping'; -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', - }, -}; - -const exampleAssignment = (valuesCount = 1, type = 'categorical', overrides = {}) => { - const color = +const exampleAssignment = ( + valuesCount = 1, + type = 'categorical', + overrides = {} +): ColorMapping.Config['assignments'][number] => { + const color: ColorMapping.Config['assignments'][number]['color'] = type === 'categorical' ? { type: 'categorical', @@ -58,11 +40,10 @@ const exampleAssignment = (valuesCount = 1, type = 'categorical', overrides = {} color, touched: false, ...overrides, - } as ColorMapping.Config['assignments'][0]; + }; }; const MANUAL_COLOR_MAPPING_CONFIG: ColorMapping.Config = { - assignmentMode: 'manual', assignments: [ exampleAssignment(4), exampleAssignment(), @@ -90,7 +71,7 @@ const MANUAL_COLOR_MAPPING_CONFIG: ColorMapping.Config = { const specialAssignmentsPalette: ColorMapping.Config['specialAssignments'] = [ { - ...DEFAULT_COLOR_MAPPING_CONFIG.specialAssignments[0], + ...DEFAULT_COLOR_MAPPING_CONFIG.specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX], color: { type: 'categorical', paletteId: EUIAmsterdamColorBlindPalette.id, @@ -100,7 +81,7 @@ const specialAssignmentsPalette: ColorMapping.Config['specialAssignments'] = [ ]; const specialAssignmentsCustom1: ColorMapping.Config['specialAssignments'] = [ { - ...DEFAULT_COLOR_MAPPING_CONFIG.specialAssignments[0], + ...DEFAULT_COLOR_MAPPING_CONFIG.specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX], color: { type: 'colorCode', colorCode: '#501a0e', @@ -109,7 +90,7 @@ const specialAssignmentsCustom1: ColorMapping.Config['specialAssignments'] = [ ]; const specialAssignmentsCustom2: ColorMapping.Config['specialAssignments'] = [ { - ...DEFAULT_COLOR_MAPPING_CONFIG.specialAssignments[0], + ...DEFAULT_COLOR_MAPPING_CONFIG.specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX], color: { type: 'colorCode', colorCode: 'red', @@ -129,11 +110,10 @@ describe('color_telemetry_helpers', () => { getColorMappingTelemetryEvents(MANUAL_COLOR_MAPPING_CONFIG, MANUAL_COLOR_MAPPING_CONFIG) ).toEqual([]); }); - it('settings (default): auto color mapping, unassigned terms neutral, default palette returns correct events', () => { + it('settings (default): unassigned terms loop, default palette returns correct events', () => { expect(getColorMappingTelemetryEvents(DEFAULT_COLOR_MAPPING_CONFIG)).toEqual([ - 'lens_color_mapping_auto', 'lens_color_mapping_palette_eui_amsterdam_color_blind', - 'lens_color_mapping_unassigned_terms_neutral', + 'lens_color_mapping_unassigned_terms_loop', ]); }); it('gradient event when user changed colorMode to gradient', () => { @@ -158,9 +138,8 @@ describe('color_telemetry_helpers', () => { ) ).toEqual(['lens_color_mapping_gradient']); }); - it('settings: manual mode, custom palette, unassigned terms from palette, 2 colors with 5 terms in total', () => { + it('settings: custom palette, unassigned terms from palette, 2 colors with 5 terms in total', () => { expect(getColorMappingTelemetryEvents(MANUAL_COLOR_MAPPING_CONFIG)).toEqual([ - 'lens_color_mapping_manual', 'lens_color_mapping_palette_elastic_brand_2023', 'lens_color_mapping_unassigned_terms_palette', 'lens_color_mapping_colors_2_to_4', @@ -170,7 +149,6 @@ describe('color_telemetry_helpers', () => { expect( getColorMappingTelemetryEvents(MANUAL_COLOR_MAPPING_CONFIG, DEFAULT_COLOR_MAPPING_CONFIG) ).toEqual([ - 'lens_color_mapping_manual', 'lens_color_mapping_palette_elastic_brand_2023', 'lens_color_mapping_unassigned_terms_palette', 'lens_color_mapping_colors_2_to_4', @@ -254,7 +232,7 @@ describe('color_telemetry_helpers', () => { }); describe('unassigned terms', () => { - it('unassigned terms changed from neutral to palette', () => { + it('unassigned terms changed from loop to palette', () => { expect( getColorMappingTelemetryEvents( { @@ -265,15 +243,15 @@ describe('color_telemetry_helpers', () => { ) ).toEqual(['lens_color_mapping_unassigned_terms_palette']); }); - it('unassigned terms changed from palette to neutral', () => { + it('unassigned terms changed from palette to loop', () => { expect( getColorMappingTelemetryEvents(DEFAULT_COLOR_MAPPING_CONFIG, { ...DEFAULT_COLOR_MAPPING_CONFIG, specialAssignments: specialAssignmentsPalette, }) - ).toEqual(['lens_color_mapping_unassigned_terms_neutral']); + ).toEqual(['lens_color_mapping_unassigned_terms_loop']); }); - it('unassigned terms changed from neutral to another custom color', () => { + it('unassigned terms changed from loop to another custom color', () => { expect( getColorMappingTelemetryEvents( { diff --git a/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.ts b/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.ts index d6b7acab55c7f..5bbfaaf290ef3 100644 --- a/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.ts +++ b/x-pack/plugins/lens/public/lens_ui_telemetry/color_telemetry_helpers.ts @@ -5,12 +5,7 @@ * 2.0. */ -import { ColorMapping, NeutralPalette } from '@kbn/coloring'; -import type { - CategoricalColor, - ColorCode, - GradientColor, -} from '@kbn/coloring/src/shared_components/color_mapping/config/types'; +import { ColorMapping, NeutralPalette, DEFAULT_OTHER_ASSIGNMENT_INDEX } from '@kbn/coloring'; import { isEqual } from 'lodash'; import { nonNullable } from '../utils'; @@ -24,17 +19,14 @@ export const getColorMappingTelemetryEvents = ( return []; } - const { assignments, specialAssignments, assignmentMode, colorMode, paletteId } = colorMapping; + const { assignments, specialAssignments, colorMode, paletteId } = colorMapping; const { - assignmentMode: prevAssignmentMode, assignments: prevAssignments, specialAssignments: prevSpecialAssignments, colorMode: prevColorMode, paletteId: prevPaletteId, } = prevColorMapping || {}; - const assignmentModeData = assignmentMode !== prevAssignmentMode ? assignmentMode : undefined; - const paletteData = prevPaletteId !== paletteId ? `palette_${paletteId}` : undefined; const gradientData = @@ -42,18 +34,16 @@ export const getColorMappingTelemetryEvents = ( const unassignedTermsType = getUnassignedTermsType(specialAssignments, prevSpecialAssignments); - const diffData = [assignmentModeData, gradientData, paletteData, unassignedTermsType].filter( - nonNullable - ); + const diffData = [gradientData, paletteData, unassignedTermsType].filter(nonNullable); - if (assignmentMode === 'manual') { + if (assignments.length > 0) { const colorCount = assignments.length && !isEqual(assignments, prevAssignments) ? `colors_${getRangeText(assignments.length)}` : undefined; - const prevCustomColors = prevAssignments?.filter((a) => isCustomColor(a.color)); - const customColors = assignments.filter((a) => isCustomColor(a.color)); + const prevCustomColors = prevAssignments?.filter((a) => a.color.type === 'colorCode'); + const customColors = assignments.filter((a) => a.color.type === 'colorCode'); const customColorEvent = customColors.length && !isEqual(prevCustomColors, customColors) ? `custom_colors_${getRangeText(customColors.length, 1)}` @@ -68,10 +58,6 @@ export const getColorMappingTelemetryEvents = ( const constructName = (eventName: string) => `${COLOR_MAPPING_PREFIX}${eventName}`; -const isCustomColor = (color: CategoricalColor | ColorCode | GradientColor): color is ColorCode => { - return color.type === 'colorCode'; -}; - function getRangeText(n: number, min = 2, max = 16) { if (n >= min && (n === 1 || n === 2)) { return String(n); @@ -92,9 +78,12 @@ const getUnassignedTermsType = ( ) => { return !isEqual(prevSpecialAssignments, specialAssignments) ? `unassigned_terms_${ - isCustomColor(specialAssignments?.[0].color) + specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX]?.color.type === 'colorCode' ? 'custom' - : specialAssignments?.[0].color.paletteId === NeutralPalette.id + : specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX]?.color.type === 'loop' + ? 'loop' + : specialAssignments[DEFAULT_OTHER_ASSIGNMENT_INDEX]?.color.paletteId === + NeutralPalette.id ? NeutralPalette.id : 'palette' }` diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index b8d9c9eb44c43..a9b62b8622cdc 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -140,8 +140,6 @@ "coloring.colorMapping.assignments.autoAssignedTermAriaLabel": "Cette couleur sera automatiquement affectée au premier terme qui ne correspond pas à toutes les autres affectations", "coloring.colorMapping.assignments.autoAssignedTermPlaceholder": "Affecté automatiquement", "coloring.colorMapping.assignments.deleteAssignmentButtonLabel": "Supprimer cette affectation", - "coloring.colorMapping.assignments.unassignedAriaLabel": "Affecter cette couleur à tout terme non affecté qui n'est pas décrit dans la liste d'affectation", - "coloring.colorMapping.assignments.unassignedPlaceholder": "Termes non affectés", "coloring.colorMapping.colorChangesModal.categoricalModeDescription": "Basculer en mode de catégorie conduira à l'abandon de toutes vos modifications de couleurs personnalisées", "coloring.colorMapping.colorChangesModal.discardButton": "Abandonner les modifications", "coloring.colorMapping.colorChangesModal.discardButtonLabel": "Abandonner les modifications", @@ -159,14 +157,11 @@ "coloring.colorMapping.colorPicker.removeGradientColorButtonLabel": "Supprimer l'étape couleur", "coloring.colorMapping.colorPicker.themeAwareColorsLabel": "Couleurs neutres", "coloring.colorMapping.colorPicker.themeAwareColorsTooltip": "Les couleurs neutres fournies se conforment au thème et s'adapteront en fonction du basculement entre les thèmes clair et sombre", - "coloring.colorMapping.container.addAssignmentButtonLabel": "Ajouter une affectation", - "coloring.colorMapping.container.autoAssignLabel": "Affectation automatique", "coloring.colorMapping.container.invertGradientButtonLabel": "Inverser le gradient", "coloring.colorMapping.container.mappingAssignmentHeader": "Mapping des affectations", "coloring.colorMapping.paletteSelector.categoricalLabel": "De catégorie", "coloring.colorMapping.paletteSelector.paletteLabel": "Palette de couleurs", "coloring.colorMapping.paletteSelector.scaleLabel": "Scaling", - "coloring.colorMapping.paletteSelector.sequentialLabel": "Séquentiel", "coloring.dynamicColoring.customPalette.addColor": "Ajouter une couleur", "coloring.dynamicColoring.customPalette.addColorAriaLabel": "Ajouter une couleur", "coloring.dynamicColoring.customPalette.colorStopsHelpPercentage": "Les types de valeurs en pourcentage sont relatifs à la plage complète des valeurs de données disponibles.", @@ -42905,4 +42900,4 @@ "xpack.serverlessObservability.nav.projectSettings": "Paramètres de projet", "xpack.serverlessObservability.nav.visualizations": "Visualisations" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index cf3ea4c3fe0a9..77335897bd208 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -140,8 +140,6 @@ "coloring.colorMapping.assignments.autoAssignedTermAriaLabel": "この色は、他のすべての割り当てと一致しない最初の用語に自動的に割り当てられます。", "coloring.colorMapping.assignments.autoAssignedTermPlaceholder": "自動割り当て済み", "coloring.colorMapping.assignments.deleteAssignmentButtonLabel": "この割り当てを削除", - "coloring.colorMapping.assignments.unassignedAriaLabel": "割り当てリストに記載されていない未割り当てのすべての色にこの色を割り当てます。", - "coloring.colorMapping.assignments.unassignedPlaceholder": "割り当てられていない用語", "coloring.colorMapping.colorChangesModal.categoricalModeDescription": "分類モードに切り替えると、カスタム色の変更はすべて破棄されます。", "coloring.colorMapping.colorChangesModal.discardButton": "変更を破棄", "coloring.colorMapping.colorChangesModal.discardButtonLabel": "変更を破棄", @@ -159,14 +157,11 @@ "coloring.colorMapping.colorPicker.removeGradientColorButtonLabel": "色ステップを削除", "coloring.colorMapping.colorPicker.themeAwareColorsLabel": "中間色", "coloring.colorMapping.colorPicker.themeAwareColorsTooltip": "提供されている中間色はテーマを意識しており、明るいテーマと暗いテーマを切り替えると適切に変化します。", - "coloring.colorMapping.container.addAssignmentButtonLabel": "割り当てを追加", - "coloring.colorMapping.container.autoAssignLabel": "自動割り当て", "coloring.colorMapping.container.invertGradientButtonLabel": "グラデーションを反転", "coloring.colorMapping.container.mappingAssignmentHeader": "マッピング割り当て", "coloring.colorMapping.paletteSelector.categoricalLabel": "分類", "coloring.colorMapping.paletteSelector.paletteLabel": "カラーパレット", "coloring.colorMapping.paletteSelector.scaleLabel": "スケール", - "coloring.colorMapping.paletteSelector.sequentialLabel": "連続", "coloring.dynamicColoring.customPalette.addColor": "色を追加", "coloring.dynamicColoring.customPalette.addColorAriaLabel": "色を追加", "coloring.dynamicColoring.customPalette.colorStopsHelpPercentage": "割合値は使用可能なデータ値の全範囲に対して相対的です。", @@ -42897,4 +42892,4 @@ "xpack.serverlessObservability.nav.projectSettings": "プロジェクト設定", "xpack.serverlessObservability.nav.visualizations": "ビジュアライゼーション" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 26029615545f8..70fe8f8c91298 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -140,8 +140,6 @@ "coloring.colorMapping.assignments.autoAssignedTermAriaLabel": "会将此颜色自动分配给第一个与所有其他分配均不匹配的词", "coloring.colorMapping.assignments.autoAssignedTermPlaceholder": "已自动分配", "coloring.colorMapping.assignments.deleteAssignmentButtonLabel": "删除此分配", - "coloring.colorMapping.assignments.unassignedAriaLabel": "将此颜色分配给分配列表中未描述的每个未分配项", - "coloring.colorMapping.assignments.unassignedPlaceholder": "未分配的词", "coloring.colorMapping.colorChangesModal.categoricalModeDescription": "切换到分类模式将丢弃您的所有定制颜色更改", "coloring.colorMapping.colorChangesModal.discardButton": "放弃更改", "coloring.colorMapping.colorChangesModal.discardButtonLabel": "放弃更改", @@ -159,14 +157,11 @@ "coloring.colorMapping.colorPicker.removeGradientColorButtonLabel": "移除色阶", "coloring.colorMapping.colorPicker.themeAwareColorsLabel": "中性色", "coloring.colorMapping.colorPicker.themeAwareColorsTooltip": "提供的中性色能够感知主题,在浅色主题与深色主题之间切换时会做出相应更改", - "coloring.colorMapping.container.addAssignmentButtonLabel": "添加分配", - "coloring.colorMapping.container.autoAssignLabel": "自动分配", "coloring.colorMapping.container.invertGradientButtonLabel": "反向渐变", "coloring.colorMapping.container.mappingAssignmentHeader": "映射分配", "coloring.colorMapping.paletteSelector.categoricalLabel": "分类", "coloring.colorMapping.paletteSelector.paletteLabel": "调色板", "coloring.colorMapping.paletteSelector.scaleLabel": "比例", - "coloring.colorMapping.paletteSelector.sequentialLabel": "顺序", "coloring.dynamicColoring.customPalette.addColor": "添加颜色", "coloring.dynamicColoring.customPalette.addColorAriaLabel": "添加颜色", "coloring.dynamicColoring.customPalette.colorStopsHelpPercentage": "百分比值是相对于全范围可用数据值的类型。", @@ -42877,4 +42872,4 @@ "xpack.serverlessObservability.nav.projectSettings": "项目设置", "xpack.serverlessObservability.nav.visualizations": "可视化" } -} +} \ No newline at end of file diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index c159fb17b4db7..c5018bda39ffa 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -1934,8 +1934,9 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await testSubjects.existOrFail('lns-indexPattern-dimensionContainerClose'); }); await testSubjects.click('lns_colorEditing_trigger'); - // disable autoAssign - await testSubjects.setEuiSwitch('lns-colorMapping-autoAssignSwitch', 'uncheck'); + + // assign all + await testSubjects.click('lns-colorMapping-assignmentsPromptAddAll'); await testSubjects.click(`lns-colorMapping-colorSwatch-${colorSwatchIndex}`);