mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 10:20:29 -06:00
Geomap: configure size and color with different fields (#36825)
This commit is contained in:
parent
48941da02e
commit
85a14a0503
@ -64,7 +64,7 @@ function getBooleanScaleCalculator(field: Field, theme: GrafanaTheme2): ScaleCal
|
||||
};
|
||||
}
|
||||
|
||||
function getMinMaxAndDelta(field: Field): NumericRange {
|
||||
export function getMinMaxAndDelta(field: Field): NumericRange {
|
||||
if (field.type !== FieldType.number) {
|
||||
return { min: 0, max: 100, delta: 100 };
|
||||
}
|
||||
|
@ -66,11 +66,15 @@ export function useFieldDisplayNames(data: DataFrame[], filter?: (field: Field)
|
||||
*/
|
||||
export function useSelectOptions(
|
||||
displayNames: FrameFieldsDisplayNames,
|
||||
currentName?: string
|
||||
currentName?: string,
|
||||
firstItem?: SelectableValue<string>
|
||||
): Array<SelectableValue<string>> {
|
||||
return useMemo(() => {
|
||||
let found = false;
|
||||
const options: Array<SelectableValue<string>> = [];
|
||||
if (firstItem) {
|
||||
options.push(firstItem);
|
||||
}
|
||||
for (const name of displayNames.display) {
|
||||
if (!found && name === currentName) {
|
||||
found = true;
|
||||
|
40
public/app/plugins/panel/geomap/dims/color.ts
Normal file
40
public/app/plugins/panel/geomap/dims/color.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { DataFrame, getFieldColorModeForField, getScaleCalculator, GrafanaTheme2 } from '@grafana/data';
|
||||
import { ColorDimensionConfig, DimensionSupplier } from './types';
|
||||
import { findField } from './utils';
|
||||
|
||||
//---------------------------------------------------------
|
||||
// Color dimension
|
||||
//---------------------------------------------------------
|
||||
|
||||
export function getColorDimension(
|
||||
frame: DataFrame,
|
||||
config: ColorDimensionConfig,
|
||||
theme: GrafanaTheme2
|
||||
): DimensionSupplier<string> {
|
||||
const field = findField(frame, config.field);
|
||||
if (!field) {
|
||||
const v = config.fixed ?? 'grey';
|
||||
return {
|
||||
isAssumed: Boolean(config.field?.length) || !config.fixed,
|
||||
fixed: v,
|
||||
get: (i) => v,
|
||||
};
|
||||
}
|
||||
const mode = getFieldColorModeForField(field);
|
||||
if (!mode.isByValue) {
|
||||
const fixed = mode.getCalculator(field, theme)(0, 0);
|
||||
return {
|
||||
fixed,
|
||||
get: (i) => fixed,
|
||||
field,
|
||||
};
|
||||
}
|
||||
const scale = getScaleCalculator(field, theme);
|
||||
return {
|
||||
get: (i) => {
|
||||
const val = field.values.get(i);
|
||||
return scale(val).color;
|
||||
},
|
||||
field,
|
||||
};
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data';
|
||||
import { ColorDimensionConfig } from '../types';
|
||||
import { Select, ColorPicker, useStyles2 } from '@grafana/ui';
|
||||
import {
|
||||
useFieldDisplayNames,
|
||||
useSelectOptions,
|
||||
} from '../../../../../../../packages/grafana-ui/src/components/MatchersUI/utils';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
const fixedColorOption: SelectableValue<string> = {
|
||||
label: 'Fixed color',
|
||||
value: '_____fixed_____',
|
||||
};
|
||||
|
||||
export const ColorDimensionEditor: FC<StandardEditorProps<ColorDimensionConfig, any, any>> = (props) => {
|
||||
const { value, context, onChange } = props;
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
const fieldName = value?.field;
|
||||
const isFixed = Boolean(!fieldName);
|
||||
const names = useFieldDisplayNames(context.data);
|
||||
const selectOptions = useSelectOptions(names, fieldName, fixedColorOption);
|
||||
|
||||
const onSelectChange = useCallback(
|
||||
(selection: SelectableValue<string>) => {
|
||||
const field = selection.value;
|
||||
if (field && field !== fixedColorOption.value) {
|
||||
onChange({
|
||||
...value,
|
||||
field,
|
||||
});
|
||||
} else {
|
||||
const fixed = value.fixed ?? 'grey';
|
||||
onChange({
|
||||
...value,
|
||||
field: undefined,
|
||||
fixed,
|
||||
});
|
||||
}
|
||||
},
|
||||
[onChange, value]
|
||||
);
|
||||
|
||||
const onColorChange = useCallback(
|
||||
(c: string) => {
|
||||
onChange({
|
||||
field: undefined,
|
||||
fixed: c ?? 'grey',
|
||||
});
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const selectedOption = isFixed ? fixedColorOption : selectOptions.find((v) => v.value === fieldName);
|
||||
return (
|
||||
<>
|
||||
<div className={styles.container}>
|
||||
<Select
|
||||
value={selectedOption}
|
||||
options={selectOptions}
|
||||
onChange={onSelectChange}
|
||||
noOptionsMessage="No fields found"
|
||||
/>
|
||||
{isFixed && (
|
||||
<div className={styles.picker}>
|
||||
<ColorPicker color={value?.fixed ?? 'grey'} onChange={onColorChange} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
container: css`
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
`,
|
||||
picker: css`
|
||||
padding-left: 8px;
|
||||
`,
|
||||
});
|
@ -0,0 +1,132 @@
|
||||
import React, { FC, useCallback, useMemo } from 'react';
|
||||
import { GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data';
|
||||
import { ScaleDimensionConfig, ScaleDimensionOptions } from '../types';
|
||||
import { InlineField, InlineFieldRow, Select, useStyles2 } from '@grafana/ui';
|
||||
import {
|
||||
useFieldDisplayNames,
|
||||
useSelectOptions,
|
||||
} from '../../../../../../../packages/grafana-ui/src/components/MatchersUI/utils';
|
||||
import { NumberInput } from '../../components/NumberInput';
|
||||
import { css } from '@emotion/css';
|
||||
import { validateScaleOptions, validateScaleConfig } from '../scale';
|
||||
|
||||
const fixedValueOption: SelectableValue<string> = {
|
||||
label: 'Fixed value',
|
||||
value: '_____fixed_____',
|
||||
};
|
||||
|
||||
export const ScaleDimensionEditor: FC<StandardEditorProps<ScaleDimensionConfig, ScaleDimensionOptions, any>> = (
|
||||
props
|
||||
) => {
|
||||
const { value, context, onChange, item } = props;
|
||||
const { settings } = item;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const fieldName = value?.field;
|
||||
const isFixed = Boolean(!fieldName);
|
||||
const names = useFieldDisplayNames(context.data);
|
||||
const selectOptions = useSelectOptions(names, fieldName, fixedValueOption);
|
||||
const minMaxStep = useMemo(() => {
|
||||
return validateScaleOptions(settings);
|
||||
}, [settings]);
|
||||
|
||||
// Validate and update
|
||||
const validateAndDoChange = useCallback(
|
||||
(v: ScaleDimensionConfig) => {
|
||||
// always called with a copy so no need to spread
|
||||
onChange(validateScaleConfig(v, minMaxStep));
|
||||
},
|
||||
[onChange, minMaxStep]
|
||||
);
|
||||
|
||||
const onSelectChange = useCallback(
|
||||
(selection: SelectableValue<string>) => {
|
||||
const field = selection.value;
|
||||
if (field && field !== fixedValueOption.value) {
|
||||
validateAndDoChange({
|
||||
...value,
|
||||
field,
|
||||
});
|
||||
} else {
|
||||
validateAndDoChange({
|
||||
...value,
|
||||
field: undefined,
|
||||
});
|
||||
}
|
||||
},
|
||||
[validateAndDoChange, value]
|
||||
);
|
||||
|
||||
const onMinChange = useCallback(
|
||||
(min: number) => {
|
||||
validateAndDoChange({
|
||||
...value,
|
||||
min,
|
||||
});
|
||||
},
|
||||
[validateAndDoChange, value]
|
||||
);
|
||||
|
||||
const onMaxChange = useCallback(
|
||||
(max: number) => {
|
||||
validateAndDoChange({
|
||||
...value,
|
||||
max,
|
||||
});
|
||||
},
|
||||
[validateAndDoChange, value]
|
||||
);
|
||||
|
||||
const onValueChange = useCallback(
|
||||
(fixed: number) => {
|
||||
validateAndDoChange({
|
||||
...value,
|
||||
fixed,
|
||||
});
|
||||
},
|
||||
[validateAndDoChange, value]
|
||||
);
|
||||
|
||||
const selectedOption = isFixed ? fixedValueOption : selectOptions.find((v) => v.value === fieldName);
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Select
|
||||
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={value.fixed} {...minMaxStep} onChange={onValueChange} />
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
)}
|
||||
{!isFixed && !minMaxStep.hideRange && (
|
||||
<>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Min" labelWidth={8} grow={true}>
|
||||
<NumberInput value={value.min} {...minMaxStep} onChange={onMinChange} />
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Max" labelWidth={8} grow={true}>
|
||||
<NumberInput value={value.max} {...minMaxStep} onChange={onMaxChange} />
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
range: css`
|
||||
padding-top: 8px;
|
||||
`,
|
||||
});
|
38
public/app/plugins/panel/geomap/dims/scale.test.ts
Normal file
38
public/app/plugins/panel/geomap/dims/scale.test.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { validateScaleConfig } from './scale';
|
||||
|
||||
describe('scale dimensions', () => {
|
||||
it('should validate empty input', () => {
|
||||
const out = validateScaleConfig({} as any, {
|
||||
min: 5,
|
||||
max: 10,
|
||||
});
|
||||
expect(out).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fixed": 2.5,
|
||||
"max": 10,
|
||||
"min": 2.5,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should assert min<max', () => {
|
||||
const out = validateScaleConfig(
|
||||
{
|
||||
max: -3,
|
||||
min: 7,
|
||||
fixed: 100,
|
||||
},
|
||||
{
|
||||
min: 5,
|
||||
max: 10,
|
||||
}
|
||||
);
|
||||
expect(out).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fixed": 7,
|
||||
"max": 7,
|
||||
"min": 5,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
93
public/app/plugins/panel/geomap/dims/scale.ts
Normal file
93
public/app/plugins/panel/geomap/dims/scale.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { DataFrame } from '@grafana/data';
|
||||
import { getMinMaxAndDelta } from '../../../../../../packages/grafana-data/src/field/scale';
|
||||
import { ScaleDimensionConfig, DimensionSupplier, ScaleDimensionOptions } from './types';
|
||||
import { findField } from './utils';
|
||||
|
||||
//---------------------------------------------------------
|
||||
// Scale dimension
|
||||
//---------------------------------------------------------
|
||||
|
||||
export function getScaledDimension(frame: DataFrame, config: ScaleDimensionConfig): DimensionSupplier<number> {
|
||||
const field = findField(frame, config.field);
|
||||
if (!field) {
|
||||
const v = config.fixed ?? 0;
|
||||
return {
|
||||
isAssumed: Boolean(config.field?.length) || !config.fixed,
|
||||
fixed: v,
|
||||
get: () => v,
|
||||
};
|
||||
}
|
||||
const info = getMinMaxAndDelta(field);
|
||||
const delta = config.max - config.min;
|
||||
const values = field.values;
|
||||
if (values.length < 1 || delta <= 0 || info.delta <= 0) {
|
||||
return {
|
||||
fixed: config.min,
|
||||
get: () => config.min,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
get: (i) => {
|
||||
const value = field.values.get(i);
|
||||
let percent = 0;
|
||||
if (value !== -Infinity) {
|
||||
percent = (value - info.min!) / info.delta;
|
||||
}
|
||||
return config.min + percent * delta;
|
||||
},
|
||||
field,
|
||||
};
|
||||
}
|
||||
|
||||
// This will mutate options
|
||||
export function validateScaleOptions(options?: ScaleDimensionOptions): ScaleDimensionOptions {
|
||||
if (!options) {
|
||||
options = { min: 0, max: 1 };
|
||||
}
|
||||
if (options.min == null) {
|
||||
options.min = 0;
|
||||
}
|
||||
if (options.max == null) {
|
||||
options.max = 1;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/** Mutates and will return a valid version */
|
||||
export function validateScaleConfig(copy: ScaleDimensionConfig, options: ScaleDimensionOptions): ScaleDimensionConfig {
|
||||
let { min, max } = validateScaleOptions(options);
|
||||
if (!copy) {
|
||||
copy = {} as any;
|
||||
}
|
||||
|
||||
if (copy.max == null) {
|
||||
copy.max = max;
|
||||
}
|
||||
if (copy.min == null) {
|
||||
copy.min = min;
|
||||
}
|
||||
// Make sure the order is right
|
||||
if (copy.min > copy.max) {
|
||||
const tmp = copy.max;
|
||||
copy.max = copy.min;
|
||||
copy.min = tmp;
|
||||
}
|
||||
// Validate range
|
||||
if (copy.min < min) {
|
||||
copy.min = min;
|
||||
}
|
||||
if (copy.max > max) {
|
||||
copy.max = max;
|
||||
}
|
||||
if (copy.fixed == null) {
|
||||
copy.fixed = copy.min = (copy.max - copy.min) / 2.0;
|
||||
}
|
||||
if (copy.fixed > copy.max) {
|
||||
copy.fixed = copy.max;
|
||||
} else if (copy.fixed < copy.min) {
|
||||
copy.fixed = copy.min;
|
||||
}
|
||||
return copy;
|
||||
}
|
45
public/app/plugins/panel/geomap/dims/types.ts
Normal file
45
public/app/plugins/panel/geomap/dims/types.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { Field } from '@grafana/data';
|
||||
|
||||
export interface BaseDimensionConfig<T = any> {
|
||||
fixed: T;
|
||||
field?: string;
|
||||
}
|
||||
|
||||
export interface DimensionSupplier<T = any> {
|
||||
/**
|
||||
* This means an explicit value was not configured
|
||||
*/
|
||||
isAssumed?: boolean;
|
||||
|
||||
/**
|
||||
* The fied used for
|
||||
*/
|
||||
field?: Field;
|
||||
|
||||
/**
|
||||
* Explicit value -- if == null, then need a value pr index
|
||||
*/
|
||||
fixed?: T;
|
||||
|
||||
/**
|
||||
* Supplier for the dimension value
|
||||
*/
|
||||
get: (index: number) => T;
|
||||
}
|
||||
|
||||
/** This will map the field value% to a scaled value within the range */
|
||||
export interface ScaleDimensionConfig extends BaseDimensionConfig<number> {
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
/** Places that use the value */
|
||||
export interface ScaleDimensionOptions {
|
||||
min: number;
|
||||
max: number;
|
||||
step?: number;
|
||||
hideRange?: boolean; // false
|
||||
}
|
||||
|
||||
/** Use the color value from field configs */
|
||||
export interface ColorDimensionConfig extends BaseDimensionConfig<string> {}
|
18
public/app/plugins/panel/geomap/dims/utils.ts
Normal file
18
public/app/plugins/panel/geomap/dims/utils.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { DataFrame, Field, getFieldDisplayName } from '@grafana/data';
|
||||
|
||||
export function findField(frame: DataFrame, name?: string): Field | undefined {
|
||||
if (!name?.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const field of frame.fields) {
|
||||
if (name === field.name) {
|
||||
return field;
|
||||
}
|
||||
const disp = getFieldDisplayName(field, frame);
|
||||
if (name === disp) {
|
||||
return field;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import {
|
||||
FieldCalcs,
|
||||
FieldType,
|
||||
getFieldColorModeForField,
|
||||
GrafanaTheme2,
|
||||
@ -7,22 +6,29 @@ import {
|
||||
MapLayerOptions,
|
||||
MapLayerRegistryItem,
|
||||
PanelData,
|
||||
reduceField,
|
||||
ReducerID,
|
||||
} 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 { dataFrameToPoints, getLocationMatchers } from '../../utils/location';
|
||||
import { ScaleDimensionConfig, } from '../../dims/types';
|
||||
import { ScaleDimensionEditor } from '../../dims/editors/ScaleDimensionEditor';
|
||||
import { getScaledDimension } from '../../dims/scale';
|
||||
|
||||
// Configuration options for Heatmap overlays
|
||||
export interface HeatmapConfig {
|
||||
weight: ScaleDimensionConfig;
|
||||
blur: number;
|
||||
radius: number;
|
||||
}
|
||||
|
||||
const defaultOptions: HeatmapConfig = {
|
||||
weight: {
|
||||
fixed: 1,
|
||||
min: 0,
|
||||
max: 1,
|
||||
},
|
||||
blur: 15,
|
||||
radius: 5,
|
||||
};
|
||||
@ -77,47 +83,54 @@ export const heatmapLayer: MapLayerRegistryItem<HeatmapConfig> = {
|
||||
return; // ???
|
||||
}
|
||||
|
||||
// Get the field of data values
|
||||
const field = frame.fields.find(field => field.type === FieldType.number); // TODO!!!!
|
||||
// Return early if metric field is not matched
|
||||
if (field === undefined) {
|
||||
return;
|
||||
};
|
||||
const weightDim = getScaledDimension(frame, config.weight);
|
||||
|
||||
// Retrieve the min, max and range of data values
|
||||
const calcs = reduceField({
|
||||
field: field,
|
||||
reducers: [
|
||||
ReducerID.min,
|
||||
ReducerID.range,
|
||||
]
|
||||
});
|
||||
// Map each data value into new points
|
||||
for (let i = 0; i < frame.length; i++) {
|
||||
const cluster = new Feature({
|
||||
geometry: info.points[i],
|
||||
value: normalize(calcs, field.values.get(i)),
|
||||
value: weightDim.get(i),
|
||||
});
|
||||
vectorSource.addFeature(cluster);
|
||||
};
|
||||
vectorLayer.setSource(vectorSource);
|
||||
|
||||
// Set gradient of heatmap
|
||||
const colorMode = getFieldColorModeForField(field);
|
||||
if (colorMode.isContinuous && colorMode.getColors) {
|
||||
// getColors return an array of color string from the color scheme chosen
|
||||
const colors = colorMode.getColors(theme);
|
||||
vectorLayer.setGradient(colors);
|
||||
} else {
|
||||
// Set the gradient back to default if threshold or single color is chosen
|
||||
vectorLayer.setGradient(['#00f', '#0ff', '#0f0', '#ff0', '#f00']);
|
||||
// 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);
|
||||
if (field) {
|
||||
const colorMode = getFieldColorModeForField(field);
|
||||
if (colorMode.isContinuous && colorMode.getColors) {
|
||||
// getColors return an array of color string from the color scheme chosen
|
||||
colors = colorMode.getColors(theme);
|
||||
}
|
||||
}
|
||||
vectorLayer.setGradient(colors);
|
||||
},
|
||||
};
|
||||
},
|
||||
// Heatmap overlay options
|
||||
registerOptionsUI: (builder) => {
|
||||
builder
|
||||
.addCustomEditor({
|
||||
id: 'config.weight',
|
||||
path: 'config.weight',
|
||||
name: 'Weight values',
|
||||
description: 'Scale the distribution for each row',
|
||||
editor: ScaleDimensionEditor,
|
||||
settings: {
|
||||
min: 0, // no contribution
|
||||
max: 1,
|
||||
hideRange: true, // Don't show the scale factor
|
||||
},
|
||||
defaultValue: { // Configured values
|
||||
fixed: 1,
|
||||
min: 0,
|
||||
max: 1,
|
||||
},
|
||||
})
|
||||
.addSliderInput({
|
||||
path: 'config.radius',
|
||||
description: 'configures the size of clusters',
|
||||
@ -144,17 +157,3 @@ export const heatmapLayer: MapLayerRegistryItem<HeatmapConfig> = {
|
||||
// fill in the default values
|
||||
defaultOptions,
|
||||
};
|
||||
|
||||
/**
|
||||
* Function that normalize the data values to a range between 0.1 and 1
|
||||
* Returns the weights for each value input
|
||||
*/
|
||||
function normalize(calcs: FieldCalcs, value: number) {
|
||||
// If all data values are the same, it should return the largest weight
|
||||
if (calcs.range == 0) {
|
||||
return 1;
|
||||
};
|
||||
// Normalize value in range of [0.1,1]
|
||||
const norm = 0.1 + ((value - calcs.min) / calcs.range) * 0.9
|
||||
return norm;
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { MapLayerRegistryItem, MapLayerOptions, MapLayerHandler, PanelData, GrafanaTheme2, reduceField, ReducerID, FieldCalcs, FieldType } from '@grafana/data';
|
||||
import { MapLayerRegistryItem, MapLayerOptions, MapLayerHandler, PanelData, GrafanaTheme2 } from '@grafana/data';
|
||||
import Map from 'ol/Map';
|
||||
import Feature from 'ol/Feature';
|
||||
import * as layer from 'ol/layer';
|
||||
@ -6,18 +6,30 @@ import * as source from 'ol/source';
|
||||
import * as style from 'ol/style';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { dataFrameToPoints, getLocationMatchers } from '../../utils/location';
|
||||
import { ColorDimensionConfig, ScaleDimensionConfig, } from '../../dims/types';
|
||||
import { getScaledDimension, } from '../../dims/scale';
|
||||
import { getColorDimension, } from '../../dims/color';
|
||||
import { ScaleDimensionEditor } from '../../dims/editors/ScaleDimensionEditor';
|
||||
import { ColorDimensionEditor } from '../../dims/editors/ColorDimensionEditor';
|
||||
|
||||
|
||||
// Configuration options for Circle overlays
|
||||
export interface MarkersConfig {
|
||||
minSize: number,
|
||||
maxSize: number,
|
||||
opacity: number,
|
||||
size: ScaleDimensionConfig;
|
||||
color: ColorDimensionConfig;
|
||||
fillOpacity: number;
|
||||
}
|
||||
|
||||
const defaultOptions: MarkersConfig = {
|
||||
minSize: 1,
|
||||
maxSize: 10,
|
||||
opacity: 0.4,
|
||||
size: {
|
||||
fixed: 5,
|
||||
min: 5,
|
||||
max: 10,
|
||||
},
|
||||
color: {
|
||||
fixed: '#f00',
|
||||
},
|
||||
fillOpacity: 0.4,
|
||||
};
|
||||
|
||||
export const MARKERS_LAYER_ID = "markers";
|
||||
@ -37,9 +49,7 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
||||
* @param options
|
||||
*/
|
||||
create: (map: Map, options: MapLayerOptions<MarkersConfig>, theme: GrafanaTheme2): MapLayerHandler => {
|
||||
const config = { ...defaultOptions, ...options.config };
|
||||
const matchers = getLocationMatchers(options.location);
|
||||
|
||||
const vectorLayer = new layer.Vector({});
|
||||
return {
|
||||
init: () => vectorLayer,
|
||||
@ -55,33 +65,25 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
||||
return; // ???
|
||||
}
|
||||
|
||||
const field = frame.fields.find(field => field.type === FieldType.number); // TODO!!!!
|
||||
// Return early if metric field is not matched
|
||||
if (field === undefined) {
|
||||
return;
|
||||
// Assert default values
|
||||
const config = {
|
||||
...defaultOptions,
|
||||
...options?.config,
|
||||
};
|
||||
|
||||
// Retrieve the min, max and range of data values
|
||||
const calcs = reduceField({
|
||||
field: field,
|
||||
reducers: [
|
||||
ReducerID.min,
|
||||
ReducerID.max,
|
||||
ReducerID.range,
|
||||
]
|
||||
});
|
||||
const colorDim = getColorDimension(frame, config.color, theme);
|
||||
const sizeDim = getScaledDimension(frame, config.size);
|
||||
const opacity = options.config?.fillOpacity ?? defaultOptions.fillOpacity;
|
||||
|
||||
const features: Feature[] = [];
|
||||
|
||||
// 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;
|
||||
const color = colorDim.get(i);
|
||||
// Set the opacity determined from user configuration
|
||||
const fillColor = tinycolor(color).setAlpha(config.opacity).toRgbString();
|
||||
|
||||
const fillColor = tinycolor(color).setAlpha(opacity).toRgbString();
|
||||
// Get circle size from user configuration
|
||||
const radius = calcCircleSize(calcs, field.values.get(i), config.minSize, config.maxSize);
|
||||
const radius = sizeDim.get(i);
|
||||
|
||||
// Create a new Feature for each point returned from dataFrameToPoints
|
||||
const dot = new Feature({
|
||||
@ -111,57 +113,45 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
||||
},
|
||||
};
|
||||
},
|
||||
// Circle overlay options
|
||||
// Marker overlay options
|
||||
registerOptionsUI: (builder) => {
|
||||
builder
|
||||
// .addFieldNamePicker({
|
||||
// path: 'fieldMapping.metricField',
|
||||
// name: 'Metric Field',
|
||||
// defaultValue: defaultOptions.fieldMapping.metricField,
|
||||
// settings: {
|
||||
// filter: (f) => f.type === FieldType.number,
|
||||
// noFieldsMessage: 'No numeric fields found',
|
||||
// },
|
||||
// })
|
||||
.addNumberInput({
|
||||
path: 'config.minSize',
|
||||
description: 'configures the min circle size',
|
||||
name: 'Min Size',
|
||||
defaultValue: defaultOptions.minSize,
|
||||
.addCustomEditor({
|
||||
id: 'config.color',
|
||||
path: 'config.color',
|
||||
name: 'Marker Color',
|
||||
editor: ColorDimensionEditor,
|
||||
settings: {},
|
||||
defaultValue: { // Configured values
|
||||
fixed: 'grey',
|
||||
},
|
||||
})
|
||||
.addNumberInput({
|
||||
path: 'config.maxSize',
|
||||
description: 'configures the max circle size',
|
||||
name: 'Max Size',
|
||||
defaultValue: defaultOptions.maxSize,
|
||||
.addCustomEditor({
|
||||
id: 'config.size',
|
||||
path: 'config.size',
|
||||
name: 'Marker Size',
|
||||
editor: ScaleDimensionEditor,
|
||||
settings: {
|
||||
min: 1,
|
||||
max: 100, // possible in the UI
|
||||
},
|
||||
defaultValue: { // Configured values
|
||||
fixed: 5,
|
||||
min: 1,
|
||||
max: 20,
|
||||
},
|
||||
})
|
||||
.addSliderInput({
|
||||
path: 'config.opacity',
|
||||
description: 'configures the amount of transparency',
|
||||
name: 'Opacity',
|
||||
defaultValue: defaultOptions.opacity,
|
||||
settings: {
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.1,
|
||||
},
|
||||
});
|
||||
path: 'config.fillOpacity',
|
||||
name: 'Fill opacity',
|
||||
defaultValue: defaultOptions.fillOpacity,
|
||||
settings: {
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.1,
|
||||
},
|
||||
});
|
||||
},
|
||||
// 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;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user