[Lens] Color stop UI follow-ups (#123924)

* Round min/max values when we are turning off auto auto mode

* Fix the behavior so that we don't see the warning "The specified value '-Infinity' cannot be parsed or is out of range."  in console.

* Allow to use correct max value in percentage mode for heatmap

* Change the wording of the `Add color range` button to `Add color` and `Distribute equally` button to `Distribute values`

* Don't show percent symbol append for the EuiFieldText components that are set to be the current data's min or max value

* Adds  tooltips for the min, max, and pencil icon buttons

* Add support of changing the opacity

* Add tooltips for disabled color range action buttons (Reverse colors, Distribute values)

* Change the `Maximum value should be greater than preceding values` error message to be `Maximum value must be greater than preceding values`

* Apply sorting if min on mid value more than max instead of showing error

* Fix test

* Change increment by +1 for adding new color ranges

* Fix CI

* Allow customize max/min values in color ranges for gauge

* Fix CI

* Change Outside Data Bounds Message from warning to info type

* Remove warnings about value outside databounds

* Fix Checks

* Add tests

* Fix some remarks

* Update x-pack/plugins/lens/public/shared_components/coloring/color_ranges/color_ranges_item_buttons.tsx

Co-authored-by: Marta Bondyra <marta.bondyra@gmail.com>

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Marta Bondyra <marta.bondyra@gmail.com>
This commit is contained in:
Uladzislau Lasitsa 2022-02-02 14:40:42 +03:00 committed by GitHub
parent a5ee534bad
commit e5a8cec25e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 233 additions and 310 deletions

View file

@ -212,7 +212,7 @@ describe('GaugeComponent', function () {
stops: [10, 20, 30] as unknown as ColorStop[],
range: 'number',
rangeMin: 0,
rangeMax: 20,
rangeMax: 30,
},
};
const customProps = {
@ -253,8 +253,8 @@ describe('GaugeComponent', function () {
},
} as GaugeRenderProps;
const goal = shallowWithIntl(<GaugeComponent {...customProps} />).find(Goal);
expect(goal.prop('ticks')).toEqual([0, 1, 2, 3, 10]);
expect(goal.prop('bands')).toEqual([0, 1, 2, 3, 10]);
expect(goal.prop('ticks')).toEqual([0, 1, 2, 3, 4, 10]);
expect(goal.prop('bands')).toEqual([0, 1, 2, 3, 4, 10]);
});
it('sets proper color bands and ticks on color bands if palette steps are smaller than minimum', () => {
const palette = {
@ -281,8 +281,8 @@ describe('GaugeComponent', function () {
},
} as GaugeRenderProps;
const goal = shallowWithIntl(<GaugeComponent {...customProps} />).find(Goal);
expect(goal.prop('ticks')).toEqual([0, 10]);
expect(goal.prop('bands')).toEqual([0, 10]);
expect(goal.prop('ticks')).toEqual([0, 4, 10]);
expect(goal.prop('bands')).toEqual([0, 4, 10]);
});
it('sets proper color bands and ticks on color bands if percent palette steps are smaller than 0', () => {
const palette = {
@ -294,7 +294,7 @@ describe('GaugeComponent', function () {
stops: [-20, -60, 80],
range: 'percent',
rangeMin: 0,
rangeMax: 4,
rangeMax: 100,
},
};
const customProps = {
@ -407,7 +407,7 @@ describe('GaugeComponent', function () {
stops: [20, 60, 80],
range: 'percent',
rangeMin: 0,
rangeMax: 10,
rangeMax: 100,
},
};
const customProps = {

View file

@ -29,7 +29,11 @@ declare global {
}
}
function normalizeColors({ colors, stops, range }: CustomPaletteState, min: number) {
function normalizeColors(
{ colors, stops, range, rangeMin, rangeMax }: CustomPaletteState,
min: number,
max: number
) {
if (!colors) {
return;
}
@ -37,23 +41,61 @@ function normalizeColors({ colors, stops, range }: CustomPaletteState, min: numb
stops.filter((stop, i) => (range === 'percent' ? stop < 0 : stop < min)).length,
0
);
return colors.slice(colorsOutOfRangeSmaller);
let updatedColors = colors.slice(colorsOutOfRangeSmaller);
let correctMin = rangeMin;
let correctMax = rangeMax;
if (range === 'percent') {
correctMin = min + rangeMin * ((max - min) / 100);
correctMax = min + rangeMax * ((max - min) / 100);
}
if (correctMin > min && isFinite(correctMin)) {
updatedColors = [`rgba(255,255,255,0)`, ...updatedColors];
}
if (correctMax < max && isFinite(correctMax)) {
updatedColors = [...updatedColors, `rgba(255,255,255,0)`];
}
return updatedColors;
}
function normalizeBands(
{ colors, stops, range }: CustomPaletteState,
{ colors, stops, range, rangeMax, rangeMin }: CustomPaletteState,
{ min, max }: { min: number; max: number }
) {
if (!stops.length) {
const step = (max - min) / colors.length;
return [min, ...colors.map((_, i) => min + (i + 1) * step)];
}
let firstRanges = [min];
let lastRanges = [max];
let correctMin = rangeMin;
let correctMax = rangeMax;
if (range === 'percent') {
const filteredStops = stops.filter((stop) => stop >= 0 && stop <= 100);
return [min, ...filteredStops.map((step) => min + step * ((max - min) / 100)), max];
correctMin = min + rangeMin * ((max - min) / 100);
correctMax = min + rangeMax * ((max - min) / 100);
}
if (correctMin > min && isFinite(correctMin)) {
firstRanges = [min, correctMin];
}
if (correctMax < max && isFinite(correctMax)) {
lastRanges = [correctMax, max];
}
if (range === 'percent') {
const filteredStops = stops.filter((stop) => stop > 0 && stop < 100);
return [
...firstRanges,
...filteredStops.map((step) => min + step * ((max - min) / 100)),
...lastRanges,
];
}
const orderedStops = stops.filter((stop, i) => stop < max && stop > min);
return [min, ...orderedStops, max];
return [...firstRanges, ...orderedStops, ...lastRanges];
}
function getTitle(
@ -179,7 +221,7 @@ export const GaugeComponent: FC<GaugeRenderProps> = memo(
},
}
);
const colors = palette?.params?.colors ? normalizeColors(palette.params, min) : undefined;
const colors = palette?.params?.colors ? normalizeColors(palette.params, min, max) : undefined;
const bands: number[] = (palette?.params as CustomPaletteState)
? normalizeBands(args.palette?.params as CustomPaletteState, { min, max })
: [min, max];
@ -193,8 +235,8 @@ export const GaugeComponent: FC<GaugeRenderProps> = memo(
<Goal
id="goal"
subtype={subtype}
base={min}
target={goal && goal >= min && goal <= max ? goal : undefined}
base={bands[0]}
target={goal && goal >= bands[0] && goal <= bands[bands.length - 1] ? goal : undefined}
actual={formattedActual}
tickValueFormatter={({ value: tickValue }) => tickFormatter.convert(tickValue)}
bands={bands}
@ -205,6 +247,8 @@ export const GaugeComponent: FC<GaugeRenderProps> = memo(
const index = bands && bands.indexOf(val.value) - 1;
return colors && index >= 0 && colors[index]
? colors[index]
: val.value <= bands[0]
? colors[0]
: colors[colors.length - 1];
}
: () => `rgba(255,255,255,0)`

View file

@ -271,8 +271,13 @@ 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?.range === 'number' ? paletteParams.rangeMax : max) + smattering;
let endValue = max + smattering;
if (paletteParams?.rangeMax || paletteParams?.rangeMax === 0) {
endValue =
(paletteParams?.range === 'number'
? paletteParams.rangeMax
: min + ((max - min) * paletteParams.rangeMax) / 100) + smattering;
}
const overwriteColors = uiState?.get('vis.colors') ?? null;

View file

@ -14,18 +14,18 @@ import { PaletteRegistry } from 'src/plugins/charts/public';
import { ColorRangesContext } from './color_ranges_context';
const extraActionSelectors = {
addColorRange: '[data-test-subj^="lnsPalettePanel_dynamicColoring_addColorRange"]',
addColor: '[data-test-subj^="lnsPalettePanel_dynamicColoring_addColor"]',
reverseColors: '[data-test-subj^="lnsPalettePanel_dynamicColoring_reverseColors"]',
distributeEqually: '[data-test-subj="lnsPalettePanel_dynamicColoring_distributeEqually"]',
distributeValues: '[data-test-subj="lnsPalettePanel_dynamicColoring_distributeValues"]',
};
const pageObjects = {
getAddColorRangeButton: (component: ReactWrapper) =>
component.find(extraActionSelectors.addColorRange).first(),
component.find(extraActionSelectors.addColor).first(),
reverseColors: (component: ReactWrapper) =>
component.find(extraActionSelectors.reverseColors).first(),
distributeEqually: (component: ReactWrapper) =>
component.find(extraActionSelectors.distributeEqually).first(),
distributeValues: (component: ReactWrapper) =>
component.find(extraActionSelectors.distributeValues).first(),
};
function renderColorRanges(props: ColorRangesProps) {
@ -142,7 +142,7 @@ describe('Color Ranges', () => {
});
});
it('should distribute equally ranges when use click on "Distribute equally" button', () => {
it('should distribute equally ranges when use click on "Distribute values" button', () => {
props.colorRanges = [
{ color: '#aaa', start: 0, end: 2 },
{ color: '#bbb', start: 3, end: 4 },
@ -153,7 +153,7 @@ describe('Color Ranges', () => {
const component = renderColorRanges(props);
act(() => {
pageObjects.distributeEqually(component).simulate('click');
pageObjects.distributeValues(component).simulate('click');
});
component.update();

View file

@ -4,7 +4,7 @@
* 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 React, { useState, useEffect, Dispatch } from 'react';
import { EuiFlexGroup, EuiTextColor, EuiFlexItem } from '@elastic/eui';
@ -22,8 +22,6 @@ import type { PaletteConfigurationActions } from '../types';
import { defaultPaletteParams } from '../constants';
import { ColorRangesContext } from './color_ranges_context';
export interface ColorRangesProps {
colorRanges: ColorRange[];
paletteConfiguration: CustomPaletteParamsConfig | undefined;
@ -37,7 +35,6 @@ export function ColorRanges({
showExtraActions = true,
dispatch,
}: ColorRangesProps) {
const { dataBounds } = useContext(ColorRangesContext);
const [colorRangesValidity, setColorRangesValidity] = useState<
Record<string, ColorRangeValidation>
>({});
@ -48,8 +45,8 @@ export function ColorRanges({
const rangeType = paletteConfiguration?.rangeType ?? defaultPaletteParams.rangeType;
useEffect(() => {
setColorRangesValidity(validateColorRanges(colorRanges, dataBounds, rangeType));
}, [colorRanges, rangeType, dataBounds]);
setColorRangesValidity(validateColorRanges(colorRanges));
}, [colorRanges]);
return (
<EuiFlexGroup

View file

@ -40,10 +40,17 @@ export function ColorRangesExtraActions({
dispatch({ type: 'reversePalette', payload: { dataBounds, palettes } });
}, [dispatch, dataBounds, palettes]);
const onDistributeEqually = useCallback(() => {
const onDistributeValues = useCallback(() => {
dispatch({ type: 'distributeEqually', payload: { dataBounds, palettes } });
}, [dataBounds, dispatch, palettes]);
const oneColorRangeWarn = i18n.translate(
'xpack.lens.dynamicColoring.customPalette.oneColorRange',
{
defaultMessage: `Requires more than one color`,
}
);
return (
<EuiFlexGroup justifyContent="flexStart" gutterSize="none" wrap={true}>
<EuiFlexItem grow={false}>
@ -59,13 +66,13 @@ export function ColorRangesExtraActions({
delay="regular"
>
<EuiButtonEmpty
data-test-subj={`lnsPalettePanel_dynamicColoring_addColorRange`}
data-test-subj={`lnsPalettePanel_dynamicColoring_addColor`}
iconType="plusInCircle"
color="primary"
aria-label={i18n.translate(
'xpack.lens.dynamicColoring.customPalette.addColorRangeAriaLabel',
'xpack.lens.dynamicColoring.customPalette.addColorAriaLabel',
{
defaultMessage: 'Add color range',
defaultMessage: 'Add color',
}
)}
size="xs"
@ -74,52 +81,66 @@ export function ColorRangesExtraActions({
onClick={onAddColorRange}
>
<FormattedMessage
id="xpack.lens.dynamicColoring.customPalette.addColorRange"
defaultMessage="Add color range"
id="xpack.lens.dynamicColoring.customPalette.addColor"
defaultMessage="Add color"
/>
</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}
<TooltipWrapper
tooltipContent={oneColorRangeWarn}
condition={shouldDisableReverse}
position="top"
delay="regular"
>
<FormattedMessage
id="xpack.lens.dynamicColoring.customPalette.reverseColors"
defaultMessage="Reverse colors"
/>
</EuiButtonEmpty>
<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>
</TooltipWrapper>
</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}
<TooltipWrapper
tooltipContent={oneColorRangeWarn}
condition={shouldDisableDistribute}
position="top"
delay="regular"
>
<FormattedMessage
id="xpack.lens.dynamicColoring.customPalette.distributeEqually"
defaultMessage="Distribute equally"
/>
</EuiButtonEmpty>
<EuiButtonEmpty
data-test-subj={`lnsPalettePanel_dynamicColoring_distributeValues`}
iconType={DistributeEquallyIcon}
color="primary"
aria-label={i18n.translate(
'xpack.lens.dynamicColoring.customPalette.distributeValuesAriaLabel',
{
defaultMessage: 'Distribute values',
}
)}
size="xs"
flush="left"
disabled={shouldDisableDistribute}
onClick={onDistributeValues}
>
<FormattedMessage
id="xpack.lens.dynamicColoring.customPalette.distributeValues"
defaultMessage="Distribute values"
/>
</EuiButtonEmpty>
</TooltipWrapper>
</EuiFlexItem>
</EuiFlexGroup>
);

View file

@ -17,7 +17,6 @@ import {
EuiIcon,
EuiColorPickerSwatch,
EuiButtonIcon,
EuiToolTip,
EuiFieldNumberProps,
} from '@elastic/eui';
@ -39,7 +38,6 @@ import {
checkIsMaxContinuity,
checkIsMinContinuity,
} from '../../../../../../../src/plugins/charts/common';
import { getOutsideDataBoundsWarningMessage } from './color_ranges_validation';
export interface ColorRangesItemProps {
colorRange: ColorRange;
@ -81,25 +79,13 @@ const getActionButton = (mode: ColorRangeItemMode) => {
return mode === 'edit' ? ColorRangeAutoDetectButton : ColorRangeEditButton;
};
const getAppend = (
rangeType: CustomPaletteParams['rangeType'],
mode: ColorRangeItemMode,
validation?: ColorRangeValidation
) => {
const getAppend = (rangeType: CustomPaletteParams['rangeType'], mode: ColorRangeItemMode) => {
const items: EuiFieldNumberProps['append'] = [];
if (rangeType === 'percent') {
if (rangeType === 'percent' && mode !== 'auto') {
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;
};
@ -127,8 +113,12 @@ export function ColorRangeItem({
(e: FocusEvent<HTMLDivElement>) => {
const prevStartValue = colorRanges[index - 1]?.start ?? Number.NEGATIVE_INFINITY;
const nextStartValue = colorRanges[index + 1]?.start ?? Number.POSITIVE_INFINITY;
const lastEndValue = colorRanges[colorRanges.length - 1]?.end ?? Number.POSITIVE_INFINITY;
const shouldSort = colorRange.start > nextStartValue || prevStartValue > colorRange.start;
const shouldSort =
colorRange.start > nextStartValue ||
prevStartValue > colorRange.start ||
(!isLast && colorRange.start > lastEndValue);
const isFocusStillInContent =
(e.currentTarget as Node)?.contains(e.relatedTarget as Node) || popoverInFocus;
@ -136,7 +126,7 @@ export function ColorRangeItem({
dispatch({ type: 'sortColorRanges', payload: { dataBounds, palettes } });
}
},
[colorRange.start, colorRanges, dispatch, index, popoverInFocus, dataBounds, palettes]
[colorRange.start, colorRanges, dispatch, index, popoverInFocus, dataBounds, palettes, isLast]
);
const onValueChange = useCallback(
@ -190,6 +180,7 @@ export function ColorRangeItem({
}
secondaryInputDisplay="top"
color={colorRange.color}
showAlpha={true}
onFocus={() => setPopoverInFocus(true)}
onBlur={() => {
setPopoverInFocus(false);
@ -205,11 +196,13 @@ export function ColorRangeItem({
compressed
fullWidth={true}
isInvalid={!isValid}
value={mode !== 'auto' ? localValue : ''}
value={
mode !== 'auto' && localValue !== undefined && isFinite(localValue) ? localValue : ''
}
disabled={isDisabled}
onChange={onValueChange}
placeholder={mode === 'auto' ? getPlaceholderForAutoMode(isLast) : ''}
append={getAppend(rangeType, mode, validation)}
append={getAppend(rangeType, mode)}
onBlur={onLeaveFocus}
data-test-subj={`lnsPalettePanel_dynamicColoring_range_value_${index}`}
prepend={<span className="euiFormLabel">{isLast ? '\u2264' : '\u2265'}</span>}

View file

@ -82,26 +82,28 @@ export function ColorRangeEditButton({
});
}, [isLast, dispatch, continuity, dataBounds, palettes]);
const title = i18n.translate('xpack.lens.dynamicColoring.customPalette.editButtonAriaLabel', {
defaultMessage: 'Edit',
});
let tooltipContent = isLast
? i18n.translate('xpack.lens.dynamicColoring.customPalette.setCustomMinValue', {
defaultMessage: `Set custom maximum value`,
})
: i18n.translate('xpack.lens.dynamicColoring.customPalette.setCustomMaxValue', {
defaultMessage: `Set custom minimum value`,
});
if (disableSwitchingContinuity) {
tooltipContent = i18n.translate(
'xpack.lens.dynamicColoring.customPalette.disallowedEditMinMaxValues',
{
defaultMessage: `You cannot set custom value for current configuration`,
}
);
}
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"
>
<TooltipWrapper tooltipContent={tooltipContent} condition={true} position="top" delay="regular">
<EuiButtonIcon
iconType="pencil"
aria-label={title}
title={title}
aria-label={tooltipContent}
disabled={disableSwitchingContinuity}
onClick={onExecuteAction}
data-test-subj={`lnsPalettePanel_dynamicColoring_editValue_${index}`}
@ -127,23 +129,24 @@ export function ColorRangeAutoDetectButton({
});
}, [continuity, dataBounds, dispatch, isLast, palettes]);
const title = isLast
? i18n.translate('xpack.lens.dynamicColoring.customPalette.autoDetectMaximumAriaLabel', {
defaultMessage: 'Auto detect maximum value',
const tooltipContent = isLast
? i18n.translate('xpack.lens.dynamicColoring.customPalette.useAutoMaxValue', {
defaultMessage: `Use maximum data value`,
})
: i18n.translate('xpack.lens.dynamicColoring.customPalette.autoDetectMinimumAriaLabel', {
defaultMessage: 'Auto detect minimum value',
: i18n.translate('xpack.lens.dynamicColoring.customPalette.useAutoMinValue', {
defaultMessage: `Use minimum data value`,
});
return (
<EuiButtonIcon
iconType={isLast ? ValueMaxIcon : ValueMinIcon}
aria-label={title}
title={title}
onClick={onExecuteAction}
data-test-subj={`lnsPalettePanel_dynamicColoring_autoDetect_${
isLast ? 'maximum' : 'minimum'
}`}
/>
<TooltipWrapper tooltipContent={tooltipContent} condition={true} position="top" delay="regular">
<EuiButtonIcon
iconType={isLast ? ValueMaxIcon : ValueMinIcon}
aria-label={tooltipContent}
onClick={onExecuteAction}
data-test-subj={`lnsPalettePanel_dynamicColoring_autoDetect_${
isLast ? 'maximum' : 'minimum'
}`}
/>
</TooltipWrapper>
);
}

View file

@ -27,99 +27,20 @@ describe('Color ranges validation', () => {
color: '#aaa',
},
];
const validation = validateColorRanges(colorRanges, { min: 0, max: 100 }, 'number');
const validation = validateColorRanges(colorRanges);
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', () => {
@ -141,10 +62,10 @@ describe('Color ranges validation', () => {
color: '#ccc',
},
];
let isValid = isAllColorRangesValid(colorRanges, { min: 5, max: 40 }, 'number');
let isValid = isAllColorRangesValid(colorRanges);
expect(isValid).toBeFalsy();
colorRanges[colorRanges.length - 1].end = 30;
isValid = isAllColorRangesValid(colorRanges, { min: 5, max: 40 }, 'number');
isValid = isAllColorRangesValid(colorRanges);
expect(isValid).toBeTruthy();
});
});

View file

@ -5,23 +5,16 @@
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { getDataMinMax, isValidColor } from '../utils';
import { 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;
}
@ -44,7 +37,7 @@ export const getErrorMessages = (colorRangesValidity: Record<string, ColorRangeV
);
case 'greaterThanMaxValue':
return i18n.translate('xpack.lens.dynamicColoring.customPalette.invalidMaxValue', {
defaultMessage: 'Maximum value should be greater than preceding values',
defaultMessage: 'Maximum value must be greater than preceding values',
});
default:
return '';
@ -54,45 +47,9 @@ export const getErrorMessages = (colorRangesValidity: Record<string, ColorRangeV
];
};
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]
) => {
export const validateColorRange = (colorRange: ColorRange, accessor: ColorRangeAccessor) => {
const errors: ColorRangeValidationErrors[] = [];
let warnings: ColorRangeValidationWarnings[] = [];
if (Number.isNaN(colorRange[accessor])) {
errors.push('invalidValue');
@ -102,53 +59,33 @@ export const validateColorRange = (
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)];
} else if (!isValidColor(colorRange.color)) {
errors.push('invalidColor');
}
return {
isValid: !errors.length,
errors,
warnings,
} as ColorRangeValidation;
};
export const validateColorRanges = (
colorRanges: ColorRange[],
dataBounds: DataBounds,
rangeType: CustomPaletteParams['rangeType']
colorRanges: ColorRange[]
): 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),
[index]: validateColorRange(item, 'start'),
}),
{}
);
return {
...validations,
last: validateColorRange(colorRanges[colorRanges.length - 1], 'end', minMax),
last: validateColorRange(colorRanges[colorRanges.length - 1], 'end'),
};
};
export const isAllColorRangesValid = (
colorRanges: ColorRange[],
dataBounds: DataBounds,
rangeType: CustomPaletteParams['rangeType']
) => {
return Object.values(validateColorRanges(colorRanges, dataBounds, rangeType)).every(
(colorRange) => colorRange.isValid
);
export const isAllColorRangesValid = (colorRanges: ColorRange[]) => {
return Object.values(validateColorRanges(colorRanges)).every((colorRange) => colorRange.isValid);
};

View file

@ -23,16 +23,7 @@ describe('addColorRange', () => {
];
});
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', () => {
it('should add new color range with the corresponding interva', () => {
colorRanges[colorRanges.length - 1].end = 80;
expect(addColorRange(colorRanges, 'number', { min: 0, max: 80 })).toEqual([
{ color: '#aaa', start: 20, end: 40 },

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { getDataMinMax, roundValue } from '../../utils';
import { calculateMaxStep } from './utils';
import type { ColorRange, ColorRangeAccessor } from '../types';
import type { DataBounds } from '../../types';
@ -43,12 +42,7 @@ export const addColorRange = (
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));
let insertEnd = roundValue(Math.min(lastStart + 1, max));
if (insertEnd === Number.NEGATIVE_INFINITY) {
insertEnd = 1;

View file

@ -9,7 +9,7 @@ import { sortColorRanges, calculateMaxStep, toColorStops, getValueForContinuity
describe('utils', () => {
it('sortColorRanges', () => {
const colorRanges = [
let colorRanges = [
{ color: '#aaa', start: 55, end: 40 },
{ color: '#bbb', start: 40, end: 60 },
{ color: '#ccc', start: 60, end: 80 },
@ -19,6 +19,17 @@ describe('utils', () => {
{ color: '#aaa', start: 55, end: 60 },
{ color: '#ccc', start: 60, end: 80 },
]);
colorRanges = [
{ color: '#aaa', start: 55, end: 90 },
{ color: '#bbb', start: 90, end: 60 },
{ color: '#ccc', start: 60, end: 80 },
];
expect(sortColorRanges(colorRanges)).toEqual([
{ color: '#aaa', start: 55, end: 60 },
{ color: '#ccc', start: 60, end: 80 },
{ color: '#bbb', start: 80, end: 90 },
]);
});
it('calculateMaxStep', () => {

View file

@ -25,15 +25,21 @@ export const isLastItem = (accessor: ColorRangeAccessor) => accessor === 'end';
* @internal
*/
export const sortColorRanges = (colorRanges: ColorRange[]) => {
const maxValue = colorRanges[colorRanges.length - 1].end;
const lastRange = colorRanges[colorRanges.length - 1];
return [...colorRanges]
return [...colorRanges, { start: lastRange.end, color: lastRange.color }]
.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,
}));
.reduce((sortedColorRange, newColorRange, i, array) => {
const color = i === array.length - 2 ? array[i + 1].color : newColorRange.color;
if (i !== array.length - 1) {
sortedColorRange.push({
color,
start: newColorRange.start,
end: array[i + 1].start,
});
}
return sortedColorRange;
}, [] as ColorRange[]);
};
/**
@ -91,16 +97,17 @@ export const getValueForContinuity = (
if (checkIsMaxContinuity(continuity)) {
value = Number.POSITIVE_INFINITY;
} else {
value =
value = roundValue(
colorRanges[colorRanges.length - 1].start > max
? colorRanges[colorRanges.length - 1].start + 1
: max;
: max
);
}
} else {
if (checkIsMinContinuity(continuity)) {
value = Number.NEGATIVE_INFINITY;
} else {
value = colorRanges[0].end < min ? colorRanges[0].end - 1 : min;
value = roundValue(colorRanges[0].end < min ? colorRanges[0].end - 1 : min);
}
}

View file

@ -17,7 +17,6 @@ import './palette_configuration.scss';
import type { CustomPaletteParams, RequiredPaletteParamTypes } from '../../../common';
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';
@ -52,12 +51,10 @@ export function CustomizablePalette({
useDebounce(
() => {
const rangeType =
localState.activePalette?.params?.rangeType ?? defaultPaletteParams.rangeType;
if (
(localState.activePalette !== activePalette ||
colorRangesToShow !== localState.colorRanges) &&
isAllColorRangesValid(localState.colorRanges, dataBounds, rangeType)
isAllColorRangesValid(localState.colorRanges)
) {
setPalette(localState.activePalette);
}

View file

@ -248,6 +248,9 @@ export function calculateStop(
oldInterval: number,
newInterval: number
) {
if (oldInterval === 0) {
return newInterval + newMin;
}
return roundValue(newMin + ((stopValue - oldMin) * newInterval) / oldInterval);
}

View file

@ -167,7 +167,6 @@ export function GaugeDimensionEditor(
palettes={props.paletteService}
activePalette={activePalette}
dataBounds={currentMinMax}
disableSwitchingContinuity={true}
setPalette={(newPalette) => {
// if the new palette is not custom, replace the rangeMin with the artificial one
if (