Power BI カスタムビジュアル開発 : Capabilities の DataViewMappings を詳しく見る

[前回](https://qiita.com/kenakamu/private/46ae6e419c49284c75ed) の記事は Data Roles について見ていきました。今回は DataViewMappings を見ていきます。

情報元: [GitHub の DataViewMapping の説明](https://github.com/Microsoft/PowerBI-visuals/blob/master/Capabilities/DataViewMappings.md) 

# `DataViewMappings` : データロールとデータのマッピング定義

DataViewMapping はデータロールに対するデータのマッピングを設定することが出来ます。

多くのビジュアルは 1 だけマッピングを定義していますが、複数の dataViewMappings を定義することも可能です。

```json
"dataViewMappings": [
    {
        "conditions": [ ... ],
        "categorical": { ... },
        "table": { ... },
        "single": { ... },
        "matrix": { ... }
    }
]
```

# conditions

現時点では最小および最大値の設定が出来ます。これはデータロールに指定できるフィールド数を限定します。
 
> メモ: もし DataViewRole で conditions が省略された場合は、任意の数のフィールドを配置できます。制限以上の数のフィールドを配置しようとしても配置できなくなります。

## condition の例 1

既定では各ロールに任意の数のフィールドを配置できます。以下の例ではカテゴリは 1 フィールド、メジャーは 2 フィールドまで配置できるように制限します。

```json
"conditions": [
    { "myCategory": { "max": 1 }, "y": { "max": 2 } },
]
```

## condition の例 2

以下の例では 2 つのうち、どちらかの制約を満たす必要があります。

- カテゴリが 1 つでメジャーが 2 つ
- カテゴリが 2 つでメジャーが 1 つ

```json
"conditions": [
    { "myCategory": { "min": 1, "max": 1 }, "myMeasure": { "min": 2, "max": 2 } },
    { "myCategory": { "min": 2, "max": 2 }, "myMeasure": { "min": 1, "max": 1 } }
]
```

# single

single データマッピングは 1 つのメージャーだけ受け取り、合計を返します。フィールドが数値の場合は合計を、他の場合はユニークな値の数を返します。既定のコントロールでは「カード」のイメージです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/ea8621d0-a418-cb9e-da4a-4b4040dba6fc.png)

定義では、single に対応するデータロール名を指定します。データロールに 2 つ以上のフィールドが配置された場合、値を何も返しません。よって condition と組み合わせて使います。

> **メモ**: single データマッピングは他のマッピング併用できません。

## single の例

以下の例は myMeasure データロールに対して 1 つしかフィールドを配置できないよう制約を設定し、かつ single を指定してます。

```json
"dataViewMappings": {
    "conditions": [
        { "myMeasure ": { "max": 1 } }
    ],
    "single": {
        "role": "myMeasure "
    }
}  
```

結果として生成される dataView は、他のプロパティ定義はあるものの single プロパティだけが値を持つため、single 配下の value のみ取得します。

```typescript
// single データを表示
this.area.selectAll("p")
    .data(options.dataViews)
    .enter()
    .append('p')
    .text((d) => { return d.single.value.toString(); });
```

# categorical

categorical データマッピングはカテゴリとメジャーのデータロールに対してグルーピングを指定します。カテゴリでグルーピングするだけでなく、任意のデータロールでグルーピングを指定することも出来ます。

categorical ではデータロールに対して以下の構文を利用できます。

- **for...in** - データロールに設定されているフィールドすべての処理
- **bind...to** - 内容は for...in と同じだが、データロールに 1 しかフィールドを配置しない想定

尚、こちらは具体的なデータがあると分かりやすいので、今回は毎月ごとの4地域の売りあげデータを使うことを想定してみます。以下の様に Excel で適当に作った 2018/1/1 から 2018/12/31 まで 4 地域での売り上げデータです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/b84e6fa8-a0c2-8c66-adfd-05c6bf895344.png)

## categorical の例 1

以下の定義では、カテゴリーに配置した各フィールドごとに、値をグルーピングします。


```json:capabilities.json
{
    "dataRoles": [
        {
            "displayName": "サンプルカテゴリ",
            "name": "myCategory",
            "kind": "Grouping"
        },
        {
            "displayName": "サンプルメジャー",
            "name": "myMeasure",
            "kind": "Measure"
        }    
    ],
    "dataViewMappings": [
        {
            "categorical": {
                "categories": {
                    "for": { "in": "myCategory" }
                },
                "values": {
                    "select": [
                        { "bind": { "to": "myMeasure" } }
                    ]
                }
            }        
        }
    ]    
}
```

