mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
[v8.3.x] Backport Latest Geomap Commits (#42399)
This commit is contained in:
parent
d246e600b8
commit
30933fbb3a
436
devenv/dev-dashboards/panel-geomap/geomap_multi-layers.json
Normal file
436
devenv/dev-dashboards/panel-geomap/geomap_multi-layers.json
Normal file
@ -0,0 +1,436 @@
|
|||||||
|
{
|
||||||
|
"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,
|
||||||
|
"fiscalYearStartMonth": 0,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"id": 157,
|
||||||
|
"links": [],
|
||||||
|
"liveNow": false,
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 22,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 2,
|
||||||
|
"options": {
|
||||||
|
"basemap": {
|
||||||
|
"config": {},
|
||||||
|
"name": "Layer 0",
|
||||||
|
"type": "default"
|
||||||
|
},
|
||||||
|
"controls": {
|
||||||
|
"mouseWheelZoom": true,
|
||||||
|
"showAttribution": true,
|
||||||
|
"showDebug": false,
|
||||||
|
"showScale": false,
|
||||||
|
"showZoom": true
|
||||||
|
},
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"check": {
|
||||||
|
"operation": "eq",
|
||||||
|
"property": "name",
|
||||||
|
"value": "Greenland"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"color": {
|
||||||
|
"fixed": "light-blue"
|
||||||
|
},
|
||||||
|
"opacity": 0.4,
|
||||||
|
"rotation": {
|
||||||
|
"fixed": 0,
|
||||||
|
"max": 360,
|
||||||
|
"min": -360,
|
||||||
|
"mode": "mod"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"fixed": 5,
|
||||||
|
"max": 15,
|
||||||
|
"min": 2
|
||||||
|
},
|
||||||
|
"symbol": {
|
||||||
|
"fixed": "img/icons/marker/circle.svg",
|
||||||
|
"mode": "fixed"
|
||||||
|
},
|
||||||
|
"textConfig": {
|
||||||
|
"fontSize": 12,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"textAlign": "center",
|
||||||
|
"textBaseline": "middle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": {
|
||||||
|
"operation": "eq",
|
||||||
|
"property": "name",
|
||||||
|
"value": "Antarctica"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"color": {
|
||||||
|
"fixed": "#FCE2DE"
|
||||||
|
},
|
||||||
|
"opacity": 0.4,
|
||||||
|
"rotation": {
|
||||||
|
"fixed": 0,
|
||||||
|
"max": 360,
|
||||||
|
"min": -360,
|
||||||
|
"mode": "mod"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"fixed": 5,
|
||||||
|
"max": 15,
|
||||||
|
"min": 2
|
||||||
|
},
|
||||||
|
"symbol": {
|
||||||
|
"fixed": "img/icons/marker/circle.svg",
|
||||||
|
"mode": "fixed"
|
||||||
|
},
|
||||||
|
"textConfig": {
|
||||||
|
"fontSize": 12,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"textAlign": "center",
|
||||||
|
"textBaseline": "middle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": {
|
||||||
|
"operation": "eq",
|
||||||
|
"property": "name",
|
||||||
|
"value": "Canada"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"color": {
|
||||||
|
"fixed": "#37872D"
|
||||||
|
},
|
||||||
|
"opacity": 0.4,
|
||||||
|
"rotation": {
|
||||||
|
"fixed": 0,
|
||||||
|
"max": 360,
|
||||||
|
"min": -360,
|
||||||
|
"mode": "mod"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"fixed": 5,
|
||||||
|
"max": 15,
|
||||||
|
"min": 2
|
||||||
|
},
|
||||||
|
"symbol": {
|
||||||
|
"fixed": "img/icons/marker/circle.svg",
|
||||||
|
"mode": "fixed"
|
||||||
|
},
|
||||||
|
"textConfig": {
|
||||||
|
"fontSize": 12,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"textAlign": "center",
|
||||||
|
"textBaseline": "middle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check": {
|
||||||
|
"operation": "eq",
|
||||||
|
"property": "name",
|
||||||
|
"value": "Mexico"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"color": {
|
||||||
|
"fixed": "#1F60C4"
|
||||||
|
},
|
||||||
|
"opacity": 0.4,
|
||||||
|
"rotation": {
|
||||||
|
"fixed": 0,
|
||||||
|
"max": 360,
|
||||||
|
"min": -360,
|
||||||
|
"mode": "mod"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"fixed": 5,
|
||||||
|
"max": 15,
|
||||||
|
"min": 2
|
||||||
|
},
|
||||||
|
"symbol": {
|
||||||
|
"fixed": "img/icons/marker/circle.svg",
|
||||||
|
"mode": "fixed"
|
||||||
|
},
|
||||||
|
"textConfig": {
|
||||||
|
"fontSize": 12,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"textAlign": "center",
|
||||||
|
"textBaseline": "middle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"src": "public/maps/countries.geojson",
|
||||||
|
"style": {
|
||||||
|
"color": {
|
||||||
|
"fixed": "dark-green"
|
||||||
|
},
|
||||||
|
"opacity": 0.1,
|
||||||
|
"rotation": {
|
||||||
|
"fixed": 0,
|
||||||
|
"max": 360,
|
||||||
|
"min": -360,
|
||||||
|
"mode": "mod"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"fixed": 5,
|
||||||
|
"max": 15,
|
||||||
|
"min": 2
|
||||||
|
},
|
||||||
|
"symbol": {
|
||||||
|
"fixed": "img/icons/marker/circle.svg",
|
||||||
|
"mode": "fixed"
|
||||||
|
},
|
||||||
|
"textConfig": {
|
||||||
|
"fontSize": 12,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"textAlign": "center",
|
||||||
|
"textBaseline": "middle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Countries",
|
||||||
|
"type": "geojson"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"showLegend": true,
|
||||||
|
"style": {
|
||||||
|
"color": {
|
||||||
|
"fixed": "dark-blue"
|
||||||
|
},
|
||||||
|
"opacity": 0.4,
|
||||||
|
"rotation": {
|
||||||
|
"fixed": 0,
|
||||||
|
"max": 360,
|
||||||
|
"min": -360,
|
||||||
|
"mode": "mod"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"fixed": 5,
|
||||||
|
"max": 15,
|
||||||
|
"min": 2
|
||||||
|
},
|
||||||
|
"symbol": {
|
||||||
|
"fixed": "img/icons/marker/square.svg",
|
||||||
|
"mode": "fixed"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"field": "Count",
|
||||||
|
"fixed": "",
|
||||||
|
"mode": "field"
|
||||||
|
},
|
||||||
|
"textConfig": {
|
||||||
|
"fontSize": 16,
|
||||||
|
"offsetX": 10,
|
||||||
|
"offsetY": 10,
|
||||||
|
"textAlign": "center",
|
||||||
|
"textBaseline": "middle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"mode": "auto"
|
||||||
|
},
|
||||||
|
"name": "Flights",
|
||||||
|
"type": "markers"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"view": {
|
||||||
|
"id": "coords",
|
||||||
|
"lat": 42.826559,
|
||||||
|
"lon": -96.868893,
|
||||||
|
"zoom": 3.58
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pluginVersion": "8.3.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"csvFileName": "flight_info_by_state.csv",
|
||||||
|
"refId": "A",
|
||||||
|
"scenarioId": "csv_file"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Multi layers",
|
||||||
|
"type": "geomap"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 22,
|
||||||
|
"w": 12,
|
||||||
|
"x": 12,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 3,
|
||||||
|
"options": {
|
||||||
|
"basemap": {
|
||||||
|
"config": {},
|
||||||
|
"name": "Layer 0",
|
||||||
|
"type": "default"
|
||||||
|
},
|
||||||
|
"controls": {
|
||||||
|
"mouseWheelZoom": true,
|
||||||
|
"showAttribution": true,
|
||||||
|
"showDebug": false,
|
||||||
|
"showScale": false,
|
||||||
|
"showZoom": true
|
||||||
|
},
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"showLegend": true,
|
||||||
|
"style": {
|
||||||
|
"color": {
|
||||||
|
"fixed": "dark-green"
|
||||||
|
},
|
||||||
|
"opacity": 0.7,
|
||||||
|
"rotation": {
|
||||||
|
"field": "Lng",
|
||||||
|
"fixed": 0,
|
||||||
|
"max": 360,
|
||||||
|
"min": -360,
|
||||||
|
"mode": "mod"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"fixed": 10,
|
||||||
|
"max": 15,
|
||||||
|
"min": 2
|
||||||
|
},
|
||||||
|
"symbol": {
|
||||||
|
"fixed": "img/icons/marker/plane.svg",
|
||||||
|
"mode": "fixed"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"field": "Lng",
|
||||||
|
"fixed": "",
|
||||||
|
"mode": "field"
|
||||||
|
},
|
||||||
|
"textConfig": {
|
||||||
|
"fontSize": 12,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 18,
|
||||||
|
"textAlign": "center",
|
||||||
|
"textBaseline": "middle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Layer 2",
|
||||||
|
"type": "markers"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"view": {
|
||||||
|
"id": "coords",
|
||||||
|
"lat": 42.826559,
|
||||||
|
"lon": -96.868893,
|
||||||
|
"zoom": 3.58
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pluginVersion": "8.3.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"csvFileName": "flight_info_by_state.csv",
|
||||||
|
"refId": "A",
|
||||||
|
"scenarioId": "csv_file"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Markers",
|
||||||
|
"type": "geomap"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"schemaVersion": 33,
|
||||||
|
"style": "dark",
|
||||||
|
"tags": [
|
||||||
|
"gdev",
|
||||||
|
"geomap",
|
||||||
|
"panel-tests"
|
||||||
|
],
|
||||||
|
"templating": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-6h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {},
|
||||||
|
"timezone": "",
|
||||||
|
"title": "Panel Tests - Geomap Multi Layers",
|
||||||
|
"uid": "2jFpEvp7z",
|
||||||
|
"version": 8,
|
||||||
|
"weekStart": ""
|
||||||
|
}
|
@ -68,7 +68,8 @@ export function useFieldDisplayNames(data: DataFrame[], filter?: (field: Field)
|
|||||||
export function useSelectOptions(
|
export function useSelectOptions(
|
||||||
displayNames: FrameFieldsDisplayNames,
|
displayNames: FrameFieldsDisplayNames,
|
||||||
currentName?: string,
|
currentName?: string,
|
||||||
firstItem?: SelectableValue<string>
|
firstItem?: SelectableValue<string>,
|
||||||
|
fieldType?: string
|
||||||
): Array<SelectableValue<string>> {
|
): Array<SelectableValue<string>> {
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
let found = false;
|
let found = false;
|
||||||
@ -81,11 +82,13 @@ export function useSelectOptions(
|
|||||||
found = true;
|
found = true;
|
||||||
}
|
}
|
||||||
const field = displayNames.fields.get(name);
|
const field = displayNames.fields.get(name);
|
||||||
options.push({
|
if (!fieldType || fieldType === field?.type) {
|
||||||
value: name,
|
options.push({
|
||||||
label: name,
|
value: name,
|
||||||
icon: field ? getFieldTypeIcon(field) : undefined,
|
label: name,
|
||||||
});
|
icon: field ? getFieldTypeIcon(field) : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const name of displayNames.raw) {
|
for (const name of displayNames.raw) {
|
||||||
if (!displayNames.display.has(name)) {
|
if (!displayNames.display.has(name)) {
|
||||||
@ -106,5 +109,5 @@ export function useSelectOptions(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return options;
|
return options;
|
||||||
}, [displayNames, currentName, firstItem]);
|
}, [displayNames, currentName, firstItem, fieldType]);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { Input } from '@grafana/ui';
|
import { Field, Input } from '@grafana/ui';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value?: number;
|
value?: number;
|
||||||
@ -13,16 +13,18 @@ interface Props {
|
|||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
text: string;
|
text: string;
|
||||||
|
inputCorrected: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is an Input field that will call `onChange` for blur and enter
|
* This is an Input field that will call `onChange` for blur and enter
|
||||||
*/
|
*/
|
||||||
export class NumberInput extends PureComponent<Props, State> {
|
export class NumberInput extends PureComponent<Props, State> {
|
||||||
state: State = { text: '' };
|
state: State = { text: '', inputCorrected: false };
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
...this.state,
|
||||||
text: isNaN(this.props.value!) ? '' : `${this.props.value}`,
|
text: isNaN(this.props.value!) ? '' : `${this.props.value}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -30,6 +32,7 @@ export class NumberInput extends PureComponent<Props, State> {
|
|||||||
componentDidUpdate(oldProps: Props) {
|
componentDidUpdate(oldProps: Props) {
|
||||||
if (this.props.value !== oldProps.value) {
|
if (this.props.value !== oldProps.value) {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
...this.state,
|
||||||
text: isNaN(this.props.value!) ? '' : `${this.props.value}`,
|
text: isNaN(this.props.value!) ? '' : `${this.props.value}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -42,11 +45,30 @@ export class NumberInput extends PureComponent<Props, State> {
|
|||||||
value = e.currentTarget.valueAsNumber;
|
value = e.currentTarget.valueAsNumber;
|
||||||
}
|
}
|
||||||
this.props.onChange(value);
|
this.props.onChange(value);
|
||||||
|
this.setState({ ...this.state, inputCorrected: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
onChange = (e: React.FocusEvent<HTMLInputElement>) => {
|
onChange = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||||
|
let newValue: string | undefined = undefined;
|
||||||
|
let corrected = false;
|
||||||
|
const min = this.props.min;
|
||||||
|
const max = this.props.max;
|
||||||
|
const currValue = e.currentTarget.valueAsNumber;
|
||||||
|
if (!Number.isNaN(currValue)) {
|
||||||
|
if (min != null && currValue < min) {
|
||||||
|
newValue = min.toString();
|
||||||
|
corrected = true;
|
||||||
|
} else if (max != null && currValue > max) {
|
||||||
|
newValue = max.toString();
|
||||||
|
corrected = true;
|
||||||
|
} else {
|
||||||
|
newValue = e.currentTarget.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
text: e.currentTarget.value,
|
...this.state,
|
||||||
|
text: newValue ? newValue : '',
|
||||||
|
inputCorrected: corrected,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -58,20 +80,22 @@ export class NumberInput extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { placeholder } = this.props;
|
const { placeholder } = this.props;
|
||||||
const { text } = this.state;
|
const { text, inputCorrected } = this.state;
|
||||||
return (
|
return (
|
||||||
<Input
|
<Field invalid={inputCorrected} error={inputCorrected ? 'Cannot go beyond range' : ''}>
|
||||||
type="number"
|
<Input
|
||||||
min={this.props.min}
|
type="number"
|
||||||
max={this.props.max}
|
min={this.props.min}
|
||||||
step={this.props.step}
|
max={this.props.max}
|
||||||
autoFocus={this.props.autoFocus}
|
step={this.props.step}
|
||||||
value={text}
|
autoFocus={this.props.autoFocus}
|
||||||
onChange={this.onChange}
|
value={text}
|
||||||
onBlur={this.onBlur}
|
onChange={this.onChange}
|
||||||
onKeyPress={this.onKeyPress}
|
onBlur={this.onBlur}
|
||||||
placeholder={placeholder}
|
onKeyPress={this.onKeyPress}
|
||||||
/>
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
116
public/app/features/dimensions/editors/ScalarDimensionEditor.tsx
Normal file
116
public/app/features/dimensions/editors/ScalarDimensionEditor.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import React, { FC, useCallback } from 'react';
|
||||||
|
import { FieldType, GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data';
|
||||||
|
import { ScalarDimensionConfig, ScalarDimensionMode, ScalarDimensionOptions } from '../types';
|
||||||
|
import { InlineField, InlineFieldRow, RadioButtonGroup, Select, useStyles2 } from '@grafana/ui';
|
||||||
|
import { useFieldDisplayNames, useSelectOptions } from '@grafana/ui/src/components/MatchersUI/utils';
|
||||||
|
import { NumberInput } from './NumberInput';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
|
const fixedValueOption: SelectableValue<string> = {
|
||||||
|
label: 'Fixed value',
|
||||||
|
value: '_____fixed_____',
|
||||||
|
};
|
||||||
|
|
||||||
|
const scalarOptions = [
|
||||||
|
{ label: 'Mod', value: ScalarDimensionMode.Mod, description: 'Use field values, mod from max' },
|
||||||
|
{ label: 'Clamped', value: ScalarDimensionMode.Clamped, description: 'Use field values, clamped to max and min' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ScalarDimensionEditor: FC<StandardEditorProps<ScalarDimensionConfig, ScalarDimensionOptions, any>> = (
|
||||||
|
props
|
||||||
|
) => {
|
||||||
|
const { value, context, onChange, item } = props;
|
||||||
|
const { settings } = item;
|
||||||
|
|
||||||
|
const DEFAULT_VALUE = 0;
|
||||||
|
|
||||||
|
const fieldName = value?.field;
|
||||||
|
const isFixed = Boolean(!fieldName);
|
||||||
|
const names = useFieldDisplayNames(context.data);
|
||||||
|
const selectOptions = useSelectOptions(names, fieldName, fixedValueOption, FieldType.number);
|
||||||
|
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
const onSelectChange = useCallback(
|
||||||
|
(selection: SelectableValue<string>) => {
|
||||||
|
const field = selection.value;
|
||||||
|
if (field && field !== fixedValueOption.value) {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
field,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const fixed = value.fixed ?? DEFAULT_VALUE;
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
field: undefined,
|
||||||
|
fixed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onChange, value]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onModeChange = useCallback(
|
||||||
|
(mode) => {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
mode,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[onChange, value]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onValueChange = useCallback(
|
||||||
|
(v: number | undefined) => {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
field: undefined,
|
||||||
|
fixed: v ?? DEFAULT_VALUE,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[onChange, value]
|
||||||
|
);
|
||||||
|
|
||||||
|
const val = value ?? {};
|
||||||
|
const mode = value?.mode ?? ScalarDimensionMode.Mod;
|
||||||
|
const selectedOption = isFixed ? fixedValueOption : selectOptions.find((v) => v.value === fieldName);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<InlineFieldRow>
|
||||||
|
<InlineField label="Limit" labelWidth={8} grow={true}>
|
||||||
|
<RadioButtonGroup value={mode} options={scalarOptions} onChange={onModeChange} fullWidth />
|
||||||
|
</InlineField>
|
||||||
|
</InlineFieldRow>
|
||||||
|
<Select
|
||||||
|
menuShouldPortal
|
||||||
|
value={selectedOption}
|
||||||
|
options={selectOptions}
|
||||||
|
onChange={onSelectChange}
|
||||||
|
noOptionsMessage="No fields found"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.range}>
|
||||||
|
{isFixed && (
|
||||||
|
<InlineFieldRow>
|
||||||
|
<InlineField label="Value" labelWidth={8} grow={true}>
|
||||||
|
<NumberInput
|
||||||
|
value={val?.fixed ?? DEFAULT_VALUE}
|
||||||
|
onChange={onValueChange}
|
||||||
|
max={settings?.max}
|
||||||
|
min={settings?.min}
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
|
</InlineFieldRow>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
range: css`
|
||||||
|
padding-top: 8px;
|
||||||
|
`,
|
||||||
|
});
|
@ -2,4 +2,5 @@ export * from './ColorDimensionEditor';
|
|||||||
export * from './IconSelector';
|
export * from './IconSelector';
|
||||||
export * from './ResourceDimensionEditor';
|
export * from './ResourceDimensionEditor';
|
||||||
export * from './ScaleDimensionEditor';
|
export * from './ScaleDimensionEditor';
|
||||||
|
export * from './ScalarDimensionEditor';
|
||||||
export * from './TextDimensionEditor';
|
export * from './TextDimensionEditor';
|
||||||
|
@ -6,3 +6,4 @@ export * from './text';
|
|||||||
export * from './utils';
|
export * from './utils';
|
||||||
export * from './resource';
|
export * from './resource';
|
||||||
export * from './context';
|
export * from './context';
|
||||||
|
export * from './scalar';
|
||||||
|
94
public/app/features/dimensions/scalar.test.ts
Normal file
94
public/app/features/dimensions/scalar.test.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { ArrayVector, DataFrame, FieldType } from '@grafana/data';
|
||||||
|
import { ScalarDimensionMode } from '.';
|
||||||
|
import { getScalarDimension } from './scalar';
|
||||||
|
|
||||||
|
describe('scalar dimensions', () => {
|
||||||
|
it('handles string field', () => {
|
||||||
|
const values = ['-720', '10', '540', '90', '-210'];
|
||||||
|
const frame: DataFrame = {
|
||||||
|
name: 'a',
|
||||||
|
length: values.length,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'test',
|
||||||
|
type: FieldType.number,
|
||||||
|
values: new ArrayVector(values),
|
||||||
|
config: {
|
||||||
|
min: -720,
|
||||||
|
max: 540,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const supplier = getScalarDimension(frame, {
|
||||||
|
min: -360,
|
||||||
|
max: 360,
|
||||||
|
field: 'test',
|
||||||
|
fixed: 0,
|
||||||
|
mode: ScalarDimensionMode.Clamped,
|
||||||
|
});
|
||||||
|
|
||||||
|
const clamped = frame.fields[0].values.toArray().map((k, i) => supplier.get(i));
|
||||||
|
expect(clamped).toEqual([0, 0, 0, 0, 0]);
|
||||||
|
});
|
||||||
|
it('clamps out of range values', () => {
|
||||||
|
const values = [-720, 10, 540, 90, -210];
|
||||||
|
const frame: DataFrame = {
|
||||||
|
name: 'a',
|
||||||
|
length: values.length,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'test',
|
||||||
|
type: FieldType.number,
|
||||||
|
values: new ArrayVector(values),
|
||||||
|
config: {
|
||||||
|
min: -720,
|
||||||
|
max: 540,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const supplier = getScalarDimension(frame, {
|
||||||
|
min: -360,
|
||||||
|
max: 360,
|
||||||
|
field: 'test',
|
||||||
|
fixed: 0,
|
||||||
|
mode: ScalarDimensionMode.Clamped,
|
||||||
|
});
|
||||||
|
|
||||||
|
const clamped = frame.fields[0].values.toArray().map((k, i) => supplier.get(i));
|
||||||
|
expect(clamped).toEqual([-360, 10, 360, 90, -210]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps remainder after divisible by max', () => {
|
||||||
|
const values = [-721, 10, 540, 390, -210];
|
||||||
|
const frame: DataFrame = {
|
||||||
|
name: 'a',
|
||||||
|
length: values.length,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'test',
|
||||||
|
type: FieldType.number,
|
||||||
|
values: new ArrayVector(values),
|
||||||
|
config: {
|
||||||
|
min: -721,
|
||||||
|
max: 540,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const supplier = getScalarDimension(frame, {
|
||||||
|
min: -360,
|
||||||
|
max: 360,
|
||||||
|
field: 'test',
|
||||||
|
fixed: 0,
|
||||||
|
mode: ScalarDimensionMode.Mod,
|
||||||
|
});
|
||||||
|
|
||||||
|
const remainder = frame.fields[0].values.toArray().map((k, i) => supplier.get(i));
|
||||||
|
expect(remainder).toEqual([-1, 10, 180, 30, -210]);
|
||||||
|
});
|
||||||
|
});
|
59
public/app/features/dimensions/scalar.ts
Normal file
59
public/app/features/dimensions/scalar.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { DataFrame, Field } from '@grafana/data';
|
||||||
|
import { DimensionSupplier, ScalarDimensionConfig, ScalarDimensionMode } from './types';
|
||||||
|
import { findField, getLastNotNullFieldValue } from './utils';
|
||||||
|
|
||||||
|
//---------------------------------------------------------
|
||||||
|
// Scalar dimension
|
||||||
|
//---------------------------------------------------------
|
||||||
|
export function getScalarDimension(
|
||||||
|
frame: DataFrame | undefined,
|
||||||
|
config: ScalarDimensionConfig
|
||||||
|
): DimensionSupplier<number> {
|
||||||
|
return getScalarDimensionForField(findField(frame, config?.field), config);
|
||||||
|
}
|
||||||
|
export function getScalarDimensionForField(
|
||||||
|
field: Field | undefined,
|
||||||
|
cfg: ScalarDimensionConfig
|
||||||
|
): DimensionSupplier<number> {
|
||||||
|
if (!field) {
|
||||||
|
const v = cfg.fixed ?? 0;
|
||||||
|
return {
|
||||||
|
isAssumed: Boolean(cfg.field?.length) || !cfg.fixed,
|
||||||
|
fixed: v,
|
||||||
|
value: () => v,
|
||||||
|
get: () => v,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//mod mode as default
|
||||||
|
let validated = (value: number) => {
|
||||||
|
return value % cfg.max;
|
||||||
|
};
|
||||||
|
|
||||||
|
//capped mode
|
||||||
|
if (cfg.mode === ScalarDimensionMode.Clamped) {
|
||||||
|
validated = (value: number) => {
|
||||||
|
if (value < cfg.min) {
|
||||||
|
return cfg.min;
|
||||||
|
}
|
||||||
|
if (value > cfg.max) {
|
||||||
|
return cfg.max;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const get = (i: number) => {
|
||||||
|
const v = field.values.get(i);
|
||||||
|
if (v === null || typeof v !== 'number') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return validated(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
field,
|
||||||
|
get,
|
||||||
|
value: () => getLastNotNullFieldValue(field),
|
||||||
|
};
|
||||||
|
}
|
@ -51,6 +51,21 @@ export interface ScaleDimensionOptions {
|
|||||||
hideRange?: boolean; // false
|
hideRange?: boolean; // false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ScalarDimensionMode {
|
||||||
|
Mod = 'mod',
|
||||||
|
Clamped = 'clamped',
|
||||||
|
}
|
||||||
|
export interface ScalarDimensionConfig extends BaseDimensionConfig<number> {
|
||||||
|
mode: ScalarDimensionMode;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScalarDimensionOptions {
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TextDimensionOptions {
|
export interface TextDimensionOptions {
|
||||||
// anything?
|
// anything?
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ import {
|
|||||||
ColorDimensionEditor,
|
ColorDimensionEditor,
|
||||||
ResourceDimensionEditor,
|
ResourceDimensionEditor,
|
||||||
ScaleDimensionEditor,
|
ScaleDimensionEditor,
|
||||||
|
ScalarDimensionEditor,
|
||||||
TextDimensionEditor,
|
TextDimensionEditor,
|
||||||
} from 'app/features/dimensions/editors';
|
} from 'app/features/dimensions/editors';
|
||||||
import {
|
import {
|
||||||
@ -27,14 +28,16 @@ import {
|
|||||||
ResourceFolderName,
|
ResourceFolderName,
|
||||||
TextDimensionConfig,
|
TextDimensionConfig,
|
||||||
defaultTextConfig,
|
defaultTextConfig,
|
||||||
|
ScalarDimensionConfig,
|
||||||
} from 'app/features/dimensions/types';
|
} from 'app/features/dimensions/types';
|
||||||
import { defaultStyleConfig, StyleConfig, TextAlignment, TextBaseline } from '../../style/types';
|
import { defaultStyleConfig, GeometryTypeId, StyleConfig, TextAlignment, TextBaseline } from '../../style/types';
|
||||||
import { styleUsesText } from '../../style/utils';
|
import { styleUsesText } from '../../style/utils';
|
||||||
import { LayerContentInfo } from '../../utils/getFeatures';
|
import { LayerContentInfo } from '../../utils/getFeatures';
|
||||||
|
|
||||||
export interface StyleEditorOptions {
|
export interface StyleEditorOptions {
|
||||||
layerInfo?: Observable<LayerContentInfo>;
|
layerInfo?: Observable<LayerContentInfo>;
|
||||||
simpleFixedValues?: boolean;
|
simpleFixedValues?: boolean;
|
||||||
|
displayRotation?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions, any>> = ({
|
export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions, any>> = ({
|
||||||
@ -43,6 +46,8 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
|||||||
onChange,
|
onChange,
|
||||||
item,
|
item,
|
||||||
}) => {
|
}) => {
|
||||||
|
const settings = item.settings;
|
||||||
|
|
||||||
const onSizeChange = (sizeValue: ScaleDimensionConfig | undefined) => {
|
const onSizeChange = (sizeValue: ScaleDimensionConfig | undefined) => {
|
||||||
onChange({ ...value, size: sizeValue });
|
onChange({ ...value, size: sizeValue });
|
||||||
};
|
};
|
||||||
@ -59,6 +64,10 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
|||||||
onChange({ ...value, opacity: opacityValue });
|
onChange({ ...value, opacity: opacityValue });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onRotationChange = (rotationValue: ScalarDimensionConfig | undefined) => {
|
||||||
|
onChange({ ...value, rotation: rotationValue });
|
||||||
|
};
|
||||||
|
|
||||||
const onTextChange = (textValue: TextDimensionConfig | undefined) => {
|
const onTextChange = (textValue: TextDimensionConfig | undefined) => {
|
||||||
onChange({ ...value, text: textValue });
|
onChange({ ...value, text: textValue });
|
||||||
};
|
};
|
||||||
@ -84,44 +93,60 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
|||||||
};
|
};
|
||||||
|
|
||||||
let featuresHavePoints = false;
|
let featuresHavePoints = false;
|
||||||
if (item.settings?.layerInfo) {
|
if (settings?.layerInfo) {
|
||||||
const propertyOptions = useObservable(item.settings?.layerInfo);
|
const propertyOptions = useObservable(settings?.layerInfo);
|
||||||
featuresHavePoints = propertyOptions?.geometryType === 'point';
|
featuresHavePoints = propertyOptions?.geometryType === GeometryTypeId.Point;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasTextLabel = styleUsesText(value);
|
const hasTextLabel = styleUsesText(value);
|
||||||
|
|
||||||
// Simple fixed value display
|
// Simple fixed value display
|
||||||
if (item.settings?.simpleFixedValues) {
|
if (settings?.simpleFixedValues) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{featuresHavePoints && (
|
{featuresHavePoints && (
|
||||||
<InlineFieldRow>
|
<>
|
||||||
<InlineField label={'Symbol'}>
|
<InlineFieldRow>
|
||||||
<ResourceDimensionEditor
|
<InlineField label={'Symbol'}>
|
||||||
value={value.symbol ?? defaultStyleConfig.symbol}
|
<ResourceDimensionEditor
|
||||||
|
value={value?.symbol ?? defaultStyleConfig.symbol}
|
||||||
|
context={context}
|
||||||
|
onChange={onSymbolChange}
|
||||||
|
item={
|
||||||
|
{
|
||||||
|
settings: {
|
||||||
|
resourceType: 'icon',
|
||||||
|
folderName: ResourceFolderName.Marker,
|
||||||
|
placeholderText: hasTextLabel ? 'Select a symbol' : 'Select a symbol or add a text label',
|
||||||
|
placeholderValue: defaultStyleConfig.symbol.fixed,
|
||||||
|
showSourceRadio: false,
|
||||||
|
},
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
|
</InlineFieldRow>
|
||||||
|
<Field label={'Rotation angle'}>
|
||||||
|
<ScalarDimensionEditor
|
||||||
|
value={value?.rotation ?? defaultStyleConfig.rotation}
|
||||||
context={context}
|
context={context}
|
||||||
onChange={onSymbolChange}
|
onChange={onRotationChange}
|
||||||
item={
|
item={
|
||||||
{
|
{
|
||||||
settings: {
|
settings: {
|
||||||
resourceType: 'icon',
|
min: defaultStyleConfig.rotation.min,
|
||||||
folderName: ResourceFolderName.Marker,
|
max: defaultStyleConfig.rotation.max,
|
||||||
placeholderText: hasTextLabel ? 'Select a symbol' : 'Select a symbol or add a text label',
|
|
||||||
placeholderValue: defaultStyleConfig.symbol.fixed,
|
|
||||||
showSourceRadio: false,
|
|
||||||
},
|
},
|
||||||
} as any
|
} as any
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</InlineField>
|
</Field>
|
||||||
</InlineFieldRow>
|
</>
|
||||||
)}
|
)}
|
||||||
<InlineFieldRow>
|
<InlineFieldRow>
|
||||||
<InlineField label="Color" labelWidth={10}>
|
<InlineField label="Color" labelWidth={10}>
|
||||||
<InlineLabel width={4}>
|
<InlineLabel width={4}>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
color={value.color?.fixed ?? defaultStyleConfig.color.fixed}
|
color={value?.color?.fixed ?? defaultStyleConfig.color.fixed}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
onColorChange({ fixed: v });
|
onColorChange({ fixed: v });
|
||||||
}}
|
}}
|
||||||
@ -132,7 +157,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
|||||||
<InlineFieldRow>
|
<InlineFieldRow>
|
||||||
<InlineField label="Opacity" labelWidth={10} grow>
|
<InlineField label="Opacity" labelWidth={10} grow>
|
||||||
<SliderValueEditor
|
<SliderValueEditor
|
||||||
value={value.opacity ?? defaultStyleConfig.opacity}
|
value={value?.opacity ?? defaultStyleConfig.opacity}
|
||||||
context={context}
|
context={context}
|
||||||
onChange={onOpacityChange}
|
onChange={onOpacityChange}
|
||||||
item={
|
item={
|
||||||
@ -155,7 +180,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
|||||||
<>
|
<>
|
||||||
<Field label={'Size'}>
|
<Field label={'Size'}>
|
||||||
<ScaleDimensionEditor
|
<ScaleDimensionEditor
|
||||||
value={value.size ?? defaultStyleConfig.size}
|
value={value?.size ?? defaultStyleConfig.size}
|
||||||
context={context}
|
context={context}
|
||||||
onChange={onSizeChange}
|
onChange={onSizeChange}
|
||||||
item={
|
item={
|
||||||
@ -170,7 +195,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
|||||||
</Field>
|
</Field>
|
||||||
<Field label={'Symbol'}>
|
<Field label={'Symbol'}>
|
||||||
<ResourceDimensionEditor
|
<ResourceDimensionEditor
|
||||||
value={value.symbol ?? defaultStyleConfig.symbol}
|
value={value?.symbol ?? defaultStyleConfig.symbol}
|
||||||
context={context}
|
context={context}
|
||||||
onChange={onSymbolChange}
|
onChange={onSymbolChange}
|
||||||
item={
|
item={
|
||||||
@ -188,7 +213,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
|||||||
</Field>
|
</Field>
|
||||||
<Field label={'Color'}>
|
<Field label={'Color'}>
|
||||||
<ColorDimensionEditor
|
<ColorDimensionEditor
|
||||||
value={value.color ?? defaultStyleConfig.color}
|
value={value?.color ?? defaultStyleConfig.color}
|
||||||
context={context}
|
context={context}
|
||||||
onChange={onColorChange}
|
onChange={onColorChange}
|
||||||
item={{} as any}
|
item={{} as any}
|
||||||
@ -196,7 +221,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
|||||||
</Field>
|
</Field>
|
||||||
<Field label={'Fill opacity'}>
|
<Field label={'Fill opacity'}>
|
||||||
<SliderValueEditor
|
<SliderValueEditor
|
||||||
value={value.opacity ?? defaultStyleConfig.opacity}
|
value={value?.opacity ?? defaultStyleConfig.opacity}
|
||||||
context={context}
|
context={context}
|
||||||
onChange={onOpacityChange}
|
onChange={onOpacityChange}
|
||||||
item={
|
item={
|
||||||
@ -210,9 +235,26 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
{settings?.displayRotation && (
|
||||||
|
<Field label={'Rotation angle'}>
|
||||||
|
<ScalarDimensionEditor
|
||||||
|
value={value?.rotation ?? defaultStyleConfig.rotation}
|
||||||
|
context={context}
|
||||||
|
onChange={onRotationChange}
|
||||||
|
item={
|
||||||
|
{
|
||||||
|
settings: {
|
||||||
|
min: defaultStyleConfig.rotation.min,
|
||||||
|
max: defaultStyleConfig.rotation.max,
|
||||||
|
},
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
<Field label={'Text label'}>
|
<Field label={'Text label'}>
|
||||||
<TextDimensionEditor
|
<TextDimensionEditor
|
||||||
value={value.text ?? defaultTextConfig}
|
value={value?.text ?? defaultTextConfig}
|
||||||
context={context}
|
context={context}
|
||||||
onChange={onTextChange}
|
onChange={onTextChange}
|
||||||
item={{} as any}
|
item={{} as any}
|
||||||
@ -224,7 +266,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
|||||||
<HorizontalGroup>
|
<HorizontalGroup>
|
||||||
<Field label={'Font size'}>
|
<Field label={'Font size'}>
|
||||||
<NumberValueEditor
|
<NumberValueEditor
|
||||||
value={value.textConfig?.fontSize ?? defaultStyleConfig.textConfig.fontSize}
|
value={value?.textConfig?.fontSize ?? defaultStyleConfig.textConfig.fontSize}
|
||||||
context={context}
|
context={context}
|
||||||
onChange={onTextFontSizeChange}
|
onChange={onTextFontSizeChange}
|
||||||
item={{} as any}
|
item={{} as any}
|
||||||
@ -232,7 +274,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
|||||||
</Field>
|
</Field>
|
||||||
<Field label={'X offset'}>
|
<Field label={'X offset'}>
|
||||||
<NumberValueEditor
|
<NumberValueEditor
|
||||||
value={value.textConfig?.offsetX ?? defaultStyleConfig.textConfig.offsetX}
|
value={value?.textConfig?.offsetX ?? defaultStyleConfig.textConfig.offsetX}
|
||||||
context={context}
|
context={context}
|
||||||
onChange={onTextOffsetXChange}
|
onChange={onTextOffsetXChange}
|
||||||
item={{} as any}
|
item={{} as any}
|
||||||
@ -240,7 +282,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
|||||||
</Field>
|
</Field>
|
||||||
<Field label={'Y offset'}>
|
<Field label={'Y offset'}>
|
||||||
<NumberValueEditor
|
<NumberValueEditor
|
||||||
value={value.textConfig?.offsetY ?? defaultStyleConfig.textConfig.offsetY}
|
value={value?.textConfig?.offsetY ?? defaultStyleConfig.textConfig.offsetY}
|
||||||
context={context}
|
context={context}
|
||||||
onChange={onTextOffsetYChange}
|
onChange={onTextOffsetYChange}
|
||||||
item={{} as any}
|
item={{} as any}
|
||||||
@ -249,7 +291,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
|||||||
</HorizontalGroup>
|
</HorizontalGroup>
|
||||||
<Field label={'Align'}>
|
<Field label={'Align'}>
|
||||||
<RadioButtonGroup
|
<RadioButtonGroup
|
||||||
value={value.textConfig?.textAlign ?? defaultStyleConfig.textConfig.textAlign}
|
value={value?.textConfig?.textAlign ?? defaultStyleConfig.textConfig.textAlign}
|
||||||
onChange={onTextAlignChange}
|
onChange={onTextAlignChange}
|
||||||
options={[
|
options={[
|
||||||
{ value: TextAlignment.Left, label: TextAlignment.Left },
|
{ value: TextAlignment.Left, label: TextAlignment.Left },
|
||||||
@ -260,7 +302,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
|
|||||||
</Field>
|
</Field>
|
||||||
<Field label={'Baseline'}>
|
<Field label={'Baseline'}>
|
||||||
<RadioButtonGroup
|
<RadioButtonGroup
|
||||||
value={value.textConfig?.textBaseline ?? defaultStyleConfig.textConfig.textBaseline}
|
value={value?.textConfig?.textBaseline ?? defaultStyleConfig.textConfig.textBaseline}
|
||||||
onChange={onTextBaselineChange}
|
onChange={onTextBaselineChange}
|
||||||
options={[
|
options={[
|
||||||
{ value: TextBaseline.Top, label: TextBaseline.Top },
|
{ value: TextBaseline.Top, label: TextBaseline.Top },
|
||||||
|
@ -12,7 +12,7 @@ import { Point } from 'ol/geom';
|
|||||||
import * as layer from 'ol/layer';
|
import * as layer from 'ol/layer';
|
||||||
import * as source from 'ol/source';
|
import * as source from 'ol/source';
|
||||||
import { dataFrameToPoints, getLocationMatchers } from '../../utils/location';
|
import { dataFrameToPoints, getLocationMatchers } from '../../utils/location';
|
||||||
import { getScaledDimension, getColorDimension, getTextDimension } from 'app/features/dimensions';
|
import { getScaledDimension, getColorDimension, getTextDimension, getScalarDimension } from 'app/features/dimensions';
|
||||||
import { ObservablePropsWrapper } from '../../components/ObservablePropsWrapper';
|
import { ObservablePropsWrapper } from '../../components/ObservablePropsWrapper';
|
||||||
import { MarkersLegend, MarkersLegendProps } from './MarkersLegend';
|
import { MarkersLegend, MarkersLegendProps } from './MarkersLegend';
|
||||||
import { ReplaySubject } from 'rxjs';
|
import { ReplaySubject } from 'rxjs';
|
||||||
@ -107,6 +107,9 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
|||||||
if (style.fields.text) {
|
if (style.fields.text) {
|
||||||
dims.text = getTextDimension(frame, style.config.text!);
|
dims.text = getTextDimension(frame, style.config.text!);
|
||||||
}
|
}
|
||||||
|
if (style.fields.rotation) {
|
||||||
|
dims.rotation = getScalarDimension(frame, style.config.rotation ?? defaultStyleConfig.rotation);
|
||||||
|
}
|
||||||
style.dims = dims;
|
style.dims = dims;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,7 +142,9 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
|||||||
path: 'config.style',
|
path: 'config.style',
|
||||||
name: 'Styles',
|
name: 'Styles',
|
||||||
editor: StyleEditor,
|
editor: StyleEditor,
|
||||||
settings: {},
|
settings: {
|
||||||
|
displayRotation: true,
|
||||||
|
},
|
||||||
defaultValue: defaultOptions.style,
|
defaultValue: defaultOptions.style,
|
||||||
})
|
})
|
||||||
.addBooleanSwitch({
|
.addBooleanSwitch({
|
||||||
|
@ -150,6 +150,12 @@ describe('geomap migrations', () => {
|
|||||||
"fixed": "dark-green",
|
"fixed": "dark-green",
|
||||||
},
|
},
|
||||||
"opacity": 0.4,
|
"opacity": 0.4,
|
||||||
|
"rotation": Object {
|
||||||
|
"fixed": 0,
|
||||||
|
"max": 360,
|
||||||
|
"min": -360,
|
||||||
|
"mode": "mod",
|
||||||
|
},
|
||||||
"size": Object {
|
"size": Object {
|
||||||
"field": "Count",
|
"field": "Count",
|
||||||
"fixed": 5,
|
"fixed": 5,
|
||||||
|
@ -121,13 +121,14 @@ const makers: SymbolMaker[] = [
|
|||||||
aliasIds: [MarkerShapePath.square],
|
aliasIds: [MarkerShapePath.square],
|
||||||
make: (cfg: StyleConfigValues) => {
|
make: (cfg: StyleConfigValues) => {
|
||||||
const radius = cfg.size ?? DEFAULT_SIZE;
|
const radius = cfg.size ?? DEFAULT_SIZE;
|
||||||
|
const rotation = cfg.rotation ?? 0;
|
||||||
return new Style({
|
return new Style({
|
||||||
image: new RegularShape({
|
image: new RegularShape({
|
||||||
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }),
|
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }),
|
||||||
fill: getFillColor(cfg),
|
fill: getFillColor(cfg),
|
||||||
points: 4,
|
points: 4,
|
||||||
radius,
|
radius,
|
||||||
angle: Math.PI / 4,
|
rotation: (rotation * Math.PI) / 180 + Math.PI / 4,
|
||||||
}),
|
}),
|
||||||
text: textLabel(cfg),
|
text: textLabel(cfg),
|
||||||
});
|
});
|
||||||
@ -139,13 +140,14 @@ const makers: SymbolMaker[] = [
|
|||||||
aliasIds: [MarkerShapePath.triangle],
|
aliasIds: [MarkerShapePath.triangle],
|
||||||
make: (cfg: StyleConfigValues) => {
|
make: (cfg: StyleConfigValues) => {
|
||||||
const radius = cfg.size ?? DEFAULT_SIZE;
|
const radius = cfg.size ?? DEFAULT_SIZE;
|
||||||
|
const rotation = cfg.rotation ?? 0;
|
||||||
return new Style({
|
return new Style({
|
||||||
image: new RegularShape({
|
image: new RegularShape({
|
||||||
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }),
|
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }),
|
||||||
fill: getFillColor(cfg),
|
fill: getFillColor(cfg),
|
||||||
points: 3,
|
points: 3,
|
||||||
radius,
|
radius,
|
||||||
rotation: Math.PI / 4,
|
rotation: (rotation * Math.PI) / 180,
|
||||||
angle: 0,
|
angle: 0,
|
||||||
}),
|
}),
|
||||||
text: textLabel(cfg),
|
text: textLabel(cfg),
|
||||||
@ -158,6 +160,7 @@ const makers: SymbolMaker[] = [
|
|||||||
aliasIds: [MarkerShapePath.star],
|
aliasIds: [MarkerShapePath.star],
|
||||||
make: (cfg: StyleConfigValues) => {
|
make: (cfg: StyleConfigValues) => {
|
||||||
const radius = cfg.size ?? DEFAULT_SIZE;
|
const radius = cfg.size ?? DEFAULT_SIZE;
|
||||||
|
const rotation = cfg.rotation ?? 0;
|
||||||
return new Style({
|
return new Style({
|
||||||
image: new RegularShape({
|
image: new RegularShape({
|
||||||
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }),
|
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }),
|
||||||
@ -166,6 +169,7 @@ const makers: SymbolMaker[] = [
|
|||||||
radius,
|
radius,
|
||||||
radius2: radius * 0.4,
|
radius2: radius * 0.4,
|
||||||
angle: 0,
|
angle: 0,
|
||||||
|
rotation: (rotation * Math.PI) / 180,
|
||||||
}),
|
}),
|
||||||
text: textLabel(cfg),
|
text: textLabel(cfg),
|
||||||
});
|
});
|
||||||
@ -177,6 +181,7 @@ const makers: SymbolMaker[] = [
|
|||||||
aliasIds: [MarkerShapePath.cross],
|
aliasIds: [MarkerShapePath.cross],
|
||||||
make: (cfg: StyleConfigValues) => {
|
make: (cfg: StyleConfigValues) => {
|
||||||
const radius = cfg.size ?? DEFAULT_SIZE;
|
const radius = cfg.size ?? DEFAULT_SIZE;
|
||||||
|
const rotation = cfg.rotation ?? 0;
|
||||||
return new Style({
|
return new Style({
|
||||||
image: new RegularShape({
|
image: new RegularShape({
|
||||||
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }),
|
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }),
|
||||||
@ -184,6 +189,7 @@ const makers: SymbolMaker[] = [
|
|||||||
radius,
|
radius,
|
||||||
radius2: 0,
|
radius2: 0,
|
||||||
angle: 0,
|
angle: 0,
|
||||||
|
rotation: (rotation * Math.PI) / 180,
|
||||||
}),
|
}),
|
||||||
text: textLabel(cfg),
|
text: textLabel(cfg),
|
||||||
});
|
});
|
||||||
@ -195,13 +201,14 @@ const makers: SymbolMaker[] = [
|
|||||||
aliasIds: [MarkerShapePath.x],
|
aliasIds: [MarkerShapePath.x],
|
||||||
make: (cfg: StyleConfigValues) => {
|
make: (cfg: StyleConfigValues) => {
|
||||||
const radius = cfg.size ?? DEFAULT_SIZE;
|
const radius = cfg.size ?? DEFAULT_SIZE;
|
||||||
|
const rotation = cfg.rotation ?? 0;
|
||||||
return new Style({
|
return new Style({
|
||||||
image: new RegularShape({
|
image: new RegularShape({
|
||||||
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }),
|
stroke: new Stroke({ color: cfg.color, width: cfg.lineWidth ?? 1 }),
|
||||||
points: 4,
|
points: 4,
|
||||||
radius,
|
radius,
|
||||||
radius2: 0,
|
radius2: 0,
|
||||||
angle: Math.PI / 4,
|
rotation: (rotation * Math.PI) / 180 + Math.PI / 4,
|
||||||
}),
|
}),
|
||||||
text: textLabel(cfg),
|
text: textLabel(cfg),
|
||||||
});
|
});
|
||||||
@ -265,6 +272,7 @@ export async function getMarkerMaker(symbol?: string, hasTextLabel?: boolean): P
|
|||||||
make: src
|
make: src
|
||||||
? (cfg: StyleConfigValues) => {
|
? (cfg: StyleConfigValues) => {
|
||||||
const radius = cfg.size ?? DEFAULT_SIZE;
|
const radius = cfg.size ?? DEFAULT_SIZE;
|
||||||
|
const rotation = cfg.rotation ?? 0;
|
||||||
return [
|
return [
|
||||||
new Style({
|
new Style({
|
||||||
image: new Icon({
|
image: new Icon({
|
||||||
@ -272,6 +280,7 @@ export async function getMarkerMaker(symbol?: string, hasTextLabel?: boolean): P
|
|||||||
color: cfg.color,
|
color: cfg.color,
|
||||||
opacity: cfg.opacity ?? 1,
|
opacity: cfg.opacity ?? 1,
|
||||||
scale: (DEFAULT_SIZE + radius) / 100,
|
scale: (DEFAULT_SIZE + radius) / 100,
|
||||||
|
rotation: (rotation * Math.PI) / 180,
|
||||||
}),
|
}),
|
||||||
text: !cfg?.text ? undefined : textLabel(cfg),
|
text: !cfg?.text ? undefined : textLabel(cfg),
|
||||||
}),
|
}),
|
||||||
@ -281,7 +290,7 @@ export async function getMarkerMaker(symbol?: string, hasTextLabel?: boolean): P
|
|||||||
fill: new Fill({ color: 'rgba(0,0,0,0)' }),
|
fill: new Fill({ color: 'rgba(0,0,0,0)' }),
|
||||||
points: 4,
|
points: 4,
|
||||||
radius: cfg.size,
|
radius: cfg.size,
|
||||||
angle: Math.PI / 4,
|
rotation: (rotation * Math.PI) / 180 + Math.PI / 4,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
@ -4,6 +4,8 @@ import {
|
|||||||
ResourceDimensionConfig,
|
ResourceDimensionConfig,
|
||||||
ResourceDimensionMode,
|
ResourceDimensionMode,
|
||||||
ScaleDimensionConfig,
|
ScaleDimensionConfig,
|
||||||
|
ScalarDimensionConfig,
|
||||||
|
ScalarDimensionMode,
|
||||||
TextDimensionConfig,
|
TextDimensionConfig,
|
||||||
} from 'app/features/dimensions';
|
} from 'app/features/dimensions';
|
||||||
import { Style } from 'ol/style';
|
import { Style } from 'ol/style';
|
||||||
@ -30,6 +32,9 @@ export interface StyleConfig {
|
|||||||
// Can show markers and text together!
|
// Can show markers and text together!
|
||||||
text?: TextDimensionConfig;
|
text?: TextDimensionConfig;
|
||||||
textConfig?: TextStyleConfig;
|
textConfig?: TextStyleConfig;
|
||||||
|
|
||||||
|
// Allow for rotation of markers
|
||||||
|
rotation?: ScalarDimensionConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SIZE = 5;
|
export const DEFAULT_SIZE = 5;
|
||||||
@ -66,6 +71,12 @@ export const defaultStyleConfig = Object.freeze({
|
|||||||
offsetX: 0,
|
offsetX: 0,
|
||||||
offsetY: 0,
|
offsetY: 0,
|
||||||
},
|
},
|
||||||
|
rotation: {
|
||||||
|
fixed: 0,
|
||||||
|
mode: ScalarDimensionMode.Mod,
|
||||||
|
min: -360,
|
||||||
|
max: 360,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -99,12 +110,14 @@ export interface StyleConfigFields {
|
|||||||
color?: string;
|
color?: string;
|
||||||
size?: string;
|
size?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
|
rotation?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StyleDimensions {
|
export interface StyleDimensions {
|
||||||
color?: DimensionSupplier<string>;
|
color?: DimensionSupplier<string>;
|
||||||
size?: DimensionSupplier<number>;
|
size?: DimensionSupplier<number>;
|
||||||
text?: DimensionSupplier<string>;
|
text?: DimensionSupplier<string>;
|
||||||
|
rotation?: DimensionSupplier<number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StyleConfigState {
|
export interface StyleConfigState {
|
||||||
|
@ -5,7 +5,7 @@ import { defaultStyleConfig, StyleConfig, StyleConfigFields, StyleConfigState }
|
|||||||
|
|
||||||
/** Indicate if the style wants to show text values */
|
/** Indicate if the style wants to show text values */
|
||||||
export function styleUsesText(config: StyleConfig): boolean {
|
export function styleUsesText(config: StyleConfig): boolean {
|
||||||
const { text } = config;
|
const text = config?.text;
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -35,7 +35,7 @@ export async function getStyleConfigState(cfg?: StyleConfig): Promise<StyleConfi
|
|||||||
opacity: cfg.opacity ?? defaultStyleConfig.opacity,
|
opacity: cfg.opacity ?? defaultStyleConfig.opacity,
|
||||||
lineWidth: cfg.lineWidth ?? 1,
|
lineWidth: cfg.lineWidth ?? 1,
|
||||||
size: cfg.size?.fixed ?? defaultStyleConfig.size.fixed,
|
size: cfg.size?.fixed ?? defaultStyleConfig.size.fixed,
|
||||||
rotation: 0, // dynamic will follow path
|
rotation: cfg.rotation?.fixed ?? defaultStyleConfig.rotation.fixed, // add ability follow path later
|
||||||
},
|
},
|
||||||
maker,
|
maker,
|
||||||
};
|
};
|
||||||
@ -46,6 +46,9 @@ export async function getStyleConfigState(cfg?: StyleConfig): Promise<StyleConfi
|
|||||||
if (cfg.size?.field?.length) {
|
if (cfg.size?.field?.length) {
|
||||||
fields.size = cfg.size.field;
|
fields.size = cfg.size.field;
|
||||||
}
|
}
|
||||||
|
if (cfg.rotation?.field?.length) {
|
||||||
|
fields.rotation = cfg.rotation.field;
|
||||||
|
}
|
||||||
|
|
||||||
if (hasText) {
|
if (hasText) {
|
||||||
state.base.text = cfg.text?.fixed;
|
state.base.text = cfg.text?.fixed;
|
||||||
|
@ -31,6 +31,9 @@ export const getFeatures = (
|
|||||||
if (dims.size) {
|
if (dims.size) {
|
||||||
values.size = dims.size.get(i);
|
values.size = dims.size.get(i);
|
||||||
}
|
}
|
||||||
|
if (dims.rotation) {
|
||||||
|
values.rotation = dims.rotation.get(i);
|
||||||
|
}
|
||||||
if (dims.text) {
|
if (dims.text) {
|
||||||
values.text = dims.text.get(i);
|
values.text = dims.text.get(i);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user