Carlos Aguni

Highly motivated self-taught IT analyst. Always learning and ready to explore new skills. An eternal apprentice.


D3Js Own 7 Force Graph Free | Grouped | Dagre

30 Sep 2021 »

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    /*circle {
      fill: orange;
    }*/
  </style>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
  <script src="https://dagrejs.github.io/project/dagre/latest/dagre.min.js"></script>
  <script src="https://dagrejs.github.io/project/dagre-d3/latest/dagre-d3.js"></script>
</head>
<body>
  <div id="content">
    <svg></svg>
    <br>
    <button type="button" onclick="setforce('free')">Free</button>
    <button type="button" onclick="setforce('grouped')">Grouped</button>
    <button type="button" onclick="setforce('dagre')">Dagre</button>
    <!-- <svg width="700" height="400">
      <g transform="translate(50, 200)"></g>
    </svg> -->
  </div>

  <script>
    var width = 1200, height = 800;

    // zoom
    // https://bl.ocks.org/puzzler10/49f13307e818ea9a909ba5adba5b6ed9
    let zoom = d3.zoom()
                  .on('zoom', (event) => {
                    // d3.select('svg g')
                    //     .attr('transform', event.transform)
                    d3.select('svg g.container')
                        .attr('transform', event.transform)
                    d3.select('svg g.overlay')
                        .attr('transform', event.transform)
                  })

    const svg = d3.select('svg')
                  .attr('width', width)
                  .attr('height', height)
                  .style('border', '1px solid black')
    const container = svg.append('g').classed('container', true).attr('x', 0).attr('y', 0)
    const overlay = svg.append('g').classed('overlay', true).attr('x', 0).attr('y', 0)

    svg.call(zoom)

    var colorScale = [
      'orange', 
      'lightblue', 
      '#B19CD9', // purple
      '#b7eb8f',  // green
      '#ffadd2', // pink
      '#87e8de', // cyan blue
    ];
    var xCenter = [100, 300, 500];
    w4 = 250
    const xGap = 950
    const yGap = 900
    const xStart = 250
    const yStart = 200
    const yCols = [
      3, // O O O
      2, // O O
      7, // O O O O O O O
      4, // O O O O
      1  // O
    ]
    const totalGroups = d3.sum(yCols)
    var color
    //color = d3.scaleSequential().domain([0, totalGroups]).range(d3.schemeSet3);
    color = d3.scaleDiverging().interpolator(d3.interpolateSpectral).domain([0, totalGroups])
    //color = d3.scaleDiverging().interpolator(d3.interpolateRainbow).domain([0, totalGroups])

    // var xyCenter = [
    //   // [200, 200],
    //   // [600, 200],
    //   // [1000, 200],
    //   // [200, 650],
    //   // [600, 650],
    //   // [1000, 650],
    //   [w4*col1, 200],
    //   [w4*col2, 200],
    //   [w4*col3, 200],
    //   [w4*col1, 800],
    //   [w4*col2, 800],
    //   [w4*col3, 800],
    // ]

    var xyCenter = yCols.reduce((g,c,i) => {
      console.log('reduce', g, c, i)
      for (let x = 0; x < c; x++)
        g = [...g, [xStart + (x*xGap), yStart + (i*yGap)]]
      return g
    }, [])

    console.log(xyCenter)

    // var numNodes = 100;
    // var nodes = d3.range(numNodes).map(function(d, i) {
    //   return {
    //     radius: Math.random() * 25,
    //     category: i % 6
    //   }
    // });
    var customNodes = [...new Array(d3.sum(yCols))].reduce((g,c,i) => {
      const limit = 7 + Math.floor(Math.random()*8)
      for (let x = 0; x < limit; x++){
        g.push({
          radius: 30,
          category: i,
          gender:  (g.length%2 == 0) ? 'men' : 'women',
          id: (g.length%2==0) ? g.length : g.length-1,
        })
      }
      return g
    }, [])


    //https://stackoverflow.com/questions/7430580/setting-rounded-corners-for-svgimage
    var defs = svg.append("defs")
    const avatarRadius = 25
    defs.append("clipPath")
        .attr("id", "avatar-clip")
        .append("circle")
        .attr("cx", avatarRadius)
        .attr("cy", avatarRadius)
        .attr("r", avatarRadius)

    var simulation, link, links, nodes, gnodes, _gnodes, _glinks

    function setforce(t){
      //d3.select('svg g').selectAll('circle.tag').remove()
      overlay.html('')
      if (t == 'free'){
        simulation
          .force('x', null)
          .force('y', null)
          .force('center', d3.forceCenter(width / 2, height / 2))
          .force('link', d3.forceLink(links)
                            .distance(20)
                            .strength(0.1)
                            .iterations(2)
          )
          .force('charge', d3.forceManyBody().strength(-200))
          //.force('charge', null)
          .force('collision', d3.forceCollide().radius(function(d) {
            return d.radius*2;
          }))
        simulation
          //.alphaDecay(0.01)
          .alpha(0.5)
          .alphaTarget(0.01)
          .restart();
      }else if (t == 'grouped'){
        overlay
          .selectAll('circle.tag')
          .data(xyCenter)
          .join('circle')
          .classed('tag', true)
          .attr('r', 5)
          .attr('cx', d => d[0])
          .attr('cy', d => d[1])
          .attr('fill', 'red')

        simulation
        .force('link', d3.forceLink(links)
                          .distance(0)
                          .strength(0)
                          .iterations(0)
        )
        .force('x', d3.forceX().x(function(d) {
            return xyCenter[d.category][0];
          })
          //.strength(0)
        )
        .force('y', d3.forceY().y(function(d) {
          return xyCenter[d.category][1];
        }))
        .force('center', null)
        .force('charge', d3.forceManyBody().strength(5))
          .force('collision', d3.forceCollide().radius(function(d) {
            return d.radius*1.3;
          }))
        simulation
          .alpha(1)
          .alphaTarget(0.1)
          .restart();
        
        setTimeout(() => {
          labelAnimation()
        }, 200)
      }else if (t == 'dagre'){
        simulation.stop()
        d3.selectAll('svg g line.t1').attr('stroke', 'none')
        const g = new dagre.graphlib.Graph();
        g.setGraph({
          rankdir: 'TB',
          nodesep: 100,
          ranksep: 100,
          //ranker: 'longest-path',
          ranker: 'tight-tree',
          //ranker: 'network-simplex', // default
        });
        g.setDefaultEdgeLabel(function() { return {}; });

        lnodes = JSON.parse(_gnodes) 
        llinks = JSON.parse(_glinks) 
        lnodes.map((d,i) => {
          g.setNode(i, {width: 30, height: 30})
        })
        llinks.map((d,i) => {
          g.setEdge(d.source, d.target)
        })

        dagre.layout(g)

        g.nodes().forEach(function(v) {
          //console.log("Node " + v + ": " + JSON.stringify(g.node(v)));
        });
        g.edges().forEach(function(e) {
          //console.log("Edge " + e.v + " -> " + e.w + ": " + JSON.stringify(g.edge(e)));
        });

        simulation
        .force('link', d3.forceLink(links)
                          .distance(0)
                          .strength(0)
                          .iterations(0)
        )
        .force('x', d3.forceX().x(function(d,i) {
            return g.node(i).x;
          })
        )
        .force('y', d3.forceY().y(function(d,i) {
          return g.node(i).y
        }))
        .force('center', null)
        .force('charge', null)
        .force('collision', null)
        simulation
          .alpha(1)
          .alphaTarget(0.1)
          .restart();
      }
    }

    const labelAnimation = () => {
      const localg = overlay.selectAll('g.categoryLabels')
                            .data(xyCenter)
                            .enter()
                            .append('g')
                            .classed('categoryLabels', true)
                            .attr('transform', d => `translate(${d[0]-80}, ${d[1]+140})`)
      localg.append('rect')
            .attr('fill', 'lightgrey')
            .attr('width', 200)
            .attr('height', 60)
            .attr('x', 0)
            .attr('y', 0)
            .attr('opacity', '0.8')
      localg.append('text')
            .attr('x', 100)
            .attr('y', 38)
            .attr('text-anchor', 'middle')
            .attr('font-size', '25px')
            .text((d,i) => `Category ${i}`)
      localg.transition()
            .duration(600)
            .attr('transform', d => `translate(${d[0]-100}, ${d[1]+140})`)
    }

    const drag = simulation => {
  
      function dragstarted(event, d) {
        if (!event.active) simulation.alphaTarget(0.4).restart();
        d.fx = d.x;
        d.fy = d.y;
      }
      
      function dragged(event, d) {
        d.fx = event.x;
        d.fy = event.y;
      }
      
      function dragended(event, d) {
        if (!event.active) simulation.alphaTarget(0.5);
        d.fx = null;
        d.fy = null;
      }
      
      return d3.drag()
          .on("start", dragstarted)
          .on("drag", dragged)
          .on("end", dragended);
    }

    const bfs = (head, nodes=[], links=[]) => {
      head.idx = nodes.length
      nodes.push(head.idx)
      if (head.children){
        for (let c of head.children){
          bfs(c, nodes, links)
          links.push({
            source: nodes.indexOf(head.idx),
            target: nodes.indexOf(c.idx),
          })
        }
      }
      return [nodes, links]
    }

    const parseData = (data) => {
      console.log(data)
      return bfs(data)
    }

    d3.json('redesignedChartLongData.json').then(rs => {
      // nodes = rs.nodes.map((d,i) => {
      //   return {
      //     ...d,
      //     radius: 30,
      //     category: +d.group > 5 ? 5 : +d.group,
      //   }
      // })
      // nodes = [...customNodes]
      // links = rs.links.map(d => Object.create(d))
      let [pnodes, plinks] = parseData(rs)
      links = plinks
      nodes = pnodes.map((d,i) => {
        return {
          radius: 30,
          category: Math.floor(Math.random()*totalGroups),
          gender:  (i%2 == 0) ? 'men' : 'women',
          id: (i%2==0) ? i : i-1,
        }
      })
      _gnodes = JSON.stringify(nodes)
      _glinks = JSON.stringify(links)
      simulation = d3.forceSimulation(nodes)
        .force('link', d3.forceLink(links)
                          .distance(0)
                          .strength(0)
        )
        .force('x', d3.forceX().x(function(d) {
          return xyCenter[d.category][0];
        }))
        .force('y', d3.forceY().y(function(d) {
          return xyCenter[d.category][1];
        }))
        .force('charge', d3.forceManyBody().strength(5))
        .force('center', d3.forceCenter(width / 2, height / 2))
        .force('collision', d3.forceCollide().radius(function(d) {
          return d.radius;
        }))
        .on('tick', ticked);
        //.attr("stroke", d => color(d.type))

      d3.select('svg g')
        .selectAll('line')
        .data(links)
        .join('line')
          .attr('stroke', 'black')
          .attr('stroke-width', '1')

      const randomAvatar = (gender, idx) => {
        //const gender = Math.random()*100 > 50 ? 'women' : 'men'
        return ''
        return `https://randomuser.me/api/portraits/${gender}/${idx}.jpg`
      }

      gnodes = container.selectAll('g')
        .data(nodes)
        .join('g')
        .call(drag(simulation))

      gnodes.append('circle')
            .attr('r', function(d) {
              return d.radius;
            })
            .style('fill', function(d) {
              return color(d.category)
              //return colorScale[d.category];
            })

      gnodes 
        .append('svg:image')
          .attr('width', '50px')
          .attr('height', '50px')
          .attr('xlink:href', (d, i) => randomAvatar(d.gender, d.id))
          .attr("clip-path", "url(#avatar-clip")
          .attr('transform', 'translate(-25,-25)')

      function ticked() {

        d3.select('svg g')
          .selectAll('line')
          .join('line')
            .attr("x1", function(d) { return d.source.x; })
            .attr("y1", function(d) { return d.source.y; })
            .attr("x2", function(d) { return d.target.x; })
            .attr("y2", function(d) { return d.target.y; });

        gnodes
          .attr('transform', d => `translate(${d.x}, ${d.y})`)

        // var u = d3.select('svg g')
        //   .selectAll('circle')
        //   .data(nodes)
        //   .join('circle')
        //   .attr('r', function(d) {
        //     return d.radius;
        //   })
        //   .style('fill', function(d) {
        //     return colorScale[d.category];
        //   })
        //   .attr('cx', function(d) {
        //     return d.x;
        //   })
        //   .attr('cy', function(d) {
        //     return d.y;
        //   })
      }

      //setforce('free')
      //setforce('grouped')
      setforce('dagre')

    })



  </script>
</body>
</html>

https://bl.ocks.org/willzjc/a11626a31c65ba5d319fcf8b8870f281

dataset from https://raw.githubusercontent.com/bumbeishvili/Assets/master/Projects/D3/Organization%20Chart/redesignedChartLongData.json