Is there a way that for me to connect to Memgraph via web-sockets and visualize graph results with Orb? I've seen something like this on Memgraph Playground and I would like to use it on my site.
There is a GIST that connects to Memgraph via web socket connection, runs a Cypher query and visualizes graph results using Orb. There is no need for installation. Just open the HTML in your browser with Memgraph running in the background.
I'll copy the GIST here:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Javascript Browser Example | Memgraph</title>
<script src="https://cdn.jsdelivr.net/npm/neo4j-driver"></script>
<script src="https://unpkg.com/@memgraph/orb/dist/browser/orb.min.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto&family=Roboto+Mono&display=swap" rel="stylesheet">
<style>
body {
font-size: 1rem;
font-family: 'Roboto', sans-serif;
font-weight: 400;
line-height: 1.5;
}
header {
padding: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
background-color: #ddd;
}
main {
padding: 0.5rem;
flex-grow: 1;
}
input {
width: 100%;
padding: 0.5rem 1.5rem 0.5rem 0.5rem;
border: 1px solid #e6e6e6;
border-radius: 5px;
background-color: #fff;
font-size: 1rem;
font-family: 'Roboto Mono', monospace;
color: #231f20;
}
input:focus {
outline: none;
}
button {
width: 5rem;
background: #fb6e00;
color: white;
border: 0;
border-radius: 5px;
}
button:hover {
background: red;
cursor: pointer;
}
.container {
display: flex;
flex-direction: column;
position: absolute;
inset: 0;
}
.connection-icon {
height: 10px;
}
.connection-closed {
color: red;
}
.connection-opened {
color: green;
}
.connection[data-is-opened="true"] .connection-icon {
fill: green;
}
.connection[data-is-opened="false"] .connection-icon {
fill: red;
}
.connection[data-is-opened="true"] .connection-closed {
display: none;
}
.connection[data-is-opened="false"] .connection-closed {
display: inline;
}
.connection[data-is-opened="true"] .connection-opened {
display: inline;
}
.connection[data-is-opened="false"] .connection-opened {
display: none;
}
.setup {
display: flex;
gap: 0.5rem;
}
.hidden {
display: none;
}
#error {
color: red;
}
#info {
padding: 0.5rem;
font-family: 'Roboto Mono', monospace;
z-index: 9999;
position: absolute;
inset: auto 0 0 0;
background-color: white;
border-top: 1px solid #e6e6e6;
word-wrap: break-word;
}
#info > strong {
color: #fb6e00;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="connection" data-is-opened="false">
<svg class="connection-icon" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="50" />
</svg>
<span class="connection-closed">Disconnected from <span class="bolt-url">localhost:7687</span>. Please check that your Memgraph is running and listening on <span class="bolt-url">localhost:7687</span></span>
<span class="connection-opened">Connected to <span class="bolt-url">localhost:7687</span></span>
</div>
<div class="setup">
<input type="text" value="MATCH (n)-[e]->(m) RETURN n, e, m;" placeholder="Add your Cypher query here and click run..." />
<button>Run</button>
</div>
</header>
<main>
<div id="info" class="hidden"></div>
<div id="error" class="hidden"></div>
<div id="graph"></div>
</main>
</div>
<script>
const inputElem = document.querySelector("input");
const buttonElem = document.querySelector("button");
const graphElem = document.getElementById("graph");
const errorElem = document.getElementById("error");
const infoElem = document.getElementById("info");
const connectionElem = document.querySelector(".connection");
const BOLT_HOSTNAME = 'localhost:7687';
const orb = new Orb.Orb(graphElem);
const driver = neo4j.driver(`bolt://${BOLT_HOSTNAME}`, neo4j.auth.basic("", ""));
// Using Orb to render nodes and edges
const renderGraph = ({ nodes, edges }) => {
orb.data.setDefaultStyle(getGraphStyling({ nodes, edges }));
orb.data.setup({ nodes, edges });
orb.view.render(() => {
orb.view.recenter();
});
orb.events.on(Orb.OrbEventType.NODE_CLICK, (event) => {
infoElem.innerHTML = `<strong>Node clicked: </strong>` + JSON.stringify(event.node.data)
infoElem.classList.remove('hidden');
});
orb.events.on(Orb.OrbEventType.EDGE_CLICK, (event) => {
infoElem.innerHTML = `<strong>Edge clicked: </strong>` + JSON.stringify(event.edge.data)
infoElem.classList.remove('hidden');
});
orb.events.on(Orb.OrbEventType.MOUSE_CLICK, (event) => {
if (!event.subject) {
infoElem.classList.add('hidden');
}
});
};
// Using Orb to get a default node/edge styling
const getGraphStyling = ({ nodes, edges }) => {
const colorByLabels = {};
nodes.forEach((node) => {
const labels = node.labels.join(':');
if (!colorByLabels[labels]) {
colorByLabels[labels] = Orb.Color.getRandomColor();
}
});
return {
getNodeStyle(node) {
const labels = node.data.labels.join(':');
const name = node.data.properties?.title ?? node.data.properties?.name ?? labels;
return {
size: 5,
color: colorByLabels[labels] ?? Orb.Color.getRandomColor(),
label: name,
};
},
getEdgeStyle(edge) {
return {
width: 0.3,
color: '#ababab',
label: edges.length < 50 ? edge.type : '',
};
},
};
};
const extractGraphFromMgResult = (mgResult) => {
const nodeById = {};
const edgeById = {};
mgResult.records.forEach((record) => {
Object.values(record).forEach((value) => {
if (_isMemgraphNode(value)) {
nodeById[value.id] = value;
}
if (_isMemgraphEdge(value)) {
edgeById[value.id] = value;
}
if (_isMemgraphPath(value)) {
value.nodes.forEach((node) => nodeById[node.id] = node);
value.relationships.forEach((edge) => edgeById[edge.id] = edge);
}
});
});
return { nodes: Object.values(nodeById), edges: Object.values(edgeById) };
};
// Parsing functions to handle Neo4j/Memgraph result objects
const _isNumber = (value) => typeof value === 'number';
const _isString = (value) => typeof value === 'string';
const _isObject = (value) => typeof value === 'object' && value !== null;
const _isArray = (value) => Array.isArray(value);
const _MG_NODE = 'node';
const _MG_EDGE = 'relationship';
const _MG_PATH = 'path';
const _isMemgraphNode = (field) => {
return (
_isObject(field) &&
_isNumber(field.id) &&
field.type === _MG_NODE
);
};
const _isMemgraphEdge = (field) => {
return (
_isObject(field) &&
_isNumber(field.id) &&
_isNumber(field.start) &&
_isNumber(field.end) &&
field.type === _MG_EDGE
);
};
const _isMemgraphPath = (field) => {
return (
_isObject(field) &&
_isArray(field.nodes) &&
field.nodes.every((node) => _isMemgraphNode(node)) &&
_isArray(field.relationships) &&
field.relationships.every((edge) => _isMemgraphEdge(edge)) &&
field.type === _MG_PATH
);
};
const _toMemgraphNode = (neo4jNode) => {
return {
id: parseNeo4jField(neo4jNode.identity),
labels: parseNeo4jField(neo4jNode.labels),
properties: parseNeo4jField(neo4jNode.properties),
type: _MG_NODE,
};
};
const _toMemgraphEdge = (neo4jEdge) => {
return {
id: parseNeo4jField(neo4jEdge.identity),
start: parseNeo4jField(neo4jEdge.start),
end: parseNeo4jField(neo4jEdge.end),
label: parseNeo4jField(neo4jEdge.type),
properties: parseNeo4jField(neo4jEdge.properties),
type: _MG_EDGE,
};
};
const _toMemgraphPath = (neo4jPath) => {
const nodeById = {};
const edgeById = {};
(neo4jPath.segments ?? []).forEach((segment) => {
if (_isNeo4jNode(segment.start)) {
const node = _toMemgraphNode(segment.start);
nodeById[node.id] = node;
}
if (_isNeo4jNode(segment.end)) {
const node = _toMemgraphNode(segment.end);
nodeById[node.id] = node;
}
if (_isNeo4jEdge(segment.relationship)) {
const edge = _toMemgraphEdge(segment.relationship);
edgeById[edge.id] = edge;
}
});
return {
nodes: Object.values(nodeById),
relationships: Object.values(edgeById),
type: _MG_PATH,
};
};
const _isNeo4jNumber = (field) => {
return (
_isObject(field) &&
Object.keys(field).length === 2 &&
_isNumber(field.low) &&
_isNumber(field.high)
);
};
const _isNeo4jNode = (field) => {
return (
_isObject(field) &&
_isNeo4jNumber(field.identity) &&
_isArray(field.labels) &&
field.labels.every((label) => _isString(label)) &&
_isObject(field.properties)
);
};
const _isNeo4jEdge = (field) => {
return (
_isObject(field) &&
_isNeo4jNumber(field.identity) &&
_isNeo4jNumber(field.start) &&
_isNeo4jNumber(field.end) &&
_isString(field.type) &&
_isObject(field.properties)
);
};
const _isNeo4jPath = (field) => {
return (
_isObject(field) &&
_isNeo4jNode(field.start) &&
_isNeo4jNode(field.end) &&
_isArray(field.segments) &&
field.segments.every((segment) => {
return (
_isObject(segment) &&
_isNeo4jNode(segment.start) &&
_isNeo4jEdge(segment.relationship) &&
_isNeo4jNode(segment.end)
);
})
);
}
const parseNeo4jField = (field) => {
if (field === undefined || field === null) {
return null;
}
if (_isArray(field)) {
return field.map((item) => parseNeo4jField(item));
}
if (_isNeo4jNumber(field)) {
return field.toNumber();
}
if (_isNeo4jNode(field)) {
return _toMemgraphNode(field);
}
if (_isNeo4jEdge(field)) {
return _toMemgraphEdge(field);
}
if (_isNeo4jPath(field)) {
return _toMemgraphPath(field);
}
if (_isObject(field)) {
const newObject = {};
Object.keys(field).forEach((key) => {
if (field.hasOwnProperty(key)) {
newObject[key] = parseNeo4jField(field[key]);
}
});
return newObject;
}
return field;
}
const parseNeo4jRecord = (record) => {
if (!_isObject(record)) {
return {};
}
const newRecord = {};
record.keys.forEach((key) => {
newRecord[key] = parseNeo4jField(record.get(key));
});
return newRecord;
}
const parseNeo4jResult = (result) => {
if (!_isObject(result)) {
return { records: [] };
}
return {
records: (result.records ?? []).map((r) => parseNeo4jRecord(r)),
summary: result.summary,
};
}
const runCypherQuery = async (query) => {
const session = driver.session();
try {
const neo4jResult = await session.run(query);
return parseNeo4jResult(neo4jResult);
} catch (error) {
throw error;
} finally {
session.close();
}
}
// Handling UI elements and results
const runQueryFromInputValue = async () => {
const query = inputElem.value ?? '';
try {
const mgResult = await runCypherQuery(query);
const graph = extractGraphFromMgResult(mgResult);
if (graph.nodes.length === 0 && graph.edges.length === 0) {
throw new Error(`Query was successful, but the graph can't be shown because there are no nodes and edges in the response.`);
}
hideError();
renderGraph(graph);
} catch (error) {
console.error(error);
showError(error);
}
};
const showError = (error) => {
errorElem.innerHTML = error.message;
errorElem.classList.remove('hidden');
graphElem.classList.add('hidden');
if (error.message.includes('WebSocket connection failure')) {
setIsDisconnected();
}
};
const hideError = () => {
setIsConnected();
errorElem.classList.add('hidden');
graphElem.classList.remove('hidden');
};
const setIsConnected = () => connectionElem.setAttribute('data-is-opened', 'true');
const setIsDisconnected = () => connectionElem.setAttribute('data-is-opened', 'false');
// Event handlers for running a cypher query
buttonElem.addEventListener('click', () => runQueryFromInputValue());
inputElem.addEventListener('keyup', (e) => {
if (e.key === 'Enter' || e.keyCode === 13) {
runQueryFromInputValue();
}
});
// Setting up the final bolt endpoint to the UI
document.querySelectorAll('.bolt-url').forEach((spanElem) => spanElem.innerHTML = BOLT_HOSTNAME);
// Running a test query to check connection
runCypherQuery('MATCH (n) RETURN n LIMIT 1').then(() => setIsConnected());
</script>
</body>
</html>