erikbrinkman / d3-dag

Layout algorithms for visualizing directed acyclic graphs
https://erikbrinkman.github.io/d3-dag/
MIT License
1.45k stars 87 forks source link

Custom node appearance #44

Closed hadrienk closed 4 years ago

hadrienk commented 4 years ago

I'm not d3 fluent, I figured I should ask: Is there a way to change the appearance of the nodes/vertex?

BenPortner commented 4 years ago

Hi @hadrienk,

very much like in d3, you can change the appearance of nodes and links by assigning them a css class. This done by selecting the corresponding links / nodes and changing the "class" attribute:

(selected object).attr('class', 'yourclass');
erikbrinkman commented 4 years ago

Yes, as @BenPortner suggested, d3-dag is just for computing coordinates to help you lay out appropriate html / svg elements. If you look (for example) at the observable demo, the actual image gets passed in an already laidout dag, and uses d3 to create the elements. Expanding the selection you;ll see code like:

  // Select nodes
  const nodes = svgSelection.append('g')
    .selectAll('g')
    .data(dag.descendants())
    .enter()
    .append('g')
    .attr('transform', ({x, y}) => `translate(${x}, ${y})`);

which is essentially creating svg groups, that are translated so that they're in the "correct" position from the layout.

Then we do two steps:

  // Plot node circles
  nodes.append('circle')
    .attr('r', 20)
    .attr('fill', n => colorMap[n.id]);

this takes those groups and adds a circle with radius 20 and the appropriate color.

then we do:

  // Add text to nodes
  nodes.append('text')
    .text(d => d.id)
    .attr('font-weight', 'bold')
    .attr('font-family', 'sans-serif')
    .attr('text-anchor', 'middle')
    .attr('alignment-baseline', 'middle')
    .attr('fill', 'white');

this adds the text with the id and sets appropriate attributes to it.

Observable supports live editing of this code, so if you're interested in changing the appearance of nodes, I suggest trying to tweak this and see if you get your desired results.

Trias commented 4 years ago

do you have recommendations on how to use non-circle nodes? like for example rectangles or ellipses?

And do you think it may be possible for the algorithms to respect width and height, such that nodes/edges for example never overlap?

Not a feature request (only slightly ;), i'm thinking of implementing it myself but may be helpful if you have some pointers / complexity estimate :)

erikbrinkman commented 4 years ago

The general complexity of laying this out with respect to the intended shape and dimension of nodes is very difficult. But I can briefly comment on other other two.

  1. non circular nodes: As I commended before, the section in observable where the nodes are created is:
    // Plot node circles
    nodes.append('circle')
     .attr('r', 20)
     .attr('fill', n => colorMap[n.id]);

    to make them squares you could do something like:

    nodes.append('rect')
     .attr('width', 40)
     .attr('height', 40)
     .attr('x', -20)
     .attr('y', -20)
     .attr('fill', n => colorMap[n.id]);

    where it's 40 because for circles you specify readius, and I set x and y to -20 as a way to center them, but there are many ways to accomplish that that ar eindependent of size. Note, that this is just d3, it has nothing to do with my library, so I encourage you to look up more of d3 if you want other things, for example, see the d3-shape library.

  2. For varying node sizes, this is supported somewhat, but it's poor and not super easy to use, but I can explain how to structure it. First because sugiyama used a layered layout, all nodes must have the same height. You can render nodes with different heights of course, but the coordinates they'll be assigned will be evenly spaced, so if you want to make a node taller, you'll have to give all the nodes the same space. In principle, each layer could have a different height, but I haven't implemented that. If you have a suitable idea, the change isn't hard and PRs are very welcome. For width there's a little more flexibility, but it's not super easy to use. There are two independent features that relate that you'll need to use.
    1. First is to use nodeSize this will tell the layout to scale based off the number of nodes, rather than cramming them into a fixed space. Set the height to be the spacing you want between nodes vertically, and the width to be the "approximate" distance you want between nodes horizontally. Note, if you want a gap, you'll want to set the larger than the height of the nodes you want to render.
    2. The second it to set the separation accessor to take the width you want per node into account. This accessor takes two nodes, and should output the relative spacing you want between them. A good rule of thumb is to structure this as left_width + right_width. If you know how large you want to make the nodes already, this should be relatively easy. Note, that this will be called on more nodes than in the original dag, as there are some dummy nodes that correspond to edges that are getting wrapped around other nodes. You'll likely want to treat these as width 0, or maybe as some minimum value, but that choice is up to you. These nodes will have a distinct type so branching off of node instanceof d3_dag.SugiDummyNode will tell you if a node is a dummy or not.

Hopefully that helps. If you have a good example that illustrates this (preferably with observable, but it doesn't really matter), I'm happy to add it to the examples section.

hadrienk commented 4 years ago

Thanks a lot for your help. I managed to change the appearance with your advices. It looks great now.

hadrienk commented 4 years ago

One last thing, is there a way to let the lib decide what would be the best size? If I remove the .size([w,h]) the node are all on top of another.

erikbrinkman commented 4 years ago

It's not really possible for this to set the layout size, as what you consider a node, might change. Conceivably you could do something like look at the elements that were updated from a node, look at the bounding box of those elements and then adjust the layout, but that is not really the way this library or d3 are designed.

The good news is that opposite, it seems like you are setting some reasonable size for your nodes, so getting something close to what you want should be possible.

The size([w, h]) argument is meant to be sued if you know the size of the area you want to lay out the nodes in. If you omit it,, it assumes 1, 1, which is probably not what you want. The alternative option is to use nodeSize([w, h]) which says to lay out the dag assuming nodes are roughly w x h. I assume at some point in your code (or in the observable) you're setting the nodes to some fixed size (40 x 40 in the observable). Then you'll want to set a node width and height of something like 60 to provide ample spacing. The downside of this approach is that you'll need to adjust the size of your rendering environment to the size of the dag that you layout.

Hope that helps!

erikbrinkman commented 4 years ago

Closing this out since it seems like things are resolved. Feel free to open a new issue or potentially ask on stackoverflow if you have questions like this.