### 動作確認

検証には以下の Visual を使います。

```typescript:visual.ts
module powerbi.extensibility.visual {
    "use strict";

    export class Visual implements IVisual {

        private area: d3.Selection<HTMLElement>
        private table: d3.Selection<HTMLElement>
        private thead: d3.Selection<HTMLElement>
        private tbody: d3.Selection<HTMLElement>
        private host: IVisualHost;
        private settings: VisualSettings;

        constructor(options: VisualConstructorOptions) {
            // カスタムビジュアルを配置しているホストの情報を取得
            this.host = options.host;
            // カスタムビジュアルのエリアを取得
            this.area = d3.select(options.element);
            this.table = this.area.append("table");
            this.thead = this.table.append("thead").append("tr");
            this.tbody = this.table.append("tbody").append("tr");
        }

        public update(options: VisualUpdateOptions) {
            debugger;

            this.thead.selectAll("th").remove();
            this.tbody.selectAll("tr").remove();

            let combinedCategories = [];

            options.dataViews[0].categorical.categories.forEach((category) => {
                category.values.forEach((value, index) => {
                    combinedCategories[index] =
                        combinedCategories[index] == undefined ?
                            value : combinedCategories[index] + value + " ";
                })
            });

            this.thead
                .selectAll("th")
                .data(combinedCategories)
                .enter()
                .append("th")
                .text((value) => { return value.toString(); });

            options.dataViews[0].categorical.values.forEach((values) => {
                this.tbody
                    .selectAll("td")
                    .data(values.values)
                    .enter()
                    .append("td")
                    .text((value) => { return value.toString(); });
            });
        }

        private static parseSettings(dataView: DataView): VisualSettings {
            return VisualSettings.parse(dataView) as VisualSettings;
        }

        public enumerateObjectInstances(options: EnumerateVisualObjectInstancesOptions): VisualObjectInstance[] | VisualObjectInstanceEnumerationObject {
            return VisualSettings.enumerateObjectInstances(this.settings || VisualSettings.getDefault(), options);
        }
    }
}
```

**カテゴリに 1 フィールドのみ配置**
[カテゴリに地域を配置]
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/304d0752-ecb6-031f-4ae0-747d3b5efb7b.png)
[dataView の categories の値]
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/572c12e0-ac70-e6e0-28a0-fd386a649645.png)
[dataView の values の値]
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/a9280bb0-59d4-c134-4f32-11960946b8b9.png)
[レポートの結果]
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/79ffe2b9-93a0-7227-9e8c-1b697cfd8790.png)

**カテゴリに複数フィールド配置**
[カテゴリに地域と売上月を配置]
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/b06051c5-9405-e197-a662-04dab03e7049.png)
[dataView の categories の値]
地域数 (4)と売上月数 (12)　でグルーピングされるため、48 データポイントが生成
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/6cf997ea-897f-2e42-5718-d667b3d93189.png)
[dataView の values の値]
同じく値も 48 データポイント
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/a31df9e8-a47c-a706-fd7e-62b0874359ce.png)
[レポートの結果]
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/9a8be605-fa87-045a-cf14-57f57ece3cb6.png)


## categorical の例 2
以下は、カテゴリだけではなく、明示的なデータロールでグルーピングを行う例です。この例では myCategory だけでなく、myGrouping でもグループ化を行います。

```json:capabilities.json
{
    "dataRoles": [
        {
            "displayName": "サンプルカテゴリ",
            "name": "myCategory",
            "kind": "Grouping"
        },
        {
            "displayName": "グルーピングフィールド",
            "name": "myGrouping",
            "kind": "Grouping"
        },
        {
            "displayName": "サンプルメジャー",
            "name": "myMeasure",
            "kind": "Measure"
        }
    ],
    "dataViewMappings": [
        {
            "categorical": {
                "categories": {
                    "for": {
                        "in": "myCategory"
                    }
                },
                "values": {
                    "group": {
                        "by": "myGrouping",
                        "select": [
                            {
                                "bind": {
                                    "to": "myMeasure"
                                }
                            }
                        ]
                    }
                }
            }
        }
    ]    
}
```

### 動作確認

検証には以下の Visual を使います。

