Geomap: Improve geojson style editor (#41926) (#41933)

This commit is contained in:
Grot (@grafanabot) 2021-11-26 16:34:27 -05:00 committed by GitHub
parent 4710572d2a
commit 03f54577a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 115 additions and 30 deletions

View File

@ -1,17 +1,27 @@
import React, { FC, useCallback } from 'react'; import React, { FC, useCallback } from 'react';
import { Button, useTheme2 } from '@grafana/ui';
import { StandardEditorProps, StandardEditorsRegistryItem } from '@grafana/data'; import { StandardEditorProps, StandardEditorsRegistryItem } from '@grafana/data';
import { FeatureStyleConfig } from '../types'; import { FeatureStyleConfig } from '../types';
import { Button } from '@grafana/ui';
import { DEFAULT_STYLE_RULE } from '../layers/data/geojsonLayer'; import { DEFAULT_STYLE_RULE } from '../layers/data/geojsonLayer';
import { StyleRuleEditor, StyleRuleEditorSettings } from './StyleRuleEditor'; import { StyleRuleEditor, StyleRuleEditorSettings } from './StyleRuleEditor';
import { defaultStyleConfig } from '../style/types';
export const GeomapStyleRulesEditor: FC<StandardEditorProps<FeatureStyleConfig[], any, any>> = (props) => { export const GeomapStyleRulesEditor: FC<StandardEditorProps<FeatureStyleConfig[], any, any>> = (props) => {
const { value, onChange, context, item } = props; const { value, onChange, context, item } = props;
const theme = useTheme2();
const settings = item.settings; const settings = item.settings;
const onAddRule = useCallback(() => { const onAddRule = useCallback(() => {
onChange([...value, DEFAULT_STYLE_RULE]); const { palette } = theme.visualization;
}, [onChange, value]); const color = {
fixed: palette[Math.floor(Math.random() * palette.length)],
};
const newRule = [...value, { ...DEFAULT_STYLE_RULE, style: { ...defaultStyleConfig, color } }];
onChange(newRule);
}, [onChange, value, theme.visualization]);
const onRuleChange = useCallback( const onRuleChange = useCallback(
(idx) => (style: FeatureStyleConfig | undefined) => { (idx) => (style: FeatureStyleConfig | undefined) => {

View File

@ -12,7 +12,6 @@ import { getUniqueFeatureValues, LayerContentInfo } from '../utils/getFeatures';
import { FeatureLike } from 'ol/Feature'; import { FeatureLike } from 'ol/Feature';
import { getSelectionInfo } from '../utils/selection'; import { getSelectionInfo } from '../utils/selection';
import { NumberInput } from 'app/features/dimensions/editors/NumberInput'; import { NumberInput } from 'app/features/dimensions/editors/NumberInput';
import { isNumber } from 'lodash';
export interface StyleRuleEditorSettings { export interface StyleRuleEditorSettings {
features: Observable<FeatureLike[]>; features: Observable<FeatureLike[]>;
@ -21,6 +20,7 @@ export interface StyleRuleEditorSettings {
const comparators = [ const comparators = [
{ label: '==', value: ComparisonOperation.EQ }, { label: '==', value: ComparisonOperation.EQ },
{ label: '!=', value: ComparisonOperation.NEQ },
{ label: '>', value: ComparisonOperation.GT }, { label: '>', value: ComparisonOperation.GT },
{ label: '>=', value: ComparisonOperation.GTE }, { label: '>=', value: ComparisonOperation.GTE },
{ label: '<', value: ComparisonOperation.LT }, { label: '<', value: ComparisonOperation.LT },
@ -40,7 +40,21 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
const uniqueSelectables = useMemo(() => { const uniqueSelectables = useMemo(() => {
const key = value?.check?.property; const key = value?.check?.property;
if (key && feats && value.check?.operation === ComparisonOperation.EQ) { if (key && feats && value.check?.operation === ComparisonOperation.EQ) {
return getUniqueFeatureValues(feats, key).map((v) => ({ value: v, label: v })); return getUniqueFeatureValues(feats, key).map((v) => {
let newValue;
let isNewValueNumber = !isNaN(Number(v));
if (isNewValueNumber) {
newValue = {
value: Number(v),
label: v,
};
} else {
newValue = { value: v, label: v };
}
return newValue;
});
} }
return []; return [];
}, [feats, value]); }, [feats, value]);
@ -143,7 +157,7 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
</InlineField> </InlineField>
<InlineField className={styles.inline} grow={true}> <InlineField className={styles.inline} grow={true}>
<> <>
{check.operation === ComparisonOperation.EQ && ( {(check.operation === ComparisonOperation.EQ || check.operation === ComparisonOperation.NEQ) && (
<Select <Select
menuShouldPortal menuShouldPortal
placeholder={'value'} placeholder={'value'}
@ -158,7 +172,7 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
{check.operation !== ComparisonOperation.EQ && ( {check.operation !== ComparisonOperation.EQ && (
<NumberInput <NumberInput
key={`${check.property}/${check.operation}`} key={`${check.property}/${check.operation}`}
value={isNumber(check.value) ? check.value : 0} value={!isNaN(Number(check.value)) ? Number(check.value) : 0}
placeholder="numeric value" placeholder="numeric value"
onChange={onChangeNumericValue} onChange={onChangeNumericValue}
/> />
@ -183,6 +197,7 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
{ {
settings: { settings: {
simpleFixedValues: true, simpleFixedValues: true,
layerInfo,
}, },
} as any } as any
} }

View File

@ -11,6 +11,8 @@ import {
RadioButtonGroup, RadioButtonGroup,
SliderValueEditor, SliderValueEditor,
} from '@grafana/ui'; } from '@grafana/ui';
import { Observable } from 'rxjs';
import { useObservable } from 'react-use';
import { import {
ColorDimensionEditor, ColorDimensionEditor,
@ -28,8 +30,10 @@ import {
} from 'app/features/dimensions/types'; } from 'app/features/dimensions/types';
import { defaultStyleConfig, StyleConfig, TextAlignment, TextBaseline } from '../../style/types'; import { defaultStyleConfig, StyleConfig, TextAlignment, TextBaseline } from '../../style/types';
import { styleUsesText } from '../../style/utils'; import { styleUsesText } from '../../style/utils';
import { LayerContentInfo } from '../../utils/getFeatures';
export interface StyleEditorOptions { export interface StyleEditorOptions {
layerInfo?: Observable<LayerContentInfo>;
simpleFixedValues?: boolean; simpleFixedValues?: boolean;
} }
@ -79,10 +83,40 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
onChange({ ...value, textConfig: { ...value.textConfig, textBaseline: textBaseline as TextBaseline } }); onChange({ ...value, textConfig: { ...value.textConfig, textBaseline: textBaseline as TextBaseline } });
}; };
let featuresHavePoints = false;
if (item.settings?.layerInfo) {
const propertyOptions = useObservable(item.settings?.layerInfo);
featuresHavePoints = propertyOptions?.geometryType === 'point';
}
const hasTextLabel = styleUsesText(value);
// Simple fixed value display // Simple fixed value display
if (item.settings?.simpleFixedValues) { if (item.settings?.simpleFixedValues) {
return ( return (
<> <>
{featuresHavePoints && (
<InlineFieldRow>
<InlineField label={'Symbol'}>
<ResourceDimensionEditor
value={value.symbol ?? defaultStyleConfig.symbol}
context={context}
onChange={onSymbolChange}
item={
{
settings: {
resourceType: 'icon',
folderName: ResourceFolderName.Marker,
placeholderText: hasTextLabel ? 'Select a symbol' : 'Select a symbol or add a text label',
placeholderValue: defaultStyleConfig.symbol.fixed,
showSourceRadio: false,
},
} as any
}
/>
</InlineField>
</InlineFieldRow>
)}
<InlineFieldRow> <InlineFieldRow>
<InlineField label="Color" labelWidth={10}> <InlineField label="Color" labelWidth={10}>
<InlineLabel width={4}> <InlineLabel width={4}>
@ -96,7 +130,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
</InlineField> </InlineField>
</InlineFieldRow> </InlineFieldRow>
<InlineFieldRow> <InlineFieldRow>
<InlineField label="Opacity" labelWidth={10} grow={true}> <InlineField label="Opacity" labelWidth={10} grow>
<SliderValueEditor <SliderValueEditor
value={value.opacity ?? defaultStyleConfig.opacity} value={value.opacity ?? defaultStyleConfig.opacity}
context={context} context={context}
@ -117,8 +151,6 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
); );
} }
const hasTextLabel = styleUsesText(value);
return ( return (
<> <>
<Field label={'Size'}> <Field label={'Size'}>

View File

@ -1,10 +1,4 @@
import { import { MapLayerRegistryItem, MapLayerOptions, PanelData, GrafanaTheme2, PluginState } from '@grafana/data';
MapLayerRegistryItem,
MapLayerOptions,
PanelData,
GrafanaTheme2,
PluginState,
} from '@grafana/data';
import Map from 'ol/Map'; import Map from 'ol/Map';
import VectorLayer from 'ol/layer/Vector'; import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector'; import VectorSource from 'ol/source/Vector';
@ -129,7 +123,7 @@ export const geojsonLayer: MapLayerRegistryItem<GeoJSONMapperConfig> = {
// get properties for first feature to use as ui options // get properties for first feature to use as ui options
const layerInfo = features.pipe( const layerInfo = features.pipe(
first(), first(),
rxjsmap((v) => getLayerPropertyInfo(v)), rxjsmap((v) => getLayerPropertyInfo(v))
); );
builder builder
@ -146,18 +140,6 @@ export const geojsonLayer: MapLayerRegistryItem<GeoJSONMapperConfig> = {
}, },
defaultValue: defaultOptions.src, defaultValue: defaultOptions.src,
}) })
.addCustomEditor({
id: 'config.rules',
path: 'config.rules',
name: 'Style Rules',
description: 'Apply styles based on feature properties',
editor: GeomapStyleRulesEditor,
settings: {
features: features,
layerInfo: layerInfo,
},
defaultValue: [],
})
.addCustomEditor({ .addCustomEditor({
id: 'config.style', id: 'config.style',
path: 'config.style', path: 'config.style',
@ -166,8 +148,21 @@ export const geojsonLayer: MapLayerRegistryItem<GeoJSONMapperConfig> = {
editor: StyleEditor, editor: StyleEditor,
settings: { settings: {
simpleFixedValues: true, simpleFixedValues: true,
layerInfo,
}, },
defaultValue: defaultOptions.style, defaultValue: defaultOptions.style,
})
.addCustomEditor({
id: 'config.rules',
path: 'config.rules',
name: 'Style Rules',
description: 'Apply styles based on feature properties',
editor: GeomapStyleRulesEditor,
settings: {
features,
layerInfo,
},
defaultValue: [],
}); });
}, },
}; };

View File

@ -57,6 +57,7 @@ export interface FeatureRuleConfig {
export enum ComparisonOperation { export enum ComparisonOperation {
EQ = 'eq', EQ = 'eq',
NEQ = 'neq',
LT = 'lt', LT = 'lt',
LTE = 'lte', LTE = 'lte',
GT = 'gt', GT = 'gt',

View File

@ -58,6 +58,16 @@ describe('check if feature matches style rule', () => {
feature feature
) )
).toEqual(true); ).toEqual(true);
expect(
checkFeatureMatchesStyleRule(
{
operation: ComparisonOperation.NEQ,
property: 'number',
value: 3,
},
feature
)
).toEqual(false);
}); });
it('can compare with strings', () => { it('can compare with strings', () => {
const feature = new Feature({ const feature = new Feature({
@ -114,6 +124,16 @@ describe('check if feature matches style rule', () => {
feature feature
) )
).toEqual(true); ).toEqual(true);
expect(
checkFeatureMatchesStyleRule(
{
operation: ComparisonOperation.NEQ,
property: 'string',
value: 'b',
},
feature
)
).toEqual(false);
}); });
it('can compare with booleans', () => { it('can compare with booleans', () => {
const feature = new Feature({ const feature = new Feature({
@ -172,5 +192,15 @@ describe('check if feature matches style rule', () => {
feature feature
) )
).toEqual(true); ).toEqual(true);
expect(
checkFeatureMatchesStyleRule(
{
operation: ComparisonOperation.NEQ,
property: 'boolean',
value: false,
},
feature
)
).toEqual(false);
}); });
}); });

View File

@ -12,6 +12,8 @@ export const checkFeatureMatchesStyleRule = (rule: FeatureRuleConfig, feature: F
switch (rule.operation) { switch (rule.operation) {
case ComparisonOperation.EQ: case ComparisonOperation.EQ:
return val === rule.value; return val === rule.value;
case ComparisonOperation.NEQ:
return val !== rule.value;
case ComparisonOperation.GT: case ComparisonOperation.GT:
return val > rule.value; return val > rule.value;
case ComparisonOperation.GTE: case ComparisonOperation.GTE: