Geomap: Add Symbol Alignment Options (#74293)

* Geomap: Add Symbol Anchor Options

* Update displacement for svg case

* For square and x, account for 45 degree offset

* Simplify displacement function

* Remove unused todo

* Update test defaults to include anchor

* Add missing anchor default to tests

* Move displacement function into utils and add test

* Simplify anchor position options UX

* Change verbage to alignment and swap direction

* Include missing alignment rename

* Update tests

---------

Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
pull/78145/head
Drew Slobodnjak 2 years ago committed by GitHub
parent 0ea047cc09
commit 03baf210b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 78
      public/app/plugins/panel/geomap/editor/StyleEditor.tsx
  2. 8
      public/app/plugins/panel/geomap/migrations.test.ts
  3. 17
      public/app/plugins/panel/geomap/style/markers.ts
  4. 21
      public/app/plugins/panel/geomap/style/types.ts
  5. 26
      public/app/plugins/panel/geomap/style/utils.test.ts
  6. 27
      public/app/plugins/panel/geomap/style/utils.ts

@ -31,7 +31,15 @@ import {
} from 'app/features/dimensions/editors';
import { ResourceFolderName, defaultTextConfig, MediaType } from 'app/features/dimensions/types';
import { defaultStyleConfig, GeometryTypeId, StyleConfig, TextAlignment, TextBaseline } from '../style/types';
import {
HorizontalAlign,
VerticalAlign,
defaultStyleConfig,
GeometryTypeId,
StyleConfig,
TextAlignment,
TextBaseline,
} from '../style/types';
import { styleUsesText } from '../style/utils';
import { LayerContentInfo } from '../utils/getFeatures';
@ -101,6 +109,14 @@ export const StyleEditor = (props: Props) => {
onChange({ ...value, textConfig: { ...value.textConfig, textBaseline: textBaseline } });
};
const onAlignHorizontalChange = (alignHorizontal: HorizontalAlign) => {
onChange({ ...value, symbolAlign: { ...value.symbolAlign, horizontal: alignHorizontal } });
};
const onAlignVerticalChange = (alignVertical: VerticalAlign) => {
onChange({ ...value, symbolAlign: { ...value.symbolAlign, vertical: alignVertical } });
};
const propertyOptions = useObservable(settings?.layerInfo ?? of());
const featuresHavePoints = propertyOptions?.geometryType === GeometryTypeId.Point;
const hasTextLabel = styleUsesText(value);
@ -200,24 +216,48 @@ export const StyleEditor = (props: Props) => {
/>
</Field>
{!settings?.hideSymbol && (
<Field label={'Symbol'}>
<ResourceDimensionEditor
value={value?.symbol ?? defaultStyleConfig.symbol}
context={context}
onChange={onSymbolChange}
item={
{
settings: {
resourceType: MediaType.Icon,
folderName: ResourceFolderName.Marker,
placeholderText: hasTextLabel ? 'Select a symbol' : 'Select a symbol or add a text label',
placeholderValue: defaultStyleConfig.symbol.fixed,
showSourceRadio: false,
},
} as StandardEditorsRegistryItem
}
/>
</Field>
<>
<Field label={'Symbol'}>
<ResourceDimensionEditor
value={value?.symbol ?? defaultStyleConfig.symbol}
context={context}
onChange={onSymbolChange}
item={
{
settings: {
resourceType: MediaType.Icon,
folderName: ResourceFolderName.Marker,
placeholderText: hasTextLabel ? 'Select a symbol' : 'Select a symbol or add a text label',
placeholderValue: defaultStyleConfig.symbol.fixed,
showSourceRadio: false,
},
} as StandardEditorsRegistryItem
}
/>
</Field>
<Field label={'Symbol Vertical Align'}>
<RadioButtonGroup
value={value?.symbolAlign?.vertical ?? defaultStyleConfig.symbolAlign.vertical}
onChange={onAlignVerticalChange}
options={[
{ value: VerticalAlign.Top, label: capitalize(VerticalAlign.Top) },
{ value: VerticalAlign.Center, label: capitalize(VerticalAlign.Center) },
{ value: VerticalAlign.Bottom, label: capitalize(VerticalAlign.Bottom) },
]}
/>
</Field>
<Field label={'Symbol Horizontal Align'}>
<RadioButtonGroup
value={value?.symbolAlign?.horizontal ?? defaultStyleConfig.symbolAlign.horizontal}
onChange={onAlignHorizontalChange}
options={[
{ value: HorizontalAlign.Left, label: capitalize(HorizontalAlign.Left) },
{ value: HorizontalAlign.Center, label: capitalize(HorizontalAlign.Center) },
{ value: HorizontalAlign.Right, label: capitalize(HorizontalAlign.Right) },
]}
/>
</Field>
</>
)}
<Field label={'Color'}>
<ColorDimensionEditor

@ -79,6 +79,10 @@ describe('Worldmap Migrations', () => {
"fixed": "img/icons/marker/circle.svg",
"mode": "fixed",
},
"symbolAlign": {
"horizontal": "center",
"vertical": "center",
},
"textConfig": {
"fontSize": 12,
"offsetX": 0,
@ -222,6 +226,10 @@ describe('geomap migrations', () => {
"fixed": "img/icons/marker/triangle.svg",
"mode": "fixed",
},
"symbolAlign": {
"horizontal": "center",
"vertical": "center",
},
"textConfig": {
"fontSize": 12,
"offsetX": 0,

@ -6,6 +6,7 @@ import { config } from '@grafana/runtime';
import { getPublicOrAbsoluteUrl } from 'app/features/dimensions';
import { defaultStyleConfig, DEFAULT_SIZE, StyleConfigValues, StyleMaker } from './types';
import { getDisplacement } from './utils';
interface SymbolMaker extends RegistryItem {
aliasIds: string[];
@ -80,11 +81,13 @@ export const textMarker = (cfg: StyleConfigValues) => {
export const circleMarker = (cfg: StyleConfigValues) => {
const stroke = new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 });
const radius = cfg.size ?? DEFAULT_SIZE;
return new Style({
image: new Circle({
stroke,
fill: getFillColor(cfg),
radius: cfg.size ?? DEFAULT_SIZE,
radius,
displacement: getDisplacement(cfg.symbolAlign ?? defaultStyleConfig.symbolAlign, radius),
}),
text: textLabel(cfg),
stroke, // in case lines are sent to the markers layer
@ -153,7 +156,9 @@ const makers: SymbolMaker[] = [
fill: getFillColor(cfg),
points: 4,
radius,
rotation: (rotation * Math.PI) / 180 + Math.PI / 4,
angle: Math.PI / 4,
rotation: (rotation * Math.PI) / 180,
displacement: getDisplacement(cfg.symbolAlign ?? defaultStyleConfig.symbolAlign, radius),
}),
text: textLabel(cfg),
});
@ -174,6 +179,7 @@ const makers: SymbolMaker[] = [
radius,
rotation: (rotation * Math.PI) / 180,
angle: 0,
displacement: getDisplacement(cfg.symbolAlign ?? defaultStyleConfig.symbolAlign, radius),
}),
text: textLabel(cfg),
});
@ -195,6 +201,7 @@ const makers: SymbolMaker[] = [
radius2: radius * 0.4,
angle: 0,
rotation: (rotation * Math.PI) / 180,
displacement: getDisplacement(cfg.symbolAlign ?? defaultStyleConfig.symbolAlign, radius),
}),
text: textLabel(cfg),
});
@ -215,6 +222,7 @@ const makers: SymbolMaker[] = [
radius2: 0,
angle: 0,
rotation: (rotation * Math.PI) / 180,
displacement: getDisplacement(cfg.symbolAlign ?? defaultStyleConfig.symbolAlign, radius),
}),
text: textLabel(cfg),
});
@ -233,7 +241,9 @@ const makers: SymbolMaker[] = [
points: 4,
radius,
radius2: 0,
rotation: (rotation * Math.PI) / 180 + Math.PI / 4,
angle: Math.PI / 4,
rotation: (rotation * Math.PI) / 180,
displacement: getDisplacement(cfg.symbolAlign ?? defaultStyleConfig.symbolAlign, radius),
}),
text: textLabel(cfg),
});
@ -315,6 +325,7 @@ export async function getMarkerMaker(symbol?: string, hasTextLabel?: boolean): P
opacity: cfg.opacity ?? 1,
scale: (DEFAULT_SIZE + radius) / 100,
rotation: (rotation * Math.PI) / 180,
displacement: getDisplacement(cfg.symbolAlign ?? defaultStyleConfig.symbolAlign, radius / 2),
}),
text: !cfg?.text ? undefined : textLabel(cfg),
}),

