C#::大量のデータをグラフ化したいと思います
Excel2003では表示しきれないような大量のCSVデータがあったので、SQLiteに突っ込んでNPlotでグラフ化してみたよ、という話。
使うものはVC#2008(Express), System.Data.SQLite, NPlot。
NPlotいいよ。軸ドラッグしてスケールを変えられたりするしね。
本題は長くなるので「続きを読む」でどうぞ・・・
CSVデータを読み込む。どうせ実験データのログだし変なものなど混ざっていないとタカをくくってざっくりざっくり
//using System.Data.SQLite; var stream = new StreamReader("dat.csv"); var dbh = new SQLiteConnection("Data Source=\"dat.sqlite\""); var tr = dbh.BeginTransaction(); var cmd = dbh.CreateCommand(); cmd.CommandText = "CREATE TABLE dat (" + string.Join(",", (from col in reader.ReadLine().Split(',') select col + " REAL") .ToArray()) + ")"; cmd.ExecuteNonQuery(); while(!stream.EndOfStream){ var line = stream.ReadLine(); if(line.Contains(',')){ cmd.CommandText = "INSERT INTO dat VALUES(" + line + ")"; cmd.ExecuteNonQuery(); } } tr.Commit();
こんな感じで強引にSQLiteのデータベースに突っ込んで、
class PlotData : IList SQLiteConnection dbh; string name; int count; public PlotData(SQLiteConnection dbh, string name){ this.dbh = dbh; this.name= name; count = -1; } public int Count { get { if (count >= 0) return count; using (var cmd = dbh.CreateCommand()){ cmd.CommandText = string.Format("SELECT count(*) FROM dat"); count = (int)(Int64)cmd.ExecuteScalar(); } return count; } } public double this[int index] { get { using (var cmd = dbh.CreateCommand()){ cmd.CommandText = string.Format("SELECT {0} FROM dat LIMIT {1},1", name, index); return (double) cmd.ExecuteScalar(); } } } /* その他IListのメンバーはほぼ未実装 */ }
こういうクラスを作って(上記はイメージです。実際には効率化のためにキャッシュ処理などが組み込まれています)、
//どこかのフォームのコンストラクタあたりにNPlotのコントロール(surface)を貼って・・・ //surface is NPlot.Windows.PlotSurface2D(); public void GraphSetup(IEnumerable<object> items){ surface.RightMenu = NPlot2D.PlotSurface2D.DefaultContextMenu; surface.Clear(); surface.AddInteraction(new NPlot.Windows.PlotSurface2D.Interactions.AxisDrag(true)); surface.AddInteraction(new NPlot.Windows.PlotSurface2D.Interactions.HorizontalDrag()); surface.AddInteraction(new NPlot.Windows.PlotSurface2D.Interactions.VerticalDrag()); foreach(var data in items) { var plot = new LinePlot{ DataSource = data }; surface.Add(plot); } //最初に表示される値を0番目から100番目に制限 surface.XAxis1.WorldMin = 0; surface.XAxis1.WorldMax = 100; }
こんな感じで描画したんだけど、表示されているデータに対して反応がクソ重い。
いくらなんでも異常だと思ったのでNPlotのソースを読んでみると、LinePlotの中で再描画のたびにインデクサで0〜Count-1のすべての値に対して4回アクセスしていることが判明。データ量が増えるとそりゃー表示範囲に関わらず重たいわ。
問題の箇所は、次の部分で・・・
//http://nplot.svn.sourceforge.net/viewvc/nplot/trunk/src/LinePlot.cs?revision=35&view=markup //LinePlot.cs rev53 line:153-158 // do horizontal clipping here, to speed up if ((dx1 < leftCutoff && dx2 < leftCutoff) || (rightCutoff < dx1 && rightCutoff < dx2)) { continue; }
この親ブロックがループになっていて、dx1,dx2を求めるためにインデクサにアクセスします。
スピードアップのために水平クリッピングを行うとか書いてる割にやってることはcontinueなので、Countが大きくなったりインデクサの処理に時間がかかる(今回みたいにSQLを発行したりするとなー)と、ここでモッサリモッサリと処理時間を食うわけですな。
で、対策。ここを書き換えて(右側に入ったらbreakするだけでもかなり違う)DLLをビルドするのも手ではありますが、ここはまぁ同じような機能の高速な別クラスを作るぐらいでいいんじゃないでしょうか。インターフェース二つ分の実装を書くだけですし。とはいえソースコードを丸コピー・・・するのはライセンス的にアレな感じですし、300行ちょっとしかないクラスなのでこのぐらいなら互換クラスを自分で1から書いたほうが読みやすそうですし・・・というわけで、新しく描画クラスを起こして、描画範囲を二分探索で絞り込むように書いて、めでたく完成となりました。
ドラッグするとグリグリ動くグラフというのは見ていて無駄に楽しいです。Excelにはない楽しさ。