github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/views/cluster/containers/dataDistribution/tree.ts (about) 1 // Copyright 2018 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 import _ from "lodash"; 12 13 export interface TreeNode<T> { 14 name: string; 15 children?: TreeNode<T>[]; 16 data?: T; 17 } 18 19 export type TreePath = string[]; 20 21 export function isLeaf<T>(t: TreeNode<T>): boolean { 22 return !_.has(t, "children"); 23 } 24 25 /** 26 * A Layout is a 2d (row, column) array of LayoutCells, for rendering 27 * a tree to the screen horizontally. 28 * 29 * E.g. the layout intended to be rendered as 30 * 31 * | a | 32 * | b | c | 33 * 34 * Is represented as: 35 * 36 * [ [ <LayoutCell for a> ], 37 * [ <LayoutCell for b>, <LayoutCell for c> ] ] 38 * 39 */ 40 export type Layout<T> = LayoutCell<T>[][]; 41 42 export interface LayoutCell<T> { 43 width: number; 44 path: TreePath; 45 isCollapsed: boolean; 46 isPlaceholder: boolean; 47 isLeaf: boolean; 48 data: T; 49 } 50 51 /** 52 * layoutTreeHorizontal turns a tree into a tabular, horizontal layout. 53 * For instance, the tree 54 * 55 * a/ 56 * b 57 * c 58 * 59 * becomes: 60 * 61 * | a | 62 * | b | c | 63 * 64 * If the tree is of uneven depth, leaf nodes are pushed to the bottom and placeholder elements 65 * are returned to maintain the rectangularity of the table. 66 * 67 * For instance, the tree 68 * 69 * a/ 70 * b/ 71 * c 72 * d 73 * e 74 * 75 * becomes: 76 * 77 * | a | 78 * | b | <P> | 79 * | c | d | e | 80 * 81 * Where <P> is a LayoutCell with `isPlaceholder: true`. 82 * 83 * Further, if part of the tree is collapsed (specified by the `collapsedPaths` argument), its 84 * LayoutCells are returned with `isCollapsed: true`, and placeholders are returned to maintain 85 * rectangularity. 86 * 87 * The tree 88 * 89 * a/ 90 * b/ 91 * c 92 * d 93 * e/ 94 * f 95 * g 96 * 97 * without anything collapsed becomes: 98 * 99 * | a | 100 * | b | e | 101 * | c | d | f | g | 102 * 103 * Collapsing `e` yields: 104 * 105 * | a | 106 * | b | e | 107 * | c | d | <P> | 108 * 109 * Where <P> is a LayoutCell with `isPlaceholder: true` and e is a LayoutCell with 110 * `isCollapsed: true`. 111 * 112 */ 113 export function layoutTreeHorizontal<T>(root: TreeNode<T>, collapsedPaths: TreePath[]): Layout<T> { 114 const height = expandedHeight(root, collapsedPaths); 115 return recur(root, []); 116 117 function recur(node: TreeNode<T>, pathToThis: TreePath): Layout<T> { 118 const heightUnderThis = height - pathToThis.length; 119 120 const placeholdersLayout: Layout<T> = repeat(heightUnderThis, [{ 121 width: 1, 122 path: pathToThis, 123 data: node.data, 124 isPlaceholder: true, 125 isCollapsed: false, 126 isLeaf: false, 127 }]); 128 129 // Put placeholders above this cell if it's a leaf. 130 if (isLeaf(node)) { 131 return verticalConcatLayouts([ 132 placeholdersLayout, 133 layoutFromCell({ 134 width: 1, 135 path: pathToThis, 136 data: node.data, 137 isPlaceholder: false, 138 isCollapsed: false, 139 isLeaf: true, 140 }), 141 ]); 142 } 143 144 // Put placeholders below this if it's a collapsed internal node. 145 const isCollapsed = deepIncludes(collapsedPaths, pathToThis); 146 if (isCollapsed) { 147 return verticalConcatLayouts([ 148 layoutFromCell({ 149 width: 1, 150 path: pathToThis, 151 data: node.data, 152 isPlaceholder: false, 153 isCollapsed: true, 154 isLeaf: false, 155 }), 156 placeholdersLayout, 157 ]); 158 } 159 160 const childLayouts = node.children.map((childNode) => ( 161 recur(childNode, [...pathToThis, childNode.name]) 162 )); 163 164 const childrenLayout = horizontalConcatLayouts(childLayouts); 165 166 const currentCell = { 167 width: _.sumBy(childLayouts, (cl) => cl[0][0].width), 168 data: node.data, 169 path: pathToThis, 170 isCollapsed, 171 isPlaceholder: false, 172 isLeaf: false, 173 }; 174 175 return verticalConcatLayouts([ 176 layoutFromCell(currentCell), 177 childrenLayout, 178 ]); 179 } 180 } 181 182 /** 183 * horizontalConcatLayouts takes an array of layouts and returns 184 * a new layout composed of its inputs laid out side by side. 185 * 186 * E.g. 187 * 188 * horizontalConcatLayouts([ | a | | d | 189 * | b | c |, | e | f | ]) 190 * 191 * yields 192 * 193 * | a | d | 194 * | b | c | e | f | 195 */ 196 function horizontalConcatLayouts<T>(layouts: Layout<T>[]): Layout<T> { 197 if (layouts.length === 0) { 198 return []; 199 } 200 const output = _.range(layouts[0].length).map(() => ([])); 201 202 _.forEach(layouts, (childLayout) => { 203 _.forEach(childLayout, (row, rowIdx) => { 204 _.forEach(row, (col) => { 205 output[rowIdx].push(col); 206 }); 207 }); 208 }); 209 210 return output; 211 } 212 213 /** 214 * verticalConcatLayouts takes an array of layouts and returns 215 * a new layout composed of its inputs laid out vertically. 216 * 217 * E.g. 218 * 219 * verticalConcatLayouts([ | a | | d | 220 * | b | c |, | e | f | ]) 221 * 222 * yields 223 * 224 * | a | 225 * | b | c | 226 * | d | 227 * | e | f | 228 */ 229 function verticalConcatLayouts<T>(layouts: Layout<T>[]): Layout<T> { 230 const output: Layout<T> = []; 231 return _.concat(output, ...layouts); 232 } 233 234 function layoutFromCell<T>(cell: LayoutCell<T>): Layout<T> { 235 return [ 236 [cell], 237 ]; 238 } 239 240 export interface FlattenedNode<T> { 241 depth: number; 242 isLeaf: boolean; 243 isCollapsed: boolean; 244 data: T; 245 path: TreePath; 246 } 247 248 /** 249 * flatten takes a tree and returns it as an array with depth information. 250 * 251 * E.g. the tree 252 * 253 * a/ 254 * b 255 * c 256 * 257 * Becomes (with includeNodes = true): 258 * 259 * [ 260 * a (depth: 0), 261 * b (depth: 1), 262 * c (depth: 1), 263 * ] 264 * 265 * Or (with includeNodes = false): 266 * 267 * [ 268 * b (depth: 1), 269 * c (depth: 1), 270 * ] 271 * 272 * Collapsed nodes (specified with the `collapsedPaths` argument) 273 * are returned with `isCollapsed: true`; their children are not 274 * returned. 275 * 276 * E.g. the tree 277 * 278 * a/ 279 * b/ 280 * c 281 * d 282 * e/ 283 * f 284 * g 285 * 286 * with b collapsed becomes: 287 * 288 * [ 289 * a (depth: 0), 290 * b (depth: 1, isCollapsed: true), 291 * e (depth: 1), 292 * f (depth: 2), 293 * g (depth: 2), 294 * ] 295 * 296 */ 297 export function flatten<T>( 298 tree: TreeNode<T>, 299 collapsedPaths: TreePath[], 300 includeInternalNodes: boolean, 301 ): FlattenedNode<T>[] { 302 const output: FlattenedNode<T>[] = []; 303 304 visitNodes(tree, (node: TreeNode<T>, pathSoFar: TreePath): boolean => { 305 const depth = pathSoFar.length; 306 307 if (isLeaf(node)) { 308 output.push({ 309 depth, 310 isLeaf: true, 311 isCollapsed: false, 312 data: node.data, 313 path: pathSoFar, 314 }); 315 return true; 316 } 317 318 const isExpanded = !deepIncludes(collapsedPaths, pathSoFar); 319 const nodeBecomesLeaf = !includeInternalNodes && !isExpanded; 320 if (includeInternalNodes || nodeBecomesLeaf) { 321 output.push({ 322 depth, 323 isLeaf: false, 324 isCollapsed: !isExpanded, 325 data: node.data, 326 path: pathSoFar, 327 }); 328 } 329 330 // Continue the traversal if this node is expanded. 331 return isExpanded; 332 }); 333 334 return output; 335 } 336 337 /** 338 * nodeAtPath returns the node found under `root` at `path`, throwing 339 * an error if nothing is found. 340 */ 341 function nodeAtPath<T>(root: TreeNode<T>, path: TreePath): TreeNode<T> { 342 if (path.length === 0) { 343 return root; 344 } 345 const pathSegment = path[0]; 346 const child = root.children.find((c) => (c.name === pathSegment)); 347 if (child === undefined) { 348 throw new Error(`not found: ${path}`); 349 } 350 return nodeAtPath(child, path.slice(1)); 351 } 352 353 /** 354 * visitNodes invokes `f` on each node in the tree in pre-order 355 * (`f` is invoked on a node before being invoked on its children). 356 * 357 * If `f` returns false, the traversal stops. Otherwise, the traversal 358 * continues. 359 */ 360 function visitNodes<T>(root: TreeNode<T>, f: (node: TreeNode<T>, path: TreePath) => boolean) { 361 function recur(node: TreeNode<T>, path: TreePath) { 362 const continueTraversal = f(node, path); 363 if (!continueTraversal) { 364 return; 365 } 366 if (node.children) { 367 node.children.forEach((child) => { 368 recur(child, [...path, child.name]); 369 }); 370 } 371 } 372 recur(root, []); 373 } 374 375 /** 376 * expandedHeight returns the height of the "uncollapsed" part of the tree, 377 * i.e. the height of the tree where collapsed internal nodes count as leaf nodes. 378 */ 379 function expandedHeight<T>(root: TreeNode<T>, collapsedPaths: TreePath[]): number { 380 let maxHeight = 0; 381 visitNodes(root, (_node, path) => { 382 const depth = path.length; 383 if (depth > maxHeight) { 384 maxHeight = depth; 385 } 386 const nodeCollapsed = deepIncludes(collapsedPaths, path); 387 return !nodeCollapsed; // Only continue the traversal if the node is expanded. 388 }); 389 return maxHeight; 390 } 391 392 /** 393 * getLeafPathsUnderPath returns paths to all leaf nodes under the given 394 * `path` in `root`. 395 * 396 * E.g. for the tree T = 397 * 398 * a/ 399 * b/ 400 * c 401 * d 402 * e/ 403 * f 404 * g 405 * 406 * getLeafPaths(T, ['a', 'b']) yields: 407 * 408 * [ ['a', 'b', 'c'], 409 * ['a', 'b', 'd'] ] 410 * 411 */ 412 function getLeafPathsUnderPath<T>(root: TreeNode<T>, path: TreePath): TreePath[] { 413 const atPath = nodeAtPath(root, path); 414 const output: TreePath[] = []; 415 visitNodes(atPath, (node, subPath) => { 416 if (isLeaf(node)) { 417 output.push([...path, ...subPath]); 418 } 419 return true; 420 }); 421 return output; 422 } 423 424 /** 425 * cartProd returns all combinations of elements in `as` and `bs`. 426 * 427 * e.g. cartProd([1, 2], ['a', 'b']) 428 * yields: 429 * [ 430 * {a: 1, b: 'a'}, 431 * {a: 1, b: 'b'}, 432 * {a: 2, b: 'a'}, 433 * {a: 2, b: 'b'}, 434 * ] 435 */ 436 function cartProd<A, B>(as: A[], bs: B[]): {a: A, b: B}[] { 437 const output: {a: A, b: B}[] = []; 438 as.forEach((a) => { 439 bs.forEach((b) => { 440 output.push({ a, b }); 441 }); 442 }); 443 return output; 444 } 445 446 /** 447 * sumValuesUnderPaths returns the sum of `getValue(R, C)` 448 * for all leaf paths R under `rowPath` in `rowTree`, 449 * and all leaf paths C under `colPath` in `rowTree`. 450 * 451 * E.g. in the matrix 452 * 453 * | | C_1 | 454 * | | C_2 | C_3 | 455 * |-------|-----|-----| 456 * | R_a | | | 457 * | R_b | 1 | 2 | 458 * | R_c | 3 | 4 | 459 * 460 * represented by 461 * 462 * rowTree = (R_a [R_b R_c]) 463 * colTree = (C_1 [C_2 C_3]) 464 * 465 * calling sumValuesUnderPath(rowTree, colTree, ['R_a'], ['C_1'], getValue) 466 * sums up all the cells in the matrix, yielding 1 + 2 + 3 + 4 = 10. 467 * 468 * Calling sumValuesUnderPath(rowTree, colTree, ['R_a', 'R_b'], ['C_1'], getValue) 469 * sums up only the cells under R_b, 470 * yielding 1 + 2 = 3. 471 * 472 */ 473 export function sumValuesUnderPaths<R, C>( 474 rowTree: TreeNode<R>, 475 colTree: TreeNode<C>, 476 rowPath: TreePath, 477 colPath: TreePath, 478 getValue: (row: TreePath, col: TreePath) => number, 479 ): number { 480 const rowPaths = getLeafPathsUnderPath(rowTree, rowPath); 481 const colPaths = getLeafPathsUnderPath(colTree, colPath); 482 const prod = cartProd(rowPaths, colPaths); 483 let sum = 0; 484 prod.forEach((coords) => { 485 sum += getValue(coords.a, coords.b); 486 }); 487 return sum; 488 } 489 490 /** 491 * deepIncludes returns true if `array` contains `val`, doing 492 * a deep equality comparison. 493 */ 494 export function deepIncludes<T>(array: T[], val: T): boolean { 495 return _.some(array, (v) => _.isEqual(val, v)); 496 } 497 498 /** 499 * repeat returns an array with the given element repeated `times` 500 * times. Sadly, `_.repeat` only works for strings. 501 */ 502 function repeat<T>(times: number, item: T): T[] { 503 const output: T[] = []; 504 for (let i = 0; i < times; i++) { 505 output.push(item); 506 } 507 return output; 508 }