如何在C++項目中優雅地繪制圖表
- matplotlib
- prepare
- matplotlibcpp.h
- python3
- vs configure
- test
- Gnuplot
- prepare
- gnuplot
- gnuplot-iostream.h
- boost
- vs configure
- test
- MathGL
在C++項目中,在進行一些數據分析時往往不夠直觀,若能借助圖表進行分析可以達到事半功倍的效果,通常可以將數據寫出到文本,再在excel或python上進行圖表繪制和分析,那為什么不直接在像python一樣,借助三方庫進行圖表繪制呢?以下將介紹簡單易行的方法,嘗試在C++中優雅地繪制圖表(測試環境:Windows+VS2019)。
matplotlib
實際上是調用Python中的matplotlib提供的C++ API,無需編譯,直接包含頭文件然后在C++中配置相應python環境就可以,對于熟悉python的伙伴來說幾乎沒有學習成本(其本身語法也很簡單)。
prepare
- 可訪問github的網絡
- git(非必須)
- python3(< 3.11)
- vs環境
matplotlibcpp.h
有git的直接clone到本地:
git clone https://github.com/lava/matplotlib-cpp.git
沒有git可以下載代碼或者直接在線復制 matplotlibcpp.h
。
對 matplotlibcpp.h
作如下修改:
新版的
matplotlibcpp.h
中支持了C++17語法,將其改回C++11,且有重定義,需要注釋。
將353行至356行代碼用以下代碼覆蓋:
static_assert(sizeof(long long) == 8, "long type must occupy 8 bytes");
//template <> struct select_npy_type<long long> { const static NPY_TYPES type = NPY_INT64; };
static_assert(sizeof(unsigned long long) == 8, "long type must occupy 8 bytes");
//template <> struct select_npy_type<unsigned long long> { const static NPY_TYPES type = NPY_UINT64; };
python3
matplotlibcpp.h
語法與python3.11以上版本不兼容,報錯如下:
1>matpoltlibTest.cpp
1>D:\Program Files\MatplotlibCpp\matplotlibcpp.h(174): error C4996: 'Py_SetProgramName': deprecated in 3.11
1>d:\programdata\anaconda3\include\pylifecycle.h(37): note: 參見“Py_SetProgramName”的聲明
1>D:\Program Files\MatplotlibCpp\matplotlibcpp.h(182): error C4996: 'PySys_SetArgv': deprecated in 3.11
1>d:\programdata\anaconda3\include\sysmodule.h(13): note: 參見“PySys_SetArgv”的聲明
使用3.11以下的python版本即可,這里我創建一個新的虛擬環境:
(base)>conda create -n Env310 python=3.10.16
(base)>conda activate Env310
(Env310)>conda install matplotlib numpy
將 復制到 D:\Program Files\MatplotlibCpp 下(非必須)。
vs configure
創建一個vs項目,切換為Release x64平臺,在屬性管理器的【Release | 64】下中新增一個屬性表:
雙擊該屬性表,開始配置:
【通用屬性】-【VC++目錄】-【包含目錄】添加:
D:\Program Files\MatplotlibCpp
D:\ProgramData\anaconda3\envs\Env310\include
D:\ProgramData\anaconda3\envs\Env310\Lib\site-packages\numpy\_core\include
注意:不同版本的numpy包含目錄所在路徑可能不同,可通過以下代碼確定:
import numpy
print(numpy.get_include())
【通用屬性】-【VC++目錄】-【庫目錄】添加:
D:\ProgramData\anaconda3\envs\Env310\libs
【通用屬性】-【鏈接器】-【常規】-【附加庫目錄】添加:
D:\ProgramData\anaconda3\envs\Env310
【通用屬性】-【鏈接器】-【輸入】-【附加依賴項】添加:
python3.lib
保存屬性表,在解決方案管理器中右擊,更改屬性:
【配置屬性】-【調試】-【工作目錄】更改為:
D:\ProgramData\anaconda3\envs\Env310
test
添加C++測試代碼,如:
#include <cmath>
#include "matplotlibcpp.h"namespace plt = matplotlibcpp;#define M_PI 3.141592653589793238432643int main()
{// Prepare data.int n = 5000; // number of data pointsstd::vector<double> x(n), y(n);for (int i = 0; i < n; ++i){double t = 2 * M_PI * i / n;x.at(i) = 16 * sin(t) * sin(t) * sin(t);y.at(i) = 13 * cos(t) - 5 * cos(2 * t) - 2 * cos(3 * t) - cos(4 * t);}// plot() takes an arbitrary number of (x,y,format)-triples.// x must be iterable (that is, anything providing begin(x) and end(x)),// y must either be callable (providing operator() const) or iterable.plt::plot(x, y, "r-", x, [](double d){return 12.5 + abs(sin(d));}, "k-");// show plotsplt::show();
}
運行,可得圖片如下,配置完成。
Gnuplot
類似matplotlib,先安裝gnuplot本體,然后通過C++API調用庫,不同的是其可通過命令在終端中使用,需要一定學習成本。
prepare
- 可訪問github的網絡
- git(非必須)
- boost
- vs環境
gnuplot
在官網 可找到最新穩定版下載頁面,下載編譯好的二進制文件。
解壓后長這個樣子:
在bin
文件夾下雙擊打開gnupolt.exe
,輸入測試代碼如下:
plot [-6:6] [-3:3] sin(x),cos(x)
可得:
將整個gunplot
文件夾剪切至D:\Program Files\
下,并重命名為Gnuplot
(個人喜好),然后將D:\Program Files\Gnuplot\bin
添加至系統環境變量Path
中,通過重啟或者其他方式確保環境變量生效(打開任意終端,輸入gunplot未報錯即可)。
那么同樣可以在c++代碼中調用該exe:
#include <stdio.h>void main()
{FILE* pipe = _popen("gnuplot", "w");if (pipe == NULL){exit(-1);}fprintf(pipe, "set terminal wxt size 600, 400\n");fprintf(pipe, "unset border\n");fprintf(pipe, "set dummy u, v\n");fprintf(pipe, "set angles degrees\n");fprintf(pipe, "set parametric\n");fprintf(pipe, "set view 60, 136, 1.22, 1.26\n");fprintf(pipe, "set samples 64, 64\n");fprintf(pipe, "set isosamples 13, 13\n");fprintf(pipe, "set mapping spherical\n");fprintf(pipe, "set style data lines\n");fprintf(pipe, "unset xtics\n");fprintf(pipe, "unset ytics\n");fprintf(pipe, "unset ztics\n");fprintf(pipe, "set title 'Labels colored by GeV plotted in spherical coordinate system'\n");fprintf(pipe, "set urange [ -90.0000 : 90.0000 ] noreverse nowriteback\n");fprintf(pipe, "set vrange [ 0.00000 : 360.000 ] noreverse nowriteback\n");fprintf(pipe, "set xrange [ * : * ] noreverse writeback\n");fprintf(pipe, "set x2range [ * : * ] noreverse writeback\n");fprintf(pipe, "set yrange [ * : * ] noreverse writeback\n");fprintf(pipe, "set y2range [ * : * ] noreverse writeback\n");fprintf(pipe, "set zrange [ * : * ] noreverse writeback\n");fprintf(pipe, "set cblabel 'GeV'\n");fprintf(pipe, "set cbrange [ 0.00000 : 8.00000 ] noreverse nowriteback\n");fprintf(pipe, "set rrange [ * : * ] noreverse writeback\n");fprintf(pipe, "set colorbox user\n");fprintf(pipe, "set colorbox vertical origin screen 0.9, 0.2 size screen 0.02, 0.75 front noinvert bdefault\n");fprintf(pipe, "NO_ANIMATION = 1\n");fprintf(pipe, "splot cos(u)*cos(v),cos(u)*sin(v),sin(u) notitle with lines lt 5\n");fprintf(pipe, "pause mouse\n");_pclose(pipe);
}
結果如下:
但是,以這種方式,如果要批量繪制自定義的數據,仍需要先存數據為文本,然后再讀取,這不是我想要的直接繪制的效果。但是不慌,再做一些簡單配置就可以實現了。
gnuplot-iostream.h
有git的直接clone到本地
git clone https://github.com/dstahlke/gnuplot-iostream.git
沒有git可以下載代碼或者直接在線復制 gnuplot-iostream.h
對 gnuplot-iostream.h
作如下修改
新版的
gnuplot-iostream.h
中支持了C++17語法,將其改回C++11,跟上面的matplotlibcpp.h
是同樣類型的改動
168-169行:
static_assert( is_like_stl_container<std::vector<int>>);
static_assert(!is_like_stl_container<int>);
改為:
static_assert(is_like_stl_container<std::vector<int>>, "Should be STL-like container");
static_assert(!is_like_stl_container<int>, "int is not STL-like container");
186行:
static_assert(is_boost_tuple_nulltype<boost::tuples::null_type>);
改為:
static_assert(is_boost_tuple_nulltype<boost::tuples::null_type>, "Should be boost null type");
197-200行:
static_assert( is_boost_tuple<boost::tuple<int>>);
static_assert( is_boost_tuple<boost::tuple<int, int>>);
static_assert(!is_boost_tuple<std::tuple<int>>);
static_assert(!is_boost_tuple<std::tuple<int, int>>);
改為:
static_assert(is_boost_tuple<boost::tuple<int>>, "Should be boost::tuple");
static_assert(is_boost_tuple<boost::tuple<int, int>>, "Should be boost::tuple");
static_assert(!is_boost_tuple<std::tuple<int>>, "std::tuple is not boost::tuple");
static_assert(!is_boost_tuple<std::tuple<int, int>>, "std::tuple is not boost::tuple");
2436-2437行
static_assert( is_eigen_matrix<Eigen::MatrixXf>);
static_assert(!is_eigen_matrix<int>);
改為
static_assert(is_eigen_matrix<Eigen::MatrixXf>, "Should be Eigen::Matrix");
static_assert(!is_eigen_matrix<int>, "int is not an Eigen::Matrix");
2442行
static_assert(dont_treat_as_stl_container<Eigen::MatrixXf>);
改為
static_assert(dont_treat_as_stl_container<Eigen::MatrixXf>, "Eigen::Matrix should not be treated as an STL container");
然后在上述gnuplot的安裝目錄D:\Program Files\Gnuplot
下新建include
文件夾,將改動后的gnuplot-iostream.h
拷貝過去。
boost
由于gnuplot的C++調用需要boost支持,這里也介紹一下boost的配置,因為有預編譯好的庫,所以也比較easy。
在boost官網的下載頁面找到預編譯好的Windows庫:
根據所需環境選擇對應版本。
這里我根據自己的環境選擇較早的1.80.0 VS140版(注意和自己的環境對應)。
雙擊運行:
更改安裝目錄:
vs configure
創建一個vs項目,切換為Release x64平臺,在屬性中將【平臺工具集】改為Visual Studio 2015 (v140),在屬性管理器的【Release | 64】下中新增一個屬性表【Gnupolt_Release_x64.props】
雙擊該屬性表,開始配置
【通用屬性】-【VC++目錄】-【包含目錄】添加:
D:\Program Files\Gnuplot\include
D:\Program Files\boost_1_80_0\boost
【通用屬性】-【VC++目錄】-【庫目錄】添加:
D:\Program Files\boost_1_80_0\lib64-msvc-14.0
應用,保存屬性表。
test
添加C++測試代碼,如:
#include <cmath>
#include <vector>
#include <gnuplot-iostream.h>#define M_PI 3.141592653589793238432643int main()
{// 初始化 Gnuplot 對象Gnuplot gp;// 設置標題和樣式gp << "set title 'Sharper Heart Shape'\n";gp << "set size ratio -1\n"; // 確保 x 和 y 軸比例一致gp << "set nokey\n"; // 不顯示圖例gp << "set samples 1000\n"; // 增加采樣點使曲線更平滑gp << "set xrange [-20:20]\n"; // 設置 x 軸范圍gp << "set yrange [-20:20]\n"; // 設置 y 軸范圍// 定義參數化方程的數據點std::vector<std::pair<double, double>> points;const int num_points = 1000; // 數據點數量for (int i = 0; i <= num_points; ++i){double t = 2 * M_PI * i / num_points; // 參數 t 從 0 到 2πdouble x = 16 * pow(sin(t), 3); // 使用新的 x 方程double y = (13 * cos(t) - 5 * cos(2 * t) - 2 * cos(3 * t) - cos(4 * t)); // 使用新的 y 方程points.emplace_back(x, y);}// 繪制愛心形狀gp << "plot '-' with lines lc rgb 'red' lw 2 title 'Heart'\n";for (const auto& point : points){gp << point.first << " " << point.second << "\n";}gp << "e\n"; // 結束數據流return 0;
}
運行,可得圖片如下,配置完成。
MathGL
需要特定的環境和復雜的配置,其發布的庫也是Linux平臺下的,在Windows需要和其他依賴庫一起重新編譯。
同樣在官網找到最新版下載入口,這里有兩個版本,我兩個版本都下載并配置了,很不幸都在測試階段報錯了,要么缺少頭文件要么缺依賴庫。雖然可以拿源碼自己重新cmake,但其需要依賴其他可視化庫(背離了簡單易行的初衷),本著世上無難事只要肯放棄的精神,暫停MathGL的測試。
測試代碼:
#include <mgl2/mgl.h>int main()
{mglGraph gr;gr.SetRange('x', -1, 1);gr.SetRange('y', -1, 1);gr.Axis();gr.FPlot("sin(1.7*2*pi*x) + sin(1.9*2*pi*x)", "r-2");gr.WriteFrame("test.png");return 0;
}
mathgl-8.0.3-ucrt64
報錯:
1>D:\Program Files\MathGL2\include\mgl2\data_cf.h(26,10): fatal error C1083: 無法打開包括文件: “gsl/gsl_vector.h”: No such file or directory
mathgl-8.0.3.LGPL-ucrt64
報錯:
1>D:\Program Files\MathGL2\include\mgl2\data.h(27,10): fatal error C1083: 無法打開包括文件: “armadillo”: No such file or directory
打完收工。