Newer
Older
import * as Config from "../config";

Matthias Konitzny
committed
import * as THREE from "three";
import { ForceGraph3D } from "react-force-graph";
import { MODE, DRAG_THRESHOLD_3D } from "../config";
import { Line2, LineGeometry, LineMaterial } from "three-fatline";
import React from "react";
import PropTypes, { InferType } from "prop-types";
import SpriteText from "three-spritetext";
import { Object3D } from "three";
import Graph, { Coordinate, LinkData, NodeData } from "./graph";
export interface GraphNode extends NodeData {
x: number;
y: number;
z: number;
vx: number;
vy: number;
vz: number;
fx: number;
fy: number;
fz: number;
color: string;
__threeObj: THREE.Group;
}
export interface GraphLink extends LinkData {
__lineObj?: Line2;
}
export class GraphRenderer extends React.Component<
InferType<typeof GraphRenderer.propTypes>,
InferType<typeof GraphRenderer.stateTypes>
> {
props: InferType<typeof GraphRenderer.propTypes>;
state: InferType<typeof GraphRenderer.stateTypes>;
forceGraph: React.RefObject<any>; // using typeof ForceGraph3d produces an error here...
edgeColors: Map<string, string>;
nodeColors: Map<string, string>;
highlightedNodes: Set<GraphNode>;
highlightedLinks: Set<GraphLink>;
hoverNode: GraphNode;
graph: PropTypes.instanceOf(Graph).isRequired,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
loadingFinishedCallback: PropTypes.func,
onNodeClicked: PropTypes.func,
isFullscreen: PropTypes.bool,
constructor(props: InferType<typeof GraphRenderer.propTypes>) {
super(props);
this.highlightedNodes = new Set();
this.highlightedLinks = new Set();
this.hoverNode = null;
this.forceGraph = React.createRef();
this.edgeColors = new Map<string, string>();
this.nodeColors = new Map<string, string>();
this.mapLinkColors();
this.mapNodeColors();
componentDidMount() {
this.addBackground();
}
addBackground() {
const sphereGeometry = new THREE.SphereGeometry(20000, 32, 32);
const loader = new THREE.TextureLoader();
const planeMaterial = new THREE.MeshBasicMaterial({
map: loader.load(background),
side: THREE.DoubleSide,
}); //THREE.BackSide
const mesh = new THREE.Mesh(sphereGeometry, planeMaterial);
mesh.position.set(0, 0, 0);
//mesh.rotation.set(0.5 * Math.PI, 0, 0);
this.forceGraph.current.scene().add(mesh);
}
drawNode(node: GraphNode): Object3D {
const group = new THREE.Group();
const text = new SpriteText(node.name);
text.color = "white";
text.backgroundColor = "black";
text.textHeight = 5;
// text.padding = 2;
text.borderRadius = 5;
text.borderWidth = 3;
text.borderColor = "black";
text.translateY(12);
text.material.opacity = 0.85;
group.add(text);
// Draw node circle image
const textureLoader = new THREE.TextureLoader();
textureLoader.setCrossOrigin("anonymous");
const imageAlpha = textureLoader.load(
Config.PLUGIN_PATH + "datasets/images/alpha.png"
);
const material = new THREE.SpriteMaterial({
//map: imageTexture,
color: this.nodeColors.get(node.type),
alphaMap: imageAlpha,
transparent: true,
alphaTest: 0.2,
depthWrite: false,
depthTest: false,
});
const sprite = new THREE.Sprite(material);
//sprite.renderOrder = 999; // This may not be optimal. But it allows us to render the sprite on top of everything else.
group.add(sprite);
return group;
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
drawLink(link: LinkData) {
const colors = new Float32Array(
[].concat(
...[link.target, link.source]
.map((node) => this.nodeColors.get(node.type))
.map((color) => color.replace(/[^\d,]/g, "").split(",")) // Extract rgb() color components
.map((rgb) => rgb.map((v) => parseInt(v) / 255))
)
);
const geometry = new LineGeometry();
geometry.setPositions([0, 0, 0, 1, 1, 1]);
geometry.setColors(colors);
const material = new LineMaterial({
color: 0xffffff,
linewidth: Config.LINK_WIDTH, // in world units with size attenuation, pixels otherwise
vertexColors: true,
resolution: new THREE.Vector2(
window.screen.width,
window.screen.height
), // Set the resolution to the maximum width and height of the screen.
dashed: false,
alphaToCoverage: true,
});
const line = new Line2(geometry, material);
line.computeLineDistances();
line.scale.set(1, 1, 1);
return line;
}
onNodeHover(node: GraphNode) {
// no state change
if (
(!node && !this.highlightedNodes.size) ||
(node && this.hoverNode === node)
const highlightedNodes = new Set<GraphNode>();
const highlightedLinks = new Set<GraphLink>();
highlightedNodes.add(node);
node.neighbors.forEach((neighbor) =>
highlightedNodes.add(neighbor as GraphNode)
);
node.links.forEach((link) =>
highlightedLinks.add(link as GraphLink)
);
this.hoverNode = node || null;
this.updateHighlight(highlightedNodes, highlightedLinks);
onLinkHover(link: GraphLink) {
const highlightedNodes = new Set<GraphNode>();
const highlightedLinks = new Set<GraphLink>();
if (link && link.__lineObj) {
highlightedLinks.add(link);
highlightedNodes.add(link.source as GraphNode);
highlightedNodes.add(link.target as GraphNode);
this.updateHighlight(highlightedNodes, highlightedLinks);
}
updateHighlight(
highlightedNodes: Set<GraphNode>,
highlightedLinks: Set<GraphLink>
) {
// Update Links
this.highlightedLinks.forEach(
(link) => (link.__lineObj.material.linewidth = Config.LINK_WIDTH)
);
this.highlightedLinks = highlightedLinks;
this.highlightedLinks.forEach(
(link) =>
(link.__lineObj.material.linewidth = Config.HOVER_LINK_WIDTH)
);
// Update Nodes
this.highlightedNodes.forEach((node) => {
node.__threeObj.children[1].scale.set(...Config.NODE_SIZE);
});
this.highlightedNodes = highlightedNodes;
this.highlightedNodes.forEach((node) => {
node.__threeObj.children[1].scale.set(
...Config.HIGHLIGHTED_NODE_SIZE
);
});
}
onNodeClick(node: GraphNode) {
this.focusOnNode(node);
if (MODE === "default" && this.props.onNodeClicked) {
this.props.onNodeClicked(node);
}
}
onNodeDragEnd(node: GraphNode, translate: Coordinate) {
// NodeDrag is handled like NodeClick if distance is very short
if (
Math.hypot(translate.x, translate.y, translate.z) <
DRAG_THRESHOLD_3D
) {
this.onNodeClick(node);
}
}
focusOnNode(node: GraphNode) {
const distance = 400; // Aim at node from outside it
const speed = 0.5; // Camera travel speed through space
const minTime = 1000; // Minimum transition time
const distRatio = 1 + distance / Math.hypot(node.x, node.y, node.z);
const currentPos = this.forceGraph.current.cameraPosition();
const newPos = {
x: node.x * distRatio,
y: node.y * distRatio,
z: node.z * distRatio,
};
const travelDist = Math.hypot(
newPos.x - currentPos.x,
newPos.y - currentPos.y,
newPos.z - currentPos.z
);
this.forceGraph.current.cameraPosition(
node, // lookAt ({ x, y, z })
Math.max(travelDist / speed, minTime) // ms transition duration
);
}
/**
* Maps the colors of the color palette to the different edge types
*/
mapLinkColors() {
// TODO: Move this to the graph data structure - access is also needed in the other menues?
const linkClasses = this.props.graph.getLinkClasses();
for (let i = 0; i < linkClasses.length; i++) {
this.edgeColors.set(
linkClasses[i],
Config.COLOR_PALETTE[i % Config.COLOR_PALETTE.length]
);
}
}
/**
* Maps the colors of the color palette to the different edge types
*/

Matthias Konitzny
committed
mapNodeColors() {
// TODO: Move this to the graph data structure - access is also needed in the other menues?
const nodeClasses = this.props.graph.getNodeClasses();

Matthias Konitzny
committed
for (let i = 0; i < nodeClasses.length; i++) {
this.nodeColors.set(
nodeClasses[i],
Config.COLOR_PALETTE[i % Config.COLOR_PALETTE.length]
);

Matthias Konitzny
committed
}
}
updateLinkPosition(line: Line2, start: Coordinate, end: Coordinate) {
if (!(line instanceof Line2)) {
return false;
}
// console.log("Updating links");

Matthias Konitzny
committed
const startR = 4;
const endR = 4;
const lineLen = Math.hypot(
end.x - start.x,
end.y - start.y,
end.z - start.z

Matthias Konitzny
committed
);
const positions = [startR / lineLen, 1 - endR / lineLen]
.map((t) =>
["x", "y", "z"].map(
(dim) =>
start[dim as keyof typeof start] +
(end[dim as keyof typeof end] -
start[dim as keyof typeof start]) *
t

Matthias Konitzny
committed
)
)
.flat();

Matthias Konitzny
committed
return true;
}
render() {
return (
<ForceGraph3D
ref={this.forceGraph}
graphData={this.props.graph}
rendererConfig={{ antialias: true }}
nodeThreeObject={(node: GraphNode) => this.drawNode(node)}
linkThreeObject={(link: LinkData) => this.drawLink(link)}
onNodeClick={(node: GraphNode) => this.onNodeClick(node)}
//d3AlphaDecay={0.1}
warmupTicks={150}
cooldownTime={1000} // TODO: Do we want the simulation to unfreeze on node drag?
onNodeHover={(node: GraphNode) => this.onNodeHover(node)}
onLinkHover={(link: GraphLink) => this.onLinkHover(link)}
linkPositionUpdate={(
line: Line2,
coords: { start: Coordinate; end: Coordinate }
) => this.updateLinkPosition(line, coords.start, coords.end)}
onNodeDragEnd={(node: GraphNode, translate: Coordinate) =>
this.onNodeDragEnd(node, translate)
}
);