Geomap: Add style rules UI for geoJSON map layer (#40735)

* Geomap: Show multiple layers in ui

Co-authored-by: Ryan McKinley <ryantxu@users.noreply.github.com>

* Geomap: Add geojson style rules ui

* add style rule editor component

* update and change to two row styles

Co-authored-by: Ryan McKinley <ryantxu@users.noreply.github.com>
This commit is contained in:
nikki-kiga 2021-10-21 15:56:41 -07:00 committed by GitHub
parent 97df4a57f4
commit dfeb69dc17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 283 additions and 26 deletions

View File

@ -0,0 +1,65 @@
import React, { FC, useCallback } from 'react';
import { StandardEditorProps, StandardEditorsRegistryItem } from '@grafana/data';
import { ComparisonOperation, FeatureStyleConfig } from '../types';
import { Button } from '@grafana/ui';
import { DEFAULT_STYLE_RULE } from '../layers/data/geojsonMapper';
import { StyleRuleEditor, StyleRuleEditorSettings } from './StyleRuleEditor';
export const GeomapStyleRulesEditor: FC<StandardEditorProps<FeatureStyleConfig[], any, any>> = (props) => {
const { value, onChange, context } = props;
const OPTIONS = getComparisonOperatorOptions();
const onAddRule = useCallback(() => {
onChange([...value, DEFAULT_STYLE_RULE]);
}, [onChange, value]);
const onRuleChange = useCallback(
(idx) => (style: FeatureStyleConfig | undefined) => {
const copyStyles = [...value];
if (style) {
copyStyles[idx] = style;
} else {
//assume undefined is only returned on delete
copyStyles.splice(idx, 1);
}
onChange(copyStyles);
},
[onChange, value]
);
const styleOptions =
value &&
value.map((style, idx: number) => {
const itemSettings: StandardEditorsRegistryItem<any, StyleRuleEditorSettings> = {
settings: { options: OPTIONS },
} as any;
return (
<StyleRuleEditor
value={style}
onChange={onRuleChange(idx)}
context={context}
item={itemSettings}
key={`${idx}-${style.rule}`}
/>
);
});
return (
<>
{styleOptions}
<Button size="sm" icon="plus" onClick={onAddRule} variant="secondary" aria-label={'Add geomap style rule'}>
{'Add style rule'}
</Button>
</>
);
};
const getComparisonOperatorOptions = () => {
const options = [];
for (const value of Object.values(ComparisonOperation)) {
options.push({ value: value, label: value });
}
return options;
};

View File

@ -0,0 +1,163 @@
import React, { ChangeEvent, FC, useCallback } from 'react';
import { GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data';
import { ComparisonOperation, FeatureStyleConfig } from '../types';
import { Button, ColorPicker, InlineField, InlineFieldRow, Input, Select, useStyles2 } from '@grafana/ui';
import { css } from '@emotion/css';
import { NumberInput } from 'app/features/dimensions/editors/NumberInput';
export interface StyleRuleEditorSettings {
options: SelectableValue[];
}
export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, any, StyleRuleEditorSettings>> = (
props
) => {
const { value, onChange, item } = props;
const settings: StyleRuleEditorSettings = item.settings;
const styles = useStyles2(getStyles);
const LABEL_WIDTH = 10;
const onChangeComparisonProperty = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
onChange({
...value,
rule: {
...value.rule,
property: e.currentTarget.value,
operation: value.rule?.operation ?? ComparisonOperation.EQ,
value: value.rule?.value ?? '',
},
});
},
[onChange, value]
);
const onChangeComparison = useCallback(
(selection: SelectableValue) => {
onChange({
...value,
rule: {
...value.rule,
operation: selection.value ?? ComparisonOperation.EQ,
property: value.rule?.property ?? '',
value: value.rule?.value ?? '',
},
});
},
[onChange, value]
);
const onChangeComparisonValue = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
onChange({
...value,
rule: {
...value.rule,
value: e.currentTarget.value,
operation: value.rule?.operation ?? ComparisonOperation.EQ,
property: value.rule?.property ?? '',
},
});
},
[onChange, value]
);
const onChangeColor = useCallback(
(c: string) => {
onChange({ ...value, fillColor: c });
},
[onChange, value]
);
const onChangeStrokeWidth = useCallback(
(num: number | undefined) => {
onChange({ ...value, strokeWidth: num ?? value.strokeWidth ?? 1 });
},
[onChange, value]
);
const onDelete = useCallback(() => {
onChange(undefined);
}, [onChange]);
return (
<div className={styles.rule}>
<InlineFieldRow className={styles.row}>
<InlineField label="Rule" labelWidth={LABEL_WIDTH} grow={true}>
<Input
type="text"
placeholder={'Feature property'}
value={`${value?.rule?.property}`}
onChange={onChangeComparisonProperty}
aria-label={'Feature property'}
/>
</InlineField>
<InlineField className={styles.inline} grow={true}>
<Select
menuShouldPortal
value={`${value?.rule?.operation}` ?? ComparisonOperation.EQ}
options={settings.options}
onChange={onChangeComparison}
aria-label={'Comparison operator'}
/>
</InlineField>
<InlineField className={styles.inline} grow={true}>
<Input
type="text"
placeholder={'value'}
value={`${value?.rule?.value}`}
onChange={onChangeComparisonValue}
aria-label={'Comparison value'}
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow className={styles.row}>
<InlineField label="Style" labelWidth={LABEL_WIDTH} className={styles.color}>
<ColorPicker color={value?.fillColor} onChange={onChangeColor} />
</InlineField>
<InlineField label="Stroke" className={styles.inline} grow={true}>
<NumberInput
value={value?.strokeWidth ?? 1}
min={1}
max={20}
step={0.5}
aria-label={'Stroke width'}
onChange={onChangeStrokeWidth}
/>
</InlineField>
<Button
size="md"
icon="trash-alt"
onClick={() => onDelete()}
variant="secondary"
aria-label={'Delete style rule'}
className={styles.button}
></Button>
</InlineFieldRow>
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
rule: css`
margin-bottom: ${theme.spacing(1)};
`,
row: css`
display: flex;
margin-bottom: 4px;
`,
inline: css`
margin-bottom: 0;
margin-left: 4px;
`,
color: css`
align-items: center;
margin-bottom: 0;
margin-right: 4px;
`,
button: css`
margin-left: 4px;
`,
});

