Skip to content

Commit

Permalink
docs: better hover and mobile support (#22)
Browse files Browse the repository at this point in the history
* feat: make graph look a bit better on hover

* refactor: remove `stripANSICode`

regression reverted in Deno 1.40.1 77b90f408c4244e8ee2e4b3bd26c441d4a250671

* refactor: extract to layout

* docs(README): use easier comparision

* docs: nav

* refactor: remove unneeded info

* fix: add mobile viewport
  • Loading branch information
scarf005 authored Feb 22, 2024
1 parent d35b2f4 commit 5480f58
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 84 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@

**StackGraph**는 기존 의존성 정적 분석 솔루션과 대비해서 어떤 장점이 있을까요?

- 파일이 아닌 **선언** (변수, 클래스) 단위로 의존성을 분석해요.
- 기존 솔루션은 파일 단위로만 의존성 분석을 했어요.
- 파일 하나에 여러 `export`가 있다면 어떤 `export`가 어디에서 사용되는지 알 수 없어요.
- **StackGraph**는 타입스크립트 LSP를 사용해 변수 단위로 의존성을 분석해요.
- `export` 수에 관계없이 개별 `export`가 어디에서 사용되는지 알 수 있어요.
- 의존성 분석과 시각화 단계가 분리되어 있어 분석 결과가 프레임워크에 종속되지 않아요.

> **StackGraph**는 아직 알파 버전으로, 모든 릴리즈에서 breaking change가 발생할 수 있어요.
Expand Down
1 change: 1 addition & 0 deletions doc/_data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const layout = "base.ts"
26 changes: 26 additions & 0 deletions doc/_includes/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export default ({ content, title }: Lume.Data, {}: Lume.Helpers) => {
// console.log("pages:", search.pages())

return /*html*/ `
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>${title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/assets/style.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/[email protected]/build/styles/github.min.css" />
</head>
<body>
<header>
<h1>
<a href="https://github.com/daangn/stackgraph">StackGraph</a>
</h1>
<nav>
<a href="/">Home</a>
</nav>
</header>
<hr />
${content}
</body>
`
}
53 changes: 38 additions & 15 deletions doc/assets/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const imports = Object.fromEntries(
) => [k, v.map((x) => data.nodes.find((node) => node.id === x.target))]),
)

/** @type {import("https://esm.sh/[email protected]").NodeObject | undefined} */
let hoveredNode

