Notes from reading https://www.graphviz.org/Documentation/TSE93.pdf, which describes an algorithm for drawing an acyclic graph in basically the way which I want. This document assumes the primary flow of drawing is downward, and secondary is right. For all of this it might be easier to not even consider edge values yet, as those could be done by converting them into vertices themselves after the cyclic-edge-reversal and then converting them back later. Drawing the graph is a four step process: 1) Rank nodes in the Y axis - Graph must be acyclic. - This can be accomplished by strategically reversing edges which cause a cycle, and then reversing them back as a post-processing step. - Edges can be found by: - walking out from a particular node depth-first from some arbitrary node. - As you do so you assign a rank based on depth to each node you encounter. - If any edge is destined for a node which has already been seen you look at the ranks of the source and destination, and if the source is _greater_ than the destination you reverse the edge's direction. - I think that algorithm only works if there's a source/sink? might have to be modified, or the walk must traverse both to & from. - Assign all edges a weight, default 1, but possibly externally assigned to be greater. - Take a "feasible" minimum spanning tree (MST) of the graph - Feasibility is defined as each edge being "tight", meaning, once you rank each node by their distance from the root and define the length of an edge as the difference of rank of its head and tail, that each tree edge will have a length of 1. - Perform the following on the MST: - For each edge of the graph assign the cut value - If you were to remove any edge of an MST it would create two separate MSTs. The side the edge was pointing from is the tail, the side it was pointing to is the head. - Looking at edges _in the original graph_, sum the weights of all edges directed from the tail to the head (including the one removed) and subtract from that the sum of the weights of the edges directed from the head to the tail. This is the cut value. - "...note that the cut values can be computed using information local to an edge if the search is ordered from the leaves of the feasible tree inward. It is trivial to compute the cut value of a tree edge with one of its endpoints a leaf in the tree, since either the head or the tail component consists of a single node. Now, assuming the cut values are known for all the edges incident on a given node except one, the cut value of the remaining edge is the sum of the known cut values plus a term dependent only on the edges incident to the given node." - Take an edge with a negative cut value and remove it. Find the graph edge between the remaining head and tail MSTs with the smallest "slack" (distance in rank between its ends) and add that edge to the MST to make it connected again. - Repeat until there are no negative cut values. - Apparently searching "cyclically" through the negative edges, rather than iterating from the start each time, is worthwhile. - Normalize the MST by assigning the root node the rank of 0 (and so on), if it changed. - All edges in the MST are of length 1, and the rest can be inferred from that. - To reduce crowding, nodes with equal in/out edge weights and which could be placed on multiple rankings are moved to the ranking with the fewest nodes. 2) Order nodes in the X axis to reduce edge crossings - Add ephemeral vertices along edges with lengths greater than 1, so all "spaces" are filled. - If any vertices have edges to vertices on their same rank, those are ordered so that all these "flag edges" are pointed in the same direction across that rank, and the ordering of those particular vertices is always kept. - Iterate over the graph some fixed number of times (the paper recommends 24) - possibly with some heuristic which looks at percentage improvement each time to determine if it's worth the effort. - on one iteration move "down" the graph, on the next move "up", etc... shaker style - On each iteration: - For each vertex look at the median position of all of the vertices it has edges to in the previous rank - If the number of previous vertices is even do this complicated thing (P is the set of positions previous): ``` if |P| = 2 then return (P[0] + P[1])/2; else left = P[m-1] - P[0]; right = P[|P| -1] - P[m]; return (P[m-1]*right + P[m]*left)/(left+right); endif ``` - Sort the vertices by their median position - vertices with no previous vertices remain fixed - Then, for each vertex in the rank attempt to transpose it with its neighbor and see if that reduces the number of edge crossings between the rank and its previous. - If equality is found during these two steps (same median, or same number of crossings) the vertices in question should be flipped. 3) Compute node coordinates - Determining the Y coordinates is considered trivial: find the maxHeight of each rank, and ensure they are separated by that much plus whatever the separation value is. - For the X coordinates: do some insane shit involving the network simplex again. 4) Determine edge splines