utils.ts 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. import {
  2. ArrayVector,
  3. DataFrame,
  4. Field,
  5. FieldCache,
  6. FieldType,
  7. GrafanaTheme2,
  8. MutableDataFrame,
  9. NodeGraphDataFrameFieldNames,
  10. } from '@grafana/data';
  11. import { EdgeDatum, NodeDatum } from './types';
  12. type Line = { x1: number; y1: number; x2: number; y2: number };
  13. /**
  14. * Makes line shorter while keeping the middle in he same place.
  15. */
  16. export function shortenLine(line: Line, length: number): Line {
  17. const vx = line.x2 - line.x1;
  18. const vy = line.y2 - line.y1;
  19. const mag = Math.sqrt(vx * vx + vy * vy);
  20. const ratio = Math.max((mag - length) / mag, 0);
  21. const vx2 = vx * ratio;
  22. const vy2 = vy * ratio;
  23. const xDiff = vx - vx2;
  24. const yDiff = vy - vy2;
  25. const newx1 = line.x1 + xDiff / 2;
  26. const newy1 = line.y1 + yDiff / 2;
  27. return {
  28. x1: newx1,
  29. y1: newy1,
  30. x2: newx1 + vx2,
  31. y2: newy1 + vy2,
  32. };
  33. }
  34. export function getNodeFields(nodes: DataFrame) {
  35. const normalizedFrames = {
  36. ...nodes,
  37. fields: nodes.fields.map((field) => ({ ...field, name: field.name.toLowerCase() })),
  38. };
  39. const fieldsCache = new FieldCache(normalizedFrames);
  40. return {
  41. id: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.id.toLowerCase()),
  42. title: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.title.toLowerCase()),
  43. subTitle: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.subTitle.toLowerCase()),
  44. mainStat: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.mainStat.toLowerCase()),
  45. secondaryStat: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.secondaryStat.toLowerCase()),
  46. arc: findFieldsByPrefix(nodes, NodeGraphDataFrameFieldNames.arc),
  47. details: findFieldsByPrefix(nodes, NodeGraphDataFrameFieldNames.detail),
  48. color: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.color),
  49. };
  50. }
  51. export function getEdgeFields(edges: DataFrame) {
  52. const normalizedFrames = {
  53. ...edges,
  54. fields: edges.fields.map((field) => ({ ...field, name: field.name.toLowerCase() })),
  55. };
  56. const fieldsCache = new FieldCache(normalizedFrames);
  57. return {
  58. id: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.id.toLowerCase()),
  59. source: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.source.toLowerCase()),
  60. target: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.target.toLowerCase()),
  61. mainStat: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.mainStat.toLowerCase()),
  62. secondaryStat: fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.secondaryStat.toLowerCase()),
  63. details: findFieldsByPrefix(edges, NodeGraphDataFrameFieldNames.detail.toLowerCase()),
  64. };
  65. }
  66. function findFieldsByPrefix(frame: DataFrame, prefix: string) {
  67. return frame.fields.filter((f) => f.name.match(new RegExp('^' + prefix)));
  68. }
  69. /**
  70. * Transform nodes and edges dataframes into array of objects that the layout code can then work with.
  71. */
  72. export function processNodes(
  73. nodes: DataFrame | undefined,
  74. edges: DataFrame | undefined,
  75. theme: GrafanaTheme2
  76. ): {
  77. nodes: NodeDatum[];
  78. edges: EdgeDatum[];
  79. legend?: Array<{
  80. color: string;
  81. name: string;
  82. }>;
  83. } {
  84. if (!nodes) {
  85. return { nodes: [], edges: [] };
  86. }
  87. const nodeFields = getNodeFields(nodes);
  88. if (!nodeFields.id) {
  89. throw new Error('id field is required for nodes data frame.');
  90. }
  91. const nodesMap =
  92. nodeFields.id.values.toArray().reduce<{ [id: string]: NodeDatum }>((acc, id, index) => {
  93. acc[id] = {
  94. id: id,
  95. title: nodeFields.title?.values.get(index) || '',
  96. subTitle: nodeFields.subTitle ? nodeFields.subTitle.values.get(index) : '',
  97. dataFrameRowIndex: index,
  98. incoming: 0,
  99. mainStat: nodeFields.mainStat,
  100. secondaryStat: nodeFields.secondaryStat,
  101. arcSections: nodeFields.arc,
  102. color: nodeFields.color,
  103. };
  104. return acc;
  105. }, {}) || {};
  106. let edgesMapped: EdgeDatum[] = [];
  107. // We may not have edges in case of single node
  108. if (edges) {
  109. const edgeFields = getEdgeFields(edges);
  110. if (!edgeFields.id) {
  111. throw new Error('id field is required for edges data frame.');
  112. }
  113. edgesMapped = edgeFields.id.values.toArray().map((id, index) => {
  114. const target = edgeFields.target?.values.get(index);
  115. const source = edgeFields.source?.values.get(index);
  116. // We are adding incoming edges count so we can later on find out which nodes are the roots
  117. nodesMap[target].incoming++;
  118. return {
  119. id,
  120. dataFrameRowIndex: index,
  121. source,
  122. target,
  123. mainStat: edgeFields.mainStat ? statToString(edgeFields.mainStat, index) : '',
  124. secondaryStat: edgeFields.secondaryStat ? statToString(edgeFields.secondaryStat, index) : '',
  125. } as EdgeDatum;
  126. });
  127. }
  128. return {
  129. nodes: Object.values(nodesMap),
  130. edges: edgesMapped || [],
  131. legend: nodeFields.arc.map((f) => {
  132. return {
  133. color: f.config.color?.fixedColor ?? '',
  134. name: f.config.displayName || f.name,
  135. };
  136. }),
  137. };
  138. }
  139. export function statToString(field: Field, index: number) {
  140. if (field.type === FieldType.string) {
  141. return field.values.get(index);
  142. } else {
  143. const decimals = field.config.decimals || 2;
  144. const val = field.values.get(index);
  145. if (Number.isFinite(val)) {
  146. return field.values.get(index).toFixed(decimals) + (field.config.unit ? ' ' + field.config.unit : '');
  147. } else {
  148. return '';
  149. }
  150. }
  151. }
  152. /**
  153. * Utilities mainly for testing
  154. */
  155. export function makeNodesDataFrame(count: number) {
  156. const frame = nodesFrame();
  157. for (let i = 0; i < count; i++) {
  158. frame.add(makeNode(i));
  159. }
  160. return frame;
  161. }
  162. function makeNode(index: number) {
  163. return {
  164. id: index.toString(),
  165. title: `service:${index}`,
  166. subtitle: 'service',
  167. arc__success: 0.5,
  168. arc__errors: 0.5,
  169. mainstat: 0.1,
  170. secondarystat: 2,
  171. color: 0.5,
  172. };
  173. }
  174. function nodesFrame() {
  175. const fields: any = {
  176. [NodeGraphDataFrameFieldNames.id]: {
  177. values: new ArrayVector(),
  178. type: FieldType.string,
  179. },
  180. [NodeGraphDataFrameFieldNames.title]: {
  181. values: new ArrayVector(),
  182. type: FieldType.string,
  183. },
  184. [NodeGraphDataFrameFieldNames.subTitle]: {
  185. values: new ArrayVector(),
  186. type: FieldType.string,
  187. },
  188. [NodeGraphDataFrameFieldNames.mainStat]: {
  189. values: new ArrayVector(),
  190. type: FieldType.number,
  191. },
  192. [NodeGraphDataFrameFieldNames.secondaryStat]: {
  193. values: new ArrayVector(),
  194. type: FieldType.number,
  195. },
  196. [NodeGraphDataFrameFieldNames.arc + 'success']: {
  197. values: new ArrayVector(),
  198. type: FieldType.number,
  199. config: { color: { fixedColor: 'green' } },
  200. },
  201. [NodeGraphDataFrameFieldNames.arc + 'errors']: {
  202. values: new ArrayVector(),
  203. type: FieldType.number,
  204. config: { color: { fixedColor: 'red' } },
  205. },
  206. [NodeGraphDataFrameFieldNames.color]: {
  207. values: new ArrayVector(),
  208. type: FieldType.number,
  209. config: { color: { mode: 'continuous-GrYlRd' } },
  210. },
  211. };
  212. return new MutableDataFrame({
  213. name: 'nodes',
  214. fields: Object.keys(fields).map((key) => ({
  215. ...fields[key],
  216. name: key,
  217. })),
  218. });
  219. }
  220. export function makeEdgesDataFrame(edges: Array<[number, number]>) {
  221. const frame = edgesFrame();
  222. for (const edge of edges) {
  223. frame.add({
  224. id: edge[0] + '--' + edge[1],
  225. source: edge[0].toString(),
  226. target: edge[1].toString(),
  227. });
  228. }
  229. return frame;
  230. }
  231. function edgesFrame() {
  232. const fields: any = {
  233. [NodeGraphDataFrameFieldNames.id]: {
  234. values: new ArrayVector(),
  235. type: FieldType.string,
  236. },
  237. [NodeGraphDataFrameFieldNames.source]: {
  238. values: new ArrayVector(),
  239. type: FieldType.string,
  240. },
  241. [NodeGraphDataFrameFieldNames.target]: {
  242. values: new ArrayVector(),
  243. type: FieldType.string,
  244. },
  245. };
  246. return new MutableDataFrame({
  247. name: 'edges',
  248. fields: Object.keys(fields).map((key) => ({
  249. ...fields[key],
  250. name: key,
  251. })),
  252. });
  253. }
  254. export interface Bounds {
  255. top: number;
  256. right: number;
  257. bottom: number;
  258. left: number;
  259. center: {
  260. x: number;
  261. y: number;
  262. };
  263. }
  264. /**
  265. * Get bounds of the graph meaning the extent of the nodes in all directions.
  266. */
  267. export function graphBounds(nodes: NodeDatum[]): Bounds {
  268. if (nodes.length === 0) {
  269. return { top: 0, right: 0, bottom: 0, left: 0, center: { x: 0, y: 0 } };
  270. }
  271. const bounds = nodes.reduce(
  272. (acc, node) => {
  273. if (node.x! > acc.right) {
  274. acc.right = node.x!;
  275. }
  276. if (node.x! < acc.left) {
  277. acc.left = node.x!;
  278. }
  279. if (node.y! > acc.bottom) {
  280. acc.bottom = node.y!;
  281. }
  282. if (node.y! < acc.top) {
  283. acc.top = node.y!;
  284. }
  285. return acc;
  286. },
  287. { top: Infinity, right: -Infinity, bottom: -Infinity, left: Infinity }
  288. );
  289. const y = bounds.top + (bounds.bottom - bounds.top) / 2;
  290. const x = bounds.left + (bounds.right - bounds.left) / 2;
  291. return {
  292. ...bounds,
  293. center: {
  294. x,
  295. y,
  296. },
  297. };
  298. }
  299. export function getNodeGraphDataFrames(frames: DataFrame[]) {
  300. // TODO: this not in sync with how other types of responses are handled. Other types have a query response
  301. // processing pipeline which ends up populating redux state with proper data. As we move towards more dataFrame
  302. // oriented API it seems like a better direction to move such processing into to visualisations and do minimal
  303. // and lazy processing here. Needs bigger refactor so keeping nodeGraph and Traces as they are for now.
  304. return frames.filter((frame) => {
  305. if (frame.meta?.preferredVisualisationType === 'nodeGraph') {
  306. return true;
  307. }
  308. if (frame.name === 'nodes' || frame.name === 'edges' || frame.refId === 'nodes' || frame.refId === 'edges') {
  309. return true;
  310. }
  311. const fieldsCache = new FieldCache(frame);
  312. if (fieldsCache.getFieldByName(NodeGraphDataFrameFieldNames.id)) {
  313. return true;
  314. }
  315. return false;
  316. });
  317. }