Skip to content
  1. Examples

Drag and drop

This example shows how to implement a drag and drop feature with Ogma.
Drag a graph file within the Ogma container to load it.
If you don't have a graph file in Ogma format, you can download samples by clicking on the Download samples button.
It handles csv, xlsx and json files.

ts
import Ogma from '@linkurious/ogma';

const ogma = new Ogma({
  container: 'graph-container'
});

const layout = () =>
  ogma.layouts.force({
    locate: { padding: 80 }
  });

type File = { name: string; content: string };

ogma.styles.addEdgeRule({
  color: e => '#80ce87',
  shape: 'arrow',
  width: e => Math.log(e.getData('weight'))
});

ogma.styles.addNodeRule({
  text: {
    content: n => n.getData('label'),
    minVisibleSize: 2
  },
  radius: n => n.getDegree(),
  icon: {
    font: 'Font Awesome 5 Free',
    content: n => '\uf007',
    color: 'rgb(61,139,223)',
    minVisibleSize: 0
  },
  outerStroke: {
    color: 'rgb(61,139,223)',
    width: 2
  },
  color: 'white'
});

function addGraphCsv({ name, content }: File) {
  return new Promise(resolve => {
    Papa.parse(atob(content.replace('data:text/csv;base64,', '')), {
      header: true,
      complete: resolve
    });
  }).then(({ data }) => {
    const nodesSet = new Set();
    const edges = [];
    data.forEach((item, i) => {
      nodesSet.add(item.source);
      nodesSet.add(item.target);
      const edgeId = item.source + '-' + item.target + '-' + i;
      edges.push({
        id: edgeId,
        source: item.source,
        target: item.target,
        data: { weight: Number(item.weight) }
      });
    });
    const nodes = [...nodesSet].map(id => ({ id: id, data: { label: id } }));
    if (!nodes || !nodes.length || !edges || !edges.length) {
      throw new Error(`File ${name} does not contain any nodes or edges`);
    }
    return ogma.addGraph({ nodes: nodes, edges: edges }).then(layout);
  });
}

function addGraphJSON({ name, content }: File) {
  const { nodes, edges } = JSON.parse(
    atob(content.replace('data:application/json;base64,', ''))
  );
  if (!nodes || !nodes.length || !edges || !edges.length) {
    throw new Error(`File ${name} does not contain any nodes or edges`);
  }
  return ogma.addGraph({ nodes: nodes, edges: edges }).then(layout);
}

function addGraphXLSX({ name, content }: File) {
  const workbook = XLSX.read(
    content.replace(
      'data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,',
      ''
    ),
    { type: 'base64' }
  );
  const nodes = XLSX.utils.sheet_to_json(workbook.Sheets.nodes).map(record => {
    return { id: record.id, data: { label: record.label } };
  });
  const edges = XLSX.utils
    .sheet_to_json(workbook.Sheets.edges)
    .map((record, row) => {
      return {
        id: record.source + '-' + record.target + '-' + row,
        source: record.source,
        target: record.target,
        data: { weight: record.weight }
      };
    });

  if (!nodes || !nodes.length || !edges || !edges.length) {
    throw new Error(`File ${name} does not contain any nodes or edges`);
  }
  return ogma.addGraph({ nodes: nodes, edges: edges }).then(layout);
}

document.querySelector('#start-drop')!.addEventListener('click', () => {
  document.querySelector('#dragdrop')!.classList.toggle('hidden');
});

document.querySelector('#dragdrop')!.addEventListener('drop', ev => {
  ev.preventDefault();
  let files;
  if (ev.dataTransfer.items) {
    files = [...ev.dataTransfer.items]
      .map((item, i) => {
        return item.kind === 'file' ? item.getAsFile() : null;
      })
      .filter(e => e);
  } else {
    files = [...ev.dataTransfer.files];
  }
  document.querySelector('#dragdrop')!.classList.toggle('hidden');
  Promise.all(
    files.map(file => {
      const match = file.name.match(/\.(xlsx|csv|json)$/);
      if (!match) return Promise.resolve();
      const reader = new FileReader();
      const promise = new Promise(resolve => {
        reader.onload = function (event) {
          resolve({
            name: file.name,
            extension: match[1],
            content: event.target.result
          });
        };
      });
      reader.readAsDataURL(file);
      return promise;
    })
  )
    .then(files => {
      files.forEach(file => {
        if (file.extension === 'xlsx') {
          addGraphXLSX(file);
        } else if (file.extension === 'csv') {
          addGraphCsv(file);
        } else if (file.extension === 'json') {
          addGraphJSON(file);
        } else {
          throw new Error(
            `${file.name}: Invalid file extension ${file.extension}`
          );
        }
      });
    })
    .catch(e => {
      return Toastify({
        text: e.message,
        duration: 3000,
        gravity: 'bottom',
        position: 'bottom',
        style: {
          background: 'linear-gradient(to right, #a70000, #ff5252)',
          'min-width': '100vw',
          'max-width': '100vw'
        }
      }).showToast();
      throw e;
    });
});

document.querySelector('#dragdrop')!.addEventListener('dragover', ev => {
  ev.preventDefault();
});

