1- import React from "react" ;
1+ import React , { RefObject } from "react" ;
22import * as N3 from "n3" ;
3- import ForceGraph , { NodeObject } from "react-force-graph-2d" ;
3+ import ForceGraph , {
4+ ForceGraphMethods ,
5+ LinkObject ,
6+ NodeObject
7+ } from "react-force-graph-2d" ;
48import { TTLData } from "./util" ;
9+ import { forceCollide } from "d3-force-3d" ;
510
611type GraphViewProps = {
712 data : TTLData ;
813} ;
914
1015type Node = {
16+ x ?: number ;
17+ y ?: number ;
1118 id : string ;
1219 name : string ;
1320 data : N3 . Quad_Object ;
21+ vx : number ;
22+ vy : number ;
23+ className : string | null ;
1424} ;
1525
1626type Link = {
@@ -19,29 +29,53 @@ type Link = {
1929 data : N3 . Quad ;
2030} ;
2131
32+ type GraphRef = ForceGraphMethods <
33+ NodeObject < Node > ,
34+ LinkObject < Node , Link > | undefined
35+ > ;
36+
37+ function stripPrefix ( name : string , prefixes : Record < string , string > ) : string {
38+ for ( const [ k , v ] of Object . entries ( prefixes ) ) {
39+ if ( name . startsWith ( v ) ) {
40+ name = `${ k } :${ name . substring ( v . length ) } ` ;
41+ break ;
42+ }
43+ }
44+
45+ return name ;
46+ }
47+
2248function mkNode ( data : N3 . Quad_Object , prefixes : Record < string , string > ) : Node {
2349 let name = "?" ;
2450
2551 if ( data . termType === "NamedNode" ) {
26- name = data . value ;
27-
28- for ( const [ k , v ] of Object . entries ( prefixes ) ) {
29- if ( name . startsWith ( v ) ) {
30- name = `${ k } :${ name . substring ( v . length ) } ` ;
31- break ;
32- }
33- }
52+ name = stripPrefix ( data . value , prefixes ) ;
53+ } else if ( data . termType === "Literal" ) {
54+ name = "(Literal)" ;
3455 }
3556
3657 return {
3758 id : data . id ,
3859 name,
39- data
60+ data,
61+ vx : Math . random ( ) * 2.0 - 1.0 ,
62+ vy : Math . random ( ) * 2.0 - 1.0 ,
63+ className : null
4064 } ;
4165}
4266
67+ const LINE_HEIGHT = 16 ;
68+
4369function getNodeSize ( n : Node ) : [ number , number ] {
44- return [ n . name . length * 6 + 12 , 24 ] ;
70+ let lines = 1 ;
71+ let len = n . name . length ;
72+
73+ if ( n . className !== null ) {
74+ lines ++ ;
75+ len = Math . max ( len , n . className . length ) ;
76+ }
77+
78+ return [ len * 6 + 12 , lines * LINE_HEIGHT + 8 ] ;
4579}
4680
4781function renderNode ( n : NodeObject < Node > , ctx : CanvasRenderingContext2D ) : void {
@@ -58,8 +92,15 @@ function renderNode(n: NodeObject<Node>, ctx: CanvasRenderingContext2D): void {
5892 ctx . roundRect ( x - width * 0.5 , y - height * 0.5 , width , height , 5 ) ;
5993 ctx . fill ( ) ;
6094
61- ctx . fillStyle = "white" ;
62- ctx . fillText ( n . name , x , y ) ;
95+ if ( n . className === null ) {
96+ ctx . fillStyle = "white" ;
97+ ctx . fillText ( n . name , x , y ) ;
98+ } else {
99+ ctx . fillStyle = "#808080" ;
100+ ctx . fillText ( n . className , x , y - LINE_HEIGHT * 0.5 ) ;
101+ ctx . fillStyle = "white" ;
102+ ctx . fillText ( n . name , x , y + LINE_HEIGHT * 0.5 ) ;
103+ }
63104}
64105
65106function pointerAreaPaint (
@@ -69,21 +110,32 @@ function pointerAreaPaint(
69110) : void {
70111 const [ width , height ] = getNodeSize ( n ) ;
71112 ctx . fillStyle = color ;
72- ctx . fillRect ( n . x ! - width * 0.5 , n . y ! - height , width , height ) ;
113+ ctx . fillRect ( n . x ! - width * 0.5 , n . y ! - height * 0.5 , width , height ) ;
73114}
74115
75116const GraphView = React . memo ( function GraphView ( {
76117 data
77118} : GraphViewProps ) : React . JSX . Element {
119+ const graphRef = React . useRef < GraphRef > ( null ) ;
120+
78121 const graphData = React . useMemo ( ( ) => {
79122 const nodeMap = new Map < string , Node > ( ) ;
123+ const types : N3 . Quad [ ] = [ ] ;
80124 const links : Link [ ] = [ ] ;
81125 const prefixes = {
82126 ...data . prefixes ,
83127 ex : "http://example.org/"
84128 } ;
85129
86130 for ( const q of data . quads ) {
131+ if (
132+ q . predicate . value ===
133+ "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"
134+ ) {
135+ types . push ( q ) ;
136+ continue ;
137+ }
138+
87139 let subject = nodeMap . get ( q . subject . id ) ;
88140 if ( ! subject ) {
89141 subject = mkNode ( q . subject , prefixes ) ;
@@ -103,12 +155,38 @@ const GraphView = React.memo(function GraphView({
103155 } ) ;
104156 }
105157
158+ for ( const q of types ) {
159+ const obj = nodeMap . get ( q . subject . id ) ;
160+
161+ if ( obj && q . object . termType === "NamedNode" ) {
162+ const cn = stripPrefix ( q . object . value , prefixes ) ;
163+ obj . className = `«${ cn } »` ;
164+ }
165+ }
166+
106167 return {
107168 nodes : Array . from ( nodeMap . values ( ) ) ,
108169 links
109170 } ;
110171 } , [ data ] ) ;
111172
173+ React . useEffect ( ( ) => {
174+ const graph = graphRef . current ;
175+ if ( ! graph ) {
176+ return ;
177+ }
178+
179+ graph . d3Force (
180+ "collide" ,
181+ forceCollide ( n => {
182+ const [ w , h ] = getNodeSize ( n as Node ) ;
183+ return Math . max ( w , h ) * 0.5 + 5 ;
184+ } )
185+ ) ;
186+
187+ graph . d3Force ( "charge" , null ) ;
188+ } , [ graphData . nodes ] ) ;
189+
112190 return (
113191 < div className = "w-full h-full" >
114192 < ForceGraph
@@ -119,6 +197,9 @@ const GraphView = React.memo(function GraphView({
119197 linkWidth = { 2 }
120198 linkDirectionalArrowLength = { 10 }
121199 linkDirectionalArrowRelPos = { 1.0 }
200+ linkCurvature = { 0.1 }
201+ ref = { graphRef as RefObject < GraphRef > }
202+ cooldownTime = { Number . POSITIVE_INFINITY }
122203 />
123204 </ div >
124205 ) ;
0 commit comments