```typescript:visual.ts
module powerbi.extensibility.visual {
    "use strict";

    export class Visual implements IVisual {

        private area: d3.Selection<HTMLElement>
        private table: d3.Selection<HTMLElement>
        private thead: d3.Selection<HTMLElement>
        private tbody: d3.Selection<HTMLElement>
        private host: IVisualHost;
        private settings: VisualSettings;

        constructor(options: VisualConstructorOptions) {
            // カスタムビジュアルを配置しているホストの情報を取得
            this.host = options.host;
            // カスタムビジュアルのエリアを取得
            this.area = d3.select(options.element);
            this.table = this.area.append("table");
            this.thead = this.table.append("thead").append("tr");
            this.tbody = this.table.append("tbody");
        }

        public update(options: VisualUpdateOptions) {
            debugger;

            this.thead.selectAll("th").remove();
            this.tbody.selectAll("tr").remove();

            this.thead
                .selectAll("th")
                .data(options.dataViews[0].categorical.categories[0].values)
                .enter()
                .append("th")
                .text((value) => { return value.toString(); });

            // Add first blank header
            this.thead.insert("th", ":first-child");

            options.dataViews[0].categorical.values.forEach((values) => {

                let tr = this.tbody.append("tr");
                
                tr.selectAll("td")
                .data(values.values)
                .enter()
                .append("td")
                .text((value) => {
                    return value.toString(); });

                tr.insert("td",":first-child").text(() => { return values.source.groupName.toString() });
            });
        }

        private static parseSettings(dataView: DataView): VisualSettings {
            return VisualSettings.parse(dataView) as VisualSettings;
        }

        public enumerateObjectInstances(options: EnumerateVisualObjectInstancesOptions): VisualObjectInstance[] | VisualObjectInstanceEnumerationObject {
            return VisualSettings.enumerateObjectInstances(this.settings || VisualSettings.getDefault(), options);
        }
    }
}
```

[カテゴリに地域、グルーピングに売上月配置]
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/870012e4-f391-72ce-7ad2-9c29a86d86fa.png)
[dataView の categories の値]
カテゴリには地域の 4 つのデータ
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/5fb21687-bcff-14e3-209e-b5f4ee9b63e2.png)
[dataView の values の値]
結果は売上月毎にグルーピングされ、その配下で地域でグルーピングされた値が生成
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/f79f2147-4596-4ad0-7684-f9e091c3b9b9.png)
グルーピングされている売上月のラベルは source で取得可能
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/edaa3816-21c7-3025-c700-3402884a4784.png)
[レポートの結果]
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/152ea71b-490b-04d7-f469-5d1296e3ceac.png)

# table

データロールに配置されたフィールドについて、テーブル形式で返します。

## table の例

```json:capabilities.json
{
    "dataRoles": [
        {
            "displayName": "サンプルメジャー",
            "name": "myMeasure",
            "kind": "GroupingOrMeasure"
        }
    ],
    "dataViewMappings": [
        {
            "table": {
                "rows": {
                    "for": {
                        "in": "myMeasure"
                    }
                }
            }
        }
    ]
}
```

### 動作確認

検証には以下の Visual を使います。

```typescript:visual.ts
module powerbi.extensibility.visual {
    "use strict";

    export class Visual implements IVisual {

        private area: d3.Selection<HTMLElement>
        private table: d3.Selection<HTMLElement>
        private thead: d3.Selection<HTMLElement>
        private tbody: d3.Selection<HTMLElement>
        private host: IVisualHost;
        private settings: VisualSettings;

        constructor(options: VisualConstructorOptions) {
            // カスタムビジュアルを配置しているホストの情報を取得
            this.host = options.host;
            // カスタムビジュアルのエリアを取得
            this.area = d3.select(options.element);
            this.table = this.area.append("table");
            this.thead = this.table.append("thead").append("tr");
            this.tbody = this.table.append("tbody");
        }

        public update(options: VisualUpdateOptions) {
            debugger;

            this.thead.selectAll("th").remove();
            this.tbody.selectAll("tr").remove();
            
            // append the header row
            this.thead
                .selectAll("th")
                .data(options.dataViews[0].table.columns)
                .enter()
                .append("th")
                .text((column) => { return column.displayName; });

            options.dataViews[0].table.rows.forEach((row) => {

                this.tbody.append("tr")
                    .selectAll("td")
                    .data(row)
                    .enter()
                    .append("td")
                    .text((cell) => { return cell.toString(); });
            });

        }

        private static parseSettings(dataView: DataView): VisualSettings {
            return VisualSettings.parse(dataView) as VisualSettings;
        }

        public enumerateObjectInstances(options: EnumerateVisualObjectInstancesOptions): VisualObjectInstance[] | VisualObjectInstanceEnumerationObject {
            return VisualSettings.enumerateObjectInstances(this.settings || VisualSettings.getDefault(), options);
        }
    }
}
```

