Skip to content
  1. Tutorials

Integrating Ogma with React

The following tutorial will show how to create a Ogma component using React classes.

You may also want to check out the  @linkurious/ogma-react wrapper for seamless integration between Ogma and React.

You can install it via npm or yarn.

sh
npm i -S @linkurious/ogma-react

The component is called OgmaComponent and it exposes all Ogma events in a React fashion, providing a simple way to customise it with different actions.

The final goal of this tutorial is to build a basic React web application with Ogma that:

  • Add a node via a button
  • Shows a tooltip - the React way - with information on the node
  • An advanced expand-like feature with 2 different layouts

As first step the OgmaComponent component should create a Ogma instance when mounted:

jsx
import React, {PureComponent} from 'react';
import Ogma from '@linkurious/ogma';

// PureComponent or Component as both good, the former performs a bit better in this case
class OgmaComponent extends PureComponent {
    ...

    componentDidMount() {
        // create the Ogma instance
        this.ogmaRef = new Ogma({
            container: 'ogma-react',
            options: {
                backgroundColor: 'rgb(240, 240, 240)'
            },
            graph: this.props.data
        });

        this.ogmaRef.layouts.force({ locate: true });
    }

    render() {
        return <div id='ogma-react' style={{width: 800, height: 600}}> </div>
    }
}

export default OgmaComponent;

At this point this.ogmaRef holds the Ogma instance and it is possible to use it to bind events to it and more within the component.

Add the data

As shown in the code snippet above the component accepts a data prop from its parent component, that contains the graph data to show.

This is the parent component App.js:

jsx
import React, { Component } from 'react';

import InfoPanel from './InfoPanel';
import ControlPanel from './ControlPanel';
import Tooltip from './Tooltip';
import OgmaContainer from './OgmaComponent';
import initialGraph from './data';

import './App.css';

// external helpers
function createNode(id) {
  return {
    id,
    attributes: {
      color: id % 2 ? 'purple' : 'orange'
    }
  };
}

function createEdge(node, nodesInGraph) {
  // pick a random node in the graph
  const randomIndex = Math.floor(Math.random() * nodesInGraph.length);
  const otherNode = nodesInGraph[randomIndex];
  return {
    id: `${otherNode.id}-${node.id}`,
    source: otherNode.id,
    target: node.id
  };
}

export default class App extends Component {
  state = {
    counter: initialGraph.nodes.length,
    graph: initialGraph
  };

  addNode = () => {
    const newNode = createNode(this.state.counter);
    // note: we're creating a new array here, adding an extra node at the end
    const nodes = [...this.state.graph.nodes, newNode];
    const edges = [
      ...this.state.graph.edges,
      createEdge(newNode, this.state.graph.nodes)
    ];
    // use the counter as id
    this.setState({
      graph: { nodes, edges },
      counter: this.state.counter + 1
    });
  };

  render() {
    return (
      <div className="App">
        <OgmaContainer data={this.state.graph} />
        <div className="text-center">
          <InfoPanel nodes={this.state.graph.nodes.length} />
          <ControlPanel onAddClick={this.addNode} />
        </div>
      </div>
    );
  }
}

The ControlPanel component contains a button that calls the addNode handler in the App.js component. The addNode function creates a new node and a new edge, append them to the current item lists and creates a new graph object to be stored into the local state. This triggers a new render where this.state.graph is passed as data to the OgmaContainer component.

In the first OgmaComponent implementation, it is only loaded the graph once, on creation - there's no update code yet.

jsx
import React, { PureComponent } from 'react';
import Ogma from '@linkurious/ogma';

// PureComponent or Component as both good, the former performs a bit better in this case
class OgmaComponent extends PureComponent {
  // after a component update, add the new data, then layout
  componentDidUpdate(prevProps) {
    // add a node
    if (prevProps.data.nodes.length < this.props.data.nodes.length) {
      // update the data in ogma
      return this.ogmaRef
        .addGraph(this.props.data)
        .then(() => this.ogmaRef.layouts.force({ locate: true }));
    }
  }

