Skip to content
  1. Examples

Draw your graph

This example shows how to edit your graph interractively.
Drag images on the graph canvas to add more nodes, when the toggle is in "Connect links" mode it's possible to wire the network. To remove an element from the network use the keyboard keys "del" or "backspace". Icons byIcons8

js
import Ogma from '@linkurious/ogma';

const placeholder = document.createElement('span');
document.body.appendChild(placeholder);
placeholder.style.visibility = 'hidden';

// helper routine to get the icon HEX code
export function getIconCode(className) {
  placeholder.className = `icon-${className}`;
  const code = getComputedStyle(placeholder, ':before').content;
  return code[1];
}

// stop mobile browsers to scroll on touchstart
// Note: this demo uses the DragDropTouch polyfill for mobile browsers.
// Removed it if you do not require it.
document.addEventListener(
  'touchmove',
  e => {
    e.preventDefault();
  },
  { passive: false }
);
const nodeIcons = {
  money: getIconCode('banknote'),
  commodity: getIconCode('puzzle'),
  documents: getIconCode('files'),
  fraudster: getIconCode('cctv'),
  office: getIconCode('building')
};
const nodeColors = {
  money: '#56B956',
  commodity: '#BD8362',
  documents: '#CACACA',
  fraudster: '#F7102F',
  office: '#C7C7C7'
};
const createNode = (id, label, x, y) => {
  counter++;
  return {
    id: id + counter,
    attributes: {
      icon: {
        font: 'lucide',
        color: nodeColors[id],
        content: nodeIcons[id],
        scale: 0.5
      },
      x: x,
      y: y
    },
    data: { type: id }
  };
};
// use this for the ID generation
let counter = 0;
// compute the toolbar size
const toolbarHeight = document.querySelector('.toolbar').clientHeight;

// start with a couple of nodes
const graph = {
  nodes: [
    createNode('money', '$ 3.000', 0, 0),
    createNode('commodity', 'Assets', 45, 4)
  ],
  edges: [
    {
      id: 'money-assets',
      source: 'money1',
      target: 'commodity2',
      attributes: { shape: { head: 'arrow' } }
    }
  ]
};

// start with an empty graph
const ogma = new Ogma({
  graph: graph,
  container: 'graph-container'
});

ogma.styles.setSelectedNodeAttributes({
  color: false,
  outline: false,
  outerStroke: {
    color: 'red'
  }
});

ogma.styles.setHoveredEdgeAttributes({
  color: '#ddd',
  width: 0.8
});

ogma.styles.setSelectedEdgeAttributes({
  color: '#ddd',
  width: 0.8
});

ogma.styles.setHoveredNodeAttributes({
  color: false,
  outline: false,
  outerStroke: {
    color: '#ddd',
    width: 2
  }
});

ogma.styles.addNodeRule({
  radius: 5,
  color: 'white',
  text: {
    content: node => node.getData('type'),
    font: 'Roboto'
  },
  innerStroke: {
    color: '#ddd',
    width: 1
  }
});
ogma.styles.addEdgeRule({
  color: '#ddd',
  width: 0.4
});

// now control the drag and drop behaviour
const icons = document.querySelectorAll('[draggable="true"]');
for (let i = 0; i < icons.length; i++) {
  icons[i].addEventListener('dragstart', ev => {
    ev.dataTransfer.setData('type', ev.target.id);
  });
}

const ogmaContainer = document.querySelector('#graph-container');

// prevent the browser to do anything else than drag the image
ogmaContainer.addEventListener('dragover', ev => {
  ev.preventDefault();
});

ogmaContainer.addEventListener('drop', ev => {
  // Convert the drop point to graph coords
  const pos = ogma.view.screenToGraphCoordinates({
    x: ev.clientX,
    y: ev.clientY - toolbarHeight
  });
  // now get the icons type and its URL
  const id = ev.dataTransfer.getData('type');
  ev.dataTransfer.dropEffect = 'copy';
  ev.preventDefault();
  // create a node on the graph to the exact x and y of the drop
  ogma.addNode(createNode(id, id, pos.x, pos.y));
  ogma.view.locateGraph({ duration: 500 });
});

ogma.events.on('dragStart', () => {
  if (getMode('drag-action') === 'links') {
    ogma.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;
      }
    });
  }
});

const deleteItems = () => {
  const selectedNodes = ogma.getSelectedNodes();
  const selectedEdges = ogma.getSelectedEdges();
  if (selectedNodes || selectedEdges) {
    if (selectedNodes) {
      ogma.removeNodes(selectedNodes);
    }
    if (selectedEdges) {
      ogma.removeEdges(selectedEdges);
    }
  }
};

// Enable the delete action on keypress
ogma.events.onKeyPress('del', deleteItems);
ogma.events.onKeyPress('backspace', deleteItems);

