Git Product home page Git Product logo

dagre-reactjs's Introduction

DagreReact

React component for rendering a dagre graph layout without any dependency on d3.

Getting started

Install dagre-reactjs using npm.

npm install dagre-reactjs --save

or using yarn

yarn add dagre-reactjs

Dependencies

This project has a peer dependency on dagre so your project version can be used, but this means you will need to install it separetely from this library.

Examples

Running version of the examples can be seen here https://infallible-davinci-4bc8e0.netlify.app/

Basic usage

DagreReact component needs the data to display in a format it understands.

import * as React from "react";

import { RecursivePartial, NodeOptions, EdgeOptions, DagreReact } from "dagre-reactjs";

export const basic1: {
  nodes: Array<RecursivePartial<NodeOptions>>;
  edges: Array<RecursivePartial<EdgeOptions>>;
} = {
  nodes: [
    {
      id: "0",
      label: "Project Start",
    },
    {
      id: "1",
      label: "Project End",
    }
  ],
  edges: [
    {
      from: "0",
      to: "1"
    }
  ]
};

export default class App extends React.Component<{}, {}> {
  render() {
    return (
      <svg id="schedule" width={1000} height={1000}>
        <DagreReact
          nodes={basic1.nodes}
          edges={basic1.edges}
        />
      </svg>
    );
  }
}

Overriding dagre graph settings

Dagre's defaults can be overriden by passing the graphOptions prop to the DagreReact component.

See dagre documentation here https://github.com/dagrejs/dagre/wiki#configuring-the-layout

All values under object graph can be passed through using this prop and they will be used in the next layout pass.

Node settings

If using typescript the exact type definition for nodes is exported as NodeOptions interface.

export interface NodeOptions {
  id: string;
  label: string;
  shape: string;
  labelType: string;
  styles: {
    node: {
      className?: string;
      padding: {
        top: number;
        bottom: number;
        left: number;
        right: number;
      };
    };
    shape: {
      className?: string;
      styles?: CSS.Properties;
      cornerRadius?: number;
    };
    label: {
      className?: string;
      styles?: CSS.Properties;
    };
  };
  width?: number;
  height?: number;
  x?: number;
  y?: number;
  meta: {
    [key: string]: any;
  }
}

The prop passed into DagreReact accepts a partial representation of this object, only "id" is required. WARNING: Currently manually setting nodes width and height from this object will not work, they will be erased before the graph layout is run. This is a design issue were it interferes with the sizing hooks and the intersection logic.

User defaults

Rather than setting common configuration on every node object you can pass a defaults object to DagreReact and it will merge the custom node settings with the user defaults and the DagreReact defaults for you. This is achieved with the defaultNodeConfig prop.

const DEFAULT_NODE_CONFIG = {
  styles: {
    node: {
      padding: {
        top: 10,
        bottom: 10,
        left: 10,
        right: 10
      }
    },
    shape: {
      styles: { fill: "#845" }
    }
  }
};

...


<DagreReact
  nodes={nodes}
  edges={edges}
  defaultNodeConfig={DEFAULT_NODE_CONFIG}
/>

Edge settings

If using typescript the exact type definition for edges is exported as EdgeOptions interface.

export interface EdgeOptions {
  from: string;
  to: string;
  label?: string;
  labelPos: "l" | "r" | "c";
  labelOffset: number;
  labelType: string;
  markerType: string;
  pathType: string;
  points?: Array<Point>;
  path?: string;
  width?: number;
  height?: number;
  x?: number;
  y?: number;
  styles: {
    label: {
      className: string;
      styles: CSS.Properties;
    },
    edge: {
      className: string;
      styles: CSS.Properties;
    },
    marker: {
      className: string;
      styles: CSS.Properties;
    },
  },
  meta: {
    [key: string]: any;
  }
}

