かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

TypeScriptとd3.jsで折れ線グラフ描いてみた

ということで、これまでの練習の成果という感じで折れ線グラフを1つ描いてみた。

点と線

ということで、これまで、べたっと描いてたのを点をあらわすクラスと線を表すクラスにわけてみた。

// 点
class Point<TX, TY> {
    constructor(public x: TX = null, public y: TY = null) { }
}

// 線
class Line<TX, TY> {
    constructor(public points: Array<Point<TX, TY>> = []) { }
}

そして、Lineの配列を管理して描画するTimelineChartというクラスを作成。drawメソッドに今までやってきたことを使ってSVGでグラフを描画してる。

// 横軸が時間の折れ線グラフ
class TimelineChart {
    constructor(public lines: Array<Line<Date, number>> = []) { }

    draw(position: { x: number; y: number; width: number; height: number; padding: number }, svg: D3.Selection): void {
        // 全データから最小値最大値を取得する
        var minMax = {
            minX: d3.min(this.lines, l => d3.min(l.points, p => p.x)),
            minY: Math.min(0, d3.min(this.lines, l => d3.min(l.points, p => p.y))),
            maxX: d3.max(this.lines, l => d3.max(l.points, p => p.x)),
            maxY: d3.max(this.lines, l => d3.max(l.points, p => p.y))
        };

        // スケール。x軸は時間
        var xScale = d3.time.scale()
            .domain([minMax.minX, minMax.maxX])
            .range([position.padding, position.width - position.padding]);
        var yScale = d3.scale.linear()
            .domain([minMax.minY, minMax.maxY])
            .range([position.height - position.padding, position.padding]);

        // 軸
        // x軸は月/日でラベル表示する
        var xAxis = d3.svg.axis().scale(xScale).tickFormat(d3.time.format("%m/%d"));
        var yAxis = d3.svg.axis().scale(yScale).orient("left");

        // 描画のホストになる要素を作成
        var host = svg.append("g")
            .attr("x", position.x)
            .attr("y", position.y)
            .attr("width", position.width)
            .attr("height", position.height);

        var line = d3.svg.line()
            .x(d => xScale(d.x))
            .y(d => yScale(d.y));

        host.selectAll("path")
            .data(this.lines)
            .enter()
            .append("path")
            .attr("d", l => line(l.points))
            .attr("fill", "none")
            .attr("stroke", "steelblue")
            .attr("stroke-width", 1);

        // 軸の描画
        host.append("g")
            .attr("class", "axis")
            .attr("transform", "translate(0, " + (position.height - position.padding) + ")")
            .call(xAxis);
        host.append("g")
            .attr("class", "axis")
            .attr("transform", "translate(" + position.padding + ", 0)")
            .call(yAxis);
    }
}

目新しいのは、横軸を時間にしたかったのでx軸のデータのスケールをd3.scaleではなくd3.time.scaleから作っているところです。domainに日付を渡してrangeに数値を渡すような感じで使います。

ついでに横軸のラベルの日付は03/01みたいに月/日の形で表示するようにxAisのtickFormat関数を使って指定しています。フォーマットの処理はd3.time.formatにお任せできるので簡単でした。フォーマットに使える書式は、ドキュメントの以下のページを参照しました。

使ってみる

window.onloadで適当なデータを作って描画する処理を書きます。

window.onload = () => {
    var chart = new TimelineChart();
    // テスト用ダミーデータ 03/01~03/10まで一日単位に適当なデータを突っ込む x 3本
    for (var i = 0; i < 3; i++) {
        var l = new Line<Date, number>();
        for (var j = 0; j < 10; j++) {
            l.points.push(new Point<Date, number>(new Date(2014, 2, 1 + j), Math.round(Math.random() * 1000)));
        }
        chart.lines.push(l);
    }

    // svg要素を作成
    var svg = d3.select("#content").append("svg")
        .attr("width", 600)
        .attr("height", 600);

    // 描画
    chart.draw({ x: 10, y: 10, width: 500, height: 500, padding: 50 }, svg);
};

