@neo4j-nvl/base 1.2.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/dist/base.mjs +1 -1
  3. package/dist/types/index.d.ts +21 -20
  4. package/dist/types/layouts/animatedlayout/AnimatedLayout.d.ts +2 -2
  5. package/dist/types/layouts/d3forcelayout/constants.d.ts +0 -1
  6. package/dist/types/layouts/d3forcelayout/d3ForceLayout.d.ts +3 -3
  7. package/dist/types/layouts/forcedirectedlayout/PhysLayout.d.ts +275 -0
  8. package/dist/types/layouts/forcedirectedlayout/clustering/ClusterByMerger.d.ts +62 -0
  9. package/dist/types/layouts/forcedirectedlayout/clustering/SolarMerger.d.ts +56 -0
  10. package/dist/types/layouts/forcedirectedlayout/clustering/clustering-utils.d.ts +143 -0
  11. package/dist/types/layouts/forcedirectedlayout/clustering/clustering-utils.test.d.ts +1 -0
  12. package/dist/types/layouts/forcedirectedlayout/{physlayout/solarmerger → clustering}/types.d.ts +11 -1
  13. package/dist/types/layouts/forcedirectedlayout/shaders/multilevel-fragment.d.ts +2 -0
  14. package/dist/types/layouts/forcedirectedlayout/shaders/multilevel-repulsive-fragment.d.ts +2 -0
  15. package/dist/types/layouts/forcedirectedlayout/shaders/multilevel-update-fragment.d.ts +2 -0
  16. package/dist/types/layouts/forcedirectedlayout/shaders/sim-vertex.d.ts +2 -0
  17. package/dist/types/layouts/freeLayout/FreeLayout.d.ts +2 -4
  18. package/dist/types/layouts/gridLayout/GridLayout.d.ts +2 -6
  19. package/dist/types/layouts/hierarchicallayout/HierarchicalLayout.d.ts +5 -0
  20. package/dist/types/modules/ExternalCallbackHandler.d.ts +4 -10
  21. package/dist/types/modules/NvlController.d.ts +85 -5
  22. package/dist/types/modules/Shader.d.ts +1 -1
  23. package/dist/types/modules/dataset.d.ts +4 -2
  24. package/dist/types/modules/state/types.d.ts +118 -25
  25. package/dist/types/renderers/ClusterOverlay.d.ts +73 -0
  26. package/dist/types/renderers/ClusterOverlay.test.d.ts +1 -0
  27. package/dist/types/renderers/domrenderer/shared/ImageCache.d.ts +1 -0
  28. package/dist/types/renderers/domrenderer/shared/types.d.ts +1 -2
  29. package/dist/types/renderers/webglrenderer/Controller.d.ts +27 -6
  30. package/dist/types/renderers/webglrenderer/IconLayerRegistry.d.ts +14 -0
  31. package/dist/types/renderers/webglrenderer/IconLayerRegistry.test.d.ts +1 -0
  32. package/dist/types/renderers/webglrenderer/Renderer.d.ts +43 -3
  33. package/dist/types/renderers/webglrenderer/node-animation-fragment.d.ts +1 -1
  34. package/dist/types/renderers/webglrenderer/node-animation-vertex.d.ts +1 -1
  35. package/dist/types/renderers/webglrenderer/node-fragment-point.d.ts +1 -1
  36. package/dist/types/renderers/webglrenderer/node-vertex-point.d.ts +1 -1
  37. package/dist/types/renderers/webglrenderer/rel-fragment-line.d.ts +1 -1
  38. package/dist/types/renderers/webglrenderer/rel-glsl-utils.d.ts +1 -0
  39. package/dist/types/renderers/webglrenderer/rel-vertex-arrow.d.ts +2 -0
  40. package/dist/types/renderers/webglrenderer/rel-vertex-line.d.ts +1 -1
  41. package/dist/types/renderers/webglrenderer/viewport-box-fragment.d.ts +1 -1
  42. package/dist/types/renderers/webglrenderer/viewport-box-vertex.d.ts +1 -1
  43. package/dist/types/types/graph-element.d.ts +9 -1
  44. package/dist/types/types/nvl-window-functions.d.ts +2 -2
  45. package/dist/types/utils/canvasManagement.d.ts +1 -1
  46. package/dist/types/utils/clusterGeometry.d.ts +14 -0
  47. package/dist/types/utils/clusterGeometry.test.d.ts +1 -0
  48. package/dist/types/utils/clusterHitRegions.d.ts +42 -0
  49. package/dist/types/utils/clusterHitRegions.test.d.ts +1 -0
  50. package/dist/types/utils/constants.d.ts +6 -0
  51. package/dist/types/utils/geometry.d.ts +44 -1
  52. package/dist/types/utils/hittest.d.ts +30 -4
  53. package/dist/types/utils/jsDriverResultTransformer.d.ts +11 -9
  54. package/dist/types/utils/layoutSeeding.d.ts +7 -0
  55. package/dist/types/utils/layoutSeeding.test.d.ts +1 -0
  56. package/dist/types/utils/webWorkerUtils.d.ts +1 -1
  57. package/dist/types/utils/zoomFunctions.d.ts +9 -2
  58. package/package.json +3 -3
  59. package/dist/types/layouts/forcedirectedlayout/ForceCytoLayout.d.ts +0 -22
  60. package/dist/types/layouts/forcedirectedlayout/cosebilkentlayout/CoseBilkentLayout.d.ts +0 -33
  61. package/dist/types/layouts/forcedirectedlayout/physlayout/PhysLayout.d.ts +0 -174
  62. package/dist/types/layouts/forcedirectedlayout/physlayout/shaders/multilevel-fragment.d.ts +0 -2
  63. package/dist/types/layouts/forcedirectedlayout/physlayout/shaders/multilevel-repulsive-fragment.d.ts +0 -2
  64. package/dist/types/layouts/forcedirectedlayout/physlayout/shaders/multilevel-update-fragment.d.ts +0 -2
  65. package/dist/types/layouts/forcedirectedlayout/physlayout/shaders/sim-vertex.d.ts +0 -2
  66. package/dist/types/layouts/forcedirectedlayout/physlayout/shaders/workaround-fragment.d.ts +0 -2
  67. package/dist/types/layouts/forcedirectedlayout/physlayout/solarmerger/SolarMerger.d.ts +0 -21
  68. package/dist/types/layouts/forcedirectedlayout/physlayout/solarmerger/solar-placer.d.ts +0 -2
  69. /package/dist/types/layouts/forcedirectedlayout/{physlayout/PhysLayout.test.d.ts → PhysLayout.test.d.ts} +0 -0
  70. /package/dist/types/layouts/forcedirectedlayout/{physlayout/solarmerger/SolarMerger.bench.d.ts → clustering/ClusterByMerger.test.d.ts} +0 -0
  71. /package/dist/types/layouts/forcedirectedlayout/{physlayout/solarmerger/SolarMerger.test.d.ts → clustering/SolarMerger.bench.d.ts} +0 -0
  72. /package/dist/types/layouts/forcedirectedlayout/{physlayout/solarmerger/solar-placer.test.d.ts → clustering/SolarMerger.test.d.ts} +0 -0
  73. /package/dist/types/layouts/forcedirectedlayout/{physlayout/glwrapper.d.ts → glwrapper.d.ts} +0 -0
