|
@@ -4,15 +4,15 @@
|
|
|
import * as React from 'react';
|
|
|
import * as d3 from "d3";
|
|
|
|
|
|
-namespace d3Types {
|
|
|
+export namespace d3Types {
|
|
|
export type d3Node = {
|
|
|
id: string,
|
|
|
group: number
|
|
|
};
|
|
|
|
|
|
export type d3Link = {
|
|
|
- source: string,
|
|
|
- target: string,
|
|
|
+ source: any, // initially string, but d3 replaces it by an object (lol)
|
|
|
+ target: any, // initially string, but d3 replaces it by an object (lol)
|
|
|
value: number
|
|
|
};
|
|
|
|
|
@@ -36,14 +36,16 @@ class Link extends React.Component<{ link: d3Types.d3Link }, {}> {
|
|
|
}
|
|
|
|
|
|
render() {
|
|
|
- return <line className="link" ref={this.ref} marker-end="url(#arrow2)"/>;
|
|
|
+ return <line className="link" ref={this.ref} markerEnd="url(#arrow2)"/>;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
class Links extends React.Component<{ links: d3Types.d3Link[] }, {}> {
|
|
|
render() {
|
|
|
+ const nodeId = sourceOrTarget => sourceOrTarget.id ? sourceOrTarget.id : sourceOrTarget;
|
|
|
+ const key = link => 's'+nodeId(link.source)+'t'+nodeId(link.target);
|
|
|
const links = this.props.links.map((link: d3Types.d3Link, index: number) => {
|
|
|
- return <Link key={index} link={link} />;
|
|
|
+ return <Link key={key(link)} link={link} />;
|
|
|
});
|
|
|
|
|
|
return (
|
|
@@ -54,8 +56,16 @@ class Links extends React.Component<{ links: d3Types.d3Link[] }, {}> {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+interface NodeProps {
|
|
|
+ node: d3Types.d3Node;
|
|
|
+ color: string;
|
|
|
+ simulation: any;
|
|
|
+ mouseDownHandler: (event) => void;
|
|
|
+ mouseUpHandler: (event) => void;
|
|
|
+}
|
|
|
|
|
|
-class Node extends React.Component<{ node: d3Types.d3Node, color: string }, {}> {
|
|
|
+
|
|
|
+class Node extends React.Component<NodeProps, {}> {
|
|
|
ref: React.RefObject<SVGCircleElement>;
|
|
|
|
|
|
constructor(props) {
|
|
@@ -65,67 +75,77 @@ class Node extends React.Component<{ node: d3Types.d3Node, color: string }, {}>
|
|
|
|
|
|
componentDidMount() {
|
|
|
d3.select(this.ref.current).data([this.props.node]);
|
|
|
+
|
|
|
+ const onDragStart = (event, d: any) => {
|
|
|
+ if (!event.active) {
|
|
|
+ this.props.simulation.alphaTarget(0.3).restart();
|
|
|
+ }
|
|
|
+ d.fx = d.x;
|
|
|
+ d.fy = d.y;
|
|
|
+ }
|
|
|
+ const onDrag = (event, d: any) => {
|
|
|
+ d.fx = event.x;
|
|
|
+ d.fy = event.y;
|
|
|
+ }
|
|
|
+ const onDragEnd = (event, d: any) => {
|
|
|
+ if (!event.active) {
|
|
|
+ this.props.simulation.alphaTarget(0);
|
|
|
+ }
|
|
|
+ d.fx = null;
|
|
|
+ d.fy = null;
|
|
|
+ }
|
|
|
+ const dragBehavior = d3.drag()
|
|
|
+ .on("start", onDragStart)
|
|
|
+ .on("drag", onDrag)
|
|
|
+ .on("end", onDragEnd);
|
|
|
+ if (this.ref.current !== null) {
|
|
|
+ // @ts-ignore: Doesn't work
|
|
|
+ dragBehavior(d3.select(this.ref.current));
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
render() {
|
|
|
+ console.log("Node.render");
|
|
|
+
|
|
|
return (
|
|
|
- <circle className="node" r={5} fill={this.props.color}
|
|
|
- ref={this.ref}>
|
|
|
+ <circle
|
|
|
+ ref={this.ref}
|
|
|
+ className="node"
|
|
|
+ onMouseDown={this.props.mouseDownHandler}
|
|
|
+ onMouseUp={this.props.mouseUpHandler}
|
|
|
+ r={5}
|
|
|
+ fill={this.props.color}
|
|
|
+ >
|
|
|
<title>{this.props.node.id}</title>
|
|
|
</circle>
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-class Nodes extends React.Component<{ nodes: d3Types.d3Node[], simulation: any }, {}> {
|
|
|
+interface NodesProps {
|
|
|
+ nodes: d3Types.d3Node[],
|
|
|
+ simulation: any,
|
|
|
+ mouseDownHandler: (event, node) => void;
|
|
|
+ mouseUpHandler: (event, node) => void;
|
|
|
+}
|
|
|
+
|
|
|
+class Nodes extends React.Component<NodesProps, {}> {
|
|
|
ref: React.RefObject<SVGGElement>;
|
|
|
- // state: {nodes: d3Types.d3Node[]};
|
|
|
|
|
|
constructor(props) {
|
|
|
super(props);
|
|
|
console.log("construct Nodes")
|
|
|
this.ref = React.createRef<SVGGElement>();
|
|
|
- // this.state = {nodes: props.nodes};
|
|
|
- }
|
|
|
-
|
|
|
- componentDidMount() {
|
|
|
- }
|
|
|
-
|
|
|
- componentDidUpdate() {
|
|
|
- const simulation = this.props.simulation;
|
|
|
- d3.select(this.ref.current).selectAll(".node")
|
|
|
- // @ts-ignore: Doesn't work
|
|
|
- .call(d3.drag()
|
|
|
- .on("start", onDragStart)
|
|
|
- .on("drag", onDrag)
|
|
|
- .on("end", onDragEnd));
|
|
|
-
|
|
|
- function onDragStart(event, d: any) {
|
|
|
- if (!event.active) {
|
|
|
- simulation.alphaTarget(0.3).restart();
|
|
|
- }
|
|
|
- d.fx = d.x;
|
|
|
- d.fy = d.y;
|
|
|
- }
|
|
|
-
|
|
|
- function onDrag(event, d: any) {
|
|
|
- d.fx = event.x;
|
|
|
- d.fy = event.y;
|
|
|
- }
|
|
|
-
|
|
|
- function onDragEnd(event, d: any) {
|
|
|
- if (!event.active) {
|
|
|
- simulation.alphaTarget(0);
|
|
|
- }
|
|
|
- d.fx = null;
|
|
|
- d.fy = null;
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
render() {
|
|
|
const color = d3.scaleOrdinal(d3.schemeCategory10);
|
|
|
const nodes = this.props.nodes.map((node: d3Types.d3Node, index: number) => {
|
|
|
- return <Node key={index} node={node} color={color(node.group.toString())} />;
|
|
|
+ return <Node
|
|
|
+ key={node.id}
|
|
|
+ mouseDownHandler={event => this.props.mouseDownHandler(event, node)}
|
|
|
+ mouseUpHandler={event => this.props.mouseUpHandler(event, node)}
|
|
|
+ node={node} color={color(node.group.toString())} simulation={this.props.simulation} />;
|
|
|
});
|
|
|
|
|
|
return (
|
|
@@ -158,7 +178,7 @@ class Label extends React.Component<{ node: d3Types.d3Node }, {}> {
|
|
|
class Labels extends React.Component<{ nodes: d3Types.d3Node[] }, {}> {
|
|
|
render() {
|
|
|
const labels = this.props.nodes.map((node: d3Types.d3Node, index: number) => {
|
|
|
- return <Label key={index} node={node} />;
|
|
|
+ return <Label key={node.id} node={node} />;
|
|
|
});
|
|
|
|
|
|
return (
|
|
@@ -170,30 +190,27 @@ class Labels extends React.Component<{ nodes: d3Types.d3Node[] }, {}> {
|
|
|
}
|
|
|
|
|
|
|
|
|
-interface Props {
|
|
|
- width: number;
|
|
|
- height: number;
|
|
|
+export interface GraphProps {
|
|
|
graph: d3Types.d3Graph;
|
|
|
- onClick: (SyntheticBaseEvent) => void;
|
|
|
+ // clickHandler?: (event:React.SyntheticEvent, svgCoords: {svgX: number, svgY: number}) => void;
|
|
|
+ mouseDownHandler: (e: React.SyntheticEvent, svgCoords: {x: number, y: number}, node?: d3Types.d3Node) => void;
|
|
|
+ mouseUpHandler: (e: React.SyntheticEvent, svgCoords: {x: number, y: number}, node?: d3Types.d3Node) => void;
|
|
|
}
|
|
|
|
|
|
-interface GraphViewState {
|
|
|
+interface GraphState {
|
|
|
nodes: d3Types.d3Node[],
|
|
|
links: d3Types.d3Link[],
|
|
|
}
|
|
|
|
|
|
-export class GraphView extends React.Component<Props, {}> {
|
|
|
+export class Graph extends React.Component<GraphProps, {}> {
|
|
|
simulation: any;
|
|
|
refSVG: React.RefObject<SVGSVGElement>;
|
|
|
- state: GraphViewState;
|
|
|
+ state: GraphState;
|
|
|
|
|
|
link: any;
|
|
|
node: any;
|
|
|
label: any;
|
|
|
|
|
|
- nextId: number = 0;
|
|
|
-
|
|
|
-
|
|
|
constructor(props) {
|
|
|
super(props);
|
|
|
this.state = {
|
|
@@ -212,14 +229,39 @@ export class GraphView extends React.Component<Props, {}> {
|
|
|
// this.simulation.force("link").links(this.props.graph.links);
|
|
|
}
|
|
|
|
|
|
- addNode = (id, x,y) => {
|
|
|
- this.setState((prevState: GraphViewState) => ({
|
|
|
- nodes: [...prevState.nodes, { id, group: 8, x, y }],
|
|
|
+ createNode = (id, svgX,svgY) => {
|
|
|
+ this.setState((prevState: GraphState) => ({
|
|
|
+ nodes: [...prevState.nodes, { id, group: 8, x: svgX, y: svgY }],
|
|
|
}));
|
|
|
}
|
|
|
|
|
|
- handleClick = event => {
|
|
|
- if (event.detail === 2) { // Double-click
|
|
|
+ deleteNode = (id) => {
|
|
|
+ this.setState((prevState: GraphState) => {
|
|
|
+ console.log("DELETING NODE", id)
|
|
|
+ console.log("PREVSTATE:",prevState);
|
|
|
+ const newLinks = prevState.links.filter(l => l.source.id !== id && l.target.id !== id);
|
|
|
+ console.log("NEWLINKS:",newLinks)
|
|
|
+ return ({
|
|
|
+ nodes: prevState.nodes.filter(n => n.id !== id),
|
|
|
+ links: newLinks,
|
|
|
+ })})
|
|
|
+ }
|
|
|
+
|
|
|
+ createLink = (source, target) => {
|
|
|
+ this.setState((prevState: GraphState) => ({
|
|
|
+ links: [...prevState.links, {source, target, value: 1}],
|
|
|
+ }));
|
|
|
+ }
|
|
|
+
|
|
|
+ render() {
|
|
|
+ console.log("Graph.render")
|
|
|
+ const { graph } = this.props;
|
|
|
+
|
|
|
+ const width = 600;
|
|
|
+ const height = 600;
|
|
|
+
|
|
|
+ const clientToSvgCoords = (event, callback) => {
|
|
|
+ // console.log(event);
|
|
|
// Translate mouse event to SVG coordinates:
|
|
|
if (this.refSVG.current !== null) {
|
|
|
const ctm = this.refSVG.current.getScreenCTM();
|
|
@@ -227,30 +269,34 @@ export class GraphView extends React.Component<Props, {}> {
|
|
|
const pt = this.refSVG.current.createSVGPoint();
|
|
|
pt.x = event.clientX;
|
|
|
pt.y = event.clientY;
|
|
|
- const {x, y} = pt.matrixTransform(ctm.inverse());
|
|
|
- this.addNode(this.nextId++, x, y);
|
|
|
+ callback(event, pt.matrixTransform(ctm.inverse()));
|
|
|
}
|
|
|
- }
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- render() {
|
|
|
- const { width, height, graph } = this.props;
|
|
|
|
|
|
return (
|
|
|
<svg className="container"
|
|
|
ref={this.refSVG}
|
|
|
- width={"100%"} height={"100%"}
|
|
|
+ style={{width: "100%", height}}
|
|
|
viewBox={`${-width/2} ${-height/2} ${width} ${height}`}
|
|
|
- onClick={this.handleClick}>
|
|
|
+ onMouseDown={e => clientToSvgCoords(e, this.props.mouseDownHandler)}
|
|
|
+ onMouseUp={e => clientToSvgCoords(e, this.props.mouseUpHandler)}
|
|
|
+ onContextMenu={e => e.preventDefault()}
|
|
|
+ >
|
|
|
+
|
|
|
<defs>
|
|
|
<marker id="arrow2" markerWidth="10" markerHeight="10" refX="14" refY="3" orient="auto" markerUnits="strokeWidth">
|
|
|
<path d="M0,0 L0,6 L9,3 z" fill="#fff" />
|
|
|
</marker>
|
|
|
</defs>
|
|
|
+
|
|
|
<Links links={this.state.links} />
|
|
|
<Labels nodes={this.state.nodes} />
|
|
|
- <Nodes nodes={this.state.nodes} simulation={this.simulation} />
|
|
|
+ <Nodes nodes={this.state.nodes} simulation={this.simulation}
|
|
|
+ mouseDownHandler={(e, node) => clientToSvgCoords(e, (e,coords) => this.props.mouseDownHandler(e,coords,node))}
|
|
|
+ mouseUpHandler={(e, node) => clientToSvgCoords(e, (e,coords) => this.props.mouseUpHandler(e,coords,node))}
|
|
|
+ />
|
|
|
+
|
|
|
</svg>
|
|
|
);
|
|
|
}
|
|
@@ -301,6 +347,7 @@ export class GraphView extends React.Component<Props, {}> {
|
|
|
}
|
|
|
|
|
|
componentDidUpdate() {
|
|
|
+ console.log("UDPATED STATE:",this.state)
|
|
|
this.update();
|
|
|
this.simulation.alpha(1).restart().tick();
|
|
|
this.ticked();
|