Power BI カスタムビジュアル開発 その2: プロジェクトの理解とグラフの追加

前回の準備編 に引き続き、Power BI のカスタムビジュアルを開発していきます。今回は pbiviz で作成されたプロジェクトの中身を理解します。

カスタムビジュアルプロジェクトの構造

Power BI カスタムビジュアルは以下の要素があります。

  • TypeScript と LESS ベース
  • UI: D3.js ベースで開発することが多い
  • データ: Power BI 側からフィードされる。データの構造や設定を行う必要がある

構成ファイル

pbiviz.json

カスタムビジュアルプロジェクトの情報を保持。カスタムビジュアル自体の情報と、依存や機能ファイルの指定を含みます。

capabilities.json

ビジュアル用のカテゴリや値の設定を保持。それぞれのラベルや、何個までフィールドを持てるかなど設定を行えます。

tsconfig.json、tslint.json、package.json

TypeScript と npm プロジェクトの設定

ソースファイル

src/visual.ts

カスタムビジュアルのソース
IVisual を継承したビジュアル用のクラスが存在し、コンストラクターと画面更新のたびに実行される Update メソッドを持つ。

src/settings.ts

カスタムビジュアル用設定ソース

style/visual.less

カスタムビジュアル用 LESS ファイル

assets

画像保存用フォルダ

データ構造の定義と読み込み

まずは今回作成するグラフが使うデータ構造を定義します。サンプルは この GitHub にあるものを再利用します。

1. visual.ts の powerbi.extensibility.visual 内で、以下インターフェースを定義。

BarChartViewModel: グラフに対して指定するモデル。ここでは実際のデータと最大値をそれぞれ保持するように指定。
BarChatDataPoin: グラフに渡される実際のデータ。ここでは値をカテゴリをそれぞれ1つ。

interface BarChartViewModel {
    dataPoints: BarChartDataPoint[];
    dataMax: number;
};

interface BarChartDataPoint {
    value: number;
    category: string;
};

2. Power BI から受け取ったデータを上記の型に変換する関数として、visualTransform を追加します。ポイントはデータが options.dataViews として渡される点です。

function visualTransform(options: VisualUpdateOptions, host: IVisualHost): BarChartViewModel {
    // Power BI からのデータを受け取る
    // データは dataViews プロパティに存在
    let dataViews = options.dataViews;

    // 空の viewModel を作成。
    let viewModel: BarChartViewModel = {
        dataPoints: [],
        dataMax: 0
    }
    // 期待した値があるか確認。なければ空データを返す
    if (!dataViews
        || !dataViews[0]
        || !dataViews[0].categorical
        || !dataViews[0].categorical.categories
        || !dataViews[0].categorical.categories[0].source
        || !dataViews[0].categorical.values)
        return viewModel;
    // 値があった場合はそれぞれ変数に一旦抜き出し
    let categorical = dataViews[0].categorical;
    let category = categorical.categories[0];
    let dataValue = categorical.values[0]

    // dataPoint のインスタンス化
    let barChartDataPoints: BarChartDataPoint[] = [];
    let dataMax: number;
    // カテゴリと値のセットを dataPoint に入れていく
    for (let i = 0, len = Math.max(category.values.length, dataValue.values.length); i < len; i++){
        barChartDataPoints.push({
            category: <string>category.values[i], 
            value: <number>dataValue.values[i]
        });
    }
    // 値の最大を取得
    dataMax = <number>dataValue.maxLocal;
    // viewModel を返す
    return {
        dataPoints: barChartDataPoints,
        dataMax: dataMax
    }
}  

3. Visual クラスの変数、コンストラクタ、Update の中身を一旦削除。

export class Visual implements IVisual {
    constructor(options: VisualConstructorOptions) {
    }
    public update(options: VisualUpdateOptions) {
    }
    private static parseSettings(dataView: DataView): VisualSettings {
        return VisualSettings.parse(dataView) as VisualSettings;
    }
    /** 
     * This function gets called for each of the objects defined in the capabilities files and allows you to select which of the 
     * objects and properties you want to expose to the users in the property pane.
     * 
     */
    public enumerateObjectInstances(options: EnumerateVisualObjectInstancesOptions): VisualObjectInstance[] | VisualObjectInstanceEnumerationObject {
        return VisualSettings.enumerateObjectInstances(this.settings || VisualSettings.getDefault(), options);
    }
}

4. クラスに以下 3 つのプロパティを追加。area は D3 を使って指定する HTML の表示領域。他 2 つは Power BI の要素。

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

5. コンストラクタを以下に変更。ホストの情報とデータを表示したいエリアを取得。

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

6. update 関数を以下に変更。先ほど定義した関数を使ったデータの取得および、結果を p タグに列挙。D3 の知識が必要となるが、data でデータをバインドし、enter で各データをループするイメージ。各データごとに p と text を追加して、値を設定。

