GeoMap: Implement circle overlays (#36680)

This commit is contained in:
Eunice Kim 2021-07-12 20:47:39 -07:00 committed by GitHub
parent 32a551b4ca
commit dc8874cd2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 330 additions and 72 deletions

View File

@ -9,6 +9,7 @@ export const CSVFileEditor = ({ onChange, query }: EditorProps) => {
};
const files = [
'flight_info_by_state.csv',
'population_by_state.csv',
'gdp_per_capita.csv',
'js_libraries.csv',

View File

@ -0,0 +1,201 @@
import { MapLayerRegistryItem, MapLayerConfig, MapLayerHandler, PanelData, GrafanaTheme2, reduceField, ReducerID, FieldCalcs } from '@grafana/data';
import { dataFrameToPoints } from './utils'
import { FieldMappingOptions, QueryFormat } from '../../types'
import Map from 'ol/Map';
import Feature from 'ol/Feature';
import * as layer from 'ol/layer';
import * as source from 'ol/source';
import * as style from 'ol/style';
import tinycolor from 'tinycolor2';
// Configuration options for Circle overlays
export interface CircleConfig {
queryFormat: QueryFormat,
fieldMapping: FieldMappingOptions,
minSize: number,
maxSize: number,
opacity: number,
}
const defaultOptions: CircleConfig = {
queryFormat: {
locationType: 'coordinates',
},
fieldMapping: {
metricField: '',
geohashField: '',
latitudeField: '',
longitudeField: '',
},
minSize: 1,
maxSize: 10,
opacity: 0.4,
};
/**
* Map layer configuration for circle overlay
*/
export const circlesLayer: MapLayerRegistryItem<CircleConfig> = {
id: 'circles',
name: 'Circles',
description: 'creates circle overlays for data values',
isBaseMap: false,
/**
* Function that configures transformation and returns a transformer
* @param options
*/
create: (map: Map, options: MapLayerConfig<CircleConfig>, theme: GrafanaTheme2): MapLayerHandler => {
const config = { ...defaultOptions, ...options.config };
const vectorLayer = new layer.Vector({});
return {
init: () => vectorLayer,
update: (data: PanelData) => {
const features: Feature[] = [];
const frame = data.series[0];
// Get data values
const points = dataFrameToPoints(frame, config.fieldMapping, config.queryFormat);
const field = frame.fields.find(field => field.name === config.fieldMapping.metricField);
// Return early if metric field is not matched
if (field === undefined) {
return;
};
// Retrieve the min, max and range of data values
const calcs = reduceField({
field: field,
reducers: [
ReducerID.min,
ReducerID.max,
ReducerID.range,
]
});
// 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 = frame.fields[0].display!(field.values.get(i)).color;
// Set the opacity determined from user configuration
const fillColor = tinycolor(color).setAlpha(config.opacity).toRgbString();
// Get circle size from user configuration
const radius = calcCircleSize(calcs, field.values.get(i), config.minSize, config.maxSize);
// Create a new Feature for each point returned from dataFrameToPoints
const dot = new Feature({
geometry: points[i],
});
// Set the style of each feature dot
dot.setStyle(new style.Style({
image: new style.Circle({
// Stroke determines the outline color of the circle
stroke: new style.Stroke({
color: color,
}),
// Fill determines the color to fill the whole circle
fill: new style.Fill({
color: tinycolor(fillColor).toString(),
}),
radius: radius,
})
}));
features.push(dot);
};
// Source reads the data and provides a set of features to visualize
const vectorSource = new source.Vector({ features });
vectorLayer.setSource(vectorSource);
},
};
},
// Circle overlay options
registerOptionsUI: (builder) => {
builder
.addNumberInput({
path: 'minSize',
description: 'configures the min circle size',
name: 'Min Size',
defaultValue: defaultOptions.minSize,
})
.addNumberInput({
path: 'maxSize',
description: 'configures the max circle size',
name: 'Max Size',
defaultValue: defaultOptions.maxSize,
})
.addSliderInput({
path: 'opacity',
description: 'configures the amount of transparency',
name: 'Opacity',
defaultValue: defaultOptions.opacity,
settings: {
min: 0,
max: 1,
step: 0.1,
},
})
.addSelect({
path: 'queryFormat.locationType',
name: 'Query Format',
defaultValue: defaultOptions.queryFormat.locationType,
settings: {
options: [
{
value: 'coordinates',
label: 'Coordinates',
},
{
value: 'geohash',
label: 'Geohash',
},
],
},
})
.addTextInput({
path: 'fieldMapping.metricField',
name: 'Metric Field',
defaultValue: defaultOptions.fieldMapping.metricField,
})
.addTextInput({
path: 'fieldMapping.latitudeField',
name: 'Latitude Field',
defaultValue: defaultOptions.fieldMapping.latitudeField,
showIf: (config) =>
config.queryFormat.locationType === 'coordinates',
})
.addTextInput({
path: 'fieldMapping.longitudeField',
name: 'Longitude Field',
defaultValue: defaultOptions.fieldMapping.longitudeField,
showIf: (config) =>
config.queryFormat.locationType === 'coordinates',
})
.addTextInput({
path: 'fieldMapping.geohashField',
name: 'Geohash Field',
defaultValue: defaultOptions.fieldMapping.geohashField,
showIf: (config) =>
config.queryFormat.locationType === 'geohash',
});
},
// fill in the default values
defaultOptions,
};
/**
* Function that scales the circle size depending on the current data and user defined configurations
* Returns the scaled value in the range of min and max circle size
* Ex. If the minSize and maxSize were 5, 15: all values returned will be between 5~15
*/
function calcCircleSize(calcs: FieldCalcs, value: number, minSize: number, maxSize: number) {
if (calcs.range === 0) {
return maxSize;
}
const dataFactor = (value - calcs.min) / calcs.max;
const circleSizeRange = maxSize - minSize;
return circleSizeRange * dataFactor + minSize;
};

View File

@ -1,12 +1,12 @@
import { circlesLayer } from './circlesOverlay';
import { geojsonMapper } from './geojsonMapper';
import { lastPointTracker } from './lastPointTracker';
import { worldmapBehaviorLayer } from './worldmapBehavior';
/**
* Registry for layer handlers
*/
export const dataLayers = [
worldmapBehaviorLayer, // mimic the existing worldmap
circlesLayer,
lastPointTracker,
geojsonMapper, // dummy for now
];

View File

@ -0,0 +1,84 @@
import { DataFrame, DataFrameView } from '@grafana/data';
import { Point } from 'ol/geom';
import { fromLonLat } from 'ol/proj';
import { FieldMappingOptions, QueryFormat } from '../../types'
/**
* Function that formats dataframe into a Point[]
*/
export function dataFrameToPoints(frame: DataFrame, fieldMapping: FieldMappingOptions, queryFormat: QueryFormat): Point[] {
let points: Point[] = [];
if (frame && frame.length > 0) {
// For each data point, create a Point
const view = new DataFrameView(frame);
view.forEach(row => {
let lng;
let lat;
// Coordinate Data
if (queryFormat.locationType === "coordinates") {
lng = row[fieldMapping.longitudeField];
lat = row[fieldMapping.latitudeField];
}
// Geohash Data
else if (queryFormat.locationType === "geohash"){
const encodedGeohash = row[fieldMapping.geohashField];
const decodedGeohash = decodeGeohash(encodedGeohash);
lng = decodedGeohash.longitude;
lat = decodedGeohash.latitude;
}
points.push(new Point(fromLonLat([lng,lat])));
});
}
return points;
}
/**
* Function that decodes input geohash into latitude and longitude
*/
function decodeGeohash(geohash: string) {
if (!geohash || geohash.length === 0) {
throw new Error('Missing geohash value');
}
const BITS = [16, 8, 4, 2, 1];
const BASE32 = '0123456789bcdefghjkmnpqrstuvwxyz';
let isEven = true;
const lat: number[] = [];
const lon: number[] = [];
lat[0] = -90.0;
lat[1] = 90.0;
lon[0] = -180.0;
lon[1] = 180.0;
let base32Decoded: number;
geohash.split('').forEach((item: string) => {
base32Decoded = BASE32.indexOf(item);
BITS.forEach(mask => {
if (isEven) {
refineInterval(lon, base32Decoded, mask);
}
else {
refineInterval(lat, base32Decoded, mask);
}
isEven = !isEven;
});
});
const latCenter = (lat[0] + lat[1]) / 2;
const lonCenter = (lon[0] + lon[1]) / 2;
return { latitude: latCenter, longitude: lonCenter };
}
function refineInterval(interval: any[], base32Decoded: number, mask: number) {
/* tslint:disable no-bitwise*/
if (base32Decoded & mask) {
interval[0] = (interval[0] + interval[1]) / 2;
}
else {
interval[1] = (interval[0] + interval[1]) / 2;
}
}

View File

@ -1,70 +0,0 @@
import React from 'react';
import { MapLayerRegistryItem, MapLayerConfig, MapLayerHandler, PanelData, GrafanaTheme2 } from '@grafana/data';
import Map from 'ol/Map';
import Feature from 'ol/Feature';
import * as layer from 'ol/layer';
import * as source from 'ol/source';
import * as style from 'ol/style';
import {Point} from 'ol/geom';
import { fromLonLat } from 'ol/proj';
import tinycolor from 'tinycolor2';
import { SimpleLegend } from '../../components/SimpleLegend';
export interface WorldmapConfig {
// anything
}
const defaultOptions: WorldmapConfig = {
// icon: 'https://openlayers.org/en/latest/examples/data/icon.png',
};
export const worldmapBehaviorLayer: MapLayerRegistryItem<WorldmapConfig> = {
id: 'worldmap-behavior',
name: 'Worldmap behavior',
description: 'behave the same as worldmap plugin',
isBaseMap: false,
/**
* Function that configures transformation and returns a transformer
* @param options
*/
create: (map: Map, options: MapLayerConfig<WorldmapConfig>, theme: GrafanaTheme2): MapLayerHandler => {
// const config = { ...defaultOptions, ...options.config };
const vectorLayer = new layer.Vector({});
let legendInstance = <SimpleLegend txt={ `initalizing...`}/>;
let count = 0;
return {
init: () => vectorLayer,
legend: () => {
return legendInstance;
},
update: (data: PanelData) => {
count++;
const features:Feature[] = [];
for( let x=0; x<100; x+=20) {
for( let y=0; y<40; y+=10) {
const dot = new Feature({
geometry: new Point(fromLonLat([x,y])),
});
dot.setStyle(new style.Style({
image: new style.Circle({
fill: new style.Fill({
color: tinycolor({r:(x*2), g:(y*3), b:0}).toString(),
}),
radius: (4 + (y*0.5) + (x*0.1)),
})
}));
features.push(dot);
}
}
legendInstance = <SimpleLegend txt={ `Update: ${count}`} data={data}/>;
const vectorSource = new source.Vector({ features });
vectorLayer.setSource(vectorSource);
},
};
},
// fill in the default values
defaultOptions,
};

View File

@ -55,6 +55,12 @@ describe('Worldmap Migrations', () => {
"showLegend": true,
"showZoom": true,
},
"fieldMapping": Object {
"geohashField": "",
"latitudeField": "",
"longitudeField": "",
"metricField": "",
},
"layers": Array [],
"view": Object {
"center": Object {

View File

@ -42,6 +42,12 @@ export function worldmapToGeomapOptions(angular: any): { fieldConfig: FieldConfi
layers: [
// TODO? depends on current configs
],
fieldMapping: {
metricField: '',
geohashField: '',
latitudeField: '',
longitudeField: '',
},
};
let v = asNumber(angular.decimals);

View File

@ -49,4 +49,16 @@ export interface GeomapPanelOptions {
controls: ControlsOptions;
basemap: MapLayerConfig;
layers: MapLayerConfig[];
fieldMapping: FieldMappingOptions;
}
export interface FieldMappingOptions {
metricField: string;
geohashField: string;
latitudeField: string;
longitudeField: string;
}
export interface QueryFormat {
locationType: string;
}

View File

@ -0,0 +1,18 @@
State, Lat,Lng,DestLocation,Count,Price
Alaska, 61.3850,-152.2683,bdg,5,500
Alabama, 32.7990,-86.8073,djf,3,300
Arizona, 33.7712,-111.3877,9w0,10,150
California, 36.1700,-119.7462,9q6,12,250
Colorado, 39.0646,-105.3272,9wv,1,600
Florida, 27.8333,-81.7170,dhv,5,500
Iowa, 42.0046,-93.2140,9zm,7,700
Illinois, 40.3363,-89.0022,dp0,1,400
Indiana, 39.8647,-86.2604,dp4,5,540
Kentucky, 37.6690,-84.6514,dne,6,630
Massachusetts, 42.2373,-71.5314,drt,11,460
Michigan, 43.3504,-84.5603,dpe,2,250
North Carolina, 35.6411,-79.8431,dnr,5,500
New Jersey, 40.3140,-74.5089,dr5,12,400
New Mexico, 34.8375,-106.2371,9wh,15,800
Ohio, 40.3736,-82.7755,dpj,9,930
Oregon, 44.5672,-122.1269,9rc,3,360
1 State Lat Lng DestLocation Count Price
2 Alaska 61.3850 -152.2683 bdg 5 500
3 Alabama 32.7990 -86.8073 djf 3 300
4 Arizona 33.7712 -111.3877 9w0 10 150
5 California 36.1700 -119.7462 9q6 12 250
6 Colorado 39.0646 -105.3272 9wv 1 600
7 Florida 27.8333 -81.7170 dhv 5 500
8 Iowa 42.0046 -93.2140 9zm 7 700
9 Illinois 40.3363 -89.0022 dp0 1 400
10 Indiana 39.8647 -86.2604 dp4 5 540
11 Kentucky 37.6690 -84.6514 dne 6 630
12 Massachusetts 42.2373 -71.5314 drt 11 460
13 Michigan 43.3504 -84.5603 dpe 2 250
14 North Carolina 35.6411 -79.8431 dnr 5 500
15 New Jersey 40.3140 -74.5089 dr5 12 400
16 New Mexico 34.8375 -106.2371 9wh 15 800
17 Ohio 40.3736 -82.7755 dpj 9 930
18 Oregon 44.5672 -122.1269 9rc 3 360