import type ForceGraph from "force-graph"; import { Link, Node, NotesAndRelationsData } from "./data"; import { NodeObject } from "force-graph"; import { getColorForNode, MapType, NoteMapWidgetMode } from "./utils"; import { escapeHtml } from "../../services/utils"; export interface CssData { fontFamily: string; textColor: string; mutedTextColor: string; } interface RenderData { noteIdToSizeMap: Record; cssData: CssData; noteId: string; themeStyle: "light" | "dark"; widgetMode: NoteMapWidgetMode; notesAndRelations: NotesAndRelationsData; mapType: MapType; } export function setupRendering(graph: ForceGraph, { noteId, themeStyle, widgetMode, noteIdToSizeMap, notesAndRelations, cssData, mapType }: RenderData) { // variables for the hover effect. We have to save the neighbours of a hovered node in a set. Also we need to save the links as well as the hovered node itself const neighbours = new Set(); const highlightLinks = new Set(); let hoverNode: NodeObject | null = null; let zoomLevel: number; function paintNode(node: Node, color: string, ctx: CanvasRenderingContext2D) { const { x, y } = node; if (!x || !y) { return; } const size = noteIdToSizeMap[node.id]; ctx.fillStyle = color; ctx.beginPath(); ctx.arc(x, y, size * 0.8, 0, 2 * Math.PI, false); ctx.fill(); const toRender = zoomLevel > 2 || (zoomLevel > 1 && size > 6) || (zoomLevel > 0.3 && size > 10); if (!toRender) { return; } ctx.fillStyle = cssData.textColor; ctx.font = `${size}px ${cssData.fontFamily}`; ctx.textAlign = "center"; ctx.textBaseline = "middle"; let title = node.name; if (title.length > 15) { title = `${title.substr(0, 15)}...`; } ctx.fillText(title, x, y + Math.round(size * 1.5)); } function paintLink(link: Link, ctx: CanvasRenderingContext2D) { if (zoomLevel < 5) { return; } ctx.font = `3px ${cssData.fontFamily}`; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillStyle = cssData.mutedTextColor; const { source, target } = link; if (typeof source !== "object" || typeof target !== "object") { return; } if (source.x && source.y && target.x && target.y) { const x = (source.x + target.x) / 2; const y = (source.y + target.y) / 2; ctx.save(); ctx.translate(x, y); const deltaY = source.y - target.y; const deltaX = source.x - target.x; let angle = Math.atan2(deltaY, deltaX); let moveY = 2; if (angle < -Math.PI / 2 || angle > Math.PI / 2) { angle += Math.PI; moveY = -2; } ctx.rotate(angle); ctx.fillText(link.name, 0, moveY); } ctx.restore(); } // main code for highlighting hovered nodes and neighbours. here we "style" the nodes. the nodes are rendered several hundred times per second. graph .d3AlphaDecay(0.01) .d3VelocityDecay(0.08) .maxZoom(7) .warmupTicks(30) .nodeCanvasObject((_node, ctx) => { const node: Node = _node as Node; if (hoverNode == node) { //paint only hovered node paintNode(node, "#661822", ctx); neighbours.clear(); //clearing neighbours or the effect would be maintained after hovering is over for (const _link of notesAndRelations.links) { const link = _link as unknown as Link; //check if node is part of a link in the canvas, if so add it´s neighbours and related links to the previous defined variables to paint the nodes if (link.source.id == node.id || link.target.id == node.id) { neighbours.add(link.source); neighbours.add(link.target); highlightLinks.add(link); neighbours.delete(node); } } } else if (neighbours.has(node) && hoverNode != null) { //paint neighbours paintNode(node, "#9d6363", ctx); } else { paintNode(node, getColorForNode(node, noteId, themeStyle, widgetMode), ctx); //paint rest of nodes in canvas } }) //check if hovered and set the hovernode variable, saving the hovered node object into it. Clear links variable everytime you hover. Without clearing links will stay highlighted .onNodeHover((node) => { hoverNode = node || null; highlightLinks.clear(); }) .nodePointerAreaPaint((node, _, ctx) => paintNode(node as Node, getColorForNode(node as Node, noteId, themeStyle, widgetMode), ctx)) .nodePointerAreaPaint((node, color, ctx) => { if (!node.id) { return; } ctx.fillStyle = color; ctx.beginPath(); if (node.x && node.y) { ctx.arc(node.x, node.y, noteIdToSizeMap[node.id], 0, 2 * Math.PI, false); } ctx.fill(); }) .nodeLabel((node) => escapeHtml((node as Node).name)) .onZoom((zoom) => zoomLevel = zoom.k); // set link width to immitate a highlight effect. Checking the condition if any links are saved in the previous defined set highlightlinks graph .linkWidth((link) => (highlightLinks.has(link) ? 3 : 0.4)) .linkColor((link) => (highlightLinks.has(link) ? "white" : cssData.mutedTextColor)) .linkDirectionalArrowLength(4) .linkDirectionalArrowRelPos(0.95) // Link-specific config if (mapType) { graph .linkLabel((l) => `${escapeHtml((l as Link).source.name)} - ${escapeHtml((l as Link).name)} - ${escapeHtml((l as Link).target.name)}`) .linkCanvasObject((link, ctx) => paintLink(link as Link, ctx)) .linkCanvasObjectMode(() => "after"); } }