From e1d32901045d800e91671fdb71cbb70113aca08d Mon Sep 17 00:00:00 2001 From: Allan Bogh Date: Fri, 17 Jul 2020 00:15:50 -0700 Subject: [PATCH] Fixed #219 of upstream. Can now render multiple graphs at the same time. --- __tests__/components/edge.test.js | 61 +- __tests__/components/graph-view.test.js | 62 +- src/components/edge.js | 25 +- src/components/graph-view.js | 50 +- src/examples/app.js | 29 +- src/examples/app.scss | 32 +- src/examples/bwdl/bwdl-example-data.js | 24 +- src/examples/bwdl/bwdl.scss | 10 +- src/examples/bwdl/index.js | 2 +- src/examples/fast/index.js | 552 ------------------ .../{fast => multiple-graphs}/graph-config.js | 0 src/examples/multiple-graphs/graph1-sample.js | 127 ++++ src/examples/multiple-graphs/graph2-sample.js | 99 ++++ src/examples/multiple-graphs/index.js | 105 ++++ .../{fast => multiple-graphs}/sidebar.js | 0 src/index.js | 6 +- src/styles/main.scss | 1 + 17 files changed, 533 insertions(+), 652 deletions(-) delete mode 100644 src/examples/fast/index.js rename src/examples/{fast => multiple-graphs}/graph-config.js (100%) create mode 100644 src/examples/multiple-graphs/graph1-sample.js create mode 100644 src/examples/multiple-graphs/graph2-sample.js create mode 100644 src/examples/multiple-graphs/index.js rename src/examples/{fast => multiple-graphs}/sidebar.js (100%) diff --git a/__tests__/components/edge.test.js b/__tests__/components/edge.test.js index 4440de76..7cd7ad05 100644 --- a/__tests__/components/edge.test.js +++ b/__tests__/components/edge.test.js @@ -252,18 +252,20 @@ describe('Edge component', () => { return rect; }); - document.querySelector = jest.fn().mockImplementation(selector => { - return { - getBoundingClientRect: boundingClientRectMock, - }; - }); + const viewWrapperElem = { + querySelector: jest.fn().mockImplementation(selector => { + return { + getBoundingClientRect: boundingClientRectMock, + }; + }), + }; - const size = Edge.getArrowSize(); + const size = Edge.getArrowSize(viewWrapperElem); - expect(document.querySelector).toHaveBeenCalledWith('defs>marker>.arrow'); + expect(viewWrapperElem.querySelector).toHaveBeenCalledWith( + 'defs>marker>.arrow' + ); expect(size).toEqual(rect); - - document.querySelector.mockRestore(); }); }); @@ -280,23 +282,24 @@ describe('Edge component', () => { Edge.getEdgePathElement(fakeEdge, viewWrapperElem); expect(viewWrapperElem.querySelector).toHaveBeenCalledWith( - '#edge-fake1-fake2-container>.edge-container>.edge>.edge-path' + "[id='edge-fake1-fake2-container']>.edge-container>.edge>.edge-path" ); }); it('returns the edge element from the document', () => { - document.querySelector = jest.fn(); + const viewWrapperElem = { + querySelector: jest.fn(), + }; const fakeEdge = { source: 'fake1', target: 'fake2', }; - Edge.getEdgePathElement(fakeEdge); + Edge.getEdgePathElement(fakeEdge, viewWrapperElem); - expect(document.querySelector).toHaveBeenCalledWith( - '#edge-fake1-fake2-container>.edge-container>.edge>.edge-path' + expect(viewWrapperElem.querySelector).toHaveBeenCalledWith( + "[id='edge-fake1-fake2-container']>.edge-container>.edge>.edge-path" ); - document.querySelector.mockRestore(); }); }); @@ -744,7 +747,7 @@ describe('Edge component', () => { }), }; - document.getElementById = jest.fn().mockImplementation(() => { + viewWrapperElem.querySelector = jest.fn().mockImplementation(() => { return node; }); @@ -758,10 +761,10 @@ describe('Edge component', () => { ); const expected = defaultExpected; - expect(document.getElementById).toHaveBeenCalledWith('node-test2'); + expect(viewWrapperElem.querySelector).toHaveBeenCalledWith( + "[id='node-test2']" + ); expect(JSON.stringify(result)).toEqual(JSON.stringify(expected)); - - document.getElementById.mockRestore(); }); it('returns a default response when there is no xlinkHref', () => { @@ -776,7 +779,7 @@ describe('Edge component', () => { }), }; - document.getElementById = jest.fn().mockImplementation(() => { + viewWrapperElem.querySelector = jest.fn().mockImplementation(() => { return node; }); @@ -790,14 +793,14 @@ describe('Edge component', () => { ); const expected = defaultExpected; - expect(document.getElementById).toHaveBeenCalledWith('node-test2'); + expect(viewWrapperElem.querySelector).toHaveBeenCalledWith( + "[id='node-test2']" + ); expect(trgNode.getAttributeNS).toHaveBeenCalledWith( 'http://www.w3.org/1999/xlink', 'href' ); expect(JSON.stringify(result)).toEqual(JSON.stringify(expected)); - - document.getElementById.mockRestore(); }); it('gets a response for a rect element', () => { @@ -812,7 +815,15 @@ describe('Edge component', () => { }), }; - document.getElementById = jest.fn().mockImplementation(() => { + let callNumber = 0; + + viewWrapperElem.querySelector = jest.fn().mockImplementation(() => { + if (callNumber === 1) { + return rectElement; + } + + callNumber = 1; + return node; }); @@ -834,8 +845,6 @@ describe('Edge component', () => { }; expect(JSON.stringify(result)).toEqual(JSON.stringify(expected)); - - document.getElementById.mockRestore(); }); }); }); diff --git a/__tests__/components/graph-view.test.js b/__tests__/components/graph-view.test.js index 8642ba03..c0efba68 100644 --- a/__tests__/components/graph-view.test.js +++ b/__tests__/components/graph-view.test.js @@ -336,6 +336,16 @@ describe('GraphView component', () => { target: 'b', }; + const querySelectorResponse = undefined; + + instance.viewWrapper = { + current: { + querySelector: jest.fn().mockImplementation(selector => { + return querySelectorResponse; + }), + }, + }; + // using Date.getTime to make this a unique edge instance.renderEdge(`test-${new Date().getTime()}`, element, edge); @@ -347,17 +357,23 @@ describe('GraphView component', () => { const container = document.createElement('g'); container.id = 'test-container'; - jest.spyOn(document, 'getElementById').mockReturnValue(container); const edge = { source: 'a', target: 'b', }; + instance.viewWrapper = { + current: { + querySelector: jest.fn().mockImplementation(selector => { + return container; + }), + }, + }; + instance.renderEdge('test', element, edge); expect(instance.entities.appendChild).not.toHaveBeenCalled(); expect(ReactDOM.render).toHaveBeenCalledWith(element, container); - document.getElementById.mockRestore(); }); }); @@ -565,6 +581,16 @@ describe('GraphView component', () => { it('appends a node element into the entities element', () => { const element = document.createElement('g'); + const querySelectorResponse = undefined; + + instance.viewWrapper = { + current: { + querySelector: jest.fn().mockImplementation(selector => { + return querySelectorResponse; + }), + }, + }; + instance.renderNode('test', element); expect(instance.entities.appendChild).toHaveBeenCalled(); @@ -575,12 +601,19 @@ describe('GraphView component', () => { const container = document.createElement('g'); container.id = 'test-container'; - jest.spyOn(document, 'getElementById').mockReturnValue(container); + + instance.viewWrapper = { + current: { + querySelector: jest.fn().mockImplementation(selector => { + return container; + }), + }, + }; + instance.renderNode('test', element); expect(instance.entities.appendChild).not.toHaveBeenCalled(); expect(ReactDOM.render).toHaveBeenCalledWith(element, container); - document.getElementById.mockRestore(); }); }); @@ -861,6 +894,14 @@ describe('GraphView component', () => { }); it('handles the zoom event when a node is not hovered nor an edge is being dragged', () => { + instance.viewWrapper = { + current: { + querySelector: jest.fn().mockImplementation(selector => { + return {}; + }), + }, + }; + instance.handleZoom(event); expect(instance.renderGraphControls).toHaveBeenCalled(); expect(instance.dragEdge).not.toHaveBeenCalled(); @@ -885,6 +926,14 @@ describe('GraphView component', () => { }); it('zooms when a node is hovered', () => { + instance.viewWrapper = { + current: { + querySelector: jest.fn().mockImplementation(selector => { + return {}; + }), + }, + }; + output.setState({ hoveredNode: {}, }); @@ -907,10 +956,7 @@ describe('GraphView component', () => { instance.selectedView = d3.select(document.createElement('g')); mouse = jest.fn().mockReturnValue([5, 15]); output.setProps({ - nodes: [ - { id: 'a', x: 5, y: 10 }, - { id: 'b', x: 10, y: 20 }, - ], + nodes: [{ id: 'a', x: 5, y: 10 }, { id: 'b', x: 10, y: 20 }], }); output.setState({ draggedEdge, diff --git a/src/components/edge.js b/src/components/edge.js index ab8e1ca8..aed78c15 100644 --- a/src/components/edge.js +++ b/src/components/edge.js @@ -82,9 +82,7 @@ class Edge extends React.Component { })(srcTrgDataArray); } - static getArrowSize( - viewWrapperElem: HTMLDivElement | HTMLDocument = document - ) { + static getArrowSize(viewWrapperElem: HTMLDivElement) { const defEndArrowElement: any = viewWrapperElem.querySelector( `defs>marker>.arrow` ); @@ -106,12 +104,9 @@ class Edge extends React.Component { return size; } - static getEdgePathElement( - edge: IEdge, - viewWrapperElem: HTMLDivElement | HTMLDocument = document - ) { + static getEdgePathElement(edge: IEdge, viewWrapperElem: HTMLDivElement) { return viewWrapperElem.querySelector( - `#edge-${edge.source}-${edge.target}-container>.edge-container>.edge>.edge-path` + `[id='edge-${edge.source}-${edge.target}-container']>.edge-container>.edge>.edge-path` ); } @@ -162,7 +157,7 @@ class Edge extends React.Component { src: any, trg: any, includesArrow: boolean, - viewWrapperElem: HTMLDivElement | HTMLDocument = document + viewWrapperElem: HTMLDivElement ) { const response = Edge.getDefaultIntersectResponse(); const arrowSize = Edge.getArrowSize(viewWrapperElem); @@ -262,7 +257,7 @@ class Edge extends React.Component { src: any, trg: any, includesArrow?: boolean = true, - viewWrapperElem: HTMLDivElement | HTMLDocument = document + viewWrapperElem: HTMLDivElement ) { const response = Edge.getDefaultIntersectResponse(); const arrowSize = Edge.getArrowSize(viewWrapperElem); @@ -366,7 +361,7 @@ class Edge extends React.Component { src: any, trg: any, includesArrow?: boolean = true, - viewWrapperElem: HTMLDivElement | HTMLDocument = document + viewWrapperElem: HTMLDivElement ) { const response = Edge.getDefaultIntersectResponse(); const arrowSize = Edge.getArrowSize(viewWrapperElem); @@ -426,7 +421,7 @@ class Edge extends React.Component { trg: any, nodeKey: string, includesArrow?: boolean = true, - viewWrapperElem?: HTMLDivElement = document + viewWrapperElem: React.RefObject ) { let response = Edge.getDefaultIntersectResponse(); @@ -437,7 +432,11 @@ class Edge extends React.Component { // Note: document.getElementById is by far the fastest way to get a node. // compare 2.82ms for querySelector('#node-a2 use.node') vs // 0.31ms and 99us for document.getElementById() - const nodeElem = document.getElementById(`node-${trg[nodeKey]}`); + // Although it doesn't allow multiple graphs. + // We can use viewWrapperElem to scope the querySelector to a smaller set of elements to improve the speed + const nodeElem = viewWrapperElem.querySelector( + `[id='node-${trg[nodeKey]}']` + ); if (!nodeElem) { return response; diff --git a/src/components/graph-view.js b/src/components/graph-view.js index 25b930ba..4b98f31a 100644 --- a/src/components/graph-view.js +++ b/src/components/graph-view.js @@ -186,9 +186,16 @@ class GraphView extends React.Component { componentDidMount() { const { initialBBox, zoomDelay, minZoom, maxZoom } = this.props; - // TODO: can we target the element rather than the document? - document.addEventListener('keydown', this.handleWrapperKeydown); - document.addEventListener('click', this.handleDocumentClick); + if (this.viewWrapper.current) { + this.viewWrapper.current.addEventListener( + 'keydown', + this.handleWrapperKeydown + ); + this.viewWrapper.current.addEventListener( + 'click', + this.handleDocumentClick + ); + } this.zoom = d3 .zoom() @@ -230,8 +237,14 @@ class GraphView extends React.Component { } componentWillUnmount() { - document.removeEventListener('keydown', this.handleWrapperKeydown); - document.removeEventListener('click', this.handleDocumentClick); + this.viewWrapper.current.removeEventListener( + 'keydown', + this.handleWrapperKeydown + ); + this.viewWrapper.current.removeEventListener( + 'click', + this.handleDocumentClick + ); } shouldComponentUpdate( @@ -1277,9 +1290,10 @@ class GraphView extends React.Component { } const containerId = `${id}-container`; - let nodeContainer: HTMLElement | Element | null = document.getElementById( - containerId - ); + let nodeContainer: + | HTMLElement + | Element + | null = this.viewWrapper.current.querySelector(`[id='${containerId}']`); if (!nodeContainer) { nodeContainer = document.createElementNS( @@ -1360,14 +1374,20 @@ class GraphView extends React.Component { const customContainerId = `${id}-custom-container`; const { draggedEdge } = this.state; const { afterRenderEdge } = this.props; - let edgeContainer = document.getElementById(containerId); + let edgeContainer = this.viewWrapper.current.querySelector( + `[id='${containerId}']` + ); if (nodeMoving && edgeContainer) { edgeContainer.style.display = 'none'; containerId = `${id}-custom-container`; - edgeContainer = document.getElementById(containerId); + edgeContainer = this.viewWrapper.current.querySelector( + `[id='${containerId}']` + ); } else if (edgeContainer) { - const customContainer = document.getElementById(customContainerId); + const customContainer = this.viewWrapper.current.querySelector( + `[id='${customContainerId}']` + ); edgeContainer.style.display = ''; @@ -1457,8 +1477,8 @@ class GraphView extends React.Component { return; } - const graphControlsWrapper = this.viewWrapper.current.ownerDocument.getElementById( - 'react-digraph-graph-controls-wrapper' + const graphControlsWrapper = this.viewWrapper.current.querySelector( + '#react-digraph-graph-controls-wrapper' ); if (!graphControlsWrapper) { @@ -1565,7 +1585,7 @@ class GraphView extends React.Component { return; } - const node = this.entities.querySelector(`#node-${id}-container`); + const node = this.entities.querySelector(`[id='node-${id}-container']`); this.panToEntity(node, zoom); } @@ -1576,7 +1596,7 @@ class GraphView extends React.Component { } const edge = this.entities.querySelector( - `#edge-${source}-${target}-container` + `[id='edge-${source}-${target}-container']` ); this.panToEntity(edge, zoom); diff --git a/src/examples/app.js b/src/examples/app.js index fcbd9cae..072cd399 100644 --- a/src/examples/app.js +++ b/src/examples/app.js @@ -27,7 +27,7 @@ import { import Bwdl from './bwdl'; import BwdlEditable from './bwdl-editable'; import Graph from './graph'; -import GraphFast from './fast'; +import MultipleGraphs from './multiple-graphs'; import './app.scss'; @@ -35,15 +35,26 @@ class App extends React.Component { render() { return ( -
+
@@ -53,7 +64,7 @@ class App extends React.Component { {/* The following is for typos */} - +
diff --git a/src/examples/app.scss b/src/examples/app.scss index 54486126..ecdbbfb9 100644 --- a/src/examples/app.scss +++ b/src/examples/app.scss @@ -27,7 +27,6 @@ button { #graph { height: 100%; width: 100%; - display: flex; } .total-nodes { @@ -40,15 +39,27 @@ button { > nav { height: 25px; - > a { - border-right: 1px solid black; - line-height: 25px; - min-width: 150px; - padding: 10px; - - &.active { - background: #333; - color: white; + > ul { + list-style: none; + display: flex; + margin: 0; + padding-left: 0; + + > li { + border-right: 1px solid black; + height: 100%; + display: flex; + align-items: center; + + >a { + + padding: 10px; + + &.active { + background: #333; + color: white; + } + } } } } @@ -56,7 +67,6 @@ button { .graph-header { border-bottom: 1px solid black; - position: fixed; width: 100%; background-color: #fff; padding: 10px; diff --git a/src/examples/bwdl/bwdl-example-data.js b/src/examples/bwdl/bwdl-example-data.js index 806d7bf3..a6150ef4 100644 --- a/src/examples/bwdl/bwdl-example-data.js +++ b/src/examples/bwdl/bwdl-example-data.js @@ -16,20 +16,18 @@ */ export default { - ExampleSource: - 'https://code.uberinternal.com/file/data/aioyv5yrrs3dadbmxlap/PHID-FILE-v36jeiyn4y3gphtdwjsm/1.json', + ExampleSource: 'https://fake.com/1.json', Name: 'Colombo_Intercity_Driver_dispatch', - Comment: - 'Send SMS message to drivers accept dispatch for Colombo intercity trip', + Comment: 'Send SMS message to accept trip', Version: 1, - Domain: '//Autobots', - Id: '//Autobots/ColomboIntercityDriverDispatch', + Domain: '//Domain', + Id: '//Domain/Dispatch', StartAt: 'Init', AllowReentry: true, States: { Init: { Type: 'Terminator', - Resource: 'kafka://hp_demand_job-assigned', + Resource: 'k://demand_job-assigned', ResultPath: '$.event', Next: 'Check City and Vehicle View', }, @@ -45,7 +43,7 @@ export default { }, { Variable: '$.vehicleViewId', - NumberEquals: 20006733, + NumberEquals: 99999999, }, ], Next: 'SMS for Dispatch accepted', @@ -90,11 +88,11 @@ export default { InputPath: '$.event', Result: { expirationMinutes: 60, - fromUserUUID: '71af5aea-9eaa-45a1-9825-2c124030b063', + fromUserUUID: '55555555-4444-3333-2222-111111111111', toUserUUID: 'Eval($.supplyUUID)', getSMSReply: false, message: - 'Hithawath Partner, Oba labegena athi mema trip eka UberGALLE trip ekaki, Karunakara rider wa amatha drop location eka confirm karaganna. Sthuthi', + 'Partner, Oba labegena athi mema trip eka Blah trip ekaki, rider wa amatha drop location eka confirm.', messageType: 'SEND_SMS', priority: 1, actionUUID: 'd259c34d-457a-411e-8c93-6edd63a7ddc6', @@ -107,11 +105,11 @@ export default { InputPath: '$.event', Result: { expirationMinutes: 60, - fromUserUUID: '71af5aea-9eaa-45a1-9825-2c124030b063', + fromUserUUID: '55555555-4444-3333-2222-111111111111', toUserUUID: 'Eval($.supplyUUID)', getSMSReply: false, message: - 'Hithawath Partner, Oba labegena athi mema trip eka UberGALLE trip ekaki, Karunakara rider wa amatha drop location eka confirm karaganna. Sthuthi', + 'Partner, Oba labegena athi mema trip eka Blah trip ekaki, rider wa amatha drop location eka confirm.', messageType: 'SEND_SMS', priority: 1, actionUUID: 'd259c34d-457a-411e-8c93-6edd63a7ddc6', @@ -122,7 +120,7 @@ export default { 'Send SMS': { Type: 'Task', InputPath: '$.actionParam', - Resource: 'uns://sjc1/sjc1-prod01/us1/cleopatra/Cleopatra::sendSMS', + Resource: 'uns://dc/server/Thing::sendSMS', InputSchema: { '$.expirationMinutes': 'int', '$.toUserUUID': 'string', diff --git a/src/examples/bwdl/bwdl.scss b/src/examples/bwdl/bwdl.scss index 827c250e..cc4d84a5 100644 --- a/src/examples/bwdl/bwdl.scss +++ b/src/examples/bwdl/bwdl.scss @@ -15,9 +15,17 @@ */ #bwdl-graph { - height: 100%; + height: calc(100% - 26px); width: 100%; display: flex; + + .sidebar.left { + > .sidebar-main-container { + > div { + height: 100%; + } + } + } } .selected-node-container { diff --git a/src/examples/bwdl/index.js b/src/examples/bwdl/index.js index fcd977ec..e5560b6b 100644 --- a/src/examples/bwdl/index.js +++ b/src/examples/bwdl/index.js @@ -172,7 +172,7 @@ class Bwdl extends React.Component<{}, IBwdlState> { (this.GraphView = el)} nodeKey={NODE_KEY} - readOnly={true} + readOnly={false} nodes={nodes} edges={edges} selected={selected} diff --git a/src/examples/fast/index.js b/src/examples/fast/index.js deleted file mode 100644 index f11d0f1d..00000000 --- a/src/examples/fast/index.js +++ /dev/null @@ -1,552 +0,0 @@ -// @flow -/* - Copyright(c) 2018 Uber Technologies, Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -/* - Example usage of GraphView component -*/ - -import * as React from 'react'; - -import { - GraphViewFast, - type IEdgeType as IEdge, - type INodeType as INode, - type LayoutEngineType, -} from '../../'; -import GraphConfig, { - edgeTypes, - EMPTY_EDGE_TYPE, - EMPTY_TYPE, - NODE_KEY, - nodeTypes, - POLY_TYPE, - SPECIAL_CHILD_SUBTYPE, - SPECIAL_EDGE_TYPE, - SPECIAL_TYPE, - SKINNY_TYPE, -} from './graph-config'; // Configures node/edge types - -type IGraph = { - nodes: INode[], - edges: IEdge[], -}; - -// NOTE: Edges must have 'source' & 'target' attributes -// In a more realistic use case, the graph would probably originate -// elsewhere in the App or be generated from some other state upstream of this component. -const sample: IGraph = { - edges: [ - { - handleText: '5', - source: 'start1', - target: 'a1', - type: SPECIAL_EDGE_TYPE, - }, - { - handleText: '5', - source: 'a1', - target: 'a2', - type: SPECIAL_EDGE_TYPE, - }, - { - handleText: '54', - source: 'a2', - target: 'a4', - type: EMPTY_EDGE_TYPE, - }, - { - handleText: '54', - source: 'a1', - target: 'a3', - type: EMPTY_EDGE_TYPE, - }, - { - handleText: '54', - source: 'a3', - target: 'a4', - type: EMPTY_EDGE_TYPE, - }, - { - handleText: '54', - source: 'a1', - target: 'a5', - type: EMPTY_EDGE_TYPE, - }, - { - handleText: '54', - source: 'a4', - target: 'a1', - type: EMPTY_EDGE_TYPE, - }, - { - handleText: '54', - source: 'a1', - target: 'a6', - type: EMPTY_EDGE_TYPE, - }, - { - handleText: '24', - source: 'a1', - target: 'a7', - type: EMPTY_EDGE_TYPE, - }, - ], - nodes: [ - { - id: 'start1', - title: 'Start (0)', - type: SPECIAL_TYPE, - }, - { - id: 'a1', - title: 'Node A (1)', - type: SPECIAL_TYPE, - x: 258.3976135253906, - y: 331.9783248901367, - }, - { - id: 'a2', - subtype: SPECIAL_CHILD_SUBTYPE, - title: 'Node B (2)', - type: EMPTY_TYPE, - x: 593.9393920898438, - y: 260.6060791015625, - }, - { - id: 'a3', - title: 'Node C (3)', - type: EMPTY_TYPE, - x: 237.5757598876953, - y: 61.81818389892578, - }, - { - id: 'a4', - title: 'Node D (4)', - type: EMPTY_TYPE, - x: 600.5757598876953, - y: 600.81818389892578, - }, - { - id: 'a5', - title: 'Node E (5)', - type: null, - x: 50.5757598876953, - y: 500.81818389892578, - }, - { - id: 'a6', - title: 'Node E (6)', - type: SKINNY_TYPE, - x: 300, - y: 600, - }, - { - id: 'a7', - title: 'Node F (7)', - type: POLY_TYPE, - x: 0, - y: 300, - }, - ], -}; - -function generateSample(totalNodes) { - const generatedSample: IGraph = { - edges: [], - nodes: [], - }; - let y = 0; - let x = 0; - - const numNodes = totalNodes ? totalNodes : 0; - - // generate large array of nodes - // These loops are fast enough. 1000 nodes = .45ms + .34ms - // 2000 nodes = .86ms + .68ms - // implying a linear relationship with number of nodes. - for (let i = 1; i <= numNodes; i++) { - if (i % 20 === 0) { - y++; - x = 0; - } else { - x++; - } - - generatedSample.nodes.push({ - id: `a${i}`, - title: `Node ${i}`, - type: nodeTypes[Math.floor(nodeTypes.length * Math.random())], - x: 0 + 200 * x, - y: 0 + 200 * y, - }); - } - // link each node to another node - for (let i = 1; i < numNodes; i++) { - generatedSample.edges.push({ - source: `a${i}`, - target: `a${i + 1}`, - type: edgeTypes[Math.floor(edgeTypes.length * Math.random())], - }); - } - - return generatedSample; -} - -type IGraphProps = {}; - -type IGraphState = { - graph: any, - selected: any, - totalNodes: number, - copiedNode: any, - layoutEngineType?: LayoutEngineType, -}; - -class Graph extends React.Component { - GraphView; - - constructor(props: IGraphProps) { - super(props); - - this.state = { - copiedNode: null, - graph: sample, - layoutEngineType: undefined, - selected: null, - totalNodes: sample.nodes.length, - }; - - this.GraphView = React.createRef(); - } - - // Helper to find the index of a given node - getNodeIndex(searchNode: INode | any) { - return this.state.graph.nodes.findIndex(node => { - return node[NODE_KEY] === searchNode[NODE_KEY]; - }); - } - - // Helper to find the index of a given edge - getEdgeIndex(searchEdge: IEdge) { - return this.state.graph.edges.findIndex(edge => { - return ( - edge.source === searchEdge.source && edge.target === searchEdge.target - ); - }); - } - - // Given a nodeKey, return the corresponding node - getViewNode(nodeKey: string) { - const searchNode = {}; - - searchNode[NODE_KEY] = nodeKey; - const i = this.getNodeIndex(searchNode); - - return this.state.graph.nodes[i]; - } - - makeItLarge = () => { - const graph = this.state.graph; - const generatedSample = generateSample(this.state.totalNodes); - - graph.nodes = generatedSample.nodes; - graph.edges = generatedSample.edges; - this.setState(this.state); - }; - - addStartNode = () => { - const graph = this.state.graph; - - // using a new array like this creates a new memory reference - // this will force a re-render - graph.nodes = [ - { - id: Date.now(), - title: 'Node A', - type: SPECIAL_TYPE, - x: 0, - y: 0, - }, - ...this.state.graph.nodes, - ]; - this.setState({ - graph, - }); - }; - deleteStartNode = () => { - const graph = this.state.graph; - - graph.nodes.splice(0, 1); - // using a new array like this creates a new memory reference - // this will force a re-render - graph.nodes = [...this.state.graph.nodes]; - this.setState({ - graph, - }); - }; - - handleChange = (event: any) => { - this.setState( - { - totalNodes: parseInt(event.target.value || '0', 10), - }, - this.makeItLarge - ); - }; - - /* - * Handlers/Interaction - */ - - // Called by 'drag' handler, etc.. - // to sync updates from D3 with the graph - onUpdateNode = (viewNode: INode) => { - const graph = this.state.graph; - const i = this.getNodeIndex(viewNode); - - graph.nodes[i] = viewNode; - this.setState({ graph }); - }; - - // Node 'mouseUp' handler - onSelectNode = (viewNode: INode | null) => { - // Deselect events will send Null viewNode - this.setState({ selected: viewNode }); - }; - - // Edge 'mouseUp' handler - onSelectEdge = (viewEdge: IEdge) => { - this.setState({ selected: viewEdge }); - }; - - // Updates the graph with a new node - onCreateNode = (x: number, y: number) => { - const graph = this.state.graph; - - // This is just an example - any sort of logic - // could be used here to determine node type - // There is also support for subtypes. (see 'sample' above) - // The subtype geometry will underlay the 'type' geometry for a node - const type = Math.random() < 0.25 ? SPECIAL_TYPE : EMPTY_TYPE; - - const viewNode = { - id: Date.now(), - title: '', - type, - x, - y, - }; - - graph.nodes = [...graph.nodes, viewNode]; - this.setState({ graph }); - }; - - // Deletes a node from the graph - onDeleteNode = (viewNode: INode, nodeId: string, nodeArr: INode[]) => { - const graph = this.state.graph; - // Delete any connected edges - const newEdges = graph.edges.filter((edge, i) => { - return ( - edge.source !== viewNode[NODE_KEY] && edge.target !== viewNode[NODE_KEY] - ); - }); - - graph.nodes = nodeArr; - graph.edges = newEdges; - - this.setState({ graph, selected: null }); - }; - - // Creates a new node between two edges - onCreateEdge = (sourceViewNode: INode, targetViewNode: INode) => { - const graph = this.state.graph; - // This is just an example - any sort of logic - // could be used here to determine edge type - const type = - sourceViewNode.type === SPECIAL_TYPE - ? SPECIAL_EDGE_TYPE - : EMPTY_EDGE_TYPE; - - const viewEdge = { - source: sourceViewNode[NODE_KEY], - target: targetViewNode[NODE_KEY], - type, - }; - - // Only add the edge when the source node is not the same as the target - if (viewEdge.source !== viewEdge.target) { - graph.edges = [...graph.edges, viewEdge]; - this.setState({ - graph, - selected: viewEdge, - }); - } - }; - - // Called when an edge is reattached to a different target. - onSwapEdge = ( - sourceViewNode: INode, - targetViewNode: INode, - viewEdge: IEdge - ) => { - const graph = this.state.graph; - const i = this.getEdgeIndex(viewEdge); - const edge = JSON.parse(JSON.stringify(graph.edges[i])); - - edge.source = sourceViewNode[NODE_KEY]; - edge.target = targetViewNode[NODE_KEY]; - graph.edges[i] = edge; - // reassign the array reference if you want the graph to re-render a swapped edge - graph.edges = [...graph.edges]; - - this.setState({ - graph, - selected: edge, - }); - }; - - // Called when an edge is deleted - onDeleteEdge = (viewEdge: IEdge, edges: IEdge[]) => { - const graph = this.state.graph; - - graph.edges = edges; - this.setState({ - graph, - selected: null, - }); - }; - - onUndo = () => { - // Not implemented - console.warn('Undo is not currently implemented in the example.'); - // Normally any add, remove, or update would record the action in an array. - // In order to undo it one would simply call the inverse of the action performed. For instance, if someone - // called onDeleteEdge with (viewEdge, i, edges) then an undelete would be a splicing the original viewEdge - // into the edges array at position i. - }; - - onCopySelected = () => { - if (this.state.selected.source) { - console.warn('Cannot copy selected edges, try selecting a node instead.'); - - return; - } - - const x = this.state.selected.x + 10; - const y = this.state.selected.y + 10; - - this.setState({ - copiedNode: { ...this.state.selected, x, y }, - }); - }; - - onPasteSelected = () => { - if (!this.state.copiedNode) { - console.warn( - 'No node is currently in the copy queue. Try selecting a node and copying it with Ctrl/Command-C' - ); - } - - const graph = this.state.graph; - const newNode = { ...this.state.copiedNode, id: Date.now() }; - - graph.nodes = [...graph.nodes, newNode]; - this.forceUpdate(); - }; - - handleChangeLayoutEngineType = (event: any) => { - this.setState({ - layoutEngineType: (event.target.value: LayoutEngineType | 'None'), - }); - }; - - onSelectPanNode = (event: any) => { - if (this.GraphView) { - this.GraphView.panToNode(event.target.value, true); - } - }; - - /* - * Render - */ - - render() { - const { nodes, edges } = this.state.graph; - const selected = this.state.selected; - const { NodeTypes, NodeSubtypes, EdgeTypes } = GraphConfig; - - return ( -
-
- - - -
- Layout Engine: - -
-
- Pan To: - -
-
- (this.GraphView = el)} - nodeKey={NODE_KEY} - nodes={nodes} - edges={edges} - selected={selected} - nodeTypes={NodeTypes} - nodeSubtypes={NodeSubtypes} - edgeTypes={EdgeTypes} - onSelectNode={this.onSelectNode} - onCreateNode={this.onCreateNode} - onUpdateNode={this.onUpdateNode} - onDeleteNode={this.onDeleteNode} - onSelectEdge={this.onSelectEdge} - onCreateEdge={this.onCreateEdge} - onSwapEdge={this.onSwapEdge} - onDeleteEdge={this.onDeleteEdge} - onUndo={this.onUndo} - onCopySelected={this.onCopySelected} - onPasteSelected={this.onPasteSelected} - layoutEngineType={this.state.layoutEngineType} - /> -
- ); - } -} - -export default Graph; diff --git a/src/examples/fast/graph-config.js b/src/examples/multiple-graphs/graph-config.js similarity index 100% rename from src/examples/fast/graph-config.js rename to src/examples/multiple-graphs/graph-config.js diff --git a/src/examples/multiple-graphs/graph1-sample.js b/src/examples/multiple-graphs/graph1-sample.js new file mode 100644 index 00000000..fa5f601f --- /dev/null +++ b/src/examples/multiple-graphs/graph1-sample.js @@ -0,0 +1,127 @@ +// @flow + +import { + EMPTY_EDGE_TYPE, + EMPTY_TYPE, + POLY_TYPE, + SPECIAL_CHILD_SUBTYPE, + SPECIAL_EDGE_TYPE, + SPECIAL_TYPE, + SKINNY_TYPE, +} from './graph-config'; // Configures node/edge types + +export default { + edges: [ + { + handleText: '5', + source: 'start1', + target: 'a1', + type: SPECIAL_EDGE_TYPE, + }, + { + handleText: '5', + source: 'a1', + target: 'a2', + type: SPECIAL_EDGE_TYPE, + }, + { + handleText: '54', + source: 'a2', + target: 'a4', + type: EMPTY_EDGE_TYPE, + }, + { + handleText: '54', + source: 'a1', + target: 'a3', + type: EMPTY_EDGE_TYPE, + }, + { + handleText: '54', + source: 'a3', + target: 'a4', + type: EMPTY_EDGE_TYPE, + }, + { + handleText: '54', + source: 'a1', + target: 'a5', + type: EMPTY_EDGE_TYPE, + }, + { + handleText: '54', + source: 'a4', + target: 'a1', + type: EMPTY_EDGE_TYPE, + }, + { + handleText: '54', + source: 'a1', + target: 'a6', + type: EMPTY_EDGE_TYPE, + }, + { + handleText: '24', + source: 'a1', + target: 'a7', + type: EMPTY_EDGE_TYPE, + }, + ], + nodes: [ + { + id: 'start1', + title: 'Start (0)', + type: SPECIAL_TYPE, + }, + { + id: 'a1', + title: 'Node A (1)', + type: SPECIAL_TYPE, + x: 258.3976135253906, + y: 331.9783248901367, + }, + { + id: 'a2', + subtype: SPECIAL_CHILD_SUBTYPE, + title: 'Node B (2)', + type: EMPTY_TYPE, + x: 593.9393920898438, + y: 260.6060791015625, + }, + { + id: 'a3', + title: 'Node C (3)', + type: EMPTY_TYPE, + x: 237.5757598876953, + y: 61.81818389892578, + }, + { + id: 'a4', + title: 'Node D (4)', + type: EMPTY_TYPE, + x: 600.5757598876953, + y: 600.81818389892578, + }, + { + id: 'a5', + title: 'Node E (5)', + type: null, + x: 50.5757598876953, + y: 500.81818389892578, + }, + { + id: 'a6', + title: 'Node E (6)', + type: SKINNY_TYPE, + x: 300, + y: 600, + }, + { + id: 'a7', + title: 'Node F (7)', + type: POLY_TYPE, + x: 0, + y: 300, + }, + ], +}; diff --git a/src/examples/multiple-graphs/graph2-sample.js b/src/examples/multiple-graphs/graph2-sample.js new file mode 100644 index 00000000..cc0fc159 --- /dev/null +++ b/src/examples/multiple-graphs/graph2-sample.js @@ -0,0 +1,99 @@ +// @flow + +import { + EMPTY_EDGE_TYPE, + EMPTY_TYPE, + SPECIAL_CHILD_SUBTYPE, + SPECIAL_EDGE_TYPE, + SPECIAL_TYPE, +} from './graph-config'; // Configures node/edge types + +export default { + edges: [ + { + handleText: '5', + source: 'start1', + target: 'a1', + type: SPECIAL_EDGE_TYPE, + }, + { + handleText: '5', + source: 'a1', + target: 'a2', + type: SPECIAL_EDGE_TYPE, + }, + { + handleText: '54', + source: 'a2', + target: 'a4', + type: EMPTY_EDGE_TYPE, + }, + { + handleText: '54', + source: 'a1', + target: 'a3', + type: EMPTY_EDGE_TYPE, + }, + { + handleText: '54', + source: 'a3', + target: 'a4', + type: EMPTY_EDGE_TYPE, + }, + { + handleText: '54', + source: 'a1', + target: 'a5', + type: EMPTY_EDGE_TYPE, + }, + { + handleText: '54', + source: 'a4', + target: 'a1', + type: EMPTY_EDGE_TYPE, + }, + ], + nodes: [ + { + id: 'start1', + title: 'Start (0)', + type: SPECIAL_TYPE, + }, + { + id: 'a1', + title: 'Node A (1)', + type: SPECIAL_TYPE, + x: 258.3976135253906, + y: 331.9783248901367, + }, + { + id: 'a2', + subtype: SPECIAL_CHILD_SUBTYPE, + title: 'Node B (2)', + type: EMPTY_TYPE, + x: 593.9393920898438, + y: 260.6060791015625, + }, + { + id: 'a3', + title: 'Node C (3)', + type: EMPTY_TYPE, + x: 237.5757598876953, + y: 61.81818389892578, + }, + { + id: 'a4', + title: 'Node D (4)', + type: EMPTY_TYPE, + x: 600.5757598876953, + y: 600.81818389892578, + }, + { + id: 'a5', + title: 'Node E (5)', + type: null, + x: 50.5757598876953, + y: 500.81818389892578, + }, + ], +}; diff --git a/src/examples/multiple-graphs/index.js b/src/examples/multiple-graphs/index.js new file mode 100644 index 00000000..a42db69f --- /dev/null +++ b/src/examples/multiple-graphs/index.js @@ -0,0 +1,105 @@ +// @flow +/* + Copyright(c) 2018 Uber Technologies, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +/* + Example usage of GraphView component +*/ + +import * as React from 'react'; +import { GraphView } from '../../'; +import GraphConfig, { NODE_KEY } from './graph-config'; // Configures node/edge types +import sample1 from './graph1-sample'; +import sample2 from './graph2-sample'; + +type IGraphProps = {}; +type IGraphState = {}; + +class Graph extends React.Component { + GraphViewRef; + + constructor(props: IGraphProps) { + super(props); + this.GraphViewRef = React.createRef(); + } + + /* + * Render + */ + + render() { + const { nodes: graph1Nodes, edges: graph1Edges } = sample1; + const { nodes: graph2Nodes, edges: graph2Edges } = sample2; + const { NodeTypes, NodeSubtypes, EdgeTypes } = GraphConfig; + + return ( +
+
+ (this.GraphViewRef = el)} + nodeKey={NODE_KEY} + nodes={graph1Nodes} + edges={graph1Edges} + selected={null} + nodeTypes={NodeTypes} + nodeSubtypes={NodeSubtypes} + edgeTypes={EdgeTypes} + readOnly={false} + onSelectNode={() => {}} + onCreateNode={() => {}} + onUpdateNode={() => {}} + onDeleteNode={() => {}} + onSelectEdge={() => {}} + onCreateEdge={() => {}} + onSwapEdge={() => {}} + onDeleteEdge={() => {}} + /> +
+
+ (this.GraphViewRef = el)} + nodeKey={NODE_KEY} + nodes={graph2Nodes} + edges={graph2Edges} + selected={null} + nodeTypes={NodeTypes} + nodeSubtypes={NodeSubtypes} + edgeTypes={EdgeTypes} + readOnly={false} + onSelectNode={() => {}} + onCreateNode={() => {}} + onUpdateNode={() => {}} + onDeleteNode={() => {}} + onSelectEdge={() => {}} + onCreateEdge={() => {}} + onSwapEdge={() => {}} + onDeleteEdge={() => {}} + /> +
+
+ ); + } +} + +export default Graph; diff --git a/src/examples/fast/sidebar.js b/src/examples/multiple-graphs/sidebar.js similarity index 100% rename from src/examples/fast/sidebar.js rename to src/examples/multiple-graphs/sidebar.js diff --git a/src/index.js b/src/index.js index 0684cda0..0b7b56ce 100644 --- a/src/index.js +++ b/src/index.js @@ -20,14 +20,14 @@ import { type LayoutEngine as LayoutEngineConfigTypes } from './utilities/layout import type { IEdge } from './components/edge'; import type { INode } from './components/node'; -export { default as GraphViewFast } from './components/graph-view'; - export { default as Edge } from './components/edge'; export type IEdgeType = IEdge; export { default as GraphUtils } from './utilities/graph-util'; export { default as Node } from './components/node'; export type INodeType = INode; -export { default as BwdlTransformer } from './utilities/transformers/bwdl-transformer'; +export { + default as BwdlTransformer, +} from './utilities/transformers/bwdl-transformer'; export { GV as GraphView }; export type LayoutEngineType = LayoutEngineConfigTypes; export default GV; diff --git a/src/styles/main.scss b/src/styles/main.scss index c0b17fd5..6afc3d59 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -31,6 +31,7 @@ $background-color: #f9f9f9; opacity: 1; outline: none; user-select: none; + position: relative; > .graph { align-content: stretch;