mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06: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:
parent
3dc74bd74b
commit
5c7f77e402
179
devenv/dev-dashboards/panel-geomap/geomap-route-layer.json
Normal file
179
devenv/dev-dashboards/panel-geomap/geomap-route-layer.json
Normal 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": ""
|
||||
}
|
@ -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') +
|
||||
{
|
||||
|
@ -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]) {
|
||||
|
Loading…
Reference in New Issue
Block a user