mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
a5ee534bad
commit
e5a8cec25e
17 changed files with 233 additions and 310 deletions
|
@ -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 = {
|
||||
|
|
|
@ -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)`
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -248,6 +248,9 @@ export function calculateStop(
|
|||
oldInterval: number,
|
||||
newInterval: number
|
||||
) {
|
||||
if (oldInterval === 0) {
|
||||
return newInterval + newMin;
|
||||
}
|
||||
return roundValue(newMin + ((stopValue - oldMin) * newInterval) / oldInterval);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 (
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue