...ing logging 4.0

はてなブログに移行しました。D言語の話とかいろいろ。

DFL: タイムチャートを描くサンプルコード

std.csvをデータモデルとして受け取って、タイムチャートを描くTimeChartRendererクラスを、dfl.chartモジュールに追加しました。 2023年末にdfl.printingモジュールを作り始めたときからここを目指していましたが、やっとできましたね。

縦軸は信号名、横軸は時間です。 系列ごとのデジタル信号とアナログ信号の別に応じて、表現方法を切り替えます。

CSVの要件

string csv =
    "Time (ms),D1,D2,D3,D4,A1,A2,A3,A4\n" ~
    "0,0,0,0,0,0,0,0,0\n" ~
    "100,1,0,0,0,5,2,10,2\n" ~
    "200,0,1,0,0,6,3,10,-3\n" ~
    "300,1,1,1,0,7,4,9,4\n" ~
    "400,0,0,1,1,8,5,9,-5\n" ~
    "500,1,0,1,1,9,2,8,6\n" ~
    "600,0,0,0,1,8,3,8,-7\n" ~
    "700,1,1,0,1,7,4,7,8\n" ~
    "800,0,1,0,0,6,5,7,-9\n" ~
    "900,1,0,1,0,5,2,6,10\n" ~
    "1000,0,0,1,0,4,3,6,-10\n" ~
    "1100,1,1,1,0,3,4,5,9\n" ~
    "1200,0,1,0,1,2,5,5,-9\n" ~
    "1300,1,0,0,1,1,2,4,8\n" ~
    "1400,0,0,0,1,0,3,4,-8\n";

上のコードに示したものが、図中のタイムチャートの元になっているCSVです。

TimeChartRendererが取り込むCSVは、ヘッダー行が必須です。1行目がヘッダー行として読み込まれます。

ヘッダー行の1列目は、横軸のタイトルとして使われます。

各レコード行の1列目は、横軸(時間軸)の時間として使われます。 2列目以降は、プロットする値として使われます。

コンストラクタ呼び出し

auto _graph = new TimeChartRenderer!(int,int,int,int,int,int,int,int,int)(csv, 15);

TimeChartRenderer(T...)に与えるテンプレートパラメータは、 ヘッダー行(1行目)を除いたレコード部分(2行目以降)のカラムの型を列挙します。 時間軸である1列目の型も含めます。

コンストラクタの引数には、第1引数には文字列を、第2引数にはレコード数を与えます。 ほかのコンストラクタを使うことで描画するレコード範囲を指定できますが、あまり使うことはないでしょう。

設定できる要素

たくさんありますが上の図で使っているものを列挙します。

  • 図の位置
  • 図のマージン
  • 系列ごとのデジタル/アナログの別、色、1つの系列を描く高さ、目盛りの最小値、目盛りの最大値
  • プロットエリアのパディング(上、下、左、右)
  • プロットエリアの枠の色
  • プロットエリアと横軸エリアの隙間
  • 横軸の有無
  • 横軸の目盛りのピクセル
  • 横軸の目盛りのステップ数(いくつのレコードごとに目盛りを打つか)
  • 横軸の目盛りの長さ(内、外)
  • 横軸の目盛りエリアの高さ
  • 縦軸の有無
  • 縦軸の目盛りエリアの幅
  • 各系列のゼロ点を示す線の有無
  • 背景色

サンプルコード

最後の_table関係のコメントアウトを解除すれば、ウィンドウの右の方に表も描画されます。 ウィンドウを最大化しないと表は見えませんのであしからず。

import dfl;

version(Have_dfl) // For DUB.
{
}
else
{
    pragma(lib, "dfl.lib");
}

class MainForm : Form
{
    alias CustomTimeChartRenderer = TimeChartRenderer!(int,int,int,int,int,int,int,int,int);
    CustomTimeChartRenderer _graph;

    alias CustomTableRenderer = TableRenderer!(int,int,int,int,int,int,int,int,int);
    CustomTableRenderer _table;

    public this()
    {
        this.text = "TimeChartRenderer example";
        this.size = Size(600, 650);

        string csv =
            "Time (ms),D1,D2,D3,D4,A1,A2,A3,A4\n" ~
            "0,0,0,0,0,0,0,0,0\n" ~
            "100,1,0,0,0,5,2,10,2\n" ~
            "200,0,1,0,0,6,3,10,-3\n" ~
            "300,1,1,1,0,7,4,9,4\n" ~
            "400,0,0,1,1,8,5,9,-5\n" ~
            "500,1,0,1,1,9,2,8,6\n" ~
            "600,0,0,0,1,8,3,8,-7\n" ~
            "700,1,1,0,1,7,4,7,8\n" ~
            "800,0,1,0,0,6,5,7,-9\n" ~
            "900,1,0,1,0,5,2,6,10\n" ~
            "1000,0,0,1,0,4,3,6,-10\n" ~
            "1100,1,1,1,0,3,4,5,9\n" ~
            "1200,0,1,0,1,2,5,5,-9\n" ~
            "1300,1,0,0,1,1,2,4,8\n" ~
            "1400,0,0,0,1,0,3,4,-8\n";
        _graph = new CustomTimeChartRenderer(csv, 15);
        _graph.location = Point(50, 50);
        _graph.chartMargins = ChartMargins(50, 50, 50, 50);
        _graph.seriesStyleList[0..4] = TimeChartSeriesStyle(true, Color.blue, 20); // Digital
        _graph.seriesStyleList[4..7] = TimeChartSeriesStyle(false, Color.red, 50, 0, 10); // Analog
        _graph.seriesStyleList[7] = TimeChartSeriesStyle(false, Color.red, 100, -10, 20,); // Analog
        _graph.plotAreaTopPadding = 20;
        _graph.plotAreaBottomPadding = 20;
        _graph.plotAreaLeftPadding = 20;
        _graph.plotAreaRightPadding = 20;
        _graph.plotAreaBoundsColor = Color.black;
        _graph.plotAreaAndHorizontalScaleSpanY = 10;
        _graph.hasHorizontalScale = true;
        _graph.horizontalScaleSpan = 20;
        _graph.horizontalScaleStep = 2;
        _graph.horizontalScaleLineInnerSide = 5;
        _graph.horizontalScaleLineOuterSide = 5;
        _graph.horizontalScaleHeight = 20;
        _graph.hasVerticalScale = true;
        _graph.verticalScaleWidth = 40;
        _graph.hasZeroLine = true;
        _graph.backColor = Color.white;

        _table = new CustomTableRenderer(csv, 15);
        _table.location = Point(600, 50);
        _table.hasHeader = true;
        _table.showHeader = true;
        _table.headerLine = true;
        _table.width[] = 50;
    }

    protected override void onPaint(PaintEventArgs e)
    {
        if (_graph)
            _graph.draw(e.graphics);
        // if (_table)
        //     _table.draw(e.graphics);
    }
}

static this()
{
    Application.enableVisualStyles();
}

void main()
{
    Application.run(new MainForm());
}

今後の課題

  • dfl.printingと組み合わせて使う場合、画面と用紙でdpiが全然違うので、画面に丁度いい大きさで表示できていても、印刷したら小さくなってしまう。簡単にスケールを変更できる仕組みが欲しい。

DFLのダウンロード

github.com