  componentDidMount() {
    // create the Ogma instance
    this.ogmaRef = new Ogma({
      container: 'ogma-react',
      options: {
        backgroundColor: 'rgb(240, 240, 240)'
      },
      graph: this.props.data
    });

    this.ogmaRef.layouts.force({ locate: true });
  }

  render() {
    return (
      <div id="ogma-react" style={{ width: 800, height: 600 }}>
        {' '}
      </div>
    );
  }
}

export default OgmaComponent;

Now the chart can be updated with new nodes!

Ogma component updated with new nodes

Add the tooltips

Ogma comes with the whole events namespace already exposing many different event hooks: we can leverage such namespace in React as follow:

js
import React, {PureComponent} from 'react';
import Ogma from '@linkurious/ogma';

// PureComponent or Component as both good, the former performs a bit better in this case
class OgmaComponent extends PureComponent {
    ...

    componentDidMount() {
        // create the Ogma instance
        this.ogmaRef = new Ogma({
            container: 'ogma-react',
            options: {
                backgroundColor: 'rgb(240, 240, 240)'
            },
            graph: this.props.data
        });

        this.ogmaRef.layouts.force({ locate: true });

// See the Ogma.events namespace: https://doc.linkurio.us/ogma/latest/api/ogma/events
        this.bindEvents()
    }

    // when passed a on* event as React prop it checks it and bind it to the ogma instance
    bindEvents() {
        Object.keys(this.props).forEach(prop => {
            if (/^on/.test(prop) && prop in this.ogmaRef.events) {
                this.ogmaRef.events[prop](this.props[prop]);
            }
        });
    }

    render() {
        return <div id='ogma-react' style={{width: 800, height: 600}}> </div>
    }
}

export default OgmaComponent;

This makes possible to attach any event listener function (for instance onNodeClick or onHover) when using the component in React. In the current tutorial the onHover and onUnhover events are used to show/hide a tooltip on nodes.

First it requires a simple Tooltip to be created:

jsx
import React from 'react';

export default function Tooltip({ position, data }) {
  return (
    <div className="tooltip" style={{ top: position.y, left: position.x }}>
      <div className="tooltip-content">
        <table>
          <tbody>
            <tr>
              <td>
                <strong>ID</strong>
              </td>
              <td>{data.id}</td>
            </tr>
            <tr>
              <td>
                <strong>Type</strong>
              </td>
              <td>
                <div
                  className="circle"
                  style={{ background: data.attributes.color }}
                ></div>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  );
}

The Tooltip components receives two specific information from its parent (App.js): the data to show and the position where to be placed.

Both information will come from the Ogma hover/unhover events: the App.js component listens to the Ogma event and update its local state, then propagated to the Tooltip component.

jsx
import React, { Component } from 'react';

import InfoPanel from './InfoPanel';
import ControlPanel from './ControlPanel';
import Tooltip from './Tooltip';
import OgmaContainer from './OgmaComponent';
import initialGraph from './data';

import './App.css';

...

export default class App extends Component {

  state = {
    counter: initialGraph.nodes.length,
    graph: initialGraph,
    // tooltip state
    tooltipPosition: null,
    tooltipNodeId: null
  }

  ...
  // on hunover clear the tooltip state
  onNodeUnHover = () => {
    // clear the tooltip state
    return this.setState({
      tooltipPosition: null,
      tooltipNodeId: null
    });
  }

  // on hover save the position and the id of the hovered item/node
  onNodeHover = ({ x, y, target }) => {
    if (target.isNode) {
      // save the tooltip state (offset by 20px the vertical position)
      this.setState({
        tooltipPosition: { x, y: y + 20 },
        tooltipNodeId: target.getId()
      });
    }
  }

  handleTooltip(){
      // check if there's a position stored for the tooltip
    const nodeTooltip = this.state.tooltipPosition ? this.state.graph.nodes.find(node => node.id === this.state.tooltipNodeId) : null;

    if(!nodeTooltip){
        // nothing in the state, drop it
        return;
    }
    // show the tooltip
    return <Tooltip
            position={this.state.tooltipPosition}
            data={nodeTooltip} />
  }


