jamisonjiang / graph-support

Java re-implementation of tiny graphviz
Apache License 2.0
24 stars 7 forks source link

Add shape to clusters #4

Closed PavelTurk closed 1 month ago

PavelTurk commented 6 months ago

Currently we can add shape only to nodes:

Node A = Node.builder().label("A").shape(shape).build();

However, it would be very nice if it were possible to add shapes to clusters. Although it is not a standard graphviz feature it is possible to do - https://forum.graphviz.org/t/clusters-outline-geometry-attribute/313/2

So, I suggest to add something like this

Cluster.builder().shape(shape)...

or like this (to show it is not standard) :

ShapedCluster.builder().shape(shape)...

The reason is that it is very often necessary to add shapes not only to nodes but also for containers: For example, in UML:

image

If this feature is implemented then I think it is also necessary to add possibility to add edges between clusters and between clusters and nodes.

jamisonjiang commented 6 months ago

Good idea, Maybe I can try and only need small changed for current code architecture, but the hardest part is that considered the overlap between clusters if have multi cluster shapes, because per shape which not rectangle should have outer and inner container size and should detected the outer container size as the original rectangle in anticipation of enough space to avoid overlap with other clusters. For the cluster edges, both the Graphviz compound and graph-support Example GraphAttrTest#compoundCases are supported line cut by cluster border, but PlantUML used another heuristic way to implemented similar request by standard Graphviz, first split the original graph to multiple graphs without cluster and rendered one by one with nested order and detected rendered graph size as the size of parent node size to continue parent graph rendered, so maybe have another way to implement this request and without change original cluster feature.

We can adds Graph-Label feature learned from PlantUML heuristic method to perfect implemented the multi-shapes cluster and cluster edges request like following, and cluster concept transfer to node as the similar result but I think it is enormous challenge.

Node n = Node.builder()
    .shape(NodeShapeEnum.TRIANGLE)
    .graphLabel(
        Graphviz.digraph()
            ...
            .build()
    )
    .build()
PavelTurk commented 6 months ago

@jamisonjiang I think that

Node n = Node.builder()
    .shape(NodeShapeEnum.TRIANGLE)
    .graphLabel(
        Graphviz.digraph()
            ...
            .build()
    )
    .build()

would be very nice. Because using it we can set any node as a "parent" of other nodes. But it is also necessary to support edges between any "child" nodes and any "parent" nodes. For example, edges between any parent nodes.

jamisonjiang commented 6 months ago

If nested graph in node we can not connect "child" node to "parent" node, this is meaning of cluster existence. Cluster and nest graph complementary each other, so the criterion when choose nest graph or cluster is whether needed connect the inner node to outside node. Because recursive graph it means node only care the child graph size rather than the layout, but cluster is a part of original graph and participated all layout phases like rank assigned/mincross/position, there are totally difference but seems like similar in some scenarios. Following case show a different result when use "nested" and "cluster" respectively: image

PavelTurk commented 6 months ago

Ok, I will show on example. It is a terrible but valid relations between two JPMS modules:

Screenshot from 2023-12-30 15-14-38

Is it possible to create such graph with such relations?

jamisonjiang commented 6 months ago

@PavelTurk Just a example with dot:

digraph G {
    compound=true
    rankdir=LR
    node[shape=rect]
    a->d[dir=back label="requires" ltail="cluster_0" lhead="cluster_2"]
    b->e[label="exports" lhead="cluster_2"]
    b->e[dir=back label="opens" ltail="cluster_0"]
    subgraph cluster_0 {
        label="ModuleA"
        subgraph cluster_1 {
            label="coo.foo"
            a[label="api"]
            b[label="internal"]
        }
    }

    subgraph cluster_2 {
        label="ModuleB"
        subgraph cluster_3 {
            label="com.bar"
            d->e[minlen=0 style=invis]
            d[style=invis fixedsize=true height=0]
            e[label="impl"]
        }
    }
}

image

PavelTurk commented 6 months ago

@jamisonjiang Thank you very much. So it is possible to do it with graph-support with custom shapes?

