mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -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) {
|
if (field.type !== FieldType.number) {
|
||||||
return { min: 0, max: 100, delta: 100 };
|
return { min: 0, max: 100, delta: 100 };
|
||||||
}
|
}
|
||||||
|
@ -66,11 +66,15 @@ 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>
|
||||||
): Array<SelectableValue<string>> {
|
): Array<SelectableValue<string>> {
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
let found = false;
|
let found = false;
|
||||||
const options: Array<SelectableValue<string>> = [];
|
const options: Array<SelectableValue<string>> = [];
|
||||||
|
if (firstItem) {
|
||||||
|
options.push(firstItem);
|
||||||
|
}
|
||||||
for (const name of displayNames.display) {
|
for (const name of displayNames.display) {
|
||||||
if (!found && name === currentName) {
|
if (!found && name === currentName) {
|
||||||
found = true;
|
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 {
|
import {
|
||||||
FieldCalcs,
|
|
||||||
FieldType,
|
FieldType,
|
||||||
getFieldColorModeForField,
|
getFieldColorModeForField,
|
||||||
GrafanaTheme2,
|
GrafanaTheme2,
|
||||||
@ -7,22 +6,29 @@ import {
|
|||||||
MapLayerOptions,
|
MapLayerOptions,
|
||||||
MapLayerRegistryItem,
|
MapLayerRegistryItem,
|
||||||
PanelData,
|
PanelData,
|
||||||
reduceField,
|
|
||||||
ReducerID,
|
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import Map from 'ol/Map';
|
import Map from 'ol/Map';
|
||||||
import Feature from 'ol/Feature';
|
import Feature from 'ol/Feature';
|
||||||
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 { ScaleDimensionConfig, } from '../../dims/types';
|
||||||
|
import { ScaleDimensionEditor } from '../../dims/editors/ScaleDimensionEditor';
|
||||||
|
import { getScaledDimension } from '../../dims/scale';
|
||||||
|
|
||||||
// Configuration options for Heatmap overlays
|
// Configuration options for Heatmap overlays
|
||||||
export interface HeatmapConfig {
|
export interface HeatmapConfig {
|
||||||
|
weight: ScaleDimensionConfig;
|
||||||
blur: number;
|
blur: number;
|
||||||
radius: number;
|
radius: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions: HeatmapConfig = {
|
const defaultOptions: HeatmapConfig = {
|
||||||
|
weight: {
|
||||||
|
fixed: 1,
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
},
|
||||||
blur: 15,
|
blur: 15,
|
||||||
radius: 5,
|
radius: 5,
|
||||||
};
|
};
|
||||||
@ -77,47 +83,54 @@ export const heatmapLayer: MapLayerRegistryItem<HeatmapConfig> = {
|
|||||||
return; // ???
|
return; // ???
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the field of data values
|
const weightDim = getScaledDimension(frame, config.weight);
|
||||||
const field = frame.fields.find(field => field.type === FieldType.number); // TODO!!!!
|
|
||||||
// Return early if metric field is not matched
|
|
||||||
if (field === undefined) {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Retrieve the min, max and range of data values
|
|
||||||
const calcs = reduceField({
|
|
||||||
field: field,
|
|
||||||
reducers: [
|
|
||||||
ReducerID.min,
|
|
||||||
ReducerID.range,
|
|
||||||
]
|
|
||||||
});
|
|
||||||
// Map each data value into new points
|
// Map each data value into new points
|
||||||
for (let i = 0; i < frame.length; i++) {
|
for (let i = 0; i < frame.length; i++) {
|
||||||
const cluster = new Feature({
|
const cluster = new Feature({
|
||||||
geometry: info.points[i],
|
geometry: info.points[i],
|
||||||
value: normalize(calcs, field.values.get(i)),
|
value: weightDim.get(i),
|
||||||
});
|
});
|
||||||
vectorSource.addFeature(cluster);
|
vectorSource.addFeature(cluster);
|
||||||
};
|
};
|
||||||
vectorLayer.setSource(vectorSource);
|
vectorLayer.setSource(vectorSource);
|
||||||
|
|
||||||
// Set gradient of heatmap
|
// Set heatmap gradient colors
|
||||||
const colorMode = getFieldColorModeForField(field);
|
let colors = ['#00f', '#0ff', '#0f0', '#ff0', '#f00'];
|
||||||
if (colorMode.isContinuous && colorMode.getColors) {
|
|
||||||
// getColors return an array of color string from the color scheme chosen
|
// Either the configured field or the first numeric field value
|
||||||
const colors = colorMode.getColors(theme);
|
const field = weightDim.field ?? frame.fields.find(field => field.type === FieldType.number);
|
||||||
vectorLayer.setGradient(colors);
|
if (field) {
|
||||||
} else {
|
const colorMode = getFieldColorModeForField(field);
|
||||||
// Set the gradient back to default if threshold or single color is chosen
|
if (colorMode.isContinuous && colorMode.getColors) {
|
||||||
vectorLayer.setGradient(['#00f', '#0ff', '#0f0', '#ff0', '#f00']);
|
// getColors return an array of color string from the color scheme chosen
|
||||||
|
colors = colorMode.getColors(theme);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
vectorLayer.setGradient(colors);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
// Heatmap overlay options
|
// Heatmap overlay options
|
||||||
registerOptionsUI: (builder) => {
|
registerOptionsUI: (builder) => {
|
||||||
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({
|
.addSliderInput({
|
||||||
path: 'config.radius',
|
path: 'config.radius',
|
||||||
description: 'configures the size of clusters',
|
description: 'configures the size of clusters',
|
||||||
@ -144,17 +157,3 @@ export const heatmapLayer: MapLayerRegistryItem<HeatmapConfig> = {
|
|||||||
// fill in the default values
|
// fill in the default values
|
||||||
defaultOptions,
|
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 Map from 'ol/Map';
|
||||||
import Feature from 'ol/Feature';
|
import Feature from 'ol/Feature';
|
||||||
import * as layer from 'ol/layer';
|
import * as layer from 'ol/layer';
|
||||||
@ -6,18 +6,30 @@ import * as source from 'ol/source';
|
|||||||
import * as style from 'ol/style';
|
import * as style from 'ol/style';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import { dataFrameToPoints, getLocationMatchers } from '../../utils/location';
|
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
|
// Configuration options for Circle overlays
|
||||||
export interface MarkersConfig {
|
export interface MarkersConfig {
|
||||||
minSize: number,
|
size: ScaleDimensionConfig;
|
||||||
maxSize: number,
|
color: ColorDimensionConfig;
|
||||||
opacity: number,
|
fillOpacity: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions: MarkersConfig = {
|
const defaultOptions: MarkersConfig = {
|
||||||
minSize: 1,
|
size: {
|
||||||
maxSize: 10,
|
fixed: 5,
|
||||||
opacity: 0.4,
|
min: 5,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
fixed: '#f00',
|
||||||
|
},
|
||||||
|
fillOpacity: 0.4,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MARKERS_LAYER_ID = "markers";
|
export const MARKERS_LAYER_ID = "markers";
|
||||||
@ -37,9 +49,7 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
|||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
create: (map: Map, options: MapLayerOptions<MarkersConfig>, theme: GrafanaTheme2): MapLayerHandler => {
|
create: (map: Map, options: MapLayerOptions<MarkersConfig>, theme: GrafanaTheme2): MapLayerHandler => {
|
||||||
const config = { ...defaultOptions, ...options.config };
|
|
||||||
const matchers = getLocationMatchers(options.location);
|
const matchers = getLocationMatchers(options.location);
|
||||||
|
|
||||||
const vectorLayer = new layer.Vector({});
|
const vectorLayer = new layer.Vector({});
|
||||||
return {
|
return {
|
||||||
init: () => vectorLayer,
|
init: () => vectorLayer,
|
||||||
@ -55,33 +65,25 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
|||||||
return; // ???
|
return; // ???
|
||||||
}
|
}
|
||||||
|
|
||||||
const field = frame.fields.find(field => field.type === FieldType.number); // TODO!!!!
|
// Assert default values
|
||||||
// Return early if metric field is not matched
|
const config = {
|
||||||
if (field === undefined) {
|
...defaultOptions,
|
||||||
return;
|
...options?.config,
|
||||||
};
|
};
|
||||||
|
const colorDim = getColorDimension(frame, config.color, theme);
|
||||||
// Retrieve the min, max and range of data values
|
const sizeDim = getScaledDimension(frame, config.size);
|
||||||
const calcs = reduceField({
|
const opacity = options.config?.fillOpacity ?? defaultOptions.fillOpacity;
|
||||||
field: field,
|
|
||||||
reducers: [
|
|
||||||
ReducerID.min,
|
|
||||||
ReducerID.max,
|
|
||||||
ReducerID.range,
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const features: Feature[] = [];
|
const features: Feature[] = [];
|
||||||
|
|
||||||
// Map each data value into new points
|
// Map each data value into new points
|
||||||
for (let i = 0; i < frame.length; i++) {
|
for (let i = 0; i < frame.length; i++) {
|
||||||
// Get the circle color for a specific data value depending on color scheme
|
// 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
|
// 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
|
// 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
|
// Create a new Feature for each point returned from dataFrameToPoints
|
||||||
const dot = new Feature({
|
const dot = new Feature({
|
||||||
@ -111,57 +113,45 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
// Circle overlay options
|
// Marker overlay options
|
||||||
registerOptionsUI: (builder) => {
|
registerOptionsUI: (builder) => {
|
||||||
builder
|
builder
|
||||||
// .addFieldNamePicker({
|
.addCustomEditor({
|
||||||
// path: 'fieldMapping.metricField',
|
id: 'config.color',
|
||||||
// name: 'Metric Field',
|
path: 'config.color',
|
||||||
// defaultValue: defaultOptions.fieldMapping.metricField,
|
name: 'Marker Color',
|
||||||
// settings: {
|
editor: ColorDimensionEditor,
|
||||||
// filter: (f) => f.type === FieldType.number,
|
settings: {},
|
||||||
// noFieldsMessage: 'No numeric fields found',
|
defaultValue: { // Configured values
|
||||||
// },
|
fixed: 'grey',
|
||||||
// })
|
},
|
||||||
.addNumberInput({
|
|
||||||
path: 'config.minSize',
|
|
||||||
description: 'configures the min circle size',
|
|
||||||
name: 'Min Size',
|
|
||||||
defaultValue: defaultOptions.minSize,
|
|
||||||
})
|
})
|
||||||
.addNumberInput({
|
.addCustomEditor({
|
||||||
path: 'config.maxSize',
|
id: 'config.size',
|
||||||
description: 'configures the max circle size',
|
path: 'config.size',
|
||||||
name: 'Max Size',
|
name: 'Marker Size',
|
||||||
defaultValue: defaultOptions.maxSize,
|
editor: ScaleDimensionEditor,
|
||||||
|
settings: {
|
||||||
|
min: 1,
|
||||||
|
max: 100, // possible in the UI
|
||||||
|
},
|
||||||
|
defaultValue: { // Configured values
|
||||||
|
fixed: 5,
|
||||||
|
min: 1,
|
||||||
|
max: 20,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.addSliderInput({
|
.addSliderInput({
|
||||||
path: 'config.opacity',
|
path: 'config.fillOpacity',
|
||||||
description: 'configures the amount of transparency',
|
name: 'Fill opacity',
|
||||||
name: 'Opacity',
|
defaultValue: defaultOptions.fillOpacity,
|
||||||
defaultValue: defaultOptions.opacity,
|
settings: {
|
||||||
settings: {
|
min: 0,
|
||||||
min: 0,
|
max: 1,
|
||||||
max: 1,
|
step: 0.1,
|
||||||
step: 0.1,
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
},
|
},
|
||||||
// fill in the default values
|
// fill in the default values
|
||||||
defaultOptions,
|
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