UNPKG

d3-graph-controller

Version:

A TypeScript library for visualizing and simulating directed, interactive graphs.

1 lines 83.5 kB
{"version":3,"file":"index.mjs","names":["Centered: PositionInitializer<NodeTypeToken, GraphNode>","Randomized: PositionInitializer<NodeTypeToken, GraphNode>","PositionInitializers: {\n /**\n * Initializes node positions to a graph's center.\n */\n Centered: PositionInitializer<string, GraphNode<string>>\n /**\n * Randomly initializes node positions within the visible area.\n */\n Randomized: PositionInitializer<string, GraphNode<string>>\n /**\n * Initializes node positions based on other graph.\n */\n Stable: typeof Stable\n}","merge","zoom","foundLinks: Link[]","drag","nodeDefaults: Omit<GraphNode, 'id' | 'type'>"],"sources":["../src/config/alpha.ts","../../deepmerge/dist/index.mjs","../src/config/forces.ts","../src/config/initial.ts","../src/lib/utils.ts","../src/config/marker.ts","../src/config/position.ts","../src/config/config.ts","../src/lib/canvas.ts","../src/lib/drag.ts","../src/lib/filter.ts","../src/lib/paths.ts","../src/lib/link.ts","../src/lib/marker.ts","../src/lib/node.ts","../src/lib/simulation.ts","../src/lib/zoom.ts","../src/controller.ts","../src/model/graph.ts","../src/model/link.ts","../src/model/node.ts"],"sourcesContent":["import type { NodeTypeToken } from '../model/graph'\nimport type { GraphNode } from '../model/node'\n\n/**\n * Alpha values when label display changes.\n */\nexport interface LabelAlphas {\n /**\n * Alpha value when labels are turned off.\n */\n readonly hide: number\n /**\n * Alpha value when labels are turned on.\n */\n readonly show: number\n}\n\n/**\n * Context of a resize.\n */\nexport interface ResizeContext {\n /**\n * The old height.\n */\n readonly oldHeight: number\n /**\n * The old width.\n */\n readonly oldWidth: number\n /**\n * The new height.\n */\n readonly newHeight: number\n /**\n * The new width.\n */\n readonly newWidth: number\n}\n\n/**\n * Alpha value configuration for controlling simulation activity.\n */\nexport interface AlphaConfig<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n> {\n /**\n * Target alpha values for dragging.\n */\n readonly drag: {\n /**\n * Target alpha when a drag starts.\n * Should be larger than 0.\n */\n readonly start: number\n /**\n * Target alpha when a drag stops.\n * Should generally be 0.\n */\n readonly end: number\n }\n /**\n * Alpha values for filter changes.\n */\n readonly filter: {\n /**\n * Alpha value when the link filter changes.\n */\n readonly link: number\n /**\n * Alpha value when the node type filter changes.\n */\n readonly type: number\n /**\n * Alpha values when the inclusion of unlinked nodes changes.\n */\n readonly unlinked: {\n /**\n * Alpha value when unlinked nodes are included.\n */\n readonly include: number\n /**\n * Alpha value when unlinked nodes are excluded.\n */\n readonly exclude: number\n }\n }\n /**\n * Alpha values when node focus changes.\n */\n readonly focus: {\n /**\n * Alpha value when a node is focused.\n * @param node - The focused node.\n * @returns The alpha value.\n */\n readonly acquire: (node: Node) => number\n /**\n * Alpha value when a node is unfocused.\n * @param node - The unfocused node.\n * @returns The alpha value.\n */\n readonly release: (node: Node) => number\n }\n /**\n * Alpha value when the graph is initialized.\n */\n readonly initialize: number\n /**\n * Alpha values when label display changes.\n */\n readonly labels: {\n /**\n * Alpha values when link label display changes.\n */\n readonly links: LabelAlphas\n /**\n * Alpha values when node label display changes.\n */\n readonly nodes: LabelAlphas\n }\n /**\n * Alpha values when the graph is resized.\n */\n readonly resize: number | ((context: ResizeContext) => number)\n}\n\n/**\n * Create the default alpha configuration.\n */\nexport function createDefaultAlphaConfig<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n>(): AlphaConfig<T, Node> {\n return {\n drag: {\n end: 0,\n start: 0.1,\n },\n filter: {\n link: 1,\n type: 0.1,\n unlinked: {\n include: 0.1,\n exclude: 0.1,\n },\n },\n focus: {\n acquire: () => 0.1,\n release: () => 0.1,\n },\n initialize: 1,\n labels: {\n links: {\n hide: 0,\n show: 0,\n },\n nodes: {\n hide: 0,\n show: 0,\n },\n },\n resize: 0.5,\n }\n}\n","//#region src/index.ts\nfunction isObject(obj) {\n\tif (typeof obj === \"object\" && obj !== null) {\n\t\tif (typeof Object.getPrototypeOf === \"function\") {\n\t\t\tconst prototype = Object.getPrototypeOf(obj);\n\t\t\treturn prototype === Object.prototype || prototype === null;\n\t\t}\n\t\treturn Object.prototype.toString.call(obj) === \"[object Object]\";\n\t}\n\treturn false;\n}\nfunction merge(...objects) {\n\treturn objects.reduce((result, current) => {\n\t\tif (Array.isArray(current)) throw new TypeError(\"Arguments provided to deepmerge must be objects, not arrays.\");\n\t\tObject.keys(current).forEach((key) => {\n\t\t\tif ([\n\t\t\t\t\"__proto__\",\n\t\t\t\t\"constructor\",\n\t\t\t\t\"prototype\"\n\t\t\t].includes(key)) return;\n\t\t\tif (Array.isArray(result[key]) && Array.isArray(current[key])) result[key] = merge.options.mergeArrays ? Array.from(new Set(result[key].concat(current[key]))) : current[key];\n\t\t\telse if (isObject(result[key]) && isObject(current[key])) result[key] = merge(result[key], current[key]);\n\t\t\telse result[key] = current[key];\n\t\t});\n\t\treturn result;\n\t}, {});\n}\nconst defaultOptions = { mergeArrays: true };\nmerge.options = defaultOptions;\nmerge.withOptions = (options, ...objects) => {\n\tmerge.options = {\n\t\tmergeArrays: true,\n\t\t...options\n\t};\n\tconst result = merge(...objects);\n\tmerge.options = defaultOptions;\n\treturn result;\n};\nvar src_default = merge;\n\n//#endregion\nexport { src_default as default };\n//# sourceMappingURL=index.mjs.map","import type { NodeTypeToken } from '../model/graph'\nimport type { GraphLink } from '../model/link'\nimport type { GraphNode } from '../model/node'\n\n/**\n * Simulation force.\n */\nexport interface Force<Subject> {\n /**\n * Whether the force is enabled.\n */\n readonly enabled: boolean\n /**\n * The strength of the force.\n * Can be a static number or a function receiving the force's subject and returning a number.\n */\n readonly strength: number | ((subject: Subject) => number)\n}\n\n/**\n * Simulation force applied to nodes.\n */\nexport type NodeForce<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n> = Force<Node>\n\n/**\n * Collision force applied to nodes.\n */\nexport interface CollisionForce<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n> extends NodeForce<T, Node> {\n /**\n * Multiplier of the node radius.\n * Tip: Large values can drastically reduce link intersection.\n */\n readonly radiusMultiplier: number\n}\n\n/**\n * Simulation force applied to links.\n */\nexport interface LinkForce<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n> extends Force<Link> {\n /**\n * Define the length of a link for the simulation.\n */\n readonly length: number | ((link: Link) => number)\n}\n\n/**\n * Simulation force configuration.\n */\nexport interface SimulationForceConfig<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n> {\n /**\n * Centering force applied to nodes.\n */\n readonly centering: false | NodeForce<T, Node>\n /**\n * Charge force applied to nodes.\n */\n readonly charge: false | NodeForce<T, Node>\n /**\n * Collision force applied to nodes.\n */\n readonly collision: false | CollisionForce<T, Node>\n /**\n * Link force applied to links.\n */\n readonly link: false | LinkForce<T, Node, Link>\n}\n\n/**\n * Create the default force configuration.\n */\nexport function createDefaultForceConfig<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>(): SimulationForceConfig<T, Node, Link> {\n return {\n centering: {\n enabled: true,\n strength: 0.1,\n },\n charge: {\n enabled: true,\n strength: -1,\n },\n collision: {\n enabled: true,\n strength: 1,\n radiusMultiplier: 2,\n },\n link: {\n enabled: true,\n strength: 1,\n length: 128,\n },\n }\n}\n","import type { LinkFilter } from './filter'\nimport type { NodeTypeToken } from '../model/graph'\nimport type { GraphLink } from '../model/link'\nimport type { GraphNode } from '../model/node'\n\n/**\n * Initial settings of a controller.\n */\nexport interface InitialGraphSettings<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n> {\n /**\n * Whether nodes without incoming or outgoing links will be shown or not.\n */\n readonly includeUnlinked: boolean\n /**\n * Link filter that decides whether links should be included or not.\n */\n readonly linkFilter: LinkFilter<T, Node, Link>\n /**\n * Node types that should be included.\n * If undefined, all node types will be included.\n */\n readonly nodeTypeFilter?: T[] | undefined\n /**\n * Whether link labels are shown or not.\n */\n readonly showLinkLabels: boolean\n /**\n * Whether node labels are shown or not.\n */\n readonly showNodeLabels: boolean\n}\n\n/**\n * Create default initial settings.\n */\nexport function createDefaultInitialGraphSettings<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>(): InitialGraphSettings<T, Node, Link> {\n return {\n includeUnlinked: true,\n linkFilter: () => true,\n nodeTypeFilter: undefined,\n showLinkLabels: true,\n showNodeLabels: true,\n }\n}\n","import type { GraphConfig } from '../config/config'\nimport type { NodeTypeToken } from '../model/graph'\nimport type { GraphLink } from '../model/link'\nimport type { GraphNode } from '../model/node'\n\nexport function terminateEvent(event: Event): void {\n event.preventDefault()\n event.stopPropagation()\n}\n\nexport function isNumber(value: unknown): value is number {\n return typeof value === 'number'\n}\n\nexport function getNodeRadius<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>(config: GraphConfig<T, Node, Link>, node: Node): number {\n return isNumber(config.nodeRadius)\n ? config.nodeRadius\n : config.nodeRadius(node)\n}\n\n/**\n * Get the id of a link.\n * @param link - The link.\n */\nexport function getLinkId<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>(link: Link): string {\n return `${link.source.id}-${link.target.id}`\n}\n\n/**\n * Get the ID of a marker.\n * @param color - The color of the link.\n */\nexport function getMarkerId(color: string): string {\n return `link-arrow-${color}`.replace(/[()]/g, '~')\n}\n\n/**\n * Get the URL of a marker.\n * @param link - The link of the marker.\n */\nexport function getMarkerUrl<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>(link: Link): string {\n return `url(#${getMarkerId(link.color)})`\n}\n","import type { GraphConfig } from './config'\nimport { getNodeRadius } from '../lib/utils'\nimport type { NodeTypeToken } from '../model/graph'\nimport type { GraphLink } from '../model/link'\nimport type { GraphNode } from '../model/node'\n\n/**\n * Marker configuration.\n */\nexport interface MarkerConfig {\n /**\n * Size of the marker's box.\n */\n readonly size: number\n /**\n * Get padding of the marker for calculating link paths.\n * @param node - The node the marker is pointing at.\n * @param config - The current config.\n * @returns The padding of the marker.\n */\n readonly padding: <\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n >(\n node: Node,\n config: GraphConfig<T, Node, Link>,\n ) => number\n /**\n * The ref of the marker.\n */\n readonly ref: [number, number]\n /**\n * The path of the marker.\n */\n readonly path: [number, number][]\n /**\n * The ViewBox of the marker.\n */\n readonly viewBox: string\n}\n\nfunction defaultMarkerConfig(size: number): MarkerConfig {\n return {\n size,\n padding: <\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n >(\n node: Node,\n config: GraphConfig<T, Node, Link>,\n ) => getNodeRadius(config, node) + 2 * size,\n ref: [size / 2, size / 2],\n path: [\n [0, 0],\n [0, size],\n [size, size / 2],\n ] as [number, number][],\n viewBox: [0, 0, size, size].join(','),\n }\n}\n\n/**\n * Collection of built-in markers.\n */\nexport const Markers = {\n /**\n * Create an arrow marker configuration.\n * @param size - The size of the arrow\n */\n Arrow: (size: number): MarkerConfig => defaultMarkerConfig(size),\n}\n","import type { Graph, NodeTypeToken } from '../model/graph'\nimport type { GraphNode } from '../model/node'\n\n/**\n * Initializes a node's position in context of a graph's width and height.\n */\nexport type PositionInitializer<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n> = (node: Node, width: number, height: number) => [number, number]\n\nconst Centered: PositionInitializer<NodeTypeToken, GraphNode> = (\n _,\n width,\n height,\n) => [width / 2, height / 2]\n\nconst Randomized: PositionInitializer<NodeTypeToken, GraphNode> = (\n _,\n width,\n height,\n) => [randomInRange(0, width), randomInRange(0, height)]\n\nfunction randomInRange(min: number, max: number): number {\n return Math.random() * (max - min) + min\n}\n\nfunction Stable<T extends NodeTypeToken, Node extends GraphNode<T>>(\n previousGraph: Graph<T, Node>,\n): PositionInitializer<T, Node> {\n const positions = Object.fromEntries(\n previousGraph.nodes.map((node) => [node.id, [node.x, node.y]] as const),\n )\n return (node, width, height) => {\n const [x, y] = positions[node.id] ?? []\n if (!x || !y) {\n return Randomized(node, width, height)\n }\n return [x, y]\n }\n}\n\n/**\n * Collection of built-in position initializers.\n */\nexport const PositionInitializers: {\n /**\n * Initializes node positions to a graph's center.\n */\n Centered: PositionInitializer<string, GraphNode<string>>\n /**\n * Randomly initializes node positions within the visible area.\n */\n Randomized: PositionInitializer<string, GraphNode<string>>\n /**\n * Initializes node positions based on other graph.\n */\n Stable: typeof Stable\n} = {\n Centered,\n Randomized,\n Stable,\n}\n","import merge from '@yeger/deepmerge'\n\nimport { createDefaultAlphaConfig } from './alpha'\nimport type { Callbacks } from './callbacks'\nimport { createDefaultForceConfig } from './forces'\nimport type { InitialGraphSettings } from './initial'\nimport { createDefaultInitialGraphSettings } from './initial'\nimport type { MarkerConfig } from './marker'\nimport { Markers } from './marker'\nimport type { Modifiers } from './modifiers'\nimport type { PositionInitializer } from './position'\nimport { PositionInitializers } from './position'\nimport type { SimulationConfig } from './simulation'\nimport type { ZoomConfig } from './zoom'\nimport type { NodeTypeToken } from '../model/graph'\nimport type { GraphLink } from '../model/link'\nimport type { GraphNode } from '../model/node'\n\nexport interface GraphConfig<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n> {\n /**\n * Set to true to enable automatic resizing.\n * Warning: Do call shutdown(), once the controller is no longer required.\n */\n readonly autoResize: boolean\n /**\n * Callback configuration.\n */\n readonly callbacks: Callbacks<T, Node>\n readonly hooks: {\n afterZoom?: (scale: number, xOffset: number, yOffset: number) => void\n }\n /**\n * Initial settings of a controller.\n */\n readonly initial: InitialGraphSettings<T, Node, Link>\n /**\n * Marker configuration.\n */\n readonly marker: MarkerConfig\n /**\n * Low-level callbacks for modifying the underlying d3-selection.\n */\n readonly modifiers: Modifiers<T, Node, Link>\n /**\n * Define the radius of a node for the simulation and visualization.\n * Can be a static number or a function receiving a node as its parameter.\n */\n readonly nodeRadius: number | ((node: Node) => number)\n /**\n * Initializes a node's position in context of a graph's width and height.\n */\n readonly positionInitializer: PositionInitializer<T, Node>\n /**\n * Simulation configuration.\n */\n readonly simulation: SimulationConfig<T, Node, Link>\n /**\n * Zoom configuration.\n */\n readonly zoom: ZoomConfig\n}\n\nfunction defaultGraphConfig<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>(): GraphConfig<T, Node, Link> {\n return {\n autoResize: false,\n callbacks: {},\n hooks: {},\n initial: createDefaultInitialGraphSettings(),\n nodeRadius: 16,\n marker: Markers.Arrow(4),\n modifiers: {},\n positionInitializer: PositionInitializers.Centered,\n simulation: {\n alphas: createDefaultAlphaConfig(),\n forces: createDefaultForceConfig(),\n },\n zoom: {\n initial: 1,\n min: 0.1,\n max: 2,\n },\n }\n}\n\n/**\n * Utility type for deeply partial objects.\n */\nexport type DeepPartial<T> = {\n readonly [P in keyof T]?: DeepPartial<T[P]>\n}\n\n/**\n * Define the configuration of a controller.\n * Will be merged with the default configuration.\n * @param config - The partial configuration.\n * @returns The merged configuration.\n */\nexport function defineGraphConfig<\n T extends NodeTypeToken = NodeTypeToken,\n Node extends GraphNode<T> = GraphNode<T>,\n Link extends GraphLink<T, Node> = GraphLink<T, Node>,\n>(\n config: DeepPartial<GraphConfig<T, Node, Link>> = {},\n): GraphConfig<T, Node, Link> {\n return merge.withOptions(\n { mergeArrays: false },\n defaultGraphConfig<T, Node, Link>(),\n config,\n ) as GraphConfig<T, Node, Link>\n}\n","import { zoomIdentity } from 'd3-zoom'\n\nimport type { Canvas, GraphHost, Zoom } from './types'\nimport { terminateEvent } from './utils'\n\nexport interface DefineCanvasParams {\n readonly applyZoom: boolean\n readonly container: GraphHost\n readonly offset: [number, number]\n readonly onDoubleClick?: (event: PointerEvent) => void\n readonly onPointerMoved?: (event: PointerEvent) => void\n readonly onPointerUp?: (event: PointerEvent) => void\n readonly scale: number\n readonly zoom: Zoom\n}\n\nexport function defineCanvas({\n applyZoom,\n container,\n onDoubleClick,\n onPointerMoved,\n onPointerUp,\n offset: [xOffset, yOffset],\n scale,\n zoom,\n}: DefineCanvasParams): Canvas {\n const svg = container\n .classed('graph', true)\n .append('svg')\n .attr('height', '100%')\n .attr('width', '100%')\n .call(zoom)\n .on('contextmenu', (event: MouseEvent) => terminateEvent(event))\n .on('dblclick', (event: PointerEvent) => onDoubleClick?.(event))\n .on('dblclick.zoom', null)\n .on('pointermove', (event: PointerEvent) => onPointerMoved?.(event))\n .on('pointerup', (event: PointerEvent) => onPointerUp?.(event))\n .style('cursor', 'grab')\n\n if (applyZoom) {\n svg.call(\n zoom.transform,\n zoomIdentity.translate(xOffset, yOffset).scale(scale),\n )\n }\n\n return svg.append('g')\n}\n\nexport interface UpdateCanvasParams {\n readonly canvas?: Canvas | undefined\n readonly scale: number\n readonly xOffset: number\n readonly yOffset: number\n}\n\nexport function updateCanvasTransform({\n canvas,\n scale,\n xOffset,\n yOffset,\n}: UpdateCanvasParams): void {\n canvas?.attr('transform', `translate(${xOffset},${yOffset})scale(${scale})`)\n}\n","import { drag } from 'd3-drag'\nimport { select } from 'd3-selection'\n\nimport type { GraphConfig } from '../config/config'\nimport type { Drag, NodeDragEvent } from './types'\nimport type { NodeTypeToken } from '../model/graph'\nimport type { GraphLink } from '../model/link'\nimport type { GraphNode } from '../model/node'\n\nexport interface DefineDragParams<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n> {\n readonly config: GraphConfig<T, Node, Link>\n readonly onDragStart: (event: NodeDragEvent<T, Node>, d: Node) => void\n readonly onDragEnd: (event: NodeDragEvent<T, Node>, d: Node) => void\n}\n\nexport function defineDrag<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>({\n config,\n onDragStart,\n onDragEnd,\n}: DefineDragParams<T, Node, Link>): Drag<T, Node> {\n const drg = drag<SVGGElement, Node, Node>()\n .filter((event: MouseEvent | TouchEvent) => {\n if (event.type === 'mousedown') {\n return (event as MouseEvent).button === 0 // primary (left) mouse button\n } else if (event.type === 'touchstart') {\n return (event as TouchEvent).touches.length === 1\n }\n return false\n })\n .on('start', (event: NodeDragEvent<T, Node>, d) => {\n if (event.active === 0) {\n onDragStart(event, d)\n }\n select(event.sourceEvent.target).classed('grabbed', true)\n d.fx = d.x\n d.fy = d.y\n })\n .on('drag', (event: NodeDragEvent<T, Node>, d) => {\n d.fx = event.x\n d.fy = event.y\n })\n .on('end', (event: NodeDragEvent<T, Node>, d) => {\n if (event.active === 0) {\n onDragEnd(event, d)\n }\n select(event.sourceEvent.target).classed('grabbed', false)\n d.fx = undefined\n d.fy = undefined\n })\n\n config.modifiers.drag?.(drg)\n\n return drg\n}\n","import type { LinkFilter } from '../config/filter'\nimport type { Graph, NodeTypeToken } from '../model/graph'\nimport type { GraphLink } from '../model/link'\nimport type { GraphNode } from '../model/node'\n\nexport interface GraphFilterParams<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n> {\n readonly graph: Graph<T, Node, Link>\n readonly filter: T[]\n readonly focusedNode?: Node | undefined\n readonly includeUnlinked: boolean\n readonly linkFilter: LinkFilter<T, Node, Link>\n}\n\nexport function filterGraph<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>({\n graph,\n filter,\n focusedNode,\n includeUnlinked,\n linkFilter,\n}: GraphFilterParams<T, Node, Link>): Graph<T, Node, Link> {\n const links = graph.links.filter(\n (d) =>\n filter.includes(d.source.type) &&\n filter.includes(d.target.type) &&\n linkFilter(d),\n )\n\n const isLinked = (node: Node) =>\n links.find(\n (link) => link.source.id === node.id || link.target.id === node.id,\n ) !== undefined\n const nodes = graph.nodes.filter(\n (d) => filter.includes(d.type) && (includeUnlinked || isLinked(d)),\n )\n\n if (focusedNode === undefined || !filter.includes(focusedNode.type)) {\n return {\n nodes,\n links,\n }\n }\n\n return getFocusedSubgraph({ nodes, links }, focusedNode)\n}\n\nfunction getFocusedSubgraph<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>(graph: Graph<T, Node, Link>, source: Node): Graph<T, Node, Link> {\n const links = [\n ...getIncomingLinksTransitively(graph, source),\n ...getOutgoingLinksTransitively(graph, source),\n ]\n\n const nodes = links.flatMap((link) => [link.source, link.target])\n\n return {\n nodes: [...new Set([...nodes, source])],\n links: [...new Set(links)],\n }\n}\n\nfunction getIncomingLinksTransitively<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>(graph: Graph<T, Node, Link>, source: Node): Link[] {\n return getLinksInDirectionTransitively(\n graph,\n source,\n (link, node) => link.target.id === node.id,\n )\n}\n\nfunction getOutgoingLinksTransitively<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>(graph: Graph<T, Node, Link>, source: Node): Link[] {\n return getLinksInDirectionTransitively(\n graph,\n source,\n (link, node) => link.source.id === node.id,\n )\n}\n\nfunction getLinksInDirectionTransitively<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>(\n graph: Graph<T, Node, Link>,\n source: Node,\n directionPredicate: (link: Link, node: Node) => boolean,\n): Link[] {\n const remainingLinks = new Set(graph.links)\n const foundNodes = new Set([source])\n const foundLinks: Link[] = []\n\n while (remainingLinks.size > 0) {\n const newLinks = [...remainingLinks].filter((link) =>\n [...foundNodes].some((node) => directionPredicate(link, node)),\n )\n\n if (newLinks.length === 0) {\n return foundLinks\n }\n\n newLinks.forEach((link) => {\n foundNodes.add(link.source)\n foundNodes.add(link.target)\n foundLinks.push(link)\n remainingLinks.delete(link)\n })\n }\n\n return foundLinks\n}\n","import { Vector } from 'vecti'\n\nimport type { GraphConfig } from '../config/config'\nimport { getNodeRadius } from './utils'\nimport type { NodeTypeToken } from '../model/graph'\nimport type { GraphLink } from '../model/link'\nimport type { GraphNode } from '../model/node'\n\n// ##################################################\n// COMMON\n// ##################################################\n\nexport interface PathParams<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n> {\n readonly config: GraphConfig<T, Node, Link>\n readonly source: Node\n readonly target: Node\n}\n\nexport interface ReflexivePathParams<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n> {\n readonly config: GraphConfig<T, Node, Link>\n readonly node: Node\n readonly center: Vector\n}\n\nfunction getX<T extends NodeTypeToken, Node extends GraphNode<T>>(\n node: Node,\n): number {\n return node.x ?? 0\n}\n\nfunction getY<T extends NodeTypeToken, Node extends GraphNode<T>>(\n node: Node,\n): number {\n return node.y ?? 0\n}\n\ninterface VectorData {\n readonly s: Vector\n readonly t: Vector\n readonly dist: number\n readonly norm: Vector\n readonly endNorm: Vector\n}\n\nfunction calculateVectorData<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>({ source, target }: PathParams<T, Node, Link>): VectorData {\n const s = new Vector(getX(source), getY(source))\n const t = new Vector(getX(target), getY(target))\n const diff = t.subtract(s)\n const dist = diff.length()\n const norm = diff.normalize()\n const endNorm = norm.multiply(-1)\n return {\n s,\n t,\n dist,\n norm,\n endNorm,\n }\n}\n\nfunction calculateCenter<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>({ center, node }: ReflexivePathParams<T, Node, Link>) {\n const n = new Vector(getX(node), getY(node))\n let c = center\n if (n.x === c.x && n.y === c.y) {\n // Nodes at the exact center of the Graph should have their reflexive edge above them.\n c = c.add(new Vector(0, 1))\n }\n return {\n n,\n c,\n }\n}\n\nfunction calculateSourceAndTarget<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>({ config, source, target }: PathParams<T, Node, Link>) {\n const { s, t, norm } = calculateVectorData({ config, source, target })\n const start = s.add(norm.multiply(getNodeRadius(config, source) - 1))\n const end = t.subtract(norm.multiply(config.marker.padding(target, config)))\n return {\n start,\n end,\n }\n}\n\n// ##################################################\n// LINE\n// ##################################################\n\nfunction paddedLinePath<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>(params: PathParams<T, Node, Link>): string {\n const { start, end } = calculateSourceAndTarget(params)\n return `M${start.x},${start.y}\n L${end.x},${end.y}`\n}\n\nfunction lineLinkTextTransform<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>(params: PathParams<T, Node, Link>): string {\n const { start, end } = calculateSourceAndTarget(params)\n\n const midpoint = end.subtract(start).multiply(0.5)\n const result = start.add(midpoint)\n\n return `translate(${result.x - 8},${result.y - 4})`\n}\n\n// ##################################################\n// ARC\n// ##################################################\n\nfunction paddedArcPath<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>({ config, source, target }: PathParams<T, Node, Link>): string {\n const { s, t, dist, norm, endNorm } = calculateVectorData({\n config,\n source,\n target,\n })\n const rotation = 10\n const start = norm\n .rotateByDegrees(-rotation)\n .multiply(getNodeRadius(config, source) - 1)\n .add(s)\n const end = endNorm\n .rotateByDegrees(rotation)\n .multiply(getNodeRadius(config, target))\n .add(t)\n .add(endNorm.rotateByDegrees(rotation).multiply(2 * config.marker.size))\n const arcRadius = 1.2 * dist\n return `M${start.x},${start.y}\n A${arcRadius},${arcRadius},0,0,1,${end.x},${end.y}`\n}\n\n// ##################################################\n// REFLEXIVE\n// ##################################################\n\nfunction paddedReflexivePath<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>({ center, config, node }: ReflexivePathParams<T, Node, Link>): string {\n const { n, c } = calculateCenter({ center, config, node })\n const radius = getNodeRadius(config, node)\n const diff = n.subtract(c)\n const norm = diff.multiply(1 / diff.length())\n const rotation = 40\n const start = norm\n .rotateByDegrees(rotation)\n .multiply(radius - 1)\n .add(n)\n const end = norm\n .rotateByDegrees(-rotation)\n .multiply(radius)\n .add(n)\n .add(norm.rotateByDegrees(-rotation).multiply(2 * config.marker.size))\n return `M${start.x},${start.y}\n A${radius},${radius},0,1,0,${end.x},${end.y}`\n}\n\nfunction bidirectionalLinkTextTransform<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>({ config, source, target }: PathParams<T, Node, Link>): string {\n const { t, dist, endNorm } = calculateVectorData({ config, source, target })\n const rotation = 10\n const end = endNorm\n .rotateByDegrees(rotation)\n .multiply(0.5 * dist)\n .add(t)\n return `translate(${end.x},${end.y})`\n}\n\nfunction reflexiveLinkTextTransform<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>({ center, config, node }: ReflexivePathParams<T, Node, Link>): string {\n const { n, c } = calculateCenter({ center, config, node })\n const diff = n.subtract(c)\n const offset = diff\n .multiply(1 / diff.length())\n .multiply(3 * getNodeRadius(config, node) + 8)\n .add(n)\n return `translate(${offset.x},${offset.y})`\n}\n\n// ##################################################\n// EXPORT\n// ##################################################\n\nexport const Paths = {\n line: {\n labelTransform: lineLinkTextTransform satisfies typeof lineLinkTextTransform as typeof lineLinkTextTransform,\n path: paddedLinePath satisfies typeof paddedLinePath as typeof paddedLinePath,\n },\n arc: {\n labelTransform: bidirectionalLinkTextTransform satisfies typeof bidirectionalLinkTextTransform as typeof bidirectionalLinkTextTransform,\n path: paddedArcPath satisfies typeof paddedArcPath as typeof paddedArcPath,\n },\n reflexive: {\n labelTransform: reflexiveLinkTextTransform satisfies typeof reflexiveLinkTextTransform as typeof reflexiveLinkTextTransform,\n path: paddedReflexivePath satisfies typeof paddedReflexivePath as typeof paddedReflexivePath,\n },\n}\n","import type { Vector } from 'vecti'\n\nimport type { GraphConfig } from '../config/config'\nimport { Paths } from './paths'\nimport type { Canvas, LinkSelection } from './types'\nimport { getLinkId, getMarkerUrl } from './utils'\nimport type { Graph, NodeTypeToken } from '../model/graph'\nimport type { GraphLink } from '../model/link'\nimport type { GraphNode } from '../model/node'\n\nexport function defineLinkSelection<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>(canvas: Canvas): LinkSelection<T, Node, Link> {\n return canvas.append('g').classed('links', true).selectAll('path')\n}\n\nexport interface CreateLinksParams<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n> {\n readonly config: GraphConfig<T, Node, Link>\n readonly graph: Graph<T, Node, Link>\n readonly selection?: LinkSelection<T, Node, Link> | undefined\n readonly showLabels: boolean\n}\n\nexport function createLinks<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>({\n config,\n graph,\n selection,\n showLabels,\n}: CreateLinksParams<T, Node, Link>): LinkSelection<T, Node, Link> | undefined {\n const result = selection\n ?.data(graph.links, (d) => getLinkId(d))\n .join((enter) => {\n const linkGroup = enter.append('g')\n\n const linkPath = linkGroup\n .append('path')\n .classed('link', true)\n .style('marker-end', (d) => getMarkerUrl(d))\n .style('stroke', (d) => d.color)\n\n config.modifiers.link?.(linkPath)\n\n const linkLabel = linkGroup\n .append('text')\n .classed('link__label', true)\n .style('fill', (d) => (d.label ? d.label.color : null))\n .style('font-size', (d) => (d.label ? d.label.fontSize : null))\n .text((d) => (d.label ? d.label.text : null))\n\n config.modifiers.linkLabel?.(linkLabel)\n\n return linkGroup\n })\n\n result\n ?.select('.link__label')\n .attr('opacity', (d) => (d.label && showLabels ? 1 : 0))\n\n return result\n}\n\nexport interface UpdateLinksParams<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n> {\n readonly center: Vector\n readonly config: GraphConfig<T, Node, Link>\n readonly graph: Graph<T, Node, Link>\n readonly selection: LinkSelection<T, Node, Link> | undefined\n}\n\nexport function updateLinks<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>(params: UpdateLinksParams<T, Node, Link>): void {\n updateLinkPaths(params)\n updateLinkLabels(params)\n}\n\nfunction updateLinkPaths<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>({\n center,\n config,\n graph,\n selection,\n}: UpdateLinksParams<T, Node, Link>): void {\n selection?.selectAll<SVGPathElement, Link>('path').attr('d', (d) => {\n if (\n d.source.x === undefined ||\n d.source.y === undefined ||\n d.target.x === undefined ||\n d.target.y === undefined\n ) {\n return ''\n }\n if (d.source.id === d.target.id) {\n return Paths.reflexive.path({\n config,\n node: d.source,\n center,\n })\n } else if (areBidirectionallyConnected(graph, d.source, d.target)) {\n return Paths.arc.path({ config, source: d.source, target: d.target })\n } else {\n return Paths.line.path({ config, source: d.source, target: d.target })\n }\n })\n}\n\nfunction updateLinkLabels<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>({\n config,\n center,\n graph,\n selection,\n}: UpdateLinksParams<T, Node, Link>): void {\n selection?.select('.link__label').attr('transform', (d) => {\n if (\n d.source.x === undefined ||\n d.source.y === undefined ||\n d.target.x === undefined ||\n d.target.y === undefined\n ) {\n return 'translate(0, 0)'\n }\n if (d.source.id === d.target.id) {\n return Paths.reflexive.labelTransform({\n config,\n node: d.source,\n center,\n })\n } else if (areBidirectionallyConnected(graph, d.source, d.target)) {\n return Paths.arc.labelTransform({\n config,\n source: d.source,\n target: d.target,\n })\n } else {\n return Paths.line.labelTransform({\n config,\n source: d.source,\n target: d.target,\n })\n }\n })\n}\n\nfunction areBidirectionallyConnected<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>(graph: Graph<T, Node, Link>, source: Node, target: Node): boolean {\n return (\n source.id !== target.id &&\n graph.links.some(\n (l) => l.target.id === source.id && l.source.id === target.id,\n ) &&\n graph.links.some(\n (l) => l.target.id === target.id && l.source.id === source.id,\n )\n )\n}\n","import type { GraphConfig } from '../config/config'\nimport type { Canvas, MarkerSelection } from './types'\nimport { getMarkerId } from './utils'\nimport type { Graph, NodeTypeToken } from '../model/graph'\nimport type { GraphLink } from '../model/link'\nimport type { GraphNode } from '../model/node'\n\nexport function defineMarkerSelection(canvas: Canvas): MarkerSelection {\n return canvas.append('defs').selectAll('marker')\n}\n\nexport interface CreateMarkerParams<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n> {\n readonly config: GraphConfig<T, Node, Link>\n readonly graph: Graph<T, Node, Link>\n readonly selection?: MarkerSelection | undefined\n}\n\nexport function createMarkers<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>({\n config,\n graph,\n selection,\n}: CreateMarkerParams<T, Node, Link>): MarkerSelection | undefined {\n return selection\n ?.data(getUniqueColors(graph), (d) => d)\n .join((enter) => {\n const marker = enter\n .append('marker')\n .attr('id', (d) => getMarkerId(d))\n .attr('markerHeight', 4 * config.marker.size)\n .attr('markerWidth', 4 * config.marker.size)\n .attr('markerUnits', 'userSpaceOnUse')\n .attr('orient', 'auto')\n .attr('refX', config.marker.ref[0])\n .attr('refY', config.marker.ref[1])\n .attr('viewBox', config.marker.viewBox)\n .style('fill', (d) => d)\n marker.append('path').attr('d', makeLine(config.marker.path))\n return marker\n })\n}\n\nfunction getUniqueColors<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>(graph: Graph<T, Node, Link>): string[] {\n return [...new Set(graph.links.map((link) => link.color))]\n}\n\nfunction makeLine(points: [number, number][]): string {\n const [start, ...rest] = points\n if (!start) {\n return 'M0,0'\n }\n const [startX, startY] = start\n return rest.reduce(\n (line, [x, y]) => `${line}L${x},${y}`,\n `M${startX},${startY}`,\n )\n}\n","import type { GraphConfig } from '../config/config'\nimport type { Canvas, Drag, NodeSelection } from './types'\nimport { getNodeRadius, terminateEvent } from './utils'\nimport type { Graph, NodeTypeToken } from '../model/graph'\nimport type { GraphLink } from '../model/link'\nimport type { GraphNode } from '../model/node'\n\nexport function defineNodeSelection<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n>(canvas: Canvas): NodeSelection<T, Node> {\n return canvas.append('g').classed('nodes', true).selectAll('circle')\n}\n\nexport interface CreateNodesParams<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n> {\n readonly config: GraphConfig<T, Node, Link>\n readonly drag?: Drag<T, Node> | undefined\n readonly graph: Graph<T, Node, Link>\n readonly onNodeSelected: ((node: Node) => void) | undefined\n readonly onNodeContext: (node: Node) => void\n readonly selection?: NodeSelection<T, Node> | undefined\n readonly showLabels: boolean\n}\n\nexport function createNodes<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>({\n config,\n drag,\n graph,\n onNodeContext,\n onNodeSelected,\n selection,\n showLabels,\n}: CreateNodesParams<T, Node, Link>): NodeSelection<T, Node> | undefined {\n const result = selection\n ?.data(graph.nodes, (d) => d.id)\n .join((enter) => {\n const nodeGroup = enter.append('g')\n\n if (drag !== undefined) {\n nodeGroup.call(drag)\n }\n\n const nodeCircle = nodeGroup\n .append('circle')\n .classed('node', true)\n .attr('r', (d) => getNodeRadius(config, d))\n .on('contextmenu', (event, d) => {\n terminateEvent(event)\n onNodeContext(d)\n })\n .on('pointerdown', (event: PointerEvent, d) =>\n onPointerDown(event, d, onNodeSelected ?? onNodeContext))\n .style('fill', (d) => d.color)\n\n config.modifiers.node?.(nodeCircle)\n\n const nodeLabel = nodeGroup\n .append('text')\n .classed('node__label', true)\n .attr('dy', `0.33em`)\n .style('fill', (d) => (d.label ? d.label.color : null))\n .style('font-size', (d) => (d.label ? d.label.fontSize : null))\n .style('stroke', 'none')\n .text((d) => (d.label ? d.label.text : null))\n\n config.modifiers.nodeLabel?.(nodeLabel)\n\n return nodeGroup\n })\n\n result?.select('.node').classed('focused', (d) => d.isFocused)\n result?.select('.node__label').attr('opacity', showLabels ? 1 : 0)\n\n return result\n}\n\nconst DOUBLE_CLICK_INTERVAL_MS = 500\n\nfunction onPointerDown<T extends NodeTypeToken, Node extends GraphNode<T>>(\n event: PointerEvent,\n node: Node,\n onNodeSelected: (node: Node) => void,\n): void {\n if (event.button !== undefined && event.button !== 0) {\n return\n }\n\n const lastInteractionTimestamp = node.lastInteractionTimestamp\n const now = Date.now()\n if (\n lastInteractionTimestamp === undefined ||\n now - lastInteractionTimestamp > DOUBLE_CLICK_INTERVAL_MS\n ) {\n node.lastInteractionTimestamp = now\n return\n }\n node.lastInteractionTimestamp = undefined\n onNodeSelected(node)\n}\n\nexport function updateNodes<T extends NodeTypeToken, Node extends GraphNode<T>>(\n selection?: NodeSelection<T, Node>,\n): void {\n selection?.attr('transform', (d) => `translate(${d.x ?? 0},${d.y ?? 0})`)\n}\n","import {\n forceCollide,\n forceLink,\n forceManyBody,\n forceSimulation,\n forceX,\n forceY,\n} from 'd3-force'\nimport type { Vector } from 'vecti'\n\nimport type { GraphConfig } from '../config/config'\nimport type { GraphSimulation } from './types'\nimport { getNodeRadius } from './utils'\nimport type { Graph, NodeTypeToken } from '../model/graph'\nimport type { GraphLink } from '../model/link'\nimport type { GraphNode } from '../model/node'\n\nexport interface DefineSimulationParams<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n> {\n readonly center: () => Vector\n readonly config: GraphConfig<T, Node, Link>\n readonly graph: Graph<T, Node, Link>\n readonly onTick: () => void\n}\n\nexport function defineSimulation<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>({\n center,\n config,\n graph,\n onTick,\n}: DefineSimulationParams<T, Node, Link>): GraphSimulation<T, Node, Link> {\n const simulation = forceSimulation<Node, Link>(graph.nodes)\n\n const centeringForce = config.simulation.forces.centering\n if (centeringForce && centeringForce.enabled) {\n const strength = centeringForce.strength\n simulation\n .force('x', forceX<Node>(() => center().x).strength(strength))\n .force('y', forceY<Node>(() => center().y).strength(strength))\n }\n\n const chargeForce = config.simulation.forces.charge\n if (chargeForce && chargeForce.enabled) {\n simulation.force(\n 'charge',\n forceManyBody<Node>().strength(chargeForce.strength),\n )\n }\n\n const collisionForce = config.simulation.forces.collision\n if (collisionForce && collisionForce.enabled) {\n simulation.force(\n 'collision',\n forceCollide<Node>().radius(\n (d) => collisionForce.radiusMultiplier * getNodeRadius(config, d),\n ),\n )\n }\n\n const linkForce = config.simulation.forces.link\n if (linkForce && linkForce.enabled) {\n simulation.force(\n 'link',\n forceLink<Node, Link>(graph.links)\n .id((d) => d.id)\n .distance(config.simulation.forces.link.length)\n .strength(linkForce.strength),\n )\n }\n\n simulation.on('tick', () => onTick())\n\n config.modifiers.simulation?.(simulation)\n\n return simulation\n}\n","import type { Selection } from 'd3-selection'\nimport type { D3ZoomEvent } from 'd3-zoom'\nimport { zoom } from 'd3-zoom'\n\nimport type { GraphConfig } from '../config/config'\nimport type { Zoom } from './types'\nimport type { NodeTypeToken } from '../model/graph'\nimport type { GraphLink } from '../model/link'\nimport type { GraphNode } from '../model/node'\n\nexport interface DefineZoomParams<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n> {\n readonly canvasContainer: () => Selection<\n SVGSVGElement,\n unknown,\n null,\n undefined\n >\n readonly config: GraphConfig<T, Node, Link>\n readonly min: number\n readonly max: number\n readonly onZoom: (event: D3ZoomEvent<SVGSVGElement, undefined>) => void\n}\n\nexport function defineZoom<\n T extends NodeTypeToken,\n Node extends GraphNode<T>,\n Link extends GraphLink<T, Node>,\n>({\n canvasContainer,\n config,\n min,\n max,\n onZoom,\n}: DefineZoomParams<T, Node, Link>): Zoom {\n const z = zoom<SVGSVGElement, undefined>()\n .scaleExtent([min, max])\n .filter((event) => event.button === 0 || event.touches?.length >= 2)\n .on('start', () => canvasContainer().classed('grabbed', true))\n .on('zoom', (event) => onZoom(event))\n .on('end', () => canvasContainer().classed('grabbed', false))\n\n config.modifiers.zoom?.(z)\n\n return z\n}\n","import { debounce } from '@yeger/debounce'\nimport { select } from 'd3-selection'\nimport type { D3ZoomEvent } from 'd3-zoom'\nimport { Vector } from 'vecti'\n\nimport type { GraphConfig } from './config/config'\nimport type { LinkFilter } from './config/filter'\nimport { defineCanvas, updateCanvasTransform } from './lib/canvas'\nimport { defineDrag } from './lib/drag'\nimport { filterGraph } from './lib/filter'\nimport { createLinks, defineLinkSelection, updateLinks } from './lib/link'\nimport { createMarkers, defineMarkerSelection } from './lib/marker'\nimport { createNodes, defineNodeSelection, updateNodes } from './lib/node'\nimport { defineSimulation } from './lib/simulation'\nimport type {\n Canvas,\n Drag,\n GraphSimulation,\n LinkSelection,\n MarkerSelection,\n NodeSelection,\n Zoom,\n} from './lib/types'\nimport { isNumber } from './lib/utils'\nimport { defineZoom } from './lib/zoom'\nimport type { Graph, NodeTypeToken } from './model/graph'\nimport type { GraphLink } from './model/link'\nimport type { GraphNode } from './model/node'\n\n/**\n * Controller for a graph view.\n */\nexport class GraphController<\n T extends NodeTypeToken = NodeTypeToken,\n Node extends GraphNode<T> = GraphNode<T>,\n Link extends GraphLink<T, Node> = GraphLink<T, Node>,\n> {\n /**\n * Array of all node types included in the controller's graph.\n */\n public readonly nodeTypes: T[]\n private _nodeTypeFilter: T[]\n private _includeUnlinked = true\n\n private _linkFilter: LinkFilter<T, Node, Link> = () => true\n\n private _showLinkLabels = true\n private _showNodeLabels = true\n\n private filteredGraph!: Graph<T, Node, Link>\n\n private width = 0\n private height = 0\n\n private simulation: GraphSimulation<T, Node, Link> | undefined\n\n private canvas: Canvas | undefined\n private linkSelection: LinkSelection<T, Node, Link> | undefined\n private nodeSelection: NodeSelection<T, Node> | undefined\n private markerSelection: MarkerSelection | undefined\n\n private zoom: Zoom | undefined\n private drag: Drag<T, Node> | undefined\n\n private xOffset = 0\n private yOffset = 0\n private scale: number\n\n private focusedNode: Node | undefined = undefined\n\n private resizeObserver?: ResizeObserver\n\n private readonly container: HTMLDivElement\n private readonly graph: Graph<T, Node, Link>\n private readonly config: GraphConfig<T, Node, Link>\n\n /**\n * Create a new controller and initialize the view.\n * @param container - The container the graph will be placed in.\n * @param graph - The graph of the controller.\n * @param config - The config of the controller.\n */\n public constructor(\n container: HTMLDivElement,\n graph: Graph<T, Node, Link>,\n config: GraphConfig<T, Node, Link>,\n ) {\n this.container = container\n this.graph = graph\n this.config = config\n\n this.scale = config.zoom.initial\n\n this.resetView()\n\n this.graph.nodes.forEach((node) => {\n const [x, y] = config.positionInitializer(\n node,\n this.effectiveWidth,\n this.effectiveHeight,\n )\n node.x = node.x ?? x\n