LoginSignup
1
0

More than 3 years have passed since last update.

Power BI カスタムビジュアル開発 : 積み上げ棒グラフの開発 : 動的な文字列長計算と書式設定

Last updated at Posted at 2020-03-17

前回でほぼグラフの描写が終わりました。今回は凡例のアイテムが変わった場合にうまく描写する方法と書式設定を見ていきます。

ハードコードの課題

現在凡例の場所はハードコードされているため、商品名が入らない、もしくはスペースが無駄になる場合があります。
image.png
また X 軸、Y 軸のフォントサイズもハードコードされていて変更できません。これらを解消していきます。

文字列の長さを計算

powerbi-visuals-utils-formattingutils にある機能を利用できます。

1. モジュールをインストール。

npm install powerbi-visuals-utils-formattingutils

2. visual.ts に import を追加。

import { textMeasurementService } from "powerbi-visuals-utils-formattingutils";

3. update 関数で legends を取得したコードの下に、以下コードを追加。

  • フォントサイズとフォントファミリーを指定して幅を予測
  • circle や余白のために 40 追加
let legendTextWidth = textMeasurementService.measureSvgTextWidth({
    text: legends
        .slice()
        .sort((a, b) => b.length - a.length)[0],
    fontSize: "16px",
    fontFamily: "'Segoe UI',wf_segoe-ui_normal,helvetica,arial,sans-serif"
});

4. マージンを変更。

let margin = { top: 0, bottom: 30, left: 40, right: legendTextWidth + 40 }

5. カスタムビジュアルで動作確認。

[すべてを選択した場合]
image.png
[3つだけ選択した場合]
image.png

書式

次に X 軸、Y 軸、凡例のフォントサイズを指定できるようにします。

1. capabilities.json に objects を追加。

{
    "dataRoles": [
        {
            "displayName": "軸",
            "name": "axis",
            "kind": "Grouping"
        },
        {
            "displayName": "凡例",
            "name": "legend",
            "kind": "Grouping"
        },
        {
            "displayName": "値",
            "name": "measure",
            "kind": "Measure"
        }
    ],
    "objects": {
        "fontSettings": {
            "displayName": "フォント設定",
            "properties": {
                "fontSize": {
                    "displayName": "フォントサイズ",
                    "type": {
                        "formatting": {
                            "fontSize": true
                        }
                    }
                },
                "fontColor": {
                    "displayName": "フォントの色",
                    "type": {
                        "fill": {
                            "solid": {
                                "color": true
                            }
                        }
                    }
                }
            }
        }
    },
    "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"
                            }
                        }
                    ]
                }
            }
        }
    ]
}

2. 対応するように src/settings.ts を更新。

"use strict";

import { dataViewObjectsParser } from "powerbi-visuals-utils-dataviewutils";
import DataViewObjectsParser = dataViewObjectsParser.DataViewObjectsParser;

export class VisualSettings extends DataViewObjectsParser {
  public fontSettings: fontSettings = new fontSettings();
}

export class fontSettings {
  public fontColor: string = "";
  public fontSize: number = 16;
}

3. visual.ts で設定を取得。update 関数の一番初めに以下コードを追加。

this.settings = Visual.parseSettings(options.dataViews[0]);

4. ハードコードされている部分を改修。最終的な update 関数は以下の通り。

public update(options: VisualUpdateOptions) {
    debugger;
    this.settings = Visual.parseSettings(options.dataViews[0]);
    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 legendTextWidth = textMeasurementService.measureSvgTextWidth({
        text: legends
            .slice()
            .sort((a, b) => b.length - a.length)[0],
        fontSize: `${this.settings.fontSettings.fontSize}px`,
        fontFamily: "'Segoe UI',wf_segoe-ui_normal,helvetica,arial,sans-serif"
    });

    let yTextWidth = textMeasurementService.measureSvgTextWidth({
        text: viewModel.maxValue.toString(),
        fontSize: `${this.settings.fontSettings.fontSize}px`,
        fontFamily: "'Segoe UI',wf_segoe-ui_normal,helvetica,arial,sans-serif"
    });

    let width = options.viewport.width;
    let height = options.viewport.height;
    let margin = { top: 0, bottom: 30, left: yTextWidth+20, right: legendTextWidth + 40 }

    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());

    let xAxis = g => g
        .attr("transform", `translate(0,${height - margin.bottom})`)
        .style("color", this.settings.fontSettings.fontColor)
        .style("font-size", this.settings.fontSettings.fontSize)
        .attr("fill", this.settings.fontSettings.fontColor)
        .call(d3.axisBottom(x).tickSizeOuter(0))
        .call(g => g.selectAll(".domain").remove());
    this.xAxis.call(xAxis);

    let yAxis = g => g
        .attr("transform", `translate(${margin.left},0)`)
        .style("color", this.settings.fontSettings.fontColor)
        .style("font-size", this.settings.fontSettings.fontSize)
        .attr("fill", this.settings.fontSettings.fontColor)
        .call(d3.axisLeft(y).ticks(null, "s"))
        .call(g => g.selectAll(".domain").remove());
    this.yAxis.call(yAxis);

    let legend = svg => {
        let g = svg
            .attr("transform", `translate(${width - margin.right}, 0)`)
            .selectAll("g")
            .data(legends.reverse())
            .join("g")
            .attr("transform", (d, i) => `translate(0,${i *
                (Number.parseInt(this.settings.fontSettings.fontSize.toString()) + 4)})`)

        g.call(g => g.selectAll("circle").remove());
        g.call(g => g.selectAll("text").remove());

        g.append("circle")
            .attr("cx", 20)
            .attr("cy", 10)
            .attr("r", 5)
            .attr("fill", d => color(d));

        g.append("text")
            .attr("fill", this.settings.fontSettings.fontColor)
            .attr("font-size", this.settings.fontSettings.fontSize)
            .attr("x", 30)
            .attr("y", 9.5)
            .attr("dy", "0.35em")
            .text(d => d);
    }
    this.legend.call(legend);
}

5. 動作確認。
image.png

まとめ

今回で積み上げ棒グラフを作ってみるシリーズは終わりです。他のグラフとのインタラクションやハイライトなど、自由に追加してみてください。

目次へ戻る

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0