Power BI カスタムビジュアル開発 : ChartUtils でチャートや凡例、ラベルを描写する

# ChartUtils 

`ChartUtils` はカスタムビジュアルを作るためのインターフェースのセットです。軸の管理や凡例の操作、データのラベルの管理などが行えるなど、ビジュアルに関する重要な機能を提供します。

# インストール

以下手順で追加。

1\. npm コマンドでインストール。

```shell
npm install powerbi-visuals-utils-chartutils --save
```
2\. tsconfig.json の files に型情報を追加。

```json:tsconfig.json
{
  "compilerOptions": {
  ...
  },
  "files": [
    ...,　
    "node_modules/powerbi-visuals-utils-chartutils/lib/index.d.ts"
  ]
}
```

3\. pbiviz.json の externalJS に追加。

```json:pbiviz.json
{
  ...
  "externalJS": [
    ...,
    "node_modules/powerbi-visuals-utils-chartutils/lib/index.js"
  ],
  ...
}
```

4\. デザインの情報を style/visual.less に追加。

```css:visual.less
@import (less) "node_modules/powerbi-visuals-utils-interactivityutils/lib/index.css";
@import (less) "node_modules/powerbi-visuals-utils-chartutils/lib/index.css";
...
```

# 機能

- [Axis Helper](#axis) : チャートの軸の管理
- [Legend Service](#legendservice) : 凡例作成用のサービス
- [DataLabelUtils](#datalabelutils) : データに対するラベル作成と管理

# Axis Helper
`Axis Helper` はチャートの軸の操作を簡単にします。

`powerbi.extensibility.utils.chart.axis` モジュールは多くの機能を提供するため、以下にいくつかよく使うものを紹介します。

- [getRecommendedNumberOfTicksForYAxis](#getrecommendednumberofticksforyaxis)
- [getRecommendedNumberOfTicksForXAxis](#getrecommendednumberofticksforxaxis)
- [getBestNumberOfTicks](#getbestnumberofticks)
- [getCategoryValueType](#getcategoryvaluetype)
- [isOrdinal](#isordinal)
- [isDateTime](#isdatetime)
- [getCategoryThickness](#getcategorythickness)
- [createOrdinalScale](#createordinalscale)
- 
## getRecommendedNumberOfTicksForYAxis

チャート領域の高さによって最適な Y 軸の項目数を返す。

```typescript
function getRecommendedNumberOfTicksForYAxis(availableWidth: number) 
```

### getRecommendedNumberOfTicksForYAxis の例

```typescript
import axis = powerbi.extensibility.utils.chart.axis;

public update(options: VisualUpdateOptions) {
...

    let yAxis = d3.svg.axis()
        .scale(yAxisScale)
        .orient('left')
        .ticks(axis.getRecommendedNumberOfTicksForYAxis(options.viewport.height));
...
```

[高さがない場合は 5]
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/a65f4861-982e-18f6-cb98-886a50e34c1b.png)
[高さがある場合 8]
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/4c7d6630-32ed-3b77-9ae5-7b9532d9fc87.png)

## getRecommendedNumberOfTicksForXAxis

チャート領域の幅によって最適な X 軸の値の項目数を返す。

```typescript
function getRecommendedNumberOfTicksForXAxis(availableWidth: number): number;
```

### getRecommendedNumberOfTicksForXAxis の例

```typescript
import axis = powerbi.extensibility.utils.chart.axis;

public update(options: VisualUpdateOptions) {
...

    let xAxis = d3.svg.axis()
        .scale(xScale)
        .ticks(axis.getRecommendedNumberOfTicksForXAxis(width))
        .orient('bottom');
...
```

## getBestNumberOfTicks

最小値、最大値、メジャーの値、項目数の最大から最適な項目数を返す。

```typescript
function getBestNumberOfTicks(min: number, max: number, valuesMetadata: DataViewMetadataColumn[], maxTickCount: number, isDateTime?: boolean): number;
```

### getBestNumberOfTicks の例

```typescript
import axis = powerbi.extensibility.utils.chart.axis;

public update(options: VisualUpdateOptions) {
...
    let yAxis = d3.svg.axis()
        .scale(yAxisScale)
        .orient('left')
        .ticks(axis.getBestNumberOfTicks( 
                this.viewModel.dataMin,
                this.viewModel.dataMax, 
                options.dataViews[0].metadata.columns,
                6)
        );
...
```

## contains

元の文字列が指定した文字列を含むか確認する。

```typescript
function contains(source: string, substring: string): boolean;
```

### contains の例

```typescript
import axis = powerbi.extensibility.utils.chart.axis;

axis.contains("Microsoft Power BI Visuals", "Power BI"); // true
```
## getCategoryValueType

カテゴリー列のタイプ (ValueType) を返す。タイプがない場合はテキスト型として処理。

```typescript
function getCategoryValueType(metadataColumn: DataViewMetadataColumn, isScalar?: boolean): ValueType;
```

### getCategoryValueType の例

```typescript
import axis = powerbi.extensibility.utils.chart.axis;

public update(options: VisualUpdateOptions) {
...
    let categorySource = options.dataViews[0].categorical.categories[0].source;
    let categoryType = axis.getCategoryValueType(categorySource, true);
...

```

## isOrdinal

type がテキスト、ブーリアン、バーコードまたは郵便番号か確認。詳細は[ソースコード](https://github.com/Microsoft/powerbi-visuals-utils-chartutils/blob/64e2e2b7ada900382b6284acd914086f3d0c5f0c/src/axis/axis.ts#L443)を参照。

```typescript
function isOrdinal(type: ValueTypeDescriptor): boolean;
```

### isOrdinal の例

```typescript
import axis = powerbi.extensibility.utils.chart.axis;

public update(options: VisualUpdateOptions) {
...
    let categorySource = options.dataViews[0].categorical.categories[0].source;
    let categoryType = axis.getCategoryValueType(categorySource, true);
    axis.isOrdinal(categoryType); 
...
```

## isDateTime

type が日付型か確認。

```typescript
function isDateTime(type: ValueTypeDescriptor): boolean;
```

### isDateTime の例

```typescript
import axis = powerbi.extensibility.utils.chart.axis;

public update(options: VisualUpdateOptions) {
...
    let categorySource = options.dataViews[0].categorical.categories[0].source;
    let categoryType = axis.getCategoryValueType(categorySource, true);
    axis.isDateTime(categoryType); 
...
```

## getCategoryThickness

実際のカテゴリの幅を取得。

```typescript
function getCategoryThickness(scale: any): number;
```

### getCategoryThickness の例

```typescript
import axis = powerbi.extensibility.utils.chart.axis;

    let range = [0, 100];
    let domain = [0, 10];
    let scale = d3.scale.linear().domain(domain).range(range);
    let actualThickness = axis.getCategoryThickness(scale);
```

## createOrdinalScale

D3 の OrdinalScale を作成。

```typescript
function createOrdinalScale(pixelSpan: number, dataDomain: any[], outerPaddingRatio?: number, innerPaddingRatio?: number, useRangePoints?: boolean): d3.scale.Ordinal<any, any>;
```

### createOrdinalScale の例

```typescript
import axis = powerbi.extensibility.utils.chart.axis;

public update(options: VisualUpdateOptions) {
...
    let xScale = axis.createOrdinalScale(
        options.viewport.width,
        this.viewModel.dataPoints.map(d => d.category)
    );
...
```
# Legend service
`Legend service` を使うとカスタムビジュアルに凡例を簡単に表示できます。

`powerbi.visuals` モジュールは以下の機能を提供します。

- [createLegend](#createLegend)
- [ILegend](#ILegend)
- [drawLegend](#drawLegend)

## createLegend

凡例オブジェクトの作成。描写は別途必要。

```typescript
function createLegend(
    legendParentElement: HTMLElement,                   // 凡例を配置する親エレメント
    interactive: boolean,                               // 凡例がインタラクティブか指定
    interactivityService: IInteractivityService,        // 凡例をクリックした際のインタラクションのハンドルをするサービス 
    isScrollable: boolean = false,                      // 凡例でスクロールを使うか
    legendPosition: LegendPosition = LegendPosition.Top // 凡例の位置
): ILegend;
```
### createLegend の例

```typescript
import legend = powerbi.extensibility.utils.chart.legend;
...

export class Visual implements IVisual {

    private seriesLegend: legend.ILegend;

    public init(options: VisualConstructorOptions) {
        ...
        this.interactivityService = createInteractivityService(options.host);

        this.seriesLegend= legend.createLegend(
            options.element,
            options.interactivity && options.interactivity.isInteractiveLegend,
            this.interactivityService,
            true,
            legend.LegendPosition.Left);
        }
...
```

## ILegend

凡例の操作に必要な関数を定義したインターフェース

```typescript
export interface ILegend {
    getMargins(): IViewport;
    isVisible(): boolean;
    changeOrientation(orientation: LegendPosition): void;
    getOrientation(): LegendPosition;
    drawLegend(data: LegendData, viewport: IViewport): any;
    /**
     * Reset the legend by clearing it
     */
    reset(): void;
}
```

### drawLegend

SVG 上に凡例を描写

```typescript
function drawLegend(data: LegendData, viewport: IViewport): void;
```

#### drawLegend の例

以下の例では表示する凡例データがある場合、設定から取得した位置に凡例を表示。データが無ければ上部に配置して表示しない。

```typescript
private renderLegend(): void {
    if (!this.isInteractive) {
        let legendObjectProperties = this.data.legendObjectProperties;
        if (legendObjectProperties) {
            let legendData = this.data.legendData;
            LegendData.update(legendData, legendObjectProperties);
            let position = <string>legendObjectProperties[legendProps.position];
            if (position)
                this.legend.changeOrientation(LegendPosition[position]);

            this.legend.drawLegend(legendData, this.parentViewport);
        } else {
            this.legend.changeOrientation(LegendPosition.Top);
            this.legend.drawLegend({ dataPoints: [] }, this.parentViewport);
        }
    }
}
```
# DataLabelUtils

`DataLabelUtils` データラベルの管理を行うユーティリティー

`powerbi.extensibility.utils.chart.dataLabel.utils` モジュールは以下の機能を提供

- [drawDefaultLabelsForDataPointChart](#drawdefaultlabelsfordatapointchart)
- [getLabelFormattedText](#getlabelformattedtext)
- [enumerateDataLabels](#enumeratedatalabels)
- [enumerateCategoryLabels](#enumeratecategorylabels)
- [createColumnFormatterCacheManager](#createcolumnformattercachemanager)

## drawDefaultLabelsForDataPointChart

データ用のラベルを描写

```typescript
function drawDefaultLabelsForDataPointChart(
    data: any[],                   // ラベル用のデータ
    context: d3.Selection<any>,    // ラベルを表示する領域
    layout: ILabelLayout,          // ラベルのデータや位置などのレイアウト
    viewport: IViewport,           // Viewport
    isAnimator?: boolean,          // アニメーションの有無
    animationDuration?: number,    // アニメーション時間
    hasSelection?: boolean,        // 選択されたアイテムの透過率設定
    hideCollidedLabels?: Boolean   // 重なるラベルを非表示にするか指定
  ): d3.selection.Update<any>;
```
#### drawDefaultLabelsForDataPointChart の例

データポイントが座標の情報など必要なものを持っている前提。

```typescript
import dataLabelUtils = powerbi.extensibility.utils.chart.dataLabel.utils;

dataLabelUtils.drawDefaultLabelsForDataPointChart(
    this.viewModel.dataPoints, 
    this.svg, 
    this.getLayout(), 
    this.viewport, 
    true, 
    1, 
    false, 
    false);

// データラベル用のレイアウトを返す
private getLayout(): dataLabel.ILabelLayout {
    return {
        labelText: (dataGroup: BarChartDataPoint) => {
            return dataLabelUtils.getLabelFormattedText({
                label: dataGroup.value,
                maxWidth: this.viewport.width,
            });
        },
        labelLayout: {
            x: (d: BarChartDataPoint) => {
                return d.x;
            },
            y: (d: BarChartDataPoint) => {
                return d.y;
            }
        },
        filter: (dataGroup: BarChartDataPoint) => {
            return true; // フィルターはせず、すべてのデータを返す
        },
        style: {
            "fill": "black",
            "font-family": "helvetica,arial,sans-serif"
        },
    };
}
```
## getLabelFormattedText

ラベルのフォーマットオプションの取得

```typescript
function getLabelFormattedText(options: LabelFormattedTextOptions): string
```

#### getLabelFormattedText の例

```typescript
import dataLabelUtils = powerbi.extensibility.utils.chart.dataLabel.utils;

let options: LabelFormattedTextOptions = {
    text: 'some text',
    fontFamily: 'sans',
    fontSize: '15',
    fontWeight: 'normal',
};

dataLabelUtils.getLabelFormattedText(options);
```

# まとめ

軸や凡例を描写するのは結構面倒なため、このユーティリティーは使い方を覚えればとても役に立ちます。是非試してください。

[目次に戻る](https://qiita.com/kenakamu/items/f57ab7f73d6ea10bc9d9)

# 参考
[ChartUtils](https://github.com/Microsoft/powerbi-visuals-utils-chartutils)