Power BI カスタムビジュアル開発 : 積み上げ棒グラフの開発 : グラフの描画

[前回](https://qiita.com/kenakamu/items/3d49de5736a665450602)は積み上げ棒グラフに必要なデータの構造の検討と実装を行いました。今回は取得したデータを使ってグラフを描画します。

# グラフの入れ物を用意

まずは必要なライブラリの読込みと、キャンバスの準備を行います。キャンバスは d3 を利用して SVG の中にグラフの領域を追加します。

1\. import を visual.ts に追加。

```typescript
import * as d3 from 'd3';
```

2\. Visual クラスのプロパティを以下に差し替え。

```typescript
private settings: VisualSettings;
private svg: d3.Selection<d3.BaseType, any, HTMLElement, any>;
private stackBarContainer: d3.Selection<d3.BaseType, any, any, any>;
```

3\. update 関数を以下に差し替え。

```typescript
public update(options: VisualUpdateOptions) {
    debugger;
    let viewModel = this.converData(options);

    let width = options.viewport.width;
    let height = options.viewport.height;
    let margin = { top: 0, bottom: 0, left: 0, right: 0 }
   
    this.svg
        .attr("width", width)
        .attr("height", height);
}
```
この状態で描画すると以下のように svc と g 要素が確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/524e5e10-9bfd-da1b-89b8-434b7423d33f.png)

# 軸と凡例の要素

変換されたデータはそれぞれが軸と凡例の値を持っているため、値が重複しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/afbb07ba-bf86-d0ca-d25d-69f682a5bb4d.png)

update 関数に以下のコードを追加して重複を取り除きます。
```let viewModel = this.converData(options); ``` の下に以下コードを追加。

- map ですべてのオブジェクトから必要な要素 (axis や legend) だけを抽出
- filter で重複を除外

```typescript
let axises = viewModel.dataPoints.map(d => d.axis).filter((element, index, array) => {
    return array.indexOf(element) === index
});
let legends = viewModel.dataPoints.map(d => d.legend).filter((element, index, array) => {
    return array.indexOf(element) === index
});
```

# スケール用関数の作成

X 軸、Y 軸の計算は d3 の機能を使うと簡単に実装できます。

## d3.scaleBand
特定範囲の数字を、帯(Band)毎に計算して返すため、X 軸の計算に利用します。
例えば以下のように関数を定義したとします。

```typescript
let x = d3.scaleBand()
    .domain(["orange","apple","banana"])
    .range([0, 90])
    .padding(0.1);

console.log(x("orange"));
console.log(x("apple"));
console.log(x("banana"));
```
この場合 0 から 90 の数値に対して orange, apple, banana の要素ごとに間隔(padding) 10% を考慮した値を返します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/86422c7d-17c1-f62e-735f-cdef7f982ede.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/75df17b2-fbe7-907c-df5e-a02d7c96940b.png)

## d3.scaleLinear
特定範囲の数値に対して、ある数値(Linear)に対してスケールした値を返すため、Y 軸の計算に利用します。
例えば以下のように関数を定義したとします。

```typescript
let y = d3.scaleLinear()
    .domain([0, 1000])
    .rangeRound([0, 300])

console.log(y(0));
console.log(y(1000));
console.log(y(500));
```

この場合 0 から 1000 の値を 0 から 300 にスケールして返します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/e4c48f07-42ab-ed48-1d7d-147f6efceb61.png)

また範囲を逆にすることもでき、棒グラフではよくこの形式を使います。

```typescript
let y = d3.scaleLinear()
    .domain([0, 1000])
    .rangeRound([0, 300])

console.log(y(0));
console.log(y(1000));
console.log(y(500));
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/934b4623-adbe-8526-3bf5-efac75a92edc.png)


## スケール関数の追加

上記を踏まえ、以下のコードを update 関数に追加します。

- 値の範囲はカスタムビジュアルのサイズとマージンから計算

```typescript
let x = d3.scaleBand()
    .domain(axises)
    .range([margin.left, width - margin.right])
    .padding(0.1);

let y = d3.scaleLinear()
    .domain([0, viewModel.maxValue])
    .rangeRound([height - margin.bottom, margin.top]);
```

# 凡例の色

凡例毎に別の色を付けたい場合、d3.scaleOrdinal を活用できます。scaleBand と似た動きをしますが、数値の代わりのオブジェクトを返すことができます。また d3 はあらかじめ色の配列を何パータンか提供しているため、今回はそちらを利用します。

update 関数に以下コードを追加します。これで凡例の値を渡すと対応する色を返してくれます。またドメインに指定していない値を渡すと、既定の #ccc を返します。

```typescript
let color = d3.scaleOrdinal()
    .domain(legends)
    .range(d3.schemeYlOrBr[legends.length])
    .unknown("#ccc");
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/15657339-dd8c-3ffa-a602-949bd5be9dc3.png)

尚、schemeYlOrBr をはじめ用意されているものは、渡せる range の数に制限がある点注意してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/30e4d283-ac4c-04f9-bf0d-8ccd9768eccc.png)

# チャートの描写

最後に積み上げ棒を描写します。d3 の機能を使ってすべてのデータについて、四角(rect)を描写します。

- fill, x, y, height, width は全て計算で取得

```typescript
this.stackBarContainer.selectAll('rect')
    .data(viewModel.dataPoints)
    .join('rect')
    .attr('fill', d => color(d.legend).toString())
    .attr("x", (d, i) => x(d.axis.toString()))
    .attr("y", d => y(d.value2))
    .attr("height", d => y(viewModel.maxValue - d.value))
    .attr("width", x.bandwidth());
```
最終的に update 関数は以下のようになります。

```typescript
public update(options: VisualUpdateOptions) {
    debugger;
    let viewModel = this.converData(options);

    let axises = viewModel.dataPoints.map(d => d.axis).filter((element, index, array) => {
        return array.indexOf(element) === index
    });

    let legends = viewModel.dataPoints.map(d => d.legend).filter((element, index, array) => {
        return array.indexOf(element) === index
    });

    let width = options.viewport.width;
    let height = options.viewport.height;
    let margin = { top: 0, bottom: 0, left: 0, right: 0 }
    
    this.svg
        .attr("width", width)
        .attr("height", height);
    
    let x = d3.scaleBand()
        .domain(axises)
        .range([margin.left, width - margin.right])
        .padding(0.1);

    let y = d3.scaleLinear()
        .domain([0, viewModel.maxValue])
        .rangeRound([height - margin.bottom, margin.top]);

    let color = d3.scaleOrdinal()
        .domain(legends)
        .range(d3.schemeYlOrBr[legends.length])
        .unknown("#ccc");
    
    this.stackBarContainer.selectAll('rect')
        .data(viewModel.dataPoints)
        .join('rect')
        .attr('fill', d => color(d.legend).toString())
        .attr("x", (d, i) => x(d.axis.toString()))
        .attr("y", d => y(d.value2))
        .attr("height", d => y(viewModel.maxValue - d.value))
        .attr("width", x.bandwidth());
}
```

# 動作確認

全てのコードが書き終わったので、実際にカスタムビジュアルを実行します。意図した通りに描写されることを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214116/76590c2c-a088-0584-30f0-4bdde5f74a2d.png)

# まとめ

今回は d3 のスケール関数を利用しながら積み上げ棒グラフを描写しました。次回は X 軸、Y 軸および凡例の表示を行います。

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