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:
Drew Slobodnjak 2022-12-15 09:54:08 -08:00 committed by GitHub
parent 3dc74bd74b
commit 5c7f77e402
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 265 additions and 23 deletions

View File

@ -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": ""
}

View File

@ -205,6 +205,13 @@ local dashboard = grafana.dashboard;
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') +
resource.addMetadata('folder', 'dev-dashboards') +
{

View File

@ -15,8 +15,7 @@ import Map from 'ol/Map';
import { FeatureLike } from 'ol/Feature';
import { Subscription, throttleTime } from 'rxjs';
import { getGeometryField, getLocationMatchers } from 'app/features/geo/utils/location';
import { getColorDimension } from 'app/features/dimensions';
import { defaultStyleConfig, StyleConfig, StyleDimensions } from '../../style/types';
import { defaultStyleConfig, StyleConfig } from '../../style/types';
import { StyleEditor } from '../../editor/StyleEditor';
import { getStyleConfigState } from '../../style/utils';
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 Feature from 'ol/Feature';
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
export interface RouteConfig {
style: StyleConfig;
arrow?: 0 | 1 | -1;
}
const defaultOptions: RouteConfig = {
@ -40,6 +44,7 @@ const defaultOptions: RouteConfig = {
opacity: 1,
lineWidth: 2,
},
arrow: 0,
};
export const ROUTE_LAYER_ID = 'route';
@ -81,10 +86,16 @@ export const routeLayer: MapLayerRegistryItem<RouteConfig> = {
const location = await getLocationMatchers(options.location);
const source = new FrameVectorSource(location);
const vectorLayer = new VectorLayer({ source });
const hasArrows = config.arrow == 1 || config.arrow == -1;
if (!style.fields) {
if (!style.fields && !hasArrows) {
// 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 {
vectorLayer.setStyle((feature: FeatureLike) => {
const idx = feature.get('rowIndex') as number;
@ -93,6 +104,53 @@ export const routeLayer: MapLayerRegistryItem<RouteConfig> = {
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 };
if (dims.color) {
@ -110,10 +168,10 @@ export const routeLayer: MapLayerRegistryItem<RouteConfig> = {
radius: crosshairRadius,
stroke: new Stroke({
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({
@ -124,7 +182,7 @@ export const routeLayer: MapLayerRegistryItem<RouteConfig> = {
});
const layer = new LayerGroup({
layers: [vectorLayer, crosshairLayer]
layers: [vectorLayer, crosshairLayer],
});
// Crosshair sharing subscriptions
@ -172,12 +230,8 @@ export const routeLayer: MapLayerRegistryItem<RouteConfig> = {
}
for (const frame of data.series) {
if (style.fields) {
const dims: StyleDimensions = {};
if (style.fields.color) {
dims.color = getColorDimension(frame, style.config.color ?? defaultStyleConfig.color, theme);
}
style.dims = dims;
if (style.fields || hasArrows) {
style.dims = getStyleDimension(frame, style, theme);
}
source.updateLineString(frame);
@ -194,19 +248,21 @@ export const routeLayer: MapLayerRegistryItem<RouteConfig> = {
name: 'Style',
editor: StyleEditor,
settings: {
simpleFixedValues: true,
simpleFixedValues: false,
},
defaultValue: defaultOptions.style,
})
.addSliderInput({
path: 'config.style.lineWidth',
name: 'Line width',
defaultValue: defaultOptions.style.lineWidth,
.addRadio({
path: 'config.arrow',
name: 'Arrow',
settings: {
min: 1,
max: 10,
step: 1,
options: [
{ label: 'None', value: 0 },
{ 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;
}
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]) {
for (let i = probableIdx; i > 0; i--) {
if (time > timestamps[i]) {