mirror of
https://github.com/grafana/grafana.git
synced 2026-07-29 15:59:50 -05:00
Geomap: Add color gradients to route layer (#59062)
* Geomap: Add color gradients to route layer * Add support for all color schemes * Address PR feedback: remove ! from verified object * Add arrow support and simplify color functions * Simplify and clean-up code * Remove line width slider and drive width by size * Drive arrow size based on size * Allow arrows for fixed color * Add gdev dashboard * Use square line caps only when arrows are active * Apply size to width for fixed color and size * Handle arrows when size and color are fixed * Add tags to gdev dashboard * Fix null error in backend test for gdev dashboard Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
This commit is contained in:
co-authored by
nmarrs
parent
3dc74bd74b
commit
5c7f77e402
@@ -0,0 +1,179 @@
|
|||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana",
|
||||||
|
"uid": "-- Grafana --"
|
||||||
|
},
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"target": {
|
||||||
|
"limit": 100,
|
||||||
|
"matchAny": false,
|
||||||
|
"tags": [],
|
||||||
|
"type": "dashboard"
|
||||||
|
},
|
||||||
|
"type": "dashboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editable": true,
|
||||||
|
"fiscalYearStartMonth": 0,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"id": 231,
|
||||||
|
"links": [],
|
||||||
|
"liveNow": false,
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "testdata",
|
||||||
|
"uid": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "continuous-RdYlGr"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "percentage",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "dark-red",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "yellow",
|
||||||
|
"value": 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": 100
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 16,
|
||||||
|
"w": 18,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 2,
|
||||||
|
"options": {
|
||||||
|
"basemap": {
|
||||||
|
"config": {
|
||||||
|
"server": "streets"
|
||||||
|
},
|
||||||
|
"name": "Layer 0",
|
||||||
|
"type": "esri-xyz"
|
||||||
|
},
|
||||||
|
"controls": {
|
||||||
|
"mouseWheelZoom": true,
|
||||||
|
"showAttribution": true,
|
||||||
|
"showDebug": false,
|
||||||
|
"showMeasure": false,
|
||||||
|
"showScale": false,
|
||||||
|
"showZoom": true
|
||||||
|
},
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"arrow": 1,
|
||||||
|
"style": {
|
||||||
|
"color": {
|
||||||
|
"field": "val",
|
||||||
|
"fixed": "dark-green"
|
||||||
|
},
|
||||||
|
"lineWidth": 2,
|
||||||
|
"opacity": 1,
|
||||||
|
"rotation": {
|
||||||
|
"fixed": 0,
|
||||||
|
"max": 360,
|
||||||
|
"min": -360,
|
||||||
|
"mode": "mod"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"field": "val",
|
||||||
|
"fixed": 5,
|
||||||
|
"max": 20,
|
||||||
|
"min": 5
|
||||||
|
},
|
||||||
|
"symbol": {
|
||||||
|
"fixed": "img/icons/marker/circle.svg",
|
||||||
|
"mode": "fixed"
|
||||||
|
},
|
||||||
|
"textConfig": {
|
||||||
|
"fontSize": 12,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"textAlign": "center",
|
||||||
|
"textBaseline": "middle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"mode": "auto"
|
||||||
|
},
|
||||||
|
"name": "Layer 2",
|
||||||
|
"tooltip": true,
|
||||||
|
"type": "route"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "details"
|
||||||
|
},
|
||||||
|
"view": {
|
||||||
|
"allLayers": true,
|
||||||
|
"id": "coords",
|
||||||
|
"lat": 2.359794,
|
||||||
|
"lon": 8.135816,
|
||||||
|
"zoom": 4.45
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pluginVersion": "9.4.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"csvContent": "lat,lon,val\n-5,2,0\n1,5,25\n6,10,50\n9,15,75\n10,20,100",
|
||||||
|
"datasource": {
|
||||||
|
"type": "testdata",
|
||||||
|
"uid": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
"refId": "A",
|
||||||
|
"scenarioId": "csv_content"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Route with Colors",
|
||||||
|
"type": "geomap"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"schemaVersion": 37,
|
||||||
|
"style": "dark",
|
||||||
|
"tags": ["gdev", "panel-tests", "geomap"],
|
||||||
|
"templating": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-6h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {},
|
||||||
|
"timezone": "",
|
||||||
|
"title": "Panel Tests - Geomap Route Layer",
|
||||||
|
"uid": "OYTKK3DVk",
|
||||||
|
"version": 25,
|
||||||
|
"weekStart": ""
|
||||||
|
}
|
||||||
@@ -205,6 +205,13 @@ local dashboard = grafana.dashboard;
|
|||||||
id: 0,
|
id: 0,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
dashboard.new('geomap-route-layer', import '../dev-dashboards/panel-geomap/geomap-route-layer.json') +
|
||||||
|
resource.addMetadata('folder', 'dev-dashboards') +
|
||||||
|
{
|
||||||
|
spec+: {
|
||||||
|
id: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
dashboard.new('geomap-spatial-operations-transformer', import '../dev-dashboards/panel-geomap/geomap-spatial-operations-transformer.json') +
|
dashboard.new('geomap-spatial-operations-transformer', import '../dev-dashboards/panel-geomap/geomap-spatial-operations-transformer.json') +
|
||||||
resource.addMetadata('folder', 'dev-dashboards') +
|
resource.addMetadata('folder', 'dev-dashboards') +
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ import Map from 'ol/Map';
|
|||||||
import { FeatureLike } from 'ol/Feature';
|
import { FeatureLike } from 'ol/Feature';
|
||||||
import { Subscription, throttleTime } from 'rxjs';
|
import { Subscription, throttleTime } from 'rxjs';
|
||||||
import { getGeometryField, getLocationMatchers } from 'app/features/geo/utils/location';
|
import { getGeometryField, getLocationMatchers } from 'app/features/geo/utils/location';
|
||||||
import { getColorDimension } from 'app/features/dimensions';
|
import { defaultStyleConfig, StyleConfig } from '../../style/types';
|
||||||
import { defaultStyleConfig, StyleConfig, StyleDimensions } from '../../style/types';
|
|
||||||
import { StyleEditor } from '../../editor/StyleEditor';
|
import { StyleEditor } from '../../editor/StyleEditor';
|
||||||
import { getStyleConfigState } from '../../style/utils';
|
import { getStyleConfigState } from '../../style/utils';
|
||||||
import VectorLayer from 'ol/layer/Vector';
|
import VectorLayer from 'ol/layer/Vector';
|
||||||
@@ -28,10 +27,15 @@ import VectorSource from 'ol/source/Vector';
|
|||||||
import { Fill, Stroke, Style, Circle } from 'ol/style';
|
import { Fill, Stroke, Style, Circle } from 'ol/style';
|
||||||
import Feature from 'ol/Feature';
|
import Feature from 'ol/Feature';
|
||||||
import { alpha } from '@grafana/data/src/themes/colorManipulator';
|
import { alpha } from '@grafana/data/src/themes/colorManipulator';
|
||||||
|
import { LineString, SimpleGeometry } from 'ol/geom';
|
||||||
|
import FlowLine from 'ol-ext/style/FlowLine';
|
||||||
|
import tinycolor from 'tinycolor2';
|
||||||
|
import { getStyleDimension } from '../../utils/utils';
|
||||||
|
|
||||||
// Configuration options for Circle overlays
|
// Configuration options for Circle overlays
|
||||||
export interface RouteConfig {
|
export interface RouteConfig {
|
||||||
style: StyleConfig;
|
style: StyleConfig;
|
||||||
|
arrow?: 0 | 1 | -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions: RouteConfig = {
|
const defaultOptions: RouteConfig = {
|
||||||
@@ -40,6 +44,7 @@ const defaultOptions: RouteConfig = {
|
|||||||
opacity: 1,
|
opacity: 1,
|
||||||
lineWidth: 2,
|
lineWidth: 2,
|
||||||
},
|
},
|
||||||
|
arrow: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ROUTE_LAYER_ID = 'route';
|
export const ROUTE_LAYER_ID = 'route';
|
||||||
@@ -81,10 +86,16 @@ export const routeLayer: MapLayerRegistryItem<RouteConfig> = {
|
|||||||
const location = await getLocationMatchers(options.location);
|
const location = await getLocationMatchers(options.location);
|
||||||
const source = new FrameVectorSource(location);
|
const source = new FrameVectorSource(location);
|
||||||
const vectorLayer = new VectorLayer({ source });
|
const vectorLayer = new VectorLayer({ source });
|
||||||
|
const hasArrows = config.arrow == 1 || config.arrow == -1;
|
||||||
|
|
||||||
if (!style.fields) {
|
if (!style.fields && !hasArrows) {
|
||||||
// Set a global style
|
// Set a global style
|
||||||
vectorLayer.setStyle(routeStyle(style.base));
|
const styleBase = routeStyle(style.base);
|
||||||
|
if (style.config.size && style.config.size.fixed) {
|
||||||
|
// Applies width to base style if specified
|
||||||
|
styleBase.getStroke().setWidth(style.config.size.fixed);
|
||||||
|
}
|
||||||
|
vectorLayer.setStyle(styleBase);
|
||||||
} else {
|
} else {
|
||||||
vectorLayer.setStyle((feature: FeatureLike) => {
|
vectorLayer.setStyle((feature: FeatureLike) => {
|
||||||
const idx = feature.get('rowIndex') as number;
|
const idx = feature.get('rowIndex') as number;
|
||||||
@@ -93,6 +104,53 @@ export const routeLayer: MapLayerRegistryItem<RouteConfig> = {
|
|||||||
return routeStyle(style.base);
|
return routeStyle(style.base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const styles = [];
|
||||||
|
const geom = feature.getGeometry();
|
||||||
|
const opacity = style.config.opacity ?? 1;
|
||||||
|
if (geom instanceof SimpleGeometry) {
|
||||||
|
const coordinates = geom.getCoordinates();
|
||||||
|
if (coordinates) {
|
||||||
|
for (let i = 0; i < coordinates.length - 1; i++) {
|
||||||
|
const color1 = tinycolor(
|
||||||
|
theme.visualization.getColorByName((dims.color && dims.color.get(i)) ?? style.base.color)
|
||||||
|
)
|
||||||
|
.setAlpha(opacity)
|
||||||
|
.toString();
|
||||||
|
const color2 = tinycolor(
|
||||||
|
theme.visualization.getColorByName((dims.color && dims.color.get(i + 1)) ?? style.base.color)
|
||||||
|
)
|
||||||
|
.setAlpha(opacity)
|
||||||
|
.toString();
|
||||||
|
|
||||||
|
const arrowSize1 = (dims.size && dims.size.get(i)) ?? style.base.size;
|
||||||
|
const arrowSize2 = (dims.size && dims.size.get(i + 1)) ?? style.base.size;
|
||||||
|
|
||||||
|
const flowStyle = new FlowLine({
|
||||||
|
visible: true,
|
||||||
|
lineCap: config.arrow == 0 ? 'round' : 'square',
|
||||||
|
color: color1,
|
||||||
|
color2: color2,
|
||||||
|
width: (dims.size && dims.size.get(i)) ?? style.base.size,
|
||||||
|
width2: (dims.size && dims.size.get(i + 1)) ?? style.base.size,
|
||||||
|
});
|
||||||
|
if (config.arrow) {
|
||||||
|
flowStyle.setArrow(config.arrow);
|
||||||
|
if (config.arrow > 0) {
|
||||||
|
flowStyle.setArrowColor(color2);
|
||||||
|
flowStyle.setArrowSize((arrowSize2 ?? 0) * 1.5);
|
||||||
|
} else {
|
||||||
|
flowStyle.setArrowColor(color1);
|
||||||
|
flowStyle.setArrowSize((arrowSize1 ?? 0) * 1.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const LS = new LineString([coordinates[i], coordinates[i + 1]]);
|
||||||
|
flowStyle.setGeometry(LS);
|
||||||
|
styles.push(flowStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return styles;
|
||||||
|
}
|
||||||
|
|
||||||
const values = { ...style.base };
|
const values = { ...style.base };
|
||||||
|
|
||||||
if (dims.color) {
|
if (dims.color) {
|
||||||
@@ -110,10 +168,10 @@ export const routeLayer: MapLayerRegistryItem<RouteConfig> = {
|
|||||||
radius: crosshairRadius,
|
radius: crosshairRadius,
|
||||||
stroke: new Stroke({
|
stroke: new Stroke({
|
||||||
color: alpha(style.base.color, 0.4),
|
color: alpha(style.base.color, 0.4),
|
||||||
width: crosshairRadius + 2
|
width: crosshairRadius + 2,
|
||||||
}),
|
}),
|
||||||
fill: new Fill({color: style.base.color}),
|
fill: new Fill({ color: style.base.color }),
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const crosshairLayer = new VectorLayer({
|
const crosshairLayer = new VectorLayer({
|
||||||
@@ -124,7 +182,7 @@ export const routeLayer: MapLayerRegistryItem<RouteConfig> = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const layer = new LayerGroup({
|
const layer = new LayerGroup({
|
||||||
layers: [vectorLayer, crosshairLayer]
|
layers: [vectorLayer, crosshairLayer],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Crosshair sharing subscriptions
|
// Crosshair sharing subscriptions
|
||||||
@@ -172,12 +230,8 @@ export const routeLayer: MapLayerRegistryItem<RouteConfig> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const frame of data.series) {
|
for (const frame of data.series) {
|
||||||
if (style.fields) {
|
if (style.fields || hasArrows) {
|
||||||
const dims: StyleDimensions = {};
|
style.dims = getStyleDimension(frame, style, theme);
|
||||||
if (style.fields.color) {
|
|
||||||
dims.color = getColorDimension(frame, style.config.color ?? defaultStyleConfig.color, theme);
|
|
||||||
}
|
|
||||||
style.dims = dims;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
source.updateLineString(frame);
|
source.updateLineString(frame);
|
||||||
@@ -194,19 +248,21 @@ export const routeLayer: MapLayerRegistryItem<RouteConfig> = {
|
|||||||
name: 'Style',
|
name: 'Style',
|
||||||
editor: StyleEditor,
|
editor: StyleEditor,
|
||||||
settings: {
|
settings: {
|
||||||
simpleFixedValues: true,
|
simpleFixedValues: false,
|
||||||
},
|
},
|
||||||
defaultValue: defaultOptions.style,
|
defaultValue: defaultOptions.style,
|
||||||
})
|
})
|
||||||
.addSliderInput({
|
.addRadio({
|
||||||
path: 'config.style.lineWidth',
|
path: 'config.arrow',
|
||||||
name: 'Line width',
|
name: 'Arrow',
|
||||||
defaultValue: defaultOptions.style.lineWidth,
|
|
||||||
settings: {
|
settings: {
|
||||||
min: 1,
|
options: [
|
||||||
max: 10,
|
{ label: 'None', value: 0 },
|
||||||
step: 1,
|
{ label: 'Forward', value: 1 },
|
||||||
|
{ label: 'Reverse', value: -1 },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
defaultValue: defaultOptions.arrow,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -229,7 +285,7 @@ function findNearestTimeIndex(timestamps: number[], time: number): number | null
|
|||||||
return lastIdx;
|
return lastIdx;
|
||||||
}
|
}
|
||||||
|
|
||||||
const probableIdx = Math.abs(Math.round(lastIdx * (time - timestamps[0]) / (timestamps[lastIdx] - timestamps[0])));
|
const probableIdx = Math.abs(Math.round((lastIdx * (time - timestamps[0])) / (timestamps[lastIdx] - timestamps[0])));
|
||||||
if (time < timestamps[probableIdx]) {
|
if (time < timestamps[probableIdx]) {
|
||||||
for (let i = probableIdx; i > 0; i--) {
|
for (let i = probableIdx; i > 0; i--) {
|
||||||
if (time > timestamps[i]) {
|
if (time > timestamps[i]) {
|
||||||
|
|||||||
Reference in New Issue
Block a user