Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Lens] Color mapping for categorical dimensions #162389

Merged
merged 90 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from 66 commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
93bc658
feat: add color mapping component
markov00 Aug 25, 2023
ff5fdb4
feat: add multiFieldKey typeguard
markov00 Aug 25, 2023
324a54e
feat: intregrate color mapping in XY
markov00 Aug 25, 2023
b9a83fd
feat: integrate color mapping in partition
markov00 Aug 25, 2023
7336f59
feat: integrate color mapping with tagcloud
markov00 Aug 25, 2023
c00284c
feat: add color mapping panel title and badge
markov00 Aug 25, 2023
69f25fc
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Aug 25, 2023
5ab0768
feat: handle numeric fields as categorical ones
markov00 Aug 28, 2023
7d5be20
feat: replace confirm dialog with EUI modal
markov00 Aug 28, 2023
87c7c97
fix: add missing colorMapping usage blockers
markov00 Aug 28, 2023
ae0fc29
[CI] Auto-commit changed files from 'node scripts/precommit_hook.js -…
kibanamachine Aug 28, 2023
733501d
feat: switch legacy/new color mapping
markov00 Aug 31, 2023
89d336b
feat: move available palette to coloring package
markov00 Aug 31, 2023
79ec603
fix moved function
markov00 Aug 31, 2023
e5303f3
fix: flat opaque colors and theme aware greys colors
markov00 Aug 31, 2023
41d19b0
WIP
markov00 Sep 1, 2023
47a6150
add tests
markov00 Sep 4, 2023
d9b0b35
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Sep 4, 2023
c9c5b0d
update color function tests
markov00 Sep 6, 2023
297e6e1
feat: add telemetry to color mapping switch
markov00 Sep 6, 2023
e9bc830
fix errors on lints
markov00 Sep 6, 2023
e7f62a9
add README
markov00 Sep 7, 2023
603139e
fix unit tests
markov00 Sep 7, 2023
fcf0b60
fix functional test assert palette and set palette
markov00 Sep 7, 2023
7d4aa40
fix missing/changed types
markov00 Sep 7, 2023
be1fe7a
fix snapshots
markov00 Sep 7, 2023
701de66
fix functional tests
markov00 Sep 7, 2023
1c1e0ae
fix eslint
markov00 Sep 8, 2023
2943cec
fix eslint
markov00 Sep 8, 2023
c9a0f91
fix FTR expressions
markov00 Sep 8, 2023
3259f0b
increase bundle limit
markov00 Sep 8, 2023
ffb01f3
add i18n
markov00 Sep 8, 2023
06fb3db
Merge branch 'main' into 2023_07_17-color_mapping_lens
markov00 Sep 8, 2023
d94aa1b
fix wrong import
markov00 Sep 8, 2023
b87d563
change i18n namespace
markov00 Sep 11, 2023
db6c451
add simple FTs
markov00 Sep 11, 2023
5e36f8c
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Sep 11, 2023
ddd2164
increase bundle limit
markov00 Sep 11, 2023
5be7faf
Merge branch 'main' into 2023_07_17-color_mapping_lens
stratoula Sep 12, 2023
1f8e9f7
fix single category gradient color
markov00 Sep 12, 2023
23d61c8
Merge branch 'main' into 2023_07_17-color_mapping_lens
dej611 Sep 12, 2023
04647ee
use chroma scale instead of d3
markov00 Sep 12, 2023
c123823
update tests after changing d3 scale to chroma
markov00 Sep 13, 2023
2e876ab
fix allow categories with whitespaces
markov00 Sep 13, 2023
0548fc7
renamed multi-field-value-separator const
markov00 Sep 13, 2023
04ec3f1
import MULTI_FIELD_KEY_SEPARATOR from data plugin
markov00 Sep 13, 2023
0506999
Merge branch 'main' into 2023_07_17-color_mapping_lens
markov00 Sep 13, 2023
f54bad2
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Sep 13, 2023
a7eddf8
fix dependency on kbn-coloring
markov00 Sep 13, 2023
738e56a
Code cleanup
markov00 Sep 14, 2023
b7a7b6b
make gradient input editable
markov00 Sep 14, 2023
229a50b
fix type checks
markov00 Sep 14, 2023
6ebd877
fix multi-metric pie charts
markov00 Sep 18, 2023
385fca4
finish the readme description
markov00 Sep 18, 2023
0e7f29d
update selector naming convention
markov00 Sep 18, 2023
3ec9a02
reposition popover on scroll
markov00 Sep 18, 2023
ae2fe91
open color picker popup preferably toward up
markov00 Sep 18, 2023
20d497c
Merge branch 'main' into 2023_07_17-color_mapping_lens
markov00 Sep 18, 2023
0a5c0cd
Merge branch 'main' into 2023_07_17-color_mapping_lens
dej611 Sep 18, 2023
a0ac286
Merge branch 'main' into 2023_07_17-color_mapping_lens
markov00 Sep 19, 2023
d2990b7
increase bundle limit
markov00 Sep 19, 2023
15c2fcc
cleanup and update color palettes
markov00 Sep 19, 2023
2b889e8
fix partition with no specified buckets
markov00 Sep 19, 2023
c875cab
fix snapshot test after palette id change
markov00 Sep 19, 2023
422f8c1
Merge branch 'main' into 2023_07_17-color_mapping_lens
markov00 Sep 19, 2023
2bdc4e8
Merge branch 'main' into 2023_07_17-color_mapping_lens
stratoula Sep 20, 2023
135f8ea
fix imports in functional tests
markov00 Sep 20, 2023
400fd4d
improve style based on review comments
markov00 Sep 25, 2023
dda05ce
use emotion instead of scss
markov00 Sep 25, 2023
d8e1b7a
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Sep 25, 2023
75a704b
fix test errors
markov00 Sep 25, 2023
60395d4
Merge branch 'main' into 2023_07_17-color_mapping_lens
markov00 Sep 25, 2023
c7a8036
fix FT palette id change
markov00 Sep 25, 2023
c491573
Merge branch 'main' into 2023_07_17-color_mapping_lens
markov00 Sep 25, 2023
cf736ad
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Sep 25, 2023
f873343
fix wrong push
markov00 Sep 25, 2023
411e6af
change color input warnings
markov00 Sep 26, 2023
71449a3
fix tech preview badge and tests
markov00 Sep 26, 2023
d442274
Merge branch 'main' into 2023_07_17-color_mapping_lens
markov00 Sep 26, 2023
15cbef0
Merge branch 'main' into 2023_07_17-color_mapping_lens
markov00 Sep 26, 2023
63c3168
Merge branch 'main' into 2023_07_17-color_mapping_lens
markov00 Sep 27, 2023
ded9411
Merge branch 'main' into 2023_07_17-color_mapping_lens
stratoula Sep 27, 2023
5e92c8b
Merge branch 'main' into 2023_07_17-color_mapping_lens
stratoula Sep 27, 2023
0475e13
use tag cloud color mapping in suggestions
markov00 Sep 27, 2023
6701a1f
show correct dimension colors from color mapping
markov00 Sep 27, 2023
b492532
reuse color scale generator
markov00 Sep 27, 2023
e662e29
fix test color validate to hex
markov00 Sep 27, 2023
ad7e058
Merge branch 'main' into 2023_07_17-color_mapping_lens
stratoula Sep 28, 2023
9c5f02f
Merge branch 'main' into 2023_07_17-color_mapping_lens
kibanamachine Sep 28, 2023
dba89cc
Merge branch 'main' into 2023_07_17-color_mapping_lens
stratoula Sep 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
stratoula marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

