microsoft / automatic-graph-layout

A set of tools for graph layout and viewing
Other
1.36k stars 304 forks source link

Subgraph Box Geo Transform #252

Open ghost opened 4 years ago

ghost commented 4 years ago

After graph.CreateGeometryGraph(), each Node has populated geometry. When we write to SVG we use a transpose to flip y-coordinate (TransformGraphByFlippingY). This does not touch the geometry on the subgraph, making the node and edge geo not intersect with the subgraph box, this only becomes an issue when wanting to draw the subgraph (for example, as a box). There are two ways around this:

  1. Manually transpose the GeometryNode on the subgraph before using the subgraph geo:
    graph.RootSubgraph.AllSubgraphsDepthFirstExcludingSelf()
    |> Seq.iter (fun e -> e.GeometryNode.Transform(PlaneTransformation(1., 0., 0., 0., -1., 0.)))
  2. Add the actual subgraph to the geometry, after CreateGeometryGraph, before using the subgraph geometry (e.g. SvgWriter)

graph.GeometryGraph.Nodes.Add(actualSubgraph.GeometryNode)

I am not sure which is the correct way of doing it, but provided they both occur post-layout, it shouldn't affect things dramatically

I don't think this is a bug, but it took me a good couple hours so solve, so I wanted to document somewhere others could find it.

levnach commented 4 years ago

I think the bug is that SvgGraphWriter does not output the boxes and labels of subgraphs.

ghost commented 4 years ago

Mostly that's easy to do in WriteNodes I think, keeping in mind placing the label can be trickier, I used the following algorithm to place label at top of a subgraph box, which ill openly admit to being a hack!

//  e.g. graph.RootSubgraph.AllSubgraphsDepthFirstExcludingSelf() |> Seq.iter (writeNode true)
if isSubgraph then
    let n = Node(node.Label.Text)
    n.Attr.Color <- node.Attr.Color
    n.Attr.FillColor <- Color.White // make clean for text

    // take the subgraph box
    // take the subgraph label
    // write a new box ontop of subgraph box containing label
    let l = node.Label

    // guess box size for label based on text length
    // scale is a bit of a hack @ 1.5
    // TODO: StringMeasure.MeasureWithFont might be better here
    let h = l.FontSize + 4.0 // ensure box is slightly bigger
    let w = ((l.FontSize * float l.Text.Length) * 1.5) / 2.0 

    let boundingBox = node.GeometryNode.BoundingBox
    let topCenter = boundingBox.Left + boundingBox.Width/2.0

    let x = topCenter - (w/2.0)
    let y = boundingBox.Top + (h/2.0) // intersect

    // y-coordinates must be inverted to fir in svg space
    // normally done by geo transform but we are manually calculating label rect
    writeBox2 n.Attr x -y w h

    // we also want to make sure we dont right over top of box line
    // so y must be adjusted to fit
    let labelY = -(y-(h/2.0))
    let labelX = x + (w/2.0) // we dont want to write directly at box start
    writeLabel xml node.Label (Some(labelX, labelY))

This works well when the graph is laid out with default Sugiyama, however when using Layout.Layered.LayeredLayout, it seems to not respect the subgraphs bounds, and the layout ends up being very odd (subgraph box is nowhere near subgraph nodes, as the nodes are all spread out).

If you have any ideas on how to use a layered layout (similar to this), I would be grateful, I am struggling a bit with my very limited understanding of graph layout.

levnach commented 4 years ago

We can to reuse the code from GraphViewerGdi here, for example look for CreateDNodeAndSetNodeBoundaryCurveForSubgraph. Sorry, I have no time to implement it now.

ghost commented 4 years ago

I understand, thank you for your help :) I will dig into that section of the code!