はじめに
この記事は、Power Apps Advent Calendar 2022 12月22日担当分の記事です。
Power Appsで以下のようなマトリクス形式の表を表示させてみたかったのでやってみました。
こんな感じのマトリクス表をPower Appsで表示させてみました。(ファイルサイズの関係上Gif小さいけど許して)
最初に注意喚起しておくと、この方法ではマトリクス表の列数は10列までという制限があります。
Power Appsでマトリクス表を作成する
前提条件/事前準備
マトリクス表を作成するのに必要なデータ
Power BIのマトリクス表も同じですが、例えば冒頭に例で挙げたようなマトリクス表を作成する場合、データとしては以下のような形式で持っておく必要があります。
チーム名 | 結果 | 値 |
---|---|---|
日本 | 勝点 | 6 |
日本 | 勝 | 2 |
日本 | 引 | 0 |
日本 | 敗 | 1 |
日本 | 得点 | 4 |
日本 | 失点 | 3 |
日本 | 得失点 | +1 |
スペイン | 勝点 | 4 |
スペイン | 勝 | 1 |
スペイン | 引 | 1 |
スペイン | 敗 | 1 |
スペイン | 得点 | 9 |
スペイン | 失点 | 3 |
スペイン | 得失点 | +6 |
ドイツ | 勝点 | 4 |
ドイツ | 勝 | 1 |
ドイツ | 引 | 1 |
ドイツ | 敗 | 1 |
ドイツ | 得点 | 6 |
ドイツ | 失点 | 5 |
ドイツ | 得失点 | +1 |
コスタリカ | 勝点 | 3 |
コスタリカ | 勝 | 1 |
コスタリカ | 引 | 0 |
コスタリカ | 敗 | 2 |
コスタリカ | 得点 | 3 |
コスタリカ | 失点 | 11 |
コスタリカ | 得失点 | -8 |
データはテーブル形式で保持しなくてはならないので上記のようになりますね。
もし元データがマトリクス形式であった場合一旦テーブル形式にPower Queryとかでデータを変換してあげる必要があります。
テストデータの用意
n * mのテストデータを簡単に用意するために以下のようなサンプルデータを用意しました。
自身で試す際に活用していただければと思います。
ClearCollect( col, With( {n: Value(ColumnTxt.Text), m: Value(RowTxt.Text)}, ForAll( Sequence(n * m, 0), { Column: $"Column-{Char(65 + RoundDown(ThisRecord.Value / m, 0))}", Row: $"Row-{Char(65 + Mod(ThisRecord.Value, m))}", Value: Switch( RandBetween(0, 5), 0, Icon.Check, 1, Icon.CheckBadge, 2, Icon.Cancel, 3, Icon.CancelBadge, 4, Icon.ThumbsUp, 5, Icon.ThumbsDown ) } ) ) )
これを実行するとn(列) * m(行)のデータが作成されます。
列ヘッダーには「Column-[添え字]」という列ヘッダーが、行ヘッダーには「Row-[添え字]」という行ヘッダーがそれぞれ作成されます。
[添え字]にはAからはじまるアルファベットがn(もしくはm)番目のアルファベットが付与さえます。
値(Value)にはランダムなアイコンを設定しています。
例えば n * m = 3 * 4
のデータを作成すると以下のようなデータが作成されます。
縦と横にスクロール可能なギャラリーを用意する
今回のマトリクス表を表示するにあたり、データテーブルコントロールのような縦にも横にもスクロール可能な表をギャラリーで表現したいです。
ただし、Power Appsのギャラリーだけではこれを実現することができないので工夫が必要です。
これを実現するために、ギャラリー(垂直ギャラリー)を水平コンテナーの中に入れてあげます。
水平コンテナーを利用することでコンテナー内のアイテムがコンテナの表示領域の外にある場合にスクロールさせて表示させることができるようになります。
今回垂直ギャラリーを利用するので、実施したいスクロールは横方向のスクロールなので「水平方向のオーバーフロー」(LayoutOverflowX
)を「スクロール」(LayoutOverflow.Scroll
)に設定します。
LayoutOverflowX
LayoutOverflow.Scroll
続いてコンテナーの中に設定したギャラリーですがこれはコンテナーの描画領域を超えた幅を設定したいので、「幅(伸縮可能)」(FillPortions
)を「オフ」(0)に設定してあげます。
FillPortions
0
ギャラリーの幅は適当な数値設定しておいてください。
あとで計算値入れます。
* ギャラリーinギャラリーを利用する方法もありますが、これだと子ギャラリーのスクロールが各行(列)とスクロールが連動してくれないので今回の用途では使えないですね。
ギャラリーにマトリクス表を表示させるためにデータを加工する
まず最初に式の全容ですが以下のようになります。
Items
With( { // マトリクスはヘッダー分1行(1列)増える row: CountRows(Distinct(col, Row)) + 1, column: CountRows(Distinct(col, Column)) + 1 }, ForAll( // マトリクスの左上を2回カウントしてしまっているので1減算 Sequence(CountRows(col) + column + row - 1), { Id: ThisRecord.Value, Column: // ヘッダーでない場合のみ If( ThisRecord.Value > row && Mod(ThisRecord.Value, row) <> 1, Index(col, ThisRecord.Value - row - RoundDown((ThisRecord.Value - 1) / row, 0)).Column ), Row: // ヘッダーでない場合のみ If( ThisRecord.Value > row && Mod(ThisRecord.Value, row) <> 1, Index(col, ThisRecord.Value - row - RoundDown((ThisRecord.Value - 1) / row, 0)).Row ), Value: // ヘッダーでない場合のみ If( ThisRecord.Value > row && Mod(ThisRecord.Value, row) <> 1, Index(col, ThisRecord.Value - row - RoundDown((ThisRecord.Value - 1) / row, 0)).Value ), rowHeader: // 行ヘッダー(マトリクスの左上は空白) If( ThisRecord.Value <= row && ThisRecord.Value <> 1, Index(Distinct(col, Row), ThisRecord.Value - 1).Result ), columnHeader: // 列ヘッダー(マトリクスの左上は空白) If( Mod(ThisRecord.Value, row) = 1 && ThisRecord.Value <> 1, Index(Distinct(col, Column), RoundDown((ThisRecord.Value - 1) / row, 0)).Result ) } ) )
↓↓↓↓↓以下で上記式の解説をしていきます。↓↓↓↓↓
ギャラリーで表示される項目数 =
Items
で設定したテーブルのアイテム数
となりますね。
今用意しているデータは行ヘッダー、列ヘッダーは考慮しておらず、値の項目数分しかデータをもっていません。
よって、元データを加工して行ヘッダーと列ヘッダーを追加してあげます。
この際の注意点として大きく2点があります。
- ギャラリーはデータを上から順に表示させるため、そのことを考慮して加工する必要がある
- 左上の要素を考慮してあげる必要がある
「左上の要素を考慮してあげる必要がある」とはここのことです。
例えば3 * 4のマトリクス表であった場合、加工後のデータの数は「[元データの数] + 3(列の数) + 4(行の数) + 1(左上の要素)」となっていなければなりません。
行ヘッダー、列ヘッダーは、それぞれヘッダーに設定したいフィールドから重複を削除してあげれば取得することが可能ですね。
行ヘッダー
Distinct(col, Row)
列ヘッダー
Distinct(col, Column)
これでヘッダー情報が取得できましたので、加工後のアイテム数を式で表すと
CountRows(col) + CountRows(Distinct(col, Column)) + CountRows(Distinct(col, Row)) + 1
となります。
この回数分処理を実行してあげれば欲しいデータを作成できますので、
ForAll( Sequence(CountRows(col) + CountRows(Distinct(col, Column)) + CountRows(Distinct(col, Row)) + 1), /* 処理 */ )
でデータの作成が行えそうですね。
ただし、後の処理で参照を行うために、行ヘッダー、列ヘッダーはそれぞれ左上の要素も行ヘッダー、列ヘッダーとして扱った方が処理がしやすいので左上の要素分 + 1
してあげます。
ただし、行ヘッダー、列ヘッダーでそれぞれ +1
してしまうと、左上の要素が2回登場してしまっているので、処理回数としては -1
しなくてはなりません。
また、行ヘッダーの数、列ヘッダーの数はのちの処理でも利用したいのでWith 関数を利用することとしています。
With( { // マトリクスはヘッダー分1行(1列)増える row: CountRows(Distinct(col, Row)) + 1, column: CountRows(Distinct(col, Column)) + 1 }, ForAll( // マトリクスの左上を2回カウントしてしまっているので1減算 Sequence(CountRows(col) + column + row - 1), /* 処理 */ ) )
次に「処理」とコメントアウトしてある箇所にテーブルのアイテム要素を記載していきます。
作成されるデータは冒頭の例で表現すると
左上の要素 |
[行ヘッダー]勝点 |
[行ヘッダー]勝 |
[行ヘッダー]引 |
[行ヘッダー]敗 |
[行ヘッダー]得点 |
[行ヘッダー]失点 |
[行ヘッダー]得失点 |
[列ヘッダー]日本 |
日本の勝点 |
日本の勝 |
日本の引 |
日本の敗 |
日本の得点 |
日本の失点 |
日本の得失点 |
・・・ |
[列ヘッダー]コスタリカ |
コスタリカの勝点 |
コスタリカの勝 |
コスタリカの引 |
コスタリカの敗 |
コスタリカの得点 |
コスタリカの失点 |
コスタリカの得失点 |
みたいな感じとする必要があります。
このようになるように現在何番目のアイテムの処理なのか?を考えたうえでアイテムを作成する必要があります。
加工後のデータは以下のフィールドを持つようにします。
フィールド名 | 役割 | 備考 |
---|---|---|
Id | 連番 | なくてもいい |
Column | 元データの列要素 | なくてもいい |
Row | 元データの行要素 | なくてもいい |
Value | 元データの値 | |
rowHeader | 行ヘッダー | |
columnHeader | 列ヘッダー |
これらのフィールドはギャラリーで表示/非表示を切り替えるために、現在のアイテムがヘッダーなのか?値なのか?によって空白を入れることとします。
例えば、行ヘッダーのアイテムであれば、 rowHeader
にのみが値が入っていて他は空白、値のアイテムであれば、 元データの要素のみに値が入っていて、他は空白みたいになりますね。
* 連番はすべてのアイテムに含まれます。
このことを踏まえたうえでアイテム作成を処理していきます。
まず最初に作成されるアイテムは左上の要素になりますが、これは特になにもしなくてもよいので連番以外のフィールドはIf 関数で条件を設定することで空白が入る(なにもしない)ようにしています。
{ Id: ThisRecord.Value, Column: If( ThisRecord.Value <> 1, [処理] ), Row: If( ThisRecord.Value <> 1, [処理] ), Value: If( ThisRecord.Value <> 1, [処理] ), rowHeader: If( ThisRecord.Value <> 1, [処理] ), columnHeader: If( ThisRecord.Value <> 1, [処理] ) }
/* 順に説明するために式は最終結果と異なっています。
次に作成する必要があるのは行ヘッダーですね。
行ヘッダーは2番目の処理 ~ 行ヘッダーの数分処理する必要があります。
ここで設定される値は、行ヘッダー要素から重複を削除した値を順に設定していけばよいですね。
よって以下のような式となります。
rowHeader: // 行ヘッダー(マトリクスの左上は空白) If( ThisRecord.Value <= row && ThisRecord.Value <> 1, Index(Distinct(col, Row), ThisRecord.Value - 1).Result )
Index 関数で要素を順に参照していますが、現在何回目の処理か?を表す ThisRecord.Value
は今回でいうと 2 から始まりますので ThisRecord.Value - 1
としています。
このように条件によってIndexで参照すべき数値を今後の処理でも加工してあげる必要があります。
行ヘッダーの次は列ヘッダーの最初の要素ですね。
列ヘッダーは行ヘッダーのように連続でアイテムが作成されるわけではないので注意が必要です。
列ヘッダーはどういう特徴をもっているか?というと必ず1番左の行にて作成されます。
この「1番左の行」とは式で表すとどうなるか考えると
[アイテム番号(連番)] を [マトリクス表の列数] で割ったあまりが 1
で表現できますね。
これはPower Fx式に起こせば条件が設定できそうです。
列ヘッダーに設定される式は行ヘッダー同様、列ヘッダー要素から重複を削除した値を順に設定していけばよいですね。
ただ、行ヘッダーのように連続でアイテムが作成されるわけではないので、参照方法をちょっと考える必要があります。
列ヘッダーは各行ごとに1つ作成されることになるので、現在の処理は何行目の処理か?を考えることで参照ができそうです。
これを式に表すと以下のようになります。
RoundDown((ThisRecord.Value - 1) / row, 0)
行番号をこの式では表したいので、小数以下は不要なので切捨てを行っています。
また、この計算を行うにはアイテム番号は0から始まっていないと計算できないので ThisRecord.Value - 1
をいれています。(実際の数値いれて計算してみればわかるはずです。)
これらをまとめると以下のようになりますね。
columnHeader: // 列ヘッダー(マトリクスの左上は空白) If( Mod(ThisRecord.Value, row) = 1 && ThisRecord.Value <> 1, Index(Distinct(col, Column), RoundDown((ThisRecord.Value - 1) / row, 0)).Result )
最後に値(元データ)です。
これが処理されるのは、
- 左上の要素
- 行ヘッダーの要素
- 列ヘッダーの要素
のいずれでもないときですね。
よって式にすると以下のようになります。
ThisRecord.Value <> 1 && ThisRecord.Value > row && Mod(ThisRecord.Value, row) <> 1
ここで、左上の要素は行ヘッダーであり、また列ヘッダーであるので、行ヘッダーまたは列ヘッダーでない場合とした時点で、左上の要素も省かれます。
よって式を少し簡略化して以下としています。
ThisRecord.Value > row && Mod(ThisRecord.Value, row) <> 1
元データのフィールドに設定すべき値は元データから直接参照すればいいので、あとは何番目を参照するか?が課題ですね。
元データですが、
[アイテム番号(連番)] - [マトリクス表の列数] - [作成した列ヘッダー]
の要素を参照すればいいです。
これを式にすると
ThisRecord.Value - row - RoundDown((ThisRecord.Value - 1) / row, 0)
となりますね。
[作成した列ヘッダー] = [現在処理されている行番号]
とみなしてよいので上記の式となっています。
まとめると以下の式になります。
Column: // ヘッダーでない場合のみ If( ThisRecord.Value > row && Mod(ThisRecord.Value, row) <> 1, Index(col, ThisRecord.Value - row - RoundDown((ThisRecord.Value - 1) / row, 0)).Column ), Row: // ヘッダーでない場合のみ If( ThisRecord.Value > row && Mod(ThisRecord.Value, row) <> 1, Index(col, ThisRecord.Value - row - RoundDown((ThisRecord.Value - 1) / row, 0)).Row ), Value: // ヘッダーでない場合のみ If( ThisRecord.Value > row && Mod(ThisRecord.Value, row) <> 1, Index(col, ThisRecord.Value - row - RoundDown((ThisRecord.Value - 1) / row, 0)).Value )
元データの箇所はフィールドわざわざ用意するのめんどくさければ、
Origin: // ヘッダーでない場合のみ If( ThisRecord.Value > row && Mod(ThisRecord.Value, row) <> 1, Index(col, ThisRecord.Value - row - RoundDown((ThisRecord.Value - 1) / row, 0)) )
みたいな感じでレコードの中にレコード設定する感じでも多分いいですね。
これでギャラリーでマトリクス表を作成するためのデータ加工が完了しました!
ここが難所でした!!!!
データをもとにギャラリーでマトリクス表を作成する
ギャラリーの設定
マトリクス表を作成するにあたり、1行あたりマトリクスの列数分アイテムを表示させていです。
これは、「折り返しの数」(WrapCount)を設定することで実現できますね。
WrapCount
CountRows(Distinct(col, Row)) + 1
ただ、WrapCountは10までしか設定できない点に注意してください。
これが最初で注意喚起した制限事項に引っかかってきます。
列ヘッダーも考慮すると、行ヘッダーにて設定できるのは9種類までということになりますね。
続いてギャラリーの幅です。
ギャラリーの幅はマトリクス表の列数によって可変にしたいです。
よって以下のように設定します。
Width
CountRows(Distinct(col, Row)) * 200
「テンプレートのサイズ」(TemplateSize
)と「テンプレートのパディング」(TemplatePadding
)はお好きな数を設定してください。
参考までに、子要素のサイズは
幅 | 高さ |
---|---|
ギャラリーの幅 / 折り返しの数 | TemplateSize |
が適用されます。
(正確にいうと、TemplatePadding
分サイズ変わりますが。)
子要素の設定
子要素のヘッダー、値は描画するアイテムがヘッダーなのか?値なのか?で表示/非表示を分けます。
これはフィールドが空かどうか?を確認すればいいですね。
やり方としては、自身のテキストが空か確認する方法と、それに設定されるべき値が空かどうかで確認する2種があります。
Visible
!IsBlank(Self.Text)
!IsBlank(ThisItem.Value)
あとの要素は実際の画面みながら好きなデザインや設定を行っていくだけです。
区切り線
私が例で出した区切り線は見栄えをちょっとよくするために、表示条件を切り替えています。
普通に表示させたら、行や列の最後にも線引かれちゃいますがデザイン的に引きたくないなーと思ったので制御を加えました。
ここは説明なしで式だけ記載します。(これまでの式理解していたらわかるはず...!!)
行の区切り線
Visible
Mod(ThisItem.Id, CountRows(Distinct(col, Row)) + 1) <> 0
行の区切り線はアイテムの右端にくるように設定しています。
X
Parent.TemplateWidth - Self.Width
列の区切り線
Visible
RoundDown((ThisItem.Id - 1) / (CountRows(Distinct(col, Row)) + 1), 0) < CountRows(Distinct(col, Column))
列の区切り線はアイテムの下端にくるように設定しています。
X
Parent.TemplateHeight - Self.Height
おわりに
以上、Power Appsでマトリクス表を表示する方法の1例でした。
OnSelect
やどのアイテムが選択されたかの判断を別に行う必要がなく、ただ表示が行えればいい。
というのであれば、
- htmlで表現する
- SVGで表現する
- Power BIでマトリクス表を表示させたレポート埋め込む
等のやり方もあるかと思います。
冒頭でも触れましたが、このやり方ではマトリクス表の列は10列までという制限がある点にご注意ください。
めんどくさくなって途中説明雑になったところもありますが、不明点やもっとこうしたほうがいいのでは?などありましたらお気軽にどうぞー
<追記>
Items
に設定している式ですが、ここの内容を一部取り入れるとより式が改善できます。
また、10行以上表示させたい場合もこちらの方法が利用できます。