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

[前回](https://qiita.com/kenakamu/items/32d83de4f525ecad4435) の記事は DataViewMappings について見ていきました。今回は objects を見ていきます。
情報元: [GitHub の Objects の説明](https://github.com/Microsoft/PowerBI-visuals/blob/master/Capabilities/Objects.md)

# `Objects` : カスタムプロパティの定義

Objects を使うとビジュアルの書式パネルに、カスタム書式の定義が行えます。1 つの書式には複数のプロパティが設定できます。プロパティのカスタマイズは [フィールド書式のカスタマイズ](https://qiita.com/kenakamu/items/6465e757c0d2e52183e0) で試しましたが、今回はより詳細を見ていきます。

カスタム書式を使うには、以下の 3 ステップが必要です。

- Capabilities.json の objects にプロパティを定義する
- コード側の enumerateObjectInstances 関数でプロパティの定義と値の受け取り先を指定する
- コード側の update 関数で値が変わった場合の変更を処理する

まずは objects の各プロパティについて見ていきます。

## displayName

object の表示名を定義します。以下のオブジェクトの場合、一番上の「タイトル」が表示名となります。

```json
"objects": {
    "myCustomObject": {
        "displayName": "My Object Name",
        "properties": { ... }
    }
}
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/dec59e7d-9e45-dc1f-08b8-1e9b15a8ef9e.png)

## properties

カスタム書式は複数のプロパティを持てます。properties には各プロパティの詳細を定義します。以下のオブジェクトの場合「タイトル」「フォントの色」「背景色」「配置」などがオブジェクトのプロパティとなります。

```json
"properties": {
    "myFirstProperty": {
        "displayName": "firstPropertyName",
        "type": ValueTypeDescriptor | StructuralTypeDescriptor
    }
}
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/57ff2b54-562e-88a0-5389-ae3a37e7ba9b.png)

※ show は特殊なプロパティで、オブジェクト全体に対して On/Off のスイッチを設定します。表示名の横にある「オン/オフ」のトグルを表示します。

```json
"properties": {
    "show": {
        "displayName": "My Property Switch",
        "type": {"bool": true}
    }
}
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/6f2d0830-9104-004e-52b5-79a3e339526a.png)


### プロパティのタイプ

プロパティで設定できるタイプは以下の 2 種類ありますが、定義方法はあまり変わらないので、開発者が意識する必要は無さそうです。

- Value Type Descriptor: テキストや数値など単純な型
- Structural Type Descriptor: Fill や Formatting など追加情報があるもの

以下に例をいくつかあげます。

```json:文字
"displayText": {
    "displayName": "表示するテキスト",
    "type": {
        "text": true
    }
},
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/96cee8ab-efec-086d-e273-c5c5c3fb9da8.png)

```json:ブーリアン
"boolBold": {
    "displayName": "太字にする",
    "type": {
        "bool": true
    }
}
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/8f7a4656-ac82-c553-fc39-d2457d916379.png)

```json:配置
"alignText": {
    "displayName": "配置",
    "type": {
        "formatting": {
            "alignment": true
        }
    }
}
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/5c36cfba-aef0-b2b5-e67a-b0292989ef0f.png)

```json:塗りつぶし
"textColor": {
    "displayName": "色",
    "type": {
        "fill": {
            "solid": {
                "color": true
            }
        }
    }
}
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/fc58d61a-d9a5-2341-9103-6945c7a38f3e.png)

```json:ユニット
"labelDisplayUnits": {
    "displayName": "ディスプレイユニット",
    "type": {
        "formatting": {
            "labelDisplayUnits": true
        }
    }
}
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/6301a9f0-0b41-5eba-b710-b48395aba41d.png)

