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