2023年8月31日木曜日

Oracle JETのSankey LayoutのダイアグラムをOracle APEXで扱う

今回はOracle JETのUse CaseにあるSankey Layoutを実装したダイアグラムをOracle APEXで扱ってみます。

作成されたアプリケーションは以下のように動作します。


Use Case: Sankey Layoutは、Oracle JET Cookbookの以下のページで紹介されています。


実装手順はこちらの記事で紹介している基本的なDiagramと同じです。以下より、実装手順の詳細は省いたSankey Layoutの設定を紹介します。

Sankey LayoutのダイアグラムのソースとなるJSONファイルはsochiOlympics.jsonです。ソチオリンピックでの国別および競技別のメダルの個数が記録されています。

このファイルに含まれるノードを保存する表としてDG_SANKEY_NODESリンク(エッジ)を保存する表としてDG_SANKEY_LINKSを作成します。表の作成には以下のクイックSQLのモデルを使用します。
# prefix: dg_sankey
# pk: none
nodes
    id
    name vc40 /nn
    category vc20 /nn

links
    id
    source_node /fk nodes
    target_node /fk nodes
    items num /nn
それぞれの列IDはNUMBER型の主キーとして作成されますが、実際のデータは文字列です。クイックSQLが生成したDDLを、以下のように修正します。表DG_SANKEY_NODESの列IDとそれを参照している列の型はVARCHAR2(40 CHAR)、表DG_SANKEY_LINKSの列IDの型はVARCHAR2(4 CHAR)に変更しています。表DG_SANKEY_LINKSの列IDのデータにはidという名前に関わらず重複した値が含まれているため、主キー制約を外しています
-- create tables
create table dg_sankey_nodes (
    id                             varchar2(40 char) not null constraint dg_sankey_nodes_id_pk primary key,
    name                           varchar2(40 char) not null,
    category                       varchar2(20 char) not null
)
;

create table dg_sankey_links (
    id                             varchar2(4 char) not null,
    source_node                    varchar2(40 char)
                                   constraint dg_sankey_links_source_node_fk
                                   references dg_sankey_nodes on delete cascade,
    target_node                    varchar2(40 char)
                                   constraint dg_sankey_links_target_node_fk
                                   references dg_sankey_nodes on delete cascade,
    items                          number not null
)
;

-- table index
create index dg_sankey_links_i1 on dg_sankey_links (source_node);
create index dg_sankey_links_i52 on dg_sankey_links (target_node);

-- load data
APEXアプリケーションの名前JET Diagram Sankeyとし、空のアプリケーションを作成します。ダイアグラムはホーム・ページに実装します。


データ・ロード定義は、グラフのデータ(sochiOlympics.json)よりノードをロードするGraph Nodesと、リンクをロードするGraph Linksを作成します。


データ・ロード定義ロード・メソッド置換とします。

データ・ロード定義Graph Nodes行セレクタnodesデータ・プロファイルとしてID(セレクタはid)、NAME(同name)、CATEGORY(同category)を定義します。


データ・ロード定義Graph Links行セレクタlinksデータ・プロファイルとしてID(セレクタはid)、SOURCE_NODE(同source)、TARGET_NODE(同target)、ITEMS(同items)を定義します。列ITEMSはデータ型がNUMBERになります。


ホーム・ページにはデータを初期化するボタンINIT、ダイアグラムの表示を行なう静的コンテンツのリージョンDiagram、表DG_SANKEY_NODESを編集する対話グリッドNodesと、DG_SANKEY_LINKSを編集する対話グリッドLinksを作成します。


リージョンDiagramHTMLソースの記述は以下になります。

<div id="diagram-container">
<oj-diagram
id="diagram1"
node-data="[[nodeDataProvider]]"
link-data="[[linkDataProvider]]"
layout="[[layoutFunc]]"
style-defaults="[[styleDefaults]]"
class="demo-diagram-sankeylayout-height-style">
</oj-diagram>
</div>
データのロードは、ボタンINITを押した時に実行される実行チェーン初期化に含まれるプロセス(タイプデータのロード)、ノードリンクが行います。