@ -29,6 +29,7 @@ export interface StyleConfig {
// Used for points and dynamic text
size?: ScaleDimensionConfig;
symbol?: ResourceDimensionConfig;
symbolAlign?: SymbolAlign;
// Can show markers and text together!
text?: TextDimensionConfig;
@ -50,6 +51,16 @@ export enum TextBaseline {
Middle = 'middle',
Bottom = 'bottom',
}
export enum HorizontalAlign {
Left = 'left',
Center = 'center',
Right = 'right',
}
export enum VerticalAlign {
Top = 'top',
Center = 'center',
Bottom = 'bottom',
}
export const defaultStyleConfig = Object.freeze({
size: {
@ -65,6 +76,10 @@ export const defaultStyleConfig = Object.freeze({
mode: ResourceDimensionMode.Fixed,
fixed: 'img/icons/marker/circle.svg',
},
symbolAlign: {
horizontal: HorizontalAlign.Center,
vertical: VerticalAlign.Center,
},
textConfig: {
fontSize: 12,
textAlign: TextAlignment.Center,
@ -80,6 +95,11 @@ export const defaultStyleConfig = Object.freeze({
},
});
export interface SymbolAlign {
horizontal?: HorizontalAlign;
vertical?: VerticalAlign;
}
/**
* Static options for text display. See:
* https://openlayers.org/en/latest/apidoc/module-ol_style_Text.html
@ -99,6 +119,7 @@ export interface StyleConfigValues {
lineWidth?: number;
size?: number;
symbol?: string; // the point symbol
symbolAlign?: SymbolAlign;
rotation?: number;
text?: string;

@ -1,7 +1,7 @@
import { ResourceDimensionMode } from '@grafana/schema';
import { StyleConfig } from './types';
import { getStyleConfigState } from './utils';
import { HorizontalAlign, VerticalAlign, StyleConfig, SymbolAlign } from './types';
import { getDisplacement, getStyleConfigState } from './utils';
describe('style utils', () => {
it('should fill in default values', async () => {
@ -41,6 +41,10 @@ describe('style utils', () => {
"opacity": 0.4,
"rotation": 0,
"size": 5,
"symbolAlign": {
"horizontal": "center",
"vertical": "center",
},
},
"config": null,
"fields": {
@ -52,4 +56,22 @@ describe('style utils', () => {
}
`);
});
it('should return correct displacement array for top left', async () => {
const symbolAlign: SymbolAlign = { horizontal: HorizontalAlign.Left, vertical: VerticalAlign.Top };
const radius = 10;
const displacement = getDisplacement(symbolAlign, radius);
expect(displacement).toEqual([-10, 10]);
});
it('should return correct displacement array for bottom right', async () => {
const symbolAlign: SymbolAlign = { horizontal: HorizontalAlign.Right, vertical: VerticalAlign.Bottom };
const radius = 10;
const displacement = getDisplacement(symbolAlign, radius);
expect(displacement).toEqual([10, -10]);
});
it('should return correct displacement array for center center', async () => {
const symbolAlign: SymbolAlign = { horizontal: HorizontalAlign.Center, vertical: VerticalAlign.Center };
const radius = 10;
const displacement = getDisplacement(symbolAlign, radius);
expect(displacement).toEqual([0, 0]);
});
});

@ -2,7 +2,15 @@ import { config } from '@grafana/runtime';
import { TextDimensionMode } from '@grafana/schema';
import { getMarkerMaker } from './markers';
import { defaultStyleConfig, StyleConfig, StyleConfigFields, StyleConfigState } from './types';
import {
HorizontalAlign,
VerticalAlign,
defaultStyleConfig,
StyleConfig,
StyleConfigFields,
StyleConfigState,
SymbolAlign,
} from './types';
/** Indicate if the style wants to show text values */
export function styleUsesText(config: StyleConfig): boolean {
@ -37,6 +45,7 @@ export async function getStyleConfigState(cfg?: StyleConfig): Promise<StyleConfi
lineWidth: cfg.lineWidth ?? 1,
size: cfg.size?.fixed ?? defaultStyleConfig.size.fixed,
rotation: cfg.rotation?.fixed ?? defaultStyleConfig.rotation.fixed, // add ability follow path later
symbolAlign: cfg.symbolAlign ?? defaultStyleConfig.symbolAlign,
},
maker,
};
@ -66,3 +75,19 @@ export async function getStyleConfigState(cfg?: StyleConfig): Promise<StyleConfi
}
return state;
}
/** Return a displacment array depending on alignment and icon radius */
export function getDisplacement(symbolAlign: SymbolAlign, radius: number) {
const displacement = [0, 0];
if (symbolAlign?.horizontal === HorizontalAlign.Left) {
displacement[0] = -radius;
} else if (symbolAlign?.horizontal === HorizontalAlign.Right) {
displacement[0] = radius;
}
if (symbolAlign?.vertical === VerticalAlign.Top) {
displacement[1] = radius;
} else if (symbolAlign?.vertical === VerticalAlign.Bottom) {
displacement[1] = -radius;
}
return displacement;
}

Loading…
Cancel
Save