Geomap: use same style config for makers and geojson (#41846)

pull/41454/head
Ryan McKinley 4 years ago committed by GitHub
parent 18cc552edb
commit 837e268395
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      public/app/plugins/panel/geomap/editor/GeomapStyleRulesEditor.tsx
  2. 87
      public/app/plugins/panel/geomap/editor/StyleRuleEditor.tsx
  3. 67
      public/app/plugins/panel/geomap/layers/data/StyleEditor.tsx
  4. 102
      public/app/plugins/panel/geomap/layers/data/geojsonLayer.ts
  5. 4
      public/app/plugins/panel/geomap/layers/data/index.ts
  6. 8
      public/app/plugins/panel/geomap/style/markers.ts
  7. 7
      public/app/plugins/panel/geomap/types.ts
  8. 20
      public/app/plugins/panel/geomap/utils/getGeoMapStyle.ts

@ -2,7 +2,7 @@ import React, { FC, useCallback } from 'react';
import { StandardEditorProps, StandardEditorsRegistryItem } from '@grafana/data'; import { StandardEditorProps, StandardEditorsRegistryItem } from '@grafana/data';
import { ComparisonOperation, FeatureStyleConfig } from '../types'; import { ComparisonOperation, FeatureStyleConfig } from '../types';
import { Button } from '@grafana/ui'; import { Button } from '@grafana/ui';
import { DEFAULT_STYLE_RULE } from '../layers/data/geojsonMapper'; import { DEFAULT_STYLE_RULE } from '../layers/data/geojsonLayer';
import { StyleRuleEditor, StyleRuleEditorSettings } from './StyleRuleEditor'; import { StyleRuleEditor, StyleRuleEditorSettings } from './StyleRuleEditor';
export const GeomapStyleRulesEditor: FC<StandardEditorProps<FeatureStyleConfig[], any, any>> = (props) => { export const GeomapStyleRulesEditor: FC<StandardEditorProps<FeatureStyleConfig[], any, any>> = (props) => {
@ -41,7 +41,7 @@ export const GeomapStyleRulesEditor: FC<StandardEditorProps<FeatureStyleConfig[]
onChange={onRuleChange(idx)} onChange={onRuleChange(idx)}
context={context} context={context}
item={itemSettings} item={itemSettings}
key={`${idx}-${style.rule}`} key={`${idx}-${style.check?.property}`}
/> />
); );
}); });

