Power BI カスタムビジュアル開発 : 積み上げ棒グラフの開発 : プロジェクト作成とデータの定義

この記事では積み上げ棒グラフの開発について見ていきます。カスタムビジュアル開発の基本については [カスタムビジュアル開発入門](https://qiita.com/kenakamu/items/f57ab7f73d6ea10bc9d9#%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%93%E3%82%B8%E3%83%A5%E3%82%A2%E3%83%AB%E9%96%8B%E7%99%BA%E5%85%A5%E9%96%80) を参照してください。

尚、積み上げ棒グラフとは以下のようなグラフです
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/7bd75245-d8d9-8de4-3cbb-0b250b78c231.png)

今回は積み上げ棒グラフに必要なデータ構造の検討と実装を行います。

# ソリューションの作成

1\. pbiviz を使ってソリューションを作成。

```shell
pbiviz new MyStackedBarChart
cd MyStackedBarChart
```
2\. Visual Studio Code で開き、カスタムビジュアルを実行。

```
code .
pbiviz start
```

3\. カスタムビジュアルが動作することを確認。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/08234c4c-9a10-349c-4de1-ef8b1647d336.png)

# 積み上げ棒グラフの作り方を検討

まずデータは以下のように日付、商品、数列があるとします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/058a1e32-434a-b9f3-9718-70627c4be2a9.png)

## Y 座標
上記のデータを積み上げにしたい場合、例えばとあるグラフの高さを 300 とすると以下のようになります。
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/6205aa3d-3582-5eeb-da26-3eaf81cc7aaf.png"/>

一見一番上の 76 が分かれば計算できるように見えますが、実際は下からの合計値が必要です。これは棒が 2 つ以上になった場合、必ずしもすべての棒が Y 座標 0 から始まらないためです。
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/70e705dc-ea58-e3e5-710b-d48132208cbb.png"/>

よって下からの積み上げ合計をもって Y 座標を計算します。上図の？の計算は 3 月の合計が 218 であった場合、
300-218/364*300≒120 となります。もし 4 月より合計値が高いデータがある場合、計算に利用する 364 が変わります。

## X 座標
次に X 座標を考えます。これは集計するデータの分類数に依存します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/b312946b-77e9-4dd4-0760-3c5e2aae0219.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/fef009b0-bf7a-aa5e-2ce5-a2941311f9e7.png)

## 1 つの要素が持つべき値
X,Y それぞれを決めて各四角を描写するためには、それぞれの要素に以下の情報が必要です。

**自身の数値とそれまでに積まれた箱の合計**: 自身の Y 座標を決めるために必要
**自身の数値**: 数表示するために必要
**商品名**: 自身の分類を認識し、色付けするために必要
**日付**: X 座標を決めるのに必要

またグラフ全体としては以下のデータが必要です。

**各月合計の最大値**: 棒グラフ自体の Y 座標を決めるために必要
**すべての要素**: 上記要素をデータ数分

これを踏まえ、以下インターフェースを visual.ts に追加します。

```typescript
interface StackedBarChartViewModel {
    dataPoints: StackedBarChartDataPoint[];
    maxValue: number; // 各月合計の最大値
}

interface StackedBarChartDataPoint {
    axis: string; // 日付
    legend: string; // 商品名
    value: number; // 自分の数
    value2: number; // 積み上げた数
}
```

# 必要な項目と受け取るデータの形式

ユーザーに指定してほしい項目は「軸」、「凡例」、「値」の 3 つです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/e73c8f05-fdfa-6280-d470-c1beb4c6e2c4.png)

また Power BI から受け取るデータの形式ですが、最終的にデータを成型できればいいため、今回は matrix を使ってみます。
Capabilities.json の中身を書き換え。

- conditions で受け入れる数を制限

