Dan
Dan

Reputation: 741

Filter nodes for D3 directed graph dynamically

I am using D3 library which is new to me, with D3 visualizing a directed graph based on the graph details. As I have huge data, it's difficult to picture everything on a single page.

I am trying to filter the node with its inward & outwards dependencies by adding a dropdown option but I am stuck, not sure how to proceed.

Below is something I am trying to achieve. For example, Beta node is dependent on gamma but likewise Alpha and Delta are dependent on Beta. The below graph focuses more on Beta node and its inwards & outward dependencies.

enter image description here

<!DOCTYPE html>

<html>

<head>
  <style type="text/css">
    .node {}
    
    .link {
      stroke: #999;
      stroke-opacity: .6;
      stroke-width: 1px;
    }
    
    svg {
      border: 1px solid black
    }
  </style>

</head>

<body>
  <script src="https://d3js.org/d3.v4.min.js" type="text/javascript"></script>
  <script src="https://d3js.org/d3-selection-multi.v1.js"></script>
  
  <select name="cars" id="cars">
    <option value="volvo">All</option>
    <option value="volvo">Alpha</option>
    <option value="saab">Beta</option>
    <option value="volvo">Charlie</option>
    <option value="saab">Delta</option>
    <option value="audi">Echo</option>
    <option value="opel">Foxtrot</option>
    <option value="audi">Gamma</option>
    <option value="opel">Hotel</option>    
  </select>
  <br><br>
  <svg width="400" height="200"></svg>
  <script>
    var colors = d3.scaleOrdinal(d3.schemeCategory10);

    var svg = d3.select("svg"),
      width = +svg.attr("width"),
      height = +svg.attr("height"),
      node,
      link;

    svg.append('defs').append('marker')
      .attrs({
        'id': 'arrowhead',
        'viewBox': '-0 -5 10 10',
        'refX': 13,
        'refY': 0,
        'orient': 'auto',
        'markerWidth': 13,
        'markerHeight': 13,
        'xoverflow': 'visible'
      })
      .append('svg:path')
      .attr('d', 'M 0,-5 L 10 ,0 L 0,5')
      .attr('fill', '#999')
      .style('stroke', 'none');

    var simulation = d3.forceSimulation()
      .force("link", d3.forceLink().id(function(d) {
        return d.id;
      }).distance(100).strength(1))
      .force("charge", d3.forceManyBody())
      .force("center", d3.forceCenter(width / 2, height / 2));

    var graph = {
      "nodes": [{
          "name": "Alpha",
          "label": "Person",
          "id": 1
        },
        {
          "name": "Beta",
          "label": "Person",
          "id": 2
        },
        {
          "name": "Gamma",
          "label": "Database",
          "id": 3
        },
        {
          "name": "Delta",
          "label": "Database",
          "id": 4
        },
        {
          "name": "Echo",
          "label": "Person",
          "id": 5
        },
        {
          "name": "Foxtrot",
          "label": "Person",
          "id": 6
        },
        {
          "name": "Golf",
          "label": "Database",
          "id": 7
        },
        {
          "name": "Hotel",
          "label": "Database",
          "id": 8
        }
      ],
      "links": [{
          "source": 1,
          "target": 2,
          "type": "KNOWS",
          "since": 2010
        },
        {
          "source": 1,
          "target": 3,
          "type": "FOUNDED"
        },
        {
          "source": 2,
          "target": 3,
          "type": "WORKS_ON"
        },
        {
          "source": 3,
          "target": 4,
          "type": "IS_A"
        },
        {
          "source": 4,
          "target": 2,
          "type": "KNOWS",
          "since": 2010
        },
        {
          "source": 5,
          "target": 6,
          "type": "FOUNDED"
        },
        {
          "source": 3,
          "target": 4,
          "type": "WORKS_ON"
        },
        {
          "source": 1,
          "target": 4,
          "type": "IS_A"
        }
      ]
    };

    update(graph.links, graph.nodes);

    function update(links, nodes) {
      link = svg.selectAll(".link")
        .data(links)
        .enter()
        .append("line")
        .attr("class", "link")
        .attr('marker-end', 'url(#arrowhead)')

      link.append("title")
        .text(function(d) {
          return d.type;
        });

      edgepaths = svg.selectAll(".edgepath")
        .data(links)
        .enter()
        .append('path')
        .attrs({
          'class': 'edgepath',
          'fill-opacity': 0,
          'stroke-opacity': 0,
          'id': function(d, i) {
            return 'edgepath' + i
          }
        })
        .style("pointer-events", "none");

      edgelabels = svg.selectAll(".edgelabel")
        .data(links)
        .enter()
        .append('text')
        .style("pointer-events", "none")
        .attrs({
          'class': 'edgelabel',
          'id': function(d, i) {
            return 'edgelabel' + i
          },
          'font-size': 10,
          'fill': '#aaa'
        });

      edgelabels.append('textPath')
        .attr('xlink:href', function(d, i) {
          return '#edgepath' + i
        })
        .style("text-anchor", "middle")
        .style("pointer-events", "none")
        .attr("startOffset", "50%")
        .text(function(d) {
          return d.type
        });

      node = svg.selectAll(".node")
        .data(nodes)
        .enter()
        .append("g")
        .attr("class", "node")
        .call(d3.drag()
          .on("start", dragstarted)
          .on("drag", dragged)
          //.on("end", dragended)
        );

      node.append("circle")
        .attr("r", 5)
        .style("fill", function(d, i) {
          return colors(i);
        })

      node.append("title")
        .text(function(d) {
          return d.id;
        });

      node.append("text")
        .attr("dy", -3)
        .text(function(d) {
          return d.name;
        });

      simulation.nodes(nodes)
        .on("tick", ticked);

      simulation.force("link")
        .links(links);
    }

    function ticked() {

      link
        .attr("x1", function(d) {
          checkBounds(d.source);
          return d.source.x;
        })
        .attr("y1", function(d) {
          return d.source.y;
        })
        .attr("x2", function(d) {
          checkBounds(d.target);
          return d.target.x;
        })
        .attr("y2", function(d) {
          return d.target.y;
        });

      node
        .attr("transform", function(d) {
          checkBounds(d);
          return "translate(" + d.x + ", " + d.y + ")";
        });

      edgepaths.attr('d', function(d) {
        return 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y;
      });

      edgelabels.attr('transform', function(d) {
        if (d.target.x < d.source.x) {
          var bbox = this.getBBox();

          rx = bbox.x + bbox.width / 2;
          ry = bbox.y + bbox.height / 2;
          return 'rotate(180 ' + rx + ' ' + ry + ')';
        } else {
          return 'rotate(0)';
        }
      });
    }

    function dragstarted(d) {
      if (!d3.event.active) simulation.alphaTarget(0.3).restart()
      d.fx = d.x;
      d.fy = d.y;
    }

    function dragged(d) {
      d.fx = d3.event.x;
      d.fy = d3.event.y;
    }

    function checkBounds(d) {
      if (d.x < 0) d.x = 0;
      if (d.x > (width - 20)) d.x = width - 20;
      if (d.y < 0) d.y = 0;
      if (d.y > (height - 20)) d.y = height - 20;
    }
  </script>
