[Maps] only show styles that apply to layer feature types in legend (#52335)

* [Maps] only show styles that apply to layer feature types in legend

* update hasLegendDetails check to include style property filters

* clean up
This commit is contained in:
Nathan Reese 2019-12-09 13:26:50 -07:00 committed by GitHub
parent 942f5420ed
commit cb60a77bb9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 196 additions and 126 deletions

View file

@ -21,12 +21,14 @@ export class TOCEntry extends React.Component {
state = {
displayName: null,
hasLegendDetails: false,
shouldShowModal: false
};
componentDidMount() {
this._isMounted = true;
this._updateDisplayName();
this._loadHasLegendDetails();
}
componentWillUnmount() {
@ -35,6 +37,7 @@ export class TOCEntry extends React.Component {
componentDidUpdate() {
this._updateDisplayName();
this._loadHasLegendDetails();
}
_toggleLayerDetailsVisibility = () => {
@ -45,6 +48,13 @@ export class TOCEntry extends React.Component {
}
}
async _loadHasLegendDetails() {
const hasLegendDetails = await this.props.layer.hasLegendDetails();
if (this._isMounted && hasLegendDetails !== this.state.hasLegendDetails) {
this.setState({ hasLegendDetails });
}
}
async _updateDisplayName() {
const label = await this.props.layer.getDisplayName();
if (this._isMounted) {
@ -143,7 +153,7 @@ export class TOCEntry extends React.Component {
}
_renderDetailsToggle() {
if (!this.props.layer.hasLegendDetails()) {
if (!this.state.hasLegendDetails) {
return null;
}
@ -223,7 +233,7 @@ export class TOCEntry extends React.Component {
}
_renderLegendDetails = () => {
if (!this.props.isLegendDetailsOpen || !this.props.layer.hasLegendDetails()) {
if (!this.props.isLegendDetailsOpen || !this.state.hasLegendDetails) {
return null;
}

View file

@ -13,6 +13,7 @@ const LAYER_ID = '1';
const mockLayer = {
getId: () => { return LAYER_ID; },
hasLegendDetails: async () => { return true; },
renderLegendDetails: () => { return (<div>TOC details mock</div>); },
getDisplayName: () => { return 'layer 1'; },
isVisible: () => { return true; },

View file

@ -98,7 +98,7 @@ export class HeatmapLayer extends VectorLayer {
return 'heatmap';
}
hasLegendDetails() {
async hasLegendDetails() {
return true;
}

View file

@ -186,7 +186,7 @@ export class AbstractLayer {
};
}
hasLegendDetails() {
async hasLegendDetails() {
return false;
}

View file

@ -16,77 +16,16 @@ const EMPTY_VALUE = '';
export class StylePropertyLegendRow extends Component {
state = {
label: '',
hasLoadedFieldFormatter: false,
}
componentDidMount() {
this._isMounted = true;
this._prevLabel = undefined;
this._fieldValueFormatter = undefined;
this._loadLabel();
this._loadFieldFormatter();
}
componentDidUpdate() {
// label could change so it needs to be loaded on update
this._loadLabel();
}
componentWillUnmount() {
this._isMounted = false;
}
async _loadFieldFormatter() {
if (this.props.style.isDynamic() && this.props.style.isComplete() && this.props.style.getField().getSource()) {
const field = this.props.style.getField();
const source = field.getSource();
this._fieldValueFormatter = await source.getFieldFormatter(field.getName());
} else {
this._fieldValueFormatter = null;
}
if (this._isMounted) {
this.setState({ hasLoadedFieldFormatter: true });
}
}
_loadLabel = async () => {
if (this._excludeFromHeader()) {
return;
}
// have to load label and then check for changes since field name stays constant while label may change
const label = await this.props.style.getField().getLabel();
if (this._prevLabel === label) {
return;
}
this._prevLabel = label;
if (this._isMounted) {
this.setState({ label });
}
}
_excludeFromHeader() {
return !this.props.style.isDynamic() || !this.props.style.isComplete() || !this.props.style.getField().getName();
}
_formatValue = value => {
if (!this.state.hasLoadedFieldFormatter || !this._fieldValueFormatter || value === EMPTY_VALUE) {
if (!this.props.fieldFormatter || value === EMPTY_VALUE) {
return value;
}
return this._fieldValueFormatter(value);
return this.props.fieldFormatter(value);
}
render() {
const { range, style } = this.props;
if (this._excludeFromHeader()) {
return null;
}
const header = style.renderHeader();
const min = this._formatValue(_.get(range, 'min', EMPTY_VALUE));
const minLabel = this.props.style.isFieldMetaEnabled() && range && range.isMinOutsideStdRange ? `< ${min}` : min;
@ -96,17 +35,19 @@ export class StylePropertyLegendRow extends Component {
return (
<StyleLegendRow
header={header}
header={style.renderHeader()}
minLabel={minLabel}
maxLabel={maxLabel}
propertyLabel={getVectorStyleLabel(style.getStyleName())}
fieldLabel={this.state.label}
fieldLabel={this.props.label}
/>
);
}
}
StylePropertyLegendRow.propTypes = {
label: PropTypes.string,
fieldFormatter: PropTypes.func,
range: rangeShape,
style: PropTypes.object
};

View file

@ -4,29 +4,59 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import _ from 'lodash';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { rangeShape } from '../style_option_shapes';
import { StylePropertyLegendRow } from './style_property_legend_row';
export function VectorStyleLegend({ styleProperties }) {
return styleProperties.map(styleProperty => {
return (
<StylePropertyLegendRow
style={styleProperty.style}
key={styleProperty.style.getStyleName()}
range={styleProperty.range}
/>
);
});
export class VectorStyleLegend extends Component {
state = {
rows: [],
}
componentDidMount() {
this._isMounted = true;
this._prevRowDescriptors = undefined;
this._loadRows();
}
componentDidUpdate() {
this._loadRows();
}
componentWillUnmount() {
this._isMounted = false;
}
_loadRows = _.debounce(async () => {
const rows = await this.props.loadRows();
const rowDescriptors = rows.map(row => {
return {
label: row.label,
range: row.range,
styleOptions: row.style.getOptions(),
};
});
if (this._isMounted && !_.isEqual(rowDescriptors, this._prevRowDescriptors)) {
this._prevRowDescriptors = rowDescriptors;
this.setState({ rows });
}
}, 100);
render() {
return this.state.rows.map(rowProps => {
return (
<StylePropertyLegendRow
key={rowProps.style.getStyleName()}
{...rowProps}
/>
);
});
}
}
const stylePropertyShape = PropTypes.shape({
range: rangeShape,
style: PropTypes.object
});
VectorStyleLegend.propTypes = {
styleProperties: PropTypes.arrayOf(stylePropertyShape).isRequired
loadRows: PropTypes.func.isRequired,
};

View file

@ -12,6 +12,24 @@ export function getComputedFieldNamePrefix(fieldName) {
return `__kbn__dynamic__${fieldName}`;
}
export function isOnlySingleFeatureType(featureType, supportedFeatures, hasFeatureType) {
if (supportedFeatures.length === 1) {
return supportedFeatures[0] === featureType;
}
if (!hasFeatureType) {
return false;
}
const featureTypes = Object.keys(hasFeatureType);
return featureTypes.reduce((isOnlyTargetFeatureType, featureTypeKey) => {
const hasFeature = hasFeatureType[featureTypeKey];
return featureTypeKey === featureType
? isOnlyTargetFeatureType && hasFeature
: isOnlyTargetFeatureType && !hasFeature;
}, true);
}
export function scaleValue(value, range) {
if (isNaN(value) || !range) {
return -1; //Nothing to scale, put outside scaled range

View file

@ -4,7 +4,57 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { scaleValue } from './style_util';
import { isOnlySingleFeatureType, scaleValue } from './style_util';
import { VECTOR_SHAPE_TYPES } from '../../sources/vector_feature_types';
describe('isOnlySingleFeatureType', () => {
describe('source supports single feature type', () => {
const supportedFeatures = [VECTOR_SHAPE_TYPES.POINT];
test('Is only single feature type when only supported feature type is target feature type', () => {
expect(isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.POINT, supportedFeatures)).toBe(true);
});
test('Is not single feature type when only supported feature type is not target feature type', () => {
expect(isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.LINE, supportedFeatures)).toBe(false);
});
});
describe('source supports multiple feature types', () => {
const supportedFeatures = [
VECTOR_SHAPE_TYPES.POINT,
VECTOR_SHAPE_TYPES.LINE,
VECTOR_SHAPE_TYPES.POLYGON
];
test('Is only single feature type when data only has target feature type', () => {
const hasFeatureType = {
[VECTOR_SHAPE_TYPES.POINT]: true,
[VECTOR_SHAPE_TYPES.LINE]: false,
[VECTOR_SHAPE_TYPES.POLYGON]: false,
};
expect(isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.POINT, supportedFeatures, hasFeatureType)).toBe(true);
});
test('Is not single feature type when data has multiple feature types', () => {
const hasFeatureType = {
[VECTOR_SHAPE_TYPES.POINT]: true,
[VECTOR_SHAPE_TYPES.LINE]: true,
[VECTOR_SHAPE_TYPES.POLYGON]: true,
};
expect(isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.LINE, supportedFeatures, hasFeatureType)).toBe(false);
});
test('Is not single feature type when data does not have target feature types', () => {
const hasFeatureType = {
[VECTOR_SHAPE_TYPES.POINT]: false,
[VECTOR_SHAPE_TYPES.LINE]: true,
[VECTOR_SHAPE_TYPES.POLYGON]: false,
};
expect(isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.POINT, supportedFeatures, hasFeatureType)).toBe(false);
});
});
});
describe('scaleValue', () => {
test('Should scale value between 0 and 1', () => {

View file

@ -7,7 +7,7 @@
import _ from 'lodash';
import React from 'react';
import { VectorStyleEditor } from './components/vector_style_editor';
import { getDefaultProperties, VECTOR_STYLES } from './vector_style_defaults';
import { getDefaultProperties, LINE_STYLES, POLYGON_STYLES, VECTOR_STYLES } from './vector_style_defaults';
import { AbstractStyle } from '../abstract_style';
import {
GEO_JSON_TYPE,
@ -21,7 +21,7 @@ import { VectorStyleLegend } from './components/legend/vector_style_legend';
import { VECTOR_SHAPE_TYPES } from '../../sources/vector_feature_types';
import { SYMBOLIZE_AS_CIRCLE, SYMBOLIZE_AS_ICON } from './vector_constants';
import { getMakiSymbolAnchor } from './symbol_utils';
import { getComputedFieldName, scaleValue } from './style_util';
import { getComputedFieldName, isOnlySingleFeatureType, scaleValue } from './style_util';
import { StaticStyleProperty } from './properties/static_style_property';
import { DynamicStyleProperty } from './properties/dynamic_style_property';
import { DynamicSizeProperty } from './properties/dynamic_size_property';
@ -271,32 +271,23 @@ export class VectorStyle extends AbstractStyle {
return styleProperties.filter(styleProperty => (styleProperty.isDynamic() && styleProperty.isComplete()));
}
_checkIfOnlyFeatureType = async (featureType) => {
const supportedFeatures = await this._source.getSupportedShapeTypes();
if (supportedFeatures.length === 1) {
return supportedFeatures[0] === featureType;
}
if (!this._descriptor.__styleMeta || !this._descriptor.__styleMeta.hasFeatureType) {
return false;
}
const featureTypes = Object.keys(this._descriptor.__styleMeta.hasFeatureType);
return featureTypes.reduce((isOnlySingleFeatureType, featureTypeKey) => {
const hasFeature = this._descriptor.__styleMeta.hasFeatureType[featureTypeKey];
return featureTypeKey === featureType
? isOnlySingleFeatureType && hasFeature
: isOnlySingleFeatureType && !hasFeature;
}, true);
_isOnlySingleFeatureType = async (featureType) => {
return isOnlySingleFeatureType(
featureType,
await this._source.getSupportedShapeTypes(),
this._getStyleMeta().hasFeatureType);
}
_getIsPointsOnly = async () => {
return this._checkIfOnlyFeatureType(VECTOR_SHAPE_TYPES.POINT);
return this._isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.POINT);
}
_getIsLinesOnly = async () => {
return this._checkIfOnlyFeatureType(VECTOR_SHAPE_TYPES.LINE);
return this._isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.LINE);
}
_getIsPolygonsOnly = async () => {
return this._isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.POLYGON);
}
_getFieldRange = (fieldName) => {
@ -352,6 +343,10 @@ export class VectorStyle extends AbstractStyle {
};
}
_getStyleMeta = () => {
return _.get(this._descriptor, '__styleMeta', {});
}
getIcon = () => {
const styles = this.getRawProperties();
const symbolId = this.arePointsSymbolizedAsCircles()
@ -368,21 +363,43 @@ export class VectorStyle extends AbstractStyle {
);
}
renderLegendDetails() {
const styles = this._getAllStyleProperties();
const styleProperties = styles.map((style) => {
return {
// eslint-disable-next-line max-len
range: (style.isDynamic() && style.isComplete() && style.getField().getName()) ? this._getFieldRange(style.getField().getName()) : null,
style: style
};
});
async _getLegendDetailStyleProperties() {
const isLinesOnly = await this._getIsLinesOnly();
const isPolygonsOnly = await this._getIsPolygonsOnly();
return (
<VectorStyleLegend
styleProperties={styleProperties}
/>
);
return this.getDynamicPropertiesArray().filter(styleProperty => {
if (isLinesOnly) {
return LINE_STYLES.includes(styleProperty.getStyleName());
}
if (isPolygonsOnly) {
return POLYGON_STYLES.includes(styleProperty.getStyleName());
}
return true;
});
}
async hasLegendDetails() {
const styles = await this._getLegendDetailStyleProperties();
return styles.length > 0;
}
renderLegendDetails() {
const loadRows = async () => {
const styles = await this._getLegendDetailStyleProperties();
const promises = styles.map(async (style) => {
return {
label: await style.getField().getLabel(),
fieldFormatter: await this._source.getFieldFormatter(style.getField().getName()),
range: this._getFieldRange(style.getField().getName()),
style,
};
});
return await Promise.all(promises);
};
return <VectorStyleLegend loadRows={loadRows} />;
}
_getStyleFields() {

View file

@ -27,6 +27,9 @@ export const VECTOR_STYLES = {
ICON_ORIENTATION: 'iconOrientation'
};
export const LINE_STYLES = [VECTOR_STYLES.LINE_COLOR, VECTOR_STYLES.LINE_WIDTH];
export const POLYGON_STYLES = [VECTOR_STYLES.FILL_COLOR, VECTOR_STYLES.LINE_COLOR, VECTOR_STYLES.LINE_WIDTH];
export function getDefaultProperties(mapColors = []) {
return {
...getDefaultStaticProperties(mapColors),

View file

@ -145,8 +145,8 @@ export class VectorLayer extends AbstractLayer {
return 'vector';
}
hasLegendDetails() {
return this._style.getDynamicPropertiesArray().length > 0;
async hasLegendDetails() {
return this._style.hasLegendDetails();
}
renderLegendDetails() {