```json:capabilities.json
{
    "dataRoles": [
        {
            "displayName": "軸",
            "name": "axis",
            "kind": "Grouping"
        },
        {
            "displayName": "凡例",
            "name": "legend",
            "kind": "Grouping"
        },
        {
            "displayName": "値",
            "name": "measure",
            "kind": "Measure"
        }
    ],    
    "dataViewMappings": [
        {
            "conditions": [
                {
                    "axis": {
                        "max": 1
                    },
                    "legend": {
                        "max": 1
                    },
                    "measure": {
                        "max": 1
                    }
                }
            ],
            "matrix": {
                "rows": {
                    "for": {
                        "in": "axis"
                    }
                },
                "columns": {
                    "for": {
                        "in": "legend"
                    }
                },
                "values": {
                    "select": [
                        {
                            "for": {
                                "in": "measure"
                            }
                        }
                    ]
                }
            }
        }
    ]
}
```

# デバッグでデータを確認

最後にどのようにデータが返ってくるか確認します。

1\. visual.ts の update 関数に debugger を追加。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/07f2e2a5-effd-07f6-29d4-477a94e83bb2.png)

2\. カスタムビジュアルが引き続き動くこととフィールドが指定できることを確認。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/03074430-87bd-bb16-c2b4-ecb21f3cb98c.png)

3\. F12 キーを押下して開発者ツールを起動。ビジュアルを更新したらブレークポイントがヒットすることを確認。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/183e8394-19d0-da89-0c0c-5c9d5dbd5177.png)

4\. コンソールで options.dataView[0] の中身を確認。
matrix.columns.root.children に凡例の情報が格納
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/0898dca3-481a-ee10-da18-b0736f0286a4.png)
matrix.rows.root.children に月ごとの集計が格納
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/e462ea93-61e7-b166-7927-210a125c5716.png)

# データの変換

最後に Power BI より受け取ったデータを定義した型に変換する関数を追加します。

1\. Visual クラスに以下関数を追加。

```typescript
private converData(options: VisualUpdateOptions): StackedBarChartViewModel {
    let dataViews = options.dataViews;

    let dataModel: StackedBarChartViewModel = {
        dataPoints: [],
        maxValue: 0
    }

    if (!this.checkDataset(dataViews)) return dataModel;

    let columns = dataViews[0].matrix.columns.root.children;
    let rows = dataViews[0].matrix.rows.root.children;

    let maxValue = 0;
    let dataPoints: StackedBarChartDataPoint[] = [];
    rows.forEach(row => {
        let maxInAxis = 0;
        let axisName = <string> row.value;

        for (let key of Object.keys(row.values)) {
            let value = <number> row.values[key].value;
            maxInAxis += value;
            let dataPoint: StackedBarChartDataPoint = {
                axis: axisName,
                legend: <string> columns[key].value,
                value: value,
                value2: maxInAxis,
            };
            dataPoints.push(dataPoint);
        }

        maxValue = Math.max(maxValue, maxInAxis);
    });

    dataModel.dataPoints = dataPoints;
    dataModel.maxValue = maxValue;
    return dataModel;
}

private checkDataset(dataViews: powerbi.DataView[]): boolean {
    if (!dataViews
        || !dataViews[0]
        || !dataViews[0].matrix
        || !dataViews[0].matrix.columns
        || !dataViews[0].matrix.columns.root
        || !dataViews[0].matrix.columns.root.children
        || !dataViews[0].matrix.rows
        || !dataViews[0].matrix.rows.root
        || !dataViews[0].matrix.rows.root.children
    ) {
        return false;
    }

    return true;
}
```

2\. update 関数の debugger; 直後に以下コードを追加。

```typescript
let viewModel = this.converData(options);
```

3\. 再度デバッグを実行してデータを確認。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/4ddfc14e-a6fb-5b18-ea72-367e1afddfc4.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/e8423eaf-ca2c-e30c-3b90-95d3bf3e120b.png)

# まとめ

今回はどのようなデータ構造が必要かを検討して実装しました。次回はグラフを描画します。

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