作った(嘘)1。
もう更新することはないと思っていたC++用GnuplotライブラリADAPT-GPM2であるが、データ分析ライブラリADAPTへ取り込むことでメンテナンスを再開した。
C++には既にMatplot++というライブラリがあり、私もこの一年ほどはあちらを導入しようと色々な調整を行っていたのだが、残念ながら様々な問題に直面したので断念し、昔の自作ライブラリに戻ってきたのである。
元々あったGitHubリポジトリはアーカイブし、今後はデータ分析ライブラリADAPTの一機能としてメンテナンスを続ける。ADAPTの一部として配布することになるので、クローンすると余計なコードがくっついてきてしまうが、#include <OpenADAPT/Plot/Canvas.h>
のようにプロット機能だけをincludeすることは可能だ。
使い方
以前のものと使い方はほぼ同じだが、統合に伴い名前空間やクラス名、内部実装などを整理している。若干コードの書き方が変わったので、以前のサンプルコードを現バージョン用に書き直したものを残しておく。とはいえ表面的にはそれほど大きな変化はなく、GPM2
という名称が削除されたことくらいだ。名前空間adapt::gpm2
がなくなってadapt
単独になり、GPMCanvas2D
などのクラス名からGPM
が消えてCanvas2D
のように短縮された程度。
あとは細かいところで言えば、plot::color = "light-red"
、plot::pointtype = 7
、plot::style = Style::steps
のようなオプション指定の長たらしさ分かりにくさは気に食わない部分の一つだったので、plot::c_light_red
、plot::pt_fcir
、plot::s_steps
などのように短縮版を用意した。従来通りの書き方でも記述が少々長い以外に問題はない。
ここに乗せていないサンプルについてはExamplesを参照。
例.1 散布図、可変サイズ
int main() { std::vector<double> longitudes{ 141.3469, 140.74, 141.1526, 140.8694, 140.1023, 140.3633, 140.4676, 140.4468, 139.8836, 139.0608, 139.6489, 140.1233, 139.6917, 139.6423, 139.0235, 137.2113, 136.6256, 136.2219, 138.5684, 138.1812, 136.7223, 138.3828, 136.9066, 136.5086, 135.8686, 135.7556, 135.5023, 135.183, 135.8048, 135.1675, 134.2383, 133.0505, 133.9344, 132.4553, 131.4714, 134.5594, 134.0434, 132.7657, 133.5311, 130.4017, 130.3009, 129.8737, 130.7417, 131.6126, 131.4202, 130.5581, 127.6809 }; std::vector<double> latitudes{ 43.0642, 40.8244, 39.7036, 38.2682, 39.7186, 38.2404, 37.7503, 36.3418, 36.5658, 36.3911, 35.8569, 35.6051, 35.6895, 35.4475, 37.9026, 36.6953, 36.5944, 36.0652, 35.6642, 36.6513, 35.3912, 34.9756, 35.1802, 34.7303, 35.0045, 35.021, 34.6937, 34.6913, 34.6851, 34.226, 35.5036, 35.4723, 34.6618, 34.3853, 34.1858, 34.0658, 34.3402, 33.8416, 33.5597, 33.5902, 33.2635, 32.7448, 32.7898, 33.2382, 31.9077, 31.5602, 26.2124 }; std::vector<double> populations{ 5250, 1230, 1220, 2330, 970, 1070, 1840, 2860, 1940, 1930, 7330, 6290, 13960, 9200, 2200, 1040, 1140, 770, 810, 2030, 1970, 3630, 7550, 1790, 1410, 2580, 8820, 5450, 1310, 930, 550, 670, 1890, 2810, 1320, 720, 960, 1340, 690, 5100, 810, 1280, 1720, 1130, 1080, 1590, 1450 }; auto pop_size = populations | std::views::transform([](double x) { return x / 1000; }); namespace plot = adapt::plot; adapt::Canvas2D g("example_scatter.png"); g.SetXRange(128, 150); g.SetYRange(29, 46); g.SetSizeRatio(1); g.SetXLabel("longitude"); g.SetYLabel("latitude"); g.SetTitle("Japan population distribution"); // world_10m.txt can be downloaded from https://gnuplotting.org/plotting-the-world-revisited/ g.PlotPoints("PlotExamples/world_10m.txt", "1", "2", plot::notitle, plot::c_dark_gray, plot::s_lines). PlotPoints(longitudes, latitudes, plot::notitle, plot::s_points, plot::pt_fcir, plot::color_rgb = "0xAA6688FF", plot::variable_size = pop_size); return 0; }
例.2 関数、エラーバー
#include <random> #include <OpenADAPT/Plot/Canvas.h> int main() { std::string norm = std::to_string(250. / std::sqrt(2 * 3.1415926535)); std::string equation = norm + "*exp(-x*x/2)"; std::mt19937_64 mt(0); std::normal_distribution<> nd(0., 1.); std::vector<double> x1(32, 0); std::vector<double> y1(32, 0); std::vector<double> e1(32); for (int i = 0; i < 1000; ++i) { double x = nd(mt); if (x < -4.0 || x >= 4.0) continue; ++y1[static_cast<size_t>(std::floor(x / 0.25) + 16)]; } for (int i = 0; i < 32; ++i) { x1[i] = i * 0.25 - 4. + 0.125; e1[i] = std::sqrt(y1[i]); } namespace plot = adapt::plot; adapt::Canvas2D g("example_2d.png"); //g.ShowCommands(true); g.SetTitle("example\\_2d"); g.SetXRange(-4.0, 4.0); g.SetXLabel("x"); g.SetYLabel("y"); g.PlotPoints(equation, plot::title = "mu = 0, sigma = 1", plot::s_lines). PlotPoints(x1, y1, plot::xerrorbar = 0.125, plot::yerrorbar = e1, plot::title = "data", plot::c_black, plot::s_points, plot::pt_fcir, plot::ps_med_small); return 0; }
例.3 カラーマップ、ベクトル、等高線
#include <thread> #include <OpenADAPT/Plot/Canvas.h> double calc_r(double x, double y) { return std::sqrt(x * x + y * y); }; double potential(double x, double y) { double r1 = calc_r(x - 3, y); double r2 = calc_r(x + 3, y); if (r1 == 0.) r1 = 0.000001; if (r2 == 0.) r2 = 0.000001; double p1 = 1 / r1; double p2 = 3 / r2; return p1 - p2; }; double fieldx(double x, double y) { double f1 = (x - 3) / std::pow(calc_r(x - 3, y), 3); double f2 = 3 * (x + 3) / std::pow(calc_r(x + 3, y), 3); return f1 - f2; } double fieldy(double x, double y) { double f1 = y / std::pow(calc_r(x - 3, y), 3); double f2 = 3 * y / std::pow(calc_r(x + 3, y), 3); return f1 - f2; } int main() { adapt::Matrix<double> m(100, 100); std::pair<double, double> xrange = { -9.9, 9.9 }; std::pair<double, double> yrange = { -9.9, 9.9 }; for (int iy = -50; iy < 50; ++iy) { double y = iy * 0.2 + 0.1; for (int ix = -50; ix < 50; ++ix) { double x = ix * 0.2 + 0.1; m[ix + 50][iy + 50] = potential(x, y); } } std::vector<double> xfrom(441), yfrom(441), xlen(441), ylen(441); std::vector<double> arrowcolor(441); for (int iy = -10; iy <= 10; ++iy) { for (int ix = -10; ix <= 10; ++ix) { size_t jx = (ix + 10); size_t jy = (iy + 10); double xlen_ = fieldx(ix, iy); double ylen_ = fieldy(ix, iy); double len = std::sqrt(xlen_ * xlen_ + ylen_ * ylen_); xlen_ = xlen_ / len * 0.8; ylen_ = ylen_ / len * 0.8; xlen[jy * 21 + jx] = xlen_; ylen[jy * 21 + jx] = ylen_; xfrom[jy * 21 + jx] = ix - xlen_ / 2.; yfrom[jy * 21 + jx] = iy - ylen_ / 2.; arrowcolor[jy * 21 + jx] = potential(ix - xlen_ / 2., iy - ylen_ / 2.); } } namespace plot = adapt::plot; { std::string output_filename = "example_colormap.png"; adapt::MultiPlot multi(output_filename, 1, 2, 1200, 600); adapt::CanvasCM g1(output_filename + ".map_tmp"); g1.SetTitle("example\\_colormap"); g1.SetPaletteDefined({ {0, "yellow" }, { 4.5, "red" }, { 5., "black" }, { 5.5, "blue"}, { 10, "cyan" } }); g1.SetSizeRatio(-1); g1.SetXLabel("x"); g1.SetYLabel("y"); g1.SetXRange(-10, 10); g1.SetYRange(-10, 10); g1.SetCBRange(-5, 5); g1.PlotColormap(m, xrange, yrange, plot::notitle). PlotVectors(xfrom, yfrom, xlen, ylen, plot::notitle, plot::c_white); //sleep for a short time to avoid the output image broken by multiplot. std::this_thread::sleep_for(std::chrono::milliseconds(300)); adapt::CanvasCM g2(output_filename + ".cntr_tmp"); g2.SetTitle("example\\_contour"); g2.SetPaletteDefined({ {0, "yellow" }, { 4.5, "red" }, { 5., "black" }, { 5.5, "blue"}, { 10, "cyan" } }); g2.SetSizeRatio(-1); g2.SetXLabel("x"); g2.SetYLabel("y"); g2.SetXRange(-10, 10); g2.SetYRange(-10, 10); g2.SetCBRange(-5, 5); g2.PlotColormap(m, xrange, yrange, plot::notitle, plot::with_contour, plot::without_surface, plot::variable_cntrcolor, plot::cntrlevels_incremental = { -20., 0.2, 20. }). PlotVectors(xfrom, yfrom, xlen, ylen, plot::notitle, plot::variable_color = arrowcolor); } return 0; }
例.4 日付ラベル、塗りつぶし
#include <ranges> #include <random> #include <ctime> #include <chrono> #include <OpenADAPT/Plot/Canvas.h> int GetTested(int d) { static std::random_device rd; static std::mt19937_64 mt(rd()); double a = d * d * 0.05; double b = a * std::pow((std::sin(2 * 3.141592 * d / 120) + 1) / 2, 3) + d; return b <= 0 ? 0 : std::poisson_distribution<>(b)(mt); } int GetPositive(int t) { static std::random_device rd; static std::mt19937_64 mt(rd()); return t == 0 ? 0 : std::binomial_distribution<>(t, 0.05)(mt); } std::pair<int, int> GetDate(int a) { static constexpr std::array<int, 12> x = { 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 }; int m = 0; int d = 0; int p = 0; for (int i = 0; i < 12; ++i) { if (a + 1 <= x[i]) { m = i + 1, d = a + 1 - p; break; }; p = x[i]; } return { m, d }; } struct Accumulator { int operator()(int t) const { return sum += t; } mutable int sum; }; int main() { std::vector<int> yr(366); std::iota(yr.begin(), yr.end(), 0); auto x = std::views::all(yr) | std::views::transform([](int a) { auto [m, d] = GetDate(a); return std::format("2020-{}-{}", m, d); }); auto y = std::views::all(yr) | std::views::transform([](int a) { return GetTested(a); }); auto y2 = y | std::views::transform([](int t) { return GetPositive(t); }); auto y3 = y | std::views::transform(Accumulator{}); auto y4 = y2 | std::views::transform(Accumulator{}); namespace plot = adapt::plot; adapt::Canvas2D g("example_datetime.png"); g.SetXTicsRotate(-45); g.SetTitle("example\\_datetime"); g.SetYLabel("per day"); g.SetY2Label("total"); g.SetKeyTopRight(); g.SetKeyOpaque(); g.SetKeyBox(); g.SetXDataTime("%Y-%m-%d"); g.SetFormatX("%02m/%02d"); g.PlotPoints(x, y, plot::title = "tested", plot::s_steps, plot::fillsolid = 0.5). PlotPoints(x, y2, plot::title = "positive", plot::s_steps, plot::fillsolid = 0.5). PlotLines(x, y3, plot::title = "tested\\_total", plot::ax_x1y2, plot::lw_ex_thick). PlotLines(x, y4, plot::title = "positive\\_total", plot::ax_x1y2, plot::lw_ex_thick); return 0; }
メンテナンス再開の経緯
私は2018年頃にADAPT-GPM2の制作を開始し、2019年にはGitHubで公開した。まあ鳴かず飛ばずで、使ってくれた人は数えるほどしかいなかったと思われるが、私個人が必要とする機能を十分に実装したし、満足していた。
そんな折、2020年頃にMatplot++というC++用の本格的なプロットライブラリが登場した。傍目にはかなりしっかり作り込まれ、Examplesも充実しており、これは私も導入できるのではと期待を寄せていた。自作したことはしたが、別に自作したかったわけではなく他に真っ当な選択肢がなかったから仕方なく作ったのであって、もっと優れたサードパーティ製ライブラリが出てきてメンテナンスの手間から開放されるのなら願ったり叶ったりだった。
そして2023年の暮れに本格的に導入し、およそ1年間使い続け、私はMatplot++を捨てることに決めた。……見た目ほど優れたライブラリではなかった。ああいや、ADAPT-GPM2よりは多機能だし便利だと思う。が、私にとっては些か使いにくかった。任意のrangeに対応すると謳っているくせに実質的にはstd::vector<double>
しか受け付けない、1000万個くらいのデータ点をプロットすると凄まじく遅い、PNG画像の取り扱いなどに難点がある、多数の画像を出力したい場合にいちいち既存のaxes_handleを個々に削除してfigure_handleを使い回す必要があり手間がかかる、など。細かい不満を挙げていくときりがない。
特に致命的な問題だったrangeについてはIssueを投げてみたものの、開発者はすでに半ばメンテナンスを放棄した状態で、時間がないから無理だと断られてしまった。もちろんOSS開発は個人の善意によって成り立つもので、メンテナンスできない作者を責めるつもりはない。が、不具合だらけなのにメンテナンスされていないライブラリを使い続けられるほどこちらも我慢強くはないのだ。
ちなみに、私は使ったことがないが、sciplotという類似のライブラリも同時期に出てきている。ただしこちらももう、プルリクエストを反映する以上のメンテナンスは3年以上行われていないようである。Matplot++の教訓に乗っ取るのであれば、こちらも導入は厳しいだろう。
そういうわけで、少なくとも自分で使う限りは殆ど不満の出なかった自作ライブラリに戻ってくることにしたのである。
もともとADAPT-GPM2はその名前の通りADAPTの一部として開発したもので、Gnuplotラッパーの部分だけを切り抜いてOSSにしていたのだ。2024年1月にADAPT本体をOSS化したことに伴い、ADAPT-GPM2も切り離して管理する必要がなくなったので、それならばと統合することにした。
せっかくC++20に対応させるならと、若干のコード修正も行った。キーワード引数を使ったオプション指定は、どうせならC++20で対応した指示付き初期化を使おうと思ったのだが、いくつかの難点があり断念した。例えば指示付き初期化は基底クラスのメンバ変数初期化をシンプルに書き下せない。本ライブラリではグラフ描画用オプションをキーワード引数として実装していたのだが、グラフの種類(点、線、塗りつぶしなど)でそれぞれ対応する範囲が微妙に異なり、それを基底-派生の関係で表現していた。が、指示付き初期化では事実上クラスを派生させられないので、オプションの整理が大変になる。また初期化子の順序を守らなければならない点も厄介だった。Gnuplotでオプション指定順序に悩まされた身としては、この仕様は受け入れがたかった(まあコンパイルエラーが出るから明らか、という考え方もできるが……)。このあたりの話は前回の記事に詳しく書いている。
このため、キーワード引数は過去に使っていた実装をC++20で若干修正しつつ残すことにした。こちらのほうが遥かに使い勝手が良い、と判断した。結果的にキーワード短縮記法なども実現したので、まあ悪くはなかった。
今後の更新予定としては、一次元/二次元ヒストグラムなどは私自身が頻繁に使うので遠からず実装されるだろう。
その他、当時は色々と拡張性を考慮して設けておいた機能のうち、ほとんど使わなかったものや、ADAPT本体と統合したことで不要になったものなども、切り捨てて整理しているところだ。いずれPythonバインディングを作ることを想定してType erased rangeみたいなものを設けるなど色々工夫していたが、実用していた4年あまりの間、有意義に働くことはほとんどなかった。
これらを少しずつ整備していきたい。仕事に追われているのでいつになるかは分からないが。
-
昔の記事の書き直しのようなものなので、以前の記事と似たタイトルを付けたいと思ったのだが、詐欺臭くなってしまった。
でも大抵こういう釣りタイトルのほうがPV稼ぎやすいよね。↩