feat: add network graph component and add tag graph
All checks were successful
Deploy eolas-app / deploy (push) Successful in 56s

This commit is contained in:
Thomas Bishop 2025-12-05 18:33:22 +00:00
parent 6a5598e81f
commit 777f6a0723
12 changed files with 7917 additions and 7669 deletions

118
package-lock.json generated
View file

@ -1,11 +1,11 @@
{
"name": "test-shadcn",
"name": "eolas-app",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "test-shadcn",
"name": "eolas-app",
"version": "0.0.0",
"dependencies": {
"@radix-ui/react-collapsible": "^1.1.7",
@ -17,6 +17,8 @@
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-tooltip": "^1.2.3",
"@react-sigma/core": "^5.0.4",
"@react-sigma/layout-forceatlas2": "^5.0.6",
"@tailwindcss/vite": "^4.1.4",
"@tanstack/react-query": "^5.83.0",
"@tanstack/react-query-devtools": "^5.83.0",
@ -25,6 +27,8 @@
"axios": "^1.10.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"graphology": "^0.26.0",
"graphology-types": "^0.24.8",
"highlight.js": "^11.11.1",
"lucide-react": "^0.501.0",
"react": "^19.0.0",
@ -36,6 +40,7 @@
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"shiki": "^3.9.1",
"sigma": "^3.0.2",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.4",
"tw-animate-css": "^1.2.7"
@ -96,6 +101,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz",
"integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==",
"dev": true,
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.26.2",
@ -2101,6 +2107,38 @@
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="
},
"node_modules/@react-sigma/core": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/@react-sigma/core/-/core-5.0.6.tgz",
"integrity": "sha512-Xu2qXyvDZIhmvGC1n8d7Kcxm5Ntcz4HbPIM7CPDD2e4h3s/oxVpVPX7wtsNreJRRPj9mK+3oqB6SWXNI4mTqVg==",
"license": "MIT",
"peerDependencies": {
"graphology": "^0.26.0",
"react": "^18.0.0 || ^19.0.0",
"sigma": "^3.0.2"
}
},
"node_modules/@react-sigma/layout-core": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/@react-sigma/layout-core/-/layout-core-5.0.6.tgz",
"integrity": "sha512-69ec5IrzJamrzSuccBwnjvse2dMmIUGmoxlFnOIoAhqqpNVEnzsrwVRd5G13tAdk30FyxvKw/E1dEgOP8lQM8g==",
"license": "MIT",
"dependencies": {
"@react-sigma/core": "^5.0.6"
}
},
"node_modules/@react-sigma/layout-forceatlas2": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/@react-sigma/layout-forceatlas2/-/layout-forceatlas2-5.0.6.tgz",
"integrity": "sha512-BYd+6iDMRrpNUxm7xWhtiLLn7sksoH6xHLXvhbDOtbCIVUEceS57Mxbe4NGZs5zR//+YBIGAbwxQKIk3KrhnMQ==",
"license": "MIT",
"dependencies": {
"@react-sigma/layout-core": "^5.0.6"
},
"peerDependencies": {
"graphology-layout-forceatlas2": "^0.10.1"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.35",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.35.tgz",
@ -2891,6 +2929,7 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.83.0.tgz",
"integrity": "sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/query-core": "5.83.0"
},
@ -3059,6 +3098,7 @@
"integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@ -3067,6 +3107,7 @@
"version": "19.1.2",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz",
"integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@ -3076,6 +3117,7 @@
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.2.tgz",
"integrity": "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==",
"devOptional": true,
"peer": true,
"peerDependencies": {
"@types/react": "^19.0.0"
}
@ -3120,6 +3162,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.30.1.tgz",
"integrity": "sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg==",
"dev": true,
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.30.1",
"@typescript-eslint/types": "8.30.1",
@ -3342,6 +3385,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -3481,6 +3525,7 @@
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.73",
@ -3945,6 +3990,7 @@
"integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@ -4122,6 +4168,15 @@
"node": ">=0.10.0"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"license": "MIT",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@ -4401,6 +4456,48 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true
},
"node_modules/graphology": {
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/graphology/-/graphology-0.26.0.tgz",
"integrity": "sha512-8SSImzgUUYC89Z042s+0r/vMibY7GX/Emz4LDO5e7jYXhuoWfHISPFJYjpRLUSJGq6UQ6xlenvX1p/hJdfXuXg==",
"license": "MIT",
"peer": true,
"dependencies": {
"events": "^3.3.0"
},
"peerDependencies": {
"graphology-types": ">=0.24.0"
}
},
"node_modules/graphology-layout-forceatlas2": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/graphology-layout-forceatlas2/-/graphology-layout-forceatlas2-0.10.1.tgz",
"integrity": "sha512-ogzBeF1FvWzjkikrIFwxhlZXvD2+wlY54lqhsrWprcdPjopM2J9HoMweUmIgwaTvY4bUYVimpSsOdvDv1gPRFQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"graphology-utils": "^2.1.0"
},
"peerDependencies": {
"graphology-types": ">=0.19.0"
}
},
"node_modules/graphology-types": {
"version": "0.24.8",
"resolved": "https://registry.npmjs.org/graphology-types/-/graphology-types-0.24.8.tgz",
"integrity": "sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q==",
"license": "MIT",
"peer": true
},
"node_modules/graphology-utils": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/graphology-utils/-/graphology-utils-2.5.2.tgz",
"integrity": "sha512-ckHg8MXrXJkOARk56ZaSCM1g1Wihe2d6iTmz1enGOz4W/l831MBCKSayeFQfowgF8wd+PQ4rlch/56Vs/VZLDQ==",
"license": "MIT",
"peerDependencies": {
"graphology-types": ">=0.23.0"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -6396,6 +6493,7 @@
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -6404,6 +6502,7 @@
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
@ -6807,6 +6906,17 @@
"@types/hast": "^3.0.4"
}
},
"node_modules/sigma": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/sigma/-/sigma-3.0.2.tgz",
"integrity": "sha512-/BUbeOwPGruiBOm0YQQ6ZMcLIZ6tf/W+Jcm7dxZyAX0tK3WP9/sq7/NAWBxPIxVahdGjCJoGwej0Gdrv0DxlQQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"events": "^3.3.0",
"graphology-utils": "^2.5.2"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -6936,6 +7046,7 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"peer": true,
"engines": {
"node": ">=12"
},
@ -7018,6 +7129,7 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"dev": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -7297,6 +7409,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@ -7383,6 +7496,7 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"peer": true,
"engines": {
"node": ">=12"
},

