Skip to content
  1. Examples

Draw your graph

tsx
import React from 'react';
import ReactDOM from 'react-dom';
import ReactDOMServer from 'react-dom/server';
import { createRoot } from 'react-dom/client';
import App from './App';

const root = createRoot(document.getElementById('root'));
root.render(<App />);
html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta charset="utf-8" />
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://cdn.jsdelivr.net/npm/lucide-static@0.483.0/font/lucide.css"
      rel="stylesheet"
    />
    <link
      href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap"
      rel="stylesheet"
    />
    <script src="https://cdn.jsdelivr.net/npm/drag-drop-touch-polyfill-es5-compiled@1.0.0/DragDropTouch.min.js"></script>
    <title>Draw your graph</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="./index.tsx"></script>
  </body>
</html>
css
body {
  margin: 0;
  padding: 0;
  width: 100vw;
  height: 100vh;
  font-family: 'IBM Plex Sans', Arial, Helvetica, sans-serif;
}

#root {
  width: 100%;
  height: 100%;
}

.ogma-container {
  width: 100%;
  height: 100%;
}
json
{
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@linkurious/ogma-react": "latest",
    "react": "19.1.0",
    "react-dom": "19.1.0"
  },
  "devDependencies": {
    "@types/react": "^19.1.4",
    "@types/react-dom": "^19.1.4",
    "@vitejs/plugin-react": "^4.4.1",
    "vite": "latest"
  }
}
tsx
import React from 'react';
import { Ogma, useEvent } from '@linkurious/ogma-react';
import OgmaLib, { RawGraph } from '@linkurious/ogma';
import './style.css';
import { Controls } from './Controls';
import { createNode } from './utils';
import { Styles } from './Styles';

function App() {
  const ogmaRef = React.useRef<OgmaLib<unknown, unknown>>(null);
  const [isMove, setIsMove] = React.useState(true);
  const [graph, setGraph] = React.useState<RawGraph<unknown, unknown>>();

  const controlsRef = React.useRef<{
    getIconCode: (className: string) => string;
    getToolbarHeight: () => number;
  }>(null);

  React.useEffect(() => {
    // Initialize the graph when the component mounts
    const graph: RawGraph<unknown, unknown> = {
      nodes: [
        createNode('money', controlsRef.current!.getIconCode('banknote'), 0, 0),
        createNode(
          'commodity',
          controlsRef.current!.getIconCode('puzzle'),
          45,
          4
        )
      ],
      edges: [
        {
          id: 'money-assets',
          source: 'money1',
          target: 'commodity2',
          attributes: { shape: { head: 'arrow' } }
        }
      ]
    };
    setGraph(graph);
  }, []);

  const onDragEnd = useEvent('dragEnd', ev => {
    ev.domEvent.preventDefault();
  });

  const onDrop = useEvent('drop', ev => {
    // Convert the drop point to graph coords
    const pos = ogmaRef.current!.view.screenToGraphCoordinates({
      x: ev.domEvent.clientX,
      y: ev.domEvent.clientY - controlsRef.current!.getToolbarHeight()
    });
    // now get the icons type and its URL
    const id = ev.domEvent.dataTransfer!.getData(
      'type'
    ) as keyof typeof createNode;
    ev.domEvent.dataTransfer!.dropEffect = 'copy';
    ev.domEvent.preventDefault();
    // create a node on the graph to the exact x and y of the drop
    ogmaRef.current!.addNode(
      createNode(id, controlsRef.current!.getIconCode(id), pos.x, pos.y)
    );
    ogmaRef.current!.view.locateGraph({ duration: 500 });
  });

  // Enable the connect tool when dragging a node and when the user is not in move mode
  const onDragStart = useEvent(
    'dragStart',
    () => {
      if (isMove) return;
      ogmaRef.current!.tools.connectNodes.enable({
        strokeColor: 'red',
        createNodes: false,
        // avoid self edges
        condition: (source, target) => source.getId() !== target!.getId(),
        createEdge: edge => {
          edge.attributes = { shape: { head: 'arrow' } };
          return edge;
        }
      });
    },
    [isMove]
  );

  const deleteItems = () => {
    const selectedNodes = ogmaRef.current!.getSelectedNodes();
    const selectedEdges = ogmaRef.current!.getSelectedEdges();
    if (selectedNodes) {
      ogmaRef.current!.removeNodes(selectedNodes);
    }
    if (selectedEdges) {
      ogmaRef.current!.removeEdges(selectedEdges);
    }
  };

  // Delete selected nodes and edges on backspace or delete key
  const onKeyDown = useEvent('keydown', ev => {
    if (ev.key === 'del' || ev.key === 'backspace') {
      deleteItems();
    }
  });

  return (
    <>
      {graph && (
        <Ogma
          ref={ogmaRef}
          onReady={ref => {
            ogmaRef.current = ref;
          }}
          graph={graph}
          onDragEnd={onDragEnd}
          onDrop={onDrop}
          onDragStart={onDragStart}
          onKeydown={onKeyDown}
        >
          <Styles />
        </Ogma>
      )}
      <Controls isMove={isMove} setIsMove={setIsMove} ref={controlsRef} />
    </>
  );
}

