Skip to content

Commit

Permalink
feat: separate dependency arrow drawing
Browse files Browse the repository at this point in the history
  • Loading branch information
scarf005 committed Feb 21, 2024
1 parent 1604718 commit c45699b
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 11 deletions.
2 changes: 1 addition & 1 deletion deno.jsonc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"exclude": ["doc/assets", "render/assets", "_site", "__snapshots__"],
"exclude": ["render/assets", "_site", "__snapshots__"],
"tasks": {
"lume": "echo \"import 'lume/cli.ts'\" | deno run --unstable -A -",
"build": "deno task lume",
Expand Down
118 changes: 115 additions & 3 deletions doc/assets/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
/// <reference lib="dom" />

import ForceGraph from "https://esm.sh/[email protected]"
import { forceCluster } from "https://esm.sh/d3-force-cluster"

// import ForceGraph2D from "https://esm.sh/[email protected]"
// import ReactDom from "https://esm.sh/react-dom@17"
Expand All @@ -18,8 +17,19 @@ const graphDom = document.querySelector("div#graph")
if (!graphDom) throw new Error("graph dom not found")

const data = await fetch("./assets/data.json").then((res) => res.json())
const imports = Object.fromEntries(
Object.entries(Object.groupBy(data.imports, (x) => x.source))
.map((
[k, v],
) => [k, v.map((x) => data.nodes.find((node) => node.id === x.target))]),
)

let hoveredNode

// console.log(imports)
// ReactDOM.render()
const ARROW_WH_RATIO = 1.6
const ARROW_VLEN_RATIO = 0.2

const graph = ForceGraph()(graphDom)
.width(graphDom.clientWidth)
Expand All @@ -29,9 +39,15 @@ const graph = ForceGraph()(graphDom)
.nodeAutoColorBy("path")
.linkAutoColorBy("color")
.nodeLabel("url")
.linkDirectionalArrowLength((link) => link.type === "import" ? 5 : 2)
.linkWidth((link) => link.type === "import" ? 3 : 0.5)
.linkDirectionalArrowLength(3)
.linkWidth(0.5)
.nodeCanvasObject((node, ctx, globalScale) => {
ctx.globalAlpha = hoveredNode
? ((hoveredNode === node || imports[hoveredNode.id]?.includes(node))
? 1
: 0.1)
: 1

const label = /**@type{string}*/ (node.name)
const fontSize = (node.type === "import" ? 20 : 16) / globalScale
ctx.font = `${fontSize}px Sans-Serif`
Expand Down Expand Up @@ -80,6 +96,97 @@ const graph = ForceGraph()(graphDom)
node.bgWidth = bgWidth
// @ts-ignore: to re-use in nodePointerAreaPaint
node.bgHeight = bgHeight

// if (hoveredNode === node) {
{
ctx.save()
ctx.globalAlpha = (hoveredNode === node) ? 1 : 0.25
ctx.lineWidth = (hoveredNode === node) ? 3 : 1
const arrowLength = (hoveredNode === node) ? 24 : 6
const arrowRelPos = 0.5
const arrowColor = node.color // "rgba(241, 21, 21, 0.521)"
const arrowHalfWidth = arrowLength / ARROW_WH_RATIO / 2

imports[node.id]?.forEach((target) => {
// draws line
ctx.beginPath()
ctx.moveTo(
// @ts-ignore: node do has x and y but force-graph marks it optional
node.x,
// @ts-ignore: node do has x and y but force-graph marks it optional
node.y,
)
ctx.lineTo(
// @ts-ignore: node do has x and y but force-graph marks it optional
target.x,
// @ts-ignore: node do has x and y but force-graph marks it optional
target.y,
)
ctx.strokeStyle = arrowColor
ctx.stroke()

// draws arrow

const start = node
const end = target

if (
!start || !end || !start.hasOwnProperty("x") ||
!end.hasOwnProperty("x")
) return // skip invalid link

// Construct bezier for curved lines
// const bzLine = link.__controlPoints &&
// new Bezier(start.x, start.y, ...link.__controlPoints, end.x, end.y)

const getCoordsAlongLine =
// bzLine
// ? (t) => bzLine.get(t) // get position along bezier line
// :
(t) => ({ // straight line: interpolate linearly
x: start.x + (end.x - start.x) * t || 0,
y: start.y + (end.y - start.y) * t || 0,
})

const lineLen =
// bzLine
// ? bzLine.length()
// :
Math.sqrt(Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2))

const posAlongLine = 1 + arrowLength +
(lineLen - 1 - 1 - arrowLength) * arrowRelPos

const arrowHead = getCoordsAlongLine(posAlongLine / lineLen)
const arrowTail = getCoordsAlongLine(
(posAlongLine - arrowLength) / lineLen,
)
const arrowTailVertex = getCoordsAlongLine(
(posAlongLine - arrowLength * (1 - ARROW_VLEN_RATIO)) / lineLen,
)

const arrowTailAngle =
Math.atan2(arrowHead.y - arrowTail.y, arrowHead.x - arrowTail.x) -
Math.PI / 2

ctx.beginPath()

ctx.moveTo(arrowHead.x, arrowHead.y)
ctx.lineTo(
arrowTail.x + arrowHalfWidth * Math.cos(arrowTailAngle),
arrowTail.y + arrowHalfWidth * Math.sin(arrowTailAngle),
)
ctx.lineTo(arrowTailVertex.x, arrowTailVertex.y)
ctx.lineTo(
arrowTail.x - arrowHalfWidth * Math.cos(arrowTailAngle),
arrowTail.y - arrowHalfWidth * Math.sin(arrowTailAngle),
)

ctx.fillStyle = arrowColor
ctx.fill()
})
ctx.restore()
}
})
.nodePointerAreaPaint((node, color, ctx) => {
ctx.fillStyle = color
Expand All @@ -96,7 +203,12 @@ const graph = ForceGraph()(graphDom)
})
// @ts-ignore: node has url but force-graph lacks generics to know it
.onNodeClick((node) => globalThis.open(node.url))
.onNodeHover((node) => {
hoveredNode = node
})
.autoPauseRedraw(false)