@MichaelMarcialis MichaelMarcialis Sep 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design review

Root-level configuration flyout

  • At the root-level dimension item configuration, under the "Appearance" section, can we add a "Color mapping" label before the color mapping display and "Edit" button to better match the wireframes?

Color configuration flyout

Flyout header

  • Change the "Color" flyout header to "Edit color by term mapping" or "Edit color by range mapping" respectively.

Opt-in switch

  • Should the new "Use new color mapping (tech preview)" switch be scoped and relocated from the individual dimension item level to a larger visualization level? By scoping this setting to individual dimension items, it may require users with multiple color-oriented dimension to change the setting in multiple locations. Worse still, it opens up the possibility for a single visualization using the new color mapping feature in some dimensions and the legacy color selection in others. This may make it more difficult for us to avoid breaking changes in the future and may also confuse users as to why they are seeing two different color interfaces within the same visualization. Thoughts? CCing @timductive and @ninoslavmiskovic.

Answer: you are right about this, but the idea here was to make it prominent and quickly available for the switch. It could generate confusion, but I believe is fine now for this tech preview. For every new chart this option will be already on ON so only when the hard case of 2 layers with color mapping this could cause confusions. Waiting for @timductive @ninoslavmiskovic opinions.

Palette selector

  • Change the "Palette" label text to "Color palette".

  • Change "Palette" selector to use compressed EUI size props.

Scale button group

  • Change the "Scale" button group to use the isIconOnly prop.

  • For the time being, let's use these icons that I created previously for our quick high-fidelity design concept. If anyone has issue with them, Gio and I can work together to iterate on them and get them added formally to EUI.

    CleanShot 2023-09-20 at 15 57 27

  • Relocate the "Scale" button group to be in the same row and to the right of the "Palette" selector, per the wireframes.

  • Are we confident that the term "Sequential" will resonate more with users than something like "Gradient"? To my mind, "Gradient" seems easier to understand, but I'm curious to know what others think.

