2025年5月22日木曜日

APEXアプリのページ上でGoogle AI EdgeのMediaPipe LLM Inference APIを呼び出す

GoogleがGoogle AI Edgeのソリューションとして提供しているMediaPipe LLM Inference APIを、Oracle APEXのアプリケーションのページ上で呼び出してみます。

以下のサンプル・アプリケーションに含まれるindex.jsを、Oracle APEXで動くように改変ししました。

MediaPipe LLM Inference task for web
https://github.com/google-ai-edge/mediapipe-samples/tree/main/examples/llm_inference/js

実行環境はmacOSのChromeです。作業としては、ウェブ用LLM推論ガイドのクイックスタートを参照しています。

元々は最近発表されたGemma 3nを動かすつもりだったのですが、Gemma 3nは現時点では.taskファイルのみ公開されているようです。ウェブ用LLM推論ガイドモデル変換に.taskの対応プラットフォームとしてAndroid、iOSはありますが、ウェブがありません。そのため、クイックスタートのとおりにgemma2-2b-it-gpu-int8を使用しました。

サンプル・アプリケーションのindex.htmlは、APEXのアプリケーションのページとして作り直しています。

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


空のAPEXアプリケーションを作成し、機能はすべてホーム・ページに実装します。

APEX向けに書き直したindex.jsは以下になります。

このファイルを静的アプリケーション・ファイルとしてアップロードします。作成した静的アプリケーション・ファイル参照をメモします。


静的アプリケーション・ファイルindex.jsをページに読み込むために、ホーム・ページのページ・プロパティJavaScriptファイルURLに以下を記述します。moduleを指定します。

[module]#APP_FILES#index#MIN#.js


LLMに送信する文字列は、ページ・アイテムP1_INPUTに入力します。タイプテキスト領域を選択します。セッション・ステートデータ型CLOBストレージリクエストごと(メモリーのみ)を設定します。


ボタンSUBMITを作成します。ボタンをクリックしたときに実行されるイベントハンドラは、index.js内に記述されています。APEXアプリとしては何も処理をしないので、動作アクションとして動的アクションで定義を選択します。また、index.js内でこのボタンをsubmitというIDで見つけているので、詳細静的IDsubmitを設定します。


LLMの出力はページ・アイテムP1_OUTPUTに表示します。LLMの出力フォーマットはMarkdownなので、ページ・アイテムのタイプとしてMarkdownエディタを選択します。セッション・ステートデータ型CLOBストレージリクエストごと(メモリーのみ)を設定します。


Oracle APEXでMediaPipe LLM Inference APIを呼び出すサンプルは以上で完成です。

モデル・ファイルのgemma2-2b-it-gpu-int8.binは、kaggle.comからダウンロードしました。


LLMとしては小さなモデルとはいえ、数GBはあります。Oracle APEXの静的アプリケーション・ファイルとして保存するのも避けたかったので、ORDSから直接ファイルとしてダウンロードできる場所に配置しました。

(CDNではなく)ORDSがOracle APEXのイメージ・ファイルを提供するように構成している場合、以下のようにstandalone.static.pathが設定されています。(global/settings.xml内)

<entry key="standalone.static.path">/opt/oracle/apex/images</entry>

このパスの下はURLから見ると/i/以下にあるファイルとして参照されます。そのため、(上記の例では/opt/oracle/apex/images以下に)ディレクトリmodelsを作成し、その下にgemma2-2b-it-gpu-int8.binを置きました。

index.js内ではモデル・ファイルを以下のように指定しています。
const modelFileName = '/i/models/gemma2-2b-it-gpu-int8.bin'; /* Update the file name */
CORSのエラーが発生しないよう、APEXが動作しているホストと同じホストからモデル・ファイルをダウンロードする必要があるようです。また、ダウンロード元がlocalhost以外の場合、https経由でダウンロードする必要があるとの記載もありました。今回の作業ではAPEXはlocalhostでアクセスしているため、両方の条件が必須かどうかは確認できていません。

