Skip to content

Commit 20ca1ea

Browse files
committed
Better labels
1 parent cef940d commit 20ca1ea

3 files changed

Lines changed: 101 additions & 16 deletions

File tree

custom.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
declare module "d3-force-3d" {
2+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3+
export function forceCollide(f: number | ((n: unknown) => number)): any;
4+
}

src/graph-view.tsx

Lines changed: 96 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
1-
import React from "react";
1+
import React, { RefObject } from "react";
22
import * 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";
48
import { TTLData } from "./util";
9+
import { forceCollide } from "d3-force-3d";
510

611
type GraphViewProps = {
712
data: TTLData;
813
};
914

1015
type 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

1626
type 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+
2248
function 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+
4369
function 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

4781
function 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

65106
function 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

75116
const 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
);

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
4747

4848
/* Emit */
49-
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
49+
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
5050
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
5151
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
5252
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */

0 commit comments

Comments
 (0)