[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:
parent
b3bccc2816
commit
0eee8a2a86
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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%,`;
|
||||
|
|
|
@ -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)",
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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}
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -23,6 +23,10 @@ export class DynamicOrientationProperty extends DynamicStyleProperty<Orientation
|
|||
}
|
||||
}
|
||||
|
||||
supportsFieldMeta() {
|
||||
return false;
|
||||
}
|
||||
|
||||
supportsMbFeatureState(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -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()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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": "削除",
|
||||
|
|
|
@ -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": "删除",
|
||||
|
|
Loading…
Reference in a new issue