今回使用したモデルはWebGPUを使用します。Chromeのchrome://flagsを開いて、WebGPUに関する以下の2つの設定を有効にしています。

#enable-unsafe-webgpu、#enable-webgpu-developer-features


今回の記事は以上になります。

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

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

2025年5月21日水曜日

X軸に時間を割り当てたバー・チャートでシリーズの積み上げを行う

Oracle APEXのバー・チャートで、X軸に時間、つまりDATE型(またはTIMESTAMP型およびその派生型)のデータを割り当て、複数のシリーズを積み上げ表示してみます。

色々なチャートの属性があり、その組み合わせで適切に表示できたりできなかったりします。適切な組み合わせとそうでない組み合わせについて、以下より紹介します。

バー・チャートのソースとしてサンプル・データセットのEMP/DEPTに含まれるビューEMP_DEPT_Vを使用します。採用月を時系列としてX軸に割り当て、部門ごとの採用人数をバー・チャートに積み上げ表示します。

意図しているバー・チャートの表示は以下のようになります。


シリーズのソースSQL問合せは以下です。
select dname, mon, sum(cnt) cnt from (
    select dname, trunc(hiredate,'Month') mon, 1 cnt from emp_dept_v
)
group by mon, dname order by mon asc, dname
列のマッピングシリーズ名は部門名である列DNAMEを割り当てます。積み上げはこの部門ごとに行われます。ラベルはX軸となる採用月MONです。時間軸として認識させるため、ラベルに割り当てる列はDATE型またはTIMESTAMP型(WITH TIME ZONEやWITH LOCAL TIME ZONE含む)である必要があります。今回は月ごとに集計するためにTRUNC関数で月単位にHIREDATEを丸めています。月への丸めにTO_CHAR関数を使用すると、VARCHAR2型になるため時間軸として扱うことができなくなるので注意が必要です。には列CNTを指定します。


チャートの属性は以下のように設定しています。

  • シリーズごとに積み上げを行うため外観積上げオンにします。
  • 複数のシリーズ間で時間軸を一致させるために、マルチシリーズ・チャート・データチャート・データのギャップを埋めるオンにします。
  • 時間軸が左から右になるように、ソート順序ラベル - 昇順にします。
  • バー・チャートなのでギャップをゼロとしてレンダリングオンでもオフでも結果は同じです。折れ線グラフの場合、ギャップをゼロとしてレンダリングするとその時刻の値が0になります。オフの場合はその時刻は無視されます。
  • 設定時間軸タイプ有効にします。時間として扱いたい場合は時間軸タイプ有効または混合頻度にします

X軸の日付データの書式は軸X書式で設定します。このラベルの書式を指定するためにソースのSELECT文でTO_CHAR関数を使用すると、時間軸として認識できなくなります。

書式が無指定(- 選択 -)のときは、自動的に書式が決まります。


書式のドロップダウン・リストから事前定義された書式を選択することもできます。また、書式を選択するとパターンを設定ができるようになります。パターンに記述するのはオラクル・データベースの日付書式ではなく、Unicode CLDR(Common Locale Data Repository)による日付フォーマット指定です。パターンを設定すると、選択した書式よりパターンが優先されます。


以上を設定すると、バー・チャートは以下のように表示されます。


隣り合うバーが重なっています。重なり合いを解消するために、初期化JavaScriptファンクションbarGapRatioを設定します。

初期化JavaScriptファンクションに以下を記述します。
function ( options ) {
    options.styleDefaults = options.styleDefaults || {};
    options.styleDefaults.barGapRatio = 0.75;
    return options;
}

以上で想定した通りにバー・チャートが表示されます。

このチャートの設定では、マルチシリーズ・チャート・データソート順序を設定しています。


時間軸でのデータのソートはOracle JETのチャートが行うため、ソースのSELECT文のORDER BY句は必ずも必要はありません。ただし、チャートのソースに関わる設定としてパフォーマンス処理する最大行数があります。

仮に最大行数に10を設定します。