The prop passed into DagreReact accepts a partial representation of this object, only "to" and "from" is required. WARNING: Currently manually setting edge width and height from this object will not work, they will be erased before the graph layout is run. This is a design issue were it interferes with the sizing hooks and the intersection logic.

User defaults

Rather than setting common configuration on every edge object you can pass a defaults object to DagreReact and it will merge the custom edge settings with the user defaults and the DagreReact defaults for you. This is achieved with the defaultEdgeConfig prop.

const DEFAULT_EDGE_CONFIG = {
  styles: {
    edge: {
      styles: { fillOpacity: 0, stroke: "#000", strokeWidth: "1px" }
    }
  }
};
...


<DagreReact
  nodes={nodes}
  edges={edges}
  defaultEdgeConfig={DEFAULT_EDGE_CONFIG}
/>

Custom Shapes

Each built in node is rendered as a shape layer and a label layer. The shape layer is responsible for rendering a shape if required and handling the intersection detection so that edge lines can line up correctly with complex shapes. Shapes do not have to render anything to the screen.

Build in there are three shapes circle/rect/diamond, if something else is needed the user can pass a map of custom shapes. The examples include a custom shapes demonstration.

To add a house shape for example, you need to create a component for the renderer.

export const calculateHousePoints = (size: Size): Array<Point> => {
  const width = size.width;
  const height = size.height;

  const xOffset = width / 2;
  const yOffset = (-height * 3) / 5;

  const points = [
    { x: 0 - xOffset, y: 0 - yOffset },
    { x: width - xOffset, y: 0 - yOffset },
    { x: width - xOffset, y: -height - yOffset },
    { x: width / 2 - xOffset, y: (-height * 3) / 2 - yOffset },
    { x: 0 - xOffset, y: -height - yOffset }
  ];

  return points;
};

export const House: React.FC<ShapeComponentProps> = ({ node, innerSize }) => {
  if (!node || !innerSize || !(innerSize.width && innerSize.height)) {
    return null;
  }

  const points = calculateHousePoints(innerSize);

  return (
    <polygon
      style={node.styles.shape.styles || {}}
      points={points.map(d => `${d.x}, ${d.y}`).join(" ")}
    />
  );
};

The house component above is passed a reference to the node being rendered and the innerSize. innerSize is a reference to the size of the label (svg or foreign object) including its padding, this is the content that will appear inside the custom shape. A shape can then increases its size beyond this innerSize if required, this house for example increases the height to allow for its roof. The sizing hooks will take care of reporting the final rendered size to the graph layout, the component does not have to report the change.

The second requirement is an intersection method, creating a custom component you are required to handle the intersection maths. I'd recommend using a third party library like kld-intersections. Using a kld-intersection you could calculate polygon intersections using the pattern in the examples directory, create a generic intersection method like:

export const intersectPolygon = (node:NodeOptions, point: Point, polyPoints:Array<Point>): Point => {
  const polygon = ShapeInfo.polygon(polyPoints);
  const line = ShapeInfo.line([point.x - node.x!, point.y - node.y!, 0, 0]);
  const intersections2 = Intersection.intersect(polygon, line);
  
  if (intersections2.points.length > 0) {
    return {
      x: intersections2.points[0].x + node.x!,
      y: intersections2.points[0].y + node.y!
    };
  }
  return { x: node.y!, y: node.y! };
}

Then in your application code you would instantiate DagreReact like

<DagreReact
  nodes={nodes}
  edges={edges}
  customShapes={{
    house: {
      renderer: House,
      intersection: (node: NodeOptions, point: Point, valueCache: ValueCache) => {
        const labelSize = valueCache.value(`${node.id}-label-size`);
        const polyPoints = calculateHousePoints(labelSize);
        return intersectPolygon2(node, point, polyPoints);
      }
      }}
  }
/>

The intersection method above illustrates a compromise that currently exists in the components API, valueCache. In order to handle intersections correctly the edge positioning needs to know about the Node renderings internals, to work around this for now DagreReact will put the labels rendered size into a valueCache map that is available to all parts of the layout and render system. This allows the graph layout to recreate a rendered polygons points without rendering it and check intersections against a line.

