GraphQL
The GraphQL endpoint can be explored here, and is based on a MongoDB + Relay GraphQL server.
The network data is based on the Northwind database: is a sample database used by Microsoft that contains the sales data for Northwind Traders, a fictitious specialty foods export-import company.
No GraphQL client library is used in this demo, but feel free to check your favourite GraphQL client library.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="../build/ogma.min.js"></script>
<link href="fonts/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.19.0/themes/prism.min.css"
integrity="sha256-cuvic28gVvjQIo3Q4hnRpQSNB0aMw3C+kjkR0i+hrWg=" crossorigin="anonymous" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.19.0/prism.min.js"
integrity="sha256-YZQM6/hLBZYkb01VYf17isoQM4qpaOP+aX96hhYrWhg=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.19.0/plugins/autoloader/prism-autoloader.min.js"
integrity="sha256-WIuEtgHNTdrDT2obGtHYz/emxxAj04sJBdMhRjDXd8I=" crossorigin="anonymous"></script>
<style>
#graph-container {
top: 0;
bottom: 0;
left: 0;
right: 0;
position: absolute;
margin: 0;
overflow: hidden;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
#info {
position: absolute;
color: #fff;
background: #141229;
font-size: 12px;
font-family: monospace;
padding: 5px;
top: 0;
left: 0;
white-space: pre;
}
.toolbar {
display: block;
position: absolute;
top: 20px;
right: 20px;
padding: 10px;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65);
border-radius: 4px;
background: #ffffff;
color: #222222;
font-weight: 300;
min-width: 200px;
}
.toolbar h3 {
display: block;
font-weight: 300;
border-bottom: 1px solid #ddd;
color: #606060;
font-size: 1rem;
font-family: Arial, Helvetica, sans-serif;
}
.controls {
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;
}
pre {
max-width: 350px;
max-height: 300px;
overflow-y: scroll;
}
</style>
</head>
<body>
<div id="graph-container"></div>
<div class="toolbar">
<h3>Current GraphQL query</h3>
<pre><code class="language-graphql" id="query-text"></code></pre>
<pre><code class="language-json" id="variables-text"></code></pre>
<div class="controls">
<button id="layout" class="btn menu">Layout</button>
</div>
</div>
<div id="info">loading...</div>
<script>
'use strict';
var ogma = new Ogma({
container: 'graph-container'
});
// This is a very simple function that wraps some GraphQL configuration
function queryGraphQL(query, variables) {
return fetch('https://graphql-compose.herokuapp.com/northwind/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
},
body: JSON.stringify(query)
}).then(function (response) {
return response.json();
});
}
function toggleLoading() {
var el = document.querySelector('#info');
var enable = el.style.display;
el.style.display = enable === 'block' ? 'none' : 'block';
}
function updateGraphQLQuery(query) {
// Update the query
var codeBlock = document.querySelector('#query-text');
codeBlock.innerHTML = query.query;
Prism.highlightElement(codeBlock);
// Update the variables
var jsonBlock = document.querySelector('#variables-text');
jsonBlock.innerHTML = JSON.stringify(query.variables);
Prism.highlightElement(jsonBlock);
}
function getStartTemplate() {
return getExpandProductQuery({ name: 'Northwoods Cranberry Sauce' });
}
function getExpandCustomerQuery(filter) {
return {
query: [
'query getCustomer($filter: FilterFindOneCustomerInput) {',
' viewer {',
' item: customer(filter: $filter){',
' id: customerID,',
' name: companyName,',
' type: __typename',
' outwards: orderConnection(first: 20) {',
' edges {',
' node {',
' id: orderID,',
' name: orderID,',
' type: __typename',
' }',
' }',
' }',
' }',
' }',
'}'
].join('\n'),
variables: { filter: filter }
};
}
function getExpandOrderQuery(filter) {
// from the order find ouy all the associated products
return {
query: [
'query getOrder($filter: FilterFindOneOrderInput) {',
' viewer {',
' item: order(filter: $filter){',
' id: orderID,',
' name: orderID,',
' type: __typename',
' outwards: details {',
' product {',
' id: productID,',
' name: name,',
' type: __typename',
' }',
' }',
' inwards: customer {',
' id: customerID,',
' name: companyName,',
' type: __typename',
' }',
' }',
' }',
'}'
].join('\n'),
variables: { filter: filter }
};
}
function getExpandProductQuery(filter) {
return {
query: [
'query getProduct($filter: FilterFindOneProductInput) {',
' viewer {',
' item: product(filter: $filter){',
' id: productID,',
' name: name,',
' type: __typename',
' inwards: orderList(limit: 20) {',
' id: orderID,',
' name: orderID,',
' type: __typename',
' }',
' }',
' }',
'}'
].join('\n'),
variables: { filter: filter }
};
}
function getExpandQuery(id, type) {
// each query here is designed to always return items in the format
// { id, name, type, inwards: [], outwards: [] }
// The shape of in/outwards properties may be different for each type,
// but we're handling it in a single function
switch (type) {
case 'Product':
return getExpandProductQuery({ productID: id });
case 'Customer':
return getExpandCustomerQuery({ customerID: id });
case 'Order':
return getExpandOrderQuery({ orderID: id });
default:
return getStartTemplate();
}
}
function createNode(item) {
return { id: item.id, data: { type: item.type, name: item.name } };
}
function createEdge(source, target) {
return {
id: 'edge-' + source.id + '-' + target.id,
source: source.id,
target: target.id
};
}
function extractWithDirection(item) {
return {
out: extractItems(item.outwards),
in: extractItems(item.inwards)
};
}
// This function must return an array of items
// here we handle the whole domain complexity of the different relationships shape
function extractItems(items) {
if (!items) {
return [];
}
// This edges+nodes structure comes from a Relay GraphQL endpoint,
// when a <item>Connection() is used in the query (like the productConnection)
if (items.edges) {
return items.edges.map(function (wrapper) {
return wrapper.node;
});
}
// Orders <--> Products have a to pass thru the details relationship
// and here we're flattening it
if (Array.isArray(items)) {
var isWrapped = items.every(function (item) {
return item.product;
});
if (isWrapped) {
return items.map(function (wrapper) {
return wrapper.product;
});
}
return items;
}
return [items];
}
// GraphQL format -> RawGraph Ogma's format
function toOgmaFormat(json) {
// check inside the json.data.viewer for the data
var graph = { nodes: [], edges: [] };
var source = createNode(json.data.viewer.item);
graph.nodes.push(source);
// at this point items is in the shape {in: [...nodes], out: [...nodes]}
var items = extractWithDirection(json.data.viewer.item);
// iterate now on INs and OUTs sets and create nodes/edges
items.in.forEach(function (item) {
var target = createNode(item);
graph.nodes.push(target);
graph.edges.push(createEdge(target, source));
});
items.out.forEach(function (item) {
var target = createNode(item);
graph.nodes.push(target);
graph.edges.push(createEdge(source, target));
});
return graph;
}
function sendQuery(id, type) {
var query = getExpandQuery(id, type);
toggleLoading();
updateGraphQLQuery(query);
return queryGraphQL(query)
.then(function (json) {
// We using GraphQL alias on in queries so that the result is mostly normalized at this point
var graph = toOgmaFormat(json);
toggleLoading();
return ogma.addGraph(graph);
})
.then(function () {
return layout();
});
}
function layout() {
return ogma.layouts.forceLink({
duration: 550,
locate: { padding: 80 }
});
}
// Define what to use as node and edge captions.
ogma.styles.addEdgeRule({
color: function (e) {
return '#92e5a1';
},
shape: 'arrow'
});
ogma.styles.addNodeRule({
text: {
content: function (n) {
if (n.getData('type') === 'Order') {
return 'Order #' + n.getId();
}
return n.getData('name');
},
backgroundColor: 'white'
},
icon: {
content: function (n) {
var type = n.getData('type');
if (type === 'Product') {
return '\uf290';
}
if (type === 'Customer') {
return '\uf275';
}
// type === 'Order'
return '\uf07a';
},
font: 'FontAwesome',
color: function (n) {
var type = n.getData('type');
if (type === 'Customer') {
return '#000';
}
if (type === 'Product') {
return 'rgb(61,139,223)';
}
// type === 'Order'
return '#204829';
},
minVisibleSize: 0
},
outerStroke: {
color: '#204829',
width: 2
},
color: 'white'
});
ogma.events.onDoubleClick(function (evt) {
if (evt.target && evt.target.isNode) {
sendQuery(evt.target.getId(), evt.target.getData('type'));
}
});
document.querySelector('#layout').addEventListener('click', function (evt) {
evt.preventDefault();
layout();
});
sendQuery();
</script>
</body>
</html>