ソースにORDER BYの指定があると、時間軸の最後の方がまとめて表示されません。そのため、最大行数の上限に達していることに容易に気が付きます。


ORDER BY句がないと、どのデータが処理対象外になったのか、一見しただけでは分かりません。


そのため、JETチャートが時間軸でソートするとしても、SQLでもソートしておいた方が安全でしょう。

チャートの設定時間軸タイプで、時間軸として扱われる設定は有効混合頻度間隔のスキップです。ただし、時間軸タイプ間隔のスキップを指定すると、時刻に関係なく等間隔でデータが表示されます。


積み上げチャートの場合、まったく意味のないチャートになります。


シリーズが1つだけのときに、以下のように経過時間に関係なく、データを等間隔で表示する場合に間隔のスキップを使用します。


X軸が時間軸で複数のシリーズがある場合は、マルチシリーズ・チャート・データチャート・データのギャップを埋めるオンにします。

オフの場合(時間軸タイプ間隔のスキップのときと同様に)、以下のような意味のない積み上げチャートになります。


時間軸タイプ混合頻度を選択すると、時間軸がシリーズごとに扱われます。そのため、マルチ・シリーズ・チャート・データチャート・データのギャップを埋めるオフでも、それぞれのシリーズのバーは、その採用月に表示されます。ただし、X軸の時刻の扱いがシリーズごとであるため、時間軸タイプ混合頻度の場合、積み上げはできません。


外観積上げオンにしても、以下の表示は変わらずシリーズは積み上がりません。


マルチシリーズ・チャート・データチャート・データのギャップを埋めるオフにし、ソースのSQLでギャップを埋めてみます。ビューEMP_DEPT_Vの列HIREDATEの最大値と最小値の間で、従業員の採用がない月を0で埋めます。

バインド変数P1_START_DATEおよびP1_END_DATEに、列HIREDATEの最小値および最大値を設定します。

シリーズのソースのSQL問合せとして以下を記述します。
with timeseries_v as (
    select trunc(add_months(:P1_START_DATE, level - 1), 'Month') AS mon
    from dual
    connect by level < months_between(:P1_END_DATE, :P1_START_DATE) + 1
),
dept_v as (
    select dname from emp_dept_v group by dname
),
all_timeseries_v as (
    select dname, mon from timeseries_v, dept_v
)
select dname, mon, case sum(cnt) when 0 then null else sum(cnt) end cnt from
(
    select dname, trunc(hiredate, 'Month') mon, 1 cnt from emp_dept_v
    union all
    select dname, mon, 0 cnt from all_timeseries_v
)
group by mon, dname order by mon asc, dname
ソースのSQLでギャップを埋めているため、マルチシリーズ・チャート・データチャート・データのギャップを埋めるオフでも、シリーズの積み上げができます。


チャートは以下のように表示されます。


何らかの理由でマルチシリーズ・チャート・データチャート・データのギャップを埋めるオンにできない、設定時間軸タイプ混合頻度にできない、といった際に使用を検討できます。

今回の記事は以上になります。

説明に使用したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/new-employees-by-month.zip

余談ですが、Oracle APEXでもOracle JETでも、ChatGPTに聞くと、そこそこ回答してくれます。

質問その1「Oracle JETのチャートのラベルを45度傾ける方法を教えて。」


チャートの属性の初期化JavaScriptファンクションで、xAxis.tickLabel.rotationに45を設定すれば良い、との回答です。

バー・チャートにxAxis.tickLabel.rotationという設定は存在しますが、設定できるのはnoneとautoで数値は設定できません。

質問その2「Orale JETのばー・チャートのバーの太さを変更する方法を教えて。」


チャートの属性の初期化JavaScriptファンクションで、plotArea.barGapRatioを設定すれば良い、との回答です。

barGapRatioは合っているのですが、plotAreaではなくstyleDefaultsです。

質問その3「横軸が時間の場合のステップの指定は?」


xAxisにtickStepという設定はなく、stepがあります。また、stepに設定できるのは数値のみなので、時間軸についてはステップは設定不可の模様です。