export default App;
css
.toolbar {
    display: flex;
    align-items: center;
    padding: 10px 20px;
    position: fixed;
    top: 0;
    box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65);
    border-radius: 4px;
    background: #ffffff;
    color: #222222;
    height: 50px;
    width: calc(100% - 40px);
  }
tsx
import React from 'react';
import { Icons } from './Icons';
import { ModeToggle } from './ModeToggle';
import './Controls.css';

export const Controls = (props: {
  isMove: boolean;
  setIsMove: (isGeo: boolean) => void;
  ref: React.Ref<{
    getIconCode: (className: string) => string;
  }>;
}) => {
  const { isMove, setIsMove } = props;
  const iconsRef = React.createRef<{
    getIconCode: (className: string) => string;
  }>();

  // Expose methods to the parent component
  React.useImperativeHandle(props.ref, () => {
    return {
      getIconCode: (className: string) => {
        if (iconsRef.current) {
          return iconsRef.current.getIconCode(className);
        }
        return '';
      },
      getToolbarHeight: () => {
        const toolbar = document.querySelector('.toolbar');
        if (toolbar) {
          return toolbar.clientHeight;
        }
        return 0;
      }
    };
  });

  return (
    <div className="toolbar">
      <Icons ref={iconsRef} />
      <ModeToggle isMove={isMove} setIsMove={setIsMove} />
    </div>
  );
};
css
.toolbar-icons i {
  font-size: 2.5em;
}

img {
  width: 50px;
  height: 50px;
}

.toolbar-icons {
  flex-basis: 70%;
}

.inline-images {
  display: flex;
  justify-content: space-around;
}

#banknote:hover {
  color: #56b956;
}
#puzzle:hover {
  color: #bd8362;
}
#files:hover {
  color: #cacaca;
}
#cctv:hover {
  color: #f7102f;
}
#building:hover {
  color: #c7c7c7;
}

.inline-images > * {
  cursor: pointer;
}
tsx
import React from 'react';
import './Icons.css';

export const Icons = (props: {
  ref: React.Ref<{
    getIconCode: (className: string) => string;
  }>;
}) => {
  // Expose methods to the parent component
  React.useImperativeHandle(props.ref, () => {
    return {
      getIconCode(className: string) {
        const iconContainer = document.querySelector(
          '.icon-' + className
        ) as HTMLElement;
        const code = getComputedStyle(iconContainer, ':before').content;
        return code[1];
      }
    };
  });

  function handleDragStart(ev: React.DragEvent<HTMLDivElement>) {
    ev.dataTransfer.setData('type', ev.currentTarget.id);
  }

  return (
    <div className="toolbar-icons">
      <div className="inline-images">
        <i
          className="icon-puzzle"
          draggable="true"
          id="puzzle"
          onDragStart={handleDragStart}
        />
        <i
          className="icon-files"
          draggable="true"
          id="files"
          onDragStart={handleDragStart}
        />
        <i
          className="icon-cctv"
          draggable="true"
          id="cctv"
          onDragStart={handleDragStart}
        />
        <i
          className="icon-banknote"
          draggable="true"
          id="banknote"
          onDragStart={handleDragStart}
        />
        <i
          className="icon-building"
          draggable="true"
          id="building"
          onDragStart={handleDragStart}
        />
      </div>
    </div>
  );
};
css
/* From Uiverse.io by arghyaBiswasDev */
/* The switch - the box around the slider */
.switch .switch--horizontal {
  width: 100%;
}

