mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Geomap: Improve geojson style editor (#41926)
This commit is contained in:
parent
e904f423e4
commit
541d1543db
@ -1,17 +1,27 @@
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { Button, useTheme2 } from '@grafana/ui';
|
||||
import { StandardEditorProps, StandardEditorsRegistryItem } from '@grafana/data';
|
||||
|
||||
import { FeatureStyleConfig } from '../types';
|
||||
import { Button } from '@grafana/ui';
|
||||
import { DEFAULT_STYLE_RULE } from '../layers/data/geojsonLayer';
|
||||
import { StyleRuleEditor, StyleRuleEditorSettings } from './StyleRuleEditor';
|
||||
import { defaultStyleConfig } from '../style/types';
|
||||
|
||||
export const GeomapStyleRulesEditor: FC<StandardEditorProps<FeatureStyleConfig[], any, any>> = (props) => {
|
||||
const { value, onChange, context, item } = props;
|
||||
const theme = useTheme2();
|
||||
|
||||
const settings = item.settings;
|
||||
const onAddRule = useCallback(() => {
|
||||
onChange([...value, DEFAULT_STYLE_RULE]);
|
||||
}, [onChange, value]);
|
||||
const { palette } = theme.visualization;
|
||||
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(
|
||||
(idx) => (style: FeatureStyleConfig | undefined) => {
|
||||
|
@ -12,7 +12,6 @@ import { getUniqueFeatureValues, LayerContentInfo } from '../utils/getFeatures';
|
||||
import { FeatureLike } from 'ol/Feature';
|
||||
import { getSelectionInfo } from '../utils/selection';
|
||||
import { NumberInput } from 'app/features/dimensions/editors/NumberInput';
|
||||
import { isNumber } from 'lodash';
|
||||
|
||||
export interface StyleRuleEditorSettings {
|
||||
features: Observable<FeatureLike[]>;
|
||||
@ -21,6 +20,7 @@ export interface StyleRuleEditorSettings {
|
||||
|
||||
const comparators = [
|
||||
{ label: '==', value: ComparisonOperation.EQ },
|
||||
{ label: '!=', value: ComparisonOperation.NEQ },
|
||||
{ label: '>', value: ComparisonOperation.GT },
|
||||
{ label: '>=', value: ComparisonOperation.GTE },
|
||||
{ label: '<', value: ComparisonOperation.LT },
|
||||
@ -40,7 +40,21 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
|
||||
const uniqueSelectables = useMemo(() => {
|
||||
const key = value?.check?.property;
|
||||
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 [];
|
||||
}, [feats, value]);
|
||||
@ -143,7 +157,7 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
|
||||
</InlineField>
|
||||
<InlineField className={styles.inline} grow={true}>
|
||||
<>
|
||||
{check.operation === ComparisonOperation.EQ && (
|
||||
{(check.operation === ComparisonOperation.EQ || check.operation === ComparisonOperation.NEQ) && (
|
||||
<Select
|
||||
menuShouldPortal
|
||||
placeholder={'value'}
|
||||
@ -158,7 +172,7 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
|
||||
{check.operation !== ComparisonOperation.EQ && (
|
||||
<NumberInput
|
||||
key={`${check.property}/${check.operation}`}
|
||||
value={isNumber(check.value) ? check.value : 0}
|
||||
value={!isNaN(Number(check.value)) ? Number(check.value) : 0}
|
||||
placeholder="numeric value"
|
||||
onChange={onChangeNumericValue}
|
||||
/>
|
||||
@ -183,6 +197,7 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
|
||||
{
|
||||
settings: {
|
||||
simpleFixedValues: true,
|
||||
layerInfo,
|
||||
},
|
||||
} as any
|
||||
}
|
||||
|
@ -11,6 +11,8 @@ import {
|
||||
RadioButtonGroup,
|
||||
SliderValueEditor,
|
||||
} from '@grafana/ui';
|
||||
import { Observable } from 'rxjs';
|
||||
import { useObservable } from 'react-use';
|
||||
|
||||
import {
|
||||
ColorDimensionEditor,
|
||||
@ -28,8 +30,10 @@ import {
|
||||
} from 'app/features/dimensions/types';
|
||||
import { defaultStyleConfig, StyleConfig, TextAlignment, TextBaseline } from '../../style/types';
|
||||
import { styleUsesText } from '../../style/utils';
|
||||
import { LayerContentInfo } from '../../utils/getFeatures';
|
||||
|
||||
export interface StyleEditorOptions {
|
||||
layerInfo?: Observable<LayerContentInfo>;
|
||||
simpleFixedValues?: boolean;
|
||||
}
|
||||
|
||||
@ -79,10 +83,40 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
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
|
||||
if (item.settings?.simpleFixedValues) {
|
||||
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>
|
||||
<InlineField label="Color" labelWidth={10}>
|
||||
<InlineLabel width={4}>
|
||||
@ -96,7 +130,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Opacity" labelWidth={10} grow={true}>
|
||||
<InlineField label="Opacity" labelWidth={10} grow>
|
||||
<SliderValueEditor
|
||||
value={value.opacity ?? defaultStyleConfig.opacity}
|
||||
context={context}
|
||||
@ -117,8 +151,6 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
||||
);
|
||||
}
|
||||
|
||||
const hasTextLabel = styleUsesText(value);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Field label={'Size'}>
|
||||
|
@ -1,10 +1,4 @@
|
||||
import {
|
||||
MapLayerRegistryItem,
|
||||
MapLayerOptions,
|
||||
PanelData,
|
||||
GrafanaTheme2,
|
||||
PluginState,
|
||||
} from '@grafana/data';
|
||||
import { MapLayerRegistryItem, MapLayerOptions, PanelData, GrafanaTheme2, PluginState } from '@grafana/data';
|
||||
import Map from 'ol/Map';
|
||||
import VectorLayer from 'ol/layer/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
|
||||
const layerInfo = features.pipe(
|
||||
first(),
|
||||
rxjsmap((v) => getLayerPropertyInfo(v)),
|
||||
rxjsmap((v) => getLayerPropertyInfo(v))
|
||||
);
|
||||
|
||||
builder
|
||||
@ -146,18 +140,6 @@ export const geojsonLayer: MapLayerRegistryItem<GeoJSONMapperConfig> = {
|
||||
},
|
||||
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({
|
||||
id: 'config.style',
|
||||
path: 'config.style',
|
||||
@ -166,8 +148,21 @@ export const geojsonLayer: MapLayerRegistryItem<GeoJSONMapperConfig> = {
|
||||
editor: StyleEditor,
|
||||
settings: {
|
||||
simpleFixedValues: true,
|
||||
layerInfo,
|
||||
},
|
||||
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: [],
|
||||
});
|
||||
},
|
||||
};
|
||||
|
@ -57,6 +57,7 @@ export interface FeatureRuleConfig {
|
||||
|
||||
export enum ComparisonOperation {
|
||||
EQ = 'eq',
|
||||
NEQ = 'neq',
|
||||
LT = 'lt',
|
||||
LTE = 'lte',
|
||||
GT = 'gt',
|
||||
|
@ -58,6 +58,16 @@ describe('check if feature matches style rule', () => {
|
||||
feature
|
||||
)
|
||||
).toEqual(true);
|
||||
expect(
|
||||
checkFeatureMatchesStyleRule(
|
||||
{
|
||||
operation: ComparisonOperation.NEQ,
|
||||
property: 'number',
|
||||
value: 3,
|
||||
},
|
||||
feature
|
||||
)
|
||||
).toEqual(false);
|
||||
});
|
||||
it('can compare with strings', () => {
|
||||
const feature = new Feature({
|
||||
@ -114,6 +124,16 @@ describe('check if feature matches style rule', () => {
|
||||
feature
|
||||
)
|
||||
).toEqual(true);
|
||||
expect(
|
||||
checkFeatureMatchesStyleRule(
|
||||
{
|
||||
operation: ComparisonOperation.NEQ,
|
||||
property: 'string',
|
||||
value: 'b',
|
||||
},
|
||||
feature
|
||||
)
|
||||
).toEqual(false);
|
||||
});
|
||||
it('can compare with booleans', () => {
|
||||
const feature = new Feature({
|
||||
@ -172,5 +192,15 @@ describe('check if feature matches style rule', () => {
|
||||
feature
|
||||
)
|
||||
).toEqual(true);
|
||||
expect(
|
||||
checkFeatureMatchesStyleRule(
|
||||
{
|
||||
operation: ComparisonOperation.NEQ,
|
||||
property: 'boolean',
|
||||
value: false,
|
||||
},
|
||||
feature
|
||||
)
|
||||
).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
@ -12,6 +12,8 @@ export const checkFeatureMatchesStyleRule = (rule: FeatureRuleConfig, feature: F
|
||||
switch (rule.operation) {
|
||||
case ComparisonOperation.EQ:
|
||||
return val === rule.value;
|
||||
case ComparisonOperation.NEQ:
|
||||
return val !== rule.value;
|
||||
case ComparisonOperation.GT:
|
||||
return val > rule.value;
|
||||
case ComparisonOperation.GTE:
|
||||
|
Loading…
Reference in New Issue
Block a user