public update(options: VisualUpdateOptions) {
    // Power BI より データを取得
    let viewModel: BarChartViewModel = visualTransform(options, this.host);

    // D3 を使って、データを p タグとして書き込み
    this.area.selectAll('p')
        .data(viewModel.dataPoints)
        .enter()
        .append('p')
        .append('text')
        .text(function (d) { return d.value; })
}

7. ファイルを保存して、Power BI サービスでカスタムビジュアルを更新。任意のフィールドをいくつか設定して動作を確認。

Capture.PNG

棒グラフの実装

次に D3 を使って棒グラフを作ってみます。

1. Visual クラスプロパティを以下のように変更。

private svg: d3.Selection<SVGElement>; // グラフ全体用
private barContainer: d3.Selection<SVGElement>; // 棒グラフ用
private xAxis: d3.Selection<SVGElement>; // X軸表示用
private bars: d3.selection.Update<BarChartDataPoint>; // グラフのセレクション
private host: IVisualHost;
private settings: VisualSettings;

2. コンストラクターを以下コードに変更。

constructor(options: VisualConstructorOptions) {
   // カスタムビジュアルを配置しているホストの情報を取得
   this.host = options.host;

   // カスタムビジュアルのエリアを取得し、svg を追加
   this.svg = d3.select(options.element)
       .append('svg')
   // svg 内に棒グラフ用のエリアと xAxis 用のエリアをそれぞれグループ指定
   this.barContainer = this.svg
       .append('g');
   this.xAxis = this.svg
       .append('g');
}

3. update を以下コードに変更。

public update(options: VisualUpdateOptions) {
    // Power BI より データを取得
    let viewModel: BarChartViewModel = visualTransform(options, this.host)
    // 現在のビジュアルの幅と高さを取得
    let width = options.viewport.width;
    let height = options.viewport.height;
    // 棒グラフ用にマージンを指定。bottom を 25 上げる
    let margin = { top: 0, bottom: 25, left: 0, right: 0 }
    // メインの svg をカスタムビジュアルと同じ大きさに指定
    this.svg.attr({
        width: width,
        height: height
    })
    // 棒グラフ用の高さとして、マージンの bottom を引いたものを用意
    height -= margin.bottom
    // Y軸スケール用の計算。0 から値の最大値を、高さから 0 にマップ。
    // 値が 0 の場合は高さが最大になり、値が最大の場合は、高さが 0 となる。
    let yScale = d3.scale.linear()
        .domain([0, viewModel.dataMax])
        .range([height, 0])
    // X軸スケール用の計算。カテゴリをドメインとし、全体の幅をグラフの数で分割
    // グラフとグラフの間は 0.1 の割合であける。
    let xScale = d3.scale.ordinal()
        .domain(viewModel.dataPoints.map(d => d.category))
        .rangeRoundBands([0, width], 0.1)
    // xAxis の場所をグラフ内各棒の下に設定
    let xAxis = d3.svg.axis()
        .scale(xScale)
        .orient('bottom')
    // xAxis の属性として transform を追加。
    this.xAxis.attr('transform', 'translate(0, ' + height + ')')
        .call(xAxis)
    // 棒グラフ内のすべてのグラフを取得し、データをバインド
    let bars = this.barContainer
        .selectAll('.bar')
        .data(viewModel.dataPoints)
    // 各グラフ毎に rect (四角) を追加してクラスを定義
    bars.enter()
        .append('rect')
        .classed('bar', true);
    // グラフの属性として幅、高さ、x、y を指定
    bars.attr({
        width: xScale.rangeBand(),
        height: d => height - yScale(d.value),
        y: d => yScale(d.value),
        x: d => xScale(d.category)
    })
    // 値がないグラフがあった場合は表示から削除
    bars.exit()
        .remove();
}

4. 保存して Power BI サービスのカスタムビジュアルを更新。

Capture.PNG

5. 一番下がつながっているので、CSS で表示を少し調整。まず CSS で要素を指定できるよう xAxis にクラスを付与。コンストラクターで xAxis を作成している場所を以下のように変更。

this.xAxis = this.svg
    .append('g')
    .classed('xAxis', true);

6. style/visual.less の中身を以下に変更。

.xAxis {
    path {
        display: none;
    }
} 

7. 保存後、再度カスタムビジュアルを更新。

Capture.PNG

8. 値がないものは削除したはずだが、2 つしか棒が出ていない。理由は値がないのではなく、負の値となっているため。最大値を同じように対応するため、まずは BarChartViewModel で最小値を持つように変更。

interface BarChartViewModel {
    dataPoints: BarChartDataPoint[];
    dataMax: number;
    dataMin: number;
};

9. visualTransform 関数を以下のように変更。

