123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266 |
- import {
- DataFrame,
- DataFrameType,
- Field,
- FieldType,
- formattedValueToString,
- getDisplayProcessor,
- GrafanaTheme2,
- outerJoinDataFrames,
- PanelData,
- ValueFormatter,
- } 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;
- // 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): HeatmapData {
- let frames = data.series;
- if (!frames?.length) {
- return {};
- }
- const exemplars = data.annotations?.find((f) => f.name === 'exemplar');
- 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
- );
- const data: HeatmapData = {
- heatmap: frame,
- exemplars: exemplars?.length ? exemplars : undefined,
- xBucketSize: xBinIncr,
- yBucketSize: yBinIncr,
- xBucketCount: xBinQty,
- yBucketCount: yBinQty,
- 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;
- }
|