Answer gradient is about the color, and sequential is about the scale. I believe in educating the user to the right words even if these don't resonate well because they can then build up a vocabulary that can be reused in other software.

Assignments

General

  • Please change the "Assignments" label text to "Mapping assignments".

  • Relocate the "Auto assign categories to colors" switch to be a label append (i.e. right aligned) to the "Assignments" label.

  • Decrease the "Auto assign categories to colors" label font size to 12px (ex. <EuiText size="xs">).

  • Change "Auto assign categories to colors" label text to read "Auto mapping". If you feel strongly that we need any additional description for this feature, consider adding it in an appended EuiIconTip .

  • Change the padding for the "Assignments" gray container from 16px to 8px to better match wireframes.

  • Ensure the spacing between each color assignment row is 8px. Currently, there appears to be no spacing.

  • The EuiColorPickerSwatch components for each row should be 32x32px to match the height of the adjacent compressed form elements. Currently they are 24x24px.

  • The color for the arrow (which is used to indicate whether a color swatch is interactable) appears to always be white. Can we instead dynamically change the color of this arrow (either black or white) depending on the color in the swatch to ensure proper contrast?

  • The color swatch arrow is set to be 2.5px from the right edge. Can we change to 2px to bring it slightly closer to the right edge and avoid subpixel values (for the sake of those not on retina displays)?

  • The gap between color swatch and combobox is currently 4px. Please increase to 8px to better match the wireframes.

  • Drag-and-drop reordering of assignment comboboxes isn't implemented currently. Is this being scoped as a possible future enhancement?

NOTE yes this could be a possible future enhancement

  • All EuiComboBox components within "Assignments" should be utilize EUI's compressed size prop.

  • For lengthy term values, the comboboxes appear to expand horizontally and overflow the assignment container. Can we fix this so the combobox adhears to the width of its container and truncation/line-breaks are utilized as needed?

  • When the delete assignments buttons are enabled, can we configure it so that they use the color="text" styles by default, and then on hover/focus, we change to color="danger" (similar to how we do on layer dimension items)?

  • A term of "Other" currently is available for use in assignments when "Group remaining values as other" is enabled. The original intent was that the "All other terms" catch-all assignment would account for this "Other" term (as well as anything else that hasn't been defined). By that logic, "Other" should never appear as an individual assignment option. Does that make sense? Or do we feel strongly that users can/will want to have a separate color assignment for "Other" versus "All other terms"?

NOTE we want to consider "other" as a category, so that's why it doesn't fall into the "all other terms" bucket

  • The "All other terms" assignment row should have a 1px border divider separating itself visually from the preceding color assignments, with 8px of space above and below the border.

  • There should be whitespace to the right of the "All other terms" combobox, so that the right edge of the combobox is correctly aligned with the right edge of all the comboboxes above it.

Color picker popover

  • Can we adjust the width of the color picker popover so that a maximum 5 swatches fit per row? Doing so will ensure we get a nice, even 5x2 grid of swatches when the maximum of 10 palette colors is in use.

  • Change the color picker popover's tab interface to be 100% the width of the container so the border tab border extends fully to the popover's bleeding edge.

  • Change the "Palette" tab text to the a generic term, like "Colors". We are already using the term "Palette" as a section header within that tab, and the duplicative heading may be confusing.

  • Change the popover's section titles from 14px to 12px (<EuiTitle size="xxxs">).

  • Change the "Palette" section title to "Palette colors".

  • Can we add a full-bleed dividing border between the "Palette" and "Contrast-aware greys" sections?

  • Change the "Contrast-aware greys" section title to "Neutral colors" and append it with an EuiIconTip explaining that "The provided neutral colors are theme-aware and will change appropriately when switching between light and dark themes."

  • In the "Custom" color tab, please remove the buttons to "Use this color" and "Close". Selecting the custom tab should immediately assume the user wishes to use a custom color, and closing popovers already occurs via the escape key or clicking away (like all Kibana popovers).

NOTEdone with debounce

  • If we've chosen to omit an opacity option for palette colors in the color picker, should we still be offering an opacity option in the "Custom" color tab? I'm thinking we should be consistent and remove opacity from both in that case.

  • Can we add a secondary hex value input to the "Custom" color tab, similar to how we offer on standard EUI color pickers (<EuiColorPicker secondaryInputDisplay="bottom">)?

  • Rather than displaying an ever-present contrast-checker for light/dark themes, I think we should simply show a conditional warning if/when AA contrast-checking doesn't pass for one or both of these themes (i.e. only show when something is wrong). Perhaps we could engineer this in an unobtrusive way by appending a conditional warning icon to the aforementioned secondary hex value input (with accompanying tooltip that explains there is an issue with the selected color's contrast in light and/or dark mode). Thoughts?

