From 721ad063f6f6a0b7f9a424a00c9b96c9eb96afe9 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Sat, 11 Jan 2020 22:07:53 -0500 Subject: [PATCH] add categories remove cruft remove cruft more palettes move options to size as well default rename auto-select tmp --- .../legacy/plugins/maps/common/constants.js | 7 + .../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_search_source/es_search_source.js | 15 ++ .../public/layers/sources/vector_source.js | 4 + .../maps/public/layers/styles/color_utils.js | 43 +++++ .../components/color/color_palette_select.js | 130 ++++++++++++++ .../components/color/color_ramp_select.js | 20 ++- .../color/color_stops_categorical.js | 147 ++++++++++++++++ ...{color_stops.js => color_stops_ordinal.js} | 7 +- .../components/color/dynamic_color_form.js | 163 +++++++++++++----- .../extract_color_from_style_property.js | 31 ++-- ... => ordinal_field_meta_options_popover.js} | 2 +- .../components/size/dynamic_size_form.js | 17 ++ .../vector/components/style_prop_editor.js | 17 -- .../vector/components/vector_style_editor.js | 18 +- .../properties/dynamic_color_property.js | 97 ++++++++--- .../properties/dynamic_style_property.js | 98 +++++++++-- .../maps/public/layers/vector_layer.js | 4 + 21 files changed, 743 insertions(+), 119 deletions(-) create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_palette_select.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js rename x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/{color_stops.js => color_stops_ordinal.js} (96%) 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 6e7776d43f4d46c..ab8b853377b3b60 100644 --- a/x-pack/legacy/plugins/maps/common/constants.js +++ b/x-pack/legacy/plugins/maps/common/constants.js @@ -140,3 +140,10 @@ 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; 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 189aad378503438..65109cb99809fce 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 ee082e4546b8b8d..0b605b851880709 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 yet support categorical styling for number/date fields + return null; + } + + const topTerms = { + size: COLOR_PALETTE_MAX_SIZE, + }; + 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 f1401a78e217480..b5d157ad1697aa9 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 21c5f15fb61225a..80da9555b384e4b 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 getStringFields() { + return []; + } + async getFields() { return []; } 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 8ef4966e03c1b4c..39015f85ce87551 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 @@ -125,6 +125,21 @@ export class ESSearchSource extends AbstractESSource { } } + async getStringFields() { + try { + const indexPattern = await this.getIndexPattern(); + const aggFields = indexPattern.fields.getByType('string').filter(field => { + return field.aggregatable; + }); + 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 bf7267e9c5858ee..0dec50d77dd703d 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 getStringFields() { + 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 8aa32fa7e09c08e..a6188b317573bdb 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 { palettes } 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; @@ -84,3 +85,45 @@ export function getLinearGradient(colorStrings) { } return `${linearGradient} ${colorStrings[colorStrings.length - 1]} 100%)`; } + +export const COLOR_PALETTES = [ + { + 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.find(palette => palette.id === paletteId); + return palette ? palette.colors : null; +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_palette_select.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_palette_select.js new file mode 100644 index 000000000000000..b161e9a24df5809 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_palette_select.js @@ -0,0 +1,130 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { ColorStopsCategorical } from './color_stops_categorical'; +import { COLOR_PALETTES } from '../../../color_utils'; +import { COLOR_MAP_TYPE } from '../../../../../../common/constants'; + +const CUSTOM_COLOR_PALETTE = 'CUSTOM_COLOR_PALETTE'; +const CUSTOM_OPTION = { + value: CUSTOM_COLOR_PALETTE, + inputDisplay: ( + + ), +}; + +const colorPaletteInputs = COLOR_PALETTES.map(palette => { + const paletteDisplay = palette.colors.map(color => { + const style = { + backgroundColor: color, + width: '10%', + position: 'relative', + height: '100%', + display: 'inline-block', + }; + // eslint-disable-next-line react/no-danger + return
; + }); + return { + value: palette.id, + inputDisplay:
{paletteDisplay}
, + }; +}); + +export class ColorPaletteSelect extends Component { + state = {}; + + static getDerivedStateFromProps(nextProps, prevState) { + if (nextProps.customColorPalette !== prevState.prevPropsCustomColorPalette) { + return { + prevPropsCustomColorPalette: nextProps.customColorPalette, // reset tracker to latest value + customColorPalette: nextProps.customColorPalette, // reset customColorPalette to latest value + }; + } + + return null; + } + + _onColorPaletteSelect = selectedValue => { + const useCustomColorPalette = selectedValue === CUSTOM_COLOR_PALETTE; + this.props.onChange({ + type: COLOR_MAP_TYPE.CATEGORICAL, + color: useCustomColorPalette ? null : selectedValue, + useCustomColorPalette, + }); + }; + + _onCustomColorPaletteChange = ({ colorStops }) => { + this.props.onChange({ + type: COLOR_MAP_TYPE.CATEGORICAL, + customColorPalette: colorStops, + }); + }; + + render() { + const { + color, + onChange, // eslint-disable-line no-unused-vars + useCustomColorPalette, + customColorPalette, // eslint-disable-line no-unused-vars + ...rest + } = this.props; + + let colorStopsInput; + if (useCustomColorPalette) { + colorStopsInput = ( + + + + + ); + } + + const colorPaletteOptions = [CUSTOM_OPTION, ...colorPaletteInputs]; + let valueOfSelected; + if (useCustomColorPalette) { + valueOfSelected = CUSTOM_COLOR_PALETTE; + } else { + if (colorPaletteOptions.find(option => option.value === color)) { + valueOfSelected = color; + } else { + valueOfSelected = colorPaletteInputs[0].value; + this._onColorPaletteSelect(valueOfSelected); + } + } + + return ( + + + {colorStopsInput} + + ); + } +} + +ColorPaletteSelect.propTypes = { + color: PropTypes.string, + onChange: PropTypes.func.isRequired, + useCustomColorPalette: PropTypes.bool, + customColorPalette: PropTypes.array, +}; 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 index c2dd51a0182e34c..f8dd7fa11c27cd1 100644 --- 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 @@ -10,7 +10,8 @@ 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'; +import { ColorStopsOrdinal } from './color_stops_ordinal'; +import { COLOR_MAP_TYPE } from '../../../../../../common/constants'; const CUSTOM_COLOR_RAMP = 'CUSTOM_COLOR_RAMP'; @@ -33,6 +34,7 @@ export class ColorRampSelect extends Component { this.props.onChange({ color: useCustomColorRamp ? null : selectedValue, useCustomColorRamp, + type: COLOR_MAP_TYPE.ORDINAL, }); }; @@ -45,6 +47,7 @@ export class ColorRampSelect extends Component { this.props.onChange({ customColorRamp: colorStops, + type: COLOR_MAP_TYPE.ORDINAL, }); }; @@ -62,7 +65,7 @@ export class ColorRampSelect extends Component { colorStopsInput = ( - @@ -82,13 +85,24 @@ export class ColorRampSelect extends Component { }, ...COLOR_GRADIENTS, ]; + let valueOfSelected; + if (useCustomColorRamp) { + valueOfSelected = CUSTOM_COLOR_RAMP; + } else { + if (colorRampOptions.find(option => option.value === color)) { + valueOfSelected = color; + } else { + valueOfSelected = COLOR_GRADIENTS[0].value; + this._onColorRampSelect(valueOfSelected); + } + } return ( 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 000000000000000..3c6f47f25638874 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import React from 'react'; +import PropTypes from 'prop-types'; + +import { + EuiColorPicker, + EuiFormRow, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiButtonIcon, +} from '@elastic/eui'; +import { addRow, removeRow, isColorInvalid } from './color_stops_utils'; + +const DEFAULT_COLOR = '#FF0000'; + +export const ColorStopsCategorical = ({ + colorStops = [{ stop: 'foobar', color: DEFAULT_COLOR }], + onChange, +}) => { + function getStopInput(stop, index) { + const onStopChange = e => { + const newColorStops = _.cloneDeep(colorStops); + newColorStops[index].stop = e.target.value; + onChange({ + colorStops: newColorStops, + isInvalid: false, + }); + }; + + return { + stopInput: , + }; + } + + function getColorInput(color, index) { + const onColorChange = color => { + const newColorStops = _.cloneDeep(colorStops); + newColorStops[index].color = color; + onChange({ + colorStops: newColorStops, + isInvalid: false, + }); + }; + + 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 errors = []; + if (stopError) { + errors.push(stopError); + } + if (colorError) { + errors.push(colorError); + } + + const onRemove = () => { + const newColorStops = removeRow(colorStops, index); + onChange({ + colorStops: newColorStops, + isInvalid: false, + }); + }; + + const onAdd = () => { + const newColorStops = addRow(colorStops, index); + + onChange({ + colorStops: newColorStops, + isInvalid: false, + }); + }; + + let deleteButton; + if (colorStops.length > 1) { + deleteButton = ( + + ); + } + + return ( + +
+ + {stopInput} + {colorInput} + +
+ {deleteButton} + +
+
+
+ ); + }); + + return
{rows}
; +}; + +ColorStopsCategorical.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.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js similarity index 96% rename from x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops.js rename to x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js index d523cf5870912d1..e5c98804364ec2f 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_ordinal.js @@ -20,7 +20,10 @@ import { addRow, removeRow, isColorInvalid, isStopInvalid, isInvalid } from './c const DEFAULT_COLOR = '#FF0000'; -export const ColorStops = ({ colorStops = [{ stop: 0, color: DEFAULT_COLOR }], onChange }) => { +export const ColorStopsOrdinal = ({ + colorStops = [{ stop: 0, color: DEFAULT_COLOR }], + onChange, +}) => { function getStopInput(stop, index) { const onStopChange = e => { const newColorStops = _.cloneDeep(colorStops); @@ -135,7 +138,7 @@ export const ColorStops = ({ colorStops = [{ stop: 0, color: DEFAULT_COLOR }], o return
{rows}
; }; -ColorStops.propTypes = { +ColorStopsOrdinal.propTypes = { /** * Array of { stop, color }. * Stops are numbers in strictly ascending order. 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 5e0f7434b04d08e..b7ebe9823803737 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 @@ -8,55 +8,134 @@ import _ from 'lodash'; import React, { Fragment } from 'react'; import { FieldSelect } from '../field_select'; import { ColorRampSelect } from './color_ramp_select'; +import { ColorPaletteSelect } from './color_palette_select'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { COLOR_MAP_TYPE } from '../../../../../../common/constants'; +import { OrdinalFieldMetaOptionsPopover } from '../ordinal_field_meta_options_popover'; -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(); } - return ( - - - {staticDynamicSelect} - - { + const options = { + ...this.props.styleProperty.getOptions(), + fieldMetaOptions, + }; + this.props.onDynamicStyleChange(this.props.styleProperty.getStyleName(), options); + }; + + _getColorSelector() { + const { onDynamicStyleChange, styleProperty } = this.props; + const styleOptions = styleProperty.getOptions(); + + let colorSelect; + if (styleOptions.field && styleOptions.field.name) { + const onColorChange = colorOptions => { + // if (colorOptions.type === COLOR_MAP_TYPE.ORDINAL) { + // delete styleOptions.useCustomColorPalette; + // delete styleOptions.customColorPalette; + // } else if (colorOptions.type === COLOR_MAP_TYPE.CATEGORICAL) { + // delete styleOptions.useCustomColorRamp; + // delete styleOptions.customColorRamp; + // } + + onDynamicStyleChange(styleProperty.getStyleName(), { + ...styleOptions, + ...colorOptions, + }); + }; + if (this.state.colorMapType === COLOR_MAP_TYPE.ORDINAL) { + colorSelect = ( + onColorChange(options)} + color={styleOptions.color} + customColorRamp={styleOptions.customColorRamp} + useCustomColorRamp={_.get(styleOptions, 'useCustomColorRamp', false)} + compressed + /> + ); + } else if (this.state.colorMapType === COLOR_MAP_TYPE.CATEGORICAL) { + colorSelect = ( + onColorChange(options)} + color={styleOptions.color} + customColorPalette={styleOptions.customColorPalette} + useCustomColorPalette={_.get(styleOptions, 'useCustomColorPalette', false)} compressed /> - - - - {colorRampSelect} - - ); + ); + } + return colorSelect; + } + } + + render() { + const { fields, onDynamicStyleChange, staticDynamicSelect, styleProperty } = this.props; + const styleOptions = styleProperty.getOptions(); + const onFieldChange = ({ field }) => { + onDynamicStyleChange(styleProperty.getStyleName(), { ...styleOptions, field }); + }; + + const colorSelect = this._getColorSelector(); + + const fieldMetaOptionsPopover = + this.state.colorMapType === COLOR_MAP_TYPE.ORDINAL && styleProperty.supportsFieldMeta() ? ( + + ) : null; + + return ( + + + {staticDynamicSelect} + + + + + + {colorSelect} + {fieldMetaOptionsPopover} + + ); + } } 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 157b863ac498673..00965d2c212e664 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 @@ -6,6 +6,7 @@ import { VectorStyle } from '../../vector_style'; import { getColorRampCenterColor } from '../../../color_utils'; +import { COLOR_MAP_TYPE } from '../../../../../../common/constants'; export function extractColorFromStyleProperty(colorStyleProperty, defaultColor) { if (!colorStyleProperty) { @@ -21,19 +22,23 @@ export function extractColorFromStyleProperty(colorStyleProperty, defaultColor) return defaultColor; } - // return middle of gradient for dynamic style property - - if (colorStyleProperty.options.useCustomColorRamp) { - if ( - !colorStyleProperty.options.customColorRamp || - !colorStyleProperty.options.customColorRamp.length - ) { - return defaultColor; + if (colorStyleProperty.options.type === COLOR_MAP_TYPE.CATEGORICAL) { + //todo + return null; + } 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); + 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 471403e1f3999d6..dee333f1639607f 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/size/dynamic_size_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/dynamic_size_form.js index 8b069cd53b731ac..2f3a80684b3b140 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/dynamic_size_form.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/dynamic_size_form.js @@ -9,6 +9,7 @@ import React, { Fragment } from 'react'; import { FieldSelect } from '../field_select'; import { SizeRangeSelector } from './size_range_selector'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { OrdinalFieldMetaOptionsPopover } from '../ordinal_field_meta_options_popover'; export function DynamicSizeForm({ fields, @@ -22,6 +23,14 @@ export function DynamicSizeForm({ onDynamicStyleChange(styleProperty.getStyleName(), { ...styleOptions, field }); }; + const onFieldMetaOptionsChange = fieldMetaOptions => { + const options = { + ...styleProperty.getOptions(), + fieldMetaOptions, + }; + onDynamicStyleChange(styleProperty.getStyleName(), options); + }; + const onSizeRangeChange = ({ minSize, maxSize }) => { onDynamicStyleChange(styleProperty.getStyleName(), { ...styleOptions, @@ -43,6 +52,13 @@ export function DynamicSizeForm({ ); } + const fieldMetaOptionsPopover = styleProperty.supportsFieldMeta() ? ( + + ) : null; + return ( @@ -58,6 +74,7 @@ export function DynamicSizeForm({ {sizeRange} + {fieldMetaOptionsPopover} ); } 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 1ac8edfb2cc694d..ed80e94358a106a 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'; @@ -35,14 +34,6 @@ export class StylePropEditor extends Component { } }; - _onFieldMetaOptionsChange = fieldMetaOptions => { - const options = { - ...this.props.styleProperty.getOptions(), - fieldMetaOptions, - }; - this.props.onDynamicStyleChange(this.props.styleProperty.getStyleName(), options); - }; - renderStaticDynamicSelect() { const options = [ { @@ -80,13 +71,6 @@ export class StylePropEditor extends Component { } render() { - const fieldMetaOptionsPopover = this.props.styleProperty.isDynamic() ? ( - - ) : null; - return ( ); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js index 8e80e036dbb8bc0..d65a75a795738ba 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js @@ -31,6 +31,7 @@ export class VectorStyleEditor extends Component { state = { dateFields: [], numberFields: [], + stringFields: [], fields: [], defaultDynamicProperties: getDefaultDynamicProperties(), defaultStaticProperties: getDefaultStaticProperties(), @@ -76,6 +77,13 @@ export class VectorStyleEditor extends Component { this.setState({ numberFields: numberFieldsArray }); } + const stringFields = await this.props.layer.getStringFields(); + const stringFieldPromises = stringFields.map(getFieldMeta); + const stringFieldsArray = await Promise.all(stringFieldPromises); + if (this._isMounted && !_.isEqual(stringFieldsArray, this.state.stringFields)) { + this.setState({ stringFields: stringFieldsArray }); + } + const fields = await this.props.layer.getFields(); const fieldPromises = fields.map(getFieldMeta); const fieldsArray = await Promise.all(fieldPromises); @@ -125,6 +133,10 @@ export class VectorStyleEditor extends Component { return [...this.state.dateFields, ...this.state.numberFields]; } + _getOrdinalAndStringFields() { + return [...this.state.dateFields, ...this.state.numberFields, ...this.state.stringFields]; + } + _handleSelectedFeatureChange = selectedFeature => { this.setState({ selectedFeature }); }; @@ -156,7 +168,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._getOrdinalAndStringFields()} defaultStaticStyleOptions={ this.state.defaultStaticProperties[VECTOR_STYLES.FILL_COLOR].options } @@ -174,7 +186,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._getOrdinalAndStringFields()} defaultStaticStyleOptions={ this.state.defaultStaticProperties[VECTOR_STYLES.LINE_COLOR].options } @@ -241,7 +253,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._getOrdinalAndStringFields()} defaultStaticStyleOptions={ this.state.defaultStaticProperties[VECTOR_STYLES.LABEL_COLOR].options } 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 200df9e5cc33d30..74df47c6a7f2202 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,13 @@ import { DynamicStyleProperty } from './dynamic_style_property'; import _ from 'lodash'; import { getComputedFieldName } from '../style_util'; -import { getColorRampStops } from '../../color_utils'; +import { getColorRampStops, getColorPalette } from '../../color_utils'; import { ColorGradient } from '../../components/color_gradient'; import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiToolTip } from '@elastic/eui'; import { VectorIcon } from '../components/legend/vector_icon'; import { VECTOR_STYLES } from '../vector_style_defaults'; +import { COLOR_MAP_TYPE } from '../../../../../common/constants'; export class DynamicColorProperty extends DynamicStyleProperty { syncCircleColorWithMb(mbLayerId, mbMap, alpha) { @@ -55,7 +56,17 @@ export class DynamicColorProperty extends DynamicStyleProperty { mbMap.setPaintProperty(mbLayerId, 'text-opacity', alpha); } - 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; } @@ -64,15 +75,15 @@ export class DynamicColorProperty extends DynamicStyleProperty { } isScaled() { - return !this.isCustomColorRamp(); + return this.isOrdinal() && !this.isCustomOrdinalColorRamp(); } isRanged() { - return !this.isCustomColorRamp(); + return this.isOrdinal() && !this.isCustomOrdinalColorRamp(); } hasBreaks() { - return this.isCustomColorRamp(); + return (this.isOrdinal() && this.isCustomOrdinalColorRamp()) || this.isCategorical(); } _getMbColor() { @@ -82,6 +93,12 @@ export class DynamicColorProperty extends DynamicStyleProperty { return null; } + return this._getMBDataDrivenColor({ + targetName: getComputedFieldName(this._styleName, this._options.field.name), + }); + } + + _getMbDataDrivenOrdinalColor({ targetName }) { if ( this._options.useCustomColorRamp && (!this._options.customColorRamp || !this._options.customColorRamp.length) @@ -89,15 +106,8 @@ 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, - }); - } - - _getMBDataDrivenColor({ targetName, colorStops, isSteps }) { - if (isSteps) { + const colorStops = this._getMbOrdinalColorStops(); + if (this._options.useCustomColorRamp) { const firstStopValue = colorStops[0]; const lessThenFirstStopValue = firstStopValue - 1; return [ @@ -107,7 +117,6 @@ export class DynamicColorProperty extends DynamicStyleProperty { ...colorStops, ]; } - return [ 'interpolate', ['linear'], @@ -118,14 +127,60 @@ export class DynamicColorProperty extends DynamicStyleProperty { ]; } - _getMBColorStops() { - if (this._options.useCustomColorRamp) { - return this._options.customColorRamp.reduce((accumulatedStops, nextStop) => { - return [...accumulatedStops, nextStop.stop, nextStop.color]; - }, []); + _getMbDataDrivenCategoricalColor() { + if ( + this._options.useCustomColorPalette && + (!this._options.customColorPalette || !this._options.customColorPalette.length) + ) { + return null; + } + + if (this._options.useCustomColorPalette && this._options.customColorPalette) { + const mbStops = []; + this._options.customColorPalette.forEach(stop => { + mbStops.push(stop.stop); + mbStops.push(stop.color); + }); + mbStops.push('rgba(0,0,0,0)'); + return ['match', ['get', this._options.field.name], ...mbStops]; + } else { + const fieldMeta = this.getFieldMeta(); + if (!fieldMeta || !fieldMeta.categories) { + return; + } + + const colors = getColorPalette(this._options.color); + const maxLength = Math.min(colors.length, fieldMeta.categories.length); + const mbStops = []; + for (let i = 0; i < maxLength; i++) { + mbStops.push(fieldMeta.categories[i].key); + mbStops.push(colors[i]); + } + mbStops.push('rgba(0,0,0,0)'); + return ['match', ['get', this._options.field.name], ...mbStops]; } + } - return getColorRampStops(this._options.color); + _getMBDataDrivenColor({ targetName }) { + if (this.isCategorical()) { + return this._getMbDataDrivenCategoricalColor({ targetName }); + } else { + return this._getMbDataDrivenOrdinalColor({ targetName }); + } + } + + _getOrdinalColorStopsFromCustom() { + return this._options.customColorRamp.reduce((accumulatedStops, nextStop) => { + return [...accumulatedStops, nextStop.stop, nextStop.color]; + }, []); + } + + _getMbOrdinalColorStops() { + if (this._options.useCustomColorRamp) { + return this._getOrdinalColorStopsFromCustom(); + } else { + return getColorRampStops(this._options.color); + } } renderRangeLegendHeader() { 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 bac3c96581967c7..55c452cacaa8405 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,7 +7,7 @@ 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 } from '../style_util'; import React from 'react'; import { OrdinalLegend } from './components/ordinal_legend'; @@ -39,6 +39,10 @@ export class DynamicStyleProperty extends AbstractStyleProperty { return true; } + isCategorical() { + return false; + } + hasBreaks() { return false; } @@ -61,14 +65,26 @@ export class DynamicStyleProperty extends AbstractStyleProperty { } supportsFieldMeta() { - return this.isComplete() && this.isScaled() && this._field.supportsFieldMeta(); + if (this.isOrdinal()) { + return this.isComplete() && this.isScaled() && 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() { @@ -83,11 +99,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; @@ -109,11 +121,44 @@ 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]; + 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 a.count - b.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(); @@ -136,6 +181,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(); 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 dd9a1b7a14c10de..54618dc8d5b4462 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 getStringFields() { + return await this._source.getStringFields(); + } + async getFields() { const sourceFields = await this._source.getFields(); return [...sourceFields, ...this._getJoinFields()];