mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Geomap: Add text labels layer (#40778)
* Geomap: add initial text labels layer * add fontsize to text labels layer * refactor feature styles in marker and text layers * hide template and pick default field Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
parent
3ed5ade78d
commit
5449bd9ae7
@ -12,7 +12,7 @@ import { FieldNamePicker } from '../../../../../packages/grafana-ui/src/componen
|
||||
const textOptions = [
|
||||
{ label: 'Fixed', value: TextDimensionMode.Fixed, description: 'Fixed value' },
|
||||
{ label: 'Field', value: TextDimensionMode.Field, description: 'Display field value' },
|
||||
{ label: 'Template', value: TextDimensionMode.Template, description: 'use template text' },
|
||||
// { label: 'Template', value: TextDimensionMode.Template, description: 'use template text' },
|
||||
];
|
||||
|
||||
const dummyFieldSettings: StandardEditorsRegistryItem<string, FieldNamePickerConfigSettings> = {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { DataFrame, Field, formattedValueToString } from '@grafana/data';
|
||||
import { DataFrame, Field, FieldType, formattedValueToString } from '@grafana/data';
|
||||
import { DimensionSupplier, TextDimensionConfig, TextDimensionMode } from './types';
|
||||
import { findField, getLastNotNullFieldValue } from './utils';
|
||||
|
||||
@ -7,7 +7,8 @@ import { findField, getLastNotNullFieldValue } from './utils';
|
||||
//---------------------------------------------------------
|
||||
|
||||
export function getTextDimension(frame: DataFrame | undefined, config: TextDimensionConfig): DimensionSupplier<string> {
|
||||
return getTextDimensionForField(findField(frame, config.field), config);
|
||||
const field = config.field ? findField(frame, config.field) : frame?.fields.find((f) => f.type === FieldType.string);
|
||||
return getTextDimensionForField(field, config);
|
||||
}
|
||||
|
||||
export function getTextDimensionForField(
|
||||
|
@ -2,6 +2,7 @@ import { markersLayer } from './markersLayer';
|
||||
import { geojsonMapper } from './geojsonMapper';
|
||||
import { heatmapLayer } from './heatMap';
|
||||
import { lastPointTracker } from './lastPointTracker';
|
||||
import { textLabelsLayer } from './textLabelsLayer';
|
||||
|
||||
/**
|
||||
* Registry for layer handlers
|
||||
@ -11,4 +12,5 @@ export const dataLayers = [
|
||||
heatmapLayer,
|
||||
lastPointTracker,
|
||||
geojsonMapper, // dummy for now
|
||||
textLabelsLayer,
|
||||
];
|
||||
|
@ -12,8 +12,6 @@ import { Point } from 'ol/geom';
|
||||
import * as layer from 'ol/layer';
|
||||
import * as source from 'ol/source';
|
||||
import * as style from 'ol/style';
|
||||
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { dataFrameToPoints, getLocationMatchers } from '../../utils/location';
|
||||
import {
|
||||
ColorDimensionConfig,
|
||||
@ -28,8 +26,10 @@ import {
|
||||
import { ScaleDimensionEditor, ColorDimensionEditor, ResourceDimensionEditor } from 'app/features/dimensions/editors';
|
||||
import { ObservablePropsWrapper } from '../../components/ObservablePropsWrapper';
|
||||
import { MarkersLegend, MarkersLegendProps } from './MarkersLegend';
|
||||
import { StyleMaker, getMarkerFromPath } from '../../utils/regularShapes';
|
||||
import { getMarkerFromPath } from '../../utils/regularShapes';
|
||||
import { ReplaySubject } from 'rxjs';
|
||||
import { FeaturesStylesBuilderConfig, getFeatures } from '../../utils/getFeatures';
|
||||
import { StyleMaker, StyleMakerConfig } from '../../types';
|
||||
|
||||
// Configuration options for Circle overlays
|
||||
export interface MarkersConfig {
|
||||
@ -112,13 +112,13 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
||||
|
||||
const marker = getMarkerFromPath(config.markerSymbol?.fixed);
|
||||
|
||||
const makeIconStyle = (color: string, fillColor: string, radius: number) => {
|
||||
const makeIconStyle = (cfg: StyleMakerConfig) => {
|
||||
return new style.Style({
|
||||
image: new style.Icon({
|
||||
src: markerPath,
|
||||
color,
|
||||
color: cfg.color,
|
||||
// opacity,
|
||||
scale: (DEFAULT_SIZE + radius) / 100,
|
||||
scale: (DEFAULT_SIZE + cfg.size) / 100,
|
||||
}),
|
||||
});
|
||||
};
|
||||
@ -138,23 +138,17 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
||||
const sizeDim = getScaledDimension(frame, config.size);
|
||||
const opacity = options.config?.fillOpacity ?? defaultOptions.fillOpacity;
|
||||
|
||||
// Map each data value into new points
|
||||
for (let i = 0; i < frame.length; i++) {
|
||||
// Get the circle color for a specific data value depending on color scheme
|
||||
const color = colorDim.get(i);
|
||||
// Set the opacity determined from user configuration
|
||||
const fillColor = tinycolor(color).setAlpha(opacity).toRgbString();
|
||||
// Get circle size from user configuration
|
||||
const radius = sizeDim.get(i);
|
||||
const featureDimensionConfig: FeaturesStylesBuilderConfig = {
|
||||
colorDim: colorDim,
|
||||
sizeDim: sizeDim,
|
||||
opacity: opacity,
|
||||
styleMaker: shape,
|
||||
};
|
||||
|
||||
// Create a new Feature for each point returned from dataFrameToPoints
|
||||
const dot = new Feature(info.points[i]);
|
||||
dot.setProperties({
|
||||
frame,
|
||||
rowIndex: i,
|
||||
});
|
||||
dot.setStyle(shape(color, fillColor, radius));
|
||||
features.push(dot);
|
||||
const frameFeatures = getFeatures(frame, info, featureDimensionConfig);
|
||||
|
||||
if (frameFeatures) {
|
||||
features.push(...frameFeatures);
|
||||
}
|
||||
|
||||
// Post updates to the legend component
|
||||
|
162
public/app/plugins/panel/geomap/layers/data/textLabelsLayer.ts
Normal file
162
public/app/plugins/panel/geomap/layers/data/textLabelsLayer.ts
Normal file
@ -0,0 +1,162 @@
|
||||
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 * as style from 'ol/style';
|
||||
import { dataFrameToPoints, getLocationMatchers } from '../../utils/location';
|
||||
import {
|
||||
ColorDimensionConfig,
|
||||
getColorDimension,
|
||||
getScaledDimension,
|
||||
getTextDimension,
|
||||
ScaleDimensionConfig,
|
||||
TextDimensionConfig,
|
||||
TextDimensionMode,
|
||||
} from 'app/features/dimensions';
|
||||
import { ColorDimensionEditor, ScaleDimensionEditor, TextDimensionEditor } from 'app/features/dimensions/editors';
|
||||
import { Fill, Stroke } from 'ol/style';
|
||||
import { FeaturesStylesBuilderConfig, getFeatures } from '../../utils/getFeatures';
|
||||
import { Feature } from 'ol';
|
||||
import { Point } from 'ol/geom';
|
||||
import { StyleMaker, StyleMakerConfig } from '../../types';
|
||||
|
||||
interface TextLabelsConfig {
|
||||
labelText: TextDimensionConfig;
|
||||
color: ColorDimensionConfig;
|
||||
fillOpacity: number;
|
||||
fontSize: ScaleDimensionConfig;
|
||||
}
|
||||
|
||||
export const TEXT_LABELS_LAYER = 'text-labels';
|
||||
|
||||
const defaultOptions: TextLabelsConfig = {
|
||||
labelText: {
|
||||
fixed: '',
|
||||
mode: TextDimensionMode.Field,
|
||||
},
|
||||
color: {
|
||||
fixed: 'dark-blue',
|
||||
},
|
||||
fillOpacity: 0.6,
|
||||
fontSize: {
|
||||
fixed: 10,
|
||||
min: 5,
|
||||
max: 100,
|
||||
},
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
const fontFamily = theme.typography.fontFamily;
|
||||
|
||||
const getTextStyle = (text: string, fillColor: string, fontSize: number) => {
|
||||
return new style.Text({
|
||||
text: text,
|
||||
fill: new Fill({ color: fillColor }),
|
||||
stroke: new Stroke({ color: fillColor }),
|
||||
font: `normal ${fontSize}px ${fontFamily}`,
|
||||
});
|
||||
};
|
||||
|
||||
const getStyle: StyleMaker = (cfg: StyleMakerConfig) => {
|
||||
return new style.Style({
|
||||
text: getTextStyle(cfg.text ?? defaultOptions.labelText.fixed, cfg.fillColor, cfg.size),
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
init: () => vectorLayer,
|
||||
update: (data: PanelData) => {
|
||||
if (!data.series?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const features: Feature<Point>[] = [];
|
||||
|
||||
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, config.color, theme);
|
||||
const textDim = getTextDimension(frame, config.labelText);
|
||||
const sizeDim = getScaledDimension(frame, config.fontSize);
|
||||
const opacity = options.config?.fillOpacity ?? defaultOptions.fillOpacity;
|
||||
|
||||
const featureDimensionConfig: FeaturesStylesBuilderConfig = {
|
||||
colorDim: colorDim,
|
||||
sizeDim: sizeDim,
|
||||
textDim: textDim,
|
||||
opacity: opacity,
|
||||
styleMaker: getStyle,
|
||||
};
|
||||
|
||||
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.labelText',
|
||||
name: 'Text label',
|
||||
path: 'config.labelText',
|
||||
editor: TextDimensionEditor,
|
||||
})
|
||||
.addCustomEditor({
|
||||
id: 'config.color',
|
||||
path: 'config.color',
|
||||
name: 'Text color',
|
||||
editor: ColorDimensionEditor,
|
||||
settings: {},
|
||||
})
|
||||
.addSliderInput({
|
||||
path: 'config.fillOpacity',
|
||||
name: 'Text opacity',
|
||||
defaultValue: defaultOptions.fillOpacity,
|
||||
settings: {
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.1,
|
||||
},
|
||||
})
|
||||
.addCustomEditor({
|
||||
id: 'config.fontSize',
|
||||
path: 'config.fontSize',
|
||||
name: 'Text size',
|
||||
editor: ScaleDimensionEditor,
|
||||
settings: {
|
||||
fixed: defaultOptions.fontSize.fixed,
|
||||
min: defaultOptions.fontSize.min,
|
||||
max: defaultOptions.fontSize.max,
|
||||
},
|
||||
});
|
||||
},
|
||||
defaultOptions,
|
||||
};
|
@ -1,5 +1,6 @@
|
||||
import { MapLayerOptions } from '@grafana/data';
|
||||
import { Units } from 'ol/proj/Units';
|
||||
import { Style } from 'ol/style';
|
||||
import { MapCenterID } from './view';
|
||||
|
||||
export interface ControlsOptions {
|
||||
@ -61,3 +62,12 @@ export enum ComparisonOperation {
|
||||
GT = 'gt',
|
||||
GTE = 'gte',
|
||||
}
|
||||
export interface StyleMakerConfig {
|
||||
color: string;
|
||||
fillColor: string;
|
||||
size: number;
|
||||
markerPath?: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export type StyleMaker = (config: StyleMakerConfig) => Style;
|
||||
|
54
public/app/plugins/panel/geomap/utils/getFeatures.ts
Normal file
54
public/app/plugins/panel/geomap/utils/getFeatures.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { DataFrame } from '@grafana/data';
|
||||
import { DimensionSupplier } from 'app/features/dimensions';
|
||||
import { Feature } from 'ol';
|
||||
import { Point } from 'ol/geom';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { StyleMaker } from '../types';
|
||||
import { LocationInfo } from './location';
|
||||
|
||||
export interface FeaturesStylesBuilderConfig {
|
||||
colorDim: DimensionSupplier<string>;
|
||||
sizeDim: DimensionSupplier<number>;
|
||||
opacity: number;
|
||||
styleMaker: StyleMaker;
|
||||
textDim?: DimensionSupplier<string>;
|
||||
}
|
||||
|
||||
export const getFeatures = (
|
||||
frame: DataFrame,
|
||||
info: LocationInfo,
|
||||
config: FeaturesStylesBuilderConfig
|
||||
): Array<Feature<Point>> | undefined => {
|
||||
const features: Array<Feature<Point>> = [];
|
||||
|
||||
// Map each data value into new points
|
||||
for (let i = 0; i < frame.length; i++) {
|
||||
// Get the color for the feature based on color scheme
|
||||
const color = config.colorDim.get(i);
|
||||
|
||||
// Get the size for the feature based on size dimension
|
||||
const size = config.sizeDim.get(i);
|
||||
|
||||
// Get the text for the feature based on text dimension
|
||||
const label = config?.textDim ? config?.textDim.get(i) : undefined;
|
||||
|
||||
// Set the opacity determined from user configuration
|
||||
const fillColor = tinycolor(color).setAlpha(config?.opacity).toRgbString();
|
||||
|
||||
// Create a new Feature for each point returned from dataFrameToPoints
|
||||
const dot = new Feature(info.points[i]);
|
||||
dot.setProperties({
|
||||
frame,
|
||||
rowIndex: i,
|
||||
});
|
||||
|
||||
if (config?.textDim) {
|
||||
dot.setStyle(config.styleMaker({ color, fillColor, size, text: label }));
|
||||
} else {
|
||||
dot.setStyle(config.styleMaker({ color, fillColor, size }));
|
||||
}
|
||||
features.push(dot);
|
||||
}
|
||||
|
||||
return features;
|
||||
};
|
@ -1,8 +1,6 @@
|
||||
import { Fill, RegularShape, Stroke, Style, Circle } from 'ol/style';
|
||||
import { Registry, RegistryItem } from '@grafana/data';
|
||||
|
||||
export type StyleMaker = (color: string, fillColor: string, radius: number, markerPath?: string) => Style;
|
||||
|
||||
import { StyleMaker, StyleMakerConfig } from '../types';
|
||||
export interface MarkerMaker extends RegistryItem {
|
||||
// path to icon that will be shown (but then replaced)
|
||||
aliasIds: string[];
|
||||
@ -33,12 +31,12 @@ export const circleMarker: MarkerMaker = {
|
||||
name: 'Circle',
|
||||
hasFill: true,
|
||||
aliasIds: [MarkerShapePath.circle],
|
||||
make: (color: string, fillColor: string, radius: number) => {
|
||||
make: (cfg: StyleMakerConfig) => {
|
||||
return new Style({
|
||||
image: new Circle({
|
||||
stroke: new Stroke({ color: color }),
|
||||
fill: new Fill({ color: fillColor }),
|
||||
radius: radius,
|
||||
stroke: new Stroke({ color: cfg.color }),
|
||||
fill: new Fill({ color: cfg.fillColor }),
|
||||
radius: cfg.size,
|
||||
}),
|
||||
});
|
||||
},
|
||||
@ -51,13 +49,13 @@ const makers: MarkerMaker[] = [
|
||||
name: 'Square',
|
||||
hasFill: true,
|
||||
aliasIds: [MarkerShapePath.square],
|
||||
make: (color: string, fillColor: string, radius: number) => {
|
||||
make: (cfg: StyleMakerConfig) => {
|
||||
return new Style({
|
||||
image: new RegularShape({
|
||||
fill: new Fill({ color: fillColor }),
|
||||
stroke: new Stroke({ color: color, width: 1 }),
|
||||
fill: new Fill({ color: cfg.fillColor }),
|
||||
stroke: new Stroke({ color: cfg.color, width: 1 }),
|
||||
points: 4,
|
||||
radius: radius,
|
||||
radius: cfg.size,
|
||||
angle: Math.PI / 4,
|
||||
}),
|
||||
});
|
||||
@ -68,13 +66,13 @@ const makers: MarkerMaker[] = [
|
||||
name: 'Triangle',
|
||||
hasFill: true,
|
||||
aliasIds: [MarkerShapePath.triangle],
|
||||
make: (color: string, fillColor: string, radius: number) => {
|
||||
make: (cfg: StyleMakerConfig) => {
|
||||
return new Style({
|
||||
image: new RegularShape({
|
||||
fill: new Fill({ color: fillColor }),
|
||||
stroke: new Stroke({ color: color, width: 1 }),
|
||||
fill: new Fill({ color: cfg.fillColor }),
|
||||
stroke: new Stroke({ color: cfg.color, width: 1 }),
|
||||
points: 3,
|
||||
radius: radius,
|
||||
radius: cfg.size,
|
||||
rotation: Math.PI / 4,
|
||||
angle: 0,
|
||||
}),
|
||||
@ -86,14 +84,14 @@ const makers: MarkerMaker[] = [
|
||||
name: 'Star',
|
||||
hasFill: true,
|
||||
aliasIds: [MarkerShapePath.star],
|
||||
make: (color: string, fillColor: string, radius: number) => {
|
||||
make: (cfg: StyleMakerConfig) => {
|
||||
return new Style({
|
||||
image: new RegularShape({
|
||||
fill: new Fill({ color: fillColor }),
|
||||
stroke: new Stroke({ color: color, width: 1 }),
|
||||
fill: new Fill({ color: cfg.fillColor }),
|
||||
stroke: new Stroke({ color: cfg.color, width: 1 }),
|
||||
points: 5,
|
||||
radius: radius,
|
||||
radius2: radius * 0.4,
|
||||
radius: cfg.size,
|
||||
radius2: cfg.size * 0.4,
|
||||
angle: 0,
|
||||
}),
|
||||
});
|
||||
@ -104,13 +102,13 @@ const makers: MarkerMaker[] = [
|
||||
name: 'Cross',
|
||||
hasFill: false,
|
||||
aliasIds: [MarkerShapePath.cross],
|
||||
make: (color: string, fillColor: string, radius: number) => {
|
||||
make: (cfg: StyleMakerConfig) => {
|
||||
return new Style({
|
||||
image: new RegularShape({
|
||||
fill: new Fill({ color: fillColor }),
|
||||
stroke: new Stroke({ color: color, width: 1 }),
|
||||
fill: new Fill({ color: cfg.fillColor }),
|
||||
stroke: new Stroke({ color: cfg.color, width: 1 }),
|
||||
points: 4,
|
||||
radius: radius,
|
||||
radius: cfg.size,
|
||||
radius2: 0,
|
||||
angle: 0,
|
||||
}),
|
||||
@ -122,13 +120,13 @@ const makers: MarkerMaker[] = [
|
||||
name: 'X',
|
||||
hasFill: false,
|
||||
aliasIds: [MarkerShapePath.x],
|
||||
make: (color: string, fillColor: string, radius: number) => {
|
||||
make: (cfg: StyleMakerConfig) => {
|
||||
return new Style({
|
||||
image: new RegularShape({
|
||||
fill: new Fill({ color: fillColor }),
|
||||
stroke: new Stroke({ color: color, width: 1 }),
|
||||
fill: new Fill({ color: cfg.fillColor }),
|
||||
stroke: new Stroke({ color: cfg.color, width: 1 }),
|
||||
points: 4,
|
||||
radius: radius,
|
||||
radius: cfg.size,
|
||||
radius2: 0,
|
||||
angle: Math.PI / 4,
|
||||
}),
|
||||
|
Loading…
Reference in New Issue
Block a user