</body>

</html>

Upvotes: 0

Views: 435

Answers (1)

D. Seah
D. Seah

Reputation: 4592

I made minor changes to your code because the select item value appear to be incorrect; and I make it works for Beta, you need to fix the rest of the values to get everything to work.

    var graph = {
        "nodes": [{
            "name": "Alpha",
            "label": "Person",
            "id": 1
          },
          {
            "name": "Beta",
            "label": "Person",
            "id": 2
          },
          {
            "name": "Gamma",
            "label": "Database",
            "id": 3
          },
          {
            "name": "Delta",
            "label": "Database",
            "id": 4
          },
          {
            "name": "Echo",
            "label": "Person",
            "id": 5
          },
          {
            "name": "Foxtrot",
            "label": "Person",
            "id": 6
          },
          {
            "name": "Golf",
            "label": "Database",
            "id": 7
          },
          {
            "name": "Hotel",
            "label": "Database",
            "id": 8
          }
        ],
        "links": [{
            "source": 1,
            "target": 2,
            "type": "KNOWS",
            "since": 2010
          },
          {
            "source": 1,
            "target": 3,
            "type": "FOUNDED"
          },
          {
            "source": 2,
            "target": 3,
            "type": "WORKS_ON"
          },
          {
            "source": 3,
            "target": 4,
            "type": "IS_A"
          },
          {
            "source": 4,
            "target": 2,
            "type": "KNOWS",
            "since": 2010
          },
          {
            "source": 5,
            "target": 6,
            "type": "FOUNDED"
          },
          {
            "source": 3,
            "target": 4,
            "type": "WORKS_ON"
          },
          {
            "source": 1,
            "target": 4,
            "type": "IS_A"
          }
        ]
      };

      // here is what you need to filter on
      document.getElementById("cars").addEventListener("change", event => {
        var val = parseInt(event.target.value, 10);
        var links = graph.links.filter(link => link.source.id === val || link.target.id === val);
        document.getElementsByTagName("svg")[0].innerHTML = "";

        if (links.length === 0) {
          update([], graph.nodes.filter(n => n.id === val));
        } else {
          var selectedNodes = Object.keys(links.reduce((acc, curr) => {
            acc[curr.source.id] = true;
            acc[curr.target.id] = true;
            return acc;
          }, {}));
          var nodes = graph.nodes.filter(n => selectedNodes.indexOf("" + n.id) !== -1);
          update(links, nodes);
        }
      });
 
      var colors = d3.scaleOrdinal(d3.schemeCategory10);
      var svg = d3.select("svg"),
        width = +svg.attr("width"),
        height = +svg.attr("height"),
        node,
        link;
      var simulation = null;


      update(graph.links, graph.nodes);

      function update(links, nodes) {

        svg.append('defs').append('marker')
          .attrs({
            'id': 'arrowhead',
            'viewBox': '-0 -5 10 10',
            'refX': 13,
            'refY': 0,
            'orient': 'auto',
            'markerWidth': 13,
            'markerHeight': 13,
            'xoverflow': 'visible'
          })
          .append('svg:path')
          .attr('d', 'M 0,-5 L 10 ,0 L 0,5')
          .attr('fill', '#999')
          .style('stroke', 'none');

        simulation = d3.forceSimulation()
          .force("link", d3.forceLink().id(function(d) {
            return d.id;
          }).distance(100).strength(1))
          .force("charge", d3.forceManyBody())
          .force("center", d3.forceCenter(width / 2, height / 2));

        link = svg.selectAll(".link")
          .data(links)
          .enter()
          .append("line")
          .attr("class", "link")
          .attr('marker-end', 'url(#arrowhead)')

        link.append("title")
          .text(function(d) {
            return d.type;
          });

        edgepaths = svg.selectAll(".edgepath")
          .data(links)
          .enter()
          .append('path')
          .attrs({
            'class': 'edgepath',
            'fill-opacity': 0,
            'stroke-opacity': 0,
            'id': function(d, i) {
              return 'edgepath' + i
            }
          })
          .style("pointer-events", "none");

        edgelabels = svg.selectAll(".edgelabel")
          .data(links)
          .enter()
          .append('text')
          .style("pointer-events", "none")
          .attrs({
            'class': 'edgelabel',
            'id': function(d, i) {
              return 'edgelabel' + i
            },
            'font-size': 10,
            'fill': '#aaa'
          });

        edgelabels.append('textPath')
          .attr('xlink:href', function(d, i) {
            return '#edgepath' + i
          })
          .style("text-anchor", "middle")
          .style("pointer-events", "none")
          .attr("startOffset", "50%")
          .text(function(d) {
            return d.type
          });

        node = svg.selectAll(".node")
          .data(nodes)
          .enter()
          .append("g")
          .attr("class", "node")
          .call(d3.drag()
            .on("start", dragstarted)
            .on("drag", dragged)
            //.on("end", dragended)
          );

        node.append("circle")
          .attr("r", 5)
          .style("fill", function(d, i) {
            return colors(i);
          })

        node.append("title")
          .text(function(d) {
            return d.id;
          });

        node.append("text")
          .attr("dy", -3)
          .text(function(d) {
            return d.name;
          });

        simulation.nodes(nodes)
          .on("tick", ticked);

        simulation.force("link")
          .links(links);
      }

      function ticked() {

        link
          .attr("x1", function(d) {
            checkBounds(d.source);
            return d.source.x;
          })
          .attr("y1", function(d) {
            return d.source.y;
          })
          .attr("x2", function(d) {
            checkBounds(d.target);
            return d.target.x;
          })
          .attr("y2", function(d) {
            return d.target.y;
          });

        node
          .attr("transform", function(d) {
            checkBounds(d);
            return "translate(" + d.x + ", " + d.y + ")";
          });

        edgepaths.attr('d', function(d) {
          return 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y;
        });

        edgelabels.attr('transform', function(d) {
          if (d.target.x < d.source.x) {
            var bbox = this.getBBox();

            rx = bbox.x + bbox.width / 2;
            ry = bbox.y + bbox.height / 2;
            return 'rotate(180 ' + rx + ' ' + ry + ')';
          } else {
            return 'rotate(0)';
          }
        });
      }

      function dragstarted(d) {
        if (!d3.event.active) simulation.alphaTarget(0.3).restart()
        d.fx = d.x;
        d.fy = d.y;
      }

      function dragged(d) {
        d.fx = d3.event.x;
        d.fy = d3.event.y;
      }

      function checkBounds(d) {
        if (d.x < 0) d.x = 0;
        if (d.x > (width - 20)) d.x = width - 20;
        if (d.y < 0) d.y = 0;
        if (d.y > (height - 20)) d.y = height - 20;
      }
.node {}

.link {
  stroke: #999;
  stroke-opacity: .6;
  stroke-width: 1px;
}

svg {
  border: 1px solid black
}
<!DOCTYPE html>

<html>
  <head>
    <script src="https://d3js.org/d3.v4.min.js" type="text/javascript"></script>
    <script src="https://d3js.org/d3-selection-multi.v1.js"></script>
  </head>

  <body>
    <select name="cars" id="cars">
      <option value="volvo">All</option>
      <option value="volvo">Alpha</option>
      <option value="2">Beta</option>
      <option value="volvo">Charlie</option>
      <option value="saab">Delta</option>
      <option value="audi">Echo</option>
      <option value="opel">Foxtrot</option>
      <option value="audi">Gamma</option>
      <option value="opel">Hotel</option>
    </select>
    <br><br>
    <svg width="400" height="200"></svg>
  </body>
</html>

Upvotes: 1

Related Questions