grafana/public/app/plugins/panel/heatmap/fields.ts
Galen Kistler f37997aec0
Prometheus: Add traceID link to heatmap exemplar popover (#60039)
* Add traceID link to heatmap exemplar popover
* Render links for each field individually

Co-authored-by: ismail simsek <ismailsimsek09@gmail.com>
2022-12-08 12:02:17 -06:00

295 lines
7.3 KiB
TypeScript

import {
DataFrame,
DataFrameType,
Field,
FieldType,
formattedValueToString,
getDisplayProcessor,
GrafanaTheme2,
LinkModel,
outerJoinDataFrames,
PanelData,
ValueFormatter,
ValueLinkConfig,
} from '@grafana/data';
import {
calculateHeatmapFromData,
isHeatmapCellsDense,
readHeatmapRowsCustomMeta,
rowsToCellsHeatmap,
} from 'app/features/transformers/calculateHeatmap/heatmap';
import { HeatmapCellLayout } from 'app/features/transformers/calculateHeatmap/models.gen';
import { CellValues, PanelOptions } from './models.gen';
import { boundedMinMax } from './utils';
export interface HeatmapData {
heatmap?: DataFrame; // data we will render
exemplars?: DataFrame; // optionally linked exemplars
exemplarColor?: string;
xBucketSize?: number;
yBucketSize?: number;
xBucketCount?: number;
yBucketCount?: number;
xLayout?: HeatmapCellLayout;
yLayout?: HeatmapCellLayout;
xLog?: number;
yLog?: number;
xLogSplit?: number;
yLogSplit?: number;
// color scale range
minValue?: number;
maxValue?: number;
// Print a heatmap cell value
display?: (v: number) => string;
// Errors
warning?: string;
}
export function prepareHeatmapData(
data: PanelData,
options: PanelOptions,
theme: GrafanaTheme2,
getFieldLinks?: (exemplars: DataFrame, field: Field) => (config: ValueLinkConfig) => Array<LinkModel<Field>>
): HeatmapData {
let frames = data.series;
if (!frames?.length) {
return {};
}
const exemplars = data.annotations?.find((f) => f.name === 'exemplar');
if (getFieldLinks) {
exemplars?.fields.forEach((field, index) => {
exemplars.fields[index].getLinks = getFieldLinks(exemplars, field);
});
}
if (options.calculate) {
return getDenseHeatmapData(calculateHeatmapFromData(frames, options.calculation ?? {}), exemplars, options, theme);
}
// Check for known heatmap types
let rowsHeatmap: DataFrame | undefined = undefined;
for (const frame of frames) {
switch (frame.meta?.type) {
case DataFrameType.HeatmapCells:
return isHeatmapCellsDense(frame)
? getDenseHeatmapData(frame, exemplars, options, theme)
: getSparseHeatmapData(frame, exemplars, options, theme);
case DataFrameType.HeatmapRows:
rowsHeatmap = frame; // the default format
}
}
// Everything past here assumes a field for each row in the heatmap (buckets)
if (!rowsHeatmap) {
if (frames.length > 1) {
rowsHeatmap = [
outerJoinDataFrames({
frames,
})!,
][0];
} else {
rowsHeatmap = frames[0];
}
}
return getDenseHeatmapData(
rowsToCellsHeatmap({
unit: options.yAxis?.unit, // used to format the ordinal lookup values
decimals: options.yAxis?.decimals,
...options.rowsFrame,
frame: rowsHeatmap,
}),
exemplars,
options,
theme
);
}
const getSparseHeatmapData = (
frame: DataFrame,
exemplars: DataFrame | undefined,
options: PanelOptions,
theme: GrafanaTheme2
): HeatmapData => {
if (frame.meta?.type !== DataFrameType.HeatmapCells || isHeatmapCellsDense(frame)) {
return {
warning: 'Expected sparse heatmap format',
heatmap: frame,
};
}
// y axis tick label display
updateFieldDisplay(frame.fields[1], options.yAxis, theme);
// cell value display
const disp = updateFieldDisplay(frame.fields[3], options.cellValues, theme);
let [minValue, maxValue] = boundedMinMax(
frame.fields[3].values.toArray(),
options.color.min,
options.color.max,
options.filterValues?.le,
options.filterValues?.ge
);
return {
heatmap: frame,
minValue,
maxValue,
exemplars,
display: (v) => formattedValueToString(disp(v)),
};
};
const getDenseHeatmapData = (
frame: DataFrame,
exemplars: DataFrame | undefined,
options: PanelOptions,
theme: GrafanaTheme2
): HeatmapData => {
if (frame.meta?.type !== DataFrameType.HeatmapCells) {
return {
warning: 'Expected heatmap scanlines format',
heatmap: frame,
};
}
if (frame.fields.length < 2 || frame.length < 2) {
return { heatmap: frame };
}
const meta = readHeatmapRowsCustomMeta(frame);
let xName: string | undefined = undefined;
let yName: string | undefined = undefined;
let valueField: Field | undefined = undefined;
// validate field display properties
for (const field of frame.fields) {
switch (field.name) {
case 'y':
yName = field.name;
case 'yMin':
case 'yMax': {
if (!yName) {
yName = field.name;
}
if (meta.yOrdinalDisplay == null) {
updateFieldDisplay(field, options.yAxis, theme);
}
break;
}
case 'x':
case 'xMin':
case 'xMax':
xName = field.name;
break;
default: {
if (field.type === FieldType.number && !valueField) {
valueField = field;
}
}
}
}
if (!yName) {
return { warning: 'Missing Y field', heatmap: frame };
}
if (!yName) {
return { warning: 'Missing X field', heatmap: frame };
}
if (!valueField) {
return { warning: 'Missing value field', heatmap: frame };
}
const disp = updateFieldDisplay(valueField, options.cellValues, theme);
// infer bucket sizes from data (for now)
// the 'heatmap-scanlines' dense frame format looks like:
// x: 1,1,1,1,2,2,2,2
// y: 3,4,5,6,3,4,5,6
// count: 0,0,0,7,0,3,0,1
const xs = frame.fields[0].values.toArray();
const ys = frame.fields[1].values.toArray();
const dlen = xs.length;
// below is literally copy/paste from the pathBuilder code in utils.ts
// detect x and y bin qtys by detecting layout repetition in x & y data
let yBinQty = dlen - ys.lastIndexOf(ys[0]);
let xBinQty = dlen / yBinQty;
let yBinIncr = ys[1] - ys[0];
let xBinIncr = xs[yBinQty] - xs[0];
let [minValue, maxValue] = boundedMinMax(
valueField.values.toArray(),
options.color.min,
options.color.max,
options.filterValues?.le,
options.filterValues?.ge
);
let calcX = options.calculation?.xBuckets;
let calcY = options.calculation?.yBuckets;
const data: HeatmapData = {
heatmap: frame,
exemplars: exemplars?.length ? exemplars : undefined,
xBucketSize: xBinIncr,
yBucketSize: yBinIncr,
xBucketCount: xBinQty,
yBucketCount: yBinQty,
yLog: calcY?.scale?.log ?? 0,
xLog: calcX?.scale?.log ?? 0,
xLogSplit: calcX?.scale?.log ? +(calcX?.value ?? '1') : 1,
yLogSplit: calcY?.scale?.log ? +(calcY?.value ?? '1') : 1,
minValue,
maxValue,
// TODO: improve heuristic
xLayout:
xName === 'xMax' ? HeatmapCellLayout.le : xName === 'xMin' ? HeatmapCellLayout.ge : HeatmapCellLayout.unknown,
yLayout:
yName === 'yMax' ? HeatmapCellLayout.le : yName === 'yMin' ? HeatmapCellLayout.ge : HeatmapCellLayout.unknown,
display: (v) => formattedValueToString(disp(v)),
};
return data;
};
function updateFieldDisplay(field: Field, opts: CellValues | undefined, theme: GrafanaTheme2): ValueFormatter {
if (opts?.unit?.length || opts?.decimals != null) {
const { unit, decimals } = opts;
field.display = undefined;
field.config = { ...field.config };
if (unit?.length) {
field.config.unit = unit;
}
if (decimals != null) {
field.config.decimals = decimals;
}
}
if (!field.display) {
field.display = getDisplayProcessor({ field, theme });
}
return field.display;
}