Appearance
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;
}