diff --git a/public/app/plugins/panel/geomap/layers/data/index.ts b/public/app/plugins/panel/geomap/layers/data/index.ts index b8ecabee5fb..a70820a52c0 100644 --- a/public/app/plugins/panel/geomap/layers/data/index.ts +++ b/public/app/plugins/panel/geomap/layers/data/index.ts @@ -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, ]; diff --git a/public/app/plugins/panel/geomap/layers/data/markersLayer.tsx b/public/app/plugins/panel/geomap/layers/data/markersLayer.tsx index f5c0bbaa7b0..1e7302d0660 100644 --- a/public/app/plugins/panel/geomap/layers/data/markersLayer.tsx +++ b/public/app/plugins/panel/geomap/layers/data/markersLayer.tsx @@ -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 = { 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 = { 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', diff --git a/public/app/plugins/panel/geomap/layers/data/textLabelsLayer.ts b/public/app/plugins/panel/geomap/layers/data/textLabelsLayer.ts deleted file mode 100644 index 0923e9904ce..00000000000 --- a/public/app/plugins/panel/geomap/layers/data/textLabelsLayer.ts +++ /dev/null @@ -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 = { - id: TEXT_LABELS_LAYER, - name: 'Text labels', - description: 'render text labels', - isBaseMap: false, - state: PluginState.alpha, - showLocation: true, - - create: async (map: Map, options: MapLayerOptions, 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[] = []; - - - 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, -}; diff --git a/public/app/plugins/panel/geomap/migrations.test.ts b/public/app/plugins/panel/geomap/migrations.test.ts index b9ecd98786e..fa53e7d972f 100644 --- a/public/app/plugins/panel/geomap/migrations.test.ts +++ b/public/app/plugins/panel/geomap/migrations.test.ts @@ -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", diff --git a/public/app/plugins/panel/geomap/style/markers.ts b/public/app/plugins/panel/geomap/style/markers.ts index 6dec47d8183..ca83d5fabec 100644 --- a/public/app/plugins/panel/geomap/style/markers.ts +++ b/public/app/plugins/panel/geomap/style/markers.ts @@ -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 { opacity: cfg.opacity ?? 1, scale: (DEFAULT_SIZE + radius) / 100, }), + text: !cfg?.text ? undefined : textLabel(cfg), }), // transparent bounding box for featureAtPixel detection new Style({ diff --git a/public/app/plugins/panel/geomap/style/text.ts b/public/app/plugins/panel/geomap/style/text.ts deleted file mode 100644 index 0269855a567..00000000000 --- a/public/app/plugins/panel/geomap/style/text.ts +++ /dev/null @@ -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}`, - }), - }); -}; diff --git a/public/app/plugins/panel/geomap/style/types.ts b/public/app/plugins/panel/geomap/style/types.ts index 9a814bf0797..bc4465310c0 100644 --- a/public/app/plugins/panel/geomap/style/types.ts +++ b/public/app/plugins/panel/geomap/style/types.ts @@ -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 diff --git a/public/app/plugins/panel/geomap/utils/getFeatures.ts b/public/app/plugins/panel/geomap/utils/getFeatures.ts index 54c6e2530c9..09d87ac5039 100644 --- a/public/app/plugins/panel/geomap/utils/getFeatures.ts +++ b/public/app/plugins/panel/geomap/utils/getFeatures.ts @@ -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; + 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); }