Now all that is left to do is set the shape on your node object to "house" and you will have custom shapes rendering.

Custom node labels

The content rendered inside a node is referred to as a label, it can be a simple svg text element or a complex foreignobject html block but its still called a label. Custom node labels are passed in as a map using the customNodeLabels prop. See the foreign objects example code for a running demo. To create a custom node label create a component that takes the CustomNodeLabelProps, this only contains a reference to the node being rendered and return a react element.

Then in your application code you would instantiate DagreReact like

<DagreReact
  nodes={nodes}
  edges={edges}
  customNodeLabels={{
    "mycustomlabel": {
      renderer: YourCustomLabel,
      html: true // if true will be rendered inside a foreign object element otherwise expects pure svg component
    }
  }}
/>

In your node objects you then set the labelType to "mycustomlabel"

Custom markers

Edges have markers or arrowheads on the end of them to show direction. There are three arrowheads built in normal, undirected and vee same as dagre-d3. Custom markers can be used by passing them into ReactDagre using the customMarkerComponents prop.

export const CircleMarker: React.FC<MarkerProps> = ({ edgeMeta, markerId }) => {
  return ( 
    <marker id={markerId} markerWidth="14" markerHeight="14" refX="5" refY="5">
      <circle cx="5" cy="5" r="3" style={edgeMeta.styles.marker.styles} />
    </marker>
  );
}

<DagreReact
  nodes={nodes}
  edges={edges}
  customMarkerComponents={{
    "circle": CircleMarker //custom react component
  }}
/>

Markers should accept the edgeMeta containing the options for that edge and markerid that svg uses to link the marker to its edge.

See the custom path types example.

Custom edge labels

Custom edge labels can be a simple svg text element or a complex foreignobject html block. Custom edge labels are passed in as a map using the customEdgeLabels prop. See the foreign objects example code for a running demo. To create a custom edge label create a component that takes the CustomEdgeLabelProps, this only contains a reference to the edge being rendered and return a react element.

Then in your application code you would instantiate DagreReact like

<DagreReact
  nodes={nodes}
  edges={edges}
  customEdgeLabels={{
    "mycustomlabel": {
      renderer: YourCustomLabel,
      html: true // if true will be rendered inside a foreign object element otherwise expects pure svg component
    }
  }}
/>

In your edge objects you then set the labelType to "mycustomlabel"

Override render methods

If you need to handle button presses, pass custom data, or do something I can't think of inside your node or edge you can override the render methods and take over how nodes or edges are displayed. I'd recommend you continue to use the Node component unless you know what you are doing as Node handles all the size reporting for you. To not use it you would be required to report the sizes to the layout yourself, I have no examples available that do this.

  renderNode = (
    node: NodeOptions,
    reportSize: ReportSize,
    valueCache: ValueCache,
    layoutStage: number
  ) => {
    return (
      <Node
        key={node.id}
        node={node}
        reportSize={reportSize}
        valueCache={valueCache}
        layoutStage={layoutStage}
        html={true}
      >
        {{
          shape: (innerSize: Size) => (
            <Rect node={node} innerSize={innerSize} />
          ),
          label: () => (
            <CustomButtonLabel
              onButtonClicked={() => this.buttonClicked(node)}
              title={node.label}
              description={node.meta.description}
            />
          )
        }}
      </Node>
    );
  };

  .....

  <DagreReact
    nodes={nodes}
    edges={edges}
    renderNode={this.renderNode}
  />

Node takes a render map, expecting a render function for the shape and the label. Doing this you are free to render whatever you want, pass event handlers etc.

WARNING: if overriding this function and using a custom shape class you are still required to pass in the customShapes map to DagreReact with an intersection method for your custom shape.

EdgeLabel and Edge can be overridden in a similar manner.

