1

I've been trying to solve this graph problem for a couple weeks now... still pretty new to D3.js, so things that seem like they might be simple still elude me.

Here's an illustration of what I'm trying to do:

relationship Chart

The Goals:

  1. I want to show relationships between Industry Nodes/Size Nodes and Product Nodes.

  2. When I hover a Product node, I want to highlight the Link, Source (Industry or Size) and Target (Product) of each relevant relationship.

  3. When I hover an Industry or Size Node, I want to highlight it's links to all relevant Products.

The Questions

  1. How do I draw the links? I know it somehow involves using d3.map... but can't figure it out.

  2. How do I highlight the nodes and links (goals 2 and 3)?

  3. If there is a better, more efficient way of getting this layout and behavior, please let me know - trying hard to learn the tricks!

The Fiddle renders the basic layout from a simplified set of data: http://jsfiddle.net/9hGbD/

The Data currently looks like this:

    var data = {
    "Product": [
        {
            "type": "product",
            "name": "Product 1"
        },
        {
            "type": "product",
            "name": "Product 2"
        },
        {
            "type": "product",
            "name": "Product 3"
        },
        {
            "type": "product",
            "name": "Product 4"
        },
        {
            "type": "product",
            "name": "Product 5"
        }
    ],
    "Industry": [

        {
            "type": "industry",
            "name": "Industry 1"
        },
        {
            "type": "industry",
            "name": "Industry 2"
        },
        {
            "type": "industry",
            "name": "Industry 3"
        },
        {
            "type": "industry",
            "name": "Industry 4"
        },
        {
            "type": "industry",
            "name": "Industry 5"
        }
    ],
    "Size": [
        {
            "type": "size",
            "name": "Size 1"
        },
        {
            "type": "size",
            "name": "Size 2"
        },
        {
            "type": "size",
            "name": "Size 3"
        },
        {
            "type": "size",
            "name": "Size 4"
        },
        {
            "type": "size",
            "name": "Size 5"
        }
    ],
    "links": [
        {
            "source": "Industry 1",
            "target": "Product 1"
        },
        {
            "source": "Industry 3",
            "target": "Product 1"
        },
        {
            "source": "Industry 5",
            "target": "Product 1"
        },
        {
            "source": "Industry 2",
            "target": "Product 2"
        },
        ...etc..
    ]
};

The javascript I'm using looks like this:

