fields.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. import {
  2. DataFrame,
  3. DataFrameType,
  4. Field,
  5. FieldType,
  6. formattedValueToString,
  7. getDisplayProcessor,
  8. GrafanaTheme2,
  9. outerJoinDataFrames,
  10. PanelData,
  11. ValueFormatter,
  12. } from '@grafana/data';
  13. import {
  14. calculateHeatmapFromData,
  15. isHeatmapCellsDense,
  16. readHeatmapRowsCustomMeta,
  17. rowsToCellsHeatmap,
  18. } from 'app/features/transformers/calculateHeatmap/heatmap';
  19. import { HeatmapCellLayout } from 'app/features/transformers/calculateHeatmap/models.gen';
  20. import { CellValues, PanelOptions } from './models.gen';
  21. import { boundedMinMax } from './utils';
  22. export interface HeatmapData {
  23. heatmap?: DataFrame; // data we will render
  24. exemplars?: DataFrame; // optionally linked exemplars
  25. exemplarColor?: string;
  26. xBucketSize?: number;
  27. yBucketSize?: number;
  28. xBucketCount?: number;
  29. yBucketCount?: number;
  30. xLayout?: HeatmapCellLayout;
  31. yLayout?: HeatmapCellLayout;
  32. // color scale range
  33. minValue?: number;
  34. maxValue?: number;
  35. // Print a heatmap cell value
  36. display?: (v: number) => string;
  37. // Errors
  38. warning?: string;
  39. }
  40. export function prepareHeatmapData(data: PanelData, options: PanelOptions, theme: GrafanaTheme2): HeatmapData {
  41. let frames = data.series;
  42. if (!frames?.length) {
  43. return {};
  44. }
  45. const exemplars = data.annotations?.find((f) => f.name === 'exemplar');
  46. if (options.calculate) {
  47. return getDenseHeatmapData(calculateHeatmapFromData(frames, options.calculation ?? {}), exemplars, options, theme);
  48. }
  49. // Check for known heatmap types
  50. let rowsHeatmap: DataFrame | undefined = undefined;
  51. for (const frame of frames) {
  52. switch (frame.meta?.type) {
  53. case DataFrameType.HeatmapCells:
  54. return isHeatmapCellsDense(frame)
  55. ? getDenseHeatmapData(frame, exemplars, options, theme)
  56. : getSparseHeatmapData(frame, exemplars, options, theme);
  57. case DataFrameType.HeatmapRows:
  58. rowsHeatmap = frame; // the default format
  59. }
  60. }
  61. // Everything past here assumes a field for each row in the heatmap (buckets)
  62. if (!rowsHeatmap) {
  63. if (frames.length > 1) {
  64. rowsHeatmap = [
  65. outerJoinDataFrames({
  66. frames,
  67. })!,
  68. ][0];
  69. } else {
  70. rowsHeatmap = frames[0];
  71. }
  72. }
  73. return getDenseHeatmapData(
  74. rowsToCellsHeatmap({
  75. unit: options.yAxis?.unit, // used to format the ordinal lookup values
  76. decimals: options.yAxis?.decimals,
  77. ...options.rowsFrame,
  78. frame: rowsHeatmap,
  79. }),
  80. exemplars,
  81. options,
  82. theme
  83. );
  84. }
  85. const getSparseHeatmapData = (
  86. frame: DataFrame,
  87. exemplars: DataFrame | undefined,
  88. options: PanelOptions,
  89. theme: GrafanaTheme2
  90. ): HeatmapData => {
  91. if (frame.meta?.type !== DataFrameType.HeatmapCells || isHeatmapCellsDense(frame)) {
  92. return {
  93. warning: 'Expected sparse heatmap format',
  94. heatmap: frame,
  95. };
  96. }
  97. // y axis tick label display
  98. updateFieldDisplay(frame.fields[1], options.yAxis, theme);
  99. // cell value display
  100. const disp = updateFieldDisplay(frame.fields[3], options.cellValues, theme);
  101. let [minValue, maxValue] = boundedMinMax(
  102. frame.fields[3].values.toArray(),
  103. options.color.min,
  104. options.color.max,
  105. options.filterValues?.le,
  106. options.filterValues?.ge
  107. );
  108. return {
  109. heatmap: frame,
  110. minValue,
  111. maxValue,
  112. exemplars,
  113. display: (v) => formattedValueToString(disp(v)),
  114. };
  115. };
  116. const getDenseHeatmapData = (
  117. frame: DataFrame,
  118. exemplars: DataFrame | undefined,
  119. options: PanelOptions,
  120. theme: GrafanaTheme2
  121. ): HeatmapData => {
  122. if (frame.meta?.type !== DataFrameType.HeatmapCells) {
  123. return {
  124. warning: 'Expected heatmap scanlines format',
  125. heatmap: frame,
  126. };
  127. }
  128. if (frame.fields.length < 2 || frame.length < 2) {
  129. return { heatmap: frame };
  130. }
  131. const meta = readHeatmapRowsCustomMeta(frame);
  132. let xName: string | undefined = undefined;
  133. let yName: string | undefined = undefined;
  134. let valueField: Field | undefined = undefined;
  135. // validate field display properties
  136. for (const field of frame.fields) {
  137. switch (field.name) {
  138. case 'y':
  139. yName = field.name;
  140. case 'yMin':
  141. case 'yMax': {
  142. if (!yName) {
  143. yName = field.name;
  144. }
  145. if (meta.yOrdinalDisplay == null) {
  146. updateFieldDisplay(field, options.yAxis, theme);
  147. }
  148. break;
  149. }
  150. case 'x':
  151. case 'xMin':
  152. case 'xMax':
  153. xName = field.name;
  154. break;
  155. default: {
  156. if (field.type === FieldType.number && !valueField) {
  157. valueField = field;
  158. }
  159. }
  160. }
  161. }
  162. if (!yName) {
  163. return { warning: 'Missing Y field', heatmap: frame };
  164. }
  165. if (!yName) {
  166. return { warning: 'Missing X field', heatmap: frame };
  167. }
  168. if (!valueField) {
  169. return { warning: 'Missing value field', heatmap: frame };
  170. }
  171. const disp = updateFieldDisplay(valueField, options.cellValues, theme);
  172. // infer bucket sizes from data (for now)
  173. // the 'heatmap-scanlines' dense frame format looks like:
  174. // x: 1,1,1,1,2,2,2,2
  175. // y: 3,4,5,6,3,4,5,6
  176. // count: 0,0,0,7,0,3,0,1
  177. const xs = frame.fields[0].values.toArray();
  178. const ys = frame.fields[1].values.toArray();
  179. const dlen = xs.length;
  180. // below is literally copy/paste from the pathBuilder code in utils.ts
  181. // detect x and y bin qtys by detecting layout repetition in x & y data
  182. let yBinQty = dlen - ys.lastIndexOf(ys[0]);
  183. let xBinQty = dlen / yBinQty;
  184. let yBinIncr = ys[1] - ys[0];
  185. let xBinIncr = xs[yBinQty] - xs[0];
  186. let [minValue, maxValue] = boundedMinMax(
  187. valueField.values.toArray(),
  188. options.color.min,
  189. options.color.max,
  190. options.filterValues?.le,
  191. options.filterValues?.ge
  192. );
  193. const data: HeatmapData = {
  194. heatmap: frame,
  195. exemplars: exemplars?.length ? exemplars : undefined,
  196. xBucketSize: xBinIncr,
  197. yBucketSize: yBinIncr,
  198. xBucketCount: xBinQty,
  199. yBucketCount: yBinQty,
  200. minValue,
  201. maxValue,
  202. // TODO: improve heuristic
  203. xLayout:
  204. xName === 'xMax' ? HeatmapCellLayout.le : xName === 'xMin' ? HeatmapCellLayout.ge : HeatmapCellLayout.unknown,
  205. yLayout:
  206. yName === 'yMax' ? HeatmapCellLayout.le : yName === 'yMin' ? HeatmapCellLayout.ge : HeatmapCellLayout.unknown,
  207. display: (v) => formattedValueToString(disp(v)),
  208. };
  209. return data;
  210. };
  211. function updateFieldDisplay(field: Field, opts: CellValues | undefined, theme: GrafanaTheme2): ValueFormatter {
  212. if (opts?.unit?.length || opts?.decimals != null) {
  213. const { unit, decimals } = opts;
  214. field.display = undefined;
  215. field.config = { ...field.config };
  216. if (unit?.length) {
  217. field.config.unit = unit;
  218. }
  219. if (decimals != null) {
  220. field.config.decimals = decimals;
  221. }
  222. }
  223. if (!field.display) {
  224. field.display = getDisplayProcessor({ field, theme });
  225. }
  226. return field.display;
  227. }