[Visualize] Remove visualization:colorMapping advanced setting (#197802)

## Summary

Fixes https://github.com/elastic/kibana/issues/193682 Removes the deprecated `visualization:colorMapping` advanced setting.
This commit is contained in:
Marta Bondyra 2024-11-21 21:37:53 +01:00 committed by GitHub
parent c842db549a
commit 407f6be053
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 128 additions and 864 deletions

View file

@ -53,26 +53,23 @@ On the client, the `uiSettings` service is accessible directly from `core` and t
The following is a basic example for using the `uiSettings` service:
**src/plugins/charts/public/plugin.ts**
```ts
import { Plugin, CoreSetup } from '@kbn/core/public';
import { ExpressionsSetup } from '../../expressions/public';
import { ExpressionsSetup } from '@kbn/expressions-plugin/public';
import { palette, systemPalette } from '../common';
import { ThemeService, LegacyColorsService } from './services';
import { ThemeService } from './services';
import { PaletteService } from './services/palettes/service';
import { ActiveCursor } from './services/active_cursor';
export type Theme = Omit<ThemeService, 'init'>;
export type Color = Omit<LegacyColorsService, 'init'>;
interface SetupDependencies {
expressions: ExpressionsSetup;
}
/** @public */
export interface ChartsPluginSetup {
legacyColors: Color;
theme: Theme;
theme: Omit<ThemeService, 'init'>;
palettes: ReturnType<PaletteService['setup']>;
}
@ -84,7 +81,6 @@ export type ChartsPluginStart = ChartsPluginSetup & {
/** @public */
export class ChartsPlugin implements Plugin<ChartsPluginSetup, ChartsPluginStart> {
private readonly themeService = new ThemeService();
private readonly legacyColorsService = new LegacyColorsService();
private readonly paletteService = new PaletteService();
private readonly activeCursor = new ActiveCursor();
@ -93,14 +89,12 @@ export class ChartsPlugin implements Plugin<ChartsPluginSetup, ChartsPluginStart
public setup(core: CoreSetup, dependencies: SetupDependencies): ChartsPluginSetup {
dependencies.expressions.registerFunction(palette);
dependencies.expressions.registerFunction(systemPalette);
this.themeService.init(core.uiSettings);
this.legacyColorsService.init(core.uiSettings);
this.palettes = this.paletteService.setup(this.legacyColorsService);
this.themeService.init(core.theme);
this.palettes = this.paletteService.setup();
this.activeCursor.setup();
return {
legacyColors: this.legacyColorsService,
theme: this.themeService,
palettes: this.palettes,
};
@ -108,14 +102,12 @@ export class ChartsPlugin implements Plugin<ChartsPluginSetup, ChartsPluginStart
public start(): ChartsPluginStart {
return {
legacyColors: this.legacyColorsService,
theme: this.themeService,
palettes: this.palettes!,
activeCursor: this.activeCursor,
};
}
}
```
### Server side usage
@ -124,66 +116,50 @@ On the server side, `uiSettings` are accessible directly from `core`. The follow
The example also shows how plugins can leverage the optional deprecation parameter on registration for handling deprecation notices and renames. The deprecation warnings are rendered in the Advanced Settings UI and should also be added to the Configure Kibana guide.
<DocCallOut>
Refer to [the server-side uiSettings service API docs](https://github.com/elastic/kibana/blob/main/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md)
Refer to [the server-side uiSettings service API
docs](https://github.com/elastic/kibana/blob/main/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md)
</DocCallOut>
**src/plugins/charts/server/plugin.ts**
**src/plugins/dev_tools/server/plugin.ts**
```ts
import { i18n } from '@kbn/i18n';
import { schema } from '@kbn/config-schema';
import { CoreSetup, Plugin } from '@kbn/core/server';
import { COLOR_MAPPING_SETTING, LEGACY_TIME_AXIS, palette, systemPalette } from '../common';
import { ExpressionsServerSetup } from '../../expressions/server';
import { PluginInitializerContext, Plugin, CoreSetup } from '@kbn/core/server';
interface SetupDependencies {
expressions: ExpressionsServerSetup;
}
import { uiSettings } from './ui_settings';
export class DevToolsServerPlugin implements Plugin<object, object> {
constructor(initializerContext: PluginInitializerContext) {}
export class ChartsServerPlugin implements Plugin<object, object> {
public setup(core: CoreSetup, dependencies: SetupDependencies) {
dependencies.expressions.registerFunction(palette);
dependencies.expressions.registerFunction(systemPalette);
public setup(core: CoreSetup<object>) {
/**
* Register Dev Tools UI Settings
*/
core.uiSettings.register({
[COLOR_MAPPING_SETTING]: {
name: i18n.translate('charts.advancedSettings.visualization.colorMappingTitle', {
defaultMessage: 'Color mapping',
}),
value: JSON.stringify({
Count: '#00A69B',
}),
type: 'json',
description: i18n.translate('charts.advancedSettings.visualization.colorMappingText', {
[ENABLE_PERSISTENT_CONSOLE_UI_SETTING_ID]: {
category: [DEV_TOOLS_FEATURE_ID],
description: i18n.translate('devTools.uiSettings.persistentConsole.description', {
defaultMessage:
'Maps values to specific colors in charts using the <strong>Compatibility</strong> palette.',
'Enables a persistent console in the Kibana UI. This setting does not affect the standard Console in Dev Tools.',
}),
deprecation: {
message: i18n.translate(
'charts.advancedSettings.visualization.colorMappingTextDeprecation',
{
defaultMessage:
'This setting is deprecated and will not be supported in a future version.',
}
),
docLinksKey: 'visualizationSettings',
},
category: ['visualization'],
schema: schema.string(),
name: i18n.translate('devTools.uiSettings.persistentConsole.name', {
defaultMessage: 'Persistent Console',
}),
requiresPageReload: true,
schema: schema.boolean(),
value: true,
},
...
});
return {};
}
public start() {
return {};
}
public stop() {}
}
}
```
For optimal Kibana performance, `uiSettings` are cached. Any changes that require a cache refresh should use the `requiresPageReload` parameter on registration.
For optimal Kibana performance, `uiSettings` are cached. Any changes that require a cache refresh should use the `requiresPageReload` parameter on registration.
For example, changing the time filter refresh interval triggers a prompt in the UI that the page needs to be refreshed to save the new value:

View file

@ -10,7 +10,6 @@
import { ChartsPlugin } from './plugin';
import { themeServiceMock } from './services/theme/mock';
import { activeCursorMock } from './services/active_cursor/mock';
import { colorsServiceMock } from './services/legacy_colors/mock';
import { getPaletteRegistry, paletteServiceMock } from './services/palettes/mock';
export { MOCK_SPARKLINE_THEME } from './services/theme/mock';
@ -19,16 +18,14 @@ export type Setup = jest.Mocked<ReturnType<ChartsPlugin['setup']>>;
export type Start = jest.Mocked<ReturnType<ChartsPlugin['start']>>;
const createSetupContract = (): Setup => ({
legacyColors: colorsServiceMock,
theme: themeServiceMock,
palettes: paletteServiceMock.setup({} as any),
palettes: paletteServiceMock.setup(),
});
const createStartContract = (): Start => ({
legacyColors: colorsServiceMock,
theme: themeServiceMock,
activeCursor: activeCursorMock,
palettes: paletteServiceMock.setup({} as any),
palettes: paletteServiceMock.setup(),
});
export const chartPluginMock = {

View file

@ -11,7 +11,7 @@ import { Plugin, CoreSetup } from '@kbn/core/public';
import { ExpressionsSetup } from '@kbn/expressions-plugin/public';
import { palette, systemPalette } from '../common';
import { ThemeService, LegacyColorsService } from './services';
import { ThemeService } from './services';
import { PaletteService } from './services/palettes/service';
import { ActiveCursor } from './services/active_cursor';
@ -21,7 +21,6 @@ interface SetupDependencies {
/** @public */
export interface ChartsPluginSetup {
legacyColors: Omit<LegacyColorsService, 'init'>;
theme: Omit<ThemeService, 'init'>;
palettes: ReturnType<PaletteService['setup']>;
}
@ -34,7 +33,6 @@ export type ChartsPluginStart = ChartsPluginSetup & {
/** @public */
export class ChartsPlugin implements Plugin<ChartsPluginSetup, ChartsPluginStart> {
private readonly themeService = new ThemeService();
private readonly legacyColorsService = new LegacyColorsService();
private readonly paletteService = new PaletteService();
private readonly activeCursor = new ActiveCursor();
@ -44,13 +42,11 @@ export class ChartsPlugin implements Plugin<ChartsPluginSetup, ChartsPluginStart
dependencies.expressions.registerFunction(palette);
dependencies.expressions.registerFunction(systemPalette);
this.themeService.init(core.theme);
this.legacyColorsService.init(core.uiSettings);
this.palettes = this.paletteService.setup(this.legacyColorsService);
this.palettes = this.paletteService.setup();
this.activeCursor.setup();
return {
legacyColors: this.legacyColorsService,
theme: this.themeService,
palettes: this.palettes,
};
@ -58,7 +54,6 @@ export class ChartsPlugin implements Plugin<ChartsPluginSetup, ChartsPluginStart
public start(): ChartsPluginStart {
return {
legacyColors: this.legacyColorsService,
theme: this.themeService,
palettes: this.palettes!,
activeCursor: this.activeCursor,

View file

@ -7,6 +7,5 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export { LegacyColorsService } from './legacy_colors';
export { ThemeService } from './theme';
export { ActiveCursor, useActiveCursor } from './active_cursor';

View file

@ -1,140 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { coreMock } from '@kbn/core/public/mocks';
import { COLOR_MAPPING_SETTING } from '../../../common';
import { seedColors } from '../../static/colors';
import { LegacyColorsService } from './colors';
// Local state for config
const config = new Map<string, any>();
describe('Vislib Color Service', () => {
const colors = new LegacyColorsService();
const mockUiSettings = coreMock.createSetup().uiSettings;
mockUiSettings.get.mockImplementation((a) => config.get(a));
mockUiSettings.set.mockImplementation((...a) => config.set(...a) as any);
colors.init(mockUiSettings);
let color: any;
let previousConfig: any;
const arr = ['good', 'better', 'best', 'never', 'let', 'it', 'rest'];
const arrayOfNumbers = [1, 2, 3, 4, 5];
const arrayOfUndefinedValues = [undefined, undefined, undefined];
const arrayOfObjects = [{}, {}, {}];
const arrayOfBooleans = [true, false, true];
const arrayOfNullValues = [null, null, null];
const emptyObject = {};
const nullValue = null;
beforeEach(() => {
previousConfig = config.get(COLOR_MAPPING_SETTING);
config.set(COLOR_MAPPING_SETTING, {});
color = colors.createColorLookupFunction(arr, {});
});
afterEach(() => {
config.set(COLOR_MAPPING_SETTING, previousConfig);
});
it('should throw error if not initialized', () => {
const colorsBad = new LegacyColorsService();
expect(() => colorsBad.createColorLookupFunction(arr, {})).toThrowError();
});
it('should throw an error if input is not an array', () => {
expect(() => {
// @ts-expect-error
colors.createColorLookupFunction(200);
}).toThrowError();
expect(() => {
// @ts-expect-error
colors.createColorLookupFunction('help');
}).toThrowError();
expect(() => {
// @ts-expect-error
colors.createColorLookupFunction(true);
}).toThrowError();
expect(() => {
colors.createColorLookupFunction();
}).toThrowError();
expect(() => {
// @ts-expect-error
colors.createColorLookupFunction(nullValue);
}).toThrowError();
expect(() => {
// @ts-expect-error
colors.createColorLookupFunction(emptyObject);
}).toThrowError();
});
describe('when array is not composed of numbers, strings, or undefined values', () => {
it('should throw an error', () => {
expect(() => {
// @ts-expect-error
colors.createColorLookupFunction(arrayOfObjects);
}).toThrowError();
expect(() => {
// @ts-expect-error
colors.createColorLookupFunction(arrayOfBooleans);
}).toThrowError();
expect(() => {
// @ts-expect-error
colors.createColorLookupFunction(arrayOfNullValues);
}).toThrowError();
});
});
describe('when input is an array of strings, numbers, or undefined values', () => {
it('should not throw an error', () => {
expect(() => {
colors.createColorLookupFunction(arr);
}).not.toThrowError();
expect(() => {
colors.createColorLookupFunction(arrayOfNumbers);
}).not.toThrowError();
expect(() => {
// @ts-expect-error
colors.createColorLookupFunction(arrayOfUndefinedValues);
}).not.toThrowError();
});
});
it('should be a function', () => {
expect(typeof colors.createColorLookupFunction).toBe('function');
});
it('should return a function', () => {
expect(typeof color).toBe('function');
});
it('should return the first hex color in the seed colors array', () => {
expect(color(arr[0])).toBe(seedColors[0]);
});
it('should return the value from the mapped colors', () => {
expect(color(arr[1])).toBe(colors.mappedColors.get(arr[1]));
});
it('should return the value from the specified color mapping overrides', () => {
const colorFn = colors.createColorLookupFunction(arr, { good: 'red' });
expect(colorFn('good')).toBe('red');
});
});

View file

@ -1,64 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import _ from 'lodash';
import { CoreSetup } from '@kbn/core/public';
import { MappedColors } from '../mapped_colors';
import { seedColors } from '../../static/colors';
/**
* Accepts an array of strings or numbers that are used to create a
* a lookup table that associates the values (key) with a hex color (value).
* Returns a function that accepts a value (i.e. a string or number)
* and returns a hex color associated with that value.
*/
export class LegacyColorsService {
private _mappedColors?: MappedColors;
public readonly seedColors = seedColors;
public get mappedColors() {
if (!this._mappedColors) {
throw new Error('ColorService not yet initialized');
}
return this._mappedColors;
}
init(uiSettings: CoreSetup['uiSettings']) {
this._mappedColors = new MappedColors(uiSettings);
}
createColorLookupFunction(
arrayOfStringsOrNumbers?: Array<string | number>,
colorMapping: Partial<Record<string, string>> = {}
) {
if (!Array.isArray(arrayOfStringsOrNumbers)) {
throw new Error(
`createColorLookupFunction expects an array but recived: ${typeof arrayOfStringsOrNumbers}`
);
}
arrayOfStringsOrNumbers.forEach(function (val) {
if (!_.isString(val) && !_.isNumber(val) && !_.isUndefined(val)) {
throw new TypeError(
'createColorLookupFunction expects an array of strings, numbers, or undefined values'
);
}
});
this.mappedColors.mapKeys(arrayOfStringsOrNumbers);
return (value: string | number) => {
return colorMapping[value] || this.mappedColors.get(value);
};
}
}

View file

@ -1,87 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { seedColors } from '../../static/colors';
import { createColorPalette } from '../../static/colors';
describe('Color Palette', () => {
const num1 = 45;
const num2 = 72;
const num3 = 90;
const string = 'Welcome';
const bool = true;
const nullValue = null;
const emptyArr: [] = [];
const emptyObject = {};
let colorPalette: string[];
beforeEach(() => {
colorPalette = createColorPalette(num1);
});
it('should throw an error if input is not a number', () => {
expect(() => {
// @ts-expect-error
createColorPalette(string);
}).toThrowError();
expect(() => {
// @ts-expect-error
createColorPalette(bool);
}).toThrowError();
expect(() => {
// @ts-expect-error
createColorPalette(nullValue);
}).toThrowError();
expect(() => {
// @ts-expect-error
createColorPalette(emptyArr);
}).toThrowError();
expect(() => {
// @ts-expect-error
createColorPalette(emptyObject);
}).toThrowError();
expect(() => {
// @ts-expect-error
createColorPalette();
}).toThrowError();
});
it('should be a function', () => {
expect(typeof createColorPalette).toBe('function');
});
it('should return an array', () => {
expect(colorPalette).toBeInstanceOf(Array);
});
it('should return an array of the same length as the input', () => {
expect(colorPalette.length).toBe(num1);
});
it('should return the seed color array when input length is 72', () => {
expect(createColorPalette(num2)[71]).toBe(seedColors[71]);
});
it('should return an array of the same length as the input when input is greater than 72', () => {
expect(createColorPalette(num3).length).toBe(num3);
});
it('should create new darker colors when input is greater than 72', () => {
expect(createColorPalette(num3)[72]).not.toEqual(seedColors[0]);
});
it('should create new colors and convert them correctly', () => {
expect(createColorPalette(num3)[72]).toEqual('#404ABF');
});
});

View file

@ -1,10 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export { LegacyColorsService } from './colors';

View file

@ -1,23 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { coreMock } from '@kbn/core/public/mocks';
import { LegacyColorsService } from './colors';
const colors = new LegacyColorsService();
colors.init(coreMock.createSetup().uiSettings);
export const colorsServiceMock: LegacyColorsService = {
createColorLookupFunction: jest.fn(colors.createColorLookupFunction.bind(colors)),
mappedColors: {
mapKeys: jest.fn(),
get: jest.fn(),
getColorFromConfig: jest.fn(),
},
} as any;

View file

@ -8,128 +8,33 @@
*/
import _ from 'lodash';
import Color from 'color';
import { coreMock } from '@kbn/core/public/mocks';
import { COLOR_MAPPING_SETTING } from '../../../common';
import { seedColors } from '../../static/colors';
import { MappedColors } from './mapped_colors';
// Local state for config
const config = new Map<string, any>();
describe('Mapped Colors', () => {
const mockUiSettings = coreMock.createSetup().uiSettings;
mockUiSettings.get.mockImplementation((a) => config.get(a));
mockUiSettings.set.mockImplementation((...a) => config.set(...a) as any);
const mappedColors = new MappedColors(mockUiSettings);
let previousConfig: any;
beforeEach(() => {
previousConfig = config.get(COLOR_MAPPING_SETTING);
mappedColors.purge();
});
afterEach(() => {
config.set(COLOR_MAPPING_SETTING, previousConfig);
});
it('should properly map keys to unique colors', () => {
config.set(COLOR_MAPPING_SETTING, {});
const mappedColors = new MappedColors();
const arr = [1, 2, 3, 4, 5];
mappedColors.mapKeys(arr);
expect(_(mappedColors.mapping).values().uniq().size()).toBe(arr.length);
expect(_.keys(mappedColors.mapping).length).toBe(5);
});
it('should not include colors used by the config', () => {
const newConfig = { bar: seedColors[0] };
config.set(COLOR_MAPPING_SETTING, newConfig);
const arr = ['foo', 'baz', 'qux'];
mappedColors.mapKeys(arr);
const colorValues = _(mappedColors.mapping).values();
expect(colorValues).not.toContain(seedColors[0]);
expect(colorValues.uniq().size()).toBe(arr.length);
});
it('should create a unique array of colors even when config is set', () => {
const newConfig = { bar: seedColors[0] };
config.set(COLOR_MAPPING_SETTING, newConfig);
const arr = ['foo', 'bar', 'baz', 'qux'];
mappedColors.mapKeys(arr);
const expectedSize = _(arr).difference(_.keys(newConfig)).size();
expect(_(mappedColors.mapping).values().uniq().size()).toBe(expectedSize);
expect(mappedColors.get(arr[0])).not.toBe(seedColors[0]);
});
it('should treat different formats of colors as equal', () => {
const color = new Color(seedColors[0]);
const rgb = `rgb(${color.red()}, ${color.green()}, ${color.blue()})`;
const newConfig = { bar: rgb };
config.set(COLOR_MAPPING_SETTING, newConfig);
const arr = ['foo', 'bar', 'baz', 'qux'];
mappedColors.mapKeys(arr);
const expectedSize = _(arr).difference(_.keys(newConfig)).size();
expect(_(mappedColors.mapping).values().uniq().size()).toBe(expectedSize);
expect(mappedColors.get(arr[0])).not.toBe(seedColors[0]);
expect(mappedColors.get('bar')).toBe(seedColors[0]);
});
it('should have a flush method that moves the current map to the old map', function () {
it('should allow to map keys multiple times and add new colors when doing so', function () {
const mappedColors = new MappedColors();
const arr = [1, 2, 3, 4, 5];
mappedColors.mapKeys(arr);
expect(_.keys(mappedColors.mapping).length).toBe(5);
expect(_.keys(mappedColors.oldMap).length).toBe(0);
mappedColors.mapKeys([6, 7]);
mappedColors.flush();
expect(_.keys(mappedColors.oldMap).length).toBe(5);
expect(_.keys(mappedColors.mapping).length).toBe(0);
mappedColors.flush();
expect(_.keys(mappedColors.oldMap).length).toBe(0);
expect(_.keys(mappedColors.mapping).length).toBe(0);
});
it('should use colors in the oldMap if they are available', function () {
const arr = [1, 2, 3, 4, 5];
mappedColors.mapKeys(arr);
expect(_.keys(mappedColors.mapping).length).toBe(5);
expect(_.keys(mappedColors.oldMap).length).toBe(0);
mappedColors.flush();
mappedColors.mapKeys([3, 4, 5]);
expect(_.keys(mappedColors.oldMap).length).toBe(5);
expect(_.keys(mappedColors.mapping).length).toBe(3);
expect(mappedColors.mapping[1]).toBe(undefined);
expect(mappedColors.mapping[2]).toBe(undefined);
expect(mappedColors.mapping[3]).toEqual(mappedColors.oldMap[3]);
expect(mappedColors.mapping[4]).toEqual(mappedColors.oldMap[4]);
expect(mappedColors.mapping[5]).toEqual(mappedColors.oldMap[5]);
});
it('should have a purge method that clears both maps', function () {
const arr = [1, 2, 3, 4, 5];
mappedColors.mapKeys(arr);
mappedColors.flush();
mappedColors.mapKeys(arr);
expect(_.keys(mappedColors.mapping).length).toBe(5);
expect(_.keys(mappedColors.oldMap).length).toBe(5);
mappedColors.purge();
expect(_.keys(mappedColors.mapping).length).toBe(0);
expect(_.keys(mappedColors.oldMap).length).toBe(0);
expect(_.keys(mappedColors.mapping).length).toBe(7);
expect(mappedColors.mapping).toEqual({
'1': '#00a69b',
'2': '#57c17b',
'3': '#6f87d8',
'4': '#663db8',
'5': '#bc52bc',
'6': '#9e3533',
'7': '#daa05d',
});
});
});

View file

@ -8,84 +8,37 @@
*/
import _ from 'lodash';
import Color from 'color';
import { CoreSetup } from '@kbn/core/public';
import { COLOR_MAPPING_SETTING } from '../../../common';
import { createColorPalette } from '../../static/colors';
const standardizeColor = (color: string) => new Color(color).hex().toLowerCase();
/**
* Maintains a lookup table that associates the value (key) with a hex color (value)
* across the visualizations.
* Provides functions to interact with the lookup table
*/
export class MappedColors {
private _oldMap: any;
private _mapping: any;
constructor(
private uiSettings?: CoreSetup['uiSettings'],
private colorPaletteFn: (num: number) => string[] = createColorPalette
) {
this._oldMap = {};
constructor(private colorPaletteFn: (num: number) => string[] = createColorPalette) {
this._mapping = {};
}
private getConfigColorMapping(): Record<string, string> {
return _.mapValues(this.uiSettings?.get(COLOR_MAPPING_SETTING) || {}, standardizeColor);
}
public get oldMap(): any {
return this._oldMap;
}
public get mapping(): any {
return this._mapping;
}
get(key: string | number) {
return this.getConfigColorMapping()[key as any] || this._mapping[key];
}
getColorFromConfig(key: string | number) {
return this.getConfigColorMapping()[key as any];
}
flush() {
this._oldMap = _.clone(this._mapping);
this._mapping = {};
}
purge() {
this._oldMap = {};
this._mapping = {};
return this._mapping[key];
}
mapKeys(keys: Array<string | number>) {
const configMapping = this.getConfigColorMapping();
const configColors = _.values(configMapping);
const oldColors = _.values(this._oldMap);
const keysToMap: Array<string | number> = [];
_.each(keys, (key) => {
// If this key is mapped in the config, it's unnecessary to have it mapped here
if (configMapping[key as any]) delete this._mapping[key];
// If this key is mapped to a color used by the config color mapping, we need to remap it
if (_.includes(configColors, this._mapping[key])) keysToMap.push(key);
// if key exist in oldMap, move it to mapping
if (this._oldMap[key]) this._mapping[key] = this._oldMap[key];
// If this key isn't mapped, we need to map it
if (this.get(key) == null) keysToMap.push(key);
});
// Generate a color palette big enough that all new keys can have unique color values
const allColors = _(this._mapping).values().union(configColors).union(oldColors).value();
const allColors = _(this._mapping).values().value();
const colorPalette = this.colorPaletteFn(allColors.length + keysToMap.length);
let newColors = _.difference(colorPalette, allColors);

View file

@ -7,14 +7,12 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { PaletteDefinition } from '@kbn/coloring';
import { createColorPalette as createLegacyColorPalette } from '../..';
import { buildPalettes } from './palettes';
import { colorsServiceMock } from '../legacy_colors/mock';
import { euiPaletteColorBlind, euiPaletteColorBlindBehindText } from '@elastic/eui';
describe('palettes', () => {
const palettes: Record<string, PaletteDefinition> = buildPalettes(colorsServiceMock);
const palettes = buildPalettes();
describe('default palette', () => {
describe('syncColors: false', () => {
it('should return different colors based on behind text flag', () => {
@ -294,147 +292,6 @@ describe('palettes', () => {
});
});
describe('legacy palette', () => {
const palette = palettes.kibana_palette;
beforeEach(() => {
(colorsServiceMock.mappedColors.mapKeys as jest.Mock).mockClear();
(colorsServiceMock.mappedColors.getColorFromConfig as jest.Mock).mockReset();
(colorsServiceMock.mappedColors.get as jest.Mock).mockClear();
});
describe('syncColors: false', () => {
it('should not query legacy color service', () => {
palette.getCategoricalColor(
[
{
name: 'abc',
rankAtDepth: 0,
totalSeriesAtDepth: 10,
},
],
{
syncColors: false,
}
);
expect(colorsServiceMock.mappedColors.mapKeys).not.toHaveBeenCalled();
expect(colorsServiceMock.mappedColors.get).not.toHaveBeenCalled();
});
it('should respect the advanced settings color mapping', () => {
const configColorGetter = colorsServiceMock.mappedColors.getColorFromConfig as jest.Mock;
configColorGetter.mockImplementation(() => 'blue');
const result = palette.getCategoricalColor(
[
{
name: 'abc',
rankAtDepth: 2,
totalSeriesAtDepth: 10,
},
{
name: 'def',
rankAtDepth: 0,
totalSeriesAtDepth: 10,
},
],
{
syncColors: false,
}
);
expect(result).toEqual('blue');
expect(configColorGetter).toHaveBeenCalledWith('abc');
});
it('should return a color from the legacy palette based on position of first series', () => {
const result = palette.getCategoricalColor(
[
{
name: 'abc',
rankAtDepth: 2,
totalSeriesAtDepth: 10,
},
{
name: 'def',
rankAtDepth: 0,
totalSeriesAtDepth: 10,
},
],
{
syncColors: false,
}
);
expect(result).toEqual(createLegacyColorPalette(20)[2]);
});
});
describe('syncColors: true', () => {
it('should query legacy color service', () => {
palette.getCategoricalColor(
[
{
name: 'abc',
rankAtDepth: 0,
totalSeriesAtDepth: 10,
},
],
{
syncColors: true,
}
);
expect(colorsServiceMock.mappedColors.mapKeys).toHaveBeenCalledWith(['abc']);
expect(colorsServiceMock.mappedColors.get).toHaveBeenCalledWith('abc');
});
it('should respect the advanced settings color mapping', () => {
const configColorGetter = colorsServiceMock.mappedColors.getColorFromConfig as jest.Mock;
configColorGetter.mockImplementation(() => 'blue');
const result = palette.getCategoricalColor(
[
{
name: 'abc',
rankAtDepth: 2,
totalSeriesAtDepth: 10,
},
{
name: 'def',
rankAtDepth: 0,
totalSeriesAtDepth: 10,
},
],
{
syncColors: false,
}
);
expect(result).toEqual('blue');
expect(configColorGetter).toHaveBeenCalledWith('abc');
});
it('should always use root series', () => {
palette.getCategoricalColor(
[
{
name: 'abc',
rankAtDepth: 0,
totalSeriesAtDepth: 10,
},
{
name: 'def',
rankAtDepth: 0,
totalSeriesAtDepth: 10,
},
],
{
syncColors: true,
}
);
expect(colorsServiceMock.mappedColors.mapKeys).toHaveBeenCalledTimes(1);
expect(colorsServiceMock.mappedColors.mapKeys).toHaveBeenCalledWith(['abc']);
expect(colorsServiceMock.mappedColors.get).toHaveBeenCalledTimes(1);
expect(colorsServiceMock.mappedColors.get).toHaveBeenCalledWith('abc');
});
});
});
describe('custom palette', () => {
const palette = palettes.custom;
it('should return different colors based on rank at current series', () => {

View file

@ -23,19 +23,20 @@ import {
} from '@elastic/eui';
import type { ChartColorConfiguration, PaletteDefinition, SeriesLayer } from '@kbn/coloring';
import { flatten, zip } from 'lodash';
import { ChartsPluginSetup, createColorPalette as createLegacyColorPalette } from '../..';
import { createColorPalette as createLegacyColorPalette } from '../..';
import { lightenColor } from './lighten_color';
import { LegacyColorsService } from '../legacy_colors';
import { MappedColors } from '../mapped_colors';
import { workoutColorForValue } from './helpers';
function buildRoundRobinCategoricalWithMappedColors(): Omit<PaletteDefinition, 'title'> {
const colors = euiPaletteColorBlind({ rotations: 2 });
const behindTextColors = euiPaletteColorBlindBehindText({ rotations: 2 });
function buildRoundRobinCategoricalWithMappedColors(
id = 'default',
colors = euiPaletteColorBlind({ rotations: 2 }),
behindTextColors = euiPaletteColorBlindBehindText({ rotations: 2 })
): Omit<PaletteDefinition, 'title'> {
const behindTextColorMap: Record<string, string> = Object.fromEntries(
zip(colors, behindTextColors)
);
const mappedColors = new MappedColors(undefined, (num: number) => {
const mappedColors = new MappedColors((num: number) => {
return flatten(new Array(Math.ceil(num / 10)).fill(colors)).map((color) => color.toLowerCase());
});
function getColor(
@ -61,9 +62,9 @@ function buildRoundRobinCategoricalWithMappedColors(): Omit<PaletteDefinition, '
return lightenColor(outputColor, series.length, chartConfiguration.maxDepth);
}
return {
id: 'default',
id,
getCategoricalColor: getColor,
getCategoricalColors: () => euiPaletteColorBlind(),
getCategoricalColors: () => colors.slice(0, 10),
toExpression: () => ({
type: 'expression',
chain: [
@ -71,7 +72,7 @@ function buildRoundRobinCategoricalWithMappedColors(): Omit<PaletteDefinition, '
type: 'function',
function: 'system_palette',
arguments: {
name: ['default'],
name: [id],
},
},
],
@ -118,45 +119,6 @@ function buildGradient(
};
}
function buildSyncedKibanaPalette(
colors: ChartsPluginSetup['legacyColors']
): Omit<PaletteDefinition, 'title'> {
const staticColors = createLegacyColorPalette(20);
function getColor(series: SeriesLayer[], chartConfiguration: ChartColorConfiguration = {}) {
let outputColor: string;
if (chartConfiguration.syncColors) {
colors.mappedColors.mapKeys([series[0].name]);
outputColor = colors.mappedColors.get(series[0].name);
} else {
const configColor = colors.mappedColors.getColorFromConfig(series[0].name);
outputColor = configColor || staticColors[series[0].rankAtDepth % staticColors.length];
}
if (!chartConfiguration.maxDepth || chartConfiguration.maxDepth === 1) {
return outputColor;
}
return lightenColor(outputColor, series.length, chartConfiguration.maxDepth);
}
return {
id: 'kibana_palette',
getCategoricalColor: getColor,
getCategoricalColors: () => colors.seedColors.slice(0, 10),
toExpression: () => ({
type: 'expression',
chain: [
{
type: 'function',
function: 'system_palette',
arguments: {
name: ['kibana_palette'],
},
},
],
}),
};
}
function buildCustomPalette(): PaletteDefinition {
return {
id: 'custom',
@ -258,54 +220,50 @@ function buildCustomPalette(): PaletteDefinition {
} as PaletteDefinition<unknown>;
}
export const buildPalettes: (
legacyColorsService: LegacyColorsService
) => Record<string, PaletteDefinition> = (legacyColorsService) => {
return {
default: {
title: i18n.translate('charts.palettes.defaultPaletteLabel', { defaultMessage: 'Default' }),
...buildRoundRobinCategoricalWithMappedColors(),
},
status: {
title: i18n.translate('charts.palettes.statusLabel', { defaultMessage: 'Status' }),
...buildGradient('status', euiPaletteForStatus),
},
temperature: {
title: i18n.translate('charts.palettes.temperatureLabel', { defaultMessage: 'Temperature' }),
...buildGradient('temperature', euiPaletteForTemperature),
},
complementary: {
title: i18n.translate('charts.palettes.complementaryLabel', {
defaultMessage: 'Complementary',
}),
...buildGradient('complementary', euiPaletteComplementary),
},
negative: {
title: i18n.translate('charts.palettes.negativeLabel', { defaultMessage: 'Negative' }),
...buildGradient('negative', euiPaletteRed),
},
positive: {
title: i18n.translate('charts.palettes.positiveLabel', { defaultMessage: 'Positive' }),
...buildGradient('positive', euiPaletteGreen),
},
cool: {
title: i18n.translate('charts.palettes.coolLabel', { defaultMessage: 'Cool' }),
...buildGradient('cool', euiPaletteCool),
},
warm: {
title: i18n.translate('charts.palettes.warmLabel', { defaultMessage: 'Warm' }),
...buildGradient('warm', euiPaletteWarm),
},
gray: {
title: i18n.translate('charts.palettes.grayLabel', { defaultMessage: 'Gray' }),
...buildGradient('gray', euiPaletteGray),
},
kibana_palette: {
title: i18n.translate('charts.palettes.kibanaPaletteLabel', {
defaultMessage: 'Compatibility',
}),
...buildSyncedKibanaPalette(legacyColorsService),
},
custom: buildCustomPalette() as PaletteDefinition<unknown>,
};
};
export const buildPalettes = (): Record<string, PaletteDefinition> => ({
default: {
title: i18n.translate('charts.palettes.defaultPaletteLabel', { defaultMessage: 'Default' }),
...buildRoundRobinCategoricalWithMappedColors(),
},
status: {
title: i18n.translate('charts.palettes.statusLabel', { defaultMessage: 'Status' }),
...buildGradient('status', euiPaletteForStatus),
},
temperature: {
title: i18n.translate('charts.palettes.temperatureLabel', { defaultMessage: 'Temperature' }),
...buildGradient('temperature', euiPaletteForTemperature),
},
complementary: {
title: i18n.translate('charts.palettes.complementaryLabel', {
defaultMessage: 'Complementary',
}),
...buildGradient('complementary', euiPaletteComplementary),
},
negative: {
title: i18n.translate('charts.palettes.negativeLabel', { defaultMessage: 'Negative' }),
...buildGradient('negative', euiPaletteRed),
},
positive: {
title: i18n.translate('charts.palettes.positiveLabel', { defaultMessage: 'Positive' }),
...buildGradient('positive', euiPaletteGreen),
},
cool: {
title: i18n.translate('charts.palettes.coolLabel', { defaultMessage: 'Cool' }),
...buildGradient('cool', euiPaletteCool),
},
warm: {
title: i18n.translate('charts.palettes.warmLabel', { defaultMessage: 'Warm' }),
...buildGradient('warm', euiPaletteWarm),
},
gray: {
title: i18n.translate('charts.palettes.grayLabel', { defaultMessage: 'Gray' }),
...buildGradient('gray', euiPaletteGray),
},
kibana_palette: {
title: i18n.translate('charts.palettes.kibanaPaletteLabel', {
defaultMessage: 'Compatibility',
}),
...buildRoundRobinCategoricalWithMappedColors('kibana_palette', createLegacyColorPalette(20)),
},
custom: buildCustomPalette() as PaletteDefinition<unknown>,
});

View file

@ -11,7 +11,6 @@ import type { PaletteRegistry, PaletteDefinition } from '@kbn/coloring';
import { getActivePaletteName } from '@kbn/coloring';
import type { ExpressionsSetup } from '@kbn/expressions-plugin/public';
import type { ChartsPluginSetup } from '../..';
import type { LegacyColorsService } from '../legacy_colors';
export interface PaletteSetupPlugins {
expressions: ExpressionsSetup;
@ -22,12 +21,12 @@ export class PaletteService {
private palettes: Record<string, PaletteDefinition<unknown>> | undefined = undefined;
constructor() {}
public setup(colorsService: LegacyColorsService) {
public setup() {
return {
getPalettes: async (): Promise<PaletteRegistry> => {
if (!this.palettes) {
const { buildPalettes } = await import('./palettes');
this.palettes = buildPalettes(colorsService);
this.palettes = buildPalettes();
}
return {
get: (name: string) => {

View file

@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n';
import { schema } from '@kbn/config-schema';
import { CoreSetup, Plugin } from '@kbn/core/server';
import { ExpressionsServerSetup } from '@kbn/expressions-plugin/server';
import { COLOR_MAPPING_SETTING, LEGACY_TIME_AXIS, palette, systemPalette } from '../common';
import { LEGACY_TIME_AXIS, palette, systemPalette } from '../common';
interface SetupDependencies {
expressions: ExpressionsServerSetup;
@ -22,32 +22,6 @@ export class ChartsServerPlugin implements Plugin<object, object> {
dependencies.expressions.registerFunction(palette);
dependencies.expressions.registerFunction(systemPalette);
core.uiSettings.register({
[COLOR_MAPPING_SETTING]: {
name: i18n.translate('charts.advancedSettings.visualization.colorMappingTitle', {
defaultMessage: 'Color mapping',
}),
value: JSON.stringify({
Count: '#00A69B',
}),
type: 'json',
description: i18n.translate('charts.advancedSettings.visualization.colorMappingText', {
defaultMessage:
'Maps values to specific colors in charts using the <strong>Compatibility</strong> palette.',
values: { strong: (chunks) => `<strong>${chunks}</strong>` },
}),
deprecation: {
message: i18n.translate(
'charts.advancedSettings.visualization.colorMappingTextDeprecation',
{
defaultMessage:
'This setting is deprecated and will not be supported in a future version.',
}
),
docLinksKey: 'visualizationSettings',
},
category: ['visualization'],
schema: schema.string(),
},
[LEGACY_TIME_AXIS]: {
name: i18n.translate('charts.advancedSettings.visualization.useLegacyTimeAxis.name', {
defaultMessage: 'Legacy chart time axis',

View file

@ -62,10 +62,6 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
type: 'long',
_meta: { description: 'Non-default value of setting.' },
},
'visualization:colorMapping': {
type: 'text',
_meta: { description: 'Non-default value of setting.' },
},
'visualization:useLegacyTimeAxis': {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },

View file

@ -54,10 +54,15 @@ describe('telemetry_application_usage_collector', () => {
test('fetch()', async () => {
uiSettingsClient.getUserProvided.mockImplementationOnce(async () => ({
'visualization:colorMapping': { userValue: 'red' },
'timepicker:timeDefaults': {
userValue: {
from: 'now-7d',
to: 'now-6d',
},
},
}));
await expect(collector.fetch(mockedFetchContext)).resolves.toEqual({
'visualization:colorMapping': 'red',
'timepicker:timeDefaults': { from: 'now-7d', to: 'now-6d' },
});
});

View file

@ -58,7 +58,6 @@ export interface UsageStats {
'observability:aiAssistantSimulatedFunctionCalling': boolean;
'observability:aiAssistantSearchConnectorIndexPattern': string;
'visualization:heatmap:maxBuckets': number;
'visualization:colorMapping': string;
'visualization:useLegacyTimeAxis': boolean;
'visualization:regionmap:showWarnings': boolean;
'visualization:tileMap:maxPrecision': number;

View file

@ -9960,12 +9960,6 @@
"description": "Non-default value of setting."
}
},
"visualization:colorMapping": {
"type": "text",
"_meta": {
"description": "Non-default value of setting."
}
},
"visualization:useLegacyTimeAxis": {
"type": "boolean",
"_meta": {

View file

@ -45,15 +45,11 @@ class D3MappableObject {
* @param attr {Object|*} Visualization options
*/
export class Data {
constructor(data, uiState, createColorLookupFunction) {
constructor(data, uiState) {
this.uiState = uiState;
this.createColorLookupFunction = createColorLookupFunction;
this.data = this.copyDataObj(data);
this.type = this.getDataType();
this.labels = this._getLabels(this.data);
this.color = this.labels
? createColorLookupFunction(this.labels, uiState.get('vis.colors'))
: undefined;
this._normalizeOrdered();
}
@ -385,7 +381,7 @@ export class Data {
const defaultColors = this.uiState.get('vis.defaultColors');
const overwriteColors = this.uiState.get('vis.colors');
const colors = defaultColors ? _.defaults({}, overwriteColors, defaultColors) : overwriteColors;
return this.createColorLookupFunction(this.getLabels(), colors);
return (value) => colors[value];
}
/**

View file

@ -25,8 +25,8 @@ const DEFAULT_VIS_CONFIG = {
};
export class VisConfig {
constructor(visConfigArgs, data, uiState, el, createColorLookupFunction) {
this.data = new Data(data, uiState, createColorLookupFunction);
constructor(visConfigArgs, data, uiState, el) {
this.data = new Data(data, uiState);
const visType = visTypes[visConfigArgs.type];
const typeDefaults = visType(visConfigArgs, this.data);

View file

@ -41,13 +41,7 @@ export class Vis extends EventEmitter {
initVisConfig(data, uiState) {
this.data = data;
this.uiState = uiState;
this.visConfig = new VisConfig(
this.visConfigArgs,
this.data,
this.uiState,
this.element,
this.charts.legacyColors.createColorLookupFunction.bind(this.charts.legacyColors)
);
this.visConfig = new VisConfig(this.visConfigArgs, this.data, this.uiState, this.element);
}
/**

View file

@ -406,9 +406,6 @@
"cellActions.extraActionsAriaLabel": "Actions supplémentaires",
"cellActions.showMoreActionsLabel": "Plus d'actions",
"cellActions.youAreInADialogContainingOptionsScreenReaderOnly": "Vous êtes dans une boîte de dialogue contenant des options pour le champ {fieldName}. Appuyez sur Tab pour naviguer entre les options. Appuyez sur Échap pour quitter.",
"charts.advancedSettings.visualization.colorMappingText": "Mappe des valeurs à des couleurs spécifiques dans les graphiques avec la palette <strong>Compatibilité</strong>.",
"charts.advancedSettings.visualization.colorMappingTextDeprecation": "Ce paramètre est déclassé et ne sera plus compatible avec les futures versions.",
"charts.advancedSettings.visualization.colorMappingTitle": "Mapping des couleurs",
"charts.advancedSettings.visualization.useLegacyTimeAxis.deprecation": "Ce paramètre est déclassé et ne sera plus compatible avec les futures versions.",
"charts.advancedSettings.visualization.useLegacyTimeAxis.description": "Active l'axe de temps hérité pour les graphiques dans Lens, Discover, Visualize et TSVB",
"charts.advancedSettings.visualization.useLegacyTimeAxis.name": "Axe de temps hérité pour les graphiques",

View file

@ -406,9 +406,6 @@
"cellActions.extraActionsAriaLabel": "追加のアクション",
"cellActions.showMoreActionsLabel": "さらにアクションを表示",
"cellActions.youAreInADialogContainingOptionsScreenReaderOnly": "フィールド {fieldName} のオプションを含む、ダイアログを表示しています。Tab を押すと、オプションを操作します。Escapeを押すと、終了します。",
"charts.advancedSettings.visualization.colorMappingText": "<strong>互換性</strong>パレットを使用して、グラフで値を特定の色にマッピングします。",
"charts.advancedSettings.visualization.colorMappingTextDeprecation": "この設定はサポートが終了し、将来のバージョンではサポートされません。",
"charts.advancedSettings.visualization.colorMappingTitle": "カラーマッピング",
"charts.advancedSettings.visualization.useLegacyTimeAxis.deprecation": "この設定はサポートが終了し、将来のバージョンではサポートされません。",
"charts.advancedSettings.visualization.useLegacyTimeAxis.description": "Lens、Discover、Visualize、およびTSVBでグラフのレガシー時間軸を有効にします",
"charts.advancedSettings.visualization.useLegacyTimeAxis.name": "レガシーグラフ時間軸",

View file

@ -402,9 +402,6 @@
"cellActions.extraActionsAriaLabel": "附加操作",
"cellActions.showMoreActionsLabel": "更多操作",
"cellActions.youAreInADialogContainingOptionsScreenReaderOnly": "您在对话框中,其中包含 {fieldName} 字段的选项。按 tab 键导航选项。按 escape 退出。",
"charts.advancedSettings.visualization.colorMappingText": "使用<strong>兼容性</strong>调色板将值映射到图表中的特定颜色。",
"charts.advancedSettings.visualization.colorMappingTextDeprecation": "此设置已过时,在未来版本中将不受支持。",
"charts.advancedSettings.visualization.colorMappingTitle": "颜色映射",
"charts.advancedSettings.visualization.useLegacyTimeAxis.deprecation": "此设置已过时,在未来版本中将不受支持。",
"charts.advancedSettings.visualization.useLegacyTimeAxis.description": "在 Lens、Discover、Visualize 和 TSVB 中为图表启用旧版时间轴",
"charts.advancedSettings.visualization.useLegacyTimeAxis.name": "旧版图表时间轴",