VB: JET Diagram Component Implementation

Steve Zebib
Oracle VB Studio and JET
7 min readJul 23, 2021

--

DISCLAIMER: The views expressed in this story are my own and do not necessarily reflect the views of Oracle.

Overview

This article provides an example on how to implement the JET Diagram component in Oracle Visual Builder. This component is useful for displaying nested containers or hierarchy.

Setup

All of the below files including the JSON, HTML, and JS can be found in the VBCS web application main page flow.

JSON

  • The Metadata below includes all variables, types, action chains, and events required for this web application. Copy the following into the main-start-page.json file:
{
"title": "",
"description": "",
"variables": {
"expandedNodes": {
"type": "any"
},
"layoutFunc": {
"type": "any"
},
"linkDataProvider": {
"type": "any"
},
"nodeDataProvider": {
"type": "any"
}
},
"metadata": {},
"types": {},
"chains": {
"initActionChain": {
"description": "",
"variables": {},
"root": "callFunctionInit",
"actions": {
"callFunctionInit": {
"module": "vb/action/builtin/callModuleFunctionAction",
"parameters": {
"module": "[[ $functions ]]",
"functionName": "init"
},
"outcomes": {
"success": "callFunctionGetExpandedNodes"
}
},
"callFunctionGetExpandedNodes": {
"module": "vb/action/builtin/callModuleFunctionAction",
"parameters": {
"module": "[[ $functions ]]",
"functionName": "getExpandedNodes",
"returnType": "any"
},
"outcomes": {
"success": "callFunctionGetLayoutFunc"
}
},
"callFunctionGetLayoutFunc": {
"module": "vb/action/builtin/callModuleFunctionAction",
"parameters": {
"module": "[[ $functions ]]",
"functionName": "getLayoutFunc",
"returnType": "any"
},
"outcomes": {
"success": "callFunctionGetLinkDataProvider"
}
},
"callFunctionGetLinkDataProvider": {
"module": "vb/action/builtin/callModuleFunctionAction",
"parameters": {
"module": "[[ $functions ]]",
"functionName": "getLinkDataProvider",
"returnType": "any"
},
"outcomes": {
"success": "callFunctionGetNodeDataProvider"
}
},
"callFunctionGetNodeDataProvider": {
"module": "vb/action/builtin/callModuleFunctionAction",
"parameters": {
"module": "[[ $functions ]]",
"functionName": "getNodeDataProvider",
"returnType": "any"
},
"outcomes": {
"success": "assignVariablesExpandedNodes"
}
},
"assignVariablesExpandedNodes": {
"module": "vb/action/builtin/assignVariablesAction",
"parameters": {
"$page.variables.expandedNodes": {
"source": "{{ $chain.results.callFunctionGetExpandedNodes }}"
},
"$page.variables.layoutFunc": {
"source": "{{ $chain.results.callFunctionGetLayoutFunc }}"
},
"$page.variables.linkDataProvider": {
"source": "{{ $chain.results.callFunctionGetLinkDataProvider }}"
},
"$page.variables.nodeDataProvider": {
"source": "{{ $chain.results.callFunctionGetNodeDataProvider }}"
}
}
}
}
}
},
"eventListeners": {
"vbEnter": {
"chains": [
{
"chainId": "initActionChain",
"parameters": {}
}
]
}
},
"imports": {
"components": {
"oj-diagram": {
"path": "ojs/ojdiagram"
},
"oj-diagram-link": {
"path": "ojs/ojdiagram"
},
"oj-diagram-node": {
"path": "ojs/ojdiagram"
}
}
}
}

HTML

  • The HTML below includes diagram component with its templates. Copy the following into main-start-page.html file:
<div id="componentDemoContent" style="min-width: 100%;">
<oj-diagram id='diagram-container' node-data='[[$variables.nodeDataProvider]]' link-data='[[$variables.linkDataProvider]]' layout='[[ $variables.layoutFunc ]]' animation-on-data-change='auto' animation-on-display='auto' max-zoom='2.0' promoted-link-behavior='full' expanded="{{ $variables.expandedNodes }}">
<template slot="nodeTemplate" data-oj-as="node">
<oj-diagram-node label='[[node.data.name]]' icon.width='100' icon.height='60' icon.shape='rectangle'
icon.color='#f9f9f9' icon.border-radius='1px' icon.border
width='0.5' icon.border-color='#444444'
short-desc='[[node.data.name]]'>
</oj-diagram-node>
</template>
<template slot="linkTemplate" data-oj-as="link">
<oj-diagram-link start-node="[[link.data.startNode]]" end-node="[[link.data.endNode]]" start-connector-type="none" end-connector-type="none" short-desc='[[link.data.name]]'>
</oj-diagram-link>
</template>
</oj-diagram>
</div>

JS

  • The JS below includes functions needed to initialize the diagram component. This includes functions which will be assigned to variables that are referenced by the component. Copy the following into main-start-page.js file:
