2023年8月28日月曜日

Oracle JETのTreemapをOracle APEXで扱う

Oracle APEXではチャートを表示するために使用するJavaScriptライブラリとして、Oracle JETを採用しています。Oracle JETのチャート・コンポーネントについては、おおむねOracle APEXのチャート・リージョンで利用できます。

Oracle JETは、チャート以外にもビジュアリゼーションのコンポーネントを提供しています。

以下よりOracle JETのTreemapをOracle APEXのアプリケーションに組み込んでみます。

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


Oracle JET Cookbookで紹介されているTreemapのBasicを、Oracle APEXに実装しています。


以下より実装手順を説明します。

Oracle JET CookbookではJSON形式のファイルusaMeanIncomeSubregion.jsonをデータソースとしています。Oracle APEXのアプリケーションでは、データベースの表をデータソースとして使用します。

usaMeanIncomeSubregion.jsonには米国の地域ごとの人口と平均所得が記載されています。また、米国全体、地域、州の単位で階層化されたデータになっています。

以下のクイックSQLのモデルより、表TMAP_POPULATIONSを作成します。
# prefix: tmap
populations
    label vc80 /nn
    population num /nn
    mean_income num /nn
    parent_node_id /fk populations

SQLの生成SQLスクリプトを保存レビューおよび実行を順次実施します。表の作成までを実施し、アプリケーションは作成しません。


Treemapを組み込むAPEXのアプリケーションを作成します。

アプリケーション作成ウィザードを起動します。アプリケーションの名前JET Treemapとします。

デフォルトで作成されているホーム・ページを削除し、表TMAP_POPULATIONSをデータソースとした対話グリッドをページとして追加します。


対話グリッドページ名Treemap表またはビューとしてTMAP_POPULATIONSを選択します。編集を許可を選択します。

以上でアプリケーションの作成を実行します。


アプリケーションが作成されます。Treemapはページ番号Treemapのページに実装します。


Breadcrumb BarにあるJET Treemapを削除します。JET Treemapの上でコンテキスト・メニューを表示させ、削除を実行します。


表TMAP_POPULATIONSに初期データを投入するボタンを作成します。

識別ボタン名INITラベルInitとします。対話グリッドTmap Populationsの上に配置します。外観テンプレート・オプションWidthStretchに変更し、画面の横幅いっぱいにボタンを表示させます。

動作アクションはデフォルトのページの送信のまま変更しません。


左ペインでプロセス・ビューを開き、ボタンINITを押した時に実行するプロセスを作成します。

識別名前初期化タイプとしてコードを実行を選択します。ソースPL/SQLコードとして以下を記述します。

declare
l_response clob;
l_nodes json_array_t;
l_node json_object_t;
l_label varchar2(80);
l_population number;
l_mean_income number;
procedure process_node(
p_node in json_object_t
,p_parent_node_id in number
)
as
l_label tmap_populations.label%type;
l_population tmap_populations.population%type;
l_mean_income tmap_populations.mean_income%type;
l_node_id tmap_populations.id%type;
l_nodes json_array_t;
l_node_count pls_integer;
l_node json_object_t;
begin
l_label := p_node.get_string('label');
l_population := p_node.get_number('population');
l_mean_income := p_node.get_number('meanIncome');
insert into tmap_populations(label, population, mean_income, parent_node_id)
values(l_label, l_population, l_mean_income, p_parent_node_id) returning id into l_node_id;
dbms_output.put_line(l_node_id || ',' || l_label || ',' || l_population || ',' || l_mean_income);
l_nodes := p_node.get_array('nodes');
if l_nodes is null then
return;
end if;
l_node_count := l_nodes.get_size();
if not l_node_count > 0 then
return;
end if;
for i in 1..l_node_count
loop
l_node := treat(l_nodes.get(i-1) as json_object_t);
process_node(l_node, l_node_id);
end loop;
end process_node;
begin
delete from tmap_populations;
commit;
execute immediate 'alter table tmap_populations modify id generated by default on null as identity (start with limit value)';
l_response := apex_web_service.make_rest_request(
p_url => 'https://www.oracle.com/webfolder/technetwork/jet/cookbook/dataVisualizations/treeView/resources/usaMeanIncomeSubregion.json'
,p_http_method => 'GET'
);
l_nodes := json_array_t(l_response);
for i in 1..l_nodes.get_size()
loop
l_node := treat(l_nodes.get(i-1) as json_object_t);
process_node(l_node, null);
end loop;
commit;
end;

サーバー側の条件ボタン押下時INITを設定します。


この状態でアプリケーションを実行し、ボタンInitをクリックします。

表TMAP_POPULATIONSにデータが投入され、対話グリッドで編集できるようになります。


Treemapを表示するリージョンを作成します。

識別タイトルTreemapとします。タイプとして静的コンテンツを選択します。ボタンINITと対話グリッドの間にリージョンを配置します。

ソースHTMLコードとして以下を記述します。Oracle JET Cookbookのdemo.htmlとほぼ同じです。

