Junaid
Junaid

Reputation: 1030

Printing a JSON object in hierarchical/tree format

Object:

[
   {
      "Item":{
         "Name":"User 4"
      },
      "Children":[
         
      ]
   },
   {
      "Item":{
         "Name":"User 1"
      },
      "Children":[
         {
            "Item":{
               "Name":"User 6"
            }
         },
         {
            "Item":{
               "Name":"User 2"
            }
         }
      ]
   }
]

I am traversing this object with the following code:

(function Traverse(o) {
    for (var i in o) {
        console.log('Value: ' + o[i].Item.Name);

        if (o[i].Children !== null && o[i].Children !== [] && typeof(o[i].Children) == "object") {
            Traverse(o[i].Children);
        }
    }
  })
(data);

Output:

value: User 4
value: User 1
value: User 6
value: User 2
undefined

I want the output to be in a hierarchal/tree format. I have found several libraries but I do not want a proper graphical representation, just a simple use of text to indicate the hierarchy.

Something like this:

enter image description here

P.S. I am not a javascript programmer.

Upvotes: 2

Views: 5367

Answers (3)

vincent
vincent

Reputation: 2181

Here is a different approach using object-scan: Convert the input into a tree structure first and then use an existing library of your choice to convert it into the tree representations.

Upside is that this solution does not use recursion (even under the hood) - so stack overflows should be impossible for deeply nested data.

Note that name collisions will automatically get merged. So depending on if that is desired I also did like the solution @trincot posted.

// const objectScan = require('object-scan');
// const objectTreeify = require('object-treeify');

const data = [ { Item: { Name: 'A' }, Children: [] }, { Item: { Name: 'B' }, Children: [ { Item: { Name: 'BA' }, Children: [ { Item: { Name: 'BAA' }, Children: [ { Item: { Name: 'BAAA' } }, { Item: { Name: 'BAAB' } }, { Item: { Name: 'BAAC' } } ] }, { Item: { Name: 'BAB' }, Children: [ { Item: { Name: 'BABA' } } ] } ] }, { Item: { Name: 'BB' } } ] } ];

const treeify = (input) => {
  const tree = objectScan(['**[*]'], {
    reverse: false,
    breakFn: ({ isMatch, value, context }) => {
      if (isMatch) {
        const cur = context[context.length - 1];
        const name = value.Item.Name;
        if (!(name in cur)) {
          cur[name] = {};
        }
        context.push(cur[name]);
      }
    },
    filterFn: ({ context }) => {
      context.pop();
    }
  })(input, [{}])[0];
  return objectTreeify(tree);
};

console.log(treeify(data));
/* =>
├─ A
└─ B
   ├─ BA
   │  ├─ BAA
   │  │  ├─ BAAA
   │  │  ├─ BAAB
   │  │  └─ BAAC
   │  └─ BAB
   │     └─ BABA
   └─ BB
 */
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/[email protected]"></script>
<script src="https://bundle.run/[email protected]"></script>

Disclaimer: I'm the author of object-scan and object-treeify

Upvotes: 1

trincot
trincot

Reputation: 350776

You could use this recursive function. As an example I have used an object with a bit more items and levels:

function toText(arr) {
    const recur = ({Item, Children}) => Item?.Name + 
        (Children?.length ? "\n" + Children.map(recur).map((text, i, {length}) =>
            i < length-1 ? "├──" + text.replace(/\n/g, "\n│  ")
                         : "└──" + text.replace(/\n/g, "\n   ")
        ).join("\n") : "")
    return arr.map(recur).join("\n");
}

// Example:
let arr = [{
    "Item": {
        "Name": "A"
    },
    "Children": [
    ]
}, {
    "Item": {
        "Name": "B"
    },
    "Children": [{
        "Item": {
           "Name": "BA"
        },
        "Children": [{
            "Item": {
                "Name": "BAA"
            },
            "Children": [{
                "Item": {
                    "Name": "BAAA"
                }
            }, {
                "Item": {
                    "Name": "BAAB"
                }
            }, {
                "Item": {
                    "Name": "BAAC"
                }
            }]
        }, {
            "Item": {
                "Name": "BAB"
            },
            "Children": [{
                "Item": {
                    "Name": "BABA"
                }
            }]
       }]
    }, {
        "Item":{
            "Name": "BB"
        }
    }]
}];
console.log(toText(arr));

