tomshanley / d3-sankey-circular

A fork of the d3-sankey library to allow circular links.
MIT License
89 stars 41 forks source link

Allow a user to define the node order #23

Open tomshanley opened 6 years ago

tomshanley commented 6 years ago

Currently the node.depth, node.height and node.column is determined by the incoming and outgoing links, and the order in which the nodes are passed to the sankey function. However, with circular links, the order in which they appear in the diagram may not be the intuitive order for a user.

It would preferable to allow the user to optionally preset the order of nodes, and fall back to automatic ordering if required.

tomshanley commented 6 years ago

Potential solution: Provide an option on the sankey object, eg sankey.sortNodes([fieldName]) which takes the name of field. This field would be provided in the array of node objects ( [ {"id": 1, "fieldName": 0}, ...etc] ), which would be numeric, and set the node.depth, node.height and node.column based on that, and skip the functions that automatically assign those values.

If sortNodes(null), which is default, then use the existing automatic depth/height/column assignment.

If there was a node linking to a node in the same column, this could be linked using a circular link, (or create a new type of link?)

tomshanley commented 6 years ago

re nodes which link to others nodes in the same column. Potential approaches are:

  1. Don't draw the links, and leave it to the user to make sure that linked nodes are either before or after, or accept the links won't be shown
  2. Draw circular links - leaves the source node from the right and loops around to enter the left of the target
  3. Draw an 's' shaped link - leaves the source node from the right and loops around to enter the left of the target. This would be new type of link, and potentially require amendments to other layout functions
  4. Draw a vertical link - leaves the top/bottom of the source node, and enters the top/bottom of the target node. The height's of the nodes probably should remain as is (ie sum of the values of links), so the height look greater than the incoming/outgoing links. Also, not sure if/how the width of the node should change with respect to the link value. Also, not sure what would happen if there links both directions from the respective nodes.
  5. Update the node's columns, overiding the user's preset order. Not preferred, as it contradicts this customisation's intent.

I think for now, I will develop 2, and leave it to the user to change the order if its not desirable.

tomshanley commented 6 years ago

Re sortNodes. probably should make it so it can accept function or constant, like nodeID.

tomshanley commented 6 years ago

Example of S shaped curve for option 3.

https://bl.ocks.org/tomshanley/2c2bfaebfe249c96451c0e61861ce1c2

Not sure how to handle instances where 3 or more nodes are linked in the same column

tomshanley commented 6 years ago

Current version implements a basic API (sankey.sortNodes()) to allow the user to choose an attribute in the nodes array which holds a column number (0,1,2,..). If specified, the column number is used instead of automatically assigning column, depth and height.

Circular links are identified if the link's source node column is greater than or equal to the target node's column. Circular links are drawn using the existing circular link path function.

To do:

tomshanley commented 6 years ago

Need to update computeNodeDepths too, as it currently relies on the circular links being ignored, which may lead to some columns not matching the user's preference

guushoekman commented 3 years ago

Hi @tomshanley,

You mentioned in a previous comment:

Current version implements a basic API (sankey.sortNodes()) to allow the user to choose an attribute in the nodes array which holds a column number (0,1,2,..). If specified, the column number is used instead of automatically assigning column, depth and height.

I'm using this and it does seem to affect the column, but I was wondering if it's possible to specify the vertical order (height)?

Your first comment on this issue says:

Currently the node.depth, node.height and node.column is determined by the incoming and outgoing links, and the order in which the nodes are passed to the sankey function

I've tried to reorder the nodes and links in my data object, but that doesn't seem to make a difference.

zealot128 commented 3 years ago

I had luck trying to manipulate the y positioning of the nodes.

First, I modified the library, to accept a new parameter function (beforeCalculateNodeYPosition) with the according boilerplate of a sankeyCircular.beforeCalculateNodeYPosition...

This function, I just call that function right after initializeNodeBreadth for every node https://github.com/tomshanley/d3-sankey-circular/blob/de98c76c7208e8155d8aa61f3eac1c49bc1a92ec/src/sankeyCircular.js#L454

       initializeNodeBreadth(id);