@@ -0,0 +1,143 @@
1
+ import type { Node, Relationship } from '../../../types/graph-element';
2
+ import type { CoarsenedGraph, PartialPlanet, PartialSun, SolarNode, SolarRelationship, Sun } from './types';
3
+ /**
4
+ * Gets the original node ids belonging to a sun and its planets and moons.
5
+ * @param sun - The sun to get the original node ids for.
6
+ * @returns The original node ids belonging to the sun system.
7
+ *
8
+ * @note Synthetic suns are not included in the returned array.
9
+ */
10
+ export declare const getSunSystemOriginalIds: (sun: Sun) => string[];
11
+ /**
12
+ * How many times a node's degree must exceed the graph's average degree before it is treated as a hub, see
13
+ * {@link isHub}.
14
+ *
15
+ * @remarks
16
+ * Mi et al., "Interactive Graph Layout of a Million Nodes", Sec. 4.3, proposes capping cluster *size* to avoid
17
+ * the outlier clusters this multiplier targets. `HubDegreeMultiplier` targets this problem by checking a node's
18
+ * own degree at claim time, before moon-matching runs.
19
+ * @see {@link https://www.mdpi.com/2227-9709/3/4/23 Interactive Graph Layout of a Million Nodes} Sec. 4.3
20
+ */
21
+ export declare const HubDegreeMultiplier = 3;
22
+ /**
23
+ * @param relationships - The graph's adjacency list, indexed by node id.
24
+ * @returns The degree a node must exceed to be treated as a hub by {@link isHub}.
25
+ */
26
+ export declare const getHubDegreeThreshold: (relationships: number[][]) => number;
27
+ /**
28
+ * Whether a node's degree is enough of an outlier that it must become its own sun rather than being absorbed as
29
+ * a planet/moon of another sun.
30
+ *
31
+ * @param index - The node id to check.
32
+ * @param relationships - The graph's adjacency list, indexed by node id.
33
+ * @param hubDegreeThreshold - The threshold from {@link getHubDegreeThreshold}.
34
+ * @see {@link HubDegreeMultiplier}
35
+ */
36
+ export declare const isHub: (index: number, relationships: number[][], hubDegreeThreshold: number) => boolean;
37
+ /**
38
+ * Builds adjacency lists and relationship id maps from the input graph.
39
+ * @param inputGraph - The nodes and relationships to convert.
40
+ * @param nodeIdToIndex - Maps node ids to contiguous array indices.
41
+ * @returns Parsed adjacency data for coarsening.
42
+ */
43
+ export declare const buildAdjacencyGraph: (inputGraph: {
44
+ nodes: Node[];
45
+ rels: Relationship[];
46
+ }, nodeIdToIndex: Record<string, number>) => {
47
+ nodes: Node[];
48
+ relationships: number[][];
49
+ relIdMap: string[][];
50
+ idToRel: Record<string, SolarRelationship>;
51
+ };
52
+ /**
53
+ * Wraps input nodes as {@link SolarNode}s indexed by their original position.
54
+ * @param nodes - The input graph nodes.
55
+ * @returns {SolarNode[]} Solar nodes with positional ids and original ids preserved.
56
+ */
57
+ export declare const createSolarNodesFromInput: (nodes: Node[]) => SolarNode[];
58
+ /**
59
+ * Creates a Planet with an unsettled parent link (caller must attach via {@link attachPlanetsToSun}).
60
+ */
61
+ export declare const createPlanet: (params: {
62
+ id: number;
63
+ sunId: number;
64
+ weight: number;
65
+ originalId: string;
66
+ finestIndex?: number;
67
+ size?: () => number;
68
+ }) => PartialPlanet;
69
+ /**
70
+ * Finalises a sun's planets list, weight/size/children closures, and back-links parent on each planet.
71
+ * @param partialSun - The sun under construction (planets/size/children/weight not yet set).
72
+ * @param planets - The planets to attach to the sun (parent not yet set).
73
+ * @param centreWeight - The weight of the sun itself, added to the sum of its planets' weights.
74
+ * @param centreSize - The size of the sun itself, added to the sum of its planets' sizes.
75
+ * @returns A new sun object with planets attached. `previousIndex` is a placeholder until
76
+ * {@link sortHierarchyForShaderLayout} assigns the real value.
77
+ */
78
+ export declare const attachPlanetsToSun: (partialSun: PartialSun, planets: PartialPlanet[], centreWeight: number, centreSize: number) => Sun;
79
+ /**
80
+ * Appends a synthetic sun node to the finest-level arrays for a multi-member clusterBy cluster.
81
+ * Uses a reserved {@link SolarNode.originalId} (`synth-sun:${clusterId}`) so it cannot collide with
82
+ * real node ids; {@link SolarNode.synthetic} marks it for detection.
83
+ * @param params.clusterId - Semantic cluster id embedded in the synth sun's stable originalId.
84
+ * @param params.solarNodesWithSynths - Finest solar nodes; the synth sun is appended here.
85
+ * @param params.relationshipsWithSynths - Finest adjacency lists; an empty row is appended for the synth sun.
86
+ * @param params.relIdMapWithSynths - Finest relationship id lists; an empty row is appended for the synth sun.
87
+ * @returns A sun under construction, ready for {@link attachPlanetsToSun} (separate from the finest SolarNode).
88
+ */
89
+ export declare const createSynthClusterSun: (params: {
90
+ clusterId: string | number;
91
+ solarNodesWithSynths: SolarNode[];
92
+ relationshipsWithSynths: number[][];
93
+ relIdMapWithSynths: string[][];
94
+ }) => PartialSun;
95
+ /**
96
+ * Rolls node-level adjacency up to sun-level adjacency between clusters.
97
+ * When `weakLinkSunIds` is provided, edges where either endpoint is a Sun are skipped.
98
+ * @param relationships - Node adjacency lists indexed by node position.
99
+ * @param nodeIndexToSunIndex - Maps each node index to its sun index.
100
+ * @param sunCount - Number of suns in the coarse graph.
101
+ * @param weakLinkSunIds - Optional set of node indices that are Suns. When set, Sun-leaf and Sun-Sun edges are ignored.
102
+ * @returns {number[][]} Sun-level adjacency lists without duplicate edges.
103
+ */
104
+ export declare const rollupInterSunRelationships: (relationships: number[][], nodeIndexToSunIndex: Record<number, number>, sunCount: number, weakLinkSunIds?: Set<number>) => number[][];
105
+ /**
106
+ * Remaps adjacency lists to a sorted node order.
107
+ * @param relationships - Node adjacency lists in the pre-sort index order.
108
+ * @param nodeSortMap - Maps old node indices to sorted indices.
109
+ * @param relIdMap - Relationship ids aligned with adjacency lists.
110
+ * @param remapRelIds - Whether relationship ids should be remapped as well.
111
+ * @returns {{ relationships: number[][]; relIdMap: string[][] }} Remapped adjacency data.
112
+ */
113
+ export declare const remapAdjacencyWithSortMap: (relationships: number[][], nodeSortMap: Record<number, number>, relIdMap: string[][], remapRelIds?: boolean) => {
114
+ relationships: number[][];
115
+ relIdMap: string[][];
116
+ };
117
+ /** Input nodes that may receive a layout `finestIndex` during sorting (plain Node on first pass). */
118
+ type SourceNode = (Node | SolarNode) & {
119
+ finestIndex?: number;
120
+ };
121
+ export type SortHierarchyOptions = {
122
+ suns: Sun[];
123
+ solarNodes: SolarNode[];
124
+ sourceNodes?: SourceNode[];
125
+ relationships: number[][];
126
+ relIdMap: string[][];
127
+ idToRel: Record<string, SolarRelationship>;
128
+ firstIteration?: boolean;
129
+ includeMoons?: boolean;
130
+ };
131
+ /**
132
+ * Sorts suns, planets, and optionally moons into shader layout order and builds the finest subgraph.
133
+ * @param options - Hierarchy members and adjacency data to sort.
134
+ * @returns The finest subgraph, sort map, member-sun maps, and remapped relationship ids.
135
+ */
136
+ export declare const sortHierarchyForShaderLayout: ({ suns, solarNodes, sourceNodes, relationships, relIdMap, idToRel, firstIteration, includeMoons }: SortHierarchyOptions) => {
137
+ sortedInput: CoarsenedGraph;
138
+ nodeSortMap: Record<number, number>;
139
+ sunMap: Record<string, string>;
140
+ memberMap: Record<string, string[]>;
141
+ relIdMap: string[][];
142
+ };
143
+ export {};
@@ -1,4 +1,4 @@
1
- import type { Node } from '../../../../types/graph-element';
1
+ import type { Node } from '../../../types/graph-element';
2
2
  export interface SolarNode extends Omit<Node, 'id' | 'size'> {
3
3
  id: number;
4
4
  weight?: number;
@@ -13,6 +13,7 @@ export interface SolarNode extends Omit<Node, 'id' | 'size'> {
13
13
  y: number;
14
14
  };
15
15
  size?: number | (() => number);
16
+ synthetic?: boolean;
16
17
  }
