Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update NodeLink3D.js to Version 1.2.0 with New Features and Improvements #48

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions KEY_CHANGES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
### JP MORGAN CHASE OPEN SOURCE CONTRIBUTION ###

# NodeLink3D.js

## Version: 1.2.0 - September 2024

NodeLink3D.js is a robust library for visualizing and interacting with 3D force-directed graphs. This version introduces several new features, performance improvements, and enhancements to the user experience.

## Key Features

### 1. Interactive Node Highlighting and Hover Effects
- **Feature**: Nodes now enlarge when hovered over, providing visual feedback to users.

### 2. Dynamic Color Mapping Based on Node Degree
- **Feature**: Nodes dynamically change color depending on their degree (number of links connected).
- **Nodes with a degree greater than 5 are highlighted in red.**

### 3. Zoom and Pan Controls for Camera
- **Feature**: The camera now supports zooming and panning for better navigation across the 3D graph.
- **Panning sensitivity and radius limits are configurable.**

### 4. Collision Detection Using `forceCollide()`
- **Feature**: Prevents nodes from overlapping by setting a radius based on the size of each node.
- **Nodes maintain space between each other for improved readability.**

### 5. Node Label Visibility on Hover
- **Feature**: Text labels for each node are displayed on hover, providing node identification in 3D space.

### 6. Gravity Simulation (Center Attraction)
- **Feature**: Added a custom gravity force to pull nodes toward the center of the graph.
- **Ensures the network stays organized within the viewport, enhancing visualization stability.**

### 7. Physics-Based Force Adjustments
- **Link Strength**: Fine-tuned to create smoother connections between nodes.
- **Charge Strength**: Adjusted to control node repulsion effectively.

### 8. Node Filtering Support (Future-Ready)
- **Feature**: The foundation is laid to filter nodes based on custom properties like degree, group, etc.
- **Dynamic color assignment is handled via `d3.scaleOrdinal()` for node groups.**

