Visualizing Customer Networks with React Flow and ELK

Visualizing Customer Networks with React Flow and ELK

Kohei Abe

Kohei Abe

May 11, 2023

Sardine provides a platform offering risk, compliance, and payment protection solutions, helping businesses increase customer trust and loyalty. A crucial aspect of our platform involves understanding complex relationships within customer data.

In this blog post, I'll demonstrate the process of building a powerful network visualization tool using React Flow, a flexible and easy-to-use library for rendering interactive graphs in React applications. This customer network graph will help visualize connections between various entities, such as clients, customers, devices, and contact information, providing invaluable insights for data-driven decision-making processes. Users can cluster accounts, identify flow of funds and flag activities for follow up in the Sardine case management system.

Customer Network Graph

Creating Custom Nodes and Fetching Graph Data

For our Customer Network Graph, we need several custom node types to represent different entities. To create a custom node, let me first define functional components, then style it using CSS. Here's an example of simple custom nodes representing an email address, phone number, and device:

import React from "react";

export const EmailNode = ({ data }) => {
  return (
    <div className="email-node">
      <div className="email-node-header">Email</div>
      <div className="email-node-value">{data.value}</div>
      // ...
    </div>
  );
};

// Custom node for phone numbers
export const PhoneNumberNode = ({ data }) => {
  return (
    <div className="phone-number-node">
      <div className="phone-number-node-header">Phone Number</div>
      <div className="phone-number-node-value">{data.value}</div>
      // ...
    </div>
  );
};

// Custom node for devices
export const DeviceNode = ({ data }) => {
  return (
    <div className="device-node">
      <div className="device-node-header">Device</div>
      <div className="device-node-value">{data.value}</div>
      // ...
    </div>
  );
};

and we define node types constant and pass it to ReactFlow later so ReactFlow recognizes custom node different designs.

const nodeTypes = {
  emailNode: EmailNode,
  phoneNumberNode: PhoneNumberNode,
  deviceNode: DeviceNode,
  // ...
}

<ReactFlow
  // ...
  nodeTypes={nodeTypes}
  // ...
/>

Next, we'll define a custom React hook, useCustomerNetworkGraphData, which will fetch the graph data from the API, process it, and return it in a format suitable for React Flow.

export const useCustomerNetworkGraphData = (props: Props): ReturnValue => {
  // ...
};

We'll fetch the graph data using a GraphQL query and store it in our local state using useState.

const [nodes, setNodes] = useState<ReactFlowNode[]>([]);
const [graphLayoutUpdating, setGraphLayoutUpdating] = useState(false);

// Fetch graph data using a custom query hook
const { data: graphData, status: graphDataStatus } =
  useCustomerNetworkGraphQuery(useCustomerNetworkGraphQueryParams);

const isLoadingGraphData = graphDataStatus === QUERY_STATUS.LOADING;

Processing Graph Data and Calculating Node Positions

To process the raw graph data and transform it into a format that React Flow can understand, we'll perform several operations, such as:

  • Converting the raw graph nodes and edges into ReactFlowNode and ReactFlowEdge types.
  • Generating ElkNode and ElkEdge to calculate node positions using ELK.
  • Filtering and displaying the nodes and edges based on user-defined criteria.
// Process the raw graph data into React Flow nodes and edges
const reactFlowNodes = useMemo(() => {
  // ...
}, [graphData, displayedTypes]);

const reactFlowEdges = useMemo<ReactFlowEdge[]>(() => {
  // ...
}, [graphEdgeData]);

// Generate ELK graph and use it to calculate node positions
const elkGraphData = useMemo(() => {
  // ...
}, [reactFlowNodes, reactFlowEdges]);

const handleElkLayoutCallback = (layout: ElkNode) => {
  const nodePositions: { [key: string]: { x: number; y: number } } = {};

  layout.children?.forEach((node: ElkNode) => {
    nodePositions[node.id] = { x: node.x, y: node.y };
  });

  const newNodes = reactFlowNodes.map((node: ReactFlowNode) => {
    const nodePosition = nodePositions[node.id];
    return {
      ...node,
      position: {
        x: nodePosition?.x || 200,
        y: nodePosition?.y || 200,
      },
    };
  });
  setNodes(newNodes);
};

const ELK_LAYOUT_OPTIONS = {
  layoutOptions: {
    // Set your layout algorithm depending on your requirement!
    algorithm: "force", 
    "elk.spacing.nodeNode": "2",
     // ...
  },
  // ...
} as const;

