Project STORM React Diagrams

repository·master·Indexed 27 days ago

https://github.com/projectstorm/react-diagrams

A library for building interactive, customizable diagramming applications in React. It uses a Scene Graph architecture with a Model-Widget-Factory pattern to manage nodes, ports, and links. The library supports custom extensions via AbstractModelFactory and provides integration with dagre for automatic layout through the @projectstorm/react-diagrams-routing package.

Tokens
12.9K
Snippets
16
Records
101
Agent score
89%

What's inside @projectstorm/react-diagrams

  1. Understand the Model vs Widget relationship

    master

    The library follows an MVC-like pattern using a Scene Graph architecture:

    • Model: Represents the entire graph as a traversable virtual graph of nodes and links. You can mutate this model imperatively or use serialization to store snapshots. The model represents the business logic/layer.
    • Widget: Every model in the library is represented by a widget. The widgets (powered by React) handle the rendering based on the state of the model.
    • Factories: These act as the glue that connects models and widgets together.
  2. Get started with @projectstorm/react-diagrams using TypeScript

    master

    To use the library in a TypeScript project, import the core engine creation function, default models, and the canvas widget. The library provides advanced types out of the box.

    Follow these steps to bootstrap a diagram:

    1. Initialize the Engine: Use createEngine() to create a DiagramEngine instance with default configurations.
    2. Create Nodes: Instantiate DefaultNodeModel objects, set their positions, and add ports using addOutPort or addInPort.
    3. Create Links: Link ports together using the .link<T>() method on a port. You can add labels to the resulting link.
    4. Assemble the Model: Create a DiagramModel, add your nodes and links to it using .addAll(), and then pass the model to the engine via engine.setModel(model).
    5. Render: Use the CanvasWidget component from @projectstorm/react-canvas-core and pass the engine as a prop.
    import createEngine, { 
        DefaultLinkModel, 
        DefaultNodeModel,
        DiagramModel 
    } from '@projectstorm/react-diagrams';
    
    import {
        CanvasWidget
    } from '@projectstorm/react-canvas-core';
    
    // 1. Create the engine
    const engine = createEngine();
    
    // 2. Create nodes
    const node1 = new DefaultNodeModel({
    	name: 'Node 1',
    	color: 'rgb(0,192,255)',
    });
    node1.setPosition(100, 100);
    let port1 = node1.addOutPort('Out');
    
    const node2 = new DefaultNodeModel({
    	name: 'Node 2',
    	color: 'rgb(0,192,255)',
    });
    node2.setPosition(100, 100);
    let port2 = node2.addInPort('In');
    
    // 3. Link ports
    const link = port1.link<DefaultLinkModel>(port2);
    link.addLabel('Hello World!');
    
    // 4. Setup the model
    const model = new DiagramModel();
    model.addAll(node1, node2, link);
    engine.setModel(model);
    
    // 5. Render in React
    // <CanvasWidget engine={engine} />
  3. Customize components in react-diagrams

    master

    Most components in react-diagrams (nodes, ports, and links) can be customized by following a consistent three-step pattern:

    1. Extend the Model Factory: Create a custom model factory by extending AbstractModelFactory. This factory must be registered with the engine under a specific model type.
    2. Extend the Data Model (Optional): If your data requirements differ from the defaults, extend existing base classes such as NodeModel, PortModel, or DefaultLinkModel.
    3. Create a Custom Component: Develop a React component that renders using your customized data model. For certain components like ports, you can also use composition to modify appearance without a full rewrite.
  4. Create a custom LinkFactory for custom link models

    master

    To integrate a custom link model into the system, extend DefaultLinkFactory.

    1. Pass the custom type string to the super() call in the constructor to match the type defined in your DefaultLinkModel.
    2. Override generateModel() to return instances of your custom model.
    3. Override generateLinkSegment() to define how the link is rendered. The DefaultLinkWidget uses this method to render the path segments. By returning a custom React component (like an AdvancedLinkSegment) within a <g> tag, you can change the visual appearance of the link path.
    export class AdvancedLinkFactory extends DefaultLinkFactory {
    	constructor() {
    		super('advanced'); // <-- this matches with the link model above
    	}
    
    	generateModel(): AdvancedLinkModel {
    		return new AdvancedLinkModel(); // <-- this is how we get new instances
    	}
    
        /**
         * @override the DefaultLinkWidget makes use of this, and it normally renders that
         * familiar gray line, so in this case we simply make it return a new advanced segment.
         */
    	generateLinkSegment(model: AdvancedLinkModel, selected: boolean, path: string) {
    		return (
    			<g>
    				<AdvancedLinkSegment model={model} path={path} />
    			</g>
    		);
    	}
    }
  5. Implement a Node Factory to register custom nodes

    master

    To integrate a custom node into the diagram engine, you must implement a factory by extending AbstractReactFactory. This factory maps your custom node type string to its model and widget creation logic.

    • generateModel(event): Returns a new instance of your custom NodeModel.
    • generateReactWidget(event): Returns the React component (Widget) used to render the node, passing the engine and the newly created model from the event.
    import { DiamondNodeWidget } from './DiamondNodeWidget';
    import { DiamondNodeModel } from './DiamondNodeModel';
    import * as React from 'react';
    import { AbstractReactFactory } from '@projectstorm/react-canvas-core';
    import { DiagramEngine } from '@projectstorm/react-diagrams-core';
    
    export class DiamondNodeFactory extends AbstractReactFactory<DiamondNodeModel, DiagramEngine> {
    	constructor() {
    		super('diamond');
    	}
    
    	generateReactWidget(event): JSX.Element {
    		return <DiamondNodeWidget engine={this.engine} size={50} node={event.model} />;
    	}
    
    	generateModel(event) {
    		return new DiamondNodeModel();
    	}
    }
  6. Perform end-to-end testing with Puppeteer and Jest

    master

    End-to-end (e2e) testing in this library is performed using Puppeteer to drive a headless Chrome instance and Jest for assertions. The tests programmatically simulate user interactions such as clicking and dragging elements on the diagram canvas.

    When writing e2e tests, use the provided helper methods available within the test environment. These helpers allow you to:

    • Drag links between nodes.
    • Select elements.
    • Assert information about diagram elements.

    Note: These helpers interact with the UI layer to simulate physical mouse movements and do not touch the underlying model directly. Using these helpers is recommended to ensure tests are defensive and to reduce the manual overhead of writing interaction logic.

  7. Create custom port widgets

    master

    To create a custom visual representation for a port, create a React widget and wrap it in a PortWidget. The PortWidget requires the port (a PortModel) and the engine as props. You can use any styling method (Emotion, BEM, CSS) inside the wrapper.

    <PortWidget
        port={this.props.node.getPort("in")}
        engine={this.props.engine} >
        <div
            style={{
                width: 40,
                height: 40,
                background: 'orange'
            }}
        />
    </PortWidget>