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