17
18
  export interface SolarRelationship {
18
19
  id: string;
@@ -23,6 +24,11 @@ export interface CoarsenedGraph {
23
24
  nodes: SolarNode[];
24
25
  relationships: number[][];
25
26
  idToRel: Record<string, SolarRelationship>;
27
+ /**
28
+ * Contains the "real" {@link relationships} plus any synthetic edges purely for layout purposes.
29
+ * Undefined when there are no synthetic edges (e.g. SolarMerger).
30
+ */
31
+ springRelationships?: number[][];
26
32
  }
27
33
  export interface Sun extends SolarNode {
28
34
  planets: Planet[];
@@ -30,6 +36,8 @@ export interface Sun extends SolarNode {
30
36
  children: () => Planet[];
31
37
  previousIndex: number;
32
38
  }
39
+ /** Sun fields known before {@link attachPlanetsToSun} fills planets/size/children/weight. */
40
+ export type PartialSun = Omit<Sun, 'planets' | 'size' | 'children' | 'previousIndex' | 'weight'>;
33
41
  export interface Planet extends SolarNode {
34
42
  parent: Sun;
35
43
  sunId: number;
@@ -37,6 +45,8 @@ export interface Planet extends SolarNode {
37
45
  size: () => number;
38
46
  children: () => Moon[];
39
47
  }
48
+ /** Planet fields known before {@link attachPlanetsToSun} sets parent. */
49
+ export type PartialPlanet = Omit<Planet, 'parent'>;
40
50
  export interface Moon extends SolarNode {
41
51
  parent: Planet;
42
52
  sunId: number;
@@ -0,0 +1,2 @@
1
+ declare const _default: "#version 300 es\nprecision mediump float;\n\nuniform sampler2D u_physData;\nuniform sampler2D u_connections;\nuniform sampler2D u_connectionOffsets;\nuniform sampler2D u_pinnedNodes;\nuniform sampler2D u_sizeTexture;\nuniform float u_baseLength;\nuniform float u_curIteration;\nuniform float u_iterationMultiplier;\nuniform vec2 u_gravityCenter;\nuniform float u_numNodes;\nuniform float u_gravity;\n\nuniform sampler2D u_clusterData;\nuniform sampler2D u_prevForce;\nuniform float u_collisionMultiplier;\nuniform float u_maxNodeSize;\nuniform float u_crossClusterRelationshipMultiplier;\n\nout vec4 fragColor;\n\nfloat DAMP = 0.6;\nfloat COOL = 0.99998;\nfloat TIMESTEP = 1.0 / 30.0;\nfloat VELOCITYDECAY = 0.6;\nfloat accLimitLow = 500000.0;\nfloat accLimitHigh = 750000.0;\nfloat accLimitPosHigh = 10000000.0;\n\nconst float denseNodeThreshold = 1000.0;\nconst float MIN_DISTANCE = 0.00000001;\nconst float MAX_DISTANCE = 1000000000.0;\nconst float MAX_ACCELERATION = 50000.0;\n\nvec4 getTextureData(sampler2D tex, float index, float base) {\n float x = mod(index, base);\n float y = (index - x) / base;\n return texture(tex, vec2(x + 0.5, y + 0.5) / base);\n}\n\nbool isNan(float val) {\n return (val < 0.0 || 0.0 < val || val == 0.0) ? false : true;\n}\n\nconst float BIG_NUMBER = 999999999999999999.0;\nbool isInf(float val) {\n return val > BIG_NUMBER || val < -BIG_NUMBER;\n}\n\nvec2 getDelta(vec2 v1, vec2 v2) {\n vec2 delta = v1 - v2;\n float dist = length(delta);\n if (dist < MIN_DISTANCE || isNan(dist)) {\n return vec2(MIN_DISTANCE, MIN_DISTANCE);\n }\n if (dist > MAX_DISTANCE) {\n vec2 normDelta = delta / dist;\n return normDelta * MAX_DISTANCE;\n }\n return delta;\n}\n\nvec4 getOtherNodePosition(float i) {\n return getTextureData(u_physData, i, 256.0);\n}\n\nfloat getCombinedNodeSize(float i, float nodeSize) {\n float otherNodeSize = getTextureData(u_sizeTexture, i, 256.0).r;\n return nodeSize + otherNodeSize;\n}\n\nvec2 getSpringForce(float curConnection, float numConnections, float springFScale, vec4 myPosition, float i, float myClusterStartIndex) {\n float curSpring = getTextureData(u_connections, curConnection + i, 4096.0).r;\n\n vec4 otherPosition = getTextureData(u_physData, curSpring, 256.0);\n float otherDataPosition = getTextureData(u_connectionOffsets, curSpring, 256.0).r;\n float otherNumConnections = getTextureData(u_connections, otherDataPosition, 4096.0).r;\n float avgDegree = max((otherNumConnections + numConnections * 3.0) / 4.0, 4.0);\n\n vec2 delta = getDelta(myPosition.xy, otherPosition.xy);\n float dist = length(delta);\n\n float F = (dist * dist * springFScale) / (u_baseLength * avgDegree) * u_collisionMultiplier;\n\n // u_crossClusterRelationshipMultiplier only damps edges that cross a cluster boundary\n if (u_crossClusterRelationshipMultiplier < 1.0) {\n float otherClusterStartIndex = getTextureData(u_clusterData, curSpring, 256.0).y;\n F *= otherClusterStartIndex == myClusterStartIndex ? 1.0 : u_crossClusterRelationshipMultiplier;\n }\n\n return (-delta / dist) * F;\n}\n\nvec2 getCollisionForce(float combinedNodeSize, float dist, float fScale, vec2 delta) {\n float collisionForce = (combinedNodeSize - dist) / (dist) * u_collisionMultiplier;\n return (delta * collisionForce * fScale * combinedNodeSize) / (combinedNodeSize + 1.0);\n}\n\nvec2 getRepulsionForce(float dist, float fScale, vec2 delta) {\n float F = (u_baseLength * u_baseLength * fScale) / (dist) * u_collisionMultiplier;\n return (delta / dist) * F * 0.05;\n}\n\nvoid main(void) {\n float textureSide = 256.0; //#TEXTURE_SIDE#;\n float index = (gl_FragCoord.x - 0.5) + (gl_FragCoord.y - 0.5) * textureSide;\n\n if (index >= u_numNodes) {\n discard;\n }\n\n vec4 clusterData = getTextureData(u_clusterData, index, 256.0);\n\n float clusterIndex = clusterData.x;\n float clusterStartIndex = clusterData.y;\n float clusterSize = clusterData.z;\n\n vec4 myPosition = getTextureData(u_physData, index, 256.0);\n vec4 previousForce = getTextureData(u_prevForce, clusterIndex, 256.0);\n\n vec2 acceleration = previousForce.xy;\n\n float isPinned = getTextureData(u_pinnedNodes, index, 256.0).r;\n float nodeSize = getTextureData(u_sizeTexture, index, 256.0).r;\n\n if (isPinned > 0.5) {\n fragColor = vec4(myPosition.xy, 0.0, 0.0);\n return;\n }\n\n // Prime the second ping-pong texture with the initial state without calculating forces\n // that would otherwise be discarded at the end of this first iteration.\n if (u_curIteration == 0.0) {\n fragColor = vec4(myPosition.xy, myPosition.zw);\n return;\n }\n\n float curConnection = getTextureData(u_connectionOffsets, index, 256.0).r;\n float numConnections = getTextureData(u_connections, curConnection, 4096.0).r;\n\n float fScale = 1.0 + sqrt(u_iterationMultiplier);\n float springFScale = fScale;\n\n if (numConnections > denseNodeThreshold) {\n springFScale = sqrt(fScale);\n }\n\n float numOfRels = 0.0;\n\n // Springs\n for (float i = 1.0; i <= 256.0 * 256.0; i++) {\n if (numOfRels >= numConnections) {\n break;\n }\n acceleration += getSpringForce(curConnection, numConnections, springFScale, myPosition, i, clusterStartIndex);\n numOfRels += 1.0;\n }\n\n // Repulsion && collision detection.\n if (u_collisionMultiplier > 0.0) {\n float number_of_collisions = 0.0;\n for (float i = 0.0; i < 256.0 * 256.0; i++) {\n if (i >= u_numNodes) {\n break;\n }\n\n if (i == index) {\n continue;\n }\n\n vec4 otherPosition = getOtherNodePosition(i);\n vec2 delta = getDelta(myPosition.xy, otherPosition.xy);\n float dist = length(delta);\n\n if (number_of_collisions < 40.0 && dist < (nodeSize + u_maxNodeSize) * 2.0) {\n float combinedNodeSize = getCombinedNodeSize(i, nodeSize) * 2.0;\n if (dist < combinedNodeSize) {\n number_of_collisions++;\n acceleration += getCollisionForce(combinedNodeSize, dist, fScale, delta);\n }\n }\n\n if (i >= clusterStartIndex && i < clusterStartIndex + clusterSize) {\n acceleration += getRepulsionForce(dist, fScale, delta);\n }\n }\n }\n\n // Gravity\n vec2 delta = getDelta(u_gravityCenter, myPosition.xy);\n float dist = length(delta);\n\n vec2 grav = (delta / dist) * u_gravity * fScale * (dist / 1000.0);\n acceleration += grav;\n\n float accMagnitude = length(acceleration);\n acceleration *= min(MAX_ACCELERATION, accMagnitude) / accMagnitude;\n\n float iterationFrictionThreshold = 1000.0;\n\n if (u_curIteration > iterationFrictionThreshold) {\n float friction = 1.0 + pow((u_curIteration - iterationFrictionThreshold), 2.0) / 100.0;\n acceleration *= 1.0 / friction;\n }\n\n float TEMP = max(0.02, pow(COOL, u_curIteration));\n vec2 prevVelocity = myPosition.zw;\n vec2 currentPos = myPosition.xy;\n\n vec2 newPos = currentPos + TEMP * ( DAMP * (prevVelocity) + acceleration * TIMESTEP * TIMESTEP);\n vec2 newVelocity = newPos - currentPos;\n fragColor = vec4(newPos, newVelocity);\n}";
2
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: "#version 300 es\nprecision mediump float;\nuniform sampler2D u_physData;\nuniform sampler2D u_clusterData;\nuniform sampler2D u_finestIndexes;\nuniform sampler2D u_prevForce;\n\nuniform float u_baseLength;\nuniform float u_numNodes;\nuniform float u_iterationMultiplier;\nuniform float u_isTopLevel;\n\nout vec4 fragColor;\n\nfloat PI = 3.1415926535897932384626433832795;\nfloat TIMESTEP = 1.0 / 30.0;\nfloat VELOCITYDECAY = 1.0;\n\nvec4 getTextureData(sampler2D tex, float index, float base) {\n float x = mod(index, base);\n float y = (index - x) / base;\n return texture(tex, vec2(x + 0.5, y + 0.5) / base);\n}\n\nfloat getAdjustedClusterWeight(float clusterWeight) {\n return (clusterWeight * clusterWeight) / (clusterWeight + 1.0);\n}\n\n// Repulsion area from peer mass (weight). .z size is only for mid-level loop bounds.\nfloat getPeerFootprintArea(float peerWeight) {\n return pow(sqrt(peerWeight) * 4.0 + u_baseLength * 2.0, 2.0);\n}\n\nvoid main(void) {\n float index = (gl_FragCoord.x - 0.5) + (gl_FragCoord.y - 0.5) * 256.0;\n\n if (index >= u_numNodes) {\n discard;\n }\n\n vec4 clusterData = getTextureData(u_clusterData, index, 256.0);\n bool isTopLevel = u_isTopLevel == 1.0;\n\n float clusterIndex = clusterData.x;\n float clusterStartIndex = clusterData.y;\n float clusterSize = clusterData.z;\n float singleNodeRepulsionScale = isTopLevel && clusterSize == 1.0 ? 0.5 : 1.0;\n\n float finestIndex = getTextureData(u_finestIndexes, index, 256.0).r;\n vec4 myPosition = getTextureData(u_physData, finestIndex, 256.0);\n vec4 previousForce = getTextureData(u_prevForce, clusterIndex, 256.0);\n float fScale = 1.0 + sqrt(u_iterationMultiplier);\n\n vec2 acceleration = previousForce.xy;\n\n float loopEnd = isTopLevel ? u_numNodes : (clusterStartIndex + clusterSize);\n\n // Repulsion & Collision Detection\n for (float i = 0.0; i < 256.0 * 256.0; i++) {\n if (i >= loopEnd || i >= u_numNodes) {\n break;\n }\n if ((!isTopLevel && i < clusterStartIndex) || i == index) {\n continue;\n }\n\n vec4 otherClusterData = getTextureData(u_clusterData, i, 256.0);\n float otherClusterWeight = getAdjustedClusterWeight(otherClusterData.w);\n\n float otherFinestIndex = getTextureData(u_finestIndexes, i, 256.0).r;\n vec4 otherPosition = getTextureData(u_physData, otherFinestIndex, 256.0);\n\n vec2 delta = myPosition.xy - otherPosition.xy;\n float dist = max(length(delta), 0.0000001);\n float maxDist = 25.0;\n float repulsionForceScale = 0.1;\n\n float area = getPeerFootprintArea(otherClusterData.w);\n float F = (area * fScale) / (dist * dist);\n\n if (isTopLevel) {\n repulsionForceScale = 0.2;\n }\n\n acceleration += ((delta / sqrt(dist)) * F) * repulsionForceScale * otherClusterWeight * singleNodeRepulsionScale;\n\n if (dist < maxDist) {\n float collide = (maxDist - dist) / (dist * dist);\n acceleration += delta * collide * fScale * maxDist / (maxDist + 1.0);\n }\n }\n\n fragColor = vec4(acceleration, vec2(finestIndex, 0));\n}";
2
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: "#version 300 es\nprecision mediump float;\n\nuniform sampler2D u_physData;\nuniform sampler2D u_updateData;\nuniform float u_numNodesNew;\n\nout vec4 fragColor;\n\nconst float INDEX_FLAG = 999999.0;\n\nvec4 getTextureData(sampler2D tex, float index, float base) {\n float x = mod(index, base);\n float y = (index - x) / base;\n return texture(tex, vec2(x + 0.5, y + 0.5) / base);\n}\n\nvoid main(void) {\n float textureSide = 256.0; //#TEXTURE_SIDE#;\n float index = (gl_FragCoord.x - 0.5) + (gl_FragCoord.y - 0.5)*textureSide;\n\n if (index >= u_numNodesNew) {\n discard;\n }\n\n float firstValue = getTextureData(u_updateData, index * 2.0, 256.0).r;\n float secondValue = getTextureData(u_updateData, index * 2.0 + 1.0, 256.0).r;\n\n if (secondValue == INDEX_FLAG) {\n vec4 myPosition = getTextureData(u_physData, firstValue, 256.0);\n fragColor = myPosition;\n } else {\n fragColor = vec4(firstValue, secondValue, 0.0, 0.0);\n }\n}\n";
2
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: "#version 300 es\nuniform mat4 u_projection;\n\nin vec2 a_position;\n//in float a_index;\n\n//out highp float index;\n\nvoid main() {\n// index = a_index;\n gl_Position = u_projection * vec4(a_position, 0.0, 1.0);\n}";
2
+ export default _default;
@@ -1,5 +1,5 @@
1
1
  import type { NvlState } from '../../modules/state/types';
2
- import type { Node } from '../../types/graph-element';
2
+ import type { Node, PartialNode } from '../../types/graph-element';
3
3
  import type { Point } from '../../utils/geometry';
4
4
  export declare class FreeLayout {
5
5
  private state;
@@ -10,9 +10,7 @@ export declare class FreeLayout {
10
10
  state: NvlState;
11
11
  });
12
12
  setOptions(): any;
13
- updateNodes(positionList: (Point & {
14
- id: string;
15
- })[]): void;
13
+ updateNodes(positionList: PartialNode[]): void;
16
14
  update(refreshPositions?: boolean): void;
17
15
  layout(nodes: Node[], nodeIds: Record<string, Node>, idToPosition: Record<string, Node>): void;
18
16
  setNodePositions(p: Record<string, Point>): void;
@@ -1,5 +1,5 @@
1
1
  import type { NvlState } from '../../modules/state/types';
2
- import type { Node } from '../../types/graph-element';
2
+ import type { Node, PartialNode } from '../../types/graph-element';
3
3
  import type { Point } from '../../utils/geometry';
4
4
  export declare class GridLayout {
5
5
  state: NvlState;
@@ -10,11 +10,7 @@ export declare class GridLayout {
10
10
  state: NvlState;
11
11
  });
12
12
  setOptions(): void;
13
- updateNodes(positionList: {
14
- id: string;
15
- x: number;
16
- y: number;
17
- }[]): void;
13
+ updateNodes(positionList: PartialNode[]): void;
18
14
  update(refreshPositions?: boolean): void;
19
15
  layout(mobxNodes: any): void;
20
16
  setNodePositions(idToPosition: Record<string, Node & Point>): void;
@@ -7,6 +7,8 @@ type HierarchicalLayoutConfig = {
7
7
  export declare class HierarchicalLayout extends AnimatedLayout {
8
8
  private direction;
9
9
  private packing;
10
+ private clusterBy?;
11
+ private lastClusterByAssignments;
10
12
  private stateDisposers;
11
13
  private oldComputing;
12
14
  private computing;
@@ -14,9 +16,12 @@ export declare class HierarchicalLayout extends AnimatedLayout {
14
16
  private worker;
15
17
  private directionChanged;
16
18
  private packingChanged;
19
+ private clusterByChanged;
17
20
  private workersDisabled;
18
21
  constructor(config: HierarchicalLayoutConfig);
19
22
  setOptions(options: LayoutOptions): void;
23
+ private applyClusterByOption;
24
+ private buildClusterAssignments;
20
25
  update(refreshPositions?: boolean): void;
21
26
  getShouldUpdate(): boolean;
22
27
  getComputing(): boolean;
@@ -1,18 +1,12 @@
1
- import type { Node } from '../types/graph-element';
1
+ import type { NodeWithClientPosition } from '../types/graph-element';
2
2
  /**
3
3
  * An object specifying the required callback functions for certain actions
4
4
  */
5
5
  export interface ExternalCallbacks {
6
6
  /** Triggered when a layout is done moving. */
7
7
  onLayoutDone?: () => void;
8
- /**
9
- * Triggered on each step of a layout.
10
- * Node `x` and `y` are client/screen coordinates relative to the canvas.
11
- *
12
- * @note In the next major release, `x` and `y` become layout coordinates and
13
- * screen coordinates move to a `clientPosition` field.
14
- */
15
- onLayoutStep?: (p: Node[]) => void;
8
+ /** Triggered on each step of a layout. */
9
+ onLayoutStep?: (p: NodeWithClientPosition[]) => void;
16
10
  /** Triggered when NVL throws an error after initialization. */
17
11
  onError?: (error: Error) => void;
18
12
  /** Triggered when NVL is initialized. */
@@ -38,7 +32,7 @@ export default class ExternalCallbackHandler {
38
32
  onInitialization(): void;
39
33
  onZoomTransitionDone(): void;
40
34
  onLayoutDone(): void;
41
- onLayoutStep(p: Node[]): void;
35
+ onLayoutStep(p: NodeWithClientPosition[]): void;
42
36
  onLayoutComputing(isComputing: boolean): void;
43
37
  onError(error: Error): void;
44
38
  onWebGLContextLost(webGLContextEvent: WebGLContextEvent): void;
@@ -1,4 +1,4 @@
1
- import { type Node, type PartialNode, type PartialRelationship, type Relationship } from '../types/graph-element';
1
+ import { type Node, type NodeWithClientPosition, type PartialNode, type PartialRelationship, type Relationship } from '../types/graph-element';
2
2
  import '../types/nvl-window-functions';
3
3
  import type { Point } from '../utils/geometry';
4
4
  import type { ExternalCallbacks } from './ExternalCallbackHandler';
@@ -31,10 +31,13 @@ export default class NvlController {
31
31
  private readonly forceLayout;
32
32
  private readonly canvasRenderer;
33
33
  private readonly svgRenderer;
34
+ private clusterOverlay;
34
35
  private readonly glCanvas;
35
36
  private canvasRect;
36
37
  private readonly glMinimapCanvas;
37
38
  private readonly c2dCanvas;
39
+ private clusterOverlayCanvas;
40
+ private readonly canvasParent;
38
41
  private readonly svg;
39
42
  private isInRenderSwitchAnimation;
40
43
  private previousRenderer;
@@ -46,25 +49,96 @@ export default class NvlController {
46
49
  private readonly stateDisposers;
47
50
  private readonly zoomTransitionHandler;
48
51
  private currentLayout;
52
+ private lastTimestamp;
53
+ private accumulator;
49
54
  private readonly layoutTimeLimit;
55
+ private readonly smoothFrameBudgetMs;
50
56
  private pixelRatio;
51
57
  private readonly removeResizeListener;
52
58
  private readonly removeMinimapResizeListener;
53
59
  private pendingZoom;
54
60
  private pendingPan;
55
61
  private pendingViewportOverride;
56
- private layoutRunner;
57
62
  private animationRequestId;
58
63
  private layoutDoneCallback;
59
64
  private layoutComputingCallback;
60
65
  private currentLayoutType;
61
66
  private descriptionElement;
67
+ /** True when a minimap container was provided at construction. */
68
+ private readonly hasMinimap;
69
+ private lastMinimapPositions;
70
+ private lastMinimapCacheVersion;
62
71
  constructor(state: NvlState, frame: HTMLElement, options: NvlOptions);
63
72
  private onWebGLContextLost;
73
+ private fpsToFrameBudgetMs;
74
+ private getLayoutNeedsToUpdate;
75
+ private isWithinBudget;
76
+ /**
77
+ * Add elapsed frame time to the layout-step accumulator, capped at `layoutTimeLimit`.
78
+ * Prevents a slow frame (e.g. blocked GPU readback) from inflating catch-up debt and
79
+ * scheduling ever more work in a feedback loop; the layout falls behind gracefully instead.
80
+ */
81
+ private accumulateLayoutElapsedTime;
82
+ /**
83
+ * Whether the GPU force-directed layout is active.
84
+ * @returns `true` when PhysLayout is the current layout.
85
+ */
86
+ private usesGpuSimulation;
87
+ /**
88
+ * Whether the GPU-texture fast path can be taken this frame.
89
+ * Requires the WebGL renderer, GPU layout active, and no CPU-position consumers
90
+ * (layout-step callback, HTML overlays). A provided minimap no longer blocks this path;
91
+ * the minimap and cluster overlay are updated from the lagged async position cache instead.
92
+ * @returns `true` when rendering directly from the GPU position texture is safe.
93
+ */
94
+ private canRenderFromGpuTexture;
95
+ /**
96
+ * Uploads lagged PhysLayout cache positions to the minimap when the cache version changed.
97
+ * @returns `true` when positions were refreshed this call.
98
+ */
99
+ private refreshMinimapFromCacheIfStale;
100
+ /**
101
+ * Fits, renders, and caches minimap positions (and PhysLayout cache version when applicable).
102
+ */
103
+ private syncMinimap;
104
+ /** Whether either cluster decoration is enabled in the current layout options. */
105
+ private hasClusterDecorations;
106
+ /**
107
+ * Creates the decoration canvas and its overlay.
108
+ * @returns The overlay to draw with.
109
+ */
110
+ private createClusterOverlay;
111
+ /**
112
+ * Resolves the overlay to draw into, allocating it on first use and tearing it down again
113
+ * while cluster decorations are off.
114
+ * @returns The overlay to draw with, or null when decorations are disabled.
115
+ */
116
+ private ensureClusterOverlay;
117
+ /** Tears the overlay and its canvas back down once decorations are switched off. */
118
+ private destroyClusterOverlay;
119
+ /**
120
+ * Draws (or clears) cluster decorations on the Canvas2D overlay from explicit positions.
121
+ * @param positions - CPU node positions for this frame, or null to clear.
122
+ */
123
+ private renderClusterOverlay;
124
+ /**
125
+ * Draws cluster decorations from PhysLayout's lagged async PBO cache without a sync dump.
126
+ * @param forceLayout - The PhysLayout instance to render the cluster overlay from.
127
+ */
128
+ private renderClusterOverlayFromCache;
129
+ /** Syncs idToPosition from the GPU layout on demand; no-op for CPU layouts. */
130
+ syncPositionsFromLayout(): void;
131
+ /**
132
+ * Fitted {@link ClusteringOptions.clusterBy} hull regions for the current layout, or [].
133
+ * @returns Cluster hit regions for picking.
134
+ */
135
+ getClusterByHitRegions(): import("../utils/clusterHitRegions").ClusterHitRegion[];
64
136
  private updateMinimapZoom;
65
137
  private startMainLoop;
138
+ private renderNodeHtmlOverlays;
139
+ private renderRelationshipHtmlOverlays;
66
140
  updateHtmlOverlay(): void;
67
- dumpNodes(): Node[];
141
+ dumpNodes(): NodeWithClientPosition[];
68
142
  dumpRelationships(): Relationship[];
69
143
  /**
70
144
  * Map a relative Canvas space to the DOM position
@@ -93,7 +167,7 @@ export default class NvlController {
93
167
  };
94
168
  registerDoneCallback(cb: NvlController['layoutDoneCallback']): void;
95
169
  registerComputingCallback(cb: NvlController['layoutComputingCallback']): void;
96
- getPosition(nodeId: string): Node;
170
+ getPosition(nodeId: string): Node & Point;
97
171
  getScale(): number;
98
172
  getPan(): {
99
173
  x: number;
@@ -113,10 +187,16 @@ export default class NvlController {
113
187
  getCanvasNodesAt(point: Point, hitNodeMarginWidth?: number): import("..").HitTargetNode[];
114
188
  private getLayout;
115
189
  setLayout(layoutType: Layout): void;
116
- setLayoutOptions(options: LayoutOptions): void;
190
+ setLayoutOptions(options?: LayoutOptions): void;
191
+ /**
192
+ * Syncs positions if needed and redraws (or clears) the cluster overlay from current state.
193
+ */
194
+ private refreshClusterOverlay;
117
195
  private getDataUrlForCanvas;
118
196
  private initiateFileDownload;
119
197
  private updateLayoutAndPositions;
198
+ private getAndUpdatePositions;
199
+ private getLayoutPositions;
120
200
  saveToFile(options: SaveToFileOptions): void;
121
201
  private createSvgString;
122
202
  saveToSvg(options?: SaveToFileOptions): Promise<void>;
@@ -4,7 +4,7 @@ export default class Shader {
4
4
  private curTexture;
5
5
  private attributeInfo;
6
6
  private uniformInfo;
7
- constructor(gl: WebGLRenderingContext, vsSource: string, fsSource: string, defines?: {});
7
+ constructor(gl: WebGL2RenderingContext, vsSource: string, fsSource: string, defines?: {});
8
8
  setUniform(name: string, value: any): void;
9
9
  setAttributePointer(name: string, size: number, start: number, stride: number): void;
10
10
  setAttributePointerFloat(name: string, size: number, start: number, stride: number): void;
@@ -1,5 +1,5 @@
1
1
  import type { Node, Relationship } from '../types/graph-element';
2
- import type { Point } from '../utils/geometry';
2
+ import { type Point } from '../utils/geometry';
3
3
  /** A graph stored with mobx. */
4
4
  export interface DataSet<T> {
5
5
  items: T[];
@@ -8,6 +8,7 @@ export interface DataSet<T> {
8
8
  idToPosition: Record<string, Node & Point>;
9
9
  idToHtmlOverlay?: Record<string, HTMLElement>;
10
10
  version: number;
11
+ maxDimension: number;
11
12
  addChannel: (this: DataSet<T>, channelName: string) => void;
12
13
  removeChannel: (this: DataSet<T>, channelName: string) => void;
13
14
  clearChannel: (this: DataSet<T>, channelName: string) => void;
@@ -29,7 +30,8 @@ export interface Channel<T> {
29
30
  updates: Record<string, T>;
30
31
  removes: Record<string, T>;
31
32
  }
32
- export declare const getHtmlProperty: (item: Partial<Node | Relationship>) => HTMLElement | undefined;
33
+ export declare const getHtmlKey: (item: Node | Relationship) => "html" | "captionHtml";
34
+ export declare const getHtmlProperty: (item: Node | Relationship) => HTMLElement | undefined;
33
35
  /**
34
36
  * Creates an empty dataset in which a graph be stored and updated using mobx actions.
35
37
  * @returns A new and empty {@link DataSet}.