rdataframenested-lists

Data frame into nested list (tree)


How to turn a data frame into a recursive list of lists (tree)?

Input

roles_data <- data.frame(
  Child = c("A", "B", "C", "D", "E", "F"),
  ParentID = c(NA, "A", "B", "B", "D", "D")  # The top-level role has no parent (NA)
)

Desired Output

desired_output <- list(
 list(
 text = "A",
 children = list(
 list(
 text = "B",
 children = list(
 list(
 text = "C"),
 list(
 text = "D"
 ,
 children = list(
 list(
  text = "E"
 ), 
  list(
  text = "F"
 )  ) ) ) ) ) )

(Hopefully, I did not mess with the output table at the bottom, just need to have more than one level of depth.)


Solution

  • You're describing a tree structure: each parent can have multiple children, but each child has exactly one parent. Searching for tree questions, this is similar in principle to Transform a dataframe into a tree structure list of lists, in the sense that we can start from the root and recurse over children. However, as your input and output data are in a different format, we can do something a little more ergonomic. Basically, for each node, create a list with text = node_name, and if there are children, recursively build a subtree for them.

    generate_tree <- function(dat, root = dat$Child[is.na(dat$ParentID)]) {
        node <- list(text = root)
        children <- na.omit(dat$Child[dat$ParentID == root])
        if (length(children) > 0) {
            node$children <- lapply(children, \(child) generate_tree(dat, child))
        }
        node
    }
    

    If you really want the outer list() you can wrap it in one:

    identical(list(generate_tree(roles_data)), desired_output) 
    # [1] TRUE
    

    And here is the output as json for printing purposes:

    generate_tree(roles_data)  |> jsonlite::toJSON(pretty = TRUE, auto_unbox = TRUE)
    {
      "text": "A",
      "children": [
        {
          "text": "B",
          "children": [
            {
              "text": "C"
            },
            {
              "text": "D",
              "children": [
                {
                  "text": "E"
                },
                {
                  "text": "F"
                }
              ]
            }
          ]
        }
      ]
    }