```json:フォントのサイズ
"fontSize": {
    "displayName": "フォントサイズ",
    "type": {
        "formatting": {
            "fontSize": true
        }
    }
}
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/9e640fc7-0d02-70ca-6ee7-ba8f34c954cd.png)

```json:選択(ドロップダウン)
"enumration": {
    "displayName": "サンプルドロップダウン",
    "type": {
        "enumeration": [
            {
                "displayName": "オプション１",
                "value": "1"
            },
            {
                "displayName": "オプション2",
                "value": "2"
            },
            {
                "displayName": "オプション3",
                "value": "3"
            }
        ]
    }
}     
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/362f1921-90cd-b3a7-bfa8-997316c87337.png)

# コード側での処理

Capabiities.json に定義した objects はコード側でも処理が必要です。例えば以下のような object 定義があるとします。

```json:capabilities.json
{
    "dataRoles": [
        {
            "displayName": "サンプルメジャー",
            "name": "myMeasure",
            "kind": "GroupingOrMeasure"
        }
    ],
    "dataViewMappings": [
        {
            "single": {
                "role": "myMeasure"
            }
        }
    ],
    "objects": {
        "myProperty": {
            "displayName": "サンプルプロパティ",
            "properties": {
                "show": {
                    "displayName": "表示する",
                    "type": {
                        "bool": true
                    }
                },
                "displayText": {
                    "displayName": "表示するテキスト",
                    "type": {
                        "text": true
                    }
                },
                "alignText": {
                    "displayName": "配置",
                    "type": {
                        "formatting": {
                            "alignment": true
                        }
                    }
                }           
            }
        }
    }
}
```

この場合、visual.ts の enumerateObjectInstances で各種プロパティを変数を関連付けします。

```typescript:visual.ts
private showMyProperty: boolean;
private myPropertyText: string;
private myPropertyTextAlginment: string;

public enumerateObjectInstances(options: EnumerateVisualObjectInstancesOptions): VisualObjectInstance[] | VisualObjectInstanceEnumerationObject {
    debugger;
  
    let objectName = options.objectName;
    let objectEnumeration: VisualObjectInstance[] = []
    // 設定名によって処理を実施
    switch (objectName) {
        case 'myProperty':
            objectEnumeration.push({
                objectName: objectName,
                properties: {
                    show: this.showMyProperty,
                    displayText: this.myPropertyText,
                    alignText: this.myPropertyTextAlginment
                },
                selector: null
            });
            break;
    }
    return objectEnumeration;
}
```

また設定が変わった場合に update 関数で処理します。`DataViewObjects.getValue` 関数に取得したいオブジェクト名とプロパティ名を指定、また値がない場合の既定値を設定します。

```typescript:visual.ts
public update(options: VisualUpdateOptions) {
            debugger;

            this.showMyProperty = DataViewObjects.getValue(
                options.dataViews[0].metadata.objects, {
                    objectName: "myProperty",
                    propertyName: "show"
                }, true
            );
            this.myPropertyText = DataViewObjects.getValue(
                options.dataViews[0].metadata.objects, {
                    objectName: "myProperty",
                    propertyName: "displayText"
                }, ""
            )
            this.myPropertyTextAlginment = DataViewObjects.getValue(
                options.dataViews[0].metadata.objects, {
                    objectName: "myProperty",
                    propertyName: "alignText"
                }, "center"
            )
...
```

# enumerateObjectInstances 関数

この関数では、利用するオブジェクト毎にプロパティを処理します。capabilities.json に指定したオブジェクトの数だけ呼ばれるため、`switch` 文で都度処理を行います。最終的に `VisualObjectInstance` の配列を返します。

# VisualObjectInstance

オブジェクトに対応する定義を格納するクラスです。

## objectName
オブジェクトの名前を指定します。

## properties
利用するプロパティと対応するローカル変数を指定します。

## selector

セレクターは、このプロパティが dataView に対してどこに配置されるかを指定します。

### static

selector に null を指定した場合、`dataviews[index].metadata.objects` に設定されます。上記の例でも selector に null を指定しているため `options.dataViews[0].metadata.objects` から値を取得しています。

```json
selector: null
```
### columns

selector で metadata を指定した場合、QueryName に一致する column に対してオブジェクトが設定されます。`dataViews[index].table.columns[index].objects`