<div id="treemap-container">
<oj-treemap style="width: 100%; height: 600px;"
id="treemap1"
animation-on-display="auto"
animation-on-data-change="auto"
data="[[treemapData]]">
<template slot="nodeTemplate">
<oj-treemap-node
label="[[$current.data.label]]"
value="[[$current.data.population]]"
color="[[getColor($current.data.meanIncome)]]"
short-desc="[[getShortDesc($current.data.label, $current.data.population, $current.data.meanIncome)]]"></oj-treemap-node>
</template>
</oj-treemap>
</div>
view raw treemap.html hosted with ❤ by GitHub

余計な装飾を除くため、外観テンプレートとしてBlank with Attributes (No Grid)を選択します。


TreemapのデータはAjaxコールバックを呼び出して取得します。

プロセス・ビューを開き、Ajaxコールバックにプロセスを作成します。Ajaxコールバックは、JSON形式で表TMAP_POPULATIONSの内容を木構造で返します。

識別名前GET_DATAタイプとしてコードを実行を選択します。ソースPL/SQLコードとして以下を記述します。

declare
function get_child_nodes(
l_node_id in number
)
return json_array_t
as
l_node_count pls_integer;
l_nodes json_array_t;
l_node json_object_t;
l_child_nodes json_array_t;
begin
select count(*) into l_node_count from tmap_populations
where (l_node_id is null and parent_node_id is null) or (parent_node_id = l_node_id);
if l_node_count = 0 then
return null;
end if;
l_nodes := json_array_t;
for r in (
select
id,
json_object(
key 'label' value label,
key 'population' value population,
key 'meanIncome' value mean_income
) as json
from tmap_populations
where (l_node_id is null and parent_node_id is null) or (parent_node_id = l_node_id)
)
loop
l_node := json_object_t(r.json);
l_child_nodes := get_child_nodes(r.id);
if l_child_nodes is not null then
l_node.put('nodes', l_child_nodes);
end if;
l_nodes.append(l_node);
end loop;
return l_nodes;
end get_child_nodes;
begin
htp.p(get_child_nodes(null).to_clob());
end;


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

[require jet]

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

var treemap;

ページ・ロード時に実行に以下を記述します。TreemapのモデルとなるクラスTreemapModelと、Treemapを更新するapex.actionのupdate-treemapを定義しています。内容は概ねOracle JET Cookbookのdemo.jsを踏襲しています。

require(["require", "exports", "knockout", "ojs/ojbootstrap", "ojs/ojarraytreedataprovider", "ojs/ojpalette", "ojs/ojpaletteutils", "ojs/ojknockout", "ojs/ojtreemap"], function (require, exports, ko, ojbootstrap_1, ArrayTreeDataProvider, ojpalette_1, ojpaletteutils_1) {
"use strict";
class TreemapModel {
/*
* Treemapで表示するデータを更新する。
*/
update(nodes) {
this.treemapData(new ArrayTreeDataProvider(nodes, {
keyAttributes: "label",
childrenAttribute: "nodes",
}));
}
constructor() {
/*
* this.treemapDataはknockoutのobservableArrayに変更し、
* データはコンストラクタの外から設定する。
*/
this.treemapData = ko.observableArray();
/*
this.data = JSON.parse(jsonData);
this.treemapData = new ArrayTreeDataProvider(this.data, {
keyAttributes: 'label',
childrenAttribute: 'nodes'
});
*/
this.maxIncome = 70000;
this.minIncome = 35000;
this.colors = (0, ojpalette_1.getColorValuesFromPalette)('viridis');
this.getColor = (meanIncome) => {
return (0, ojpaletteutils_1.getColorValue)(this.colors, (meanIncome - this.minIncome) / (this.maxIncome - this.minIncome));
};
this.getShortDesc = (label, population, meanIncome) => {
return ('&lt;b&gt;' +
label +
'&lt;/b&gt;&lt;br/&gt;Population: ' +
population +
'&lt;br/&gt;Income: ' +
meanIncome);
};
}
}
(0, ojbootstrap_1.whenDocumentReady)().then(() => {
treemap = new TreemapModel();
ko.applyBindings(treemap, document.getElementById('treemap-container'));
/* ページ・ロード時の表示 */
apex.actions.invoke("update-treemap");
});
});
/*
* Treemapを更新する。
*/
apex.actions.add([
{
name: "update-treemap",
action: () => {
apex.server.process ( "GET_DATA", {},
{
success: (data) => {
treemap.update(data);
}
}
);
}
}
]);
CSSファイルURLに以下を指定します。

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


対話グリッドでデータを変更し保存したときに、Treemapを更新する動的アクションを作成します。

識別名前変更の保存とします。タイミングイベントとして保存[対話グリッド]を選択し、選択タイプリージョンリージョンとしてTmap Populationsを指定します。


TRUEアクションとしてJavaScriptコードの実行を選択し、設定コードに以下の1行を記述します。

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


以上でアプリケーションは完成です。アプリケーションを実行すると、記事の先頭のGIF動画のように動作します。

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

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