+      if (typeof beforeCalculateNodeYPosition == 'function') {
+       columns.forEach(function (nodes) {
+          nodes.forEach(node => {
+            beforeCalculateNodeYPosition(node)
+          })
+        })
+     }

With this in place, I now have a hook from the outside to directly manipulate y0 and y1. Then, e.g. the backend tells me if nodes should be valigned by top/bottom/middle and I have some very simple user space code:

    .beforeCalculateNodeYPosition((node) => {
      if (node.valign == 'top') {
        const target = height * 0.1
        const nodeHeight = node.y1 - node.y0
        node.y0 = target
        node.y1 = target + nodeHeight
      }
      if (node.valign == 'middle') {
        const target = height * 0.45
        const nodeHeight = node.y1 - node.y0
        node.y0 = target
        node.y1 = target + nodeHeight
      }
      if (node.valign == 'bottom') {
        const target = height * 0.8
        const nodeHeight = node.y1 - node.y0
        node.y0 = target
        node.y1 = target + nodeHeight
      }
    })

Your excellent collision algorithm than tidy up everything :) Works for now, I will try different chart-data from our user. I feel, this is a real hack, I wouldn't provide a PR or so, maybe you have another strategy in mind for placing the nodes.


example output:

Before:

Bildschirmfoto 2020-12-11 um 20 24 34

After: Placing the green Node "Absagen" on the right to "bottom" with the code above:

Bildschirmfoto 2020-12-11 um 20 25 07

Final Output, by placing most nodes (The data is the state transitioning from an Applicant Tracking System, so it shows the number of applicants transitioning through the stages) which is much more clearer for our use cases, as the "main road" from Inbox to hire is aligned and "Rejection" is a sidetrack/exit

Bildschirmfoto 2020-12-11 um 20 28 51

guushoekman commented 3 years ago

This looks really promising @zealot128!

I'm finally trying to implement this myself but am having a bit of trouble following you.

I added

  if (typeof beforeCalculateNodeYPosition == 'function') {
    columns.forEach(function(nodes) {
      nodes.forEach(node => {
        beforeCalculateNodeYPosition(node)
      })
    })
  }

to my sankeyCircular.js file. But where are you adding .beforeCalculateNodeYPosition((node) => { ... exactly?

Any help is much appreciated, but if you have time could you perhaps share a fiddle with all the code?

zealot128 commented 3 years ago

@guushoekman I've put the relevant files into a Gist:

https://gist.github.com/zealot128/e3db9aa9131ab3f2ad9e1adab5b96853

It contains 3 files:

That file, i use from app componets like this:

import chart from "./sankey-chart"
...
chart(svgElement, flowchartData, tooltipElement)
guushoekman commented 3 years ago

Thank you again @zealot128. I'm not at all familiar with TypeScript and wasn't able to get this to work unfortunately.

I decided to use Rick Lupton's implementation of the D3 sankey, which allows for decent positioning and ordering out of the box. This ended up being quite straight forward and fits my needs rather well.

Thank you for the help on this @zealot128 and apologies for asking you about something I didn't end up using. I hope it wasn't too much effort and perhaps it ends up helping someone else.

ioanachi commented 2 years ago

@guushoekman how did you manage to implement in an app Rick Lupton's implementation of the D3 sankey for the node positioning?

guushoekman commented 2 years ago

@ioanachi I followed the readme of the repository. For details on node positioning please refer to https://github.com/ricklupton/d3-sankey-diagram#adjusting-layout

I used layout.ordering([ordering]) to adjust the order of the nodes. You can see it in action here: https://ricklupton.github.io/d3-sankey-diagram/

If you scroll down to the "Try it!" header and select the "Bands and groups" example. In the JSON on the left you'll see "order" followed by the nodes in a specific order. If you change the order around and a bit and click on "Update" you'll see the order change.

ioanachi commented 2 years ago

@guushoekman thank you for the answer. Did you integrate it in a react application?