##はじめに
Analytical List Page(ALP)はデータを可視化し、ドリルダウンやトランザクションデータへのナビゲーションなどの機能を提供します。
この記事では、家計の支出を可視化するためのレポートを想定して作ってみます。
CDSのアノテーションやアプリ側での設定がどのような役割を果たしているのか理解するため、段階を踏んで少しずつ完成させてきたいと思います。
<やること>
・チャートとテーブルを出力
・Visual Filterを出力
・KPIを出力
・Visual Filterのデフォルト値を設定
##ゴール
以下のようなAnalytical List Pageを作成します。
##開発環境
- フロントエンド:SAP WebIDE Full-Stack
- バックエンド: S/4HANA 1909 (FPS01)
##データモデル
データモデルは以下のように簡単な構成です。ベースに支出を記録するためのテーブル(ZMYEXPENSES)と、支出カテゴリのマスタ(ZEXCATEGORYTEXT)があります。ALPで使用するビューはクエリブラウザで使うのと同じ、分析用のビューです。トップのビューは@Analytics.query: true
のアノテーション、その下のベースになるビューには@Analytics.dataCategory: #CUBE
(または用途によりFACT, DIMENSION, HIERARCHY)のアノテーションをつけます。
ZMYEXPENSES
データは以下のような形で入っています。
ZEXCATEGORYTEXT
簡単にするために、カテゴリは3種類のみとします。
##ステップ
- CDSビューの作成
- チャートとテーブルを出力
- Visual Filterを出力
- KPIを出力
- Visual Filterのデフォルト値を設定
###1. CDSビューの作成
####支出カテゴリのビュー:ZI_Category_Text
支出カテゴリ取得用のビューを作成します。
@AbapCatalog.sqlViewName: 'ZICATTEXT'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Category Text'
@ObjectModel.dataCategory: #TEXT
@ObjectModel.representativeKey: 'category'
define view ZI_Category_Text as select from zexcategorytext {
key category,
@Semantics.language: true
key lanugage,
@Semantics.text: true
text
}
####集計用のCube View:ZI_MyExpenses_Cube
集計用のCube Viewを作成します。I_CalendarDateは標準のビューで、日付を年、年月などに変換することができます。ALPのフィルタで年や年月を選択できるようにしたいので、I_CalendarDateとAssociationを持たせています。
@AbapCatalog.sqlViewName: 'ZIMYEXPENSES'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'My Expences Cube'
@Analytics.dataCategory: #CUBE
define view ZI_MYEXPENSES_CUBE as select from zmyexpenses
association [0..1] to ZI_Category_Text as _Text
on $projection.Cateogry = _Text.category
association [0..1] to I_CalendarDate as _CalendarDate
on $projection.PostingDate = _CalendarDate.CalendarDate
{
@ObjectModel.foreignKey.association: '_CalendarDate'
posting_date as PostingDate,
@ObjectModel.text.association: '_Text'
category as Cateogry,
@Semantics.amount.currencyCode: 'Currency'
@DefaultAggregation: #SUM
amount as Amount,
@Semantics.currencyCode: true
currency as Currency,
_CalendarDate._CalendarYear.CalendarYear,
_CalendarDate._YearMonth.YearMonth,
_Text,
_CalendarDate
}
ALPで使用するために必要なアノテーションは以下の部分です。
分析用のビューにするために、@Analytics.dataCategory
を設定します。トランザクションデータ(支出)とマスタデータ(支出カテゴリ)を合わせたビューなので、カテゴリは#CUBE
です。
@Analytics.dataCategory: #CUBE
分析用のビューは集計対象の項目(measure)を持つ必要があります。@DefaultAggregation
をつけた項目が集計対象になります。
@DefaultAggregation: #SUM
amount as Amount,
####Consumption View:ZC_MyExpenses
最後に、ODataのもとになる一番上のビューを作成します。
@AbapCatalog.sqlViewName: 'ZCMYEXPENSES'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'My Expences Query'
@Analytics.query: true
@OData.publish: true
@Metadata.allowExtensions: true
define view ZC_MyExpenses
as select from ZI_MYEXPENSES_CUBE {
PostingDate,
@AnalyticsDetails.query.display: #KEY_TEXT
Cateogry,
Amount,
Currency,
@Semantics.calendar.year: true
CalendarYear,
@Semantics.calendar.yearMonth: true
YearMonth
}
ここでも重要なのは@Analytics.query: true
というアノテーションです。このビューにはキーがないのですが@Analytics.query: true
とするとキーなしでも許容されます。
もう1点重要なのが以下のアノテーションです。
@Semantics.calendar.year: true
CalendarYear,
@Semantics.calendar.yearMonth: true
YearMonth
CalendarYear(年)、YearMonth(年月)はODataの型がedm.stringになります。そのままだと時間に関連する項目として認識されないので、@Semantics.calendar
をつける必要があります。(参考:Chart Cards Used in Overview Pages>Time Series Chart)
###2. チャートとテーブルを出力
つづいて、チャートとテーブルを出力するための最低限のアノテーションをつけて、アプリを作成してみましょう。
ZC_MyExpensesのMetadata Extensionを作成します。まずは選択項目(selectionField)とテーブルの列項目(lineItem)を定義します。
@Metadata.layer: #CORE
annotate view ZC_MyExpenses
with
{
@UI:{
lineItem: [{ position: 10 }]
}
PostingDate;
@UI:{
selectionField: [{position: 10 }],
lineItem: [{ position: 20 }]
}
Cateogry;
@UI:{
lineItem: [{ position: 30 }]
}
Amount;
@UI:{
selectionField: [{position: 20 }]
}
CalendarYear;
@UI:{
selectionField: [{position: 30 }]
}
YearMonth;
}
続いて、チャートを表示するためのアノテーションを設定します。annotate view ZC_MyExpenses...の上に以下を追加します。
これはアプリを開いたときの表示用のバリアント(presentationVariant)と選択用のバリアント(selectionVariant)を決めるアノテーションです。それぞれ'Default'というqualifier(ID)を指定しています。
@UI.selectionPresentationVariant: [{
qualifier: 'Default',
presentationVariantQualifier: 'Default',
selectionVariantQualifier: 'Default'
}]
実際のバリアントの中身を以下で定義します。
presentationVariantにはデータの見せ方、すなわちチャートして表示する(type: #AS_CHART)ということと、チャート定義への参照(qualifier)を指定します。
selectionVariantにはデフォルトのフィルタ定義を指定できますが、ここでは特に何も指定しません。
@UI.presentationVariant: [{
qualifier: 'Default',
visualizations: [{
type: #AS_CHART,
qualifier: 'ChartDefault'
}]
}]
@UI.selectionVariant: [{
qualifier: 'Default',
text: 'Default'
}]
最後に、チャートの定義です。年月ごと、支出カテゴリごとの積み上げ棒グラフを作ります。
@UI.chart: [{
qualifier: 'ChartDefault',
chartType: #COLUMN_STACKED,
dimensions: ['Cateogry', 'YearMonth'],
measures: ['Amount'],
dimensionAttributes: [{
dimension: 'Cateogry',
role: #SERIES
},{
dimension: 'YearMonth',
role: #CATEGORY
}],
measureAttributes: [{
measure: 'Amount',
role: #AXIS_1
}]
}]
項目の意味は以下のとおりです。
項目 | 説明 |
---|---|
qualifier | チャートのID。presentationVariantで指定したqualifierと同じ |
chartType | チャートの種類。#COLUM_STACKEDは積み上げ棒グラフ |
dimensions | 分析軸を指定する。チャートの種類により複数指定可能 |
measures | 分析対象の数値項目を指定する。チャートの種類により複数指定可能 |
dimensionAttributes | 分析軸それぞれについてrole(用途)を指定する(※) |
measureAttributes | 数値項目それぞれについてrole(用途)を指定する。何番目の軸かを指定する |
※dimentionのroleの意味 |
- category: 棒グラフでいうところのX軸になるdimensionに対して設定する
- series: 同じcategoryの中で分類(色)を分けたい場合に使用する
- category2: heatmapなどで使われる第二の軸
####ALPを作成
WebIDEでALPを作成します。
ODataは公開しておきましょう。(Tr: /IWFND/MAINT_SERVICE)
Analytical List Pageのテンプレートを選択します。
今回作成したODataを選択します。
アノテーションを全て選択します。
Qualifierにステップ2で設定したselectionPresentationVariantのqualifierを指定します。
この時点でmanifest.jsonのpagesセクションの中は以下のようになっています。
"pages": {
"AnalyticalListPage|ZC_MYEXPENSES": {
"entitySet": "ZC_MYEXPENSES",
"component": {
"name": "sap.suite.ui.generic.template.AnalyticalListPage",
"list": true,
"settings": {
"condensedTableLayout": true,
"showGoButtonOnFilterBar": true,
"tableType": "ResponsiveTable",
"multiSelect": false,
"qualifier": "Default",
"autoHide": true,
"smartVariantManagement": true,
"keyPerformanceIndicators": {}
}
},
"pages": {
"ObjectPage|ZC_MYEXPENSES": {
"entitySet": "ZC_MYEXPENSES",
"component": {
"name": "sap.suite.ui.generic.template.ObjectPage"
}
}
}
}
}
####実行結果
立ち上げた時点では何も表示されません。
Goを押すとチャート、テーブルが表示されました。
右上のCompact Filterのボタンを押すと、selectionFieldで定義したフィルタ項目が表示されます。
ただ、年月のグラフなのに年月の順になっていません。
####調整
2点調整を入れます。
- グラフが年月順に並ぶようにする
- アプリを立ち上げた時点でロードされるようにする
Metadata Extensionファイルを更新したら、CDSビューの有効化も忘れずに!
グラフが年月順に並ぶようにする
presentationVariantにsortOrderを追加します。YearMonthの昇順、PostingDateの降順に並ぶようにします。PostingDateを入れているのは、このソート順がテーブルの見え方にも影響するので、最新のレコードが上に来るようにするためです。
@UI.presentationVariant: [{
qualifier: 'Default',
sortOrder: [{
by: 'PostingDate',
direction: #DESC
},{
by: 'YearMonth',
direction: #ASC
}],
visualizations: [{
type: #AS_CHART,
qualifier: 'ChartDefault'
}]
}]
また、YearMonthをlineItemにも追加します。sortOrderに指定した項目がlineItemに含まれていないとエラーになってしまうためです。
@UI:{
selectionField: [{position: 30 }],
lineItem: [ { position: 40 } ]
}
YearMonth;
アプリを立ち上げた時点でロードされるようにする
アプリを立ち上げた時点でロードするには、manifest.jsonのsettingsの中にdataLoadSettingsを追加します。loadDataOnAppLaunchには"always"/"never"/"ifAnyFilterExist"の3種類あり、本当はifAnyFilterExistにしたいですが、今はデフォルトフィルタの設定がないので"always"を選択します。
"settings": {
"condensedTableLayout": true,
"showGoButtonOnFilterBar": true,
"tableType": "ResponsiveTable",
"multiSelect": false,
"qualifier": "Default",
"autoHide": true,
"smartVariantManagement": true,
"keyPerformanceIndicators": {},
"dataLoadSettings": {
"loadDataOnAppLaunch": "always"
}
この結果、実行するとすぐにグラフが表示され、年月の昇順に並ぶようになりました。
###3. Visual Filterを出力
ここでは、3つのVisual Filterを作成します。
Visual Filterでサポートされる3つのチャートタイプを使って、以下のフィルタを作成します。それぞれカテゴリ、年度、年月を指定するためのフィルタになります。
- カテゴリごとの支出:円グラフ
- 年度ごとの支出:棒グラフ
- 年月ごとの支出:折線グラフ
####アノテーションを追加
presentationVariantを3つ追加します。
@UI.presentationVariant: [{...
},{
qualifier: 'FilterCategory',
visualizations: [{
type: #AS_CHART,
qualifier: 'ChartCategory'
}]
},{
qualifier: 'FilterYear',
visualizations: [{
type: #AS_CHART,
qualifier: 'ChartYear'
}]
},{
qualifier: 'FilterYearMonth',
visualizations: [{
type: #AS_CHART,
qualifier: 'ChartYearMonth'
}]
}]
chartアノテーションを追加します。最初に作ったチャートと同じ要領で、chartTypeを変えるだけです。
@UI.chart: [{...
},{
qualifier: 'ChartCategory',
chartType: #DONUT,
dimensions: ['Cateogry'],
measures: ['Amount'],
dimensionAttributes: [{
dimension: 'Cateogry',
role: #CATEGORY
}],
measureAttributes: [{
measure: 'Amount',
role: #AXIS_1
}]
},{
qualifier: 'ChartYear',
chartType: #BAR,
dimensions: ['CalendarYear'],
measures: ['Amount'],
dimensionAttributes: [{
dimension: 'CalendarYear',
role: #CATEGORY
}],
measureAttributes: [{
measure: 'Amount',
role: #AXIS_1
}]
},{
qualifier: 'ChartYearMonth',
chartType: #LINE,
dimensions: ['YearMonth'],
measures: ['Amount'],
dimensionAttributes: [{
dimension: 'YearMonth',
role: #CATEGORY
}],
measureAttributes: [{
measure: 'Amount',
role: #AXIS_1
}]
}]
####ローカルアノテーションを設定
VisualFilterを表示するためにはフィルタ項目とpresentationVariantを紐づける必要があり、それをローカルアノテーションで設定します。
annotations.xmlファイルを開き、SelectTargetsをクリックします。
フィルタしたい項目を選択します。
CategoryのLocal Anntationsを開きます。
ValueListを選択します。
CollectionPathにEntitySet名を入力します。
Common.ValueListの行の+ボタンを開き、PresentationVariantQualifierを選択します。
qualifierを入力します。
他の2つも同様に設定します。
####実行結果
Visual Filterが表示されました。フィルタを選択すると、選択した値で中央のチャートが絞り込まれます。
###4. KPIを出力
KPIとはレポートの一番上に出るふせんのようなもので、重要な数字とその評価(色で表現)が表示されます。
####アノテーションを追加
selectionPresentationVariantを追加します。
@UI.selectionPresentationVariant: [{...},{
qualifier: 'KPITotalExpense',
presentationVariantQualifier: 'KPITotalExpense',
selectionVariantQualifier: 'KPITotalExpense'
}]
selectionVariantを追加します。中身は特にありません。
@UI.selectionVariant: [{...
},{
qualifier: 'KPITotalExpense',
text: 'Default'
}]
presentationVariantを追加します。visualizationsが二つあり、一つがchartを指し、もう一つがdataPointを指します。dataPointはKPIでどの数字を表示するかや、評価(criticality)をどうするのかを定義します。
@UI.presentationVariant: [{...
},{
qualifier: 'KPITotalExpense',
visualizations: [{
type: #AS_CHART,
qualifier: 'ChartTotalExpensePerCategory'
},{
type: #AS_DATAPOINT,
qualifier: 'Amount'
}]
}]
chartを以下のように定義します。CategoryのVisualFilterで指定したものと同じです。
@UI.chart: [{...
},{
qualifier: 'ChartTotalExpensePerCategory',
chartType: #DONUT,
dimensions: ['Cateogry'],
measures: ['Amount'],
dimensionAttributes: [{
dimension: 'Cateogry',
role: #CATEGORY
}],
measureAttributes: [{
measure: 'Amount',
role: #AXIS_1
}]
}]
Amountに対してdataPointアノテーションをつけます。
@UI:{
lineItem: [{ position: 30 }],
dataPoint: {
criticalityCalculation: {
improvementDirection: #MINIMIZE,
toleranceRangeHighValue: 100000,
deviationRangeHighValue: 150000
}
}
}
Amount;
criticalityCalculationはmeasure(今回はAmount)の値によってKPIの色を変えるための設定です。
項目 | 説明 |
---|---|
improvementDirection | measureの値がどこに向かうのが望ましいかを指定する #TARGET: 目標とする値があり、それを上回っても下回ってもよくない #MINIMIZE: 値が小さいほうがよい #MAXIMIZE: 値が大きいほうがよい |
toleranceRangeHighValue | #MINIMIZEの場合、これを超えたら警告(黄色)とする閾値を定義する |
deviationRangeHighValue | #MINIMIZEの場合、これを超えたら危険(赤色)とする閾値を定義する |
Create KPIs with data points and criticality calculation in a SAP Fiori Overview Pageより引用
toleranceRangeHighValue、deviationRangeHighValueの意味合いはimprovementDirectionによって変わります。上記ブログの説明がわかりやすいです。
####manifest.jsonの設定
KPI用のモデルを追加します。見る先は同じODataサービスなのですが、なぜか名前を指定する必要があります。
keyPerformanceIndicatorsの設定を追加します。modelに上記で作成したモデル名、qualifierにselectionPresentationVariantのqualifierを指定します。
"keyPerformanceIndicators": {
"TotalExpense": {
"model": "kpi",
"entitySet": "ZC_MYEXPENSES",
"qualifier": "KPITotalExpense"
}
},
####実行結果
KPIは表示されますが文字が切れてしまっています。
KPIをクリックすると、グラフはちゃんと表示されました。
KPIを複数表示すると切れずに表示されます。同じものを2つ並べただけですが。
同じ事象がSAP Communityに質問として上がっています。
Analytical List Page : Global KPI Tag not displaying proper value
質問者の方に聞いたところ、Fiori LaunchpadにデプロイしたらKPI一つでもちゃんと表示されたそうです。WebIDE固有の問題のようでした。
####実行時の年のデータに絞る
家計簿としては過去全部の合計が出ていても意味がないので、実行時の年で絞ってKPIを出したいと思います。KPIのフィルタ条件はselectionVariantで指定します。残念ながらここに動的な値を設定する方法がわからなかったので、固定値で'2020'を指定しています。
ローカルでアノテーションを開き、External Annotationsの下にあるUI.SelectionVariant#をコピーします。(画像は既にコピー済の状態)
以下のようにローカルアノテーションを指定します。
コードは以下のようになります。
<Annotation Term="UI.SelectionVariant" Qualifier="KPITotalExpense">
<Record Type="UI.SelectionVariantType">
<PropertyValue Property="Text" String="Default"/>
<PropertyValue Property="SelectOptions">
<Collection>
<Record Type="UI.SelectOptionType">
<PropertyValue Property="PropertyName" PropertyPath="CalendarYear"/>
<PropertyValue Property="Ranges">
<Collection>
<Record Type="UI.SelectionRangeType">
<PropertyValue Property="Sign" EnumMember="UI.SelectionRangeSignType/I"/>
<PropertyValue Property="Option" EnumMember="UI.SelectionRangeOptionType/EQ"/>
<PropertyValue Property="Low" String="2020"/>
</Record>
</Collection>
</PropertyValue>
</Record>
</Collection>
</PropertyValue>
</Record>
</Annotation>
ほぼ2020年のデータなので違いがわかりにくいですが、グラフの上に2020と表示されています。
###5. Visual Filterのデフォルト値を設定
続いて、メインのチャートとテーブルに対して実行日の年でデフォルトのフィルタがかかるようにします。以下のブログを参考にコントローラーの拡張を追加します。
SAP Fiori Elements – Initial Filter values
onInitSmartFilterBarExtension: function (oEvent) {
var sFilterBarId = "demo.myexpenses::sap.suite.ui.generic.template.AnalyticalListPage.view.AnalyticalListPage::ZC_MYEXPENSES--template::SmartFilterBar";
var oFilterBar = this.getView().byId(sFilterBarId);
var sYear = new Date().getFullYear().toString();
var oDefaultFilter = {
"CalendarYear": {
"items": [
{ "key": sYear, "text": sYear }
]
}
}
oFilterBar.setFilterData(oDefaultFilter);
}
##おわりに
ALPを初めて作ってみての感想として、大枠はわりと簡単にできるのですが、細かいところ(デフォルトのフィルタ、Criticalityの設定など)がなかなか思うようにいかず苦労しました。今回は作りたいものを想定してどう実現するか調べていったので、ALPでできることや制約に気付けたのがよかったです。
##参考
ブログ
- Create an Analytical List Page using ABAP CDS views and annotations
- Create an analytical model based on ABAP CDS views
- Create KPIs with data points and criticality calculation in a SAP Fiori Overview Page
- Configuring filters area of an Analytical List Page (ALP) application
- SAP Fiori Elements – Initial Filter values
ドキュメント
##Appendix
Metadata Extensionの全体
@Metadata.layer: #CORE
@UI.selectionPresentationVariant: [{
qualifier: 'Default',
presentationVariantQualifier: 'Default',
selectionVariantQualifier: 'Default'
},{
qualifier: 'KPITotalExpense',
presentationVariantQualifier: 'KPITotalExpense',
selectionVariantQualifier: 'KPITotalExpense'
}]
@UI.presentationVariant: [{
qualifier: 'Default',
sortOrder: [{
by: 'PostingDate',
direction: #DESC
},{
by: 'YearMonth',
direction: #ASC
}],
visualizations: [{
type: #AS_CHART,
qualifier: 'ChartDefault'
}]
},{
qualifier: 'FilterCategory',
text: 'Filter: Categories',
visualizations: [{
type: #AS_CHART,
qualifier: 'ChartCategory'
}]
},{
qualifier: 'FilterYear',
visualizations: [{
type: #AS_CHART,
qualifier: 'ChartYear'
}]
},{
qualifier: 'FilterYearMonth',
text: 'Filter: Year Month',
visualizations: [{
type: #AS_CHART,
qualifier: 'ChartYearMonth'
}]
},{
qualifier: 'KPITotalExpense',
visualizations: [{
type: #AS_CHART,
qualifier: 'ChartTotalExpensePerCategory'
},{
type: #AS_DATAPOINT,
qualifier: 'Amount'
}]
}]
@UI.selectionVariant: [{
qualifier: 'Default',
text: 'Default'
},{
qualifier: 'KPITotalExpense',
text: 'Default'
}]
@UI.chart: [{
qualifier: 'ChartDefault',
chartType: #COLUMN_STACKED,
dimensions: ['Cateogry', 'YearMonth'],
measures: ['Amount'],
dimensionAttributes: [{
dimension: 'Cateogry',
role: #SERIES
},{
dimension: 'YearMonth',
role: #CATEGORY
}],
measureAttributes: [{
measure: 'Amount',
role: #AXIS_1
}]
},{
qualifier: 'ChartCategory',
chartType: #DONUT,
dimensions: ['Cateogry'],
measures: ['Amount'],
dimensionAttributes: [{
dimension: 'Cateogry',
role: #CATEGORY
}],
measureAttributes: [{
measure: 'Amount',
role: #AXIS_1
}]
},{
qualifier: 'ChartYearMonth',
chartType: #LINE,
dimensions: ['YearMonth'],
measures: ['Amount'],
dimensionAttributes: [{
dimension: 'YearMonth',
role: #CATEGORY
}],
measureAttributes: [{
measure: 'Amount',
role: #AXIS_1
}]
},{
qualifier: 'ChartYear',
chartType: #BAR,
dimensions: ['CalendarYear'],
measures: ['Amount'],
dimensionAttributes: [{
dimension: 'CalendarYear',
role: #CATEGORY
}],
measureAttributes: [{
measure: 'Amount',
role: #AXIS_1,
asDataPoint: true
}]
},{
qualifier: 'ChartTotalExpensePerCategory',
chartType: #DONUT,
dimensions: ['Cateogry'],
measures: ['Amount'],
dimensionAttributes: [{
dimension: 'Cateogry',
role: #CATEGORY
}],
measureAttributes: [{
measure: 'Amount',
role: #AXIS_1
}]
}]
annotate view ZC_MyExpenses with
{
@UI:{
lineItem: [{ position: 10 }]
}
PostingDate;
@UI:{
selectionField: [{position: 10 }],
lineItem: [{ position: 20 }]
}
Cateogry;
@UI:{
lineItem: [{ position: 30 }],
dataPoint: {
criticalityCalculation: {
improvementDirection: #MINIMIZE,
toleranceRangeHighValue: 100000,
deviationRangeHighValue: 150000
}
}
}
Amount;
@UI:{
selectionField: [{position: 20 }]
}
CalendarYear;
@UI:{
selectionField: [{position: 30 }],
lineItem: [ { position: 40 } ]
}
YearMonth;
}