{"id":50432725,"url":"https://github.com/danielsobrado/galaxy-nodes","last_synced_at":"2026-05-31T15:01:30.421Z","repository":{"id":361578103,"uuid":"1254929286","full_name":"danielsobrado/galaxy-nodes","owner":"danielsobrado","description":"Three.js component library for exploring dense graph data in 3D as an interactive “galaxy” of nodes, clusters, and weighted relationships. It provides a polished React API for GPU-accelerated point clouds, cluster filament visuals, camera controls, search/filter workflows, and node/edge inspection.","archived":false,"fork":false,"pushed_at":"2026-05-31T09:57:31.000Z","size":168,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-31T11:08:17.678Z","etag":null,"topics":["3d-visualization","data-visualization","data-viz","graph-data","graph-visualization","interactive-ui","network-graph","npm-package","react","threejs","typescript","typescript-library","webgl"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/danielsobrado.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-31T07:09:42.000Z","updated_at":"2026-05-31T09:57:34.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/danielsobrado/galaxy-nodes","commit_stats":null,"previous_names":["danielsobrado/galaxy-nodes"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/danielsobrado/galaxy-nodes","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danielsobrado%2Fgalaxy-nodes","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danielsobrado%2Fgalaxy-nodes/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danielsobrado%2Fgalaxy-nodes/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danielsobrado%2Fgalaxy-nodes/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/danielsobrado","download_url":"https://codeload.github.com/danielsobrado/galaxy-nodes/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danielsobrado%2Fgalaxy-nodes/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33735663,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-31T02:00:06.040Z","response_time":95,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["3d-visualization","data-visualization","data-viz","graph-data","graph-visualization","interactive-ui","network-graph","npm-package","react","threejs","typescript","typescript-library","webgl"],"created_at":"2026-05-31T15:01:29.410Z","updated_at":"2026-05-31T15:01:30.415Z","avatar_url":"https://github.com/danielsobrado.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Galaxy Nodes\n\nA framework-neutral Three.js library for navigating dense graph data as a galaxy, with a ready-made React adapter. It renders GPU point clouds, planet-like graph nodes, selectable relationships, sparse cluster labels, camera navigation, search focus, group filters, and hover/click inspection.\n\n![Galaxy Nodes screenshot](docs/images/demo1_v0.1.jpg)\n\n## Install\n\n```bash\nnpm install galaxy-nodes three\n```\n\nReact consumers should also install React peer dependencies:\n\n```bash\nnpm install react react-dom\n```\n\n## Generic Usage\n\nThe root package export remains the React adapter for backward compatibility. New React integrations can also import from `galaxy-nodes/react`. Because `react` and `react-dom` are optional peer dependencies, non-React consumers should import from `galaxy-nodes/core` instead of the bare `galaxy-nodes` root.\n\n```tsx\nimport { GalaxyGraphVisualizer, type GraphAccessors, type GraphDataset } from 'galaxy-nodes';\nimport 'galaxy-nodes/styles.css';\n\ninterface PersonMeta {\n  role: 'engineer' | 'designer';\n}\n\nconst dataset: GraphDataset\u003cPersonMeta\u003e = {\n  nodes: [\n    {\n      id: 'ada',\n      label: 'Ada',\n      group: 'team-a',\n      major: true,\n      meta: { role: 'engineer' },\n    },\n    {\n      id: 'grace',\n      label: 'Grace',\n      group: 'team-a',\n      meta: { role: 'designer' },\n    },\n  ],\n  edges: [{ source: 'ada', target: 'grace', weight: 0.8 }],\n};\n\nconst accessors: GraphAccessors\u003cPersonMeta\u003e = {\n  nodeColor: (node) =\u003e (node.meta?.role === 'engineer' ? '#6bd7ff' : '#f5cf5b'),\n  nodeLabel: (node) =\u003e node.label ?? node.id,\n};\n\nexport function GraphView() {\n  return (\n    \u003cGalaxyGraphVisualizer\n      dataset={dataset}\n      accessors={accessors}\n      layout={{ seed: 'team-graph' }}\n      renderNodeDetail={(node) =\u003e (\n        \u003cdl\u003e\n          \u003cdiv\u003e\n            \u003cdt\u003eRole\u003c/dt\u003e\n            \u003cdd\u003e{node.meta?.role ?? 'Unknown'}\u003c/dd\u003e\n          \u003c/div\u003e\n        \u003c/dl\u003e\n      )}\n    /\u003e\n  );\n}\n```\n\nLower-level React scene-only embedding is available through `GalaxyScene` when you want to provide your own HUD, panels, and data controls.\n\n## Framework-Neutral Core\n\nUse `galaxy-nodes/core` when your app owns the framework lifecycle, such as vanilla DOM, Svelte, or a custom element. Vue and Angular can use the imperative core directly; `galaxy-nodes/vue` and `galaxy-nodes/angular` only provide framework-named aliases for the same renderer.\n\n```ts\nimport { createGalaxyRenderer, type GraphDataset } from 'galaxy-nodes/core';\nimport 'galaxy-nodes/styles.css';\n\nconst dataset: GraphDataset = {\n  nodes: [{ id: 'api', label: 'API', group: 'Services', major: true }],\n  edges: [],\n};\n\nconst renderer = createGalaxyRenderer(\n  document.querySelector\u003cHTMLElement\u003e('#graph')!,\n  {\n    dataset,\n    activeGroup: null,\n    showClusters: true,\n    galaxyMode: true,\n    cameraCommand: null,\n    selectedNodeId: null,\n    selectedEdgeId: null,\n  },\n  {\n    onSelectNode: (node) =\u003e console.log(node?.id),\n    onSceneFailure: (failure) =\u003e console.warn(failure.reason, failure.message),\n  },\n);\n\n// Later, patch state without remounting when topology/layout did not change.\nrenderer.update({\n  dataset,\n  activeGroup: 'Services',\n  showClusters: true,\n  galaxyMode: true,\n  cameraCommand: null,\n  selectedNodeId: null,\n  selectedEdgeId: null,\n});\n\nrenderer.dispose();\n```\n\nVue and Angular lifecycle examples are in [docs/examples.md](docs/examples.md).\n\n## Reduced Motion\n\nGalaxy Nodes respects `prefers-reduced-motion` by default. With the default `motionPreference: 'system'`, ambient galaxy rotation, planet spin, and selection marker spin are paused for users who request reduced motion while direct interactions such as orbiting, keyboard navigation, search focus, and selection remain available.\n\nOverride the system preference only when your product has its own motion setting:\n\n```tsx\n\u003cGalaxyGraphVisualizer dataset={dataset} options={{ motionPreference: 'reduced' }} /\u003e\n```\n\nUse `motionPreference: 'full'` to force ambient motion, or omit the option to follow the user's OS/browser preference.\n\n## WebGL Fallback\n\nIf WebGL is unavailable, scene initialization fails, or the browser loses the WebGL context, the component renders an accessible fallback panel with dataset counts. Scene-only controls are disabled while the renderer is unavailable, and recoverable failures include a retry button.\n\n```tsx\n\u003cGalaxyGraphVisualizer\n  dataset={dataset}\n  onSceneFailure={(failure) =\u003e {\n    console.warn(failure.reason, failure.message);\n  }}\n/\u003e\n```\n\nThe failure reason is one of `'webgl-unavailable'`, `'context-lost'`, or `'scene-error'`.\n\n## Next.js\n\nGalaxy Nodes is a client-rendered React component. In the App Router, put the graph in a client component and import the stylesheet there or from a client-side wrapper:\n\n```tsx\n'use client';\n\nimport { GalaxyGraphVisualizer, type GraphDataset } from 'galaxy-nodes';\nimport 'galaxy-nodes/styles.css';\n\nexport function GraphClient({ dataset }: { dataset: GraphDataset }) {\n  return \u003cGalaxyGraphVisualizer dataset={dataset} /\u003e;\n}\n```\n\nThe package guards browser-only work so server rendering can safely encounter the component tree, but the WebGL scene starts only after hydration. For very large graphs, `next/dynamic(() =\u003e import('./GraphClient'), { ssr: false })` can still be useful to defer client bundle work; it is not required for correctness.\n\n## Performance Envelope\n\nThe built-in renderer is tuned for sparse, large graph exploration:\n\n- Target envelope: up to about 100k nodes with a sampled sparse relationship layer. The bundled corporate demo caps generated relationships at 12k.\n- Nodes are always kept in one GPU point cloud, including nodes marked `major`.\n- Major nodes get a capped instanced planet/ring overlay for inspection; the point cloud still represents every node.\n- Group filters, cluster toggles, galaxy mode, selection, theme changes, and accessor changes update existing buffers, uniforms, materials, and visibility in place instead of rebuilding the renderer.\n- Dataset identity/topology changes and layout option changes can still rebuild the scene because they alter coordinates, lookup maps, and edge geometry.\n- Edges use sparse TubeGeometry for visual quality. Full dense enterprise graphs need a separate line-buffer or level-of-detail renderer.\n\nFor best results, keep the `dataset`, `layout`, and accessor objects stable with `useMemo` when their inputs have not changed.\n\n## Optional Large Graph Loading\n\nFor remote graphs, keep `largeGraph.enabled` off until you want URL-backed detail and explicit expansion controls. The component calls host-provided callbacks for node details, edge details, and graph expansion; it does not own URLs, auth, or caching.\n\nExpansion callbacks return a `GraphDatasetPatch`, which can be merged with `mergeGraphDataset(base, patch, { edgeBudget: 12_000 })`. The merge upserts nodes, clusters, and edges, preserves filaments first, and keeps the highest-weight relationship edges inside the budget.\n\nKeep auth in your host app by wrapping the callback transport. The visualizer passes an `AbortSignal` into each callback so authenticated requests can still be cancelled when the selection or expansion target changes:\n\n```tsx\nfunction graphApiHeaders(jwt: string, headers?: HeadersInit) {\n  const nextHeaders = new Headers(headers);\n  nextHeaders.set('Authorization', `Bearer ${jwt}`);\n  return nextHeaders;\n}\n\nconst fetchGraphApi = (input: string | URL, init: RequestInit = {}) =\u003e {\n  return fetch(input, {\n    ...init,\n    headers: graphApiHeaders(jwt, init.headers),\n  });\n};\n\nconst largeGraph = {\n  enabled: true,\n  async expandGraph(request, signal) {\n    const response = await fetchGraphApi(`/graph/expand/node/${request.nodeId}`, { signal });\n    if (!response.ok) throw new Error(`Graph expansion returned ${response.status}`);\n    return response.json();\n  },\n  async loadNodeDetail(node, signal) {\n    const response = await fetchGraphApi(`/graph/node/${node.id}/detail`, { signal });\n    if (!response.ok) throw new Error(`Node detail returned ${response.status}`);\n    return response.json();\n  },\n};\n```\n\nThe bundled example also accepts `VITE_GRAPH_API_TOKEN` for local testing. Browser-exposed Vite variables are not a secure place for production secrets; production apps should obtain JWTs from their normal client auth flow and enforce them on the graph API.\n\n## Layout\n\nCoordinates are optional. When any node positions or cluster spatial fields are missing, Galaxy Nodes computes a deterministic 3D galaxy layout from stable node ids, node `group` values, and connected components. Authored node positions, cluster centers, and cluster radii are preserved by default.\n\n```tsx\n\u003cGalaxyGraphVisualizer dataset={dataset} layout={{ seed: 'docs-demo', spacing: 320 }} /\u003e\n```\n\nSet `layout={false}` when you want strict pre-positioned data; missing positions then throw a clear error. The same resolver is exported for headless use:\n\n```ts\nimport { resolveGraphLayout } from 'galaxy-nodes';\n\nconst resolved = resolveGraphLayout(dataset, { seed: 'docs-demo' });\nconst adaPosition = resolved.nodePositions.get('ada');\n```\n\n## Styling And Colors\n\nLayout is spatial only. Use `layout` for coordinates, spacing, cluster radius, and deterministic seeds; use `accessors` and `theme` for colors, sizes, labels, and scene chrome. Accessor and theme changes update the existing renderer in place when the dataset topology and layout are unchanged.\n\nNodes and edges can carry direct `color` fields. Without custom accessors, node colors prefer `node.color`, then hash `node.group` into the built-in palette, then fall back to a neutral gray. Edge colors prefer `edge.color`, with separate defaults for normal relationships and filament edges.\n\nFor product palettes or data-driven styling, provide `GraphAccessors`:\n\n```tsx\nimport { GalaxyGraphVisualizer, defaultNodeColor, type GraphAccessors, type GraphDataset } from 'galaxy-nodes';\n\nconst paletteByGroup: Record\u003cstring, string\u003e = {\n  Product: '#facc15',\n  Platform: '#38bdf8',\n  Security: '#fb7185',\n};\n\nconst accessors: GraphAccessors = {\n  nodeColor: (node) =\u003e node.color ?? paletteByGroup[node.group ?? ''] ?? defaultNodeColor(node),\n  edgeColor: (edge) =\u003e edge.color ?? (edge.weight \u0026\u0026 edge.weight \u003e 0.75 ? '#f5cf5b' : '#6bd7ff'),\n  nodeSize: (node) =\u003e node.size ?? (node.major ? 3 : 1),\n  nodeLabel: (node) =\u003e node.label ?? null,\n};\n\nexport function StyledGraph({ dataset }: { dataset: GraphDataset }) {\n  return (\n    \u003cGalaxyGraphVisualizer\n      dataset={dataset}\n      accessors={accessors}\n      theme={{\n        background: '#07090d',\n        panelAccentColor: '#67e8c9',\n        selectedColor: '#f8fafc',\n      }}\n    /\u003e\n  );\n}\n```\n\n`theme.background` controls the WebGL clear color and app background. `theme.panelAccentColor` controls HUD accents and secondary selection markers. `theme.selectedColor` controls selected/highlighted scene elements. There is no separate `palette` prop; keep palette logic in `nodeColor` or domain-specific accessor helpers.\n\n## Corporate Demo Preset\n\nThe corporate operations demo is available as a preset, not part of the generic root module:\n\n```tsx\nimport { GalaxyGraphVisualizer } from 'galaxy-nodes';\nimport {\n  createInitiativeAccessors,\n  generateGalaxyDataset,\n  renderInitiativeEdgeDetail,\n  renderInitiativeNodeDetail,\n} from 'galaxy-nodes/presets/initiatives';\nimport 'galaxy-nodes/styles.css';\n\nconst dataset = generateGalaxyDataset(75_000);\nconst accessors = createInitiativeAccessors({ sharpMoney: true });\n\nexport function InitiativeGraph() {\n  return (\n    \u003cGalaxyGraphVisualizer\n      dataset={dataset}\n      accessors={accessors}\n      renderNodeDetail={renderInitiativeNodeDetail}\n      renderEdgeDetail={renderInitiativeEdgeDetail}\n    /\u003e\n  );\n}\n```\n\n## Dataset Shape\n\n```ts\ninterface GraphDataset\u003cNMeta = unknown, EMeta = unknown, CMeta = unknown\u003e {\n  nodes: GraphNode\u003cNMeta\u003e[];\n  edges: GraphEdge\u003cEMeta\u003e[];\n  clusters?: GraphCluster\u003cCMeta\u003e[];\n  generatedAt?: string;\n}\n\ninterface GraphDatasetPatch\u003cNMeta = unknown, EMeta = unknown, CMeta = unknown\u003e {\n  nodes?: GraphNode\u003cNMeta\u003e[];\n  edges?: GraphEdge\u003cEMeta\u003e[];\n  clusters?: GraphCluster\u003cCMeta\u003e[];\n  generatedAt?: string;\n}\n\ninterface GraphNode\u003cTMeta = unknown\u003e {\n  id: string;\n  position?: { x: number; y: number; z: number };\n  label?: string;\n  size?: number;\n  major?: boolean;\n  group?: string;\n  color?: string;\n  meta?: TMeta;\n}\n\ninterface GraphEdge\u003cTMeta = unknown\u003e {\n  id?: string;\n  source: string;\n  target: string;\n  weight?: number;\n  kind?: string;\n  color?: string;\n  meta?: TMeta;\n}\n\ninterface GraphCluster\u003cTMeta = unknown\u003e {\n  id: string;\n  label: string;\n  center?: { x: number; y: number; z: number };\n  radius?: number;\n  group?: string;\n  color?: string;\n  meta?: TMeta;\n}\n```\n\n`parseGraphDataset` validates untrusted JSON, accepts positionless nodes, rejects malformed provided coordinates, passes `meta` through untouched, defaults missing `clusters` to `[]`, and supplies `generatedAt` when absent.\n\nEdges whose `source` and `target` match cluster ids render as large-scale galaxy filaments. Edges whose endpoints match node ids render as selectable graph relationships. Nodes marked `major` become interactive planet nodes.\n\n## Develop\n\n```bash\nnpm install\nnpm run dev\n```\n\nThe dev server runs `examples/basic`, which imports the library through the package export path. Build the package and example separately:\n\n```bash\nnpm run build\nnpm run build:example\n```\n\nRun the full contribution gate locally with:\n\n```bash\nnpm run ci\n```\n\nAPI documentation is generated with TypeDoc:\n\n```bash\nnpm run docs:api\n```\n\nFocused examples are in [docs/examples.md](docs/examples.md), covering a minimal graph, custom data shape, custom theme, scene-only/no-HUD usage, and Next.js client components. GitHub Actions builds the package, example app, tests, lint, format check, and generated API docs on every PR. The Pages workflow publishes the live demo and API docs from `main`.\n\n## Memgraph Demo\n\nThis repo includes a Dockerized Memgraph demo in `demo/memgraph`.\n\n```bash\ncd demo/memgraph\ndocker compose up --build\n```\n\nThat starts Memgraph Platform, seeds a graph dataset into Memgraph, and exposes a graph API on `http://localhost:8787/graph`. Run the example with `VITE_GRAPH_API_URL=http://127.0.0.1:8787`, then use the database button in the left toolbar to load nodes and relationships from Memgraph.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanielsobrado%2Fgalaxy-nodes","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdanielsobrado%2Fgalaxy-nodes","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanielsobrado%2Fgalaxy-nodes/lists"}