正しくない回答を取り上げましたが、それでも、正解は回答の近くにありました。Oracle JETのように誰に何を聞けばいいのかさえ分からないようなライブラリでは「マニュアルのこの辺を当たればいい。」というのが分かるだけでも大幅な時短になります。

質問が大変短いですが、私はChatGPTでたくさんAPEXについて聞いているため、ChatGPTは私の質問がAPEX関連だと判断しているようです。APEXに関する質問が履歴にないと、同じ質問でも回答の精度は大幅に落ちると思います。

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

2025年5月20日火曜日

対話グリッドの新規行を任意の処理で初期化する

対話グリッドで行の追加を実行する際に、ブラウザ上のJavaScriptで計算した値を列の初期値として設定します。対話グリッドののプロパティとしてデフォルトがありますが、この値はサーバー側で設定するため、データベースに保存されている値か、それを元にした値に限定されます。

今回はサンプル・データセットのEMP/DEPTに含まれる表EMPをデータ・ソースとした対話グリッドを作成し、対話グリッド上で選択した従業員の給与の最大値を、新規行の給与として設定します。

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


以下より実装について紹介します。

空のAPEXアプリケーションを作成し、ホーム・ページに対話グリッドのリージョンを作成します。

ソース表名EMPを指定し、ROWIDを含めるオンにします。表EMPの主キー列はEMPNOですが、対話グリッドでは主キーは自動採番されることを想定しているため、主キー列として列EMPNOは選択できません。


ROWIDを含めるオンにすることにより、主キー列としてROWIDが対話グリッドに追加されます。

EMPNOを選択し、ソース問合せのみ主キーの両方をオフにします。また、列EMPNOは実際には主キーなので、検証必須の値オンにします。


対話グリッドで行の追加をできるようにするため、対話グリッドの属性を開き、編集有効オンにします。


選択した行の主キーの値を保持するページ・アイテムとしてP1_SELECTIONを作成します。

属性タイプ非表示が適切ですが、今回はデバッグしやすいようにタイプをテキスト・フィールドにしています。


対話グリッド上で選択した行の主キー値がページ・アイテムP1_SELECTIONに設定されるように、初期化JavaScriptファンクションに以下を記述し、selectionStateItemとしてP1_SELECTIONを割り当てます。
function( config ) {
    config.defaultGridViewOptions = {
        selectionStateItem: "P1_SELECTION"
    };
    return config;
}

以上で対話グリッドで選択した行のROWIDが、: (コロン)区切りでページ・アイテムP1_SELECTIONに設定されます。


最後に対話グリッドの動的アクションを作成します。

タイミングイベントとして行の初期化[対話グリッド]を選択します。


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


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

対話グリッドで行の追加をクリックすると、デフォルトではすべての列が空白になります。サーバー側でデフォルト値を設定できますが、クライアント側のJavaScriptのコーディングにより初期値を設定できると、さらに新規行の入力の手間が減るでしょう。

今回の記事は以上になります。

今回作成したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/set-initial-value-for-new-row-on-interactive-grid.zip

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

2025年5月19日月曜日

円グラフの凡例の選択とレポートの表示を連動させる

チャート・リージョンでは凡例のクリックにより、チャートからその凡例を取り除いたり、再度、チャートに含めたりすることができます。この選択された凡例をレポートの検索条件として、同時にレポートも更新します。

作成するアプリケーションは以下のように動作します。データ・ソースとしてサンプル・データセットEMP/DEPTに含まれる表EMPを使用し、従業員のジョブを凡例に表示し、円グラフにはジョブごとの給与の合計を表示します。


空のAPEXアプリケーションを作成し、ホーム・ページにすべて実装します。

チャート・リージョンのソースSQL問合せとして、以下を記述します。

select job, sal from emp


チャートの属性を開き、タイプにします。また、今回の実装では凡例をクリックするため、凡例表示オンにします。


シリーズ名前Salaryとします。ソース位置リージョン・ソースを選択し、列のマッピングラベルに列JOB値集計合計に列SALを設定します。


