1- import React , { useEffect , useRef } from 'react' ;
2- import cytoscape from "cytoscape" ;
3- import CytoscapeDagre from "cytoscape-dagre" ;
4-
5- cytoscape [ 'use' ] ( CytoscapeDagre ) ;
1+ import React , { useCallback , useEffect , useRef } from 'react' ;
2+ import cytoscape from 'cytoscape' ;
63
74export interface GraphMutation {
85 fit : ( ) => void ;
96}
107
118interface GraphProps {
129 elements : cytoscape . ElementDefinition [ ] ;
13- rankDir : "TB" | "LR" ;
10+ rankDir : 'TB' | 'LR' ;
1411 graphRef ?: React . RefObject < GraphMutation | null > ;
1512}
1613
17- const DAG_LAYOUT = {
18- name : "dagre" ,
19- fit : true ,
20- nodeSep : 50 ,
21- rankSep : 100 ,
22- } ;
23-
24- const STYLESHEET : NonNullable < cytoscape . CytoscapeOptions [ "style" ] > = [
14+ const STYLESHEET : NonNullable < cytoscape . CytoscapeOptions [ 'style' ] > = [
2515 {
26- selector : " edge" ,
16+ selector : ' edge' ,
2717 style : {
28- " target-arrow-shape" : " triangle" ,
29- " line-color" : " #666" ,
30- " target-arrow-color" : " #666" ,
31- " curve-style" : " bezier" ,
32- " width" : 1.5 ,
18+ ' target-arrow-shape' : ' triangle' ,
19+ ' line-color' : ' #666' ,
20+ ' target-arrow-color' : ' #666' ,
21+ ' curve-style' : ' bezier' ,
22+ width : 1.5 ,
3323 } ,
3424 } ,
3525 {
3626 selector : 'node[type="node"]' ,
3727 style : {
38- content : " data(label)" ,
39- shape : " round-rectangle" ,
40- " background-color" : " #1e293b" ,
41- " border-color" : " #3b82f6" ,
42- " border-width" : 1.5 ,
43- padding : " 8px" ,
44- " font-size" : " 12px" ,
45- " color" : " #3b82f6" ,
46- " text-valign" : " center" ,
47- " text-halign" : " center" ,
28+ content : ' data(label)' ,
29+ shape : ' round-rectangle' ,
30+ ' background-color' : ' #1e293b' ,
31+ ' border-color' : ' #3b82f6' ,
32+ ' border-width' : 1.5 ,
33+ padding : ' 8px' ,
34+ ' font-size' : ' 12px' ,
35+ color : ' #3b82f6' ,
36+ ' text-valign' : ' center' ,
37+ ' text-halign' : ' center' ,
4838 } ,
4939 } ,
5040 {
5141 selector : 'node[type="topic"]' ,
5242 style : {
53- content : " data(label)" ,
54- shape : " diamond" ,
55- " background-color" : " #4c1d95" ,
56- " border-color" : " #8b5cf6" ,
57- " border-width" : 1 ,
58- " font-size" : " 11px" ,
59- " color" : " #fff" ,
60- " text-valign" : " center" ,
61- " text-halign" : " center" ,
62- padding : " 10px" ,
43+ content : ' data(label)' ,
44+ shape : ' diamond' ,
45+ ' background-color' : ' #4c1d95' ,
46+ ' border-color' : ' #8b5cf6' ,
47+ ' border-width' : 1 ,
48+ ' font-size' : ' 11px' ,
49+ color : ' #fff' ,
50+ ' text-valign' : ' center' ,
51+ ' text-halign' : ' center' ,
52+ padding : ' 10px' ,
6353 } ,
6454 } ,
6555] ;
6656
57+ function applyLeftRightOrientation ( cy : cytoscape . Core ) : void {
58+ cy . nodes ( ) . positions ( ( node ) => {
59+ const pos = node . position ( ) ;
60+ return { x : pos . y , y : pos . x } ;
61+ } ) ;
62+ cy . fit ( undefined , 30 ) ;
63+ }
64+
65+ function createBreadthfirstLayout (
66+ cy : cytoscape . Core ,
67+ rankDir : 'TB' | 'LR' ,
68+ ) : cytoscape . LayoutOptions {
69+ const publisherRoots = cy . nodes ( '[type="node"]' ) ;
70+ return {
71+ name : 'breadthfirst' ,
72+ fit : rankDir === 'TB' ,
73+ directed : true ,
74+ padding : 30 ,
75+ spacingFactor : 1.2 ,
76+ grid : true ,
77+ avoidOverlap : true ,
78+ ...( publisherRoots . length > 0 ? { roots : publisherRoots } : { } ) ,
79+ stop : ( ) => {
80+ if ( rankDir === 'LR' ) {
81+ applyLeftRightOrientation ( cy ) ;
82+ }
83+ } ,
84+ } ;
85+ }
86+
6787export const Graph : React . FC < GraphProps > = ( { elements, rankDir, graphRef } ) => {
6888 const containerRef = useRef < HTMLDivElement > ( null ) ;
6989 const cyRef = useRef < cytoscape . Core | null > ( null ) ;
7090
91+ const runLayout = useCallback ( ( cy : cytoscape . Core , direction : 'TB' | 'LR' ) => {
92+ cy . layout ( createBreadthfirstLayout ( cy , direction ) ) . run ( ) ;
93+ } , [ ] ) ;
94+
7195 useEffect ( ( ) => {
7296 if ( ! containerRef . current ) return ;
7397
74- const layout = { ...DAG_LAYOUT , rankDir } as cytoscape . LayoutOptions ;
7598 const cy = cytoscape ( {
7699 container : containerRef . current ,
77- elements : elements ,
100+ elements,
78101 style : STYLESHEET ,
79- layout,
80102 userZoomingEnabled : true ,
81103 userPanningEnabled : true ,
82104 } ) ;
83105
84106 cyRef . current = cy ;
107+ runLayout ( cy , rankDir ) ;
85108
86109 if ( graphRef ) {
87110 graphRef . current = {
88- fit : ( ) => cy . fit ( ) ,
111+ fit : ( ) => cy . fit ( undefined , 30 ) ,
89112 } ;
90113 }
91114
@@ -97,14 +120,15 @@ export const Graph: React.FC<GraphProps> = ({ elements, rankDir, graphRef }) =>
97120
98121 useEffect ( ( ) => {
99122 if ( ! cyRef . current ) return ;
100-
123+
101124 cyRef . current . batch ( ( ) => {
102125 cyRef . current ?. elements ( ) . remove ( ) ;
103126 cyRef . current ?. add ( elements ) ;
104- const layout = { ...DAG_LAYOUT , rankDir } as cytoscape . LayoutOptions ;
105- cyRef . current ?. layout ( layout ) . run ( ) ;
127+ if ( cyRef . current ) {
128+ runLayout ( cyRef . current , rankDir ) ;
129+ }
106130 } ) ;
107- } , [ elements , rankDir ] ) ;
131+ } , [ elements , rankDir , runLayout ] ) ;
108132
109133 return < div ref = { containerRef } className = "w-full h-full bg-background" /> ;
110134} ;
0 commit comments