[Maps] clean-up vector style defaults (#28998) (#29028)

* rename StaticDynamicStyleSelector to StaticDynamicStyleRow

* clean up color styling

* refactor size editing

* more clean up

* remove console statements

* ensure default styles exist when loading saved objects

* do not call callback when range is invalid because bad input in text field
This commit is contained in:
Nathan Reese 2019-01-18 20:08:01 -07:00 committed by GitHub
parent 32be17749e
commit 5f7a27165c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 536 additions and 452 deletions

View file

@ -0,0 +1,82 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment } from 'react';
import { EuiRange, EuiFormErrorText } from '@elastic/eui';
function isWithinRange(min, max, value) {
if (value >= min && value <= max) {
return true;
}
return false;
}
// TODO move to EUI
// Wrapper around EuiRange that ensures onChange callback is only called when value is number and within min/max
export class ValidatedRange extends React.Component {
state = {};
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.value !== prevState.prevValue) {
return {
value: nextProps.value,
prevValue: nextProps.value,
isValid: isWithinRange(nextProps.min, nextProps.max, nextProps.value),
};
}
return null;
}
_onRangeChange = (e) => {
const sanitizedValue = parseInt(e.target.value, 10);
let newValue = isNaN(sanitizedValue) ? '' : sanitizedValue;
// work around for https://github.com/elastic/eui/issues/1458
// TODO remove once above EUI issue is resolved
newValue = Number(newValue);
const isValid = isWithinRange(this.props.min, this.props.max, newValue)
? true
: false;
this.setState({
value: newValue,
isValid,
});
if (isValid) {
this.props.onChange(newValue);
}
};
render() {
let errorMessage;
if (!this.state.isValid) {
errorMessage = (
<EuiFormErrorText>
{`Must be between ${this.props.min} and ${this.props.max}`}
</EuiFormErrorText>
);
}
return (
<Fragment>
<EuiRange
min={this.props.min}
max={this.props.max}
value={this.state.value.toString()}
onChange={this._onRangeChange}
showInput
showRange
/>
{errorMessage}
</Fragment>
);
}
}

View file

@ -1,2 +1,2 @@
@import './components/static_dynamic_styling_option';
@import './components/static_dynamic_style_row';
@import './components/vector/color/static_color_selection';

View file

@ -1,129 +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 _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Fragment } from 'react';
import {
EuiComboBox,
EuiSpacer
} from '@elastic/eui';
import { ColorRampSelector } from './vector/color/color_ramp_selector';
import { SizeRangeSelector } from './vector/size/size_range_selector';
export const styleTypes = {
COLOR_RAMP: 'color_ramp',
SIZE_RANGE: 'size_range'
};
export function DynamicOrdinalStyleOption({ fields, selectedOptions, onChange, type }) {
const onFieldChange = (selectedFields) => {
const field = selectedFields.length > 0 ? selectedFields[0].value : null;
onChange({ field });
};
const groupFieldsByOrigin = () => {
const fieldsByOriginMap = new Map();
fields
.forEach(field => {
if (fieldsByOriginMap.has(field.origin)) {
const fieldsList = fieldsByOriginMap.get(field.origin);
fieldsList.push(field);
fieldsByOriginMap.set(field.origin, fieldsList);
} else {
fieldsByOriginMap.set(field.origin, [field]);
}
});
const optionGroups = [];
fieldsByOriginMap.forEach((fieldsList, fieldOrigin) => {
optionGroups.push({
label: fieldOrigin,
options: fieldsList
.map(field => {
return { value: field, label: field.label };
})
.sort((a, b) => {
return a.label.toLowerCase().localeCompare(b.label.toLowerCase());
})
});
});
optionGroups.sort((a, b) => {
return a.label.toLowerCase().localeCompare(b.label.toLowerCase());
});
return optionGroups;
};
const renderAdditionalOptions = () => {
if (!_.has(selectedOptions, 'field')) {
// TODO: Don't hide the component
return;
}
const onAdditionalOptionsChange = (additionalOptions) => {
onChange({ field: selectedOptions.field, ...additionalOptions });
};
switch (type) {
case styleTypes.COLOR_RAMP:
return (
<Fragment>
<ColorRampSelector
onChange={onAdditionalOptionsChange}
color={_.get(selectedOptions, 'color')}
/>
<EuiSpacer size="s" />
</Fragment>
);
case styleTypes.SIZE_RANGE:
return (
<SizeRangeSelector
onChange={onAdditionalOptionsChange}
minSize={_.get(selectedOptions, 'minSize')}
maxSize={_.get(selectedOptions, 'maxSize')}
/>
);
default:
throw new Error(`Unhandled type ${type}`);
}
};
return (
<Fragment>
{renderAdditionalOptions()}
<EuiComboBox
selectedOptions={
_.has(selectedOptions, 'field')
? [{ label: selectedOptions.field.label, value: selectedOptions.field }]
: []
}
options={groupFieldsByOrigin()}
onChange={onFieldChange}
singleSelection={{ asPlainText: true }}
isClearable={false}
fullWidth
placeholder="Select a field"
/>
</Fragment>
);
}
DynamicOrdinalStyleOption.propTypes = {
selectedOptions: PropTypes.object,
fields: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
type: PropTypes.oneOf(
Object.keys(styleTypes).map(styleType => {
return styleTypes[styleType];
})
).isRequired,
};