設定データ・ソース型としてSQL Queryを選択し、SQL問合せとして以下を記述します。

select
apex_web_service.make_rest_request_b(
p_url => :G_DATA_SOURCE_URL
,p_http_method => 'GET'
) as blob_content
from dual;
ソースとなるsochiOlympics.jsonのURLは、置換文字列G_DATA_SOURCE_URLに設定します。


アプリケーション定義置換に置換文字列G_DATA_SOURCE_URLを設定します。置換値となるURLは以下になります。

https://www.oracle.com/webfolder/technetwork/jet/cookbook/dataVisualizations/diagram/resources/sochiOlympics.json


グラフのデータを取得するAjaxコールバックGET_DATAソースPL/SQLコードは以下になります。

JSON_OBJECTを使ったSELECT文は、32676文字を超えるレスポンスを生成することができません。レスポンスがそれ以上になる場合は、JSON_OBJECT_TやJSON_ARRAY_Tといったタイプ扱うPL/SQLコードを記述します。

declare
l_response_json json_object_t;
l_nodes json_array_t;
l_links json_array_t;
begin
/* create node array */
l_nodes := json_array_t();
for r in (
select json_object(
key 'id' value id
,key 'name' value name
,key 'category' value category
) as jo from dg_sankey_nodes
)
loop
l_nodes.append(json_object_t(r.jo));
end loop;
/* create link array */
l_links := json_array_t();
for r in (
select json_object(
key 'id' value id
,key 'source' value source_node
,key 'target' value target_node
,key 'items' value items
) as jo from dg_sankey_links
)
loop
l_links.append(json_object_t(r.jo));
end loop;
/* create reponse */
l_response_json := json_object_t();
l_response_json.put('nodes', l_nodes);
l_response_json.put('links', l_links);
htp.p(l_response_json.to_clob());
end;


ページ・プロパティJavaScriptファイルURLに以下を記述します。

[require jet]

ファンクションおよびグローバル変数の宣言に以下の1行を記述します。

var diagram;

ページ・ロード時の実行に以下を記述します。

