Skip to content

Commit

Permalink
test: d3-force
Browse files Browse the repository at this point in the history
  • Loading branch information
junkisai committed Dec 10, 2024
1 parent b9af9ee commit 03f9ae8
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 70 deletions.
3 changes: 3 additions & 0 deletions frontend/packages/erd-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
"@radix-ui/react-toolbar": "1.1.0",
"@xyflow/react": "12.3.5",
"clsx": "2.1.1",
"d3-force": "3.0.0",
"elkjs": "0.9.3",
"nanoid": "5.0.9",
"react": "18.3.1",
"valibot": "^1.0.0-beta.5",
"valtio": "2.1.2"
Expand All @@ -18,6 +20,7 @@
"@biomejs/biome": "1.9.3",
"@liam-hq/configs": "workspace:*",
"@liam-hq/db-structure": "workspace:*",
"@types/d3-force": "3.0.10",
"@types/react": "18",
"typed-css-modules": "0.9.1",
"typescript": "5",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import styles from './ERDContent.module.css'
import { RelationshipEdge } from './RelationshipEdge'
import { TableNode } from './TableNode'
import { Toolbar } from './Toolbar'
import { useAutoLayout } from './useAutoLayout'
import { useForceLayout } from './useForceLayout'

const nodeTypes = {
table: TableNode,
Expand All @@ -38,7 +38,7 @@ export const ERDContent: FC<Props> = ({ nodes: _nodes, edges: _edges }) => {
setEdges(_edges)
}, [_nodes, _edges, setNodes, setEdges])

useAutoLayout()
useForceLayout()

const handleMouseEnterNode: NodeMouseHandler<Node> = useCallback(
(_, { id }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { type Node, useNodesInitialized, useReactFlow } from '@xyflow/react'
import {
type SimulationNodeDatum,
forceLink,
forceManyBody,
forceSimulation,
forceX,
forceY,
} from 'd3-force'
import { useEffect } from 'react'

type SimNodeType = SimulationNodeDatum & Node

export function useForceLayout() {
const nodesInitialized = useNodesInitialized()
const { getNodes, setNodes, getEdges, fitView } = useReactFlow()

useEffect(() => {
const nodes = getNodes()
const edges = getEdges()

if (!nodes.length || !nodesInitialized) {
return
}

const simulationNodes: SimNodeType[] = [...nodes].map((node) => ({
...node,
x: node.position.x,
y: node.position.y,
}))

const simulationLinks = [...edges]

const simulation = forceSimulation()
.nodes(simulationNodes)
.force('charge', forceManyBody().strength(-2500))
.force('x', forceX().x(0).strength(0.01))
.force('y', forceY().y(0).strength(0.01))
.force(
'link',
forceLink(simulationLinks)
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
.id((d: any) => d.id)
.strength(0.08)
.distance(60),
)
.on('tick', () => {
setNodes(
nodes.map((node, i) => {
const simulationNode = simulationNodes[i]

return {
...node,
position: {
x: simulationNode?.x ?? 0,
y: simulationNode?.y ?? 0,
},
style: {
opacity: 1,
},
}
}),
)
})
.on('end', () => {
setTimeout(() => fitView(), 0)
})

return () => {
simulation.stop()
}
}, [nodesInitialized, getNodes, setNodes, getEdges, fitView])
}
Original file line number Diff line number Diff line change
@@ -1,76 +1,14 @@
import type { DBStructure, Table } from '@liam-hq/db-structure'
import type { DBStructure } from '@liam-hq/db-structure'
import type { Edge, Node } from '@xyflow/react'

type Data = {
table: Table
}

type TableNodeType = Node<Data, 'table'>

const getCategory = (
score: { primary: number; foreign: number } | undefined,
): number => {
if (!score) return 4
if (score.foreign === 0 && score.primary > 0) return 1
if (score.primary > 0) return 2
if (score.foreign > 0) return 3
return 4
}

const sortNodes = (
nodes: TableNodeType[],
dbStructure: DBStructure,
): TableNodeType[] => {
const relationships = Object.values(dbStructure.relationships)
const tableScore: Record<string, { primary: number; foreign: number }> = {}

for (const node of nodes) {
tableScore[node.data.table.name] = { primary: 0, foreign: 0 }
}

for (const { primaryTableName, foreignTableName } of relationships) {
if (tableScore[primaryTableName]) {
tableScore[primaryTableName].primary++
}
if (tableScore[foreignTableName]) {
tableScore[foreignTableName].foreign++
}
}

return nodes.sort((a, b) => {
const scoreA = tableScore[a.data.table.name]
const scoreB = tableScore[b.data.table.name]

const categoryA = getCategory(scoreA)
const categoryB = getCategory(scoreB)

if (categoryA !== categoryB) {
return categoryA - categoryB
}

const primaryA = scoreA?.primary ?? 0
const primaryB = scoreB?.primary ?? 0
if (primaryA !== primaryB) {
return primaryB - primaryA
}

const foreignA = scoreA?.foreign ?? 0
const foreignB = scoreB?.foreign ?? 0
if (foreignA !== foreignB) {
return foreignA - foreignB
}

return a.data.table.name.localeCompare(b.data.table.name)
})
}
import { nanoid } from 'nanoid'

export const convertDBStructureToNodes = (
dbStructure: DBStructure,
): { nodes: Node[]; edges: Edge[] } => {
const tables = Object.values(dbStructure.tables)
const relationships = Object.values(dbStructure.relationships)

const nodes: TableNodeType[] = tables.map((table) => {
const nodes: Node[] = tables.map((table) => {
return {
id: table.name,
type: 'table',
Expand All @@ -83,10 +21,9 @@ export const convertDBStructureToNodes = (
},
}
})
const sortedNodes = sortNodes(nodes, dbStructure)

const edges = relationships.map((rel) => ({
id: rel.name,
id: rel.name !== '' ? rel.name : nanoid(),
type: 'relationship',
source: rel.primaryTableName,
target: rel.foreignTableName,
Expand All @@ -98,5 +35,5 @@ export const convertDBStructureToNodes = (
},
}))

return { nodes: sortedNodes, edges }
return { nodes, edges }
}
37 changes: 37 additions & 0 deletions frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 03f9ae8

Please sign in to comment.