Geomap: Combine Text layer with Markers Layer and add text options (#41768)

* add text to markers

* add textConfig

* remove separate text layer

* update test

* Update public/app/plugins/panel/geomap/style/markers.ts

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>

* Update public/app/plugins/panel/geomap/style/markers.ts

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>

* update textConfig naming

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
nikki-kiga 2021-11-16 13:22:57 -08:00 committed by GitHub
parent 88aec85e3d
commit 306f0785e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 108 additions and 172 deletions

View File

@ -2,7 +2,6 @@ import { markersLayer } from './markersLayer';
import { geojsonMapper } from './geojsonMapper';
import { heatmapLayer } from './heatMap';
import { lastPointTracker } from './lastPointTracker';
import { textLabelsLayer } from './textLabelsLayer';
/**
* Registry for layer handlers
@ -12,5 +11,4 @@ export const dataLayers = [
heatmapLayer,
lastPointTracker,
geojsonMapper, // dummy for now
textLabelsLayer,
];

View File

@ -16,14 +16,15 @@ import {
getScaledDimension,
getColorDimension,
ResourceFolderName,
getTextDimension,
} from 'app/features/dimensions';
import { ScaleDimensionEditor, ColorDimensionEditor, ResourceDimensionEditor } from 'app/features/dimensions/editors';
import { ScaleDimensionEditor, ColorDimensionEditor, ResourceDimensionEditor, TextDimensionEditor } from 'app/features/dimensions/editors';
import { ObservablePropsWrapper } from '../../components/ObservablePropsWrapper';
import { MarkersLegend, MarkersLegendProps } from './MarkersLegend';
import { ReplaySubject } from 'rxjs';
import { FeaturesStylesBuilderConfig, getFeatures } from '../../utils/getFeatures';
import { getMarkerMaker } from '../../style/markers';
import { defaultStyleConfig, StyleConfig } from '../../style/types';
import { defaultStyleConfig, StyleConfig, TextAlignment, TextBaseline } from '../../style/types';
// Configuration options for Circle overlays
export interface MarkersConfig {
@ -99,11 +100,14 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
const colorDim = getColorDimension(frame, style.color ?? defaultStyleConfig.color, theme);
const sizeDim = getScaledDimension(frame, style.size ?? defaultStyleConfig.size);
const textDim = style?.text && getTextDimension(frame, style.text);
const opacity = style?.opacity ?? defaultStyleConfig.opacity;
const featureDimensionConfig: FeaturesStylesBuilderConfig = {
colorDim: colorDim,
sizeDim: sizeDim,
textDim: textDim,
textConfig: style?.textConfig,
opacity: opacity,
styleMaker: markerMaker,
};
@ -173,6 +177,55 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
step: 0.1,
},
})
.addCustomEditor({
id: 'config.style.text',
path: 'config.style.text',
name: 'Text label',
editor: TextDimensionEditor,
defaultValue: defaultOptions.style.text,
})
.addNumberInput({
name: 'Text font size',
path: 'config.style.textConfig.fontSize',
defaultValue: defaultOptions.style.textConfig?.fontSize,
})
.addNumberInput({
name: 'Text X offset',
path: 'config.style.textConfig.offsetX',
defaultValue: 0,
})
.addNumberInput({
name: 'Text Y offset',
path: 'config.style.textConfig.offsetY',
defaultValue: 0,
})
.addRadio({
name: 'Text align',
path: 'config.style.textConfig.textAlign',
description: '',
defaultValue: defaultOptions.style.textConfig?.textAlign,
settings: {
options: [
{ value: TextAlignment.Left, label: TextAlignment.Left },
{ value: TextAlignment.Center, label: TextAlignment.Center },
{ value: TextAlignment.Right, label: TextAlignment.Right },
],
},
})
.addRadio({
name: 'Text baseline',
path: 'config.style.textConfig.textBaseline',
description: '',
defaultValue: defaultOptions.style.textConfig?.textBaseline,
settings: {
options: [
{ value: TextBaseline.Top, label: TextBaseline.Top },
{ value: TextBaseline.Middle, label: TextBaseline.Middle },
{ value: TextBaseline.Bottom, label: TextBaseline.Bottom },
],
},
})
// baseline?: 'bottom' | 'top' | 'middle';
.addBooleanSwitch({
path: 'config.showLegend',
name: 'Show legend',

View File

@ -1,146 +0,0 @@
import { GrafanaTheme2, MapLayerOptions, MapLayerRegistryItem, PanelData, PluginState } from '@grafana/data';
import Map from 'ol/Map';
import * as layer from 'ol/layer';
import * as source from 'ol/source';
import { dataFrameToPoints, getLocationMatchers } from '../../utils/location';
import {
getColorDimension,
getScaledDimension,
getTextDimension,
TextDimensionMode,
} from 'app/features/dimensions';
import { ColorDimensionEditor, ScaleDimensionEditor, TextDimensionEditor } from 'app/features/dimensions/editors';
import { FeaturesStylesBuilderConfig, getFeatures } from '../../utils/getFeatures';
import { Feature } from 'ol';
import { Point } from 'ol/geom';
import { textMarkerMaker } from '../../style/text';
import { MarkersConfig } from './markersLayer';
export const TEXT_LABELS_LAYER = 'text-labels';
// Same configuration
type TextLabelsConfig = MarkersConfig;
const defaultOptions = {
style: {
text: {
fixed: '',
mode: TextDimensionMode.Field,
},
color: {
fixed: 'dark-blue',
},
opacity: 1,
size: {
fixed: 10,
min: 5,
max: 100,
},
},
showLegend: false,
};
export const textLabelsLayer: MapLayerRegistryItem<TextLabelsConfig> = {
id: TEXT_LABELS_LAYER,
name: 'Text labels',
description: 'render text labels',
isBaseMap: false,
state: PluginState.alpha,
showLocation: true,
create: async (map: Map, options: MapLayerOptions<TextLabelsConfig>, theme: GrafanaTheme2) => {
const matchers = await getLocationMatchers(options.location);
const vectorLayer = new layer.Vector({});
const config = {
...defaultOptions,
...options?.config,
};
return {
init: () => vectorLayer,
update: (data: PanelData) => {
if (!data.series?.length) {
return;
}
const features: Feature<Point>[] = [];
const style = config.style ?? defaultOptions.style;
for (const frame of data.series) {
const info = dataFrameToPoints(frame, matchers);
if (info.warning) {
console.log('Could not find locations', info.warning);
return;
}
const colorDim = getColorDimension(frame, style.color ?? defaultOptions.style.color, theme);
const sizeDim = getScaledDimension(frame, style.size ?? defaultOptions.style.size);
const opacity = style?.opacity ?? defaultOptions.style.opacity;
const textDim = getTextDimension(frame, style.text ?? defaultOptions.style.text );
const featureDimensionConfig: FeaturesStylesBuilderConfig = {
colorDim: colorDim,
sizeDim: sizeDim,
textDim: textDim,
opacity: opacity,
styleMaker: textMarkerMaker,
};
const frameFeatures = getFeatures(frame, info, featureDimensionConfig);
if (frameFeatures) {
features.push(...frameFeatures);
}
}
// Source reads the data and provides a set of features to visualize
const vectorSource = new source.Vector({ features });
vectorLayer.setSource(vectorSource);
},
registerOptionsUI: (builder) => {
builder
.addCustomEditor({
id: 'config.style.text',
path: 'config.style.text',
name: 'Text label',
editor: TextDimensionEditor,
defaultValue: defaultOptions.style.text,
})
.addCustomEditor({
id: 'config.style.color',
path: 'config.style.color',
name: 'Text color',
editor: ColorDimensionEditor,
defaultValue: defaultOptions.style.color,
settings: {},
})
.addSliderInput({
path: 'config.style.opacity',
name: 'Text opacity',
defaultValue: defaultOptions.style.opacity,
settings: {
min: 0,
max: 1,
step: 0.1,
},
})
.addCustomEditor({
id: 'config.style.size',
path: 'config.style.size',
name: 'Text size',
editor: ScaleDimensionEditor,
defaultValue: defaultOptions.style.size,
settings: {
min: 2,
max: 50,
},
});
},
};
},
defaultOptions,
};

View File

@ -160,6 +160,11 @@ describe('geomap migrations', () => {
"fixed": "img/icons/marker/triangle.svg",
"mode": "fixed",
},
"textConfig": Object {
"fontSize": 12,
"textAlign": "center",
"textBaseline": "middle",
},
},
},
"type": "markers",

View File

@ -1,8 +1,9 @@
import { Fill, RegularShape, Stroke, Circle, Style, Icon } from 'ol/style';
import { Fill, RegularShape, Stroke, Circle, Style, Icon, Text } from 'ol/style';
import { Registry, RegistryItem } from '@grafana/data';
import { DEFAULT_SIZE, StyleConfigValues, StyleMaker } from './types';
import { defaultStyleConfig, DEFAULT_SIZE, StyleConfigValues, StyleMaker } from './types';
import { getPublicOrAbsoluteUrl } from 'app/features/dimensions';
import tinycolor from 'tinycolor2';
import { config } from '@grafana/runtime';
interface SymbolMaker extends RegistryItem {
aliasIds: string[];
@ -39,6 +40,20 @@ export function getFillColor(cfg: StyleConfigValues) {
return undefined;
}
const textLabel = (cfg: StyleConfigValues) => {
const fontFamily = config.theme2.typography.fontFamily;
const textConfig = {
...defaultStyleConfig.textConfig,
...cfg.textConfig,
};
return new Text({
text: cfg.text ?? '?',
fill: new Fill({ color: cfg.color ?? defaultStyleConfig.color.fixed }),
font: `normal ${textConfig.fontSize}px ${fontFamily}`,
...textConfig,
});
};
export const circleMarker = (cfg: StyleConfigValues) => {
return new Style({
image: new Circle({
@ -46,6 +61,7 @@ export const circleMarker = (cfg: StyleConfigValues) => {
fill: getFillColor(cfg),
radius: cfg.size ?? DEFAULT_SIZE,
}),
text: textLabel(cfg),
});
};
@ -95,6 +111,7 @@ const makers: SymbolMaker[] = [
radius,
angle: Math.PI / 4,
}),
text: textLabel(cfg),
});
},
},
@ -113,6 +130,7 @@ const makers: SymbolMaker[] = [
rotation: Math.PI / 4,
angle: 0,
}),
text: textLabel(cfg),
});
},
},
@ -131,6 +149,7 @@ const makers: SymbolMaker[] = [
radius2: radius * 0.4,
angle: 0,
}),
text: textLabel(cfg),
});
},
},
@ -148,6 +167,7 @@ const makers: SymbolMaker[] = [
radius2: 0,
angle: 0,
}),
text: textLabel(cfg),
});
},
},
@ -165,6 +185,7 @@ const makers: SymbolMaker[] = [
radius2: 0,
angle: Math.PI / 4,
}),
text: textLabel(cfg),
});
},
},
@ -234,6 +255,7 @@ export async function getMarkerMaker(symbol?: string): Promise<StyleMaker> {
opacity: cfg.opacity ?? 1,
scale: (DEFAULT_SIZE + radius) / 100,
}),
text: !cfg?.text ? undefined : textLabel(cfg),
}),
// transparent bounding box for featureAtPixel detection
new Style({

View File

@ -1,16 +0,0 @@
import { Style, Text } from 'ol/style';
import { config } from '@grafana/runtime';
import { StyleConfigValues, StyleMaker } from './types';
import { getFillColor } from './markers';
export const textMarkerMaker: StyleMaker = (cfg: StyleConfigValues) => {
const fontFamily = config.theme2.typography.fontFamily;
const fontSize = cfg.size ?? 12;
return new Style({
text: new Text({
text: cfg.text ?? '?',
fill: getFillColor(cfg),
font: `normal ${fontSize}px ${fontFamily}`,
}),
});
};

View File

@ -33,6 +33,17 @@ export interface StyleConfig {
export const DEFAULT_SIZE = 5;
export enum TextAlignment {
Left = 'left',
Center = 'center',
Right = 'right',
}
export enum TextBaseline {
Top = 'top',
Middle = 'middle',
Bottom = 'bottom',
}
export const defaultStyleConfig = Object.freeze({
size: {
fixed: DEFAULT_SIZE,
@ -47,6 +58,11 @@ export const defaultStyleConfig = Object.freeze({
mode: ResourceDimensionMode.Fixed,
fixed: 'img/icons/marker/circle.svg',
},
textConfig: {
fontSize: 12,
textAlign: TextAlignment.Center,
textBaseline: TextBaseline.Middle,
},
});
/**
@ -57,8 +73,8 @@ export interface TextStyleConfig {
fontSize?: number;
offsetX?: number;
offsetY?: number;
align?: 'left' | 'right' | 'center';
baseline?: 'bottom' | 'top' | 'middle';
textAlign?: TextAlignment;
textBaseline?: TextBaseline;
}
// Applying the config to real data gives the values

View File

@ -2,7 +2,7 @@ import { DataFrame } from '@grafana/data';
import { DimensionSupplier } from 'app/features/dimensions';
import { Feature } from 'ol';
import { Point } from 'ol/geom';
import { StyleMaker } from '../style/types';
import { StyleMaker, TextStyleConfig } from '../style/types';
import { LocationInfo } from './location';
export interface FeaturesStylesBuilderConfig {
@ -11,6 +11,7 @@ export interface FeaturesStylesBuilderConfig {
opacity: number;
styleMaker: StyleMaker;
textDim?: DimensionSupplier<string>;
textConfig?: TextStyleConfig;
}
export const getFeatures = (
@ -32,6 +33,9 @@ export const getFeatures = (
// Get the text for the feature based on text dimension
const text = config?.textDim ? config?.textDim.get(i) : undefined;
// Get the textConfig
const textConfig = config?.textConfig;
// Create a new Feature for each point returned from dataFrameToPoints
const dot = new Feature(info.points[i]);
dot.setProperties({
@ -39,7 +43,7 @@ export const getFeatures = (
rowIndex: i,
});
dot.setStyle(config.styleMaker({ color, size, text, opacity }));
dot.setStyle(config.styleMaker({ color, size, text, opacity, textConfig }));
features.push(dot);
}