fix(graph): make graph non-singleton, proper cleanup, fix radial
This commit is contained in:
parent
8d33608808
commit
23df17233d
5 changed files with 84 additions and 46 deletions
|
@ -48,7 +48,7 @@ const defaultOptions: GraphOptions = {
|
||||||
depth: -1,
|
depth: -1,
|
||||||
scale: 0.9,
|
scale: 0.9,
|
||||||
repelForce: 0.5,
|
repelForce: 0.5,
|
||||||
centerForce: 0.3,
|
centerForce: 0.2,
|
||||||
linkDistance: 30,
|
linkDistance: 30,
|
||||||
fontSize: 0.6,
|
fontSize: 0.6,
|
||||||
opacityScale: 1,
|
opacityScale: 1,
|
||||||
|
@ -67,8 +67,8 @@ export default ((opts?: Partial<GraphOptions>) => {
|
||||||
<div class={classNames(displayClass, "graph")}>
|
<div class={classNames(displayClass, "graph")}>
|
||||||
<h3>{i18n(cfg.locale).components.graph.title}</h3>
|
<h3>{i18n(cfg.locale).components.graph.title}</h3>
|
||||||
<div class="graph-outer">
|
<div class="graph-outer">
|
||||||
<div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
|
<div class="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
|
||||||
<button id="global-graph-icon" aria-label="Global Graph">
|
<button class="global-graph-icon" aria-label="Global Graph">
|
||||||
<svg
|
<svg
|
||||||
version="1.1"
|
version="1.1"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
@ -95,8 +95,8 @@ export default ((opts?: Partial<GraphOptions>) => {
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="global-graph-outer">
|
<div class="global-graph-outer">
|
||||||
<div id="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div>
|
<div class="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -68,11 +68,9 @@ type TweenNode = {
|
||||||
stop: () => void
|
stop: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderGraph(container: string, fullSlug: FullSlug) {
|
async function renderGraph(graph: HTMLElement, fullSlug: FullSlug) {
|
||||||
const slug = simplifySlug(fullSlug)
|
const slug = simplifySlug(fullSlug)
|
||||||
const visited = getVisited()
|
const visited = getVisited()
|
||||||
const graph = document.getElementById(container)
|
|
||||||
if (!graph) return
|
|
||||||
removeAllChildren(graph)
|
removeAllChildren(graph)
|
||||||
|
|
||||||
let {
|
let {
|
||||||
|
@ -167,16 +165,14 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||||
const height = Math.max(graph.offsetHeight, 250)
|
const height = Math.max(graph.offsetHeight, 250)
|
||||||
|
|
||||||
// we virtualize the simulation and use pixi to actually render it
|
// we virtualize the simulation and use pixi to actually render it
|
||||||
// Calculate the radius of the container circle
|
|
||||||
const radius = Math.min(width, height) / 2 - 40 // 40px padding
|
|
||||||
const simulation: Simulation<NodeData, LinkData> = forceSimulation<NodeData>(graphData.nodes)
|
const simulation: Simulation<NodeData, LinkData> = forceSimulation<NodeData>(graphData.nodes)
|
||||||
.force("charge", forceManyBody().strength(-100 * repelForce))
|
.force("charge", forceManyBody().strength(-100 * repelForce))
|
||||||
.force("center", forceCenter().strength(centerForce))
|
.force("center", forceCenter().strength(centerForce))
|
||||||
.force("link", forceLink(graphData.links).distance(linkDistance))
|
.force("link", forceLink(graphData.links).distance(linkDistance))
|
||||||
.force("collide", forceCollide<NodeData>((n) => nodeRadius(n)).iterations(3))
|
.force("collide", forceCollide<NodeData>((n) => nodeRadius(n)).iterations(3))
|
||||||
|
|
||||||
if (enableRadial)
|
const radius = (Math.min(width, height) / 2) * 0.8
|
||||||
simulation.force("radial", forceRadial(radius * 0.8, width / 2, height / 2).strength(0.3))
|
if (enableRadial) simulation.force("radial", forceRadial(radius).strength(0.2))
|
||||||
|
|
||||||
// precompute style prop strings as pixi doesn't support css variables
|
// precompute style prop strings as pixi doesn't support css variables
|
||||||
const cssVars = [
|
const cssVars = [
|
||||||
|
@ -524,7 +520,9 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let stopAnimation = false
|
||||||
function animate(time: number) {
|
function animate(time: number) {
|
||||||
|
if (stopAnimation) return
|
||||||
for (const n of nodeRenderData) {
|
for (const n of nodeRenderData) {
|
||||||
const { x, y } = n.simulationData
|
const { x, y } = n.simulationData
|
||||||
if (!x || !y) continue
|
if (!x || !y) continue
|
||||||
|
@ -548,61 +546,101 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||||
requestAnimationFrame(animate)
|
requestAnimationFrame(animate)
|
||||||
}
|
}
|
||||||
|
|
||||||
const graphAnimationFrameHandle = requestAnimationFrame(animate)
|
requestAnimationFrame(animate)
|
||||||
window.addCleanup(() => cancelAnimationFrame(graphAnimationFrameHandle))
|
return () => {
|
||||||
|
stopAnimation = true
|
||||||
|
app.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let localGraphCleanups: (() => void)[] = []
|
||||||
|
let globalGraphCleanups: (() => void)[] = []
|
||||||
|
|
||||||
|
function cleanupLocalGraphs() {
|
||||||
|
for (const cleanup of localGraphCleanups) {
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
localGraphCleanups = []
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupGlobalGraphs() {
|
||||||
|
for (const cleanup of globalGraphCleanups) {
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
globalGraphCleanups = []
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||||
const slug = e.detail.url
|
const slug = e.detail.url
|
||||||
addToVisited(simplifySlug(slug))
|
addToVisited(simplifySlug(slug))
|
||||||
await renderGraph("graph-container", slug)
|
|
||||||
|
|
||||||
// Function to re-render the graph when the theme changes
|
async function renderLocalGraph() {
|
||||||
const handleThemeChange = () => {
|
cleanupLocalGraphs()
|
||||||
renderGraph("graph-container", slug)
|
const localGraphContainers = document.getElementsByClassName("graph-container")
|
||||||
|
for (const container of localGraphContainers) {
|
||||||
|
localGraphCleanups.push(await renderGraph(container as HTMLElement, slug))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// event listener for theme change
|
await renderLocalGraph()
|
||||||
document.addEventListener("themechange", handleThemeChange)
|
const handleThemeChange = () => {
|
||||||
|
void renderLocalGraph()
|
||||||
|
}
|
||||||
|
|
||||||
// cleanup for the event listener
|
document.addEventListener("themechange", handleThemeChange)
|
||||||
window.addCleanup(() => {
|
window.addCleanup(() => {
|
||||||
document.removeEventListener("themechange", handleThemeChange)
|
document.removeEventListener("themechange", handleThemeChange)
|
||||||
})
|
})
|
||||||
|
|
||||||
const container = document.getElementById("global-graph-outer")
|
const containers = [...document.getElementsByClassName("global-graph-outer")] as HTMLElement[]
|
||||||
const sidebar = container?.closest(".sidebar") as HTMLElement
|
async function renderGlobalGraph() {
|
||||||
|
|
||||||
function renderGlobalGraph() {
|
|
||||||
const slug = getFullSlug(window)
|
const slug = getFullSlug(window)
|
||||||
container?.classList.add("active")
|
for (const container of containers) {
|
||||||
if (sidebar) {
|
container.classList.add("active")
|
||||||
sidebar.style.zIndex = "1"
|
const sidebar = container.closest(".sidebar") as HTMLElement
|
||||||
}
|
if (sidebar) {
|
||||||
|
sidebar.style.zIndex = "1"
|
||||||
|
}
|
||||||
|
|
||||||
renderGraph("global-graph-container", slug)
|
const graphContainer = container.querySelector(".global-graph-container") as HTMLElement
|
||||||
registerEscapeHandler(container, hideGlobalGraph)
|
registerEscapeHandler(container, hideGlobalGraph)
|
||||||
|
if (graphContainer) {
|
||||||
|
globalGraphCleanups.push(await renderGraph(graphContainer, slug))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideGlobalGraph() {
|
function hideGlobalGraph() {
|
||||||
container?.classList.remove("active")
|
cleanupGlobalGraphs()
|
||||||
if (sidebar) {
|
for (const container of containers) {
|
||||||
sidebar.style.zIndex = ""
|
container.classList.remove("active")
|
||||||
|
const sidebar = container.closest(".sidebar") as HTMLElement
|
||||||
|
if (sidebar) {
|
||||||
|
sidebar.style.zIndex = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
|
async function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
|
||||||
if (e.key === "g" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
if (e.key === "g" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const globalGraphOpen = container?.classList.contains("active")
|
const anyGlobalGraphOpen = containers.some((container) =>
|
||||||
globalGraphOpen ? hideGlobalGraph() : renderGlobalGraph()
|
container.classList.contains("active"),
|
||||||
|
)
|
||||||
|
anyGlobalGraphOpen ? hideGlobalGraph() : renderGlobalGraph()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const containerIcon = document.getElementById("global-graph-icon")
|
const containerIcons = document.getElementsByClassName("global-graph-icon")
|
||||||
containerIcon?.addEventListener("click", renderGlobalGraph)
|
Array.from(containerIcons).forEach((icon) => {
|
||||||
window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph))
|
icon.addEventListener("click", renderGlobalGraph)
|
||||||
|
window.addCleanup(() => icon.removeEventListener("click", renderGlobalGraph))
|
||||||
|
})
|
||||||
|
|
||||||
document.addEventListener("keydown", shortcutHandler)
|
document.addEventListener("keydown", shortcutHandler)
|
||||||
window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler))
|
window.addCleanup(() => {
|
||||||
|
document.removeEventListener("keydown", shortcutHandler)
|
||||||
|
cleanupLocalGraphs()
|
||||||
|
cleanupGlobalGraphs()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -384,7 +384,7 @@ async function setupSearch(searchElement: Element, currentSlug: FullSlug, data:
|
||||||
preview.replaceChildren(previewInner)
|
preview.replaceChildren(previewInner)
|
||||||
|
|
||||||
// scroll to longest
|
// scroll to longest
|
||||||
const highlights = [...preview.querySelectorAll(".highlight")].sort(
|
const highlights = [...preview.getElementsByClassName("highlight")].sort(
|
||||||
(a, b) => b.innerHTML.length - a.innerHTML.length,
|
(a, b) => b.innerHTML.length - a.innerHTML.length,
|
||||||
)
|
)
|
||||||
highlights[0]?.scrollIntoView({ block: "start" })
|
highlights[0]?.scrollIntoView({ block: "start" })
|
||||||
|
@ -488,7 +488,7 @@ async function fillDocument(data: ContentIndex) {
|
||||||
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||||
const currentSlug = e.detail.url
|
const currentSlug = e.detail.url
|
||||||
const data = await fetchData
|
const data = await fetchData
|
||||||
const searchElement = document.querySelectorAll(".search")
|
const searchElement = document.getElementsByClassName("search")
|
||||||
for (const element of searchElement) {
|
for (const element of searchElement) {
|
||||||
await setupSearch(element, currentSlug, data)
|
await setupSearch(element, currentSlug, data)
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ function toggleToc(this: HTMLElement) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupToc() {
|
function setupToc() {
|
||||||
for (const toc of document.querySelectorAll(".toc")) {
|
for (const toc of document.getElementsByClassName("toc")) {
|
||||||
const button = toc.querySelector(".toc-header")
|
const button = toc.querySelector(".toc-header")
|
||||||
const content = toc.querySelector(".toc-content")
|
const content = toc.querySelector(".toc-content")
|
||||||
if (!button || !content) return
|
if (!button || !content) return
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
& > #global-graph-icon {
|
& > .global-graph-icon {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > #global-graph-outer {
|
& > .global-graph-outer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -53,7 +53,7 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > #global-graph-container {
|
& > .global-graph-container {
|
||||||
border: 1px solid var(--lightgray);
|
border: 1px solid var(--lightgray);
|
||||||
background-color: var(--light);
|
background-color: var(--light);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|
Loading…
Add table
Reference in a new issue