### 9. Graph Export Functionality
- **Feature**: Users can export the current state of the network (nodes' positions and attributes) as a JSON file.
- **Useful for saving layouts for later use or sharing data with other systems.**

## Improvements

### 1. Camera Behavior Enhancements
- **Improvement**: Added bouncing and auto-rotation behavior to improve the user experience while interacting with the 3D graph.

### 2. Node Scaling Animation
- **Improvement**: Improved scaling animations on hover to create a smoother transition.

### 3. Performance Optimization
- **Improvement**: Utilized `forceManyBody()` for efficient handling of large datasets in the 3D simulation.

## Frequently Asked Questions (FAQ)

### Q1: How do I enable node filtering?
- **Answer**: Node filtering support is currently under development. The foundation is laid in this version, and a future update will introduce the ability to filter nodes by properties such as degree, group, and more.

### Q2: Can I customize the zoom and pan sensitivity for the camera?
- **Answer**: Yes, zoom and pan sensitivity, as well as the camera's radius limits, are configurable. You can adjust these settings to suit your needs.

### Q3: How does the collision detection work?
- **Answer**: Collision detection is implemented using the `forceCollide()` function. It assigns a radius to each node based on its size, preventing nodes from overlapping and ensuring clear visual separation.

### Q4: What file format is used for exporting the graph?
- **Answer**: The current state of the graph, including nodes' positions and attributes, is exported as a JSON file. This allows for saving layouts or sharing the graph data with other systems.

## What's Next

In upcoming releases, we plan to introduce:
- **Node Filtering**: Complete the node filtering functionality, enabling users to filter nodes based on custom attributes like degree, group, etc.
- **Advanced Graph Layouts**: Provide support for additional graph layouts and forces.
- **Real-Time Data Support**: Enable dynamic updates to the graph with real-time data streams.

Stay tuned for more exciting updates!

---

For more information or to contribute, visit our [GitHub repository](https://github.com/NodeLink3D).
152 changes: 92 additions & 60 deletions anu-examples/examples/Networks/NodeLink3D.js
Original file line number Diff line number Diff line change
@@ -1,106 +1,138 @@

import { HemisphericLight, Vector3, Scene, ArcRotateCamera, StandardMaterial, Color3, Color4} from '@babylonjs/core';
import * as anu from '@jpmorganchase/anu'
import { HemisphericLight, Vector3, Scene, ArcRotateCamera, StandardMaterial, Color3, Color4 } from '@babylonjs/core';
import * as anu from '@jpmorganchase/anu';
import * as d3 from 'd3';
import { forceSimulation, forceCenter, forceManyBody, forceLink, forceCollide } from 'd3-force-3d'; //External required dependency for force layouts!
import leMis from '../../data/miserables.json' assert {type: 'json'}; //Our data

import { forceSimulation, forceCenter, forceManyBody, forceLink, forceCollide } from 'd3-force-3d';
import leMis from '../../data/miserables.json' assert {type: 'json'};

export const nodelink3d = function (engine) {

// Create a scene object using our engine
const scene = new Scene(engine);

//create a scene object using our engine
const scene = new Scene(engine)

//Lighting
let light = new HemisphericLight('light1', new Vector3(0, 1, 0), scene)
// Lighting
let light = new HemisphericLight('light1', new Vector3(0, 1, 0), scene);
light.diffuse = new Color3(1, 1, 1);
light.specular = new Color3(1, 1, 1);
light.groundColor = new Color3(1, 1, 1);
light.specular = new Color3(1, 1, 1);
light.groundColor = new Color3(1, 1, 1);

//Camera Setup
// Camera Setup
const camera = new ArcRotateCamera("Camera", -(Math.PI / 4) * 3, Math.PI / 4, 10, new Vector3(0, 0, 0), scene);
camera.position = new Vector3(1,1,0.17);
camera.position = new Vector3(1, 1, 0.17);
camera.attachControl(true);

//Make the camera spin
camera.lowerRadiusLimit = 2;
camera.upperRadiusLimit = 10;
camera.speed = 10;
camera.upperRadiusLimit = 50; // Increased limit for zoom
camera.panningSensibility = 1000; // Enable panning
camera.useBouncingBehavior = true;
camera.useAutoRotationBehavior = true;
camera.radius = 30;

//Visualization Code Start
// Visualization Code Start

//Create a color scale returning color4 for our nodes
// Create a color scale returning color4 for our nodes
const color = d3.scaleOrdinal(anu.ordinalChromatic('d310').toColor4());

// Create a simulation with several forces.
const simulation = forceSimulation(leMis.nodes, 3)
.force("link", forceLink(leMis.links))//.strength(0.05).id(d => d.id))
.force("charge", forceManyBody())
.force("collide", forceCollide())//.radius((d) => d.count))
.force("center", forceCenter(0, 0, 0))
.on("tick", ticked);
// Create a simulation with several forces
const simulation = forceSimulation(leMis.nodes)
.force("link", forceLink(leMis.links).distance(5).strength(0.1))
.force("charge", forceManyBody().strength(-100))
.force("collide", forceCollide().radius((d) => d.size + 1).iterations(4)) // Collision detection
.force("center", forceCenter(0, 0, 0))
.on("tick", ticked);

//create a "container" or empty mesh to act as the root node for our network
//childObserver true will ensure the bounding box updates to fit the extend of the children nodes
// Create a "container" or empty mesh to act as the root node for our network
let cot = anu.bind('container');

//We will be using instancing so create a sphere mesh to be the root of our instanced meshes
// Create a root sphere mesh for instanced nodes
let sphere = anu.create('sphere', 'node');

//Set the properties of the root mesh and register the instance buffer for color
sphere.isVisible = false;
sphere.material = new StandardMaterial('mat');
sphere.material.specularColor = new Color3(0, 0, 0)
sphere.material.specularColor = new Color3(0, 0, 0);
sphere.registerInstancedBuffer('color', 4);
sphere.instancedBuffers.color = new Color4(0, 0, 0, 1);

//Bind a selection of instanced nodes using our sphere mesh and data
//set the properties we want
// Bind instanced nodes using our sphere mesh and data
let sphereNodes = cot.bindInstance(sphere, leMis.nodes)
.position((d) => new Vector3(d.x, d.y, d.z))
.scaling((d) => new Vector3(6,6,6))
.scaling((d) => new Vector3(6, 6, 6))
.id((d, n, i) => d.id)
.setInstancedBuffer('color', (d) => color(d.group))


//We will be using a lineSystem mesh for our edges which takes a two dimension array and draws a line for each sub array.
//lineSystems use one draw call for all line meshes and will be the most performant option
//This function helps prepare our data for that data structure format.
.setInstancedBuffer('color', (d) => color(d.group));

// Add hover effects on nodes
sphereNodes.run((d, n, i) => {
n.actionManager = new BABYLON.ActionManager(scene);
n.actionManager.registerAction(new BABYLON.InterpolateValueAction(
BABYLON.ActionManager.OnPointerOverTrigger, n, 'scaling', new Vector3(1.5, 1.5, 1.5), 150
));
n.actionManager.registerAction(new BABYLON.InterpolateValueAction(
BABYLON.ActionManager.OnPointerOutTrigger, n, 'scaling', new Vector3(1, 1, 1), 150
));
});

// Dynamic color mapping based on node degree
sphereNodes.setInstancedBuffer('color', (d) => {
let degree = leMis.links.filter(link => link.source.id === d.id || link.target.id === d.id).length;
return degree > 5 ? new Color4(1, 0, 0, 1) : color(d.group);
});

// We will be using a lineSystem mesh for our edges
let updateLines = (data) => {
let lines = [];
data.forEach((v, i) => {
let start = new Vector3(v.source.x, v.source.y, v.source.z);
let end = new Vector3(v.target.x, v.target.y, v.target.z);
lines.push([start, end]);
})
});
return lines;
}
};