function renderRelationshipGraph(){

        var width = 800,
            boxWidth = 200,
            boxHeight = 20,
            gap = 4,
            margin = {top: 16, right: 16, bottom: 16, left: 16},
            height = (data.Product.length * (boxHeight + gap)) + margin.top + margin.bottom;

        var pNodes = [];
        var iNodes = [];
        var sNodes = [];
        var links = [];

        data.Product.forEach(function(d, i) {
            d.x = ((width-margin.left-margin.right)/3)/2 - boxWidth/2;
            d.y = margin.top + (boxHeight+ 4)*i;
            pNodes.push(d);
        });

        data.Industry.forEach(function(d, i) {
            d.x = 0;
            d.y = margin.top + (boxHeight+ 4)*i; 
            iNodes.push(d);
        });

        data.Size.forEach(function(d, i) {
            d.x = ((width-margin.left-margin.right)/3) - boxWidth;
            d.y = margin.top + (boxHeight+ 4)*i; 
            sNodes.push(d);
        });

        var svg = d3.select("#graph").append("svg")
                .attr("width", width)
                .attr("height", height)
                .append("g");

        svg.append("g")
                .attr("class", "industries");


        svg.append("g")
                .attr("class", "products")
                .attr("transform", "translate("+ (width-margin.left-margin.right)/3 + ", 0)"); 

        svg.append("g")
                .attr("class", "sizes")
                .attr("transform", "translate("+ 2*((width-margin.left-margin.right)/3) + ", 0)"); 

        var products = svg.select(".products");
        var product = products.selectAll("g")
                .data(pNodes)
                .enter()
                .append("g")
                .attr("class", "unit");

                product.append("rect")
                .attr("x", function(d) {return d.x;})
                .attr("y", function(d) {return d.y;})
                .attr("width", boxWidth)
                .attr("height", boxHeight)
                .attr("class", "product")
                .attr("rx", 6)
                .attr("ry", 6)
                .on("mouseover", function() { d3.select(this).classed("active", true); })
                .on("mouseout", function() { d3.select(this).classed("active", false); });

                product.append("text")
                .attr("class", "label")
                .attr("x", function(d) {return d.x + 14;})
                .attr("y", function(d) {return d.y + 15;})
                .text(function(d) {return d.name;});

        var industries = svg.select(".industries");
        var industry = industries.selectAll("g")
                .data(iNodes)
                .enter()
                .append("g")
                .attr("class", "unit");

                industry.append("rect")
                .attr("x", function(d) {return d.x;})
                .attr("y", function(d) {return d.y;})
                .attr("width", boxWidth)
                .attr("height", boxHeight)
                .attr("class", "industry")
                .attr("rx", 6)
                .attr("ry", 6)
                .on("mouseover", function() { d3.select(this).classed("active", true); })
                .on("mouseout", function() { d3.select(this).classed("active", false); });

                industry.append("text")
                .attr("class", "label")
                .attr("x", function(d) {return d.x + 14;})
                .attr("y", function(d) {return d.y + 15;})
                .text(function(d) {return d.name;});

        var sizes = svg.select(".sizes");
        var size = sizes.selectAll("g")
                .data(sNodes)
                .enter()
                .append("g")
                .attr("class", "unit");

                size.append("rect")
                .attr("x", function(d) {return d.x;})
                .attr("y", function(d) {return d.y;})
                .attr("width", boxWidth)
                .attr("height", boxHeight)
                .attr("class", "size")
                .attr("rx", 6)
                .attr("ry", 6)
                .on("mouseover", function() { d3.select(this).classed("active", true); })
                .on("mouseout", function() { d3.select(this).classed("active", false); });

                size.append("text")
                .attr("class", "label")
                .attr("x", function(d) {return d.x + 14;})
                .attr("y", function(d) {return d.y + 15;})
                .text(function(d) {return d.name;});
    }

    renderRelationshipGraph();

Thanks for the help on this!

