How to Develop a Relationship Graph with G6
Recently, I was thinking about how to implement the relationship diagram of Feishu Docs, so I remembered the G6 I used before, so I simply implemented a version.
Final effect
The main functions are as follows:
- The node is divided into two parts, the top half is the icon and the bottom half is the text
- When the mouse is placed on the icon, it will produce a diffusion effect of water ripples, and a circle of borders will appear at the same time
- Text needs to be underlined and have a white background, and the water ripple effect cannot be blocked by the white background
- The underline of the text needs to be a square
- The connection should have a gradual change of color, and the gradual change of color should be from the beginning of the arrow to the end of the arrow, and if the two-way relationship is supported, how to achieve two edges
- There should be a Label in the middle of the connection
- Nodes and connections should support translucency
- The overall layout should be a force-oriented layout, and node dragging is supported. When dragging, nodes cannot overlap, but other nodes cannot be dragged due to connection (this point is raised separately because G6’s built-in force-oriented layout nodes will be dragged).
Concrete realization
Custom node
The first difficulty of this Functional Button is:
- G6 Only keyShapre can respond to events, and the connection is connected to the keyShape injury, and the keyShape is the first shape of each group
- So if we want the mouse to Hover the image to produce water ripples, we should make the image node the keyShape
- But if we let image be keyShape, even if our picture is round, the box of keyShape is square, if the line is connected to the four corners of the image, there will be a little white space between the line and the picture, so we need to Let circle be keyShape, and then image is added later, but this will cause our keyShapre, that is, circle is covered by image, so that the hover event cannot be triggered
There are two solutions, depending on whether the renderer you use is svg or canvas
If it is canvas, let circle be keyShape, but add zIndex to both circle and image, and then call group.sort ()
If it is svg, group.sort is invalid, we need to change the hierarchy through the js method in the afterDraw method, and move the back circle to the front (the earlier you add it, the later it will be)
The second difficulty is how to add the water ripple effect and how to turn on the water ripple when the Hover circle
The solution is divided into two steps:
- Use the water ripple code from the official example, but remember to set visible to false first, and pay attention to the hierarchy, so that the text is at the top
- Then listen to the’node: mousemove ‘event, if the Hover target is circle, call setItemState, then when registering the node, set the setState callback, and then set visible to true through the shape.attr method, then listen to’node: mouseleave’, set the state to another, and set visible to false in the setState callback
The scheme of setting and listening to state changes can also be used to implement borders. Of course, this simple style can also be solved by stateStyle
The third difficulty is how to add text underline and how to break lines if the text is too long, because the text in G6 does not support the underline I need
The solution is:
- Calculate the length of the text by yourself, break if it exceeds a certain length, delete and add ellipsis after more than two lines
- Then add the path shape as an underscore below the text, and the length should be the text length.
Note that custom nodes should remember to inherit a built-in node, at least inherit single-node, otherwise many features are not available
Here only shows the canvas code:
1 | export enum ItemStatus { |
Connection
The first difficulty is that you can’t simply add a lable to draw or afterdraw. This only applies to the situation where the relative position of the text and the starting point of the line remains unchanged. When we need it, the node can be dragged and dropped, which means that the length of the line can be changed, and what we require is that the text is in the middle of the line.
The solution is:
- Remove the previous added text shape and add new text shape every time in afterUpdate
- There is actually no group in afterUpdate. You can forcibly mount group to cfg after draw.
The second difficulty is that how to keep the gradual change of color must be from the starting point to the end point of the connection, because G6 only supports the setting of a fixed angle, such as setting a gradual change of 0 degrees, it must be a gradual change from left to right, if A line is from right to left, then this gradual change of color is reversed
The solution is:
- Or in afterUpdate, constantly calculate the position of the new startPoint and endPoint to calculate the angle
- Set the gradual change color attribute of stroke by calculating the angle and then using shape.attr
The third difficulty is how to support bilinear curves, because the default is a straight line, and the two-way lines will overlap
The solution lies in:
- Define another custom type, based on Bezier curve extension
The specific code is as follows:
1 | const EDGE_LABEL_HEIGHT = 18; |
Event monitoring changes node and connection styles
Code for event monitoring:
1 | function highlightLocalNodesAndEdges(graphInstance: IGraph, e: IG6GraphEvent) { |
Layout algorithms and drag nodes
G6 actually comes with a force-oriented layout, but it’s weird, the effect is different from d3, and it doesn’t support only collision detection when dragging
There are a few difficulties here:
- How to use force-oriented layout of d3, here mainly d3 will change the data structure of points, causing G6 to execute abnormally
- If you implement collision detection yourself
- How to smooth a bit when dragging nodes
The solution is as follows:
- Use Promise to encapsulate the layout algorithm of d3, process the data into the format supported by G6 after monitoring the end event, otherwise let G6 render without waiting for all ticks to be completed. Halfway through, d3 changes the data format again, an error will be reported
- Self-implemented collision detection is actually a simple way to determine the distance between nodes. If it is less than a certain value, push other nodes away, then record the nodes that are pushed away, and calculate the distance between the nodes that are pushed away and all nodes., if there is still close, modify the position and continue the recursion of the nodes that modify the position in this round. In order to prevent stack overflow, you can set the maximum recursion layer of 30
- If the refreshPosition method is called after each node is calculated, it will actually cause line shaking. The solution is to enable force layout, so layout can be used to achieve smooth re-rendering, but the positions of all nodes are set with fx and fy to prevent force-oriented layout from modifying position
The code is as follows:
1 | const REFRESH_NODE_POSITION_RECURSION_COUNT = 30; |