transform flat json object to hierarchical d3 object structure

Viewed 171

I want to transform json data like packageJson into a hierarchical format suitable for a tree graph

using d3.hierarchy to transform the Object as I understood every property which contains nested data needs to be transformed using a helper function inside like

const test = d3.hierarchy(packageJson, d => d.scripts);

is the only way to transform packageJson by finding every property with a nested object
or are any d3 helper function which Im unaware of for this kinda task ?

const fam = d3.hierarchy({
    name: "root",
    children: [
      {name: "child #1"},
      {
        name: "child #2",
        children: [
          {name: "grandchild #1"},
          {name: "grandchild #2"},
          {name: "grandchild #3"}
        ]
      }
    ]
  })


// transform json 

const packageJson = {
  "name": "visualize-data",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "core-js": "^3.6.5",
    "d3-color": "^3.0.1",
    "d3-hierarchy": "^3.0.1",
    "d3-selection": "^3.0.0",
    "d3-shape": "^3.0.1",
    "vue": "^3.0.0"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "@vue/compiler-sfc": "^3.0.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^7.0.0"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/vue3-essential",
      "eslint:recommended"
    ],
    "parserOptions": {
      "parser": "babel-eslint"
    },
    "rules": {}
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}



const test = d3.hierarchy(packageJson, d => d.scripts);

console.log(test)
    #limit {
        max-width: 100px;
    }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>



 <div id="limit">

    </div>

1 Answers

The simplest way of doing this is to create an array containing all child properties as the child array used by d3.hierarchy:

let root = d3.hierarchy(packageJson, function(d) {
  if(typeof d == "object")
   return Object.keys(d).map(k=>{ return d[k]; }); 
})

We need to check to see if the property is an object(/array) rather than text otherwise we'll be pulling up the keys of the strings that mark the leaf nodes.

This isn't the most satisfactory outcome: no nodes have names. We can fix this by giving nodes names based on the property name their parent gives them (the root remains nameless, but this is easy enough to rectify):

let root = d3.hierarchy(packageJson, function(d) {
  if(typeof d == "object")
   return Object.keys(d).filter(d=>d!="$name").map(k=>{
     if(typeof d[k] == "object") d[k].$name = k;
     else d[k] = k + " : " + d[k];
     return d[k];
   }); 
})

The $name property tracks the names for non-leaf nodes (since these are objects we can just add a property), it is filtered out by the children array generator. Since the leaf nodes are strings, I'm just modifying them to reflect the property name and the value: their values reside at d.data whereas the non-leaf nodes have their name at d.data.$name.

Depending on your desired outcome you may or may not like the above approach. But for the sake of demonstration, it's shown below:

Here's a demonstration:

const packageJson = {
  "name": "visualize-data",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "core-js": "^3.6.5",
    "d3-color": "^3.0.1",
    "d3-hierarchy": "^3.0.1",
    "d3-selection": "^3.0.0",
    "d3-shape": "^3.0.1",
    "vue": "^3.0.0"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "@vue/compiler-sfc": "^3.0.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^7.0.0"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/vue3-essential",
      "eslint:recommended"
    ],
    "parserOptions": {
      "parser": "babel-eslint"
    },
    "rules": {}
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}

let root = d3.hierarchy(packageJson, function(d) {
  if(typeof d == "object")
   return Object.keys(d).filter(d=>d!="$name").map(k=>{
     if(typeof d[k] == "object") d[k].$name = k;
     else d[k] = k + " : " + d[k];
     return d[k];
   }); 
})


var width = 600;
var height = 300;

margin = {left: 10, top: 10, right: 50, bottom: 50}

var svg = d3.select("body").append("svg")
      .attr("width", width)
      .attr("height", height);
      
var g = svg.append("g").attr('transform','translate('+ margin.left +','+ margin.right +')');

var tree = d3.tree()
    .size([height-margin.top-margin.bottom,width-margin.left-margin.right]);

 var link = g.selectAll(".link")
    .data(tree(root).links())
    .enter().append("path")
      .attr("class", "link")
      .attr("d", d3.linkHorizontal()
          .x(function(d) { return d.y; })
          .y(function(d) { return d.x; }));

  var node = g.selectAll(".node")
    .data(root.descendants())
    .enter().append("g")
      .attr("class", function(d) { return "node" + (d.children ? " node--internal" : " node--leaf"); })
      .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })

  node.append("circle")
      .attr("r", 2.5);
      
  node.append("text")
     .text(function(d) { return d.data.$name || d.data; })
     .attr('y',-10)
     .attr('x',-10)
     .attr('text-anchor','middle');
.node circle {
          fill: #fff;
          stroke: steelblue;
          stroke-width: 3px;
        }

        .link {
          fill: none;
          stroke: #ccc;
          stroke-width: 2px;
        }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

Related