function visualTransform(options: VisualUpdateOptions, host: IVisualHost): BarChartViewModel {
    // Power BI からのデータを受け取る
    // データは dataViews プロパティに存在
    let dataViews = options.dataViews;
    // 空の viewModel を作成。
    let viewModel: BarChartViewModel = {
        dataPoints: [],
        dataMax: 0,
        dataMin: 0
    }
    // 期待した値があるか確認。なければ空データを返す
    if (!dataViews
        || !dataViews[0]
        || !dataViews[0].categorical
        || !dataViews[0].categorical.categories
        || !dataViews[0].categorical.categories[0].source
        || !dataViews[0].categorical.values)
        return viewModel;
    // 値があった場合はそれぞれ変数に一旦抜き出し
    let categorical = dataViews[0].categorical;
    let category = categorical.categories[0];
    let dataValue = categorical.values[0]
    // dataPoint のインスタンス化
    let barChartDataPoints: BarChartDataPoint[] = [];
    let dataMax: number;
    let dataMin: number;
    // カテゴリと値のセットを dataPoint に入れていく
    for (let i = 0, len = Math.max(category.values.length, dataValue.values.length); i < len; i++) {
        barChartDataPoints.push({
            category: <string>category.values[i],
            value: <number>dataValue.values[i]
        });
    }
    // 値の最大を取得
    dataMax = <number>dataValue.maxLocal;
    // 値の最小値を取得
    dataMin = <number>dataValue.minLocal;
    // viewModel を返す
    return {
        dataPoints: barChartDataPoints,
        dataMax: dataMax,
        dataMin: dataMin
    }
}

10. 最後に update 関数を変更。

public update(options: VisualUpdateOptions) {
    // Power BI より データを取得
    let viewModel: BarChartViewModel = visualTransform(options, this.host)
    // 現在のビジュアルの幅と高さを取得
    let width = options.viewport.width;
    let height = options.viewport.height;
    // 棒グラフ用にマージンを指定。bottom を 25 上げる
    let margin = { top: 0, bottom: 25, left: 0, right: 0 }
    // メインの svg をカスタムビジュアルと同じ大きさに指定
    this.svg.attr({
        width: width,
        height: height
    })
    // 棒グラフ用の高さとして、マージンの bottom を引いたものを用意
    height -= margin.bottom
    // 値が負になることを考慮して高さマップ。
    let max = Math.max(viewModel.dataMax, -viewModel.dataMin);
    let yScale = d3.scale.linear()
        .domain([0, max])
        .range([0, height * (max / (viewModel.dataMax - viewModel.dataMin))])
        .nice()
    // X軸スケール用の計算。カテゴリをドメインとし、全体の幅をグラフの数で分割
    // グラフとグラフの間は 0.1 の割合であける。
    let xScale = d3.scale.ordinal()
        .domain(viewModel.dataPoints.map(d => d.category))
        .rangeRoundBands([0, width], 0.1)
    // xAxis の場所をグラフ内各棒の下に設定
    let xAxis = d3.svg.axis()
        .scale(xScale)
        .orient('bottom')
    // xAxis の属性として transform を追加。
    this.xAxis.attr('transform', 'translate(0, ' + height + ')')
        .call(xAxis)
    // 棒グラフ内のすべてのグラフを取得し、データをバインド
    let bars = this.barContainer
        .selectAll('.bar')
        .data(viewModel.dataPoints);
    // 各グラフ毎に rect (四角) を追加
    bars.enter()
        .append('rect')
        .classed('bar', true);
    // グラフの属性として幅、高さ、x、y を指定
    bars.attr({
        width: xScale.rangeBand(),
        height: d => yScale(Math.abs(d.value)),
        y: function (d) {
            // 値が正の場合と負の場合でスタート位置を調整
            if (d.value > 0) {
                return yScale(max) - yScale(Math.abs(d.value));
            }
            else {
                return yScale(max);
            }
        },
        x: d => xScale(d.category)
    })
    // 値がないグラフがあった場合は表示から削除
    bars.exit()
        .remove();
}

11. グラフを更新して確認。

Capture.PNG

棒グラフに色を付ける

今回は値の正負のみで色分けしてみます。

1. update 関数最後にあるグラフの属性指定を変更して、fill を追加。

// グラフの属性として幅、高さ、x、y、色を指定
bars.attr({
    width: xScale.rangeBand(),
    height: d => yScale(Math.abs(d.value)),
    y: function (d) {
        // 値が正の場合と負の場合でスタート位置を調整
        if (d.value > 0) {
            return yScale(max) - yScale(Math.abs(d.value));
        }
        else {
            return yScale(max);
        }
    },
    x: d => xScale(d.category),
    fill: function (d) {
        if (d.value > 0) {
            return 'green';
        }
        else {
            return 'red';
        }
    }
});

2. グラフを更新して確認。

Capture.PNG

今回はプロジェクトの構成と棒グラフの追加をしました。次回は既定のグラフのように、各棒グラフをクリックした際に他の要素が薄くなるなどインタラクションを追加していきます。次の記事へ

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.