作成されたアプリケーションは以下のように動作します。
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とし、空のアプリケーションを作成します。ダイアグラムはホーム・ページに実装します。
データ・ロード定義のロード・メソッドは置換とします。
データ・ロード定義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を作成します。
リージョンDiagramのHTMLソースの記述は以下になります。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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問合せとして以下を記述します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
JSON_OBJECTを使ったSELECT文は、32676文字を超えるレスポンスを生成することができません。レスポンスがそれ以上になる場合は、JSON_OBJECT_TやJSON_ARRAY_Tといったタイプ扱うPL/SQLコードを記述します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]
var diagram;
ページ・ロード時の実行に以下を記述します。
ページ・ロード時の実行に以下を記述します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
#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のアプリケーション作成の参考になれば幸いです。
完