mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Lens] Improve color stop UI (#119165)
* First very draft version * Added validation, clean up code * Some fixes * Adapt components to the new UI design * Some fixes * Fix validation * Fix lint errors * Fix metric vis for new color stop UI * Fix problems with keeping state of auto detecting max/min value * Add tests * Fix CI * Fix tests * Fix some lint problems * Fix CI * Fix min/max behavior for heatmap * Fix checks. * Fix auto value when we add new color range * Fix check task * Fix some issues * Some fixes * Fix functional tests * small fix for heatmap * Fix test * Update comment-description * fix PR comments * do some refactoring (work in progress) * do some refactoring (work in progress) * some cleanup * some cleanup * wp: fix validation * wip: fix validation * push some refactoring * do some refactoring * add useDebounce * add useReducer * remove autoValue * fix validation * update validation logic * revert getStopsForFixedMode * some updates * update EuiColorPaletteDisplay palette arg * push some logic * push some logic * update validation messages * push some updates * fix some logic * fix some cases * fix JES * fix CI * reset continuity * fix functional tests * fix issue with -infinite/+infinite * fix CI * push some updates * Update x-pack/plugins/lens/public/shared_components/coloring/color_ranges/color_ranges_reducer.tsx Co-authored-by: Marco Liberati <dej611@users.noreply.github.com> * Update x-pack/plugins/lens/public/shared_components/coloring/color_ranges/color_ranges_validation.tsx Co-authored-by: Marco Liberati <dej611@users.noreply.github.com> * fix some comments * make color ranges crud methods "immutable" * fix Max. value input size * fix PR comment * fix tests * Fix edit/min/max buttons behavior * Fix entering decimal values and max/min value behavior * Fix lint * Fix getNormalizedValueByRange for case when min == max * Fix table cell coloring * add warning messages * Move color ranges reducer upper to palette_configuration (#3) * Move color ranges reducer upper to palette_configuration * Remove from local state unnecessary params * Fix some cases * Fix lint * use one dataBounds type across palette configuration * cleanup * Fix some behavior * Fix checks * Some clean up * Some fixes * Some fixes * Fix validation * Fix CI * Add unit tests for color_ranges_crud util * Fix unit test * Add unit tests for color ranges utils.ts * Add allowEditMinMaxValues props and fix validation * Fix CI * Rename allowEditMinMaxValues to disableSwitchingContinuity * Add unit tests for color_ranges_validation * Add unit tests for updateRangeType and changeColorPalette functions * Add unit tests for color_ranges_extra_actions * Fix checks * Clean up code * Some fixes * Fix unit-tests * Fix comments * Some changes * Fix tests * Fix all comments * Fix Checks * Fix CI Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Alexey Antonov <alexwizp@gmail.com> Co-authored-by: Marco Liberati <dej611@users.noreply.github.com>
This commit is contained in:
parent
bf2626f101
commit
95f2967c24
67 changed files with 3084 additions and 1180 deletions
|
@ -156,7 +156,7 @@ describe('HeatmapComponent', function () {
|
|||
expect(component.find(Heatmap).prop('colorScale')).toEqual({
|
||||
bands: [
|
||||
{ color: 'rgb(0, 0, 0)', end: 0, start: 0 },
|
||||
{ color: 'rgb(112, 38, 231)', end: 150, start: 0 },
|
||||
{ color: 'rgb(112, 38, 231)', end: 150.00001, start: 0 },
|
||||
],
|
||||
type: 'bands',
|
||||
});
|
||||
|
|
|
@ -269,8 +269,10 @@ export const HeatmapComponent: FC<HeatmapRenderProps> = memo(
|
|||
);
|
||||
|
||||
// adds a very small number to the max value to make sure the max value will be included
|
||||
const smattering = 0.00001;
|
||||
const endValue =
|
||||
paletteParams && paletteParams.range === 'number' ? paletteParams.rangeMax : max + 0.00000001;
|
||||
(paletteParams?.range === 'number' ? paletteParams.rangeMax : max) + smattering;
|
||||
|
||||
const overwriteColors = uiState?.get('vis.colors') ?? null;
|
||||
|
||||
const bands = ranges.map((start, index, array) => {
|
||||
|
|
|
@ -18,7 +18,6 @@ export type {
|
|||
export { defaultCustomColors, palette, systemPalette } from './palette';
|
||||
|
||||
export { paletteIds } from './constants';
|
||||
|
||||
export type { ColorSchema, RawColorSchema, ColorMap } from './static';
|
||||
export {
|
||||
ColorSchemas,
|
||||
|
@ -31,6 +30,8 @@ export {
|
|||
LabelRotation,
|
||||
defaultCountLabel,
|
||||
MULTILAYER_TIME_AXIS_STYLE,
|
||||
checkIsMinContinuity,
|
||||
checkIsMaxContinuity,
|
||||
} from './static';
|
||||
|
||||
export type { ColorSchemaParams, Labels, Style } from './types';
|
||||
export type { ColorSchemaParams, Labels, Style, PaletteContinuity } from './types';
|
||||
|
|
|
@ -6,10 +6,13 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
|
||||
import type { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { last } from 'lodash';
|
||||
import { paletteIds } from './constants';
|
||||
import { checkIsMaxContinuity, checkIsMinContinuity } from './static';
|
||||
|
||||
import type { PaletteContinuity } from './types';
|
||||
|
||||
export interface CustomPaletteArguments {
|
||||
color?: string[];
|
||||
|
@ -19,7 +22,7 @@ export interface CustomPaletteArguments {
|
|||
range?: 'number' | 'percent';
|
||||
rangeMin?: number;
|
||||
rangeMax?: number;
|
||||
continuity?: 'above' | 'below' | 'all' | 'none';
|
||||
continuity?: PaletteContinuity;
|
||||
}
|
||||
|
||||
export interface CustomPaletteState {
|
||||
|
@ -29,7 +32,7 @@ export interface CustomPaletteState {
|
|||
range: 'number' | 'percent';
|
||||
rangeMin: number;
|
||||
rangeMax: number;
|
||||
continuity?: 'above' | 'below' | 'all' | 'none';
|
||||
continuity?: PaletteContinuity;
|
||||
}
|
||||
|
||||
export interface SystemPaletteArguments {
|
||||
|
@ -169,8 +172,12 @@ export function palette(): ExpressionFunctionDefinition<
|
|||
range: range ?? 'percent',
|
||||
gradient,
|
||||
continuity,
|
||||
rangeMin: calculateRange(rangeMin, stops[0], rangeMinDefault),
|
||||
rangeMax: calculateRange(rangeMax, last(stops), rangeMaxDefault),
|
||||
rangeMin: checkIsMinContinuity(continuity)
|
||||
? Number.NEGATIVE_INFINITY
|
||||
: calculateRange(rangeMin, stops[0], rangeMinDefault),
|
||||
rangeMax: checkIsMaxContinuity(continuity)
|
||||
? Number.POSITIVE_INFINITY
|
||||
: calculateRange(rangeMax, last(stops), rangeMaxDefault),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
|
@ -17,5 +17,5 @@ export {
|
|||
} from './color_maps';
|
||||
|
||||
export { ColorMode, LabelRotation, defaultCountLabel } from './components';
|
||||
|
||||
export { checkIsMaxContinuity, checkIsMinContinuity } from './palette';
|
||||
export * from './styles';
|
||||
|
|
15
src/plugins/charts/common/static/palette/index.ts
Normal file
15
src/plugins/charts/common/static/palette/index.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 { PaletteContinuity } from '../../types';
|
||||
|
||||
export const checkIsMinContinuity = (continuity: PaletteContinuity | undefined) =>
|
||||
Boolean(continuity && ['below', 'all'].includes(continuity));
|
||||
|
||||
export const checkIsMaxContinuity = (continuity: PaletteContinuity | undefined) =>
|
||||
Boolean(continuity && ['above', 'all'].includes(continuity));
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
import { ColorSchemas, LabelRotation } from './static';
|
||||
|
||||
export type PaletteContinuity = 'above' | 'below' | 'none' | 'all';
|
||||
|
||||
export interface ColorSchemaParams {
|
||||
colorSchema: ColorSchemas;
|
||||
invertColors: boolean;
|
||||
|
|
|
@ -73,7 +73,7 @@ describe('workoutColorForValue', () => {
|
|||
{
|
||||
...DEFAULT_PROPS,
|
||||
continuity: 'all',
|
||||
rangeMax: 100,
|
||||
rangeMax: Infinity,
|
||||
stops: [20, 40, 60, 80],
|
||||
},
|
||||
{ min: 0, max: 200 }
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { CustomPaletteState } from '../..';
|
||||
import { checkIsMinContinuity, checkIsMaxContinuity } from '../../../common';
|
||||
import type { CustomPaletteState } from '../..';
|
||||
|
||||
function findColorSegment(
|
||||
value: number,
|
||||
|
@ -20,7 +21,11 @@ function findColorSegment(
|
|||
|
||||
// what about values in range
|
||||
const index = colors.findIndex((c, i) => comparison(value, rangeMin + (1 + i) * step) <= 0);
|
||||
return colors[index] || colors[0];
|
||||
// see comment below in function 'findColorsByStops'
|
||||
return (
|
||||
colors[index] ??
|
||||
(value >= rangeMin + colors.length * step ? colors[colors.length - 1] : colors[0])
|
||||
);
|
||||
}
|
||||
|
||||
function findColorsByStops(
|
||||
|
@ -30,25 +35,62 @@ function findColorsByStops(
|
|||
stops: number[]
|
||||
) {
|
||||
const index = stops.findIndex((s) => comparison(value, s) < 0);
|
||||
return colors[index] || colors[0];
|
||||
// as we now we can provide 'rangeMax' as end for last interval (iterval [lastStop, rangeMax]),
|
||||
// value can be more that last stop but will be valid
|
||||
// because of this we should provide for that value the last color.
|
||||
// (For example, value = 100, last stop = 80, rangeMax = 120, before we was return the first color,
|
||||
// but now we will return the last one)
|
||||
return (
|
||||
colors[index] ?? (value >= stops[stops.length - 1] ? colors[colors.length - 1] : colors[0])
|
||||
);
|
||||
}
|
||||
|
||||
function getNormalizedValueByRange(
|
||||
value: number,
|
||||
{ range }: CustomPaletteState,
|
||||
{ range, rangeMin }: CustomPaletteState,
|
||||
minMax: { min: number; max: number }
|
||||
) {
|
||||
let result = value;
|
||||
if (range === 'percent') {
|
||||
result = (100 * (value - minMax.min)) / (minMax.max - minMax.min);
|
||||
|
||||
// for a range of 1 value the formulas above will divide by 0, so here's a safety guard
|
||||
if (Number.isNaN(result)) {
|
||||
return rangeMin;
|
||||
}
|
||||
}
|
||||
// for a range of 1 value the formulas above will divide by 0, so here's a safety guard
|
||||
if (Number.isNaN(result)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const getNormalizedMaxRange = (
|
||||
{
|
||||
stops,
|
||||
colors,
|
||||
rangeMax,
|
||||
}: Pick<CustomPaletteState, 'stops' | 'continuity' | 'colors' | 'rangeMax'>,
|
||||
isMaxContinuity: boolean,
|
||||
[min, max]: [number, number]
|
||||
) => {
|
||||
if (isMaxContinuity) {
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
return stops.length ? rangeMax : max - (max - min) / colors.length;
|
||||
};
|
||||
|
||||
const getNormalizedMinRange = (
|
||||
{ stops, rangeMin }: Pick<CustomPaletteState, 'stops' | 'continuity' | 'rangeMin'>,
|
||||
isMinContinuity: boolean,
|
||||
min: number
|
||||
) => {
|
||||
if (isMinContinuity) {
|
||||
return Number.NEGATIVE_INFINITY;
|
||||
}
|
||||
|
||||
return stops.length ? rangeMin : min;
|
||||
};
|
||||
|
||||
/**
|
||||
* When stops are empty, it is assumed a predefined palette, so colors are distributed uniformly in the whole data range
|
||||
* When stops are passed, then rangeMin/rangeMax are used as reference for user defined limits:
|
||||
|
@ -63,29 +105,30 @@ export function workoutColorForValue(
|
|||
return;
|
||||
}
|
||||
const { colors, stops, range = 'percent', continuity = 'above', rangeMax, rangeMin } = params;
|
||||
|
||||
const isMinContinuity = checkIsMinContinuity(continuity);
|
||||
const isMaxContinuity = checkIsMaxContinuity(continuity);
|
||||
// ranges can be absolute numbers or percentages
|
||||
// normalized the incoming value to the same format as range to make easier comparisons
|
||||
const normalizedValue = getNormalizedValueByRange(value, params, minMax);
|
||||
const dataRangeArguments = range === 'percent' ? [0, 100] : [minMax.min, minMax.max];
|
||||
|
||||
const [min, max]: [number, number] = range === 'percent' ? [0, 100] : [minMax.min, minMax.max];
|
||||
|
||||
const minRange = getNormalizedMinRange({ stops, rangeMin }, isMinContinuity, min);
|
||||
const maxRange = getNormalizedMaxRange({ stops, colors, rangeMax }, isMaxContinuity, [min, max]);
|
||||
|
||||
const comparisonFn = (v: number, threshold: number) => v - threshold;
|
||||
|
||||
// if steps are defined consider the specific rangeMax/Min as data boundaries
|
||||
// as of max reduce its value by 1/colors.length for correct continuity checks
|
||||
const maxRange = stops.length
|
||||
? rangeMax
|
||||
: dataRangeArguments[1] - (dataRangeArguments[1] - dataRangeArguments[0]) / colors.length;
|
||||
const minRange = stops.length ? rangeMin : dataRangeArguments[0];
|
||||
|
||||
// in case of shorter rangers, extends the steps on the sides to cover the whole set
|
||||
if (comparisonFn(normalizedValue, maxRange) > 0) {
|
||||
if (continuity === 'above' || continuity === 'all') {
|
||||
return colors[colors.length - 1];
|
||||
if (comparisonFn(normalizedValue, minRange) < 0) {
|
||||
if (isMinContinuity) {
|
||||
return colors[0];
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (comparisonFn(normalizedValue, minRange) < 0) {
|
||||
if (continuity === 'below' || continuity === 'all') {
|
||||
return colors[0];
|
||||
|
||||
if (comparisonFn(normalizedValue, maxRange) > 0) {
|
||||
if (isMaxContinuity) {
|
||||
return colors[colors.length - 1];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -94,11 +137,5 @@ export function workoutColorForValue(
|
|||
return findColorsByStops(normalizedValue, comparisonFn, colors, stops);
|
||||
}
|
||||
|
||||
return findColorSegment(
|
||||
normalizedValue,
|
||||
comparisonFn,
|
||||
colors,
|
||||
dataRangeArguments[0],
|
||||
dataRangeArguments[1]
|
||||
);
|
||||
return findColorSegment(normalizedValue, comparisonFn, colors, min, max);
|
||||
}
|
||||
|
|
|
@ -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":{"field":"response.raw","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":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"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":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"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":100,"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":{"field":"response.raw","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":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"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":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"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"}}
|
|
@ -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":{"field":"response.raw","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":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"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":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","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":100,"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":{"field":"response.raw","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":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"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":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","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":{"field":"response.raw","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":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","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":100,"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":{"field":"response.raw","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":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","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":{"field":"response.raw","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":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"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":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"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":100,"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":{"field":"response.raw","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":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"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":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"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":{"field":"response.raw","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":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"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":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","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":100,"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":{"field":"response.raw","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":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"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":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","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":{"field":"response.raw","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":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"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":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"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":100,"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":{"field":"response.raw","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":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"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":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"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"}}
|
|
@ -11,6 +11,7 @@ import type {
|
|||
SerializedFieldFormat,
|
||||
} from '../../../../src/plugins/field_formats/common';
|
||||
import type { Datatable } from '../../../../src/plugins/expressions/common';
|
||||
import type { PaletteContinuity } from '../../../../src/plugins/charts/common';
|
||||
|
||||
export type FormatFactory = (mapping?: SerializedFieldFormat) => IFieldFormat;
|
||||
|
||||
|
@ -50,7 +51,7 @@ export interface CustomPaletteParams {
|
|||
name?: string;
|
||||
reverse?: boolean;
|
||||
rangeType?: 'number' | 'percent';
|
||||
continuity?: 'above' | 'below' | 'all' | 'none';
|
||||
continuity?: PaletteContinuity;
|
||||
progression?: 'fixed';
|
||||
rangeMin?: number;
|
||||
rangeMax?: number;
|
||||
|
|
22
x-pack/plugins/lens/public/assets/distribute_equally.tsx
Normal file
22
x-pack/plugins/lens/public/assets/distribute_equally.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiIconProps } from '@elastic/eui';
|
||||
|
||||
export const DistributeEquallyIcon = (props: Omit<EuiIconProps, 'type'>) => (
|
||||
<svg
|
||||
width="15"
|
||||
height="12"
|
||||
viewBox="0 0 15 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path d="M4 .5a.5.5 0 0 1 1 0v11a.5.5 0 0 1-1 0V.5ZM0 3a1 1 0 0 1 1-1h2v8H1a1 1 0 0 1-1-1V3ZM10.5 0a.5.5 0 0 0-.5.5v11a.5.5 0 0 0 1 0V.5a.5.5 0 0 0-.5-.5ZM6 2h3v8H6V2ZM14 2h-2v8h2a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1Z" />
|
||||
</svg>
|
||||
);
|
15
x-pack/plugins/lens/public/assets/related.tsx
Normal file
15
x-pack/plugins/lens/public/assets/related.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiIconProps } from '@elastic/eui';
|
||||
|
||||
export const RelatedIcon = (props: Omit<EuiIconProps, 'type'>) => (
|
||||
<svg width="24" height="24" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path d="M6 .5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5V8h4V6.5a.5.5 0 0 1 .82-.384l3 2.5a.5.5 0 0 1 0 .768l-3 2.5A.5.5 0 0 1 12 11.5V10H6.5a.5.5 0 0 1-.5-.5v-9Z" />
|
||||
</svg>
|
||||
);
|
21
x-pack/plugins/lens/public/assets/value_max.tsx
Normal file
21
x-pack/plugins/lens/public/assets/value_max.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiIconProps } from '@elastic/eui';
|
||||
|
||||
export const ValueMaxIcon = (props: Omit<EuiIconProps, 'type'>) => (
|
||||
<svg width="16" height="14" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path d="M8 0a.5.5 0 0 1 .384.18l2.5 3A.5.5 0 0 1 10.5 4h-5a.5.5 0 0 1-.384-.82l2.5-3A.5.5 0 0 1 8 0ZM.916 5.223A.5.5 0 0 0 0 5.5v5a.5.5 0 0 0 1 0V7.151l1.084 1.626a.5.5 0 0 0 .832 0L4 7.151V10.5a.5.5 0 0 0 1 0v-5a.5.5 0 0 0-.916-.277L2.5 7.599.916 5.223Z" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.5 5h-.545l-.098.006a3.572 3.572 0 0 0-1.33.36 2.693 2.693 0 0 0-1.052.911C6.18 6.72 6 7.287 6 8v2.5a.5.5 0 0 0 1 0V9h2v1.5a.5.5 0 0 0 1 0v-5a.5.5 0 0 0-.5-.5Zm-.5.5V5v.5ZM7 8h2V6h-.011a2.57 2.57 0 0 0-.297.032c-.202.034-.463.1-.718.228a1.695 1.695 0 0 0-.667.572C7.133 7.092 7 7.462 7 8Z"
|
||||
/>
|
||||
<path d="M11.188 5.11a.5.5 0 0 1 .702.078L13.5 7.2l1.61-2.012a.5.5 0 1 1 .78.624L14.14 8l1.75 2.188a.5.5 0 1 1-.78.624L13.5 8.8l-1.61 2.012a.5.5 0 0 1-.78-.624L12.86 8l-1.75-2.188a.5.5 0 0 1 .078-.702Z" />
|
||||
</svg>
|
||||
);
|
15
x-pack/plugins/lens/public/assets/value_min.tsx
Normal file
15
x-pack/plugins/lens/public/assets/value_min.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiIconProps } from '@elastic/eui';
|
||||
|
||||
export const ValueMinIcon = (props: Omit<EuiIconProps, 'type'>) => (
|
||||
<svg width="16" height="14" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path d="M10.367.018a.5.5 0 0 1 .562.225L13 3.695V.5a.5.5 0 0 1 1 0v5a.5.5 0 0 1-.929.257L11 2.305V5.5a.5.5 0 0 1-1 0v-5a.5.5 0 0 1 .367-.482ZM.916.223A.5.5 0 0 0 0 .5v5a.5.5 0 0 0 1 0V2.151l1.084 1.626a.5.5 0 0 0 .832 0L4 2.151V5.5a.5.5 0 0 0 1 0v-5a.5.5 0 0 0-.916-.277L2.5 2.599.916.223ZM7 11a.5.5 0 0 0 .384-.18l2.5-3A.5.5 0 0 0 9.5 7h-5a.5.5 0 0 0-.384.82l2.5 3A.5.5 0 0 0 7 11ZM6.5 0a.5.5 0 0 0 0 1H7v4h-.5a.5.5 0 0 0 0 1h2a.5.5 0 0 0 0-1H8V1h.5a.5.5 0 0 0 0-1h-2Z" />
|
||||
</svg>
|
||||
);
|
|
@ -27,7 +27,6 @@ import {
|
|||
applyPaletteParams,
|
||||
defaultPaletteParams,
|
||||
FIXED_PROGRESSION,
|
||||
getStopsForFixedMode,
|
||||
useDebouncedValue,
|
||||
PalettePanelContainer,
|
||||
findMinMaxByColumnId,
|
||||
|
@ -352,7 +351,7 @@ export function TableDimensionEditor(
|
|||
<EuiFlexItem>
|
||||
<EuiColorPaletteDisplay
|
||||
data-test-subj="lnsDatatable_dynamicColoring_palette"
|
||||
palette={getStopsForFixedMode(displayStops, activePalette.params?.colorStops)}
|
||||
palette={displayStops.map(({ color }) => color)}
|
||||
type={FIXED_PROGRESSION}
|
||||
onClick={() => {
|
||||
setIsPaletteOpen(!isPaletteOpen);
|
||||
|
|
|
@ -22,7 +22,6 @@ import type {
|
|||
import { LensIconChartDatatable } from '../assets/chart_datatable';
|
||||
import { TableDimensionEditor } from './components/dimension_editor';
|
||||
import { CUSTOM_PALETTE } from '../shared_components/coloring/constants';
|
||||
import { getStopsForFixedMode } from '../shared_components';
|
||||
import { LayerType, layerTypes } from '../../common';
|
||||
import { getDefaultSummaryLabel, PagingState } from '../../common/expressions';
|
||||
import type { ColumnState, SortingState } from '../../common/expressions';
|
||||
|
@ -241,9 +240,9 @@ export const getDatatableVisualization = ({
|
|||
.filter((c) => !datasource!.getOperationForColumnId(c)?.isBucketed)
|
||||
.map((accessor) => {
|
||||
const columnConfig = columnMap[accessor];
|
||||
const hasColoring = Boolean(
|
||||
columnConfig.colorMode !== 'none' && columnConfig.palette?.params?.stops
|
||||
);
|
||||
const stops = columnConfig.palette?.params?.stops;
|
||||
const hasColoring = Boolean(columnConfig.colorMode !== 'none' && stops);
|
||||
|
||||
return {
|
||||
columnId: accessor,
|
||||
triggerIcon: columnConfig.hidden
|
||||
|
@ -251,12 +250,7 @@ export const getDatatableVisualization = ({
|
|||
: hasColoring
|
||||
? 'colorBy'
|
||||
: undefined,
|
||||
palette: hasColoring
|
||||
? getStopsForFixedMode(
|
||||
columnConfig.palette?.params?.stops || [],
|
||||
columnConfig.palette?.params?.colorStops
|
||||
)
|
||||
: undefined,
|
||||
palette: hasColoring && stops ? stops.map(({ color }) => color) : undefined,
|
||||
};
|
||||
}),
|
||||
supportsMoreColumns: true,
|
||||
|
|
|
@ -19,7 +19,6 @@ import type { VisualizationDimensionEditorProps } from '../types';
|
|||
import {
|
||||
CustomizablePalette,
|
||||
FIXED_PROGRESSION,
|
||||
getStopsForFixedMode,
|
||||
PalettePanelContainer,
|
||||
} from '../shared_components/';
|
||||
import './dimension_editor.scss';
|
||||
|
@ -64,7 +63,7 @@ export function HeatmapDimensionEditor(
|
|||
<EuiFlexItem>
|
||||
<EuiColorPaletteDisplay
|
||||
data-test-subj="lnsHeatmap_dynamicColoring_palette"
|
||||
palette={getStopsForFixedMode(displayStops, activePalette?.params?.colorStops)}
|
||||
palette={displayStops.map(({ color }) => color)}
|
||||
type={FIXED_PROGRESSION}
|
||||
onClick={() => {
|
||||
setIsPaletteOpen(!isPaletteOpen);
|
||||
|
@ -93,23 +92,24 @@ export function HeatmapDimensionEditor(
|
|||
isOpen={isPaletteOpen}
|
||||
handleClose={() => setIsPaletteOpen(!isPaletteOpen)}
|
||||
>
|
||||
<CustomizablePalette
|
||||
palettes={props.paletteService}
|
||||
activePalette={activePalette}
|
||||
dataBounds={currentMinMax}
|
||||
showContinuity={false}
|
||||
setPalette={(newPalette) => {
|
||||
// make sure to always have a list of stops
|
||||
if (newPalette.params && !newPalette.params.stops) {
|
||||
newPalette.params.stops = displayStops;
|
||||
}
|
||||
(newPalette as HeatmapVisualizationState['palette'])!.accessor = accessor;
|
||||
setState({
|
||||
...state,
|
||||
palette: newPalette as HeatmapVisualizationState['palette'],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{activePalette && (
|
||||
<CustomizablePalette
|
||||
palettes={props.paletteService}
|
||||
activePalette={activePalette}
|
||||
dataBounds={currentMinMax}
|
||||
setPalette={(newPalette) => {
|
||||
// make sure to always have a list of stops
|
||||
if (newPalette.params && !newPalette.params.stops) {
|
||||
newPalette.params.stops = displayStops;
|
||||
}
|
||||
(newPalette as HeatmapVisualizationState['palette'])!.accessor = accessor;
|
||||
setState({
|
||||
...state,
|
||||
palette: newPalette as HeatmapVisualizationState['palette'],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</PalettePanelContainer>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -19,7 +19,9 @@ export type HeatmapLayerState = HeatmapArguments & {
|
|||
shape: ChartShapes;
|
||||
};
|
||||
|
||||
export type Palette = PaletteOutput<CustomPaletteParams> & { accessor: string };
|
||||
|
||||
export type HeatmapVisualizationState = HeatmapLayerState & {
|
||||
// need to store the current accessor to reset the color stops at accessor change
|
||||
palette?: PaletteOutput<CustomPaletteParams> & { accessor: string };
|
||||
palette?: Palette;
|
||||
};
|
||||
|
|
|
@ -9,7 +9,7 @@ import type { PaletteRegistry } from 'src/plugins/charts/public';
|
|||
import type { Datatable } from 'src/plugins/expressions';
|
||||
import { applyPaletteParams, findMinMaxByColumnId } from '../shared_components';
|
||||
import { DEFAULT_PALETTE_NAME } from './constants';
|
||||
import type { HeatmapVisualizationState } from './types';
|
||||
import type { HeatmapVisualizationState, Palette } from './types';
|
||||
|
||||
export function getSafePaletteParams(
|
||||
paletteService: PaletteRegistry,
|
||||
|
@ -18,9 +18,9 @@ export function getSafePaletteParams(
|
|||
activePalette?: HeatmapVisualizationState['palette']
|
||||
) {
|
||||
if (currentData == null || accessor == null) {
|
||||
return { displayStops: [], activePalette: {} as HeatmapVisualizationState['palette'] };
|
||||
return { displayStops: [], activePalette };
|
||||
}
|
||||
const finalActivePalette: HeatmapVisualizationState['palette'] = activePalette ?? {
|
||||
const finalActivePalette: Palette = activePalette ?? {
|
||||
type: 'palette',
|
||||
name: DEFAULT_PALETTE_NAME,
|
||||
accessor,
|
||||
|
|
|
@ -153,10 +153,7 @@ describe('heatmap', () => {
|
|||
{
|
||||
columnId: 'v-accessor',
|
||||
triggerIcon: 'colorBy',
|
||||
palette: [
|
||||
{ color: 'blue', stop: 100 },
|
||||
{ color: 'yellow', stop: 350 },
|
||||
],
|
||||
palette: ['blue', 'yellow'],
|
||||
},
|
||||
],
|
||||
filterOperations: isCellValueSupported,
|
||||
|
@ -406,6 +403,7 @@ describe('heatmap', () => {
|
|||
],
|
||||
},
|
||||
],
|
||||
lastRangeIsRightOpen: [true],
|
||||
legend: [
|
||||
{
|
||||
type: 'expression',
|
||||
|
|
|
@ -29,7 +29,7 @@ import {
|
|||
LENS_HEATMAP_ID,
|
||||
} from './constants';
|
||||
import { HeatmapToolbar } from './toolbar_component';
|
||||
import { CUSTOM_PALETTE, getStopsForFixedMode } from '../shared_components';
|
||||
import { CUSTOM_PALETTE } from '../shared_components';
|
||||
import { HeatmapDimensionEditor } from './dimension_editor';
|
||||
import { getSafePaletteParams } from './utils';
|
||||
import type { CustomPaletteParams } from '../../common';
|
||||
|
@ -205,10 +205,7 @@ export const getHeatmapVisualization = ({
|
|||
? {
|
||||
columnId: state.valueAccessor,
|
||||
triggerIcon: 'colorBy',
|
||||
palette: getStopsForFixedMode(
|
||||
displayStops,
|
||||
activePalette?.params?.colorStops
|
||||
),
|
||||
palette: displayStops.map(({ color }) => color),
|
||||
}
|
||||
: {
|
||||
columnId: state.valueAccessor,
|
||||
|
@ -317,6 +314,11 @@ export const getHeatmapVisualization = ({
|
|||
xAccessor: [state.xAccessor ?? ''],
|
||||
yAccessor: [state.yAccessor ?? ''],
|
||||
valueAccessor: [state.valueAccessor ?? ''],
|
||||
lastRangeIsRightOpen: [
|
||||
state.palette?.params?.continuity
|
||||
? ['above', 'all'].includes(state.palette.params.continuity)
|
||||
: true,
|
||||
],
|
||||
palette: state.palette?.params
|
||||
? [
|
||||
paletteService
|
||||
|
|
|
@ -23,7 +23,6 @@ import {
|
|||
CustomizablePalette,
|
||||
CUSTOM_PALETTE,
|
||||
FIXED_PROGRESSION,
|
||||
getStopsForFixedMode,
|
||||
PalettePanelContainer,
|
||||
} from '../shared_components';
|
||||
import type { VisualizationDimensionEditorProps } from '../types';
|
||||
|
@ -165,14 +164,7 @@ export function MetricDimensionEditor(
|
|||
<EuiFlexItem>
|
||||
<EuiColorPaletteDisplay
|
||||
data-test-subj="lnsMetric_dynamicColoring_palette"
|
||||
palette={
|
||||
activePalette.params?.name === CUSTOM_PALETTE
|
||||
? getStopsForFixedMode(
|
||||
activePalette.params.stops!,
|
||||
activePalette.params.colorStops
|
||||
)
|
||||
: displayStops.map(({ color }) => color)
|
||||
}
|
||||
palette={displayStops.map(({ color }) => color)}
|
||||
type={FIXED_PROGRESSION}
|
||||
onClick={togglePalette}
|
||||
/>
|
||||
|
|
|
@ -420,7 +420,7 @@ describe('metric_expression', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('it renders no color styling for numeric value if value is higher than rangeMax and continuity is "none"', () => {
|
||||
test('it renders no color styling for numeric value if value is higher than rangeMax', () => {
|
||||
const { data, args } = sampleArgs();
|
||||
|
||||
data.tables.l1.rows[0].c = 500;
|
||||
|
@ -432,7 +432,6 @@ describe('metric_expression', () => {
|
|||
gradient: false,
|
||||
range: 'number',
|
||||
colors: ['red', 'yellow', 'green'],
|
||||
continuity: 'none',
|
||||
};
|
||||
|
||||
const instance = mount(
|
||||
|
@ -453,7 +452,7 @@ describe('metric_expression', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('it renders no color styling for numeric value if value is lower than rangeMin and continuity is "none"', () => {
|
||||
test('it renders no color styling for numeric value if value is lower than rangeMin', () => {
|
||||
const { data, args } = sampleArgs();
|
||||
|
||||
data.tables.l1.rows[0].c = -1;
|
||||
|
@ -465,7 +464,6 @@ describe('metric_expression', () => {
|
|||
gradient: false,
|
||||
range: 'number',
|
||||
colors: ['red', 'yellow', 'green'],
|
||||
continuity: 'none',
|
||||
};
|
||||
|
||||
const instance = mount(
|
||||
|
@ -486,19 +484,18 @@ describe('metric_expression', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('it renders the color styling for numeric value if value is higher than rangeMax and continuity is "all"', () => {
|
||||
test('it renders the correct color styling for numeric value if user select auto detect max value', () => {
|
||||
const { data, args } = sampleArgs();
|
||||
|
||||
data.tables.l1.rows[0].c = 500;
|
||||
args.colorMode = ColorMode.Labels;
|
||||
args.palette.params = {
|
||||
rangeMin: 0,
|
||||
rangeMax: 400,
|
||||
rangeMin: 20,
|
||||
rangeMax: Infinity,
|
||||
stops: [100, 200, 400],
|
||||
gradient: false,
|
||||
range: 'number',
|
||||
colors: ['red', 'yellow', 'green'],
|
||||
continuity: 'all',
|
||||
};
|
||||
|
||||
const instance = mount(
|
||||
|
@ -519,19 +516,18 @@ describe('metric_expression', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('it renders the color styling for numeric value if value is lower than rangeMin and continuity is "all"', () => {
|
||||
test('it renders the correct color styling for numeric value if user select auto detect min value', () => {
|
||||
const { data, args } = sampleArgs();
|
||||
|
||||
data.tables.l1.rows[0].c = -1;
|
||||
args.colorMode = ColorMode.Labels;
|
||||
args.palette.params = {
|
||||
rangeMin: 0,
|
||||
rangeMin: -Infinity,
|
||||
rangeMax: 400,
|
||||
stops: [100, 200, 400],
|
||||
stops: [-Infinity, 200, 400],
|
||||
gradient: false,
|
||||
range: 'number',
|
||||
colors: ['red', 'yellow', 'green'],
|
||||
continuity: 'all',
|
||||
};
|
||||
|
||||
const instance = mount(
|
||||
|
|
|
@ -70,29 +70,28 @@ function getColorStyling(
|
|||
return {};
|
||||
}
|
||||
|
||||
const { continuity = 'above', rangeMin, stops, colors } = palette.params;
|
||||
const penultimateStop = stops[stops.length - 2];
|
||||
const { rangeMin, rangeMax, stops, colors } = palette.params;
|
||||
|
||||
if (continuity === 'none' && (value < rangeMin || value > penultimateStop)) {
|
||||
if (value > rangeMax) {
|
||||
return {};
|
||||
}
|
||||
if (continuity === 'below' && value > penultimateStop) {
|
||||
return {};
|
||||
}
|
||||
if (continuity === 'above' && value < rangeMin) {
|
||||
if (value < rangeMin) {
|
||||
return {};
|
||||
}
|
||||
const cssProp = colorMode === ColorMode.Background ? 'backgroundColor' : 'color';
|
||||
const rawIndex = stops.findIndex((v) => v > value);
|
||||
let rawIndex = stops.findIndex((v) => v > value);
|
||||
|
||||
let colorIndex = rawIndex;
|
||||
if (['all', 'below'].includes(continuity) && value < rangeMin && colorIndex < 0) {
|
||||
colorIndex = 0;
|
||||
if (!isFinite(rangeMax) && value > stops[stops.length - 1]) {
|
||||
rawIndex = stops.length - 1;
|
||||
}
|
||||
if (['all', 'above'].includes(continuity) && value > penultimateStop && colorIndex < 0) {
|
||||
colorIndex = stops.length - 1;
|
||||
|
||||
// in this case first stop is -Infinity
|
||||
if (!isFinite(rangeMin) && value < (isFinite(stops[0]) ? stops[0] : stops[1])) {
|
||||
rawIndex = 0;
|
||||
}
|
||||
|
||||
const colorIndex = rawIndex;
|
||||
|
||||
const color = colors[colorIndex];
|
||||
const styling = {
|
||||
[cssProp]: color,
|
||||
|
|
|
@ -19,7 +19,7 @@ import { LensIconChartMetric } from '../assets/chart_metric';
|
|||
import { Visualization, OperationMetadata, DatasourcePublicAPI } from '../types';
|
||||
import type { MetricConfig, MetricState } from '../../common/expressions';
|
||||
import { layerTypes } from '../../common';
|
||||
import { CUSTOM_PALETTE, getStopsForFixedMode, shiftPalette } from '../shared_components';
|
||||
import { CUSTOM_PALETTE, shiftPalette } from '../shared_components';
|
||||
import { MetricDimensionEditor } from './dimension_editor';
|
||||
|
||||
const toExpression = (
|
||||
|
@ -146,11 +146,7 @@ export const getMetricVisualization = ({
|
|||
{
|
||||
columnId: props.state.accessor,
|
||||
triggerIcon: hasColoring ? 'colorBy' : undefined,
|
||||
palette: hasColoring
|
||||
? props.state.palette?.params?.name === CUSTOM_PALETTE
|
||||
? getStopsForFixedMode(stops, props.state.palette?.params.colorStops)
|
||||
: stops.map(({ color }) => color)
|
||||
: undefined,
|
||||
palette: hasColoring ? stops.map(({ color }) => color) : undefined,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
|
|
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { mountWithIntl } from '@kbn/test/jest';
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { ColorRanges, ColorRangesProps } from './color_ranges';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { PaletteRegistry } from 'src/plugins/charts/public';
|
||||
import { ColorRangesContext } from './color_ranges_context';
|
||||
|
||||
const extraActionSelectors = {
|
||||
addColorRange: '[data-test-subj^="lnsPalettePanel_dynamicColoring_addColorRange"]',
|
||||
reverseColors: '[data-test-subj^="lnsPalettePanel_dynamicColoring_reverseColors"]',
|
||||
distributeEqually: '[data-test-subj="lnsPalettePanel_dynamicColoring_distributeEqually"]',
|
||||
};
|
||||
|
||||
const pageObjects = {
|
||||
getAddColorRangeButton: (component: ReactWrapper) =>
|
||||
component.find(extraActionSelectors.addColorRange).first(),
|
||||
reverseColors: (component: ReactWrapper) =>
|
||||
component.find(extraActionSelectors.reverseColors).first(),
|
||||
distributeEqually: (component: ReactWrapper) =>
|
||||
component.find(extraActionSelectors.distributeEqually).first(),
|
||||
};
|
||||
|
||||
function renderColorRanges(props: ColorRangesProps) {
|
||||
return mountWithIntl(
|
||||
<ColorRangesContext.Provider
|
||||
value={{
|
||||
dataBounds: { min: 0, max: 100 },
|
||||
palettes: {} as PaletteRegistry,
|
||||
}}
|
||||
>
|
||||
<ColorRanges {...props} />
|
||||
</ColorRangesContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('Color Ranges', () => {
|
||||
let props: ColorRangesProps;
|
||||
const dispatch = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
dispatch.mockClear();
|
||||
props = {
|
||||
colorRanges: [
|
||||
{ color: '#aaa', start: 20, end: 40 },
|
||||
{ color: '#bbb', start: 40, end: 60 },
|
||||
{ color: '#ccc', start: 60, end: 80 },
|
||||
],
|
||||
paletteConfiguration: {
|
||||
rangeType: 'number',
|
||||
continuity: 'none',
|
||||
},
|
||||
showExtraActions: true,
|
||||
dispatch,
|
||||
};
|
||||
});
|
||||
|
||||
it('should display all the color ranges passed', () => {
|
||||
const component = renderColorRanges(props);
|
||||
|
||||
expect(component.find('ColorRangeItem')).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('should disable "add new" button if there is maxStops configured', () => {
|
||||
props.colorRanges = [
|
||||
{ color: '#aaa', start: 20, end: 40 },
|
||||
{ color: '#bbb', start: 40, end: 60 },
|
||||
{ color: '#ccc', start: 60, end: 80 },
|
||||
{ color: '#ccc', start: 80, end: 90 },
|
||||
{ color: '#ccc', start: 90, end: 100 },
|
||||
];
|
||||
const component = renderColorRanges({ ...props, paletteConfiguration: { maxSteps: 5 } });
|
||||
|
||||
expect(pageObjects.getAddColorRangeButton(component).prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('should add a new range with default color and reasonable distance from last one', () => {
|
||||
const component = renderColorRanges(props);
|
||||
|
||||
act(() => {
|
||||
pageObjects.getAddColorRangeButton(component).simulate('click');
|
||||
});
|
||||
|
||||
component.update();
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: 'addColorRange',
|
||||
payload: { dataBounds: { min: 0, max: 100 }, palettes: {} },
|
||||
});
|
||||
});
|
||||
|
||||
it('should sort ranges value on whole component blur', () => {
|
||||
props.colorRanges = [
|
||||
{ color: '#aaa', start: 65, end: 40 },
|
||||
{ color: '#bbb', start: 40, end: 60 },
|
||||
{ color: '#ccc', start: 60, end: 80 },
|
||||
];
|
||||
const component = renderColorRanges(props);
|
||||
const firstInput = component.find('ColorRangeItem').first().find('input').first();
|
||||
|
||||
act(() => {
|
||||
firstInput.simulate('blur');
|
||||
});
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: 'sortColorRanges',
|
||||
payload: {
|
||||
dataBounds: { min: 0, max: 100 },
|
||||
palettes: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should reverse colors when user click "reverse"', () => {
|
||||
props.colorRanges = [
|
||||
{ color: '#aaa', start: 10, end: 40 },
|
||||
{ color: '#bbb', start: 40, end: 60 },
|
||||
{ color: '#ccc', start: 60, end: 90 },
|
||||
{ color: '#ddd', start: 90, end: 130 },
|
||||
];
|
||||
const component = renderColorRanges(props);
|
||||
|
||||
act(() => {
|
||||
pageObjects.reverseColors(component).simulate('click');
|
||||
});
|
||||
|
||||
component.update();
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: 'reversePalette',
|
||||
payload: {
|
||||
dataBounds: { min: 0, max: 100 },
|
||||
palettes: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should distribute equally ranges when use click on "Distribute equally" button', () => {
|
||||
props.colorRanges = [
|
||||
{ color: '#aaa', start: 0, end: 2 },
|
||||
{ color: '#bbb', start: 3, end: 4 },
|
||||
{ color: '#ccc', start: 5, end: 6 },
|
||||
{ color: '#ccc', start: 7, end: 8 },
|
||||
];
|
||||
|
||||
const component = renderColorRanges(props);
|
||||
|
||||
act(() => {
|
||||
pageObjects.distributeEqually(component).simulate('click');
|
||||
});
|
||||
|
||||
component.update();
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: 'distributeEqually',
|
||||
payload: { dataBounds: { min: 0, max: 100 }, palettes: {} },
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useState, useEffect, Dispatch, useContext } from 'react';
|
||||
|
||||
import { EuiFlexGroup, EuiTextColor, EuiFlexItem } from '@elastic/eui';
|
||||
|
||||
import { ColorRangesExtraActions } from './color_ranges_extra_actions';
|
||||
import { ColorRangeItem } from './color_ranges_item';
|
||||
import {
|
||||
validateColorRanges,
|
||||
getErrorMessages,
|
||||
ColorRangeValidation,
|
||||
} from './color_ranges_validation';
|
||||
|
||||
import type { CustomPaletteParamsConfig } from '../../../../common';
|
||||
import type { ColorRange } from './types';
|
||||
import type { PaletteConfigurationActions } from '../types';
|
||||
|
||||
import { defaultPaletteParams } from '../constants';
|
||||
|
||||
import { ColorRangesContext } from './color_ranges_context';
|
||||
|
||||
export interface ColorRangesProps {
|
||||
colorRanges: ColorRange[];
|
||||
paletteConfiguration: CustomPaletteParamsConfig | undefined;
|
||||
showExtraActions?: boolean;
|
||||
dispatch: Dispatch<PaletteConfigurationActions>;
|
||||
}
|
||||
|
||||
export function ColorRanges({
|
||||
colorRanges,
|
||||
paletteConfiguration,
|
||||
showExtraActions = true,
|
||||
dispatch,
|
||||
}: ColorRangesProps) {
|
||||
const { dataBounds } = useContext(ColorRangesContext);
|
||||
const [colorRangesValidity, setColorRangesValidity] = useState<
|
||||
Record<string, ColorRangeValidation>
|
||||
>({});
|
||||
|
||||
const lastColorRange = colorRanges[colorRanges.length - 1];
|
||||
const errors = getErrorMessages(colorRangesValidity);
|
||||
const continuity = paletteConfiguration?.continuity ?? defaultPaletteParams.continuity;
|
||||
const rangeType = paletteConfiguration?.rangeType ?? defaultPaletteParams.rangeType;
|
||||
|
||||
useEffect(() => {
|
||||
setColorRangesValidity(validateColorRanges(colorRanges, dataBounds, rangeType));
|
||||
}, [colorRanges, rangeType, dataBounds]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
data-test-subj={`lnsPalettePanel_dynamicColoring_custom_color_ranges`}
|
||||
direction="column"
|
||||
gutterSize="s"
|
||||
>
|
||||
{colorRanges.map((colorRange, index) => (
|
||||
<EuiFlexItem grow={false} key={`${colorRange.end ?? 0 + colorRange.start ?? 0}${index}`}>
|
||||
<ColorRangeItem
|
||||
colorRange={colorRange}
|
||||
dispatch={dispatch}
|
||||
colorRanges={colorRanges}
|
||||
continuity={continuity}
|
||||
rangeType={rangeType}
|
||||
index={index}
|
||||
validation={colorRangesValidity[index]}
|
||||
accessor="start"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
{lastColorRange ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<ColorRangeItem
|
||||
colorRange={lastColorRange}
|
||||
dispatch={dispatch}
|
||||
colorRanges={colorRanges}
|
||||
continuity={continuity}
|
||||
rangeType={rangeType}
|
||||
index={colorRanges.length - 1}
|
||||
validation={colorRangesValidity.last}
|
||||
accessor="end"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
<EuiFlexItem grow={false}>
|
||||
{errors.map((error) => (
|
||||
<EuiTextColor color="danger">{error}</EuiTextColor>
|
||||
))}
|
||||
</EuiFlexItem>
|
||||
{showExtraActions ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<ColorRangesExtraActions
|
||||
dispatch={dispatch}
|
||||
shouldDisableAdd={Boolean(
|
||||
(paletteConfiguration?.maxSteps &&
|
||||
colorRanges.length >= paletteConfiguration?.maxSteps) ||
|
||||
errors.length
|
||||
)}
|
||||
shouldDisableDistribute={Boolean(colorRanges.length === 1)}
|
||||
shouldDisableReverse={Boolean(colorRanges.length === 1)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type { PaletteRegistry } from 'src/plugins/charts/public';
|
||||
import type { DataBounds } from '../types';
|
||||
|
||||
interface ColorRangesContextType {
|
||||
dataBounds: DataBounds;
|
||||
palettes: PaletteRegistry;
|
||||
disableSwitchingContinuity?: boolean;
|
||||
}
|
||||
|
||||
export const ColorRangesContext = React.createContext<ColorRangesContextType>(
|
||||
{} as ColorRangesContextType
|
||||
);
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { useCallback, Dispatch, useContext } from 'react';
|
||||
import { EuiFlexGroup, EuiButtonEmpty, EuiFlexItem } from '@elastic/eui';
|
||||
|
||||
import { DistributeEquallyIcon } from '../../../assets/distribute_equally';
|
||||
import { TooltipWrapper } from '../../index';
|
||||
|
||||
import type { ColorRangesActions } from './types';
|
||||
import { ColorRangesContext } from './color_ranges_context';
|
||||
|
||||
export interface ColorRangesExtraActionsProps {
|
||||
dispatch: Dispatch<ColorRangesActions>;
|
||||
shouldDisableAdd?: boolean;
|
||||
shouldDisableReverse?: boolean;
|
||||
shouldDisableDistribute?: boolean;
|
||||
}
|
||||
|
||||
export function ColorRangesExtraActions({
|
||||
dispatch,
|
||||
shouldDisableAdd = false,
|
||||
shouldDisableReverse = false,
|
||||
shouldDisableDistribute = false,
|
||||
}: ColorRangesExtraActionsProps) {
|
||||
const { dataBounds, palettes } = useContext(ColorRangesContext);
|
||||
const onAddColorRange = useCallback(() => {
|
||||
dispatch({
|
||||
type: 'addColorRange',
|
||||
payload: { dataBounds, palettes },
|
||||
});
|
||||
}, [dataBounds, dispatch, palettes]);
|
||||
|
||||
const onReversePalette = useCallback(() => {
|
||||
dispatch({ type: 'reversePalette', payload: { dataBounds, palettes } });
|
||||
}, [dispatch, dataBounds, palettes]);
|
||||
|
||||
const onDistributeEqually = useCallback(() => {
|
||||
dispatch({ type: 'distributeEqually', payload: { dataBounds, palettes } });
|
||||
}, [dataBounds, dispatch, palettes]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup justifyContent="flexStart" gutterSize="none" wrap={true}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<TooltipWrapper
|
||||
tooltipContent={i18n.translate(
|
||||
'xpack.lens.dynamicColoring.customPalette.maximumStepsApplied',
|
||||
{
|
||||
defaultMessage: `You've applied the maximum number of steps`,
|
||||
}
|
||||
)}
|
||||
condition={shouldDisableAdd}
|
||||
position="top"
|
||||
delay="regular"
|
||||
>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj={`lnsPalettePanel_dynamicColoring_addColorRange`}
|
||||
iconType="plusInCircle"
|
||||
color="primary"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.lens.dynamicColoring.customPalette.addColorRangeAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Add color range',
|
||||
}
|
||||
)}
|
||||
size="xs"
|
||||
flush="left"
|
||||
disabled={shouldDisableAdd}
|
||||
onClick={onAddColorRange}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.dynamicColoring.customPalette.addColorRange"
|
||||
defaultMessage="Add color range"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</TooltipWrapper>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj={`lnsPalettePanel_dynamicColoring_reverseColors`}
|
||||
iconType="sortable"
|
||||
color="primary"
|
||||
aria-label={i18n.translate('xpack.lens.dynamicColoring.customPaletteAriaLabel', {
|
||||
defaultMessage: 'Reverse colors',
|
||||
})}
|
||||
size="xs"
|
||||
flush="left"
|
||||
onClick={onReversePalette}
|
||||
disabled={shouldDisableReverse}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.dynamicColoring.customPalette.reverseColors"
|
||||
defaultMessage="Reverse colors"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj={`lnsPalettePanel_dynamicColoring_distributeEqually`}
|
||||
iconType={DistributeEquallyIcon}
|
||||
color="primary"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.lens.dynamicColoring.customPalette.distributeEquallyAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Distribute equally',
|
||||
}
|
||||
)}
|
||||
size="xs"
|
||||
flush="left"
|
||||
disabled={shouldDisableDistribute}
|
||||
onClick={onDistributeEqually}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.dynamicColoring.customPalette.distributeEqually"
|
||||
defaultMessage="Distribute equally"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,238 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import useUpdateEffect from 'react-use/lib/useUpdateEffect';
|
||||
import React, { useState, useCallback, Dispatch, FocusEvent, useContext } from 'react';
|
||||
|
||||
import {
|
||||
EuiFieldNumber,
|
||||
EuiColorPicker,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiIcon,
|
||||
EuiColorPickerSwatch,
|
||||
EuiButtonIcon,
|
||||
EuiToolTip,
|
||||
EuiFieldNumberProps,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { RelatedIcon } from '../../../assets/related';
|
||||
import { isLastItem } from './utils';
|
||||
import { isValidColor } from '../utils';
|
||||
import {
|
||||
ColorRangeDeleteButton,
|
||||
ColorRangeAutoDetectButton,
|
||||
ColorRangeEditButton,
|
||||
} from './color_ranges_item_buttons';
|
||||
|
||||
import type { ColorRange, ColorRangeAccessor, ColorRangesActions } from './types';
|
||||
import { ColorRangesContext } from './color_ranges_context';
|
||||
import type { ColorRangeValidation } from './color_ranges_validation';
|
||||
import type { CustomPaletteParams } from '../../../../common';
|
||||
import {
|
||||
PaletteContinuity,
|
||||
checkIsMaxContinuity,
|
||||
checkIsMinContinuity,
|
||||
} from '../../../../../../../src/plugins/charts/common';
|
||||
import { getOutsideDataBoundsWarningMessage } from './color_ranges_validation';
|
||||
|
||||
export interface ColorRangesItemProps {
|
||||
colorRange: ColorRange;
|
||||
index: number;
|
||||
colorRanges: ColorRange[];
|
||||
dispatch: Dispatch<ColorRangesActions>;
|
||||
rangeType: CustomPaletteParams['rangeType'];
|
||||
continuity: PaletteContinuity;
|
||||
accessor: ColorRangeAccessor;
|
||||
validation?: ColorRangeValidation;
|
||||
}
|
||||
|
||||
type ColorRangeItemMode = 'value' | 'auto' | 'edit';
|
||||
|
||||
const getMode = (
|
||||
index: ColorRangesItemProps['index'],
|
||||
isLast: boolean,
|
||||
continuity: PaletteContinuity
|
||||
): ColorRangeItemMode => {
|
||||
if (!isLast && index > 0) {
|
||||
return 'value';
|
||||
}
|
||||
return (isLast ? checkIsMaxContinuity : checkIsMinContinuity)(continuity) ? 'auto' : 'edit';
|
||||
};
|
||||
|
||||
const getPlaceholderForAutoMode = (isLast: boolean) =>
|
||||
isLast
|
||||
? i18n.translate('xpack.lens.dynamicColoring.customPalette.maxValuePlaceholder', {
|
||||
defaultMessage: 'Max. value',
|
||||
})
|
||||
: i18n.translate('xpack.lens.dynamicColoring.customPalette.minValuePlaceholder', {
|
||||
defaultMessage: 'Min. value',
|
||||
});
|
||||
|
||||
const getActionButton = (mode: ColorRangeItemMode) => {
|
||||
if (mode === 'value') {
|
||||
return ColorRangeDeleteButton;
|
||||
}
|
||||
return mode === 'edit' ? ColorRangeAutoDetectButton : ColorRangeEditButton;
|
||||
};
|
||||
|
||||
const getAppend = (
|
||||
rangeType: CustomPaletteParams['rangeType'],
|
||||
mode: ColorRangeItemMode,
|
||||
validation?: ColorRangeValidation
|
||||
) => {
|
||||
const items: EuiFieldNumberProps['append'] = [];
|
||||
|
||||
if (rangeType === 'percent') {
|
||||
items.push('%');
|
||||
}
|
||||
|
||||
if (mode !== 'auto' && validation?.warnings.length) {
|
||||
items.push(
|
||||
<EuiToolTip position="top" content={getOutsideDataBoundsWarningMessage(validation.warnings)}>
|
||||
<EuiIcon type="alert" size="m" color="warning" />
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
export function ColorRangeItem({
|
||||
accessor,
|
||||
index,
|
||||
colorRange,
|
||||
rangeType,
|
||||
colorRanges,
|
||||
validation,
|
||||
continuity,
|
||||
dispatch,
|
||||
}: ColorRangesItemProps) {
|
||||
const { dataBounds, palettes } = useContext(ColorRangesContext);
|
||||
const [popoverInFocus, setPopoverInFocus] = useState<boolean>(false);
|
||||
const [localValue, setLocalValue] = useState<number | undefined>(colorRange[accessor]);
|
||||
const isLast = isLastItem(accessor);
|
||||
const mode = getMode(index, isLast, continuity);
|
||||
const isDisabled = mode === 'auto';
|
||||
const isColorValid = isValidColor(colorRange.color);
|
||||
const ActionButton = getActionButton(mode);
|
||||
const isValid = validation?.isValid ?? true;
|
||||
|
||||
const onLeaveFocus = useCallback(
|
||||
(e: FocusEvent<HTMLDivElement>) => {
|
||||
const prevStartValue = colorRanges[index - 1]?.start ?? Number.NEGATIVE_INFINITY;
|
||||
const nextStartValue = colorRanges[index + 1]?.start ?? Number.POSITIVE_INFINITY;
|
||||
|
||||
const shouldSort = colorRange.start > nextStartValue || prevStartValue > colorRange.start;
|
||||
const isFocusStillInContent =
|
||||
(e.currentTarget as Node)?.contains(e.relatedTarget as Node) || popoverInFocus;
|
||||
|
||||
if (shouldSort && !isFocusStillInContent) {
|
||||
dispatch({ type: 'sortColorRanges', payload: { dataBounds, palettes } });
|
||||
}
|
||||
},
|
||||
[colorRange.start, colorRanges, dispatch, index, popoverInFocus, dataBounds, palettes]
|
||||
);
|
||||
|
||||
const onValueChange = useCallback(
|
||||
({ target: { value: targetValue } }) => {
|
||||
setLocalValue(targetValue);
|
||||
dispatch({
|
||||
type: 'updateValue',
|
||||
payload: { index, value: targetValue, accessor, dataBounds, palettes },
|
||||
});
|
||||
},
|
||||
[dispatch, index, accessor, dataBounds, palettes]
|
||||
);
|
||||
|
||||
const onUpdateColor = useCallback(
|
||||
(color) => {
|
||||
dispatch({ type: 'updateColor', payload: { index, color, dataBounds, palettes } });
|
||||
},
|
||||
[dispatch, index, dataBounds, palettes]
|
||||
);
|
||||
|
||||
useUpdateEffect(() => {
|
||||
if (!Number.isNaN(colorRange[accessor]) && colorRange[accessor] !== localValue) {
|
||||
setLocalValue(colorRange[accessor]);
|
||||
}
|
||||
}, [localValue, colorRange, accessor]);
|
||||
|
||||
const selectNewColorText = i18n.translate(
|
||||
'xpack.lens.dynamicColoring.customPalette.selectNewColor',
|
||||
{
|
||||
defaultMessage: 'Select a new color',
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" wrap={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
{!isLast ? (
|
||||
<EuiColorPicker
|
||||
onChange={onUpdateColor}
|
||||
button={
|
||||
isColorValid ? (
|
||||
<EuiColorPickerSwatch color={colorRange.color} aria-label={selectNewColorText} />
|
||||
) : (
|
||||
<EuiButtonIcon
|
||||
color="danger"
|
||||
iconType="stopSlash"
|
||||
iconSize="l"
|
||||
aria-label={selectNewColorText}
|
||||
/>
|
||||
)
|
||||
}
|
||||
secondaryInputDisplay="top"
|
||||
color={colorRange.color}
|
||||
onFocus={() => setPopoverInFocus(true)}
|
||||
onBlur={() => {
|
||||
setPopoverInFocus(false);
|
||||
}}
|
||||
isInvalid={!isColorValid}
|
||||
/>
|
||||
) : (
|
||||
<EuiIcon type={RelatedIcon} size="l" />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiFieldNumber
|
||||
compressed
|
||||
fullWidth={true}
|
||||
isInvalid={!isValid}
|
||||
value={mode !== 'auto' ? localValue : ''}
|
||||
disabled={isDisabled}
|
||||
onChange={onValueChange}
|
||||
placeholder={mode === 'auto' ? getPlaceholderForAutoMode(isLast) : ''}
|
||||
append={getAppend(rangeType, mode, validation)}
|
||||
onBlur={onLeaveFocus}
|
||||
data-test-subj={`lnsPalettePanel_dynamicColoring_range_value_${index}`}
|
||||
prepend={<span className="euiFormLabel">{isLast ? '\u2264' : '\u2265'}</span>}
|
||||
aria-label={i18n.translate('xpack.lens.dynamicColoring.customPalette.rangeAriaLabel', {
|
||||
defaultMessage: 'Range {index}',
|
||||
values: {
|
||||
index: index + 1,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{ActionButton ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<ActionButton
|
||||
index={index}
|
||||
continuity={continuity}
|
||||
rangeType={rangeType}
|
||||
colorRanges={colorRanges}
|
||||
dispatch={dispatch}
|
||||
accessor={accessor}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Dispatch, useCallback, useContext } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiButtonIcon } from '@elastic/eui';
|
||||
|
||||
import { ValueMaxIcon } from '../../../assets/value_max';
|
||||
import { ValueMinIcon } from '../../../assets/value_min';
|
||||
import { isLastItem } from './utils';
|
||||
import { TooltipWrapper } from '../../index';
|
||||
|
||||
import type { ColorRangesActions, ColorRange, ColorRangeAccessor } from './types';
|
||||
import { ColorRangesContext } from './color_ranges_context';
|
||||
import type { CustomPaletteParams } from '../../../../common';
|
||||
import type { PaletteContinuity } from '../../../../../../../src/plugins/charts/common';
|
||||
|
||||
export interface ColorRangesItemButtonProps {
|
||||
index: number;
|
||||
colorRanges: ColorRange[];
|
||||
rangeType: CustomPaletteParams['rangeType'];
|
||||
continuity: PaletteContinuity;
|
||||
dispatch: Dispatch<ColorRangesActions>;
|
||||
accessor: ColorRangeAccessor;
|
||||
}
|
||||
|
||||
const switchContinuity = (isLast: boolean, continuity: PaletteContinuity) => {
|
||||
switch (continuity) {
|
||||
case 'none':
|
||||
return isLast ? 'above' : 'below';
|
||||
case 'above':
|
||||
return isLast ? 'none' : 'all';
|
||||
case 'below':
|
||||
return isLast ? 'all' : 'none';
|
||||
case 'all':
|
||||
return isLast ? 'below' : 'above';
|
||||
}
|
||||
};
|
||||
|
||||
export function ColorRangeDeleteButton({ index, dispatch }: ColorRangesItemButtonProps) {
|
||||
const { dataBounds, palettes } = useContext(ColorRangesContext);
|
||||
const onExecuteAction = useCallback(() => {
|
||||
dispatch({ type: 'deleteColorRange', payload: { index, dataBounds, palettes } });
|
||||
}, [dispatch, index, dataBounds, palettes]);
|
||||
|
||||
const title = i18n.translate('xpack.lens.dynamicColoring.customPalette.deleteButtonAriaLabel', {
|
||||
defaultMessage: 'Delete',
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
aria-label={title}
|
||||
title={title}
|
||||
onClick={onExecuteAction}
|
||||
data-test-subj={`lnsPalettePanel_dynamicColoring_removeColorRange_${index}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ColorRangeEditButton({
|
||||
index,
|
||||
continuity,
|
||||
dispatch,
|
||||
accessor,
|
||||
}: ColorRangesItemButtonProps) {
|
||||
const { dataBounds, palettes, disableSwitchingContinuity } = useContext(ColorRangesContext);
|
||||
const isLast = isLastItem(accessor);
|
||||
|
||||
const onExecuteAction = useCallback(() => {
|
||||
const newContinuity = switchContinuity(isLast, continuity);
|
||||
|
||||
dispatch({
|
||||
type: 'updateContinuity',
|
||||
payload: { isLast, continuity: newContinuity, dataBounds, palettes },
|
||||
});
|
||||
}, [isLast, dispatch, continuity, dataBounds, palettes]);
|
||||
|
||||
const title = i18n.translate('xpack.lens.dynamicColoring.customPalette.editButtonAriaLabel', {
|
||||
defaultMessage: 'Edit',
|
||||
});
|
||||
|
||||
return (
|
||||
<TooltipWrapper
|
||||
tooltipContent={i18n.translate(
|
||||
'xpack.lens.dynamicColoring.customPalette.disallowedEditMinMaxValues',
|
||||
{
|
||||
defaultMessage: `For current configuration you can not set custom value`,
|
||||
}
|
||||
)}
|
||||
condition={Boolean(disableSwitchingContinuity)}
|
||||
position="top"
|
||||
delay="regular"
|
||||
>
|
||||
<EuiButtonIcon
|
||||
iconType="pencil"
|
||||
aria-label={title}
|
||||
title={title}
|
||||
disabled={disableSwitchingContinuity}
|
||||
onClick={onExecuteAction}
|
||||
data-test-subj={`lnsPalettePanel_dynamicColoring_editValue_${index}`}
|
||||
/>
|
||||
</TooltipWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export function ColorRangeAutoDetectButton({
|
||||
continuity,
|
||||
dispatch,
|
||||
accessor,
|
||||
}: ColorRangesItemButtonProps) {
|
||||
const { dataBounds, palettes } = useContext(ColorRangesContext);
|
||||
const isLast = isLastItem(accessor);
|
||||
|
||||
const onExecuteAction = useCallback(() => {
|
||||
const newContinuity = switchContinuity(isLast, continuity);
|
||||
|
||||
dispatch({
|
||||
type: 'updateContinuity',
|
||||
payload: { isLast, continuity: newContinuity, dataBounds, palettes },
|
||||
});
|
||||
}, [continuity, dataBounds, dispatch, isLast, palettes]);
|
||||
|
||||
const title = isLast
|
||||
? i18n.translate('xpack.lens.dynamicColoring.customPalette.autoDetectMaximumAriaLabel', {
|
||||
defaultMessage: 'Auto detect maximum value',
|
||||
})
|
||||
: i18n.translate('xpack.lens.dynamicColoring.customPalette.autoDetectMinimumAriaLabel', {
|
||||
defaultMessage: 'Auto detect minimum value',
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
iconType={isLast ? ValueMaxIcon : ValueMinIcon}
|
||||
aria-label={title}
|
||||
title={title}
|
||||
onClick={onExecuteAction}
|
||||
data-test-subj={`lnsPalettePanel_dynamicColoring_autoDetect_${
|
||||
isLast ? 'maximum' : 'minimum'
|
||||
}`}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { validateColorRanges, isAllColorRangesValid } from './color_ranges_validation';
|
||||
|
||||
describe('Color ranges validation', () => {
|
||||
describe('validateColorRanges', () => {
|
||||
it('should return correct valid state for color ranges', () => {
|
||||
const colorRanges = [
|
||||
{
|
||||
start: 0,
|
||||
end: 10,
|
||||
color: '#aaa',
|
||||
},
|
||||
{
|
||||
start: 10,
|
||||
end: 20,
|
||||
color: '',
|
||||
},
|
||||
{
|
||||
start: 20,
|
||||
end: 15,
|
||||
color: '#aaa',
|
||||
},
|
||||
];
|
||||
const validation = validateColorRanges(colorRanges, { min: 0, max: 100 }, 'number');
|
||||
expect(validation['0']).toEqual({
|
||||
errors: [],
|
||||
warnings: [],
|
||||
isValid: true,
|
||||
});
|
||||
expect(validation['1']).toEqual({
|
||||
errors: ['invalidColor'],
|
||||
warnings: [],
|
||||
isValid: false,
|
||||
});
|
||||
expect(validation.last).toEqual({
|
||||
errors: ['greaterThanMaxValue'],
|
||||
warnings: [],
|
||||
isValid: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return correct warnings for color ranges', () => {
|
||||
const colorRanges = [
|
||||
{
|
||||
start: 0,
|
||||
end: 10,
|
||||
color: '#aaa',
|
||||
},
|
||||
{
|
||||
start: 10,
|
||||
end: 20,
|
||||
color: '#bbb',
|
||||
},
|
||||
{
|
||||
start: 20,
|
||||
end: 35,
|
||||
color: '#ccc',
|
||||
},
|
||||
];
|
||||
const validation = validateColorRanges(colorRanges, { min: 5, max: 30 }, 'number');
|
||||
expect(validation['0']).toEqual({
|
||||
errors: [],
|
||||
warnings: ['lowerThanDataBounds'],
|
||||
isValid: true,
|
||||
});
|
||||
expect(validation['1']).toEqual({
|
||||
errors: [],
|
||||
warnings: [],
|
||||
isValid: true,
|
||||
});
|
||||
expect(validation.last).toEqual({
|
||||
errors: [],
|
||||
warnings: ['greaterThanDataBounds'],
|
||||
isValid: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not return warnings for color ranges in number mode if we get fallback as data bounds', () => {
|
||||
const colorRanges = [
|
||||
{
|
||||
start: 0,
|
||||
end: 10,
|
||||
color: '#aaa',
|
||||
},
|
||||
{
|
||||
start: 10,
|
||||
end: 20,
|
||||
color: '#bbb',
|
||||
},
|
||||
{
|
||||
start: 20,
|
||||
end: 35,
|
||||
color: '#ccc',
|
||||
},
|
||||
];
|
||||
const validation = validateColorRanges(
|
||||
colorRanges,
|
||||
{ min: 5, max: 30, fallback: true },
|
||||
'number'
|
||||
);
|
||||
expect(validation['0']).toEqual({
|
||||
errors: [],
|
||||
warnings: [],
|
||||
isValid: true,
|
||||
});
|
||||
expect(validation['1']).toEqual({
|
||||
errors: [],
|
||||
warnings: [],
|
||||
isValid: true,
|
||||
});
|
||||
expect(validation.last).toEqual({
|
||||
errors: [],
|
||||
warnings: [],
|
||||
isValid: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAllColorRangesValid', () => {
|
||||
it('should return true if all color ranges is valid', () => {
|
||||
const colorRanges = [
|
||||
{
|
||||
start: 0,
|
||||
end: 10,
|
||||
color: '#aaa',
|
||||
},
|
||||
{
|
||||
start: 10,
|
||||
end: 20,
|
||||
color: '#bbb',
|
||||
},
|
||||
{
|
||||
start: 20,
|
||||
end: 15,
|
||||
color: '#ccc',
|
||||
},
|
||||
];
|
||||
let isValid = isAllColorRangesValid(colorRanges, { min: 5, max: 40 }, 'number');
|
||||
expect(isValid).toBeFalsy();
|
||||
colorRanges[colorRanges.length - 1].end = 30;
|
||||
isValid = isAllColorRangesValid(colorRanges, { min: 5, max: 40 }, 'number');
|
||||
expect(isValid).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { getDataMinMax, isValidColor } from '../utils';
|
||||
|
||||
import type { ColorRange, ColorRangeAccessor } from './types';
|
||||
import type { DataBounds } from '../types';
|
||||
|
||||
import { CustomPaletteParams } from '../../../../common';
|
||||
|
||||
/** @internal **/
|
||||
type ColorRangeValidationErrors = 'invalidColor' | 'invalidValue' | 'greaterThanMaxValue';
|
||||
|
||||
/** @internal **/
|
||||
type ColorRangeValidationWarnings = 'lowerThanDataBounds' | 'greaterThanDataBounds';
|
||||
|
||||
/** @internal **/
|
||||
export interface ColorRangeValidation {
|
||||
errors: ColorRangeValidationErrors[];
|
||||
warnings: ColorRangeValidationWarnings[];
|
||||
isValid: boolean;
|
||||
}
|
||||
|
||||
/** @internal **/
|
||||
export const getErrorMessages = (colorRangesValidity: Record<string, ColorRangeValidation>) => {
|
||||
return [
|
||||
...new Set(
|
||||
Object.values(colorRangesValidity)
|
||||
.map((item) => item.errors)
|
||||
.flat()
|
||||
.map((item) => {
|
||||
switch (item) {
|
||||
case 'invalidColor':
|
||||
case 'invalidValue':
|
||||
return i18n.translate(
|
||||
'xpack.lens.dynamicColoring.customPalette.invalidValueOrColor',
|
||||
{
|
||||
defaultMessage: 'At least one color range contains the wrong value or color',
|
||||
}
|
||||
);
|
||||
case 'greaterThanMaxValue':
|
||||
return i18n.translate('xpack.lens.dynamicColoring.customPalette.invalidMaxValue', {
|
||||
defaultMessage: 'Maximum value should be greater than preceding values',
|
||||
});
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
})
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
export const getOutsideDataBoundsWarningMessage = (warnings: ColorRangeValidation['warnings']) => {
|
||||
for (const warning of warnings) {
|
||||
switch (warning) {
|
||||
case 'lowerThanDataBounds':
|
||||
return i18n.translate('xpack.lens.dynamicColoring.customPalette.lowerThanDataBounds', {
|
||||
defaultMessage: 'This value is outside the minimum data bound',
|
||||
});
|
||||
case 'greaterThanDataBounds':
|
||||
return i18n.translate('xpack.lens.dynamicColoring.customPalette.greaterThanDataBounds', {
|
||||
defaultMessage: 'This value is outside the maximum data bound',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const checkForComplianceWithDataBounds = (value: number, minMax?: [number, number]) => {
|
||||
const warnings: ColorRangeValidationWarnings[] = [];
|
||||
if (minMax) {
|
||||
const [min, max] = minMax;
|
||||
|
||||
if (value < min) {
|
||||
warnings.push('lowerThanDataBounds');
|
||||
}
|
||||
if (value > max) {
|
||||
warnings.push('greaterThanDataBounds');
|
||||
}
|
||||
}
|
||||
|
||||
return warnings;
|
||||
};
|
||||
|
||||
/** @internal **/
|
||||
export const validateColorRange = (
|
||||
colorRange: ColorRange,
|
||||
accessor: ColorRangeAccessor,
|
||||
minMax?: [number, number]
|
||||
) => {
|
||||
const errors: ColorRangeValidationErrors[] = [];
|
||||
let warnings: ColorRangeValidationWarnings[] = [];
|
||||
|
||||
if (Number.isNaN(colorRange[accessor])) {
|
||||
errors.push('invalidValue');
|
||||
}
|
||||
|
||||
if (accessor === 'end') {
|
||||
if (colorRange.start > colorRange.end) {
|
||||
errors.push('greaterThanMaxValue');
|
||||
}
|
||||
warnings = [...warnings, ...checkForComplianceWithDataBounds(colorRange.end, minMax)];
|
||||
} else {
|
||||
if (!isValidColor(colorRange.color)) {
|
||||
errors.push('invalidColor');
|
||||
}
|
||||
warnings = [...warnings, ...checkForComplianceWithDataBounds(colorRange.start, minMax)];
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: !errors.length,
|
||||
errors,
|
||||
warnings,
|
||||
} as ColorRangeValidation;
|
||||
};
|
||||
|
||||
export const validateColorRanges = (
|
||||
colorRanges: ColorRange[],
|
||||
dataBounds: DataBounds,
|
||||
rangeType: CustomPaletteParams['rangeType']
|
||||
): Record<string, ColorRangeValidation> => {
|
||||
let minMax: [number, number] | undefined;
|
||||
|
||||
if ((dataBounds.fallback && rangeType === 'percent') || !dataBounds.fallback) {
|
||||
const { min, max } = getDataMinMax(rangeType, dataBounds);
|
||||
minMax = [min, max];
|
||||
}
|
||||
|
||||
const validations = colorRanges.reduce<Record<string, ColorRangeValidation>>(
|
||||
(acc, item, index) => ({
|
||||
...acc,
|
||||
[index]: validateColorRange(item, 'start', minMax),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
return {
|
||||
...validations,
|
||||
last: validateColorRange(colorRanges[colorRanges.length - 1], 'end', minMax),
|
||||
};
|
||||
};
|
||||
|
||||
export const isAllColorRangesValid = (
|
||||
colorRanges: ColorRange[],
|
||||
dataBounds: DataBounds,
|
||||
rangeType: CustomPaletteParams['rangeType']
|
||||
) => {
|
||||
return Object.values(validateColorRanges(colorRanges, dataBounds, rangeType)).every(
|
||||
(colorRange) => colorRange.isValid
|
||||
);
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { ColorRanges } from './color_ranges';
|
||||
export { ColorRangesContext } from './color_ranges_context';
|
||||
export type { ColorRange, ColorRangesActions } from './types';
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { PaletteRegistry } from 'src/plugins/charts/public';
|
||||
import type { CustomPaletteParams } from '../../../../common';
|
||||
import type { PaletteContinuity } from '../../../../../../../src/plugins/charts/common';
|
||||
import type { DataBounds } from '../types';
|
||||
|
||||
export interface ColorRange {
|
||||
color: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
/** @internal **/
|
||||
export interface ColorRangesState {
|
||||
colorRanges: ColorRange[];
|
||||
rangeType: CustomPaletteParams['rangeType'];
|
||||
continuity: PaletteContinuity;
|
||||
}
|
||||
|
||||
/** @internal **/
|
||||
interface BasicPayload {
|
||||
dataBounds: DataBounds;
|
||||
palettes?: PaletteRegistry;
|
||||
}
|
||||
|
||||
/** @internal **/
|
||||
export interface UpdateColorPayload extends BasicPayload {
|
||||
index: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
/** @internal **/
|
||||
export interface UpdateColorRangeValuePayload extends BasicPayload {
|
||||
index: number;
|
||||
value: string;
|
||||
accessor: ColorRangeAccessor;
|
||||
}
|
||||
|
||||
/** @internal **/
|
||||
export interface DeleteColorRangePayload extends BasicPayload {
|
||||
index: number;
|
||||
}
|
||||
|
||||
/** @internal **/
|
||||
export interface UpdateContinuityPayload extends BasicPayload {
|
||||
isLast: boolean;
|
||||
continuity: PaletteContinuity;
|
||||
}
|
||||
|
||||
/** @internal **/
|
||||
export type ColorRangesActions =
|
||||
| { type: 'reversePalette'; payload: BasicPayload }
|
||||
| { type: 'sortColorRanges'; payload: BasicPayload }
|
||||
| { type: 'distributeEqually'; payload: BasicPayload }
|
||||
| { type: 'updateContinuity'; payload: UpdateContinuityPayload }
|
||||
| { type: 'deleteColorRange'; payload: DeleteColorRangePayload }
|
||||
| {
|
||||
type: 'addColorRange';
|
||||
payload: BasicPayload;
|
||||
}
|
||||
| { type: 'updateColor'; payload: UpdateColorPayload }
|
||||
| {
|
||||
type: 'updateValue';
|
||||
payload: UpdateColorRangeValuePayload;
|
||||
};
|
||||
|
||||
/** @internal **/
|
||||
export type ColorRangeAccessor = 'start' | 'end';
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
addColorRange,
|
||||
deleteColorRange,
|
||||
updateColorRangeValue,
|
||||
updateColorRangeColor,
|
||||
} from './color_ranges_crud';
|
||||
import type { ColorRange } from '../types';
|
||||
|
||||
describe('addColorRange', () => {
|
||||
let colorRanges: ColorRange[];
|
||||
beforeEach(() => {
|
||||
colorRanges = [
|
||||
{ color: '#aaa', start: 20, end: 40 },
|
||||
{ color: '#bbb', start: 40, end: 60 },
|
||||
{ color: '#ccc', start: 60, end: 81 },
|
||||
];
|
||||
});
|
||||
|
||||
it('should add new color range with the corresponding interval', () => {
|
||||
expect(addColorRange(colorRanges, 'number', { min: 0, max: 81 })).toEqual([
|
||||
{ color: '#aaa', start: 20, end: 40 },
|
||||
{ color: '#bbb', start: 40, end: 60 },
|
||||
{ color: '#ccc', start: 60, end: 80 },
|
||||
{ color: '#ccc', start: 80, end: 81 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should add new color range with the interval equal 1 if new range out of max bound', () => {
|
||||
colorRanges[colorRanges.length - 1].end = 80;
|
||||
expect(addColorRange(colorRanges, 'number', { min: 0, max: 80 })).toEqual([
|
||||
{ color: '#aaa', start: 20, end: 40 },
|
||||
{ color: '#bbb', start: 40, end: 60 },
|
||||
{ color: '#ccc', start: 60, end: 61 },
|
||||
{ color: '#ccc', start: 61, end: 80 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteColorRange', () => {
|
||||
let colorRanges: ColorRange[];
|
||||
beforeEach(() => {
|
||||
colorRanges = [
|
||||
{ color: '#aaa', start: 20, end: 40 },
|
||||
{ color: '#bbb', start: 40, end: 60 },
|
||||
{ color: '#ccc', start: 60, end: 80 },
|
||||
];
|
||||
});
|
||||
|
||||
it('delete the last range', () => {
|
||||
expect(deleteColorRange(colorRanges.length - 1, colorRanges)).toEqual([
|
||||
{ color: '#aaa', start: 20, end: 40 },
|
||||
{ color: '#bbb', start: 40, end: 80 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('delete the another range', () => {
|
||||
expect(deleteColorRange(1, colorRanges)).toEqual([
|
||||
{ color: '#aaa', start: 20, end: 60 },
|
||||
{ color: '#ccc', start: 60, end: 80 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateColorRangeValue', () => {
|
||||
let colorRanges: ColorRange[];
|
||||
beforeEach(() => {
|
||||
colorRanges = [
|
||||
{ color: '#aaa', start: 20, end: 40 },
|
||||
{ color: '#bbb', start: 40, end: 60 },
|
||||
{ color: '#ccc', start: 60, end: 80 },
|
||||
];
|
||||
});
|
||||
|
||||
it('update the last end color range value', () => {
|
||||
expect(updateColorRangeValue(colorRanges.length - 1, '90', 'end', colorRanges)).toEqual([
|
||||
{ color: '#aaa', start: 20, end: 40 },
|
||||
{ color: '#bbb', start: 40, end: 60 },
|
||||
{ color: '#ccc', start: 60, end: 90 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('update the first start color range value', () => {
|
||||
expect(updateColorRangeValue(0, '10', 'start', colorRanges)).toEqual([
|
||||
{ color: '#aaa', start: 10, end: 40 },
|
||||
{ color: '#bbb', start: 40, end: 60 },
|
||||
{ color: '#ccc', start: 60, end: 80 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('update the color range value between the first and last color ranges', () => {
|
||||
expect(updateColorRangeValue(1, '50', 'start', colorRanges)).toEqual([
|
||||
{ color: '#aaa', start: 20, end: 50 },
|
||||
{ color: '#bbb', start: 50, end: 60 },
|
||||
{ color: '#ccc', start: 60, end: 80 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateColorRangeColor', () => {
|
||||
let colorRanges: ColorRange[];
|
||||
beforeEach(() => {
|
||||
colorRanges = [
|
||||
{ color: '#aaa', start: 20, end: 40 },
|
||||
{ color: '#bbb', start: 40, end: 60 },
|
||||
{ color: '#ccc', start: 60, end: 80 },
|
||||
];
|
||||
});
|
||||
|
||||
it('update color for color range', () => {
|
||||
expect(updateColorRangeColor(0, '#ddd', colorRanges)).toEqual([
|
||||
{ color: '#ddd', start: 20, end: 40 },
|
||||
{ color: '#bbb', start: 40, end: 60 },
|
||||
{ color: '#ccc', start: 60, end: 80 },
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { getDataMinMax, roundValue } from '../../utils';
|
||||
import { calculateMaxStep } from './utils';
|
||||
|
||||
import type { ColorRange, ColorRangeAccessor } from '../types';
|
||||
import type { DataBounds } from '../../types';
|
||||
import type { CustomPaletteParamsConfig } from '../../../../../common';
|
||||
|
||||
/**
|
||||
* Allows to update a ColorRange
|
||||
* @private
|
||||
*/
|
||||
const updateColorRangeItem = (
|
||||
colorRanges: ColorRange[],
|
||||
index: number,
|
||||
payload: Partial<ColorRange>
|
||||
): ColorRange[] => {
|
||||
const ranges = [...colorRanges];
|
||||
ranges[index] = { ...ranges[index], ...payload };
|
||||
return ranges;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add new color range after the last item
|
||||
* @internal
|
||||
*/
|
||||
export const addColorRange = (
|
||||
colorRanges: ColorRange[],
|
||||
rangeType: CustomPaletteParamsConfig['rangeType'],
|
||||
dataBounds: DataBounds
|
||||
) => {
|
||||
let newColorRanges = [...colorRanges];
|
||||
const lastIndex = newColorRanges.length - 1;
|
||||
const lastStart = newColorRanges[lastIndex].start;
|
||||
const lastEnd = newColorRanges[lastIndex].end;
|
||||
const lastColor = newColorRanges[lastIndex].color;
|
||||
|
||||
const { max: dataMax } = getDataMinMax(rangeType, dataBounds);
|
||||
const max = Math.max(dataMax, lastEnd);
|
||||
|
||||
const step = calculateMaxStep(
|
||||
newColorRanges.map((item) => item.start),
|
||||
max
|
||||
);
|
||||
|
||||
let insertEnd = roundValue(Math.min(lastStart + step, max));
|
||||
|
||||
if (insertEnd === Number.NEGATIVE_INFINITY) {
|
||||
insertEnd = 1;
|
||||
}
|
||||
|
||||
newColorRanges = updateColorRangeItem(newColorRanges, lastIndex, { end: insertEnd });
|
||||
newColorRanges.push({
|
||||
color: lastColor,
|
||||
start: insertEnd,
|
||||
end: lastEnd === insertEnd ? lastEnd + 1 : lastEnd,
|
||||
});
|
||||
|
||||
return newColorRanges;
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete ColorRange
|
||||
* @internal
|
||||
*/
|
||||
export const deleteColorRange = (index: number, colorRanges: ColorRange[]) => {
|
||||
const lastIndex = colorRanges.length - 1;
|
||||
let ranges = colorRanges;
|
||||
|
||||
if (index !== 0) {
|
||||
if (index !== lastIndex) {
|
||||
ranges = updateColorRangeItem(ranges, index - 1, { end: ranges[index + 1].start });
|
||||
}
|
||||
if (index === lastIndex) {
|
||||
ranges = updateColorRangeItem(ranges, index - 1, { end: colorRanges[index].end });
|
||||
}
|
||||
}
|
||||
return ranges.filter((item, i) => i !== index);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update ColorRange value
|
||||
* @internal
|
||||
*/
|
||||
export const updateColorRangeValue = (
|
||||
index: number,
|
||||
value: string,
|
||||
accessor: ColorRangeAccessor,
|
||||
colorRanges: ColorRange[]
|
||||
) => {
|
||||
const parsedValue = value ? parseFloat(value) : Number.NaN;
|
||||
let ranges = colorRanges;
|
||||
|
||||
if (accessor === 'end') {
|
||||
ranges = updateColorRangeItem(ranges, index, { end: parsedValue });
|
||||
} else {
|
||||
ranges = updateColorRangeItem(ranges, index, { start: parsedValue });
|
||||
if (index > 0) {
|
||||
ranges = updateColorRangeItem(ranges, index - 1, { end: parsedValue });
|
||||
}
|
||||
}
|
||||
return ranges;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update ColorRange color
|
||||
* @internal
|
||||
*/
|
||||
export const updateColorRangeColor = (index: number, color: string, colorRanges: ColorRange[]) =>
|
||||
updateColorRangeItem(colorRanges, index, { color });
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { distributeEqually, reversePalette } from './color_ranges_extra_actions';
|
||||
import type { ColorRange } from '../types';
|
||||
|
||||
describe('distributeEqually', () => {
|
||||
let colorRanges: ColorRange[];
|
||||
beforeEach(() => {
|
||||
colorRanges = [
|
||||
{ color: '#aaa', start: 20, end: 40 },
|
||||
{ color: '#bbb', start: 40, end: 60 },
|
||||
{ color: '#ccc', start: 60, end: 80 },
|
||||
{ color: '#ddd', start: 80, end: 100 },
|
||||
];
|
||||
});
|
||||
|
||||
it('should equally distribute the color ranges', () => {
|
||||
expect(distributeEqually(colorRanges, 'number', 'none', { min: 0, max: 4000 })).toEqual([
|
||||
{ color: '#aaa', start: 0, end: 1000 },
|
||||
{ color: '#bbb', start: 1000, end: 2000 },
|
||||
{ color: '#ccc', start: 2000, end: 3000 },
|
||||
{ color: '#ddd', start: 3000, end: 4000 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should work correctly with continuity to both sides', () => {
|
||||
expect(distributeEqually(colorRanges, 'percent', 'all', { min: 0, max: 5000 })).toEqual([
|
||||
{ color: '#aaa', start: Number.NEGATIVE_INFINITY, end: 25 },
|
||||
{ color: '#bbb', start: 25, end: 50 },
|
||||
{ color: '#ccc', start: 50, end: 75 },
|
||||
{ color: '#ddd', start: 75, end: Number.POSITIVE_INFINITY },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reversePalette', () => {
|
||||
let colorRanges: ColorRange[];
|
||||
beforeEach(() => {
|
||||
colorRanges = [
|
||||
{ color: '#aaa', start: 20, end: 40 },
|
||||
{ color: '#bbb', start: 40, end: 60 },
|
||||
{ color: '#ccc', start: 60, end: 81 },
|
||||
];
|
||||
});
|
||||
|
||||
it('should return reversed color palette of given color range', () => {
|
||||
expect(reversePalette(colorRanges)).toEqual([
|
||||
{ color: '#ccc', start: 20, end: 40 },
|
||||
{ color: '#bbb', start: 40, end: 60 },
|
||||
{ color: '#aaa', start: 60, end: 81 },
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getDataMinMax, roundValue } from '../../utils';
|
||||
|
||||
import type { ColorRange } from '../types';
|
||||
import type { DataBounds } from '../../types';
|
||||
import type { CustomPaletteParamsConfig } from '../../../../../common';
|
||||
import {
|
||||
PaletteContinuity,
|
||||
checkIsMinContinuity,
|
||||
checkIsMaxContinuity,
|
||||
} from '../../../../../../../../src/plugins/charts/common';
|
||||
|
||||
/**
|
||||
* Distribute equally
|
||||
* @internal
|
||||
*/
|
||||
export const distributeEqually = (
|
||||
colorRanges: ColorRange[],
|
||||
rangeType: CustomPaletteParamsConfig['rangeType'],
|
||||
continuity: PaletteContinuity,
|
||||
dataBounds: DataBounds
|
||||
) => {
|
||||
const items = colorRanges.length;
|
||||
const lastIndex = colorRanges.length - 1;
|
||||
const { min, max } = getDataMinMax(rangeType, dataBounds);
|
||||
const step = roundValue((max - min) / items);
|
||||
|
||||
const getValueForIndex = (index: number) => roundValue(min + step * index);
|
||||
const getStartValue = (index: number) => {
|
||||
if (index === 0) {
|
||||
return checkIsMinContinuity(continuity) ? Number.NEGATIVE_INFINITY : roundValue(min);
|
||||
}
|
||||
return getValueForIndex(index);
|
||||
};
|
||||
const getEndValue = (index: number) => {
|
||||
if (index === lastIndex) {
|
||||
return checkIsMaxContinuity(continuity) ? Number.POSITIVE_INFINITY : roundValue(max);
|
||||
}
|
||||
return getValueForIndex(index + 1);
|
||||
};
|
||||
|
||||
return colorRanges.map((colorRange, index) => ({
|
||||
color: colorRange.color,
|
||||
start: getStartValue(index),
|
||||
end: getEndValue(index),
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Reverse Palette
|
||||
* @internal
|
||||
*/
|
||||
export const reversePalette = (colorRanges: ColorRange[]) =>
|
||||
colorRanges
|
||||
.map(({ color }, i) => ({
|
||||
color,
|
||||
start: colorRanges[colorRanges.length - i - 1].start,
|
||||
end: colorRanges[colorRanges.length - i - 1].end,
|
||||
}))
|
||||
.reverse();
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './utils';
|
||||
export * from './color_ranges_crud';
|
||||
export * from './color_ranges_extra_actions';
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { sortColorRanges, calculateMaxStep, toColorStops, getValueForContinuity } from './utils';
|
||||
|
||||
describe('utils', () => {
|
||||
it('sortColorRanges', () => {
|
||||
const colorRanges = [
|
||||
{ color: '#aaa', start: 55, end: 40 },
|
||||
{ color: '#bbb', start: 40, end: 60 },
|
||||
{ color: '#ccc', start: 60, end: 80 },
|
||||
];
|
||||
expect(sortColorRanges(colorRanges)).toEqual([
|
||||
{ color: '#bbb', start: 40, end: 55 },
|
||||
{ color: '#aaa', start: 55, end: 60 },
|
||||
{ color: '#ccc', start: 60, end: 80 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('calculateMaxStep', () => {
|
||||
const stops = [20, 40, 60];
|
||||
expect(calculateMaxStep(stops, 90)).toEqual(20);
|
||||
// should return 1 if the last stop with calculated interval more than max
|
||||
expect(calculateMaxStep(stops, 75)).toEqual(1);
|
||||
// should return 1 if we don't provide stops
|
||||
expect(calculateMaxStep([], 75)).toEqual(1);
|
||||
});
|
||||
|
||||
it('toColorStops', () => {
|
||||
const colorRanges = [
|
||||
{ color: '#aaa', start: 20, end: 40 },
|
||||
{ color: '#bbb', start: 40, end: 60 },
|
||||
{ color: '#ccc', start: 60, end: 80 },
|
||||
];
|
||||
const colorStops = [
|
||||
{
|
||||
color: '#aaa',
|
||||
stop: 20,
|
||||
},
|
||||
{
|
||||
color: '#bbb',
|
||||
stop: 40,
|
||||
},
|
||||
{
|
||||
color: '#ccc',
|
||||
stop: 60,
|
||||
},
|
||||
];
|
||||
|
||||
// if continuity is none then min should be the first range value
|
||||
// and max should be the last range value
|
||||
expect(toColorStops(colorRanges, 'none')).toEqual({
|
||||
min: 20,
|
||||
max: 80,
|
||||
colorStops,
|
||||
});
|
||||
|
||||
colorStops[0].stop = Number.NEGATIVE_INFINITY;
|
||||
// if continuity is below then min should be -Infinity
|
||||
expect(toColorStops(colorRanges, 'below')).toEqual({
|
||||
min: Number.NEGATIVE_INFINITY,
|
||||
max: 80,
|
||||
colorStops,
|
||||
});
|
||||
|
||||
colorStops[0].stop = 20;
|
||||
// if continuity is above then max should be Infinity
|
||||
expect(toColorStops(colorRanges, 'above')).toEqual({
|
||||
min: 20,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
colorStops,
|
||||
});
|
||||
|
||||
colorStops[0].stop = Number.NEGATIVE_INFINITY;
|
||||
// if continuity is all then max should be Infinity and min should be -Infinity
|
||||
expect(toColorStops(colorRanges, 'all')).toEqual({
|
||||
min: Number.NEGATIVE_INFINITY,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
colorStops,
|
||||
});
|
||||
});
|
||||
|
||||
describe('getValueForContinuity', () => {
|
||||
it('should return Infinity if continuity is all or above and that last range', () => {
|
||||
const colorRanges = [
|
||||
{ color: '#aaa', start: 20, end: 40 },
|
||||
{ color: '#bbb', start: 40, end: 60 },
|
||||
{ color: '#ccc', start: 60, end: 80 },
|
||||
];
|
||||
expect(
|
||||
getValueForContinuity(colorRanges, 'above', true, 'number', { min: 0, max: 100 })
|
||||
).toEqual(Number.POSITIVE_INFINITY);
|
||||
|
||||
expect(
|
||||
getValueForContinuity(colorRanges, 'all', true, 'number', { min: 0, max: 100 })
|
||||
).toEqual(Number.POSITIVE_INFINITY);
|
||||
});
|
||||
|
||||
it('should return -Infinity if continuity is all or below and that first range', () => {
|
||||
const colorRanges = [
|
||||
{ color: '#aaa', start: 20, end: 40 },
|
||||
{ color: '#bbb', start: 40, end: 60 },
|
||||
{ color: '#ccc', start: 60, end: 80 },
|
||||
];
|
||||
expect(
|
||||
getValueForContinuity(colorRanges, 'below', false, 'number', { min: 0, max: 100 })
|
||||
).toEqual(Number.NEGATIVE_INFINITY);
|
||||
|
||||
expect(
|
||||
getValueForContinuity(colorRanges, 'all', false, 'number', { min: 0, max: 100 })
|
||||
).toEqual(Number.NEGATIVE_INFINITY);
|
||||
});
|
||||
|
||||
it('should return new max if continuity is none or below and that last range', () => {
|
||||
const colorRanges = [
|
||||
{ color: '#aaa', start: 20, end: 40 },
|
||||
{ color: '#bbb', start: 40, end: 60 },
|
||||
{ color: '#ccc', start: 60, end: 80 },
|
||||
];
|
||||
expect(
|
||||
getValueForContinuity(colorRanges, 'below', true, 'number', { min: 0, max: 100 })
|
||||
).toEqual(100);
|
||||
|
||||
expect(
|
||||
getValueForContinuity(colorRanges, 'none', true, 'number', { min: 0, max: 55 })
|
||||
).toEqual(61);
|
||||
});
|
||||
|
||||
it('should return new min if continuity is none or above and that first range', () => {
|
||||
const colorRanges = [
|
||||
{ color: '#aaa', start: 20, end: 40 },
|
||||
{ color: '#bbb', start: 40, end: 60 },
|
||||
{ color: '#ccc', start: 60, end: 80 },
|
||||
];
|
||||
expect(
|
||||
getValueForContinuity(colorRanges, 'above', false, 'number', { min: 0, max: 100 })
|
||||
).toEqual(0);
|
||||
|
||||
expect(
|
||||
getValueForContinuity(colorRanges, 'none', false, 'number', { min: 45, max: 100 })
|
||||
).toEqual(39);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { roundValue, getDataMinMax } from '../../utils';
|
||||
import {
|
||||
PaletteContinuity,
|
||||
checkIsMaxContinuity,
|
||||
checkIsMinContinuity,
|
||||
} from '../../../../../../../../src/plugins/charts/common';
|
||||
import type { CustomPaletteParams } from '../../../../../common';
|
||||
import type { ColorRange, ColorRangeAccessor } from '../types';
|
||||
import type { DataBounds } from '../../types';
|
||||
|
||||
/**
|
||||
* Check if item is last
|
||||
* @internal
|
||||
*/
|
||||
export const isLastItem = (accessor: ColorRangeAccessor) => accessor === 'end';
|
||||
|
||||
/**
|
||||
* Sort Color ranges array
|
||||
* @internal
|
||||
*/
|
||||
export const sortColorRanges = (colorRanges: ColorRange[]) => {
|
||||
const maxValue = colorRanges[colorRanges.length - 1].end;
|
||||
|
||||
return [...colorRanges]
|
||||
.sort(({ start: startA }, { start: startB }) => Number(startA) - Number(startB))
|
||||
.map((newColorRange, i, array) => ({
|
||||
color: newColorRange.color,
|
||||
start: newColorRange.start,
|
||||
end: i !== array.length - 1 ? array[i + 1].start : maxValue,
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate max step
|
||||
* @internal
|
||||
*/
|
||||
export const calculateMaxStep = (stops: number[], max: number) => {
|
||||
let step = 1;
|
||||
if (stops.length > 1) {
|
||||
const last = stops[stops.length - 1];
|
||||
const last2step = stops[stops.length - 1] - stops[stops.length - 2];
|
||||
|
||||
if (last + last2step < max) {
|
||||
step = last2step;
|
||||
}
|
||||
}
|
||||
return roundValue(step);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert ColorRange to ColorStops
|
||||
* @internal
|
||||
*/
|
||||
|
||||
export const toColorStops = (colorRanges: ColorRange[], continuity: PaletteContinuity) => {
|
||||
const min = checkIsMinContinuity(continuity) ? Number.NEGATIVE_INFINITY : colorRanges[0].start;
|
||||
const max = checkIsMaxContinuity(continuity)
|
||||
? Number.POSITIVE_INFINITY
|
||||
: colorRanges[colorRanges.length - 1].end;
|
||||
|
||||
return {
|
||||
min,
|
||||
max,
|
||||
colorStops: colorRanges.map((colorRange, i) => ({
|
||||
color: colorRange.color,
|
||||
stop: i === 0 ? min : colorRange.start,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate right max or min value for new continuity
|
||||
*/
|
||||
|
||||
export const getValueForContinuity = (
|
||||
colorRanges: ColorRange[],
|
||||
continuity: PaletteContinuity,
|
||||
isLast: boolean,
|
||||
rangeType: CustomPaletteParams['rangeType'],
|
||||
dataBounds: DataBounds
|
||||
) => {
|
||||
const { max, min } = getDataMinMax(rangeType, dataBounds);
|
||||
let value;
|
||||
if (isLast) {
|
||||
if (checkIsMaxContinuity(continuity)) {
|
||||
value = Number.POSITIVE_INFINITY;
|
||||
} else {
|
||||
value =
|
||||
colorRanges[colorRanges.length - 1].start > max
|
||||
? colorRanges[colorRanges.length - 1].start + 1
|
||||
: max;
|
||||
}
|
||||
} else {
|
||||
if (checkIsMinContinuity(continuity)) {
|
||||
value = Number.NEGATIVE_INFINITY;
|
||||
} else {
|
||||
value = colorRanges[0].end < min ? colorRanges[0].end - 1 : min;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
|
@ -1,191 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiColorPicker } from '@elastic/eui';
|
||||
import { mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { CustomStops, CustomStopsProps } from './color_stops';
|
||||
|
||||
// mocking random id generator function
|
||||
jest.mock('@elastic/eui', () => {
|
||||
const original = jest.requireActual('@elastic/eui');
|
||||
|
||||
return {
|
||||
...original,
|
||||
htmlIdGenerator: (fn: unknown) => {
|
||||
let counter = 0;
|
||||
return () => counter++;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('Color Stops component', () => {
|
||||
let props: CustomStopsProps;
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
colorStops: [
|
||||
{ color: '#aaa', stop: 20 },
|
||||
{ color: '#bbb', stop: 40 },
|
||||
{ color: '#ccc', stop: 60 },
|
||||
],
|
||||
paletteConfiguration: {},
|
||||
dataBounds: { min: 0, max: 200 },
|
||||
onChange: jest.fn(),
|
||||
'data-test-prefix': 'my-test',
|
||||
};
|
||||
});
|
||||
it('should display all the color stops passed', () => {
|
||||
const component = mount(<CustomStops {...props} />);
|
||||
expect(
|
||||
component.find('input[data-test-subj^="my-test_dynamicColoring_stop_value_"]')
|
||||
).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should disable the delete buttons when there are 2 stops or less', () => {
|
||||
// reduce to 2 stops
|
||||
props.colorStops = props.colorStops.slice(0, 2);
|
||||
const component = mount(<CustomStops {...props} />);
|
||||
expect(
|
||||
component
|
||||
.find('[data-test-subj="my-test_dynamicColoring_removeStop_0"]')
|
||||
.first()
|
||||
.prop('isDisabled')
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should disable "add new" button if there is maxStops configured', () => {
|
||||
props.colorStops = [
|
||||
{ color: '#aaa', stop: 20 },
|
||||
{ color: '#bbb', stop: 40 },
|
||||
{ color: '#ccc', stop: 60 },
|
||||
{ color: '#ccc', stop: 80 },
|
||||
{ color: '#ccc', stop: 90 },
|
||||
];
|
||||
const component = mount(<CustomStops {...props} />);
|
||||
const componentWithMaxSteps = mount(
|
||||
<CustomStops {...props} paletteConfiguration={{ maxSteps: 5 }} />
|
||||
);
|
||||
expect(
|
||||
component
|
||||
.find('[data-test-subj="my-test_dynamicColoring_addStop"]')
|
||||
.first()
|
||||
.prop('isDisabled')
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
componentWithMaxSteps
|
||||
.find('[data-test-subj="my-test_dynamicColoring_addStop"]')
|
||||
.first()
|
||||
.prop('isDisabled')
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should add a new stop with default color and reasonable distance from last one', () => {
|
||||
let component = mount(<CustomStops {...props} />);
|
||||
const addStopButton = component
|
||||
.find('[data-test-subj="my-test_dynamicColoring_addStop"]')
|
||||
.first();
|
||||
act(() => {
|
||||
addStopButton.simulate('click');
|
||||
});
|
||||
component = component.update();
|
||||
|
||||
expect(
|
||||
component.find('input[data-test-subj^="my-test_dynamicColoring_stop_value_"]')
|
||||
).toHaveLength(4);
|
||||
expect(
|
||||
component.find('input[data-test-subj="my-test_dynamicColoring_stop_value_3"]').prop('value')
|
||||
).toBe('80'); // 60-40 + 60
|
||||
expect(
|
||||
component
|
||||
// workaround for https://github.com/elastic/eui/issues/4792
|
||||
.find('[data-test-subj="my-test_dynamicColoring_stop_color_3"]')
|
||||
.last() // pick the inner element
|
||||
.childAt(0)
|
||||
.prop('color')
|
||||
).toBe('#ccc'); // pick previous color
|
||||
});
|
||||
|
||||
it('should restore previous color when abandoning the field with an empty color', () => {
|
||||
let component = mount(<CustomStops {...props} />);
|
||||
expect(
|
||||
component
|
||||
.find('[data-test-subj="my-test_dynamicColoring_stop_row_0"]')
|
||||
.first()
|
||||
.find(EuiColorPicker)
|
||||
.first()
|
||||
.prop('color')
|
||||
).toBe('#aaa');
|
||||
act(() => {
|
||||
component
|
||||
.find('[data-test-subj="my-test_dynamicColoring_stop_row_0"]')
|
||||
.first()
|
||||
.find(EuiColorPicker)
|
||||
.first()
|
||||
.prop('onChange')!('', {
|
||||
rgba: [NaN, NaN, NaN, NaN],
|
||||
hex: '',
|
||||
isValid: false,
|
||||
});
|
||||
});
|
||||
component = component.update();
|
||||
expect(
|
||||
component
|
||||
.find('[data-test-subj="my-test_dynamicColoring_stop_row_0"]')
|
||||
.first()
|
||||
.find(EuiColorPicker)
|
||||
.first()
|
||||
.prop('color')
|
||||
).toBe('');
|
||||
act(() => {
|
||||
component
|
||||
.find('[data-test-subj="my-test_dynamicColoring_stop_color_0"]')
|
||||
.first()
|
||||
.simulate('blur');
|
||||
});
|
||||
component = component.update();
|
||||
expect(
|
||||
component
|
||||
.find('[data-test-subj="my-test_dynamicColoring_stop_row_0"]')
|
||||
.first()
|
||||
.find(EuiColorPicker)
|
||||
.first()
|
||||
.prop('color')
|
||||
).toBe('#aaa');
|
||||
});
|
||||
|
||||
it('should sort stops value on whole component blur', () => {
|
||||
let component = mount(<CustomStops {...props} />);
|
||||
let firstStopValueInput = component.find(
|
||||
'[data-test-subj="my-test_dynamicColoring_stop_value_0"] input[type="number"]'
|
||||
);
|
||||
|
||||
act(() => {
|
||||
firstStopValueInput.simulate('change', { target: { value: ' 90' } });
|
||||
});
|
||||
act(() => {
|
||||
component
|
||||
.find('[data-test-subj="my-test_dynamicColoring_stop_row_0"]')
|
||||
.first()
|
||||
.simulate('blur');
|
||||
});
|
||||
component = component.update();
|
||||
|
||||
// retrieve again the input
|
||||
firstStopValueInput = component.find(
|
||||
'[data-test-subj="my-test_dynamicColoring_stop_value_0"] input[type="number"]'
|
||||
);
|
||||
expect(firstStopValueInput.prop('value')).toBe('40');
|
||||
// the previous one move at the bottom
|
||||
expect(
|
||||
component
|
||||
.find('[data-test-subj="my-test_dynamicColoring_stop_value_2"] input[type="number"]')
|
||||
.prop('value')
|
||||
).toBe('90');
|
||||
});
|
||||
});
|
|
@ -1,310 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import type { FocusEvent } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiFieldNumber,
|
||||
EuiColorPicker,
|
||||
EuiButtonIcon,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiButtonEmpty,
|
||||
EuiSpacer,
|
||||
EuiScreenReaderOnly,
|
||||
htmlIdGenerator,
|
||||
} from '@elastic/eui';
|
||||
import useUnmount from 'react-use/lib/useUnmount';
|
||||
import { DEFAULT_COLOR } from './constants';
|
||||
import { getDataMinMax, getStepValue, isValidColor } from './utils';
|
||||
import { TooltipWrapper, useDebouncedValue } from '../index';
|
||||
import type { ColorStop, CustomPaletteParamsConfig } from '../../../common';
|
||||
|
||||
const idGeneratorFn = htmlIdGenerator();
|
||||
|
||||
function areStopsValid(colorStops: Array<{ color: string; stop: string }>) {
|
||||
return colorStops.every(
|
||||
({ color, stop }) => isValidColor(color) && !Number.isNaN(parseFloat(stop))
|
||||
);
|
||||
}
|
||||
|
||||
function shouldSortStops(colorStops: Array<{ color: string; stop: string | number }>) {
|
||||
return colorStops.some(({ stop }, i) => {
|
||||
const numberStop = Number(stop);
|
||||
const prevNumberStop = Number(colorStops[i - 1]?.stop ?? -Infinity);
|
||||
return numberStop < prevNumberStop;
|
||||
});
|
||||
}
|
||||
|
||||
export interface CustomStopsProps {
|
||||
colorStops: ColorStop[];
|
||||
onChange: (colorStops: ColorStop[]) => void;
|
||||
dataBounds: { min: number; max: number };
|
||||
paletteConfiguration: CustomPaletteParamsConfig | undefined;
|
||||
'data-test-prefix': string;
|
||||
}
|
||||
export const CustomStops = ({
|
||||
colorStops,
|
||||
onChange,
|
||||
paletteConfiguration,
|
||||
dataBounds,
|
||||
['data-test-prefix']: dataTestPrefix,
|
||||
}: CustomStopsProps) => {
|
||||
const onChangeWithValidation = useCallback(
|
||||
(newColorStops: Array<{ color: string; stop: string }>) => {
|
||||
const areStopsValuesValid = areStopsValid(newColorStops);
|
||||
const shouldSort = shouldSortStops(newColorStops);
|
||||
if (areStopsValuesValid && !shouldSort) {
|
||||
onChange(newColorStops.map(({ color, stop }) => ({ color, stop: Number(stop) })));
|
||||
}
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const memoizedValues = useMemo(() => {
|
||||
return colorStops.map(({ color, stop }, i) => ({
|
||||
color,
|
||||
stop: String(stop),
|
||||
id: idGeneratorFn(),
|
||||
}));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [paletteConfiguration?.name, paletteConfiguration?.reverse, paletteConfiguration?.rangeType]);
|
||||
|
||||
const { inputValue: localColorStops, handleInputChange: setLocalColorStops } = useDebouncedValue({
|
||||
onChange: onChangeWithValidation,
|
||||
value: memoizedValues,
|
||||
});
|
||||
const [sortedReason, setSortReason] = useState<string>('');
|
||||
const shouldEnableDelete = localColorStops.length > 2;
|
||||
const shouldDisableAdd = Boolean(
|
||||
paletteConfiguration?.maxSteps && localColorStops.length >= paletteConfiguration?.maxSteps
|
||||
);
|
||||
|
||||
const [popoverInFocus, setPopoverInFocus] = useState<boolean>(false);
|
||||
|
||||
// refresh on unmount:
|
||||
// the onChange logic here is a bit different than the one above as it has to actively sort if required
|
||||
useUnmount(() => {
|
||||
const areStopsValuesValid = areStopsValid(localColorStops);
|
||||
const shouldSort = shouldSortStops(localColorStops);
|
||||
if (areStopsValuesValid && shouldSort) {
|
||||
onChange(
|
||||
localColorStops
|
||||
.map(({ color, stop }) => ({ color, stop: Number(stop) }))
|
||||
.sort(({ stop: stopA }, { stop: stopB }) => Number(stopA) - Number(stopB))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const rangeType = paletteConfiguration?.rangeType || 'percent';
|
||||
|
||||
return (
|
||||
<>
|
||||
{sortedReason ? (
|
||||
<EuiScreenReaderOnly>
|
||||
<p aria-live="assertive">
|
||||
{i18n.translate('xpack.lens.dynamicColoring.customPalette.sortReason', {
|
||||
defaultMessage: 'Color stops have been sorted due to new stop value {value}',
|
||||
values: {
|
||||
value: sortedReason,
|
||||
},
|
||||
})}
|
||||
</p>
|
||||
</EuiScreenReaderOnly>
|
||||
) : null}
|
||||
|
||||
<EuiFlexGroup
|
||||
data-test-subj={`${dataTestPrefix}_dynamicColoring_custom_stops`}
|
||||
direction="column"
|
||||
gutterSize="s"
|
||||
>
|
||||
{localColorStops.map(({ color, stop, id }, index) => {
|
||||
const prevStopValue = Number(localColorStops[index - 1]?.stop ?? -Infinity);
|
||||
const nextStopValue = Number(localColorStops[index + 1]?.stop ?? Infinity);
|
||||
|
||||
return (
|
||||
<EuiFlexItem
|
||||
key={id}
|
||||
data-test-subj={`${dataTestPrefix}_dynamicColoring_stop_row_${index}`}
|
||||
onBlur={(e: FocusEvent<HTMLDivElement>) => {
|
||||
// sort the stops when the focus leaves the row container
|
||||
const shouldSort = Number(stop) > nextStopValue || prevStopValue > Number(stop);
|
||||
const isFocusStillInContent =
|
||||
(e.currentTarget as Node)?.contains(e.relatedTarget as Node) || popoverInFocus;
|
||||
const hasInvalidColor = !isValidColor(color);
|
||||
if ((shouldSort && !isFocusStillInContent) || hasInvalidColor) {
|
||||
// replace invalid color with previous valid one
|
||||
const lastValidColor = hasInvalidColor ? colorStops[index].color : color;
|
||||
const localColorStopsCopy = localColorStops.map((item, i) =>
|
||||
i === index ? { color: lastValidColor, stop, id } : item
|
||||
);
|
||||
setLocalColorStops(
|
||||
localColorStopsCopy.sort(
|
||||
({ stop: stopA }, { stop: stopB }) => Number(stopA) - Number(stopB)
|
||||
)
|
||||
);
|
||||
setSortReason(stop);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<EuiFieldNumber
|
||||
compressed
|
||||
data-test-subj={`${dataTestPrefix}_dynamicColoring_stop_value_${index}`}
|
||||
value={stop}
|
||||
min={-Infinity}
|
||||
onChange={({ target }) => {
|
||||
const newStopString = target.value.trim();
|
||||
const newColorStops = [...localColorStops];
|
||||
newColorStops[index] = {
|
||||
color,
|
||||
stop: newStopString,
|
||||
id,
|
||||
};
|
||||
setLocalColorStops(newColorStops);
|
||||
}}
|
||||
append={rangeType === 'percent' ? '%' : undefined}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.lens.dynamicColoring.customPalette.stopAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Stop {index}',
|
||||
values: {
|
||||
index: index + 1,
|
||||
},
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem
|
||||
data-test-subj={`${dataTestPrefix}_dynamicColoring_stop_color_${index}`}
|
||||
onBlur={() => {
|
||||
// make sure that the popover is closed
|
||||
if (color === '' && !popoverInFocus) {
|
||||
const newColorStops = [...localColorStops];
|
||||
newColorStops[index] = { color: colorStops[index].color, stop, id };
|
||||
setLocalColorStops(newColorStops);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<EuiColorPicker
|
||||
key={stop}
|
||||
onChange={(newColor) => {
|
||||
const newColorStops = [...localColorStops];
|
||||
newColorStops[index] = { color: newColor, stop, id };
|
||||
setLocalColorStops(newColorStops);
|
||||
}}
|
||||
secondaryInputDisplay="top"
|
||||
color={color}
|
||||
isInvalid={!isValidColor(color)}
|
||||
showAlpha
|
||||
compressed
|
||||
onFocus={() => setPopoverInFocus(true)}
|
||||
onBlur={() => {
|
||||
setPopoverInFocus(false);
|
||||
if (color === '') {
|
||||
const newColorStops = [...localColorStops];
|
||||
newColorStops[index] = { color: colorStops[index].color, stop, id };
|
||||
setLocalColorStops(newColorStops);
|
||||
}
|
||||
}}
|
||||
placeholder=" "
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<TooltipWrapper
|
||||
tooltipContent={i18n.translate(
|
||||
'xpack.lens.dynamicColoring.customPalette.deleteButtonDisabled',
|
||||
{
|
||||
defaultMessage:
|
||||
'This color stop cannot be deleted, as two or more stops are required',
|
||||
}
|
||||
)}
|
||||
condition={!shouldEnableDelete}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.lens.dynamicColoring.customPalette.deleteButtonAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Delete',
|
||||
}
|
||||
)}
|
||||
title={i18n.translate(
|
||||
'xpack.lens.dynamicColoring.customPalette.deleteButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Delete',
|
||||
}
|
||||
)}
|
||||
onClick={() => {
|
||||
const newColorStops = localColorStops.filter((_, i) => i !== index);
|
||||
setLocalColorStops(newColorStops);
|
||||
}}
|
||||
data-test-subj={`${dataTestPrefix}_dynamicColoring_removeStop_${index}`}
|
||||
isDisabled={!shouldEnableDelete}
|
||||
/>
|
||||
</TooltipWrapper>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<TooltipWrapper
|
||||
tooltipContent={i18n.translate(
|
||||
'xpack.lens.dynamicColoring.customPalette.maximumStepsApplied',
|
||||
{
|
||||
defaultMessage: `You've applied the maximum number of steps`,
|
||||
}
|
||||
)}
|
||||
condition={shouldDisableAdd}
|
||||
position="top"
|
||||
delay="regular"
|
||||
>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj={`${dataTestPrefix}_dynamicColoring_addStop`}
|
||||
iconType="plusInCircle"
|
||||
color="primary"
|
||||
aria-label={i18n.translate('xpack.lens.dynamicColoring.customPalette.addColorStop', {
|
||||
defaultMessage: 'Add color stop',
|
||||
})}
|
||||
size="xs"
|
||||
isDisabled={shouldDisableAdd}
|
||||
flush="left"
|
||||
onClick={() => {
|
||||
const newColorStops = [...localColorStops];
|
||||
const length = newColorStops.length;
|
||||
const { max } = getDataMinMax(rangeType, dataBounds);
|
||||
const step = getStepValue(
|
||||
colorStops,
|
||||
newColorStops.map(({ color, stop }) => ({ color, stop: Number(stop) })),
|
||||
max
|
||||
);
|
||||
const prevColor = localColorStops[length - 1].color || DEFAULT_COLOR;
|
||||
const newStop = step + Number(localColorStops[length - 1].stop);
|
||||
newColorStops.push({
|
||||
color: prevColor,
|
||||
stop: String(newStop),
|
||||
id: idGeneratorFn(),
|
||||
});
|
||||
setLocalColorStops(newColorStops);
|
||||
}}
|
||||
>
|
||||
{i18n.translate('xpack.lens.dynamicColoring.customPalette.addColorStop', {
|
||||
defaultMessage: 'Add color stop',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</TooltipWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -11,15 +11,17 @@ export const DEFAULT_PALETTE_NAME = 'positive';
|
|||
export const FIXED_PROGRESSION = 'fixed' as const;
|
||||
export const CUSTOM_PALETTE = 'custom';
|
||||
export const DEFAULT_CONTINUITY = 'above';
|
||||
export const DEFAULT_RANGE_TYPE = 'percent';
|
||||
export const DEFAULT_MIN_STOP = 0;
|
||||
export const DEFAULT_MAX_STOP = 100;
|
||||
export const DEFAULT_COLOR_STEPS = 5;
|
||||
export const DEFAULT_COLOR = '#6092C0'; // Same as EUI ColorStops default for new stops
|
||||
|
||||
export const defaultPaletteParams: RequiredPaletteParamTypes = {
|
||||
maxSteps: undefined,
|
||||
name: DEFAULT_PALETTE_NAME,
|
||||
reverse: false,
|
||||
rangeType: 'percent',
|
||||
rangeType: DEFAULT_RANGE_TYPE,
|
||||
rangeMin: DEFAULT_MIN_STOP,
|
||||
rangeMax: DEFAULT_MAX_STOP,
|
||||
progression: FIXED_PROGRESSION,
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
export { CustomizablePalette } from './palette_configuration';
|
||||
export { PalettePanelContainer } from './palette_panel_container';
|
||||
export { CustomStops } from './color_stops';
|
||||
export { ColorRanges } from './color_ranges';
|
||||
|
||||
export * from './utils';
|
||||
export * from './constants';
|
||||
|
|
|
@ -12,10 +12,9 @@ import { chartPluginMock } from 'src/plugins/charts/public/mocks';
|
|||
import type { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import type { CustomPaletteParams } from '../../../common';
|
||||
import { applyPaletteParams } from './utils';
|
||||
import { CustomizablePalette } from './palette_configuration';
|
||||
import { CUSTOM_PALETTE } from './constants';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import type { DataBounds } from './types';
|
||||
|
||||
// mocking random id generator function
|
||||
jest.mock('@elastic/eui', () => {
|
||||
|
@ -30,39 +29,14 @@ jest.mock('@elastic/eui', () => {
|
|||
};
|
||||
});
|
||||
|
||||
describe('palette utilities', () => {
|
||||
const paletteRegistry = chartPluginMock.createPaletteRegistry();
|
||||
describe('applyPaletteParams', () => {
|
||||
it('should return a set of colors for a basic configuration', () => {
|
||||
expect(
|
||||
applyPaletteParams(
|
||||
paletteRegistry,
|
||||
{ type: 'palette', name: 'positive' },
|
||||
{ min: 0, max: 100 }
|
||||
)
|
||||
).toEqual([
|
||||
{ color: 'blue', stop: 20 },
|
||||
{ color: 'yellow', stop: 70 },
|
||||
]);
|
||||
});
|
||||
// mocking isAllColorRangesValid function
|
||||
jest.mock('./color_ranges/color_ranges_validation', () => {
|
||||
const original = jest.requireActual('./color_ranges/color_ranges_validation');
|
||||
|
||||
it('should reverse the palette color stops correctly', () => {
|
||||
expect(
|
||||
applyPaletteParams(
|
||||
paletteRegistry,
|
||||
{
|
||||
type: 'palette',
|
||||
name: 'positive',
|
||||
params: { reverse: true },
|
||||
},
|
||||
{ min: 0, max: 100 }
|
||||
)
|
||||
).toEqual([
|
||||
{ color: 'yellow', stop: 20 },
|
||||
{ color: 'blue', stop: 70 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
return {
|
||||
...original,
|
||||
isAllColorRangesValid: () => true,
|
||||
};
|
||||
});
|
||||
|
||||
describe('palette panel', () => {
|
||||
|
@ -71,7 +45,7 @@ describe('palette panel', () => {
|
|||
palettes: PaletteRegistry;
|
||||
activePalette: PaletteOutput<CustomPaletteParams>;
|
||||
setPalette: (palette: PaletteOutput<CustomPaletteParams>) => void;
|
||||
dataBounds: { min: number; max: number };
|
||||
dataBounds: DataBounds;
|
||||
};
|
||||
|
||||
describe('palette picker', () => {
|
||||
|
@ -82,6 +56,8 @@ describe('palette panel', () => {
|
|||
setPalette: jest.fn(),
|
||||
dataBounds: { min: 0, max: 100 },
|
||||
};
|
||||
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
function changePaletteIn(instance: ReactWrapper, newPaletteName: string) {
|
||||
|
@ -113,7 +89,11 @@ describe('palette panel', () => {
|
|||
it('should set the colorStops and stops when selecting the Custom palette from the list', () => {
|
||||
const instance = mountWithIntl(<CustomizablePalette {...props} />);
|
||||
|
||||
changePaletteIn(instance, 'custom');
|
||||
act(() => {
|
||||
changePaletteIn(instance, 'custom');
|
||||
});
|
||||
|
||||
jest.advanceTimersByTime(250);
|
||||
|
||||
expect(props.setPalette).toHaveBeenCalledWith({
|
||||
type: 'palette',
|
||||
|
@ -135,7 +115,11 @@ describe('palette panel', () => {
|
|||
it('should restore the reverse initial state on transitioning', () => {
|
||||
const instance = mountWithIntl(<CustomizablePalette {...props} />);
|
||||
|
||||
changePaletteIn(instance, 'negative');
|
||||
act(() => {
|
||||
changePaletteIn(instance, 'negative');
|
||||
});
|
||||
|
||||
jest.advanceTimersByTime(250);
|
||||
|
||||
expect(props.setPalette).toHaveBeenCalledWith({
|
||||
type: 'palette',
|
||||
|
@ -150,69 +134,27 @@ describe('palette panel', () => {
|
|||
it('should rewrite the min/max range values on palette change', () => {
|
||||
const instance = mountWithIntl(<CustomizablePalette {...props} />);
|
||||
|
||||
changePaletteIn(instance, 'custom');
|
||||
act(() => {
|
||||
changePaletteIn(instance, 'custom');
|
||||
});
|
||||
|
||||
jest.advanceTimersByTime(250);
|
||||
|
||||
expect(props.setPalette).toHaveBeenCalledWith({
|
||||
type: 'palette',
|
||||
name: 'custom',
|
||||
params: expect.objectContaining({
|
||||
rangeMin: 0,
|
||||
rangeMax: 50,
|
||||
rangeMax: Number.POSITIVE_INFINITY,
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reverse option', () => {
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
activePalette: { type: 'palette', name: 'positive' },
|
||||
palettes: paletteRegistry,
|
||||
setPalette: jest.fn(),
|
||||
dataBounds: { min: 0, max: 100 },
|
||||
};
|
||||
});
|
||||
|
||||
function toggleReverse(instance: ReactWrapper, checked: boolean) {
|
||||
return instance
|
||||
.find('[data-test-subj="lnsPalettePanel_dynamicColoring_reverse"]')
|
||||
.first()
|
||||
.prop('onClick')!({} as React.MouseEvent);
|
||||
}
|
||||
|
||||
it('should reverse the colorStops on click', () => {
|
||||
const instance = mountWithIntl(<CustomizablePalette {...props} />);
|
||||
|
||||
toggleReverse(instance, true);
|
||||
|
||||
expect(props.setPalette).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
reverse: true,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should transition a predefined palette to a custom one on reverse click', () => {
|
||||
const instance = mountWithIntl(<CustomizablePalette {...props} />);
|
||||
|
||||
toggleReverse(instance, true);
|
||||
|
||||
expect(props.setPalette).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
name: CUSTOM_PALETTE,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('percentage / number modes', () => {
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
activePalette: { type: 'palette', name: 'positive' },
|
||||
activePalette: { type: 'palette', name: 'custom' },
|
||||
palettes: paletteRegistry,
|
||||
setPalette: jest.fn(),
|
||||
dataBounds: { min: 5, max: 200 },
|
||||
|
@ -228,6 +170,8 @@ describe('palette panel', () => {
|
|||
.prop('onChange')!('number');
|
||||
});
|
||||
|
||||
jest.advanceTimersByTime(250);
|
||||
|
||||
act(() => {
|
||||
instance
|
||||
.find('[data-test-subj="lnsPalettePanel_dynamicColoring_custom_range_groups"]')
|
||||
|
@ -235,13 +179,15 @@ describe('palette panel', () => {
|
|||
.prop('onChange')!('percent');
|
||||
});
|
||||
|
||||
jest.advanceTimersByTime(250);
|
||||
|
||||
expect(props.setPalette).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
rangeType: 'number',
|
||||
rangeMin: 5,
|
||||
rangeMax: 102.5 /* (200 - (200-5)/ colors.length: 2) */,
|
||||
rangeMax: Number.POSITIVE_INFINITY,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
@ -252,7 +198,7 @@ describe('palette panel', () => {
|
|||
params: expect.objectContaining({
|
||||
rangeType: 'percent',
|
||||
rangeMin: 0,
|
||||
rangeMax: 50 /* 100 - (100-0)/ colors.length: 2 */,
|
||||
rangeMax: Number.POSITIVE_INFINITY,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
@ -282,7 +228,9 @@ describe('palette panel', () => {
|
|||
it('should be visible for predefined palettes', () => {
|
||||
const instance = mountWithIntl(<CustomizablePalette {...props} />);
|
||||
expect(
|
||||
instance.find('[data-test-subj="lnsPalettePanel_dynamicColoring_custom_stops"]').exists()
|
||||
instance
|
||||
.find('[data-test-subj="lnsPalettePanel_dynamicColoring_custom_color_ranges"]')
|
||||
.exists()
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
|
@ -300,7 +248,9 @@ describe('palette panel', () => {
|
|||
/>
|
||||
);
|
||||
expect(
|
||||
instance.find('[data-test-subj="lnsPalettePanel_dynamicColoring_custom_stops"]').exists()
|
||||
instance
|
||||
.find('[data-test-subj="lnsPalettePanel_dynamicColoring_custom_color_ranges"]')
|
||||
.exists()
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,378 +5,179 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { useReducer, useMemo } from 'react';
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
import type { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public';
|
||||
import {
|
||||
EuiFormRow,
|
||||
htmlIdGenerator,
|
||||
EuiButtonGroup,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSuperSelect,
|
||||
EuiIcon,
|
||||
EuiIconTip,
|
||||
EuiLink,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { EuiFormRow, htmlIdGenerator, EuiButtonGroup, EuiIconTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { PalettePicker } from './palette_picker';
|
||||
import type { DataBounds } from './types';
|
||||
|
||||
import './palette_configuration.scss';
|
||||
|
||||
import { CustomStops } from './color_stops';
|
||||
import { defaultPaletteParams, CUSTOM_PALETTE, DEFAULT_COLOR_STEPS } from './constants';
|
||||
import type { CustomPaletteParams, RequiredPaletteParamTypes } from '../../../common';
|
||||
import {
|
||||
getColorStops,
|
||||
getPaletteStops,
|
||||
mergePaletteParams,
|
||||
getDataMinMax,
|
||||
remapStopsByNewInterval,
|
||||
getSwitchToCustomParams,
|
||||
reversePalette,
|
||||
roundStopValues,
|
||||
} from './utils';
|
||||
const idPrefix = htmlIdGenerator()();
|
||||
|
||||
const ContinuityOption: FC<{ iconType: string }> = ({ children, iconType }) => {
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type={iconType} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{children}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Some name conventions here:
|
||||
* * `displayStops` => It's an additional transformation of `stops` into a [0, N] domain for the EUIPaletteDisplay component.
|
||||
* * `stops` => final steps used to table coloring. It is a rightShift of the colorStops
|
||||
* * `colorStops` => user's color stop inputs. Used to compute range min.
|
||||
*
|
||||
* When the user inputs the colorStops, they are designed to be the initial part of the color segment,
|
||||
* so the next stops indicate where the previous stop ends.
|
||||
* Both table coloring logic and EuiPaletteDisplay format implementation works differently than our current `colorStops`,
|
||||
* by having the stop values at the end of each color segment rather than at the beginning: `stops` values are computed by a rightShift of `colorStops`.
|
||||
* EuiPaletteDisplay has an additional requirement as it is always mapped against a domain [0, N]: from `stops` the `displayStops` are computed with
|
||||
* some continuity enrichment and a remap against a [0, 100] domain to make the palette component work ok.
|
||||
*
|
||||
* These naming conventions would be useful to track the code flow in this feature as multiple transformations are happening
|
||||
* for a single change.
|
||||
*/
|
||||
import { toColorRanges, getFallbackDataBounds } from './utils';
|
||||
import { defaultPaletteParams } from './constants';
|
||||
import { ColorRanges, ColorRangesContext } from './color_ranges';
|
||||
import { isAllColorRangesValid } from './color_ranges/color_ranges_validation';
|
||||
import { paletteConfigurationReducer } from './palette_configuration_reducer';
|
||||
|
||||
export function CustomizablePalette({
|
||||
palettes,
|
||||
activePalette,
|
||||
setPalette,
|
||||
dataBounds,
|
||||
showContinuity = true,
|
||||
dataBounds = getFallbackDataBounds(activePalette.params?.rangeType),
|
||||
showRangeTypeSelector = true,
|
||||
disableSwitchingContinuity = false,
|
||||
}: {
|
||||
palettes: PaletteRegistry;
|
||||
activePalette?: PaletteOutput<CustomPaletteParams>;
|
||||
activePalette: PaletteOutput<CustomPaletteParams>;
|
||||
setPalette: (palette: PaletteOutput<CustomPaletteParams>) => void;
|
||||
dataBounds?: { min: number; max: number };
|
||||
showContinuity?: boolean;
|
||||
dataBounds?: DataBounds;
|
||||
showRangeTypeSelector?: boolean;
|
||||
disableSwitchingContinuity?: boolean;
|
||||
}) {
|
||||
if (!dataBounds || !activePalette) {
|
||||
return null;
|
||||
}
|
||||
const isCurrentPaletteCustom = activePalette.params?.name === CUSTOM_PALETTE;
|
||||
const idPrefix = useMemo(() => htmlIdGenerator()(), []);
|
||||
const colorRangesToShow = toColorRanges(
|
||||
palettes,
|
||||
activePalette.params?.colorStops || [],
|
||||
activePalette,
|
||||
dataBounds
|
||||
);
|
||||
|
||||
const colorStopsToShow = roundStopValues(
|
||||
getColorStops(palettes, activePalette?.params?.colorStops || [], activePalette, dataBounds)
|
||||
const [localState, dispatch] = useReducer(paletteConfigurationReducer, {
|
||||
activePalette,
|
||||
colorRanges: colorRangesToShow,
|
||||
});
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
const rangeType =
|
||||
localState.activePalette?.params?.rangeType ?? defaultPaletteParams.rangeType;
|
||||
if (
|
||||
(localState.activePalette !== activePalette ||
|
||||
colorRangesToShow !== localState.colorRanges) &&
|
||||
isAllColorRangesValid(localState.colorRanges, dataBounds, rangeType)
|
||||
) {
|
||||
setPalette(localState.activePalette);
|
||||
}
|
||||
},
|
||||
250,
|
||||
[localState]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="lnsPalettePanel__section lnsPalettePanel__section--shaded">
|
||||
<div className="lnsPalettePanel__section lnsPalettePanel__section--shaded">
|
||||
<EuiFormRow
|
||||
display="rowCompressed"
|
||||
label={i18n.translate('xpack.lens.palettePicker.label', {
|
||||
defaultMessage: 'Color palette',
|
||||
})}
|
||||
fullWidth
|
||||
>
|
||||
<PalettePicker
|
||||
data-test-subj="lnsPalettePanel_dynamicColoring_palette_picker"
|
||||
palettes={palettes}
|
||||
activePalette={localState.activePalette}
|
||||
setPalette={(newPalette) => {
|
||||
const isPaletteChanged = newPalette.name !== activePalette.name;
|
||||
if (isPaletteChanged) {
|
||||
dispatch({
|
||||
type: 'changeColorPalette',
|
||||
payload: { palette: newPalette, dataBounds, palettes, disableSwitchingContinuity },
|
||||
});
|
||||
}
|
||||
}}
|
||||
showCustomPalette
|
||||
showDynamicColorOnly
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{showRangeTypeSelector && (
|
||||
<EuiFormRow
|
||||
display="rowCompressed"
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.lens.palettePicker.label', {
|
||||
defaultMessage: 'Color palette',
|
||||
})}
|
||||
>
|
||||
<PalettePicker
|
||||
data-test-subj="lnsPalettePanel_dynamicColoring_palette_picker"
|
||||
palettes={palettes}
|
||||
activePalette={activePalette}
|
||||
setPalette={(newPalette) => {
|
||||
const isNewPaletteCustom = newPalette.name === CUSTOM_PALETTE;
|
||||
const newParams: CustomPaletteParams = {
|
||||
...activePalette.params,
|
||||
name: newPalette.name,
|
||||
colorStops: undefined,
|
||||
reverse: false, // restore the reverse flag
|
||||
};
|
||||
|
||||
const newColorStops = getColorStops(palettes, [], activePalette, dataBounds);
|
||||
if (isNewPaletteCustom) {
|
||||
newParams.colorStops = newColorStops;
|
||||
}
|
||||
|
||||
newParams.stops = getPaletteStops(palettes, newParams, {
|
||||
prevPalette:
|
||||
isNewPaletteCustom || isCurrentPaletteCustom ? undefined : newPalette.name,
|
||||
dataBounds,
|
||||
mapFromMinValue: true,
|
||||
});
|
||||
|
||||
newParams.rangeMin = newColorStops[0].stop;
|
||||
newParams.rangeMax = newColorStops[newColorStops.length - 1].stop;
|
||||
|
||||
setPalette({
|
||||
...newPalette,
|
||||
params: newParams,
|
||||
});
|
||||
}}
|
||||
showCustomPalette
|
||||
showDynamicColorOnly
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{showContinuity && (
|
||||
<EuiFormRow
|
||||
label={
|
||||
<>
|
||||
{i18n.translate('xpack.lens.table.dynamicColoring.continuity.label', {
|
||||
defaultMessage: 'Color continuity',
|
||||
})}{' '}
|
||||
<EuiIconTip
|
||||
content={i18n.translate(
|
||||
'xpack.lens.table.dynamicColoring.customPalette.continuityHelp',
|
||||
{
|
||||
defaultMessage:
|
||||
'Specify how colors appear before the first color stop, and after the last color stop.',
|
||||
}
|
||||
)}
|
||||
position="top"
|
||||
size="s"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
display="rowCompressed"
|
||||
>
|
||||
<EuiSuperSelect
|
||||
data-test-subj="lnsPalettePanel_dynamicColoring_continuity"
|
||||
compressed
|
||||
options={[
|
||||
{
|
||||
value: 'above',
|
||||
inputDisplay: (
|
||||
<ContinuityOption iconType="continuityAbove">
|
||||
{i18n.translate('xpack.lens.table.dynamicColoring.continuity.aboveLabel', {
|
||||
defaultMessage: 'Above range',
|
||||
})}
|
||||
</ContinuityOption>
|
||||
),
|
||||
'data-test-subj': 'continuity-above',
|
||||
},
|
||||
{
|
||||
value: 'below',
|
||||
inputDisplay: (
|
||||
<ContinuityOption iconType="continuityBelow">
|
||||
{i18n.translate('xpack.lens.table.dynamicColoring.continuity.belowLabel', {
|
||||
defaultMessage: 'Below range',
|
||||
})}
|
||||
</ContinuityOption>
|
||||
),
|
||||
'data-test-subj': 'continuity-below',
|
||||
},
|
||||
{
|
||||
value: 'all',
|
||||
inputDisplay: (
|
||||
<ContinuityOption iconType="continuityAboveBelow">
|
||||
{i18n.translate('xpack.lens.table.dynamicColoring.continuity.allLabel', {
|
||||
defaultMessage: 'Above and below range',
|
||||
})}
|
||||
</ContinuityOption>
|
||||
),
|
||||
'data-test-subj': 'continuity-all',
|
||||
},
|
||||
{
|
||||
value: 'none',
|
||||
inputDisplay: (
|
||||
<ContinuityOption iconType="continuityWithin">
|
||||
{i18n.translate('xpack.lens.table.dynamicColoring.continuity.noneLabel', {
|
||||
defaultMessage: 'Within range',
|
||||
})}
|
||||
</ContinuityOption>
|
||||
),
|
||||
'data-test-subj': 'continuity-none',
|
||||
},
|
||||
]}
|
||||
valueOfSelected={activePalette.params?.continuity || defaultPaletteParams.continuity}
|
||||
onChange={(continuity: Required<CustomPaletteParams>['continuity']) =>
|
||||
setPalette(
|
||||
mergePaletteParams(activePalette, {
|
||||
continuity,
|
||||
})
|
||||
)
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
{showRangeTypeSelector && (
|
||||
<EuiFormRow
|
||||
label={
|
||||
<>
|
||||
{i18n.translate('xpack.lens.table.dynamicColoring.rangeType.label', {
|
||||
defaultMessage: 'Value type',
|
||||
})}{' '}
|
||||
<EuiIconTip
|
||||
content={i18n.translate(
|
||||
'xpack.lens.table.dynamicColoring.customPalette.colorStopsHelpPercentage',
|
||||
{
|
||||
defaultMessage:
|
||||
'Percent value types are relative to the full range of available data values.',
|
||||
}
|
||||
)}
|
||||
position="top"
|
||||
size="s"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
display="rowCompressed"
|
||||
>
|
||||
<EuiButtonGroup
|
||||
isFullWidth
|
||||
legend={i18n.translate('xpack.lens.table.dynamicColoring.rangeType.label', {
|
||||
label={
|
||||
<>
|
||||
{i18n.translate('xpack.lens.table.dynamicColoring.rangeType.label', {
|
||||
defaultMessage: 'Value type',
|
||||
})}
|
||||
data-test-subj="lnsPalettePanel_dynamicColoring_custom_range_groups"
|
||||
name="dynamicColoringRangeType"
|
||||
buttonSize="compressed"
|
||||
options={[
|
||||
{
|
||||
id: `${idPrefix}percent`,
|
||||
label: i18n.translate('xpack.lens.table.dynamicColoring.rangeType.percent', {
|
||||
defaultMessage: 'Percent',
|
||||
}),
|
||||
'data-test-subj': 'lnsPalettePanel_dynamicColoring_rangeType_groups_percent',
|
||||
},
|
||||
{
|
||||
id: `${idPrefix}number`,
|
||||
label: i18n.translate('xpack.lens.table.dynamicColoring.rangeType.number', {
|
||||
defaultMessage: 'Number',
|
||||
}),
|
||||
'data-test-subj': 'lnsPalettePanel_dynamicColoring_rangeType_groups_number',
|
||||
},
|
||||
]}
|
||||
idSelected={
|
||||
activePalette.params?.rangeType
|
||||
? `${idPrefix}${activePalette.params?.rangeType}`
|
||||
: `${idPrefix}percent`
|
||||
}
|
||||
onChange={(id) => {
|
||||
const newRangeType = id.replace(
|
||||
idPrefix,
|
||||
''
|
||||
) as RequiredPaletteParamTypes['rangeType'];
|
||||
|
||||
const params: CustomPaletteParams = { rangeType: newRangeType };
|
||||
const { min: newMin, max: newMax } = getDataMinMax(newRangeType, dataBounds);
|
||||
const { min: oldMin, max: oldMax } = getDataMinMax(
|
||||
activePalette.params?.rangeType,
|
||||
dataBounds
|
||||
);
|
||||
const newColorStops = remapStopsByNewInterval(colorStopsToShow, {
|
||||
oldInterval: oldMax - oldMin,
|
||||
newInterval: newMax - newMin,
|
||||
newMin,
|
||||
oldMin,
|
||||
});
|
||||
if (isCurrentPaletteCustom) {
|
||||
const stops = getPaletteStops(
|
||||
palettes,
|
||||
{ ...activePalette.params, colorStops: newColorStops, ...params },
|
||||
{ dataBounds }
|
||||
);
|
||||
params.colorStops = newColorStops;
|
||||
params.stops = stops;
|
||||
} else {
|
||||
params.stops = getPaletteStops(
|
||||
palettes,
|
||||
{ ...activePalette.params, ...params },
|
||||
{ prevPalette: activePalette.name, dataBounds }
|
||||
);
|
||||
}
|
||||
// why not use newMin/newMax here?
|
||||
// That's because there's the concept of continuity to accomodate, where in some scenarios it has to
|
||||
// take into account the stop value rather than the data value
|
||||
params.rangeMin = newColorStops[0].stop;
|
||||
params.rangeMax = newColorStops[newColorStops.length - 1].stop;
|
||||
setPalette(mergePaletteParams(activePalette, params));
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
<EuiFormRow
|
||||
display="rowCompressed"
|
||||
label={i18n.translate('xpack.lens.table.dynamicColoring.customPalette.colorStopsLabel', {
|
||||
defaultMessage: 'Color stops',
|
||||
})}
|
||||
labelAppend={
|
||||
<EuiText size="xs">
|
||||
<EuiLink
|
||||
className="lnsPalettePanel__reverseButton"
|
||||
data-test-subj="lnsPalettePanel_dynamicColoring_reverse"
|
||||
onClick={() => {
|
||||
// when reversing a palette, the palette is automatically transitioned to a custom palette
|
||||
const newParams = getSwitchToCustomParams(
|
||||
palettes,
|
||||
activePalette,
|
||||
{
|
||||
colorStops: reversePalette(colorStopsToShow),
|
||||
steps: activePalette.params?.steps || DEFAULT_COLOR_STEPS,
|
||||
reverse: !activePalette.params?.reverse, // Store the reverse state
|
||||
rangeMin: colorStopsToShow[0]?.stop,
|
||||
rangeMax: colorStopsToShow[colorStopsToShow.length - 1]?.stop,
|
||||
},
|
||||
dataBounds
|
||||
);
|
||||
setPalette(newParams);
|
||||
}}
|
||||
>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon size="s" type="sortable" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{i18n.translate('xpack.lens.table.dynamicColoring.reverse.label', {
|
||||
defaultMessage: 'Reverse colors',
|
||||
})}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiLink>
|
||||
</EuiText>
|
||||
})}{' '}
|
||||
<EuiIconTip
|
||||
content={i18n.translate(
|
||||
'xpack.lens.table.dynamicColoring.customPalette.colorStopsHelpPercentage',
|
||||
{
|
||||
defaultMessage:
|
||||
'Percent value types are relative to the full range of available data values.',
|
||||
}
|
||||
)}
|
||||
position="top"
|
||||
size="s"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
display="rowCompressed"
|
||||
>
|
||||
<CustomStops
|
||||
paletteConfiguration={activePalette?.params}
|
||||
data-test-prefix="lnsPalettePanel"
|
||||
colorStops={colorStopsToShow}
|
||||
dataBounds={dataBounds}
|
||||
onChange={(colorStops) => {
|
||||
const newParams = getSwitchToCustomParams(
|
||||
palettes,
|
||||
activePalette,
|
||||
{
|
||||
colorStops,
|
||||
steps: activePalette.params?.steps || DEFAULT_COLOR_STEPS,
|
||||
rangeMin: colorStops[0]?.stop,
|
||||
rangeMax: colorStops[colorStops.length - 1]?.stop,
|
||||
},
|
||||
dataBounds
|
||||
);
|
||||
return setPalette(newParams);
|
||||
<EuiButtonGroup
|
||||
legend={i18n.translate('xpack.lens.table.dynamicColoring.rangeType.label', {
|
||||
defaultMessage: 'Value type',
|
||||
})}
|
||||
data-test-subj="lnsPalettePanel_dynamicColoring_custom_range_groups"
|
||||
name="dynamicColoringRangeType"
|
||||
buttonSize="compressed"
|
||||
options={[
|
||||
{
|
||||
id: `${idPrefix}percent`,
|
||||
label: i18n.translate('xpack.lens.table.dynamicColoring.rangeType.percent', {
|
||||
defaultMessage: 'Percent',
|
||||
}),
|
||||
'data-test-subj': 'lnsPalettePanel_dynamicColoring_rangeType_groups_percent',
|
||||
},
|
||||
{
|
||||
id: `${idPrefix}number`,
|
||||
label: i18n.translate('xpack.lens.table.dynamicColoring.rangeType.number', {
|
||||
defaultMessage: 'Number',
|
||||
}),
|
||||
'data-test-subj': 'lnsPalettePanel_dynamicColoring_rangeType_groups_number',
|
||||
},
|
||||
]}
|
||||
idSelected={
|
||||
localState.activePalette.params?.rangeType
|
||||
? `${idPrefix}${localState.activePalette.params?.rangeType}`
|
||||
: `${idPrefix}percent`
|
||||
}
|
||||
onChange={(id) => {
|
||||
const newRangeType = id.replace(
|
||||
idPrefix,
|
||||
''
|
||||
) as RequiredPaletteParamTypes['rangeType'];
|
||||
|
||||
dispatch({
|
||||
type: 'updateRangeType',
|
||||
payload: { rangeType: newRangeType, dataBounds, palettes },
|
||||
});
|
||||
}}
|
||||
isFullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.palettePicker.colorRangesLabel', {
|
||||
defaultMessage: 'Color Ranges',
|
||||
})}
|
||||
display="rowCompressed"
|
||||
fullWidth
|
||||
>
|
||||
<ColorRangesContext.Provider
|
||||
value={{
|
||||
dataBounds,
|
||||
palettes,
|
||||
disableSwitchingContinuity,
|
||||
}}
|
||||
>
|
||||
<ColorRanges
|
||||
paletteConfiguration={localState.activePalette?.params}
|
||||
colorRanges={localState.colorRanges}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
</ColorRangesContext.Provider>
|
||||
</EuiFormRow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Reducer } from 'react';
|
||||
import {
|
||||
addColorRange,
|
||||
deleteColorRange,
|
||||
distributeEqually,
|
||||
reversePalette,
|
||||
sortColorRanges,
|
||||
updateColorRangeColor,
|
||||
updateColorRangeValue,
|
||||
getValueForContinuity,
|
||||
} from './color_ranges/utils';
|
||||
import { DEFAULT_CONTINUITY, DEFAULT_RANGE_TYPE } from './constants';
|
||||
|
||||
import {
|
||||
mergePaletteParams,
|
||||
updateRangeType,
|
||||
changeColorPalette,
|
||||
withUpdatingPalette,
|
||||
withUpdatingColorRanges,
|
||||
} from './utils';
|
||||
|
||||
import type { PaletteConfigurationState, PaletteConfigurationActions } from './types';
|
||||
|
||||
export const paletteConfigurationReducer: Reducer<
|
||||
PaletteConfigurationState,
|
||||
PaletteConfigurationActions
|
||||
> = (state, action) => {
|
||||
switch (action.type) {
|
||||
case 'updateContinuity': {
|
||||
const { continuity, isLast, dataBounds, palettes } = action.payload;
|
||||
const rangeType = state.activePalette.params?.rangeType ?? DEFAULT_RANGE_TYPE;
|
||||
|
||||
const value = getValueForContinuity(
|
||||
state.colorRanges,
|
||||
continuity,
|
||||
isLast,
|
||||
rangeType,
|
||||
dataBounds
|
||||
);
|
||||
|
||||
return withUpdatingPalette(
|
||||
palettes!,
|
||||
state.activePalette,
|
||||
updateColorRangeValue(
|
||||
isLast ? state.colorRanges.length - 1 : 0,
|
||||
`${value}`,
|
||||
isLast ? 'end' : 'start',
|
||||
state.colorRanges
|
||||
),
|
||||
dataBounds,
|
||||
continuity
|
||||
);
|
||||
}
|
||||
case 'addColorRange': {
|
||||
const { dataBounds, palettes } = action.payload;
|
||||
return withUpdatingPalette(
|
||||
palettes!,
|
||||
state.activePalette,
|
||||
addColorRange(
|
||||
state.colorRanges,
|
||||
state.activePalette.params?.rangeType ?? DEFAULT_RANGE_TYPE,
|
||||
dataBounds
|
||||
),
|
||||
dataBounds
|
||||
);
|
||||
}
|
||||
case 'reversePalette': {
|
||||
const { dataBounds, palettes } = action.payload;
|
||||
return withUpdatingPalette(
|
||||
palettes!,
|
||||
state.activePalette,
|
||||
reversePalette(state.colorRanges),
|
||||
dataBounds
|
||||
);
|
||||
}
|
||||
case 'distributeEqually': {
|
||||
const { dataBounds, palettes } = action.payload;
|
||||
return withUpdatingPalette(
|
||||
palettes!,
|
||||
state.activePalette,
|
||||
distributeEqually(
|
||||
state.colorRanges,
|
||||
state.activePalette.params?.rangeType,
|
||||
state.activePalette.params?.continuity ?? DEFAULT_CONTINUITY,
|
||||
dataBounds
|
||||
),
|
||||
dataBounds
|
||||
);
|
||||
}
|
||||
case 'updateColor': {
|
||||
const { index, color, palettes, dataBounds } = action.payload;
|
||||
return withUpdatingPalette(
|
||||
palettes!,
|
||||
state.activePalette,
|
||||
updateColorRangeColor(index, color, state.colorRanges),
|
||||
dataBounds
|
||||
);
|
||||
}
|
||||
case 'sortColorRanges': {
|
||||
const { dataBounds, palettes } = action.payload;
|
||||
return withUpdatingPalette(
|
||||
palettes!,
|
||||
state.activePalette,
|
||||
sortColorRanges(state.colorRanges),
|
||||
dataBounds
|
||||
);
|
||||
}
|
||||
case 'updateValue': {
|
||||
const { index, value, accessor, dataBounds, palettes } = action.payload;
|
||||
return withUpdatingPalette(
|
||||
palettes!,
|
||||
state.activePalette,
|
||||
updateColorRangeValue(index, value, accessor, state.colorRanges),
|
||||
dataBounds
|
||||
);
|
||||
}
|
||||
case 'deleteColorRange': {
|
||||
const { index, dataBounds, palettes } = action.payload;
|
||||
return withUpdatingPalette(
|
||||
palettes!,
|
||||
state.activePalette,
|
||||
deleteColorRange(index, state.colorRanges),
|
||||
dataBounds
|
||||
);
|
||||
}
|
||||
case 'updateRangeType': {
|
||||
const { dataBounds, palettes, rangeType } = action.payload;
|
||||
const paletteParams = updateRangeType(
|
||||
rangeType,
|
||||
state.activePalette,
|
||||
dataBounds,
|
||||
palettes,
|
||||
state.colorRanges
|
||||
);
|
||||
|
||||
const newPalette = mergePaletteParams(state.activePalette, paletteParams);
|
||||
|
||||
return withUpdatingColorRanges(palettes, newPalette, dataBounds);
|
||||
}
|
||||
case 'changeColorPalette': {
|
||||
const { dataBounds, palettes, palette, disableSwitchingContinuity } = action.payload;
|
||||
const newPalette = changeColorPalette(
|
||||
palette,
|
||||
state.activePalette,
|
||||
palettes,
|
||||
dataBounds,
|
||||
disableSwitchingContinuity
|
||||
);
|
||||
return withUpdatingColorRanges(palettes, newPalette, dataBounds);
|
||||
}
|
||||
default:
|
||||
throw new Error('wrong action');
|
||||
}
|
||||
};
|
|
@ -25,13 +25,13 @@ import {
|
|||
export function PalettePanelContainer({
|
||||
isOpen,
|
||||
handleClose,
|
||||
children,
|
||||
siblingRef,
|
||||
children,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
children: React.ReactElement | React.ReactElement[];
|
||||
siblingRef: MutableRefObject<HTMLDivElement | null>;
|
||||
children?: React.ReactElement | React.ReactElement[];
|
||||
}) {
|
||||
const [focusTrapIsEnabled, setFocusTrapIsEnabled] = useState(false);
|
||||
|
||||
|
@ -91,7 +91,7 @@ export function PalettePanelContainer({
|
|||
</EuiFlexGroup>
|
||||
</EuiFlyoutHeader>
|
||||
|
||||
<div className="lnsPalettePanelContainer__content">{children}</div>
|
||||
{children && <div className="lnsPalettePanelContainer__content">{children}</div>}
|
||||
|
||||
<EuiFlyoutFooter className="lnsPalettePanelContainer__footer">
|
||||
<EuiButtonEmpty flush="left" size="s" iconType="sortLeft" onClick={closeFlyout}>
|
||||
|
|
|
@ -15,7 +15,6 @@ import {
|
|||
defaultPaletteParams,
|
||||
} from './constants';
|
||||
import type { CustomPaletteParams } from '../../../common';
|
||||
import { getStopsForFixedMode } from './utils';
|
||||
|
||||
function getCustomPaletteConfig(
|
||||
palettes: PaletteRegistry,
|
||||
|
@ -52,7 +51,9 @@ function getCustomPaletteConfig(
|
|||
title,
|
||||
type: FIXED_PROGRESSION,
|
||||
'data-test-subj': `custom-palette`,
|
||||
palette: getStopsForFixedMode(activePalette.params.stops, activePalette.params.colorStops),
|
||||
palette: (activePalette.params.colorStops || activePalette.params.stops).map(
|
||||
(colorStop) => colorStop.color
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public';
|
||||
import type { CustomPaletteParams } from '../../../common';
|
||||
import type { ColorRange, ColorRangesActions } from './color_ranges';
|
||||
|
||||
export interface PaletteConfigurationState {
|
||||
activePalette: PaletteOutput<CustomPaletteParams>;
|
||||
colorRanges: ColorRange[];
|
||||
}
|
||||
|
||||
/** @internal **/
|
||||
export interface DataBounds {
|
||||
min: number;
|
||||
max: number;
|
||||
fallback?: boolean;
|
||||
}
|
||||
|
||||
/** @internal **/
|
||||
export interface UpdateRangeTypePayload {
|
||||
rangeType: CustomPaletteParams['rangeType'];
|
||||
palettes: PaletteRegistry;
|
||||
dataBounds: DataBounds;
|
||||
}
|
||||
|
||||
/** @internal **/
|
||||
export interface ChangeColorPalettePayload {
|
||||
palette: PaletteOutput<CustomPaletteParams>;
|
||||
palettes: PaletteRegistry;
|
||||
dataBounds: DataBounds;
|
||||
disableSwitchingContinuity: boolean;
|
||||
}
|
||||
|
||||
export type PaletteConfigurationActions =
|
||||
| ColorRangesActions
|
||||
| { type: 'updateRangeType'; payload: UpdateRangeTypePayload }
|
||||
| { type: 'changeColorPalette'; payload: ChangeColorPalettePayload };
|
|
@ -17,7 +17,8 @@ import {
|
|||
mergePaletteParams,
|
||||
remapStopsByNewInterval,
|
||||
reversePalette,
|
||||
roundStopValues,
|
||||
updateRangeType,
|
||||
changeColorPalette,
|
||||
} from './utils';
|
||||
|
||||
describe('applyPaletteParams', () => {
|
||||
|
@ -411,14 +412,6 @@ describe('isValidColor', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('roundStopValues', () => {
|
||||
it('should round very long values', () => {
|
||||
expect(roundStopValues([{ color: 'red', stop: 0.1515 }])).toEqual([
|
||||
{ color: 'red', stop: 0.15 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStepValue', () => {
|
||||
it('should compute the next step based on the last 2 stops', () => {
|
||||
expect(
|
||||
|
@ -490,3 +483,310 @@ describe('getContrastColor', () => {
|
|||
expect(getContrastColor('rgba(255,255,255,0)', false)).toBe('#000000');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateRangeType', () => {
|
||||
const paletteRegistry = chartPluginMock.createPaletteRegistry();
|
||||
const colorRanges = [
|
||||
{
|
||||
start: 0,
|
||||
end: 40,
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
start: 40,
|
||||
end: 80,
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
start: 80,
|
||||
end: 100,
|
||||
color: 'red',
|
||||
},
|
||||
];
|
||||
it('should correctly update palette params with new range type if continuity is none', () => {
|
||||
const newPaletteParams = updateRangeType(
|
||||
'number',
|
||||
{
|
||||
type: 'palette',
|
||||
name: 'custom',
|
||||
params: {
|
||||
continuity: 'none',
|
||||
name: 'custom',
|
||||
rangeType: 'percent',
|
||||
rangeMax: 100,
|
||||
rangeMin: 0,
|
||||
colorStops: [
|
||||
{ color: 'green', stop: 0 },
|
||||
{ color: 'blue', stop: 40 },
|
||||
{ color: 'red', stop: 80 },
|
||||
],
|
||||
},
|
||||
},
|
||||
{ min: 0, max: 200 },
|
||||
paletteRegistry,
|
||||
colorRanges
|
||||
);
|
||||
expect(newPaletteParams).toEqual({
|
||||
rangeType: 'number',
|
||||
rangeMin: 0,
|
||||
rangeMax: 200,
|
||||
colorStops: [
|
||||
{
|
||||
color: 'green',
|
||||
stop: 0,
|
||||
},
|
||||
{
|
||||
color: 'blue',
|
||||
stop: 80,
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
stop: 160,
|
||||
},
|
||||
],
|
||||
stops: [
|
||||
{
|
||||
color: 'green',
|
||||
stop: 80,
|
||||
},
|
||||
{
|
||||
color: 'blue',
|
||||
stop: 160,
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
stop: 200,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly update palette params with new range type if continuity is all', () => {
|
||||
const newPaletteParams = updateRangeType(
|
||||
'number',
|
||||
{
|
||||
type: 'palette',
|
||||
name: 'custom',
|
||||
params: {
|
||||
continuity: 'all',
|
||||
name: 'custom',
|
||||
rangeType: 'percent',
|
||||
rangeMax: 100,
|
||||
rangeMin: 0,
|
||||
colorStops: [
|
||||
{ color: 'green', stop: 0 },
|
||||
{ color: 'blue', stop: 40 },
|
||||
{ color: 'red', stop: 80 },
|
||||
],
|
||||
},
|
||||
},
|
||||
{ min: 0, max: 200 },
|
||||
paletteRegistry,
|
||||
colorRanges
|
||||
);
|
||||
expect(newPaletteParams).toEqual({
|
||||
rangeType: 'number',
|
||||
rangeMin: Number.NEGATIVE_INFINITY,
|
||||
rangeMax: Number.POSITIVE_INFINITY,
|
||||
colorStops: [
|
||||
{
|
||||
color: 'green',
|
||||
stop: 0,
|
||||
},
|
||||
{
|
||||
color: 'blue',
|
||||
stop: 80,
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
stop: 160,
|
||||
},
|
||||
],
|
||||
stops: [
|
||||
{
|
||||
color: 'green',
|
||||
stop: 80,
|
||||
},
|
||||
{
|
||||
color: 'blue',
|
||||
stop: 160,
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
stop: 200,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly update palette params with new range type if continuity is below', () => {
|
||||
const newPaletteParams = updateRangeType(
|
||||
'number',
|
||||
{
|
||||
type: 'palette',
|
||||
name: 'custom',
|
||||
params: {
|
||||
continuity: 'below',
|
||||
name: 'custom',
|
||||
rangeType: 'percent',
|
||||
rangeMax: 100,
|
||||
rangeMin: 0,
|
||||
colorStops: [
|
||||
{ color: 'green', stop: 0 },
|
||||
{ color: 'blue', stop: 40 },
|
||||
{ color: 'red', stop: 80 },
|
||||
],
|
||||
},
|
||||
},
|
||||
{ min: 0, max: 200 },
|
||||
paletteRegistry,
|
||||
colorRanges
|
||||
);
|
||||
expect(newPaletteParams).toEqual({
|
||||
rangeType: 'number',
|
||||
rangeMin: Number.NEGATIVE_INFINITY,
|
||||
rangeMax: 200,
|
||||
colorStops: [
|
||||
{
|
||||
color: 'green',
|
||||
stop: 0,
|
||||
},
|
||||
{
|
||||
color: 'blue',
|
||||
stop: 80,
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
stop: 160,
|
||||
},
|
||||
],
|
||||
stops: [
|
||||
{
|
||||
color: 'green',
|
||||
stop: 80,
|
||||
},
|
||||
{
|
||||
color: 'blue',
|
||||
stop: 160,
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
stop: 200,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly update palette params with new range type if continuity is above', () => {
|
||||
const newPaletteParams = updateRangeType(
|
||||
'number',
|
||||
{
|
||||
type: 'palette',
|
||||
name: 'custom',
|
||||
params: {
|
||||
continuity: 'above',
|
||||
name: 'custom',
|
||||
rangeType: 'percent',
|
||||
rangeMax: 100,
|
||||
rangeMin: 0,
|
||||
colorStops: [
|
||||
{ color: 'green', stop: 0 },
|
||||
{ color: 'blue', stop: 40 },
|
||||
{ color: 'red', stop: 80 },
|
||||
],
|
||||
},
|
||||
},
|
||||
{ min: 0, max: 200 },
|
||||
paletteRegistry,
|
||||
colorRanges
|
||||
);
|
||||
expect(newPaletteParams).toEqual({
|
||||
rangeType: 'number',
|
||||
rangeMin: 0,
|
||||
rangeMax: Number.POSITIVE_INFINITY,
|
||||
colorStops: [
|
||||
{
|
||||
color: 'green',
|
||||
stop: 0,
|
||||
},
|
||||
{
|
||||
color: 'blue',
|
||||
stop: 80,
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
stop: 160,
|
||||
},
|
||||
],
|
||||
stops: [
|
||||
{
|
||||
color: 'green',
|
||||
stop: 80,
|
||||
},
|
||||
{
|
||||
color: 'blue',
|
||||
stop: 160,
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
stop: 200,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('changeColorPalette', () => {
|
||||
const paletteRegistry = chartPluginMock.createPaletteRegistry();
|
||||
|
||||
it('should correct update params for new palette', () => {
|
||||
const newPaletteParams = changeColorPalette(
|
||||
{
|
||||
type: 'palette',
|
||||
name: 'default',
|
||||
},
|
||||
{
|
||||
type: 'palette',
|
||||
name: 'custom',
|
||||
params: {
|
||||
continuity: 'above',
|
||||
name: 'custom',
|
||||
rangeType: 'percent',
|
||||
rangeMax: 100,
|
||||
rangeMin: 0,
|
||||
colorStops: [
|
||||
{ color: 'green', stop: 0 },
|
||||
{ color: 'blue', stop: 40 },
|
||||
{ color: 'red', stop: 80 },
|
||||
],
|
||||
},
|
||||
},
|
||||
paletteRegistry,
|
||||
{ min: 0, max: 200 },
|
||||
false
|
||||
);
|
||||
expect(newPaletteParams).toEqual({
|
||||
name: 'default',
|
||||
type: 'palette',
|
||||
params: {
|
||||
rangeType: 'percent',
|
||||
name: 'default',
|
||||
continuity: 'above',
|
||||
rangeMin: 0,
|
||||
rangeMax: Number.POSITIVE_INFINITY,
|
||||
reverse: false,
|
||||
colorStops: undefined,
|
||||
stops: [
|
||||
{
|
||||
color: 'red',
|
||||
stop: 0,
|
||||
},
|
||||
{
|
||||
color: 'black',
|
||||
stop: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,8 +16,16 @@ import {
|
|||
DEFAULT_COLOR_STEPS,
|
||||
DEFAULT_MAX_STOP,
|
||||
DEFAULT_MIN_STOP,
|
||||
DEFAULT_CONTINUITY,
|
||||
} from './constants';
|
||||
import type { ColorRange } from './color_ranges';
|
||||
import { toColorStops, sortColorRanges } from './color_ranges/utils';
|
||||
import type { PaletteConfigurationState, DataBounds } from './types';
|
||||
import type { CustomPaletteParams, ColorStop } from '../../../common';
|
||||
import {
|
||||
checkIsMinContinuity,
|
||||
checkIsMaxContinuity,
|
||||
} from '../../../../../../src/plugins/charts/common';
|
||||
|
||||
/**
|
||||
* Some name conventions here:
|
||||
|
@ -36,10 +44,171 @@ import type { CustomPaletteParams, ColorStop } from '../../../common';
|
|||
* for a single change.
|
||||
*/
|
||||
|
||||
export function updateRangeType(
|
||||
newRangeType: CustomPaletteParams['rangeType'],
|
||||
activePalette: PaletteConfigurationState['activePalette'],
|
||||
dataBounds: DataBounds,
|
||||
palettes: PaletteRegistry,
|
||||
colorRanges: PaletteConfigurationState['colorRanges']
|
||||
) {
|
||||
const continuity = activePalette.params?.continuity ?? DEFAULT_CONTINUITY;
|
||||
const params: CustomPaletteParams = { rangeType: newRangeType };
|
||||
const { min: newMin, max: newMax } = getDataMinMax(newRangeType, dataBounds);
|
||||
const { min: oldMin, max: oldMax } = getDataMinMax(activePalette.params?.rangeType, dataBounds);
|
||||
const newColorStops = getStopsFromColorRangesByNewInterval(colorRanges, {
|
||||
oldInterval: oldMax - oldMin,
|
||||
newInterval: newMax - newMin,
|
||||
newMin,
|
||||
oldMin,
|
||||
});
|
||||
|
||||
if (activePalette.name === CUSTOM_PALETTE) {
|
||||
const stops = getPaletteStops(
|
||||
palettes,
|
||||
{ ...activePalette.params, colorStops: newColorStops, ...params },
|
||||
{ dataBounds }
|
||||
);
|
||||
params.colorStops = newColorStops;
|
||||
params.stops = stops;
|
||||
} else {
|
||||
params.stops = getPaletteStops(
|
||||
palettes,
|
||||
{ ...activePalette.params, ...params },
|
||||
{ prevPalette: activePalette.name, dataBounds }
|
||||
);
|
||||
}
|
||||
|
||||
const lastStop =
|
||||
activePalette.name === CUSTOM_PALETTE
|
||||
? newColorStops[newColorStops.length - 1].stop
|
||||
: params.stops[params.stops.length - 1].stop;
|
||||
|
||||
params.rangeMin = checkIsMinContinuity(continuity)
|
||||
? Number.NEGATIVE_INFINITY
|
||||
: activePalette.name === CUSTOM_PALETTE
|
||||
? newColorStops[0].stop
|
||||
: params.stops[0].stop;
|
||||
|
||||
params.rangeMax = checkIsMaxContinuity(continuity)
|
||||
? Number.POSITIVE_INFINITY
|
||||
: activePalette.params?.rangeMax
|
||||
? calculateStop(activePalette.params.rangeMax, newMin, oldMin, oldMax - oldMin, newMax - newMin)
|
||||
: lastStop > newMax
|
||||
? lastStop + 1
|
||||
: newMax;
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
export function changeColorPalette(
|
||||
newPalette: PaletteConfigurationState['activePalette'],
|
||||
activePalette: PaletteConfigurationState['activePalette'],
|
||||
palettes: PaletteRegistry,
|
||||
dataBounds: DataBounds,
|
||||
disableSwitchingContinuity: boolean
|
||||
) {
|
||||
const isNewPaletteCustom = newPalette.name === CUSTOM_PALETTE;
|
||||
const newParams: CustomPaletteParams = {
|
||||
...activePalette.params,
|
||||
name: newPalette.name,
|
||||
colorStops: undefined,
|
||||
continuity: disableSwitchingContinuity
|
||||
? activePalette.params?.continuity ?? DEFAULT_CONTINUITY
|
||||
: DEFAULT_CONTINUITY,
|
||||
reverse: false, // restore the reverse flag
|
||||
};
|
||||
|
||||
// we should pass colorStops so that correct calculate new color stops (if there was before) for custom palette
|
||||
const newColorStops = getColorStops(
|
||||
palettes,
|
||||
activePalette.params?.colorStops || [],
|
||||
activePalette,
|
||||
dataBounds
|
||||
);
|
||||
|
||||
if (isNewPaletteCustom) {
|
||||
newParams.colorStops = newColorStops;
|
||||
}
|
||||
|
||||
return {
|
||||
...newPalette,
|
||||
params: {
|
||||
...newParams,
|
||||
stops: getPaletteStops(palettes, newParams, {
|
||||
prevPalette:
|
||||
isNewPaletteCustom || activePalette.name === CUSTOM_PALETTE ? undefined : newPalette.name,
|
||||
dataBounds,
|
||||
mapFromMinValue: true,
|
||||
}),
|
||||
rangeMin: checkIsMinContinuity(newParams.continuity)
|
||||
? Number.NEGATIVE_INFINITY
|
||||
: Math.min(dataBounds.min, newColorStops[0].stop),
|
||||
rangeMax: checkIsMaxContinuity(newParams.continuity)
|
||||
? Number.POSITIVE_INFINITY
|
||||
: Math.min(dataBounds.max, newColorStops[newColorStops.length - 1].stop),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function withUpdatingPalette(
|
||||
palettes: PaletteRegistry,
|
||||
activePalette: PaletteConfigurationState['activePalette'],
|
||||
colorRanges: ColorRange[],
|
||||
dataBounds: DataBounds,
|
||||
continuity?: CustomPaletteParams['continuity']
|
||||
) {
|
||||
const currentContinuity = continuity ?? activePalette.params?.continuity ?? DEFAULT_CONTINUITY;
|
||||
let sortedColorRanges = colorRanges;
|
||||
if (
|
||||
colorRanges.some((value, index) =>
|
||||
index !== colorRanges.length - 1 ? value.start > colorRanges[index + 1].start : false
|
||||
)
|
||||
) {
|
||||
sortedColorRanges = sortColorRanges(colorRanges);
|
||||
}
|
||||
|
||||
const { max, colorStops } = toColorStops(sortedColorRanges, currentContinuity);
|
||||
|
||||
const newPallete = getSwitchToCustomParams(
|
||||
palettes,
|
||||
activePalette!,
|
||||
{
|
||||
continuity: currentContinuity,
|
||||
colorStops,
|
||||
steps: activePalette!.params?.steps || DEFAULT_COLOR_STEPS,
|
||||
reverse: activePalette!.params?.reverse,
|
||||
rangeMin: colorStops[0]?.stop,
|
||||
rangeMax: max,
|
||||
},
|
||||
dataBounds!
|
||||
);
|
||||
|
||||
return {
|
||||
activePalette: newPallete,
|
||||
colorRanges,
|
||||
};
|
||||
}
|
||||
|
||||
export function withUpdatingColorRanges(
|
||||
palettes: PaletteRegistry,
|
||||
activePalette: PaletteConfigurationState['activePalette'],
|
||||
dataBounds: DataBounds
|
||||
) {
|
||||
return {
|
||||
colorRanges: toColorRanges(
|
||||
palettes,
|
||||
activePalette.params?.colorStops || [],
|
||||
activePalette,
|
||||
dataBounds
|
||||
),
|
||||
activePalette,
|
||||
};
|
||||
}
|
||||
|
||||
export function applyPaletteParams<T extends PaletteOutput<CustomPaletteParams>>(
|
||||
palettes: PaletteRegistry,
|
||||
activePalette: T,
|
||||
dataBounds: { min: number; max: number }
|
||||
dataBounds: DataBounds
|
||||
) {
|
||||
// make a copy of it as they have to be manipulated later on
|
||||
const displayStops = getPaletteStops(palettes, activePalette?.params || {}, {
|
||||
|
@ -60,6 +229,7 @@ export function shiftPalette(stops: ColorStop[], max: number) {
|
|||
...entry,
|
||||
stop: i + 1 < array.length ? array[i + 1].stop : max,
|
||||
}));
|
||||
|
||||
if (stops[stops.length - 1].stop === max) {
|
||||
// extends the range by a fair amount to make it work the extra case for the last stop === max
|
||||
const computedStep = getStepValue(stops, result, max) || 1;
|
||||
|
@ -70,6 +240,17 @@ export function shiftPalette(stops: ColorStop[], max: number) {
|
|||
return result;
|
||||
}
|
||||
|
||||
/** @internal **/
|
||||
export function calculateStop(
|
||||
stopValue: number,
|
||||
newMin: number,
|
||||
oldMin: number,
|
||||
oldInterval: number,
|
||||
newInterval: number
|
||||
) {
|
||||
return roundValue(newMin + ((stopValue - oldMin) * newInterval) / oldInterval);
|
||||
}
|
||||
|
||||
// Utility to remap color stops within new domain
|
||||
export function remapStopsByNewInterval(
|
||||
controlStops: ColorStop[],
|
||||
|
@ -83,18 +264,40 @@ export function remapStopsByNewInterval(
|
|||
return (controlStops || []).map(({ color, stop }) => {
|
||||
return {
|
||||
color,
|
||||
stop: newMin + ((stop - oldMin) * newInterval) / oldInterval,
|
||||
stop: calculateStop(stop, newMin, oldMin, oldInterval, newInterval),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getOverallMinMax(
|
||||
params: CustomPaletteParams | undefined,
|
||||
dataBounds: { min: number; max: number }
|
||||
// Utility to remap color stops within new domain
|
||||
export function getStopsFromColorRangesByNewInterval(
|
||||
colorRanges: ColorRange[],
|
||||
{
|
||||
newInterval,
|
||||
oldInterval,
|
||||
newMin,
|
||||
oldMin,
|
||||
}: { newInterval: number; oldInterval: number; newMin: number; oldMin: number }
|
||||
) {
|
||||
return (colorRanges || []).map(({ color, start }) => {
|
||||
let stop = calculateStop(start, newMin, oldMin, oldInterval, newInterval);
|
||||
|
||||
if (oldInterval === 0) {
|
||||
stop = newInterval + newMin;
|
||||
}
|
||||
|
||||
return {
|
||||
color,
|
||||
stop: roundValue(stop),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getOverallMinMax(params: CustomPaletteParams | undefined, dataBounds: DataBounds) {
|
||||
const { min: dataMin, max: dataMax } = getDataMinMax(params?.rangeType, dataBounds);
|
||||
const minStopValue = params?.colorStops?.[0]?.stop ?? Infinity;
|
||||
const maxStopValue = params?.colorStops?.[params.colorStops.length - 1]?.stop ?? -Infinity;
|
||||
const minStopValue = params?.colorStops?.[0]?.stop ?? Number.POSITIVE_INFINITY;
|
||||
const maxStopValue =
|
||||
params?.colorStops?.[params.colorStops.length - 1]?.stop ?? Number.NEGATIVE_INFINITY;
|
||||
const overallMin = Math.min(dataMin, minStopValue);
|
||||
const overallMax = Math.max(dataMax, maxStopValue);
|
||||
return { min: overallMin, max: overallMax };
|
||||
|
@ -102,7 +305,7 @@ function getOverallMinMax(
|
|||
|
||||
export function getDataMinMax(
|
||||
rangeType: CustomPaletteParams['rangeType'] | undefined,
|
||||
dataBounds: { min: number; max: number }
|
||||
dataBounds: DataBounds
|
||||
) {
|
||||
const dataMin = rangeType === 'number' ? dataBounds.min : DEFAULT_MIN_STOP;
|
||||
const dataMax = rangeType === 'number' ? dataBounds.max : DEFAULT_MAX_STOP;
|
||||
|
@ -123,7 +326,7 @@ export function getPaletteStops(
|
|||
defaultPaletteName,
|
||||
}: {
|
||||
prevPalette?: string;
|
||||
dataBounds: { min: number; max: number };
|
||||
dataBounds: DataBounds;
|
||||
mapFromMinValue?: boolean;
|
||||
defaultPaletteName?: string;
|
||||
}
|
||||
|
@ -145,9 +348,9 @@ export function getPaletteStops(
|
|||
)
|
||||
.getCategoricalColors(steps, otherParams);
|
||||
|
||||
const newStopsMin = mapFromMinValue ? minValue : interval / steps;
|
||||
const newStopsMin = mapFromMinValue || interval === 0 ? minValue : interval / steps;
|
||||
|
||||
const stops = remapStopsByNewInterval(
|
||||
return remapStopsByNewInterval(
|
||||
colorStopsFromPredefined.map((color, index) => ({ color, stop: index })),
|
||||
{
|
||||
newInterval: interval,
|
||||
|
@ -156,7 +359,6 @@ export function getPaletteStops(
|
|||
oldMin: 0,
|
||||
}
|
||||
);
|
||||
return stops;
|
||||
}
|
||||
|
||||
export function reversePalette(paletteColorRepresentation: ColorStop[] = []) {
|
||||
|
@ -198,12 +400,8 @@ export function isValidColor(colorString: string) {
|
|||
return colorString !== '' && /^#/.test(colorString) && isValidPonyfill(colorString);
|
||||
}
|
||||
|
||||
export function roundStopValues(colorStops: ColorStop[]) {
|
||||
return colorStops.map(({ color, stop }) => {
|
||||
// when rounding mind to not go in excess, rather use the floor function
|
||||
const roundedStop = Number((Math.floor(stop * 100) / 100).toFixed(2));
|
||||
return { color, stop: roundedStop };
|
||||
});
|
||||
export function roundValue(value: number, fractionDigits: number = 2) {
|
||||
return Number((Math.floor(value * 100) / 100).toFixed(fractionDigits));
|
||||
}
|
||||
|
||||
// very simple heuristic: pick last two stops and compute a new stop based on the same distance
|
||||
|
@ -227,7 +425,7 @@ export function getSwitchToCustomParams(
|
|||
palettes: PaletteRegistry,
|
||||
activePalette: PaletteOutput<CustomPaletteParams>,
|
||||
newParams: CustomPaletteParams,
|
||||
dataBounds: { min: number; max: number }
|
||||
dataBounds: DataBounds
|
||||
) {
|
||||
// if it's already a custom palette just return the params
|
||||
if (activePalette?.params?.name === CUSTOM_PALETTE) {
|
||||
|
@ -272,7 +470,7 @@ export function getColorStops(
|
|||
palettes: PaletteRegistry,
|
||||
colorStops: Required<CustomPaletteParams>['stops'],
|
||||
activePalette: PaletteOutput<CustomPaletteParams>,
|
||||
dataBounds: { min: number; max: number }
|
||||
dataBounds: DataBounds
|
||||
) {
|
||||
// just forward the current stops if custom
|
||||
if (activePalette?.name === CUSTOM_PALETTE && colorStops?.length) {
|
||||
|
@ -293,6 +491,47 @@ export function getColorStops(
|
|||
return freshColorStops;
|
||||
}
|
||||
|
||||
/**
|
||||
* Both table coloring logic and EuiPaletteDisplay format implementation works differently than our current `colorStops`,
|
||||
* by having the stop values at the end of each color segment rather than at the beginning: `stops` values are computed by a rightShift of `colorStops`.
|
||||
* EuiPaletteDisplay has an additional requirement as it is always mapped against a domain [0, N]: from `stops` the `displayStops` are computed with
|
||||
* some continuity enrichment and a remap against a [0, 100] domain to make the palette component work ok.
|
||||
*
|
||||
* These naming conventions would be useful to track the code flow in this feature as multiple transformations are happening
|
||||
* for a single change.
|
||||
*/
|
||||
export function toColorRanges(
|
||||
palettes: PaletteRegistry,
|
||||
colorStops: CustomPaletteParams['colorStops'],
|
||||
activePalette: PaletteOutput<CustomPaletteParams>,
|
||||
dataBounds: DataBounds
|
||||
) {
|
||||
const {
|
||||
continuity = defaultPaletteParams.continuity,
|
||||
rangeType = defaultPaletteParams.rangeType,
|
||||
} = activePalette.params ?? {};
|
||||
const { min: dataMin, max: dataMax } = getDataMinMax(rangeType, dataBounds);
|
||||
|
||||
return getColorStops(palettes, colorStops || [], activePalette, dataBounds).map(
|
||||
(colorStop, index, array) => {
|
||||
const isFirst = index === 0;
|
||||
const isLast = index === array.length - 1;
|
||||
|
||||
return {
|
||||
color: colorStop.color,
|
||||
start:
|
||||
isFirst && checkIsMinContinuity(continuity)
|
||||
? Number.NEGATIVE_INFINITY
|
||||
: colorStop.stop ?? activePalette.params?.rangeMin ?? dataMin,
|
||||
end:
|
||||
isLast && checkIsMaxContinuity(continuity)
|
||||
? Number.POSITIVE_INFINITY
|
||||
: array[index + 1]?.stop ?? activePalette.params?.rangeMax ?? dataMax,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function getContrastColor(
|
||||
color: string,
|
||||
isDarkTheme: boolean,
|
||||
|
@ -312,27 +551,6 @@ export function getContrastColor(
|
|||
return isColorDark(...finalColor.rgb()) ? lightColor : darkColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as stops, but remapped against a range 0-100
|
||||
*/
|
||||
export function getStopsForFixedMode(stops: ColorStop[], colorStops?: ColorStop[]) {
|
||||
const referenceStops =
|
||||
colorStops || stops.map(({ color }, index) => ({ color, stop: 20 * index }));
|
||||
const fallbackStops = stops;
|
||||
|
||||
// what happens when user set two stops with the same value? we'll fallback to the display interval
|
||||
const oldInterval =
|
||||
referenceStops[referenceStops.length - 1].stop - referenceStops[0].stop ||
|
||||
fallbackStops[fallbackStops.length - 1].stop - fallbackStops[0].stop;
|
||||
|
||||
return remapStopsByNewInterval(stops, {
|
||||
newInterval: 100,
|
||||
oldInterval,
|
||||
newMin: 0,
|
||||
oldMin: referenceStops[0].stop,
|
||||
});
|
||||
}
|
||||
|
||||
function getId(id: string) {
|
||||
return id;
|
||||
}
|
||||
|
@ -344,17 +562,35 @@ export function getNumericValue(rowValue: number | number[] | undefined) {
|
|||
return rowValue;
|
||||
}
|
||||
|
||||
export const getFallbackDataBounds = (
|
||||
rangeType: CustomPaletteParams['rangeType'] = 'percent'
|
||||
): DataBounds =>
|
||||
rangeType === 'percent'
|
||||
? {
|
||||
min: 0,
|
||||
max: 100,
|
||||
fallback: true,
|
||||
}
|
||||
: {
|
||||
min: 1,
|
||||
max: 1,
|
||||
fallback: true,
|
||||
};
|
||||
|
||||
export const findMinMaxByColumnId = (
|
||||
columnIds: string[],
|
||||
table: Datatable | undefined,
|
||||
getOriginalId: (id: string) => string = getId
|
||||
) => {
|
||||
const minMax: Record<string, { min: number; max: number; fallback?: boolean }> = {};
|
||||
const minMax: Record<string, DataBounds> = {};
|
||||
|
||||
if (table != null) {
|
||||
for (const columnId of columnIds) {
|
||||
const originalId = getOriginalId(columnId);
|
||||
minMax[originalId] = minMax[originalId] || { max: -Infinity, min: Infinity };
|
||||
minMax[originalId] = minMax[originalId] || {
|
||||
max: Number.NEGATIVE_INFINITY,
|
||||
min: Number.POSITIVE_INFINITY,
|
||||
};
|
||||
table.rows.forEach((row) => {
|
||||
const rowValue = row[columnId];
|
||||
const numericValue = getNumericValue(rowValue);
|
||||
|
@ -368,8 +604,8 @@ export const findMinMaxByColumnId = (
|
|||
}
|
||||
});
|
||||
// what happens when there's no data in the table? Fallback to a percent range
|
||||
if (minMax[originalId].max === -Infinity) {
|
||||
minMax[originalId] = { max: 100, min: 0, fallback: true };
|
||||
if (minMax[originalId].max === Number.NEGATIVE_INFINITY) {
|
||||
minMax[originalId] = getFallbackDataBounds();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,6 @@ import {
|
|||
CustomizablePalette,
|
||||
CUSTOM_PALETTE,
|
||||
FIXED_PROGRESSION,
|
||||
getStopsForFixedMode,
|
||||
PalettePanelContainer,
|
||||
TooltipWrapper,
|
||||
} from '../../shared_components/';
|
||||
|
@ -70,6 +69,7 @@ export function GaugeDimensionEditor(
|
|||
name: defaultPaletteParams.name,
|
||||
params: {
|
||||
...defaultPaletteParams,
|
||||
continuity: 'all',
|
||||
colorStops: undefined,
|
||||
stops: undefined,
|
||||
rangeMin: currentMinMax.min,
|
||||
|
@ -141,14 +141,7 @@ export function GaugeDimensionEditor(
|
|||
<EuiFlexItem>
|
||||
<EuiColorPaletteDisplay
|
||||
data-test-subj="lnsGauge_dynamicColoring_palette"
|
||||
palette={
|
||||
activePalette.params?.name === CUSTOM_PALETTE
|
||||
? getStopsForFixedMode(
|
||||
activePalette.params.stops!,
|
||||
activePalette.params.colorStops
|
||||
)
|
||||
: displayStops.map(({ color }) => color)
|
||||
}
|
||||
palette={displayStops.map(({ color }) => color)}
|
||||
type={FIXED_PROGRESSION}
|
||||
onClick={togglePalette}
|
||||
/>
|
||||
|
@ -174,7 +167,7 @@ export function GaugeDimensionEditor(
|
|||
palettes={props.paletteService}
|
||||
activePalette={activePalette}
|
||||
dataBounds={currentMinMax}
|
||||
showContinuity={false}
|
||||
disableSwitchingContinuity={true}
|
||||
setPalette={(newPalette) => {
|
||||
// if the new palette is not custom, replace the rangeMin with the artificial one
|
||||
if (
|
||||
|
|
|
@ -29,7 +29,7 @@ import type { DatasourcePublicAPI, OperationMetadata, Visualization } from '../.
|
|||
import { getSuggestions } from './suggestions';
|
||||
import { GROUP_ID, LENS_GAUGE_ID, GaugeVisualizationState } from './constants';
|
||||
import { GaugeToolbar } from './toolbar_component';
|
||||
import { applyPaletteParams, CUSTOM_PALETTE, getStopsForFixedMode } from '../../shared_components';
|
||||
import { applyPaletteParams, CUSTOM_PALETTE } from '../../shared_components';
|
||||
import { GaugeDimensionEditor } from './dimension_editor';
|
||||
import { CustomPaletteParams, layerTypes } from '../../../common';
|
||||
import { generateId } from '../../id_generator';
|
||||
|
@ -223,7 +223,7 @@ export const getGaugeVisualization = ({
|
|||
const currentMinMax = { min: getMinValue(row, state), max: getMaxValue(row, state) };
|
||||
|
||||
const displayStops = applyPaletteParams(paletteService, state?.palette, currentMinMax);
|
||||
palette = getStopsForFixedMode(displayStops, state?.palette?.params?.colorStops);
|
||||
palette = displayStops.map(({ color }) => color);
|
||||
}
|
||||
const invalidProps = checkInvalidConfiguration(row, state) || {};
|
||||
|
||||
|
|
|
@ -224,12 +224,7 @@
|
|||
"xpack.lens.dragDrop.keyboardInstructionsReorder": "スペースまたはEnterを押してドラッグを開始します。ドラッグするときには、上下矢印キーを使用すると、グループの項目を並べ替えます。左右矢印キーを使用すると、グループの外側でドロップ対象を選択します。もう一度スペースまたはEnterを押すと終了します。",
|
||||
"xpack.lens.dragDrop.shift": "Shift",
|
||||
"xpack.lens.dragDrop.swap": "入れ替える",
|
||||
"xpack.lens.dynamicColoring.customPalette.addColorStop": "色経由点を追加",
|
||||
"xpack.lens.dynamicColoring.customPalette.deleteButtonAriaLabel": "削除",
|
||||
"xpack.lens.dynamicColoring.customPalette.deleteButtonDisabled": "2つ以上の経由点が必要であるため、この色経由点を削除することはできません",
|
||||
"xpack.lens.dynamicColoring.customPalette.deleteButtonLabel": "削除",
|
||||
"xpack.lens.dynamicColoring.customPalette.sortReason": "新しい経由値{value}のため、色経由点が並べ替えられました",
|
||||
"xpack.lens.dynamicColoring.customPalette.stopAriaLabel": "{index}を停止",
|
||||
"xpack.lens.editorFrame.buildExpressionError": "グラフの準備中に予期しないエラーが発生しました",
|
||||
"xpack.lens.editorFrame.colorIndicatorLabel": "このディメンションの色:{hex}",
|
||||
"xpack.lens.editorFrame.dataFailure": "データの読み込み中にエラーが発生しました。",
|
||||
|
@ -712,20 +707,12 @@
|
|||
"xpack.lens.table.columnVisibilityLabel": "列を非表示",
|
||||
"xpack.lens.table.defaultAriaLabel": "データ表ビジュアライゼーション",
|
||||
"xpack.lens.table.dynamicColoring.cell": "セル",
|
||||
"xpack.lens.table.dynamicColoring.continuity.aboveLabel": "範囲の上",
|
||||
"xpack.lens.table.dynamicColoring.continuity.allLabel": "範囲の上下",
|
||||
"xpack.lens.table.dynamicColoring.continuity.belowLabel": "範囲の下",
|
||||
"xpack.lens.table.dynamicColoring.continuity.label": "色の連続",
|
||||
"xpack.lens.table.dynamicColoring.continuity.noneLabel": "範囲内",
|
||||
"xpack.lens.table.dynamicColoring.customPalette.colorStopsHelpPercentage": "割合値は使用可能なデータ値の全範囲に対して相対的です。",
|
||||
"xpack.lens.table.dynamicColoring.customPalette.colorStopsLabel": "色経由点",
|
||||
"xpack.lens.table.dynamicColoring.customPalette.continuityHelp": "最初の色経由点の前、最後の色経由点の後に色が表示される方法を指定します。",
|
||||
"xpack.lens.table.dynamicColoring.label": "値別の色",
|
||||
"xpack.lens.table.dynamicColoring.none": "なし",
|
||||
"xpack.lens.table.dynamicColoring.rangeType.label": "値型",
|
||||
"xpack.lens.table.dynamicColoring.rangeType.number": "数字",
|
||||
"xpack.lens.table.dynamicColoring.rangeType.percent": "割合(%)",
|
||||
"xpack.lens.table.dynamicColoring.reverse.label": "色を反転",
|
||||
"xpack.lens.table.dynamicColoring.text": "テキスト",
|
||||
"xpack.lens.table.hide.hideLabel": "非表示",
|
||||
"xpack.lens.table.palettePanelContainer.back": "戻る",
|
||||
|
|
|
@ -228,12 +228,7 @@
|
|||
"xpack.lens.dragDrop.keyboardInstructionsReorder": "按空格键或 enter 键开始拖动。拖动时,请使用上下箭头键重新排列组中的项目,使用左右箭头键在组之外选择拖动目标。再次按空格键或 enter 键结束操作。",
|
||||
"xpack.lens.dragDrop.shift": "Shift 键",
|
||||
"xpack.lens.dragDrop.swap": "交换",
|
||||
"xpack.lens.dynamicColoring.customPalette.addColorStop": "添加颜色停止",
|
||||
"xpack.lens.dynamicColoring.customPalette.deleteButtonAriaLabel": "删除",
|
||||
"xpack.lens.dynamicColoring.customPalette.deleteButtonDisabled": "此颜色停止无法删除,因为需要两个或更多停止",
|
||||
"xpack.lens.dynamicColoring.customPalette.deleteButtonLabel": "删除",
|
||||
"xpack.lens.dynamicColoring.customPalette.sortReason": "由于新停止值 {value},颜色停止已排序",
|
||||
"xpack.lens.dynamicColoring.customPalette.stopAriaLabel": "停止 {index}",
|
||||
"xpack.lens.editorFrame.buildExpressionError": "准备图表时发生意外错误",
|
||||
"xpack.lens.editorFrame.colorIndicatorLabel": "此维度的颜色:{hex}",
|
||||
"xpack.lens.editorFrame.configurationFailureMoreErrors": " +{errors} 个{errors, plural, other {错误}}",
|
||||
|
@ -724,20 +719,12 @@
|
|||
"xpack.lens.table.columnVisibilityLabel": "隐藏列",
|
||||
"xpack.lens.table.defaultAriaLabel": "数据表可视化",
|
||||
"xpack.lens.table.dynamicColoring.cell": "单元格",
|
||||
"xpack.lens.table.dynamicColoring.continuity.aboveLabel": "高于范围",
|
||||
"xpack.lens.table.dynamicColoring.continuity.allLabel": "高于和低于范围",
|
||||
"xpack.lens.table.dynamicColoring.continuity.belowLabel": "低于范围",
|
||||
"xpack.lens.table.dynamicColoring.continuity.label": "颜色连续性",
|
||||
"xpack.lens.table.dynamicColoring.continuity.noneLabel": "范围内",
|
||||
"xpack.lens.table.dynamicColoring.customPalette.colorStopsHelpPercentage": "百分比值是相对于全范围可用数据值的类型。",
|
||||
"xpack.lens.table.dynamicColoring.customPalette.colorStopsLabel": "颜色停止",
|
||||
"xpack.lens.table.dynamicColoring.customPalette.continuityHelp": "指定颜色在第一个颜色停止之前和最后一个颜色停止之后的出现方式。",
|
||||
"xpack.lens.table.dynamicColoring.label": "按值上色",
|
||||
"xpack.lens.table.dynamicColoring.none": "无",
|
||||
"xpack.lens.table.dynamicColoring.rangeType.label": "值类型",
|
||||
"xpack.lens.table.dynamicColoring.rangeType.number": "数字",
|
||||
"xpack.lens.table.dynamicColoring.rangeType.percent": "百分比",
|
||||
"xpack.lens.table.dynamicColoring.reverse.label": "反转颜色",
|
||||
"xpack.lens.table.dynamicColoring.text": "文本",
|
||||
"xpack.lens.table.hide.hideLabel": "隐藏",
|
||||
"xpack.lens.table.palettePanelContainer.back": "返回",
|
||||
|
|
|
@ -73,7 +73,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await PageObjects.lens.openDimensionEditor('lnsHeatmap_cellPanel > lns-dimensionTrigger');
|
||||
await PageObjects.lens.openPalettePanel('lnsHeatmap');
|
||||
await retry.try(async () => {
|
||||
await testSubjects.setValue('lnsPalettePanel_dynamicColoring_stop_value_0', '10', {
|
||||
await testSubjects.setValue('lnsPalettePanel_dynamicColoring_range_value_0', '10', {
|
||||
clearWithKeyboard: true,
|
||||
typeCharByChar: true,
|
||||
});
|
||||
|
@ -108,16 +108,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
// assert legend has changed
|
||||
expect(debugState.legend!.items).to.eql([
|
||||
{ key: '7,126 - 8,529.22', name: '7,126 - 8,529.22', color: '#6092c0' },
|
||||
{ key: '8,529.22 - 11,335.66', name: '8,529.22 - 11,335.66', color: '#a8bfda' },
|
||||
{ key: '11,335.66 - 14,142.11', name: '11,335.66 - 14,142.11', color: '#ebeff5' },
|
||||
{ key: '14,142.11 - 16,948.55', name: '14,142.11 - 16,948.55', color: '#ecb385' },
|
||||
{ key: '≥ 16,948.55', name: '≥ 16,948.55', color: '#e7664c' },
|
||||
{ key: '7,125.99 - 8,529.2', name: '7,125.99 - 8,529.2', color: '#6092c0' },
|
||||
{ key: '8,529.2 - 11,335.66', name: '8,529.2 - 11,335.66', color: '#a8bfda' },
|
||||
{ key: '11,335.66 - 14,142.1', name: '11,335.66 - 14,142.1', color: '#ebeff5' },
|
||||
{ key: '14,142.1 - 16,948.55', name: '14,142.1 - 16,948.55', color: '#ecb385' },
|
||||
{
|
||||
color: '#e7664c',
|
||||
key: '≥ 16,948.55',
|
||||
name: '≥ 16,948.55',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should reflect stop changes when in number to the chart', async () => {
|
||||
await testSubjects.setValue('lnsPalettePanel_dynamicColoring_stop_value_0', '0', {
|
||||
await testSubjects.setValue('lnsPalettePanel_dynamicColoring_range_value_0', '0', {
|
||||
clearWithKeyboard: true,
|
||||
});
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
@ -130,8 +134,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
// assert legend has changed
|
||||
expect(debugState.legend!.items).to.eql([
|
||||
{ key: '0 - 8,529.21', name: '0 - 8,529.21', color: '#6092c0' },
|
||||
{ key: '8,529.21 - 11,335.66', name: '8,529.21 - 11,335.66', color: '#a8bfda' },
|
||||
{ key: '0 - 8,529.2', name: '0 - 8,529.2', color: '#6092c0' },
|
||||
{ key: '8,529.2 - 11,335.66', name: '8,529.2 - 11,335.66', color: '#a8bfda' },
|
||||
{ key: '11,335.66 - 14,142.1', name: '11,335.66 - 14,142.1', color: '#ebeff5' },
|
||||
{ key: '14,142.1 - 16,948.55', name: '14,142.1 - 16,948.55', color: '#ecb385' },
|
||||
{ key: '≥ 16,948.55', name: '≥ 16,948.55', color: '#e7664c' },
|
||||
|
|
|
@ -34,7 +34,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
it('should change the color of the metric when tweaking the values in the panel', async () => {
|
||||
await PageObjects.lens.openPalettePanel('lnsMetric');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await testSubjects.setValue('lnsPalettePanel_dynamicColoring_stop_value_1', '21000', {
|
||||
await testSubjects.setValue('lnsPalettePanel_dynamicColoring_range_value_1', '21000', {
|
||||
clearWithKeyboard: true,
|
||||
});
|
||||
await PageObjects.lens.waitForVisualization();
|
||||
|
@ -43,7 +43,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should change the color when reverting the palette', async () => {
|
||||
await testSubjects.click('lnsPalettePanel_dynamicColoring_reverse');
|
||||
await testSubjects.click('lnsPalettePanel_dynamicColoring_reverseColors');
|
||||
await PageObjects.lens.waitForVisualization();
|
||||
const styleObj = await PageObjects.lens.getMetricStyle();
|
||||
expect(styleObj.color).to.be('rgb(204, 86, 66)');
|
||||
|
|
|
@ -144,11 +144,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await PageObjects.lens.changePaletteTo('temperature');
|
||||
await testSubjects.click('lnsPalettePanel_dynamicColoring_rangeType_groups_percent');
|
||||
// now tweak the value
|
||||
await testSubjects.setValue('lnsPalettePanel_dynamicColoring_stop_value_0', '30', {
|
||||
await testSubjects.setValue('lnsPalettePanel_dynamicColoring_range_value_0', '30', {
|
||||
clearWithKeyboard: true,
|
||||
});
|
||||
// when clicking on another row will trigger a sorting + update
|
||||
await testSubjects.click('lnsPalettePanel_dynamicColoring_stop_value_1');
|
||||
await testSubjects.click('lnsPalettePanel_dynamicColoring_range_value_1');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
// pick a cell without color as is below the range
|
||||
const styleObj = await PageObjects.lens.getDatatableCellStyle(3, 3);
|
||||
|
@ -158,7 +158,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should allow the user to reverse the palette', async () => {
|
||||
await testSubjects.click('lnsPalettePanel_dynamicColoring_reverse');
|
||||
await testSubjects.click('lnsPalettePanel_dynamicColoring_reverseColors');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
const styleObj = await PageObjects.lens.getDatatableCellStyle(1, 1);
|
||||
expect(styleObj['background-color']).to.be('rgb(168, 191, 218)');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue