From 1cf848041db9d5f4426520a96c5aac44bdd09502 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Wed, 15 Jan 2020 00:36:38 -0500 Subject: [PATCH] [Maps] Add categorical styling (#54408) (#54860) This allows users to style fields by category. Users can either uses one of default color palettes or specify a custom ramp. Co-authored-by: Elastic Machine --- .../legacy/plugins/maps/common/constants.js | 9 + .../maps/public/layers/fields/es_agg_field.js | 4 +- .../maps/public/layers/fields/es_doc_field.js | 28 ++- .../maps/public/layers/fields/field.js | 6 +- .../plugins/maps/public/layers/layer.js | 4 + .../es_geo_grid_source/es_geo_grid_source.js | 3 +- .../es_pew_pew_source/es_pew_pew_source.js | 3 +- .../es_search_source/es_search_source.js | 22 ++ .../public/layers/sources/vector_source.js | 4 + .../maps/public/layers/styles/color_utils.js | 68 +++++- .../public/layers/styles/color_utils.test.js | 4 +- .../layers/styles/heatmap/heatmap_style.js | 4 +- .../components/color/color_map_select.js | 117 ++++++++++ .../components/color/color_ramp_select.js | 106 --------- .../vector/components/color/color_stops.js | 194 ++++++++-------- .../color/color_stops_categorical.js | 117 ++++++++++ .../components/color/color_stops_ordinal.js | 94 ++++++++ .../components/color/color_stops_utils.js | 40 +++- .../components/color/dynamic_color_form.js | 180 +++++++++++---- .../extract_color_from_style_property.js | 45 ++-- ... => ordinal_field_meta_options_popover.js} | 2 +- .../vector/components/style_prop_editor.js | 10 +- .../vector/components/vector_style_editor.js | 20 +- .../dynamic_color_property.test.js.snap | 216 +++++++++++++++++- .../properties/dynamic_color_property.js | 188 ++++++++++++--- .../properties/dynamic_color_property.test.js | 187 ++++++++++++--- .../dynamic_orientation_property.js | 2 +- .../properties/dynamic_style_property.js | 132 +++++++++-- .../properties/dynamic_text_property.js | 2 +- .../vector/properties/style_property.js | 4 + .../styles/vector/vector_style_defaults.js | 12 +- .../maps/public/layers/vector_layer.js | 4 + 32 files changed, 1456 insertions(+), 375 deletions(-) create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_map_select.js delete mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_ramp_select.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js rename x-pack/legacy/plugins/maps/public/layers/styles/vector/components/{field_meta_options_popover.js => ordinal_field_meta_options_popover.js} (98%) diff --git a/x-pack/legacy/plugins/maps/common/constants.js b/x-pack/legacy/plugins/maps/common/constants.js index 6e7776d43f4d4..b3cefbf5c0b41 100644 --- a/x-pack/legacy/plugins/maps/common/constants.js +++ b/x-pack/legacy/plugins/maps/common/constants.js @@ -140,3 +140,12 @@ export const LAYER_STYLE_TYPE = { VECTOR: 'VECTOR', HEATMAP: 'HEATMAP', }; + +export const COLOR_MAP_TYPE = { + CATEGORICAL: 'CATEGORICAL', + ORDINAL: 'ORDINAL', +}; + +export const COLOR_PALETTE_MAX_SIZE = 10; + +export const CATEGORICAL_DATA_TYPES = ['string', 'ip', 'boolean']; diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js b/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js index 189aad3785034..65109cb99809f 100644 --- a/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js +++ b/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js @@ -82,7 +82,7 @@ export class ESAggMetricField extends AbstractField { return !isMetricCountable(this.getAggType()); } - async getFieldMetaRequest(config) { - return this._esDocField.getFieldMetaRequest(config); + async getOrdinalFieldMetaRequest(config) { + return this._esDocField.getOrdinalFieldMetaRequest(config); } } diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/es_doc_field.js b/x-pack/legacy/plugins/maps/public/layers/fields/es_doc_field.js index ee082e4546b8b..f9baf180dfe5c 100644 --- a/x-pack/legacy/plugins/maps/public/layers/fields/es_doc_field.js +++ b/x-pack/legacy/plugins/maps/public/layers/fields/es_doc_field.js @@ -6,6 +6,7 @@ import { AbstractField } from './field'; import { ESTooltipProperty } from '../tooltips/es_tooltip_property'; +import { COLOR_PALETTE_MAX_SIZE } from '../../../common/constants'; export class ESDocField extends AbstractField { static type = 'ES_DOC'; @@ -29,7 +30,7 @@ export class ESDocField extends AbstractField { return true; } - async getFieldMetaRequest(/* config */) { + async getOrdinalFieldMetaRequest() { const field = await this._getField(); if (field.type !== 'number' && field.type !== 'date') { @@ -51,4 +52,29 @@ 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 + }; + if (field.scripted) { + topTerms.script = { + source: field.script, + lang: field.lang, + }; + } else { + topTerms.field = this._fieldName; + } + return { + [this._fieldName]: { + terms: topTerms, + }, + }; + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/field.js b/x-pack/legacy/plugins/maps/public/layers/fields/field.js index f1401a78e2174..b5d157ad1697a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/fields/field.js +++ b/x-pack/legacy/plugins/maps/public/layers/fields/field.js @@ -45,7 +45,11 @@ export class AbstractField { return false; } - async getFieldMetaRequest(/* config */) { + async getOrdinalFieldMetaRequest(/* config */) { + return null; + } + + async getCategoricalFieldMetaRequest() { return null; } } diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.js b/x-pack/legacy/plugins/maps/public/layers/layer.js index 21c5f15fb6122..09475e638de55 100644 --- a/x-pack/legacy/plugins/maps/public/layers/layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/layer.js @@ -344,6 +344,10 @@ export class AbstractLayer { return []; } + async getCategoricalFields() { + return []; + } + async getFields() { return []; } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js index d4f36a63e5989..8c394e71b3a78 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js @@ -19,6 +19,7 @@ import { getDefaultDynamicProperties, VECTOR_STYLES, } from '../../styles/vector/vector_style_defaults'; +import { COLOR_GRADIENTS } from '../../styles/color_utils'; import { RENDER_AS } from './render_as'; import { CreateSourceEditor } from './create_source_editor'; import { UpdateSourceEditor } from './update_source_editor'; @@ -249,7 +250,7 @@ export class ESGeoGridSource extends AbstractESAggSource { name: COUNT_PROP_NAME, origin: SOURCE_DATA_ID_ORIGIN, }, - color: 'Blues', + color: COLOR_GRADIENTS[0].value, }, }, [VECTOR_STYLES.LINE_COLOR]: { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js index 3579027b27847..dfc9fca96dd75 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js @@ -24,6 +24,7 @@ import { Schemas } from 'ui/vis/editors/default/schemas'; import { AggConfigs } from 'ui/agg_types'; import { AbstractESAggSource } from '../es_agg_source'; import { DynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; +import { COLOR_GRADIENTS } from '../../styles/color_utils'; const MAX_GEOTILE_LEVEL = 29; @@ -136,7 +137,7 @@ export class ESPewPewSource extends AbstractESAggSource { name: COUNT_PROP_NAME, origin: SOURCE_DATA_ID_ORIGIN, }, - color: 'Blues', + color: COLOR_GRADIENTS[0].value, }, }, [VECTOR_STYLES.LINE_WIDTH]: { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js index 8ef4966e03c1b..b8644adddcf7e 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js @@ -19,6 +19,7 @@ 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'; @@ -125,6 +126,27 @@ export class ESSearchSource extends AbstractESSource { } } + 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(); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js index bf7267e9c5858..b9d8ae86c5850 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js @@ -107,6 +107,10 @@ export class AbstractVectorSource extends AbstractSource { return [...(await this.getDateFields()), ...(await this.getNumberFields())]; } + async getCategoricalFields() { + return []; + } + async getLeftJoinFields() { return []; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js b/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js index 294ccaf92c13e..cc840d552e659 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js @@ -12,6 +12,7 @@ import { ColorGradient } from './components/color_gradient'; import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; import tinycolor from 'tinycolor2'; import chroma from 'chroma-js'; +import { COLOR_PALETTE_MAX_SIZE } from '../../../common/constants'; const GRADIENT_INTERVALS = 8; @@ -51,6 +52,9 @@ export function getHexColorRangeStrings(colorRampName, numberColors = GRADIENT_I } export function getColorRampCenterColor(colorRampName) { + if (!colorRampName) { + return null; + } const colorRamp = getColorRamp(colorRampName); const centerIndex = Math.floor(colorRamp.value.length / 2); return getColor(colorRamp.value, centerIndex); @@ -58,7 +62,10 @@ export function getColorRampCenterColor(colorRampName) { // Returns an array of color stops // [ stop_input_1: number, stop_output_1: color, stop_input_n: number, stop_output_n: color ] -export function getColorRampStops(colorRampName, numberColors = GRADIENT_INTERVALS) { +export function getOrdinalColorRampStops(colorRampName, numberColors = GRADIENT_INTERVALS) { + if (!colorRampName) { + return null; + } return getHexColorRangeStrings(colorRampName, numberColors).reduce( (accu, stopColor, idx, srcArr) => { const stopNumber = idx / srcArr.length; // number between 0 and 1, increasing as index increases @@ -84,3 +91,62 @@ export function getLinearGradient(colorStrings) { } return `${linearGradient} ${colorStrings[colorStrings.length - 1]} 100%)`; } + +const COLOR_PALETTES_CONFIGS = [ + { + id: 'palette_0', + colors: DEFAULT_FILL_COLORS.slice(0, COLOR_PALETTE_MAX_SIZE), + }, + { + id: 'palette_1', + colors: [ + '#a6cee3', + '#1f78b4', + '#b2df8a', + '#33a02c', + '#fb9a99', + '#e31a1c', + '#fdbf6f', + '#ff7f00', + '#cab2d6', + '#6a3d9a', + ], + }, + { + id: 'palette_2', + colors: [ + '#8dd3c7', + '#ffffb3', + '#bebada', + '#fb8072', + '#80b1d3', + '#fdb462', + '#b3de69', + '#fccde5', + '#d9d9d9', + '#bc80bd', + ], + }, +]; + +export function getColorPalette(paletteId) { + const palette = COLOR_PALETTES_CONFIGS.find(palette => palette.id === paletteId); + return palette ? palette.colors : null; +} + +export const COLOR_PALETTES = COLOR_PALETTES_CONFIGS.map(palette => { + const paletteDisplay = palette.colors.map(color => { + const style = { + backgroundColor: color, + width: '10%', + position: 'relative', + height: '100%', + display: 'inline-block', + }; + return
 
; + }); + return { + value: palette.id, + inputDisplay:
{paletteDisplay}
, + }; +}); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.test.js b/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.test.js index 8826c771fab19..1d7fbeb996915 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.test.js @@ -7,7 +7,7 @@ import { COLOR_GRADIENTS, getColorRampCenterColor, - getColorRampStops, + getOrdinalColorRampStops, getHexColorRangeStrings, getLinearGradient, getRGBColorRangeStrings, @@ -59,7 +59,7 @@ describe('getColorRampCenterColor', () => { describe('getColorRampStops', () => { it('Should create color stops for color ramp', () => { - expect(getColorRampStops('Blues')).toEqual([ + expect(getOrdinalColorRampStops('Blues')).toEqual([ 0, '#f7faff', 0.125, diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js index 0b4a52997c00e..1dd219d4c4cad 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js @@ -11,7 +11,7 @@ import { HeatmapStyleEditor } from './components/heatmap_style_editor'; import { HeatmapLegend } from './components/legend/heatmap_legend'; import { DEFAULT_HEATMAP_COLOR_RAMP_NAME } from './components/heatmap_constants'; import { LAYER_STYLE_TYPE } from '../../../../common/constants'; -import { getColorRampStops } from '../color_utils'; +import { getOrdinalColorRampStops } from '../color_utils'; import { i18n } from '@kbn/i18n'; import { EuiIcon } from '@elastic/eui'; @@ -81,7 +81,7 @@ export class HeatmapStyle extends AbstractStyle { const { colorRampName } = this._descriptor; if (colorRampName && colorRampName !== DEFAULT_HEATMAP_COLOR_RAMP_NAME) { - const colorStops = getColorRampStops(colorRampName); + const colorStops = getOrdinalColorRampStops(colorRampName); mbMap.setPaintProperty(layerId, 'heatmap-color', [ 'interpolate', ['linear'], diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_map_select.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_map_select.js new file mode 100644 index 0000000000000..242b71522f9a2 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_map_select.js @@ -0,0 +1,117 @@ +/* + * 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, { Component, Fragment } from 'react'; + +import { EuiSuperSelect, EuiSpacer } from '@elastic/eui'; +import { ColorStopsOrdinal } from './color_stops_ordinal'; +import { COLOR_MAP_TYPE } from '../../../../../../common/constants'; +import { ColorStopsCategorical } from './color_stops_categorical'; + +const CUSTOM_COLOR_MAP = 'CUSTOM_COLOR_MAP'; + +export class ColorMapSelect extends Component { + state = { + selected: '', + }; + + static getDerivedStateFromProps(nextProps, prevState) { + if (nextProps.customColorMap === prevState.prevPropsCustomColorMap) { + return null; + } + + return { + prevPropsCustomColorMap: nextProps.customColorMap, // reset tracker to latest value + customColorMap: nextProps.customColorMap, // reset customColorMap to latest value + }; + } + + _onColorMapSelect = selectedValue => { + const useCustomColorMap = selectedValue === CUSTOM_COLOR_MAP; + this.props.onChange({ + color: useCustomColorMap ? null : selectedValue, + useCustomColorMap, + type: this.props.colorMapType, + }); + }; + + _onCustomColorMapChange = ({ colorStops, isInvalid }) => { + // Manage invalid custom color map in local state + if (isInvalid) { + const newState = { + customColorMap: colorStops, + }; + this.setState(newState); + return; + } + + this.props.onChange({ + useCustomColorMap: true, + customColorMap: colorStops, + type: this.props.colorMapType, + }); + }; + + _renderColorStopsInput() { + let colorStopsInput; + if (this.props.useCustomColorMap) { + if (this.props.colorMapType === COLOR_MAP_TYPE.ORDINAL) { + colorStopsInput = ( + + + + + ); + } else if (this.props.colorMapType === COLOR_MAP_TYPE.CATEGORICAL) { + colorStopsInput = ( + + + + + ); + } + } + return colorStopsInput; + } + + render() { + const colorStopsInput = this._renderColorStopsInput(); + const colorMapOptionsWithCustom = [ + { + value: CUSTOM_COLOR_MAP, + inputDisplay: this.props.customOptionLabel, + }, + ...this.props.colorMapOptions, + ]; + + let valueOfSelected; + if (this.props.useCustomColorMap) { + valueOfSelected = CUSTOM_COLOR_MAP; + } else { + valueOfSelected = this.props.colorMapOptions.find(option => option.value === this.props.color) + ? this.props.color + : ''; + } + + return ( + + + {colorStopsInput} + + ); + } +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_ramp_select.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_ramp_select.js deleted file mode 100644 index c2dd51a0182e3..0000000000000 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_ramp_select.js +++ /dev/null @@ -1,106 +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. - */ - -import React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; - -import { EuiSuperSelect, EuiSpacer } from '@elastic/eui'; -import { COLOR_GRADIENTS } from '../../../color_utils'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { ColorStops } from './color_stops'; - -const CUSTOM_COLOR_RAMP = 'CUSTOM_COLOR_RAMP'; - -export class ColorRampSelect extends Component { - state = {}; - - static getDerivedStateFromProps(nextProps, prevState) { - if (nextProps.customColorRamp !== prevState.prevPropsCustomColorRamp) { - return { - prevPropsCustomColorRamp: nextProps.customColorRamp, // reset tracker to latest value - customColorRamp: nextProps.customColorRamp, // reset customColorRamp to latest value - }; - } - - return null; - } - - _onColorRampSelect = selectedValue => { - const useCustomColorRamp = selectedValue === CUSTOM_COLOR_RAMP; - this.props.onChange({ - color: useCustomColorRamp ? null : selectedValue, - useCustomColorRamp, - }); - }; - - _onCustomColorRampChange = ({ colorStops, isInvalid }) => { - // Manage invalid custom color ramp in local state - if (isInvalid) { - this.setState({ customColorRamp: colorStops }); - return; - } - - this.props.onChange({ - customColorRamp: colorStops, - }); - }; - - render() { - const { - color, - onChange, // eslint-disable-line no-unused-vars - useCustomColorRamp, - customColorRamp, // eslint-disable-line no-unused-vars - ...rest - } = this.props; - - let colorStopsInput; - if (useCustomColorRamp) { - colorStopsInput = ( - - - - - ); - } - - const colorRampOptions = [ - { - value: CUSTOM_COLOR_RAMP, - inputDisplay: ( - - ), - }, - ...COLOR_GRADIENTS, - ]; - - return ( - - - {colorStopsInput} - - ); - } -} - -ColorRampSelect.propTypes = { - color: PropTypes.string, - onChange: PropTypes.func.isRequired, - useCustomColorRamp: PropTypes.bool, - customColorRamp: PropTypes.array, -}; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops.js index d523cf5870912..6b403ff61532d 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops.js @@ -6,66 +6,106 @@ import _ from 'lodash'; import React from 'react'; -import PropTypes from 'prop-types'; - -import { - EuiColorPicker, - EuiFormRow, - EuiFieldNumber, - EuiFlexGroup, - EuiFlexItem, - EuiButtonIcon, -} from '@elastic/eui'; -import { addRow, removeRow, isColorInvalid, isStopInvalid, isInvalid } from './color_stops_utils'; - -const DEFAULT_COLOR = '#FF0000'; - -export const ColorStops = ({ colorStops = [{ stop: 0, color: DEFAULT_COLOR }], onChange }) => { +import { removeRow, isColorInvalid } from './color_stops_utils'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonIcon, EuiColorPicker, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; + +function getColorStopRow({ index, errors, stopInput, colorInput, deleteButton, onAdd }) { + return ( + +
+ + {stopInput} + {colorInput} + +
+ {deleteButton} + +
+
+
+ ); +} + +export function getDeleteButton(onRemove) { + return ( + + ); +} + +export const ColorStops = ({ + onChange, + colorStops, + isStopsInvalid, + sanitizeStopInput, + getStopError, + renderStopInput, + addNewRow, + canDeleteStop, +}) => { function getStopInput(stop, index) { const onStopChange = e => { const newColorStops = _.cloneDeep(colorStops); - const sanitizedValue = parseFloat(e.target.value); - newColorStops[index].stop = isNaN(sanitizedValue) ? '' : sanitizedValue; + newColorStops[index].stop = sanitizeStopInput(e.target.value); + const invalid = isStopsInvalid(newColorStops); onChange({ colorStops: newColorStops, - isInvalid: isInvalid(newColorStops), + isInvalid: invalid, }); }; - let error; - if (isStopInvalid(stop)) { - error = 'Stop must be a number'; - } else if (index !== 0 && colorStops[index - 1].stop >= stop) { - error = 'Stop must be greater than previous stop value'; - } - + const error = getStopError(stop, index); return { stopError: error, - stopInput: ( - - ), + stopInput: renderStopInput(stop, onStopChange, index), + }; + } + + function getColorInput(onColorChange, color) { + return { + colorError: isColorInvalid(color) + ? i18n.translate('xpack.maps.styles.colorStops.hexWarningLabel', { + defaultMessage: 'Color must provide a valid hex value', + }) + : undefined, + colorInput: , }; } - function getColorInput(color, index) { + const rows = colorStops.map((colorStop, index) => { const onColorChange = color => { const newColorStops = _.cloneDeep(colorStops); newColorStops[index].color = color; onChange({ colorStops: newColorStops, - isInvalid: isInvalid(newColorStops), + isInvalid: isStopsInvalid(newColorStops), }); }; - return { - colorError: isColorInvalid(color) ? 'Color must provide a valid hex value' : undefined, - colorInput: , - }; - } - - const rows = colorStops.map((colorStop, index) => { const { stopError, stopInput } = getStopInput(colorStop.stop, index); - const { colorError, colorInput } = getColorInput(colorStop.color, index); + const { colorError, colorInput } = getColorInput(onColorChange, colorStop.color); const errors = []; if (stopError) { errors.push(stopError); @@ -74,82 +114,28 @@ export const ColorStops = ({ colorStops = [{ stop: 0, color: DEFAULT_COLOR }], o errors.push(colorError); } - const onRemove = () => { - const newColorStops = removeRow(colorStops, index); - onChange({ - colorStops: newColorStops, - isInvalid: isInvalid(newColorStops), - }); - }; - const onAdd = () => { - const newColorStops = addRow(colorStops, index); - + const newColorStops = addNewRow(colorStops, index); onChange({ colorStops: newColorStops, - isInvalid: isInvalid(newColorStops), + isInvalid: isStopsInvalid(newColorStops), }); }; let deleteButton; - if (colorStops.length > 1) { - deleteButton = ( - - ); + if (canDeleteStop(colorStops, index)) { + const onRemove = () => { + const newColorStops = removeRow(colorStops, index); + onChange({ + colorStops: newColorStops, + isInvalid: isStopsInvalid(newColorStops), + }); + }; + deleteButton = getDeleteButton(onRemove); } - return ( - -
- - {stopInput} - {colorInput} - -
- {deleteButton} - -
-
-
- ); + return getColorStopRow({ index, errors, stopInput, colorInput, deleteButton, onAdd }); }); return
{rows}
; }; - -ColorStops.propTypes = { - /** - * Array of { stop, color }. - * Stops are numbers in strictly ascending order. - * The range is from the given stop number (inclusive) to the next stop number (exclusive). - * Colors are color hex strings (3 or 6 character). - */ - colorStops: PropTypes.arrayOf( - PropTypes.shape({ - stopKey: PropTypes.number, - color: PropTypes.string, - }) - ), - /** - * Callback for when the color stops changes. Called with { colorStops, isInvalid } - */ - onChange: PropTypes.func.isRequired, -}; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js new file mode 100644 index 0000000000000..d5948d5539bae --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js @@ -0,0 +1,117 @@ +/* + * 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 PropTypes from 'prop-types'; + +import { EuiFieldText } from '@elastic/eui'; +import { + addCategoricalRow, + isCategoricalStopsInvalid, + getOtherCategoryLabel, + DEFAULT_CUSTOM_COLOR, + DEFAULT_NEXT_COLOR, +} from './color_stops_utils'; +import { i18n } from '@kbn/i18n'; +import { ColorStops } from './color_stops'; + +export const ColorStopsCategorical = ({ + colorStops = [ + { stop: null, color: DEFAULT_CUSTOM_COLOR }, //first stop is the "other" color + { stop: '', color: DEFAULT_NEXT_COLOR }, + ], + onChange, +}) => { + const sanitizeStopInput = value => { + return value; + }; + + const getStopError = (stop, index) => { + let count = 0; + for (let i = 1; i < colorStops.length; i++) { + if (colorStops[i].stop === stop && i !== index) { + count++; + } + } + + return count + ? i18n.translate('xpack.maps.styles.colorStops.categoricalStop.noDupesWarningLabel', { + defaultMessage: 'Stop values must be unique', + }) + : null; + }; + + const renderStopInput = (stop, onStopChange, index) => { + const stopValue = typeof stop === 'string' ? stop : ''; + if (index === 0) { + return ( + + ); + } else { + return ( + + ); + } + }; + + const canDeleteStop = (colorStops, index) => { + return colorStops.length > 2 && index !== 0; + }; + + return ( + + ); +}; + +ColorStopsCategorical.propTypes = { + /** + * Array of { stop, color }. + * Stops are any strings + * Stops cannot include duplicates + * Colors are color hex strings (3 or 6 character). + */ + colorStops: PropTypes.arrayOf( + PropTypes.shape({ + stopKey: PropTypes.number, + color: PropTypes.string, + }) + ), + /** + * Callback for when the color stops changes. Called with { colorStops, isInvalid } + */ + onChange: PropTypes.func.isRequired, +}; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js new file mode 100644 index 0000000000000..61fbb376ad601 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js @@ -0,0 +1,94 @@ +/* + * 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 PropTypes from 'prop-types'; + +import { ColorStops } from './color_stops'; +import { EuiFieldNumber } from '@elastic/eui'; +import { + addOrdinalRow, + isOrdinalStopInvalid, + isOrdinalStopsInvalid, + DEFAULT_CUSTOM_COLOR, +} from './color_stops_utils'; +import { i18n } from '@kbn/i18n'; + +export const ColorStopsOrdinal = ({ + colorStops = [{ stop: 0, color: DEFAULT_CUSTOM_COLOR }], + onChange, +}) => { + const sanitizeStopInput = value => { + const sanitizedValue = parseFloat(value); + return isNaN(sanitizedValue) ? '' : sanitizedValue; + }; + + const getStopError = (stop, index) => { + let error; + if (isOrdinalStopInvalid(stop)) { + error = i18n.translate('xpack.maps.styles.colorStops.ordinalStop.numberWarningLabel', { + defaultMessage: 'Stop must be a number', + }); + } else if (index !== 0 && colorStops[index - 1].stop >= stop) { + error = i18n.translate( + 'xpack.maps.styles.colorStops.ordinalStop.numberOrderingWarningLabel', + { + defaultMessage: 'Stop must be greater than previous stop value', + } + ); + } + return error; + }; + + const renderStopInput = (stop, onStopChange) => { + return ( + + ); + }; + + const canDeleteStop = colorStops => { + return colorStops.length > 1; + }; + + return ( + + ); +}; + +ColorStopsOrdinal.propTypes = { + /** + * Array of { stop, color }. + * Stops are numbers in strictly ascending order. + * The range is from the given stop number (inclusive) to the next stop number (exclusive). + * Colors are color hex strings (3 or 6 character). + */ + colorStops: PropTypes.arrayOf( + PropTypes.shape({ + stopKey: PropTypes.number, + color: PropTypes.string, + }) + ), + /** + * Callback for when the color stops changes. Called with { colorStops, isInvalid } + */ + onChange: PropTypes.func.isRequired, +}; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_utils.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_utils.js index fb0a25cf7d5ee..3eaa6acf435dc 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_utils.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_utils.js @@ -5,6 +5,11 @@ */ import { isValidHex } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import _ from 'lodash'; + +export const DEFAULT_CUSTOM_COLOR = '#FF0000'; +export const DEFAULT_NEXT_COLOR = '#00FF00'; export function removeRow(colorStops, index) { if (colorStops.length === 1) { @@ -14,7 +19,7 @@ export function removeRow(colorStops, index) { return [...colorStops.slice(0, index), ...colorStops.slice(index + 1)]; } -export function addRow(colorStops, index) { +export function addOrdinalRow(colorStops, index) { const currentStop = colorStops[index].stop; let delta = 1; if (index === colorStops.length - 1) { @@ -28,10 +33,20 @@ export function addRow(colorStops, index) { const nextStop = colorStops[index + 1].stop; delta = (nextStop - currentStop) / 2; } + const nextValue = currentStop + delta; + return addRow(colorStops, index, nextValue); +} + +export function addCategoricalRow(colorStops, index) { + const currentStop = colorStops[index].stop; + const nextValue = currentStop === '' ? currentStop + 'a' : ''; + return addRow(colorStops, index, nextValue); +} +function addRow(colorStops, index, nextValue) { const newRow = { - stop: currentStop + delta, - color: '#FF0000', + stop: nextValue, + color: DEFAULT_CUSTOM_COLOR, }; return [...colorStops.slice(0, index + 1), newRow, ...colorStops.slice(index + 1)]; } @@ -40,11 +55,18 @@ export function isColorInvalid(color) { return !isValidHex(color) || color === ''; } -export function isStopInvalid(stop) { +export function isOrdinalStopInvalid(stop) { return stop === '' || isNaN(stop); } -export function isInvalid(colorStops) { +export function isCategoricalStopsInvalid(colorStops) { + const nonDefaults = colorStops.slice(1); // + const values = nonDefaults.map(stop => stop.stop); + const uniques = _.uniq(values); + return values.length !== uniques.length; +} + +export function isOrdinalStopsInvalid(colorStops) { return colorStops.some((colorStop, index) => { // expect stops to be in ascending order let isDescending = false; @@ -53,6 +75,12 @@ export function isInvalid(colorStops) { isDescending = prevStop >= colorStop.stop; } - return isColorInvalid(colorStop.color) || isStopInvalid(colorStop.stop) || isDescending; + return isColorInvalid(colorStop.color) || isOrdinalStopInvalid(colorStop.stop) || isDescending; + }); +} + +export function getOtherCategoryLabel() { + return i18n.translate('xpack.maps.styles.categorical.otherCategoryLabel', { + defaultMessage: 'Other', }); } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js index 5e0f7434b04d0..7994f84386a8a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js @@ -7,56 +7,146 @@ import _ from 'lodash'; import React, { Fragment } from 'react'; import { FieldSelect } from '../field_select'; -import { ColorRampSelect } from './color_ramp_select'; +import { ColorMapSelect } from './color_map_select'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { CATEGORICAL_DATA_TYPES, COLOR_MAP_TYPE } from '../../../../../../common/constants'; +import { COLOR_GRADIENTS, COLOR_PALETTES } from '../../../color_utils'; +import { i18n } from '@kbn/i18n'; -export function DynamicColorForm({ - fields, - onDynamicStyleChange, - staticDynamicSelect, - styleProperty, -}) { - const styleOptions = styleProperty.getOptions(); - - const onFieldChange = ({ field }) => { - onDynamicStyleChange(styleProperty.getStyleName(), { ...styleOptions, field }); +export class DynamicColorForm extends React.Component { + state = { + colorMapType: COLOR_MAP_TYPE.ORDINAL, }; - const onColorChange = colorOptions => { - onDynamicStyleChange(styleProperty.getStyleName(), { - ...styleOptions, - ...colorOptions, - }); - }; + constructor() { + super(); + this._isMounted = false; + } - let colorRampSelect; - if (styleOptions.field && styleOptions.field.name) { - colorRampSelect = ( - - ); + componentWillUnmount() { + this._isMounted = false; + } + + componentDidMount() { + this._isMounted = true; + this._loadColorMapType(); + } + + componentDidUpdate() { + this._loadColorMapType(); + } + + async _loadColorMapType() { + const field = this.props.styleProperty.getField(); + if (!field) { + return; + } + const dataType = await field.getDataType(); + const colorMapType = CATEGORICAL_DATA_TYPES.includes(dataType) + ? COLOR_MAP_TYPE.CATEGORICAL + : COLOR_MAP_TYPE.ORDINAL; + if (this._isMounted && this.state.colorMapType !== colorMapType) { + this.setState({ colorMapType }, () => { + const options = this.props.styleProperty.getOptions(); + this.props.onDynamicStyleChange(this.props.styleProperty.getStyleName(), { + ...options, + type: colorMapType, + }); + }); + } } - return ( - - - {staticDynamicSelect} - - - - - - {colorRampSelect} - - ); + _getColorSelector() { + const { onDynamicStyleChange, styleProperty } = this.props; + const styleOptions = styleProperty.getOptions(); + + if (!styleOptions.field || !styleOptions.field.name) { + return; + } + + let colorSelect; + const onColorChange = colorOptions => { + const newColorOptions = { + type: colorOptions.type, + }; + if (colorOptions.type === COLOR_MAP_TYPE.ORDINAL) { + newColorOptions.useCustomColorRamp = colorOptions.useCustomColorMap; + newColorOptions.customColorRamp = colorOptions.customColorMap; + newColorOptions.color = colorOptions.color; + } else { + newColorOptions.useCustomColorPalette = colorOptions.useCustomColorMap; + newColorOptions.customColorPalette = colorOptions.customColorMap; + newColorOptions.colorCategory = colorOptions.color; + } + + onDynamicStyleChange(styleProperty.getStyleName(), { + ...styleOptions, + ...newColorOptions, + }); + }; + + if (this.state.colorMapType === COLOR_MAP_TYPE.ORDINAL) { + const customOptionLabel = i18n.translate('xpack.maps.style.customColorRampLabel', { + defaultMessage: 'Custom color ramp', + }); + colorSelect = ( + onColorChange(options)} + colorMapType={COLOR_MAP_TYPE.ORDINAL} + color={styleOptions.color} + customColorMap={styleOptions.customColorRamp} + useCustomColorMap={_.get(styleOptions, 'useCustomColorRamp', false)} + compressed + /> + ); + } else if (this.state.colorMapType === COLOR_MAP_TYPE.CATEGORICAL) { + const customOptionLabel = i18n.translate('xpack.maps.style.customColorPaletteLabel', { + defaultMessage: 'Custom color palette', + }); + colorSelect = ( + onColorChange(options)} + colorMapType={COLOR_MAP_TYPE.CATEGORICAL} + color={styleOptions.colorCategory} + customColorMap={styleOptions.customColorPalette} + useCustomColorMap={_.get(styleOptions, 'useCustomColorPalette', false)} + compressed + /> + ); + } + return colorSelect; + } + + render() { + const { fields, onDynamicStyleChange, staticDynamicSelect, styleProperty } = this.props; + const styleOptions = styleProperty.getOptions(); + const onFieldChange = options => { + const field = options.field; + onDynamicStyleChange(styleProperty.getStyleName(), { ...styleOptions, field }); + }; + + const colorSelect = this._getColorSelector(); + + return ( + + + {staticDynamicSelect} + + + + + + {colorSelect} + + ); + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/extract_color_from_style_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/extract_color_from_style_property.js index 157b863ac4986..2c41fb20bd4c0 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/extract_color_from_style_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/extract_color_from_style_property.js @@ -5,7 +5,8 @@ */ import { VectorStyle } from '../../vector_style'; -import { getColorRampCenterColor } from '../../../color_utils'; +import { getColorRampCenterColor, getColorPalette } from '../../../color_utils'; +import { COLOR_MAP_TYPE } from '../../../../../../common/constants'; export function extractColorFromStyleProperty(colorStyleProperty, defaultColor) { if (!colorStyleProperty) { @@ -21,19 +22,37 @@ export function extractColorFromStyleProperty(colorStyleProperty, defaultColor) return defaultColor; } - // return middle of gradient for dynamic style property + if (colorStyleProperty.options.type === COLOR_MAP_TYPE.CATEGORICAL) { + if (colorStyleProperty.options.useCustomColorPalette) { + return colorStyleProperty.options.customColorPalette && + colorStyleProperty.options.customColorPalette.length + ? colorStyleProperty.options.customColorPalette[0].colorCategory + : defaultColor; + } - if (colorStyleProperty.options.useCustomColorRamp) { - if ( - !colorStyleProperty.options.customColorRamp || - !colorStyleProperty.options.customColorRamp.length - ) { - return defaultColor; + if (!colorStyleProperty.options.colorCategory) { + return null; + } + + const palette = getColorPalette(colorStyleProperty.options.colorCategory); + return palette[0]; + } else { + // return middle of gradient for dynamic style property + if (colorStyleProperty.options.useCustomColorRamp) { + if ( + !colorStyleProperty.options.customColorRamp || + !colorStyleProperty.options.customColorRamp.length + ) { + return defaultColor; + } + // favor the lowest color in even arrays + const middleIndex = Math.floor((colorStyleProperty.options.customColorRamp.length - 1) / 2); + return colorStyleProperty.options.customColorRamp[middleIndex].color; } - // favor the lowest color in even arrays - const middleIndex = Math.floor((colorStyleProperty.options.customColorRamp.length - 1) / 2); - return colorStyleProperty.options.customColorRamp[middleIndex].color; - } - return getColorRampCenterColor(colorStyleProperty.options.color); + if (!colorStyleProperty.options.color) { + return null; + } + return getColorRampCenterColor(colorStyleProperty.options.color); + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_meta_options_popover.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/ordinal_field_meta_options_popover.js similarity index 98% rename from x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_meta_options_popover.js rename to x-pack/legacy/plugins/maps/public/layers/styles/vector/components/ordinal_field_meta_options_popover.js index 471403e1f3999..dee333f163960 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_meta_options_popover.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/ordinal_field_meta_options_popover.js @@ -31,7 +31,7 @@ function getIsEnableToggleLabel(styleName) { } } -export class FieldMetaOptionsPopover extends Component { +export class OrdinalFieldMetaOptionsPopover extends Component { state = { isPopoverOpen: false, }; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_prop_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_prop_editor.js index 1ac8edfb2cc69..e8b544d8ede16 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_prop_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_prop_editor.js @@ -5,7 +5,6 @@ */ import React, { Component, Fragment } from 'react'; -import { FieldMetaOptionsPopover } from './field_meta_options_popover'; import { getVectorStyleLabel } from './get_vector_style_label'; import { EuiFormRow, EuiSelect } from '@elastic/eui'; import { VectorStyle } from '../vector_style'; @@ -80,12 +79,9 @@ export class StylePropEditor extends Component { } render() { - const fieldMetaOptionsPopover = this.props.styleProperty.isDynamic() ? ( - - ) : null; + const fieldMetaOptionsPopover = this.props.styleProperty.renderFieldMetaPopover( + this._onFieldMetaOptionsChange + ); return ( { this.setState({ selectedFeature }); }; @@ -141,7 +153,7 @@ export class VectorStyleEditor extends Component { onStaticStyleChange={this._onStaticStyleChange} onDynamicStyleChange={this._onDynamicStyleChange} styleProperty={this.props.styleProperties[VECTOR_STYLES.FILL_COLOR]} - fields={this._getOrdinalFields()} + fields={this._getOrdinalAndCategoricalFields()} defaultStaticStyleOptions={ this.state.defaultStaticProperties[VECTOR_STYLES.FILL_COLOR].options } @@ -159,7 +171,7 @@ export class VectorStyleEditor extends Component { onStaticStyleChange={this._onStaticStyleChange} onDynamicStyleChange={this._onDynamicStyleChange} styleProperty={this.props.styleProperties[VECTOR_STYLES.LINE_COLOR]} - fields={this._getOrdinalFields()} + fields={this._getOrdinalAndCategoricalFields()} defaultStaticStyleOptions={ this.state.defaultStaticProperties[VECTOR_STYLES.LINE_COLOR].options } @@ -226,7 +238,7 @@ export class VectorStyleEditor extends Component { onStaticStyleChange={this._onStaticStyleChange} onDynamicStyleChange={this._onDynamicStyleChange} styleProperty={this.props.styleProperties[VECTOR_STYLES.LABEL_COLOR]} - fields={this._getOrdinalFields()} + fields={this._getOrdinalAndCategoricalFields()} defaultStaticStyleOptions={ this.state.defaultStaticProperties[VECTOR_STYLES.LABEL_COLOR].options } @@ -255,7 +267,7 @@ export class VectorStyleEditor extends Component { onStaticStyleChange={this._onStaticStyleChange} onDynamicStyleChange={this._onDynamicStyleChange} styleProperty={this.props.styleProperties[VECTOR_STYLES.LABEL_BORDER_COLOR]} - fields={this._getOrdinalFields()} + fields={this._getOrdinalAndCategoricalFields()} defaultStaticStyleOptions={ this.state.defaultStaticProperties[VECTOR_STYLES.LABEL_BORDER_COLOR].options } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/__snapshots__/dynamic_color_property.test.js.snap b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/__snapshots__/dynamic_color_property.test.js.snap index 8da8cfaa71e2c..3b3cade87a4ad 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/__snapshots__/dynamic_color_property.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/__snapshots__/dynamic_color_property.test.js.snap @@ -1,8 +1,128 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Should render categorical legend 1`] = `""`; +exports[`Should render categorical legend with breaks from custom 1`] = `""`; -exports[`Should render ranged legend 1`] = ` +exports[`Should render categorical legend with breaks from default 1`] = ` +
+ + + + + + + US_format + + + + + + + + + + + + CN_format + + + + + + + + + + + + + Other + + + + + + + + + + + + + + + + foobar_label + + + + + + +
+`; + +exports[`Should render ordinal legend 1`] = ` `; + +exports[`Should render ordinal legend with breaks 1`] = ` +
+ + + + + + + 0_format + + + + + + + + + + + + 10_format + + + + + + + + + + + + + + + foobar_label + + + + + + +
+`; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js index 804a0f8975d3e..42e88220bd1d9 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js @@ -7,12 +7,26 @@ import { DynamicStyleProperty } from './dynamic_style_property'; import _ from 'lodash'; import { getComputedFieldName } from '../style_util'; -import { getColorRampStops } from '../../color_utils'; +import { getOrdinalColorRampStops, getColorPalette } from '../../color_utils'; import { ColorGradient } from '../../components/color_gradient'; import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiToolTip } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiText, + EuiToolTip, + EuiTextColor, +} from '@elastic/eui'; import { VectorIcon } from '../components/legend/vector_icon'; import { VECTOR_STYLES } from '../vector_style_defaults'; +import { COLOR_MAP_TYPE } from '../../../../../common/constants'; +import { + isCategoricalStopsInvalid, + getOtherCategoryLabel, +} from '../components/color/color_stops_utils'; + +const EMPTY_STOPS = { stops: [], defaultColor: null }; export class DynamicColorProperty extends DynamicStyleProperty { syncCircleColorWithMb(mbLayerId, mbMap, alpha) { @@ -60,7 +74,17 @@ export class DynamicColorProperty extends DynamicStyleProperty { mbMap.setPaintProperty(mbLayerId, 'text-halo-color', color); } - isCustomColorRamp() { + isOrdinal() { + return ( + typeof this._options.type === 'undefined' || this._options.type === COLOR_MAP_TYPE.ORDINAL + ); + } + + isCategorical() { + return this._options.type === COLOR_MAP_TYPE.CATEGORICAL; + } + + isCustomOrdinalColorRamp() { return this._options.useCustomColorRamp; } @@ -68,16 +92,16 @@ export class DynamicColorProperty extends DynamicStyleProperty { return true; } - isScaled() { - return !this.isCustomColorRamp(); + isOrdinalScaled() { + return this.isOrdinal() && !this.isCustomOrdinalColorRamp(); } - isRanged() { - return !this.isCustomColorRamp(); + isOrdinalRanged() { + return this.isOrdinal() && !this.isCustomOrdinalColorRamp(); } - hasBreaks() { - return this.isCustomColorRamp(); + hasOrdinalBreaks() { + return (this.isOrdinal() && this.isCustomOrdinalColorRamp()) || this.isCategorical(); } _getMbColor() { @@ -87,6 +111,15 @@ export class DynamicColorProperty extends DynamicStyleProperty { return null; } + const targetName = getComputedFieldName(this._styleName, this._options.field.name); + if (this.isCategorical()) { + return this._getMbDataDrivenCategoricalColor({ targetName }); + } else { + return this._getMbDataDrivenOrdinalColor({ targetName }); + } + } + + _getMbDataDrivenOrdinalColor({ targetName }) { if ( this._options.useCustomColorRamp && (!this._options.customColorRamp || !this._options.customColorRamp.length) @@ -94,15 +127,12 @@ export class DynamicColorProperty extends DynamicStyleProperty { return null; } - return this._getMBDataDrivenColor({ - targetName: getComputedFieldName(this._styleName, this._options.field.name), - colorStops: this._getMBColorStops(), - isSteps: this._options.useCustomColorRamp, - }); - } + const colorStops = this._getMbOrdinalColorStops(); + if (!colorStops) { + return null; + } - _getMBDataDrivenColor({ targetName, colorStops, isSteps }) { - if (isSteps) { + if (this._options.useCustomColorRamp) { const firstStopValue = colorStops[0]; const lessThenFirstStopValue = firstStopValue - 1; return [ @@ -112,7 +142,6 @@ export class DynamicColorProperty extends DynamicStyleProperty { ...colorStops, ]; } - return [ 'interpolate', ['linear'], @@ -123,14 +152,92 @@ export class DynamicColorProperty extends DynamicStyleProperty { ]; } - _getMBColorStops() { + _getColorPaletteStops() { + if (this._options.useCustomColorPalette && this._options.customColorPalette) { + if (isCategoricalStopsInvalid(this._options.customColorPalette)) { + return EMPTY_STOPS; + } + + const stops = []; + for (let i = 1; i < this._options.customColorPalette.length; i++) { + const config = this._options.customColorPalette[i]; + stops.push({ + stop: config.stop, + color: config.color, + }); + } + + return { + defaultColor: this._options.customColorPalette[0].color, + stops, + }; + } + + const fieldMeta = this.getFieldMeta(); + if (!fieldMeta || !fieldMeta.categories) { + return EMPTY_STOPS; + } + + const colors = getColorPalette(this._options.colorCategory); + if (!colors) { + return EMPTY_STOPS; + } + + const maxLength = Math.min(colors.length, fieldMeta.categories.length + 1); + const stops = []; + + for (let i = 0; i < maxLength - 1; i++) { + stops.push({ + stop: fieldMeta.categories[i].key, + color: colors[i], + }); + } + return { + stops, + defaultColor: colors[maxLength - 1], + }; + } + + _getMbDataDrivenCategoricalColor() { + if ( + this._options.useCustomColorPalette && + (!this._options.customColorPalette || !this._options.customColorPalette.length) + ) { + return null; + } + + const { stops, defaultColor } = this._getColorPaletteStops(); + if (stops.length < 1) { + //occurs when no data + return null; + } + + if (!defaultColor) { + return null; + } + + const mbStops = []; + for (let i = 0; i < stops.length; i++) { + const stop = stops[i]; + const branch = `${stop.stop}`; + if (typeof branch === 'string') { + mbStops.push(branch); + mbStops.push(stop.color); + } + } + + mbStops.push(defaultColor); //last color is default color + return ['match', ['get', this._options.field.name], ...mbStops]; + } + + _getMbOrdinalColorStops() { if (this._options.useCustomColorRamp) { return this._options.customColorRamp.reduce((accumulatedStops, nextStop) => { return [...accumulatedStops, nextStop.stop, nextStop.color]; }, []); + } else { + return getOrdinalColorRampStops(this._options.color); } - - return getColorRampStops(this._options.color); } renderRangeLegendHeader() { @@ -163,18 +270,47 @@ export class DynamicColorProperty extends DynamicStyleProperty { ); } + _getColorRampStops() { + return this._options.useCustomColorRamp && this._options.customColorRamp + ? this._options.customColorRamp + : []; + } + + _getColorStops() { + if (this.isOrdinal()) { + return { + stops: this._getColorRampStops(), + defaultColor: null, + }; + } else if (this.isCategorical()) { + return this._getColorPaletteStops(); + } else { + return EMPTY_STOPS; + } + } + _renderColorbreaks({ isLinesOnly, isPointsOnly, symbolId }) { - if (!this._options.customColorRamp) { - return null; + const { stops, defaultColor } = this._getColorStops(); + const colorAndLabels = stops.map(config => { + return { + label: this.formatField(config.stop), + color: config.color, + }; + }); + + if (defaultColor) { + colorAndLabels.push({ + label: {getOtherCategoryLabel()}, + color: defaultColor, + }); } - return this._options.customColorRamp.map((config, index) => { - const value = this.formatField(config.stop); + return colorAndLabels.map((config, index) => { return ( - {value} + {config.label} {this._renderStopIcon(config.color, isLinesOnly, isPointsOnly, symbolId)} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js index 21c24e837b412..83cd101d30212 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js @@ -15,12 +15,12 @@ import { shallow } from 'enzyme'; import { VECTOR_STYLES } from '../vector_style_defaults'; import { DynamicColorProperty } from './dynamic_color_property'; +import { COLOR_MAP_TYPE } from '../../../../../common/constants'; const mockField = { async getLabel() { return 'foobar_label'; }, - getName() { return 'foobar'; }, @@ -29,33 +29,61 @@ const mockField = { }, }; -test('Should render ranged legend', () => { - const colorStyle = new DynamicColorProperty( - { - color: 'Blues', - }, +const getOrdinalFieldMeta = () => { + return { min: 0, max: 100 }; +}; + +const getCategoricalFieldMeta = () => { + return { + categories: [ + { + key: 'US', + count: 10, + }, + { + key: 'CN', + count: 8, + }, + ], + }; +}; +const makeProperty = (options, getFieldMeta) => { + return new DynamicColorProperty( + options, VECTOR_STYLES.LINE_COLOR, mockField, - () => { - return { min: 0, max: 100 }; - }, + getFieldMeta, () => { return x => x + '_format'; } ); +}; + +const defaultLegendParams = { + isPointsOnly: true, + isLinesOnly: false, +}; + +test('Should render ordinal legend', async () => { + const colorStyle = makeProperty( + { + color: 'Blues', + type: undefined, + }, + getOrdinalFieldMeta + ); + + const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams); - const legendRow = colorStyle.renderLegendDetailRow({ - isPointsOnly: true, - isLinesOnly: false, - }); const component = shallow(legendRow); expect(component).toMatchSnapshot(); }); -test('Should render categorical legend', () => { - const colorStyle = new DynamicColorProperty( +test('Should render ordinal legend with breaks', async () => { + const colorStyle = makeProperty( { + type: COLOR_MAP_TYPE.ORDINAL, useCustomColorRamp: true, customColorRamp: [ { @@ -68,21 +96,128 @@ test('Should render categorical legend', () => { }, ], }, - VECTOR_STYLES.LINE_COLOR, - mockField, - () => { - return { min: 0, max: 100 }; + getOrdinalFieldMeta + ); + + 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 default', async () => { + const colorStyle = makeProperty( + { + type: COLOR_MAP_TYPE.CATEGORICAL, + useCustomColorPalette: false, + colorCategory: 'palette_0', }, - () => { - return x => x + '_format'; - } + getCategoricalFieldMeta ); - const legendRow = colorStyle.renderLegendDetailRow({ - isPointsOnly: true, - isLinesOnly: false, - }); + 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', + }, + ], + }, + getCategoricalFieldMeta + ); + + const legendRow = colorStyle.renderLegendDetailRow(defaultLegendParams); + const component = shallow(legendRow); expect(component).toMatchSnapshot(); }); + +function makeFeatures(foobarPropValues) { + return foobarPropValues.map(value => { + return { + type: 'Feature', + properties: { + foobar: value, + }, + }; + }); +} + +test('Should pluck the categorical style-meta', async () => { + const colorStyle = makeProperty({ + type: COLOR_MAP_TYPE.CATEGORICAL, + colorCategory: 'palette_0', + getCategoricalFieldMeta, + }); + + const features = makeFeatures(['CN', 'CN', 'US', 'CN', 'US', 'IN']); + const meta = colorStyle.pluckStyleMetaFromFeatures(features); + + expect(meta).toEqual({ + categories: [ + { key: 'CN', count: 3 }, + { key: 'US', count: 2 }, + { key: 'IN', count: 1 }, + ], + }); +}); + +test('Should pluck the categorical style-meta from fieldmeta', async () => { + const colorStyle = makeProperty({ + type: COLOR_MAP_TYPE.CATEGORICAL, + colorCategory: 'palette_0', + getCategoricalFieldMeta, + }); + + const meta = colorStyle.pluckStyleMetaFromFieldMetaData({ + foobar: { + buckets: [ + { + key: 'CN', + doc_count: 3, + }, + { key: 'US', doc_count: 2 }, + { key: 'IN', doc_count: 1 }, + ], + }, + }); + + expect(meta).toEqual({ + categories: [ + { key: 'CN', count: 3 }, + { key: 'US', count: 2 }, + { key: 'IN', count: 1 }, + ], + }); +}); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js index 5b6f494600c2a..1d2457142c082 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js @@ -26,7 +26,7 @@ export class DynamicOrientationProperty extends DynamicStyleProperty { return false; } - isScaled() { + isOrdinalScaled() { return false; } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js index 97ab7cb78015b..98e87b0305b44 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js @@ -7,11 +7,12 @@ import _ from 'lodash'; import { AbstractStyleProperty } from './style_property'; import { DEFAULT_SIGMA } from '../vector_style_defaults'; -import { STYLE_TYPE } from '../../../../../common/constants'; +import { COLOR_PALETTE_MAX_SIZE, STYLE_TYPE } from '../../../../../common/constants'; import { scaleValue, getComputedFieldName } from '../style_util'; import React from 'react'; import { OrdinalLegend } from './components/ordinal_legend'; import { CategoricalLegend } from './components/categorical_legend'; +import { OrdinalFieldMetaOptionsPopover } from '../components/ordinal_field_meta_options_popover'; export class DynamicStyleProperty extends AbstractStyleProperty { static type = STYLE_TYPE.DYNAMIC; @@ -46,11 +47,15 @@ export class DynamicStyleProperty extends AbstractStyleProperty { return true; } - hasBreaks() { + isCategorical() { return false; } - isRanged() { + hasOrdinalBreaks() { + return false; + } + + isOrdinalRanged() { return true; } @@ -68,21 +73,33 @@ export class DynamicStyleProperty extends AbstractStyleProperty { } supportsFieldMeta() { - return this.isComplete() && this.isScaled() && this._field.supportsFieldMeta(); + if (this.isOrdinal()) { + return this.isComplete() && this.isOrdinalScaled() && this._field.supportsFieldMeta(); + } else if (this.isCategorical()) { + return this.isComplete() && this._field.supportsFieldMeta(); + } else { + return false; + } } async getFieldMetaRequest() { - const fieldMetaOptions = this.getFieldMetaOptions(); - return this._field.getFieldMetaRequest({ - sigma: _.get(fieldMetaOptions, 'sigma', DEFAULT_SIGMA), - }); + if (this.isOrdinal()) { + const fieldMetaOptions = this.getFieldMetaOptions(); + return this._field.getOrdinalFieldMetaRequest({ + sigma: _.get(fieldMetaOptions, 'sigma', DEFAULT_SIGMA), + }); + } else if (this.isCategorical()) { + return this._field.getCategoricalFieldMetaRequest(); + } else { + return null; + } } supportsFeatureState() { return true; } - isScaled() { + isOrdinalScaled() { return true; } @@ -90,11 +107,7 @@ export class DynamicStyleProperty extends AbstractStyleProperty { return _.get(this.getOptions(), 'fieldMetaOptions', {}); } - pluckStyleMetaFromFeatures(features) { - if (!this.isOrdinal()) { - return null; - } - + _pluckOrdinalStyleMetaFromFeatures(features) { const name = this.getField().getName(); let min = Infinity; let max = -Infinity; @@ -116,11 +129,47 @@ export class DynamicStyleProperty extends AbstractStyleProperty { }; } - pluckStyleMetaFromFieldMetaData(fieldMetaData) { - if (!this.isOrdinal()) { + _pluckCategoricalStyleMetaFromFeatures(features) { + const fieldName = this.getField().getName(); + const counts = new Map(); + for (let i = 0; i < features.length; i++) { + const feature = features[i]; + const term = feature.properties[fieldName]; + //properties object may be sparse, so need to check if the field is effectively present + if (typeof term !== undefined) { + if (counts.has(term)) { + counts.set(term, counts.get(term) + 1); + } else { + counts.set(term, 1); + } + } + } + + const ordered = []; + for (const [key, value] of counts) { + ordered.push({ key, count: value }); + } + + ordered.sort((a, b) => { + return b.count - a.count; + }); + const truncated = ordered.slice(0, COLOR_PALETTE_MAX_SIZE); + return { + categories: truncated, + }; + } + + pluckStyleMetaFromFeatures(features) { + if (this.isOrdinal()) { + return this._pluckOrdinalStyleMetaFromFeatures(features); + } else if (this.isCategorical()) { + return this._pluckCategoricalStyleMetaFromFeatures(features); + } else { return null; } + } + _pluckOrdinalStyleMetaFromFieldMetaData(fieldMetaData) { const realFieldName = this._field.getESDocFieldName ? this._field.getESDocFieldName() : this._field.getName(); @@ -143,6 +192,33 @@ export class DynamicStyleProperty extends AbstractStyleProperty { }; } + _pluckCategoricalStyleMetaFromFieldMetaData(fieldMetaData) { + const name = this.getField().getName(); + if (!fieldMetaData[name] || !fieldMetaData[name].buckets) { + return null; + } + + const ordered = fieldMetaData[name].buckets.map(bucket => { + return { + key: bucket.key, + count: bucket.doc_count, + }; + }); + return { + categories: ordered, + }; + } + + pluckStyleMetaFromFieldMetaData(fieldMetaData) { + if (this.isOrdinal()) { + return this._pluckOrdinalStyleMetaFromFieldMetaData(fieldMetaData); + } else if (this.isCategorical()) { + return this._pluckCategoricalStyleMetaFromFieldMetaData(fieldMetaData); + } else { + return null; + } + } + formatField(value) { if (this.getField()) { const fieldName = this.getField().getName(); @@ -159,7 +235,7 @@ export class DynamicStyleProperty extends AbstractStyleProperty { } const valueAsFloat = parseFloat(value); - if (this.isScaled()) { + if (this.isOrdinalScaled()) { return scaleValue(valueAsFloat, this.getFieldMeta()); } if (isNaN(valueAsFloat)) { @@ -188,12 +264,28 @@ export class DynamicStyleProperty extends AbstractStyleProperty { } renderLegendDetailRow({ isPointsOnly, isLinesOnly, symbolId }) { - if (this.isRanged()) { - return this._renderRangeLegend(); - } else if (this.hasBreaks()) { + if (this.isOrdinal()) { + if (this.isOrdinalRanged()) { + return this._renderRangeLegend(); + } else if (this.hasOrdinalBreaks()) { + return this._renderCategoricalLegend({ isPointsOnly, isLinesOnly, symbolId }); + } else { + return null; + } + } else if (this.isCategorical()) { return this._renderCategoricalLegend({ isPointsOnly, isLinesOnly, symbolId }); } else { return null; } } + + renderFieldMetaPopover(onFieldMetaOptionsChange) { + if (!this.isOrdinal() || !this.supportsFieldMeta()) { + return null; + } + + return ( + + ); + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_text_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_text_property.js index fbc4c3af78f98..6a40a80a1a7a6 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_text_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_text_property.js @@ -29,7 +29,7 @@ export class DynamicTextProperty extends DynamicStyleProperty { return false; } - isScaled() { + isOrdinalScaled() { return false; } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.js index 52e1a46a18e94..c49fe46664025 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.js @@ -45,6 +45,10 @@ export class AbstractStyleProperty { return null; } + renderFieldMetaPopover() { + return null; + } + getDisplayStyleName() { return getVectorStyleLabel(this.getStyleName()); } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js index 3631613e7907c..54af55b61ab2e 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js @@ -6,7 +6,12 @@ import { VectorStyle } from './vector_style'; import { SYMBOLIZE_AS_CIRCLE, DEFAULT_ICON_SIZE } from './vector_constants'; -import { COLOR_GRADIENTS, DEFAULT_FILL_COLORS, DEFAULT_LINE_COLORS } from '../color_utils'; +import { + COLOR_GRADIENTS, + COLOR_PALETTES, + DEFAULT_FILL_COLORS, + DEFAULT_LINE_COLORS, +} from '../color_utils'; import chrome from 'ui/chrome'; const DEFAULT_ICON = 'airfield'; @@ -136,6 +141,7 @@ export function getDefaultDynamicProperties() { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { color: COLOR_GRADIENTS[0].value, + colorCategory: COLOR_PALETTES[0].value, field: undefined, fieldMetaOptions: { isEnabled: true, @@ -146,7 +152,7 @@ export function getDefaultDynamicProperties() { [VECTOR_STYLES.LINE_COLOR]: { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { - color: COLOR_GRADIENTS[0].value, + color: undefined, field: undefined, fieldMetaOptions: { isEnabled: true, @@ -198,6 +204,7 @@ export function getDefaultDynamicProperties() { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { color: COLOR_GRADIENTS[0].value, + colorCategory: COLOR_PALETTES[0].value, field: undefined, fieldMetaOptions: { isEnabled: true, @@ -221,6 +228,7 @@ export function getDefaultDynamicProperties() { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { color: COLOR_GRADIENTS[0].value, + colorCategory: COLOR_PALETTES[0].value, field: undefined, fieldMetaOptions: { isEnabled: true, diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js index dd9a1b7a14c10..96223aa536170 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js @@ -213,6 +213,10 @@ export class VectorLayer extends AbstractLayer { return [...(await this.getDateFields()), ...(await this.getNumberFields())]; } + async getCategoricalFields() { + return await this._source.getCategoricalFields(); + } + async getFields() { const sourceFields = await this._source.getFields(); return [...sourceFields, ...this._getJoinFields()];