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  }