requirejs.config({
paths: {
'diagramLayouts': "&APEX_PATH!RAW.#APP_FILES#layouts",
}
});
require(["require", "exports", "knockout", "diagramLayouts/DemoSankeyLayout", "ojs/ojbootstrap", "ojs/ojarraydataprovider", "ojs/ojattributegrouphandler", "ojs/ojknockout", "ojs/ojdiagram"], function (require, exports, ko, layout, ojbootstrap_1, ArrayDataProvider, ojattributegrouphandler_1) {
"use strict";
class DiagramModel {
/*
* Diagramで表示するデータを更新する。
*/
update(data) {
/* モデルをリセットする */
this.nodesMap = {};
this.nodes = [];
this.links = [];
/* ノードとリンクのデータを更新する。 */
this.data = data;
for (let i = 0; i < this.data.nodes.length; i++) {
this.nodesMap[this.data.nodes[i]['id']] = this.data.nodes[i];
}
for (let i = 0; i < this.data.links.length; i++) {
this.links.push(this.createLink(this.data.links[i]));
}
for (let nodeId in this.nodesMap) {
this.nodes.push(this.createNode(this.nodesMap[nodeId]));
}
this.nodeDataProvider(new ArrayDataProvider(this.nodes, {
keyAttributes: "id",
}));
this.linkDataProvider(new ArrayDataProvider(this.links, {
keyAttributes: "id",
}));
}
createLink(o) {
this.updateNodesWeight(o);
const source = o.source, target = o.target;
return {
id: o.id,
startNode: source,
endNode: target,
width: o.items * 3,
shortDesc: this.nodesMap[source]['category'] === 'award'
? o.items + ' ' + source + ' medals for ' + this.nodesMap[target]['name']
: this.nodesMap[source]['name'] +
' won ' +
o['items'] +
' medals in ' +
this.nodesMap[target]['name']
};
}
constructor() {
this.nodes = [];
this.links = [];
this.colorHandler = new ojattributegrouphandler_1.ColorAttributeGroupHandler();
this.nodesMap = {};
this.updateNodesWeight = (link) => {
const s = link.source, t = link.target;
if (s === 'Gold' || s === 'Silver' || s === 'Bronze')
this.nodesMap[s]['weight'] = this.nodesMap[s]['weight']
? this.nodesMap[s]['weight'] + link['items']
: link['items'];
this.nodesMap[t]['weight'] = this.nodesMap[t]['weight']
? this.nodesMap[t]['weight'] + link.items
: link.items;
};
// this.data = JSON.parse(jsonData);
this.createNode = (o) => {
const id = o['id'];
const weight = this.nodesMap[id]['weight'];
return {
id: id,
label: id,
shortDesc: o['name'],
icon: { color: this.colorHandler.getValue(id), height: weight * 3 }
};
};
this.layoutFunc = layout.layout;
this.nodeDataProvider = ko.observableArray();
this.linkDataProvider = ko.observableArray();
/*
this.nodeDataProvider = new ArrayDataProvider(this.nodes, {
keyAttributes: 'id'
});
this.linkDataProvider = new ArrayDataProvider(this.links, {
keyAttributes: 'id'
});
*/
this.styleDefaults = {
nodeDefaults: {
labelStyle: { fontSize: '30px', fontWeight: 'bold' },
icon: { width: 70, shape: 'rectangle' }
},
linkDefaults: { svgStyle: { strokeOpacity: 0.5, vectorEffect: 'none' } }
};
/*
for (let i = 0; i < this.data.nodes.length; i++) {
this.nodesMap[this.data.nodes[i]['id']] = this.data.nodes[i];
}
for (let i = 0; i < this.data.links.length; i++) {
this.links.push(this.createLink(this.data.links[i]));
}
for (let nodeId in this.nodesMap) {
this.nodes.push(this.createNode(this.nodesMap[nodeId]));
}
*/
}
}
(0, ojbootstrap_1.whenDocumentReady)().then(() => {
diagram = new DiagramModel();
ko.applyBindings(diagram, document.getElementById('diagram-container'));
/* ページ・ロード時の表示 */
apex.actions.invoke("update-diagram");
});
});
/*
* Diagramを更新する。
*/
apex.actions.add([
{
name: "update-diagram",
action: () => {
apex.server.process ( "GET_DATA", {},
{
success: (data) => {
// console.log(data);
diagram.update(data);
}
}
);
}
}
]);
CSSファイルURLに以下を記述します。

#JET_CSS_DIRECTORY#redwood/oj-redwood-notag-min.css

インラインは、Oracle JET Cookbookにdemo.cssとして記載されている内容を転記します。
.demo-diagram-sankeylayout-height-style {
    height: 37.5rem;
}

SankeyLayoutが実装されているファイルはDemoSankeyLayout.jsです。
https://www.oracle.com/webfolder/technetwork/jet/cookbook/dataVisualizations/diagram/layouts/DemoSankeyLayout.js

このファイルを静的アプリケーション・ファイルとしてアップロードします。アップロードする際にディレクトリとしてlayoutsを指定します。


アプリケーション・アイテムAPEX_PATHおよびアプリケーションの計算を設定し、APEX_UTIL.HOST_URL('APEX_PATH')の値を参照できるようにします。


対話グリッドの保存がクリックされたとき(表DG_SANKEY_NODESまたはDG_SANKEY_LINKSが変更されたとき)、ダイアグラムが更新されるように動的アクション(のTRUEアクション)を作成します。

apex.actions.invoke("update-diagram"); 


Oracle JETのUse Case: Sankey Layoutを使ったダイアグラムの実装の紹介は以上になります。

今回紹介したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/jet-diagram-sankey.zip

Oracle APEXのアプリケーション作成の参考になれば幸いです。