Skip to content

Commit 410a952

Browse files
authored
Add graph file picker to visualization app (#121)
* Add graph file picker to visualization app * Rename local constants * Address feedback * Add experimental banner * Show error message
1 parent 05a759a commit 410a952

File tree

1 file changed

+90
-9
lines changed

1 file changed

+90
-9
lines changed

web/src/App.tsx

Lines changed: 90 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPa
55
// @ts-expect-error
66
import { CSS2DObject, CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer.js'
77
import ForceGraph, { ForceGraphMethods, LinkObject, NodeObject } from "react-force-graph-3d";
8-
import { useEffect, useRef, useState } from "react";
8+
import React, { useEffect, useMemo, useRef, useState } from "react";
99
import { Leva, useControls } from "leva";
1010

1111
import { buildXGraph, XLink, XNode } from "./XGraph.ts";
@@ -53,9 +53,8 @@ const DEFAULT_SETTINGS = {
5353

5454
const UNREAL_BLOOM_PASS = new UnrealBloomPass()
5555

56-
const { xGraph: X_GRAPH, nodes: NODES, fileTree: FILE_TREE } = buildXGraph(Data.__INLINE_DATA)
57-
58-
function App () {
56+
function GraphExplorer({ graphData }: { graphData: Graph }) {
57+
const { xGraph, nodes, fileTree } = useMemo(() => buildXGraph(graphData), [graphData])
5958
const [highlightNodes, setHighlightNodes] = useState(new Set<XNode>())
6059
const [highlightLinks, setHighlightLinks] = useState(new Set<XLink>())
6160
const [selectedNode, setSelectedNode] = useState<XNode>()
@@ -161,10 +160,10 @@ function App () {
161160
if (link.isDir) f = settings.DIR_LINK_STRENGTH_FACTOR
162161
if (link.isPackage) f = settings.PACKAGE_LINK_STRENGTH_FACTOR
163162
if (link.ignore) f = 0
164-
return f / Math.min(NODES[link.from].neighbors?.length ?? 1, NODES[link.to].neighbors?.length ?? 1);
163+
return f / Math.min(nodes[link.from].neighbors?.length ?? 1, nodes[link.to].neighbors?.length ?? 1);
165164
})
166165
graph.current?.d3ReheatSimulation()
167-
}, [settings.DIR_LINK_STRENGTH_FACTOR, settings.FILE_LINK_STRENGTH_FACTOR, settings.LINK_DISTANCE, settings.PACKAGE_LINK_STRENGTH_FACTOR, updateForced])
166+
}, [nodes, settings.DIR_LINK_STRENGTH_FACTOR, settings.FILE_LINK_STRENGTH_FACTOR, settings.LINK_DISTANCE, settings.PACKAGE_LINK_STRENGTH_FACTOR, updateForced])
168167

169168
useEffect(() => {
170169
graph.current?.d3Force('charge')
@@ -187,7 +186,7 @@ function App () {
187186
<ForceGraph
188187
ref={graph}
189188
extraRenderers={[new CSS2DRenderer()]}
190-
graphData={X_GRAPH}
189+
graphData={xGraph}
191190
backgroundColor={'#000003'}
192191
nodeResolution={settings.NODE_RESOLUTION}
193192
onBackgroundClick={backgroundClick}
@@ -217,15 +216,97 @@ function App () {
217216
/>
218217
<Explorer
219218
className={'fixed top-0 left-0 max-h-full bg-transparent'}
220-
fileTree={FILE_TREE}
219+
fileTree={fileTree}
221220
onSelectNode={nodeClick}
222221
selected={selectedNode}
223222
highlighted={highlightNodes}
224223
onNodesMutated={forceUpdate}
225224
/>
226-
<Leva hidden={!X_GRAPH.enableGui}/>
225+
<Leva hidden={!xGraph.enableGui}/>
227226
</>
228227
)
229228
}
230229

230+
function DropPlaceholder() {
231+
return (
232+
<div className="flex flex-col items-center justify-center min-h-screen w-full text-gray-200">
233+
<h2 className="text-xl font-semibold mb-2">Drop Here!</h2>
234+
</div>
235+
)
236+
}
237+
238+
function FilePicker({ onFilePicked }: { onFilePicked: (file: File | undefined) => void }) {
239+
const handleFileSelected = (e: React.ChangeEvent<HTMLInputElement>) => {
240+
onFilePicked(e.target.files?.[0]);
241+
};
242+
243+
return (
244+
<>
245+
<h2 className="text-xl font-semibold mb-2">Drop Dep-Tree JSON Graph File Here</h2>
246+
<p className="text-gray-400 mb-4">or</p>
247+
<input
248+
type="file"
249+
accept=".json"
250+
onChange={handleFileSelected}
251+
className="bg-gray-800 px-4 py-2 rounded cursor-pointer hover:bg-gray-700"
252+
/>
253+
</>
254+
)
255+
}
256+
257+
function App() {
258+
const [graphData, setGraphData] = useState<Graph>(Data.__INLINE_DATA)
259+
const [isDragging, setIsDragging] = useState(false)
260+
const [forceRemountKey, setForceRemountKey] = useState(0)
261+
const [errorMessage, setErrorMessage] = useState<string | null>(null)
262+
263+
async function handleFilePicked(file: File | undefined) {
264+
if (!file) return
265+
try {
266+
const data = JSON.parse(await file.text())
267+
setGraphData(data)
268+
setForceRemountKey((x) => x + 1)
269+
setErrorMessage(null)
270+
} catch (e) {
271+
console.error('Failed to read file:', e)
272+
setErrorMessage('' + e)
273+
}
274+
}
275+
276+
function handleDrop(e: React.DragEvent<HTMLDivElement>) {
277+
e.preventDefault()
278+
setIsDragging(false)
279+
void handleFilePicked(e.dataTransfer.files[0])
280+
}
281+
282+
return (
283+
<div
284+
className={`fixed top-0 left-0 w-full h-full ${isDragging ? 'border-2 border-dashed rounded-lg border-gray-500' : ''}`}
285+
onDrop={handleDrop}
286+
onDragOver={(e) => {
287+
e.preventDefault()
288+
setIsDragging(true)
289+
}}
290+
onDragLeave={() => setIsDragging(false)}>
291+
{isDragging
292+
? <DropPlaceholder />
293+
: ((!graphData.nodes?.length || errorMessage)
294+
? <div className="flex flex-col items-center justify-center min-h-screen w-full text-gray-200">
295+
<FilePicker onFilePicked={handleFilePicked} />
296+
{errorMessage ? (
297+
<div className="bg-red-600/20 border border-red-500/50 rounded-lg px-4 py-2 mt-12 max-w-lg text-center">
298+
<span className="text-red-500 font-medium">🚨 Error</span>
299+
<p className="text-red-400/80 text-sm mt-1">{errorMessage}</p>
300+
</div>
301+
) : (
302+
<div className="bg-yellow-600/20 border border-yellow-500/50 rounded-lg px-4 py-2 mt-12 max-w-lg text-center">
303+
<span className="text-yellow-500 font-medium">⚠️ Experimental</span>
304+
<p className="text-yellow-400/80 text-sm mt-1">This feature relies on an internal structure that may break in the future.</p>
305+
</div>)}
306+
</div>
307+
: <GraphExplorer key={forceRemountKey} graphData={graphData} />)}
308+
</div>
309+
)
310+
}
311+
231312
export default App

0 commit comments

Comments
 (0)