View file

@ -16,14 +16,12 @@ import {
EuiButtonToggle
} from '@elastic/eui';
export class StaticDynamicStyleSelector extends React.Component {
export class StaticDynamicStyleRow extends React.Component {
// Store previous options locally so when type is toggled,
// previous style options can be used.
prevOptions = {
// TODO: Move default to central location with other defaults
color: '#e6194b'
}
prevStaticStyleOptions = this.props.defaultStaticStyleOptions;
prevDynamicStyleOptions = this.props.defaultDynamicStyleOptions;
_canBeDynamic() {
return this.props.ordinalFields.length > 0;
@ -58,14 +56,17 @@ export class StaticDynamicStyleSelector extends React.Component {
_onTypeToggle = () => {
if (this._isDynamic()) {
// preserve current dynmaic style
this.prevDynamicStyleOptions = this._getStyleOptions();
// toggle to static style
this._onStaticStyleChange(this.prevOptions);
} else {
// toggle to dynamic style
this._onDynamicStyleChange(this.prevOptions);
this._onStaticStyleChange(this.prevStaticStyleOptions);
return;
}
this.prevOptions = this._getStyleOptions();
// preserve current static style
this.prevStaticStyleOptions = this._getStyleOptions();
// toggle to dynamic style
this._onDynamicStyleChange(this.prevDynamicStyleOptions);
}
_renderStyleSelector() {
@ -73,9 +74,9 @@ export class StaticDynamicStyleSelector extends React.Component {
const DynamicSelector = this.props.DynamicSelector;
return (
<DynamicSelector
fields={this.props.ordinalFields}
ordinalFields={this.props.ordinalFields}
onChange={this._onDynamicStyleChange}
selectedOptions={this._getStyleOptions()}
styleOptions={this._getStyleOptions()}
/>
);
}
@ -83,8 +84,8 @@ export class StaticDynamicStyleSelector extends React.Component {
const StaticSelector = this.props.StaticSelector;
return (
<StaticSelector
changeOptions={this._onStaticStyleChange}
selectedOptions={this._getStyleOptions()}
onChange={this._onStaticStyleChange}
styleOptions={this._getStyleOptions()}
/>
);
}

View file

@ -7,43 +7,33 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
EuiSuperSelect
} from '@elastic/eui';
import { EuiSuperSelect } from '@elastic/eui';
import { vislibColorMaps } from 'ui/vislib/components/color/colormaps';
import { ColorGradient } from '../../../../../icons/color_gradient';
const COLOR_GRADIENTS = Object.keys(vislibColorMaps).map(colorKey => ({
export const COLOR_GRADIENTS = Object.keys(vislibColorMaps).map(colorKey => ({
value: colorKey,
text: colorKey,
inputDisplay: <ColorGradient color={colorKey}/>
}));
const onColorRampChange = onChange =>
selectedColorRampString => {
export function ColorRampSelect({ color, onChange }) {
const onColorRampChange = (selectedColorRampString) => {
onChange({
color: selectedColorRampString
});
};
export function ColorRampSelector({ color, onChange }) {
if (color) {
return (
<EuiSuperSelect
options={COLOR_GRADIENTS}
onChange={onColorRampChange(onChange)}
valueOfSelected={color}
hasDividers={true}
/>
);
} else {
// Default to first color gradient
onColorRampChange(onChange)(COLOR_GRADIENTS[0].value);
return null;
}
return (
<EuiSuperSelect
options={COLOR_GRADIENTS}
onChange={onColorRampChange}
valueOfSelected={color}
hasDividers={true}
/>
);
}
ColorRampSelector.propTypes = {
color: PropTypes.string,
ColorRampSelect.propTypes = {
color: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
};

View file

@ -4,20 +4,43 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { DynamicOrdinalStyleOption, styleTypes } from '../../dynamic_ordinal_styling_option';
import _ from 'lodash';
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { FieldSelect, fieldShape } from '../field_select';
import { ColorRampSelect } from './color_ramp_select';
import { EuiSpacer } from '@elastic/eui';
export class DynamicColorSelection extends React.Component {
export function DynamicColorSelection({ ordinalFields, onChange, styleOptions }) {
const onFieldChange = ({ field }) => {
onChange({ ...styleOptions, field });
};
render() {
return (
<DynamicOrdinalStyleOption
fields={this.props.fields}
selectedOptions={this.props.selectedOptions}
type={styleTypes.COLOR_RAMP}
onChange={this.props.onChange}
const onColorChange = ({ color }) => {
onChange({ ...styleOptions, color });
};
return (
<Fragment>
<ColorRampSelect
onChange={onColorChange}
color={styleOptions.color}
/>
);
}
<EuiSpacer size="s" />
<FieldSelect
fields={ordinalFields}
selectedField={_.get(styleOptions, 'field')}
onChange={onFieldChange}
/>
</Fragment>
);
}
DynamicColorSelection.propTypes = {
ordinalFields: PropTypes.arrayOf(fieldShape).isRequired,
styleOptions: PropTypes.shape({
color: PropTypes.string.isRequired,
field: fieldShape,
}).isRequired,
onChange: PropTypes.func.isRequired
};

View file

@ -5,28 +5,31 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
import {
EuiColorPicker,
EuiFormControlLayout
} from '@elastic/eui';
export class StaticColorSelection extends React.Component {
render() {
export function StaticColorSelection({ onChange, styleOptions }) {
const onColorChange = color => {
onChange({ color });
};
const onOptionChange = color => {
this.props.changeOptions({
color
});
};
return (
<EuiFormControlLayout>
<EuiColorPicker
onChange={onOptionChange}
color={this.props.selectedOptions ? this.props.selectedOptions.color : null}
className="gisColorPicker euiFieldText"
/>
</EuiFormControlLayout>
);
}
return (
<EuiFormControlLayout>
<EuiColorPicker
onChange={onColorChange}
color={styleOptions.color}
className="gisColorPicker euiFieldText"
/>
</EuiFormControlLayout>
);
}
StaticColorSelection.propTypes = {
styleOptions: PropTypes.shape({
color: PropTypes.string.isRequired,
}).isRequired,
onChange: PropTypes.func.isRequired
};

View file

@ -6,13 +6,13 @@
import React from 'react';
import { StaticDynamicStyleSelector } from '../../static_dynamic_styling_option';
import { DynamicColorSelection } from './dynamic_color_selection';
import { StaticColorSelection } from './static_color_selection';
import { StaticDynamicStyleRow } from '../../static_dynamic_style_row';
import { DynamicColorSelection } from './dynamic_color_selection';
import { StaticColorSelection } from './static_color_selection';
export function VectorStyleColorEditor(props) {
return (
<StaticDynamicStyleSelector
<StaticDynamicStyleRow
ordinalFields={props.ordinalFields}
property={props.styleProperty}
name={props.stylePropertyName}
@ -20,6 +20,8 @@ export function VectorStyleColorEditor(props) {
handlePropertyChange={props.handlePropertyChange}
DynamicSelector={DynamicColorSelection}
StaticSelector={StaticColorSelection}
defaultDynamicStyleOptions={props.defaultDynamicStyleOptions}
defaultStaticStyleOptions={props.defaultStaticStyleOptions}
/>
);
}

View file

@ -0,0 +1,81 @@
/*
* 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 PropTypes from 'prop-types';
import React from 'react';
import { EuiComboBox } from '@elastic/eui';
export function FieldSelect({ fields, selectedField, onChange }) {
const onFieldChange = (selectedFields) => {
onChange({
field: selectedFields.length > 0 ? selectedFields[0].value : null
});
};
const groupFieldsByOrigin = () => {
const fieldsByOriginMap = new Map();
fields
.forEach(field => {
if (fieldsByOriginMap.has(field.origin)) {
const fieldsList = fieldsByOriginMap.get(field.origin);
fieldsList.push(field);
fieldsByOriginMap.set(field.origin, fieldsList);
} else {
fieldsByOriginMap.set(field.origin, [field]);
}
});
const optionGroups = [];
fieldsByOriginMap.forEach((fieldsList, fieldOrigin) => {
optionGroups.push({
label: fieldOrigin,
options: fieldsList
.map(field => {
return { value: field, label: field.label };
})
.sort((a, b) => {
return a.label.toLowerCase().localeCompare(b.label.toLowerCase());
})
});
});
optionGroups.sort((a, b) => {
return a.label.toLowerCase().localeCompare(b.label.toLowerCase());
});
return optionGroups;
};
const selectedOptions = selectedField
? [{ label: selectedField.label, value: selectedField }]
: [];
return (
<EuiComboBox
selectedOptions={selectedOptions}
options={groupFieldsByOrigin()}
onChange={onFieldChange}
singleSelection={{ asPlainText: true }}
isClearable={false}
fullWidth
placeholder="Select a field"
/>
);
}
export const fieldShape = PropTypes.shape({
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
origin: PropTypes.oneOf(['join', 'source']).isRequired
});
FieldSelect.propTypes = {
selectedField: fieldShape,
fields: PropTypes.arrayOf(fieldShape).isRequired,
onChange: PropTypes.func.isRequired,
};

View file

@ -4,21 +4,45 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import _ from 'lodash';
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { FieldSelect, fieldShape } from '../field_select';
import { SizeRangeSelector } from './size_range_selector';
import { EuiSpacer } from '@elastic/eui';
import { DynamicOrdinalStyleOption, styleTypes } from '../../dynamic_ordinal_styling_option';
export function DynamicSizeSelection({ ordinalFields, styleOptions, onChange }) {
const onFieldChange = ({ field }) => {
onChange({ ...styleOptions, field });
};
const onSizeRangeChange = ({ minSize, maxSize }) => {
onChange({ ...styleOptions, minSize, maxSize });
};
export class DynamicSizeSelection extends React.Component {
render() {
return (
<DynamicOrdinalStyleOption
fields={this.props.fields}
selectedOptions={this.props.selectedOptions}
type={styleTypes.SIZE_RANGE}
onChange={this.props.onChange}
return (
<Fragment>
<SizeRangeSelector
onChange={onSizeRangeChange}
minSize={styleOptions.minSize}
maxSize={styleOptions.maxSize}
/>
);
}
<EuiSpacer size="s" />
<FieldSelect
fields={ordinalFields}
selectedField={_.get(styleOptions, 'field')}
onChange={onFieldChange}
/>
</Fragment>
);
}
DynamicSizeSelection.propTypes = {
ordinalFields: PropTypes.arrayOf(fieldShape).isRequired,
styleOptions: PropTypes.shape({
minSize: PropTypes.number.isRequired,
maxSize: PropTypes.number.isRequired,
field: fieldShape,
}).isRequired,
onChange: PropTypes.func.isRequired
};

View file

@ -10,93 +10,61 @@ import {
EuiFormRow,
EuiFlexGroup,
EuiFlexItem,
EuiRange
} from '@elastic/eui';
import { ValidatedRange } from '../../../../../components/validated_range';
import { DEFAULT_MIN_SIZE, DEFAULT_MAX_SIZE } from '../../../vector_style_defaults';
const DEFAULT_MIN_SIZE = 1;
const DEFAULT_MAX_SIZE = 64;
export function SizeRangeSelector({ minSize, maxSize, onChange }) {
export class SizeRangeSelector extends React.Component {
_onSizeChange = (min, max) => {
this.props.onChange({
const onSizeChange = (min, max) => {
onChange({
minSize: min,
maxSize: max
});
};
_areSizesValid() {
return typeof this.props.minSize === 'number' && typeof this.props.maxSize === 'number';
}
const onMinSizeChange = (updatedMinSize) => {
onSizeChange(updatedMinSize, updatedMinSize > maxSize ? updatedMinSize : maxSize);
};
componentDidMount() {
if (!this._areSizesValid()) {
this._onSizeChange(DEFAULT_MIN_SIZE, DEFAULT_MAX_SIZE);
}
}
const onMaxSizeChange = (updatedMaxSize) => {
onSizeChange(updatedMaxSize < minSize ? updatedMaxSize : minSize, updatedMaxSize);
};
componentDidUpdate() {
if (!this._areSizesValid()) {
this._onSizeChange(DEFAULT_MIN_SIZE, DEFAULT_MAX_SIZE);
}
}
render() {
if (!this._areSizesValid()) {
return null;
}
const onMinSizeChange = (e) => {
const updatedMinSize = parseInt(e.target.value, 10);
this._onSizeChange(updatedMinSize, updatedMinSize > this.props.maxSize ? updatedMinSize : this.props.maxSize);
};
const onMaxSizeChange = (e) => {
const updatedMaxSize = parseInt(e.target.value, 10);
this._onSizeChange(updatedMaxSize < this.props.minSize ? updatedMaxSize : this.props.minSize, updatedMaxSize);
};
return (
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow
label="Min size"
compressed
>
<EuiRange
min={DEFAULT_MIN_SIZE}
max={DEFAULT_MAX_SIZE}
value={this.props.minSize.toString()}
onChange={onMinSizeChange}
showInput
showRange
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
label="Max size"
compressed
>
<EuiRange
min={DEFAULT_MIN_SIZE}
max={DEFAULT_MAX_SIZE}
value={this.props.maxSize.toString()}
onChange={onMaxSizeChange}
showInput
showRange
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
);
}
return (
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow
label="Min size"
compressed
>
<ValidatedRange
min={DEFAULT_MIN_SIZE}
max={DEFAULT_MAX_SIZE}
value={minSize}
onChange={onMinSizeChange}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
label="Max size"
compressed
>
<ValidatedRange
min={DEFAULT_MIN_SIZE}
max={DEFAULT_MAX_SIZE}
value={maxSize}
onChange={onMaxSizeChange}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
);
}
SizeRangeSelector.propTypes = {
minSize: PropTypes.number,
maxSize: PropTypes.number,
minSize: PropTypes.number.isRequired,
maxSize: PropTypes.number.isRequired,
onChange: PropTypes.func.isRequired,
};

View file

@ -5,38 +5,28 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
import { ValidatedRange } from '../../../../../components/validated_range';
import {
EuiRange
} from '@elastic/eui';
export function StaticSizeSelection({ onChange, styleOptions }) {
export class StaticSizeSelection extends React.Component {
const onSizeChange = (size) => {
onChange({ size });
};
constructor() {
super();
this.state = {};
}
render() {
const onChange = (event) => {
const size = parseInt(event.target.value, 10);
this.props.changeOptions({
size: size
});
};
const selectedValue = (
this.props.selectedOptions && typeof this.props.selectedOptions.size === 'number'
) ? this.props.selectedOptions.size : 0;
return (
<EuiRange
min={0}
max={100}
value={selectedValue.toString()}
onChange={onChange}
showInput
/>
);
}
return (
<ValidatedRange
min={0}
max={100}
value={styleOptions.size}
onChange={onSizeChange}
/>
);
}
StaticSizeSelection.propTypes = {
styleOptions: PropTypes.shape({
size: PropTypes.number.isRequired,
}).isRequired,
onChange: PropTypes.func.isRequired
};

View file

@ -6,16 +6,13 @@
import React from 'react';
import {
} from '@elastic/eui';
import { StaticDynamicStyleSelector } from '../../static_dynamic_styling_option';
import { StaticDynamicStyleRow } from '../../static_dynamic_style_row';
import { DynamicSizeSelection } from './dynamic_size_selection';
import { StaticSizeSelection } from './static_size_selection';
export function VectorStyleSizeEditor(props) {
return (
<StaticDynamicStyleSelector
<StaticDynamicStyleRow
ordinalFields={props.ordinalFields}
property={props.styleProperty}
name={props.stylePropertyName}
@ -23,6 +20,8 @@ export function VectorStyleSizeEditor(props) {
handlePropertyChange={props.handlePropertyChange}
DynamicSelector={DynamicSizeSelection}
StaticSelector={StaticSizeSelection}
defaultDynamicStyleOptions={props.defaultDynamicStyleOptions}
defaultStaticStyleOptions={props.defaultStaticStyleOptions}
/>
);
}

View file

@ -9,12 +9,19 @@ import React, { Component, Fragment } from 'react';
import { VectorStyleColorEditor } from './color/vector_style_color_editor';
import { VectorStyleSizeEditor } from './size/vector_style_size_editor';
import { getDefaultDynamicProperties, getDefaultStaticProperties } from '../../vector_style_defaults';
import { EuiSpacer } from '@elastic/eui';
export class VectorStyleEditor extends Component {
state = {
ordinalFields: []
constructor(props) {
super(props);
this.state = {
ordinalFields: [],
defaultDynamicProperties: getDefaultDynamicProperties(),
defaultStaticProperties: getDefaultStaticProperties()
};
}
componentWillUnmount() {
@ -50,6 +57,8 @@ export class VectorStyleEditor extends Component {
handlePropertyChange={this.props.handlePropertyChange}
styleDescriptor={this.props.styleProperties.fillColor}
ordinalFields={this.state.ordinalFields}
defaultStaticStyleOptions={this.state.defaultStaticProperties.fillColor.options}
defaultDynamicStyleOptions={this.state.defaultDynamicProperties.fillColor.options}
/>
<EuiSpacer size="m" />
@ -60,6 +69,8 @@ export class VectorStyleEditor extends Component {
handlePropertyChange={this.props.handlePropertyChange}
styleDescriptor={this.props.styleProperties.lineColor}
ordinalFields={this.state.ordinalFields}
defaultStaticStyleOptions={this.state.defaultStaticProperties.lineColor.options}
defaultDynamicStyleOptions={this.state.defaultDynamicProperties.lineColor.options}
/>
<EuiSpacer size="m" />
@ -70,6 +81,8 @@ export class VectorStyleEditor extends Component {
handlePropertyChange={this.props.handlePropertyChange}
styleDescriptor={this.props.styleProperties.lineWidth}
ordinalFields={this.state.ordinalFields}
defaultStaticStyleOptions={this.state.defaultStaticProperties.lineWidth.options}
defaultDynamicStyleOptions={this.state.defaultDynamicProperties.lineWidth.options}
/>
<EuiSpacer size="m" />
@ -80,6 +93,8 @@ export class VectorStyleEditor extends Component {
handlePropertyChange={this.props.handlePropertyChange}
styleDescriptor={this.props.styleProperties.iconSize}
ordinalFields={this.state.ordinalFields}
defaultStaticStyleOptions={this.state.defaultStaticProperties.iconSize.options}
defaultDynamicStyleOptions={this.state.defaultDynamicProperties.iconSize.options}
/>
</Fragment>

View file

@ -11,6 +11,7 @@ import { FillableCircle, FillableVector } from '../../icons/additional_layer_ico
import { ColorGradient } from '../../icons/color_gradient';
import { getHexColorRangeStrings } from '../../utils/color_utils';
import { VectorStyleEditor } from './components/vector/vector_style_editor';
import { DEFAULT_ALPHA_VALUE, getDefaultStaticProperties } from './vector_style_defaults';
export class VectorStyle {
@ -22,7 +23,7 @@ export class VectorStyle {
}
constructor(descriptor) {
this._descriptor = descriptor;
this._descriptor = VectorStyle.createDescriptor(descriptor.properties);
}
static canEdit(styleInstance) {
@ -30,12 +31,17 @@ export class VectorStyle {
}
static createDescriptor(properties) {
const defaultStyleProperties = getDefaultStaticProperties();
return {
type: VectorStyle.type,
properties: properties
properties: { ...defaultStyleProperties, ...properties }
};
}
static createDefaultStyleProperties(mapColors) {
return getDefaultStaticProperties(mapColors);
}
static getDisplayName() {
return 'Vector style';
}
@ -81,15 +87,6 @@ export class VectorStyle {
return this._descriptor.properties || {};
}
getHexColor(colorProperty) {
if (!this._descriptor.properties[colorProperty] || !this._descriptor.properties[colorProperty].options) {
return null;
}
return this._descriptor.properties[colorProperty].options.color;
}
_isPropertyDynamic(property) {
if (!this._descriptor.properties[property]) {
return false;
@ -218,90 +215,78 @@ export class VectorStyle {
return updateStatuses.some(r => r === true);
}
_getMBDataDrivenColor(property) {
if (!this._descriptor.properties[property] || !this._descriptor.properties[property].options) {
return null;
}
const { field, color } = this._descriptor.properties[property].options;
if (field && color) {
const colorRange = getHexColorRangeStrings(color, 8)
.reduce((accu, curColor, idx, srcArr) => {
accu = [ ...accu, idx / srcArr.length, curColor ];
return accu;
}, []);
const originalFieldName = this._descriptor.properties[property].options.field.name;
const targetName = VectorStyle.getComputedFieldName(originalFieldName);
return [
'interpolate',
['linear'],
['coalesce', ['get', targetName], -1],
-1, 'rgba(0,0,0,0)',
...colorRange
];
} else {
return null;
}
_getMBDataDrivenColor({ fieldName, color }) {
const colorRange = getHexColorRangeStrings(color, 8)
.reduce((accu, curColor, idx, srcArr) => {
accu = [ ...accu, idx / srcArr.length, curColor ];
return accu;
}, []);
const targetName = VectorStyle.getComputedFieldName(fieldName);
return [
'interpolate',
['linear'],
['coalesce', ['get', targetName], -1],
-1, 'rgba(0,0,0,0)',
...colorRange
];
}
_getMbDataDrivenSize(property) {
if (!this._descriptor.properties[property] || !this._descriptor.properties[property].options) {
return null;
}
const { minSize, maxSize } = this._descriptor.properties[property].options;
if (typeof minSize === 'number' && typeof maxSize === 'number') {
const originalFieldName = this._descriptor.properties[property].options.field.name;
const targetName = VectorStyle.getComputedFieldName(originalFieldName);
return ['interpolate',
['linear'],
['get', targetName],
0, minSize,
1, maxSize
];
} else {
return null;
}
_getMbDataDrivenSize({ fieldName, minSize, maxSize }) {
const targetName = VectorStyle.getComputedFieldName(fieldName);
return ['interpolate',
['linear'],
['get', targetName],
0, minSize,
1, maxSize
];
}
_getMBColor(property) {
let color;
const hasFields = _.get(this._descriptor.properties[property].options, 'field', false)
&& _.get(this._descriptor.properties[property].options, 'color', false);
const isStatic = this._descriptor.properties[property].type === VectorStyle.STYLE_TYPE.STATIC;
if (isStatic || !hasFields) {
color = this.getHexColor(property);
} else {
color = this._getMBDataDrivenColor(property);
_getMBColor(styleDescriptor) {
const isStatic = styleDescriptor.type === VectorStyle.STYLE_TYPE.STATIC;
if (isStatic) {
return _.get(styleDescriptor, 'options.color', null);
}
return color;
const isDynamicConfigComplete = _.has(styleDescriptor, 'options.field')
&& _.has(styleDescriptor, 'options.color');
if (isDynamicConfigComplete) {
return this._getMBDataDrivenColor({
fieldName: styleDescriptor.options.field.name,
color: styleDescriptor.options.color,
});
}
return null;
}
_getMBOpacity() {
const DEFAULT_OPACITY = 1;
const opacity = typeof this._descriptor.properties.alphaValue === 'number' ? this._descriptor.properties.alphaValue : DEFAULT_OPACITY;
return opacity;
return _.get(this._descriptor.properties, 'alphaValue', DEFAULT_ALPHA_VALUE);
}
_getMbSize(property) {
if (this._descriptor.properties[property].type === VectorStyle.STYLE_TYPE.STATIC) {
return this._descriptor.properties[property].options.size;
} else {
return this._getMbDataDrivenSize(property);
_getMbSize(styleDescriptor) {
if (styleDescriptor.type === VectorStyle.STYLE_TYPE.STATIC) {
return styleDescriptor.options.size;
}
const isDynamicConfigComplete = _.has(styleDescriptor, 'options.field')
&& _.has(styleDescriptor, 'options.minSize')
&& _.has(styleDescriptor, 'options.maxSize');
if (isDynamicConfigComplete) {
return this._getMbDataDrivenSize({
fieldName: styleDescriptor.options.field.name,
minSize: styleDescriptor.options.minSize,
maxSize: styleDescriptor.options.maxSize,
});
}
return null;
}
setMBPaintProperties(mbMap, sourceId, fillLayerId, lineLayerId) {
const opacity = this._getMBOpacity();
if (this._descriptor.properties.fillColor) {
const color = this._getMBColor('fillColor');
const color = this._getMBColor(this._descriptor.properties.fillColor);
mbMap.setPaintProperty(fillLayerId, 'fill-color', color);
mbMap.setPaintProperty(fillLayerId, 'fill-opacity', opacity);
} else {
@ -310,7 +295,7 @@ export class VectorStyle {
}
if (this._descriptor.properties.lineColor) {
const color = this._getMBColor('lineColor');
const color = this._getMBColor(this._descriptor.properties.lineColor);
mbMap.setPaintProperty(lineLayerId, 'line-color', color);
mbMap.setPaintProperty(lineLayerId, 'line-opacity', opacity);
@ -319,8 +304,8 @@ export class VectorStyle {
mbMap.setPaintProperty(lineLayerId, 'line-opacity', 0);
}
if (this._descriptor.properties.lineWidth && this._descriptor.properties.lineWidth.options) {
const lineWidth = this._getMbSize('lineWidth');
if (this._descriptor.properties.lineWidth) {
const lineWidth = this._getMbSize(this._descriptor.properties.lineWidth);
mbMap.setPaintProperty(lineLayerId, 'line-width', lineWidth);
} else {
mbMap.setPaintProperty(lineLayerId, 'line-width', 0);
@ -330,7 +315,7 @@ export class VectorStyle {
setMBPaintPropertiesForPoints(mbMap, sourceId, pointLayerId) {
const opacity = this._getMBOpacity();
if (this._descriptor.properties.fillColor) {
const color = this._getMBColor('fillColor');
const color = this._getMBColor(this._descriptor.properties.fillColor);
mbMap.setPaintProperty(pointLayerId, 'circle-color', color);
mbMap.setPaintProperty(pointLayerId, 'circle-opacity', opacity);
} else {
@ -338,7 +323,7 @@ export class VectorStyle {
mbMap.setPaintProperty(pointLayerId, 'circle-opacity', 0);
}
if (this._descriptor.properties.lineColor) {
const color = this._getMBColor('lineColor');
const color = this._getMBColor(this._descriptor.properties.lineColor);
mbMap.setPaintProperty(pointLayerId, 'circle-stroke-color', color);
mbMap.setPaintProperty(pointLayerId, 'circle-stroke-opacity', opacity);
@ -346,14 +331,14 @@ export class VectorStyle {
mbMap.setPaintProperty(pointLayerId, 'circle-stroke-color', null);
mbMap.setPaintProperty(pointLayerId, 'circle-stroke-opacity', 0);
}
if (this._descriptor.properties.lineWidth && this._descriptor.properties.lineWidth.options) {
const lineWidth = this._getMbSize('lineWidth');
if (this._descriptor.properties.lineWidth) {
const lineWidth = this._getMbSize(this._descriptor.properties.lineWidth);
mbMap.setPaintProperty(pointLayerId, 'circle-stroke-width', lineWidth);
} else {
mbMap.setPaintProperty(pointLayerId, 'circle-stroke-width', 0);
}
if (this._descriptor.properties.iconSize && this._descriptor.properties.iconSize.options) {
const iconSize = this._getMbSize('iconSize');
if (this._descriptor.properties.iconSize) {
const iconSize = this._getMbSize(this._descriptor.properties.iconSize);
mbMap.setPaintProperty(pointLayerId, 'circle-radius', iconSize);
} else {
mbMap.setPaintProperty(pointLayerId, 'circle-radius', 0);

View file

@ -0,0 +1,80 @@
/*
* 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 { VectorStyle } from './vector_style';
import { COLOR_GRADIENTS } from './components/vector/color/color_ramp_select';
const DEFAULT_COLORS = ['#e6194b', '#3cb44b', '#ffe119', '#f58231', '#911eb4'];
export const DEFAULT_ALPHA_VALUE = 1;
export const DEFAULT_MIN_SIZE = 1;
export const DEFAULT_MAX_SIZE = 64;
export function getDefaultStaticProperties(mapColors = []) {
// Colors must be state-aware to reduce unnecessary incrementation
const lastColor = mapColors.pop();
const nextColorIndex = (DEFAULT_COLORS.indexOf(lastColor) + 1) % (DEFAULT_COLORS.length - 1);
const nextColor = DEFAULT_COLORS[nextColorIndex];
return {
fillColor: {
type: VectorStyle.STYLE_TYPE.STATIC,
options: {
color: nextColor,
}
},
lineColor: {
type: VectorStyle.STYLE_TYPE.STATIC,
options: {
color: '#FFFFFF'
}
},
lineWidth: {
type: VectorStyle.STYLE_TYPE.STATIC,
options: {
size: 1
}
},
iconSize: {
type: VectorStyle.STYLE_TYPE.STATIC,
options: {
size: 10
}
},
alphaValue: DEFAULT_ALPHA_VALUE
};
}
export function getDefaultDynamicProperties() {
return {
fillColor: {
type: VectorStyle.STYLE_TYPE.DYNAMIC,
options: {
color: COLOR_GRADIENTS[0].value,
}
},
lineColor: {
type: VectorStyle.STYLE_TYPE.DYNAMIC,
options: {
color: COLOR_GRADIENTS[0].value,
}
},
lineWidth: {
type: VectorStyle.STYLE_TYPE.DYNAMIC,
options: {
minSize: DEFAULT_MIN_SIZE,
maxSize: 64
}
},
iconSize: {
type: VectorStyle.STYLE_TYPE.DYNAMIC,
options: {
minSize: DEFAULT_MIN_SIZE,
maxSize: DEFAULT_MAX_SIZE
}
},
alphaValue: DEFAULT_ALPHA_VALUE
};
}

View file

@ -17,8 +17,6 @@ import { store } from '../../store/store';
import { getMapColors } from '../../selectors/map_selectors';
import _ from 'lodash';
const DEFAULT_COLORS = ['#e6194b', '#3cb44b', '#ffe119', '#f58231', '#911eb4'];
const EMPTY_FEATURE_COLLECTION = {
type: 'FeatureCollection',
features: []
@ -37,44 +35,16 @@ export class VectorLayer extends ALayer {
static tooltipContainer = document.createElement('div');
static createDescriptor(options) {
// Colors must be state-aware to reduce unnecessary incrementation
const DEFAULT_ALPHA_VALUE = 1;
const mapColors = getMapColors(store.getState());
const lastColor = mapColors.pop();
const nextColor = DEFAULT_COLORS[
(DEFAULT_COLORS.indexOf(lastColor) + 1) % (DEFAULT_COLORS.length - 1)
];
const layerDescriptor = super.createDescriptor(options);
layerDescriptor.type = VectorLayer.type;
if (!options.style) {
layerDescriptor.style = VectorStyle.createDescriptor({
fillColor: {
type: VectorStyle.STYLE_TYPE.STATIC,
options: {
color: nextColor,
}
},
lineColor: {
type: VectorStyle.STYLE_TYPE.STATIC,
options: {
color: '#FFFFFF'
}
},
lineWidth: {
type: VectorStyle.STYLE_TYPE.STATIC,
options: {
size: 1
}
},
iconSize: {
type: VectorStyle.STYLE_TYPE.STATIC,
options: {
size: 10
}
},
alphaValue: DEFAULT_ALPHA_VALUE
});
// TODO pass store in as argument. Accessing store this way is unsafe
const mapColors = getMapColors(store.getState());
const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors);
layerDescriptor.style = VectorStyle.createDescriptor(styleProperties);
}
return layerDescriptor;
}