Ogma is a client-side
ogma.min.js
- the libraryogma.min.d.ts
- TypeScript declaration fileVisualisation styling
Layouts
Layouts
Geo mode
Interactions
Transformations
Algorithms
npm i -D webpack webpack-cli \
css-loader style-loader \
webpack-dev-server html-webpack-plugin
package.json
{
"name": "01-setup",
"version": "1.0.0",
"main": "src/index.js",
"scripts": {
"build": "webpack",
"start": "webpack -w"
},
"devDependencies": {
"css-loader": "^4.3.0",
"style-loader": "^1.2.1",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12"
}
}
src/index.js
import "styles.css";
// your code will go here
console.log('Welcome to visualisation with Ogma');
src/styles.css
#container {
display: block;
width: 100%;
height: 100%;
}
Set up the Webpack config
const path = require('path');
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
module: {
rules: [{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
}]
},
entry: './src/index.js',
output: {
filename: "[name].bundle.js",
path: path.resolve(__dirname, "dist")
},
plugins: [new HtmlWebpackPlugin()],
devServer: {
contentBase: path.join(__dirname, 'dist'),
port: 9000
}
};
npm start
npm
link
npm install --save https://get.linkurio.us/.../?secret=<YOUR_API_KEY>
import Ogma from 'ogma';
// index.js
import Ogma from 'ogma';
// create a container and add CSS to it
const container = document.createElement('div');
container.id = 'vis';
document.body.appendChild(container);
// initialize Ogma
const ogma = new Ogma({ container });
// add some nodes and edges
ogma.addNode({ id: 0, attributes: {
x: -120, y: -75
}});
ogma.addNode({ id: 1, attributes: {
x: 120, y: -75
}});
// add edge
ogma.addEdge({ source: 0, target: 1 });
We encourage you to update Ogma every time you get a notification that the new version is out.
npm
link (it contains the package
version)package.json
npm install
Ogma + neo4j database
For this example we have loaded a supply chain dataset from this neo4j tutorial into a hosted neo4j instance
To load data into Ogma you need to parse it into its internal format:
{
"nodes": [{
"id": 0, // number|string, required
// see https://doc.linkurio.us/ogma/latest/api.html#NodeAttributes
"attributes": {}, // visual attributes
"data": {}, // any data properties, free format
}, ...],
"edges": [{
"source": 0, // required
"target": 1, // required,
// see https://doc.linkurio.us/ogma/latest/api.html#EdgeAttributes
"attributes": {}, // same as for nodes
"data": {}
}]
}
See NodeAttributes, EdgeAttributes docs
import { HOST, DB_NAME, PASSWORD } from '../../credentials.json';
const query = "MATCH (a)-[r]-() RETURN a, r";
return this._session
.run(query)
// parse the response into the format Ogma understands
.then(response => ogma.parse.neo4j(response));
Your dummy neo4j client looks like this:
// Create a driver to connect to the Neo4j database
const driver = neo4j.driver(
HOST, neo4j.auth.basic(DB_NAME, PASSWORD)
);
const session = driver.session({
database: DB_NAME,
defaultAccessMode: neo4j.session.READ,
});
// Query the graph to the server and load it to ogma
const query = "MATCH (a)-[r]-() RETURN a, r";
session
.run(query)
.then(response => ogma.parse.neo4j(response));
// instance of the API client
const api = new API(HOST, DB_NAME, PASSWORD);
// load the graph
api.getGraph()
.then((rawGraph) => ogma.setGraph(rawGraph))
// setup the camera so we can see the entire graph
.then(() => ogma.view.locateGraph())
.then(() => {
// show some information about the graph
const nodes = ogma.getNodes();
const edges = ogma.getEdges();
document.getElementById('controls').innerHTML =
`Loaded
${nodes.size} nodes, ${edges.size} edges`;
});
What is missing?
Positions
Let's run the layout!
api.getGraph();
.then((rawGraph) => ogma.setGraph(rawGraph))
// apply the force layout to the graph
// (places the nodes so it is readable )
.then(() => ogma.layouts.force());
Positions
Nodes color and icon coding:
// Define buckets of importance for nodes
// some values repeat themselves for different types
const supplierStyle = { color: "#6FC1A7", icon: "\uEA04" };
const rawSupplierStyle = { color: "#3DCB8D", icon: "\uEA03" };
const wholeSalerStyle = { color: "#E77152", icon: "\uEA09" };
const retailerStyle = { color: "#9AD0D0", icon: "\uEA06" };
const nodeStyles = {
'SupplierA': supplierStyle,
'SupplierB': supplierStyle,
'RawSupplierA': rawSupplierStyle,
'RawSupplierB': rawSupplierStyle,
'AllRawSupliers': rawSupplierStyle,
'Retailer': retailerStyle,
'AllRetailers': retailerStyle,
'Wholesaler': wholeSalerStyle,
'AllWholesalers': wholeSalerStyle,
'FrameSupplier': { color: "#6FC1A7", icon: "\uEA05" },
'WheelSupplier': { color: "#6FC1A7", icon: "\uEA08" },
'Product': { color: "#76378A", icon: "\uEA02" }
};
Edges color and size coding:
// edge color and width buckets
const edgeBuckets = ogma.rules.slices({
field: "neo4jProperties.quantity",
values: [
{ color: "#DDD", width: 0.5 },
{ color: "#AAA", width: 1.5 },
{ color: "#777", width: 2 },
{ color: "#444", width: 3.5 }
],
// buckets are
// (-Infinity, 15], (15, 150], (150, 280], [280, Infinity)
stops: [15, 150, 280]
});
Apply edge styling
ogma.styles.addRule({
edgeAttributes: (edge) => {
const quantity = edge.getData("neo4jProperties.quantity");
const { color, width } = edgeBuckets(edge);
return {
shape: { head: "arrow" },
color, width
};
}});
Apply edge styling
Apply node styling
ogma.styles.addRule({
nodeAttributes: (node) => {
// get data field
const type = getNodeType(node);
// match with the styles object
const { color, icon } = nodeStyles[type];
// sizing: quantity of incoming edges / 300
// nodes size depend on the quantity of Product that is exanged through them
const radius = node
.getAdjacentEdges()
.reduce((acc, e) => acc + e.getData("neo4jProperties.quantity"), 0) / 300 + 5;
return {
icon: {
font: "icon-font",
color: "#000000",
content: icon,
},
color, radius
};
}});
Apply node styling
Add insights
// Helper function to check if a node should pulse
// Nodes pulses if the total demand is higher than their stock
function hasRisk(node) {
const totalDemand = node
.getAdjacentEdges()
.filter((edge) => edge.getSource() === node)
.reduce((total, edge) => total + edge.getData("neo4jProperties.quantity"), 0);
return node.getData("neo4jProperties.stock") - totalDemand < 100;
}
// enable pulse animation
ogma.getNodes().filter(hasRisk).pulse();
Add insights
Let's look at the the same graph with the geo reference
// let Ogma know where to look for the coordinates
ogma.geo.enable({
longitudePath: 'neo4jProperties.lon',
latitudePath: 'neo4jProperties.lat'
});
Let's clear up the view a bit
First let's group the parallel edges
const edgeGrouping = ogma.transformations.addEdgeGrouping({
selector: (edge) => edge.getData("neo4jType") === "DELIVER",
generator: (edges) => {
return {
data: { neo4jProperties: { quantity: edges.reduce(...), 0), }}
};
}
});
Parallel edges are grouped
Let's group the nodes: join the raw suppliers, wholesalers and group the part suppliers:
const nodeGrouping = ogma.transformations.addNodeGrouping({
groupIdFunction: (node) => node.getData("neo4jLabels")[0],
// don't group the product
selector: (node) => node.getData("neo4jLabels")[0] !== "Product",
nodeGenerator: (nodes, groupId) => {
// choose label for the style
let label = ['Wholesalers']; // default
if (groupId === "SupplierA") label = ["FrameSupplier"];
else if (groupId === "SupplierB") label = ["WheelSupplier"];
else if (/RawSupplier/.test(groupId)) label = ["RawSuppliers"];
else if (groupId === 'Retailer') label = ["Retailers"];
// sizing criteria
const quantity = nodes.getData("neo4jProperties.quantity")
.reduce((total, q) => total + q, 0);
// generated group node
return {
data: {
neo4jLabels: label,
neo4jProperties: { quantity }
}
};
}
});
Nodes are now grouped by the type:
Let's make sense of this new view on the supply chain by applying a different layout:
Let's add some controls
// index.js
const edgeGroupingButton = document.getElementById('edge-grouping');
edgeGroupingButton.addEventListener('click', () => {
edgeGrouping.toggle()
.then(() => {
const state = edgeGrouping.isEnabled() ? 'on' : 'off';
edgeGroupingButton.innerHTML = 'Edge grouping: ' + state;
});
});
Final result
Ogma integrates nicely with all the popular front-end frameworks
ogma@linkurio.us