[Maps] Support categorical styling for numbers and dates (#57908)
This commit is contained in:
parent
4bd7b36431
commit
6e5e8c815e
|
@ -165,6 +165,7 @@ export const COLOR_MAP_TYPE = {
|
|||
export const COLOR_PALETTE_MAX_SIZE = 10;
|
||||
|
||||
export const CATEGORICAL_DATA_TYPES = ['string', 'ip', 'boolean'];
|
||||
export const ORDINAL_DATA_TYPES = ['number', 'date'];
|
||||
|
||||
export const SYMBOLIZE_AS_TYPES = {
|
||||
CIRCLE: 'circle',
|
||||
|
|
|
@ -35,11 +35,11 @@ export type AggDescriptor = {
|
|||
type: AGG_TYPE;
|
||||
};
|
||||
|
||||
export type AbstractESAggDescriptor = AbstractESSourceDescriptor & {
|
||||
export type AbstractESAggSourceDescriptor = AbstractESSourceDescriptor & {
|
||||
metrics: AggDescriptor[];
|
||||
};
|
||||
|
||||
export type ESGeoGridSourceDescriptor = AbstractESAggDescriptor & {
|
||||
export type ESGeoGridSourceDescriptor = AbstractESAggSourceDescriptor & {
|
||||
requestType?: RENDER_AS;
|
||||
resolution?: GRID_RESOLUTION;
|
||||
};
|
||||
|
@ -54,12 +54,12 @@ export type ESSearchSourceDescriptor = AbstractESSourceDescriptor & {
|
|||
topHitsSize?: number;
|
||||
};
|
||||
|
||||
export type ESPewPewSourceDescriptor = AbstractESAggDescriptor & {
|
||||
export type ESPewPewSourceDescriptor = AbstractESAggSourceDescriptor & {
|
||||
sourceGeoField: string;
|
||||
destGeoField: string;
|
||||
};
|
||||
|
||||
export type ESTermSourceDescriptor = AbstractESAggDescriptor & {
|
||||
export type ESTermSourceDescriptor = AbstractESAggSourceDescriptor & {
|
||||
indexPatternTitle: string;
|
||||
term: string; // term field name
|
||||
};
|
||||
|
|
|
@ -57,11 +57,6 @@ export class ESDocField extends AbstractField {
|
|||
|
||||
async getCategoricalFieldMetaRequest() {
|
||||
const field = await this._getField();
|
||||
if (field.type !== 'string') {
|
||||
//UX does not support categorical styling for number/date fields
|
||||
return null;
|
||||
}
|
||||
|
||||
const topTerms = {
|
||||
size: COLOR_PALETTE_MAX_SIZE - 1, //need additional color for the "other"-value
|
||||
};
|
||||
|
|
|
@ -332,18 +332,6 @@ export class AbstractLayer {
|
|||
return [];
|
||||
}
|
||||
|
||||
async getDateFields() {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getNumberFields() {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getCategoricalFields() {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getFields() {
|
||||
return [];
|
||||
}
|
||||
|
|
|
@ -7,13 +7,19 @@
|
|||
import { IESSource } from './es_source';
|
||||
import { AbstractESSource } from './es_source';
|
||||
import { AGG_TYPE } from '../../../common/constants';
|
||||
import { IESAggField } from '../fields/es_agg_field';
|
||||
import { AbstractESAggSourceDescriptor } from '../../../common/descriptor_types';
|
||||
|
||||
export interface IESAggSource extends IESSource {
|
||||
getAggKey(aggType: AGG_TYPE, fieldName: string): string;
|
||||
getAggLabel(aggType: AGG_TYPE, fieldName: string): string;
|
||||
getMetricFields(): IESAggField[];
|
||||
}
|
||||
|
||||
export class AbstractESAggSource extends AbstractESSource implements IESAggSource {
|
||||
constructor(sourceDescriptor: AbstractESAggSourceDescriptor, inspectorAdapters: object);
|
||||
|
||||
getAggKey(aggType: AGG_TYPE, fieldName: string): string;
|
||||
getAggLabel(aggType: AGG_TYPE, fieldName: string): string;
|
||||
getMetricFields(): IESAggField[];
|
||||
}
|
||||
|
|
|
@ -78,6 +78,10 @@ export class AbstractESAggSource extends AbstractESSource {
|
|||
}
|
||||
}
|
||||
|
||||
async getFields() {
|
||||
return this.getMetricFields();
|
||||
}
|
||||
|
||||
getValueAggsDsl(indexPattern) {
|
||||
const valueAggsDsl = {};
|
||||
this.getMetricFields().forEach(esAggMetric => {
|
||||
|
@ -89,10 +93,6 @@ export class AbstractESAggSource extends AbstractESSource {
|
|||
return valueAggsDsl;
|
||||
}
|
||||
|
||||
async getNumberFields() {
|
||||
return this.getMetricFields();
|
||||
}
|
||||
|
||||
async filterAndFormatPropertiesToHtmlForMetricFields(properties) {
|
||||
const metricFields = this.getMetricFields();
|
||||
const tooltipPropertiesPromises = [];
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 { AbstractESAggSource } from './es_agg_source';
|
||||
import { IField } from '../fields/field';
|
||||
import { IESAggField } from '../fields/es_agg_field';
|
||||
import _ from 'lodash';
|
||||
import { AGG_TYPE } from '../../../common/constants';
|
||||
import { AggDescriptor } from '../../../common/descriptor_types';
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
|
||||
const sumFieldName = 'myFieldGettingSummed';
|
||||
const metricExamples = [
|
||||
{
|
||||
type: AGG_TYPE.SUM,
|
||||
field: sumFieldName,
|
||||
label: 'my custom label',
|
||||
},
|
||||
{
|
||||
// metric config is invalid beause field is missing
|
||||
type: AGG_TYPE.MAX,
|
||||
},
|
||||
{
|
||||
// metric config is valid because "count" metric does not need to provide field
|
||||
type: AGG_TYPE.COUNT,
|
||||
label: '', // should ignore empty label fields
|
||||
},
|
||||
];
|
||||
|
||||
class TestESAggSource extends AbstractESAggSource {
|
||||
constructor(metrics: AggDescriptor[]) {
|
||||
super({ type: 'test', id: 'foobar', indexPatternId: 'foobarid', metrics }, []);
|
||||
}
|
||||
}
|
||||
|
||||
describe('getMetricFields', () => {
|
||||
it('should add default "count" metric when no metrics are provided', async () => {
|
||||
const source = new TestESAggSource([]);
|
||||
const metrics = source.getMetricFields();
|
||||
expect(metrics.length).toBe(1);
|
||||
|
||||
expect(metrics[0].getName()).toEqual('doc_count');
|
||||
expect(await metrics[0].getLabel()).toEqual('count');
|
||||
});
|
||||
|
||||
it('should remove incomplete metric configurations', async () => {
|
||||
const source = new TestESAggSource(metricExamples);
|
||||
const metrics = source.getMetricFields();
|
||||
expect(metrics.length).toBe(2);
|
||||
|
||||
expect(metrics[0].getRootName()).toEqual(sumFieldName);
|
||||
expect(metrics[0].getName()).toEqual('sum_of_myFieldGettingSummed');
|
||||
expect(await metrics[0].getLabel()).toEqual('my custom label');
|
||||
|
||||
expect(metrics[1].getName()).toEqual('doc_count');
|
||||
expect(await metrics[1].getLabel()).toEqual('count');
|
||||
});
|
||||
|
||||
it('getMetrics should be identical to getFields', async () => {
|
||||
const source = new TestESAggSource(metricExamples);
|
||||
const metrics = source.getMetricFields();
|
||||
const fields = await source.getFields();
|
||||
|
||||
const getFieldMeta = async (field: IField) => {
|
||||
const esAggField = field as IESAggField; // this ensures we can downcast correctly.
|
||||
return {
|
||||
name: esAggField.getName(),
|
||||
label: await esAggField.getLabel(),
|
||||
esDoc: esAggField.getRootName(),
|
||||
};
|
||||
};
|
||||
|
||||
const metricsFieldMeta = await Promise.all(metrics.map(getFieldMeta));
|
||||
const fieldsFieldMeta = await Promise.all(fields.map(getFieldMeta));
|
||||
|
||||
expect(_.isEqual(metricsFieldMeta, fieldsFieldMeta)).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -6,7 +6,10 @@
|
|||
|
||||
import { AbstractESAggSource } from '../es_agg_source';
|
||||
import { ESGeoGridSourceDescriptor } from '../../../../common/descriptor_types';
|
||||
import { GRID_RESOLUTION } from '../../../../common/constants';
|
||||
|
||||
export class ESGeoGridSource extends AbstractESAggSource {
|
||||
constructor(sourceDescriptor: ESGeoGridSourceDescriptor, inspectorAdapters: unknown);
|
||||
getGridResolution(): GRID_RESOLUTION;
|
||||
getGeoGridPrecision(zoom: number): number;
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ import { DynamicStyleProperty } from '../../styles/vector/properties/dynamic_sty
|
|||
import { StaticStyleProperty } from '../../styles/vector/properties/static_style_property';
|
||||
import { DataRequestAbortError } from '../../util/data_request';
|
||||
|
||||
const MAX_GEOTILE_LEVEL = 29;
|
||||
export const MAX_GEOTILE_LEVEL = 29;
|
||||
|
||||
export class ESGeoGridSource extends AbstractESAggSource {
|
||||
static type = ES_GEO_GRID;
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
jest.mock('../../../kibana_services', () => {});
|
||||
jest.mock('ui/new_platform');
|
||||
|
||||
import { ESGeoGridSource } from './es_geo_grid_source';
|
||||
import { ES_GEO_GRID, GRID_RESOLUTION, RENDER_AS } from '../../../../common/constants';
|
||||
|
||||
describe('ESGeoGridSource', () => {
|
||||
const geogridSource = new ESGeoGridSource(
|
||||
{
|
||||
id: 'foobar',
|
||||
indexPatternId: 'fooIp',
|
||||
geoField: 'bar',
|
||||
metrics: [],
|
||||
resolution: GRID_RESOLUTION.COARSE,
|
||||
type: ES_GEO_GRID,
|
||||
requestType: RENDER_AS.HEATMAP,
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
describe('getGridResolution', () => {
|
||||
it('should echo gridResoltuion', () => {
|
||||
expect(geogridSource.getGridResolution()).toBe(GRID_RESOLUTION.COARSE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGeoGridPrecision', () => {
|
||||
it('should clamp geo-grid derived zoom to max geotile level supported by ES', () => {
|
||||
expect(geogridSource.getGeoGridPrecision(29)).toBe(29);
|
||||
});
|
||||
|
||||
it('should use heuristic to derive precision', () => {
|
||||
expect(geogridSource.getGeoGridPrecision(10)).toBe(12);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -19,7 +19,6 @@ import {
|
|||
ES_GEO_FIELD_TYPE,
|
||||
DEFAULT_MAX_BUCKETS_LIMIT,
|
||||
SORT_ORDER,
|
||||
CATEGORICAL_DATA_TYPES,
|
||||
} from '../../../../common/constants';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { getDataSourceLabel } from '../../../../common/i18n_getters';
|
||||
|
@ -135,49 +134,6 @@ export class ESSearchSource extends AbstractESSource {
|
|||
);
|
||||
}
|
||||
|
||||
async getNumberFields() {
|
||||
try {
|
||||
const indexPattern = await this.getIndexPattern();
|
||||
return indexPattern.fields.getByType('number').map(field => {
|
||||
return this.createField({ fieldName: field.name });
|
||||
});
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getDateFields() {
|
||||
try {
|
||||
const indexPattern = await this.getIndexPattern();
|
||||
return indexPattern.fields.getByType('date').map(field => {
|
||||
return this.createField({ fieldName: field.name });
|
||||
});
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getCategoricalFields() {
|
||||
try {
|
||||
const indexPattern = await this.getIndexPattern();
|
||||
|
||||
const aggFields = [];
|
||||
CATEGORICAL_DATA_TYPES.forEach(dataType => {
|
||||
indexPattern.fields.getByType(dataType).forEach(field => {
|
||||
if (field.aggregatable) {
|
||||
aggFields.push(field);
|
||||
}
|
||||
});
|
||||
});
|
||||
return aggFields.map(field => {
|
||||
return this.createField({ fieldName: field.name });
|
||||
});
|
||||
} catch (error) {
|
||||
//error surfaces in the LayerTOC UI
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getFields() {
|
||||
try {
|
||||
const indexPattern = await this.getIndexPattern();
|
||||
|
|
|
@ -31,36 +31,27 @@ const metricExamples = [
|
|||
];
|
||||
|
||||
describe('getMetricFields', () => {
|
||||
it('should add default "count" metric when no metrics are provided', async () => {
|
||||
it('should override name and label of count metric', async () => {
|
||||
const source = new ESTermSource({
|
||||
indexPatternTitle: indexPatternTitle,
|
||||
term: termFieldName,
|
||||
});
|
||||
const metrics = source.getMetricFields();
|
||||
expect(metrics.length).toBe(1);
|
||||
|
||||
expect(metrics[0].getAggType()).toEqual('count');
|
||||
expect(metrics[0].getName()).toEqual('__kbnjoin__count_groupby_myIndex.myTermField');
|
||||
expect(await metrics[0].getLabel()).toEqual('Count of myIndex');
|
||||
});
|
||||
|
||||
it('should remove incomplete metric configurations', async () => {
|
||||
it('should override name and label of sum metric', async () => {
|
||||
const source = new ESTermSource({
|
||||
indexPatternTitle: indexPatternTitle,
|
||||
term: termFieldName,
|
||||
metrics: metricExamples,
|
||||
});
|
||||
const metrics = source.getMetricFields();
|
||||
expect(metrics.length).toBe(2);
|
||||
|
||||
expect(metrics[0].getAggType()).toEqual('sum');
|
||||
expect(metrics[0].getRootName()).toEqual(sumFieldName);
|
||||
expect(metrics[0].getName()).toEqual(
|
||||
'__kbnjoin__sum_of_myFieldGettingSummed_groupby_myIndex.myTermField'
|
||||
);
|
||||
expect(await metrics[0].getLabel()).toEqual('my custom label');
|
||||
|
||||
expect(metrics[1].getAggType()).toEqual('count');
|
||||
expect(metrics[1].getName()).toEqual('__kbnjoin__count_groupby_myIndex.myTermField');
|
||||
expect(await metrics[1].getLabel()).toEqual('Count of myIndex');
|
||||
});
|
||||
|
|
|
@ -111,19 +111,7 @@ export class AbstractVectorSource extends AbstractSource {
|
|||
return null;
|
||||
}
|
||||
|
||||
async getDateFields() {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getNumberFields() {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getFields() {
|
||||
return [...(await this.getDateFields()), ...(await this.getNumberFields())];
|
||||
}
|
||||
|
||||
async getCategoricalFields() {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
|
@ -6,10 +6,11 @@
|
|||
|
||||
import React, { Component, Fragment } from 'react';
|
||||
|
||||
import { EuiSuperSelect, EuiSpacer } from '@elastic/eui';
|
||||
import { EuiSpacer, EuiSelect, EuiSuperSelect, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { ColorStopsOrdinal } from './color_stops_ordinal';
|
||||
import { COLOR_MAP_TYPE } from '../../../../../../common/constants';
|
||||
import { ColorStopsCategorical } from './color_stops_categorical';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
const CUSTOM_COLOR_MAP = 'CUSTOM_COLOR_MAP';
|
||||
|
||||
|
@ -27,6 +28,43 @@ export class ColorMapSelect extends Component {
|
|||
};
|
||||
}
|
||||
|
||||
_renderColorMapToggle() {
|
||||
const options = [
|
||||
{
|
||||
value: COLOR_MAP_TYPE.ORDINAL,
|
||||
text: i18n.translate('xpack.maps.styles.dynamicColorSelect.quantitativeLabel', {
|
||||
defaultMessage: 'As number',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: COLOR_MAP_TYPE.CATEGORICAL,
|
||||
text: i18n.translate('xpack.maps.styles.dynamicColorSelect.qualitativeLabel', {
|
||||
defaultMessage: 'As category',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const selectedValue = this.props.styleProperty.isOrdinal()
|
||||
? COLOR_MAP_TYPE.ORDINAL
|
||||
: COLOR_MAP_TYPE.CATEGORICAL;
|
||||
|
||||
return (
|
||||
<EuiSelect
|
||||
options={options}
|
||||
value={selectedValue}
|
||||
onChange={this.props.onColorMapTypeChange}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.maps.styles.dynamicColorSelect.qualitativeOrQuantitativeAriaLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'Choose `As number` to map by number in a color range, or `As category`to categorize by color palette.',
|
||||
}
|
||||
)}
|
||||
compressed
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_onColorMapSelect = selectedValue => {
|
||||
const useCustomColorMap = selectedValue === CUSTOM_COLOR_MAP;
|
||||
this.props.onChange({
|
||||
|
@ -55,32 +93,32 @@ export class ColorMapSelect extends Component {
|
|||
return null;
|
||||
}
|
||||
|
||||
let colorStopEditor;
|
||||
if (this.props.colorMapType === COLOR_MAP_TYPE.ORDINAL) {
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiSpacer size="s" />
|
||||
<ColorStopsOrdinal
|
||||
colorStops={this.state.customColorMap}
|
||||
onChange={this._onCustomColorMapChange}
|
||||
/>
|
||||
</Fragment>
|
||||
colorStopEditor = (
|
||||
<ColorStopsOrdinal
|
||||
colorStops={this.state.customColorMap}
|
||||
onChange={this._onCustomColorMapChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiSpacer size="s" />
|
||||
} else
|
||||
colorStopEditor = (
|
||||
<ColorStopsCategorical
|
||||
colorStops={this.state.customColorMap}
|
||||
field={this.props.styleProperty.getField()}
|
||||
getValueSuggestions={this.props.styleProperty.getValueSuggestions}
|
||||
onChange={this._onCustomColorMapChange}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>{colorStopEditor}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
_renderColorMapSelections() {
|
||||
const colorMapOptionsWithCustom = [
|
||||
{
|
||||
value: CUSTOM_COLOR_MAP,
|
||||
|
@ -98,15 +136,31 @@ export class ColorMapSelect extends Component {
|
|||
: '';
|
||||
}
|
||||
|
||||
const toggle = this.props.showColorMapTypeToggle ? (
|
||||
<EuiFlexItem grow={false}>{this._renderColorMapToggle()}</EuiFlexItem>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize={'none'}>
|
||||
{toggle}
|
||||
<EuiFlexItem>
|
||||
<EuiSuperSelect
|
||||
compressed
|
||||
options={colorMapOptionsWithCustom}
|
||||
onChange={this._onColorMapSelect}
|
||||
valueOfSelected={valueOfSelected}
|
||||
hasDividers={true}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiSuperSelect
|
||||
options={colorMapOptionsWithCustom}
|
||||
onChange={this._onColorMapSelect}
|
||||
valueOfSelected={valueOfSelected}
|
||||
hasDividers={true}
|
||||
compressed
|
||||
/>
|
||||
{this._renderColorMapSelections()}
|
||||
<EuiSpacer size="s" />
|
||||
{this._renderColorStopsInput()}
|
||||
</Fragment>
|
||||
);
|
||||
|
|
|
@ -40,21 +40,44 @@ export function DynamicColorForm({
|
|||
};
|
||||
|
||||
const onFieldChange = async ({ field }) => {
|
||||
const { name, origin, type } = field;
|
||||
const { name, origin, type: fieldType } = field;
|
||||
const defaultColorMapType = CATEGORICAL_DATA_TYPES.includes(fieldType)
|
||||
? COLOR_MAP_TYPE.CATEGORICAL
|
||||
: COLOR_MAP_TYPE.ORDINAL;
|
||||
onDynamicStyleChange(styleProperty.getStyleName(), {
|
||||
...styleOptions,
|
||||
field: { name, origin },
|
||||
type: CATEGORICAL_DATA_TYPES.includes(type)
|
||||
? COLOR_MAP_TYPE.CATEGORICAL
|
||||
: COLOR_MAP_TYPE.ORDINAL,
|
||||
type: defaultColorMapType,
|
||||
});
|
||||
};
|
||||
|
||||
const onColorMapTypeChange = async e => {
|
||||
const colorMapType = e.target.value;
|
||||
onDynamicStyleChange(styleProperty.getStyleName(), {
|
||||
...styleOptions,
|
||||
type: colorMapType,
|
||||
});
|
||||
};
|
||||
|
||||
const getField = () => {
|
||||
const fieldName = styleProperty.getFieldName();
|
||||
if (!fieldName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fields.find(field => {
|
||||
return field.name === fieldName;
|
||||
});
|
||||
};
|
||||
|
||||
const renderColorMapSelect = () => {
|
||||
if (!styleOptions.field || !styleOptions.field.name) {
|
||||
const field = getField();
|
||||
if (!field) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const showColorMapTypeToggle = !CATEGORICAL_DATA_TYPES.includes(field.type);
|
||||
|
||||
if (styleProperty.isOrdinal()) {
|
||||
return (
|
||||
<ColorMapSelect
|
||||
|
@ -63,29 +86,33 @@ export function DynamicColorForm({
|
|||
defaultMessage: 'Custom color ramp',
|
||||
})}
|
||||
onChange={onColorMapSelect}
|
||||
onColorMapTypeChange={onColorMapTypeChange}
|
||||
colorMapType={COLOR_MAP_TYPE.ORDINAL}
|
||||
color={styleOptions.color}
|
||||
customColorMap={styleOptions.customColorRamp}
|
||||
useCustomColorMap={_.get(styleOptions, 'useCustomColorRamp', false)}
|
||||
styleProperty={styleProperty}
|
||||
showColorMapTypeToggle={showColorMapTypeToggle}
|
||||
/>
|
||||
);
|
||||
} else if (styleProperty.isCategorical()) {
|
||||
return (
|
||||
<ColorMapSelect
|
||||
colorMapOptions={COLOR_PALETTES}
|
||||
customOptionLabel={i18n.translate('xpack.maps.style.customColorPaletteLabel', {
|
||||
defaultMessage: 'Custom color palette',
|
||||
})}
|
||||
onColorMapTypeChange={onColorMapTypeChange}
|
||||
onChange={onColorMapSelect}
|
||||
colorMapType={COLOR_MAP_TYPE.CATEGORICAL}
|
||||
color={styleOptions.colorCategory}
|
||||
customColorMap={styleOptions.customColorPalette}
|
||||
useCustomColorMap={_.get(styleOptions, 'useCustomColorPalette', false)}
|
||||
styleProperty={styleProperty}
|
||||
showColorMapTypeToggle={showColorMapTypeToggle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ColorMapSelect
|
||||
colorMapOptions={COLOR_PALETTES}
|
||||
customOptionLabel={i18n.translate('xpack.maps.style.customColorPaletteLabel', {
|
||||
defaultMessage: 'Custom color palette',
|
||||
})}
|
||||
onChange={onColorMapSelect}
|
||||
colorMapType={COLOR_MAP_TYPE.CATEGORICAL}
|
||||
color={styleOptions.colorCategory}
|
||||
customColorMap={styleOptions.customColorPalette}
|
||||
useCustomColorMap={_.get(styleOptions, 'useCustomColorPalette', false)}
|
||||
styleProperty={styleProperty}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -26,13 +26,14 @@ import { VECTOR_SHAPE_TYPES } from '../../../sources/vector_feature_types';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiSpacer, EuiButtonGroup, EuiFormRow, EuiSwitch } from '@elastic/eui';
|
||||
import { CATEGORICAL_DATA_TYPES, ORDINAL_DATA_TYPES } from '../../../../../common/constants';
|
||||
|
||||
export class VectorStyleEditor extends Component {
|
||||
state = {
|
||||
dateFields: [],
|
||||
numberFields: [],
|
||||
categoricalFields: [],
|
||||
fields: [],
|
||||
ordinalAndCategoricalFields: [],
|
||||
defaultDynamicProperties: getDefaultDynamicProperties(),
|
||||
defaultStaticProperties: getDefaultStaticProperties(),
|
||||
supportedFeatures: undefined,
|
||||
|
@ -64,33 +65,24 @@ export class VectorStyleEditor extends Component {
|
|||
};
|
||||
};
|
||||
|
||||
const dateFields = await this.props.layer.getDateFields();
|
||||
const dateFieldPromises = dateFields.map(getFieldMeta);
|
||||
const dateFieldsArray = await Promise.all(dateFieldPromises);
|
||||
if (this._isMounted && !_.isEqual(dateFieldsArray, this.state.dateFields)) {
|
||||
this.setState({ dateFields: dateFieldsArray });
|
||||
}
|
||||
|
||||
const numberFields = await this.props.layer.getNumberFields();
|
||||
const numberFieldPromises = numberFields.map(getFieldMeta);
|
||||
const numberFieldsArray = await Promise.all(numberFieldPromises);
|
||||
if (this._isMounted && !_.isEqual(numberFieldsArray, this.state.numberFields)) {
|
||||
this.setState({ numberFields: numberFieldsArray });
|
||||
}
|
||||
|
||||
const categoricalFields = await this.props.layer.getCategoricalFields();
|
||||
const categoricalFieldMeta = categoricalFields.map(getFieldMeta);
|
||||
const categoricalFieldsArray = await Promise.all(categoricalFieldMeta);
|
||||
if (this._isMounted && !_.isEqual(categoricalFieldsArray, this.state.categoricalFields)) {
|
||||
this.setState({ categoricalFields: categoricalFieldsArray });
|
||||
}
|
||||
|
||||
//These are all fields (only used for text labeling)
|
||||
const fields = await this.props.layer.getFields();
|
||||
const fieldPromises = fields.map(getFieldMeta);
|
||||
const fieldsArray = await Promise.all(fieldPromises);
|
||||
if (this._isMounted && !_.isEqual(fieldsArray, this.state.fields)) {
|
||||
this.setState({ fields: fieldsArray });
|
||||
const fieldsArrayAll = await Promise.all(fieldPromises);
|
||||
if (!this._isMounted || _.isEqual(fieldsArrayAll, this.state.fields)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
fields: fieldsArrayAll,
|
||||
ordinalAndCategoricalFields: fieldsArrayAll.filter(field => {
|
||||
return (
|
||||
CATEGORICAL_DATA_TYPES.includes(field.type) || ORDINAL_DATA_TYPES.includes(field.type)
|
||||
);
|
||||
}),
|
||||
dateFields: fieldsArrayAll.filter(field => field.type === 'date'),
|
||||
numberFields: fieldsArrayAll.filter(field => field.type === 'number'),
|
||||
});
|
||||
}
|
||||
|
||||
async _loadSupportedFeatures() {
|
||||
|
@ -118,10 +110,6 @@ export class VectorStyleEditor extends Component {
|
|||
return [...this.state.dateFields, ...this.state.numberFields];
|
||||
}
|
||||
|
||||
_getOrdinalAndCategoricalFields() {
|
||||
return [...this.state.dateFields, ...this.state.numberFields, ...this.state.categoricalFields];
|
||||
}
|
||||
|
||||
_handleSelectedFeatureChange = selectedFeature => {
|
||||
this.setState({ selectedFeature });
|
||||
};
|
||||
|
@ -172,7 +160,7 @@ export class VectorStyleEditor extends Component {
|
|||
onStaticStyleChange={this._onStaticStyleChange}
|
||||
onDynamicStyleChange={this._onDynamicStyleChange}
|
||||
styleProperty={this.props.styleProperties[VECTOR_STYLES.FILL_COLOR]}
|
||||
fields={this._getOrdinalAndCategoricalFields()}
|
||||
fields={this.state.ordinalAndCategoricalFields}
|
||||
defaultStaticStyleOptions={
|
||||
this.state.defaultStaticProperties[VECTOR_STYLES.FILL_COLOR].options
|
||||
}
|
||||
|
@ -193,7 +181,7 @@ export class VectorStyleEditor extends Component {
|
|||
onStaticStyleChange={this._onStaticStyleChange}
|
||||
onDynamicStyleChange={this._onDynamicStyleChange}
|
||||
styleProperty={this.props.styleProperties[VECTOR_STYLES.LINE_COLOR]}
|
||||
fields={this._getOrdinalAndCategoricalFields()}
|
||||
fields={this.state.ordinalAndCategoricalFields}
|
||||
defaultStaticStyleOptions={
|
||||
this.state.defaultStaticProperties[VECTOR_STYLES.LINE_COLOR].options
|
||||
}
|
||||
|
@ -249,7 +237,7 @@ export class VectorStyleEditor extends Component {
|
|||
onStaticStyleChange={this._onStaticStyleChange}
|
||||
onDynamicStyleChange={this._onDynamicStyleChange}
|
||||
styleProperty={this.props.styleProperties[VECTOR_STYLES.LABEL_COLOR]}
|
||||
fields={this._getOrdinalAndCategoricalFields()}
|
||||
fields={this.state.ordinalAndCategoricalFields}
|
||||
defaultStaticStyleOptions={
|
||||
this.state.defaultStaticProperties[VECTOR_STYLES.LABEL_COLOR].options
|
||||
}
|
||||
|
@ -282,7 +270,7 @@ export class VectorStyleEditor extends Component {
|
|||
onStaticStyleChange={this._onStaticStyleChange}
|
||||
onDynamicStyleChange={this._onDynamicStyleChange}
|
||||
styleProperty={this.props.styleProperties[VECTOR_STYLES.LABEL_BORDER_COLOR]}
|
||||
fields={this._getOrdinalAndCategoricalFields()}
|
||||
fields={this.state.ordinalAndCategoricalFields}
|
||||
defaultStaticStyleOptions={
|
||||
this.state.defaultStaticProperties[VECTOR_STYLES.LABEL_BORDER_COLOR].options
|
||||
}
|
||||
|
@ -335,7 +323,7 @@ export class VectorStyleEditor extends Component {
|
|||
onStaticStyleChange={this._onStaticStyleChange}
|
||||
onDynamicStyleChange={this._onDynamicStyleChange}
|
||||
styleProperty={this.props.styleProperties[VECTOR_STYLES.ICON]}
|
||||
fields={this.state.categoricalFields}
|
||||
fields={this.state.ordinalAndCategoricalFields}
|
||||
defaultStaticStyleOptions={
|
||||
this.state.defaultStaticProperties[VECTOR_STYLES.ICON].options
|
||||
}
|
||||
|
|
|
@ -236,3 +236,33 @@ test('Should pluck the categorical style-meta from fieldmeta', async () => {
|
|||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('isCategorical should return true when type is categorical', async () => {
|
||||
const categoricalColorStyle = makeProperty({
|
||||
type: COLOR_MAP_TYPE.CATEGORICAL,
|
||||
colorCategory: 'palette_0',
|
||||
});
|
||||
|
||||
expect(categoricalColorStyle.isOrdinal()).toEqual(false);
|
||||
expect(categoricalColorStyle.isCategorical()).toEqual(true);
|
||||
});
|
||||
|
||||
test('isOrdinal should return true when type is ordinal', async () => {
|
||||
const ordinalColorStyle = makeProperty({
|
||||
type: undefined,
|
||||
color: 'Blues',
|
||||
});
|
||||
|
||||
expect(ordinalColorStyle.isOrdinal()).toEqual(true);
|
||||
expect(ordinalColorStyle.isCategorical()).toEqual(false);
|
||||
});
|
||||
|
||||
test('Should read out ordinal type correctly', async () => {
|
||||
const ordinalColorStyle2 = makeProperty({
|
||||
type: COLOR_MAP_TYPE.ORDINAL,
|
||||
colorCategory: 'palette_0',
|
||||
});
|
||||
|
||||
expect(ordinalColorStyle2.isOrdinal()).toEqual(true);
|
||||
expect(ordinalColorStyle2.isCategorical()).toEqual(false);
|
||||
});
|
||||
|
|
|
@ -197,19 +197,6 @@ export class VectorLayer extends AbstractLayer {
|
|||
return joinFields;
|
||||
}
|
||||
|
||||
async getDateFields() {
|
||||
return await this._source.getDateFields();
|
||||
}
|
||||
|
||||
async getNumberFields() {
|
||||
const numberFieldOptions = await this._source.getNumberFields();
|
||||
return [...numberFieldOptions, ...this._getJoinFields()];
|
||||
}
|
||||
|
||||
async getCategoricalFields() {
|
||||
return await this._source.getCategoricalFields();
|
||||
}
|
||||
|
||||
async getFields() {
|
||||
const sourceFields = await this._source.getFields();
|
||||
return [...sourceFields, ...this._getJoinFields()];
|
||||
|
|
Loading…
Reference in a new issue