[Maps] Add ability to invert color ramp and size (#143307)

* convert color forms to TS

* switch for setting invert

* reverse colors

* convert size components to TS

* invert size switch

* invert mb size expression

* invert size legend

* invert ordinal legend

* invert colors in color picker

* update jest snapshot

* add unit tests for inverting color ramp creation

* review feedback

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2022-10-24 15:10:42 -06:00 committed by GitHub
parent 7d10edcc8f
commit ee326591a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 393 additions and 170 deletions

View file

@ -78,6 +78,7 @@ export type ColorDynamicOptions = {
customColorRamp?: OrdinalColorStop[];
useCustomColorRamp?: boolean;
dataMappingFunction?: DATA_MAPPING_FUNCTION;
invert?: boolean;
// category color properties
colorCategory?: string; // TODO move color category palettes to constants and make ENUM type
@ -176,6 +177,7 @@ export type SizeDynamicOptions = {
maxSize: number;
field?: StylePropertyField;
fieldMetaOptions: FieldMetaOptions;
invert?: boolean;
};
export type SizeStaticOptions = {

View file

@ -13,7 +13,7 @@ import {
} from './color_palettes';
describe('getColorPalette', () => {
it('Should create RGB color ramp', () => {
test('Should create RGB color ramp', () => {
expect(getColorPalette('Blues')).toEqual([
'#ecf1f7',
'#d9e3ef',
@ -28,14 +28,14 @@ describe('getColorPalette', () => {
});
describe('getColorRampCenterColor', () => {
it('Should get center color from color ramp', () => {
test('Should get center color from color ramp', () => {
expect(getColorRampCenterColor('Blues')).toBe('#9eb9d8');
});
});
describe('getOrdinalMbColorRampStops', () => {
it('Should create color stops for custom range', () => {
expect(getOrdinalMbColorRampStops('Blues', 0, 1000)).toEqual([
test('Should create color stops', () => {
expect(getOrdinalMbColorRampStops('Blues', 0, 1000, false)).toEqual([
0,
'#ecf1f7',
125,
@ -55,13 +55,34 @@ describe('getOrdinalMbColorRampStops', () => {
]);
});
it('Should snap to end of color stops for identical range', () => {
expect(getOrdinalMbColorRampStops('Blues', 23, 23)).toEqual([23, '#6092c0']);
test('Should create inverted color stops', () => {
expect(getOrdinalMbColorRampStops('Blues', 0, 1000, true)).toEqual([
0,
'#6092c0',
125,
'#769fc8',
250,
'#8bacd0',
375,
'#9eb9d8',
500,
'#b2c7df',
625,
'#c5d5e7',
750,
'#d9e3ef',
875,
'#ecf1f7',
]);
});
test('Should snap to end of color stops for identical range', () => {
expect(getOrdinalMbColorRampStops('Blues', 23, 23, false)).toEqual([23, '#6092c0']);
});
});
describe('getPercentilesMbColorRampStops', () => {
it('Should create color stops for custom range', () => {
test('Should create color stops', () => {
const percentiles = [
{ percentile: '50.0', value: 5567.83 },
{ percentile: '75.0', value: 8069 },
@ -69,7 +90,7 @@ describe('getPercentilesMbColorRampStops', () => {
{ percentile: '95.0', value: 11145.5 },
{ percentile: '99.0', value: 16958.18 },
];
expect(getPercentilesMbColorRampStops('Blues', percentiles)).toEqual([
expect(getPercentilesMbColorRampStops('Blues', percentiles, false)).toEqual([
5567.83,
'#e0e8f2',
8069,
@ -82,4 +103,26 @@ describe('getPercentilesMbColorRampStops', () => {
'#6092c0',
]);
});
test('Should create inverted color stops', () => {
const percentiles = [
{ percentile: '50.0', value: 5567.83 },
{ percentile: '75.0', value: 8069 },
{ percentile: '90.0', value: 9581.13 },
{ percentile: '95.0', value: 11145.5 },
{ percentile: '99.0', value: 16958.18 },
];
expect(getPercentilesMbColorRampStops('Blues', percentiles, true)).toEqual([
5567.83,
'#6092c0',
8069,
'#82a7cd',
9581.13,
'#a2bcd9',
11145.5,
'#c2d2e6',
16958.18,
'#e0e8f2',
]);
});
});

View file

@ -153,7 +153,7 @@ export function getColorPalette(colorPaletteId: string): string[] {
const colorPalette = COLOR_PALETTES.find(({ value }: COLOR_PALETTE) => {
return value === colorPaletteId;
});
return colorPalette ? (colorPalette.palette as string[]) : [];
return colorPalette ? [...(colorPalette.palette as string[])] : [];
}
export function getColorRampCenterColor(colorPaletteId: string): string | null {
@ -169,7 +169,8 @@ export function getColorRampCenterColor(colorPaletteId: string): string | null {
export function getOrdinalMbColorRampStops(
colorPaletteId: string | null,
min: number,
max: number
max: number,
invert: boolean
): Array<number | string> | null {
if (!colorPaletteId) {
return null;
@ -180,6 +181,10 @@ export function getOrdinalMbColorRampStops(
}
const palette = getColorPalette(colorPaletteId);
if (invert) {
palette.reverse();
}
if (palette.length === 0) {
return null;
}
@ -203,7 +208,8 @@ export function getOrdinalMbColorRampStops(
// [ stop_input_1: number, stop_output_1: color, stop_input_n: number, stop_output_n: color ]
export function getPercentilesMbColorRampStops(
colorPaletteId: string | null,
percentiles: PercentilesFieldMeta
percentiles: PercentilesFieldMeta,
invert: boolean
): Array<number | string> | null {
if (!colorPaletteId) {
return null;
@ -213,13 +219,17 @@ export function getPercentilesMbColorRampStops(
return value === colorPaletteId;
});
return paletteObject
? paletteObject
.getPalette(percentiles.length)
.reduce((accu: Array<number | string>, stopColor: string, idx: number) => {
return [...accu, percentiles[idx].value, stopColor];
}, [])
: null;
if (!paletteObject) {
return null;
}
const palette = paletteObject.getPalette(percentiles.length);
if (invert) {
palette.reverse();
}
return palette.reduce((accu: Array<number | string>, stopColor: string, idx: number) => {
return [...accu, percentiles[idx].value, stopColor];
}, []);
}
export function getLinearGradient(colorStrings: string[]): string {

View file

@ -15,6 +15,7 @@ interface Props {
maxLabel: string | number;
propertyLabel: string;
fieldLabel: string;
invert: boolean;
}
export function RangedStyleLegendRow({
@ -23,6 +24,7 @@ export function RangedStyleLegendRow({
maxLabel,
propertyLabel,
fieldLabel,
invert,
}: Props) {
return (
<div>
@ -41,12 +43,12 @@ export function RangedStyleLegendRow({
<EuiFlexGroup gutterSize="xs" justifyContent="spaceBetween">
<EuiFlexItem grow={true}>
<EuiText size="xs">
<small>{minLabel}</small>
<small>{invert ? maxLabel : minLabel}</small>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={true}>
<EuiText textAlign="right" size="xs">
<small>{maxLabel}</small>
<small>{invert ? minLabel : maxLabel}</small>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -58,6 +58,7 @@ export class HeatmapLegend extends Component<Props, State> {
})}
propertyLabel={HEATMAP_COLOR_RAMP_LABEL}
fieldLabel={this.state.label}
invert={false}
/>
);
}

View file

@ -96,7 +96,8 @@ export class HeatmapStyle implements IStyle {
const colorStops = getOrdinalMbColorRampStops(
this._descriptor.colorRampName,
MIN_RANGE,
MAX_RANGE
MAX_RANGE,
false
);
if (colorStops) {
mbMap.setPaintProperty(layerId, 'heatmap-color', [

View file

@ -129,16 +129,26 @@ export class ColorMapSelect extends Component {
);
}
_getColorPalettes() {
if (this.props.colorMapType === COLOR_MAP_TYPE.CATEGORICAL) {
return CATEGORICAL_COLOR_PALETTES;
}
return this.props.invert
? NUMERICAL_COLOR_PALETTES.map((paletteProps) => {
return {
...paletteProps,
palette: [...paletteProps.palette].reverse(),
};
})
: NUMERICAL_COLOR_PALETTES;
}
_renderColorMapSelections() {
if (this.props.isCustomOnly) {
return null;
}
const palettes =
this.props.colorMapType === COLOR_MAP_TYPE.ORDINAL
? NUMERICAL_COLOR_PALETTES
: CATEGORICAL_COLOR_PALETTES;
const palettesWithCustom = [
{
value: CUSTOM_COLOR_MAP,
@ -153,7 +163,7 @@ export class ColorMapSelect extends Component {
type: 'text',
'data-test-subj': `colorMapSelectOption_${CUSTOM_COLOR_MAP}`,
},
...palettes,
...this._getColorPalettes(),
];
const toggle = this.props.showColorMapTypeToggle ? (

View file

@ -6,12 +6,40 @@
*/
import _ from 'lodash';
import React, { Fragment } from 'react';
import React, { ChangeEvent, ReactNode } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiSpacer,
EuiSwitch,
EuiSwitchEvent,
} from '@elastic/eui';
import { FieldSelect } from '../field_select';
// @ts-expect-error
import { ColorMapSelect } from './color_map_select';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { OtherCategoryColorPicker } from './other_category_color_picker';
import { CATEGORICAL_DATA_TYPES, COLOR_MAP_TYPE } from '../../../../../../common/constants';
import {
CategoryColorStop,
ColorDynamicOptions,
OrdinalColorStop,
} from '../../../../../../common/descriptor_types';
import {
CATEGORICAL_DATA_TYPES,
COLOR_MAP_TYPE,
VECTOR_STYLES,
} from '../../../../../../common/constants';
import { StyleField } from '../../style_fields_helper';
import { DynamicColorProperty } from '../../properties/dynamic_color_property';
interface Props {
fields: StyleField[];
onDynamicStyleChange: (propertyName: VECTOR_STYLES, options: ColorDynamicOptions) => void;
staticDynamicSelect?: ReactNode;
styleProperty: DynamicColorProperty;
swatches: string[];
}
export function DynamicColorForm({
fields,
@ -19,10 +47,20 @@ export function DynamicColorForm({
staticDynamicSelect,
styleProperty,
swatches,
}) {
}: Props) {
const styleOptions = styleProperty.getOptions();
const onColorMapSelect = ({ color, customColorMap, type, useCustomColorMap }) => {
const onColorMapSelect = ({
color,
customColorMap,
type,
useCustomColorMap,
}: {
color?: null | string;
customColorMap?: OrdinalColorStop[] | CategoryColorStop[];
type: COLOR_MAP_TYPE;
useCustomColorMap: boolean;
}) => {
const newColorOptions = {
...styleOptions,
type,
@ -30,7 +68,7 @@ export function DynamicColorForm({
if (type === COLOR_MAP_TYPE.ORDINAL) {
newColorOptions.useCustomColorRamp = useCustomColorMap;
if (customColorMap) {
newColorOptions.customColorRamp = customColorMap;
newColorOptions.customColorRamp = customColorMap as OrdinalColorStop[];
}
if (color) {
newColorOptions.color = color;
@ -38,7 +76,7 @@ export function DynamicColorForm({
} else {
newColorOptions.useCustomColorPalette = useCustomColorMap;
if (customColorMap) {
newColorOptions.customColorPalette = customColorMap;
newColorOptions.customColorPalette = customColorMap as CategoryColorStop[];
}
if (color) {
newColorOptions.colorCategory = color;
@ -48,7 +86,11 @@ export function DynamicColorForm({
onDynamicStyleChange(styleProperty.getStyleName(), newColorOptions);
};
const onFieldChange = async ({ field }) => {
const onFieldChange = ({ field }: { field: StyleField | null }) => {
if (!field) {
return;
}
const { name, origin, type: fieldType } = field;
const defaultColorMapType = CATEGORICAL_DATA_TYPES.includes(fieldType)
? COLOR_MAP_TYPE.CATEGORICAL
@ -60,21 +102,28 @@ export function DynamicColorForm({
});
};
const onColorMapTypeChange = async (e) => {
const colorMapType = e.target.value;
const onColorMapTypeChange = (e: ChangeEvent<HTMLSelectElement>) => {
const colorMapType = e.target.value as COLOR_MAP_TYPE;
onDynamicStyleChange(styleProperty.getStyleName(), {
...styleOptions,
type: colorMapType,
});
};
const onOtherCategoryColorChange = (color) => {
const onOtherCategoryColorChange = (color: string) => {
onDynamicStyleChange(styleProperty.getStyleName(), {
...styleOptions,
otherCategoryColor: color,
});
};
const onInvertChange = (event: EuiSwitchEvent) => {
onDynamicStyleChange(styleProperty.getStyleName(), {
...styleOptions,
invert: event.target.checked,
});
};
const getField = () => {
const fieldName = styleProperty.getFieldName();
if (!fieldName) {
@ -92,10 +141,11 @@ export function DynamicColorForm({
return null;
}
const invert = styleOptions.invert === undefined ? false : styleOptions.invert;
const showColorMapTypeToggle = !CATEGORICAL_DATA_TYPES.includes(field.type);
if (styleProperty.isOrdinal()) {
return (
return styleProperty.isOrdinal() ? (
<>
<ColorMapSelect
isCustomOnly={!field.supportsAutoDomain}
onChange={onColorMapSelect}
@ -107,37 +157,49 @@ export function DynamicColorForm({
styleProperty={styleProperty}
showColorMapTypeToggle={showColorMapTypeToggle}
swatches={swatches}
invert={invert}
/>
);
} else if (styleProperty.isCategorical()) {
return (
<>
<ColorMapSelect
isCustomOnly={!field.supportsAutoDomain}
onColorMapTypeChange={onColorMapTypeChange}
onChange={onColorMapSelect}
colorMapType={COLOR_MAP_TYPE.CATEGORICAL}
colorPaletteId={styleOptions.colorCategory}
customColorMap={styleOptions.customColorPalette}
useCustomColorMap={_.get(styleOptions, 'useCustomColorPalette', false)}
styleProperty={styleProperty}
showColorMapTypeToggle={showColorMapTypeToggle}
swatches={swatches}
/>
<OtherCategoryColorPicker
onChange={onOtherCategoryColorChange}
color={styleOptions.otherCategoryColor}
/>
</>
);
}
{!!styleOptions.useCustomColorRamp ? null : (
<EuiFormRow display="columnCompressedSwitch">
<EuiSwitch
label={i18n.translate('xpack.maps.style.revereseColorsLabel', {
defaultMessage: `Reverse colors`,
})}
checked={invert}
onChange={onInvertChange}
compressed
/>
</EuiFormRow>
)}
</>
) : (
<>
<ColorMapSelect
isCustomOnly={!field.supportsAutoDomain}
onColorMapTypeChange={onColorMapTypeChange}
onChange={onColorMapSelect}
colorMapType={COLOR_MAP_TYPE.CATEGORICAL}
colorPaletteId={styleOptions.colorCategory}
customColorMap={styleOptions.customColorPalette}
useCustomColorMap={_.get(styleOptions, 'useCustomColorPalette', false)}
styleProperty={styleProperty}
showColorMapTypeToggle={showColorMapTypeToggle}
swatches={swatches}
invert={false}
/>
<OtherCategoryColorPicker
onChange={onOtherCategoryColorChange}
color={styleOptions.otherCategoryColor}
/>
</>
);
};
return (
<Fragment>
<>
<EuiFlexGroup gutterSize="xs" justifyContent="flexEnd">
<EuiFlexItem grow={false} className="mapStyleSettings__fixedBox">
{staticDynamicSelect}
{staticDynamicSelect ? staticDynamicSelect : null}
</EuiFlexItem>
<EuiFlexItem>
<FieldSelect
@ -151,6 +213,6 @@ export function DynamicColorForm({
</EuiFlexGroup>
<EuiSpacer size="s" />
{renderColorMapSelect()}
</Fragment>
</>
);
}

View file

@ -5,24 +5,34 @@
* 2.0.
*/
import React from 'react';
import React, { ReactNode } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { VECTOR_STYLES } from '../../../../../../common/constants';
import { ColorStaticOptions } from '../../../../../../common/descriptor_types';
import { MbValidatedColorPicker } from './mb_validated_color_picker';
import { StaticColorProperty } from '../../properties/static_color_property';
interface Props {
onStaticStyleChange: (propertyName: VECTOR_STYLES, options: ColorStaticOptions) => void;
staticDynamicSelect?: ReactNode;
styleProperty: StaticColorProperty;
swatches: string[];
}
export function StaticColorForm({
onStaticStyleChange,
staticDynamicSelect,
styleProperty,
swatches,
}) {
const onColorChange = (color) => {
}: Props) {
const onColorChange = (color: string) => {
onStaticStyleChange(styleProperty.getStyleName(), { color });
};
return (
<EuiFlexGroup gutterSize="xs" justifyContent="flexEnd">
<EuiFlexItem grow={false} className="mapStyleSettings__fixedBox">
{staticDynamicSelect}
{staticDynamicSelect ? staticDynamicSelect : null}
</EuiFlexItem>
<EuiFlexItem>
<MbValidatedColorPicker

View file

@ -9,10 +9,10 @@ import React from 'react';
import { i18n } from '@kbn/i18n';
import { Props, StylePropEditor } from '../style_prop_editor';
// @ts-expect-error
import { DynamicColorForm } from './dynamic_color_form';
// @ts-expect-error
import { StaticColorForm } from './static_color_form';
import { DynamicColorProperty } from '../../properties/dynamic_color_property';
import { StaticColorProperty } from '../../properties/static_color_property';
import { ColorDynamicOptions, ColorStaticOptions } from '../../../../../../common/descriptor_types';
type ColorEditorProps = Omit<Props<ColorStaticOptions, ColorDynamicOptions>, 'children'> & {
@ -21,9 +21,9 @@ type ColorEditorProps = Omit<Props<ColorStaticOptions, ColorDynamicOptions>, 'ch
export function VectorStyleColorEditor(props: ColorEditorProps) {
const colorForm = props.styleProperty.isDynamic() ? (
<DynamicColorForm {...props} />
<DynamicColorForm {...props} styleProperty={props.styleProperty as DynamicColorProperty} />
) : (
<StaticColorForm {...props} />
<StaticColorForm {...props} styleProperty={props.styleProperty as StaticColorProperty} />
);
return (

View file

@ -91,6 +91,7 @@ export class MarkerSizeLegend extends Component<Props, State> {
if (!fieldMeta || !options) {
return null;
}
const invert = options.invert === undefined ? false : options.invert;
const circleStyle = {
fillOpacity: 0,
@ -136,14 +137,18 @@ export class MarkerSizeLegend extends Component<Props, State> {
// Markers interpolated by area instead of radius to be more consistent with how the human eye+brain perceive shapes
// and their visual relevance
// This function mirrors output of maplibre expression created from DynamicSizeProperty.getMbSizeExpression
const value = Math.pow(percentage * Math.sqrt(fieldMeta!.delta), 2) + fieldMeta!.min;
const scaledWidth = Math.pow(percentage * Math.sqrt(fieldMeta!.delta), 2);
const value = invert ? fieldMeta!.max - scaledWidth : scaledWidth + fieldMeta!.min;
return fieldMeta!.delta > 3 ? Math.round(value) : value;
}
const markers = [];
if (fieldMeta.delta > 0) {
const smallestMarker = makeMarker(options.minSize, this._formatValue(fieldMeta.min));
const smallestMarker = makeMarker(
options.minSize,
this._formatValue(invert ? fieldMeta.max : fieldMeta.min)
);
markers.push(smallestMarker);
const markerDelta = options.maxSize - options.minSize;
@ -156,7 +161,10 @@ export class MarkerSizeLegend extends Component<Props, State> {
}
}
const largestMarker = makeMarker(options.maxSize, this._formatValue(fieldMeta.max));
const largestMarker = makeMarker(
options.maxSize,
this._formatValue(invert ? fieldMeta.min : fieldMeta.max)
);
markers.push(largestMarker);
return (

View file

@ -138,6 +138,9 @@ export class OrdinalLegend extends Component<Props, State> {
this.props.style.isFieldMetaEnabled() && fieldMeta.isMaxOutsideStdRange ? `> ${max}` : max;
}
const options = this.props.style.getOptions();
const invert = options.invert === undefined ? false : options.invert;
return (
<RangedStyleLegendRow
header={header}
@ -145,6 +148,7 @@ export class OrdinalLegend extends Component<Props, State> {
maxLabel={maxLabel}
propertyLabel={this.props.style.getDisplayStyleName()}
fieldLabel={this.state.label}
invert={invert}
/>
);
}

View file

@ -1,66 +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, { Fragment } from 'react';
import { FieldSelect } from '../field_select';
import { SizeRangeSelector } from './size_range_selector';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
export function DynamicSizeForm({
fields,
onDynamicStyleChange,
staticDynamicSelect,
styleProperty,
}) {
const styleOptions = styleProperty.getOptions();
const onFieldChange = ({ field }) => {
onDynamicStyleChange(styleProperty.getStyleName(), { ...styleOptions, field });
};
const onSizeRangeChange = ({ minSize, maxSize }) => {
onDynamicStyleChange(styleProperty.getStyleName(), {
...styleOptions,
minSize,
maxSize,
});
};
let sizeRange;
if (styleOptions.field && styleOptions.field.name) {
sizeRange = (
<SizeRangeSelector
onChange={onSizeRangeChange}
minSize={styleOptions.minSize}
maxSize={styleOptions.maxSize}
showLabels
compressed
/>
);
}
return (
<Fragment>
<EuiFlexGroup gutterSize="xs" justifyContent="flexEnd">
<EuiFlexItem grow={false} className="mapStyleSettings__fixedBox">
{staticDynamicSelect}
</EuiFlexItem>
<EuiFlexItem>
<FieldSelect
styleName={styleProperty.getStyleName()}
fields={fields}
selectedFieldName={styleProperty.getFieldName()}
onChange={onFieldChange}
compressed
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
{sizeRange}
</Fragment>
);
}

View file

@ -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, { ReactNode } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiSpacer,
EuiSwitch,
EuiSwitchEvent,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FieldSelect } from '../field_select';
import { SizeRangeSelector } from './size_range_selector';
import { SizeDynamicOptions } from '../../../../../../common/descriptor_types';
import { VECTOR_STYLES } from '../../../../../../common/constants';
import { DynamicSizeProperty } from '../../properties/dynamic_size_property';
import { StyleField } from '../../style_fields_helper';
interface Props {
fields: StyleField[];
onDynamicStyleChange: (propertyName: VECTOR_STYLES, options: SizeDynamicOptions) => void;
staticDynamicSelect?: ReactNode;
styleProperty: DynamicSizeProperty;
}
export function DynamicSizeForm({
fields,
onDynamicStyleChange,
staticDynamicSelect,
styleProperty,
}: Props) {
const styleOptions = styleProperty.getOptions();
const onFieldChange = ({ field }: { field: StyleField | null }) => {
if (field) {
onDynamicStyleChange(styleProperty.getStyleName(), {
...styleOptions,
field: { name: field.name, origin: field.origin },
});
}
};
const onSizeRangeChange = ({ minSize, maxSize }: { minSize: number; maxSize: number }) => {
onDynamicStyleChange(styleProperty.getStyleName(), {
...styleOptions,
minSize,
maxSize,
});
};
const onInvertChange = (event: EuiSwitchEvent) => {
onDynamicStyleChange(styleProperty.getStyleName(), {
...styleOptions,
invert: event.target.checked,
});
};
let sizeRange;
if (styleOptions.field && styleOptions.field.name) {
sizeRange = (
<>
<SizeRangeSelector
onChange={onSizeRangeChange}
minSize={styleOptions.minSize}
maxSize={styleOptions.maxSize}
showLabels
compressed
/>
<EuiFormRow display="columnCompressedSwitch">
<EuiSwitch
label={i18n.translate('xpack.maps.style.revereseSizeLabel', {
defaultMessage: `Reverse size`,
})}
checked={!!styleOptions.invert}
onChange={onInvertChange}
compressed
/>
</EuiFormRow>
</>
);
}
return (
<>
<EuiFlexGroup gutterSize="xs" justifyContent="flexEnd">
<EuiFlexItem grow={false} className="mapStyleSettings__fixedBox">
{staticDynamicSelect}
</EuiFlexItem>
<EuiFlexItem>
<FieldSelect
styleName={styleProperty.getStyleName()}
fields={fields}
selectedFieldName={styleProperty.getFieldName()}
onChange={onFieldChange}
compressed
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
{sizeRange}
</>
);
}

View file

@ -6,13 +6,19 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
import { ValidatedDualRange } from '@kbn/kibana-react-plugin/public';
import { MIN_SIZE, MAX_SIZE } from '../../vector_style_defaults';
import { EuiDualRangeProps } from '@elastic/eui/src/components/form/range/dual_range';
import { i18n } from '@kbn/i18n';
import { MIN_SIZE, MAX_SIZE } from '../../vector_style_defaults';
export function SizeRangeSelector({ minSize, maxSize, onChange, ...rest }) {
const onSizeChange = ([min, max]) => {
interface Props extends Omit<EuiDualRangeProps, 'value' | 'onChange' | 'min' | 'max'> {
minSize: number;
maxSize: number;
onChange: ({ maxSize, minSize }: { maxSize: number; minSize: number }) => void;
}
export function SizeRangeSelector({ minSize, maxSize, onChange, ...rest }: Props) {
const onSizeChange = ([min, max]: [string, string]) => {
onChange({
minSize: Math.max(MIN_SIZE, parseInt(min, 10)),
maxSize: Math.min(MAX_SIZE, parseInt(max, 10)),
@ -37,9 +43,3 @@ export function SizeRangeSelector({ minSize, maxSize, onChange, ...rest }) {
/>
);
}
SizeRangeSelector.propTypes = {
minSize: PropTypes.number.isRequired,
maxSize: PropTypes.number.isRequired,
onChange: PropTypes.func.isRequired,
};

View file

@ -5,13 +5,23 @@
* 2.0.
*/
import React from 'react';
import { ValidatedRange } from '../../../../../components/validated_range';
import React, { ReactNode } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
// @ts-expect-error
import { ValidatedRange } from '../../../../../components/validated_range';
import { SizeStaticOptions } from '../../../../../../common/descriptor_types';
import { VECTOR_STYLES } from '../../../../../../common/constants';
import { StaticSizeProperty } from '../../properties/static_size_property';
export function StaticSizeForm({ onStaticStyleChange, staticDynamicSelect, styleProperty }) {
const onSizeChange = (size) => {
interface Props {
onStaticStyleChange: (propertyName: VECTOR_STYLES, options: SizeStaticOptions) => void;
staticDynamicSelect?: ReactNode;
styleProperty: StaticSizeProperty;
}
export function StaticSizeForm({ onStaticStyleChange, staticDynamicSelect, styleProperty }: Props) {
const onSizeChange = (size: number) => {
onStaticStyleChange(styleProperty.getStyleName(), { size });
};

View file

@ -8,19 +8,19 @@
import React from 'react';
import { Props, StylePropEditor } from '../style_prop_editor';
// @ts-expect-error
import { DynamicSizeForm } from './dynamic_size_form';
// @ts-expect-error
import { StaticSizeForm } from './static_size_form';
import { SizeDynamicOptions, SizeStaticOptions } from '../../../../../../common/descriptor_types';
import { DynamicSizeProperty } from '../../properties/dynamic_size_property';
import { StaticSizeProperty } from '../../properties/static_size_property';
type SizeEditorProps = Omit<Props<SizeStaticOptions, SizeDynamicOptions>, 'children'>;
export function VectorStyleSizeEditor(props: SizeEditorProps) {
const sizeForm = props.styleProperty.isDynamic() ? (
<DynamicSizeForm {...props} />
<DynamicSizeForm {...props} styleProperty={props.styleProperty as DynamicSizeProperty} />
) : (
<StaticSizeForm {...props} />
<StaticSizeForm {...props} styleProperty={props.styleProperty as StaticSizeProperty} />
);
return <StylePropEditor {...props}>{sizeForm}</StylePropEditor>;

View file

@ -142,6 +142,7 @@ export class DynamicColorProperty extends DynamicStyleProperty<ColorDynamicOptio
}
_getOrdinalColorMbExpression() {
const invert = this._options.invert === undefined ? false : this._options.invert;
const targetName = this.getMbFieldName();
if (this._options.useCustomColorRamp) {
if (!this._options.customColorRamp || !this._options.customColorRamp.length) {
@ -177,7 +178,8 @@ export class DynamicColorProperty extends DynamicStyleProperty<ColorDynamicOptio
const colorStops = getPercentilesMbColorRampStops(
this._options.color ? this._options.color : null,
percentilesFieldMeta
percentilesFieldMeta,
invert
);
if (!colorStops) {
return null;
@ -205,7 +207,8 @@ export class DynamicColorProperty extends DynamicStyleProperty<ColorDynamicOptio
const colorStops = getOrdinalMbColorRampStops(
this._options.color ? this._options.color : null,
rangeFieldMeta.min,
rangeFieldMeta.max
rangeFieldMeta.max,
invert
);
if (!colorStops) {
return null;
@ -350,6 +353,7 @@ export class DynamicColorProperty extends DynamicStyleProperty<ColorDynamicOptio
}
_getOrdinalBreaks(symbolId?: string, svg?: string): Break[] {
const invert = this._options.invert === undefined ? false : this._options.invert;
let colorStops: Array<number | string> | null = null;
let getValuePrefix: ((i: number, isNext: boolean) => string) | null = null;
if (this._options.useCustomColorRamp) {
@ -364,7 +368,8 @@ export class DynamicColorProperty extends DynamicStyleProperty<ColorDynamicOptio
}
colorStops = getPercentilesMbColorRampStops(
this._options.color ? this._options.color : null,
percentilesFieldMeta
percentilesFieldMeta,
invert
);
getValuePrefix = function (i: number, isNext: boolean) {
const percentile = isNext
@ -379,7 +384,9 @@ export class DynamicColorProperty extends DynamicStyleProperty<ColorDynamicOptio
return [];
}
if (rangeFieldMeta.delta === 0) {
const colors = getColorPalette(this._options.color);
const colors = invert
? getColorPalette(this._options.color).reverse()
: getColorPalette(this._options.color);
// map to last color.
return [
{
@ -393,7 +400,8 @@ export class DynamicColorProperty extends DynamicStyleProperty<ColorDynamicOptio
colorStops = getOrdinalMbColorRampStops(
this._options.color ? this._options.color : null,
rangeFieldMeta.min,
rangeFieldMeta.max
rangeFieldMeta.max,
invert
);
}

View file

@ -228,6 +228,7 @@ exports[`renderLegendDetailRow Should render line width simple range 1`] = `
</React.Fragment>
</EuiFlexGroup>
}
invert={false}
maxLabel="100_format"
minLabel="0_format"
propertyLabel="Border width"

View file

@ -112,17 +112,25 @@ export class DynamicSizeProperty extends DynamicStyleProperty<SizeDynamicOptions
this.getStyleName() === VECTOR_STYLES.ICON_SIZE && this._isSymbolizedAsIcon
? this._options.minSize / HALF_MAKI_ICON_SIZE
: this._options.minSize;
const stops =
rangeFieldMeta.min === rangeFieldMeta.max
? [maxValueStopInput, maxRangeStopOutput]
const invert = this._options.invert === undefined ? false : this._options.invert;
function getStopsWithoutRange() {
return invert
? [maxValueStopInput, minRangeStopOutput]
: [maxValueStopInput, maxRangeStopOutput];
}
function getStops() {
return invert
? [minValueStopInput, maxRangeStopOutput, maxValueStopInput, minRangeStopOutput]
: [minValueStopInput, minRangeStopOutput, maxValueStopInput, maxRangeStopOutput];
}
const stops = rangeFieldMeta.min === rangeFieldMeta.max ? getStopsWithoutRange() : getStops();
const valueExpression = makeMbClampedNumberExpression({
lookupFunction: this.getMbLookupFunction(),
maxValue: rangeFieldMeta.max,
minValue: rangeFieldMeta.min,
fieldName: this.getMbFieldName(),
fallback: rangeFieldMeta.min,
fallback: invert ? rangeFieldMeta.max : rangeFieldMeta.min,
});
const valueShiftExpression =
rangeFieldMeta.min < 1 ? ['+', valueExpression, valueShift] : valueExpression;