You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
127 lines
4.6 KiB
127 lines
4.6 KiB
import React from 'react';
|
|
import { Renderer } from 'core/renderers';
|
|
import { classes, distance } from 'common/util';
|
|
import styles from './GraphRenderer.module.scss';
|
|
|
|
class GraphRenderer extends Renderer {
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.elementRef = React.createRef();
|
|
this.selectedNode = null;
|
|
|
|
this.togglePan(true);
|
|
this.toggleZoom(true);
|
|
}
|
|
|
|
handleMouseDown(e) {
|
|
super.handleMouseDown(e);
|
|
const coords = this.computeCoords(e);
|
|
const { nodes, dimensions } = this.props.data;
|
|
const { nodeRadius } = dimensions;
|
|
this.selectedNode = nodes.find(node => distance(coords, node) <= nodeRadius);
|
|
}
|
|
|
|
handleMouseMove(e) {
|
|
if (this.selectedNode) {
|
|
const { x, y } = this.computeCoords(e);
|
|
const node = this.props.data.findNode(this.selectedNode.id);
|
|
node.x = x;
|
|
node.y = y;
|
|
this.refresh();
|
|
} else {
|
|
super.handleMouseMove(e);
|
|
}
|
|
}
|
|
|
|
computeCoords(e) {
|
|
const svg = this.elementRef.current;
|
|
const s = svg.createSVGPoint();
|
|
s.x = e.clientX;
|
|
s.y = e.clientY;
|
|
const { x, y } = s.matrixTransform(svg.getScreenCTM().inverse());
|
|
return { x, y };
|
|
}
|
|
|
|
renderData() {
|
|
const { nodes, edges, isDirected, isWeighted, dimensions } = this.props.data;
|
|
const { baseWidth, baseHeight, nodeRadius, arrowGap, nodeWeightGap, edgeWeightGap } = dimensions;
|
|
const viewBox = [
|
|
(this.centerX - baseWidth / 2) * this.zoom,
|
|
(this.centerY - baseHeight / 2) * this.zoom,
|
|
baseWidth * this.zoom,
|
|
baseHeight * this.zoom,
|
|
];
|
|
return (
|
|
<svg className={styles.graph} viewBox={viewBox} ref={this.elementRef}>
|
|
<defs>
|
|
<marker id="markerArrow" markerWidth="4" markerHeight="4" refX="2" refY="2" orient="auto">
|
|
<path d="M0,0 L0,4 L4,2 L0,0" className={styles.arrow} />
|
|
</marker>
|
|
<marker id="markerArrowSelected" markerWidth="4" markerHeight="4" refX="2" refY="2" orient="auto">
|
|
<path d="M0,0 L0,4 L4,2 L0,0" className={classes(styles.arrow, styles.selected)} />
|
|
</marker>
|
|
<marker id="markerArrowVisited" markerWidth="4" markerHeight="4" refX="2" refY="2" orient="auto">
|
|
<path d="M0,0 L0,4 L4,2 L0,0" className={classes(styles.arrow, styles.visited)} />
|
|
</marker>
|
|
</defs>
|
|
{
|
|
edges.sort((a, b) => a.visitedCount - b.visitedCount).map(edge => {
|
|
const { source, target, weight, visitedCount, selectedCount } = edge;
|
|
const sourceNode = this.props.data.findNode(source);
|
|
const targetNode = this.props.data.findNode(target);
|
|
if (!sourceNode || !targetNode) return undefined;
|
|
const { x: sx, y: sy } = sourceNode;
|
|
let { x: ex, y: ey } = targetNode;
|
|
const mx = (sx + ex) / 2;
|
|
const my = (sy + ey) / 2;
|
|
const dx = ex - sx;
|
|
const dy = ey - sy;
|
|
const degree = Math.atan2(dy, dx) / Math.PI * 180;
|
|
if (isDirected) {
|
|
const length = Math.sqrt(dx * dx + dy * dy);
|
|
if (length !== 0) {
|
|
ex = sx + dx / length * (length - nodeRadius - arrowGap);
|
|
ey = sy + dy / length * (length - nodeRadius - arrowGap);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<g className={classes(styles.edge, selectedCount && styles.selected, visitedCount && styles.visited)}
|
|
key={`${source}-${target}`}>
|
|
<path d={`M${sx},${sy} L${ex},${ey}`} className={classes(styles.line, isDirected && styles.directed)} />
|
|
{
|
|
isWeighted &&
|
|
<g transform={`translate(${mx},${my})`}>
|
|
<text className={styles.weight} transform={`rotate(${degree})`}
|
|
y={-edgeWeightGap}>{this.toString(weight)}</text>
|
|
</g>
|
|
}
|
|
</g>
|
|
);
|
|
})
|
|
}
|
|
{
|
|
nodes.map(node => {
|
|
const { id, x, y, weight, visitedCount, selectedCount } = node;
|
|
return (
|
|
<g className={classes(styles.node, selectedCount && styles.selected, visitedCount && styles.visited)}
|
|
key={id} transform={`translate(${x},${y})`}>
|
|
<circle className={styles.circle} r={nodeRadius} />
|
|
<text className={styles.id}>{id}</text>
|
|
{
|
|
isWeighted &&
|
|
<text className={styles.weight} x={nodeRadius + nodeWeightGap}>{this.toString(weight)}</text>
|
|
}
|
|
</g>
|
|
);
|
|
})
|
|
}
|
|
</svg>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default GraphRenderer;
|
|
|