See the Tooltips or mouseevents examples.

Design decisions

Not implementing pan and zoom, tooltips, positioning

The decision was made not to attempt to include these features directly in the library, I have attempted to provide enough hooks and override points to allow you to implement these features using libraries that are specifically build for these features. I have added some examples in the examples directory to help guide you on how to do this.

valueCache

Currently valueCache is passed around internally and to some hooks to allow different parts of the rendering steps to share data with each other. I do not like this, but I can't think of another way to do it. Currently is it only used for the label size value, which may in future be added directly to the NodeOptions interface instead, but I'm unsure at the moment.

dagre labelpos

Currently dagre labelpos is not respected, it can be passed but internally the edge label is being offset slightly by its size. This is because I have had issues with labelpos so do not use it. This may need to be addressed in the future.

Changing of data

Currently you cannot change the original prop data and see a change on the graph without changing the "stage" prop on DagreReact. The graph internally takes a copy of the data props on first render and that is the data that is manipulated and rendered internally. The data props are then ignored until the stage value changes. This was a decision made so that the component is not changing your state without telling you. Changing the data without triggering dagre to re-layout is not advised anyway as any style change will change the size of a node and should trigger a full stage layout. This can make using the default built in labels and shapes difficult if you want to change the background on mouse over for example, a possible work around is provided on MouseEvents example. A better solution would be to create your own shape component that takes in the style props that you can store separately from the node data. Again only do this if you know that the change does not affect the width and height on the node.

Changing of node size

In the case of triggering of change affecting the width or height of the nodes, the full dagre layout needs to rerender. For this purpose, use layoutStage in the same way you would use the stage prop. You must then pass the layoutStage variable to the Node component in the renderNode method (see Override render methods example)

dagre-reactjs's People

Contributors

andrico1234 avatar bobthekingofegypt avatar dependabot[bot] avatar josephmturner avatar layvier avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

dagre-reactjs's Issues

Dagre-reactjs with vitejs

Hi,
I moved my project from create-react-app to vitejs. Now I am facing a strange problem...
on localhost everything is working fine, but as soon as I deploy it I get the following error.
CleanShot 2022-03-08 at 14 49 57
Any ideas/help is appreciated!
CleanShot 2022-03-08 at 15 19 51@2x

Thanks

changing style attributes doesn't cause re-render

Hi there, thanks for this library!

I have a component which is trying to change the background color of a node (via the styles.shapes.styles attribute of the nodes prop to the DagreReact component) as a result of a user selecting a certain node and the component's state updating. It seems that updating attributes like this doesn't cause the graph to re-render โ€” perhaps only changing the set of nodes and edges itself, as in the timeline example.

Ideally changing attributes like background color would re-render, without re-layouting, although I realize it might be difficult to know which attributes changing requires a re-layout. Thanks again!

Foreign Object is not working in my project

Hello,
I tested your project and want to use customized node label, but I don't see it working at all

Step 1: In component

`

function DagreFlow(props){

console.log(props)
const traceRoutes = new TraceRoutes(props.data);
console.log(traceRoutes.traceRoutes[0])

const [state, setState] = useState(false);
setTimeout(()=>{
    setState({
        nodes: traceRoutes.traceRoutes[0]['hops']['graph'].nodes,
        edges: traceRoutes.traceRoutes[0]['hops']['graph'].edges,
        customNodeLabels: {
            foreign: {
                renderer: NodeLabelRender,
                html: true
            }
        },
        customEdgeLabels: {
            foreign: {
                renderer: EdgeLabelRender,
                html: true
            }
        }
    })
});


return (
    state &&
    <svg id="schedule" width={1000} height={1000}>
        <DagreReact
            nodes={state.nodes}
            edges={state.edges}
            customNodeLabels = {state.customNodeLabels}
            customEdgeLabels = {state.customEdgeLabels}
            defaultNodeConfig={DEFAULT_NODE_CONFIG}
            graphOptions={{
                marginx: 15,
                marginy: 15,
                rankdir: 'LR',
                ranksep: 55,
                nodesep: 15,
            }}
        />
    </svg>
);

}

export default DagreFlow;
`

