mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Geomap: use same style config for makers and geojson (#41846)
This commit is contained in:
parent
18cc552edb
commit
837e268395
@ -2,7 +2,7 @@ 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 { DEFAULT_STYLE_RULE } from '../layers/data/geojsonLayer';
|
||||
import { StyleRuleEditor, StyleRuleEditorSettings } from './StyleRuleEditor';
|
||||
|
||||
export const GeomapStyleRulesEditor: FC<StandardEditorProps<FeatureStyleConfig[], any, any>> = (props) => {
|
||||
@ -41,7 +41,7 @@ export const GeomapStyleRulesEditor: FC<StandardEditorProps<FeatureStyleConfig[]
|
||||
onChange={onRuleChange(idx)}
|
||||
context={context}
|
||||
item={itemSettings}
|
||||
key={`${idx}-${style.rule}`}
|
||||
key={`${idx}-${style.check?.property}`}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -1,9 +1,11 @@
|
||||
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 { Button, InlineField, InlineFieldRow, Input, Select, useStyles2 } from '@grafana/ui';
|
||||
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 {
|
||||
options: SelectableValue[];
|
||||
@ -12,7 +14,7 @@ export interface StyleRuleEditorSettings {
|
||||
export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, any, StyleRuleEditorSettings>> = (
|
||||
props
|
||||
) => {
|
||||
const { value, onChange, item } = props;
|
||||
const { value, onChange, item, context } = props;
|
||||
const settings: StyleRuleEditorSettings = item.settings;
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
@ -23,11 +25,11 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
onChange({
|
||||
...value,
|
||||
rule: {
|
||||
...value.rule,
|
||||
check: {
|
||||
...value.check,
|
||||
property: e.currentTarget.value,
|
||||
operation: value.rule?.operation ?? ComparisonOperation.EQ,
|
||||
value: value.rule?.value ?? '',
|
||||
operation: value.check?.operation ?? ComparisonOperation.EQ,
|
||||
value: value.check?.value ?? '',
|
||||
},
|
||||
});
|
||||
},
|
||||
@ -38,11 +40,11 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
|
||||
(selection: SelectableValue) => {
|
||||
onChange({
|
||||
...value,
|
||||
rule: {
|
||||
...value.rule,
|
||||
check: {
|
||||
...value.check,
|
||||
operation: selection.value ?? ComparisonOperation.EQ,
|
||||
property: value.rule?.property ?? '',
|
||||
value: value.rule?.value ?? '',
|
||||
property: value.check?.property ?? '',
|
||||
value: value.check?.value ?? '',
|
||||
},
|
||||
});
|
||||
},
|
||||
@ -53,27 +55,20 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
onChange({
|
||||
...value,
|
||||
rule: {
|
||||
...value.rule,
|
||||
check: {
|
||||
...value.check,
|
||||
value: e.currentTarget.value,
|
||||
operation: value.rule?.operation ?? ComparisonOperation.EQ,
|
||||
property: value.rule?.property ?? '',
|
||||
operation: value.check?.operation ?? ComparisonOperation.EQ,
|
||||
property: value.check?.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 });
|
||||
const onChangeStyle = useCallback(
|
||||
(style?: StyleConfig) => {
|
||||
onChange({ ...value, style });
|
||||
},
|
||||
[onChange, value]
|
||||
);
|
||||
@ -82,6 +77,8 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
|
||||
onChange(undefined);
|
||||
}, [onChange]);
|
||||
|
||||
const check = value.check ?? DEFAULT_STYLE_RULE.check!;
|
||||
|
||||
return (
|
||||
<div className={styles.rule}>
|
||||
<InlineFieldRow className={styles.row}>
|
||||
@ -89,7 +86,7 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={'Feature property'}
|
||||
value={`${value?.rule?.property}`}
|
||||
value={check.property ?? ''}
|
||||
onChange={onChangeComparisonProperty}
|
||||
aria-label={'Feature property'}
|
||||
/>
|
||||
@ -97,7 +94,7 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
|
||||
<InlineField className={styles.inline} grow={true}>
|
||||
<Select
|
||||
menuShouldPortal
|
||||
value={`${value?.rule?.operation}` ?? ComparisonOperation.EQ}
|
||||
value={check.operation ?? ComparisonOperation.EQ}
|
||||
options={settings.options}
|
||||
onChange={onChangeComparison}
|
||||
aria-label={'Comparison operator'}
|
||||
@ -107,26 +104,11 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={'value'}
|
||||
value={`${value?.rule?.value}`}
|
||||
value={`${check.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"
|
||||
@ -136,6 +118,20 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
|
||||
className={styles.button}
|
||||
></Button>
|
||||
</InlineFieldRow>
|
||||
<div>
|
||||
<StyleEditor
|
||||
value={value.style ?? defaultStyleConfig}
|
||||
context={context}
|
||||
onChange={onChangeStyle}
|
||||
item={
|
||||
{
|
||||
settings: {
|
||||
simpleFixedValues: true,
|
||||
},
|
||||
} as any
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -152,11 +148,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
margin-bottom: 0;
|
||||
margin-left: 4px;
|
||||
`,
|
||||
color: css`
|
||||
align-items: center;
|
||||
margin-bottom: 0;
|
||||
margin-right: 4px;
|
||||
`,
|
||||
button: css`
|
||||
margin-left: 4px;
|
||||
`,
|
||||
|
@ -1,6 +1,16 @@
|
||||
import React, { FC } from 'react';
|
||||
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 {
|
||||
ColorDimensionEditor,
|
||||
@ -17,8 +27,18 @@ import {
|
||||
defaultTextConfig,
|
||||
} from 'app/features/dimensions/types';
|
||||
import { defaultStyleConfig, StyleConfig, TextAlignment, TextBaseline } from '../../style/types';
|
||||
import { styleUsesText } from '../../style/utils';
|
||||
|
||||
export const StyleEditor: FC<StandardEditorProps<StyleConfig, any, any>> = ({ value, context, onChange }) => {
|
||||
export interface StyleEditorOptions {
|
||||
simpleFixedValues?: boolean;
|
||||
}
|
||||
|
||||
export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions, any>> = ({
|
||||
value,
|
||||
context,
|
||||
onChange,
|
||||
item,
|
||||
}) => {
|
||||
const onSizeChange = (sizeValue: ScaleDimensionConfig | undefined) => {
|
||||
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 } });
|
||||
};
|
||||
|
||||
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 (
|
||||
<>
|
||||
@ -128,7 +186,8 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, any, any>> = ({ va
|
||||
item={{} as any}
|
||||
/>
|
||||
</Field>
|
||||
{(value.text?.fixed || value.text?.field) && (
|
||||
|
||||
{hasTextLabel && (
|
||||
<>
|
||||
<HorizontalGroup>
|
||||
<Field label={'Font size'}>
|
||||
|
@ -4,44 +4,53 @@ import VectorLayer from 'ol/layer/Vector';
|
||||
import VectorSource from 'ol/source/Vector';
|
||||
import GeoJSON from 'ol/format/GeoJSON';
|
||||
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 { ComparisonOperation, FeatureStyleConfig } from '../../types';
|
||||
import { Stroke, Style } from 'ol/style';
|
||||
import { ComparisonOperation, FeatureRuleConfig, FeatureStyleConfig } from '../../types';
|
||||
import { Style } from 'ol/style';
|
||||
import { FeatureLike } from 'ol/Feature';
|
||||
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 {
|
||||
// URL for a geojson file
|
||||
src?: string;
|
||||
|
||||
// Styles that can be applied
|
||||
styles: FeatureStyleConfig[];
|
||||
// Pick style based on a rule
|
||||
rules: FeatureStyleConfig[];
|
||||
|
||||
// The default style (applied if no rules match)
|
||||
style: StyleConfig;
|
||||
}
|
||||
|
||||
const defaultOptions: GeoJSONMapperConfig = {
|
||||
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 = {
|
||||
fillColor: '#1F60C4',
|
||||
strokeWidth: 1,
|
||||
rule: {
|
||||
style: defaultStyleConfig,
|
||||
check: {
|
||||
property: '',
|
||||
operation: ComparisonOperation.EQ,
|
||||
value: '',
|
||||
},
|
||||
};
|
||||
|
||||
export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = {
|
||||
id: 'geojson-value-mapper',
|
||||
name: 'Map values to GeoJSON file',
|
||||
description: 'color features based on query results',
|
||||
export const geojsonLayer: MapLayerRegistryItem<GeoJSONMapperConfig> = {
|
||||
id: 'geojson',
|
||||
name: 'GeoJSON',
|
||||
description: 'Load static data from a geojson file',
|
||||
isBaseMap: false,
|
||||
state: PluginState.alpha,
|
||||
state: PluginState.beta,
|
||||
|
||||
/**
|
||||
* Function that configures transformation and returns a transformer
|
||||
@ -68,30 +77,39 @@ export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = {
|
||||
}
|
||||
});
|
||||
|
||||
const defaultStyle = new Style({
|
||||
stroke: new Stroke({
|
||||
color: DEFAULT_STYLE_RULE.fillColor,
|
||||
width: DEFAULT_STYLE_RULE.strokeWidth,
|
||||
}),
|
||||
});
|
||||
const styles: StyleCheckerState[] = [];
|
||||
if (config.rules) {
|
||||
for (const r of config.rules) {
|
||||
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({
|
||||
source,
|
||||
style: (feature: FeatureLike) => {
|
||||
const type = feature.getGeometry()?.getType();
|
||||
if (type === 'Point') {
|
||||
return circleMarker({color:DEFAULT_STYLE_RULE.fillColor});
|
||||
}
|
||||
const isPoint = feature.getGeometry()?.getType() === 'Point';
|
||||
|
||||
if (feature && config?.styles?.length) {
|
||||
for (const style of config.styles) {
|
||||
//check if there is no style rule or if the rule matches feature property
|
||||
if (!style.rule || checkFeatureMatchesStyleRule(style.rule, feature as Feature<Geometry>)) {
|
||||
return getGeoMapStyle(style, feature);
|
||||
}
|
||||
for (const check of styles) {
|
||||
if (check.rule && !checkFeatureMatchesStyleRule(check.rule, feature)) {
|
||||
continue;
|
||||
}
|
||||
return isPoint ? check.point : check.poly;
|
||||
}
|
||||
return defaultStyle;
|
||||
return undefined; // unreachable
|
||||
},
|
||||
});
|
||||
|
||||
@ -126,12 +144,24 @@ export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = {
|
||||
defaultValue: defaultOptions.src,
|
||||
})
|
||||
.addCustomEditor({
|
||||
id: 'config.styles',
|
||||
path: 'config.styles',
|
||||
id: 'config.rules',
|
||||
path: 'config.rules',
|
||||
name: 'Style Rules',
|
||||
description: 'Apply styles based on feature properties',
|
||||
editor: GeomapStyleRulesEditor,
|
||||
settings: {},
|
||||
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 { geojsonMapper } from './geojsonMapper';
|
||||
import { geojsonLayer } from './geojsonLayer';
|
||||
import { heatmapLayer } from './heatMap';
|
||||
import { lastPointTracker } from './lastPointTracker';
|
||||
|
||||
@ -10,5 +10,5 @@ export const dataLayers = [
|
||||
markersLayer,
|
||||
heatmapLayer,
|
||||
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
|
||||
const errorMarker = (cfg: StyleConfigValues) => {
|
||||
const radius = cfg.size ?? DEFAULT_SIZE;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { MapLayerHandler, MapLayerOptions, SelectableValue } from '@grafana/data';
|
||||
import BaseLayer from 'ol/layer/Base';
|
||||
import { Units } from 'ol/proj/Units';
|
||||
import { StyleConfig } from './style/types';
|
||||
import { MapCenterID } from './view';
|
||||
|
||||
export interface ControlsOptions {
|
||||
@ -45,10 +46,8 @@ export interface GeomapPanelOptions {
|
||||
layers: MapLayerOptions[];
|
||||
}
|
||||
export interface FeatureStyleConfig {
|
||||
fillColor: string; //eventually be ColorDimensionConfig
|
||||
opacity?: number;
|
||||
strokeWidth?: number;
|
||||
rule?: FeatureRuleConfig;
|
||||
style?: StyleConfig;
|
||||
check?: FeatureRuleConfig;
|
||||
}
|
||||
export interface FeatureRuleConfig {
|
||||
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…
Reference in New Issue
Block a user