@ -1,9 +1,11 @@
import React, { ChangeEvent, FC, useCallback } from 'react'; import React, { ChangeEvent, FC, useCallback } from 'react';
import { GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data'; import { GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data';
import { ComparisonOperation, FeatureStyleConfig } from '../types'; import { ComparisonOperation, FeatureStyleConfig } from '../types';
import { Button, ColorPicker, InlineField, InlineFieldRow, Input, Select, useStyles2 } from '@grafana/ui'; import { Button, InlineField, InlineFieldRow, Input, Select, useStyles2 } from '@grafana/ui';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { NumberInput } from 'app/features/dimensions/editors/NumberInput'; import { StyleEditor } from '../layers/data/StyleEditor';
import { defaultStyleConfig, StyleConfig } from '../style/types';
import { DEFAULT_STYLE_RULE } from '../layers/data/geojsonLayer';
export interface StyleRuleEditorSettings { export interface StyleRuleEditorSettings {
options: SelectableValue[]; options: SelectableValue[];
@ -12,7 +14,7 @@ export interface StyleRuleEditorSettings {
export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, any, StyleRuleEditorSettings>> = ( export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, any, StyleRuleEditorSettings>> = (
props props
) => { ) => {
const { value, onChange, item } = props; const { value, onChange, item, context } = props;
const settings: StyleRuleEditorSettings = item.settings; const settings: StyleRuleEditorSettings = item.settings;
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
@ -23,11 +25,11 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
(e: ChangeEvent<HTMLInputElement>) => { (e: ChangeEvent<HTMLInputElement>) => {
onChange({ onChange({
...value, ...value,
rule: { check: {
...value.rule, ...value.check,
property: e.currentTarget.value, property: e.currentTarget.value,
operation: value.rule?.operation ?? ComparisonOperation.EQ, operation: value.check?.operation ?? ComparisonOperation.EQ,
value: value.rule?.value ?? '', value: value.check?.value ?? '',
}, },
}); });
}, },
@ -38,11 +40,11 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
(selection: SelectableValue) => { (selection: SelectableValue) => {
onChange({ onChange({
...value, ...value,
rule: { check: {
...value.rule, ...value.check,
operation: selection.value ?? ComparisonOperation.EQ, operation: selection.value ?? ComparisonOperation.EQ,
property: value.rule?.property ?? '', property: value.check?.property ?? '',
value: value.rule?.value ?? '', value: value.check?.value ?? '',
}, },
}); });
}, },
@ -53,27 +55,20 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
(e: ChangeEvent<HTMLInputElement>) => { (e: ChangeEvent<HTMLInputElement>) => {
onChange({ onChange({
...value, ...value,
rule: { check: {
...value.rule, ...value.check,
value: e.currentTarget.value, value: e.currentTarget.value,
operation: value.rule?.operation ?? ComparisonOperation.EQ, operation: value.check?.operation ?? ComparisonOperation.EQ,
property: value.rule?.property ?? '', property: value.check?.property ?? '',
}, },
}); });
}, },
[onChange, value] [onChange, value]
); );
const onChangeColor = useCallback( const onChangeStyle = useCallback(
(c: string) => { (style?: StyleConfig) => {
onChange({ ...value, fillColor: c }); onChange({ ...value, style });
},
[onChange, value]
);
const onChangeStrokeWidth = useCallback(
(num: number | undefined) => {
onChange({ ...value, strokeWidth: num ?? value.strokeWidth ?? 1 });
}, },
[onChange, value] [onChange, value]
); );
@ -82,6 +77,8 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
onChange(undefined); onChange(undefined);
}, [onChange]); }, [onChange]);
const check = value.check ?? DEFAULT_STYLE_RULE.check!;
return ( return (
<div className={styles.rule}> <div className={styles.rule}>
<InlineFieldRow className={styles.row}> <InlineFieldRow className={styles.row}>
@ -89,7 +86,7 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
<Input <Input
type="text" type="text"
placeholder={'Feature property'} placeholder={'Feature property'}
value={`${value?.rule?.property}`} value={check.property ?? ''}
onChange={onChangeComparisonProperty} onChange={onChangeComparisonProperty}
aria-label={'Feature property'} aria-label={'Feature property'}
/> />
@ -97,7 +94,7 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
<InlineField className={styles.inline} grow={true}> <InlineField className={styles.inline} grow={true}>
<Select <Select
menuShouldPortal menuShouldPortal
value={`${value?.rule?.operation}` ?? ComparisonOperation.EQ} value={check.operation ?? ComparisonOperation.EQ}
options={settings.options} options={settings.options}
onChange={onChangeComparison} onChange={onChangeComparison}
aria-label={'Comparison operator'} aria-label={'Comparison operator'}
@ -107,26 +104,11 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
<Input <Input
type="text" type="text"
placeholder={'value'} placeholder={'value'}
value={`${value?.rule?.value}`} value={`${check.value}` ?? ''}
onChange={onChangeComparisonValue} onChange={onChangeComparisonValue}
aria-label={'Comparison value'} aria-label={'Comparison value'}
/> />
</InlineField> </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 <Button
size="md" size="md"
icon="trash-alt" icon="trash-alt"
@ -136,6 +118,20 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
className={styles.button} className={styles.button}
></Button> ></Button>
</InlineFieldRow> </InlineFieldRow>
<div>
<StyleEditor
value={value.style ?? defaultStyleConfig}
context={context}
onChange={onChangeStyle}
item={
{
settings: {
simpleFixedValues: true,
},
} as any
}
/>
</div>
</div> </div>
); );
}; };
@ -152,11 +148,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
margin-bottom: 0; margin-bottom: 0;
margin-left: 4px; margin-left: 4px;
`, `,
color: css`
align-items: center;
margin-bottom: 0;
margin-right: 4px;
`,
button: css` button: css`
margin-left: 4px; margin-left: 4px;
`, `,