[データロールにフィールドを配置]
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/0da93397-94f6-1bef-d74f-8e9940f0bda5.png)
[レポートの結果]
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/24bda08f-ebc7-5c9f-5b1c-90cfbcc560ca.png)

# dataReductionAlgorithm

dataReductionAlgorithm (データ削減アルゴリズム) を使うことで、受け取るデータ量を調整できます。`DataReductionAlgorithm` は categorical、table、matrix と併用でき、定義もそれぞれのプロパティ内で行います。

カスタムビジュアルは既定で `top` アルゴリズムが適用されており、上位 1000 件のデータだけ処理されるようになっており、以下定義と同等です。

```json
"dataReductionAlgorithm": {
    "top": {
        "count": 1000
    }
}
```

## データ削減アルゴリズムの種類

`DataReductionAlgorithm` には 4 種類のアルゴリズムがあります。

- `top` : データセットの上位 x 件を取得
- `bottom` : データセットの下位 x 兼を取得
- `sample` : データセットより、初めと最後のレコード、および均等間隔の中間レコードをx 件のサンプルを取得
- `window` : 指定した数ずつデータを取得 ※現時点では `top` と同じ結果しかサポートされませんが、今後本来のウィンドウ機能をサポートする予定。

## dataReductionAlgorithm の例 1

以下の例では table に対して上位 3 件と取得します。dataReductionAlgorithm を指定する場所が重要で、この場合は rows 直下で指定しているため、取得できる行が 3 件となります。

```json:capabilities.json
{
    "dataRoles": [
        {
            "displayName": "サンプルメジャー",
            "name": "myMeasure",
            "kind": "GroupingOrMeasure"
        }
    ],
    "dataViewMappings": [
        {
            "table": {
                "rows": {
                    "dataReductionAlgorithm": {
                        "top": {
                            "count": 3
                        }
                    },
                    "for": {
                        "in": "myMeasure"
                    }
                }                
            }
        }
    ]
}
```

### 動作確認
table の紹介で使ったビジュアルをそのまま使います。

[データロールにフィールドを配置]
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/0da93397-94f6-1bef-d74f-8e9940f0bda5.png)
[レポートの結果]
3 件だけデータが返ってくる
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/c0462b7e-daa1-298e-f1a7-afeac98814ad.png)

# matrix

[Matrix Data Mapping](https://microsoft.github.io/PowerBI-visuals/docs/concepts/dataviewmappings/) に詳細が出たためこちらの記事も追記します
内容は table と似ていますが、行が階層として表現されます。
 
以下のようなデータがあった場合、

Parents	| Children | Grand children | Columns | Values
---|---|---|---|---
Parent1	| Child1|  	Grand child1| 	Col1| 	5
Parent1	| Child1| 	Grand child1| 	Col2| 	6
Parent1	| Child1| 	Grand child2| 	Col1| 	7
Parent1	| Child1| 	Grand child2| 	Col2| 	8
Parent1	| Child2| 	Grand child3| 	Col1| 	5
Parent1	| Child2| 	Grand child3| 	Col2| 	3
Parent1	| Child2| 	Grand child4| 	Col1| 	4
Parent1	| Child2| 	Grand child4| 	Col2| 	9
Parent1	| Child2| 	Grand child5| 	Col1| 	3
Parent1	| Child2| 	Grand child5| 	Col2| 	5
Parent2	| Child3| 	Grand child6| 	Col1| 	1
Parent2	| Child3| 	Grand child6| 	Col2| 	2
Parent2	| Child3| 	Grand child7| 	Col1| 	7
Parent2	| Child3| 	Grand child7| 	Col2| 	1
Parent2	| Child3| 	Grand child8| 	Col1| 	10
Parent2	| Child3| 	Grand child8| 	Col2| 	13

capabilities.json で以下のような定義をすると

```json
"matrix": {
    "rows": {
        "for": {
            "in": "Category"
        }
    },
    "columns": {
        "for": {
            "in": "Column"
        }
    },
    "values": {
        "select": [
            {
                "for": {
                    "in": "Measure"
                }
            }
        ]
    }
}
```

column に指定した列がヘッダーとして、集計した結果が返ってきます。
![](https://microsoft.github.io/PowerBI-visuals/docs/concepts/images/MatrixVisualSmaple.png)

# まとめ

グラフが必要とするデータをうまく整形するためにも　DataViewMappings の知識は重要です。是非試してください。次回は objects を見ていきます。

[次の記事へ](https://qiita.com/kenakamu/items/3eaf0cb8ada9c3c41074)
[目次へ戻る](https://qiita.com/kenakamu/items/f57ab7f73d6ea10bc9d9)