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が全然違うので、画面に丁度いい大きさで表示できていても、印刷したら小さくなってしまう。簡単にスケールを変更できる仕組みが欲しい。