and the customNodeLabel:

import React, {useRef} from 'react'; function NodeLabelRender(props){ return( <div>NodeX</div> ) } export default NodeLabelRender;
But when rendering it, it is still the same as without customer node label render, only default label is rendered, not the foreign object at all,

can you check the issue here?

Grouping nodes with or without compound graph

Hello Bob!

Would you spare a moment to offer some advice which may lead me to working on exposing graphlib's compound graph feature in dagre-reactjs?

In our project, we'd like to be able to group a set of nodes together to distinguish them from the rest of the nodes. These nodes should be rendered close to one another, and they should have a visible container surrounding them. IIUC, a compound graph can do this. However there might be a simpler solution, since in our case, the box which contains the set of child nodes won't ever have edges pointing to/from it, i.e., it doesn't need to be a node, just a grouping of nodes. The child nodes will have edges pointing to/from nodes outside the container.

If you think that a compound graph is the correct way to accomplish this goal, I would be happy to work on implementing it in dagre-reactjs.

Thank you!!

Joseph

Rerendering keeps old node label size

Hi,

first thanks a lot for this library, it's saving me a lot of time and pretty powerful !

So I'm rendering custom label react components, and in order to make it responsive I'm trying to update the nodes width based on the max number of nodes per rank. The issue is that after updating the nodes width, the layout doesn't update even though I'm properly changing the stage prop.

I'm assuming the issue comes from not resetting the ValueCache when the stage prop changes ?

If so I'm willing to make a PR, let me know what you think !

Rendering Order

Hello! Thank you for dagre-reactjs - it has made a big difference in our project .

In our use case, nodes should render last so that they display on top of edges and markers. The change is as simple as moving

{nodes.map((node, index) => {
return renderNodeFunc(
node,
this.reportNodeSize.bind(this, index),
this.valueCache,
this.props.layoutStage
);
})}
down to the end of the group:
<g>
{nodes.map((node, index) => {
return renderNodeFunc(
node,
this.reportNodeSize.bind(this, index),
this.valueCache,
this.props.layoutStage
);
})}
{edges.map((edgeMeta, index) => {
return renderEdgeFunc(index, edgeMeta);
})}
{edges.map((edgeMeta, index) => {
return renderEdgeLabelFunc(
index,
edgeMeta,
this.reportEdgeLabelSize.bind(this, index)
);
})}
</g>

Would you be open to a PR which adds an optional attribute to the graphOptions prop which controls the rendering order? Something like renderingOrder, an array of strings like ['nodes', 'edges', edgeLabels'] (current behavior), ['edges', 'edgeLabels', 'nodes'] (what I need), or some other iteration?

DagreReact component requires graphLayoutStarted

Hey! thanks so much for this library. I was actually shown this by @Layvier , and I'm looking to refactor beautiful skill tree to use something like this to handle the rendering to the DOM.

I'm getting started withthe alpha and noticed that in the readme no other props aside from the edges and nodes are required to please the DagreReact. I'm not sure if this is intentional but i'm required to pass through graphLayoutStarted. It seems all of the other props have default values except this one. If this isn't intentional, i can whip up a super quick PR.

Get size of graph

Is it possible to get the size of the graph instead of having to specify the SVG canvas width? The library works great, but as I do not know the graph beforehand I can not make it fit.

How to create an Edge point to Nodes which contains child-node

Thanks for providing such a good library.

I think Dagre-D3 has a fatal Issus , It cannot draw an edge to
nodes which contains child-node , like this :

HeidernleeExample

Dagre-D3 cannot draw an edge from [Child Node1-1] to [Parent Node2]

Did your library solved this Issue?
Guys from Dagre-D3 are waiting.....

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.