I am not very good at dot. My experience with it is about 1 (one) week. For this time I tried different solutions - graphviz + jni, graphviz + wasm etc. I even found a bug in graphviz which was yesterday fixed. But I didn't like all found solutions. Besides I hoped that graphviz is very stable and mature (for 30 years of development). That's why I am trying to use with graph-support.

jamisonjiang commented 6 months ago

First I will try multi cluster shapes and graph nested in the future (it need some time), then I can give you some heavier solutions. graphviz-java If you want to use original graphviz in java (by the way graph-support is just use the same algorithm re-implementation but do not have any relationship with graphviz) you can try graphviz-java, it is compile the graphviz to Javascript and try to use V8 javascript engine (a js engine of java) to run the js version of graphviz, and it is a more proven solution rather than create JNI interface by yourself, it is obviously heavier but more close to graphviz than graph-support PlantUml PlantUML is a versatile component that enables swift and straightforward diagram creation, and it write by java and provide scripts and java api for users. PlantUML use the graphviz as the layout engine to caculated the node coordinate but have independent rendered engine. I think PlantUML it is good choice because it have richer features and already used multi ways to integrate graphviz and do downgrade by your enviroment:

Finally, only the PlantUML + smetana is not have other dependency seems like graph-support, so you can try all these solutions to integrate to your project without create JNI by yourself and hope helpful for you.

PavelTurk commented 6 months ago

@jamisonjiang Thank you very much.

Yes, I know about graphviz-java. I tested it it V8 engine. It works, but Java -> V8 in native code > wasm. Is this a good choice for Java? I tested this solution and it works. After that I decided if native code is used anyway then it maybe better to use JNI. However, as I found out it is better to use wasm, because graphviz have many dynamic libraries that can have other dependencies. Besides graphviz doesn't provide thread safety and it is even not safe to call same functions with different arguments because they use a lot of static variables in C. That's why they advise to use graphviz in subprocesses with its own memory.

I also know about PlantUML and smetana. I've read about it but I am not sure that it will work well. As I understand there is a lot of C code - 30 years of development and about 20 000 commits. Besides as it was found out this code has its own bugs and was developed primarily for command line usage - not as a library.

I think they way you go with a graph-support it is a best way. Yes, it is clear that graph-support won't be so powerful, but if it provides base functionality without heavy integrated solutions like java -> jni -> native code -> V8 -> javascript -> node library etc :) it will be fine.

jamisonjiang commented 5 months ago

Ideally both of following two abilities should be satisfied for multi cluster shapes:

  1. Auto adjust the suitable cluster and nodes position to avoid overlap with each other cluster;
  2. Auto calculating right container size to include all elements within the cluster to ensure they not overflow cluster.

After some deep thinking I realized the both of above two conditions cannot met at the same time under DOT layout algorithm because of some auto-expansion deadlock, and I think it's why graphviz cannot done multi shapes as standard feature even it is a commonly requirement. So if we want to implement multi cluster shape we have to give up condition 2 and let caller control some attribute to ensure that shape can complete cover all elements(but procedure should try best to avoid this happen but cannot guarantee), and I think the GVPR (customize cluster shape way of graphviz intrdouce in this topic) have the same problem becasue they only focus on rendered, and it is the simplest part. image

jamisonjiang commented 5 months ago

All shapes consists of inner and outer boxes, all elements in shape should within inner box. graph-support use ShapePropCalc#minContainerSize to describing size relationship between inner and outer boxes and auto calculate right size to node. So more safe that not overflow elements if more less gap of size between inner and outer boxes for specific cluster shape. The shape STAR more easy occurred overflow issue because of large gap between inner and outer boxes liking following example: image

jamisonjiang commented 4 months ago

@PavelTurk Latest version supported multi cluster shape but have limitation I listed below, Cluster shapes except ClusterShapeEnum#RECT no guarantee that cluster container will surround all nodes under Layout.DOT engine but will try best estimated the container size. Both node shape and cluster shape can extension by CustomizeShapeRender to customized the specific shape by yourself. image

PavelTurk commented 4 months ago

@jamisonjiang Thank you very much. I will check the latest version but some time later as I am busy with other tasks.

PavelTurk commented 1 month ago

@jamisonjiang I am sorry. I didn't have time to check - now I am working on quite different problems.