Skip to content

Commit

Permalink
chore(graph-layers): Add source code for additional examples (visgl#169)
Browse files Browse the repository at this point in the history
  • Loading branch information
ibgreen authored Dec 10, 2024
1 parent 4629cfe commit 420f239
Show file tree
Hide file tree
Showing 7 changed files with 979 additions and 0 deletions.
77 changes: 77 additions & 0 deletions examples/graph-layers/hive-plot/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React, {Component} from 'react';
import {scaleOrdinal} from 'd3-scale';
import {schemeAccent} from 'd3-scale-chromatic';
import {extent} from 'd3-array';
import Color from 'color';
import {fetchJSONFromS3} from '../../utils/data/io';

// graph.gl
import GraphGL, {JSONLoader, NODE_TYPE} from '../../src';
import HivePlot from './hive-plot-layout';

const DEFAULT_NODE_SIZE = 3;
const DEFAULT_EDGE_COLOR = 'rgba(80, 80, 80, 0.3)';
const DEFAULT_EDGE_WIDTH = 1;
const DEFAULT_WIDTH = 1000;

export default class HivePlotExample extends Component {
state = {graph: null};

componentDidMount() {
fetchJSONFromS3(['wits.json']).then(([sampleGraph]) => {
const {nodes} = sampleGraph;
const nodeIndexMap = nodes.reduce((res, node, idx) => {
res[idx] = node.name;
return res;
}, {});
const graph = JSONLoader({
json: sampleGraph,
nodeParser: node => ({id: node.name}),
edgeParser: edge => ({
id: `${edge.source}-${edge.target}`,
sourceId: nodeIndexMap[edge.source],
targetId: nodeIndexMap[edge.target],
directed: true,
}),
});
this.setState({graph});
const groupExtent = extent(nodes, n => n.group);
this._nodeColorScale = scaleOrdinal(schemeAccent).domain(groupExtent);
});
}

// node accessors
getNodeColor = node => {
const hex = this._nodeColorScale(node.getPropertyValue('group'));
return Color(hex).array();
};

render() {
if (!this.state.graph) {
return null;
}
return (
<GraphGL
graph={this.state.graph}
layout={
new HivePlot({
innerRadius: DEFAULT_WIDTH * 0.05,
outerRadius: DEFAULT_WIDTH * 0.3,
getNodeAxis: node => node.getPropertyValue('group'),
})
}
nodeStyle={[
{
type: NODE_TYPE.CIRCLE,
radius: DEFAULT_NODE_SIZE,
fill: this.getNodeColor,
},
]}
edgeStyle={{
stroke: DEFAULT_EDGE_COLOR,
strokeWidth: DEFAULT_EDGE_WIDTH,
}}
/>
);
}
}
176 changes: 176 additions & 0 deletions examples/graph-layers/hive-plot/hive-plot-layout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import {BaseLayout, EDGE_TYPE} from '../../src';

const defaultOptions = {
innerRadius: 100,
outerRadius: 500,
getNodeAxis: node => node.getPropertyValue('group'),
};

const computeControlPoint = ({
sourcePosition,
sourceNodeAxis,
targetPosition,
targetNodeAxis,
totalAxis,
}) => {
const halfAxis = (totalAxis - 1) / 2;
// check whether the source/target are at the same side.
const sameSide =
(sourceNodeAxis <= halfAxis && targetNodeAxis <= halfAxis) ||
(sourceNodeAxis > halfAxis && targetNodeAxis > halfAxis);
// curve direction
const direction =
sameSide && (sourceNodeAxis <= halfAxis && targetNodeAxis <= halfAxis)
? 1
: -1;

// flip the source/target to follow the clockwise diretion
const source =
sourceNodeAxis < targetNodeAxis && sameSide
? sourcePosition
: targetPosition;
const target =
sourceNodeAxis < targetNodeAxis && sameSide
? targetPosition
: sourcePosition;

// calculate offset
const distance = Math.hypot(source[0] - target[0], source[1] - target[1]);
const offset = distance * 0.2;

const midPoint = [(source[0] + target[0]) / 2, (source[1] + target[1]) / 2];
const dx = target[0] - source[0];
const dy = target[1] - source[1];
const normal = [dy, -dx];
const length = Math.hypot(dy, -dx);
const normalized = [normal[0] / length, normal[1] / length];
return [
midPoint[0] + normalized[0] * offset * direction,
midPoint[1] + normalized[1] * offset * direction,
];
};

export default class HivePlot extends BaseLayout {
constructor(options) {
super(options);
this._name = 'HivePlot';
this._options = {
...defaultOptions,
...options,
};
this._nodePositionMap = {};
}

initializeGraph(graph) {
this.updateGraph(graph);
}

updateGraph(graph) {
const {getNodeAxis, innerRadius, outerRadius} = this._options;
this._graph = graph;
this._nodeMap = graph.getNodes().reduce((res, node) => {
res[node.getId()] = node;
return res;
}, {});

// bucket nodes into few axis

this._axis = graph.getNodes().reduce((res, node) => {
const axis = getNodeAxis(node);
if (!res[axis]) {
res[axis] = [];
}
res[axis].push(node);
return res;
}, {});

// sort nodes along the same axis by degree
this._axis = Object.keys(this._axis).reduce((res, axis) => {
const bucketedNodes = this._axis[axis];
const sortedNodes = bucketedNodes.sort((a, b) => {
if (a.getDegree() > b.getDegree()) {
return 1;
}
if (a.getDegree() === b.getDegree()) {
return 0;
}
return -1;
});
res[axis] = sortedNodes;
return res;
}, {});
this._totalAxis = Object.keys(this._axis).length;
const center = [0, 0];
const angleInterval = 360 / Object.keys(this._axis).length;

// calculate positions
this._nodePositionMap = Object.keys(this._axis).reduce(
(res, axis, axisIdx) => {
const axisAngle = angleInterval * axisIdx;
const bucketedNodes = this._axis[axis];
const interval = (outerRadius - innerRadius) / bucketedNodes.length;

bucketedNodes.forEach((node, idx) => {
const radius = innerRadius + idx * interval;
const x = Math.cos((axisAngle / 180) * Math.PI) * radius + center[0];
const y = Math.sin((axisAngle / 180) * Math.PI) * radius + center[1];
res[node.getId()] = [x, y];
});
return res;
},
{}
);
}

start() {
this._callbacks.onLayoutChange();
this._callbacks.onLayoutDone();
}

getNodePosition = node => this._nodePositionMap[node.getId()];

getEdgePosition = edge => {
const {getNodeAxis} = this._options;
const sourceNodeId = edge.getSourceNodeId();
const targetNodeId = edge.getTargetNodeId();

const sourcePosition = this._nodePositionMap[sourceNodeId];
const targetPosition = this._nodePositionMap[targetNodeId];

const sourceNode = this._nodeMap[sourceNodeId];
const targetNode = this._nodeMap[targetNodeId];

const sourceNodeAxis = getNodeAxis(sourceNode);
const targetNodeAxis = getNodeAxis(targetNode);

if (sourceNodeAxis === targetNodeAxis) {
return {
type: EDGE_TYPE.LINE,
sourcePosition,
targetPosition,
controlPoints: [],
};
}

const controlPoint = computeControlPoint({
sourcePosition,
sourceNodeAxis,
targetPosition,
targetNodeAxis,
totalAxis: this._totalAxis,
});

return {
type: EDGE_TYPE.SPLINE_CURVE,
sourcePosition,
targetPosition,
controlPoints: [controlPoint],
};
};

lockNodePosition = (node, x, y) => {
this._nodePositionMap[node.id] = [x, y];
this._callbacks.onLayoutChange();
this._callbacks.onLayoutDone();
};
}
78 changes: 78 additions & 0 deletions examples/graph-layers/multi-graph/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
iimport React, {Component} from 'react';
import Color from 'color';

// data
import sampleGraph from './sample-graph.json';

// graph.gl
import GraphGL, {EDGE_DECORATOR_TYPE, JSONLoader, NODE_TYPE} from '../../src';
import MultiGraphLayout from './multi-graph-layout';

const DEFAULT_NODE_SIZE = 30;
const DEFAULT_NODE_PLACEHOLDER_SIZE = 40;
const DEFAULT_NODE_PLACEHOLDER_COLOR = 'rgb(240, 240, 240)';

export default class MultiGraphExample extends Component {
static defaultProps = {
showNodePlaceholder: true,
showNodeCircle: true,
nodeColor: '#cf4569',
showNodeLabel: true,
nodeLabelColor: '#ffffff',
nodeLabelSize: 14,
edgeColor: '#cf4569',
edgeWidth: 2,
showEdgeLabel: true,
edgeLabelColor: '#000000',
edgeLabelSize: 14,
};

render() {
return (
<GraphGL
graph={JSONLoader({json: sampleGraph})}
layout={
new MultiGraphLayout({
nBodyStrength: -8000,
})
}
nodeStyle={[
this.props.showNodePlaceholder && {
type: NODE_TYPE.CIRCLE,
radius: DEFAULT_NODE_PLACEHOLDER_SIZE,
fill: DEFAULT_NODE_PLACEHOLDER_COLOR,
},
this.props.showNodeCircle && {
type: NODE_TYPE.CIRCLE,
radius: DEFAULT_NODE_SIZE,
fill: this.props.nodeColor,
},
{
type: NODE_TYPE.CIRCLE,
radius: node => (node.getPropertyValue('star') ? 6 : 0),
fill: [255, 255, 0],
offset: [18, -18],
},
this.props.showNodeLabel && {
type: NODE_TYPE.LABEL,
text: node => node.getId(),
color: Color(this.props.nodeLabelColor).array(),
fontSize: this.props.nodeLabelSize,
},
]}
edgeStyle={{
stroke: this.props.edgeColor,
strokeWidth: this.props.edgeWidth,
decorators: [
this.props.showEdgeLabel && {
type: EDGE_DECORATOR_TYPE.LABEL,
text: edge => edge.getPropertyValue('type'),
color: Color(this.props.edgeLabelColor).array(),
fontSize: this.props.edgeLabelSize,
},
],
}}
/>
);
}
}
Loading

0 comments on commit 420f239

Please sign in to comment.