numerics 0.1.0
Loading...
Searching...
No Matches
plot.hpp
Go to the documentation of this file.
1/// @file plot/plot.hpp
2/// @brief Matplotlib-style plotting via a gnuplot pipe.
3#pragma once
4
6#include <cstdio>
7#include <stdexcept>
8#include <string>
9#include <utility>
10#include <vector>
11
12namespace num {
13
14using Point = std::pair<double, double>;
15
16struct Series : std::vector<Point> {
17 using std::vector<Point>::vector;
18 void store(double x, double y) { emplace_back(x, y); }
19};
20
21/// @brief Extract one row as \f$(x_j,u_j)\f$ plot data.
22inline Series row_slice(const Vector& u, int N, double h, int row) {
23 Series s;
24 s.reserve(static_cast<std::size_t>(N));
25 for (int j = 0; j < N; ++j) {
26 s.store((j + 1) * h, u[static_cast<std::size_t>(row) * N + j]);
27 }
28 return s;
29}
30
31/// @brief Extract one column as \f$(y_i,u_i)\f$ plot data.
32inline Series col_slice(const Vector& u, int N, double h, int col) {
33 Series s;
34 s.reserve(static_cast<std::size_t>(N));
35 for (int i = 0; i < N; ++i) {
36 s.store((i + 1) * h, u[static_cast<std::size_t>(i) * N + col]);
37 }
38 return s;
39}
40
41inline Series row_slice(const ScalarField2D& g, int row) {
42 return row_slice(g.vec(), g.N(), g.h(), row);
43}
44
45inline Series col_slice(const ScalarField2D& g, int col) {
46 return col_slice(g.vec(), g.N(), g.h(), col);
47}
48
49class Gnuplot {
50public:
51 explicit Gnuplot(const std::string& args = "") {
52 std::string cmd = "gnuplot " + args;
53 pipe_ = popen(cmd.c_str(), "w");
54 if (!pipe_)
55 throw std::runtime_error("could not open gnuplot -- is it installed?");
56 }
58 if (pipe_)
59 pclose(pipe_);
60 }
61 Gnuplot(const Gnuplot&) = delete;
62 Gnuplot& operator=(const Gnuplot&) = delete;
63
64 Gnuplot& operator<<(const std::string& cmd) {
65 fputs(cmd.c_str(), pipe_);
66 return *this;
67 }
68 void send1d(const Series& data) {
69 for (const auto& [x, y] : data)
70 fprintf(pipe_, "%.15g %.15g\n", x, y);
71 fputs("e\n", pipe_);
72 fflush(pipe_);
73 }
74 void flush() { fflush(pipe_); }
75
76private:
77 FILE* pipe_ = nullptr;
78};
79
80/// Apply SIAM-style theme to a raw Gnuplot pipe.
81inline void apply_siam_style(Gnuplot& gp) {
82 gp << "set style line 1 lt 1 lw 2 pt 7 ps 0.8 lc rgb 'black'\n"
83 << "set style line 2 lt 2 lw 2 pt 5 ps 0.8 lc rgb 'black'\n"
84 << "set style line 3 lt 3 lw 2 pt 9 ps 0.8 lc rgb 'black'\n"
85 << "set style line 4 lt 4 lw 2 pt 13 ps 0.8 lc rgb 'black'\n"
86 << "set style line 5 lt 5 lw 2 pt 11 ps 0.8 lc rgb 'black'\n"
87 << "set style line 6 lt 6 lw 2 pt 15 ps 0.8 lc rgb 'black'\n"
88 << "set style line 100 lt 1 lw 0.5 lc rgb '#cccccc'\n"
89 << "set grid back ls 100\n"
90 << "set border 3 lw 1.5\n"
91 << "set tics nomirror\n"
92 << "set key top left Left reverse samplen 3 spacing 1.2\n"
93 << "set key box lt 1 lw 0.5\n";
94}
95
96inline void set_loglog(Gnuplot& gp) {
97 gp << "set logscale xy\nset format x '10^{%L}'\nset format y '10^{%L}'\n";
98}
99inline void set_logx(Gnuplot& gp) {
100 gp << "set logscale x\nset format x '10^{%L}'\n";
101}
102inline void save_png(Gnuplot& gp, const std::string& filename, int w = 900, int h = 600) {
103 gp << "set terminal pngcairo size " + std::to_string(w) + "," + std::to_string(h)
104 + " enhanced font 'Arial,11'\n"
105 << "set output '" + filename + "'\n";
106}
107
108namespace plt {
109namespace detail {
110
111struct SeriesEntry {
112 Series data;
113 std::string label;
114 std::string style; // gnuplot "with" clause, e.g. "lines"
115};
116
117/// 2-D field snapshot for heatmap rendering via gnuplot pm3d map.
118struct HeatmapEntry {
119 std::vector<double> data; // NxN row-major values
120 int N = 0;
121 double h = 1.0;
122 double vmin = 0.0;
123 double vmax = 1.0;
124};
125
126struct Panel {
127 std::vector<SeriesEntry> series;
128 std::vector<HeatmapEntry> heatmaps;
129 std::string title_, xlabel_, ylabel_;
130 std::string xrange_, yrange_;
131 std::string palette_; // gnuplot palette string; empty = hot/fire
132 bool legend_ = false;
133 bool logx_ = false;
134 bool logy_ = false;
135};
136
137struct State {
138 Panel current;
139 std::vector<Panel> panels; // accumulated panels in multiplot mode
140 int mp_rows_ = 0, mp_cols_ = 0; // 0 = single-plot mode
141
142 void reset() { *this = State{}; }
143};
144
145inline State& state() {
146 static State s;
147 return s;
148}
149
150// Write all datablocks for a panel, then emit the plot command for that panel.
151// block_offset: index of the first $d_N block allocated to this panel.
152inline void write_panel(FILE* pipe, const Panel& p, int block_offset) {
153 if (p.series.empty() && p.heatmaps.empty())
154 return;
155
156 // Common decorators
157 if (!p.title_.empty())
158 fprintf(pipe, "set title '%s'\n", p.title_.c_str());
159 else
160 fputs("unset title\n", pipe);
161 if (!p.xlabel_.empty())
162 fprintf(pipe, "set xlabel '%s'\n", p.xlabel_.c_str());
163 else
164 fputs("unset xlabel\n", pipe);
165 if (!p.ylabel_.empty())
166 fprintf(pipe, "set ylabel '%s'\n", p.ylabel_.c_str());
167 else
168 fputs("unset ylabel\n", pipe);
169
170 if (!p.heatmaps.empty()) {
171 // Heatmap panel rendered with pm3d map.
172 const auto& hm = p.heatmaps[0];
173 if (!p.palette_.empty())
174 fprintf(pipe, "set palette %s\n", p.palette_.c_str());
175 else
176 fputs("set palette defined "
177 "(0 'white', 0.35 '#ffffb2', 0.65 '#fd8d3c', 1 '#bd0026')\n",
178 pipe);
179 fprintf(pipe, "set cbrange [%g:%g]\n", hm.vmin, hm.vmax);
180 fputs("set pm3d map\n", pipe);
181 fputs("set size ratio 1\n", pipe);
182 if (!p.xrange_.empty())
183 fprintf(pipe, "set xrange %s\n", p.xrange_.c_str());
184 else
185 fputs("set xrange [*:*]\n", pipe);
186 if (!p.yrange_.empty())
187 fprintf(pipe, "set yrange %s\n", p.yrange_.c_str());
188 else
189 fputs("set yrange [*:*]\n", pipe);
190 fputs("unset key\n", pipe);
191 fprintf(pipe, "splot $d_%d with pm3d notitle\n", block_offset);
192 } else {
193 // Line-plot panel
194 fputs("unset pm3d\n", pipe);
195 if (!p.xrange_.empty())
196 fprintf(pipe, "set xrange %s\n", p.xrange_.c_str());
197 else
198 fputs("set xrange [*:*]\n", pipe);
199 if (!p.yrange_.empty())
200 fprintf(pipe, "set yrange %s\n", p.yrange_.c_str());
201 else
202 fputs("set yrange [*:*]\n", pipe);
203
204 if (p.logx_ && p.logy_) {
205 fputs("set logscale xy\nset format x '10^{%L}'\nset format y "
206 "'10^{%L}'\n",
207 pipe);
208 } else if (p.logx_) {
209 fputs("set logscale x\nset format x '10^{%L}'\n", pipe);
210 } else if (p.logy_) {
211 fputs("set logscale y\nset format y '10^{%L}'\n", pipe);
212 } else {
213 fputs("unset logscale\n", pipe);
214 }
215
216 if (p.legend_) {
217 fputs("set key top right Left reverse samplen 3 spacing 1.2\n"
218 "set key box lt 1 lw 0.5\n",
219 pipe);
220 } else {
221 fputs("unset key\n", pipe);
222 }
223
224 fputs("plot ", pipe);
225 for (std::size_t i = 0; i < p.series.size(); ++i) {
226 if (i)
227 fputs(", ", pipe);
228 const auto& e = p.series[i];
229 fprintf(pipe,
230 "$d_%d with %s ls %zu",
231 block_offset + (int)i,
232 e.style.c_str(),
233 i + 1);
234 if (!e.label.empty())
235 fprintf(pipe, " title '%s'", e.label.c_str());
236 else
237 fputs(" notitle", pipe);
238 }
239 fputc('\n', pipe);
240 }
241}
242
243inline void flush_to(FILE* pipe, const std::string& outfile) {
244 auto& s = state();
245
246 // Collect all panels (push current last)
247 std::vector<Panel> all = s.panels;
248 all.push_back(s.current);
249
250 bool multiplot = (s.mp_rows_ > 0);
251
252 // Terminal
253 if (outfile.empty()) {
254 int h = multiplot ? 300 * s.mp_rows_ : 600;
255 fprintf(pipe, "set terminal qt size 900,%d\n", h);
256 } else {
257 std::string ext = outfile.size() > 4 ? outfile.substr(outfile.size() - 4) : "";
258 if (ext == ".pdf") {
259 double h = multiplot ? 3.0 * s.mp_rows_ : 4.0;
260 fprintf(pipe, "set terminal pdfcairo size 6,%.0f font 'Arial,11'\n", h);
261 } else {
262 int h = multiplot ? 350 * s.mp_rows_ : 600;
263 fprintf(pipe, "set terminal pngcairo size 900,%d enhanced font 'Arial,11'\n", h);
264 }
265 fprintf(pipe, "set output '%s'\n", outfile.c_str());
266 }
267
268 // Global theme
269 fputs("set style line 1 lt 1 lw 2 pt 7 ps 0.7 lc rgb '#2c3e50'\n", pipe);
270 fputs("set style line 2 lt 2 lw 2 pt 5 ps 0.7 lc rgb '#c0392b'\n", pipe);
271 fputs("set style line 3 lt 3 lw 2 pt 9 ps 0.7 lc rgb '#2980b9'\n", pipe);
272 fputs("set style line 4 lt 4 lw 2 pt 13 ps 0.7 lc rgb '#27ae60'\n", pipe);
273 fputs("set style line 5 lt 5 lw 2 pt 11 ps 0.7 lc rgb '#8e44ad'\n", pipe);
274 fputs("set style line 100 lt 1 lw 0.5 lc rgb '#cccccc'\n", pipe);
275 fputs("set grid back ls 100\n", pipe);
276 fputs("set border 3 lw 1.5\n", pipe);
277 fputs("set tics nomirror\n", pipe);
278
279 // Write all datablocks up front (required for multiplot; harmless for
280 // single)
281 int block = 0;
282 for (const auto& p : all) {
283 for (const auto& e : p.series) {
284 fprintf(pipe, "$d_%d << EOD\n", block++);
285 for (const auto& [x, y] : e.data)
286 fprintf(pipe, "%.15g %.15g\n", x, y);
287 fputs("EOD\n", pipe);
288 }
289 for (const auto& hm : p.heatmaps) {
290 fprintf(pipe, "$d_%d << EOD\n", block++);
291 for (int i = 0; i < hm.N; ++i) {
292 double xi = (i + 1) * hm.h;
293 for (int j = 0; j < hm.N; ++j)
294 fprintf(pipe,
295 "%.8g %.8g %.8g\n",
296 xi,
297 (j + 1) * hm.h,
298 hm.data[static_cast<std::size_t>(i) * hm.N + j]);
299 fputs("\n", pipe);
300 }
301 fputs("EOD\n", pipe);
302 }
303 }
304
305 if (multiplot) {
306 fprintf(pipe,
307 "set multiplot layout %d,%d spacing 0.08,0.12\n",
308 s.mp_rows_,
309 s.mp_cols_);
310 int off = 0;
311 for (const auto& p : all) {
312 write_panel(pipe, p, off);
313 off += (int)p.series.size() + (int)p.heatmaps.size();
314 }
315 fputs("unset multiplot\n", pipe);
316 } else {
317 write_panel(pipe, all.back(), 0);
318 }
319
320 fflush(pipe);
321}
322
323} // namespace detail
324
325// -- Series builders ----------------------------------------------------------
326
327/// Append a Series (vector of (x,y) pairs) to the current panel.
328inline void plot(const Series& data,
329 const std::string& label = "",
330 const std::string& style = "lines") {
331 detail::state().current.series.push_back({data, label, style});
332}
333
334/// Append parallel x and y vectors to the current panel.
335inline void plot(const std::vector<double>& x,
336 const std::vector<double>& y,
337 const std::string& label = "",
338 const std::string& style = "lines") {
339 Series s;
340 s.reserve(x.size());
341 for (std::size_t i = 0; i < x.size() && i < y.size(); ++i)
342 s.emplace_back(x[i], y[i]);
343 detail::state().current.series.push_back({std::move(s), label, style});
344}
345
346// -- Decorators ---------------------------------------------------------------
347
348inline void title(const std::string& t) {
349 detail::state().current.title_ = t;
350}
351inline void xlabel(const std::string& l) {
352 detail::state().current.xlabel_ = l;
353}
354inline void ylabel(const std::string& l) {
355 detail::state().current.ylabel_ = l;
356}
357
358/// Set x-axis range, e.g. xlim(0, 10).
359inline void xlim(double lo, double hi) {
360 detail::state().current.xrange_ =
361 "[" + std::to_string(lo) + ":" + std::to_string(hi) + "]";
362}
363/// Set y-axis range.
364inline void ylim(double lo, double hi) {
365 detail::state().current.yrange_ =
366 "[" + std::to_string(lo) + ":" + std::to_string(hi) + "]";
367}
368
369/// Show a legend using the labels passed to plot().
370inline void legend() {
371 detail::state().current.legend_ = true;
372}
373
374/// Log-log axes.
375inline void loglog() {
376 detail::state().current.logx_ = detail::state().current.logy_ = true;
377}
378/// Log y-axis only.
379inline void semilogy() {
380 detail::state().current.logy_ = true;
381}
382/// Log x-axis only.
383inline void semilogx() {
384 detail::state().current.logx_ = true;
385}
386
387// -- 2-D heatmap --------------------------------------------------------------
388
389/// Add a 2-D heatmap to the current panel.
390/// @tparam Container Any type with .data() and .size() (num::Vector,
391/// std::vector<double>, etc.)
392/// @param u NxN row-major field values
393/// @param N Grid side length
394/// @param h Grid spacing (node (i,j) lives at ((i+1)*h, (j+1)*h))
395/// @param vmin Lower bound of the colour scale (default 0)
396/// @param vmax Upper bound of the colour scale (default 1)
397template<typename Container>
398inline void heatmap(const Container& u,
399 int N,
400 double h,
401 double vmin = 0.0,
402 double vmax = 1.0) {
403 detail::HeatmapEntry e;
404 e.data.assign(u.data(), u.data() + u.size());
405 e.N = N;
406 e.h = h;
407 e.vmin = vmin;
408 e.vmax = vmax;
409 detail::state().current.heatmaps.push_back(std::move(e));
410}
411
412inline void heatmap(const ScalarField2D& g, double vmin = 0.0, double vmax = 1.0) {
413 heatmap<ScalarField2D>(g, g.N(), g.h(), vmin, vmax);
414}
415
416/// Override the gnuplot palette for the current panel's heatmap.
417/// @param palette A gnuplot palette definition string, e.g.
418/// "defined (0 'blue', 1 'red')" or "rgbformulae 33,13,10"
419inline void colormap(const std::string& palette) {
420 detail::state().current.palette_ = palette;
421}
422
423// -- Multiplot ----------------------------------------------------------------
424
425/// @brief Start a multiplot with the given grid dimensions.
426inline void subplot(int rows, int cols = 1) {
427 detail::state().reset();
428 detail::state().mp_rows_ = rows;
429 detail::state().mp_cols_ = cols;
430}
431
432/// @brief Advance to the next panel.
433inline void next() {
434 detail::state().panels.push_back(detail::state().current);
435 detail::state().current = detail::Panel{};
436}
437
438// -- Output -------------------------------------------------------------------
439
440/// Open an interactive gnuplot window; blocks until the window is closed.
441/// Resets figure state afterwards.
442inline void show() {
443 FILE* pipe = popen("gnuplot", "w");
444 if (!pipe)
445 throw std::runtime_error("could not open gnuplot -- is it installed?");
446 detail::flush_to(pipe, "");
447 fputs("pause mouse close\n", pipe);
448 fflush(pipe);
449 pclose(pipe);
450 detail::state().reset();
451}
452
453/// Save the figure to a file (PNG or PDF inferred from extension).
454/// Resets figure state afterwards.
455inline void savefig(const std::string& filename) {
456 FILE* pipe = popen("gnuplot", "w");
457 if (!pipe)
458 throw std::runtime_error("could not open gnuplot -- is it installed?");
459 detail::flush_to(pipe, filename);
460 fflush(pipe);
461 pclose(pipe);
462 detail::state().reset();
463}
464
465/// Clear the current figure (discard all series and settings).
466inline void clf() {
467 detail::state().reset();
468}
469
470} // namespace plt
471} // namespace num
Gnuplot(const std::string &args="")
Definition plot.hpp:51
void flush()
Definition plot.hpp:74
Gnuplot & operator=(const Gnuplot &)=delete
void send1d(const Series &data)
Definition plot.hpp:68
Gnuplot(const Gnuplot &)=delete
Gnuplot & operator<<(const std::string &cmd)
Definition plot.hpp:64
void set_loglog(Gnuplot &gp)
Definition plot.hpp:96
Series col_slice(const Vector &u, int N, double h, int col)
Extract one column as plot data.
Definition plot.hpp:32
void apply_siam_style(Gnuplot &gp)
Apply SIAM-style theme to a raw Gnuplot pipe.
Definition plot.hpp:81
void save_png(Gnuplot &gp, const std::string &filename, int w=900, int h=600)
Definition plot.hpp:102
constexpr real e
Definition math.hpp:44
std::pair< double, double > Point
Definition plot.hpp:14
Series row_slice(const Vector &u, int N, double h, int row)
Extract one row as plot data.
Definition plot.hpp:22
void set_logx(Gnuplot &gp)
Definition plot.hpp:99
Scalar field on a 2D uniform interior grid.
void store(double x, double y)
Definition plot.hpp:18