そうすると、以下のような感じで描画されます。線ごとに色情報を持たせるかランダムに色を変えたほうがよさげですね…。

f:id:okazuki:20140211161958p:plain

コード全体

一応コード全体を。

default.htm

<!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="Scripts/d3.v3.min.js"></script>
    <script src="app.js"></script>
</head>
<body>
    <h1>TypeScript HTML App</h1>

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

app.css

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

span {
    font-style: italic;
}

.axis path,
.axis line {
    fill: none;
    stroke: black;
    shape-rendering: crispEdges;
}

.axis text {
    font-family: sans-serif;
    font-size: 10px;
}

app.ts

/// <reference path="scripts/typings/d3/d3.d.ts" />

// 点
class Point<TX, TY> {
    constructor(public x: TX = null, public y: TY = null) { }
}

// 線
class Line<TX, TY> {
    constructor(public points: Array<Point<TX, TY>> = []) { }
}

// 横軸が時間の折れ線グラフ
class TimelineChart {
    constructor(public lines: Array<Line<Date, number>> = []) { }

    draw(position: { x: number; y: number; width: number; height: number; padding: number }, svg: D3.Selection): void {
        // 全データから最小値最大値を取得する
        var minMax = {
            minX: d3.min(this.lines, l => d3.min(l.points, p => p.x)),
            minY: Math.min(0, d3.min(this.lines, l => d3.min(l.points, p => p.y))),
            maxX: d3.max(this.lines, l => d3.max(l.points, p => p.x)),
            maxY: d3.max(this.lines, l => d3.max(l.points, p => p.y))
        };

        // スケール。x軸は時間
        var xScale = d3.time.scale()
            .domain([minMax.minX, minMax.maxX])
            .range([position.padding, position.width - position.padding]);
        var yScale = d3.scale.linear()
            .domain([minMax.minY, minMax.maxY])
            .range([position.height - position.padding, position.padding]);

        // 軸
        // x軸は月/日でラベル表示する
        var xAxis = d3.svg.axis().scale(xScale).tickFormat(d3.time.format("%m/%d"));
        var yAxis = d3.svg.axis().scale(yScale).orient("left");

        // 描画のホストになる要素を作成
        var host = svg.append("g")
            .attr("x", position.x)
            .attr("y", position.y)
            .attr("width", position.width)
            .attr("height", position.height);

        var line = d3.svg.line()
            .x(d => xScale(d.x))
            .y(d => yScale(d.y));

        host.selectAll("path")
            .data(this.lines)
            .enter()
            .append("path")
            .attr("d", l => line(l.points))
            .attr("fill", "none")
            .attr("stroke", "steelblue")
            .attr("stroke-width", 1);

        // 軸の描画
        host.append("g")
            .attr("class", "axis")
            .attr("transform", "translate(0, " + (position.height - position.padding) + ")")
            .call(xAxis);
        host.append("g")
            .attr("class", "axis")
            .attr("transform", "translate(" + position.padding + ", 0)")
            .call(yAxis);
    }
}

window.onload = () => {
    var chart = new TimelineChart();
    // テスト用ダミーデータ 03/01~03/10まで一日単位に適当なデータを突っ込む x 3本
    for (var i = 0; i < 3; i++) {
        var l = new Line<Date, number>();
        for (var j = 0; j < 10; j++) {
            l.points.push(new Point<Date, number>(new Date(2014, 2, 1 + j), Math.round(Math.random() * 1000)));
        }
        chart.lines.push(l);
    }

    // svg要素を作成
    var svg = d3.select("#content").append("svg")
        .attr("width", 600)
        .attr("height", 600);

    // 描画
    chart.draw({ x: 10, y: 10, width: 500, height: 500, padding: 50 }, svg);
};