[]): JSX.Element[];
56 | render(): 0 | JSX.Element;
57 | }
58 | export {};
59 |
--------------------------------------------------------------------------------
/dist/components/with-foreign-object.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | interface WithForeignObjectProps {
3 | width: number;
4 | height: number;
5 | x: number;
6 | y: number;
7 | }
8 | export declare const withForeignObject: (WrappedSVGComponent: React.ComponentType
) => React.FunctionComponent
;
9 | export {};
10 |
--------------------------------------------------------------------------------
/dist/core/graph-matrix.class.d.ts:
--------------------------------------------------------------------------------
1 | import { INodeInput, INodeOutput } from "./node.interface";
2 | import { TraverseQueue } from "./traverse-queue.class";
3 | import { Matrix } from "./matrix.class";
4 | import { GraphStruct } from "./graph-struct.class";
5 | /**
6 | * Holds iteration state of the graph
7 | */
8 | interface State {
9 | mtx: Matrix;
10 | queue: TraverseQueue;
11 | x: number;
12 | y: number;
13 | }
14 | /**
15 | * @class GraphMatrix
16 | * Compute graph subclass used to interact with matrix
17 | */
18 | export declare class GraphMatrix extends GraphStruct {
19 | constructor(list: INodeInput[]);
20 | /**
21 | * Check if item has unresolved incomes
22 | * @param item item to check
23 | */
24 | protected _joinHasUnresolvedIncomes(item: INodeOutput): boolean;
25 | /**
26 | * Main insertion method - inserts item on matrix using state x and y
27 | * or skips if it has collision on current row. Skipping is done
28 | * by passing item back to the end of the queue
29 | * @param item item to insert
30 | * @param state state of current iteration
31 | * @param checkCollision whether to check horizontal collision with existing point
32 | * on 2D matrix
33 | * @returns true if item was inserted false if skipped
34 | */
35 | private _insertOrSkipNodeOnMatrix;
36 | /**
37 | * Get all items incomes and find parent Y with the lowest
38 | * Y coordinate on the matrix
39 | * @param item target item
40 | * @param mtx matrix to use as source
41 | */
42 | private _getLowestYAmongIncomes;
43 | /**
44 | * Main processing nodes method.
45 | * If node has incomes it finds lowest Y among them and
46 | * sets state.y as lowest income Y value.
47 | * Then inserts item on matrix using state x and y
48 | * or skips if it has collision on current column. Skipping is done
49 | * by passing item back to the end of the queue
50 | * @param item item to insert
51 | * @param state state of current iteration
52 | * on 2D matrix
53 | * @returns true if item was inserted false if skipped
54 | */
55 | protected _processOrSkipNodeOnMatrix(item: INodeOutput, state: State): boolean;
56 | private hasLoops;
57 | private _handleLoopEdges;
58 | private _markIncomesAsPassed;
59 | protected _resolveCurrentJoinIncomes(mtx: Matrix, join: INodeOutput): void;
60 | private _insertLoopEdges;
61 | /**
62 | * Insert outcomes of split node
63 | * @param item item to handle
64 | * @param state current state of iteration
65 | * @param levelQueue
66 | */
67 | protected _insertSplitOutcomes(item: INodeOutput, state: State, levelQueue: TraverseQueue): void;
68 | /**
69 | * Insert incomes of join node
70 | * @param item item to handle
71 | * @param state current state of iteration
72 | * @param levelQueue
73 | * @param addItemToQueue
74 | */
75 | protected _insertJoinIncomes(item: INodeOutput, state: State, levelQueue: TraverseQueue, addItemToQueue: boolean): void;
76 | }
77 | export {};
78 |
--------------------------------------------------------------------------------
/dist/core/graph-struct.class.d.ts:
--------------------------------------------------------------------------------
1 | import { INodeInput, NodeType } from "./node.interface";
2 | /**
3 | * @class GraphStruct
4 | * Frame parent-class to simplify graph
5 | * elements recognition
6 | */
7 | export declare class GraphStruct {
8 | protected _list: INodeInput[];
9 | private _nodesMap;
10 | private _incomesByNodeIdMap;
11 | private _outcomesByNodeIdMap;
12 | private _loopsByNodeIdMap;
13 | constructor(list: INodeInput[]);
14 | /**
15 | * Fill graph with new nodes
16 | * @param list input linked list of nodes
17 | */
18 | applyList(list: INodeInput[]): void;
19 | detectIncomesAndOutcomes(): void;
20 | traverseVertically(node: INodeInput, branchSet: Set, totalSet: Set): Set;
21 | /**
22 | * Get graph roots.
23 | * Roots is nodes without incomes
24 | */
25 | roots(): INodeInput[];
26 | /**
27 | * Get type of node
28 | * @param id id of node
29 | * @returns type of the node
30 | */
31 | protected nodeType(id: string): NodeType;
32 | /**
33 | * Whether or node is split
34 | * @param id id of node
35 | */
36 | private isSplit;
37 | /**
38 | * Whether or node is join
39 | * @param id id of node
40 | */
41 | private isJoin;
42 | /**
43 | * Whether or node is root
44 | * @param id id of node
45 | */
46 | private isRoot;
47 | protected isLoopEdge(nodeId: string, outcomeId: string): boolean;
48 | /**
49 | * Get loops of node by id
50 | * @param id id of node
51 | */
52 | protected loops(id: string): string[];
53 | /**
54 | * Get outcomes of node by id
55 | * @param id id of node
56 | */
57 | protected outcomes(id: string): string[];
58 | /**
59 | * Get incomes of node by id
60 | * @param id id of node
61 | */
62 | protected incomes(id: string): string[];
63 | /**
64 | * Get node by id
65 | * @param id node id
66 | */
67 | protected node(id: string): INodeInput;
68 | /**
69 | * get outcomes inputs helper
70 | * @param itemId node id
71 | */
72 | protected getOutcomesArray(itemId: string): INodeInput[];
73 | }
74 |
--------------------------------------------------------------------------------
/dist/core/graph.class.d.ts:
--------------------------------------------------------------------------------
1 | import { INodeInput } from "./node.interface";
2 | import { Matrix } from "./matrix.class";
3 | import { GraphMatrix } from "./graph-matrix.class";
4 | /**
5 | * @class Graph
6 | * Main iteration class used to transform
7 | * linked list of nodes to coordinate matrix
8 | */
9 | export declare class Graph extends GraphMatrix {
10 | constructor(list: INodeInput[]);
11 | /**
12 | * Function to handle split nodes
13 | * @param item item to handle
14 | * @param state current state of iteration
15 | * @param levelQueue buffer subqueue of iteration
16 | */
17 | private _handleSplitNode;
18 | /**
19 | * Function to handle splitjoin nodes
20 | * @param item item to handle
21 | * @param state current state of iteration
22 | * @param levelQueue buffer subqueue of iteration
23 | */
24 | private _handleSplitJoinNode;
25 | /**
26 | * Function to handle join nodes
27 | * @param item item to handle
28 | * @param state current state of iteration
29 | * @param levelQueue buffer subqueue of iteration
30 | */
31 | private _handleJoinNode;
32 | /**
33 | * Function to handle simple nodes
34 | * @param item item to handle
35 | * @param state current state of iteration
36 | * @param levelQueue buffer subqueue of iteration
37 | */
38 | private _handleSimpleNode;
39 | /**
40 | * Method to handle single iteration item
41 | * @param item queue item to process
42 | * @param state state of iteration
43 | * @param levelQueue
44 | */
45 | private _traverseItem;
46 | /**
47 | * Iterate over one level of graph
48 | * starting from queue top item
49 | */
50 | private _traverseLevel;
51 | /**
52 | * Iterate over graph
53 | * starting from queue root items
54 | */
55 | private _traverseList;
56 | /**
57 | * traverse main method to get coordinates matrix from graph
58 | * @returns 2D matrix containing all nodes and anchors
59 | */
60 | traverse(): Matrix;
61 | }
62 |
--------------------------------------------------------------------------------
/dist/core/index.d.ts:
--------------------------------------------------------------------------------
1 | export * from "./graph.class";
2 | export * from "./matrix.class";
3 | export * from "./traverse-queue.class";
4 | export * from "./node.interface";
5 |
--------------------------------------------------------------------------------
/dist/core/matrix.class.d.ts:
--------------------------------------------------------------------------------
1 | import { INodeOutput, IMatrixNode } from "./node.interface";
2 | /**
3 | * @class Matrix
4 | * Low level class used to compute 2D polar coordinates for each node
5 | * and anchor. Use this class if you want to skip D3 rendering in favor of
6 | * something else, for example, HTML or Canvas drawing.
7 | */
8 | export declare class Matrix {
9 | private _;
10 | /**
11 | * Get with of matrix
12 | */
13 | readonly width: number;
14 | /**
15 | * Get height of matrix
16 | */
17 | readonly height: number;
18 | /**
19 | * Checks whether or not candidate point collides
20 | * with present points by X vertex.
21 | * @param point coordinates of point to check
22 | */
23 | hasHorizontalCollision([_, y]: number[]): boolean;
24 | /**
25 | * Checks whether or not candidate point collides
26 | * with present points by Y vertex.
27 | * @param point coordinates of point to check
28 | */
29 | hasVerticalCollision([x, y]: number[]): boolean;
30 | /**
31 | * Check if all next items of node already placed in matrix
32 | */
33 | private isAllChildrenOnMatrix;
34 | /**
35 | * Inspects matrix by Y vertex from top to bottom to
36 | * search first unused Y coordinate (row).
37 | * If there no free row on the matrix it returns
38 | * matrix height (Which is equal to first unused row,
39 | * that currently not exist).
40 | * @param x column coordinate to use for search
41 | */
42 | getFreeRowForColumn(x: number): number;
43 | /**
44 | * Extend matrix with empty rows
45 | * @param toValue rows to add to matrix
46 | */
47 | private _extendHeight;
48 | /**
49 | * Extend matrix with empty columns
50 | * @param toValue columns to add to matrix
51 | */
52 | private _extendWidth;
53 | /**
54 | * Insert row before y
55 | * @param y coordinate
56 | */
57 | insertRowBefore(y: number): void;
58 | /**
59 | * Insert column before x
60 | * @param x coordinate
61 | */
62 | insertColumnBefore(x: number): void;
63 | /**
64 | * Find x, y coordinate of first point item that
65 | * satisfies condition defined in callback
66 | * @param callback similar to [].find. Returns boolean
67 | */
68 | find(callback: (item: INodeOutput) => boolean): number[] | null;
69 | /**
70 | * Find first node item that
71 | * satisfies condition defined in callback
72 | * @param callback similar to [].find. Returns boolean
73 | */
74 | findNode(callback: (item: INodeOutput) => boolean): [number[], INodeOutput] | null;
75 | /**
76 | * Return point by x, y coordinate
77 | */
78 | getByCoords(x: number, y: number): INodeOutput | null;
79 | /**
80 | * Paste item to particular cell
81 | * @param coords x and y coordinates for item
82 | * @param item item to insert
83 | */
84 | insert([x, y]: number[], item: INodeOutput): void;
85 | /**
86 | * @returns key value object where key is node id and
87 | * value is node with its coordinates
88 | */
89 | normalize(): {
90 | [id: string]: IMatrixNode;
91 | };
92 | }
93 |
--------------------------------------------------------------------------------
/dist/core/node.interface.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Types of nodes for internal usage
3 | */
4 | export declare enum NodeType {
5 | RootSimple = "ROOT-SIMPLE",
6 | RootSplit = "ROOT-SPLIT",
7 | Simple = "SIMPLE",
8 | Split = "SPLIT",
9 | Join = "JOIN",
10 | SplitJoin = "SPLIT-JOIN"
11 | }
12 | /**
13 | * Types of anchors.
14 | * Join is anchor (vertex) between node and its join outcome:
15 | * N-J
16 | * N_|
17 | * N_|
18 | * Split is anchor (vertex) between node and its split income:
19 | * S-N
20 | * |_N
21 | * |_N
22 | */
23 | export declare enum AnchorType {
24 | Join = "JOIN",
25 | Split = "SPLIT",
26 | Loop = "LOOP"
27 | }
28 | export declare enum AnchorMargin {
29 | None = "NONE",
30 | Left = "LEFT",
31 | Right = "RIGHT"
32 | }
33 | export interface INodeInput {
34 | /**
35 | * Unique key for node. Duplicates are not allowed.
36 | */
37 | id: string;
38 | /**
39 | * Outcomes of current node. Empty array if node is leaf.
40 | */
41 | next: string[];
42 | /**
43 | * Name of current node. Empty if the node without a name.
44 | */
45 | name?: string;
46 | /**
47 | * Name of current node. Empty if the node orientation is bottom.
48 | */
49 | nameOrientation?: "bottom" | "top";
50 | /**
51 | * Name of node edges. Matched to the edge index with the next. Empty array if the edges without a name.
52 | */
53 | edgeNames?: string[];
54 | /**
55 | * Payload data to transfer with current node events. Use whatever you want here.
56 | */
57 | payload: T;
58 | }
59 | export interface INodeOutput extends INodeInput {
60 | /**
61 | * Defines whether or not node is pseudo-node - anchor.
62 | * Which is used to draw split and join edges.
63 | * @default false
64 | */
65 | isAnchor?: boolean;
66 | /**
67 | * Type of anchor. Only exists if isAnchor is true.
68 | */
69 | anchorType?: AnchorType;
70 | /**
71 | * Id if the anchor income. Only exists if isAnchor is true.
72 | */
73 | anchorFrom?: string;
74 | /**
75 | * Id if the anchor outcome. Only exists if isAnchor is true.
76 | */
77 | anchorTo?: string;
78 | /**
79 | * Anchor position inside cell over x axis.
80 | */
81 | anchorMargin?: AnchorMargin;
82 | /**
83 | * First level node incomes passed during traversal. Ignores join
84 | * anchor. Mostly for tech usage. To recognize rendering parents
85 | * Use renderIncomes.
86 | */
87 | passedIncomes: string[];
88 | /**
89 | * First level node incomes in rendering context. Can be used for
90 | * backward travesal. Includes both types of anchors.
91 | */
92 | renderIncomes: string[];
93 | /**
94 | * Number of outcomes that already been placed on matrix
95 | */
96 | childrenOnMatrix: number;
97 | }
98 | export interface IMatrixNode extends INodeOutput {
99 | /**
100 | * X coordinate of node
101 | */
102 | x: number;
103 | /**
104 | * Y coordinate of node
105 | */
106 | y: number;
107 | }
108 |
--------------------------------------------------------------------------------
/dist/core/traverse-queue.class.d.ts:
--------------------------------------------------------------------------------
1 | import { INodeOutput } from "./node.interface";
2 | export interface IQueueItem {
3 | id: string;
4 | payload: T;
5 | next: string[];
6 | name?: string;
7 | nameOrientation?: "bottom" | "top";
8 | edgeNames?: string[];
9 | }
10 | /**
11 | * @class TraverseQueue
12 | * Special queue that is used for horizontal
13 | * graph traversing
14 | */
15 | export declare class TraverseQueue {
16 | private _;
17 | /**
18 | * Add items to queue. If items already exist in this queue
19 | * or bufferQueue do nothing but push new passed income to
20 | * existing queue item
21 | * @param incomeId income id for each element
22 | * @param bufferQueue buffer queue to also check for duplicates
23 | * @param items queue items to add
24 | */
25 | add(incomeId: string | null, bufferQueue: TraverseQueue | null, ...items: IQueueItem[]): void;
26 | find(cb: (item: INodeOutput) => boolean): INodeOutput | void;
27 | /**
28 | * Push item to queue. Skipping `add` method additional phases.
29 | * @param item node item to add
30 | */
31 | push(item: INodeOutput): void;
32 | /**
33 | * get current queue length
34 | */
35 | readonly length: number;
36 | /**
37 | * @param cb callback with condition to check
38 | * @returns true if at list one item satified condition in callback
39 | */
40 | some(cb: (item: INodeOutput) => boolean): boolean;
41 | /**
42 | * Shift first element
43 | * @returns first element from the queue
44 | */
45 | shift(): INodeOutput | void;
46 | /**
47 | * Create new queue and extract of current
48 | * elements of this queue new clone
49 | * @returns newQueue new queue with items from old queue
50 | */
51 | drain(): TraverseQueue;
52 | }
53 |
--------------------------------------------------------------------------------
/dist/fixtures/index.d.ts:
--------------------------------------------------------------------------------
1 | export * from "./with-joins-and-splits";
2 | export * from "./with-joins-and-splits-one-root";
3 |
--------------------------------------------------------------------------------
/dist/fixtures/with-joins-and-splits-one-root.d.ts:
--------------------------------------------------------------------------------
1 | export declare const withJoinsAndSplitsFixtureOneRoot: {
2 | id: string;
3 | next: string[];
4 | payload: {
5 | exist: boolean;
6 | };
7 | }[];
8 |
--------------------------------------------------------------------------------
/dist/fixtures/with-joins-and-splits.d.ts:
--------------------------------------------------------------------------------
1 | export declare const withJoinsAndSplitsFixture: {
2 | id: string;
3 | next: string[];
4 | payload: {
5 | exist: boolean;
6 | };
7 | }[];
8 |
--------------------------------------------------------------------------------
/dist/index.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @class DirectGraph
3 | */
4 | import * as React from "react";
5 | import { INodeInput, IMatrixNode } from "./core";
6 | import { ViewProps } from "./components";
7 | declare type Props = {
8 | list: INodeInput[];
9 | cellSize: number;
10 | padding: number;
11 | };
12 | export { INodeInput, IMatrixNode } from "./core";
13 | export declare type GraphProps = Props & ViewProps;
14 | declare type GraphViewData = {
15 | nodesMap: {
16 | [id: string]: IMatrixNode;
17 | };
18 | widthInCells: number;
19 | heightInCells: number;
20 | };
21 | export default class DirectGraph extends React.Component> {
22 | getNodesMap: (list: INodeInput[]) => GraphViewData;
23 | render(): JSX.Element;
24 | }
25 |
--------------------------------------------------------------------------------
/dist/test.d.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
--------------------------------------------------------------------------------
/dist/utils/index.d.ts:
--------------------------------------------------------------------------------
1 | import { AnchorMargin, IMatrixNode, INodeOutput } from "../core";
2 | import { GraphEventFunc } from "../components/element";
3 | import * as React from "react";
4 | export declare enum VectorDirection {
5 | Top = "top",
6 | Bottom = "bottom",
7 | Right = "right",
8 | Left = "left"
9 | }
10 | export declare const getEdgeMargins: (node: INodeOutput, income: INodeOutput) => AnchorMargin[];
11 | export declare const getVectorDirection: (x1: number, y1: number, x2: number, y2: number) => VectorDirection;
12 | export declare const getCellCenter: (cellSize: number, padding: number, cellX: number, cellY: number, margin: AnchorMargin) => number[];
13 | export declare const getCellEntry: (direction: VectorDirection, cellSize: number, padding: number, cellX: number, cellY: number, margin: AnchorMargin) => number[];
14 | export declare function uniqueId(prefix: string): string;
15 | export declare function getSize(cellSize: number, padding: number): number;
16 | export declare function getCoords(cellSize: number, padding: number, node: IMatrixNode): number[];
17 | export declare function checkAnchorRenderIncomes(node: IMatrixNode): void;
18 | export declare const getAllIncomes: (node: IMatrixNode, nodesMap: {
19 | [id: string]: IMatrixNode;
20 | }) => IMatrixNode[];
21 | export declare const wrapEventHandler: (cb: GraphEventFunc, node: IMatrixNode, incomes: IMatrixNode[]) => (e: React.MouseEvent) => void;
22 |
--------------------------------------------------------------------------------
/example/config-overrides.js:
--------------------------------------------------------------------------------
1 | const rewireRawLoader = require("@baristalabs/react-app-rewire-raw-loader");
2 |
3 | module.exports = function override(config, env) {
4 | config = rewireRawLoader(config, env);
5 | return config;
6 | };
7 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-direct-graph-example",
3 | "homepage": "https://lempiy.github.io/react-direct-graph",
4 | "version": "0.0.0",
5 | "license": "MIT",
6 | "private": true,
7 | "dependencies": {
8 | "ajv": "^6.10.0",
9 | "prop-types": "^15.6.2",
10 | "react": "^16.4.1",
11 | "react-direct-graph": "file:..",
12 | "react-dom": "^16.4.1",
13 | "react-scripts": "^1.1.4",
14 | "react-syntax-highlighter": "^10.3.0"
15 | },
16 | "scripts": {
17 | "start": "react-app-rewired start",
18 | "build": "react-app-rewired build",
19 | "test": "react-app-rewired test --env=jsdom",
20 | "eject": "react-app-rewired eject"
21 | },
22 | "devDependencies": {
23 | "@baristalabs/react-app-rewire-raw-loader": "^0.1.3",
24 | "raw-loader": "^3.0.0",
25 | "react-app-rewired": "^2.1.3"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/example/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | react-direct-graph
11 |
12 |
13 |
14 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/example/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "react-direct-graph",
3 | "name": "react-direct-graph",
4 | "start_url": "./index.html",
5 | "display": "standalone",
6 | "theme_color": "#000000",
7 | "background_color": "#ffffff"
8 | }
9 |
--------------------------------------------------------------------------------
/example/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | import { Editor, Basic, WithNames, Complex, Custom, Events, Title } from "./components";
4 | const cellSize = 130;
5 | const padding = cellSize * 0.125;
6 | export default class App extends Component {
7 | render() {
8 | return (
9 |
10 |
11 | Examples
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/example/src/components/Example.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
3 | import { darcula } from "react-syntax-highlighter/dist/esm/styles/prism";
4 |
5 | export class Example extends Component {
6 | constructor(props) {
7 | super(props);
8 | this.state = {
9 | showCode: false
10 | };
11 | }
12 | toggleCode = () => {
13 | this.setState({
14 | showCode: !this.state.showCode
15 | });
16 | };
17 | render() {
18 | const { children, title, code, description } = this.props;
19 | return (
20 |
21 | {title}
22 |
26 |
32 |
37 | {code}
38 |
39 |
40 | Output:
41 |
42 | {children}
43 |
44 | );
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/example/src/components/Title.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from "react";
2 |
3 | import DirectGraph from "react-direct-graph";
4 |
5 | const react = [
6 | {
7 | id: "R",
8 | next: ["E"]
9 | },
10 | {
11 | id: "E",
12 | next: ["A"]
13 | },
14 | {
15 | id: "A",
16 | next: ["C"]
17 | },
18 | {
19 | id: "C",
20 | next: ["T"]
21 | },
22 | {
23 | id: "T",
24 | next: []
25 | }
26 | ];
27 |
28 | const direct = [
29 | {
30 | id: "D",
31 | next: ["I"]
32 | },
33 | {
34 | id: "I",
35 | next: ["R"]
36 | },
37 | {
38 | id: "R",
39 | next: ["E"]
40 | },
41 | {
42 | id: "E",
43 | next: ["C"]
44 | },
45 | {
46 | id: "C",
47 | next: ["T"]
48 | },
49 | {
50 | id: "T",
51 | next: []
52 | }
53 | ];
54 |
55 | const graph = [
56 | {
57 | id: "G",
58 | next: ["R"]
59 | },
60 | {
61 | id: "R",
62 | next: ["A"]
63 | },
64 | {
65 | id: "A",
66 | next: ["P"]
67 | },
68 | {
69 | id: "P",
70 | next: ["H"]
71 | },
72 | {
73 | id: "H",
74 | next: []
75 | }
76 | ];
77 |
78 | export class Title extends Component {
79 | render() {
80 | const { cellSize, padding } = this.props;
81 | return (
82 |
83 |
88 |
93 |
98 |
99 | );
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/example/src/components/basic/Basic.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | import { ExampleBasic } from "./";
4 | import { Example } from "../Example.jsx";
5 |
6 | /* eslint import/no-webpack-loader-syntax: off */
7 | const { default: exampleBasicCode } = require("!!raw-loader!./ExampleBasic.jsx");
8 |
9 | const cellSize = 100;
10 | const padding = cellSize * 0.25;
11 |
12 | export class Basic extends Component {
13 | description = `Basic graph rendering requires array of nodes, dimensions of cell and padding of node icon as props.
14 | Node object should has following properties:
15 |
16 | id - unique identifier of the node
17 | next array of id's - node outcomes
18 | payload some additional data to hold in`;
19 |
20 | render() {
21 | return (
22 |
27 |
28 |
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/example/src/components/basic/ExampleBasic.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | import DirectGraph from "react-direct-graph";
4 |
5 | const graph = [
6 | {
7 | id: "A",
8 | next: ["B"]
9 | },
10 | {
11 | id: "B",
12 | next: ["C", "D", "E"]
13 | },
14 | {
15 | id: "C",
16 | next: ["F"]
17 | },
18 | {
19 | id: "D",
20 | next: ["J"]
21 | },
22 | {
23 | id: "E",
24 | next: ["J"]
25 | },
26 | {
27 | id: "J",
28 | next: ["I"]
29 | },
30 | {
31 | id: "I",
32 | next: ["H"]
33 | },
34 | {
35 | id: "F",
36 | next: ["K"]
37 | },
38 | {
39 | id: "K",
40 | next: ["L"]
41 | },
42 | {
43 | id: "H",
44 | next: ["L"]
45 | },
46 | {
47 | id: "L",
48 | next: ["P"]
49 | },
50 | {
51 | id: "P",
52 | next: ["M", "N"]
53 | },
54 | {
55 | id: "M",
56 | next: []
57 | },
58 | {
59 | id: "N",
60 | next: []
61 | }
62 | ];
63 |
64 | export class ExampleBasic extends Component {
65 | render() {
66 | const { cellSize, padding } = this.props;
67 | return ;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/example/src/components/basic/index.js:
--------------------------------------------------------------------------------
1 | export {
2 | ExampleBasic
3 | }
4 | from "./ExampleBasic"
5 | export {
6 | Basic
7 | }
8 | from "./Basic"
9 |
--------------------------------------------------------------------------------
/example/src/components/complex/Complex.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | import { ExampleComplex } from "./";
4 | import { Example } from "../Example.jsx";
5 |
6 | /* eslint import/no-webpack-loader-syntax: off */
7 | const { default: exampleComplexCode } = require("!!raw-loader!./ExampleComplex.jsx");
8 |
9 | const cellSize = 100;
10 | const padding = cellSize * 0.25;
11 |
12 | export class Complex extends Component {
13 | description = `Library renders graphs with any complexity.
14 | Supported features:
15 |
16 | loops - backward references between nodes
17 | split-joins - nodes with more then one income and outcome
18 | multigraphs - multiple graphs in one view and graphs with more then one root`;
19 |
20 | render() {
21 | return (
22 |
27 |
28 |
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/example/src/components/complex/ExampleComplex.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | import DirectGraph from "react-direct-graph";
4 |
5 | const graph = [
6 | {
7 | id: "1",
8 | next: ["2"]
9 | },
10 | {
11 | id: "2",
12 | next: ["3", "4"]
13 | },
14 | {
15 | id: "3",
16 | next: ["5"]
17 | },
18 | {
19 | id: "6",
20 | next: []
21 | },
22 | {
23 | id: "7",
24 | next: ["4"]
25 | },
26 | {
27 | id: "4",
28 | next: ["8"]
29 | },
30 | {
31 | id: "8",
32 | next: ["9"]
33 | },
34 | {
35 | id: "10",
36 | next: ["11"]
37 | },
38 | {
39 | id: "11",
40 | next: ["12", "13"]
41 | },
42 | {
43 | id: "12",
44 | next: ["14"]
45 | },
46 | {
47 | id: "14",
48 | next: ["15", "16"]
49 | },
50 | {
51 | id: "15",
52 | next: ["17", "12"]
53 | },
54 | {
55 | id: "16",
56 | next: ["17"]
57 | },
58 | {
59 | id: "17",
60 | next: []
61 | },
62 | {
63 | id: "13",
64 | next: []
65 | },
66 | {
67 | id: "9",
68 | next: ["18"]
69 | },
70 | {
71 | id: "5",
72 | next: ["19"]
73 | },
74 | {
75 | id: "19",
76 | next: ["20"]
77 | },
78 | {
79 | id: "18",
80 | next: ["20"]
81 | },
82 | {
83 | id: "20",
84 | next: ["21", "6"]
85 | },
86 | {
87 | id: "21",
88 | next: ["21"]
89 | }
90 | ];
91 |
92 | export class ExampleComplex extends Component {
93 | render() {
94 | const { cellSize, padding } = this.props;
95 | return ;
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/example/src/components/complex/index.js:
--------------------------------------------------------------------------------
1 | export {
2 | ExampleComplex
3 | }
4 | from "./ExampleComplex"
5 | export {
6 | Complex
7 | }
8 | from "./Complex"
9 |
--------------------------------------------------------------------------------
/example/src/components/custom/Custom.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | import { ExampleCustom } from "./";
4 | import { Example } from "../Example.jsx";
5 |
6 | /* eslint import/no-webpack-loader-syntax: off */
7 | const { default: exampleCustomCode } = require("!!raw-loader!./ExampleCustom.jsx");
8 |
9 | const cellSize = 100;
10 | const padding = cellSize * 0.25;
11 |
12 | export class Custom extends Component {
13 | description = `You can specify custom component for icon of graph elements using "component" prop.
14 | Custom icon component receives following props:
15 |
16 | node - graph node
17 | incomes - incomes of graph node or empty array if node is root`;
18 |
19 | render() {
20 | return (
21 |
26 |
27 |
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/example/src/components/custom/ExampleCustom.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | import DirectGraph from "react-direct-graph";
4 |
5 | const graph = [
6 | {
7 | id: "A",
8 | next: ["B"]
9 | },
10 | {
11 | id: "B",
12 | next: ["C", "J"]
13 | },
14 | {
15 | id: "C",
16 | next: ["F"]
17 | },
18 | {
19 | id: "D",
20 | next: ["J"]
21 | },
22 | {
23 | id: "E",
24 | next: ["J"]
25 | },
26 | {
27 | id: "J",
28 | next: ["I"]
29 | },
30 | {
31 | id: "I",
32 | next: ["H", "K"]
33 | },
34 | {
35 | id: "F",
36 | next: ["K"]
37 | },
38 | {
39 | id: "K",
40 | next: ["L"]
41 | },
42 | {
43 | id: "H",
44 | next: ["N"]
45 | },
46 | {
47 | id: "L",
48 | next: ["P"]
49 | },
50 | {
51 | id: "P",
52 | next: ["M"]
53 | },
54 | {
55 | id: "M",
56 | next: []
57 | },
58 | {
59 | id: "N",
60 | next: ["P"]
61 | }
62 | ];
63 |
64 | class CustomNodeIcon extends Component {
65 | getColor(node, incomes) {
66 | if (incomes && incomes.length > 1) return "#501570";
67 | if (node.next && node.next.length > 1) return "#501570";
68 | return "#169676";
69 | }
70 | renderText(id) {
71 | return (
72 |
73 |
82 | {id}
83 |
84 |
85 | );
86 | }
87 | render() {
88 | const { node, incomes } = this.props;
89 | return (
90 |
115 | );
116 | }
117 | }
118 |
119 | export class ExampleCustom extends Component {
120 | render() {
121 | const { cellSize, padding } = this.props;
122 | return (
123 |
129 | );
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/example/src/components/custom/index.js:
--------------------------------------------------------------------------------
1 | export {
2 | ExampleCustom
3 | }
4 | from "./ExampleCustom"
5 | export {
6 | Custom
7 | }
8 | from "./Custom"
9 |
--------------------------------------------------------------------------------
/example/src/components/editor/Editor.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | import { ExampleEdit } from "./";
4 | import { Example } from "../Example.jsx";
5 |
6 | /* eslint import/no-webpack-loader-syntax: off */
7 | const { default: exampleEditCode } = require("!!raw-loader!./ExampleEdit.jsx");
8 |
9 | const cellSize = 100;
10 | const padding = cellSize * 0.25;
11 |
12 | export class Editor extends Component {
13 | description = `Taking advantage of graph rendering loop and event listeners it's possible to create graph editor.
14 |
15 | Click on edge to insert node
16 | Click on node to delete node
17 | Shift+Click on two node to create new edge`;
18 |
19 | render() {
20 | return (
21 |
26 |
27 |
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/example/src/components/editor/ExampleEdit.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | import DirectGraph from "react-direct-graph";
4 | import graph from "../../data/graph.json";
5 |
6 | let counter = 1;
7 |
8 | export class ExampleEdit extends Component {
9 | constructor(props) {
10 | super(props);
11 | this.state = {
12 | graph: graph.map(n => ({ ...n, next: [...n.next] })),
13 | inSelect: false,
14 | selected: []
15 | };
16 | }
17 |
18 | componentDidMount() {
19 | window.addEventListener("keydown", this.onKeyDown);
20 | window.addEventListener("keyup", this.onKeyUp);
21 | }
22 |
23 | componentWillUnmount() {
24 | window.removeEventListener("keydown", this.onKeyDown);
25 | window.removeEventListener("keyup", this.onKeyUp);
26 | }
27 |
28 | onNodeClick = (event, node, incomes) => {
29 | if (this.state.inSelect) {
30 | this.connectNode(event, node, incomes);
31 | } else {
32 | this.softDeleteNode(event, node, incomes);
33 | }
34 | };
35 |
36 | onEdgeClick = (event, node, incomes) => {
37 | this.insertNode(node, incomes[0]);
38 | };
39 |
40 | onEdgeMouseEnter = (event, node, incomes) => {
41 | event.currentTarget.style.stroke = "#f00";
42 | event.currentTarget.style.fill = "#f00";
43 | };
44 |
45 | onEdgeMouseLeave = (event, node, incomes) => {
46 | event.currentTarget.style.stroke = "rgb(45, 87, 139)";
47 | event.currentTarget.style.fill = "rgb(45, 87, 139)";
48 | };
49 |
50 | onKeyDown = event => {
51 | if (event.which === 16) {
52 | this.setState({
53 | ...this.state,
54 | ...{
55 | inSelect: true
56 | }
57 | });
58 | }
59 | };
60 |
61 | onKeyUp = event => {
62 | if (event.which === 16) {
63 | this.setState({
64 | ...this.state,
65 | ...{
66 | inSelect: false,
67 | selected: []
68 | }
69 | });
70 | }
71 | };
72 |
73 | addToNodeToSelectBuffer = (node, incomes) => {
74 | this.setState({
75 | ...this.state,
76 | ...{
77 | selected: [node]
78 | }
79 | });
80 | };
81 |
82 | connectNode = (event, node, incomes) => {
83 | event.preventDefault();
84 | if (!this.state.selected.length) {
85 | this.addToNodeToSelectBuffer(node, incomes);
86 | } else {
87 | this.applyNodeConnection(node, incomes);
88 | }
89 | };
90 |
91 | applyNodeConnection = (node, incomes) => {
92 | const target = this.state.selected[0];
93 | if (target.id === node.id) return;
94 | const outcome = node;
95 | const newGraph = [...this.state.graph];
96 | const targetInArray = newGraph.find(n => n.id === target.id);
97 | if (targetInArray.next.find(outcomeId => outcomeId === outcome.id))
98 | return;
99 | targetInArray.next.push(outcome.id);
100 | this.setState({
101 | ...this.state,
102 | ...{
103 | selected: [],
104 | graph: newGraph
105 | }
106 | });
107 | };
108 |
109 | softDeleteNode = (event, node, incomes) => {
110 | const nodeIndex = this.state.graph.findIndex(n => n.id === node.id);
111 | incomes.forEach((income, index) => {
112 | const find = n => n.id === income.anchorFrom;
113 | while (income.isAnchor) {
114 | income = this.state.graph.find(find);
115 | }
116 | const inc = this.state.graph.find(n => n.id === income.id);
117 | const i = inc.next.findIndex(outcomeId => outcomeId === node.id);
118 | const incNext = node.next.filter(nextId => nextId !== inc.id);
119 | if (!index) inc.next.splice(i, 1, ...incNext);
120 | else inc.next.splice(i, 1);
121 | });
122 | const newGraph = [...this.state.graph];
123 | newGraph.splice(nodeIndex, 1);
124 | this.setState({
125 | ...this.state,
126 | ...{
127 | graph: newGraph
128 | }
129 | });
130 | };
131 |
132 | insertNode = (node, income) => {
133 | const newId = `#${counter}`;
134 | const nodeIndex = this.state.graph.findIndex(n => n.id === node.id);
135 | const inc = this.state.graph.find(n => n.id === income.id);
136 | const newGraph = [...this.state.graph];
137 | newGraph.splice(nodeIndex, 0, {
138 | id: newId,
139 | next: [node.id],
140 | payload: {
141 | exist: true
142 | }
143 | });
144 | const i = inc.next.findIndex(outcomeId => outcomeId === node.id);
145 | inc.next.splice(i, 1, newId);
146 | this.setState({
147 | ...this.state,
148 | ...{
149 | graph: newGraph
150 | }
151 | });
152 | counter++;
153 | };
154 |
155 | render() {
156 | const { cellSize, padding } = this.props;
157 | return (
158 |
167 | );
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/example/src/components/editor/index.js:
--------------------------------------------------------------------------------
1 | export {
2 | ExampleEdit
3 | }
4 | from "./ExampleEdit"
5 | export {
6 | Editor
7 | }
8 | from "./Editor"
9 |
--------------------------------------------------------------------------------
/example/src/components/events/Events.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | import { ExampleEvents } from "./";
4 | import { Example } from "../Example.jsx";
5 |
6 | /* eslint import/no-webpack-loader-syntax: off */
7 | const { default: exampleEventsCode } = require("!!raw-loader!./ExampleEvents.jsx");
8 |
9 | const cellSize = 100;
10 | const padding = cellSize * 0.25;
11 |
12 | export class Events extends Component {
13 | description = `It's possible to handle mouse events on nodes and edges.
All event handlers recieve three arguments:
14 |
15 | event React synthetic event
16 | node node object bound to event target
17 | incomes incomes of bound event object
18 |
19 | Following event handlers available:
20 |
21 | onNodeClick - click on node
22 | onEdgeClick - click on edge
23 | onNodeMouseEnter - mouseenter on node
24 | onNodeMouseLeave - mouseleave on node
25 | onEdgeMouseEnter - mouseenter on edge
26 | onEdgeMouseLeave - mouseleave on edge
`;
27 |
28 | render() {
29 | return (
30 |
35 |
36 |
37 | );
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/example/src/components/events/ExampleEvents.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from "react";
2 |
3 | import DirectGraph from "react-direct-graph";
4 | import graph from "../../data/graph.json";
5 |
6 | export class ExampleEvents extends Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | event: `Click event info will be here...`
11 | };
12 | }
13 | onNodeClick = (event, node, incomes) => {
14 | this.setState({
15 | event: `Click on node "${node.id}"`
16 | });
17 | };
18 |
19 | onEdgeClick = (event, node, incomes) => {
20 | this.setState({
21 | event: `Click on edge from income "${incomes[0].id}" to node "${
22 | node.id
23 | }"`
24 | });
25 | };
26 |
27 | onNodeMouseEnter = (event, node, incomes) => {
28 | const path = event.currentTarget.querySelector(
29 | ".node-icon-default path"
30 | );
31 | path.style.stroke = "#f00";
32 | path.style.fill = "#f00";
33 | };
34 |
35 | onNodeMouseLeave = (event, node, incomes) => {
36 | const path = event.currentTarget.querySelector(
37 | ".node-icon-default path"
38 | );
39 | path.style.stroke = null;
40 | path.style.fill = null;
41 | };
42 |
43 | onEdgeMouseEnter = (event, node, incomes) => {
44 | event.currentTarget.style.stroke = "#f00";
45 | event.currentTarget.style.fill = "#f00";
46 | };
47 |
48 | onEdgeMouseLeave = (event, node, incomes) => {
49 | event.currentTarget.style.stroke = "rgb(45, 87, 139)";
50 | event.currentTarget.style.fill = "rgb(45, 87, 139)";
51 | };
52 |
53 | render() {
54 | const { cellSize, padding } = this.props;
55 | return (
56 |
57 | {this.state.event}
58 | ({ ...n, next: [...n.next] }))}
60 | cellSize={cellSize}
61 | padding={padding}
62 | onNodeClick={this.onNodeClick}
63 | onEdgeClick={this.onEdgeClick}
64 | onNodeMouseEnter={this.onNodeMouseEnter}
65 | onNodeMouseLeave={this.onNodeMouseLeave}
66 | onEdgeMouseEnter={this.onEdgeMouseEnter}
67 | onEdgeMouseLeave={this.onEdgeMouseLeave}
68 | />
69 |
70 | );
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/example/src/components/events/index.js:
--------------------------------------------------------------------------------
1 | export {
2 | ExampleEvents
3 | }
4 | from "./ExampleEvents"
5 | export {
6 | Events
7 | }
8 | from "./Events"
9 |
--------------------------------------------------------------------------------
/example/src/components/index.js:
--------------------------------------------------------------------------------
1 | export {
2 | Editor
3 | }
4 | from "./editor"
5 | export {
6 | Basic
7 | }
8 | from "./basic"
9 | export {
10 | WithNames
11 | }
12 | from "./withNames"
13 | export {
14 | Complex
15 | }
16 | from "./complex"
17 | export {
18 | Custom
19 | }
20 | from "./custom"
21 | export {
22 | Events
23 | }
24 | from "./events"
25 | export {
26 | Title
27 | }
28 | from "./Title"
29 |
--------------------------------------------------------------------------------
/example/src/components/withNames/ExampleWithNames.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | import DirectGraph from "react-direct-graph";
4 |
5 | const graph = [
6 | {
7 | id: "A",
8 | next: ["B"],
9 | name: "Node A",
10 | edgeNames: ["Edge AB"]
11 | },
12 | {
13 | id: "B",
14 | next: ["C", "D", "E"],
15 | name: "Node B",
16 | edgeNames: ["Edge BC", "Edge BD", "Edge BE"]
17 | },
18 | {
19 | id: "C",
20 | next: ["F"],
21 | name: "Node C",
22 | edgeNames: ["Edge CF"]
23 | },
24 | {
25 | id: "D",
26 | next: ["J"],
27 | name: "Node D",
28 | edgeNames: ["Edge DJ"]
29 | },
30 | {
31 | id: "E",
32 | next: ["J"],
33 | name: "Node E",
34 | edgeNames: ["Edge EJ"]
35 | },
36 | {
37 | id: "J",
38 | next: ["I"],
39 | name: "Node J",
40 | edgeNames: ["Edge JI"]
41 | },
42 | {
43 | id: "I",
44 | next: ["H"],
45 | name: "Node I",
46 | edgeNames: ["Edge IH"]
47 | },
48 | {
49 | id: "F",
50 | next: ["K"],
51 | name: "Node F",
52 | edgeNames: ["Edge FK"]
53 | },
54 | {
55 | id: "K",
56 | next: ["L"],
57 | name: "Node K",
58 | edgeNames: ["Edge KL"]
59 | },
60 | {
61 | id: "H",
62 | next: ["L"],
63 | name: "Node H",
64 | edgeNames: ["Edge HL"]
65 | },
66 | {
67 | id: "L",
68 | next: ["P"],
69 | name: "Node L",
70 | edgeNames: ["Edge LP"]
71 | },
72 | {
73 | id: "P",
74 | next: ["M", "N"],
75 | name: "Node P",
76 | edgeNames: ["Edge PM", "Edge PN"]
77 | },
78 | {
79 | id: "M",
80 | next: [],
81 | name: "Node M"
82 | },
83 | {
84 | id: "N",
85 | next: [],
86 | name: "Node N"
87 | }
88 | ];
89 |
90 | export class ExampleWithNames extends Component {
91 | render() {
92 | const { cellSize, padding } = this.props;
93 | return (
94 |
95 | ;
96 |
97 | )
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/example/src/components/withNames/WithNames.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | import { ExampleWithNames } from "./";
4 | import { Example } from "../Example.jsx";
5 |
6 | /* eslint import/no-webpack-loader-syntax: off */
7 | const { default: exampleWithNamesCode } = require("!!raw-loader!./ExampleWithNames.jsx");
8 |
9 | const cellSize = 100;
10 | const padding = cellSize * 0.25;
11 |
12 | export class WithNames extends Component {
13 | description = `You can give the name of the nodes and edges.
14 | Node object can has following additional properties:
15 |
16 | name - name of the node
17 | edgeNames array of names - outcome edges`;
18 |
19 | render() {
20 | return (
21 |
26 |
27 |
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/example/src/components/withNames/index.js:
--------------------------------------------------------------------------------
1 | export {
2 | ExampleWithNames
3 | }
4 | from "./ExampleWithNames"
5 | export {
6 | WithNames
7 | }
8 | from "./WithNames"
9 |
--------------------------------------------------------------------------------
/example/src/data/basic.json:
--------------------------------------------------------------------------------
1 | [{
2 | "id": "A",
3 | "next": ["B"]
4 | }, {
5 | "id": "B",
6 | "next": ["C", "D", "E"]
7 | }, {
8 | "id": "C",
9 | "next": ["F"]
10 | }, {
11 | "id": "D",
12 | "next": ["J"]
13 | },
14 | {
15 | "id": "E",
16 | "next": ["J"]
17 | }, {
18 | "id": "J",
19 | "next": ["I"]
20 | },
21 | {
22 | "id": "I",
23 | "next": ["H"]
24 | },
25 | {
26 | "id": "F",
27 | "next": "K"
28 | },
29 | {
30 | "id": "K",
31 | "next": []
32 | },
33 | {
34 | "id": "H",
35 | "next": []
36 | }
37 | ]
38 |
--------------------------------------------------------------------------------
/example/src/data/graph.json:
--------------------------------------------------------------------------------
1 | [{
2 | "id": "A",
3 | "next": ["B"],
4 | "payload": {
5 | "exist": true
6 | }
7 | }, {
8 | "id": "U",
9 | "next": ["G"],
10 | "payload": {
11 | "exist": true
12 | }
13 | }, {
14 | "id": "B",
15 | "next": ["C", "D", "E", "F", "M"],
16 | "payload": {
17 | "exist": true
18 | }
19 | }, {
20 | "id": "C",
21 | "next": ["G"],
22 | "payload": {
23 | "exist": true
24 | }
25 | }, {
26 | "id": "D",
27 | "next": ["H"],
28 | "payload": {
29 | "exist": true
30 | }
31 | }, {
32 | "id": "E",
33 | "next": ["H"],
34 | "payload": {
35 | "exist": true
36 | }
37 | }, {
38 | "id": "F",
39 | "next": ["N", "O"],
40 | "payload": {
41 | "exist": true
42 | }
43 | }, {
44 | "id": "N",
45 | "next": ["I"],
46 | "payload": {
47 | "exist": true
48 | }
49 | }, {
50 | "id": "O",
51 | "next": ["P"],
52 | "payload": {
53 | "exist": true
54 | }
55 | }, {
56 | "id": "P",
57 | "next": ["I"],
58 | "payload": {
59 | "exist": true
60 | }
61 | }, {
62 | "id": "M",
63 | "next": ["L"],
64 | "payload": {
65 | "exist": true
66 | }
67 | }, {
68 | "id": "G",
69 | "next": ["I"],
70 | "payload": {
71 | "exist": true
72 | }
73 | }, {
74 | "id": "H",
75 | "next": ["J"],
76 | "payload": {
77 | "exist": true
78 | }
79 | }, {
80 | "id": "I",
81 | "next": [],
82 | "payload": {
83 | "exist": true
84 | }
85 | }, {
86 | "id": "J",
87 | "next": ["K"],
88 | "payload": {
89 | "exist": true
90 | }
91 | }, {
92 | "id": "K",
93 | "next": ["L"],
94 | "payload": {
95 | "exist": true
96 | }
97 | }, {
98 | "id": "L",
99 | "next": [],
100 | "payload": {
101 | "exist": true
102 | }
103 | }]
104 |
--------------------------------------------------------------------------------
/example/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: "Courier";
5 | background: url("https://www.toptal.com/designers/subtlepatterns/patterns/prism.png");
6 | }
7 |
8 | main {
9 | max-width: 960px;
10 | padding: 30px;
11 | margin: 0 auto;
12 | background: #fff;
13 | }
14 |
15 | main>h1 {
16 | text-align: center;
17 | }
18 |
19 | main>svg {
20 | margin: 0 auto;
21 | display: block;
22 | }
23 |
24 | main>svg:first-child .node-icon-default path {
25 | fill: rgb(185, 0, 0);
26 | stroke: rgb(185, 0, 0);
27 | }
28 |
29 | main>svg:nth-child(2) .node-icon-default path {
30 | fill: rgb(48, 0, 160);
31 | stroke: rgb(48, 0, 160);
32 | }
33 |
34 | main>svg:nth-child(3) .node-icon-default path {
35 | fill: rgb(0, 160, 8);
36 | stroke: rgb(0, 160, 8);
37 | }
38 |
39 | .example svg {
40 | margin: 0 auto;
41 | }
42 |
43 | .description {
44 | margin-bottom: 20px;
45 | }
46 |
47 | .show-code {
48 | padding: 2px 10px;
49 | background: none;
50 | border: 2px solid #333;
51 | cursor: pointer;
52 | border-radius: 3px;
53 | user-select: none;
54 | color: #333;
55 | }
56 |
57 | .hide-code {
58 | padding: 2px 10px;
59 | background: none;
60 | border: 2px solid #2b2ba8;
61 | color: #2b2ba8;
62 | cursor: pointer;
63 | border-radius: 3px;
64 | user-select: none;
65 | }
66 |
67 | .graph-holder {
68 | overflow-x: auto;
69 | }
70 |
71 | .hidden {
72 | display: none !important;
73 | }
74 |
75 | pre {
76 | border-radius: 10px;
77 | }
78 |
79 | .hide-code:focus,
80 | .show-code:focus {
81 | outline: none
82 | }
83 |
--------------------------------------------------------------------------------
/example/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 |
4 | import './index.css'
5 | import App from './App'
6 |
7 | ReactDOM.render( < App / > , document.getElementById('root'))
8 |
--------------------------------------------------------------------------------
/img/graph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lempiy/react-direct-graph/42b999115fb193b976002db1e3291263fb88d028/img/graph.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-direct-graph",
3 | "version": "1.5.0",
4 | "description": "React component for drawing direct graphs with rectangular (non-curve) edges",
5 | "author": "lempiy",
6 | "license": "MIT",
7 | "repository": "https://github.com/lempiy/react-direct-graph.git",
8 | "main": "dist/index.js",
9 | "module": "dist/index.es.js",
10 | "jsnext:main": "dist/index.es.js",
11 | "engines": {
12 | "node": ">=8",
13 | "npm": ">=5"
14 | },
15 | "scripts": {
16 | "test": "cross-env CI=1 react-scripts-ts test --env=jsdom --coverage --collectCoverageFrom='src/**/*.{ts|tsx}' --collectCoverageFrom=!src/fixtures/** --collectCoverageFrom=!src/**/*.d.ts",
17 | "test:watch": "react-scripts-ts test --env=jsdom",
18 | "build": "rollup -c",
19 | "start": "rollup -c -w",
20 | "prepare": "npm run build",
21 | "predeploy": "cd example && npm install && npm run build",
22 | "deploy": "gh-pages -d example/build"
23 | },
24 | "dependencies": {},
25 | "peerDependencies": {
26 | "prop-types": "^15.5.4",
27 | "react": "^15.0.0 || ^16.0.0",
28 | "react-dom": "^15.0.0 || ^16.0.0"
29 | },
30 | "devDependencies": {
31 | "@svgr/rollup": "^2.4.1",
32 | "@types/jest": "^23.1.5",
33 | "@types/react": "^16.3.13",
34 | "@types/react-dom": "^16.0.5",
35 | "babel-core": "^6.26.3",
36 | "babel-runtime": "^6.26.0",
37 | "cross-env": "^5.1.4",
38 | "gh-pages": "^1.2.0",
39 | "react": "^16.4.1",
40 | "react-dom": "^16.4.1",
41 | "react-scripts-ts": "^2.16.0",
42 | "rollup": "^0.62.0",
43 | "rollup-plugin-babel": "^3.0.7",
44 | "rollup-plugin-commonjs": "^9.1.3",
45 | "rollup-plugin-node-resolve": "^3.3.0",
46 | "rollup-plugin-peer-deps-external": "^2.2.0",
47 | "rollup-plugin-postcss": "^1.6.2",
48 | "rollup-plugin-typescript2": "^0.17.0",
49 | "rollup-plugin-url": "^1.4.0",
50 | "typescript": "^2.8.3"
51 | },
52 | "files": [
53 | "dist"
54 | ]
55 | }
56 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import typescript from 'rollup-plugin-typescript2'
2 | import commonjs from 'rollup-plugin-commonjs'
3 | import external from 'rollup-plugin-peer-deps-external'
4 | // import postcss from 'rollup-plugin-postcss-modules'
5 | import postcss from 'rollup-plugin-postcss'
6 | import resolve from 'rollup-plugin-node-resolve'
7 | import url from 'rollup-plugin-url'
8 | import svgr from '@svgr/rollup'
9 |
10 | import pkg from './package.json'
11 |
12 | export default {
13 | input: 'src/index.tsx',
14 | output: [
15 | {
16 | file: pkg.main,
17 | format: 'cjs',
18 | exports: 'named',
19 | sourcemap: true
20 | },
21 | {
22 | file: pkg.module,
23 | format: 'es',
24 | exports: 'named',
25 | sourcemap: true
26 | }
27 | ],
28 | plugins: [
29 | external(),
30 | postcss({
31 | modules: true
32 | }),
33 | url(),
34 | svgr(),
35 | resolve(),
36 | typescript({
37 | rollupCommonJSResolveHack: true,
38 | clean: true
39 | }),
40 | commonjs()
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/element/element.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { GraphNodeIconComponentProps, DefaultNodeIcon } from "../node-icon";
3 | import { withForeignObject } from "../with-foreign-object";
4 | import {
5 | getSize,
6 | getCoords,
7 | getAllIncomes,
8 | wrapEventHandler,
9 | checkAnchorRenderIncomes
10 | } from "../../utils";
11 | import { IMatrixNode } from "../../core";
12 | import { GraphEventFunc, DataProps } from "./element.types";
13 |
14 | export type ViewProps = {
15 | component?: React.ComponentType>;
16 | onNodeMouseEnter?: GraphEventFunc;
17 | onNodeMouseLeave?: GraphEventFunc;
18 | onNodeClick?: GraphEventFunc;
19 | cellSize: number;
20 | padding: number;
21 | };
22 |
23 | export class GraphElement extends React.Component<
24 | DataProps & ViewProps
25 | > {
26 | diveToNodeIncome = (
27 | node: IMatrixNode,
28 | nodesMap: { [id: string]: IMatrixNode }
29 | ): IMatrixNode => {
30 | if (!node.isAnchor) return node;
31 | checkAnchorRenderIncomes(node);
32 | return this.diveToNodeIncome(nodesMap[node.renderIncomes[0]], nodesMap);
33 | };
34 |
35 | getNodeIncomes = (
36 | node: IMatrixNode,
37 | nodesMap: { [id: string]: IMatrixNode }
38 | ): IMatrixNode[] => getAllIncomes(node, nodesMap).map(n =>
39 | this.diveToNodeIncome(n, nodesMap)
40 | );
41 |
42 | getNodeHandlers(): { [eventName: string]: (e: React.MouseEvent) => void } {
43 | const {
44 | node,
45 | nodesMap,
46 | onNodeClick,
47 | onNodeMouseEnter,
48 | onNodeMouseLeave
49 | } = this.props;
50 | const incomes = this.getNodeIncomes(node, nodesMap);
51 | const handlers: {
52 | [eventName: string]: (e: React.MouseEvent) => void;
53 | } = {};
54 | if (onNodeClick)
55 | handlers.onClick = wrapEventHandler(
56 | onNodeClick,
57 | node,
58 | incomes
59 | );
60 | if (onNodeMouseEnter)
61 | handlers.onMouseEnter = wrapEventHandler(
62 | onNodeMouseEnter,
63 | node,
64 | incomes
65 | );
66 | if (onNodeMouseLeave)
67 | handlers.onMouseLeave = wrapEventHandler(
68 | onNodeMouseLeave,
69 | node,
70 | incomes
71 | );
72 | return handlers;
73 | }
74 |
75 | renderNode() {
76 | const { node, node: { isAnchor, name, nameOrientation = "bottom" }, nodesMap, cellSize, padding } = this.props;
77 | const [x, y] = getCoords(cellSize, padding, node);
78 | const size = getSize(cellSize, padding);
79 | const NodeIcon = withForeignObject>(
80 | this.props.component ? this.props.component : DefaultNodeIcon
81 | );
82 | const incomes = this.getNodeIncomes(node, nodesMap);
83 | const textY = nameOrientation === "top"
84 | ? y - size * 0.2
85 | : y + size * 1.2;
86 | return (
87 | !isAnchor && (
88 |
89 |
97 | {!!name && (
98 |
110 | {name}
111 |
112 | )}
113 |
114 | )
115 | );
116 | }
117 |
118 | render() {
119 | return (
120 |
128 | {this.renderNode()}
129 |
130 | );
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/components/element/element.types.ts:
--------------------------------------------------------------------------------
1 | import { IMatrixNode } from "../../core";
2 |
3 | export type DataProps = {
4 | node: IMatrixNode;
5 | nodesMap: { [id: string]: IMatrixNode };
6 | };
7 |
8 | export type GraphEventFunc = (
9 | event: React.MouseEvent,
10 | node: IMatrixNode,
11 | incomes: IMatrixNode[]
12 | ) => void;
13 |
--------------------------------------------------------------------------------
/src/components/element/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./element";
2 | export * from "./element.types";
3 |
--------------------------------------------------------------------------------
/src/components/graph.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { IMatrixNode } from "../core";
3 | import { GraphElement, ViewProps as ElementViewProps } from "./element";
4 | import { GraphPolyline, ViewProps as PolylineViewProps } from "./polyline";
5 | import { DefaultMarkerBody } from "./marker-default";
6 |
7 | export type Props = {
8 | nodesMap: { [id: string]: IMatrixNode };
9 | cellSize: number;
10 | padding: number;
11 | widthInCells: number;
12 | heightInCells: number;
13 | };
14 |
15 | export type ViewProps = ElementViewProps & PolylineViewProps;
16 |
17 | interface INodeElementInput {
18 | node: IMatrixNode;
19 | }
20 |
21 | export class Graph extends React.Component & Props> {
22 | getNodeElementInputs = (nodesMap: { [id: string]: IMatrixNode }): INodeElementInput[] => {
23 | return Object.entries(nodesMap)
24 | .filter(([_, node]) => !node.isAnchor)
25 | .map(([_, node]) => ({
26 | node
27 | }));
28 | };
29 | renderElements() {
30 | const { nodesMap, cellSize, padding, widthInCells, heightInCells, ...restProps } = this.props;
31 | const elements = this.getNodeElementInputs(nodesMap);
32 | return (
33 | <>
34 | {elements.map(props => (
35 |
43 | ))}
44 | {elements.map(props => (
45 |
53 | ))}
54 | >
55 | )
56 | }
57 | render() {
58 | const { cellSize, widthInCells, heightInCells } = this.props;
59 | return (
60 |
70 | );
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./graph";
2 | export { GraphNodeIconComponentProps } from "./node-icon";
3 |
--------------------------------------------------------------------------------
/src/components/marker-default.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | type Props = {
4 | id: string;
5 | width: number;
6 | height: number;
7 | };
8 |
9 | export class DefaultMarkerBody extends React.PureComponent {
10 | render() {
11 | return (
12 |
13 |
14 |
18 |
19 |
20 | );
21 | }
22 | }
23 |
24 | export class DefaultMarker extends React.PureComponent {
25 | render() {
26 | const { id, width, height } = this.props;
27 | return (
28 |
38 |
39 |
40 | );
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/node-icon/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./node-icon";
2 | export * from "./node-icon-default";
3 |
--------------------------------------------------------------------------------
/src/components/node-icon/node-icon-default.css:
--------------------------------------------------------------------------------
1 | .nodeOrange path {
2 | fill: #e25300;
3 | stroke: #e25300;
4 | }
5 |
6 | .nodeGreen path {
7 | fill: #008c15;
8 | stroke: #008c15;
9 | }
10 |
11 | .nodeBlue path {
12 | fill: #193772;
13 | stroke: #193772;
14 | }
15 |
16 | .nodePurple {
17 | fill: #6304a3;
18 | stroke: #6304a3;
19 | }
20 |
21 | .nodeDefaultIcon text {
22 | font-size: 14px;
23 | }
24 |
25 | .nodeDefaultIconGroup g {
26 | fill: #ffffff;
27 | stroke: #ffffff;
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/node-icon/node-icon-default.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { GraphNodeIconComponentProps } from "./node-icon";
3 | import { INodeInput } from "../../core";
4 | import styles from "./node-icon-default.css";
5 |
6 | export class DefaultNodeIcon extends React.Component<
7 | GraphNodeIconComponentProps
8 | > {
9 | getClass(node: INodeInput, incomes: INodeInput[]): string {
10 | if (incomes && incomes.length > 1 && node.next && node.next.length > 1)
11 | return styles.nodePurple;
12 | if (incomes && incomes.length > 1) return styles.nodeOrange;
13 | if (node.next && node.next.length > 1) return styles.nodeGreen;
14 | return styles.nodeBlue;
15 | }
16 | renderText(id: string) {
17 | return (
18 |
19 |
27 | {id}
28 |
29 |
30 | );
31 | }
32 | render() {
33 | const { node, incomes } = this.props;
34 | return (
35 |
51 | );
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/components/node-icon/node-icon.ts:
--------------------------------------------------------------------------------
1 | import { INodeInput } from "../../core"
2 |
3 | export type GraphNodeIconComponentProps = {
4 | node: INodeInput
5 | incomes: INodeInput[]
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/polyline/getPointWithResolver.ts:
--------------------------------------------------------------------------------
1 | import {getCellCenter, getCellEntry, VectorDirection} from "../../utils";
2 | import {AnchorMargin, IMatrixNode} from "../../core";
3 |
4 | export function getPointWithResolver(
5 | direction: VectorDirection,
6 | cellSize: number,
7 | padding: number,
8 | item: IMatrixNode,
9 | margin: AnchorMargin
10 | ): number[] {
11 | let x1, y1;
12 | if (item.isAnchor) {
13 | [x1, y1] = getCellCenter(cellSize, padding, item.x, item.y, margin);
14 | } else {
15 | [x1, y1] = getCellEntry(
16 | direction,
17 | cellSize,
18 | padding,
19 | item.x,
20 | item.y,
21 | margin
22 | );
23 | }
24 | return [x1, y1];
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/polyline/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./polyline";
2 | export * from "./polyline.types";
3 |
--------------------------------------------------------------------------------
/src/components/polyline/polyline.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { DefaultMarker } from "../marker-default";
3 | import { getPointWithResolver } from "./getPointWithResolver";
4 | import {
5 | getSize,
6 | uniqueId,
7 | getCoords,
8 | getAllIncomes,
9 | getEdgeMargins,
10 | wrapEventHandler,
11 | getVectorDirection,
12 | checkAnchorRenderIncomes,
13 | } from "../../utils";
14 | import { IMatrixNode } from "../../core";
15 | import { DataProps } from "../element";
16 | import { LineBranch, pointResolversMap, ViewProps } from "./polyline.types";
17 |
18 | export class GraphPolyline extends React.Component<
19 | DataProps & ViewProps
20 | > {
21 | getPolyline = (
22 | cellSize: number,
23 | padding: number,
24 | branch: IMatrixNode[]
25 | ): number[][] => branch
26 | .filter((_, i) => (i + 1) !== branch.length)
27 | .reduce((result: number[][], node, i) => {
28 | const nextNode = branch[i + 1];
29 | const line = this.getLineToIncome(
30 | cellSize,
31 | padding,
32 | node,
33 | nextNode
34 | );
35 | const [x1, y1, x2, y2] = line.line;
36 | if (i === 0) {
37 | result.push([x1, y1], [x2, y2]);
38 | } else {
39 | result.push([x2, y2]);
40 | }
41 | return result;
42 | }, []);
43 |
44 | getLineToIncome(
45 | cellSize: number,
46 | padding: number,
47 | node: IMatrixNode,
48 | income: IMatrixNode
49 | ) {
50 | const margins = getEdgeMargins(node, income);
51 | const direction = getVectorDirection(
52 | node.x,
53 | node.y,
54 | income.x,
55 | income.y
56 | );
57 | const [from, to] = pointResolversMap[direction];
58 | const [nodeMargin, incomeMargin] = margins;
59 | const [x1, y1] = getPointWithResolver(
60 | from,
61 | cellSize,
62 | padding,
63 | node,
64 | nodeMargin
65 | );
66 | const [x2, y2] = getPointWithResolver(
67 | to,
68 | cellSize,
69 | padding,
70 | income,
71 | incomeMargin
72 | );
73 | return {
74 | node,
75 | income,
76 | line: [x1, y1, x2, y2]
77 | };
78 | }
79 |
80 | getLines = (
81 | cellSize: number,
82 | padding: number,
83 | node: IMatrixNode,
84 | nodesMap: { [id: string]: IMatrixNode }
85 | ): LineBranch[] => this.getNodeBranches(node, nodesMap).map(branch => ({
86 | node: node,
87 | income: branch[branch.length - 1],
88 | line: this.getPolyline(cellSize, padding, branch)
89 | }));
90 |
91 | getNodeBranches = (
92 | node: IMatrixNode,
93 | nodesMap: { [id: string]: IMatrixNode }
94 | ): IMatrixNode[][] => getAllIncomes(node, nodesMap).map(n => [
95 | node,
96 | ...this.getIncomeBranch(n, nodesMap)
97 | ]);
98 |
99 | getIncomeBranch = (
100 | lastIncome: IMatrixNode,
101 | nodesMap: { [id: string]: IMatrixNode }
102 | ): IMatrixNode[] => {
103 | const branch: IMatrixNode[] = [];
104 | while (lastIncome.isAnchor) {
105 | checkAnchorRenderIncomes(lastIncome);
106 | branch.push(lastIncome);
107 | lastIncome = nodesMap[lastIncome.renderIncomes[0]];
108 | }
109 | branch.push(lastIncome);
110 | return branch;
111 | };
112 |
113 | getLineHandlers(
114 | node: IMatrixNode,
115 | income: IMatrixNode
116 | ): { [eventName: string]: (e: React.MouseEvent) => void } {
117 | const { onEdgeClick, onEdgeMouseEnter, onEdgeMouseLeave } = this.props;
118 | const handlers: {
119 | [eventName: string]: (e: React.MouseEvent) => void;
120 | } = {};
121 | if (onEdgeClick)
122 | handlers.onClick = wrapEventHandler(onEdgeClick, node, [
123 | income
124 | ]);
125 | if (onEdgeMouseEnter)
126 | handlers.onMouseEnter = wrapEventHandler(
127 | onEdgeMouseEnter,
128 | node,
129 | [income]
130 | );
131 | if (onEdgeMouseLeave)
132 | handlers.onMouseLeave = wrapEventHandler(
133 | onEdgeMouseLeave,
134 | node,
135 | [income]
136 | );
137 | return handlers;
138 | }
139 |
140 | getMarker(markerHash: string, incomeId: string): { [key: string]: string } {
141 | const markerId = this.getMarkerId(markerHash, incomeId);
142 | return markerId ? { markerEnd: `url(#${markerId})` } : {};
143 | }
144 |
145 | getMarkerId(markerHash: string, incomeId: string): string {
146 | const { node } = this.props;
147 | return `${markerHash}-${node.id.trim()}-${incomeId.trim()}`;
148 | }
149 |
150 | getLineNameCoords(income: IMatrixNode): number[] {
151 | const { node, node: {id}, cellSize, padding } = this.props;
152 | const index = income.next.findIndex(uuid => uuid === id);
153 |
154 | const [, nodeY] = getCoords(cellSize, padding, node);
155 | const [x, incomeY] = getCoords(cellSize, padding, income);
156 |
157 | let y = nodeY;
158 | if (incomeY > nodeY) {
159 | y = incomeY;
160 | }
161 | if (incomeY === nodeY) {
162 | y = y + cellSize * index;
163 | }
164 |
165 | return [x, y];
166 | };
167 |
168 | getLineComponentCoords(income: IMatrixNode): number[] {
169 | const { node, node: {id}, cellSize, padding } = this.props;
170 | const index = income.next.findIndex(uuid => uuid === id);
171 |
172 | const [nodeX, nodeY] = getCoords(cellSize, padding, node);
173 | const [incomeX, incomeY] = getCoords