const getMode = id => {
  const form = document.querySelector('form');
  const select = form[id];
  const currentMode = Array.prototype.filter.call(select, input => {
    return input.checked;
  })[0].value; // IE inconsistency
  return currentMode;
};
html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <link
      href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/solid.min.css"
      rel="stylesheet"
    />
    <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"
    />
    <link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"
    />
    <script src="https://cdn.jsdelivr.net/npm/drag-drop-touch-polyfill-es5-compiled@1.0.0/DragDropTouch.min.js"></script>
    <link type="text/css" rel="stylesheet" href="style.css" />
  </head>

  <body>
    <div class="toolbar">
      <div class="toolbar-icons">
        <div class="inline-images">
          <i class="icon-puzzle" draggable="true" id="commodity"></i>
          <i class="icon-files" draggable="true" id="documents"></i>
          <i class="icon-cctv" draggable="true" id="fraudster"></i>

          <i class="icon-banknote" draggable="true" id="money"></i>
          <i class="icon-building" draggable="true" id="office"></i>
        </div>
      </div>
      <div style="margin-left: auto">
        <form>
          <div class="switch switch--horizontal">
            <input type="radio" name="drag-action" value="nodes" />
            <label for="nodes">Move nodes</label>
            <input
              type="radio"
              name="drag-action"
              value="links"
              checked="checked"
            />
            <label for="links">Connect links</label>
            <span class="toggle-outside">
              <span class="toggle-inside"></span>
            </span>
          </div>
        </form>
      </div>
    </div>
    <div id="graph-container"></div>
    <script src="index.js"></script>
  </body>
</html>
css
body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  font-family: 'IBM Plex Sans', Arial, Helvetica, sans-serif;
}

#graph-container {
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  margin: 125px 0 0 0;
  overflow: hidden;
  position: absolute;
}

.toolbar {
  display: flex;
  top: 40px;
  right: 20px;
  padding: 10px;
  box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65);
  border-radius: 4px;
  background: #ffffff;
  color: #222222;
  height: 50px;
}

.toolbar-icons i {
  font-size: 2.5em;
}

.toolbar .section {
  position: relative;
  display: block;
}

.instructions {
  top: 0;
  left: 0;
  right: 0;
  margin: 0;
  border-bottom: 1px solid #aaa;
}

.instructions .message {
  font-size: 16px;
  margin: 8px 10px;
}

form {
  float: right;
}

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

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

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

.switch {
  width: 100%;
  position: relative;
}

.switch input {
  position: absolute;
  top: 0px;
  z-index: 2;
  opacity: 0;
  cursor: pointer;
}

.switch input:checked {
  z-index: 1;
}

.switch input:checked + label {
  opacity: 1;
  cursor: default;
}

.switch input:not(:checked) + label:hover {
  opacity: 0.5;
}

.switch label {
  color: #222222;
  opacity: 0.33;
  transition: opacity 0.25s ease;
  cursor: pointer;
}

.switch .toggle-outside {
  height: 100%;
  border-radius: 2rem;
  overflow: hidden;
  transition: 0.25s ease all;
}

.switch .toggle-inside {
  border-radius: 2.5rem;
  background: #4a4a4a;
  position: absolute;
  transition: 0.25s ease all;
  top: 0.25rem;
}

.switch--horizontal {
  width: 18rem;
  height: 2rem;
  margin: 10px auto;
  font-size: 0;
  margin-bottom: 1rem;
}

.switch--horizontal input {
  height: 2rem;
  width: 5rem;
  left: 5rem;
  margin: 0;
}

.switch--horizontal label {
  font-size: 0.9rem;
  line-height: 2rem;
  display: inline-block;
  width: 6rem;
  height: 100%;
  margin: 0;
  text-align: center;
}

.switch--horizontal label:last-of-type {
  margin-left: 5rem;
}

.switch--horizontal .toggle-outside {
  background: #dddddd;
  position: absolute;
  width: 5rem;
  left: 5.7rem;
}

.switch--horizontal .toggle-inside {
  height: 1.5rem;
  width: 1.5rem;
}

.switch--horizontal input:checked ~ .toggle-outside .toggle-inside {
  left: 0.25rem;
}

.switch--horizontal input ~ input:checked ~ .toggle-outside .toggle-inside {
  left: 3.25rem;
}

.switch--horizontal input:disabled ~ .toggle-outside .toggle-inside {
  background: #9a9a9a;
}

.switch--horizontal input:disabled ~ label {
  color: #9a9a9a;
}
#money:hover {
  color: #56b956;
}
#commodity:hover {
  color: #bd8362;
}
#documents:hover {
  color: #cacaca;
}
#fraudster:hover {
  color: #f7102f;
}
#office:hover {
  color: #c7c7c7;
}

.inline-images > * {
  cursor: pointer;
}