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. 42
      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'; } from 'app/features/dimensions/editors';
import { ResourceFolderName, defaultTextConfig, MediaType } from 'app/features/dimensions/types'; 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 { styleUsesText } from '../style/utils';
import { LayerContentInfo } from '../utils/getFeatures'; import { LayerContentInfo } from '../utils/getFeatures';
@ -101,6 +109,14 @@ export const StyleEditor = (props: Props) => {
onChange({ ...value, textConfig: { ...value.textConfig, textBaseline: textBaseline } }); 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 propertyOptions = useObservable(settings?.layerInfo ?? of());
const featuresHavePoints = propertyOptions?.geometryType === GeometryTypeId.Point; const featuresHavePoints = propertyOptions?.geometryType === GeometryTypeId.Point;
const hasTextLabel = styleUsesText(value); const hasTextLabel = styleUsesText(value);
@ -200,6 +216,7 @@ export const StyleEditor = (props: Props) => {
/> />
</Field> </Field>
{!settings?.hideSymbol && ( {!settings?.hideSymbol && (
<>
<Field label={'Symbol'}> <Field label={'Symbol'}>
<ResourceDimensionEditor <ResourceDimensionEditor
value={value?.symbol ?? defaultStyleConfig.symbol} value={value?.symbol ?? defaultStyleConfig.symbol}
@ -218,6 +235,29 @@ export const StyleEditor = (props: Props) => {
} }
/> />
</Field> </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'}> <Field label={'Color'}>
<ColorDimensionEditor <ColorDimensionEditor

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

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

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

@ -1,7 +1,7 @@
import { ResourceDimensionMode } from '@grafana/schema'; import { ResourceDimensionMode } from '@grafana/schema';
import { StyleConfig } from './types'; import { HorizontalAlign, VerticalAlign, StyleConfig, SymbolAlign } from './types';
import { getStyleConfigState } from './utils'; import { getDisplacement, getStyleConfigState } from './utils';
describe('style utils', () => { describe('style utils', () => {
it('should fill in default values', async () => { it('should fill in default values', async () => {
@ -41,6 +41,10 @@ describe('style utils', () => {
"opacity": 0.4, "opacity": 0.4,
"rotation": 0, "rotation": 0,
"size": 5, "size": 5,
"symbolAlign": {
"horizontal": "center",
"vertical": "center",
},
}, },
"config": null, "config": null,
"fields": { "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 { TextDimensionMode } from '@grafana/schema';
import { getMarkerMaker } from './markers'; 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 */ /** Indicate if the style wants to show text values */
export function styleUsesText(config: StyleConfig): boolean { export function styleUsesText(config: StyleConfig): boolean {
@ -37,6 +45,7 @@ export async function getStyleConfigState(cfg?: StyleConfig): Promise<StyleConfi
lineWidth: cfg.lineWidth ?? 1, lineWidth: cfg.lineWidth ?? 1,
size: cfg.size?.fixed ?? defaultStyleConfig.size.fixed, size: cfg.size?.fixed ?? defaultStyleConfig.size.fixed,
rotation: cfg.rotation?.fixed ?? defaultStyleConfig.rotation.fixed, // add ability follow path later rotation: cfg.rotation?.fixed ?? defaultStyleConfig.rotation.fixed, // add ability follow path later
symbolAlign: cfg.symbolAlign ?? defaultStyleConfig.symbolAlign,
}, },
maker, maker,
}; };
@ -66,3 +75,19 @@ export async function getStyleConfigState(cfg?: StyleConfig): Promise<StyleConfi
} }
return state; 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