Skip to content

Commit

Permalink
docs: migrate to v2 api (#16)
Browse files Browse the repository at this point in the history
* docs: migrate to v2 api

* feat: add start and end line

* feat: separate dependency arrow drawing
  • Loading branch information
scarf005 authored Feb 21, 2024
1 parent 30c2095 commit c8573a2
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 37 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
125 changes: 120 additions & 5 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,10 +39,16 @@ const graph = ForceGraph()(graphDom)
.nodeAutoColorBy("path")
.linkAutoColorBy("color")
.nodeLabel("url")
.linkDirectionalArrowLength(6)
.linkWidth((link) => link.type === "import" ? 3 : 0.5)
.linkDirectionalArrowLength(3)
.linkWidth(0.5)
.nodeCanvasObject((node, ctx, globalScale) => {
const label = /**@type{string}*/ (node.id)
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`
const textWidth = ctx.measureText(label).width
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,8 +203,16 @@ 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(60)
graph.d3Force("link")?.distance(90)
// graph.d3Force("link")?.strength(link => {
// console.log(link)
// return link.type === "import" ? 1 : 0
// })
// const clusters = Object.groupBy(data.nodes, (n) => n.dir)
// console.log(clusters)
// graph.d3Force('center', )
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
76 changes: 46 additions & 30 deletions doc/main.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { relative } from "https://deno.land/[email protected]/path/relative.ts"
import { dirname } from "https://deno.land/[email protected]/path/dirname.ts"

import { Stream } from "https://deno.land/x/[email protected]/stream/mod.ts"
import { HashSet } from "https://deno.land/x/[email protected]/hashed/mod.ts"

import { Project } from "../deps/ts_morph.ts"
import { StackGraph } from "../graph/fluent.ts"

import { colors, hashRGB } from "../render/colors.ts"
import { denoProjectOption } from "../utils/project.ts"
import { ancestors } from "./ancestors.ts"
import { HashSet } from "https://deno.land/x/[email protected]/hashed/mod.ts"
import { declDepsToGraph, getAllDecls, getDeclDeps } from "../graph/mod.ts"
import { encodeVSCodeURI } from "../graph/vscode_uri.ts"

/**
* Generate dependency map of **StackGraph** itself
Expand All @@ -22,30 +23,35 @@ export type Link = {
color: string
type: Type
}
type RawNode = Pick<Node, "path" | "id" | "type">
type RawNode = Pick<Node, "line" | "name" | "path" | "id" | "type">
export type Node = {
id: string
name: string
color: string
textColor: string
path: string
dir: string
url: string
line: string
type: Type
}

const linkNode = <const T extends { id: string; path: string }>(node: T) => {
const linkNode = <const T extends { id: string; path: string; line: string }>(
node: T,
) => {
const dir = dirname(node.path)

return {
...node,
dir,
url: `https://github.com/daangn/stackgraph/tree/main/${node.path}`,
url:
`https://github.com/daangn/stackgraph/tree/main/${node.path}${node.line}`,
}
}

const colorNode = <const T extends { dir: string }>(node: T) => ({
const colorNode = <const T extends { path: string }>(node: T) => ({
...node,
...colors(hashRGB(node.dir)),
...colors(hashRGB(node.path)),
})

const distinctById = HashSet.createContext<RawNode>({
Expand All @@ -54,13 +60,17 @@ const distinctById = HashSet.createContext<RawNode>({

if (import.meta.main) {
const project = new Project(denoProjectOption)

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

const stackgraph = StackGraph.searchAll(files)
const links: Link[] = stackgraph.graph.streamConnections()
const decls = Stream.fromObjectValues(files).flatMap(getAllDecls).toArray()
const declDeps = getDeclDeps(decls)
const graph = declDepsToGraph(declDeps)

const links: Link[] = graph.streamConnections()
.map(([source, target]) => ({
source,
target,
Expand All @@ -69,30 +79,31 @@ if (import.meta.main) {
} as const))
.toArray()

const nodes: RawNode[] = Array.from(stackgraph.depsMap.keys())
.map((node) => ({
id: node.getName()!,
path: relative(root, node.getSourceFile().getFilePath()),
type: "import" as const,
}))
const nodes: RawNode[] = Array.from(declDeps.keys())
.map((node) => {
const srcfile = node.getSourceFile()
const begin = node.getStartLineNumber()
const end = node.getEndLineNumber()

return {
id: encodeVSCodeURI(node),
name: node.getName()!,
path: relative(root, srcfile.getFilePath()),
line: `#L${begin}-L${end}`,
type: "import" as const,
}
})

const dirLinks = Stream.from(nodes)
.stream()
.flatMap((node) => {
// console.log(
// Stream.from(ancestors(node.path)).append(node.id).window(
// 2,
// { skipAmount: 1 },
// ).toArray(),
// )
const newLocal = Stream.from(ancestors(node.path)).append(node.id).window(
2,
{ skipAmount: 1 },
)
const links = Stream.from(ancestors(node.path)).append(node.id)
.window(2, { skipAmount: 1 })
.map(([target, source]) =>
({ source, target, color: "#dacaca", type: "path" }) as const
)
return newLocal

return links
})
.reduce(HashSet.reducer())
.toArray()
Expand All @@ -101,7 +112,9 @@ if (import.meta.main) {
.flatMap((node) =>
ancestors(node.path).map((path) => ({
id: path,
name: path,
path,
line: "",
type: "path" as const,
color: "#bdbbbb48",
textColor: "#00000080",
Expand All @@ -114,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 c8573a2

Please sign in to comment.