@ -1,6 +1,16 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { StandardEditorProps } from '@grafana/data'; import { StandardEditorProps } from '@grafana/data';
import { Field, HorizontalGroup, NumberValueEditor, RadioButtonGroup, SliderValueEditor } from '@grafana/ui'; import {
ColorPicker,
Field,
HorizontalGroup,
InlineField,
InlineFieldRow,
InlineLabel,
NumberValueEditor,
RadioButtonGroup,
SliderValueEditor,
} from '@grafana/ui';
import { import {
ColorDimensionEditor, ColorDimensionEditor,
@ -17,8 +27,18 @@ import {
defaultTextConfig, defaultTextConfig,
} 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';
export interface StyleEditorOptions {
simpleFixedValues?: boolean;
}
export const StyleEditor: FC<StandardEditorProps<StyleConfig, any, any>> = ({ value, context, onChange }) => { export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions, any>> = ({
value,
context,
onChange,
item,
}) => {
const onSizeChange = (sizeValue: ScaleDimensionConfig | undefined) => { const onSizeChange = (sizeValue: ScaleDimensionConfig | undefined) => {
onChange({ ...value, size: sizeValue }); onChange({ ...value, size: sizeValue });
}; };
@ -59,7 +79,45 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, any, any>> = ({ va
onChange({ ...value, textConfig: { ...value.textConfig, textBaseline: textBaseline as TextBaseline } }); onChange({ ...value, textConfig: { ...value.textConfig, textBaseline: textBaseline as TextBaseline } });
}; };
const hasTextLabel = Boolean(value.text?.fixed || value.text?.field); // Simple fixed value display
if (item.settings?.simpleFixedValues) {
return (
<>
<InlineFieldRow>
<InlineField label="Color" labelWidth={10}>
<InlineLabel width={4}>
<ColorPicker
color={value.color?.fixed ?? defaultStyleConfig.color.fixed}
onChange={(v) => {
onColorChange({ fixed: v });
}}
/>
</InlineLabel>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label="Opacity" labelWidth={10} grow={true}>
<SliderValueEditor
value={value.opacity ?? defaultStyleConfig.opacity}
context={context}
onChange={onOpacityChange}
item={
{
settings: {
min: 0,
max: 1,
step: 0.1,
},
} as any
}
/>
</InlineField>
</InlineFieldRow>
</>
);
}
const hasTextLabel = styleUsesText(value);
return ( return (
<> <>
@ -128,7 +186,8 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, any, any>> = ({ va
item={{} as any} item={{} as any}
/> />
</Field> </Field>
{(value.text?.fixed || value.text?.field) && (
{hasTextLabel && (
<> <>
<HorizontalGroup> <HorizontalGroup>
<Field label={'Font size'}> <Field label={'Font size'}>

@ -4,44 +4,53 @@ import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector'; import VectorSource from 'ol/source/Vector';
import GeoJSON from 'ol/format/GeoJSON'; import GeoJSON from 'ol/format/GeoJSON';
import { unByKey } from 'ol/Observable'; import { unByKey } from 'ol/Observable';
import { Feature } from 'ol';
import { Geometry } from 'ol/geom';
import { getGeoMapStyle } from '../../utils/getGeoMapStyle';
import { checkFeatureMatchesStyleRule } from '../../utils/checkFeatureMatchesStyleRule'; import { checkFeatureMatchesStyleRule } from '../../utils/checkFeatureMatchesStyleRule';
import { ComparisonOperation, FeatureStyleConfig } from '../../types'; import { ComparisonOperation, FeatureRuleConfig, FeatureStyleConfig } from '../../types';
import { Stroke, Style } from 'ol/style'; import { Style } from 'ol/style';
import { FeatureLike } from 'ol/Feature'; import { FeatureLike } from 'ol/Feature';
import { GeomapStyleRulesEditor } from '../../editor/GeomapStyleRulesEditor'; import { GeomapStyleRulesEditor } from '../../editor/GeomapStyleRulesEditor';
import { circleMarker } from '../../style/markers'; import { defaultStyleConfig, StyleConfig } from '../../style/types';
import { getStyleConfigState } from '../../style/utils';
import { polyStyle } from '../../style/markers';
import { StyleEditor } from './StyleEditor';
export interface GeoJSONMapperConfig { export interface GeoJSONMapperConfig {
// URL for a geojson file // URL for a geojson file
src?: string; src?: string;
// Styles that can be applied // Pick style based on a rule
styles: FeatureStyleConfig[]; rules: FeatureStyleConfig[];
// The default style (applied if no rules match)
style: StyleConfig;
} }
const defaultOptions: GeoJSONMapperConfig = { const defaultOptions: GeoJSONMapperConfig = {
src: 'public/maps/countries.geojson', src: 'public/maps/countries.geojson',
styles: [], rules: [],
style: defaultStyleConfig,
}; };
interface StyleCheckerState {
poly: Style | Style[];
point: Style | Style[];
rule?: FeatureRuleConfig;
}
export const DEFAULT_STYLE_RULE: FeatureStyleConfig = { export const DEFAULT_STYLE_RULE: FeatureStyleConfig = {
fillColor: '#1F60C4', style: defaultStyleConfig,
strokeWidth: 1, check: {
rule: {
property: '', property: '',
operation: ComparisonOperation.EQ, operation: ComparisonOperation.EQ,
value: '', value: '',
}, },
}; };
export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = { export const geojsonLayer: MapLayerRegistryItem<GeoJSONMapperConfig> = {
id: 'geojson-value-mapper', id: 'geojson',
name: 'Map values to GeoJSON file', name: 'GeoJSON',
description: 'color features based on query results', description: 'Load static data from a geojson file',
isBaseMap: false, isBaseMap: false,
state: PluginState.alpha, state: PluginState.beta,
/** /**
* Function that configures transformation and returns a transformer * Function that configures transformation and returns a transformer
@ -68,30 +77,39 @@ export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = {
} }
}); });
const defaultStyle = new Style({ const styles: StyleCheckerState[] = [];
stroke: new Stroke({ if (config.rules) {
color: DEFAULT_STYLE_RULE.fillColor, for (const r of config.rules) {
width: DEFAULT_STYLE_RULE.strokeWidth, if (r.style) {
}), const s = await getStyleConfigState(r.style);
}); styles.push({
point: s.maker(s.base),
poly: polyStyle(s.base),
rule: r.check,
});
}
}
}
if (true) {
const s = await getStyleConfigState(config.style);
styles.push({
point: s.maker(s.base),
poly: polyStyle(s.base),
});
}
const vectorLayer = new VectorLayer({ const vectorLayer = new VectorLayer({
source, source,
style: (feature: FeatureLike) => { style: (feature: FeatureLike) => {
const type = feature.getGeometry()?.getType(); const isPoint = feature.getGeometry()?.getType() === 'Point';
if (type === 'Point') {
return circleMarker({color:DEFAULT_STYLE_RULE.fillColor});
}
if (feature && config?.styles?.length) { for (const check of styles) {
for (const style of config.styles) { if (check.rule && !checkFeatureMatchesStyleRule(check.rule, feature)) {
//check if there is no style rule or if the rule matches feature property continue;
if (!style.rule || checkFeatureMatchesStyleRule(style.rule, feature as Feature<Geometry>)) {
return getGeoMapStyle(style, feature);
}
} }
return isPoint ? check.point : check.poly;
} }
return defaultStyle; return undefined; // unreachable
}, },
}); });
@ -126,12 +144,24 @@ export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = {
defaultValue: defaultOptions.src, defaultValue: defaultOptions.src,
}) })
.addCustomEditor({ .addCustomEditor({
id: 'config.styles', id: 'config.rules',
path: 'config.styles', path: 'config.rules',
name: 'Style Rules', name: 'Style Rules',
description: 'Apply styles based on feature properties',
editor: GeomapStyleRulesEditor, editor: GeomapStyleRulesEditor,
settings: {}, settings: {},
defaultValue: [], defaultValue: [],
})
.addCustomEditor({
id: 'config.style',
path: 'config.style',
name: 'Default Style',
description: 'The style to apply when no rules above match',
editor: StyleEditor,
settings: {
simpleFixedValues: true,
},
defaultValue: defaultOptions.style,
}); });
}, },
}; };

@ -1,5 +1,5 @@
import { markersLayer } from './markersLayer'; import { markersLayer } from './markersLayer';
import { geojsonMapper } from './geojsonMapper'; import { geojsonLayer } from './geojsonLayer';
import { heatmapLayer } from './heatMap'; import { heatmapLayer } from './heatMap';
import { lastPointTracker } from './lastPointTracker'; import { lastPointTracker } from './lastPointTracker';
@ -10,5 +10,5 @@ export const dataLayers = [
markersLayer, markersLayer,
heatmapLayer, heatmapLayer,
lastPointTracker, lastPointTracker,
geojsonMapper, // dummy for now geojsonLayer,
]; ];

@ -75,6 +75,14 @@ export const circleMarker = (cfg: StyleConfigValues) => {
}); });
}; };
export const polyStyle = (cfg: StyleConfigValues) => {
return new Style({
fill: getFillColor(cfg),
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }),
text: textLabel(cfg),
});
};
// Square and cross // Square and cross
const errorMarker = (cfg: StyleConfigValues) => { const errorMarker = (cfg: StyleConfigValues) => {
const radius = cfg.size ?? DEFAULT_SIZE; const radius = cfg.size ?? DEFAULT_SIZE;

@ -1,6 +1,7 @@
import { MapLayerHandler, MapLayerOptions, SelectableValue } from '@grafana/data'; import { MapLayerHandler, MapLayerOptions, SelectableValue } from '@grafana/data';
import BaseLayer from 'ol/layer/Base'; import BaseLayer from 'ol/layer/Base';
import { Units } from 'ol/proj/Units'; import { Units } from 'ol/proj/Units';
import { StyleConfig } from './style/types';
import { MapCenterID } from './view'; import { MapCenterID } from './view';
export interface ControlsOptions { export interface ControlsOptions {
@ -45,10 +46,8 @@ export interface GeomapPanelOptions {
layers: MapLayerOptions[]; layers: MapLayerOptions[];
} }
export interface FeatureStyleConfig { export interface FeatureStyleConfig {
fillColor: string; //eventually be ColorDimensionConfig style?: StyleConfig;
opacity?: number; check?: FeatureRuleConfig;
strokeWidth?: number;
rule?: FeatureRuleConfig;
} }
export interface FeatureRuleConfig { export interface FeatureRuleConfig {
property: string; property: string;

@ -1,20 +0,0 @@
import { Style, Stroke, Fill } from 'ol/style';
import { FeatureStyleConfig } from '../types';
/**
* Gets a geomap style based on fill, stroke, and stroke width
* @returns ol style
*/
export const getGeoMapStyle = (config: FeatureStyleConfig, property: any) => {
return new Style({
fill: new Fill({
color: `${config.fillColor ?? '#1F60C4'}`,
}),
stroke: config?.strokeWidth
? new Stroke({
color: `${config.fillColor ?? '#1F60C4'}`,
width: config.strokeWidth,
})
: undefined,
});
};
Loading…
Cancel
Save