Upvotes: 4

SdtElectronics
SdtElectronics

Reputation: 586

EDIT: I found the format is kinda broken when code is executed in snippet. If you paste the code to the devtools and run it, the format is correct.

I found a npm package oo-ascii-tree to do this. Here is an example for your requirement:

/**
 * A tree of nodes that can be ASCII visualized.
 */
class AsciiTree {
    /**
     * Creates a node.
     * @param text The node's text content
     * @param children Children of this node (can also be added via "add")
     */
    constructor(text, ...children) {
        this.text = text;
        this._children = new Array();
        for (const child of children) {
            this.add(child);
        }
    }
    /**
     * Prints the tree to an output stream.
     */
    printTree(output = process.stdout) {
        let ancestorsPrefix = '';
        for (const parent of this.ancestors) {
            // -1 represents a "hidden" root, and so it's children
            // will all appear as roots (level 0).
            if (parent.level <= 0) {
                continue;
            }
            if (parent.last) {
                ancestorsPrefix += '  ';
            }
            else {
                ancestorsPrefix += ' │';
            }
        }
        let myPrefix = '';
        let multilinePrefix = '';
        if (this.level > 0) {
            if (this.last) {
                if (!this.empty) {
                    myPrefix += ' └─┬ ';
                    multilinePrefix += ' └─┬ ';
                }
                else {
                    myPrefix += ' └── ';
                    multilinePrefix = '     ';
                }
            }
            else {
                if (!this.empty) {
                    myPrefix += ' ├─┬ ';
                    multilinePrefix += ' │ │ ';
                }
                else {
                    myPrefix += ' ├── ';
                    multilinePrefix += ' │   ';
                }
            }
        }
        if (this.text) {
            output.write(ancestorsPrefix);
            output.write(myPrefix);
            const lines = this.text.split('\n');
            output.write(lines[0]);
            output.write('\n');
            for (const line of lines.splice(1)) {
                output.write(ancestorsPrefix);
                output.write(multilinePrefix);
                output.write(line);
                output.write('\n');
            }
        }
        for (const child of this._children) {
            child.printTree(output);
        }
    }
    /**
     * Returns a string representation of the tree.
     */
    toString() {
        let out = '';
        this.printTree({
            write: (data) => (out += data),
        });
        return out;
    }
    /**
     * Adds children to the node.
     */
    add(...children) {
        for (const child of children) {
            child.parent = this;
            this._children.push(child);
        }
    }
    /**
     * Returns a copy of the children array.
     */
    get children() {
        return this._children.map((x) => x);
    }
    /**
     * @returns true if this is the root node
     */
    get root() {
        return !this.parent;
    }
    /**
     * @returns true if this is the last child
     */
    get last() {
        if (!this.parent) {
            return true;
        }
        return (this.parent.children.indexOf(this) === this.parent.children.length - 1);
    }
    /**
     * @returns the node level (0 is the root node)
     */
    get level() {
        if (!this.parent) {
            // if the root node does not have text, it will be considered level -1
            // so that all it's children will be roots.
            return this.text ? 0 : -1;
        }
        return this.parent.level + 1;
    }
    /**
     * @returns true if this node does not have any children
     */
    get empty() {
        return this.children.length === 0;
    }
    /**
     * @returns an array of parent nodes (from the root to this node, exclusive)
     */
    get ancestors() {
        if (!this.parent) {
            return [];
        }
        return [...this.parent.ancestors, this.parent];
    }
}

const arrEg = [
   {
      "Item":{
         "Name":"User 4"
      },
      "Children":[
         
      ]
   },
   {
      "Item":{
         "Name":"User 1"
      },
      "Children":[
         {
            "Item":{
               "Name":"User 6"
            }
         },
         {
            "Item":{
               "Name":"User 2"
            }
         }
      ]
   }
];

function obj2tree(obj, tree) {
    const subTree = new AsciiTree(`${obj.Item.Name}`);
    if(obj.hasOwnProperty("Children")){
    obj.Children.forEach(o => {
        obj2tree(o, subTree);
    });
   }
  tree.add(subTree);
}

const treeStr = (objarr => {
    const tree = new AsciiTree('root');
    objarr.forEach(obj => {
    obj2tree(obj, tree);
   });
   return tree.toString();
})(arrEg);

console.log(treeStr);

Upvotes: 2

Related Questions