rolfsf
  • 1,853
  • 3
  • 24
  • 34
  • Did you look at this examples: http://bl.ocks.org/mbostock/4063570 and http://mbostock.github.io/d3/talk/20111116/iris-parallel.html – Yogesh Sep 04 '13 at 02:55
  • Thanks @Yogesh - I have looked at both of those in the recent past, but neither seems to fit what I'm trying to do - unless I'm missing something. – rolfsf Sep 04 '13 at 03:18
  • Relevant questions: http://stackoverflow.com/a/11211391/365814 http://stackoverflow.com/a/8780277/365814 – mbostock Sep 04 '13 at 23:13
  • Any progress on this? What did you decide upon? – Satchel Aug 13 '14 at 03:44
  • @rolfsf this is exactly what I need. I know this is old but if you have solved your problem please provide an answer. – Ashkan Mar 08 '16 at 22:15
  • @ashkan I passed this problem to someone else, who built it, but used an extremely convoluted method (they didn't know how to leverage d3.js). I've never had the time to rebuild it. – rolfsf Mar 09 '16 at 02:17
  • @rolfsf a bit late but I solved it and answered here. – Ashkan Mar 13 '16 at 14:48

1 Answers1

2

Ok so I tried to make this example work since I have the same sort of problem. For the sake of posterity I am answering here as well. Maybe someone will have the same problem or finds an easier solution.
I am fairly new to javascript (tried to learn a bit in the past week) so this may not be the best solution. The files and the result is available in jsfiddle. I'll not provide the whole codes and only point to the changes.

  • In order to be able to add arbitrary number of columns I changed the json file and replaced the type with level lvl. I also replaced Industriy, Product and size with a category called Nodes.

  • Each rectangle and line in the svg created in the javascript file is setup with an id so that it can be referred to later (when changing its color).

The relevant code is

  data.Nodes.forEach(function (d, i) {
      d.x = margin.left + d.lvl * (boxWidth + gap.width);
      d.y = margin.top + (boxHeight + gap.height) * count[d.lvl];
      d.id = "n" + i;
      count[d.lvl] += 1;
      Nodes.push(d);
  });

  data.links.forEach(function (d) {
      links.push({
         source: find(d.source),
         target: find(d.target),
         id: "l" + find(d.source).id + find(d.target).id
      });
  });

The method find used here is a function that finds the node with the name passed to it.

  • The mouseover and mouseout events are updated as follows:

     .on("mouseover", function () {
         mouse_action(d3.select(this).datum(), true, true);
     })
     .on("mouseout", function () {
         mouse_action(d3.select(this).datum(), false, true);
     });
    

It uses a methods, mouse_action which accepts the starting node and the state it will be (active or inactive). In this method we visit each link, process it and change its state. The method traverses in both directions (left and right) on the node that the mouse entered and traverses only left or right on other nodes.

function mouse_action(val, stat, direction) {
    "use strict";
    d3.select("#" + val.id).classed("active", stat);

    links.forEach(function (d) {
        if (direction == "root") {
            if (d.source.id === val.id) {
                d3.select("#" + d.id).moveToFront().classed("activelink", stat); // change link color
                d3.select("#" + d.id).moveToFront().classed("link", !stat); // change link color
                if (d.target.lvl < val.lvl)
                    mouse_action(d.target, stat, "left");
                else if (d.target.lvl > val.lvl)
                    mouse_action(d.target, stat, "right");
            }
            if (d.target.id === val.id) {
                d3.select("#" + d.id).moveToFront().classed("activelink", stat); // change link color
                d3.select("#" + d.id).moveToFront().classed("link", !stat); // change link color
                if (direction == "root") {
                    if(d.source.lvl < val.lvl)
                        mouse_action(d.source, stat, "left");
                    else if (d.source.lvl > val.lvl)
                        mouse_action(d.source, stat, "right");
                }
            }
        }else if (direction == "left") {
            if (d.source.id === val.id && d.target.lvl < val.lvl) {
                d3.select("#" + d.id).moveToFront().classed("activelink", stat); // change link color
                d3.select("#" + d.id).moveToFront().classed("link", !stat); // change link color
                mouse_action(d.target, stat, direction);
            }
            if (d.target.id === val.id && d.source.lvl < val.lvl) {
                d3.select("#" + d.id).moveToFront().classed("activelink", stat); // change link color
                d3.select("#" + d.id).moveToFront().classed("link", !stat); // change link color
                mouse_action(d.source, stat, direction);
            }
        }else if (direction == "right") {
            if (d.source.id === val.id && d.target.lvl > val.lvl) {
                d3.select("#" + d.id).moveToFront().classed("activelink", stat); // change link color
                d3.select("#" + d.id).moveToFront().classed("link", !stat); // change link color
                mouse_action(d.target, stat, direction);
            }
            if (d.target.id === val.id && d.source.lvl > val.lvl) {
                d3.select("#" + d.id).moveToFront().classed("activelink", stat); // change link color
                d3.select("#" + d.id).moveToFront().classed("link", !stat); // change link color
                mouse_action(d.source, stat, direction);
            }
        }
    });
}
Ashkan
  • 1,050
  • 15
  • 32
  • thanks for that solution! I'll dig into it more when I have time. Perhaps this will be of use to you - a comment in the script I'm using says: //By default 'diagonal' adds two points an offset at y, that's why, for creating a line that looks like horizontal direction, switch x and y and after creating line, switch it back in 'projection' function – rolfsf Mar 13 '16 at 18:54
  • @rolfsf Ok now I get it. I corrected it in jsfiddle. Thank you. – Ashkan Mar 14 '16 at 12:35