View File

@ -7,15 +7,16 @@ import { Feature } from 'ol';
import { Geometry } from 'ol/geom';
import { getGeoMapStyle } from '../../utils/getGeoMapStyle';
import { checkFeatureMatchesStyleRule } from '../../utils/checkFeatureMatchesStyleRule';
import { FeatureStyleConfig } from '../../types';
import { ComparisonOperation, FeatureStyleConfig } from '../../types';
import { Stroke, Style } from 'ol/style';
import { FeatureLike } from 'ol/Feature';
import { GeomapStyleRulesEditor } from '../../editor/GeomapStyleRulesEditor';
export interface GeoJSONMapperConfig {
// URL for a geojson file
src?: string;
// Styles that can be applied
styles?: FeatureStyleConfig[];
styles: FeatureStyleConfig[];
}
const defaultOptions: GeoJSONMapperConfig = {
@ -23,6 +24,16 @@ const defaultOptions: GeoJSONMapperConfig = {
styles: [],
};
export const DEFAULT_STYLE_RULE: FeatureStyleConfig = {
fillColor: '#1F60C4',
strokeWidth: 1,
rule: {
property: '',
operation: ComparisonOperation.EQ,
value: '',
},
};
export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = {
id: 'geojson-value-mapper',
name: 'Map values to GeoJSON file',
@ -44,8 +55,8 @@ export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = {
const defaultStyle = new Style({
stroke: new Stroke({
color: '#1F60C4',
width: 1,
color: DEFAULT_STYLE_RULE.fillColor,
width: DEFAULT_STYLE_RULE.strokeWidth,
}),
});
@ -79,20 +90,27 @@ export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = {
// Geojson source url
registerOptionsUI: (builder) => {
builder.addSelect({
path: 'config.src',
name: 'GeoJSON URL',
settings: {
options: [
{ label: 'public/maps/countries.geojson', value: 'public/maps/countries.geojson' },
{ label: 'public/maps/usa-states.geojson', value: 'public/maps/usa-states.geojson' },
],
allowCustomValue: true,
},
defaultValue: defaultOptions.src,
});
builder
.addSelect({
path: 'config.src',
name: 'GeoJSON URL',
settings: {
options: [
{ label: 'public/maps/countries.geojson', value: 'public/maps/countries.geojson' },
{ label: 'public/maps/usa-states.geojson', value: 'public/maps/usa-states.geojson' },
],
allowCustomValue: true,
},
defaultValue: defaultOptions.src,
})
.addCustomEditor({
id: 'config.styles',
path: 'config.styles',
name: 'Style Rules',
editor: GeomapStyleRulesEditor,
settings: {},
defaultValue: [],
});
},
// fill in the default values
defaultOptions,
};

View File

@ -53,14 +53,25 @@ export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
);
}
builder.addNestedOptions(
getLayerEditor({
category: ['Data layer'],
path: 'layers[0]', // only one for now
basemaps: false,
current: context.options?.layers?.[0],
})
);
let layerCount = context.options?.layers?.length;
if (layerCount == null || layerCount < 1) {
layerCount = 1;
}
for (let i = 0; i < layerCount; i++) {
let name = 'Data layer';
if (i > 0) {
name += ` (${i + 1})`;
}
builder.addNestedOptions(
getLayerEditor({
category: [name],
path: `layers[${i}]`, // only one for now
basemaps: false,
current: context.options?.layers?.[i],
})
);
}
// The controls section
category = ['Map controls'];