document.querySelector('#download')!.addEventListener('click', ev => {
  ['files/solarCity.json', 'files/got-edges2.csv', 'files/got.xlsx'].forEach(
    url => {
      const a = document.createElement('a');
      a.href = url;
      a.download = url;
      document.body.appendChild(a);
      a.click();
    }
  );
});
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" />
  <script
    src="https://cdn.jsdelivr.net/npm/xlsx@0.11.12/dist/xlsx.core.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/toastify-js/1.12.0/toastify.min.js"></script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastify-js/1.12.0/toastify.css"/>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.1.0/papaparse.min.js"></script>
  <link type="text/css" rel="stylesheet" href="styles.css">
</head>

<body>
  <div id="graph-container"></div>
  </div>
  <div class="controls panel">
    <button id="start-drop" class="btn menu">Add file</button>
    <button id="download" class="btn menu">Download samples</button>

  </div>
  <div id="dragdrop" class="hidden">
    <div id="overlay"></div>
    <div id="container panel">
      <h3>Drag and drop your file here</h3>
      <h3>(CSV, XLSX, JSON)</h3>
      <div class="icons">
        <img src="https://upload.wikimedia.org/wikipedia/commons/c/c6/.csv_icon.svg" />
        <img src="https://upload.wikimedia.org/wikipedia/commons/f/f3/.xlsx_icon.svg" />
        <img src="files/json.svg" />

      </div>
      <img src="https://upload.wikimedia.org/wikipedia/commons/8/8b/OOjs_UI_icon_download.svg" />
    </div>
  </div>
  <script src="index.ts"></script>
</body>
</html>
css
#graph-container {
  margin: 0;
  overflow: hidden;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  -o-user-select: none;
  user-select: none;
  width: 100vw;
  height: 100vh;
}

.controls {
  position: absolute;
  right: 10px;
  top: 10px;
  width: 150px;
  text-align: center;
  margin-top: 5px;
}

.btn {
  padding: 6px 8px;
  background-color: white;
  cursor: pointer;
  font-size: 18px;
  border: none;
  border-radius: 5px;
  outline: none;
}

.btn:hover {
  color: #333;
  background-color: #e6e6e6;
}

.menu {
  border: 1px solid #ddd;
  width: 80%;
  font-size: 14px;
  margin-top: 10px;
}
h3 {
  margin: 0;
}

#dragdrop {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-evenly;
}
#dragdrop > #overlay {
  background-color: #bbb;
  opacity: 0.5;
  width: 100vw;
  height: 100vh;
  position: absolute;
}
#dragdrop > #container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-evenly;
  border: 5px dashed #333;
  padding: 20px;
  border-radius: 20px;
}
#container > img {
  font-size: 2em;
  opacity: 1;
  width: 200px;
}
#container > .icons {
  display: flex;
  flex-direction: row;
  justify-content: space-evenly;
}
#container > .icons > img {
  width: 54px;
}
.hidden {
  display: none;
  visibility: hidden;
}

html,
body {
  font-family: 'Inter', sans-serif;
}

:root {
  --base-color: #4999f7;
  --active-color: var(--base-color);
  --gray: #d9d9d9;
  --white: #ffffff;
  --lighter-gray: #f4f4f4;
  --light-gray: #e6e6e6;
  --inactive-color: #cee5ff;
  --group-color: #525fe1;
  --group-inactive-color: #c2c8ff;
  --selection-color: #04ddcb;
  --darker-gray: #b6b6b6;
  --dark-gray: #555;
  --dark-color: #3a3535;
  --edge-color: var(--dark-color);
  --border-radius: 5px;
  --button-border-radius: var(--border-radius);
  --edge-inactive-color: var(--light-gray);
  --button-background-color: #ffffff;
  --shadow-color: rgba(0, 0, 0, 0.25);
  --shadow-hover-color: rgba(0, 0, 0, 0.5);
  --button-shadow: 0 0 4px var(--shadow-color);
  --button-shadow-hover: 0 0 4px var(--shadow-hover-color);
  --button-icon-color: #000000;
  --button-icon-hover-color: var(--active-color);
}

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

.ui {
  position: absolute;
  display: flex;
  flex-direction: column;
  gap: 0.5em;
}

#custom-group-btn {
  top: 40px;
}

.panel {
  background: var(--button-background-color);
  border-radius: var(--button-border-radius);
  box-shadow: var(--button-shadow);
  padding: 10px;
}

.panel {
  position: absolute;
}

.panel h2 {
  text-transform: uppercase;
  font-weight: 400;
  font-size: 14px;
  margin: 0;
}

.panel {
  margin-top: 1px;
}

.panel button {
  background: var(--button-background-color);
  border: none;
  border-radius: var(--button-border-radius);
  border-color: var(--shadow-color);
  padding: 5px 10px;
  cursor: pointer;
  width: 100%;
  color: var(--dark-gray);
  border: 1px solid var(--light-gray);
}

.panel button:hover {
  background: var(--lighter-gray);
  border: 1px solid var(--darker-gray);
}

.panel button[disabled] {
  color: var(--light-gray);
  border: 1px solid var(--light-gray);
  background-color: var(--lighter-gray);
}

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

#focus-select {
  position: absolute;
  top: 20px;
  right: 20px;
  padding: 10px;
  background: white;
  z-index: 400;
  width: 200px;
  right: 1em;
  top: 1em;
}

#focus-select label {
  display: block;
}

#focus-select .controls {
  text-align: center;
  margin-top: 10px;
}

#focus-select .content {
  line-height: 1.5em;
}