TypeScriptでD3.js開発

すっかり更新をしていませんでしたお久しぶりです。

今回は「D3.jsを使用した開発をTypeScriptで始めるまで」を書きます。スタート地点はIDEをインストールするところから、ゴールは簡単なチャートを表示できるまで、にします。

準備

以下4つのソフトウェアだったりライブラリだったりをダウンロード。

  • Visual Studio Express for Web
    Microsoftが提供しているIDEで無償。(アカウントを作る必要はあったかも。)これ一つでTypeScript→JavaScriptへのコンパイルもしてくれる。1)
  • Visual Studio Update 2 (またはUpdate 2以降)
    TypeScript公式サイトのDownloadに「TypeScript 1.4 for VS2013」があるのでその先のサイトからダウンロード。TypeScriptのVersionが上がれば1.4のところも変わるかもです。
  • D3のライブラリ
    D3公式サイトからダウンロード。ダウンロードしなくても公式サイトやCDNのd3.jsを参照する方法もあるのですが、今回はダウンロードしたものを使用します。
  • D3の型定義ファイル
    DefinitelyTypedからD3の型定義ファイルd3.d.tsをダウンロード。TypeScriptでD3(や他のOSSライブラリ)を使用する場合は必ずあった方がよい。このファイルがあるとVisual Studio上でD3を使用するときにメソッド名等々を補完してくれます。

Visual Studio Express for WebとUpdate 2(またはUpdate 2以降)はインストールしてください。

Visual Studioでプロジェクトの作成

TypeScriptのプロジェクトを作成し、D3のライブラリを配置します。

01_createProjectメニューの[新しいプロジェクト]から開いたダイアログに[テンプレート]→[TypeScript]があるのでそれをクリック。(画面参照)
名前/場所/ソリューションは適当につけて下さい。

D3ライブラリの配置

ダウンロードしたd3.jsとd3.d.tsを配置。

02_directories私の場合は左図のようにしています。好き好きがあると思うので自身で管理しやすい構成にしてください。

以上で事前準備は終了です。

作成してみる

簡単なチャートを作りつつTypeScriptで型を指定できる特徴とDefinitelyTypeによってD3の型を使用できる特徴を挙げます。

作成する内容としては「CSVから個人の成績を取得し、個人の平均点を棒グラフとして表示」です。

D3の型を使用する

03_d3_types_autocomplete「D3.」とタイプすると左図のように型の候補が表示されます。d3.d.tsにはD3の各機能がinterfaceとして定義しており、d3.d.tsを取り込んだことによりVisual StodioのAuto Completeで表示できるようになります。

04_d3_types_autocomplete-2D3はD3のPackage(TypeScriptではModule)を表しており、selectAllで返ってくるinterfaceはD3.Selectionである。d3はD3自体を表しており、この点についてはJavaScriptと同じ。

例えばbodyを取得してsvgを追加するコードとすると以下のように記述できるし、これによりAuto Completeも有効になる。

var bodyElement: D3.Selection = d3.select("body");
var svgElement:D3.Selection = bodyElement.append("svg");

 CSVの読み込み及び注意点

以下の成績データを用意しました。

name,english,math
suzuki,92,73
inoue,68,91
watanabe,70,100
motoki,43, 66
ida,60,70

名前と英語及び数学の成績を持っています。score.csvに対応したClassとしてStudentScoreクラスを作成します。

module app {
    export class StudentScore {
        public name: string;
        public english: number;
        public math: number;

        constructor(name: string, english: number, math: number) {
            this.name = name;
            this.english = Number(english);
            this.math = Number(math);
        }

        public get average(): number {
            return (this.english + this.math) / 2;
        }
    }
}

CSVの読み込みは以下のコードになります。

d3.csv(url, (error: any, data: { name:string; english:string; math:string}[]): void => {
    var dataSet: Array<app.StudentScore> = new Array();
    for (var i in data) {
        dataSet.push(new app.StudentScore(data[i].name, Number(data[i].english), Number(data[i].math)));
    }
    this.buildChart(svg, dataSet, width, height);
});

urlはstringで事前にscore.csvのパスを代入しています。csvの2つめの引数はcallbackを指定します。ここでのdataには{name;english;math;}[]と型パラメータを指定しました。受け取ったdataからStudentScoreを作成しています。

コードとしては以下のようにも書けるのですが、StudentScoreの内容からこのコードには問題があります。

d3.csv(url, (error: any, data: app.StudentScore[]): void => {
    var dataSet: Array<app.StudentScore> = new Array();
    this.buildChart(svg, data, width, height);
});

score.csvの内容とStudentScoreの内容があっているので、これでも正しくStudentScoreからscore.csvの内容を読み取ることができます。ただし、計算する場合において不都合が生じます。