チャートの凡例をクリックして非表示に設定した凡例を保存するページ・アイテムをP1_HIDDENとして作成します。タイプ非表示です。JavaScriptのコードから値を設定するため、設定保護された値オフにします。今回の例ではページを送信することはないため違いはありませんが、セッション・ステートストレージリクエストごと(メモリーのみ)にします。


円グラフに連動して従業員を一覧するクラシック・レポートを作成します。

ソース表名EMPWHERE句に以下を記述します。送信するページ・アイテムとしてP1_HIDDENを設定します。
:P1_HIDDEN is null
or
job not in (
    select column_value from apex_string.split(:P1_HIDDEN,',')
)

チャートとレポートを連動させる動的アクションを作成します。

円グラフのチャートのリフレッシュ後JavaScriptコードを実行します。


TRUEアクションとして実行するJavaScriptのコードとして以下を記述します。チャート・ウィジェットに対してonoptionchangeイベントで実行されるハンドラを設定しています。チャート・ウィジェットに対するイベント・ハンドラの設定方法やonoptionchangeイベントについては、Oracle Forumや海外のパートナーのブログ記事で取り上げられているケースがありますが、公式なドキュメントからは見つけることができませんでした。



ページ・アイテムP1_HIDDENの値が変更されたときにクラシック・レポートの一覧を更新するため、ページ・アイテムP1_HIDDEN動的アクションを作成します。

タイミングイベント変更になります。


TRUEアクションリフレッシュ影響を受ける要素選択タイプリージョンとし、リージョンクラシック・レポートのリージョンを選択します。


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

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

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

2025年5月9日金曜日

ピラミッド・チャートのラベルをカスタマイズする

Orale APEXのチャートのタイプピラミッドがあります。ピラミッド・チャートではラベルは列のマッピングに指定した数値が表示され、列のマッピングラベルに指定した文字列は表示されません。

初期化JavaScriptファンクションを記述することにより、ピラミッド・チャートに以下のようにラベルを表示させてみます。


何もカスタマイズしていないと、以下のように表示されます。


以下より、実施したカスタマイズについて紹介します。チャートの表示には、サンプル・データセットEMP/DEPTに含まれる表EMPデータ・ソースとして使用しています。

識別名前Pyramidタイプチャートとしたリージョンを作成しています。ソース表名EMPです。


シリーズの名前Salaryソース位置リージョン・ソースを選択します。列のマッピングラベルは列JOB値集計合計は列SALを指定します。そしてラベルの表示オンにします。

デフォルトではこの設定で、ラベルに列SALの値が表示されます。


チャート・リージョンの属性を開き、チャートタイプピラミッドを選択します。今回の実装には関係しませんが、ツールチップ凡例表示オンにしておきます。

そして本記事のテーマであるラベルをカスタマイズするコードを、初期化JavaScriptファンクションに記述します。



以上でカスタマイズは完了です。

チャートの初期化JavaScriptファンクションで設定しているoptions.dataFilterについては、Plamen Mushkovさんのブログ記事から、その存在を見つけました。

Taking your APEX JET charts style to the next level

dataFilterに渡されるデータを確認したところ、labelにvalueと同じ数値が割当たっていたので、これを変更しています。
{
  "series": [
    {
      "name": "PRESIDENT",
      "displayInLegend": "on",
      "items": [
        {
          "id": "1",
          "value": 5000,
          "label": 5000,
          "name": "PRESIDENT"
        }
      ]
    },
    {
      "name": "MANAGER",
      "displayInLegend": "on",
      "items": [
        {
          "id": "2",
          "value": 8275,
          "label": 8275,
          "name": "MANAGER"
        }
      ]
    },
    ...
 }
デフォルトでは、ラベルに列SALの値が表示されますが、その際に数値に書式が適用されます。


dataFilterに渡されるvalueはフォーマット後の文字列ではなく数値そのものであるため、書式が適用されません。そのため、例えば単位が必要であれば、その処理も初期化JavaScriptファンクションで行う必要があります。

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

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