  render() {
    return (
      <div className="App">
        <OgmaContainer
            data={this.state.graph}
            onHover={this.onNodeHover}
            onUnhover={this.onNodeUnHover}  />
        {this.handleTooltip()}
        <div className="text-center">
          <InfoPanel
            nodes={this.state.graph.nodes.length} />
          <ControlPanel
            onAddClick={this.addNode}  />
        </div>
      </div>
    );
  }
}

Because of the generic binding in the OgmaComponent, the onHover and onUnhover handlers are already available in JSX.

Ogma component with tooltip

Layout picker - Advanced

This is an advanced step to complete the tutorial: when a node is added to the graph, the user can decide which layout to apply to the expand-like feature.

The ControlPanel component needs to be updated to have two radio inputs, and a handler when the click happens:

js
import React from 'react';

export default function ControlPanel({ onAddClick, onLayoutChange, value }) {
  return (
    <div className="form">
      <h3>Action</h3>
      <button className="btn" onClick={onAddClick}>
        Add node
      </button>
      <h3>Layout:</h3>
      <label className="radio">
        <input
          type="radio"
          name="layout"
          value="force"
          onChange={onLayoutChange}
          checked={value === 'force'}
        />
        Force Layout
      </label>
      <label className="radio">
        <input
          type="radio"
          name="layout"
          value="hierarchical"
          onChange={onLayoutChange}
          checked={value === 'hierarchical'}
        />
        Hierarchy Layout
      </label>
    </div>
  );
}

Then the main App.js component needs to have a new state layout and pass it to both ControlPanel and OgmaComponent:

jsx
import React, { Component } from 'react';

import InfoPanel from './InfoPanel';
import ControlPanel from './ControlPanel';
import Tooltip from './Tooltip';
import OgmaContainer from './OgmaComponent';
import initialGraph from './data';
...

export default class App extends Component {

  state = {
    counter: initialGraph.nodes.length,
    graph: initialGraph,
    // tooltip state
    tooltipPosition: null,
    tooltipNodeId: null,
    // which layout to use?
    layout: 'force'
  }

  onLayoutChange = ({ target }) => this.setState({
    layout: target.value
  })


  ...


  render() {
    const nodeTooltip = this.state.tooltipPosition ? this.state.graph.nodes.find(node => node.id === this.state.tooltipNodeId) : null;
    return (
      <div className="App">
        <OgmaContainer
          data={this.state.graph}
          onHover={this.onNodeHover}
          onUnhover={this.onNodeUnHover}
          layout={this.state.layout} />
        {nodeTooltip && <Tooltip position={this.state.tooltipPosition} data={nodeTooltip} />}
        <div className="text-center">
          <InfoPanel nodes={this.state.graph.nodes.length} />
          <ControlPanel
            onAddClick={this.addNode}
            onLayoutChange={this.onLayoutChange}
            value={this.state.layout} />
        </div>
      </div>
    );
  }
}

And last the OgmaComponent needs to use the new prop layout that is passed through:

jsx
import React, { PureComponent } from 'react';
import Ogma from '@linkurious/ogma';


export default class OgmaComponent extends PureComponent {

    componentDidUpdate(prevProps) {
        // add a node
        if (prevProps.data.nodes.length < this.props.data.nodes.length) {
            // update the data in ogma
            return this.ogmaRef.addGraph(this.props.data)
                .then(() =>
                    // use the passed layout after adding the node
                    this.runLayout()
                );
        }

        // if the layout changes, then run it
        if(prevProps.layout !== this.props.layout){
            return this.runLayout()
        }
    }

    runLayout(){
      const options = {locate: true};
      if(this.props.layout === 'hierarchical'){
        // set node with ID = 1 as root of the hierarchy. Shown as an example to customise options per-layout
        // Alternatively the layout can work out the root automatically, or we could use user's selection.
        options.roots = [1];
      }
      return this.ogmaRef.layouts[this.props.layout](options);
    }
    ...

    render() {
        return <div id='ogma-react' style={{width: 800, height: 600}}> </div>
    }
}

The layout function has been refactored to a generic runLayout function on the component that handles the logic in a centralised way.

This is the final result:

Ogma with expand-like layout radio inputs