View file

@ -19,6 +19,8 @@
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-tooltip": "^1.2.3",
"@react-sigma/core": "^5.0.4",
"@react-sigma/layout-forceatlas2": "^5.0.6",
"@tailwindcss/vite": "^4.1.4",
"@tanstack/react-query": "^5.83.0",
"@tanstack/react-query-devtools": "^5.83.0",
@ -27,6 +29,8 @@
"axios": "^1.10.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"graphology": "^0.26.0",
"graphology-types": "^0.24.8",
"highlight.js": "^11.11.1",
"lucide-react": "^0.501.0",
"react": "^19.0.0",
@ -38,6 +42,7 @@
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"shiki": "^3.9.1",
"sigma": "^3.0.2",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.4",
"tw-animate-css": "^1.2.7"

View file

@ -24,11 +24,7 @@ button[data-state="active"] {
var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
/* .HoverCardContent { */
/* width: 300px; */
/* max-height: 500px; */
/* } */
/* .HoverCardContent { */
/* transform-origin: 10px; */
/* } */
.sigma-container {
width: 100%;
height: 100%;
}

View file

@ -0,0 +1,34 @@
import { useEffect } from "react"
import { useNavigate } from "react-router"
import { useRegisterEvents, useSigma } from "@react-sigma/core"
const GraphEvents = () => {
const registerEvents = useRegisterEvents()
const sigma = useSigma()
const navigate = useNavigate()
useEffect(() => {
registerEvents({
enterNode: (event) => {
const graph = sigma.getGraph()
const edges = graph.edges(event.node)
edges.forEach((edgeId) => {
graph.setEdgeAttribute(edgeId, "size", 3)
graph.setEdgeAttribute(edgeId, "color", "#0a0a0a")
})
},
leaveNode: () => {
const graph = sigma.getGraph()
graph.forEachEdge((edgeId) => {
graph.removeEdgeAttribute(edgeId, "size")
})
},
clickNode: (event) => navigate(`/entries/${event.node}`),
})
}, [registerEvents, sigma])
return null
}
export default GraphEvents

View file

@ -0,0 +1,45 @@
import { useEffect } from "react"
import Graph from "graphology"
import { useLoadGraph, useSigma } from "@react-sigma/core"
import forceAtlas2 from "graphology-layout-forceatlas2"
const LoadGraph = ({ data }) => {
const loadGraph = useLoadGraph()
const sigma = useSigma()
useEffect(() => {
const graph = new Graph()
data.nodes.forEach((node) => {
graph.addNode(node.id, {
label: node.name,
x: Math.random(),
y: Math.random(),
size: 8,
color: node.focal ? "#0a0a0a" : "#a4a4a4",
})
})
data.links.forEach((link) => {
graph.addEdge(link.source, link.target, {
size: 1,
color: "#a4a4a4",
})
})
forceAtlas2.assign(graph, {
iterations: 100,
settings: {
gravity: 0,
scalingRatio: 8,
strongGravityMode: false,
slowDown: 2,
},
})
loadGraph(graph)
}, [loadGraph, data, sigma])
return null
}
export default LoadGraph

View file

@ -0,0 +1,32 @@
import { SigmaContainer } from "@react-sigma/core"
import LoadGraph from "./LoadGraph"
import GraphEvents from "./GraphEvents"
export default function NetworkGraph({ data }) {
const nodeCount = data?.nodes.length
const sigmaStyle = {
height: "400px",
width: "100%",
}
const settings = {
allowInvalidContainer: true,
defaultEdgeColor: "#a4a4a4",
labelColor: { color: "#0a0a0a" },
labelFont: "Inter",
labelSize: 14,
labelWeight: "400",
labelRenderedSizeThreshold: nodeCount > 15 ? 10 : 8,
renderLabels: true,
}
return (
<div style={{ backgroundColor: "#fff" }}>
<SigmaContainer style={sigmaStyle} settings={settings}>
<LoadGraph data={data} />
<GraphEvents />
</SigmaContainer>
</div>
)
}

View file

@ -0,0 +1,20 @@
import NetworkGraph from "./NetworkGraph"
export default function TagGraph({ tag, entries }) {
const data = {
nodes: [
{ id: tag, name: tag, focal: true },
...entries?.map((entry) => ({
id: entry?.entry_title,
name: entry?.entry_title.replace(/_/g, " "),
})),
],
links: entries?.map((entry) => ({ source: tag, target: entry?.entry_title })),
}
return (
<div className="p-4 lg:p-6">
<NetworkGraph data={data} />
</div>
)
}

View file

@ -11,7 +11,6 @@ export default function ThemeToggle() {
setTheme(theme === "dark" ? "light" : "dark")
}
console.log(darkMode)
return (
<div>
<h3 className="font-semibold mb-4">Theme</h3>

View file

@ -9,7 +9,6 @@ export default function Entry({ entryTitle, entryBody, history, metadata, isLoad
const topPanelSize = isMobile ? 90 : 75
const bottomPanelSize = isMobile ? 10 : 25
const handleSize = isMobile ? "5px" : "1px"
console.log(isMobile)
return (
<ResizablePanelGroup direction="vertical" className="w-full h-full">
<ResizablePanel defaultSize={topPanelSize}>

View file

@ -5,7 +5,6 @@ import { History } from "lucide-react"
import { Braces } from "lucide-react"
export default function EntryMetadata({ entryTitle, history, metadata }) {
console.log(history)
return (
<Tabs defaultValue="references" className="w-full h-full flex flex-col">
<TabsList className="rounded-none shadow-none drop-shadow-none w-full bg-sidebar sticky top-0 z-10 flex-shrink-0">

View file

@ -7,6 +7,7 @@ import { ArrowUpDown } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Link } from "react-router"
import { Badge } from "@/components/ui/badge"
import TagGraph from "@/components/TagGraph"
const columns = [
{
accessorKey: "entry_title",
@ -41,6 +42,7 @@ export default function TagTemplate() {
queryKey: [`entries_for_tag_${tag}`],
queryFn: () => api.get(`/entries/tag/${tag}`).then((res) => res.data),
})
return (
<PageTemplate
titleComponent={
@ -53,9 +55,12 @@ export default function TagTemplate() {
</div>
}
pageBody={
<>
{data?.data && <TagGraph tag={tag} entries={data?.data} />}
<div className="p-4 lg:p-6">
<DataTable columns={columns} data={data?.data || []} loading={isLoading} />
</div>
</>
}
/>
)