define(["require", "knockout", "ojs/ojbootstrap",
"ojs/ojarraydataprovider", "ojs/ojarraytreedataprovider", "ojs/ojknockout-keyset", "ojs/ojknockout", "ojs/ojdiagram"],
function (require, ko, ojbootstrap_1, ArrayDataProvider, ArrayTreeDataProvider, ojknockout_keyset_1) {
'use strict';class DemoContainerLayout {
/**
* Main function that performs container layout (Layout entry point)
* Top level nodes are positioned horizontally. Nodes inside containers are positioned vertically.
* @param {DvtDiagramLayoutContext} layoutContext object that defines a context for layout call
*/
static containerLayout(layoutContext) {
const nodeCount = layoutContext.getNodeCount();
let currX = 0;
for (let ni = 0; ni < nodeCount; ni++) {
const node = layoutContext.getNodeByIndex(ni);
if (node.isDisclosed() && node.getChildNodes()) {
DemoContainerLayout._layoutVertical(layoutContext, node.getChildNodes());
}
const bounds = node.getContentBounds();node.setPosition({ x: currX, y: -bounds.y - bounds.h * 0.5 });
DemoContainerLayout._positionLabel(node);
currX += bounds.w + 50;
}
//position the links
const linkCount = layoutContext.getLinkCount();
for (let i = 0; i < linkCount; i++) {
DemoContainerLayout._createLink(layoutContext, layoutContext.getLinkByIndex(i));
}
}
/**
* Layout child nodes vertically
* @param {DvtDiagramLayoutContext} layoutContext layout context
* @param {array} nodes nodes array
*/
static _layoutVertical(layoutContext, nodes) {
const padding = 20;
let currX = 0;
let currY = 0;
const nodeCount = nodes.length;
for (let ni = 0; ni < nodeCount; ni++) {
const node = nodes[ni];
if (node.isDisclosed() && node.getChildNodes()) {
DemoContainerLayout._layoutVertical(layoutContext, node.getChildNodes());
}
const bounds = node.getContentBounds();
node.setPosition({ x: currX - bounds.x - bounds.w * 0.5, y: currY });
DemoContainerLayout._positionLabel(node);
currY += bounds.h + padding;
}
}
/**
* Helper function creates a curved link between nodes
* @param {DvtDiagramLayoutContext} layoutContext Object that defines a context for layout call
* @param {DvtDiagramLayoutContextLink} link Link object
*/
static _createLink(layoutContext, link) {
const startId = link.getStartId();
const endId = link.getEndId();
const node1 = layoutContext.getNodeById(startId);
const node2 = layoutContext.getNodeById(endId);
const commonContainerId = layoutContext.getCommonContainer(startId, endId);
const n1Position = node1.getRelativePosition(commonContainerId);
const n2Position = node2.getRelativePosition(commonContainerId);
link.setCoordinateSpace(commonContainerId);
const n1Bounds = node1.getBounds();
const n2Bounds = node2.getBounds();
let startX, startY, endX, endY;
//find centers
const cn1X = n1Position.x + 0.5 * n1Bounds.w;
const cn2X = n2Position.x + 0.5 * n2Bounds.w;
if (Math.abs(cn1X - cn2X) < 10) {//vertical nodes
const cn1Y = 0.5 * (n1Position.y + n1Bounds.h);
const cn2Y = 0.5 * (n2Position.y + n2Bounds.h);
startX = n1Position.x + 0.5 * n1Bounds.w;
endX = n2Position.x + 0.5 * n2Bounds.w;
if (cn1Y < cn2Y) {
startY = n1Position.y + n1Bounds.h + link.getStartConnectorOffset();
endY = n2Position.y - link.getEndConnectorOffset();
}
else {
startY = n1Position.y - link.getEndConnectorOffset();
endY = n2Position.y + n2Bounds.h + link.getStartConnectorOffset();
}
link.setPoints(DemoContainerLayout._createVerticalLinkPath(startX, startY, endX, endY));
} else {
//horizontal
if (cn1X < cn2X) {
//left to right
startX =
n1Position.x +
n1Bounds.x +
n1Bounds.w +
link.getStartConnectorOffset();
endX = n2Position.x - link.getEndConnectorOffset();
} else {
//right to left
startX = n1Position.x - link.getStartConnectorOffset();
endX = n2Position.x + n2Bounds.x + n2Bounds.w + link.getEndConnectorOffset();
}
startY = n1Position.y + n1Bounds.y + 0.5 * n1Bounds.h;
endY = n2Position.y + n2Bounds.y + 0.5 * n2Bounds.h;
link.setPoints(DemoContainerLayout._createSideLinkPath(startX, startY, endX, endY));
}
//center label on link
const labelBounds = link.getLabelBounds();
if (labelBounds) {
const labelX = startX;
const labelY = startY - labelBounds.h;
//link.setLabelPosition(new DvtDiagramPoint(labelX, labelY));
link.setLabelPosition({ x: labelX, y: labelY });
}
}
/**
* Helper function creates a curved link that connects nodes sides
* The function uses quadratic Bezier to create a curve
* @param {number} startX X coordinate for the link start* @param {number} startY Y coordinate for the link start
* @param {number} endX X coordinate for the link end
* @param {number} endY Y coordinate for the link end
*/
static _createSideLinkPath(startX, startY, endX, endY) {
const path = ["M", startX, startY];
const midX = startX + 0.5 * (endX - startX);
const midY = startY + 0.5 * (endY - startY);
const c1X = midX; // X coordinate for the first control point
const c1Y = startY; // Y coordinate for the first control point
const c2X = midX; // X coordinate for the second control point
const c2Y = endY; // Y coordinate for the second control point
path.push("Q");
path.push(c1X);
path.push(c1Y);
path.push(midX);
path.push(midY);
path.push("Q");
path.push(c2X);
path.push(c2Y);
path.push(endX);
path.push(endY);
return path;}/**
* Helper function creates a plain link that connects nodes bottom to top
* @param {number} startX X coordinate for the link start
* @param {number} startY Y coordinate for the link start
* @param {number} endX X coordinate for the link end
* @param {number} endY Y coordinate for the link end
*/
static _createVerticalLinkPath(startX, startY, endX, endY) {
const path = ["M", startX, startY];
path.push("L");
path.push(endX);
path.push(endY);
return path;
}
/**
* Helper function that sets label position in the middle of the diagram node
* @param {DvtDiagramLayoutContextNode} node object that defines node context for the layout
*/
static _positionLabel(node) {
const nodeBounds = node.getContentBounds();
const nodePos = node.getPosition();
const nodeLabelBounds = node.getLabelBounds();
let labelY;
if (nodeLabelBounds) {if (node.isDisclosed()) {
//position label in the middle of top 20 px of the node
labelY = nodeBounds.y + nodePos.y + 0.5 * (20 - nodeLabelBounds.h);
} else {
//position label in the middle of the node
labelY = nodeBounds.y + nodePos.y + 0.5 * (nodeBounds.h - nodeLabelBounds.h);
}
const labelX = nodeBounds.x + nodePos.x + 0.5 * nodeBounds.w;
node.setLabelPosition({ x: labelX, y: labelY });
node.setLabelHalign("center");
}
}
}
var PageModule = function PageModule() { };var data = {
"nodes": [{
"id": "N0",
"name": "North America"
},
{
"id": "N1",
"name": "Country",
"nodes": [{
"id": "N01",
"name": "US"
}]
},
{
"id": "N2",
"name": "Regions",
"nodes": [{
"id": "N20",
"name": "Northeast",
"nodes": [{
"id": "N201",
"name": "Mid Atlantic"
},
{
"id": "N202",
"name": "New England"
}]
},
{
"id": "N21",
"name": "Midwest",
"nodes": [{
"id": "N212",
"name": "West North Central"
},
{
"id": "N211",
"name": "East North Central"
}]
},
{
"id": "N22",
"name": "South",
"nodes": [{
"id": "N221",
"name": "West North Central"
},
{
"id": "N222",
"name": "East North Central"
},
{
"id": "N223",
"name": "South Atlantic"
}]
},
{
"id": "N23",
"name": "West",
"nodes": [{
"id": "N231",
"name": "Pacific"
},
{
"id": "N232",
"name": "Mountain"
}]
}]
}],
"links": [{
"id": "L0",
"startNode": "N0",
"endNode": "N1"
},
{
"id": "L1",
"startNode": "N1",
"endNode": "N2"
}]
};
var nodeDataProvider;
var linkDataProvider;
var expandedNodes;
var layoutFunc;
PageModule.prototype.getNodeDataProvider = function () {
return nodeDataProvider;
};
PageModule.prototype.getLinkDataProvider = function () {
return linkDataProvider;
};PageModule.prototype.getExpandedNodes = function () {
return expandedNodes;
};
PageModule.prototype.getLayoutFunc = function () {
return layoutFunc;
};
PageModule.prototype.init = function () {
class DiagramModel {
constructor() {
nodeDataProvider = new ArrayTreeDataProvider(data.nodes, { keyAttributes: "id",
childrenAttribute: "nodes",
});
linkDataProvider = new ArrayDataProvider(data.links, {
keyAttributes: "id",
});
expandedNodes = new ojknockout_keyset_1.ObservableKeySet().add(["N0", "N1", "N2"]);
layoutFunc = DemoContainerLayout.containerLayout;
}
}
ojbootstrap_1.whenDocumentReady().then(() => {
ko.applyBindings(new DiagramModel(), document.getElementById("diagram-container"));
});};
return PageModule;
});

Test

  • Run the application and you should see the diagram shown below.

References

Oracle JET Cookbook — Diagram Component

--

--