// console.log(imports)
Expand All @@ -42,13 +43,13 @@ const graph = ForceGraph()(graphDom)
.linkDirectionalArrowLength(3)
.linkWidth(0.5)
.nodeCanvasObject((node, ctx, globalScale) => {
ctx.globalAlpha = hoveredNode
? ((hoveredNode === node || imports[hoveredNode.id]?.includes(node))
? 1
: 0.1)
: 1
const state = hoveredNode
? (hoveredNode === node || imports[hoveredNode.id]?.includes(node))
? "highlighted"
: "hidden"
: "normal"

const label = /**@type{string}*/ (node.name)
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 All @@ -58,10 +59,32 @@ const graph = ForceGraph()(graphDom)

const bgWidth = scaleBg(textWidth)
const bgHeight = scaleBg(fontSize)
// @ts-ignore: to re-use in nodePointerAreaPaint
node.bgWidth = bgWidth
// @ts-ignore: to re-use in nodePointerAreaPaint
node.bgHeight = bgHeight

// @ts-ignore: node do has color but force-graph lacks generics to know it
ctx.fillStyle = node.color

ctx.globalAlpha = 1
if (state === "hidden") {
ctx.globalAlpha = 0.2
ctx.beginPath()
ctx.arc(
// @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,
5 / globalScale,
0,
2 * Math.PI,
)
ctx.fill()
ctx.closePath()
return
}

if (node.type === "import") {
ctx.fillRect(
// @ts-ignore: node do has x and y but force-graph marks it optional
Expand Down Expand Up @@ -92,17 +115,13 @@ const graph = ForceGraph()(graphDom)
// @ts-ignore: node do has x and y but force-graph marks it optional
ctx.fillText(label, node.x, node.y)

// @ts-ignore: to re-use in nodePointerAreaPaint
node.bgWidth = bgWidth
// @ts-ignore: to re-use in nodePointerAreaPaint
node.bgHeight = bgHeight

// if (hoveredNode === node) {
{
if (state === "hidden") return
ctx.save()
ctx.globalAlpha = (hoveredNode === node) ? 1 : 0.25
ctx.lineWidth = (hoveredNode === node) ? 3 : 1
const arrowLength = (hoveredNode === node) ? 24 : 6
ctx.globalAlpha = (hoveredNode === node) ? 1 : 0.5
ctx.lineWidth = ((hoveredNode === node) ? 3 : 1) / globalScale
const arrowLength = ((hoveredNode === node) ? 24 : 6) / globalScale
const arrowRelPos = 0.5
const arrowColor = node.color // "rgba(241, 21, 21, 0.521)"
const arrowHalfWidth = arrowLength / ARROW_WH_RATIO / 2
Expand Down Expand Up @@ -208,7 +227,11 @@ const graph = ForceGraph()(graphDom)
})
.autoPauseRedraw(false)

graph.d3Force("link")?.distance(90)
globalThis.addEventListener("resize", () => {
graph.width(graphDom.clientWidth).height(graphDom.clientHeight)
console.log(graphDom.clientWidth)
})

// graph.d3Force("link")?.strength(link => {
// console.log(link)
// return link.type === "import" ? 1 : 0
Expand Down
32 changes: 32 additions & 0 deletions doc/assets/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
body {
max-width: 1200px;
margin-inline: auto;
}

blockquote {
background-color: #f2e8da;
padding: 0.5em 1em;
}

pre {
border: 1px solid #e1e4e8;
}

code {
background-color: #dff4e6;
}

#graph {
height: 60vh;
background-color: #f7fafc;
}

nav a {
font-size: 1.5em;
}

h2,
h3,
ul {
margin: 0.2em 0
}
51 changes: 6 additions & 45 deletions doc/index.page.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,13 @@
export const title = "StackGraph"

const readme = () =>
Deno.readTextFile(import.meta.dirname + "/../README.md")
.then((text) => text.split("\n").slice(3).join("\n"))

const header = /*md*/ `
# [StackGraph](https://github.com/daangn/stackgraph)
<div id="graph"></div>
(StackGraph 저장소의 모든 변수 관계도, 유색 선은 의존 관계, 무색 선은 디렉터리/파일 트리)
`

export default async (_: Lume.Data, { md }: Lume.Helpers) => /*html*/ `
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>StackGraph</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/[email protected]/build/styles/github.min.css" />
<style>
body {
margin-inline: 20vw;
}
blockquote {
background-color: #f2e8da;
padding: 0.5em 1em;
}
pre {
border: 1px solid #e1e4e8;
}
code {
background-color: #dff4e6;
}
#graph {
height: 60vh;
background-color: #f7fafc;
}
h2,
h3,
ul {
margin: 0.2em 0
}
</style>
</head>
<div id="graph"></div>
<p>(StackGraph 저장소의 모든 변수 관계도, 유색 선은 의존 관계, 무색 선은 디렉터리/파일 트리)</p>
<body>
${md(header + await readme())}
<script type="module" src="./assets/demo.js"></script>
</body>
${md(await readme())}
<script type="module" src="./assets/demo.js"></script>
`
11 changes: 3 additions & 8 deletions doc/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,20 +63,15 @@ if (import.meta.main) {

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

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,
color: "#ff6e61",
type: "import",
} as const))
const links = graph.streamConnections()
.map(([source, target]) => ({ source, target } as const))
.toArray()

const nodes: RawNode[] = Array.from(declDeps.keys())
Expand Down
26 changes: 11 additions & 15 deletions graph/_format.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
// 1.40.5: https://github.com/denoland/deno/issues/22496
import { stripAnsiCode } from "https://deno.land/[email protected]/fmt/colors.ts"
import { Reducer, Stream } from "https://deno.land/x/[email protected]/stream/mod.ts"
import { encodeVSCodeURI, prettyPrintURI } from "./vscode_uri.ts"

import type { Declaration, DeclDeps } from "./decl_deps.ts"
import type { TopDeclDeps } from "./top_decl_deps.ts"

export const serializeNoColor = (x: unknown) =>
stripAnsiCode(
Deno.inspect(x, {
depth: Infinity,
colors: false,
sorted: true,
trailingComma: true,
compact: false,
iterableLimit: Infinity,
breakLength: Infinity,
escapeSequences: false,
strAbbreviateSize: Infinity,
}),
)
Deno.inspect(x, {
depth: Infinity,
colors: false,
sorted: true,
trailingComma: true,
compact: false,
iterableLimit: Infinity,
breakLength: Infinity,
escapeSequences: false,
strAbbreviateSize: Infinity,
})

export const asRecord =
<T extends string | number | symbol>(fn: (decl: Declaration) => T) =>
Expand Down

0 comments on commit 5480f58

Please sign in to comment.