Geomap: Minor style fixes (#38532)

* Fixed hover font-weight, option casing, and added simple test dashboard with 3 panels

* Update theme colors

* Style tweaks to legend

* Updated

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
Torkel Ödegaard 2021-08-26 10:17:03 +02:00 committed by GitHub
parent 12320dda3c
commit d5ed4e9c8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 514 additions and 98 deletions

View File

@ -5,7 +5,7 @@ pkg/
node_modules
public/vendor/
vendor/
data/
/data/
e2e/tmp
public/build/
public/sass/*.generated.scss

View File

@ -0,0 +1,404 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- 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,
"gnetId": null,
"graphTooltip": 0,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-GrYlRd"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 9,
"x": 0,
"y": 0
},
"id": 62,
"options": {
"basemap": {
"config": {},
"type": "default"
},
"controls": {
"mouseWheelZoom": true,
"showAttribution": true,
"showDebug": false,
"showScale": false,
"showZoom": true
},
"layers": [
{
"config": {
"color": {
"field": "Price",
"fixed": "dark-green"
},
"fillOpacity": 0.4,
"shape": "circle",
"showLegend": true,
"size": {
"field": "Count",
"fixed": 5,
"max": 15,
"min": 2
}
},
"location": {
"gazetteer": "public/gazetteer/usa-states.json",
"lookup": "State",
"mode": "auto"
},
"type": "markers"
}
],
"view": {
"id": "coords",
"lat": 38.297683,
"lon": -99.228359,
"shared": true,
"zoom": 3.98
}
},
"targets": [
{
"csvFileName": "flight_info_by_state.csv",
"refId": "A",
"scenarioId": "csv_file"
}
],
"title": "Size, color mapped to different fields + share view",
"type": "geomap"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
},
{
"color": "#EAB839",
"value": 90
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 9,
"x": 9,
"y": 0
},
"id": 66,
"options": {
"basemap": {
"config": {},
"type": "default"
},
"controls": {
"mouseWheelZoom": true,
"showAttribution": true,
"showDebug": false,
"showScale": false,
"showZoom": true
},
"layers": [
{
"config": {
"color": {
"field": "Price",
"fixed": "dark-green"
},
"fillOpacity": 0.4,
"shape": "circle",
"showLegend": true,
"size": {
"field": "Count",
"fixed": 5,
"max": 15,
"min": 2
}
},
"location": {
"gazetteer": "public/gazetteer/usa-states.json",
"lookup": "State",
"mode": "auto"
},
"type": "markers"
}
],
"view": {
"id": "coords",
"lat": 38.297683,
"lon": -99.228359,
"shared": true,
"zoom": 3.98
}
},
"targets": [
{
"csvFileName": "flight_info_by_state.csv",
"refId": "A",
"scenarioId": "csv_file"
}
],
"title": "Thresholds legend",
"type": "geomap"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-BlYlRd"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 9,
"x": 0,
"y": 11
},
"id": 63,
"options": {
"basemap": {
"config": {},
"type": "default"
},
"controls": {
"mouseWheelZoom": true,
"showAttribution": true,
"showDebug": false,
"showScale": false,
"showZoom": true
},
"layers": [
{
"config": {
"blur": 27,
"radius": 25,
"weight": {
"field": "Count",
"fixed": 1,
"max": 1,
"min": 0
}
},
"location": {
"gazetteer": "public/gazetteer/usa-states.json",
"lookup": "State",
"mode": "auto"
},
"type": "heatmap"
}
],
"view": {
"id": "coords",
"lat": 38.251497,
"lon": -100.932144,
"shared": false,
"zoom": 4.15
}
},
"targets": [
{
"csvFileName": "flight_info_by_state.csv",
"refId": "A",
"scenarioId": "csv_file"
}
],
"title": "Heatmap data layer",
"transformations": [],
"type": "geomap"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-GrYlRd"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 9,
"x": 9,
"y": 11
},
"id": 65,
"options": {
"basemap": {
"config": {
"server": "world-imagery"
},
"type": "esri-xyz"
},
"controls": {
"mouseWheelZoom": true,
"showAttribution": true,
"showDebug": false,
"showScale": false,
"showZoom": true
},
"layers": [
{
"config": {
"color": {
"fixed": "#ff001e"
},
"fillOpacity": 0.4,
"shape": "star",
"showLegend": true,
"size": {
"field": "Count",
"fixed": 5,
"max": 15,
"min": 2
}
},
"location": {
"gazetteer": "public/gazetteer/usa-states.json",
"lookup": "State",
"mode": "auto"
},
"type": "markers"
}
],
"view": {
"id": "coords",
"lat": 40.159084,
"lon": -96.508021,
"shared": true,
"zoom": 3.83
}
},
"targets": [
{
"csvFileName": "flight_info_by_state.csv",
"refId": "A",
"scenarioId": "csv_file"
}
],
"title": "Base layer ArcGIS wold imagery + star shape + share view",
"type": "geomap"
}
],
"refresh": "",
"schemaVersion": 30,
"style": "dark",
"tags": [
"gdev",
"panel-tests"
],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
]
},
"timezone": "",
"title": "Panel Tests - Geomap",
"uid": "2xuwrgV7z",
"version": 5
}

View File

@ -46,8 +46,8 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
infoWrap: css`
padding: 8px;
th {
font-weight: bold;
padding: 2px 10px 2px 0px;
font-weight: ${theme.typography.fontWeightMedium};
padding: ${theme.spacing(0.25, 2)};
}
`,
highlight: css`

View File

@ -60,7 +60,7 @@ export const LayerEditor: FC<LayerEditorProps> = ({ options, onChange, data, fil
})
.addFieldNamePicker({
path: 'location.latitude',
name: 'Latitude Field',
name: 'Latitude field',
settings: {
filter: (f: Field) => f.type === FieldType.number,
noFieldsMessage: 'No numeric fields found',
@ -69,7 +69,7 @@ export const LayerEditor: FC<LayerEditorProps> = ({ options, onChange, data, fil
})
.addFieldNamePicker({
path: 'location.longitude',
name: 'Longitude Field',
name: 'Longitude field',
settings: {
filter: (f: Field) => f.type === FieldType.number,
noFieldsMessage: 'No numeric fields found',
@ -78,7 +78,7 @@ export const LayerEditor: FC<LayerEditorProps> = ({ options, onChange, data, fil
})
.addFieldNamePicker({
path: 'location.geohash',
name: 'Geohash Field',
name: 'Geohash field',
settings: {
filter: (f: Field) => f.type === FieldType.string,
noFieldsMessage: 'No strings fields found',
@ -89,7 +89,7 @@ export const LayerEditor: FC<LayerEditorProps> = ({ options, onChange, data, fil
})
.addFieldNamePicker({
path: 'location.lookup',
name: 'Lookup Field',
name: 'Lookup field',
settings: {
filter: (f: Field) => f.type === FieldType.string,
noFieldsMessage: 'No strings fields found',

View File

@ -2,7 +2,6 @@ import { css } from '@emotion/react';
import { GrafanaTheme2 } from '@grafana/data';
import 'ol/ol.css';
import tinycolor from 'tinycolor2';
/**
* Will be loaded *after* the css above
@ -45,11 +44,9 @@ export function getGlobalStyles(theme: GrafanaTheme2) {
// border: 2px dotted rgba(0,60,136,0.7);
// }
const bg = tinycolor(theme.v1.colors.panelBg);
const button = tinycolor(theme.colors.secondary.main);
return css`
.ol-scale-line {
background: ${bg.setAlpha(0.7).toRgbString()}; // rgba(0,60,136,0.3);
background: ${theme.colors.border.weak}; // rgba(0,60,136,0.3);
}
.ol-scale-line-inner {
border: 1px solid ${theme.colors.text.primary}; // #eee;
@ -57,28 +54,27 @@ export function getGlobalStyles(theme: GrafanaTheme2) {
color: ${theme.colors.text.primary}; // #eee;
}
.ol-control {
background-color: ${bg.setAlpha(0.4).toRgbString()}; //rgba(255,255,255,0.4);
background-color: ${theme.colors.background.secondary}; //rgba(255,255,255,0.4);
}
.ol-control:hover {
background-color: ${bg.setAlpha(0.6).toRgbString()}; // rgba(255,255,255,0.6);
background-color: ${theme.colors.action.hover}; // rgba(255,255,255,0.6);
}
.ol-control button {
color: ${bg.setAlpha(0.8).toRgbString()}; // white;
background-color: ${button.setAlpha(0.5).toRgbString()}; // rgba(0,60,136,0.5);
color: ${theme.colors.secondary.text}; // white;
background-color: ${theme.colors.secondary.main}; // rgba(0,60,136,0.5);
}
.ol-control button:hover {
background-color: ${button.setAlpha(0.7).toRgbString()}; // rgba(0,60,136,0.7);
background-color: ${theme.colors.secondary.shade}; // rgba(0,60,136,0.5);
}
.ol-control button:focus {
// same as button
background-color: ${button.setAlpha(0.5).toRgbString()}; // rgba(0,60,136,0.5);
background-color: ${theme.colors.secondary.main}; // rgba(0,60,136,0.5);
}
.ol-attribution ul {
color: ${theme.colors.text.primary}; // #000;
text-shadow: 0 0 0px #fff; // removes internal styling!
text-shadow: none;
}
.ol-attribution:not(.ol-collapsed) {
background-color: ${bg.setAlpha(0.8).toRgbString()}; // rgba(255,255,255,0.8);
background-color: ${theme.colors.background.secondary}; // rgba(255,255,255,0.8);
}
`;
}

View File

@ -1,6 +1,6 @@
import React from 'react';
import { Label, stylesFactory } from '@grafana/ui';
import { formattedValueToString, getFieldColorModeForField, GrafanaTheme } from '@grafana/data';
import { Label, stylesFactory, useTheme2 } from '@grafana/ui';
import { formattedValueToString, getFieldColorModeForField, GrafanaTheme2 } from '@grafana/data';
import { css } from '@emotion/css';
import { config } from 'app/core/config';
import { DimensionSupplier } from 'app/features/dimensions';
@ -10,21 +10,22 @@ export interface MarkersLegendProps {
color?: DimensionSupplier<string>;
size?: DimensionSupplier<number>;
}
export function MarkersLegend(props: MarkersLegendProps) {
const { color } = props;
const theme = useTheme2();
if (!color || (!color.field && color.fixed)) {
return (
<></>
)
return <></>;
}
const style = getStyles(config.theme);
const style = getStyles(theme);
const fmt = (v: any) => `${formattedValueToString(color.field!.display!(v))}`;
const colorMode = getFieldColorModeForField(color!.field!);
if (colorMode.isContinuous && colorMode.getColors) {
const colors = colorMode.getColors(config.theme2)
const colorRange = getMinMaxAndDelta(color.field!)
const colors = colorMode.getColors(config.theme2);
const colorRange = getMinMaxAndDelta(color.field!);
// TODO: explore showing mean on the gradiant scale
// const stats = reduceField({
// field: color.field!,
@ -36,13 +37,18 @@ export function MarkersLegend(props: MarkersLegendProps) {
// ]
// })
return <>
<Label>{color?.field?.name}</Label>
<div className={style.gradientContainer} style={{backgroundImage: `linear-gradient(to right, ${colors.map((c) => c).join(', ')}`}}>
<div>{fmt(colorRange.min)}</div>
<div>{fmt(colorRange.max)}</div>
</div>
</>
return (
<>
<Label>{color?.field?.name}</Label>
<div
className={style.gradientContainer}
style={{ backgroundImage: `linear-gradient(to right, ${colors.map((c) => c).join(', ')}` }}
>
<div style={{ color: theme.colors.getContrastText(colors[0]) }}>{fmt(colorRange.min)}</div>
<div style={{ color: theme.colors.getContrastText(colors[colors.length - 1]) }}>{fmt(colorRange.max)}</div>
</div>
</>
);
}
const thresholds = color.field?.config?.thresholds;
@ -54,7 +60,7 @@ export function MarkersLegend(props: MarkersLegendProps) {
<div className={style.infoWrap}>
{thresholds && (
<div className={style.legend}>
{thresholds.steps.map((step:any, idx:number) => {
{thresholds.steps.map((step: any, idx: number) => {
const next = thresholds!.steps[idx + 1];
let info = <span>?</span>;
if (idx === 0) {
@ -78,21 +84,20 @@ export function MarkersLegend(props: MarkersLegendProps) {
</div>
)}
</div>
)
);
}
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
infoWrap: css`
color: #999;
background: #CCCC;
background: ${theme.colors.background.secondary};
border-radius: 2px;
padding: 8px;
padding: ${theme.spacing(1)};
`,
legend: css`
line-height: 18px;
color: #555;
display: flex;
flex-direction: column;
font-size: ${theme.typography.bodySmall.fontSize};
i {
width: 18px;
@ -109,5 +114,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => ({
min-width: 200px;
display: flex;
justify-content: space-between;
`
font-size: ${theme.typography.bodySmall.fontSize};
padding: ${theme.spacing(0, 0.5)};
`,
}));

View File

@ -45,11 +45,11 @@ export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = {
return {
init: () => vectorLayer,
update: (data: PanelData) => {
console.log( "todo... find values matching the ID and update");
console.log('todo... find values matching the ID and update');
// Update each feature
source.getFeatures().forEach( f => {
console.log( "Find: ", f.getId(), f.getProperties() );
source.getFeatures().forEach((f) => {
console.log('Find: ', f.getId(), f.getProperties());
});
},
};

View File

@ -24,7 +24,7 @@ export interface HeatmapConfig {
const defaultOptions: HeatmapConfig = {
weight: {
fixed: 1,
min: 0,
min: 0,
max: 1,
},
blur: 15,
@ -76,8 +76,8 @@ export const heatmapLayer: MapLayerRegistryItem<HeatmapConfig> = {
// Get data points (latitude and longitude coordinates)
const info = dataFrameToPoints(frame, matchers);
if(info.warning) {
console.log( 'WARN', info.warning);
if (info.warning) {
console.log('WARN', info.warning);
return; // ???
}
@ -86,18 +86,18 @@ export const heatmapLayer: MapLayerRegistryItem<HeatmapConfig> = {
// Map each data value into new points
for (let i = 0; i < frame.length; i++) {
const cluster = new Feature({
geometry: info.points[i],
value: weightDim.get(i),
geometry: info.points[i],
value: weightDim.get(i),
});
vectorSource.addFeature(cluster);
};
}
vectorLayer.setSource(vectorSource);
// Set heatmap gradient colors
let colors = ['#00f', '#0ff', '#0f0', '#ff0', '#f00'];
// Either the configured field or the first numeric field value
const field = weightDim.field ?? frame.fields.find(field => field.type === FieldType.number);
const field = weightDim.field ?? frame.fields.find((field) => field.type === FieldType.number);
if (field) {
const colorMode = getFieldColorModeForField(field);
if (colorMode.isContinuous && colorMode.getColors) {
@ -123,7 +123,8 @@ export const heatmapLayer: MapLayerRegistryItem<HeatmapConfig> = {
max: 1,
hideRange: true, // Don't show the scale factor
},
defaultValue: { // Configured values
defaultValue: {
// Configured values
fixed: 1,
min: 0,
max: 1,
@ -135,9 +136,9 @@ export const heatmapLayer: MapLayerRegistryItem<HeatmapConfig> = {
name: 'Radius',
defaultValue: defaultOptions.radius,
settings: {
min: 1,
max: 50,
step: 1,
min: 1,
max: 50,
step: 1,
},
})
.addSliderInput({

View File

@ -7,8 +7,8 @@ import { lastPointTracker } from './lastPointTracker';
* Registry for layer handlers
*/
export const dataLayers = [
markersLayer,
heatmapLayer,
lastPointTracker,
geojsonMapper, // dummy for now
markersLayer,
heatmapLayer,
lastPointTracker,
geojsonMapper, // dummy for now
];

View File

@ -53,13 +53,13 @@ export const lastPointTracker: MapLayerRegistryItem<LastPointConfig> = {
const frame = data.series[0];
if (frame && frame.length) {
const info = dataFrameToPoints(frame, matchers);
if(info.warning) {
console.log( 'WARN', info.warning);
if (info.warning) {
console.log('WARN', info.warning);
return; // ???
}
if(info.points?.length) {
const last = info.points[info.points.length-1];
if (info.points?.length) {
const last = info.points[info.points.length - 1];
point.setGeometry(last);
}
}

View File

@ -1,6 +1,11 @@
import React, { ReactNode } from 'react';
import { MapLayerRegistryItem, MapLayerOptions, PanelData, GrafanaTheme2, FrameGeometrySourceMode } from '@grafana/data';
import {
MapLayerRegistryItem,
MapLayerOptions,
PanelData,
GrafanaTheme2,
FrameGeometrySourceMode,
} from '@grafana/data';
import Map from 'ol/Map';
import Feature from 'ol/Feature';
import * as layer from 'ol/layer';
@ -8,7 +13,12 @@ import * as source from 'ol/source';
import tinycolor from 'tinycolor2';
import { dataFrameToPoints, getLocationMatchers } from '../../utils/location';
import { ColorDimensionConfig, ScaleDimensionConfig, getScaledDimension, getColorDimension } from 'app/features/dimensions';
import {
ColorDimensionConfig,
ScaleDimensionConfig,
getScaledDimension,
getColorDimension,
} from 'app/features/dimensions';
import { ScaleDimensionEditor, ColorDimensionEditor } from 'app/features/dimensions/editors';
import { ObservablePropsWrapper } from '../../components/ObservablePropsWrapper';
import { MarkersLegend, MarkersLegendProps } from './MarkersLegend';
@ -31,23 +41,23 @@ const defaultOptions: MarkersConfig = {
max: 15,
},
color: {
fixed: 'dark-green', // picked from theme
fixed: 'dark-green', // picked from theme
},
fillOpacity: 0.4,
shape: 'circle',
showLegend: true,
};
export const MARKERS_LAYER_ID = "markers";
export const MARKERS_LAYER_ID = 'markers';
// Used by default when nothing is configured
export const defaultMarkersConfig:MapLayerOptions<MarkersConfig> = {
export const defaultMarkersConfig: MapLayerOptions<MarkersConfig> = {
type: MARKERS_LAYER_ID,
config: defaultOptions,
location: {
mode: FrameGeometrySourceMode.Auto,
}
}
},
};
/**
* Map layer configuration for circle overlay
@ -72,31 +82,27 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
...options?.config,
};
const legendProps= new ReplaySubject<MarkersLegendProps>(1);
let legend:ReactNode = null;
const legendProps = new ReplaySubject<MarkersLegendProps>(1);
let legend: ReactNode = null;
if (config.showLegend) {
legend = <ObservablePropsWrapper
watch={legendProps}
initialSubProps={{}}
child={MarkersLegend}
/>
legend = <ObservablePropsWrapper watch={legendProps} initialSubProps={{}} child={MarkersLegend} />;
}
const shape = markerMakers.getIfExists(config.shape) ?? circleMarker;
return {
init: () => vectorLayer,
legend: legend,
update: (data: PanelData) => {
if(!data.series?.length) {
if (!data.series?.length) {
return; // ignore empty
}
const features: Feature[] = [];
for(const frame of data.series) {
for (const frame of data.series) {
const info = dataFrameToPoints(frame, matchers);
if(info.warning) {
console.log( 'Could not find locations', info.warning);
if (info.warning) {
console.log('Could not find locations', info.warning);
continue; // ???
}
@ -114,7 +120,7 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
const radius = sizeDim.get(i);
// Create a new Feature for each point returned from dataFrameToPoints
const dot = new Feature( info.points[i] );
const dot = new Feature(info.points[i]);
dot.setProperties({
frame,
rowIndex: i,
@ -122,7 +128,7 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
dot.setStyle(shape!.make(color, fillColor, radius));
features.push(dot);
};
}
// Post updates to the legend component
if (legend) {
@ -149,7 +155,8 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
name: 'Marker Color',
editor: ColorDimensionEditor,
settings: {},
defaultValue: { // Configured values
defaultValue: {
// Configured values
fixed: 'grey',
},
})
@ -162,7 +169,8 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
min: 1,
max: 100, // possible in the UI
},
defaultValue: { // Configured values
defaultValue: {
// Configured values
fixed: 5,
min: 1,
max: 20,
@ -185,7 +193,7 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
max: 1,
step: 0.1,
},
showIf: (cfg) => (markerMakers.getIfExists((cfg as any).config?.shape)?.hasFill),
showIf: (cfg) => markerMakers.getIfExists((cfg as any).config?.shape)?.hasFill,
})
.addBooleanSwitch({
path: 'config.showLegend',

View File

@ -13,7 +13,7 @@ export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
.setPanelChangeHandler(mapPanelChangedHandler)
.useFieldConfig()
.setPanelOptions((builder) => {
let category = ['Map View'];
let category = ['Map view'];
builder.addCustomEditor({
category,
id: 'view',
@ -33,25 +33,25 @@ export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
});
builder.addCustomEditor({
category: ['Base Layer'],
category: ['Base layer'],
id: 'basemap',
path: 'basemap',
name: 'Base Layer',
name: 'Base layer',
editor: BaseLayerEditor,
defaultValue: DEFAULT_BASEMAP_CONFIG,
});
builder.addCustomEditor({
category: ['Data Layer'],
category: ['Data layer'],
id: 'layers',
path: 'layers',
name: 'Data Layer',
name: 'Data layer',
editor: DataLayersEditor,
defaultValue: [defaultMarkersConfig],
});
// The controls section
category = ['Map Controls'];
category = ['Map controls'];
builder
.addBooleanSwitch({
category,