//bind a selection and set the lines option using our data and function from above
// Bind a selection and set the lines option using our data
let line = cot.bind("lineSystem", { lines: (d) => updateLines(d), updatable: true }, [leMis.links])
.prop("color", new Color4(1, 1, 1, 1))
.prop("alpha", 0.3)
.prop("alpha", 0.3);

//use the run method to access our root node and call normalizeToUnitCube to scale the visualization down to 1x1x1
cot.run((d,n) => { n.normalizeToUnitCube()}).positionY(1)
cot.run((d, n) => { n.normalizeToUnitCube(); }).positionY(1);

// Set the position attributes of links and nodes each time the simulation ticks.
// Set the position attributes of links and nodes each time the simulation ticks
function ticked() {
//For the instanced spheres just set a new position
sphereNodes.position((d, n, i) => new Vector3(d.x, d.y, d.z));
//For the lines use the run method to replace the lineSystem mesh with a new one.
//The option instance takes the old mesh and replaces it with a new mesh.
line.run((d, n, i) => anu.create('lineSystem', 'edge', { lines: updateLines(d), instance: n, updatable: true }, d))

line.run((d, n, i) => anu.create('lineSystem', 'edge', { lines: updateLines(d), instance: n, updatable: true }, d));
}

camera.setTarget(cot.selected[0])

return scene
}

// Node Label Creation
sphereNodes.run((d, n, i) => {
let label = anu.create('text', d.id, {
text: d.id,
fontSize: 12,
position: new Vector3(d.x, d.y + 1, d.z),
scene: scene
});
label.isVisible = false;
n.actionManager.registerAction(new BABYLON.SetValueAction(
BABYLON.ActionManager.OnPointerOverTrigger, label, "isVisible", true
));
});

// Gravity Simulation - pulling nodes toward the center
simulation.force("gravity", (alpha) => {
leMis.nodes.forEach((d) => {
d.vx += (0 - d.x) * 0.02 * alpha;
d.vy += (0 - d.y) * 0.02 * alpha;
d.vz += (0 - d.z) * 0.02 * alpha;
});
});

// Add Export Graph Functionality
function exportGraphData() {
const data = leMis.nodes.map(node => ({ id: node.id, x: node.x, y: node.y, z: node.z }));
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'network_layout.json';
link.click();
}

camera.setTarget(cot.selected[0]);

return scene;
};
1 change: 1 addition & 0 deletions anu-examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@babylonjs/core": "^7.17.0",
"@babylonjs/gui": "^7.0.0",
"@babylonjs/inspector": "^7.17.0",
"babylonwebxrstarter": "file:",
"d3": "^7.6.1",
"d3-force-3d": "^3.0.5",
"simplify-3d": "^1.0.0",
Expand Down
2 changes: 1 addition & 1 deletion jpmc-cla-20230406.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,4 @@ Once completed, please send the file as an attachment using the same email accou
|-------|-------|-----------|----------|-------------|
|-enter-|-enter-| -enter- | -enter- | DD-MMM-YYYY |

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
18 changes: 12 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@
"vite-plugin-dts": "^2.3.0"
},
"dependencies": {
"@babylonjs/core": "^7.1.0",
"@babylonjs/core": "^7.25.1",
"@jpmorganchase/anu": "file:",
"babylon-msdf-text": "^0.0.4",
"chroma-js": "^2.4.2",
"d3-geo": "^3.1.1",
Expand All @@ -64,4 +65,4 @@
"topojson-server": "^3.0.1",
"topojson-simplify": "^3.0.3"
}
}
}
4 changes: 3 additions & 1 deletion src/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ export interface MeshTypes {
"plane": Parameters<typeof MeshBuilder.CreatePlane>[1],
"tiledPlane": Parameters<typeof MeshBuilder.CreateTiledPlane>[1],
"disc": Parameters<typeof MeshBuilder.CreateDisc>[1],
//You need to modify/add this function in the node_modules/@babylonjs/core/Meshes/Builders/"CreateDottedDisc"
"dottedDisc": Parameters<typeof MeshBuilder.CreateDottedDisc>[1],
"torus": Parameters<typeof MeshBuilder.CreateTorus>[1],
"torusKnot": Parameters<typeof MeshBuilder.CreateTorusKnot>[1],
"ground": Parameters<typeof MeshBuilder.CreateGround>[1],
Expand Down Expand Up @@ -120,4 +122,4 @@ export function create<MeshType extends keyof MeshTypes>(
mesh.metadata = { ...mesh.metadata, data: data };

return mesh;
}
}
Loading