diff --git a/graphviz/frontend/package-lock.json b/graphviz/frontend/package-lock.json index 59ea348..f895b15 100644 --- a/graphviz/frontend/package-lock.json +++ b/graphviz/frontend/package-lock.json @@ -12,7 +12,10 @@ "react": "^19.2.5", "react-dom": "^19.2.5", "react-force-graph-2d": "^1.29.1", - "tailwindcss": "^4.2.4" + "react-force-graph-3d": "^1.29.1", + "tailwindcss": "^4.2.4", + "three": "^0.184.0", + "three-spritetext": "^1.10.0" }, "devDependencies": { "@types/react": "^19.0.0", @@ -33,6 +36,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -2515,6 +2527,22 @@ "@types/react": "^19.2.0" } }, + "node_modules/3d-force-graph": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/3d-force-graph/-/3d-force-graph-1.80.0.tgz", + "integrity": "sha512-tzI353gW1nXPpnC7VTa3JjMg+3cp77qOLUFO0vucPTfF+q5R6sQsNsIqVTbRIb7RSypn14nBa4yfkOe9ThxASw==", + "license": "MIT", + "dependencies": { + "accessor-fn": "1", + "kapsule": "^1.16", + "three": ">=0.179 <1", + "three-forcegraph": "1", + "three-render-objects": "^1.41" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/accessor-fn": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.3.tgz", @@ -2930,6 +2958,18 @@ "node": ">=12" } }, + "node_modules/data-bind-mapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/data-bind-mapper/-/data-bind-mapper-1.0.3.tgz", + "integrity": "sha512-QmU3lyEnbENQPo0M1F9BMu4s6cqNNp8iJA+b/HP2sSb7pf3dxwF3+EP1eO69rwBfH9kFJ1apmzrtogAmVt2/Xw==", + "license": "MIT", + "dependencies": { + "accessor-fn": "1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", @@ -3570,6 +3610,44 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/ngraph.events": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.4.0.tgz", + "integrity": "sha512-NeDGI4DSyjBNBRtA86222JoYietsmCXbs8CEB0dZ51Xeh4lhVl1y3wpWLumczvnha8sFQIW4E0vvVWwgmX2mGw==", + "license": "BSD-3-Clause" + }, + "node_modules/ngraph.forcelayout": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/ngraph.forcelayout/-/ngraph.forcelayout-3.3.1.tgz", + "integrity": "sha512-MKBuEh1wujyQHFTW57y5vd/uuEOK0XfXYxm3lC7kktjJLRdt/KEKEknyOlc6tjXflqBKEuYBBcu7Ax5VY+S6aw==", + "license": "BSD-3-Clause", + "dependencies": { + "ngraph.events": "^1.0.0", + "ngraph.merge": "^1.0.0", + "ngraph.random": "^1.0.0" + } + }, + "node_modules/ngraph.graph": { + "version": "20.1.2", + "resolved": "https://registry.npmjs.org/ngraph.graph/-/ngraph.graph-20.1.2.tgz", + "integrity": "sha512-W/G3GBR3Y5UxMLHTUCPP9v+pbtpzwuAEIqP5oZV+9IwgxAIEZwh+Foc60iPc1idlnK7Zxu0p3puxAyNmDvBd0Q==", + "license": "BSD-3-Clause", + "dependencies": { + "ngraph.events": "^1.4.0" + } + }, + "node_modules/ngraph.merge": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ngraph.merge/-/ngraph.merge-1.0.0.tgz", + "integrity": "sha512-5J8YjGITUJeapsomtTALYsw7rFveYkM+lBj3QiYZ79EymQcuri65Nw3knQtFxQBU1r5iOaVRXrSwMENUPK62Vg==", + "license": "MIT" + }, + "node_modules/ngraph.random": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ngraph.random/-/ngraph.random-1.2.0.tgz", + "integrity": "sha512-4EUeAGbB2HWX9njd6bP6tciN6ByJfoaAvmVL9QTaZSeXrW46eNGA9GajiXiPBbvFqxUWFkEbyo6x5qsACUuVfA==", + "license": "BSD-3-Clause" + }, "node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", @@ -3698,6 +3776,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/polished": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz", + "integrity": "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.17.8" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/postcss": { "version": "8.5.12", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", @@ -3792,6 +3882,23 @@ "react": "*" } }, + "node_modules/react-force-graph-3d": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/react-force-graph-3d/-/react-force-graph-3d-1.29.1.tgz", + "integrity": "sha512-5Vp+PGpYnO+zLwgK2NvNqdXHvsWLrFzpDfJW1vUA1twjo9SPvXqfUYQrnRmAbD+K2tOxkZw1BkbH31l5b4TWHg==", + "license": "MIT", + "dependencies": { + "3d-force-graph": "^1.79", + "prop-types": "15", + "react-kapsule": "^2.5" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -3924,6 +4031,67 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/three": { + "version": "0.184.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz", + "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==", + "license": "MIT" + }, + "node_modules/three-forcegraph": { + "version": "1.43.4", + "resolved": "https://registry.npmjs.org/three-forcegraph/-/three-forcegraph-1.43.4.tgz", + "integrity": "sha512-FtmiZP/T16ZQaHza3JDaDn0YTXFtg9e7pGnTeU8nzu0NNkx7MpWbF/GvmpbQsWHx3rukHtkRv1fTorLPB3FDEA==", + "license": "MIT", + "dependencies": { + "accessor-fn": "1", + "d3-array": "1 - 3", + "d3-force-3d": "2 - 3", + "d3-scale": "1 - 4", + "d3-scale-chromatic": "1 - 3", + "data-bind-mapper": "1", + "kapsule": "^1.16", + "ngraph.forcelayout": "3", + "ngraph.graph": "20", + "tinycolor2": "1" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "three": ">=0.118.3" + } + }, + "node_modules/three-render-objects": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/three-render-objects/-/three-render-objects-1.41.1.tgz", + "integrity": "sha512-0H7l7yREPVKfO3HL7RjPQ67T0phHgnyMeEc4ww/OCEfK6jbsm7psEcrR0SGFqGDyS/pDQTPi4DyPbS/xlHRJKw==", + "license": "MIT", + "dependencies": { + "@tweenjs/tween.js": "18 - 25", + "accessor-fn": "1", + "float-tooltip": "^1.7", + "kapsule": "^1.16", + "polished": "4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "three": ">=0.179" + } + }, + "node_modules/three-spritetext": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/three-spritetext/-/three-spritetext-1.10.0.tgz", + "integrity": "sha512-t08iP1FCU1lQh8T5MmCpdijKgas8GDHJE0LqMGBuVu3xqMMpFnEZhTlih7FlxLPQizHIGoumUSpfOlY1GO/Tgg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "three": ">=0.86.0" + } + }, "node_modules/tinycolor2": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", diff --git a/graphviz/frontend/package.json b/graphviz/frontend/package.json index a73686f..79554c5 100644 --- a/graphviz/frontend/package.json +++ b/graphviz/frontend/package.json @@ -12,12 +12,18 @@ "react": "^19.2.5", "react-dom": "^19.2.5", "react-force-graph-2d": "^1.29.1", - "tailwindcss": "^4.2.4" + "react-force-graph-3d": "^1.29.1", + "tailwindcss": "^4.2.4", + "three": "^0.184.0", + "three-spritetext": "^1.10.0" }, "devDependencies": { "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "parcel": "^2.14.0", "parcel-reporter-static-files-copy": "^1.5.3" + }, + "@parcel/resolver-default": { + "packageExports": true } } diff --git a/graphviz/frontend/src/Home.tsx b/graphviz/frontend/src/Home.tsx index 8572f5a..0cb6682 100644 --- a/graphviz/frontend/src/Home.tsx +++ b/graphviz/frontend/src/Home.tsx @@ -24,6 +24,9 @@ export function Home() {

A great introduction to the dataset on a curated set of examples

+

+ Also available in 3D +

diff --git a/graphviz/frontend/src/VizSmall3D.tsx b/graphviz/frontend/src/VizSmall3D.tsx new file mode 100644 index 0000000..0ca2be0 --- /dev/null +++ b/graphviz/frontend/src/VizSmall3D.tsx @@ -0,0 +1,111 @@ +import React, { useEffect, useMemo, useRef, useState } from "react"; +import ForceGraph3D from "react-force-graph-3d"; + +import data from "./data/data.json"; +import titlesData from "./data/titles.json"; +import HomeButton from "./utils/HomeButton"; +import { FloatingPanel } from "./utils/FloatingPanel"; +import { DetailsPanel } from "./utils/DetailsPanel"; +import { FloatingPanelStack } from "./utils/FloatingPanelStack"; +import { drawRoundedRect, getConnectedComponents } from "./graph/common"; +import { buildGraph } from "./VizSmallConnected"; +import * as THREE from 'three'; +import SpriteText from 'three-spritetext'; + +export function VizSmall3D() { + const fgRef = useRef(); + const [selectedNode, setSelectedNode] = useState(null); + const [minGraphSize, setMinGraphSize] = useState(10); + const [showLabel, setShowLabel] = useState(false); + + const graphData = useMemo(() => { + const full = buildGraph(data); + + const components = getConnectedComponents(full.nodes, full.links); + + // keep only components large enough + const validIds = new Set( + components + .filter(comp => comp.length >= minGraphSize && comp.length < 50) + .flat() + ); + + const filteredNodes = full.nodes.filter(n => validIds.has(n.id)); + + const filteredLinks = full.links.filter( + l => validIds.has(l.source) && validIds.has(l.target) + ); + + return { + nodes: filteredNodes, + links: filteredLinks + }; + }, [minGraphSize]); + + + return ( +
+ + node.label} + nodeAutoColorBy="type" + linkColor={() => "black"} + linkWidth={2.5} + onNodeClick={(node) => setSelectedNode(node)} + backgroundColor="white" + nodeThreeObject={(node) => { + const circle = new THREE.Mesh( + new THREE.SphereGeometry(node.members.length * 2), + new THREE.MeshLambertMaterial({ + color: node.type.includes("claim") ? "DarkMagenta" : "green", + transparent: true, + opacity: 0.75 + })); + + if (!showLabel) { + return circle; + } + + const group = new THREE.Group(); + group.add(circle); + + const text = new SpriteText(node.label); + text.textHeight = 8; + text.offsetY = -12; + text.color = "black"; + + group.add(text); + + return group; + } + } + /> + + + + + +
+ setMinGraphSize(Number(e.target.value))} + /> +
+
+
+ ); +} \ No newline at end of file diff --git a/graphviz/frontend/src/VizSmallConnected.tsx b/graphviz/frontend/src/VizSmallConnected.tsx index 05b931c..c8320d9 100644 --- a/graphviz/frontend/src/VizSmallConnected.tsx +++ b/graphviz/frontend/src/VizSmallConnected.tsx @@ -10,7 +10,7 @@ import { DetailsPanel } from "./utils/DetailsPanel"; import { FloatingPanelStack } from "./utils/FloatingPanelStack"; import { drawRoundedRect, getConnectedComponents } from "./graph/common"; -function buildGraph(data) { +export function buildGraph(data) { const nodes = []; const links = []; diff --git a/graphviz/frontend/src/index.html b/graphviz/frontend/src/index.html index d87e486..59a2142 100644 --- a/graphviz/frontend/src/index.html +++ b/graphviz/frontend/src/index.html @@ -4,7 +4,7 @@ - Parcel React App + LLMs For Disinformation Analysis
diff --git a/graphviz/frontend/src/index.tsx b/graphviz/frontend/src/index.tsx index efd88cf..c8b0f08 100644 --- a/graphviz/frontend/src/index.tsx +++ b/graphviz/frontend/src/index.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from "react"; import { VizSmallConnected } from "./VizSmallConnected"; import { VizTimeFilter } from "./VizTimeFilter"; import { Home } from "./Home"; +import { VizSmall3D } from "./VizSmall3D"; export function AppRouter() { const [route, setRoute] = useState(() => window.location.hash); @@ -19,6 +20,7 @@ export function AppRouter() { if (route === "#small") return ; if (route === "#time") return ; + if (route === "#3d") return ; return ; } diff --git a/graphviz/frontend/static/small.png b/graphviz/frontend/static/small.png index fe20612..596236e 100644 Binary files a/graphviz/frontend/static/small.png and b/graphviz/frontend/static/small.png differ diff --git a/graphviz/frontend/static/time.png b/graphviz/frontend/static/time.png index ca5fc8a..f8b7c6d 100644 Binary files a/graphviz/frontend/static/time.png and b/graphviz/frontend/static/time.png differ