StudentScoreのenglishとmathはnumberで定義しているので何の疑いもなく数値計算します。今回で言えば平均です。csvで読み取る内容は文字列であるため、JavaScriptとして動作させた場合、StudentScoreのenglishとmathに入っている値はnumberではなくstringになり正しく動作しません。

CSVで読み込む場合は以下の何れかで対処になります。

  • d3.csvの他の機能を用いて適切に処理するか
  • CSVからの情報を変換するクラスを容易するか
  • csv内で変換する

わざわざ受取りクラスや変換クラスを用意するのは面倒だったので、csv内で変換することにしました。

その他の処理

CSVからStudentScore[]を作成した後は「selectAll → data → enter → append」等々いつものD3の処理です。
JavaScriptの場合と異なるのはfunctionでの引数に型を指定できる点やアロー関数を使用できる点ですね。

g.append("rect")
    .attr("x", 0)
    .attr("y", (d: app.StudentScore): number => {
        return y(d.average);
    })
    .attr("width", width / dataSet.length - 10)
    .attr("height", (d: app.StudentScore): number => {
        return height - 10 - y(d.average);
    })
    .attr("fill", "blue").attr("fill-opacity", 0.5);

チャートを作成する部分をChartクラスとクラス化し、Scriptを実行する部分を以下のようにしまいた。

window.onload = () => {
    var chart: app.Chart = new app.Chart();
    chart.build("#content", "data/score.csv");
    chart.build("#content", "data/score.csv", 600, 200);
};

05_chart_sample 左図が表示される画面になります。見栄えは…さておいてください(笑)。width、heightを指定しない場合はそれぞれ400 、300になります。図中2つ目のチャートは600、200を指定しているので横長になっています。クラス化することで複数のチャートを作成するのにも便利です。

今回はCSVを使用し扱うデータも簡単なものでした。取り扱うデータモデルによっては複雑になることもあるので、そういった場合に型指定ができると戸惑うことなく実装に専念できると思います。

今回作成したのを載せておきます。では。

<!DOCTYPE html>

<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>TypeScript HTML App</title>
    <link rel="stylesheet" href="app.css" type="text/css" />
    <script src="script/lib/d3.js"></script>
    <script src="script/app/Chart.js"></script>
    <script src="script/app/StudentScore.js"></script>
    <script src="app.js"></script>
</head>
<body>
    <h1>TypeScript HTML App</h1>

    <div id="content"></div>
</body>
</html>

 

body {
    font-family: 'Segoe UI', sans-serif;
}

text {
    font-size:12px;
}

svg {
    background-color:#ff8
}

 

window.onload = () => {
    var chart: app.Chart = new app.Chart();
    chart.build("#content", "data/score.csv");
    chart.build("#content", "data/score.csv", 600, 200);
};

 

module app {
    export class StudentScore {
        public name: string;
        public english: number;
        public math: number;

        constructor(name: string, english: number, math: number) {
            this.name = name;
            this.english = english;
            this.math = math;
        }

        public get average(): number {
            return (this.english + this.math) / 2;
        }
    }
}

 

module app {
    export class Chart {

        public build(elementId: string, url: string, width: number = 400, height: number = 300): void {
            var svg: D3.Selection = d3.select(elementId).append("svg").attr("width", width).attr("height", height);
            d3.csv(url, (error: any, data: { name:string; english:string; math:string}[]): void => {
                var dataSet: Array<app.StudentScore> = new Array();
                for (var i in data) {
                    dataSet.push(new app.StudentScore(data[i].name, Number(data[i].english), Number(data[i].math)));
                }
                this.buildChart(svg, dataSet, width, height);
            });
        }

        private buildChart(chart: D3.Selection, dataSet: app.StudentScore[], width: number, height: number): void {
            var y: D3.Scale.LinearScale = d3.scale.linear().domain([0, 100]).range([height - 10, 10]);
            var g:D3.Selection = chart.selectAll("g").data(dataSet).enter().append("g")
                .attr("transform", (d: app.StudentScore, i: number): string => {
                    return "translate(" + (5 + width / dataSet.length * i) + ")";
                });
            g.append("rect")
                .attr("x", 0)
                .attr("y", (d: app.StudentScore): number => {
                    return y(d.average);
                })
                .attr("width", width / dataSet.length - 10)
                .attr("height", (d: app.StudentScore): number => {
                    return height - 10 - y(d.average);
                })
                .attr("fill", "blue").attr("fill-opacity", 0.5);
            g.append("text")
                .attr("y", height - 10)
                .text((d: app.StudentScore): string => { return d.name + " : " + d.average; });
        }

    }
}

 

Footnotes
  1. Eclipse環境ではTypEcsやWebStorm等の選択肢もあるのですが色々な観点で今回はVisual Studio Expressを選択しました。 []