graph.d3Force("link")?.distance(90)
// graph.d3Force("link")?.strength(link => {
// console.log(link)
// return link.type === "import" ? 1 : 0
Expand Down
2 changes: 1 addition & 1 deletion doc/index.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const header = /*md*/ `
<div id="graph"></div>
(StackGraph 저장소의 모든 변수 관계도, 붉은 선은 의존 관계, 회색 선은 디렉터리/파일 트리)
(StackGraph 저장소의 모든 변수 관계도, 유색 선은 의존 관계, 무색 선은 디렉터리/파일 트리)
`

Expand Down
16 changes: 10 additions & 6 deletions doc/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export type Link = {
color: string
type: Type
}
type RawNode = Pick<Node, "name" | "path" | "id" | "type">
type RawNode = Pick<Node, "line" | "name" | "path" | "id" | "type">
export type Node = {
id: string
name: string
Expand All @@ -32,11 +32,11 @@ export type Node = {
path: string
dir: string
url: string
line?: string
line: string
type: Type
}

const linkNode = <const T extends { id: string; path: string; line?: string }>(
const linkNode = <const T extends { id: string; path: string; line: string }>(
node: T,
) => {
const dir = dirname(node.path)
Expand All @@ -63,7 +63,7 @@ if (import.meta.main) {

const root = import.meta.dirname + "/../"
const files = project.addSourceFilesAtPaths(
import.meta.dirname + "/../**/*.ts",
import.meta.dirname + "/../graph/**/*_test.ts",
)

const decls = Stream.fromObjectValues(files).flatMap(getAllDecls).toArray()
Expand Down Expand Up @@ -114,6 +114,7 @@ if (import.meta.main) {
id: path,
name: path,
path,
line: "",
type: "path" as const,
color: "#bdbbbb48",
textColor: "#00000080",
Expand All @@ -126,12 +127,15 @@ if (import.meta.main) {
import.meta.dirname + "/assets/data.json",
JSON.stringify(
{
links: Stream.from(links, dirLinks).toArray(),
links: dirLinks,
imports: links,
// links: Stream.from(links, dirLinks).toArray(),
nodes: [
...nodes.map(linkNode).map(colorNode),
...dirNodes.map(linkNode),
],
} satisfies { links: Link[]; nodes: Node[] },
},
// satisfies { links: Link[]; nodes: Node[] },
null,
2,
),
Expand Down

0 comments on commit c45699b

Please sign in to comment.