CleanShot 2023-09-20 at 16 18 36

Gradient stops

  • For the vertical gradient stops interface, let's remove the white arrows being used to indicate each color stop can be interacted with. There is no need for such an indicator here, as the gradient stops will always able to be interacted with (unlike the color swatches). Instead, to make the stops appear more click-friendly, let's follow the design style laid out in EUI's former color stop component by making the following stylistic changes to the color stops:

  • Change color stop dimenstions to 16x16px.

  • Add a 3px white border.

  • Apply a large shadow (euiShadow(euiThemeContext, 'l')).

  • Also, perhaps on hover/focus we could add a transition to scale the circle up a bit (happy to help with CSS if needed).

CleanShot 2023-09-20 at 16 05 53

  • The gradient's track needs a subtle outline or border (in case the chosen color blends in with the page background). Let's use a 1px border of colors.darkestShade at 20% opacity to match what EUI uses on their color palette track.

  • Can we have the buttons for adding color stops to the gradient only show only when the user is hovering/focusing the gradient interface (rather then having them be an ever-present element covering the gradient track)? Perhaps we could also add a subtle 0–100% opacity transition as well?

NOTE: I tried but the only thing I can do is to highlight the + button when over the + button, not from the line gradient. We can double check together on how to fix that.

  • Change the "Remove from gradient" button text to "Remove color stop" in the gradient-specific color picker. Also change to the button to an empty, danger, extra-small style (<EuiButtonEmpty color="danger" size="xs">). Finally, let's add a full-bleed border above it to match the wireframes.

Note: I used the term "step" rather then stop because feels more generic

  • The wireframes originally intended for a minimum of two color stops to always be present in a gradient scale. As such, only the middle (third) color stop offered the option to be deleted. This is no longer the case with your suggested change to allow a gradient to go down to a single color stop. With this change, we now must offer users the option to delete both the second and third color stops. This gets confusing when the inverts the gradient, because the current implementation also inverts which color stops are able to be deleted. For simplicity (if we intend to keep going down this path of default gradients having one color stop) let's just change it so that if there are more than one color stop in play, all color stop popovers offer the option to delete the stop. When there is only one color stop left, that delete button is no longer rendered in that remaining stop's popover. Thoughts?

NOTE: fixed now you can remove everything except the last one in whatever direction

  • Just to reiterate my concerns, I still feel it would be a better, easier-to-understand user experience to have two colors stops being the minimum for gradients. I think allowing it to go down to one color stop adds a lot of potential for user confusion and requires a lot of extra checks in the background (such as remembering the state of inversion) to account for odd situations. I'm not a fan of trying to be overly "smart" in random locations to the point where users are unable to predict how things function. CCing @timductive and @ninoslavmiskovic.

NOTE: As already discussed we prefer maintaining the current pattern mechanism until user feedbacks says that is a bad ux

Footer

  • The space between the "Assignments" gray container and "Add assignment" button should be 4px. Currently it is 8px.

  • The "Assignments" footer buttons for "Add assignment" and "Invert gradient" should use the flush="both" prop to better align to the left edge of the preceding contents. Note that you'll need to add some space between the buttons to compensate for the removal of their padding.

  • Please change the "Invert gradient" icon from sortAscending/sortDescending to sortable.

Potential bugs

  • When using the "Date histogram" function, editing the color mapping shows the value assignments in what appears to be Unix timestamp format. Can we change this to a more human-readable format (ideally the same format used in the visualization legend)?

NOTE coloring by "date" is not recommended. I can't match partial date written as string to timestamps used in the code also because I don't have a way to distinguish between a date written by the user and different type of category, I can parse only text input. If you need to specify date then we need date pickers on every category that is something too complex for an unused case. Let's collect feedback and see how it works. (also consider the fact that the date histogram could be "auto" computed, meaning that what you describe as date could change depending on the time window. Probably is better to use a color by range instead.

  • If I delete a dimension item that has a custom color configuration (i.e. user has turned off auto-assign and applied a custom set of terms/colors), adding a new dimension item restores that previously deleted dimension item's custom color configuration. Would it be better to start from scratch and apply the default presets (with auto-assign enabled)?

Note Still checking this

  • The same term can be assigned to multiple colors in assignments. This shouldn't be allowed. Even more confusing is that rather than the last assignment being the one that is honored (via a perceived cascade effect), it seems the first assignment is the one that is honored. Instead, can we simply not allow the same term to be assigned to more than one color? We can achieve this by removing the previous term assignment when the user attempts to assign a term in use to a different color.

NOTE Is a cascade effect actually, the first assigned the only one applied. I've added a warning sign on each duplicate category so the user can decide what to do and which to remove

Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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 ');
});
});
Original file line number Diff line number Diff line change
@@ -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>
);
}
}
Loading