useEffect(() => {
  if (!isLoadingGraphData) {
    setGraphLayoutUpdating(true);
    elk
      .layout(elkGraphData, ELK_LAYOUT_OPTIONS)
      .then(handleElkLayoutCallback)
      .catch(handleElkError)
      .finally(() => setGraphLayoutUpdating(false));
  }
}, [isLoadingGraphData, elkEdges, elkNodes]);

You can explore the ELK algorithms here.

Rendering the Graph and Handling User Interactions

Rendering the Graph

Now that we have the processed graph data and the calculated node positions, we can render the graph using React Flow. We'll also handle user interactions like node and edge selection, zooming, panning, and context menus.

return (
  <div className="customer-network-graph">
    <ReactFlow
      nodes={reactFlowNodes}
      edges={reactFlowEdges}
      nodeTypes={customNodeTypes}
      onSelectionChange={handleSelectionChange}
      onNodeClick={handleNodeClick}
      // ...
    >
      <Background />
    </ReactFlow>
  </div>
);

function handleSelectionChange(params: OnSelectionChangeParams) {
  // Handle selection change logic
}

function handleNodeClick(event: React.MouseEvent) {
  // Handle node click logic
}

With this implementation, you'll have a powerful and interactive network visualization tool that can display a Network Graph. You can further customize the tool to meet your specific needs and requirements. Extending the Graph's Functionality To improve the user experience and add more functionality to your network visualization tool, you can extend it by implementing features such as filtering, context menus, and tooltips.

Filtering

Filtering the graph

To provide users with the ability to filter nodes and edges based on their types or other properties, you can create a filtering panel that allows users to toggle the visibility of different node and edge types. You can then update the displayed nodes and edges in the graph based on the user's selections.

const [displayedNodeTypes, setDisplayedNodeTypes] = useState<string[]>(
  Object.keys(customNodeTypes)
);

const displayedNodes = useMemo(() => {
  // Filter nodes based on displayedNodeTypes
}, [reactFlowNodes, displayedNodeTypes]);

const displayedEdges = useMemo(() => {
  // Filter edges based on displayedNodeTypes
}, [reactFlowEdges, displayedNodeTypes]);

return (
  <div>
    <FilteringPanel
      displayedNodeTypes={displayedNodeTypes}
      setDisplayedNodeTypes={setDisplayedNodeTypes}
    />
    <ReactFlow nodes={displayedNodes} edges={displayedEdges} />
  </div>
);

Context Menus

Context Menus

To provide users with context-specific actions, you can implement context menus that appear when users right-click on nodes, edges, or the background of the graph. These context menus can offer actions such as adding or removing nodes, editing edge properties, or navigating to related resources.

const handleSelectionContextMenu = (event: React.MouseEvent) => {
    event.preventDefault();
    setIsSelectionContextMenuDisplayed(true);
    setSelectionContextPosition({ x: event.clientX, y: event.clientY });
  };

return (
  <ReactFlow
    // ...
    onSelectionContextMenu={handleSelectionContextMenu}
  >
  {isSelectionContextMenuDisplayed && (
    <SelectionContextMenu
      position={selectionContextPosition}
      onSelectionContextItemClick={handleSelectionContextItemClick}
     />
   )}
  </ReactFlow>
);

Tooltips

Tooltips

To provide users with additional information about nodes and edges, you can implement tooltips that appear when users hover over graph elements. Tooltips can display properties such as node and edge labels, metadata, and other relevant information.

return (
  <ReactFlow
    // ...
    onNodeMouseEnter={handleNodeMouseEnter}
    onNodeMouseLeave={handleNodeMouseLeave}
  >
    {isTooltipVisible && (
      <Tooltip position={tooltipPosition} content={tooltipContent} />
    )}
  </ReactFlow>
);

Conclusion

In conclusion, by leveraging the power of React Flow and its various customization options, you can create a highly interactive and visually appealing network visualization tool that meets your specific requirements. This will enable users to explore and analyze data effectively, leading to better insights and decision-making.

Remember to experiment with different layout algorithms and customization options to find the best fit for your use case. The possibilities are endless, and with the right combination of features and design, you can create a truly remarkable network visualization experience.

Read next article

A guide to SOC2 for Startups

Subscribe to our newsletter