mirror of
https://github.com/grafana/grafana.git
synced 2024-12-02 05:29:42 -06:00
Heatmap (new): add exemplar mapping function (#48780)
This commit is contained in:
parent
c1b5ea3e54
commit
454e804657
@ -42,7 +42,7 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
|
|||||||
let timeRangeRef = useRef<TimeRange>(timeRange);
|
let timeRangeRef = useRef<TimeRange>(timeRange);
|
||||||
timeRangeRef.current = timeRange;
|
timeRangeRef.current = timeRange;
|
||||||
|
|
||||||
const info = useMemo(() => prepareHeatmapData(data.series, options, theme), [data, options, theme]);
|
const info = useMemo(() => prepareHeatmapData(data, options, theme), [data, options, theme]);
|
||||||
|
|
||||||
const facets = useMemo(() => [null, info.heatmap?.fields.map((f) => f.values.toArray())], [info.heatmap]);
|
const facets = useMemo(() => [null, info.heatmap?.fields.map((f) => f.values.toArray())], [info.heatmap]);
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { createTheme } from '@grafana/data';
|
import { createTheme, ArrayVector, DataFrameType, FieldType } from '@grafana/data';
|
||||||
|
|
||||||
|
import { BucketLayout, getAnnotationMapping, HEATMAP_NOT_SCANLINES_ERROR } from './fields';
|
||||||
import { PanelOptions } from './models.gen';
|
import { PanelOptions } from './models.gen';
|
||||||
|
|
||||||
const theme = createTheme();
|
const theme = createTheme();
|
||||||
@ -12,3 +13,312 @@ describe('Heatmap data', () => {
|
|||||||
expect(options).toBeDefined();
|
expect(options).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('creating a heatmap data mapping', () => {
|
||||||
|
describe('generates a simple data mapping with orderly data', () => {
|
||||||
|
const mapping = getAnnotationMapping(
|
||||||
|
{
|
||||||
|
heatmap: {
|
||||||
|
name: 'test',
|
||||||
|
meta: {
|
||||||
|
type: DataFrameType.HeatmapScanlines,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'xMin',
|
||||||
|
type: FieldType.number,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([1]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'yMin',
|
||||||
|
type: FieldType.number,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([1, 4, 7]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'count',
|
||||||
|
type: FieldType.number,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([3, 3, 3]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
length: 3,
|
||||||
|
},
|
||||||
|
xBucketCount: 1,
|
||||||
|
xBucketSize: 9,
|
||||||
|
xLayout: BucketLayout.ge,
|
||||||
|
yBucketCount: 3,
|
||||||
|
yBucketSize: 3,
|
||||||
|
yLayout: BucketLayout.ge,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'origdata',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'time',
|
||||||
|
type: FieldType.time,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([1, 2, 3, 4, 5, 6, 7, 8, 9]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'value',
|
||||||
|
type: FieldType.number,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([1, 2, 3, 4, 5, 6, 7, 8, 9]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
length: 2,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it('takes good data, and delivers a working data mapping', () => {
|
||||||
|
expect(mapping.lookup.length).toEqual(3);
|
||||||
|
expect(mapping.lookup[0]).toEqual([0, 1, 2]);
|
||||||
|
expect(mapping.lookup[1]).toEqual([3, 4, 5]);
|
||||||
|
expect(mapping.lookup[2]).toEqual([6, 7, 8]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('generates a data mapping with less orderly data (counts are different)', () => {
|
||||||
|
const heatmap = {
|
||||||
|
heatmap: {
|
||||||
|
name: 'test',
|
||||||
|
meta: {
|
||||||
|
type: DataFrameType.HeatmapScanlines,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'xMin',
|
||||||
|
type: FieldType.number,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([1]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'yMin',
|
||||||
|
type: FieldType.number,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([1, 4, 7]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'count',
|
||||||
|
type: FieldType.number,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([2, 1, 6]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
length: 3,
|
||||||
|
},
|
||||||
|
xBucketCount: 1,
|
||||||
|
xBucketSize: 9,
|
||||||
|
xLayout: BucketLayout.ge,
|
||||||
|
yBucketCount: 3,
|
||||||
|
yBucketSize: 3,
|
||||||
|
yLayout: BucketLayout.ge,
|
||||||
|
};
|
||||||
|
const rawData = {
|
||||||
|
name: 'origdata',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'time',
|
||||||
|
type: FieldType.time,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([1, 2, 3, 4, 5, 6, 7, 8, 9]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'value',
|
||||||
|
type: FieldType.number,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([7, 1, 8, 7, 2, 7, 8, 8, 4]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
length: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
it("Puts data into the proper buckets, when we don't care about the count", () => {
|
||||||
|
// In this case, we are just finding proper values, but don't care if a values
|
||||||
|
// exists in the bucket in the original data or not. Therefore, we should see
|
||||||
|
// a value mapped into the second mapping bucket containing the value '8'.
|
||||||
|
const mapping = getAnnotationMapping(heatmap, rawData);
|
||||||
|
expect(mapping.lookup.length).toEqual(3);
|
||||||
|
expect(mapping.lookup[0]).toEqual([1, 4]);
|
||||||
|
expect(mapping.lookup[1]).toEqual([8]);
|
||||||
|
expect(mapping.lookup[2]).toEqual([0, 2, 3, 5, 6, 7]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Handles a larger data set that will not fill all buckets', () => {
|
||||||
|
const mapping = getAnnotationMapping(
|
||||||
|
{
|
||||||
|
heatmap: {
|
||||||
|
name: 'test',
|
||||||
|
meta: {
|
||||||
|
type: DataFrameType.HeatmapScanlines,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'xMin',
|
||||||
|
type: FieldType.number,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([1, 1, 1, 4, 4, 4, 7, 7, 7]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'yMin',
|
||||||
|
type: FieldType.number,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([1, 4, 7, 1, 4, 7, 1, 4, 7]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'count',
|
||||||
|
type: FieldType.number,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([0, 0, 2, 0, 0, 3, 0, 2, 0]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
length: 3,
|
||||||
|
},
|
||||||
|
xBucketCount: 3,
|
||||||
|
xBucketSize: 3,
|
||||||
|
xLayout: BucketLayout.ge,
|
||||||
|
yBucketCount: 3,
|
||||||
|
yBucketSize: 3,
|
||||||
|
yLayout: BucketLayout.ge,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'origdata',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'time',
|
||||||
|
type: FieldType.time,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([1, 2, 3, 4, 5, 6, 7, 8, 9]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'value',
|
||||||
|
type: FieldType.number,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([8, 0, 8, 7, 7, 8, 6, 10, 6]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
length: 2,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it('Creates the data mapping correctly', () => {
|
||||||
|
expect(mapping.lookup.length).toEqual(9);
|
||||||
|
expect(mapping).toEqual({
|
||||||
|
lookup: [null, null, [0, 2], null, null, [3, 4, 5], null, [6, 8], null],
|
||||||
|
low: [1],
|
||||||
|
high: [7],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters out minimum and maximum values', () => {
|
||||||
|
expect(mapping.lookup.flat()).not.toContainEqual(1);
|
||||||
|
expect(mapping.lookup.flat()).not.toContainEqual(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error scenarios', () => {
|
||||||
|
const heatmap = {
|
||||||
|
heatmap: {
|
||||||
|
name: 'test',
|
||||||
|
meta: {
|
||||||
|
type: DataFrameType.HeatmapBuckets,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'xMin',
|
||||||
|
type: FieldType.number,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([1]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'yMin',
|
||||||
|
type: FieldType.number,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([1, 4, 7]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'count',
|
||||||
|
type: FieldType.number,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([2, 0, 6]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
length: 3,
|
||||||
|
},
|
||||||
|
xBucketCount: 1,
|
||||||
|
xBucketSize: 9,
|
||||||
|
xLayout: BucketLayout.ge,
|
||||||
|
yBucketCount: 3,
|
||||||
|
yBucketSize: 3,
|
||||||
|
yLayout: BucketLayout.ge,
|
||||||
|
};
|
||||||
|
const rawData = {
|
||||||
|
name: 'origdata',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'time',
|
||||||
|
type: FieldType.time,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([1, 2, 3, 4, 5, 6, 7, 8, 9]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'value',
|
||||||
|
type: FieldType.number,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([7, 1, 8, 7, 2, 7, 8, 8, 4]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
length: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
it('Will not process heatmap buckets', () => {
|
||||||
|
expect(() =>
|
||||||
|
getAnnotationMapping(
|
||||||
|
{
|
||||||
|
...heatmap,
|
||||||
|
heatmap: {
|
||||||
|
...heatmap.heatmap,
|
||||||
|
meta: {
|
||||||
|
type: DataFrameType.HeatmapBuckets,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rawData
|
||||||
|
)
|
||||||
|
).toThrow(HEATMAP_NOT_SCANLINES_ERROR);
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
getAnnotationMapping(
|
||||||
|
{
|
||||||
|
...heatmap,
|
||||||
|
heatmap: {
|
||||||
|
...heatmap.heatmap,
|
||||||
|
meta: {
|
||||||
|
type: DataFrameType.TimeSeriesWide,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rawData
|
||||||
|
)
|
||||||
|
).toThrow(HEATMAP_NOT_SCANLINES_ERROR);
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
getAnnotationMapping(
|
||||||
|
{
|
||||||
|
...heatmap,
|
||||||
|
heatmap: {
|
||||||
|
...heatmap.heatmap,
|
||||||
|
meta: {
|
||||||
|
type: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rawData
|
||||||
|
)
|
||||||
|
).toThrow(HEATMAP_NOT_SCANLINES_ERROR);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
import {
|
import {
|
||||||
DataFrame,
|
DataFrame,
|
||||||
DataFrameType,
|
DataFrameType,
|
||||||
|
Field,
|
||||||
FieldType,
|
FieldType,
|
||||||
formattedValueToString,
|
formattedValueToString,
|
||||||
getDisplayProcessor,
|
getDisplayProcessor,
|
||||||
getFieldDisplayName,
|
getFieldDisplayName,
|
||||||
getValueFormat,
|
getValueFormat,
|
||||||
GrafanaTheme2,
|
GrafanaTheme2,
|
||||||
|
incrRoundDn,
|
||||||
|
incrRoundUp,
|
||||||
|
PanelData,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { calculateHeatmapFromData, bucketsToScanlines } from 'app/features/transformers/calculateHeatmap/heatmap';
|
import { calculateHeatmapFromData, bucketsToScanlines } from 'app/features/transformers/calculateHeatmap/heatmap';
|
||||||
|
|
||||||
@ -17,9 +21,19 @@ export const enum BucketLayout {
|
|||||||
ge = 'ge',
|
ge = 'ge',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HeatmapDataMapping {
|
||||||
|
lookup: Array<number[] | null>;
|
||||||
|
high: number[]; // index of values bigger than the max Y
|
||||||
|
low: number[]; // index of values less than the min Y
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HEATMAP_NOT_SCANLINES_ERROR = 'A calculated heatmap was expected, but not found';
|
||||||
|
|
||||||
export interface HeatmapData {
|
export interface HeatmapData {
|
||||||
// List of heatmap frames
|
// List of heatmap frames
|
||||||
heatmap?: DataFrame;
|
heatmap?: DataFrame;
|
||||||
|
annotations?: DataFrame;
|
||||||
|
annotationMappings?: HeatmapDataMapping;
|
||||||
|
|
||||||
yAxisValues?: Array<number | string | null>;
|
yAxisValues?: Array<number | string | null>;
|
||||||
|
|
||||||
@ -39,25 +53,25 @@ export interface HeatmapData {
|
|||||||
warning?: string;
|
warning?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prepareHeatmapData(
|
export function prepareHeatmapData(data: PanelData, options: PanelOptions, theme: GrafanaTheme2): HeatmapData {
|
||||||
frames: DataFrame[] | undefined,
|
const frames = data.series;
|
||||||
options: PanelOptions,
|
|
||||||
theme: GrafanaTheme2
|
|
||||||
): HeatmapData {
|
|
||||||
if (!frames?.length) {
|
if (!frames?.length) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { source } = options;
|
const { source } = options;
|
||||||
|
|
||||||
|
const annotations = data.annotations?.[0]; // TODO: Maybe join on time with frames
|
||||||
|
|
||||||
if (source === HeatmapSourceMode.Calculate) {
|
if (source === HeatmapSourceMode.Calculate) {
|
||||||
// TODO, check for error etc
|
// TODO, check for error etc
|
||||||
return getHeatmapData(calculateHeatmapFromData(frames, options.heatmap ?? {}), theme);
|
return getHeatmapData(calculateHeatmapFromData(frames, options.heatmap ?? {}), annotations, theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find a well defined heatmap
|
// Find a well defined heatmap
|
||||||
let scanlinesHeatmap = frames.find((f) => f.meta?.type === DataFrameType.HeatmapScanlines);
|
let scanlinesHeatmap = frames.find((f) => f.meta?.type === DataFrameType.HeatmapScanlines);
|
||||||
if (scanlinesHeatmap) {
|
if (scanlinesHeatmap) {
|
||||||
return getHeatmapData(scanlinesHeatmap, theme);
|
return getHeatmapData(scanlinesHeatmap, annotations, theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
let bucketsHeatmap = frames.find((f) => f.meta?.type === DataFrameType.HeatmapBuckets);
|
let bucketsHeatmap = frames.find((f) => f.meta?.type === DataFrameType.HeatmapBuckets);
|
||||||
@ -66,19 +80,79 @@ export function prepareHeatmapData(
|
|||||||
yAxisValues: frames[0].fields.flatMap((field) =>
|
yAxisValues: frames[0].fields.flatMap((field) =>
|
||||||
field.type === FieldType.number ? getFieldDisplayName(field) : []
|
field.type === FieldType.number ? getFieldDisplayName(field) : []
|
||||||
),
|
),
|
||||||
...getHeatmapData(bucketsToScanlines(bucketsHeatmap), theme),
|
...getHeatmapData(bucketsToScanlines(bucketsHeatmap), annotations, theme),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (source === HeatmapSourceMode.Data) {
|
if (source === HeatmapSourceMode.Data) {
|
||||||
return getHeatmapData(bucketsToScanlines(frames[0]), theme);
|
return getHeatmapData(bucketsToScanlines(frames[0]), annotations, theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO, check for error etc
|
// TODO, check for error etc
|
||||||
return getHeatmapData(calculateHeatmapFromData(frames, options.heatmap ?? {}), theme);
|
return getHeatmapData(calculateHeatmapFromData(frames, options.heatmap ?? {}), annotations, theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getHeatmapData = (frame: DataFrame, theme: GrafanaTheme2): HeatmapData => {
|
const getHeatmapFields = (dataFrame: DataFrame): Array<Field | undefined> => {
|
||||||
|
const xField: Field | undefined = dataFrame.fields.find((f) => f.name === 'xMin');
|
||||||
|
const yField: Field | undefined = dataFrame.fields.find((f) => f.name === 'yMin');
|
||||||
|
const countField: Field | undefined = dataFrame.fields.find((f) => f.name === 'count');
|
||||||
|
|
||||||
|
return [xField, yField, countField];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAnnotationMapping = (heatmapData: HeatmapData, rawData: DataFrame): HeatmapDataMapping => {
|
||||||
|
if (heatmapData.heatmap?.meta?.type !== DataFrameType.HeatmapScanlines) {
|
||||||
|
throw HEATMAP_NOT_SCANLINES_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [fxs, fys] = getHeatmapFields(heatmapData.heatmap!);
|
||||||
|
|
||||||
|
if (!fxs || !fys) {
|
||||||
|
throw HEATMAP_NOT_SCANLINES_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapping: HeatmapDataMapping = {
|
||||||
|
lookup: new Array(heatmapData.xBucketCount! * heatmapData.yBucketCount!).fill(null),
|
||||||
|
high: [],
|
||||||
|
low: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const xos: number[] | undefined = rawData.fields.find((f: Field) => f.type === 'time')?.values.toArray();
|
||||||
|
const yos: number[] | undefined = rawData.fields.find((f: Field) => f.type === 'number')?.values.toArray();
|
||||||
|
|
||||||
|
if (!xos || !yos) {
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
const xsmin = fxs.values.get(0);
|
||||||
|
const ysmin = fys.values.get(0);
|
||||||
|
const xsmax = fxs.values.get(fxs.values.length - 1) + heatmapData.xBucketSize!;
|
||||||
|
const ysmax = fys.values.get(fys.values.length - 1) + heatmapData.yBucketSize!;
|
||||||
|
xos.forEach((xo: number, i: number) => {
|
||||||
|
const yo = yos[i];
|
||||||
|
const xBucketIdx = Math.floor(incrRoundDn(incrRoundUp((xo - xsmin) / heatmapData.xBucketSize!, 1e-7), 1e-7));
|
||||||
|
const yBucketIdx = Math.floor(incrRoundDn(incrRoundUp((yo - ysmin) / heatmapData.yBucketSize!, 1e-7), 1e-7));
|
||||||
|
|
||||||
|
if (xo < xsmin || yo < ysmin) {
|
||||||
|
mapping.low.push(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xo >= xsmax || yo >= ysmax) {
|
||||||
|
mapping.high.push(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = xBucketIdx * heatmapData.yBucketCount! + yBucketIdx;
|
||||||
|
if (mapping.lookup[index] === null) {
|
||||||
|
mapping.lookup[index] = [];
|
||||||
|
}
|
||||||
|
mapping.lookup[index]?.push(i);
|
||||||
|
});
|
||||||
|
return mapping;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHeatmapData = (frame: DataFrame, annotations: DataFrame | undefined, theme: GrafanaTheme2): HeatmapData => {
|
||||||
if (frame.meta?.type !== DataFrameType.HeatmapScanlines) {
|
if (frame.meta?.type !== DataFrameType.HeatmapScanlines) {
|
||||||
return {
|
return {
|
||||||
warning: 'Expected heatmap scanlines format',
|
warning: 'Expected heatmap scanlines format',
|
||||||
@ -114,8 +188,9 @@ const getHeatmapData = (frame: DataFrame, theme: GrafanaTheme2): HeatmapData =>
|
|||||||
|
|
||||||
// The "count" field
|
// The "count" field
|
||||||
const disp = frame.fields[2].display ?? getValueFormat('short');
|
const disp = frame.fields[2].display ?? getValueFormat('short');
|
||||||
return {
|
const data: HeatmapData = {
|
||||||
heatmap: frame,
|
heatmap: frame,
|
||||||
|
annotations,
|
||||||
xBucketSize: xBinIncr,
|
xBucketSize: xBinIncr,
|
||||||
yBucketSize: yBinIncr,
|
yBucketSize: yBinIncr,
|
||||||
xBucketCount: xBinQty,
|
xBucketCount: xBinQty,
|
||||||
@ -127,4 +202,10 @@ const getHeatmapData = (frame: DataFrame, theme: GrafanaTheme2): HeatmapData =>
|
|||||||
|
|
||||||
display: (v) => formattedValueToString(disp(v)),
|
display: (v) => formattedValueToString(disp(v)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (annotations) {
|
||||||
|
data.annotationMappings = getAnnotationMapping(data, annotations);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
};
|
};
|
||||||
|
@ -18,7 +18,7 @@ export class HeatmapSuggestionsSupplier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const info = prepareHeatmapData(builder.data.series, defaultPanelOptions, config.theme2);
|
const info = prepareHeatmapData(builder.data, defaultPanelOptions, config.theme2);
|
||||||
if (!info || info.warning) {
|
if (!info || info.warning) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
5
public/app/plugins/panel/heatmap-new/utils.test.ts
Normal file
5
public/app/plugins/panel/heatmap-new/utils.test.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
describe('a test', () => {
|
||||||
|
it('has to have at least one test', () => {
|
||||||
|
expect(true).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user