[maps] style by percentiles (#84291)

* [maps] define style bands by percentiles

* add step function select

* percentiles form

* percentiles agg request

* create mapbox expression for stops

* legend

* small legend tweek

* clean up legend rendering

* fix dynamic color property tests

* add unit test case for percentiles legend

* re-fetch style meta when percentiles change

* name space field meta request types

* rename field_meta to data_mapping

* add tooltip to category field meta switch

* i18n fixes

* tslint

* remove duplicate file license

* fix jest tests

* only show supported step functions in fitting select

* copy updates

* add getPalette function for heatmap palette

* update jest snapshot

* another jest snapshot update

* rename EASING_BETWEEN_MIN_AND_MAX -> INTERPOLATE

* rename STEP_FUNCTION -> DATA_MAPPING_FUNCTION and text updates

* review feedback

* remove 'Apply changes' button on percentiles form

* update legend to use 'up to' and 'greater than' instead of symbols

* tslint

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2020-12-08 09:59:15 -07:00 committed by GitHub
parent b3bccc2816
commit 0eee8a2a86
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1318 additions and 443 deletions

View file

@ -270,6 +270,12 @@ export enum MB_LOOKUP_FUNCTION {
FEATURE_STATE = 'feature-state',
}
export enum DATA_MAPPING_FUNCTION {
INTERPOLATE = 'INTERPOLATE',
PERCENTILES = 'PERCENTILES',
}
export const DEFAULT_PERCENTILES = [50, 75, 90, 95, 99];
export type RawValue = string | number | boolean | undefined | null;
export type FieldFormatter = (value: RawValue) => string | number;

View file

@ -11,6 +11,7 @@ import {
LABEL_BORDER_SIZES,
SYMBOLIZE_AS_TYPES,
VECTOR_STYLES,
DATA_MAPPING_FUNCTION,
STYLE_TYPE,
} from '../constants';
@ -36,6 +37,7 @@ export type LabelBorderSizeStylePropertyDescriptor = {
export type FieldMetaOptions = {
isEnabled: boolean;
sigma?: number;
percentiles?: number[];
};
export type StylePropertyField = {
@ -63,6 +65,7 @@ export type ColorDynamicOptions = {
color?: string; // TODO move color category ramps to constants and make ENUM type
customColorRamp?: OrdinalColorStop[];
useCustomColorRamp?: boolean;
dataMappingFunction?: DATA_MAPPING_FUNCTION;
// category color properties
colorCategory?: string; // TODO move color category palettes to constants and make ENUM type
@ -200,6 +203,11 @@ export type RangeFieldMeta = {
isMaxOutsideStdRange?: boolean;
};
export type PercentilesFieldMeta = Array<{
percentile: string;
value: number;
}>;
export type Category = {
key: string;
count: number;

View file

@ -9,6 +9,14 @@ import { i18n } from '@kbn/i18n';
import { $Values } from '@kbn/utility-types';
import { ES_SPATIAL_RELATIONS } from './constants';
export const UPTO = i18n.translate('xpack.maps.upto', {
defaultMessage: 'up to',
});
export const GREAT_THAN = i18n.translate('xpack.maps.greatThan', {
defaultMessage: 'greater than',
});
export function getAppTitle() {
return i18n.translate('xpack.maps.appTitle', {
defaultMessage: 'Maps',

View file

@ -68,8 +68,14 @@ export class AggField extends CountAggField {
return this._getAggType() === AGG_TYPE.TERMS ? TERMS_AGG_SHARD_SIZE : 0;
}
async getOrdinalFieldMetaRequest(): Promise<unknown> {
return this._esDocField ? await this._esDocField.getOrdinalFieldMetaRequest() : null;
async getExtendedStatsFieldMetaRequest(): Promise<unknown | null> {
return this._esDocField ? await this._esDocField.getExtendedStatsFieldMetaRequest() : null;
}
async getPercentilesFieldMetaRequest(percentiles: number[]): Promise<unknown | null> {
return this._esDocField
? await this._esDocField.getPercentilesFieldMetaRequest(percentiles)
: null;
}
async getCategoricalFieldMetaRequest(size: number): Promise<unknown> {

View file

@ -82,7 +82,11 @@ export class CountAggField implements IESAggField {
return false;
}
async getOrdinalFieldMetaRequest(): Promise<unknown> {
async getExtendedStatsFieldMetaRequest(): Promise<unknown | null> {
return null;
}
async getPercentilesFieldMetaRequest(percentiles: number[]): Promise<unknown | null> {
return null;
}

View file

@ -68,7 +68,11 @@ export class TopTermPercentageField implements IESAggField {
return false;
}
async getOrdinalFieldMetaRequest(): Promise<unknown> {
async getExtendedStatsFieldMetaRequest(): Promise<unknown | null> {
return null;
}
async getPercentilesFieldMetaRequest(percentiles: number[]): Promise<unknown | null> {
return null;
}

View file

@ -68,7 +68,7 @@ export class ESDocField extends AbstractField implements IField {
return this._canReadFromGeoJson;
}
async getOrdinalFieldMetaRequest(): Promise<unknown> {
async getExtendedStatsFieldMetaRequest(): Promise<unknown | null> {
const indexPatternField = await this._getIndexPatternField();
if (
@ -80,18 +80,43 @@ export class ESDocField extends AbstractField implements IField {
// TODO remove local typing once Kibana has figured out a core place for Elasticsearch aggregation request types
// https://github.com/elastic/kibana/issues/60102
const extendedStats: { script?: unknown; field?: string } = {};
const metricAggConfig: { script?: unknown; field?: string } = {};
if (indexPatternField.scripted) {
extendedStats.script = {
metricAggConfig.script = {
source: indexPatternField.script,
lang: indexPatternField.lang,
};
} else {
extendedStats.field = this.getName();
metricAggConfig.field = this.getName();
}
return {
[this.getName()]: {
extended_stats: extendedStats,
[`${this.getName()}_range`]: {
extended_stats: metricAggConfig,
},
};
}
async getPercentilesFieldMetaRequest(percentiles: number[]): Promise<unknown | null> {
const indexPatternField = await this._getIndexPatternField();
if (!indexPatternField || indexPatternField.type !== 'number') {
return null;
}
const metricAggConfig: { script?: unknown; field?: string; percents: number[] } = {
percents: [0, ...percentiles],
};
if (indexPatternField.scripted) {
metricAggConfig.script = {
source: indexPatternField.script,
lang: indexPatternField.lang,
};
} else {
metricAggConfig.field = this.getName();
}
return {
[`${this.getName()}_percentiles`]: {
percentiles: metricAggConfig,
},
};
}
@ -116,7 +141,7 @@ export class ESDocField extends AbstractField implements IField {
topTerms.field = this.getName();
}
return {
[this.getName()]: {
[`${this.getName()}_terms`]: {
terms: topTerms,
},
};

View file

@ -18,7 +18,8 @@ export interface IField {
getSource(): IVectorSource;
getOrigin(): FIELD_ORIGIN;
isValid(): boolean;
getOrdinalFieldMetaRequest(): Promise<unknown>;
getExtendedStatsFieldMetaRequest(): Promise<unknown | null>;
getPercentilesFieldMetaRequest(percentiles: number[]): Promise<unknown | null>;
getCategoricalFieldMetaRequest(size: number): Promise<unknown>;
// Whether Maps-app can automatically determine the domain of the field-values
@ -85,7 +86,11 @@ export class AbstractField implements IField {
return false;
}
async getOrdinalFieldMetaRequest(): Promise<unknown> {
async getExtendedStatsFieldMetaRequest(): Promise<unknown> {
return null;
}
async getPercentilesFieldMetaRequest(percentiles: number[]): Promise<unknown | null> {
return null;
}

View file

@ -602,7 +602,7 @@ export class VectorLayer extends AbstractLayer {
}
const dynamicStyleFields = dynamicStyleProps.map((dynamicStyleProp) => {
return `${dynamicStyleProp.getFieldName()}${dynamicStyleProp.getNumberOfCategories()}`;
return `${dynamicStyleProp.getFieldName()}${dynamicStyleProp.getStyleMetaHash()}`;
});
const nextMeta = {

View file

@ -6,6 +6,7 @@
import {
getColorRampCenterColor,
getOrdinalMbColorRampStops,
getPercentilesMbColorRampStops,
getColorPalette,
} from './color_palettes';
@ -56,3 +57,27 @@ describe('getOrdinalMbColorRampStops', () => {
expect(getOrdinalMbColorRampStops('Blues', 23, 23)).toEqual([23, '#6092c0']);
});
});
describe('getPercentilesMbColorRampStops', () => {
it('Should create color stops for custom range', () => {
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)).toEqual([
5567.83,
'#e0e8f2',
8069,
'#c2d2e6',
9581.13,
'#a2bcd9',
11145.5,
'#82a7cd',
16958.18,
'#6092c0',
]);
});
});

View file

@ -6,6 +6,8 @@
import tinycolor from 'tinycolor2';
import {
// @ts-ignore
colorPalette as colorPaletteGenerator,
// @ts-ignore
euiPaletteForStatus,
// @ts-ignore
@ -24,6 +26,7 @@ import {
euiPaletteColorBlind,
} from '@elastic/eui/lib/services';
import { EuiColorPalettePickerPaletteProps } from '@elastic/eui';
import { PercentilesFieldMeta } from '../../../common/descriptor_types';
export const DEFAULT_HEATMAP_COLOR_RAMP_NAME = 'theclassic';
@ -35,84 +38,118 @@ export const DEFAULT_LINE_COLORS: string[] = [
'#FFF',
];
const COLOR_PALETTES: EuiColorPalettePickerPaletteProps[] = [
const ROYAL_BLUE = 'rgb(65, 105, 225)';
const CYAN = 'rgb(0, 256, 256)';
const LIME = 'rgb(0, 256, 0)';
const YELLOW = 'rgb(256, 256, 0)';
const RED = 'rgb(256, 0, 0)';
const HEATMAP_PALETTE = [ROYAL_BLUE, CYAN, LIME, YELLOW, RED];
type COLOR_PALETTE = EuiColorPalettePickerPaletteProps & {
getPalette: (steps: number) => string[];
};
function getColorBlindPalette(steps: number) {
const rotations = Math.ceil(steps / 10);
const palette = euiPaletteColorBlind({ rotations });
return palette.slice(0, steps - 1);
}
const COLOR_PALETTES: COLOR_PALETTE[] = [
{
value: 'Blues',
getPalette: (steps: number) => {
return euiPaletteCool(steps);
},
palette: euiPaletteCool(8),
type: 'gradient',
},
{
value: 'Greens',
getPalette: (steps: number) => {
return euiPalettePositive(steps);
},
palette: euiPalettePositive(8),
type: 'gradient',
},
{
value: 'Greys',
getPalette: (steps: number) => {
return euiPaletteGray(steps);
},
palette: euiPaletteGray(8),
type: 'gradient',
},
{
value: 'Reds',
getPalette: (steps: number) => {
return euiPaletteNegative(steps);
},
palette: euiPaletteNegative(8),
type: 'gradient',
},
{
value: 'Yellow to Red',
getPalette: (steps: number) => {
return euiPaletteWarm(steps);
},
palette: euiPaletteWarm(8),
type: 'gradient',
},
{
value: 'Green to Red',
getPalette: (steps: number) => {
return euiPaletteForStatus(steps);
},
palette: euiPaletteForStatus(8),
type: 'gradient',
},
{
value: 'Blue to Red',
getPalette: (steps: number) => {
return euiPaletteForTemperature(steps);
},
palette: euiPaletteForTemperature(8),
type: 'gradient',
},
{
value: DEFAULT_HEATMAP_COLOR_RAMP_NAME,
palette: [
'rgb(65, 105, 225)', // royalblue
'rgb(0, 256, 256)', // cyan
'rgb(0, 256, 0)', // lime
'rgb(256, 256, 0)', // yellow
'rgb(256, 0, 0)', // red
],
getPalette: (steps: number) => {
return colorPaletteGenerator(HEATMAP_PALETTE, steps, true, true);
},
palette: HEATMAP_PALETTE,
type: 'gradient',
},
{
value: 'palette_0',
getPalette: getColorBlindPalette,
palette: euiPaletteColorBlind(),
type: 'fixed',
},
{
value: 'palette_20',
getPalette: getColorBlindPalette,
palette: euiPaletteColorBlind({ rotations: 2 }),
type: 'fixed',
},
{
value: 'palette_30',
getPalette: getColorBlindPalette,
palette: euiPaletteColorBlind({ rotations: 3 }),
type: 'fixed',
},
];
export const NUMERICAL_COLOR_PALETTES = COLOR_PALETTES.filter(
(palette: EuiColorPalettePickerPaletteProps) => {
return palette.type === 'gradient';
}
);
export const NUMERICAL_COLOR_PALETTES = COLOR_PALETTES.filter((palette: COLOR_PALETTE) => {
return palette.type === 'gradient';
});
export const CATEGORICAL_COLOR_PALETTES = COLOR_PALETTES.filter(
(palette: EuiColorPalettePickerPaletteProps) => {
return palette.type === 'fixed';
}
);
export const CATEGORICAL_COLOR_PALETTES = COLOR_PALETTES.filter((palette: COLOR_PALETTE) => {
return palette.type === 'fixed';
});
export function getColorPalette(colorPaletteId: string): string[] {
const colorPalette = COLOR_PALETTES.find(({ value }: EuiColorPalettePickerPaletteProps) => {
const colorPalette = COLOR_PALETTES.find(({ value }: COLOR_PALETTE) => {
return value === colorPaletteId;
});
return colorPalette ? (colorPalette.palette as string[]) : [];
@ -161,6 +198,29 @@ export function getOrdinalMbColorRampStops(
);
}
// Returns an array of color stops
// [ stop_input_1: number, stop_output_1: color, stop_input_n: number, stop_output_n: color ]
export function getPercentilesMbColorRampStops(
colorPaletteId: string | null,
percentiles: PercentilesFieldMeta
): Array<number | string> | null {
if (!colorPaletteId) {
return null;
}
const paletteObject = NUMERICAL_COLOR_PALETTES.find(({ value }: COLOR_PALETTE) => {
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;
}
export function getLinearGradient(colorStrings: string[]): string {
const intervals = colorStrings.length;
let linearGradient = `linear-gradient(to right, ${colorStrings[0]} 0%,`;

View file

@ -16,6 +16,7 @@ exports[`HeatmapStyleEditor is rendered 1`] = `
palettes={
Array [
Object {
"getPalette": [Function],
"palette": Array [
"#ecf1f7",
"#d9e3ef",
@ -30,6 +31,7 @@ exports[`HeatmapStyleEditor is rendered 1`] = `
"value": "Blues",
},
Object {
"getPalette": [Function],
"palette": Array [
"#e6f1ee",
"#cce4de",
@ -44,6 +46,7 @@ exports[`HeatmapStyleEditor is rendered 1`] = `
"value": "Greens",
},
Object {
"getPalette": [Function],
"palette": Array [
"#e0e4eb",
"#c2c9d5",
@ -58,6 +61,7 @@ exports[`HeatmapStyleEditor is rendered 1`] = `
"value": "Greys",
},
Object {
"getPalette": [Function],
"palette": Array [
"#fdeae5",
"#f9d5cc",
@ -72,6 +76,7 @@ exports[`HeatmapStyleEditor is rendered 1`] = `
"value": "Reds",
},
Object {
"getPalette": [Function],
"palette": Array [
"#f9eac5",
"#f6d9af",
@ -86,6 +91,7 @@ exports[`HeatmapStyleEditor is rendered 1`] = `
"value": "Yellow to Red",
},
Object {
"getPalette": [Function],
"palette": Array [
"#209280",
"#3aa38d",
@ -100,6 +106,7 @@ exports[`HeatmapStyleEditor is rendered 1`] = `
"value": "Green to Red",
},
Object {
"getPalette": [Function],
"palette": Array [
"#6092c0",
"#84a9cd",
@ -114,6 +121,7 @@ exports[`HeatmapStyleEditor is rendered 1`] = `
"value": "Blue to Red",
},
Object {
"getPalette": [Function],
"palette": Array [
"rgb(65, 105, 225)",
"rgb(0, 256, 256)",

View file

@ -0,0 +1,68 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiFormRow, EuiIcon, EuiSwitch, EuiSwitchEvent, EuiText, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { DataMappingPopover } from './data_mapping_popover';
import { FieldMetaOptions } from '../../../../../../common/descriptor_types';
interface Props<DynamicOptions> {
fieldMetaOptions: FieldMetaOptions;
onChange: (updatedOptions: DynamicOptions) => void;
switchDisabled: boolean;
}
export function CategoricalDataMappingPopover<DynamicOptions>(props: Props<DynamicOptions>) {
const onIsEnabledChange = (event: EuiSwitchEvent) => {
// @ts-expect-error
props.onChange({
fieldMetaOptions: {
...props.fieldMetaOptions,
isEnabled: event.target.checked,
},
});
};
return (
<DataMappingPopover>
<EuiFormRow display="columnCompressedSwitch">
<>
<EuiSwitch
label={i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.categoricalLabel', {
defaultMessage: 'Get categories from data set',
})}
checked={props.fieldMetaOptions.isEnabled}
onChange={onIsEnabledChange}
compressed
disabled={props.switchDisabled}
/>{' '}
<EuiToolTip
content={
<EuiText>
<p>
<FormattedMessage
id="xpack.maps.styles.categoricalDataMapping.isEnabled.server"
defaultMessage="Calculate categories from the entire data set. Styling is consistent when users pan, zoom, and filter."
/>
</p>
<p>
<FormattedMessage
id="xpack.maps.styles.categoricalDataMapping.isEnabled.local"
defaultMessage="When disabled, calculate categories from local data and recalculate categories when the data changes. Styling may be inconsistent when users pan, zoom, and filter."
/>
</p>
</EuiText>
}
>
<EuiIcon type="questionInCircle" color="subdued" />
</EuiToolTip>
</>
</EuiFormRow>
</DataMappingPopover>
);
}

View file

@ -6,8 +6,8 @@
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import React, { Component, ReactElement } from 'react';
import { EuiButtonIcon, EuiPopover } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EuiButtonEmpty, EuiPopover } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
type Props = {
children: ReactElement<any>;
@ -17,7 +17,7 @@ type State = {
isPopoverOpen: boolean;
};
export class FieldMetaPopover extends Component<Props, State> {
export class DataMappingPopover extends Component<Props, State> {
state = {
isPopoverOpen: false,
};
@ -36,21 +36,24 @@ export class FieldMetaPopover extends Component<Props, State> {
_renderButton() {
return (
<EuiButtonIcon
<EuiButtonEmpty
onClick={this._togglePopover}
size="s"
iconType="gear"
aria-label={i18n.translate('xpack.maps.styles.fieldMetaOptions.popoverToggle', {
defaultMessage: 'Field meta options popover toggle',
})}
/>
size="xs"
iconType="controlsHorizontal"
iconSide="left"
>
<FormattedMessage
id="xpack.maps.styles.fieldMetaOptions.popoverToggle"
defaultMessage="Data mapping"
/>
</EuiButtonEmpty>
);
}
render() {
return (
<EuiPopover
id="fieldMetaOptionsPopover"
id="dataMappingPopover"
anchorPosition="leftCenter"
button={this._renderButton()}
isOpen={this.state.isPopoverOpen}

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { CategoricalDataMappingPopover } from './categorical_data_mapping_popover';
export { OrdinalDataMappingPopover } from './ordinal_data_mapping_popover';

View file

@ -0,0 +1,259 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import _ from 'lodash';
import React, { ChangeEvent, Fragment, MouseEvent } from 'react';
import {
EuiFormRow,
EuiHorizontalRule,
EuiIcon,
EuiRange,
EuiSuperSelect,
EuiSwitch,
EuiSwitchEvent,
EuiText,
EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { DEFAULT_SIGMA } from '../../vector_style_defaults';
import { DataMappingPopover } from './data_mapping_popover';
import { FieldMetaOptions } from '../../../../../../common/descriptor_types';
import {
DEFAULT_PERCENTILES,
DATA_MAPPING_FUNCTION,
VECTOR_STYLES,
} from '../../../../../../common/constants';
import { PercentilesForm } from './percentiles_form';
const interpolateTitle = i18n.translate('xpack.maps.styles.ordinalDataMapping.interpolateTitle', {
defaultMessage: `Interpolate between min and max`,
});
const percentilesTitle = i18n.translate('xpack.maps.styles.ordinalDataMapping.percentilesTitle', {
defaultMessage: `Use percentiles`,
});
const DATA_MAPPING_OPTIONS = [
{
value: DATA_MAPPING_FUNCTION.INTERPOLATE,
inputDisplay: interpolateTitle,
dropdownDisplay: (
<Fragment>
<strong>{interpolateTitle}</strong>
<EuiText size="s" color="subdued">
<p className="euiTextColor--subdued">
<FormattedMessage
id="xpack.maps.styles.ordinalDataMapping.interpolateDescription"
defaultMessage="Interpolate values from the data domain to the style on a linear scale"
/>
</p>
</EuiText>
</Fragment>
),
},
{
value: DATA_MAPPING_FUNCTION.PERCENTILES,
inputDisplay: percentilesTitle,
dropdownDisplay: (
<Fragment>
<strong>{percentilesTitle}</strong>
<EuiText size="s" color="subdued">
<p className="euiTextColor--subdued">
<FormattedMessage
id="xpack.maps.styles.ordinalDataMapping.percentilesDescription"
defaultMessage="Divide style into bands that map to values"
/>
</p>
</EuiText>
</Fragment>
),
},
];
interface Props<DynamicOptions> {
fieldMetaOptions: FieldMetaOptions;
styleName: VECTOR_STYLES;
onChange: (updatedOptions: DynamicOptions) => void;
switchDisabled: boolean;
dataMappingFunction: DATA_MAPPING_FUNCTION;
supportedDataMappingFunctions: DATA_MAPPING_FUNCTION[];
}
export function OrdinalDataMappingPopover<DynamicOptions>(props: Props<DynamicOptions>) {
function onIsEnabledChange(event: EuiSwitchEvent) {
// @ts-expect-error
props.onChange({
fieldMetaOptions: {
...props.fieldMetaOptions,
isEnabled: event.target.checked,
},
});
}
function onSigmaChange(event: ChangeEvent<HTMLInputElement> | MouseEvent<HTMLButtonElement>) {
// @ts-expect-error
props.onChange({
fieldMetaOptions: {
...props.fieldMetaOptions,
sigma: parseInt(event.currentTarget.value, 10),
},
});
}
function onDataMappingFunctionChange(value: DATA_MAPPING_FUNCTION) {
const updatedOptions =
value === DATA_MAPPING_FUNCTION.PERCENTILES
? {
dataMappingFunction: value,
fieldMetaOptions: {
...props.fieldMetaOptions,
isEnabled: true,
percentiles: props.fieldMetaOptions.percentiles
? props.fieldMetaOptions.percentiles
: DEFAULT_PERCENTILES,
},
}
: {
dataMappingFunction: value,
};
// @ts-expect-error
props.onChange(updatedOptions);
}
function renderEasingForm() {
const sigmaInput = props.fieldMetaOptions.isEnabled ? (
<EuiFormRow
label={
<EuiToolTip
anchorClassName="eui-alignMiddle"
content={i18n.translate('xpack.maps.styles.ordinalDataMapping.sigmaTooltipContent', {
defaultMessage: `To de-emphasize outliers, set sigma to a smaller value. Smaller sigmas move the min and max closer to the median.`,
})}
>
<span>
{i18n.translate('xpack.maps.styles.ordinalDataMapping.sigmaLabel', {
defaultMessage: 'Sigma',
})}{' '}
<EuiIcon type="questionInCircle" color="subdued" />
</span>
</EuiToolTip>
}
display="columnCompressed"
>
<EuiRange
min={1}
max={5}
step={0.25}
value={_.get(props.fieldMetaOptions, 'sigma', DEFAULT_SIGMA)}
onChange={onSigmaChange}
showTicks
tickInterval={1}
compressed
/>
</EuiFormRow>
) : null;
return (
<Fragment>
<EuiFormRow display="columnCompressedSwitch">
<>
<EuiSwitch
label={i18n.translate('xpack.maps.styles.ordinalDataMapping.isEnabledSwitchLabel', {
defaultMessage: 'Get min and max from data set',
})}
checked={props.fieldMetaOptions.isEnabled}
onChange={onIsEnabledChange}
compressed
disabled={props.switchDisabled}
/>{' '}
<EuiToolTip
content={
<EuiText>
<p>
<FormattedMessage
id="xpack.maps.styles.ordinalDataMapping.isEnabled.server"
defaultMessage="Calculate min and max from the entire data set. Styling is consistent when users pan, zoom, and filter. To minimize outliers, min and max are clamped to the standard deviation (sigma) from the medium."
/>
</p>
<p>
<FormattedMessage
id="xpack.maps.styles.ordinalDataMapping.isEnabled.local"
defaultMessage="When disabled, calculate min and max from local data and recalculate min and max when the data changes. Styling may be inconsistent when users pan, zoom, and filter."
/>
</p>
</EuiText>
}
>
<EuiIcon type="questionInCircle" color="subdued" />
</EuiToolTip>
</>
</EuiFormRow>
{sigmaInput}
</Fragment>
);
}
function renderPercentilesForm() {
function onPercentilesChange(percentiles: number[]) {
// @ts-expect-error
props.onChange({
fieldMetaOptions: {
...props.fieldMetaOptions,
percentiles: _.uniq(percentiles.sort()),
},
});
}
return (
<PercentilesForm
initialPercentiles={
props.fieldMetaOptions.percentiles
? props.fieldMetaOptions.percentiles
: DEFAULT_PERCENTILES
}
onChange={onPercentilesChange}
/>
);
}
const dataMappingOptions = DATA_MAPPING_OPTIONS.filter((option) => {
return props.supportedDataMappingFunctions.includes(option.value);
});
return (
<DataMappingPopover>
<Fragment>
<EuiFormRow
label={i18n.translate('xpack.maps.styles.ordinalDataMapping.dataMappingLabel', {
defaultMessage: 'Fitting',
})}
helpText={i18n.translate(
'xpack.maps.styles.ordinalDataMapping.dataMappingTooltipContent',
{
defaultMessage: `Fit values from the data domain to the style`,
}
)}
>
<EuiSuperSelect
options={dataMappingOptions}
valueOfSelected={props.dataMappingFunction}
onChange={onDataMappingFunctionChange}
itemLayoutAlign="top"
hasDividers
/>
</EuiFormRow>
<EuiHorizontalRule />
{props.dataMappingFunction === DATA_MAPPING_FUNCTION.PERCENTILES
? renderPercentilesForm()
: renderEasingForm()}
</Fragment>
</DataMappingPopover>
);
}

View file

@ -0,0 +1,126 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import _ from 'lodash';
import React, { ChangeEvent, Component } from 'react';
import { EuiFieldNumber, EuiFormRow } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { RowActionButtons } from '../row_action_buttons';
interface Props {
initialPercentiles: number[];
onChange: (percentiles: number[]) => void;
}
interface State {
percentiles: Array<number | string>;
}
function isInvalidPercentile(percentile: unknown) {
if (typeof percentile !== 'number') {
return true;
}
return percentile <= 0 || percentile >= 100;
}
export class PercentilesForm extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
percentiles: props.initialPercentiles,
};
}
_onSubmit = () => {
const hasInvalidPercentile = this.state.percentiles.some(isInvalidPercentile);
if (!hasInvalidPercentile) {
this.props.onChange(this.state.percentiles as number[]);
}
};
render() {
const rows = this.state.percentiles.map((percentile: number | string, index: number) => {
const onAdd = () => {
let newPercentile: number | string = '';
if (typeof percentile === 'number') {
let delta = 1;
if (index === this.state.percentiles.length - 1) {
// Adding row to end of list.
if (index !== 0) {
const prevPercentile = this.state.percentiles[index - 1];
if (typeof prevPercentile === 'number') {
delta = percentile - prevPercentile;
}
}
} else {
// Adding row in middle of list.
const nextPercentile = this.state.percentiles[index + 1];
if (typeof nextPercentile === 'number') {
delta = (nextPercentile - percentile) / 2;
}
}
newPercentile = percentile + delta;
if (newPercentile >= 100) {
newPercentile = 99;
}
}
const percentiles = [
...this.state.percentiles.slice(0, index + 1),
newPercentile,
...this.state.percentiles.slice(index + 1),
];
this.setState({ percentiles }, this._onSubmit);
};
const onRemove = () => {
const percentiles =
this.state.percentiles.length === 1
? this.state.percentiles
: [
...this.state.percentiles.slice(0, index),
...this.state.percentiles.slice(index + 1),
];
this.setState({ percentiles }, this._onSubmit);
};
const onPercentileChange = (event: ChangeEvent<HTMLInputElement>) => {
const sanitizedValue = parseFloat(event.target.value);
const percentiles = [...this.state.percentiles];
percentiles[index] = isNaN(sanitizedValue) ? '' : sanitizedValue;
this.setState({ percentiles }, this._onSubmit);
};
const isInvalid = isInvalidPercentile(percentile);
const error = isInvalid
? i18n.translate('xpack.maps.styles.invalidPercentileMsg', {
defaultMessage: `Percentile must be a number between 0 and 100, exclusive`,
})
: null;
return (
<EuiFormRow key={index} display="rowCompressed" isInvalid={isInvalid} error={error}>
<EuiFieldNumber
isInvalid={isInvalid}
value={percentile}
onChange={onPercentileChange}
append={
<RowActionButtons
onAdd={onAdd}
onRemove={onRemove}
showDeleteButton={this.state.percentiles.length > 1}
/>
}
compressed
/>
</EuiFormRow>
);
});
return <div>{rows}</div>;
}
}

View file

@ -1,43 +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;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import React from 'react';
import { EuiFormRow, EuiSwitch, EuiSwitchEvent } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FieldMetaPopover } from './field_meta_popover';
import { FieldMetaOptions } from '../../../../../../common/descriptor_types';
type Props = {
fieldMetaOptions: FieldMetaOptions;
onChange: (fieldMetaOptions: FieldMetaOptions) => void;
switchDisabled: boolean;
};
export function CategoricalFieldMetaPopover(props: Props) {
const onIsEnabledChange = (event: EuiSwitchEvent) => {
props.onChange({
...props.fieldMetaOptions,
isEnabled: event.target.checked,
});
};
return (
<FieldMetaPopover>
<EuiFormRow display="columnCompressedSwitch">
<EuiSwitch
label={i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.categoricalLabel', {
defaultMessage: 'Get categories from indices',
})}
checked={props.fieldMetaOptions.isEnabled}
onChange={onIsEnabledChange}
compressed
disabled={props.switchDisabled}
/>
</EuiFormRow>
</FieldMetaPopover>
);
}

View file

@ -1,95 +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;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import _ from 'lodash';
import React, { ChangeEvent, Fragment, MouseEvent } from 'react';
import { EuiFormRow, EuiRange, EuiSwitch, EuiSwitchEvent } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { DEFAULT_SIGMA } from '../../vector_style_defaults';
import { FieldMetaPopover } from './field_meta_popover';
import { FieldMetaOptions } from '../../../../../../common/descriptor_types';
import { VECTOR_STYLES } from '../../../../../../common/constants';
function getIsEnableToggleLabel(styleName: string) {
switch (styleName) {
case VECTOR_STYLES.FILL_COLOR:
case VECTOR_STYLES.LINE_COLOR:
return i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.colorLabel', {
defaultMessage: 'Calculate color ramp range from indices',
});
case VECTOR_STYLES.LINE_WIDTH:
return i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.widthLabel', {
defaultMessage: 'Calculate border width range from indices',
});
case VECTOR_STYLES.ICON_SIZE:
return i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.sizeLabel', {
defaultMessage: 'Calculate symbol size range from indices',
});
default:
return i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.defaultLabel', {
defaultMessage: 'Calculate symbolization range from indices',
});
}
}
type Props = {
fieldMetaOptions: FieldMetaOptions;
styleName: VECTOR_STYLES;
onChange: (fieldMetaOptions: FieldMetaOptions) => void;
switchDisabled: boolean;
};
export function OrdinalFieldMetaPopover(props: Props) {
const onIsEnabledChange = (event: EuiSwitchEvent) => {
props.onChange({
...props.fieldMetaOptions,
isEnabled: event.target.checked,
});
};
const onSigmaChange = (event: ChangeEvent<HTMLInputElement> | MouseEvent<HTMLButtonElement>) => {
props.onChange({
...props.fieldMetaOptions,
sigma: parseInt(event.currentTarget.value, 10),
});
};
return (
<FieldMetaPopover>
<Fragment>
<EuiFormRow display="columnCompressedSwitch">
<EuiSwitch
label={getIsEnableToggleLabel(props.styleName)}
checked={props.fieldMetaOptions.isEnabled}
onChange={onIsEnabledChange}
compressed
disabled={props.switchDisabled}
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.maps.styles.fieldMetaOptions.sigmaLabel', {
defaultMessage: 'Sigma',
})}
display="columnCompressed"
>
<EuiRange
min={1}
max={5}
step={0.25}
value={_.get(props.fieldMetaOptions, 'sigma', DEFAULT_SIGMA)}
onChange={onSigmaChange}
disabled={!props.fieldMetaOptions.isEnabled}
showTicks
tickInterval={1}
compressed
/>
</EuiFormRow>
</Fragment>
</FieldMetaPopover>
);
}

View file

@ -12,7 +12,7 @@ import { IDynamicStyleProperty } from '../../properties/dynamic_style_property';
const EMPTY_VALUE = '';
interface Break {
export interface Break {
color: string;
label: ReactElement<any> | string | number;
symbolId?: string;

View file

@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import _ from 'lodash';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButtonIcon } from '@elastic/eui';
const ADD_BUTTON_TITLE = i18n.translate('xpack.maps.addBtnTitle', {
defaultMessage: 'Add',
});
const DELETE_BUTTON_TITLE = i18n.translate('xpack.maps.deleteBtnTitle', {
defaultMessage: 'Delete',
});
export const RowActionButtons = ({
onAdd,
onRemove,
showDeleteButton,
}: {
onAdd: () => void;
onRemove: () => void;
showDeleteButton: boolean;
}) => {
return (
<div>
{showDeleteButton ? (
<EuiButtonIcon
iconType="trash"
color="danger"
aria-label={DELETE_BUTTON_TITLE}
title={DELETE_BUTTON_TITLE}
onClick={onRemove}
/>
) : null}
<EuiButtonIcon
iconType="plusInCircle"
color="primary"
aria-label={ADD_BUTTON_TITLE}
title={ADD_BUTTON_TITLE}
onClick={onAdd}
/>
</div>
);
};

View file

@ -16,7 +16,6 @@ import {
import { i18n } from '@kbn/i18n';
import { getVectorStyleLabel, getDisabledByMessage } from './get_vector_style_label';
import { STYLE_TYPE, VECTOR_STYLES } from '../../../../../common/constants';
import { FieldMetaOptions } from '../../../../../common/descriptor_types';
import { IStyleProperty } from '../properties/style_property';
import { StyleField } from '../style_fields_helper';
@ -59,10 +58,10 @@ export class StylePropEditor<StaticOptions, DynamicOptions> extends Component<
}
};
_onFieldMetaOptionsChange = (fieldMetaOptions: FieldMetaOptions) => {
_onDataMappingChange = (updatedObjects: Partial<DynamicOptions>) => {
const options = {
...(this.props.styleProperty.getOptions() as DynamicOptions),
fieldMetaOptions,
...updatedObjects,
};
this.props.onDynamicStyleChange(this.props.styleProperty.getStyleName(), options);
};
@ -101,10 +100,6 @@ export class StylePropEditor<StaticOptions, DynamicOptions> extends Component<
}
render() {
const fieldMetaOptionsPopover = this.props.styleProperty.renderFieldMetaPopover(
this._onFieldMetaOptionsChange
);
const staticDynamicSelect = this.renderStaticDynamicSelect();
const stylePropertyForm =
@ -127,7 +122,9 @@ export class StylePropEditor<StaticOptions, DynamicOptions> extends Component<
{React.cloneElement(this.props.children, {
staticDynamicSelect,
})}
{fieldMetaOptionsPopover}
{(this.props.styleProperty as IStyleProperty<DynamicOptions>).renderDataMappingPopover(
this._onDataMappingChange
)}
</Fragment>
);

View file

@ -1,8 +1,48 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`categorical Should render categorical legend with breaks from custom 1`] = `""`;
exports[`renderDataMappingPopover Should disable toggle when field is not backed by geojson source 1`] = `
<OrdinalDataMappingPopover
dataMappingFunction="INTERPOLATE"
fieldMetaOptions={
Object {
"isEnabled": true,
}
}
onChange={[Function]}
styleName="lineColor"
supportedDataMappingFunctions={
Array [
"INTERPOLATE",
"PERCENTILES",
]
}
switchDisabled={true}
/>
`;
exports[`categorical Should render categorical legend with breaks from default 1`] = `
exports[`renderDataMappingPopover Should enable toggle when field is backed by geojson-source 1`] = `
<OrdinalDataMappingPopover
dataMappingFunction="INTERPOLATE"
fieldMetaOptions={
Object {
"isEnabled": true,
}
}
onChange={[Function]}
styleName="lineColor"
supportedDataMappingFunctions={
Array [
"INTERPOLATE",
"PERCENTILES",
]
}
switchDisabled={false}
/>
`;
exports[`renderLegendDetailRow categorical Should render categorical legend with breaks from custom 1`] = `""`;
exports[`renderLegendDetailRow categorical Should render categorical legend with breaks from default 1`] = `
<div>
<EuiFlexGroup
gutterSize="xs"
@ -82,7 +122,7 @@ exports[`categorical Should render categorical legend with breaks from default 1
</div>
`;
exports[`ordinal Should render custom ordinal legend with breaks 1`] = `
exports[`renderLegendDetailRow ordinal Should render custom ordinal legend with breaks 1`] = `
<div>
<EuiFlexGroup
gutterSize="xs"
@ -145,59 +185,7 @@ exports[`ordinal Should render custom ordinal legend with breaks 1`] = `
</div>
`;
exports[`ordinal Should render only single band of last color when delta is 0 1`] = `
<div>
<EuiFlexGroup
gutterSize="xs"
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiToolTip
content="foobar_label"
delay="regular"
position="top"
title="Border color"
>
<EuiText
className="eui-textTruncate"
size="xs"
style={
Object {
"maxWidth": "180px",
}
}
>
<small>
<strong>
foobar_label
</strong>
</small>
</EuiText>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
direction="column"
gutterSize="none"
>
<EuiFlexItem
key="0"
>
<Category
color="#6092c0"
isLinesOnly={false}
isPointsOnly={true}
label="100_format"
styleName="lineColor"
/>
</EuiFlexItem>
</EuiFlexGroup>
</div>
`;
exports[`ordinal Should render ordinal legend as bands 1`] = `
exports[`renderLegendDetailRow ordinal Should render interpolate bands 1`] = `
<div>
<EuiFlexGroup
gutterSize="xs"
@ -326,28 +314,161 @@ exports[`ordinal Should render ordinal legend as bands 1`] = `
</div>
`;
exports[`renderFieldMetaPopover Should disable toggle when field is not backed by geojson source 1`] = `
<OrdinalFieldMetaPopover
fieldMetaOptions={
Object {
"isEnabled": true,
}
}
onChange={[Function]}
styleName="lineColor"
switchDisabled={true}
/>
exports[`renderLegendDetailRow ordinal Should render percentile bands 1`] = `
<div>
<EuiFlexGroup
gutterSize="xs"
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiToolTip
content="foobar_label"
delay="regular"
position="top"
title="Border color"
>
<EuiText
className="eui-textTruncate"
size="xs"
style={
Object {
"maxWidth": "180px",
}
}
>
<small>
<strong>
foobar_label
</strong>
</small>
</EuiText>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
direction="column"
gutterSize="none"
>
<EuiFlexItem
key="0"
>
<Category
color="#e5ecf4"
isLinesOnly={false}
isPointsOnly={true}
label="up to 50th: 5572_format"
styleName="lineColor"
/>
</EuiFlexItem>
<EuiFlexItem
key="1"
>
<Category
color="#ccd9ea"
isLinesOnly={false}
isPointsOnly={true}
label="50th: 5572_format up to 75th: 8079_format"
styleName="lineColor"
/>
</EuiFlexItem>
<EuiFlexItem
key="2"
>
<Category
color="#b2c7df"
isLinesOnly={false}
isPointsOnly={true}
label="75th: 8079_format up to 90th: 9607_format"
styleName="lineColor"
/>
</EuiFlexItem>
<EuiFlexItem
key="3"
>
<Category
color="#98b5d5"
isLinesOnly={false}
isPointsOnly={true}
label="90th: 9607_format up to 95th: 10439_format"
styleName="lineColor"
/>
</EuiFlexItem>
<EuiFlexItem
key="4"
>
<Category
color="#7da3ca"
isLinesOnly={false}
isPointsOnly={true}
label="95th: 10439_format up to 99th: 16857_format"
styleName="lineColor"
/>
</EuiFlexItem>
<EuiFlexItem
key="5"
>
<Category
color="#6092c0"
isLinesOnly={false}
isPointsOnly={true}
label="greater than 99th: 16857_format"
styleName="lineColor"
/>
</EuiFlexItem>
</EuiFlexGroup>
</div>
`;
exports[`renderFieldMetaPopover Should enable toggle when field is backed by geojson-source 1`] = `
<OrdinalFieldMetaPopover
fieldMetaOptions={
Object {
"isEnabled": true,
}
}
onChange={[Function]}
styleName="lineColor"
switchDisabled={false}
/>
exports[`renderLegendDetailRow ordinal Should render single band when interpolate range is 0 1`] = `
<div>
<EuiFlexGroup
gutterSize="xs"
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiToolTip
content="foobar_label"
delay="regular"
position="top"
title="Border color"
>
<EuiText
className="eui-textTruncate"
size="xs"
style={
Object {
"maxWidth": "180px",
}
}
>
<small>
<strong>
foobar_label
</strong>
</small>
</EuiText>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
direction="column"
gutterSize="none"
>
<EuiFlexItem
key="0"
>
<Category
color="#6092c0"
isLinesOnly={false}
isPointsOnly={true}
label="100_format"
styleName="lineColor"
/>
</EuiFlexItem>
</EuiFlexGroup>
</div>
`;

View file

@ -15,7 +15,12 @@ import { shallow } from 'enzyme';
import { Feature, Point } from 'geojson';
import { DynamicColorProperty } from './dynamic_color_property';
import { COLOR_MAP_TYPE, RawValue, VECTOR_STYLES } from '../../../../../common/constants';
import {
COLOR_MAP_TYPE,
RawValue,
DATA_MAPPING_FUNCTION,
VECTOR_STYLES,
} from '../../../../../common/constants';
import { mockField, MockLayer, MockStyle } from './__tests__/test_util';
import { ColorDynamicOptions } from '../../../../../common/descriptor_types';
import { IVectorLayer } from '../../../layers/vector_layer/vector_layer';
@ -40,125 +45,164 @@ const defaultLegendParams = {
const fieldMetaOptions = { isEnabled: true };
describe('ordinal', () => {
test('Should render ordinal legend as bands', async () => {
const colorStyle = makeProperty({
color: 'Blues',
type: undefined,
fieldMetaOptions,
});
const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
const component = shallow(legendRow);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});
test('Should render only single band of last color when delta is 0', async () => {
const colorStyle = makeProperty(
{
describe('renderLegendDetailRow', () => {
describe('ordinal', () => {
test('Should render interpolate bands', async () => {
const colorStyle = makeProperty({
color: 'Blues',
type: undefined,
fieldMetaOptions,
},
new MockStyle({ min: 100, max: 100 })
);
});
const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
const component = shallow(legendRow);
const component = shallow(legendRow);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});
test('Should render custom ordinal legend with breaks', async () => {
const colorStyle = makeProperty({
type: COLOR_MAP_TYPE.ORDINAL,
useCustomColorRamp: true,
customColorRamp: [
{
stop: 0,
color: '#FF0000',
},
{
stop: 10,
color: '#00FF00',
},
],
fieldMetaOptions,
expect(component).toMatchSnapshot();
});
const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
test('Should render single band when interpolate range is 0', async () => {
const colorStyle = makeProperty({
color: 'Blues',
type: undefined,
fieldMetaOptions,
});
colorStyle.getRangeFieldMeta = () => {
return {
min: 100,
max: 100,
delta: 0,
};
};
const component = shallow(legendRow);
const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
const component = shallow(legendRow);
expect(component).toMatchSnapshot();
});
});
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
describe('categorical', () => {
test('Should render categorical legend with breaks from default', async () => {
const colorStyle = makeProperty({
type: COLOR_MAP_TYPE.CATEGORICAL,
useCustomColorPalette: false,
colorCategory: 'palette_0',
fieldMetaOptions,
expect(component).toMatchSnapshot();
});
const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
const component = shallow(legendRow);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});
test('Should render categorical legend with breaks from custom', async () => {
const colorStyle = makeProperty({
type: COLOR_MAP_TYPE.CATEGORICAL,
useCustomColorPalette: true,
customColorPalette: [
{
stop: null, // should include the default stop
color: '#FFFF00',
test('Should render percentile bands', async () => {
const colorStyle = makeProperty({
color: 'Blues',
type: undefined,
dataMappingFunction: DATA_MAPPING_FUNCTION.PERCENTILES,
fieldMetaOptions: {
isEnabled: true,
percentiles: [50, 75, 90, 95, 99],
},
{
stop: 'US_STOP',
color: '#FF0000',
},
{
stop: 'CN_STOP',
color: '#00FF00',
},
],
fieldMetaOptions,
});
colorStyle.getPercentilesFieldMeta = () => {
return [
{ percentile: '0.0', value: 0 },
{ percentile: '50.0', value: 5571.815277777777 },
{ percentile: '75.0', value: 8078.703125 },
{ percentile: '90.0', value: 9607.2 },
{ percentile: '95.0', value: 10439.083333333334 },
{ percentile: '99.0', value: 16856.5 },
];
};
const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
const component = shallow(legendRow);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});
const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
test('Should render custom ordinal legend with breaks', async () => {
const colorStyle = makeProperty({
type: COLOR_MAP_TYPE.ORDINAL,
useCustomColorRamp: true,
customColorRamp: [
{
stop: 0,
color: '#FF0000',
},
{
stop: 10,
color: '#00FF00',
},
],
fieldMetaOptions,
});
const component = shallow(legendRow);
const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
expect(component).toMatchSnapshot();
const component = shallow(legendRow);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});
});
describe('categorical', () => {
test('Should render categorical legend with breaks from default', async () => {
const colorStyle = makeProperty({
type: COLOR_MAP_TYPE.CATEGORICAL,
useCustomColorPalette: false,
colorCategory: 'palette_0',
fieldMetaOptions,
});
const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
const component = shallow(legendRow);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});
test('Should render categorical legend with breaks from custom', async () => {
const colorStyle = makeProperty({
type: COLOR_MAP_TYPE.CATEGORICAL,
useCustomColorPalette: true,
customColorPalette: [
{
stop: null, // should include the default stop
color: '#FFFF00',
},
{
stop: 'US_STOP',
color: '#FF0000',
},
{
stop: 'CN_STOP',
color: '#00FF00',
},
],
fieldMetaOptions,
});
const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams);
const component = shallow(legendRow);
expect(component).toMatchSnapshot();
});
});
});
@ -204,7 +248,7 @@ test('Should pluck the categorical style-meta from fieldmeta', async () => {
});
const meta = colorStyle._pluckCategoricalStyleMetaFromFieldMetaData({
foobar: {
foobar_terms: {
buckets: [
{
key: 'CN',
@ -578,7 +622,7 @@ test('Should read out ordinal type correctly', async () => {
expect(ordinalColorStyle2.isCategorical()).toEqual(false);
});
describe('renderFieldMetaPopover', () => {
describe('renderDataMappingPopover', () => {
test('Should enable toggle when field is backed by geojson-source', () => {
const colorStyle = makeProperty(
{
@ -590,7 +634,7 @@ describe('renderFieldMetaPopover', () => {
mockField
);
const legendRow = colorStyle.renderFieldMetaPopover(() => {});
const legendRow = colorStyle.renderDataMappingPopover(() => {});
expect(legendRow).toMatchSnapshot();
});
@ -609,7 +653,7 @@ describe('renderFieldMetaPopover', () => {
nonGeoJsonField
);
const legendRow = colorStyle.renderFieldMetaPopover(() => {});
const legendRow = colorStyle.renderDataMappingPopover(() => {});
expect(legendRow).toMatchSnapshot();
});
});

View file

@ -5,24 +5,55 @@
*/
import { Map as MbMap } from 'mapbox-gl';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { EuiTextColor } from '@elastic/eui';
import { DynamicStyleProperty } from './dynamic_style_property';
import { makeMbClampedNumberExpression, dynamicRound } from '../style_util';
import { getOrdinalMbColorRampStops, getColorPalette } from '../../color_palettes';
import { COLOR_MAP_TYPE } from '../../../../../common/constants';
import {
getOrdinalMbColorRampStops,
getPercentilesMbColorRampStops,
getColorPalette,
} from '../../color_palettes';
import { COLOR_MAP_TYPE, DATA_MAPPING_FUNCTION } from '../../../../../common/constants';
import { GREAT_THAN, UPTO } from '../../../../../common/i18n_getters';
import {
isCategoricalStopsInvalid,
getOtherCategoryLabel,
// @ts-expect-error
} from '../components/color/color_stops_utils';
import { BreakedLegend } from '../components/legend/breaked_legend';
import { Break, BreakedLegend } from '../components/legend/breaked_legend';
import { ColorDynamicOptions, OrdinalColorStop } from '../../../../../common/descriptor_types';
import { LegendProps } from './style_property';
const EMPTY_STOPS = { stops: [], defaultColor: null };
const RGBA_0000 = 'rgba(0,0,0,0)';
function getOrdinalSuffix(value: number) {
const lastDigit = value % 10;
if (lastDigit === 1 && value !== 11) {
return i18n.translate('xpack.maps.styles.firstOrdinalSuffix', {
defaultMessage: 'st',
});
}
if (lastDigit === 2 && value !== 12) {
return i18n.translate('xpack.maps.styles.secondOrdinalSuffix', {
defaultMessage: 'nd',
});
}
if (lastDigit === 3 && value !== 13) {
return i18n.translate('xpack.maps.styles.thirdOrdinalSuffix', {
defaultMessage: 'rd',
});
}
return i18n.translate('xpack.maps.styles.ordinalSuffix', {
defaultMessage: 'th',
});
}
export class DynamicColorProperty extends DynamicStyleProperty<ColorDynamicOptions> {
syncCircleColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: number) {
const color = this._getMbColor();
@ -99,6 +130,10 @@ export class DynamicColorProperty extends DynamicStyleProperty<ColorDynamicOptio
return colors ? colors.length : 0;
}
_getSupportedDataMappingFunctions(): DATA_MAPPING_FUNCTION[] {
return [DATA_MAPPING_FUNCTION.INTERPOLATE, DATA_MAPPING_FUNCTION.PERCENTILES];
}
_getMbColor() {
if (!this.getFieldName()) {
return null;
@ -131,37 +166,60 @@ export class DynamicColorProperty extends DynamicStyleProperty<ColorDynamicOptio
RGBA_0000, // MB will assign the base value to any features that is below the first stop value
...colorStops,
];
} else {
const rangeFieldMeta = this.getRangeFieldMeta();
if (!rangeFieldMeta) {
}
if (this.getDataMappingFunction() === DATA_MAPPING_FUNCTION.PERCENTILES) {
const percentilesFieldMeta = this.getPercentilesFieldMeta();
if (!percentilesFieldMeta || !percentilesFieldMeta.length) {
return null;
}
const colorStops = getOrdinalMbColorRampStops(
const colorStops = getPercentilesMbColorRampStops(
this._options.color ? this._options.color : null,
rangeFieldMeta.min,
rangeFieldMeta.max
percentilesFieldMeta
);
if (!colorStops) {
return null;
}
const lessThanFirstStopValue = rangeFieldMeta.min - 1;
const lessThanFirstStopValue = percentilesFieldMeta[0].value - 1;
return [
'interpolate',
['linear'],
makeMbClampedNumberExpression({
minValue: rangeFieldMeta.min,
maxValue: rangeFieldMeta.max,
lookupFunction: this.getMbLookupFunction(),
fallback: lessThanFirstStopValue,
fieldName: targetName,
}),
lessThanFirstStopValue,
'step',
['coalesce', [this.getMbLookupFunction(), targetName], lessThanFirstStopValue],
RGBA_0000,
...colorStops,
];
}
const rangeFieldMeta = this.getRangeFieldMeta();
if (!rangeFieldMeta) {
return null;
}
const colorStops = getOrdinalMbColorRampStops(
this._options.color ? this._options.color : null,
rangeFieldMeta.min,
rangeFieldMeta.max
);
if (!colorStops) {
return null;
}
const lessThanFirstStopValue = rangeFieldMeta.min - 1;
return [
'interpolate',
['linear'],
makeMbClampedNumberExpression({
minValue: rangeFieldMeta.min,
maxValue: rangeFieldMeta.max,
lookupFunction: this.getMbLookupFunction(),
fallback: lessThanFirstStopValue,
fieldName: targetName,
}),
lessThanFirstStopValue,
RGBA_0000,
...colorStops,
];
}
_getColorPaletteStops() {
@ -242,9 +300,65 @@ export class DynamicColorProperty extends DynamicStyleProperty<ColorDynamicOptio
return ['match', ['to-string', ['get', this.getFieldName()]], ...mbStops];
}
_getColorRampStops() {
_getOrdinalBreaks(symbolId?: string): Break[] {
if (this._options.useCustomColorRamp && this._options.customColorRamp) {
return this._options.customColorRamp;
return this._options.customColorRamp.map((ordinalColorStop) => {
return {
color: ordinalColorStop.color,
symbolId,
label: this.formatField(ordinalColorStop.stop),
};
});
}
if (this.getDataMappingFunction() === DATA_MAPPING_FUNCTION.PERCENTILES) {
const percentilesFieldMeta = this.getPercentilesFieldMeta();
if (!percentilesFieldMeta) {
return [];
}
const colorStops = getPercentilesMbColorRampStops(
this._options.color ? this._options.color : null,
percentilesFieldMeta
);
if (!colorStops || colorStops.length <= 2) {
return [];
}
const breaks = [];
const lastStopIndex = colorStops.length - 2;
for (let i = 0; i < colorStops.length; i += 2) {
const hasNext = i < lastStopIndex;
const stopValue = colorStops[i];
const formattedStopValue = this.formatField(dynamicRound(stopValue));
const color = colorStops[i + 1] as string;
const percentile = parseFloat(percentilesFieldMeta[i / 2].percentile);
const percentileLabel = `${percentile}${getOrdinalSuffix(percentile)}`;
let label = '';
if (!hasNext) {
label = `${GREAT_THAN} ${percentileLabel}: ${formattedStopValue}`;
} else {
const nextStopValue = colorStops[i + 2];
const formattedNextStopValue = this.formatField(dynamicRound(nextStopValue));
const nextPercentile = parseFloat(percentilesFieldMeta[i / 2 + 1].percentile);
const nextPercentileLabel = `${nextPercentile}${getOrdinalSuffix(nextPercentile)}`;
if (i === 0) {
label = `${UPTO} ${nextPercentileLabel}: ${formattedNextStopValue}`;
} else {
const begin = `${percentileLabel}: ${formattedStopValue}`;
const end = `${nextPercentileLabel}: ${formattedNextStopValue}`;
label = `${begin} ${UPTO} ${end}`;
}
}
breaks.push({
color,
label,
symbolId,
});
}
return breaks;
}
if (!this._options.color) {
@ -263,7 +377,8 @@ export class DynamicColorProperty extends DynamicStyleProperty<ColorDynamicOptio
return [
{
color: colors[colors.length - 1],
stop: dynamicRound(rangeFieldMeta.max),
label: this.formatField(dynamicRound(rangeFieldMeta.max)),
symbolId,
},
];
}
@ -272,27 +387,15 @@ export class DynamicColorProperty extends DynamicStyleProperty<ColorDynamicOptio
const rawStopValue = rangeFieldMeta.min + rangeFieldMeta.delta * (index / colors.length);
return {
color,
stop: dynamicRound(rawStopValue),
label: this.formatField(dynamicRound(rawStopValue)),
symbolId,
};
});
}
_getColorStops() {
if (this.isOrdinal()) {
return {
stops: this._getColorRampStops(),
defaultColor: null,
};
} else if (this.isCategorical()) {
return this._getColorPaletteStops();
} else {
return EMPTY_STOPS;
}
}
renderLegendDetailRow({ isPointsOnly, isLinesOnly, symbolId }: LegendProps) {
const { stops, defaultColor } = this._getColorStops();
const breaks = [];
_getCategoricalBreaks(symbolId?: string): Break[] {
const breaks: Break[] = [];
const { stops, defaultColor } = this._getColorPaletteStops();
stops.forEach(({ stop, color }: { stop: string | number | null; color: string }) => {
if (stop !== null) {
breaks.push({
@ -309,7 +412,16 @@ export class DynamicColorProperty extends DynamicStyleProperty<ColorDynamicOptio
symbolId,
});
}
return breaks;
}
renderLegendDetailRow({ isPointsOnly, isLinesOnly, symbolId }: LegendProps) {
let breaks: Break[] = [];
if (this.isOrdinal()) {
breaks = this._getOrdinalBreaks(symbolId);
} else if (this.isCategorical()) {
breaks = this._getCategoricalBreaks(symbolId);
}
return (
<BreakedLegend
style={this}

View file

@ -23,6 +23,10 @@ export class DynamicOrientationProperty extends DynamicStyleProperty<Orientation
}
}
supportsFieldMeta() {
return false;
}
supportsMbFeatureState(): boolean {
return false;
}

View file

@ -11,16 +11,20 @@ import { FeatureIdentifier, Map as MbMap } from 'mapbox-gl';
import { AbstractStyleProperty, IStyleProperty } from './style_property';
import { DEFAULT_SIGMA } from '../vector_style_defaults';
import {
DEFAULT_PERCENTILES,
FIELD_ORIGIN,
MB_LOOKUP_FUNCTION,
SOURCE_META_DATA_REQUEST_ID,
DATA_MAPPING_FUNCTION,
STYLE_TYPE,
VECTOR_STYLES,
RawValue,
FieldFormatter,
} from '../../../../../common/constants';
import { OrdinalFieldMetaPopover } from '../components/field_meta/ordinal_field_meta_popover';
import { CategoricalFieldMetaPopover } from '../components/field_meta/categorical_field_meta_popover';
import {
CategoricalDataMappingPopover,
OrdinalDataMappingPopover,
} from '../components/data_mapping';
import {
CategoryFieldMeta,
FieldMetaOptions,
@ -40,11 +44,14 @@ export interface IDynamicStyleProperty<T> extends IStyleProperty<T> {
getFieldOrigin(): FIELD_ORIGIN | null;
getRangeFieldMeta(): RangeFieldMeta | null;
getCategoryFieldMeta(): CategoryFieldMeta | null;
getNumberOfCategories(): number;
/*
* Returns hash that signals style meta needs to be re-fetched when value changes
*/
getStyleMetaHash(): string;
isFieldMetaEnabled(): boolean;
isOrdinal(): boolean;
supportsFieldMeta(): boolean;
getFieldMetaRequest(): Promise<unknown>;
getFieldMetaRequest(): Promise<unknown | null>;
pluckOrdinalStyleMetaFromFeatures(features: Feature[]): RangeFieldMeta | null;
pluckCategoricalStyleMetaFromFeatures(features: Feature[]): CategoryFieldMeta | null;
getValueSuggestions(query: string): Promise<string[]>;
@ -119,6 +126,35 @@ export class DynamicStyleProperty<T>
return rangeFieldMeta ? rangeFieldMeta : rangeFieldMetaFromLocalFeatures;
}
getPercentilesFieldMeta() {
if (!this._field) {
return null;
}
const dataRequestId = this._getStyleMetaDataRequestId(this.getFieldName());
if (!dataRequestId) {
return null;
}
const styleMetaDataRequest = this._layer.getDataRequest(dataRequestId);
if (!styleMetaDataRequest || !styleMetaDataRequest.hasData()) {
return null;
}
const styleMetaData = styleMetaDataRequest.getData() as StyleMetaData;
const percentiles = styleMetaData[`${this._field.getRootName()}_percentiles`] as
| undefined
| { values?: { [key: string]: number } };
return percentiles !== undefined && percentiles.values !== undefined
? Object.keys(percentiles.values).map((key) => {
return {
percentile: key,
value: percentiles.values![key],
};
})
: null;
}
getCategoryFieldMeta() {
const style = this._layer.getStyle() as IVectorStyle;
const styleMeta = style.getStyleMeta();
@ -168,6 +204,24 @@ export class DynamicStyleProperty<T>
return 0;
}
getStyleMetaHash(): string {
const fieldMetaOptions = this.getFieldMetaOptions();
const parts: string[] = [fieldMetaOptions.isEnabled.toString()];
if (this.isOrdinal()) {
const dataMappingFunction = this.getDataMappingFunction();
parts.push(dataMappingFunction);
if (
dataMappingFunction === DATA_MAPPING_FUNCTION.PERCENTILES &&
fieldMetaOptions.percentiles
) {
parts.push(fieldMetaOptions.percentiles.join(''));
}
} else if (this.isCategorical()) {
parts.push(this.getNumberOfCategories().toString());
}
return parts.join('');
}
isComplete() {
return !!this._field;
}
@ -191,13 +245,21 @@ export class DynamicStyleProperty<T>
}
if (this.isOrdinal()) {
return this._field.getOrdinalFieldMetaRequest();
} else if (this.isCategorical()) {
return this.getDataMappingFunction() === DATA_MAPPING_FUNCTION.INTERPOLATE
? this._field.getExtendedStatsFieldMetaRequest()
: this._field.getPercentilesFieldMetaRequest(
this.getFieldMetaOptions().percentiles !== undefined
? this.getFieldMetaOptions().percentiles
: DEFAULT_PERCENTILES
);
}
if (this.isCategorical()) {
const numberOfCategories = this.getNumberOfCategories();
return this._field.getCategoricalFieldMetaRequest(numberOfCategories);
} else {
return null;
}
return null;
}
supportsMbFeatureState() {
@ -214,6 +276,12 @@ export class DynamicStyleProperty<T>
return _.get(this.getOptions(), 'fieldMetaOptions', { isEnabled: true });
}
getDataMappingFunction() {
return 'dataMappingFunction' in this._options
? (this._options as T & { dataMappingFunction: DATA_MAPPING_FUNCTION }).dataMappingFunction
: DATA_MAPPING_FUNCTION.INTERPOLATE;
}
pluckOrdinalStyleMetaFromFeatures(features: Feature[]) {
if (!this.isOrdinal()) {
return null;
@ -279,7 +347,7 @@ export class DynamicStyleProperty<T>
return null;
}
const stats = styleMetaData[this._field.getRootName()];
const stats = styleMetaData[`${this._field.getRootName()}_range`];
if (!stats || !('avg' in stats)) {
return null;
}
@ -303,7 +371,7 @@ export class DynamicStyleProperty<T>
return null;
}
const fieldMeta = styleMetaData[this._field.getRootName()];
const fieldMeta = styleMetaData[`${this._field.getRootName()}_terms`];
if (!fieldMeta || !('buckets' in fieldMeta)) {
return null;
}
@ -328,7 +396,11 @@ export class DynamicStyleProperty<T>
}
}
renderFieldMetaPopover(onFieldMetaOptionsChange: (fieldMetaOptions: FieldMetaOptions) => void) {
_getSupportedDataMappingFunctions(): DATA_MAPPING_FUNCTION[] {
return [DATA_MAPPING_FUNCTION.INTERPOLATE];
}
renderDataMappingPopover(onChange: (updatedOptions: Partial<T>) => void) {
if (!this.supportsFieldMeta()) {
return null;
}
@ -336,17 +408,19 @@ export class DynamicStyleProperty<T>
const switchDisabled = !!this._field && !this._field.canReadFromGeoJson();
return this.isCategorical() ? (
<CategoricalFieldMetaPopover
<CategoricalDataMappingPopover<T>
fieldMetaOptions={this.getFieldMetaOptions()}
onChange={onFieldMetaOptionsChange}
onChange={onChange}
switchDisabled={switchDisabled}
/>
) : (
<OrdinalFieldMetaPopover
<OrdinalDataMappingPopover<T>
fieldMetaOptions={this.getFieldMetaOptions()}
styleName={this.getStyleName()}
onChange={onFieldMetaOptionsChange}
onChange={onChange}
switchDisabled={switchDisabled}
dataMappingFunction={this.getDataMappingFunction()}
supportedDataMappingFunctions={this._getSupportedDataMappingFunctions()}
/>
);
}

View file

@ -8,7 +8,6 @@
import { ReactElement } from 'react';
// @ts-ignore
import { getVectorStyleLabel } from '../components/get_vector_style_label';
import { FieldMetaOptions } from '../../../../../common/descriptor_types';
import { RawValue, VECTOR_STYLES } from '../../../../../common/constants';
export type LegendProps = {
@ -24,8 +23,8 @@ export interface IStyleProperty<T> {
getStyleName(): VECTOR_STYLES;
getOptions(): T;
renderLegendDetailRow(legendProps: LegendProps): ReactElement<any> | null;
renderFieldMetaPopover(
onFieldMetaOptionsChange: (fieldMetaOptions: FieldMetaOptions) => void
renderDataMappingPopover(
onChange: (updatedOptions: Partial<T>) => void
): ReactElement<any> | null;
getDisplayStyleName(): string;
}
@ -75,8 +74,8 @@ export class AbstractStyleProperty<T> implements IStyleProperty<T> {
return null;
}
renderFieldMetaPopover(
onFieldMetaOptionsChange: (fieldMetaOptions: FieldMetaOptions) => void
renderDataMappingPopover(
onChange: (updatedOptions: Partial<T>) => void
): ReactElement<any> | null {
return null;
}

View file

@ -11425,12 +11425,7 @@
"xpack.maps.styles.dynamicColorSelect.qualitativeOrQuantitativeAriaLabel": "「番号として」を選択して色範囲内の番号でマップするか、または「カテゴリーとして」を選択してカラーパレットで分類します。",
"xpack.maps.styles.dynamicColorSelect.quantitativeLabel": "番号として",
"xpack.maps.styles.fieldMetaOptions.isEnabled.categoricalLabel": "インデックスからカテゴリーを取得",
"xpack.maps.styles.fieldMetaOptions.isEnabled.colorLabel": "インデックスからカラーランプ範囲を計算",
"xpack.maps.styles.fieldMetaOptions.isEnabled.defaultLabel": "インデックスからシンボル化範囲を計算",
"xpack.maps.styles.fieldMetaOptions.isEnabled.sizeLabel": "インデックスからシンボルサイズを計算",
"xpack.maps.styles.fieldMetaOptions.isEnabled.widthLabel": "インデックスから枠線幅を計算",
"xpack.maps.styles.fieldMetaOptions.popoverToggle": "フィールドメタオプションポップオーバー切り替え",
"xpack.maps.styles.fieldMetaOptions.sigmaLabel": "シグマ",
"xpack.maps.styles.icon.customMapLabel": "カスタムアイコンパレット",
"xpack.maps.styles.iconStops.deleteButtonAriaLabel": "削除",
"xpack.maps.styles.iconStops.deleteButtonLabel": "削除",

View file

@ -11438,12 +11438,7 @@
"xpack.maps.styles.dynamicColorSelect.qualitativeOrQuantitativeAriaLabel": "选择`作为数字`以在颜色范围中按数字映射,或选择`作为类别`以按调色板归类。",
"xpack.maps.styles.dynamicColorSelect.quantitativeLabel": "作为数字",
"xpack.maps.styles.fieldMetaOptions.isEnabled.categoricalLabel": "从索引获取类别",
"xpack.maps.styles.fieldMetaOptions.isEnabled.colorLabel": "从索引计算颜色渐变范围",
"xpack.maps.styles.fieldMetaOptions.isEnabled.defaultLabel": "从索引计算符号化范围",
"xpack.maps.styles.fieldMetaOptions.isEnabled.sizeLabel": "从索引计算符号大小范围",
"xpack.maps.styles.fieldMetaOptions.isEnabled.widthLabel": "从索引计算边框宽度范围",
"xpack.maps.styles.fieldMetaOptions.popoverToggle": "字段元数据选项弹出框切换",
"xpack.maps.styles.fieldMetaOptions.sigmaLabel": "Sigma",
"xpack.maps.styles.icon.customMapLabel": "定制图标调色板",
"xpack.maps.styles.iconStops.deleteButtonAriaLabel": "删除",
"xpack.maps.styles.iconStops.deleteButtonLabel": "删除",