```json
selector: { metadata: 'QueryName' }
```

#### columns の例

以下の例では `selector: { metadata: 'Table1.金額' }` のように Table1.金額 column に対するオブジェクト設定をしています。処理の方法はいくつかありますが、ここではデータをバインドするタイミングで値を確認して、設定しています。

```json:capabilities.json
{
    "dataRoles": [
        {
            "displayName": "サンプルメジャー",
            "name": "myMeasure",
            "kind": "GroupingOrMeasure"
        }
    ],
    "dataViewMappings": [
        {
            "table": {
                "rows": {
                    "for": {
                        "in": "myMeasure"
                    }
                }
            }
        }
    ],
    "objects": {
        "myProperty": {
            "displayName": "サンプルプロパティ",
            "properties": {
                "show": {
                    "displayName": "表示する",
                    "type": {
                        "bool": true
                    }
                },
                "textColor": {
                    "displayName": "色",
                    "type": {
                        "fill": {
                            "solid": {
                                "color": true
                            }
                        }
                    }
                }           
            }
        }
    }
}
```

```typescript:visual.ts
import DataViewObjects = powerbi.extensibility.utils.dataview.DataViewObjects;

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;
        private showMyProperty: boolean;
        private myPropertyTextColor: any;
  
        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")
                .append("font")
                .attr('color', (column) => {
                    if (column.objects != null) {
                        this.showMyProperty = DataViewObjects.getValue(
                            column.objects, {
                                objectName: "myProperty",
                                propertyName: "show"
                            }, true
                        );
                        let columnColor = DataViewObjects.getValue(
                            column.objects, { objectName: "myProperty", propertyName: "textColor" }, null);
                        if (columnColor != null && this.showMyProperty) {
                            this.myPropertyTextColor = columnColor.solid.color;
                            return columnColor.solid.color;
                        }
                    }
                    else {
                        return "black"
                    }
                })
                .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 {
            debugger;

            let objectName = options.objectName;
            let objectEnumeration: VisualObjectInstance[] = []
            // 設定名によって処理を実施
            switch (objectName) {
                
                case 'myProperty':

                    objectEnumeration.push({
                        objectName: objectName,
                        properties: {
                            show: this.showMyProperty,
                            textColor: this.myPropertyTextColor,
                        },
                        selector: { metadata: 'Table1.金額' }
                    });
                    break;
            }
            return objectEnumeration;
        }
    }
}
```

[メジャーに金額と地域を設定]
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/2454ac4b-1396-b124-bc8d-4f2db9e5b39e.png)

[プロパティで色を指定]
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/f40e502d-3320-96c7-913d-be7c0e9c2ae5.png)

[結果]
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/f83758fa-b0ca-64a2-36c6-406e6d4ec11a.png)

### selector

ISelectionId プロパティを持ったインスタンスに対して、オブジェクトを配置します。棒グラフのように複数のインスタンスが画面上にあり、それぞれ異なる色を付ける場合などに利用します。具体例は [フィールド書式のカスタマイズ](https://qiita.com/kenakamu/items/6465e757c0d2e52183e0) を参照してください。

### scope identity

例えばカテゴリーとして、["Jan", "Feb", "March", ...] があり、他のカテゴリとして、["Small", "Medium", "Large"] があった場合、"Feb"  かつ "Large" に対してオブジェクトを配置したい場合、scope identity が使えます。それぞれの DataViewScopeIdentity を selector に対して指定します。

```json
selector: {
    data: <DataViewScopeIdentity[]>identities
}
```

# まとめ

高度なビジュアルの開発にはカスタム書式の利用は欠かせません。既存のビジュアルを参考にしながら、色々試してください。次回は Sorting、SupportsHighlight、AdvancedEditModeSupport を見ていきます。

尚、書式をより簡単に扱うためのユーティリティとして [DataViewObjectsParser](https://qiita.com/kenakamu/items/915ef37c0f7f52828021#dataviewobjectsparser) があります。是非そちらもご覧ください。

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