.switch {
  display: flex;
  flex-direction: row;
  justify-content: space-around;
  align-items: center;
  gap: 10px;
  padding: 0 10px;
  font-size: 17px;
  position: relative;
  width: 100%;
  height: 2em;
}

/* Hide default HTML checkbox */
.switch input {
  opacity: 0;
  width: 0;
  height: 0;
  display: none;
}

/* The slider */
.slider {
  display: flex;
  position: relative;
  width: 60px;
  height: 30px;
  background-color: #ccc;
  border-radius: 20px;
  cursor: pointer;
  transition: background-color 0.2s;
  flex-shrink: 0;
}

.slider:before {
  content: '';
  position: absolute;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background-color: white;
  top: 1px;
  left: 1px;
  transition: transform 0.2s;
}

input:focus + .slider {
  box-shadow: 0 0 1px #007bff;
}

input:checked + .slider:before {
  transform: translateX(30px);
}

.label-text {
  color: #808080;
  text-align: center;
}

.selected {
  color: #222222;
}
tsx
import React from 'react';
import './ModeToggle.css';

export const ModeToggle = (props: {
  isMove: boolean;
  setIsMove: (isGeo: boolean) => void;
}) => {
  const { isMove, setIsMove } = props;

  return (
    <div className="section mode">
      <div className="switch switch--horizontal">
        <label className="switch">
          <span className={'label-text' + (isMove ? ' selected' : '')}>
            Move nodes
          </span>
          <input
            name="_"
            checked={!isMove}
            type="checkbox"
            onChange={() => setIsMove(!isMove)}
          />
          <span className="slider"></span>
          <span className={'label-text' + (!isMove ? ' selected' : '')}>
            Connect links
          </span>
        </label>
      </div>
    </div>
  );
};
tsx
import { EdgeStyle, NodeStyle } from '@linkurious/ogma-react';
import React from 'react';

export const Styles = () => {
  return (
    <>
      {/* Selected style for nodes */}
      <NodeStyle
        selector={node => node.isSelected()}
        attributes={{
          color: null,
          outline: false,
          halo: null,
          outerStroke: {
            color: 'red'
          }
        }}
      />

      {/* Default style for nodes */}
      <NodeStyle
        attributes={{
          radius: 5,
          color: 'white',
          text: {
            content: node => node.getData('type'),
            font: 'IBM Plex Sans'
          },
          innerStroke: {
            color: '#ddd',
            width: 1
          }
        }}
      />

      {/* Default style for edges */}
      <EdgeStyle
        attributes={{
          color: '#ddd',
          width: 0.4
        }}
      />
    </>
  );
};
ts
const nodeColors = {
  money: '#56B956',
  commodity: '#BD8362',
  documents: '#CACACA',
  fraudster: '#F7102F',
  office: '#C7C7C7'
};

let counter = 0;
export const createNode = (
  id: keyof typeof nodeColors,
  icon: string,
  x: number,
  y: number
) => {
  counter++;
  return {
    id: id + counter,
    attributes: {
      icon: {
        font: 'lucide',
        color: nodeColors[id],
        content: icon,
        scale: 0.5
      },
      x: x,
      y: y
    },
    data: { type: id }
  };
};
ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

// https://vite.dev/config/
export default defineConfig({
  plugins: [react()]
});