mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Lens] Color mapping for categorical dimensions (#162389)
## Summary This PR introduces the new color mapping feature into Lens. The color mapping feature is introduced as a standalone sharable component available from `@kbn/coloring`. The [README.md](ddd216457d/packages/kbn-coloring/src/shared_components/color_mapping/README.md
) file describes the components and the logic behind it. The Color Mapping component is also connected to Lens and is available in the following charts: - XY (you can specify the mappings from a breakdown dimension - Partition (you can specify the mappings from the main slice/group by dimension) - Tag cloud (you can specify the mappings from the tags dimension) This MVP feature will be released under the Tech Preview flag. This PR needs to prove the user experience and the ease of use. UI styles, design improvements and embellishments will be released in subsequent PRs. The current MVP-provided palettes are just a placeholder. I'm coordinating with @gvnmagni for a final set of palettes. close https://github.com/elastic/kibana/issues/155037 close https://github.com/elastic/kibana/issues/6480 fix https://github.com/elastic/kibana/issues/28618 fix https://github.com/elastic/kibana/issues/96044 fix https://github.com/elastic/kibana/issues/101942 fix https://github.com/elastic/kibana/issues/112839 fix https://github.com/elastic/kibana/issues/116634 ## Release note This feature introduces the ability to change and map colors to break down dimensions in Lens. The feature provides an improved way to specify colors and their association with categories by giving the user a predefined set of color choices or customized one that drives the user toward a correct color selection. It provides ways to pick new colors and generate gradients. This feature is in Tech Preview and is enabled by default on every new visualization but can be turned off at will. 
This commit is contained in:
parent
6ab0c68ae6
commit
b12a42261b
132 changed files with 5209 additions and 215 deletions
|
@ -0,0 +1,87 @@
|
|||
# Color Mapping
|
||||
|
||||
This shared component can be used to define a color mapping as an association of one or multiple string values to a color definition.
|
||||
|
||||
This package provides:
|
||||
- a React component, called `CategoricalColorMapping` that provides a simplified UI (that in general can be hosted in a flyout), that helps the user generate a `ColorMapping.Config` object that descibes the mappings configuration
|
||||
- a function `getColorFactory` that given a color mapping configuration returns a function that maps a passed category to the corresponding color
|
||||
- a definition scheme for the color mapping, based on the type `ColorMapping.Config`, that provides an extensible way of describing the link between colors and rules. Collects the minimal information required apply colors based on categories. Together with the `ColorMappingInputData` can be used to get colors in a deterministic way.
|
||||
|
||||
|
||||
An example of the configuration is the following:
|
||||
```ts
|
||||
const DEFAULT_COLOR_MAPPING_CONFIG: ColorMapping.Config = {
|
||||
assignmentMode: 'auto',
|
||||
assignments: [
|
||||
{
|
||||
rule: {
|
||||
type: 'matchExactly',
|
||||
values: [''];
|
||||
},
|
||||
color: {
|
||||
type: 'categorical',
|
||||
paletteId: 'eui',
|
||||
colorIndex: 2,
|
||||
}
|
||||
}
|
||||
],
|
||||
specialAssignments: [
|
||||
{
|
||||
rule: {
|
||||
type: 'other',
|
||||
},
|
||||
color: {
|
||||
type: 'categorical',
|
||||
paletteId: 'neutral',
|
||||
colorIndex: 2
|
||||
},
|
||||
touched: false,
|
||||
},
|
||||
],
|
||||
paletteId: EUIPalette.id,
|
||||
colorMode: {
|
||||
type: 'categorical',
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
The function `getColorFactory` is a curry function where, given the model, a palette getter, the theme mode (dark/light) and a list of categories, returns a function that can be used to pick the right color based on a given category.
|
||||
|
||||
```ts
|
||||
function getColorFactory(
|
||||
model: ColorMapping.Config,
|
||||
getPaletteFn: (paletteId: string) => ColorMapping.CategoricalPalette,
|
||||
isDarkMode: boolean,
|
||||
data: {
|
||||
type: 'categories';
|
||||
categories: Array<string | string[]>;
|
||||
}
|
||||
): (category: string | string[]) => Color
|
||||
```
|
||||
|
||||
|
||||
|
||||
A `category` can be in the shape of a plain string or an array of strings. Numbers, MultiFieldKey, IP etc needs to be stringified.
|
||||
|
||||
|
||||
The `CategoricalColorMapping` React component has the following props:
|
||||
|
||||
```tsx
|
||||
function CategoricalColorMapping(props: {
|
||||
/** The initial color mapping model, usually coming from a the visualization saved object */
|
||||
model: ColorMapping.Config;
|
||||
/** A map of paletteId and palette configuration */
|
||||
palettes: Map<string, ColorMapping.CategoricalPalette>;
|
||||
/** A data description of what needs to be colored */
|
||||
data: ColorMappingInputData;
|
||||
/** Theme dark mode */
|
||||
isDarkMode: boolean;
|
||||
/** A map between original and formatted tokens used to handle special cases, like the Other bucket and the empty bucket */
|
||||
specialTokens: Map<string, string>;
|
||||
/** A function called at every change in the model */
|
||||
onModelUpdate: (model: ColorMapping.Config) => void;
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
the `onModelUpdate` callback is called everytime a change in the model is applied from within the component. Is not called when the `model` prop is updated.
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { EuiFlyout, EuiForm } from '@elastic/eui';
|
||||
import { ComponentStory } from '@storybook/react';
|
||||
import { CategoricalColorMapping, ColorMappingProps } from '../categorical_color_mapping';
|
||||
import { AVAILABLE_PALETTES } from '../palettes';
|
||||
import { DEFAULT_COLOR_MAPPING_CONFIG } from '../config/default_color_mapping';
|
||||
|
||||
export default {
|
||||
title: 'Color Mapping',
|
||||
component: CategoricalColorMapping,
|
||||
decorators: [
|
||||
(story: Function) => (
|
||||
<EuiFlyout style={{ width: 350, padding: '8px' }} onClose={() => {}} hideCloseButton>
|
||||
<EuiForm>{story()}</EuiForm>
|
||||
</EuiFlyout>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
const Template: ComponentStory<FC<ColorMappingProps>> = (args) => (
|
||||
<CategoricalColorMapping {...args} />
|
||||
);
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
||||
Default.args = {
|
||||
model: {
|
||||
...DEFAULT_COLOR_MAPPING_CONFIG,
|
||||
assignmentMode: 'manual',
|
||||
colorMode: {
|
||||
type: 'gradient',
|
||||
steps: [
|
||||
{
|
||||
type: 'categorical',
|
||||
colorIndex: 0,
|
||||
paletteId: DEFAULT_COLOR_MAPPING_CONFIG.paletteId,
|
||||
touched: false,
|
||||
},
|
||||
{
|
||||
type: 'categorical',
|
||||
colorIndex: 1,
|
||||
paletteId: DEFAULT_COLOR_MAPPING_CONFIG.paletteId,
|
||||
touched: false,
|
||||
},
|
||||
{
|
||||
type: 'categorical',
|
||||
colorIndex: 2,
|
||||
paletteId: DEFAULT_COLOR_MAPPING_CONFIG.paletteId,
|
||||
touched: false,
|
||||
},
|
||||
],
|
||||
sort: 'asc',
|
||||
},
|
||||
assignments: [
|
||||
{
|
||||
rule: {
|
||||
type: 'matchExactly',
|
||||
values: ['this is', 'a multi-line combobox that is very long and that will be truncated'],
|
||||
},
|
||||
color: {
|
||||
type: 'gradient',
|
||||
},
|
||||
touched: false,
|
||||
},
|
||||
{
|
||||
rule: {
|
||||
type: 'matchExactly',
|
||||
values: ['b', ['double', 'value']],
|
||||
},
|
||||
color: {
|
||||
type: 'gradient',
|
||||
},
|
||||
touched: false,
|
||||
},
|
||||
{
|
||||
rule: {
|
||||
type: 'matchExactly',
|
||||
values: ['c'],
|
||||
},
|
||||
color: {
|
||||
type: 'gradient',
|
||||
},
|
||||
touched: false,
|
||||
},
|
||||
{
|
||||
rule: {
|
||||
type: 'matchExactly',
|
||||
values: [
|
||||
'this is',
|
||||
'a multi-line wrap',
|
||||
'combo box',
|
||||
'test combo',
|
||||
'3 lines',
|
||||
['double', 'value'],
|
||||
],
|
||||
},
|
||||
color: {
|
||||
type: 'gradient',
|
||||
},
|
||||
touched: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
isDarkMode: false,
|
||||
data: {
|
||||
type: 'categories',
|
||||
categories: [
|
||||
'a',
|
||||
'b',
|
||||
'c',
|
||||
'd',
|
||||
'this is',
|
||||
'a multi-line wrap',
|
||||
'combo box',
|
||||
'test combo',
|
||||
'3 lines',
|
||||
],
|
||||
},
|
||||
|
||||
palettes: AVAILABLE_PALETTES,
|
||||
specialTokens: new Map(),
|
||||
// eslint-disable-next-line no-console
|
||||
onModelUpdate: (model) => console.log(model),
|
||||
};
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { CategoricalColorMapping, ColorMappingInputData } from './categorical_color_mapping';
|
||||
import { AVAILABLE_PALETTES } from './palettes';
|
||||
import { DEFAULT_COLOR_MAPPING_CONFIG } from './config/default_color_mapping';
|
||||
import { MULTI_FIELD_KEY_SEPARATOR } from '@kbn/data-plugin/common';
|
||||
|
||||
const AUTO_ASSIGN_SWITCH = '[data-test-subj="lns-colorMapping-autoAssignSwitch"]';
|
||||
const ASSIGNMENTS_LIST = '[data-test-subj="lns-colorMapping-assignmentsList"]';
|
||||
const ASSIGNMENT_ITEM = (i: number) => `[data-test-subj="lns-colorMapping-assignmentsItem${i}"]`;
|
||||
|
||||
describe('color mapping', () => {
|
||||
it('load a default color mapping', () => {
|
||||
const dataInput: ColorMappingInputData = {
|
||||
type: 'categories',
|
||||
categories: ['categoryA', 'categoryB'],
|
||||
};
|
||||
const onModelUpdateFn = jest.fn();
|
||||
const component = mount(
|
||||
<CategoricalColorMapping
|
||||
data={dataInput}
|
||||
isDarkMode={false}
|
||||
model={{ ...DEFAULT_COLOR_MAPPING_CONFIG }}
|
||||
palettes={AVAILABLE_PALETTES}
|
||||
onModelUpdate={onModelUpdateFn}
|
||||
specialTokens={new Map()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find(AUTO_ASSIGN_SWITCH).hostNodes().prop('aria-checked')).toEqual(true);
|
||||
expect(component.find(ASSIGNMENTS_LIST).hostNodes().children().length).toEqual(
|
||||
dataInput.categories.length
|
||||
);
|
||||
dataInput.categories.forEach((category, index) => {
|
||||
const assignment = component.find(ASSIGNMENT_ITEM(index)).hostNodes();
|
||||
expect(assignment.text()).toEqual(category);
|
||||
expect(assignment.hasClass('euiComboBox-isDisabled')).toEqual(true);
|
||||
});
|
||||
expect(onModelUpdateFn).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('switch to manual assignments', () => {
|
||||
const dataInput: ColorMappingInputData = {
|
||||
type: 'categories',
|
||||
categories: ['categoryA', 'categoryB'],
|
||||
};
|
||||
const onModelUpdateFn = jest.fn();
|
||||
const component = mount(
|
||||
<CategoricalColorMapping
|
||||
data={dataInput}
|
||||
isDarkMode={false}
|
||||
model={{ ...DEFAULT_COLOR_MAPPING_CONFIG }}
|
||||
palettes={AVAILABLE_PALETTES}
|
||||
onModelUpdate={onModelUpdateFn}
|
||||
specialTokens={new Map()}
|
||||
/>
|
||||
);
|
||||
component.find(AUTO_ASSIGN_SWITCH).hostNodes().simulate('click');
|
||||
expect(onModelUpdateFn).toBeCalledTimes(1);
|
||||
expect(component.find(AUTO_ASSIGN_SWITCH).hostNodes().prop('aria-checked')).toEqual(false);
|
||||
expect(component.find(ASSIGNMENTS_LIST).hostNodes().children().length).toEqual(
|
||||
dataInput.categories.length
|
||||
);
|
||||
dataInput.categories.forEach((category, index) => {
|
||||
const assignment = component.find(ASSIGNMENT_ITEM(index)).hostNodes();
|
||||
expect(assignment.text()).toEqual(category);
|
||||
expect(assignment.hasClass('euiComboBox-isDisabled')).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('handle special tokens, multi-fields keys and non-trimmed whitespaces', () => {
|
||||
const dataInput: ColorMappingInputData = {
|
||||
type: 'categories',
|
||||
categories: ['__other__', ['fieldA', 'fieldB'], '__empty__', ' with-whitespaces '],
|
||||
};
|
||||
const onModelUpdateFn = jest.fn();
|
||||
const component = mount(
|
||||
<CategoricalColorMapping
|
||||
data={dataInput}
|
||||
isDarkMode={false}
|
||||
model={{ ...DEFAULT_COLOR_MAPPING_CONFIG }}
|
||||
palettes={AVAILABLE_PALETTES}
|
||||
onModelUpdate={onModelUpdateFn}
|
||||
specialTokens={
|
||||
new Map([
|
||||
['__other__', 'Other'],
|
||||
['__empty__', '(Empty)'],
|
||||
])
|
||||
}
|
||||
/>
|
||||
);
|
||||
expect(component.find(ASSIGNMENTS_LIST).hostNodes().children().length).toEqual(
|
||||
dataInput.categories.length
|
||||
);
|
||||
const assignment1 = component.find(ASSIGNMENT_ITEM(0)).hostNodes();
|
||||
expect(assignment1.text()).toEqual('Other');
|
||||
|
||||
const assignment2 = component.find(ASSIGNMENT_ITEM(1)).hostNodes();
|
||||
expect(assignment2.text()).toEqual(`fieldA${MULTI_FIELD_KEY_SEPARATOR}fieldB`);
|
||||
|
||||
const assignment3 = component.find(ASSIGNMENT_ITEM(2)).hostNodes();
|
||||
expect(assignment3.text()).toEqual('(Empty)');
|
||||
|
||||
const assignment4 = component.find(ASSIGNMENT_ITEM(3)).hostNodes();
|
||||
expect(assignment4.text()).toEqual(' with-whitespaces ');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { type EnhancedStore, configureStore } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { colorMappingReducer, updateModel } from './state/color_mapping';
|
||||
import { Container } from './components/container/container';
|
||||
import { ColorMapping } from './config';
|
||||
import { uiReducer } from './state/ui';
|
||||
|
||||
/**
|
||||
* A configuration object that is required to populate correctly the visible categories
|
||||
* or the ranges in the CategoricalColorMapping component
|
||||
*/
|
||||
export type ColorMappingInputData =
|
||||
| {
|
||||
type: 'categories';
|
||||
/** an ORDERED array of categories rendered in the visualization */
|
||||
categories: Array<string | string[]>;
|
||||
}
|
||||
| {
|
||||
type: 'ranges';
|
||||
min: number;
|
||||
max: number;
|
||||
bins: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* The props of the CategoricalColorMapping component
|
||||
*/
|
||||
export interface ColorMappingProps {
|
||||
/** The initial color mapping model, usually coming from a the visualization saved object */
|
||||
model: ColorMapping.Config;
|
||||
/** A map of paletteId and palette configuration */
|
||||
palettes: Map<string, ColorMapping.CategoricalPalette>;
|
||||
/** A data description of what needs to be colored */
|
||||
data: ColorMappingInputData;
|
||||
/** Theme dark mode */
|
||||
isDarkMode: boolean;
|
||||
/** A map between original and formatted tokens used to handle special cases, like the Other bucket and the empty bucket */
|
||||
specialTokens: Map<string, string>;
|
||||
/** A function called at every change in the model */
|
||||
onModelUpdate: (model: ColorMapping.Config) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The React component for mapping categorical values to colors
|
||||
*/
|
||||
export class CategoricalColorMapping extends React.Component<ColorMappingProps> {
|
||||
store: EnhancedStore<{ colorMapping: ColorMapping.Config }>;
|
||||
unsubscribe: () => void;
|
||||
constructor(props: ColorMappingProps) {
|
||||
super(props);
|
||||
// configure the store at mount time
|
||||
this.store = configureStore({
|
||||
preloadedState: {
|
||||
colorMapping: props.model,
|
||||
},
|
||||
reducer: {
|
||||
colorMapping: colorMappingReducer,
|
||||
ui: uiReducer,
|
||||
},
|
||||
});
|
||||
// subscribe to store changes to update external tools
|
||||
this.unsubscribe = this.store.subscribe(() => {
|
||||
this.props.onModelUpdate(this.store.getState().colorMapping);
|
||||
});
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.unsubscribe();
|
||||
}
|
||||
componentDidUpdate(prevProps: Readonly<ColorMappingProps>) {
|
||||
if (!isEqual(prevProps.model, this.props.model)) {
|
||||
this.store.dispatch(updateModel(this.props.model));
|
||||
}
|
||||
}
|
||||
render() {
|
||||
const { palettes, data, isDarkMode, specialTokens } = this.props;
|
||||
return (
|
||||
<Provider store={this.store}>
|
||||
<Container
|
||||
palettes={palettes}
|
||||
data={data}
|
||||
isDarkMode={isDarkMode}
|
||||
specialTokens={specialTokens}
|
||||
/>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,294 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import {
|
||||
DEFAULT_COLOR_MAPPING_CONFIG,
|
||||
DEFAULT_NEUTRAL_PALETTE_INDEX,
|
||||
} from '../config/default_color_mapping';
|
||||
import { getColorFactory } from './color_handling';
|
||||
import { getPalette, AVAILABLE_PALETTES } from '../palettes';
|
||||
import {
|
||||
EUIAmsterdamColorBlindPalette,
|
||||
EUI_AMSTERDAM_PALETTE_COLORS,
|
||||
} from '../palettes/eui_amsterdam';
|
||||
import { NeutralPalette, NEUTRAL_COLOR_DARK, NEUTRAL_COLOR_LIGHT } from '../palettes/neutral';
|
||||
import { toHex } from './color_math';
|
||||
|
||||
import { ColorMapping } from '../config';
|
||||
|
||||
describe('Color mapping - color generation', () => {
|
||||
const getPaletteFn = getPalette(AVAILABLE_PALETTES, NeutralPalette);
|
||||
it('returns EUI light colors from default config', () => {
|
||||
const colorFactory = getColorFactory(DEFAULT_COLOR_MAPPING_CONFIG, getPaletteFn, false, {
|
||||
type: 'categories',
|
||||
categories: ['catA', 'catB', 'catC'],
|
||||
});
|
||||
expect(colorFactory('catA')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]);
|
||||
expect(colorFactory('catB')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[1]);
|
||||
expect(colorFactory('catC')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[2]);
|
||||
// if the category is not available in the `categories` list then a default neutral color is used
|
||||
expect(colorFactory('not_available')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]);
|
||||
});
|
||||
|
||||
it('returns max number of colors defined in palette, use other color otherwise', () => {
|
||||
const twoColorPalette: ColorMapping.CategoricalPalette = {
|
||||
id: 'twoColors',
|
||||
name: 'twoColors',
|
||||
colorCount: 2,
|
||||
type: 'categorical',
|
||||
getColor(valueInRange, isDarkMode) {
|
||||
return ['red', 'blue'][valueInRange];
|
||||
},
|
||||
};
|
||||
|
||||
const simplifiedGetPaletteGn = getPalette(
|
||||
new Map([[twoColorPalette.id, twoColorPalette]]),
|
||||
NeutralPalette
|
||||
);
|
||||
const colorFactory = getColorFactory(
|
||||
{
|
||||
...DEFAULT_COLOR_MAPPING_CONFIG,
|
||||
paletteId: twoColorPalette.id,
|
||||
},
|
||||
simplifiedGetPaletteGn,
|
||||
false,
|
||||
{
|
||||
type: 'categories',
|
||||
categories: ['cat1', 'cat2', 'cat3', 'cat4'],
|
||||
}
|
||||
);
|
||||
expect(colorFactory('cat1')).toBe('#ff0000');
|
||||
expect(colorFactory('cat2')).toBe('#0000ff');
|
||||
// return a palette color only up to the max number of color in the palette
|
||||
expect(colorFactory('cat3')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]);
|
||||
expect(colorFactory('cat4')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]);
|
||||
});
|
||||
|
||||
// currently there is no difference in the two colors, but this could change in the future
|
||||
// this test will catch the change
|
||||
it('returns EUI dark colors from default config', () => {
|
||||
const colorFactory = getColorFactory(DEFAULT_COLOR_MAPPING_CONFIG, getPaletteFn, true, {
|
||||
type: 'categories',
|
||||
categories: ['catA', 'catB', 'catC'],
|
||||
});
|
||||
expect(colorFactory('catA')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]);
|
||||
expect(colorFactory('catB')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[1]);
|
||||
expect(colorFactory('catC')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[2]);
|
||||
// if the category is not available in the `categories` list then a default neutral color is used
|
||||
expect(colorFactory('not_available')).toBe(NEUTRAL_COLOR_DARK[DEFAULT_NEUTRAL_PALETTE_INDEX]);
|
||||
});
|
||||
|
||||
it('handles special tokens, multi-field categories and non-trimmed whitespaces', () => {
|
||||
const colorFactory = getColorFactory(DEFAULT_COLOR_MAPPING_CONFIG, getPaletteFn, false, {
|
||||
type: 'categories',
|
||||
categories: ['__other__', ['fieldA', 'fieldB'], '__empty__', ' with-whitespaces '],
|
||||
});
|
||||
expect(colorFactory('__other__')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]);
|
||||
expect(colorFactory(['fieldA', 'fieldB'])).toBe(EUI_AMSTERDAM_PALETTE_COLORS[1]);
|
||||
expect(colorFactory('__empty__')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[2]);
|
||||
expect(colorFactory(' with-whitespaces ')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[3]);
|
||||
});
|
||||
|
||||
it('ignores configured assignments in auto mode', () => {
|
||||
const colorFactory = getColorFactory(
|
||||
{
|
||||
...DEFAULT_COLOR_MAPPING_CONFIG,
|
||||
assignments: [
|
||||
{
|
||||
color: { type: 'colorCode', colorCode: 'red' },
|
||||
rule: { type: 'matchExactly', values: ['assignmentToIgnore'] },
|
||||
touched: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
getPaletteFn,
|
||||
false,
|
||||
{
|
||||
type: 'categories',
|
||||
categories: ['catA', 'catB', 'assignmentToIgnore'],
|
||||
}
|
||||
);
|
||||
expect(colorFactory('catA')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[0]);
|
||||
expect(colorFactory('catB')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[1]);
|
||||
expect(colorFactory('assignmentToIgnore')).toBe(EUI_AMSTERDAM_PALETTE_COLORS[2]);
|
||||
});
|
||||
|
||||
it('color with auto rule are assigned in order of the configured data input', () => {
|
||||
const colorFactory = getColorFactory(
|
||||
{
|
||||
...DEFAULT_COLOR_MAPPING_CONFIG,
|
||||
assignmentMode: 'manual',
|
||||
assignments: [
|
||||
{
|
||||
color: { type: 'colorCode', colorCode: 'red' },
|
||||
rule: { type: 'auto' },
|
||||
touched: false,
|
||||
},
|
||||
{
|
||||
color: { type: 'colorCode', colorCode: 'blue' },
|
||||
rule: { type: 'matchExactly', values: ['blueCat'] },
|
||||
touched: false,
|
||||
},
|
||||
{
|
||||
color: { type: 'colorCode', colorCode: 'green' },
|
||||
rule: { type: 'auto' },
|
||||
touched: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
getPaletteFn,
|
||||
false,
|
||||
{
|
||||
type: 'categories',
|
||||
categories: ['blueCat', 'redCat', 'greenCat'],
|
||||
}
|
||||
);
|
||||
// this matches exactly
|
||||
expect(colorFactory('blueCat')).toBe('blue');
|
||||
// this matches with the first availabe "auto" rule
|
||||
expect(colorFactory('redCat')).toBe('red');
|
||||
// this matches with the second availabe "auto" rule
|
||||
expect(colorFactory('greenCat')).toBe('green');
|
||||
// if the category is not available in the `categories` list then a default neutral color is used
|
||||
expect(colorFactory('not_available')).toBe(NEUTRAL_COLOR_LIGHT[DEFAULT_NEUTRAL_PALETTE_INDEX]);
|
||||
});
|
||||
|
||||
it('returns sequential gradient colors from darker to lighter [desc, lightMode]', () => {
|
||||
const colorFactory = getColorFactory(
|
||||
{
|
||||
...DEFAULT_COLOR_MAPPING_CONFIG,
|
||||
colorMode: {
|
||||
type: 'gradient',
|
||||
steps: [
|
||||
{
|
||||
type: 'categorical',
|
||||
paletteId: EUIAmsterdamColorBlindPalette.id,
|
||||
colorIndex: 0,
|
||||
touched: false,
|
||||
},
|
||||
],
|
||||
sort: 'desc',
|
||||
},
|
||||
},
|
||||
getPaletteFn,
|
||||
false,
|
||||
{
|
||||
type: 'categories',
|
||||
categories: ['cat1', 'cat2', 'cat3'],
|
||||
}
|
||||
);
|
||||
// this matches exactly with the initial step selected
|
||||
expect(toHex(colorFactory('cat1'))).toBe(toHex(EUI_AMSTERDAM_PALETTE_COLORS[0]));
|
||||
expect(toHex(colorFactory('cat2'))).toBe('#93cebc');
|
||||
expect(toHex(colorFactory('cat3'))).toBe('#cce8e0');
|
||||
});
|
||||
|
||||
it('returns sequential gradient colors from lighter to darker [asc, lightMode]', () => {
|
||||
const colorFactory = getColorFactory(
|
||||
{
|
||||
...DEFAULT_COLOR_MAPPING_CONFIG,
|
||||
colorMode: {
|
||||
type: 'gradient',
|
||||
steps: [
|
||||
{
|
||||
type: 'categorical',
|
||||
paletteId: EUIAmsterdamColorBlindPalette.id,
|
||||
colorIndex: 0,
|
||||
touched: false,
|
||||
},
|
||||
],
|
||||
sort: 'asc',
|
||||
},
|
||||
},
|
||||
getPaletteFn,
|
||||
false,
|
||||
{
|
||||
type: 'categories',
|
||||
categories: ['cat1', 'cat2', 'cat3'],
|
||||
}
|
||||
);
|
||||
expect(toHex(colorFactory('cat1'))).toBe('#cce8e0');
|
||||
expect(toHex(colorFactory('cat2'))).toBe('#93cebc');
|
||||
// this matches exactly with the initial step selected
|
||||
expect(toHex(colorFactory('cat3'))).toBe(toHex(EUI_AMSTERDAM_PALETTE_COLORS[0]));
|
||||
});
|
||||
|
||||
it('returns 2 colors gradient [desc, lightMode]', () => {
|
||||
const colorFactory = getColorFactory(
|
||||
{
|
||||
...DEFAULT_COLOR_MAPPING_CONFIG,
|
||||
colorMode: {
|
||||
type: 'gradient',
|
||||
steps: [
|
||||
{
|
||||
type: 'categorical',
|
||||
paletteId: EUIAmsterdamColorBlindPalette.id,
|
||||
colorIndex: 0,
|
||||
touched: false,
|
||||
},
|
||||
{
|
||||
type: 'categorical',
|
||||
paletteId: EUIAmsterdamColorBlindPalette.id,
|
||||
colorIndex: 2,
|
||||
touched: false,
|
||||
},
|
||||
],
|
||||
sort: 'desc',
|
||||
},
|
||||
},
|
||||
getPaletteFn,
|
||||
false,
|
||||
{
|
||||
type: 'categories',
|
||||
categories: ['cat1', 'cat2', 'cat3'],
|
||||
}
|
||||
);
|
||||
expect(toHex(colorFactory('cat1'))).toBe(toHex(EUI_AMSTERDAM_PALETTE_COLORS[0])); // EUI green
|
||||
expect(toHex(colorFactory('cat2'))).toBe('#a4908f'); // red gray green
|
||||
expect(toHex(colorFactory('cat3'))).toBe(toHex(EUI_AMSTERDAM_PALETTE_COLORS[2])); // EUI pink
|
||||
});
|
||||
|
||||
it('returns divergent gradient [asc, darkMode]', () => {
|
||||
const colorFactory = getColorFactory(
|
||||
{
|
||||
...DEFAULT_COLOR_MAPPING_CONFIG,
|
||||
colorMode: {
|
||||
type: 'gradient',
|
||||
steps: [
|
||||
{
|
||||
type: 'categorical',
|
||||
paletteId: EUIAmsterdamColorBlindPalette.id,
|
||||
colorIndex: 0,
|
||||
touched: false,
|
||||
},
|
||||
{ type: 'categorical', paletteId: NeutralPalette.id, colorIndex: 0, touched: false },
|
||||
{
|
||||
type: 'categorical',
|
||||
paletteId: EUIAmsterdamColorBlindPalette.id,
|
||||
colorIndex: 2,
|
||||
touched: false,
|
||||
},
|
||||
],
|
||||
sort: 'asc', // testing in ascending order
|
||||
},
|
||||
},
|
||||
getPaletteFn,
|
||||
true, // testing in dark mode
|
||||
{
|
||||
type: 'categories',
|
||||
categories: ['cat1', 'cat2', 'cat3'],
|
||||
}
|
||||
);
|
||||
expect(toHex(colorFactory('cat1'))).toBe(toHex(EUI_AMSTERDAM_PALETTE_COLORS[2])); // EUI pink
|
||||
expect(toHex(colorFactory('cat2'))).toBe(NEUTRAL_COLOR_DARK[0]); // NEUTRAL LIGHT GRAY
|
||||
expect(toHex(colorFactory('cat3'))).toBe(toHex(EUI_AMSTERDAM_PALETTE_COLORS[0])); // EUI green
|
||||
expect(toHex(colorFactory('not available cat'))).toBe(
|
||||
toHex(NEUTRAL_COLOR_DARK[DEFAULT_NEUTRAL_PALETTE_INDEX])
|
||||
); // check the other
|
||||
});
|
||||
});
|
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import chroma from 'chroma-js';
|
||||
import { ColorMapping } from '../config';
|
||||
import { changeAlpha, combineColors, getValidColor } from './color_math';
|
||||
import { generateAutoAssignmentsForCategories } from '../config/assignment_from_categories';
|
||||
import { getPalette } from '../palettes';
|
||||
import { ColorMappingInputData } from '../categorical_color_mapping';
|
||||
import { ruleMatch } from './rule_matching';
|
||||
import { GradientColorMode } from '../config/types';
|
||||
|
||||
export function getAssignmentColor(
|
||||
colorMode: ColorMapping.Config['colorMode'],
|
||||
color: ColorMapping.Config['assignments'][number]['color'],
|
||||
getPaletteFn: ReturnType<typeof getPalette>,
|
||||
isDarkMode: boolean,
|
||||
index: number,
|
||||
total: number
|
||||
) {
|
||||
switch (color.type) {
|
||||
case 'colorCode':
|
||||
case 'categorical':
|
||||
return getColor(color, getPaletteFn, isDarkMode);
|
||||
case 'gradient': {
|
||||
if (colorMode.type === 'categorical') {
|
||||
return 'red';
|
||||
}
|
||||
const colorScale = getGradientColorScale(colorMode, getPaletteFn, isDarkMode);
|
||||
return total === 0 ? 'red' : total === 1 ? colorScale(0) : colorScale(index / (total - 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getColor(
|
||||
color: ColorMapping.ColorCode | ColorMapping.CategoricalColor,
|
||||
getPaletteFn: ReturnType<typeof getPalette>,
|
||||
isDarkMode: boolean
|
||||
) {
|
||||
return color.type === 'colorCode'
|
||||
? color.colorCode
|
||||
: getValidColor(getPaletteFn(color.paletteId).getColor(color.colorIndex, isDarkMode)).hex();
|
||||
}
|
||||
|
||||
export function getColorFactory(
|
||||
model: ColorMapping.Config,
|
||||
getPaletteFn: ReturnType<typeof getPalette>,
|
||||
isDarkMode: boolean,
|
||||
data: ColorMappingInputData
|
||||
): (category: string | string[]) => string {
|
||||
const palette = getPaletteFn(model.paletteId);
|
||||
// generate on-the-fly assignments in auto-mode based on current data.
|
||||
// This simplify the code by always using assignments, even if there is no real static assigmnets
|
||||
const assignments =
|
||||
model.assignmentMode === 'auto'
|
||||
? generateAutoAssignmentsForCategories(data, palette, model.colorMode)
|
||||
: model.assignments;
|
||||
|
||||
// find auto-assigned colors
|
||||
const autoAssignedColors =
|
||||
data.type === 'categories'
|
||||
? assignments.filter((a) => {
|
||||
return (
|
||||
a.rule.type === 'auto' || (a.rule.type === 'matchExactly' && a.rule.values.length === 0)
|
||||
);
|
||||
})
|
||||
: [];
|
||||
|
||||
// find all categories that doesn't match with an assignment
|
||||
const nonAssignedCategories =
|
||||
data.type === 'categories'
|
||||
? data.categories.filter((category) => {
|
||||
return !assignments.some(({ rule }) => ruleMatch(rule, category));
|
||||
})
|
||||
: [];
|
||||
|
||||
return (category: string | string[]) => {
|
||||
if (typeof category === 'string' || Array.isArray(category)) {
|
||||
const nonAssignedCategoryIndex = nonAssignedCategories.indexOf(category);
|
||||
|
||||
// return color for a non assigned category
|
||||
if (nonAssignedCategoryIndex > -1) {
|
||||
if (nonAssignedCategoryIndex < autoAssignedColors.length) {
|
||||
const autoAssignmentIndex = assignments.findIndex(
|
||||
(d) => d === autoAssignedColors[nonAssignedCategoryIndex]
|
||||
);
|
||||
return getAssignmentColor(
|
||||
model.colorMode,
|
||||
autoAssignedColors[nonAssignedCategoryIndex].color,
|
||||
getPaletteFn,
|
||||
isDarkMode,
|
||||
autoAssignmentIndex,
|
||||
assignments.length
|
||||
);
|
||||
}
|
||||
// if no auto-assign color rule/color is available then use the other color
|
||||
// TODO: the specialAssignment[0] position is arbitrary, we should fix it better
|
||||
return getColor(model.specialAssignments[0].color, getPaletteFn, isDarkMode);
|
||||
}
|
||||
|
||||
// find the assignment where the category matches the rule
|
||||
const matchingAssignmentIndex = assignments.findIndex(({ rule }) => {
|
||||
return ruleMatch(rule, category);
|
||||
});
|
||||
|
||||
// return the assigned color
|
||||
if (matchingAssignmentIndex > -1) {
|
||||
const assignment = assignments[matchingAssignmentIndex];
|
||||
return getAssignmentColor(
|
||||
model.colorMode,
|
||||
assignment.color,
|
||||
getPaletteFn,
|
||||
isDarkMode,
|
||||
matchingAssignmentIndex,
|
||||
assignments.length
|
||||
);
|
||||
}
|
||||
// if no assign color rule/color is available then use the other color
|
||||
// TODO: the specialAssignment[0] position is arbitrary, we should fix it better
|
||||
return getColor(model.specialAssignments[0].color, getPaletteFn, isDarkMode);
|
||||
} else {
|
||||
const matchingAssignmentIndex = assignments.findIndex(({ rule }) => {
|
||||
return ruleMatch(rule, category);
|
||||
});
|
||||
|
||||
if (matchingAssignmentIndex > -1) {
|
||||
const assignment = assignments[matchingAssignmentIndex];
|
||||
return getAssignmentColor(
|
||||
model.colorMode,
|
||||
assignment.color,
|
||||
getPaletteFn,
|
||||
isDarkMode,
|
||||
matchingAssignmentIndex,
|
||||
assignments.length
|
||||
);
|
||||
}
|
||||
return getColor(model.specialAssignments[0].color, getPaletteFn, isDarkMode);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getGradientColorScale(
|
||||
colorMode: GradientColorMode,
|
||||
getPaletteFn: ReturnType<typeof getPalette>,
|
||||
isDarkMode: boolean
|
||||
): (value: number) => string {
|
||||
const steps =
|
||||
colorMode.steps.length === 1
|
||||
? [
|
||||
getColor(colorMode.steps[0], getPaletteFn, isDarkMode),
|
||||
combineColors(
|
||||
changeAlpha(getColor(colorMode.steps[0], getPaletteFn, isDarkMode), 0.3),
|
||||
isDarkMode ? 'black' : 'white'
|
||||
),
|
||||
]
|
||||
: colorMode.steps.map((d) => getColor(d, getPaletteFn, isDarkMode));
|
||||
steps.sort(() => (colorMode.sort === 'asc' ? -1 : 1));
|
||||
const scale = chroma.scale(steps).mode('lab');
|
||||
return (value: number) => scale(value).hex();
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import chroma from 'chroma-js';
|
||||
|
||||
export function getValidColor(color: string): chroma.Color {
|
||||
try {
|
||||
return chroma(color);
|
||||
} catch {
|
||||
return chroma('red');
|
||||
}
|
||||
}
|
||||
|
||||
export function hasEnoughContrast(color: string, isDark: boolean, threshold = 4.5) {
|
||||
return chroma.contrast(getValidColor(color), isDark ? 'black' : 'white') >= threshold;
|
||||
}
|
||||
|
||||
export function changeAlpha(color: string, alpha: number) {
|
||||
const [r, g, b] = getValidColor(color).rgb();
|
||||
return `rgba(${r},${g},${b},${alpha})`;
|
||||
}
|
||||
|
||||
export function toHex(color: string) {
|
||||
return getValidColor(color).hex().toLowerCase();
|
||||
}
|
||||
|
||||
export function isSameColor(color1: string, color2: string) {
|
||||
return toHex(color1) === toHex(color2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Blend a foreground (fg) color with a background (bg) color
|
||||
*/
|
||||
export function combineColors(fg: string, bg: string): string {
|
||||
const [fgR, fgG, fgB, fgA] = getValidColor(fg).rgba();
|
||||
const [bgR, bgG, bgB, bgA] = getValidColor(bg).rgba();
|
||||
|
||||
// combine colors only if foreground has transparency
|
||||
if (fgA === 1) {
|
||||
return chroma.rgb(fgR, fgG, fgB).hex();
|
||||
}
|
||||
|
||||
// For reference on alpha calculations:
|
||||
// https://en.wikipedia.org/wiki/Alpha_compositing
|
||||
const alpha = fgA + bgA * (1 - fgA);
|
||||
|
||||
if (alpha === 0) {
|
||||
return '#00000000';
|
||||
}
|
||||
|
||||
const r = Math.round((fgR * fgA + bgR * bgA * (1 - fgA)) / alpha);
|
||||
const g = Math.round((fgG * fgA + bgG * bgA * (1 - fgA)) / alpha);
|
||||
const b = Math.round((fgB * fgA + bgB * bgA * (1 - fgA)) / alpha);
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ColorMapping } from '../config';
|
||||
|
||||
export function ruleMatch(
|
||||
rule: ColorMapping.Config['assignments'][number]['rule'],
|
||||
value: string | number | string[]
|
||||
) {
|
||||
switch (rule.type) {
|
||||
case 'matchExactly':
|
||||
if (Array.isArray(value)) {
|
||||
return rule.values.some(
|
||||
(v) =>
|
||||
Array.isArray(v) && v.length === value.length && v.every((part, i) => part === value[i])
|
||||
);
|
||||
}
|
||||
return rule.values.includes(`${value}`);
|
||||
case 'matchExactlyCI':
|
||||
return rule.values.some((d) => d.toLowerCase() === `${value}`);
|
||||
case 'range':
|
||||
// TODO: color by value not yet possible in all charts in elastic-charts
|
||||
return typeof value === 'number' ? rangeMatch(rule, value) : false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function rangeMatch(rule: ColorMapping.RuleRange, value: number) {
|
||||
return (
|
||||
(rule.min === rule.max && rule.min === value) ||
|
||||
((rule.minInclusive ? value >= rule.min : value > rule.min) &&
|
||||
(rule.maxInclusive ? value <= rule.max : value < rule.max))
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: move in some data/table related package
|
||||
export const SPECIAL_TOKENS_STRING_CONVERTION = new Map([
|
||||
['__other__', 'Other'],
|
||||
['', '(empty)'],
|
||||
]);
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { css } from '@emotion/react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import {
|
||||
removeAssignment,
|
||||
updateAssignmentColor,
|
||||
updateAssignmentRule,
|
||||
} from '../../state/color_mapping';
|
||||
import { ColorMapping } from '../../config';
|
||||
import { Range } from './range';
|
||||
import { Match } from './match';
|
||||
import { getPalette } from '../../palettes';
|
||||
|
||||
import { ColorMappingInputData } from '../../categorical_color_mapping';
|
||||
import { ColorSwatch } from '../color_picker/color_swatch';
|
||||
|
||||
export function Assignment({
|
||||
data,
|
||||
assignment,
|
||||
disableDelete,
|
||||
index,
|
||||
total,
|
||||
canPickColor,
|
||||
editable,
|
||||
palette,
|
||||
colorMode,
|
||||
getPaletteFn,
|
||||
isDarkMode,
|
||||
specialTokens,
|
||||
assignmentValuesCounter,
|
||||
}: {
|
||||
data: ColorMappingInputData;
|
||||
index: number;
|
||||
total: number;
|
||||
colorMode: ColorMapping.Config['colorMode'];
|
||||
assignment: ColorMapping.Config['assignments'][number];
|
||||
disableDelete: boolean;
|
||||
palette: ColorMapping.CategoricalPalette;
|
||||
getPaletteFn: ReturnType<typeof getPalette>;
|
||||
canPickColor: boolean;
|
||||
editable: boolean;
|
||||
isDarkMode: boolean;
|
||||
specialTokens: Map<string, string>;
|
||||
assignmentValuesCounter: Map<string | string[], number>;
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
alignItems="flexStart"
|
||||
justifyContent="spaceBetween"
|
||||
>
|
||||
<EuiFlexItem grow={0}>
|
||||
<ColorSwatch
|
||||
forType="assignment"
|
||||
isDarkMode={isDarkMode}
|
||||
swatchShape="square"
|
||||
canPickColor={canPickColor}
|
||||
colorMode={colorMode}
|
||||
assignmentColor={assignment.color}
|
||||
getPaletteFn={getPaletteFn}
|
||||
index={index}
|
||||
palette={palette}
|
||||
total={total}
|
||||
onColorChange={(color) => {
|
||||
dispatch(updateAssignmentColor({ assignmentIndex: index, color }));
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
{assignment.rule.type === 'auto' ||
|
||||
assignment.rule.type === 'matchExactly' ||
|
||||
assignment.rule.type === 'matchExactlyCI' ? (
|
||||
<Match
|
||||
editable={editable}
|
||||
index={index}
|
||||
rule={assignment.rule}
|
||||
options={data.type === 'categories' ? data.categories : []}
|
||||
specialTokens={specialTokens}
|
||||
updateValue={(values: Array<string | string[]>) => {
|
||||
dispatch(
|
||||
updateAssignmentRule({
|
||||
assignmentIndex: index,
|
||||
rule: values.length === 0 ? { type: 'auto' } : { type: 'matchExactly', values },
|
||||
})
|
||||
);
|
||||
}}
|
||||
assignmentValuesCounter={assignmentValuesCounter}
|
||||
/>
|
||||
) : assignment.rule.type === 'range' ? (
|
||||
<Range
|
||||
rule={assignment.rule}
|
||||
editable={editable}
|
||||
updateValue={(min, max, minInclusive, maxInclusive) => {
|
||||
const rule: ColorMapping.RuleRange = {
|
||||
type: 'range',
|
||||
min,
|
||||
max,
|
||||
minInclusive,
|
||||
maxInclusive,
|
||||
};
|
||||
dispatch(updateAssignmentRule({ assignmentIndex: index, rule }));
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<EuiFlexItem grow={0}>
|
||||
<EuiButtonIcon
|
||||
iconType="trash"
|
||||
size="xs"
|
||||
disabled={disableDelete}
|
||||
onClick={() => dispatch(removeAssignment(index))}
|
||||
aria-label={i18n.translate(
|
||||
'coloring.colorMapping.assignments.deleteAssignmentButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Delete this assignment',
|
||||
}
|
||||
)}
|
||||
color="danger"
|
||||
css={
|
||||
!disableDelete
|
||||
? css`
|
||||
color: ${euiThemeVars.euiTextSubduedColor};
|
||||
transition: ${euiThemeVars.euiAnimSpeedFast} ease-in-out;
|
||||
transition-property: color;
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: ${euiThemeVars.euiColorDangerText};
|
||||
}
|
||||
`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiComboBox, EuiFlexItem, EuiIcon } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { MULTI_FIELD_KEY_SEPARATOR } from '@kbn/data-plugin/common';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { ColorMapping } from '../../config';
|
||||
|
||||
export const Match: React.FC<{
|
||||
index: number;
|
||||
editable: boolean;
|
||||
rule:
|
||||
| ColorMapping.RuleAuto
|
||||
| ColorMapping.RuleMatchExactly
|
||||
| ColorMapping.RuleMatchExactlyCI
|
||||
| ColorMapping.RuleRegExp;
|
||||
updateValue: (values: Array<string | string[]>) => void;
|
||||
options: Array<string | string[]>;
|
||||
specialTokens: Map<unknown, string>;
|
||||
assignmentValuesCounter: Map<string | string[], number>;
|
||||
}> = ({ index, rule, updateValue, editable, options, specialTokens, assignmentValuesCounter }) => {
|
||||
const selectedOptions =
|
||||
rule.type === 'auto'
|
||||
? []
|
||||
: typeof rule.values === 'string'
|
||||
? [
|
||||
{
|
||||
label: rule.values,
|
||||
value: rule.values,
|
||||
append:
|
||||
(assignmentValuesCounter.get(rule.values) ?? 0) > 1 ? (
|
||||
<EuiIcon size="s" type="warning" color={euiThemeVars.euiColorWarningText} />
|
||||
) : undefined,
|
||||
},
|
||||
]
|
||||
: rule.values.map((value) => {
|
||||
const ruleValues = Array.isArray(value) ? value : [value];
|
||||
return {
|
||||
label: ruleValues.map((v) => specialTokens.get(v) ?? v).join(MULTI_FIELD_KEY_SEPARATOR),
|
||||
value,
|
||||
append:
|
||||
(assignmentValuesCounter.get(value) ?? 0) > 1 ? (
|
||||
<EuiIcon size="s" type="warning" color={euiThemeVars.euiColorWarningText} />
|
||||
) : undefined,
|
||||
};
|
||||
});
|
||||
|
||||
const convertedOptions = options.map((value) => {
|
||||
const ruleValues = Array.isArray(value) ? value : [value];
|
||||
return {
|
||||
label: ruleValues.map((v) => specialTokens.get(v) ?? v).join(MULTI_FIELD_KEY_SEPARATOR),
|
||||
value,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFlexItem style={{ minWidth: 1, width: 1 }}>
|
||||
<EuiComboBox
|
||||
data-test-subj={`lns-colorMapping-assignmentsItem${index}`}
|
||||
isDisabled={!editable}
|
||||
fullWidth={true}
|
||||
aria-label={i18n.translate('coloring.colorMapping.assignments.autoAssignedTermAriaLabel', {
|
||||
defaultMessage:
|
||||
"This color will be automatically assigned to the first term that doesn't match with all the other assignments",
|
||||
})}
|
||||
placeholder={i18n.translate(
|
||||
'coloring.colorMapping.assignments.autoAssignedTermPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Auto assigned',
|
||||
}
|
||||
)}
|
||||
options={convertedOptions}
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={(changedOptions) => {
|
||||
updateValue(
|
||||
changedOptions.reduce<Array<string | string[]>>((acc, option) => {
|
||||
if (option.value !== undefined) {
|
||||
acc.push(option.value);
|
||||
}
|
||||
return acc;
|
||||
}, [])
|
||||
);
|
||||
}}
|
||||
onCreateOption={(label) => {
|
||||
if (selectedOptions.findIndex((option) => option.label.toLowerCase() === label) === -1) {
|
||||
updateValue([...selectedOptions, { label, value: label }].map((d) => d.value));
|
||||
}
|
||||
}}
|
||||
isClearable={false}
|
||||
compressed
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiButtonEmpty, EuiFieldNumber, EuiFlexItem } from '@elastic/eui';
|
||||
import { ColorMapping } from '../../config';
|
||||
|
||||
export const Range: React.FC<{
|
||||
rule: ColorMapping.RuleRange;
|
||||
editable: boolean;
|
||||
updateValue: (min: number, max: number, minInclusive: boolean, maxInclusive: boolean) => void;
|
||||
}> = ({ rule, updateValue, editable }) => {
|
||||
const minValid = rule.min <= rule.max;
|
||||
const maxValid = rule.max >= rule.min;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexItem>
|
||||
<EuiFieldNumber
|
||||
compressed
|
||||
prepend={
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
onClick={() => updateValue(rule.min, rule.max, !rule.minInclusive, rule.maxInclusive)}
|
||||
>
|
||||
{rule.minInclusive ? 'GTE' : 'GT'}
|
||||
</EuiButtonEmpty>
|
||||
}
|
||||
placeholder="min"
|
||||
value={rule.min}
|
||||
isInvalid={!minValid}
|
||||
disabled={!editable}
|
||||
onChange={(e) =>
|
||||
updateValue(+e.currentTarget.value, rule.max, rule.minInclusive, rule.maxInclusive)
|
||||
}
|
||||
aria-label="The min value"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFieldNumber
|
||||
compressed
|
||||
isInvalid={!maxValid}
|
||||
prepend={
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
onClick={() => updateValue(rule.min, rule.max, rule.minInclusive, !rule.maxInclusive)}
|
||||
>
|
||||
{rule.maxInclusive ? 'LTE' : 'LT'}
|
||||
</EuiButtonEmpty>
|
||||
}
|
||||
placeholder="max"
|
||||
disabled={!editable}
|
||||
value={rule.max}
|
||||
onChange={(e) =>
|
||||
updateValue(rule.min, +e.currentTarget.value, rule.minInclusive, rule.maxInclusive)
|
||||
}
|
||||
aria-label="The max value"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EuiFieldText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ColorMapping } from '../../config';
|
||||
import { getPalette } from '../../palettes';
|
||||
import { ColorSwatch } from '../color_picker/color_swatch';
|
||||
import { updateSpecialAssignmentColor } from '../../state/color_mapping';
|
||||
|
||||
export function SpecialAssignment({
|
||||
assignment,
|
||||
index,
|
||||
palette,
|
||||
getPaletteFn,
|
||||
isDarkMode,
|
||||
total,
|
||||
}: {
|
||||
isDarkMode: boolean;
|
||||
index: number;
|
||||
assignment: ColorMapping.Config['specialAssignments'][number];
|
||||
palette: ColorMapping.CategoricalPalette;
|
||||
getPaletteFn: ReturnType<typeof getPalette>;
|
||||
total: number;
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const canPickColor = true;
|
||||
return (
|
||||
<EuiFlexGroup direction="row" gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={0}>
|
||||
<ColorSwatch
|
||||
forType="specialAssignment"
|
||||
canPickColor={canPickColor}
|
||||
colorMode={{ type: 'categorical' }}
|
||||
assignmentColor={assignment.color}
|
||||
getPaletteFn={getPaletteFn}
|
||||
index={index}
|
||||
palette={palette}
|
||||
total={total}
|
||||
swatchShape="square"
|
||||
isDarkMode={isDarkMode}
|
||||
onColorChange={(color) => {
|
||||
dispatch(
|
||||
updateSpecialAssignmentColor({
|
||||
assignmentIndex: index,
|
||||
color,
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
style={{
|
||||
marginRight: 32,
|
||||
}}
|
||||
>
|
||||
<EuiFieldText
|
||||
compressed
|
||||
fullWidth
|
||||
disabled={true}
|
||||
placeholder={i18n.translate('coloring.colorMapping.assignments.unassignedPlaceholder', {
|
||||
defaultMessage: 'Unassigned terms',
|
||||
})}
|
||||
aria-label={i18n.translate('coloring.colorMapping.assignments.unassignedAriaLabel', {
|
||||
defaultMessage:
|
||||
'Assign this color to every unassigned not described in the assignment list',
|
||||
})}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiPopoverTitle,
|
||||
EuiTab,
|
||||
EuiTabs,
|
||||
EuiTitle,
|
||||
EuiHorizontalRule,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ColorMapping } from '../../config';
|
||||
import { getPalette } from '../../palettes';
|
||||
import { PaletteColors } from './palette_colors';
|
||||
import { RGBPicker } from './rgb_picker';
|
||||
import { NeutralPalette } from '../../palettes/neutral';
|
||||
|
||||
export function ColorPicker({
|
||||
palette,
|
||||
getPaletteFn,
|
||||
color,
|
||||
close,
|
||||
selectColor,
|
||||
isDarkMode,
|
||||
deleteStep,
|
||||
}: {
|
||||
color: ColorMapping.CategoricalColor | ColorMapping.ColorCode;
|
||||
getPaletteFn: ReturnType<typeof getPalette>;
|
||||
palette: ColorMapping.CategoricalPalette;
|
||||
isDarkMode: boolean;
|
||||
close: () => void;
|
||||
selectColor: (color: ColorMapping.CategoricalColor | ColorMapping.ColorCode) => void;
|
||||
deleteStep?: () => void;
|
||||
}) {
|
||||
const [tab, setTab] = useState(
|
||||
color.type === 'categorical' &&
|
||||
(color.paletteId === palette.id || color.paletteId === NeutralPalette.id)
|
||||
? 'palette'
|
||||
: 'custom'
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ width: 168 }}>
|
||||
<EuiPopoverTitle
|
||||
paddingSize="none"
|
||||
style={{
|
||||
borderBottom: 'none',
|
||||
}}
|
||||
>
|
||||
<EuiTabs size="m" expand>
|
||||
<EuiTab onClick={() => setTab('palette')} isSelected={tab === 'palette'}>
|
||||
<EuiTitle size="xxxs">
|
||||
<span>
|
||||
{i18n.translate('coloring.colorMapping.colorPicker.paletteTabLabel', {
|
||||
defaultMessage: 'Colors',
|
||||
})}
|
||||
</span>
|
||||
</EuiTitle>
|
||||
</EuiTab>
|
||||
<EuiTab onClick={() => setTab('custom')} isSelected={tab === 'custom'}>
|
||||
<EuiTitle size="xxxs">
|
||||
<span>
|
||||
{i18n.translate('coloring.colorMapping.colorPicker.customTabLabel', {
|
||||
defaultMessage: 'Custom',
|
||||
})}
|
||||
</span>
|
||||
</EuiTitle>
|
||||
</EuiTab>
|
||||
</EuiTabs>
|
||||
</EuiPopoverTitle>
|
||||
{tab === 'palette' ? (
|
||||
<PaletteColors
|
||||
color={color}
|
||||
getPaletteFn={getPaletteFn}
|
||||
palette={palette}
|
||||
selectColor={selectColor}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
) : (
|
||||
<RGBPicker
|
||||
color={color}
|
||||
getPaletteFn={getPaletteFn}
|
||||
isDarkMode={isDarkMode}
|
||||
selectColor={selectColor}
|
||||
palette={palette}
|
||||
close={close}
|
||||
/>
|
||||
)}
|
||||
{deleteStep ? (
|
||||
<>
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
<EuiButtonEmpty
|
||||
color="danger"
|
||||
size="xs"
|
||||
iconType="trash"
|
||||
onClick={() => {
|
||||
close();
|
||||
deleteStep();
|
||||
}}
|
||||
style={{ paddingBottom: 8 }}
|
||||
>
|
||||
{i18n.translate('coloring.colorMapping.colorPicker.removeGradientColorButtonLabel', {
|
||||
defaultMessage: 'Remove color step',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiColorPickerSwatch,
|
||||
EuiPopover,
|
||||
euiShadowSmall,
|
||||
isColorDark,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { css } from '@emotion/react';
|
||||
import { ColorPicker } from './color_picker';
|
||||
import { getAssignmentColor } from '../../color/color_handling';
|
||||
import { ColorMapping } from '../../config';
|
||||
import { getPalette } from '../../palettes';
|
||||
import { removeGradientColorStep } from '../../state/color_mapping';
|
||||
|
||||
import { selectColorPickerVisibility } from '../../state/selectors';
|
||||
import { colorPickerVisibility, hideColorPickerVisibility } from '../../state/ui';
|
||||
import { getValidColor } from '../../color/color_math';
|
||||
|
||||
interface ColorPickerSwatchProps {
|
||||
colorMode: ColorMapping.Config['colorMode'];
|
||||
assignmentColor:
|
||||
| ColorMapping.Config['assignments'][number]['color']
|
||||
| ColorMapping.Config['specialAssignments'][number]['color'];
|
||||
getPaletteFn: ReturnType<typeof getPalette>;
|
||||
canPickColor: boolean;
|
||||
index: number;
|
||||
total: number;
|
||||
palette: ColorMapping.CategoricalPalette;
|
||||
onColorChange: (color: ColorMapping.CategoricalColor | ColorMapping.ColorCode) => void;
|
||||
swatchShape: 'square' | 'round';
|
||||
isDarkMode: boolean;
|
||||
forType: 'assignment' | 'specialAssignment' | 'gradient';
|
||||
}
|
||||
export const ColorSwatch = ({
|
||||
colorMode,
|
||||
assignmentColor,
|
||||
getPaletteFn,
|
||||
canPickColor,
|
||||
index,
|
||||
total,
|
||||
palette,
|
||||
onColorChange,
|
||||
swatchShape,
|
||||
isDarkMode,
|
||||
forType,
|
||||
}: ColorPickerSwatchProps) => {
|
||||
const colorPickerState = useSelector(selectColorPickerVisibility);
|
||||
const dispatch = useDispatch();
|
||||
const colorPickerVisible =
|
||||
colorPickerState.index === index &&
|
||||
colorPickerState.type === forType &&
|
||||
colorPickerState.visibile;
|
||||
const colorHex = getAssignmentColor(
|
||||
colorMode,
|
||||
assignmentColor,
|
||||
getPaletteFn,
|
||||
isDarkMode,
|
||||
index,
|
||||
total
|
||||
);
|
||||
const colorIsDark = isColorDark(...getValidColor(colorHex).rgb());
|
||||
const euiTheme = useEuiTheme();
|
||||
return canPickColor && assignmentColor.type !== 'gradient' ? (
|
||||
<EuiPopover
|
||||
panelPaddingSize="none"
|
||||
isOpen={colorPickerVisible}
|
||||
repositionOnScroll={true}
|
||||
closePopover={() => dispatch(hideColorPickerVisibility())}
|
||||
anchorPosition="upLeft"
|
||||
button={
|
||||
swatchShape === 'round' ? (
|
||||
<button
|
||||
aria-label={i18n.translate('coloring.colorMapping.colorPicker.pickAColorAriaLabel', {
|
||||
defaultMessage: 'Pick a color',
|
||||
})}
|
||||
data-test-subj={`lns-colorMapping-colorSwatch-${index}`}
|
||||
onClick={() =>
|
||||
dispatch(
|
||||
colorPickerVisible
|
||||
? hideColorPickerVisibility()
|
||||
: colorPickerVisibility({ index, visible: true, type: forType })
|
||||
)
|
||||
}
|
||||
css={css`
|
||||
background: ${colorHex};
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
top: 8px;
|
||||
border: 3px solid white;
|
||||
${euiShadowSmall(euiTheme)};
|
||||
backgroundcolor: ${colorHex};
|
||||
cursor: ${canPickColor ? 'pointer' : 'not-allowed'};
|
||||
`}
|
||||
/>
|
||||
) : (
|
||||
<EuiColorPickerSwatch
|
||||
color={colorHex}
|
||||
aria-label={i18n.translate('coloring.colorMapping.colorPicker.pickAColorAriaLabel', {
|
||||
defaultMessage: 'Pick a color',
|
||||
})}
|
||||
data-test-subj={`lns-colorMapping-colorSwatch-${index}`}
|
||||
onClick={() =>
|
||||
dispatch(
|
||||
colorPickerVisible
|
||||
? hideColorPickerVisibility()
|
||||
: colorPickerVisibility({ index, visible: true, type: forType })
|
||||
)
|
||||
}
|
||||
style={{
|
||||
// the color swatch can't pickup colors written in rgb/css standard
|
||||
backgroundColor: colorHex,
|
||||
cursor: canPickColor ? 'pointer' : 'not-allowed',
|
||||
width: 32,
|
||||
height: 32,
|
||||
}}
|
||||
css={css`
|
||||
&::after {
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 3px solid transparent;
|
||||
border-right: 3px solid transparent;
|
||||
border-top: 4px solid ${colorIsDark ? 'white' : 'black'};
|
||||
margin: 0;
|
||||
bottom: 2px;
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
<ColorPicker
|
||||
key={
|
||||
assignmentColor.type === 'categorical'
|
||||
? `${assignmentColor.colorIndex}-${assignmentColor.paletteId}`
|
||||
: assignmentColor.colorCode
|
||||
}
|
||||
color={assignmentColor}
|
||||
palette={palette}
|
||||
getPaletteFn={getPaletteFn}
|
||||
close={() => dispatch(hideColorPickerVisibility())}
|
||||
isDarkMode={isDarkMode}
|
||||
selectColor={(color) => {
|
||||
// dispatch update
|
||||
onColorChange(color);
|
||||
}}
|
||||
deleteStep={
|
||||
colorMode.type === 'gradient' && total > 1
|
||||
? () => dispatch(removeGradientColorStep(index))
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</EuiPopover>
|
||||
) : (
|
||||
<EuiColorPickerSwatch
|
||||
color={colorHex}
|
||||
aria-label={i18n.translate('coloring.colorMapping.colorPicker.newColorAriaLabel', {
|
||||
defaultMessage: 'Select a new color',
|
||||
})}
|
||||
disabled
|
||||
style={{
|
||||
// the color swatch can't pickup colors written in rgb/css standard
|
||||
backgroundColor: colorHex,
|
||||
cursor: 'not-allowed',
|
||||
width: 32,
|
||||
height: 32,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiColorPickerSwatch,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
EuiIcon,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ColorMapping } from '../../config';
|
||||
import { getPalette } from '../../palettes';
|
||||
import { isSameColor } from '../../color/color_math';
|
||||
import { NeutralPalette } from '../../palettes/neutral';
|
||||
|
||||
export function PaletteColors({
|
||||
palette,
|
||||
isDarkMode,
|
||||
color,
|
||||
getPaletteFn,
|
||||
selectColor,
|
||||
}: {
|
||||
palette: ColorMapping.CategoricalPalette;
|
||||
isDarkMode: boolean;
|
||||
color: ColorMapping.CategoricalColor | ColorMapping.ColorCode;
|
||||
getPaletteFn: ReturnType<typeof getPalette>;
|
||||
selectColor: (color: ColorMapping.CategoricalColor | ColorMapping.ColorCode) => void;
|
||||
}) {
|
||||
const colors = Array.from({ length: palette.colorCount }, (d, i) => {
|
||||
return palette.getColor(i, isDarkMode);
|
||||
});
|
||||
const neutralColors = Array.from({ length: NeutralPalette.colorCount }, (d, i) => {
|
||||
return NeutralPalette.getColor(i, isDarkMode);
|
||||
});
|
||||
const originalColor =
|
||||
color.type === 'categorical'
|
||||
? color.paletteId === NeutralPalette.id
|
||||
? NeutralPalette.getColor(color.colorIndex, isDarkMode)
|
||||
: getPaletteFn(color.paletteId).getColor(color.colorIndex, isDarkMode)
|
||||
: color.colorCode;
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup direction="column" style={{ padding: 8 }}>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<strong>
|
||||
{i18n.translate('coloring.colorMapping.colorPicker.paletteColorsLabel', {
|
||||
defaultMessage: 'Palette colors',
|
||||
})}
|
||||
</strong>
|
||||
</EuiText>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
wrap={true}
|
||||
alignItems="center"
|
||||
justifyContent="flexStart"
|
||||
>
|
||||
{colors.map((c, index) => (
|
||||
<EuiFlexItem key={c} grow={0}>
|
||||
<EuiColorPickerSwatch
|
||||
data-test-subj={`lns-colorMapping-colorPicker-staticColor-${index}`}
|
||||
style={{
|
||||
border: isSameColor(c, originalColor) ? '2px solid black' : 'transparent',
|
||||
}}
|
||||
color={c}
|
||||
onClick={() =>
|
||||
selectColor({ type: 'categorical', paletteId: palette.id, colorIndex: index })
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
<EuiFlexGroup style={{ padding: 8, paddingTop: 0 }}>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<strong>
|
||||
{i18n.translate('coloring.colorMapping.colorPicker.themeAwareColorsLabel', {
|
||||
defaultMessage: 'Neutral colors',
|
||||
})}
|
||||
</strong>
|
||||
<EuiToolTip
|
||||
position="bottom"
|
||||
content={i18n.translate('coloring.colorMapping.colorPicker.themeAwareColorsTooltip', {
|
||||
defaultMessage:
|
||||
'The provided neutral colors are theme-aware and will change appropriately when switching between light and dark themes',
|
||||
})}
|
||||
>
|
||||
<EuiIcon tabIndex={0} type="questionInCircle" />
|
||||
</EuiToolTip>
|
||||
</EuiText>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
wrap={true}
|
||||
alignItems="center"
|
||||
justifyContent="flexStart"
|
||||
>
|
||||
{neutralColors.map((c, index) => (
|
||||
<EuiFlexItem key={c} grow={0}>
|
||||
<EuiColorPickerSwatch
|
||||
style={{
|
||||
border: isSameColor(c, originalColor) ? '2px solid black' : 'transparent',
|
||||
}}
|
||||
data-test-subj={`lns-colorMapping-colorPicker-neutralColor-${index}`}
|
||||
color={c}
|
||||
onClick={() =>
|
||||
selectColor({
|
||||
type: 'categorical',
|
||||
paletteId: NeutralPalette.id,
|
||||
colorIndex: index,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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 { EuiColorPicker, EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui';
|
||||
import React, { useState } from 'react';
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
import chromajs from 'chroma-js';
|
||||
import { css } from '@emotion/react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { ColorMapping } from '../../config';
|
||||
|
||||
import { hasEnoughContrast } from '../../color/color_math';
|
||||
import { getPalette } from '../../palettes';
|
||||
|
||||
export function RGBPicker({
|
||||
isDarkMode,
|
||||
color,
|
||||
getPaletteFn,
|
||||
selectColor,
|
||||
close,
|
||||
}: {
|
||||
palette: ColorMapping.CategoricalPalette;
|
||||
isDarkMode: boolean;
|
||||
color: ColorMapping.CategoricalColor | ColorMapping.ColorCode;
|
||||
getPaletteFn: ReturnType<typeof getPalette>;
|
||||
selectColor: (color: ColorMapping.CategoricalColor | ColorMapping.ColorCode) => void;
|
||||
close: () => void;
|
||||
}) {
|
||||
const [customColorMappingColor, setCustomColorMappingColor] = useState<
|
||||
ColorMapping.CategoricalColor | ColorMapping.ColorCode
|
||||
>(color);
|
||||
|
||||
const customColorHex =
|
||||
customColorMappingColor.type === 'categorical'
|
||||
? getPaletteFn(customColorMappingColor.paletteId).getColor(
|
||||
customColorMappingColor.colorIndex,
|
||||
isDarkMode
|
||||
)
|
||||
: customColorMappingColor.colorCode;
|
||||
|
||||
const [colorTextInput, setColorTextInput] = useState<string>(customColorHex);
|
||||
|
||||
// check contrasts with WCAG 2.1 with a min contrast ratio of 3
|
||||
const lightContrast = hasEnoughContrast(customColorHex, false, 3);
|
||||
const darkContrast = hasEnoughContrast(customColorHex, true, 3);
|
||||
|
||||
const errorMessage = [
|
||||
lightContrast === false ? 'light' : undefined,
|
||||
darkContrast === false ? 'dark' : undefined,
|
||||
].filter(Boolean);
|
||||
|
||||
const isColorTextValid = chromajs.valid(colorTextInput);
|
||||
const colorHasContrast = lightContrast && darkContrast;
|
||||
|
||||
// debounce setting the color from the rgb picker by 500ms
|
||||
useDebounce(
|
||||
() => {
|
||||
if (color !== customColorMappingColor) {
|
||||
selectColor(customColorMappingColor);
|
||||
}
|
||||
},
|
||||
500,
|
||||
[color, customColorMappingColor]
|
||||
);
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="s" style={{ padding: 8 }}>
|
||||
<EuiFlexItem>
|
||||
<EuiColorPicker
|
||||
onChange={(c) => {
|
||||
setCustomColorMappingColor({
|
||||
type: 'colorCode',
|
||||
colorCode: c,
|
||||
});
|
||||
setColorTextInput(c);
|
||||
}}
|
||||
color={customColorHex}
|
||||
display="inline"
|
||||
swatches={[]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<div
|
||||
css={
|
||||
!colorHasContrast && isColorTextValid
|
||||
? css`
|
||||
svg {
|
||||
fill: ${euiThemeVars.euiColorWarningText} !important;
|
||||
}
|
||||
input {
|
||||
background-image: linear-gradient(
|
||||
to top,
|
||||
${euiThemeVars.euiColorWarning},
|
||||
${euiThemeVars.euiColorWarning} 2px,
|
||||
transparent 2px,
|
||||
transparent 100%
|
||||
) !important;
|
||||
}
|
||||
.euiFormErrorText {
|
||||
color: ${euiThemeVars.euiColorWarningText} !important;
|
||||
}
|
||||
`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<EuiFormRow
|
||||
isInvalid={!isColorTextValid || !colorHasContrast}
|
||||
error={
|
||||
!isColorTextValid
|
||||
? `Please input a valid color hex code`
|
||||
: !colorHasContrast
|
||||
? `This color has a low contrast in ${errorMessage} mode${
|
||||
errorMessage.length > 1 ? 's' : ''
|
||||
}`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
placeholder="Please enter an hex color code"
|
||||
value={colorTextInput}
|
||||
compressed
|
||||
isInvalid={!isColorTextValid || !colorHasContrast}
|
||||
onChange={(e) => {
|
||||
const textColor = e.currentTarget.value;
|
||||
setColorTextInput(textColor);
|
||||
if (chromajs.valid(textColor)) {
|
||||
setCustomColorMappingColor({
|
||||
type: 'colorCode',
|
||||
colorCode: chromajs(textColor).hex(),
|
||||
});
|
||||
}
|
||||
}}
|
||||
aria-label="hex color input"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,252 @@
|
|||
/*
|
||||
* 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 { useSelector, useDispatch } from 'react-redux';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormLabel,
|
||||
EuiHorizontalRule,
|
||||
EuiPanel,
|
||||
EuiSwitch,
|
||||
EuiText,
|
||||
} 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 { ColorMapping } from '../../config';
|
||||
import { getPalette } from '../../palettes';
|
||||
import { getUnusedColorForNewAssignment } from '../../config/assignments';
|
||||
import {
|
||||
selectColorMode,
|
||||
selectPalette,
|
||||
selectSpecialAssignments,
|
||||
selectIsAutoAssignmentMode,
|
||||
} from '../../state/selectors';
|
||||
import { ColorMappingInputData } from '../../categorical_color_mapping';
|
||||
import { Gradient } from '../palette_selector/gradient';
|
||||
import { NeutralPalette } from '../../palettes/neutral';
|
||||
|
||||
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: {
|
||||
palettes: Map<string, ColorMapping.CategoricalPalette>;
|
||||
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<string, string>;
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const getPaletteFn = getPalette(props.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<Map<string | string[], number>>(
|
||||
(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()
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="s" justifyContent="flexStart">
|
||||
<EuiFlexItem>
|
||||
<PaletteSelector
|
||||
palettes={props.palettes}
|
||||
getPaletteFn={getPaletteFn}
|
||||
isDarkMode={props.isDarkMode}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="row">
|
||||
<EuiFlexItem>
|
||||
<EuiFormLabel>
|
||||
{i18n.translate('coloring.colorMapping.container.mappingAssignmentHeader', {
|
||||
defaultMessage: 'Mapping assignments',
|
||||
})}
|
||||
</EuiFormLabel>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={0}>
|
||||
<EuiSwitch
|
||||
data-test-subj="lns-colorMapping-autoAssignSwitch"
|
||||
label={
|
||||
<EuiText size="xs">
|
||||
{i18n.translate('coloring.colorMapping.container.autoAssignLabel', {
|
||||
defaultMessage: 'Auto assign',
|
||||
})}
|
||||
</EuiText>
|
||||
}
|
||||
checked={autoAssignmentMode}
|
||||
compressed
|
||||
onChange={() => {
|
||||
if (autoAssignmentMode) {
|
||||
dispatch(assignStatically(assignments));
|
||||
} else {
|
||||
dispatch(assignAutomatically());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiPanel color="subdued" borderRadius="none" hasShadow={false} paddingSize="s">
|
||||
<div
|
||||
data-test-subj="lns-colorMapping-assignmentsList"
|
||||
css={css`
|
||||
display: grid;
|
||||
grid-template-columns: ${colorMode.type === 'gradient' ? '[gradient] 16px' : ''} [assignment] auto;
|
||||
gap: 8px;
|
||||
`}
|
||||
>
|
||||
{colorMode.type !== 'gradient' ? null : (
|
||||
<Gradient
|
||||
colorMode={colorMode}
|
||||
getPaletteFn={getPaletteFn}
|
||||
isDarkMode={props.isDarkMode}
|
||||
paletteId={palette.id}
|
||||
assignmentsSize={assignments.length}
|
||||
/>
|
||||
)}
|
||||
{assignments.map((assignment, i) => {
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
css={css`
|
||||
position: relative;
|
||||
grid-column: ${colorMode.type === 'gradient' ? 2 : 1};
|
||||
grid-row: ${i + 1};
|
||||
width: 100%;
|
||||
`}
|
||||
>
|
||||
<Assignment
|
||||
data={props.data}
|
||||
index={i}
|
||||
total={assignments.length}
|
||||
colorMode={colorMode}
|
||||
editable={!autoAssignmentMode}
|
||||
canPickColor={!autoAssignmentMode && colorMode.type !== 'gradient'}
|
||||
palette={palette}
|
||||
isDarkMode={props.isDarkMode}
|
||||
getPaletteFn={getPaletteFn}
|
||||
assignment={assignment}
|
||||
disableDelete={assignments.length <= 1 || autoAssignmentMode}
|
||||
specialTokens={props.specialTokens}
|
||||
assignmentValuesCounter={assignmentValuesCounter}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
<EuiFlexGroup direction="row" gutterSize="s">
|
||||
<EuiFlexItem data-test-subj="lns-colorMapping-specialAssignmentsList">
|
||||
{props.data.type === 'categories' &&
|
||||
specialAssignments.map((assignment, i) => {
|
||||
return (
|
||||
<SpecialAssignment
|
||||
key={i}
|
||||
index={i}
|
||||
palette={palette}
|
||||
isDarkMode={props.isDarkMode}
|
||||
getPaletteFn={getPaletteFn}
|
||||
assignment={assignment}
|
||||
total={specialAssignments.length}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
<EuiFlexGroup direction="row">
|
||||
<EuiFlexItem style={{ display: 'block' }}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="lns-colorMapping-addNewAssignment"
|
||||
iconType="plusInCircleFilled"
|
||||
size="xs"
|
||||
flush="both"
|
||||
onClick={() => {
|
||||
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',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
{colorMode.type === 'gradient' && (
|
||||
<EuiButtonEmpty
|
||||
flush="both"
|
||||
data-test-subj="lns-colorMapping-invertGradient"
|
||||
iconType="sortable"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
dispatch(changeGradientSortOrder(colorMode.sort === 'asc' ? 'desc' : 'asc'));
|
||||
}}
|
||||
>
|
||||
{i18n.translate('coloring.colorMapping.container.invertGradientButtonLabel', {
|
||||
defaultMessage: 'Invert gradient',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,336 @@
|
|||
/*
|
||||
* 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 { 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 { 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';
|
||||
|
||||
export function Gradient({
|
||||
paletteId,
|
||||
colorMode,
|
||||
getPaletteFn,
|
||||
isDarkMode,
|
||||
assignmentsSize,
|
||||
}: {
|
||||
paletteId: string;
|
||||
isDarkMode: boolean;
|
||||
colorMode: ColorMapping.Config['colorMode'];
|
||||
getPaletteFn: ReturnType<typeof getPalette>;
|
||||
assignmentsSize: number;
|
||||
}) {
|
||||
if (colorMode.type === 'categorical') {
|
||||
return null;
|
||||
}
|
||||
const currentPalette = getPaletteFn(paletteId);
|
||||
const gradientColorScale = getGradientColorScale(colorMode, getPaletteFn, isDarkMode);
|
||||
|
||||
const topMostColorStop =
|
||||
colorMode.sort === 'asc'
|
||||
? colorMode.steps.length === 1
|
||||
? undefined
|
||||
: colorMode.steps.at(-1)
|
||||
: colorMode.steps.at(0);
|
||||
const topMostColorStopIndex =
|
||||
colorMode.sort === 'asc'
|
||||
? colorMode.steps.length === 1
|
||||
? NaN
|
||||
: colorMode.steps.length - 1
|
||||
: 0;
|
||||
|
||||
const bottomMostColorStop =
|
||||
colorMode.sort === 'asc'
|
||||
? colorMode.steps.at(0)
|
||||
: colorMode.steps.length === 1
|
||||
? undefined
|
||||
: colorMode.steps.at(-1);
|
||||
const bottomMostColorStopIndex =
|
||||
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;
|
||||
|
||||
return (
|
||||
<>
|
||||
{assignmentsSize > 1 && (
|
||||
<div
|
||||
className="gradientLine"
|
||||
css={css`
|
||||
position: relative;
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
width: 6px;
|
||||
margin-left: 5px;
|
||||
top: 16px;
|
||||
height: calc(100% - 12px);
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
${[gradientColorScale(0), gradientColorScale(1 / assignmentsSize)].join(',')}
|
||||
);
|
||||
border-left: 1px solid ${changeAlpha(euiThemeVars.euiColorDarkestShade, 0.2)};
|
||||
border-top: 1px solid ${changeAlpha(euiThemeVars.euiColorDarkestShade, 0.2)};
|
||||
border-right: 1px solid ${changeAlpha(euiThemeVars.euiColorDarkestShade, 0.2)};
|
||||
`}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className="gradientStop"
|
||||
css={css`
|
||||
position: relative;
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
margin-top: 8px;
|
||||
`}
|
||||
>
|
||||
{topMostColorStop ? (
|
||||
<ColorStop
|
||||
colorMode={colorMode}
|
||||
step={topMostColorStop}
|
||||
index={topMostColorStopIndex}
|
||||
currentPalette={currentPalette}
|
||||
getPaletteFn={getPaletteFn}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
) : (
|
||||
<AddStop colorMode={colorMode} currentPalette={currentPalette} at={1} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{assignmentsSize > 1 && (
|
||||
<div
|
||||
className="gradientLine"
|
||||
css={css`
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
grid-column: 1;
|
||||
grid-row-start: 2;
|
||||
grid-row-end: ${assignmentsSize};
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
${[
|
||||
gradientColorScale(1 / assignmentsSize),
|
||||
gradientColorScale((assignmentsSize - 1) / assignmentsSize),
|
||||
].join(',')}
|
||||
);
|
||||
margin: -4px 0;
|
||||
width: 6px;
|
||||
margin-left: 5px;
|
||||
${assignmentsSize === 2 ? 'height: 0;' : ''};
|
||||
border-left: 1px solid ${changeAlpha(euiThemeVars.euiColorDarkestShade, 0.2)};
|
||||
border-right: 1px solid ${changeAlpha(euiThemeVars.euiColorDarkestShade, 0.2)};
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={css`
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: calc(50% - 5px);
|
||||
margin-left: -6px;
|
||||
margin-top: -3px;
|
||||
`}
|
||||
>
|
||||
{middleMostColorSep ? (
|
||||
<ColorStop
|
||||
colorMode={colorMode}
|
||||
step={middleMostColorSep}
|
||||
index={middleMostColorStopIndex}
|
||||
currentPalette={currentPalette}
|
||||
getPaletteFn={getPaletteFn}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
) : colorMode.steps.length === 2 ? (
|
||||
<AddStop colorMode={colorMode} currentPalette={currentPalette} at={1} />
|
||||
) : undefined}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{assignmentsSize > 1 && (
|
||||
<div
|
||||
className="gradientLine"
|
||||
css={css`
|
||||
position: relative;
|
||||
|
||||
grid-column: 1;
|
||||
grid-row: ${assignmentsSize};
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
|
||||
${[
|
||||
gradientColorScale((assignmentsSize - 1) / assignmentsSize),
|
||||
gradientColorScale(1),
|
||||
].join(',')}
|
||||
);
|
||||
top: -4px;
|
||||
height: 24px;
|
||||
width: 6px;
|
||||
margin-left: 5px;
|
||||
border-bottom-left-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
border-left: 1px solid ${changeAlpha(euiThemeVars.euiColorDarkestShade, 0.2)};
|
||||
border-bottom: 1px solid ${changeAlpha(euiThemeVars.euiColorDarkestShade, 0.2)};
|
||||
border-right: 1px solid ${changeAlpha(euiThemeVars.euiColorDarkestShade, 0.2)};
|
||||
`}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
css={css`
|
||||
position: relative;
|
||||
grid-column: 1;
|
||||
grid-row: ${assignmentsSize};
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: 8px;
|
||||
`}
|
||||
>
|
||||
{bottomMostColorStop ? (
|
||||
<ColorStop
|
||||
colorMode={colorMode}
|
||||
step={bottomMostColorStop}
|
||||
index={bottomMostColorStopIndex}
|
||||
currentPalette={currentPalette}
|
||||
getPaletteFn={getPaletteFn}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
) : (
|
||||
<AddStop colorMode={colorMode} currentPalette={currentPalette} at={1} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<button
|
||||
css={css`
|
||||
position: relative;
|
||||
border-radius: 50%;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
padding: 0 0.5px;
|
||||
${euiFocusRing(euiTheme)};
|
||||
`}
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
addGradientColorStep({
|
||||
color: {
|
||||
type: 'categorical',
|
||||
// TODO assign the next available color or a better one
|
||||
colorIndex: colorMode.steps.length,
|
||||
paletteId: currentPalette.id,
|
||||
},
|
||||
at,
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
colorPickerVisibility({
|
||||
index: at,
|
||||
type: 'gradient',
|
||||
visible: true,
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
css={css`
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 50%;
|
||||
transition: 200ms background-color;
|
||||
background-color: lightgrey;
|
||||
&:hover {
|
||||
background-color: #696f7d;
|
||||
}
|
||||
${euiShadowSmall(euiTheme)}
|
||||
`}
|
||||
>
|
||||
<EuiIcon
|
||||
type="plus"
|
||||
css={css`
|
||||
position: absolute;
|
||||
top: 0.5px;
|
||||
left: 0;
|
||||
transition: 200ms fill;
|
||||
&:hover {
|
||||
fill: white;
|
||||
}
|
||||
`}
|
||||
color={'#696f7d'}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function ColorStop({
|
||||
colorMode,
|
||||
step,
|
||||
index,
|
||||
currentPalette,
|
||||
getPaletteFn,
|
||||
isDarkMode,
|
||||
}: {
|
||||
colorMode: ColorMapping.GradientColorMode;
|
||||
step: ColorMapping.CategoricalColor | ColorMapping.ColorCode;
|
||||
index: number;
|
||||
currentPalette: ColorMapping.CategoricalPalette;
|
||||
getPaletteFn: ReturnType<typeof getPalette>;
|
||||
isDarkMode: boolean;
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
return (
|
||||
<ColorSwatch
|
||||
canPickColor={true}
|
||||
colorMode={colorMode}
|
||||
assignmentColor={step}
|
||||
getPaletteFn={getPaletteFn}
|
||||
index={index}
|
||||
palette={currentPalette}
|
||||
total={colorMode.steps.length}
|
||||
swatchShape="round"
|
||||
isDarkMode={isDarkMode}
|
||||
onColorChange={(color) => {
|
||||
dispatch(
|
||||
updateGradientColorStep({
|
||||
index,
|
||||
color,
|
||||
})
|
||||
);
|
||||
}}
|
||||
forType="gradient"
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,263 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import {
|
||||
EuiButtonGroup,
|
||||
EuiColorPalettePicker,
|
||||
EuiConfirmModal,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ScaleCategoricalIcon } from './scale_categorical';
|
||||
import { ScaleSequentialIcon } from './scale_sequential';
|
||||
|
||||
import { RootState, updatePalette } from '../../state/color_mapping';
|
||||
import { ColorMapping } from '../../config';
|
||||
import { updateAssignmentsPalette, updateColorModePalette } from '../../config/assignments';
|
||||
import { getPalette } from '../../palettes';
|
||||
|
||||
export function PaletteSelector({
|
||||
palettes,
|
||||
getPaletteFn,
|
||||
isDarkMode,
|
||||
}: {
|
||||
getPaletteFn: ReturnType<typeof getPalette>;
|
||||
palettes: Map<string, ColorMapping.CategoricalPalette>;
|
||||
isDarkMode: boolean;
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const colorMode = useSelector((state: RootState) => state.colorMapping.colorMode);
|
||||
const model = useSelector((state: RootState) => state.colorMapping);
|
||||
|
||||
const { paletteId } = model;
|
||||
|
||||
const switchPaletteFn = useCallback(
|
||||
(selectedPaletteId: string, preserveColorChanges: boolean) => {
|
||||
dispatch(
|
||||
updatePalette({
|
||||
paletteId: selectedPaletteId,
|
||||
assignments: updateAssignmentsPalette(
|
||||
model.assignments,
|
||||
model.assignmentMode,
|
||||
model.colorMode,
|
||||
selectedPaletteId,
|
||||
getPaletteFn,
|
||||
preserveColorChanges
|
||||
),
|
||||
colorMode: updateColorModePalette(
|
||||
model.colorMode,
|
||||
selectedPaletteId,
|
||||
preserveColorChanges
|
||||
),
|
||||
})
|
||||
);
|
||||
},
|
||||
[getPaletteFn, model, dispatch]
|
||||
);
|
||||
|
||||
const updateColorMode = useCallback(
|
||||
(type: 'gradient' | 'categorical', preserveColorChanges: boolean) => {
|
||||
const updatedColorMode: ColorMapping.Config['colorMode'] =
|
||||
type === 'gradient'
|
||||
? {
|
||||
type: 'gradient',
|
||||
steps: [
|
||||
{
|
||||
type: 'categorical',
|
||||
paletteId,
|
||||
colorIndex: 0,
|
||||
touched: false,
|
||||
},
|
||||
],
|
||||
sort: 'desc',
|
||||
}
|
||||
: { type: 'categorical' };
|
||||
|
||||
const assignments = updateAssignmentsPalette(
|
||||
model.assignments,
|
||||
model.assignmentMode,
|
||||
updatedColorMode,
|
||||
paletteId,
|
||||
getPaletteFn,
|
||||
preserveColorChanges
|
||||
);
|
||||
dispatch(updatePalette({ paletteId, assignments, colorMode: updatedColorMode }));
|
||||
},
|
||||
[getPaletteFn, model, dispatch, paletteId]
|
||||
);
|
||||
|
||||
const [preserveModalPaletteId, setPreserveModalPaletteId] = useState<string | null>(null);
|
||||
|
||||
const preserveChangesModal =
|
||||
preserveModalPaletteId !== null ? (
|
||||
<EuiConfirmModal
|
||||
title={i18n.translate('coloring.colorMapping.colorChangesModal.title', {
|
||||
defaultMessage: 'Color changes detected',
|
||||
})}
|
||||
onCancel={() => {
|
||||
if (preserveModalPaletteId) switchPaletteFn(preserveModalPaletteId, true);
|
||||
setPreserveModalPaletteId(null);
|
||||
}}
|
||||
onConfirm={() => {
|
||||
if (preserveModalPaletteId) switchPaletteFn(preserveModalPaletteId, false);
|
||||
setPreserveModalPaletteId(null);
|
||||
}}
|
||||
confirmButtonText={i18n.translate('coloring.colorMapping.colorChangesModal.discardButton', {
|
||||
defaultMessage: 'Discard changes',
|
||||
})}
|
||||
cancelButtonText={i18n.translate('coloring.colorMapping.colorChangesModal.preserveButton', {
|
||||
defaultMessage: 'Preserve changes',
|
||||
})}
|
||||
buttonColor="danger"
|
||||
defaultFocusedButton="confirm"
|
||||
>
|
||||
<p>
|
||||
{i18n.translate('coloring.colorMapping.colorChangesModal.switchPaletteDescription', {
|
||||
defaultMessage: 'Switching palette will discard all your custom color changes',
|
||||
})}
|
||||
</p>
|
||||
</EuiConfirmModal>
|
||||
) : null;
|
||||
|
||||
const [colorScaleModalId, setColorScaleModalId] = useState<'gradient' | 'categorical' | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const colorScaleModal =
|
||||
colorScaleModalId !== null ? (
|
||||
<EuiConfirmModal
|
||||
title={i18n.translate('coloring.colorMapping.colorChangesModal.modalTitle', {
|
||||
defaultMessage: 'Color changes detected',
|
||||
})}
|
||||
onCancel={() => {
|
||||
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"
|
||||
>
|
||||
<p>
|
||||
{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`,
|
||||
})}
|
||||
</p>
|
||||
</EuiConfirmModal>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{preserveChangesModal}
|
||||
{colorScaleModal}
|
||||
<EuiFlexGroup direction="row">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('coloring.colorMapping.paletteSelector.paletteLabel', {
|
||||
defaultMessage: `Color palette`,
|
||||
})}
|
||||
>
|
||||
<EuiColorPalettePicker
|
||||
data-test-subj="kbnColoring_ColorMapping_PalettePicker"
|
||||
palettes={[...palettes.values()]
|
||||
.filter((d) => 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}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={0}>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('coloring.colorMapping.paletteSelector.scaleLabel', {
|
||||
defaultMessage: `Scale`,
|
||||
})}
|
||||
>
|
||||
<EuiButtonGroup
|
||||
legend="Scale"
|
||||
data-test-subj="lns_colorMapping_scaleSwitch"
|
||||
options={[
|
||||
{
|
||||
id: `categorical`,
|
||||
label: i18n.translate('coloring.colorMapping.paletteSelector.categoricalLabel', {
|
||||
defaultMessage: `Categorical`,
|
||||
}),
|
||||
iconType: ScaleCategoricalIcon,
|
||||
},
|
||||
{
|
||||
id: `gradient`,
|
||||
label: i18n.translate('coloring.colorMapping.paletteSelector.sequentialLabel', {
|
||||
defaultMessage: `Sequential`,
|
||||
}),
|
||||
iconType: ScaleSequentialIcon,
|
||||
},
|
||||
]}
|
||||
isFullWidth
|
||||
buttonSize="compressed"
|
||||
idSelected={colorMode.type}
|
||||
onChange={(id) => {
|
||||
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
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
export function ScaleCategoricalIcon() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
||||
<path d="M4 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm2 2a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM5 6a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm4-2a1 1 0 1 1-2 0 1 1 0 0 1 2 0Zm2 2a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" />
|
||||
<path d="M8 1a7 7 0 0 0 0 14h2a2 2 0 1 0 0-4 1 1 0 1 1 0-2h3.98C14.515 9 15 8.583 15 8a7 7 0 0 0-7-7ZM2 8a6 6 0 0 1 12-.005.035.035 0 0 1-.02.005H10a2 2 0 1 0 0 4 1 1 0 1 1 0 2H8a6 6 0 0 1-6-6Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export function ScaleSequentialIcon() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M1 2a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2Zm4 0h1v12H5V2Zm3 12V2h2v12H8Zm3 0h3V2h-3v12Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ColorMapping } from '.';
|
||||
import { ColorMappingInputData } from '../categorical_color_mapping';
|
||||
import { MAX_ASSIGNABLE_COLORS } from '../components/container/container';
|
||||
|
||||
export function generateAutoAssignmentsForCategories(
|
||||
data: ColorMappingInputData,
|
||||
palette: ColorMapping.CategoricalPalette,
|
||||
colorMode: ColorMapping.Config['colorMode']
|
||||
): ColorMapping.Config['assignments'] {
|
||||
const isCategorical = colorMode.type === 'categorical';
|
||||
|
||||
const maxColorAssignable = data.type === 'categories' ? data.categories.length : data.bins;
|
||||
|
||||
const assignableColors = isCategorical
|
||||
? Math.min(palette.colorCount, maxColorAssignable)
|
||||
: Math.min(MAX_ASSIGNABLE_COLORS, maxColorAssignable);
|
||||
|
||||
const autoRules: Array<ColorMapping.RuleMatchExactly | ColorMapping.RuleRange> =
|
||||
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<ColorMapping.Config['assignments'][number]>((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;
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { ColorMapping } from '.';
|
||||
import { MAX_ASSIGNABLE_COLORS } from '../components/container/container';
|
||||
import { getPalette, NeutralPalette } from '../palettes';
|
||||
import { DEFAULT_NEUTRAL_PALETTE_INDEX } from './default_color_mapping';
|
||||
|
||||
export function updateAssignmentsPalette(
|
||||
assignments: ColorMapping.Config['assignments'],
|
||||
assignmentMode: ColorMapping.Config['assignmentMode'],
|
||||
colorMode: ColorMapping.Config['colorMode'],
|
||||
paletteId: string,
|
||||
getPaletteFn: ReturnType<typeof getPalette>,
|
||||
preserveColorChanges: boolean
|
||||
): ColorMapping.Config['assignments'] {
|
||||
const palette = getPaletteFn(paletteId);
|
||||
const maxColors = palette.type === 'categorical' ? palette.colorCount : MAX_ASSIGNABLE_COLORS;
|
||||
return assignmentMode === 'auto'
|
||||
? []
|
||||
: assignments.map(({ rule, color, touched }, index) => {
|
||||
if (preserveColorChanges && touched) {
|
||||
return { rule, color, touched };
|
||||
} else {
|
||||
const newColor: ColorMapping.Config['assignments'][number]['color'] =
|
||||
colorMode.type === 'categorical'
|
||||
? {
|
||||
type: 'categorical',
|
||||
paletteId: index < maxColors ? paletteId : NeutralPalette.id,
|
||||
colorIndex: index < maxColors ? index : 0,
|
||||
}
|
||||
: { type: 'gradient' };
|
||||
return {
|
||||
rule,
|
||||
color: newColor,
|
||||
touched: false,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function updateColorModePalette(
|
||||
colorMode: ColorMapping.Config['colorMode'],
|
||||
paletteId: string,
|
||||
preserveColorChanges: boolean
|
||||
): ColorMapping.Config['colorMode'] {
|
||||
return colorMode.type === 'categorical'
|
||||
? colorMode
|
||||
: {
|
||||
type: 'gradient',
|
||||
steps: colorMode.steps.map((step, stepIndex) => {
|
||||
return preserveColorChanges
|
||||
? step
|
||||
: { type: 'categorical', paletteId, colorIndex: stepIndex, touched: false };
|
||||
}),
|
||||
sort: colorMode.sort,
|
||||
};
|
||||
}
|
||||
|
||||
export function getUnusedColorForNewAssignment(
|
||||
palette: ColorMapping.CategoricalPalette,
|
||||
colorMode: ColorMapping.Config['colorMode'],
|
||||
assignments: ColorMapping.Config['assignments']
|
||||
): ColorMapping.Config['assignments'][number]['color'] {
|
||||
if (colorMode.type === 'categorical') {
|
||||
// TODO: change the type of color assignment depending on palette
|
||||
// compute the next unused color index in the palette.
|
||||
const maxColors = palette.type === 'categorical' ? palette.colorCount : MAX_ASSIGNABLE_COLORS;
|
||||
const colorIndices = new Set(Array.from({ length: maxColors }, (d, i) => i));
|
||||
assignments.forEach(({ color }) => {
|
||||
if (color.type === 'categorical' && color.paletteId === palette.id) {
|
||||
colorIndices.delete(color.colorIndex);
|
||||
}
|
||||
});
|
||||
const paletteForNextUnusedColorIndex = colorIndices.size > 0 ? palette.id : NeutralPalette.id;
|
||||
const nextUnusedColorIndex =
|
||||
colorIndices.size > 0 ? [...colorIndices][0] : DEFAULT_NEUTRAL_PALETTE_INDEX;
|
||||
return {
|
||||
type: 'categorical',
|
||||
paletteId: paletteForNextUnusedColorIndex,
|
||||
colorIndex: nextUnusedColorIndex,
|
||||
};
|
||||
} else {
|
||||
return { type: 'gradient' };
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ColorMapping } from '.';
|
||||
import { AVAILABLE_PALETTES, getPalette } from '../palettes';
|
||||
import { EUIAmsterdamColorBlindPalette } from '../palettes/eui_amsterdam';
|
||||
import { NeutralPalette } from '../palettes/neutral';
|
||||
import { getColor, getGradientColorScale } from '../color/color_handling';
|
||||
|
||||
export const DEFAULT_NEUTRAL_PALETTE_INDEX = 1;
|
||||
|
||||
/**
|
||||
* The default color mapping used in Kibana, starts with the EUI color palette
|
||||
*/
|
||||
export const DEFAULT_COLOR_MAPPING_CONFIG: ColorMapping.Config = {
|
||||
assignmentMode: 'auto',
|
||||
assignments: [],
|
||||
specialAssignments: [
|
||||
{
|
||||
rule: {
|
||||
type: 'other',
|
||||
},
|
||||
color: {
|
||||
type: 'categorical',
|
||||
paletteId: NeutralPalette.id,
|
||||
colorIndex: DEFAULT_NEUTRAL_PALETTE_INDEX,
|
||||
},
|
||||
touched: false,
|
||||
},
|
||||
],
|
||||
paletteId: EUIAmsterdamColorBlindPalette.id,
|
||||
colorMode: {
|
||||
type: 'categorical',
|
||||
},
|
||||
};
|
||||
|
||||
export function getPaletteColors(
|
||||
isDarkMode: boolean,
|
||||
colorMappings?: ColorMapping.Config
|
||||
): string[] {
|
||||
const colorMappingModel = colorMappings ?? { ...DEFAULT_COLOR_MAPPING_CONFIG };
|
||||
const palette = getPalette(AVAILABLE_PALETTES, NeutralPalette)(colorMappingModel.paletteId);
|
||||
return Array.from({ length: palette.colorCount }, (d, i) => palette.getColor(i, isDarkMode));
|
||||
}
|
||||
|
||||
export function getColorsFromMapping(
|
||||
isDarkMode: boolean,
|
||||
colorMappings?: ColorMapping.Config
|
||||
): string[] {
|
||||
const { colorMode, paletteId, assignmentMode, assignments, specialAssignments } =
|
||||
colorMappings ?? {
|
||||
...DEFAULT_COLOR_MAPPING_CONFIG,
|
||||
};
|
||||
|
||||
const getPaletteFn = getPalette(AVAILABLE_PALETTES, NeutralPalette);
|
||||
if (colorMode.type === 'gradient') {
|
||||
const colorScale = getGradientColorScale(colorMode, getPaletteFn, isDarkMode);
|
||||
return Array.from({ length: 6 }, (d, i) => colorScale(i / 6));
|
||||
} else {
|
||||
const palette = getPaletteFn(paletteId);
|
||||
if (assignmentMode === 'auto') {
|
||||
return Array.from({ length: palette.colorCount }, (d, i) => palette.getColor(i, isDarkMode));
|
||||
} else {
|
||||
return [
|
||||
...assignments.map((a) => {
|
||||
return a.color.type === 'gradient' ? '' : getColor(a.color, getPaletteFn, isDarkMode);
|
||||
}),
|
||||
...specialAssignments.map((a) => {
|
||||
return getColor(a.color, getPaletteFn, isDarkMode);
|
||||
}),
|
||||
].filter((color) => color !== '');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export * as ColorMapping from './types';
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A color specified as a CSS color datatype (rgb/a,hex,keywords,lab,lch etc)
|
||||
*/
|
||||
export interface ColorCode {
|
||||
type: 'colorCode';
|
||||
colorCode: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An index specified categorical color, coming from paletteId
|
||||
*/
|
||||
export interface CategoricalColor {
|
||||
type: 'categorical';
|
||||
paletteId: string;
|
||||
colorIndex: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that the Color in an Assignment needs to be taken from a gradient defined in the `Config.colorMode`
|
||||
*/
|
||||
export interface GradientColor {
|
||||
type: 'gradient';
|
||||
}
|
||||
|
||||
/**
|
||||
* A special rule that match automatically, in order, all the categories that are not matching a specified rule
|
||||
*/
|
||||
export interface RuleAuto {
|
||||
/* tag */
|
||||
type: 'auto';
|
||||
}
|
||||
/**
|
||||
* A rule that match exactly, case sensitive, with the provided strings
|
||||
*/
|
||||
export interface RuleMatchExactly {
|
||||
/* tag */
|
||||
type: 'matchExactly';
|
||||
values: Array<string | string[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A Match rule to match the values case insensitive
|
||||
* @ignore not used yet
|
||||
*/
|
||||
export interface RuleMatchExactlyCI {
|
||||
/* tag */
|
||||
type: 'matchExactlyCI';
|
||||
values: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A range rule, not used yet, but can be used for numerical data assignments
|
||||
*/
|
||||
export interface RuleRange {
|
||||
/* tag */
|
||||
type: 'range';
|
||||
/**
|
||||
* The min value of the range
|
||||
*/
|
||||
min: number;
|
||||
/**
|
||||
* The max value of the range
|
||||
*/
|
||||
max: number;
|
||||
/**
|
||||
* `true` if the range is left-closed (the `min` value is considered within the range), false otherwise (only values that are
|
||||
* greater than the `min` are considered within the range)
|
||||
*/
|
||||
minInclusive: boolean;
|
||||
/**
|
||||
* `true` if the range is right-closed (the `max` value is considered within the range), false otherwise (only values less than
|
||||
* the `max` are considered within the range)
|
||||
*/
|
||||
maxInclusive: boolean;
|
||||
}
|
||||
/**
|
||||
* Regex rule.
|
||||
* @ignore not used yet
|
||||
*/
|
||||
export interface RuleRegExp {
|
||||
/* tag */
|
||||
type: 'regex';
|
||||
/**
|
||||
* TODO: not sure how we can store a regexp
|
||||
*/
|
||||
values: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A specific catch-everything-else rule
|
||||
*/
|
||||
export interface RuleOthers {
|
||||
/* tag */
|
||||
type: 'other';
|
||||
}
|
||||
|
||||
/**
|
||||
* An assignment is the connection link between a rule and a color
|
||||
*/
|
||||
export interface Assignment<R, C> {
|
||||
/**
|
||||
* Describe the rule used to assign the color.
|
||||
*/
|
||||
rule: R;
|
||||
/**
|
||||
* The color definition
|
||||
*/
|
||||
color: C;
|
||||
|
||||
/**
|
||||
* Specify if the color was changed from the original one
|
||||
* TODO: rename
|
||||
*/
|
||||
touched: boolean;
|
||||
}
|
||||
|
||||
export interface CategoricalColorMode {
|
||||
type: 'categorical';
|
||||
}
|
||||
export interface GradientColorMode {
|
||||
type: 'gradient';
|
||||
steps: Array<(CategoricalColor | ColorCode) & { touched: boolean }>;
|
||||
sort: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
paletteId: string;
|
||||
colorMode: CategoricalColorMode | GradientColorMode;
|
||||
assignmentMode: 'auto' | 'manual';
|
||||
assignments: Array<
|
||||
Assignment<
|
||||
RuleAuto | RuleMatchExactly | RuleMatchExactlyCI | RuleRange | RuleRegExp,
|
||||
CategoricalColor | ColorCode | GradientColor
|
||||
>
|
||||
>;
|
||||
specialAssignments: Array<Assignment<RuleOthers, CategoricalColor | ColorCode>>;
|
||||
}
|
||||
|
||||
export interface CategoricalPalette {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'categorical';
|
||||
colorCount: number;
|
||||
getColor: (valueInRange: number, isDarkMode: boolean) => string;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { CategoricalColorMapping, type ColorMappingProps } from './categorical_color_mapping';
|
||||
export type { ColorMappingInputData } from './categorical_color_mapping';
|
||||
export type { ColorMapping } from './config';
|
||||
export * from './palettes';
|
||||
export * from './color/color_handling';
|
||||
export { SPECIAL_TOKENS_STRING_CONVERTION } from './color/rule_matching';
|
||||
export {
|
||||
DEFAULT_COLOR_MAPPING_CONFIG,
|
||||
getPaletteColors,
|
||||
getColorsFromMapping,
|
||||
} from './config/default_color_mapping';
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ColorMapping } from '../config';
|
||||
|
||||
export const ELASTIC_BRAND_PALETTE_COLORS = [
|
||||
'#20377d',
|
||||
'#7de2d1',
|
||||
'#ff957d',
|
||||
'#f04e98',
|
||||
'#0077cc',
|
||||
'#fec514',
|
||||
];
|
||||
|
||||
export const ElasticBrandPalette: ColorMapping.CategoricalPalette = {
|
||||
id: 'elastic_brand_2023',
|
||||
name: 'Elastic Brand',
|
||||
colorCount: ELASTIC_BRAND_PALETTE_COLORS.length,
|
||||
type: 'categorical',
|
||||
getColor(valueInRange) {
|
||||
return ELASTIC_BRAND_PALETTE_COLORS[valueInRange];
|
||||
},
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ColorMapping } from '../config';
|
||||
|
||||
export const EUI_AMSTERDAM_PALETTE_COLORS = [
|
||||
'#54b399',
|
||||
'#6092c0',
|
||||
'#d36086',
|
||||
'#9170b8',
|
||||
'#ca8eae',
|
||||
'#d6bf57',
|
||||
'#b9a888',
|
||||
'#da8b45',
|
||||
'#aa6556',
|
||||
'#e7664c',
|
||||
];
|
||||
|
||||
export const EUIAmsterdamColorBlindPalette: ColorMapping.CategoricalPalette = {
|
||||
id: 'eui_amsterdam_color_blind',
|
||||
name: 'Default',
|
||||
colorCount: EUI_AMSTERDAM_PALETTE_COLORS.length,
|
||||
type: 'categorical',
|
||||
getColor(valueInRange) {
|
||||
return EUI_AMSTERDAM_PALETTE_COLORS[valueInRange];
|
||||
},
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ColorMapping } from '../config';
|
||||
import { ElasticBrandPalette } from './elastic_brand';
|
||||
import { EUIAmsterdamColorBlindPalette } from './eui_amsterdam';
|
||||
import { KibanaV7LegacyPalette } from './kibana_legacy';
|
||||
import { NeutralPalette } from './neutral';
|
||||
|
||||
export const AVAILABLE_PALETTES = new Map<string, ColorMapping.CategoricalPalette>([
|
||||
[EUIAmsterdamColorBlindPalette.id, EUIAmsterdamColorBlindPalette],
|
||||
[ElasticBrandPalette.id, ElasticBrandPalette],
|
||||
[KibanaV7LegacyPalette.id, KibanaV7LegacyPalette],
|
||||
[NeutralPalette.id, NeutralPalette],
|
||||
]);
|
||||
|
||||
/**
|
||||
* This function should be instanciated once at the root of the component with the available palettes and
|
||||
* a choosed default one and shared across components to keep a single point of truth of the available palettes and the default
|
||||
* one.
|
||||
*/
|
||||
export function getPalette(
|
||||
palettes: Map<string, ColorMapping.CategoricalPalette>,
|
||||
defaultPalette: ColorMapping.CategoricalPalette
|
||||
): (paletteId: string) => ColorMapping.CategoricalPalette {
|
||||
return (paletteId) => palettes.get(paletteId) ?? defaultPalette;
|
||||
}
|
||||
|
||||
export * from './eui_amsterdam';
|
||||
export * from './elastic_brand';
|
||||
export * from './kibana_legacy';
|
||||
export * from './neutral';
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ColorMapping } from '../config';
|
||||
|
||||
export const KIBANA_V7_LEGACY_PALETTE_COLORS = [
|
||||
'#00a69b',
|
||||
'#57c17b',
|
||||
'#6f87d8',
|
||||
'#663db8',
|
||||
'#bc52bc',
|
||||
'#9e3533',
|
||||
'#daa05d',
|
||||
];
|
||||
|
||||
export const KibanaV7LegacyPalette: ColorMapping.CategoricalPalette = {
|
||||
id: 'kibana_v7_legacy',
|
||||
name: 'Kibana Legacy',
|
||||
colorCount: KIBANA_V7_LEGACY_PALETTE_COLORS.length,
|
||||
type: 'categorical',
|
||||
getColor(valueInRange) {
|
||||
return KIBANA_V7_LEGACY_PALETTE_COLORS[valueInRange];
|
||||
},
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ColorMapping } from '../config';
|
||||
|
||||
const schemeGreys = ['#f2f4fb', '#d4d9e5', '#98a2b3', '#696f7d', '#353642'];
|
||||
export const NEUTRAL_COLOR_LIGHT = schemeGreys.slice();
|
||||
export const NEUTRAL_COLOR_DARK = schemeGreys.slice().reverse();
|
||||
|
||||
export const NeutralPalette: ColorMapping.CategoricalPalette = {
|
||||
id: 'neutral',
|
||||
name: 'Neutral',
|
||||
colorCount: NEUTRAL_COLOR_LIGHT.length,
|
||||
type: 'categorical',
|
||||
getColor(valueInRange, isDarkMode) {
|
||||
return isDarkMode ? NEUTRAL_COLOR_DARK[valueInRange] : NEUTRAL_COLOR_LIGHT[valueInRange];
|
||||
},
|
||||
};
|
|
@ -0,0 +1,225 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import type { ColorMapping } from '../config';
|
||||
|
||||
export interface RootState {
|
||||
colorMapping: ColorMapping.Config;
|
||||
ui: {
|
||||
colorPicker: {
|
||||
index: number;
|
||||
visibile: boolean;
|
||||
type: 'gradient' | 'assignment' | 'specialAssignment';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const initialState: RootState['colorMapping'] = {
|
||||
assignmentMode: 'auto',
|
||||
assignments: [],
|
||||
specialAssignments: [],
|
||||
paletteId: 'eui',
|
||||
colorMode: { type: 'categorical' },
|
||||
};
|
||||
|
||||
export const colorMappingSlice = createSlice({
|
||||
name: 'colorMapping',
|
||||
initialState,
|
||||
reducers: {
|
||||
updateModel: (state, action: PayloadAction<ColorMapping.Config>) => {
|
||||
state.assignmentMode = action.payload.assignmentMode;
|
||||
state.assignments = [...action.payload.assignments];
|
||||
state.specialAssignments = [...action.payload.specialAssignments];
|
||||
state.paletteId = action.payload.paletteId;
|
||||
state.colorMode = { ...action.payload.colorMode };
|
||||
},
|
||||
updatePalette: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
assignments: ColorMapping.Config['assignments'];
|
||||
paletteId: ColorMapping.Config['paletteId'];
|
||||
colorMode: ColorMapping.Config['colorMode'];
|
||||
}>
|
||||
) => {
|
||||
state.paletteId = action.payload.paletteId;
|
||||
state.assignments = [...action.payload.assignments];
|
||||
state.colorMode = { ...action.payload.colorMode };
|
||||
},
|
||||
assignStatically: (state, action: PayloadAction<ColorMapping.Config['assignments']>) => {
|
||||
state.assignmentMode = 'manual';
|
||||
state.assignments = [...action.payload];
|
||||
},
|
||||
assignAutomatically: (state) => {
|
||||
state.assignmentMode = 'auto';
|
||||
state.assignments = [];
|
||||
},
|
||||
|
||||
addNewAssignment: (
|
||||
state,
|
||||
action: PayloadAction<ColorMapping.Config['assignments'][number]>
|
||||
) => {
|
||||
state.assignments.push({ ...action.payload });
|
||||
},
|
||||
updateAssignment: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
assignmentIndex: number;
|
||||
assignment: ColorMapping.Config['assignments'][number];
|
||||
}>
|
||||
) => {
|
||||
state.assignments[action.payload.assignmentIndex] = {
|
||||
...action.payload.assignment,
|
||||
touched: true,
|
||||
};
|
||||
},
|
||||
updateAssignmentRule: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
assignmentIndex: number;
|
||||
rule: ColorMapping.Config['assignments'][number]['rule'];
|
||||
}>
|
||||
) => {
|
||||
state.assignments[action.payload.assignmentIndex] = {
|
||||
...state.assignments[action.payload.assignmentIndex],
|
||||
rule: action.payload.rule,
|
||||
};
|
||||
},
|
||||
updateAssignmentColor: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
assignmentIndex: number;
|
||||
color: ColorMapping.Config['assignments'][number]['color'];
|
||||
}>
|
||||
) => {
|
||||
state.assignments[action.payload.assignmentIndex] = {
|
||||
...state.assignments[action.payload.assignmentIndex],
|
||||
color: action.payload.color,
|
||||
touched: true,
|
||||
};
|
||||
},
|
||||
|
||||
updateSpecialAssignmentColor: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
assignmentIndex: number;
|
||||
color: ColorMapping.Config['specialAssignments'][number]['color'];
|
||||
}>
|
||||
) => {
|
||||
state.specialAssignments[action.payload.assignmentIndex] = {
|
||||
...state.specialAssignments[action.payload.assignmentIndex],
|
||||
color: action.payload.color,
|
||||
touched: true,
|
||||
};
|
||||
},
|
||||
removeAssignment: (state, action: PayloadAction<number>) => {
|
||||
state.assignments.splice(action.payload, 1);
|
||||
},
|
||||
changeColorMode: (state, action: PayloadAction<ColorMapping.Config['colorMode']>) => {
|
||||
state.colorMode = { ...action.payload };
|
||||
},
|
||||
updateGradientColorStep: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
index: number;
|
||||
color: ColorMapping.CategoricalColor | ColorMapping.ColorCode;
|
||||
}>
|
||||
) => {
|
||||
if (state.colorMode.type !== 'gradient') {
|
||||
return;
|
||||
}
|
||||
|
||||
state.colorMode = {
|
||||
...state.colorMode,
|
||||
steps: state.colorMode.steps.map((step, index) => {
|
||||
return index === action.payload.index
|
||||
? { ...action.payload.color, touched: true }
|
||||
: { ...step, touched: false };
|
||||
}),
|
||||
};
|
||||
},
|
||||
removeGradientColorStep: (state, action: PayloadAction<number>) => {
|
||||
if (state.colorMode.type !== 'gradient') {
|
||||
return;
|
||||
}
|
||||
const steps = [...state.colorMode.steps];
|
||||
steps.splice(action.payload, 1);
|
||||
|
||||
// this maintain the correct sort direciton depending on which step
|
||||
// gets removed from the array when only 2 steps are left.
|
||||
const sort =
|
||||
state.colorMode.steps.length === 2
|
||||
? state.colorMode.sort === 'desc'
|
||||
? action.payload === 0
|
||||
? 'asc'
|
||||
: 'desc'
|
||||
: action.payload === 0
|
||||
? 'desc'
|
||||
: 'asc'
|
||||
: state.colorMode.sort;
|
||||
|
||||
state.colorMode = {
|
||||
...state.colorMode,
|
||||
steps: [...steps],
|
||||
sort,
|
||||
};
|
||||
},
|
||||
addGradientColorStep: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
color: ColorMapping.CategoricalColor | ColorMapping.ColorCode;
|
||||
at: number;
|
||||
}>
|
||||
) => {
|
||||
if (state.colorMode.type !== 'gradient') {
|
||||
return;
|
||||
}
|
||||
|
||||
state.colorMode = {
|
||||
...state.colorMode,
|
||||
steps: [
|
||||
...state.colorMode.steps.slice(0, action.payload.at),
|
||||
{ ...action.payload.color, touched: false },
|
||||
...state.colorMode.steps.slice(action.payload.at),
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
changeGradientSortOrder: (state, action: PayloadAction<'asc' | 'desc'>) => {
|
||||
if (state.colorMode.type !== 'gradient') {
|
||||
return;
|
||||
}
|
||||
|
||||
state.colorMode = {
|
||||
...state.colorMode,
|
||||
sort: action.payload,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
// Action creators are generated for each case reducer function
|
||||
export const {
|
||||
updatePalette,
|
||||
assignStatically,
|
||||
assignAutomatically,
|
||||
addNewAssignment,
|
||||
updateAssignment,
|
||||
updateAssignmentColor,
|
||||
updateSpecialAssignmentColor,
|
||||
updateAssignmentRule,
|
||||
removeAssignment,
|
||||
changeColorMode,
|
||||
updateGradientColorStep,
|
||||
removeGradientColorStep,
|
||||
addGradientColorStep,
|
||||
changeGradientSortOrder,
|
||||
updateModel,
|
||||
} = colorMappingSlice.actions;
|
||||
|
||||
export const colorMappingReducer = colorMappingSlice.reducer;
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { getPalette } from '../palettes';
|
||||
import { RootState } from './color_mapping';
|
||||
|
||||
export function selectPalette(getPaletteFn: ReturnType<typeof getPalette>) {
|
||||
return (state: RootState) => getPaletteFn(state.colorMapping.paletteId);
|
||||
}
|
||||
export function selectColorMode(state: RootState) {
|
||||
return state.colorMapping.colorMode;
|
||||
}
|
||||
export function selectSpecialAssignments(state: RootState) {
|
||||
return state.colorMapping.specialAssignments;
|
||||
}
|
||||
export function selectIsAutoAssignmentMode(state: RootState) {
|
||||
return state.colorMapping.assignmentMode === 'auto';
|
||||
}
|
||||
export function selectColorPickerVisibility(state: RootState) {
|
||||
return state.ui.colorPicker;
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { type PayloadAction, createSlice } from '@reduxjs/toolkit';
|
||||
import { RootState } from './color_mapping';
|
||||
|
||||
const initialState: RootState['ui'] = {
|
||||
colorPicker: {
|
||||
index: 0,
|
||||
visibile: false,
|
||||
type: 'assignment',
|
||||
},
|
||||
};
|
||||
|
||||
export const uiSlice = createSlice({
|
||||
name: 'colorMapping',
|
||||
initialState,
|
||||
reducers: {
|
||||
colorPickerVisibility: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
index: number;
|
||||
type: RootState['ui']['colorPicker']['type'];
|
||||
visible: boolean;
|
||||
}>
|
||||
) => {
|
||||
state.colorPicker.visibile = action.payload.visible;
|
||||
state.colorPicker.index = action.payload.index;
|
||||
state.colorPicker.type = action.payload.type;
|
||||
},
|
||||
switchColorPickerVisibility: (state) => {
|
||||
state.colorPicker.visibile = !state.colorPicker.visibile;
|
||||
},
|
||||
showColorPickerVisibility: (state) => {
|
||||
state.colorPicker.visibile = true;
|
||||
},
|
||||
hideColorPickerVisibility: (state) => {
|
||||
state.colorPicker.visibile = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
colorPickerVisibility,
|
||||
switchColorPickerVisibility,
|
||||
showColorPickerVisibility,
|
||||
hideColorPickerVisibility,
|
||||
} = uiSlice.actions;
|
||||
|
||||
export const uiReducer = uiSlice.reducer;
|
|
@ -21,3 +21,5 @@ export const CustomizablePaletteLazy = React.lazy(() => import('./coloring'));
|
|||
* a predefined fallback and error boundary.
|
||||
*/
|
||||
export const CustomizablePalette = withSuspense(CustomizablePaletteLazy);
|
||||
|
||||
export * from './color_mapping';
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.scss",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
|
@ -21,6 +20,8 @@
|
|||
"@kbn/utility-types",
|
||||
"@kbn/shared-ux-utility",
|
||||
"@kbn/test-jest-helpers",
|
||||
"@kbn/data-plugin",
|
||||
"@kbn/ui-theme",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -50,13 +50,13 @@ pageLoadAssetSize:
|
|||
expressionLegacyMetricVis: 23121
|
||||
expressionMetric: 22238
|
||||
expressionMetricVis: 23121
|
||||
expressionPartitionVis: 26338
|
||||
expressionPartitionVis: 28000
|
||||
expressionRepeatImage: 22341
|
||||
expressionRevealImage: 25675
|
||||
expressions: 140958
|
||||
expressionShape: 34008
|
||||
expressionTagcloud: 27505
|
||||
expressionXY: 39500
|
||||
expressionXY: 45000
|
||||
features: 21723
|
||||
fieldFormats: 65209
|
||||
files: 22673
|
||||
|
|
41
src/plugins/chart_expressions/common/color_categories.ts
Normal file
41
src/plugins/chart_expressions/common/color_categories.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { DatatableRow } from '@kbn/expressions-plugin/common';
|
||||
import { isMultiFieldKey } from '@kbn/data-plugin/common';
|
||||
|
||||
/**
|
||||
* Get the stringified version of all the categories that needs to be colored in the chart.
|
||||
* Multifield keys will return as array of string and simple fields (numeric, string) will be returned as a plain unformatted string.
|
||||
*/
|
||||
export function getColorCategories(
|
||||
rows: DatatableRow[],
|
||||
accessor?: string
|
||||
): Array<string | string[]> {
|
||||
return accessor
|
||||
? rows.reduce<{ keys: Set<string>; categories: Array<string | string[]> }>(
|
||||
(acc, r) => {
|
||||
const value = r[accessor];
|
||||
if (value === undefined) {
|
||||
return acc;
|
||||
}
|
||||
// The categories needs to be stringified in their unformatted version.
|
||||
// We can't distinguish between a number and a string from a text input and the match should
|
||||
// work with both numeric field values and string values.
|
||||
const key = (isMultiFieldKey(value) ? [...value.keys] : [value]).map(String);
|
||||
const stringifiedKeys = key.join(',');
|
||||
if (!acc.keys.has(stringifiedKeys)) {
|
||||
acc.keys.add(stringifiedKeys);
|
||||
acc.categories.push(key.length === 1 ? key[0] : key);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{ keys: new Set(), categories: [] }
|
||||
).categories
|
||||
: [];
|
||||
}
|
|
@ -13,3 +13,4 @@ export {
|
|||
isOnAggBasedEditor,
|
||||
} from './utils';
|
||||
export type { Simplify, MakeOverridesSerializable } from './types';
|
||||
export { getColorCategories } from './color_categories';
|
||||
|
|
|
@ -15,5 +15,7 @@
|
|||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core-execution-context-common",
|
||||
"@kbn/expressions-plugin",
|
||||
"@kbn/data-plugin",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
"fieldFormats",
|
||||
"charts",
|
||||
"visualizations",
|
||||
"presentationUtil"
|
||||
"presentationUtil",
|
||||
"data"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"usageCollection"
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
"fieldFormats",
|
||||
"charts",
|
||||
"visualizations",
|
||||
"presentationUtil"
|
||||
"presentationUtil",
|
||||
"data"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"usageCollection"
|
||||
|
|
|
@ -71,6 +71,7 @@ Object {
|
|||
"type": "vis_dimension",
|
||||
},
|
||||
],
|
||||
"colorMapping": undefined,
|
||||
"dimensions": Object {
|
||||
"buckets": Array [
|
||||
Object {
|
||||
|
|
|
@ -61,6 +61,7 @@ Object {
|
|||
"type": "vis_dimension",
|
||||
},
|
||||
],
|
||||
"colorMapping": undefined,
|
||||
"dimensions": Object {
|
||||
"buckets": Array [
|
||||
Object {
|
||||
|
@ -203,6 +204,7 @@ Object {
|
|||
"type": "vis_dimension",
|
||||
},
|
||||
],
|
||||
"colorMapping": undefined,
|
||||
"dimensions": Object {
|
||||
"buckets": Array [
|
||||
Object {
|
||||
|
|
|
@ -71,6 +71,7 @@ Object {
|
|||
"type": "vis_dimension",
|
||||
},
|
||||
],
|
||||
"colorMapping": undefined,
|
||||
"dimensions": Object {
|
||||
"buckets": Array [
|
||||
Object {
|
||||
|
|
|
@ -53,6 +53,7 @@ Object {
|
|||
},
|
||||
"type": "vis_dimension",
|
||||
},
|
||||
"colorMapping": undefined,
|
||||
"dimensions": Object {
|
||||
"buckets": Array [
|
||||
Object {
|
||||
|
|
|
@ -122,6 +122,10 @@ export const strings = {
|
|||
i18n.translate('expressionPartitionVis.reusable.function.dimension.splitrow', {
|
||||
defaultMessage: 'Row split',
|
||||
}),
|
||||
getColorMappingHelp: () =>
|
||||
i18n.translate('expressionPartitionVis.layer.colorMapping.help', {
|
||||
defaultMessage: 'JSON key-value pairs of the color mapping model',
|
||||
}),
|
||||
};
|
||||
|
||||
export const errors = {
|
||||
|
|
|
@ -110,6 +110,10 @@ export const mosaicVisFunction = (): MosaicVisExpressionFunctionDefinition => ({
|
|||
help: strings.getAriaLabelHelp(),
|
||||
required: false,
|
||||
},
|
||||
colorMapping: {
|
||||
types: ['string'],
|
||||
help: strings.getColorMappingHelp(),
|
||||
},
|
||||
},
|
||||
fn(context, args, handlers) {
|
||||
const maxSupportedBuckets = 2;
|
||||
|
@ -146,6 +150,7 @@ export const mosaicVisFunction = (): MosaicVisExpressionFunctionDefinition => ({
|
|||
splitColumn: args.splitColumn,
|
||||
splitRow: args.splitRow,
|
||||
},
|
||||
colorMapping: args.colorMapping,
|
||||
};
|
||||
|
||||
if (handlers?.inspectorAdapters?.tables) {
|
||||
|
|
|
@ -141,6 +141,10 @@ export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({
|
|||
help: strings.getAriaLabelHelp(),
|
||||
required: false,
|
||||
},
|
||||
colorMapping: {
|
||||
types: ['string'],
|
||||
help: strings.getColorMappingHelp(),
|
||||
},
|
||||
},
|
||||
fn(context, args, handlers) {
|
||||
if (args.splitColumn && args.splitRow) {
|
||||
|
@ -173,6 +177,7 @@ export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({
|
|||
splitColumn: args.splitColumn,
|
||||
splitRow: args.splitRow,
|
||||
},
|
||||
colorMapping: args.colorMapping,
|
||||
};
|
||||
|
||||
if (handlers?.inspectorAdapters?.tables) {
|
||||
|
|
|
@ -115,6 +115,10 @@ export const treemapVisFunction = (): TreemapVisExpressionFunctionDefinition =>
|
|||
help: strings.getAriaLabelHelp(),
|
||||
required: false,
|
||||
},
|
||||
colorMapping: {
|
||||
types: ['string'],
|
||||
help: strings.getColorMappingHelp(),
|
||||
},
|
||||
},
|
||||
fn(context, args, handlers) {
|
||||
const maxSupportedBuckets = 2;
|
||||
|
@ -152,6 +156,7 @@ export const treemapVisFunction = (): TreemapVisExpressionFunctionDefinition =>
|
|||
splitColumn: args.splitColumn,
|
||||
splitRow: args.splitRow,
|
||||
},
|
||||
colorMapping: args.colorMapping,
|
||||
};
|
||||
|
||||
if (handlers?.inspectorAdapters?.tables) {
|
||||
|
|
|
@ -114,6 +114,10 @@ export const waffleVisFunction = (): WaffleVisExpressionFunctionDefinition => ({
|
|||
help: strings.getAriaLabelHelp(),
|
||||
required: false,
|
||||
},
|
||||
colorMapping: {
|
||||
types: ['string'],
|
||||
help: strings.getColorMappingHelp(),
|
||||
},
|
||||
},
|
||||
fn(context, args, handlers) {
|
||||
if (args.splitColumn && args.splitRow) {
|
||||
|
@ -147,6 +151,7 @@ export const waffleVisFunction = (): WaffleVisExpressionFunctionDefinition => ({
|
|||
splitColumn: args.splitColumn,
|
||||
splitRow: args.splitRow,
|
||||
},
|
||||
colorMapping: args.colorMapping,
|
||||
};
|
||||
|
||||
if (handlers?.inspectorAdapters?.tables) {
|
||||
|
|
|
@ -61,6 +61,7 @@ interface VisCommonParams {
|
|||
maxLegendLines: number;
|
||||
legendSize?: LegendSize;
|
||||
ariaLabel?: string;
|
||||
colorMapping?: string; // JSON stringified object of the color mapping
|
||||
}
|
||||
|
||||
interface VisCommonConfig extends VisCommonParams {
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { NodeColorAccessor, PATH_KEY } from '@elastic/charts';
|
||||
import { lightenColor } from '@kbn/charts-plugin/public';
|
||||
import { MultiFieldKey } from '@kbn/data-plugin/common';
|
||||
import { getColorFactory } from '@kbn/coloring';
|
||||
import { isMultiFieldKey } from '@kbn/data-plugin/common';
|
||||
import { ChartTypes } from '../../../common/types';
|
||||
|
||||
export function getCategoryKeys(category: string | MultiFieldKey): string | string[] {
|
||||
return isMultiFieldKey(category) ? category.keys.map(String) : `${category}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the color of a specific slice/section in Pie,donut,waffle and treemap.
|
||||
* These chart type shares the same color assignment mechanism.
|
||||
*/
|
||||
const getPieFillColor =
|
||||
(
|
||||
layerIndex: number,
|
||||
numOfLayers: number,
|
||||
getColorFn: ReturnType<typeof getColorFactory>
|
||||
): NodeColorAccessor =>
|
||||
(_key, _sortIndex, node) => {
|
||||
const path = node[PATH_KEY];
|
||||
// the category used to color the pie/donut is at the third level of the path
|
||||
// first two are: small multiple and pie whole center.
|
||||
const category = getCategoryKeys(path[2].value);
|
||||
const color = getColorFn(category);
|
||||
// increase the lightness of the color on each layer.
|
||||
return lightenColor(color, layerIndex + 1, numOfLayers);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the color of a section in a Mosaic chart.
|
||||
* This chart has a slight variation in the way color are applied. Mosaic can represent up to 2 layers,
|
||||
* described in lens as the horizontal and vertical axes.
|
||||
* With a single layer the color is simply applied per each category, with 2 layer, the color is applied only
|
||||
* to the category that describe a row, not by column.
|
||||
*/
|
||||
const getMosaicFillColor =
|
||||
(
|
||||
layerIndex: number,
|
||||
numOfLayers: number,
|
||||
getColorFn: ReturnType<typeof getColorFactory>
|
||||
): NodeColorAccessor =>
|
||||
(_key, _sortIndex, node) => {
|
||||
// Special case for 2 layer mosaic where the color is per rows and the columns are not colored
|
||||
if (numOfLayers === 2 && layerIndex === 0) {
|
||||
// transparent color will fallback to the kibana/context background
|
||||
return 'rgba(0,0,0,0)';
|
||||
}
|
||||
const path = node[PATH_KEY];
|
||||
|
||||
// the category used to color the pie/donut is at the third level of the `path` when using a single layer mosaic
|
||||
// and are at fourth level of `path` when using 2 layer mosaic
|
||||
// first two are: small multiple and pie whole center.
|
||||
const category = getCategoryKeys(numOfLayers === 2 ? path[3].value : path[2].value);
|
||||
return getColorFn(category);
|
||||
};
|
||||
|
||||
export const getPartitionFillColor = (
|
||||
chartType: ChartTypes,
|
||||
layerIndex: number,
|
||||
numOfLayers: number,
|
||||
getColorFn: ReturnType<typeof getColorFactory>
|
||||
): NodeColorAccessor => {
|
||||
return chartType === ChartTypes.MOSAIC
|
||||
? getMosaicFillColor(layerIndex, numOfLayers, getColorFn)
|
||||
: getPieFillColor(layerIndex, numOfLayers, getColorFn);
|
||||
};
|
|
@ -7,16 +7,25 @@
|
|||
*/
|
||||
|
||||
import { Datum, PartitionLayer } from '@elastic/charts';
|
||||
import type { PaletteRegistry } from '@kbn/coloring';
|
||||
import {
|
||||
PaletteRegistry,
|
||||
getColorFactory,
|
||||
getPalette,
|
||||
AVAILABLE_PALETTES,
|
||||
NeutralPalette,
|
||||
} from '@kbn/coloring';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FieldFormat } from '@kbn/field-formats-plugin/common';
|
||||
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import type { Datatable, DatatableRow } from '@kbn/expressions-plugin/public';
|
||||
|
||||
import { getColorCategories } from '@kbn/chart-expressions-common';
|
||||
import { getDistinctSeries } from '..';
|
||||
import { BucketColumns, ChartTypes, PartitionVisParams } from '../../../common/types';
|
||||
import { sortPredicateByType, sortPredicateSaveSourceOrder } from './sort_predicate';
|
||||
import { byDataColorPaletteMap, getColor } from './get_color';
|
||||
import { getNodeLabel } from './get_node_labels';
|
||||
import { getPartitionFillColor } from '../colors/color_mapping_accessors';
|
||||
|
||||
// This is particularly useful in case of a text based languages where
|
||||
// it's no possible to use a missingBucketLabel
|
||||
|
@ -62,6 +71,15 @@ export const getLayers = (
|
|||
|
||||
const distinctSeries = getDistinctSeries(rows, columns);
|
||||
|
||||
// return a fn only if color mapping is available in visParams
|
||||
const getColorFromMappingFn = getColorFromMappingFactory(
|
||||
chartType,
|
||||
columns,
|
||||
rows,
|
||||
isDarkMode,
|
||||
visParams
|
||||
);
|
||||
|
||||
return columns.map((col, layerIndex) => {
|
||||
return {
|
||||
groupByRollup: (d: Datum) => (col.id ? d[col.id] ?? emptySliceLabel : col.name),
|
||||
|
@ -75,26 +93,74 @@ export const getLayers = (
|
|||
? sortPredicateSaveSourceOrder()
|
||||
: sortPredicateForType,
|
||||
shape: {
|
||||
fillColor: (key, sortIndex, node) =>
|
||||
getColor(
|
||||
chartType,
|
||||
key,
|
||||
node,
|
||||
layerIndex,
|
||||
isSplitChart,
|
||||
overwriteColors,
|
||||
distinctSeries,
|
||||
{ columnsLength: columns.length, rowsLength: rows.length },
|
||||
visParams,
|
||||
palettes,
|
||||
byDataPalette,
|
||||
syncColors,
|
||||
isDarkMode,
|
||||
formatter,
|
||||
col,
|
||||
formatters
|
||||
),
|
||||
// this applies color mapping only if visParams.colorMapping is available
|
||||
fillColor: getColorFromMappingFn
|
||||
? getPartitionFillColor(chartType, layerIndex, columns.length, getColorFromMappingFn)
|
||||
: (key, sortIndex, node) =>
|
||||
getColor(
|
||||
chartType,
|
||||
key,
|
||||
node,
|
||||
layerIndex,
|
||||
isSplitChart,
|
||||
overwriteColors,
|
||||
distinctSeries,
|
||||
{ columnsLength: columns.length, rowsLength: rows.length },
|
||||
visParams,
|
||||
palettes,
|
||||
byDataPalette,
|
||||
syncColors,
|
||||
isDarkMode,
|
||||
formatter,
|
||||
col,
|
||||
formatters
|
||||
),
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* If colorMapping is available, returns a function that accept a string or an array of strings (used in case of multi-field-key)
|
||||
* and returns a color specified in the provided mapping
|
||||
*/
|
||||
function getColorFromMappingFactory(
|
||||
chartType: ChartTypes,
|
||||
columns: Array<Partial<BucketColumns>>,
|
||||
rows: DatatableRow[],
|
||||
isDarkMode: boolean,
|
||||
visParams: PartitionVisParams
|
||||
): undefined | ((category: string | string[]) => string) {
|
||||
const { colorMapping, dimensions } = visParams;
|
||||
|
||||
if (!colorMapping) {
|
||||
// return undefined, we will use the legacy color mapping instead
|
||||
return undefined;
|
||||
}
|
||||
// if pie/donut/treemap with no buckets use the default color mode
|
||||
if (
|
||||
(chartType === ChartTypes.DONUT ||
|
||||
chartType === ChartTypes.PIE ||
|
||||
chartType === ChartTypes.TREEMAP) &&
|
||||
(!dimensions.buckets || dimensions.buckets?.length === 0)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
// the mosaic configures the main categories in the second column, instead of the first
|
||||
// as it happens in all the other partition types.
|
||||
// Independentely from the bucket aggregation used, the categories will always be casted
|
||||
// as string to make it nicely working with a text input field, avoiding a field
|
||||
const categories =
|
||||
chartType === ChartTypes.MOSAIC && columns.length === 2
|
||||
? getColorCategories(rows, columns[1]?.id)
|
||||
: getColorCategories(rows, columns[0]?.id);
|
||||
return getColorFactory(
|
||||
JSON.parse(colorMapping),
|
||||
getPalette(AVAILABLE_PALETTES, NeutralPalette),
|
||||
isDarkMode,
|
||||
{
|
||||
type: 'categories',
|
||||
categories,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -66,6 +66,7 @@ Object {
|
|||
"bucket": Object {
|
||||
"accessor": 1,
|
||||
},
|
||||
"colorMapping": undefined,
|
||||
"isPreview": false,
|
||||
"maxFontSize": 72,
|
||||
"metric": Object {
|
||||
|
@ -126,6 +127,7 @@ Object {
|
|||
},
|
||||
"type": "vis_dimension",
|
||||
},
|
||||
"colorMapping": undefined,
|
||||
"isPreview": false,
|
||||
"maxFontSize": 72,
|
||||
"metric": Object {
|
||||
|
|
|
@ -51,6 +51,9 @@ const strings = {
|
|||
isPreview: i18n.translate('expressionTagcloud.functions.tagcloud.args.isPreviewHelpText', {
|
||||
defaultMessage: 'Set isPreview to true to avoid showing out of room warnings',
|
||||
}),
|
||||
colorMapping: i18n.translate('expressionTagcloud.layer.colorMapping.help', {
|
||||
defaultMessage: 'JSON key-value pairs of the color mapping model',
|
||||
}),
|
||||
},
|
||||
dimension: {
|
||||
tags: i18n.translate('expressionTagcloud.functions.tagcloud.dimension.tags', {
|
||||
|
@ -146,6 +149,10 @@ export const tagcloudFunction: ExpressionTagcloudFunction = () => {
|
|||
default: false,
|
||||
required: false,
|
||||
},
|
||||
colorMapping: {
|
||||
types: ['string'],
|
||||
help: argHelp.colorMapping,
|
||||
},
|
||||
},
|
||||
fn(input, args, handlers) {
|
||||
validateAccessor(args.metric, input.columns);
|
||||
|
@ -167,6 +174,7 @@ export const tagcloudFunction: ExpressionTagcloudFunction = () => {
|
|||
(handlers.variables?.embeddableTitle as string) ??
|
||||
handlers.getExecutionContext?.()?.description,
|
||||
isPreview: Boolean(args.isPreview),
|
||||
colorMapping: args.colorMapping,
|
||||
};
|
||||
|
||||
if (handlers?.inspectorAdapters?.tables) {
|
||||
|
|
|
@ -27,6 +27,7 @@ interface TagCloudCommonParams {
|
|||
metric: ExpressionValueVisDimension | string;
|
||||
bucket?: ExpressionValueVisDimension | string;
|
||||
palette: PaletteOutput;
|
||||
colorMapping?: string; // JSON stringified object of the color mapping
|
||||
}
|
||||
|
||||
export interface TagCloudVisConfig extends TagCloudCommonParams {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"server": true,
|
||||
"browser": true,
|
||||
"requiredPlugins": [
|
||||
"data",
|
||||
"expressions",
|
||||
"visualizations",
|
||||
"charts",
|
||||
|
|
|
@ -105,6 +105,7 @@ describe('TagCloudChart', function () {
|
|||
renderComplete: jest.fn(),
|
||||
syncColors: false,
|
||||
visType: 'tagcloud',
|
||||
isDarkMode: false,
|
||||
};
|
||||
|
||||
wrapperPropsWithColumnNames = {
|
||||
|
@ -135,6 +136,7 @@ describe('TagCloudChart', function () {
|
|||
renderComplete: jest.fn(),
|
||||
syncColors: false,
|
||||
visType: 'tagcloud',
|
||||
isDarkMode: false,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -13,11 +13,19 @@ import { EuiIconTip, EuiResizeObserver } from '@elastic/eui';
|
|||
import { IconChartTagcloud } from '@kbn/chart-icons';
|
||||
import { Chart, Settings, Wordcloud, RenderChangeListener } from '@elastic/charts';
|
||||
import { EmptyPlaceholder } from '@kbn/charts-plugin/public';
|
||||
import type { PaletteRegistry, PaletteOutput } from '@kbn/coloring';
|
||||
import { IInterpreterRenderHandlers } from '@kbn/expressions-plugin/public';
|
||||
import { getOverridesFor } from '@kbn/chart-expressions-common';
|
||||
import {
|
||||
PaletteRegistry,
|
||||
PaletteOutput,
|
||||
getColorFactory,
|
||||
getPalette,
|
||||
AVAILABLE_PALETTES,
|
||||
NeutralPalette,
|
||||
} from '@kbn/coloring';
|
||||
import { IInterpreterRenderHandlers, DatatableRow } from '@kbn/expressions-plugin/public';
|
||||
import { getColorCategories, getOverridesFor } from '@kbn/chart-expressions-common';
|
||||
import type { AllowedSettingsOverrides, AllowedChartOverrides } from '@kbn/charts-plugin/common';
|
||||
import { getColumnByAccessor, getFormatByAccessor } from '@kbn/visualizations-plugin/common/utils';
|
||||
import { isMultiFieldKey } from '@kbn/data-plugin/common';
|
||||
import { getFormatService } from '../format_service';
|
||||
import { TagcloudRendererConfig } from '../../common/types';
|
||||
import { ScaleOptions, Orientation } from '../../common/constants';
|
||||
|
@ -31,6 +39,7 @@ export type TagCloudChartProps = TagcloudRendererConfig & {
|
|||
renderComplete: IInterpreterRenderHandlers['done'];
|
||||
palettesRegistry: PaletteRegistry;
|
||||
overrides?: AllowedSettingsOverrides & AllowedChartOverrides;
|
||||
isDarkMode: boolean;
|
||||
};
|
||||
|
||||
const calculateWeight = (value: number, x1: number, y1: number, x2: number, y2: number) =>
|
||||
|
@ -84,9 +93,10 @@ export const TagCloudChart = ({
|
|||
renderComplete,
|
||||
syncColors,
|
||||
overrides,
|
||||
isDarkMode,
|
||||
}: TagCloudChartProps) => {
|
||||
const [warning, setWarning] = useState(false);
|
||||
const { bucket, metric, scale, palette, showLabel, orientation } = visParams;
|
||||
const { bucket, metric, scale, palette, showLabel, orientation, colorMapping } = visParams;
|
||||
|
||||
const bucketFormatter = useMemo(() => {
|
||||
return bucket
|
||||
|
@ -96,23 +106,35 @@ export const TagCloudChart = ({
|
|||
|
||||
const tagCloudData = useMemo(() => {
|
||||
const bucketColumn = bucket ? getColumnByAccessor(bucket, visData.columns)! : null;
|
||||
const tagColumn = bucket ? bucketColumn!.id : null;
|
||||
const tagColumn = bucket ? bucketColumn!.id : undefined;
|
||||
const metricColumn = getColumnByAccessor(metric, visData.columns)!.id;
|
||||
|
||||
const metrics = visData.rows.map((row) => row[metricColumn]);
|
||||
const values = bucket && tagColumn !== null ? visData.rows.map((row) => row[tagColumn]) : [];
|
||||
const values =
|
||||
bucket && tagColumn !== undefined ? visData.rows.map((row) => row[tagColumn]) : [];
|
||||
const maxValue = Math.max(...metrics);
|
||||
const minValue = Math.min(...metrics);
|
||||
|
||||
const colorFromMappingFn = getColorFromMappingFactory(
|
||||
tagColumn,
|
||||
visData.rows,
|
||||
isDarkMode,
|
||||
colorMapping
|
||||
);
|
||||
|
||||
return visData.rows.map((row) => {
|
||||
const tag = tagColumn === null ? 'all' : row[tagColumn];
|
||||
const tag = tagColumn === undefined ? 'all' : row[tagColumn];
|
||||
|
||||
const category = isMultiFieldKey(tag) ? tag.keys.map(String) : `${tag}`;
|
||||
return {
|
||||
text: bucketFormatter ? bucketFormatter.convert(tag, 'text') : tag,
|
||||
weight:
|
||||
tag === 'all' || visData.rows.length <= 1
|
||||
? 1
|
||||
: calculateWeight(row[metricColumn], minValue, maxValue, 0, 1) || 0,
|
||||
color: getColor(palettesRegistry, palette, tag, values, syncColors) || 'rgba(0,0,0,0)',
|
||||
color: colorFromMappingFn
|
||||
? colorFromMappingFn(category)
|
||||
: getColor(palettesRegistry, palette, tag, values, syncColors) || 'rgba(0,0,0,0)',
|
||||
};
|
||||
});
|
||||
}, [
|
||||
|
@ -124,6 +146,8 @@ export const TagCloudChart = ({
|
|||
syncColors,
|
||||
visData.columns,
|
||||
visData.rows,
|
||||
colorMapping,
|
||||
isDarkMode,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -278,3 +302,28 @@ export const TagCloudChart = ({
|
|||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { TagCloudChart as default };
|
||||
|
||||
/**
|
||||
* If colorMapping is available, returns a function that accept a string or an array of strings (used in case of multi-field-key)
|
||||
* and returns a color specified in the provided mapping
|
||||
*/
|
||||
function getColorFromMappingFactory(
|
||||
tagColumn: string | undefined,
|
||||
rows: DatatableRow[],
|
||||
isDarkMode: boolean,
|
||||
colorMapping?: string
|
||||
): undefined | ((category: string | string[]) => string) {
|
||||
if (!colorMapping) {
|
||||
// return undefined, we will use the legacy color mapping instead
|
||||
return undefined;
|
||||
}
|
||||
return getColorFactory(
|
||||
JSON.parse(colorMapping),
|
||||
getPalette(AVAILABLE_PALETTES, NeutralPalette),
|
||||
isDarkMode,
|
||||
{
|
||||
type: 'categories',
|
||||
categories: getColorCategories(rows, tagColumn),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -67,6 +67,12 @@ export const tagcloudRenderer: (
|
|||
};
|
||||
|
||||
const palettesRegistry = await plugins.charts.palettes.getPalettes();
|
||||
let isDarkMode = false;
|
||||
plugins.charts.theme.darkModeEnabled$
|
||||
.subscribe((val) => {
|
||||
isDarkMode = val.darkMode;
|
||||
})
|
||||
.unsubscribe();
|
||||
|
||||
render(
|
||||
<KibanaThemeProvider theme$={core.theme.theme$}>
|
||||
|
@ -87,6 +93,7 @@ export const tagcloudRenderer: (
|
|||
fireEvent={handlers.event}
|
||||
syncColors={config.syncColors}
|
||||
overrides={config.overrides}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
</VisualizationContainer>
|
||||
)}
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
"@kbn/analytics",
|
||||
"@kbn/chart-expressions-common",
|
||||
"@kbn/chart-icons",
|
||||
"@kbn/data-plugin",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -94,4 +94,8 @@ export const commonDataLayerArgs: Omit<
|
|||
help: strings.getPaletteHelp(),
|
||||
default: '{palette}',
|
||||
},
|
||||
colorMapping: {
|
||||
types: ['string'],
|
||||
help: strings.getColorMappingHelp(),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -52,6 +52,7 @@ const createDataLayer = (args: XYArgs, table: Datatable): DataLayerConfigResult
|
|||
layerType: LayerTypes.DATA,
|
||||
table: normalizedTable,
|
||||
showLines: args.showLines,
|
||||
colorMapping: args.colorMapping,
|
||||
...accessors,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -209,6 +209,10 @@ export const strings = {
|
|||
i18n.translate('expressionXY.dataLayer.palette.help', {
|
||||
defaultMessage: 'Palette',
|
||||
}),
|
||||
getColorMappingHelp: () =>
|
||||
i18n.translate('expressionXY.layer.colorMapping.help', {
|
||||
defaultMessage: 'JSON key-value pairs of the color mapping model',
|
||||
}),
|
||||
getTableHelp: () =>
|
||||
i18n.translate('expressionXY.layers.table.help', {
|
||||
defaultMessage: 'Table',
|
||||
|
|
|
@ -136,6 +136,7 @@ export interface DataLayerArgs {
|
|||
isStacked: boolean;
|
||||
isHorizontal: boolean;
|
||||
palette: PaletteOutput;
|
||||
colorMapping?: string; // JSON stringified object of the color mapping
|
||||
decorations?: DataDecorationConfigResult[];
|
||||
curveType?: XYCurveType;
|
||||
}
|
||||
|
@ -163,6 +164,7 @@ export interface ExtendedDataLayerArgs {
|
|||
isStacked: boolean;
|
||||
isHorizontal: boolean;
|
||||
palette: PaletteOutput;
|
||||
colorMapping?: string;
|
||||
// palette will always be set on the expression
|
||||
decorations?: DataDecorationConfigResult[];
|
||||
curveType?: XYCurveType;
|
||||
|
|
|
@ -1099,6 +1099,7 @@ exports[`XYChart component it renders area 1`] = `
|
|||
},
|
||||
}
|
||||
}
|
||||
isDarkMode={false}
|
||||
layers={
|
||||
Array [
|
||||
Object {
|
||||
|
@ -2107,6 +2108,7 @@ exports[`XYChart component it renders bar 1`] = `
|
|||
},
|
||||
}
|
||||
}
|
||||
isDarkMode={false}
|
||||
layers={
|
||||
Array [
|
||||
Object {
|
||||
|
@ -3115,6 +3117,7 @@ exports[`XYChart component it renders horizontal bar 1`] = `
|
|||
},
|
||||
}
|
||||
}
|
||||
isDarkMode={false}
|
||||
layers={
|
||||
Array [
|
||||
Object {
|
||||
|
@ -4123,6 +4126,7 @@ exports[`XYChart component it renders line 1`] = `
|
|||
},
|
||||
}
|
||||
}
|
||||
isDarkMode={false}
|
||||
layers={
|
||||
Array [
|
||||
Object {
|
||||
|
@ -5131,6 +5135,7 @@ exports[`XYChart component it renders stacked area 1`] = `
|
|||
},
|
||||
}
|
||||
}
|
||||
isDarkMode={false}
|
||||
layers={
|
||||
Array [
|
||||
Object {
|
||||
|
@ -6139,6 +6144,7 @@ exports[`XYChart component it renders stacked bar 1`] = `
|
|||
},
|
||||
}
|
||||
}
|
||||
isDarkMode={false}
|
||||
layers={
|
||||
Array [
|
||||
Object {
|
||||
|
@ -7147,6 +7153,7 @@ exports[`XYChart component it renders stacked horizontal bar 1`] = `
|
|||
},
|
||||
}
|
||||
}
|
||||
isDarkMode={false}
|
||||
layers={
|
||||
Array [
|
||||
Object {
|
||||
|
@ -8381,6 +8388,7 @@ exports[`XYChart component split chart should render split chart if both, splitR
|
|||
},
|
||||
}
|
||||
}
|
||||
isDarkMode={false}
|
||||
layers={
|
||||
Array [
|
||||
Object {
|
||||
|
@ -9622,6 +9630,7 @@ exports[`XYChart component split chart should render split chart if splitColumnA
|
|||
},
|
||||
}
|
||||
}
|
||||
isDarkMode={false}
|
||||
layers={
|
||||
Array [
|
||||
Object {
|
||||
|
@ -10861,6 +10870,7 @@ exports[`XYChart component split chart should render split chart if splitRowAcce
|
|||
},
|
||||
}
|
||||
}
|
||||
isDarkMode={false}
|
||||
layers={
|
||||
Array [
|
||||
Object {
|
||||
|
|
|
@ -57,6 +57,7 @@ interface Props {
|
|||
fieldFormats: LayersFieldFormats;
|
||||
uiState?: PersistedState;
|
||||
singleTable?: boolean;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export const DataLayers: FC<Props> = ({
|
||||
|
@ -80,6 +81,7 @@ export const DataLayers: FC<Props> = ({
|
|||
fieldFormats,
|
||||
uiState,
|
||||
singleTable,
|
||||
isDarkMode,
|
||||
}) => {
|
||||
// for singleTable mode we should use y accessors from all layers for creating correct series name and getting color
|
||||
const allYAccessors = layers.flatMap((layer) => layer.accessors);
|
||||
|
@ -169,6 +171,7 @@ export const DataLayers: FC<Props> = ({
|
|||
allYAccessors,
|
||||
singleTable,
|
||||
multipleLayersWithSplits,
|
||||
isDarkMode,
|
||||
});
|
||||
|
||||
const index = `${layer.layerId}-${accessorIndex}`;
|
||||
|
|
|
@ -964,6 +964,7 @@ export function XYChart({
|
|||
fieldFormats={fieldFormats}
|
||||
uiState={uiState}
|
||||
singleTable={singleTable}
|
||||
isDarkMode={darkMode}
|
||||
/>
|
||||
)}
|
||||
{referenceLineLayers.length ? (
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { SeriesColorAccessorFn } from '@elastic/charts';
|
||||
import { getColorFactory, type ColorMapping, type ColorMappingInputData } from '@kbn/coloring';
|
||||
import { MULTI_FIELD_KEY_SEPARATOR } from '@kbn/data-plugin/common';
|
||||
|
||||
/**
|
||||
* Return a color accessor function for XY charts depending on the split accessors received.
|
||||
*/
|
||||
export function getColorSeriesAccessorFn(
|
||||
config: ColorMapping.Config,
|
||||
getPaletteFn: (paletteId: string) => ColorMapping.CategoricalPalette,
|
||||
isDarkMode: boolean,
|
||||
mappingData: ColorMappingInputData,
|
||||
fieldId: string,
|
||||
specialTokens: Map<string, string>
|
||||
): SeriesColorAccessorFn {
|
||||
// inverse map to handle the conversion between the formatted string and their original format
|
||||
// for any specified special tokens
|
||||
const specialHandlingInverseMap: Map<string, string> = new Map(
|
||||
[...specialTokens.entries()].map((d) => [d[1], d[0]])
|
||||
);
|
||||
|
||||
const getColor = getColorFactory(config, getPaletteFn, isDarkMode, mappingData);
|
||||
|
||||
return ({ splitAccessors }) => {
|
||||
const splitValue = splitAccessors.get(fieldId);
|
||||
// if there isn't a category associated in the split accessor, let's use the default color
|
||||
if (splitValue === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// category can be also a number, range, ip, multi-field. We need to stringify it to be sure
|
||||
// we can correctly match it a with user string
|
||||
// if the separator exist, we de-construct it into a multifieldkey into values.
|
||||
const categories = `${splitValue}`.split(MULTI_FIELD_KEY_SEPARATOR).map((category) => {
|
||||
return specialHandlingInverseMap.get(category) ?? category;
|
||||
});
|
||||
// we must keep the array nature of a multi-field key or just use a single string
|
||||
// This is required because the rule stored are checked differently for single values or multi-values
|
||||
return getColor(categories.length > 1 ? categories : categories[0]);
|
||||
};
|
||||
}
|
|
@ -95,6 +95,11 @@ export const getAllSeries = (
|
|||
return allSeries;
|
||||
};
|
||||
|
||||
/**
|
||||
* This function joins every data series name available on each layer by the same color palette.
|
||||
* The returned function `getRank` should return the position of a series name in this unified list by palette.
|
||||
*
|
||||
*/
|
||||
export function getColorAssignments(
|
||||
layers: CommonXYLayerConfig[],
|
||||
titles: LayersAccessorsTitles,
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
SeriesName,
|
||||
StackMode,
|
||||
XYChartSeriesIdentifier,
|
||||
SeriesColorAccessorFn,
|
||||
} from '@elastic/charts';
|
||||
import { IFieldFormat } from '@kbn/field-formats-plugin/common';
|
||||
import type { PersistedState } from '@kbn/visualizations-plugin/public';
|
||||
|
@ -23,6 +24,13 @@ import { Datatable } from '@kbn/expressions-plugin/common';
|
|||
import { getAccessorByDimension } from '@kbn/visualizations-plugin/common/utils';
|
||||
import type { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common/expression_functions';
|
||||
import { PaletteRegistry, SeriesLayer } from '@kbn/coloring';
|
||||
import {
|
||||
getPalette,
|
||||
AVAILABLE_PALETTES,
|
||||
NeutralPalette,
|
||||
SPECIAL_TOKENS_STRING_CONVERTION,
|
||||
} from '@kbn/coloring';
|
||||
import { getColorCategories } from '@kbn/chart-expressions-common';
|
||||
import { isDataLayer } from '../../common/utils/layer_types_guards';
|
||||
import { CommonXYDataLayerConfig, CommonXYLayerConfig, XScaleType } from '../../common';
|
||||
import { AxisModes, SeriesTypes } from '../../common/constants';
|
||||
|
@ -32,6 +40,7 @@ import { ColorAssignments } from './color_assignment';
|
|||
import { GroupsConfiguration } from './axes_configuration';
|
||||
import { LayerAccessorsTitles, LayerFieldFormats, LayersFieldFormats } from './layers';
|
||||
import { getFormat } from './format';
|
||||
import { getColorSeriesAccessorFn } from './color/color_mapping_accessor';
|
||||
|
||||
type SeriesSpec = LineSeriesProps & BarSeriesProps & AreaSeriesProps;
|
||||
|
||||
|
@ -57,6 +66,7 @@ type GetSeriesPropsFn = (config: {
|
|||
allYAccessors: Array<string | ExpressionValueVisDimension>;
|
||||
singleTable?: boolean;
|
||||
multipleLayersWithSplits: boolean;
|
||||
isDarkMode: boolean;
|
||||
}) => SeriesSpec;
|
||||
|
||||
type GetSeriesNameFn = (
|
||||
|
@ -399,6 +409,7 @@ export const getSeriesProps: GetSeriesPropsFn = ({
|
|||
allYAccessors,
|
||||
singleTable,
|
||||
multipleLayersWithSplits,
|
||||
isDarkMode,
|
||||
}): SeriesSpec => {
|
||||
const { table, isStacked, markSizeAccessor } = layer;
|
||||
const isPercentage = layer.isPercentage;
|
||||
|
@ -478,6 +489,34 @@ export const getSeriesProps: GetSeriesPropsFn = ({
|
|||
);
|
||||
};
|
||||
|
||||
const colorAccessorFn: SeriesColorAccessorFn =
|
||||
// if colorMapping exist then we can apply it, if not let's use the legacy coloring method
|
||||
layer.colorMapping && splitColumnIds.length > 0
|
||||
? getColorSeriesAccessorFn(
|
||||
JSON.parse(layer.colorMapping), // the color mapping is at this point just a strinfigied JSON
|
||||
getPalette(AVAILABLE_PALETTES, NeutralPalette),
|
||||
isDarkMode,
|
||||
{
|
||||
type: 'categories',
|
||||
categories: getColorCategories(table.rows, splitColumnIds[0]),
|
||||
},
|
||||
splitColumnIds[0],
|
||||
SPECIAL_TOKENS_STRING_CONVERTION
|
||||
)
|
||||
: (series) =>
|
||||
getColor(
|
||||
series,
|
||||
{
|
||||
layer,
|
||||
colorAssignments,
|
||||
paletteService,
|
||||
getSeriesNameFn,
|
||||
syncColors,
|
||||
},
|
||||
uiState,
|
||||
singleTable
|
||||
);
|
||||
|
||||
return {
|
||||
splitSeriesAccessors: splitColumnIds.length ? splitColumnIds : [],
|
||||
stackAccessors: isStacked ? [xColumnId || 'unifiedX'] : [],
|
||||
|
@ -497,19 +536,7 @@ export const getSeriesProps: GetSeriesPropsFn = ({
|
|||
formatter?.id === 'bytes' && scaleType === ScaleType.Linear
|
||||
? ScaleType.LinearBinary
|
||||
: scaleType,
|
||||
color: (series) =>
|
||||
getColor(
|
||||
series,
|
||||
{
|
||||
layer,
|
||||
colorAssignments,
|
||||
paletteService,
|
||||
getSeriesNameFn,
|
||||
syncColors,
|
||||
},
|
||||
uiState,
|
||||
singleTable
|
||||
),
|
||||
color: colorAccessorFn,
|
||||
groupId: yAxis?.groupId,
|
||||
enableHistogramMode,
|
||||
stackMode,
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
"server": true,
|
||||
"browser": true,
|
||||
"requiredPlugins": [
|
||||
"expressions"
|
||||
"expressions",
|
||||
"data"
|
||||
],
|
||||
"extraPublicDirs": [
|
||||
"common"
|
||||
|
|
|
@ -37,7 +37,7 @@ export * from './significant_text_fn';
|
|||
export * from './significant_text';
|
||||
export * from './terms_fn';
|
||||
export * from './terms';
|
||||
export { MultiFieldKey } from './multi_field_key';
|
||||
export { MultiFieldKey, isMultiFieldKey, MULTI_FIELD_KEY_SEPARATOR } from './multi_field_key';
|
||||
export * from './multi_terms_fn';
|
||||
export * from './multi_terms';
|
||||
export * from './rare_terms_fn';
|
||||
|
|
|
@ -38,3 +38,13 @@ export class MultiFieldKey {
|
|||
return this[id];
|
||||
}
|
||||
}
|
||||
|
||||
export function isMultiFieldKey(field: unknown): field is MultiFieldKey {
|
||||
return field instanceof MultiFieldKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Multi-field key separator used in Visualizations (Lens, AggBased, TSVB).
|
||||
* This differs from the separator used in the toString method of the MultiFieldKey
|
||||
*/
|
||||
export const MULTI_FIELD_KEY_SEPARATOR = ' › ';
|
||||
|
|
|
@ -1 +1 @@
|
|||
{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"isPreview":false,"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
|
||||
{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
|
|
@ -1 +1 @@
|
|||
{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
|
||||
{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
|
|
@ -1 +1 @@
|
|||
{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[],"type":"datatable"},"visParams":{"ariaLabel":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
|
||||
{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[],"type":"datatable"},"visParams":{"ariaLabel":null,"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
|
|
@ -1 +1 @@
|
|||
{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"isPreview":false,"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
|
||||
{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"colorMapping":null,"isPreview":false,"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
|
|
@ -1 +1 @@
|
|||
{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
|
||||
{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
|
|
@ -1 +1 @@
|
|||
{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"log","showLabel":true},"visType":"tagcloud"}}
|
||||
{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"log","showLabel":true},"visType":"tagcloud"}}
|
|
@ -1 +1 @@
|
|||
{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
|
||||
{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
|
|
@ -1 +1 @@
|
|||
{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
|
||||
{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
|
|
@ -1 +1 @@
|
|||
{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[],"type":"datatable"},"visParams":{"ariaLabel":null,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
|
||||
{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[],"type":"datatable"},"visParams":{"ariaLabel":null,"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
|
|
@ -1 +1 @@
|
|||
{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"isPreview":false,"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
|
||||
{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"colorMapping":null,"isPreview":false,"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
|
|
@ -1 +1 @@
|
|||
{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
|
||||
{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
|
|
@ -1 +1 @@
|
|||
{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"log","showLabel":true},"visType":"tagcloud"}}
|
||||
{"as":"tagcloud","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"excludeIsRegex":true,"field":"response.raw","includeIsRegex":true,"missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"ariaLabel":null,"bucket":{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"},"colorMapping":null,"isPreview":false,"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"custom","params":{"colors":["#882E72","#B178A6","#D6C1DE","#1965B0","#5289C7","#7BAFDE","#4EB265","#90C987","#CAE0AB","#F7EE55","#F6C141","#F1932D","#E8601C","#DC050C"],"continuity":"above","gradient":false,"range":"percent","rangeMax":null,"rangeMin":0,"stops":[]},"type":"palette"},"scale":"log","showLabel":true},"visType":"tagcloud"}}
|
|
@ -8,7 +8,7 @@
|
|||
import type { Filter, FilterMeta } from '@kbn/es-query';
|
||||
import type { Position } from '@elastic/charts';
|
||||
import type { $Values } from '@kbn/utility-types';
|
||||
import type { CustomPaletteParams, PaletteOutput } from '@kbn/coloring';
|
||||
import { CustomPaletteParams, PaletteOutput, ColorMapping } from '@kbn/coloring';
|
||||
import type { ColorMode } from '@kbn/charts-plugin/common';
|
||||
import type { LegendSize } from '@kbn/visualizations-plugin/common';
|
||||
import { CategoryDisplay, LegendDisplay, NumberDisplay, PieChartTypes } from './constants';
|
||||
|
@ -71,6 +71,7 @@ export interface SharedPieLayerState {
|
|||
legendMaxLines?: number;
|
||||
legendSize?: LegendSize;
|
||||
truncateLegend?: boolean;
|
||||
colorMapping?: ColorMapping.Config;
|
||||
}
|
||||
|
||||
export type PieLayerState = SharedPieLayerState & {
|
||||
|
|
|
@ -154,7 +154,7 @@ export function App({
|
|||
|
||||
useExecutionContext(executionContext, {
|
||||
type: 'application',
|
||||
id: savedObjectId || 'new',
|
||||
id: savedObjectId || 'new', // TODO: this doesn't consider when lens is saved by value
|
||||
page: 'editor',
|
||||
});
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
type EventAnnotationGroupConfig,
|
||||
EVENT_ANNOTATION_GROUP_TYPE,
|
||||
} from '@kbn/event-annotation-common';
|
||||
import { DEFAULT_COLOR_MAPPING_CONFIG } from '@kbn/coloring';
|
||||
import type {
|
||||
Datasource,
|
||||
DatasourceMap,
|
||||
|
@ -289,7 +290,8 @@ export function initializeVisualization({
|
|||
visualizationMap[visualizationState.activeId]?.initialize(
|
||||
() => '',
|
||||
visualizationState.state,
|
||||
undefined,
|
||||
// initialize a new visualization always with the new color mapping
|
||||
{ type: 'colorMapping', value: { ...DEFAULT_COLOR_MAPPING_CONFIG } },
|
||||
annotationGroups,
|
||||
references
|
||||
) ?? visualizationState.state
|
||||
|
|
|
@ -457,7 +457,13 @@ describe('suggestion helpers', () => {
|
|||
it('should pass passed in main palette if specified', () => {
|
||||
const mockVisualization1 = createMockVisualization();
|
||||
const mockVisualization2 = createMockVisualization();
|
||||
const mainPalette: PaletteOutput = { type: 'palette', name: 'mock' };
|
||||
const mainPalette: { type: 'legacyPalette'; value: PaletteOutput } = {
|
||||
type: 'legacyPalette',
|
||||
value: {
|
||||
type: 'palette',
|
||||
name: 'mock',
|
||||
},
|
||||
};
|
||||
datasourceMap.mock.getDatasourceSuggestionsFromCurrentState.mockReturnValue([
|
||||
generateSuggestion(0),
|
||||
generateSuggestion(1),
|
||||
|
@ -490,7 +496,13 @@ describe('suggestion helpers', () => {
|
|||
it('should query active visualization for main palette if not specified', () => {
|
||||
const mockVisualization1 = createMockVisualization();
|
||||
const mockVisualization2 = createMockVisualization();
|
||||
const mainPalette: PaletteOutput = { type: 'palette', name: 'mock' };
|
||||
const mainPalette: { type: 'legacyPalette'; value: PaletteOutput } = {
|
||||
type: 'legacyPalette',
|
||||
value: {
|
||||
type: 'palette',
|
||||
name: 'mock',
|
||||
},
|
||||
};
|
||||
mockVisualization1.getMainPalette = jest.fn(() => mainPalette);
|
||||
datasourceMap.mock.getDatasourceSuggestionsFromCurrentState.mockReturnValue([
|
||||
generateSuggestion(0),
|
||||
|
|
|
@ -6,12 +6,11 @@
|
|||
*/
|
||||
|
||||
import type { Datatable } from '@kbn/expressions-plugin/common';
|
||||
import type { PaletteOutput } from '@kbn/coloring';
|
||||
import type { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public';
|
||||
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
|
||||
import type { DragDropIdentifier } from '@kbn/dom-drag-drop';
|
||||
import { showMemoizedErrorNotification } from '../../lens_ui_errors';
|
||||
import type {
|
||||
import {
|
||||
Visualization,
|
||||
Datasource,
|
||||
TableSuggestion,
|
||||
|
@ -21,6 +20,7 @@ import type {
|
|||
VisualizeEditorContext,
|
||||
Suggestion,
|
||||
DatasourceLayers,
|
||||
SuggestionRequest,
|
||||
} from '../../types';
|
||||
import type { LayerType } from '../../../common/types';
|
||||
import {
|
||||
|
@ -64,7 +64,7 @@ export function getSuggestions({
|
|||
visualizeTriggerFieldContext?: VisualizeFieldContext | VisualizeEditorContext;
|
||||
activeData?: Record<string, Datatable>;
|
||||
dataViews: DataViewsState;
|
||||
mainPalette?: PaletteOutput;
|
||||
mainPalette?: SuggestionRequest['mainPalette'];
|
||||
allowMixed?: boolean;
|
||||
}): Suggestion[] {
|
||||
const datasources = Object.entries(datasourceMap).filter(
|
||||
|
@ -237,7 +237,7 @@ function getVisualizationSuggestions(
|
|||
datasourceSuggestion: DatasourceSuggestion & { datasourceId: string },
|
||||
currentVisualizationState: unknown,
|
||||
subVisualizationId?: string,
|
||||
mainPalette?: PaletteOutput,
|
||||
mainPalette?: SuggestionRequest['mainPalette'],
|
||||
isFromContext?: boolean,
|
||||
activeData?: Record<string, Datatable>,
|
||||
allowMixed?: boolean
|
||||
|
|
|
@ -519,7 +519,10 @@ describe('chart_switch', () => {
|
|||
it('should query main palette from active chart and pass into suggestions', async () => {
|
||||
const visualizationMap = mockVisualizationMap();
|
||||
const mockPalette: PaletteOutput = { type: 'palette', name: 'mock' };
|
||||
visualizationMap.visA.getMainPalette = jest.fn(() => mockPalette);
|
||||
visualizationMap.visA.getMainPalette = jest.fn(() => ({
|
||||
type: 'legacyPalette',
|
||||
value: mockPalette,
|
||||
}));
|
||||
visualizationMap.visB.getSuggestions.mockReturnValueOnce([]);
|
||||
const frame = mockFrame(['a', 'b', 'c']);
|
||||
const currentVisState = {};
|
||||
|
@ -550,7 +553,7 @@ describe('chart_switch', () => {
|
|||
expect(visualizationMap.visB.getSuggestions).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
keptLayerIds: ['a'],
|
||||
mainPalette: mockPalette,
|
||||
mainPalette: { type: 'legacyPalette', value: mockPalette },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
|
@ -27,8 +27,10 @@ export function PalettePanelContainer({
|
|||
handleClose,
|
||||
siblingRef,
|
||||
children,
|
||||
title,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
handleClose: () => void;
|
||||
siblingRef: MutableRefObject<HTMLDivElement | null>;
|
||||
children?: React.ReactElement | React.ReactElement[];
|
||||
|
@ -76,16 +78,12 @@ export function PalettePanelContainer({
|
|||
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h2
|
||||
<h3
|
||||
id="lnsPalettePanelContainerTitle"
|
||||
className="lnsPalettePanelContainer__headerTitle"
|
||||
>
|
||||
<strong>
|
||||
{i18n.translate('xpack.lens.table.palettePanelTitle', {
|
||||
defaultMessage: 'Color',
|
||||
})}
|
||||
</strong>
|
||||
</h2>
|
||||
{title}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -36,28 +36,24 @@ export function PalettePicker({
|
|||
});
|
||||
return (
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.lens.palettePicker.label', {
|
||||
defaultMessage: 'Color palette',
|
||||
defaultMessage: 'Palette',
|
||||
})}
|
||||
>
|
||||
<>
|
||||
<EuiColorPalettePicker
|
||||
fullWidth
|
||||
data-test-subj="lns-palettePicker"
|
||||
compressed
|
||||
palettes={palettesToShow}
|
||||
onChange={(newPalette) => {
|
||||
setPalette({
|
||||
type: 'palette',
|
||||
name: newPalette,
|
||||
});
|
||||
}}
|
||||
valueOfSelected={activePalette?.name || 'default'}
|
||||
selectionDisplay={'palette'}
|
||||
/>
|
||||
</>
|
||||
<EuiColorPalettePicker
|
||||
fullWidth
|
||||
data-test-subj="lns-palettePicker"
|
||||
palettes={palettesToShow}
|
||||
onChange={(newPalette) => {
|
||||
setPalette({
|
||||
type: 'palette',
|
||||
name: newPalette,
|
||||
});
|
||||
}}
|
||||
valueOfSelected={activePalette?.name || 'default'}
|
||||
selectionDisplay={'palette'}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import type { Ast } from '@kbn/interpreter';
|
||||
import type { IconType } from '@elastic/eui/src/components/icon/icon';
|
||||
import type { CoreStart, SavedObjectReference, ResolvedSimpleSavedObject } from '@kbn/core/public';
|
||||
import type { PaletteOutput } from '@kbn/coloring';
|
||||
import type { ColorMapping, PaletteOutput } from '@kbn/coloring';
|
||||
import type { TopNavMenuData } from '@kbn/navigation-plugin/public';
|
||||
import type { MutableRefObject, ReactElement } from 'react';
|
||||
import type { Filter, TimeRange } from '@kbn/es-query';
|
||||
|
@ -863,7 +863,12 @@ export interface SuggestionRequest<T = unknown> {
|
|||
* State is only passed if the visualization is active.
|
||||
*/
|
||||
state?: T;
|
||||
mainPalette?: PaletteOutput;
|
||||
/**
|
||||
* Passing the legacy palette or the new color mapping if available
|
||||
*/
|
||||
mainPalette?:
|
||||
| { type: 'legacyPalette'; value: PaletteOutput }
|
||||
| { type: 'colorMapping'; value: ColorMapping.Config };
|
||||
isFromContext?: boolean;
|
||||
/**
|
||||
* The visualization needs to know which table is being suggested
|
||||
|
@ -1026,11 +1031,15 @@ export interface Visualization<T = unknown, P = T, ExtraAppendLayerArg = unknown
|
|||
* - When using suggestions, the suggested state is passed in
|
||||
*/
|
||||
initialize: {
|
||||
(addNewLayer: () => string, nonPersistedState?: T, mainPalette?: PaletteOutput): T;
|
||||
(
|
||||
addNewLayer: () => string,
|
||||
nonPersistedState?: T,
|
||||
mainPalette?: SuggestionRequest['mainPalette']
|
||||
): T;
|
||||
(
|
||||
addNewLayer: () => string,
|
||||
persistedState: P,
|
||||
mainPalette?: PaletteOutput,
|
||||
mainPalette?: SuggestionRequest['mainPalette'],
|
||||
annotationGroups?: AnnotationGroups,
|
||||
references?: SavedObjectReference[]
|
||||
): T;
|
||||
|
@ -1042,7 +1051,7 @@ export interface Visualization<T = unknown, P = T, ExtraAppendLayerArg = unknown
|
|||
*/
|
||||
getUsedDataViews?: (state?: T) => string[];
|
||||
|
||||
getMainPalette?: (state: T) => undefined | PaletteOutput;
|
||||
getMainPalette?: (state: T) => undefined | SuggestionRequest['mainPalette'];
|
||||
|
||||
/**
|
||||
* Supported triggers of this visualization type when embedded somewhere
|
||||
|
|
|
@ -253,6 +253,9 @@ export function TableDimensionEditor(
|
|||
siblingRef={props.panelRef}
|
||||
isOpen={isPaletteOpen}
|
||||
handleClose={() => setIsPaletteOpen(!isPaletteOpen)}
|
||||
title={i18n.translate('xpack.lens.table.colorByRangePanelTitle', {
|
||||
defaultMessage: 'Color',
|
||||
})}
|
||||
>
|
||||
<CustomizablePalette
|
||||
palettes={props.paletteService}
|
||||
|
|
|
@ -159,6 +159,9 @@ export function GaugeDimensionEditor(
|
|||
siblingRef={props.panelRef}
|
||||
isOpen={isPaletteOpen}
|
||||
handleClose={togglePalette}
|
||||
title={i18n.translate('xpack.lens.table.colorByRangePanelTitle', {
|
||||
defaultMessage: 'Color',
|
||||
})}
|
||||
>
|
||||
<CustomizablePalette
|
||||
palettes={props.paletteService}
|
||||
|
|
|
@ -224,7 +224,7 @@ export const getGaugeVisualization = ({
|
|||
layerId: addNewLayer(),
|
||||
layerType: LayerTypes.DATA,
|
||||
shape: GaugeShapes.HORIZONTAL_BULLET,
|
||||
palette: mainPalette,
|
||||
palette: mainPalette?.type === 'legacyPalette' ? mainPalette.value : undefined,
|
||||
ticksPosition: 'auto',
|
||||
labelMajorMode: 'auto',
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue