Skip to content

Commit e226058

Browse files
committed
core: add interactive preview, auto-layout heuristics, crosshair option, and fan/violin uncertainty helpers
1 parent dde01bf commit e226058

4 files changed

Lines changed: 116 additions & 1 deletion

File tree

include/gnuplotpp/plot.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ struct FigureSpec {
6565
bool share_x = false;
6666
bool share_y = false;
6767
bool hide_inner_tick_labels = false;
68+
bool auto_layout = true;
69+
bool interactive_preview = false;
6870
};
6971

7072
/** @brief Legend placement presets. */
@@ -127,6 +129,7 @@ struct AxesSpec {
127129
bool grid = false;
128130
bool legend = true;
129131
LegendSpec legend_spec{};
132+
bool enable_crosshair = false;
130133

131134
bool has_xlim = false;
132135
double xmin = 0.0;

include/gnuplotpp/statistics.hpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,30 @@ void percentile_band(const std::vector<std::vector<double>>& ensemble,
4040
std::vector<double>& low,
4141
std::vector<double>& high);
4242

43+
/**
44+
* @brief Build multiple percentile ribbons for fan-chart visualization.
45+
* @param ensemble Row-major series vectors with equal length.
46+
* @param quantiles Ascending quantiles in (0,1), e.g. {0.1,0.25,0.75,0.9}.
47+
* @param lows Output low bands matching each ribbon.
48+
* @param highs Output high bands matching each ribbon.
49+
*/
50+
void fan_chart_bands(const std::vector<std::vector<double>>& ensemble,
51+
const std::vector<double>& quantiles,
52+
std::vector<std::vector<double>>& lows,
53+
std::vector<std::vector<double>>& highs);
54+
55+
/**
56+
* @brief Approximate violin density profile.
57+
* @param samples Input values.
58+
* @param y_grid Output y positions.
59+
* @param half_width Output normalized half-width density [0,1].
60+
* @param points Number of y-grid points.
61+
*/
62+
void violin_profile(std::span<const double> samples,
63+
std::vector<double>& y_grid,
64+
std::vector<double>& half_width,
65+
std::size_t points = 120);
66+
4367
/**
4468
* @brief Simple moving average.
4569
* @param y Input signal.

src/gnuplot_backend.cpp

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,25 @@ void emit_typed_objects(std::ostream& os, const AxesSpec& axis_spec) {
375375
}
376376
}
377377

378+
void emit_auto_layout(std::ostream& os, const FigureSpec& spec, const AxesSpec& axis_spec) {
379+
if (!spec.auto_layout) {
380+
return;
381+
}
382+
// Heuristic margins in character units for single-panel readability.
383+
const std::size_t yl = axis_spec.ylabel.size();
384+
const std::size_t y2l = axis_spec.y2label.size();
385+
const std::size_t xl = axis_spec.xlabel.size();
386+
const std::size_t tl = axis_spec.title.size();
387+
const int lmargin = static_cast<int>(std::clamp<std::size_t>(8 + yl / 2, 8, 16));
388+
const int rmargin = static_cast<int>(std::clamp<std::size_t>(4 + y2l / 2, 4, 14));
389+
const int bmargin = static_cast<int>(std::clamp<std::size_t>(4 + xl / 10, 4, 8));
390+
const int tmargin = static_cast<int>(std::clamp<std::size_t>(2 + tl / 18, 2, 6));
391+
os << "set lmargin " << lmargin << "\n";
392+
os << "set rmargin " << rmargin << "\n";
393+
os << "set bmargin " << bmargin << "\n";
394+
os << "set tmargin " << tmargin << "\n";
395+
}
396+
378397
void emit_plot_body(std::ostream& os,
379398
const Figure& fig,
380399
const std::vector<std::vector<std::filesystem::path>>& data_files) {
@@ -422,6 +441,7 @@ void emit_plot_body(std::ostream& os,
422441
for (std::size_t axis_idx = 0; axis_idx < all_axes.size(); ++axis_idx) {
423442
const auto& axis = all_axes[axis_idx];
424443
const auto& axis_spec = axis.spec();
444+
emit_auto_layout(os, spec, axis_spec);
425445

426446
os << "unset title\n";
427447
os << "unset xlabel\n";
@@ -440,7 +460,12 @@ void emit_plot_body(std::ostream& os,
440460

441461
const bool legend_enabled = axis_spec.legend && axis_spec.legend_spec.enabled;
442462
if (legend_enabled) {
443-
os << "set key " << legend_position_token(axis_spec.legend_spec.position) << "\n";
463+
LegendPosition legend_pos = axis_spec.legend_spec.position;
464+
if (spec.auto_layout && axis.series().size() >= 4 &&
465+
legend_pos == LegendPosition::TopRight) {
466+
legend_pos = LegendPosition::OutsideRight;
467+
}
468+
os << "set key " << legend_position_token(legend_pos) << "\n";
444469
os << "set key maxcols " << std::max(1, axis_spec.legend_spec.columns) << "\n";
445470
os << "set key " << (axis_spec.legend_spec.opaque ? "opaque" : "noopaque") << "\n";
446471
os << "set key box linewidth " << (axis_spec.legend_spec.boxed ? "0.5" : "0.0") << "\n";
@@ -474,6 +499,12 @@ void emit_plot_body(std::ostream& os,
474499
if (axis_spec.grid || spec.style.grid) {
475500
os << "set grid xtics ytics linewidth 0.35 linecolor rgb '#e2e2e2'\n";
476501
}
502+
if (axis_spec.enable_crosshair) {
503+
os << "set mouse\n";
504+
os << "set mxtics 4\n";
505+
os << "set mytics 4\n";
506+
os << "set grid mxtics mytics linewidth 0.25 linecolor rgb '#f0f0f0'\n";
507+
}
477508
if (axis_spec.xlog) {
478509
os << "set logscale x\n";
479510
}
@@ -705,6 +736,19 @@ RenderResult GnuplotBackend::render(const Figure& fig, const std::filesystem::pa
705736
return result;
706737
}
707738

739+
if (fig.spec().interactive_preview) {
740+
const auto preview_path = out_dir / "tmp" / "interactive_preview.gp";
741+
std::ofstream preview_os(preview_path);
742+
if (preview_os.is_open()) {
743+
preview_os << "set encoding utf8\n";
744+
preview_os << "set terminal qt persist\n";
745+
emit_plot_body(preview_os, fig, data_files);
746+
preview_os << "pause mouse close\n";
747+
} else {
748+
gnuplotpp::log::Warn("failed to write interactive preview script: ", preview_path.string());
749+
}
750+
}
751+
708752
const std::string check_cmd = "command -v " + executable_ + " >/dev/null 2>&1";
709753
if (std::system(check_cmd.c_str()) != 0) {
710754
result.ok = false;

src/statistics.cpp

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,50 @@ void percentile_band(const std::vector<std::vector<double>>& ensemble,
8484
}
8585
}
8686

87+
void fan_chart_bands(const std::vector<std::vector<double>>& ensemble,
88+
const std::vector<double>& quantiles,
89+
std::vector<std::vector<double>>& lows,
90+
std::vector<std::vector<double>>& highs) {
91+
lows.clear();
92+
highs.clear();
93+
if (quantiles.size() < 2 || quantiles.size() % 2 != 0) {
94+
return;
95+
}
96+
const std::size_t half = quantiles.size() / 2;
97+
lows.resize(half);
98+
highs.resize(half);
99+
for (std::size_t i = 0; i < half; ++i) {
100+
const double ql = quantiles[i];
101+
const double qh = quantiles[quantiles.size() - 1 - i];
102+
percentile_band(ensemble, ql, qh, lows[i], highs[i]);
103+
}
104+
}
105+
106+
void violin_profile(std::span<const double> samples,
107+
std::vector<double>& y_grid,
108+
std::vector<double>& half_width,
109+
std::size_t points) {
110+
y_grid.clear();
111+
half_width.clear();
112+
if (samples.empty() || points < 2) {
113+
return;
114+
}
115+
const auto [it_min, it_max] = std::minmax_element(samples.begin(), samples.end());
116+
const double y_min = *it_min;
117+
const double y_max = *it_max;
118+
y_grid.resize(points);
119+
for (std::size_t i = 0; i < points; ++i) {
120+
y_grid[i] = y_min + (y_max - y_min) * static_cast<double>(i) / static_cast<double>(points - 1);
121+
}
122+
half_width = gaussian_kde(samples, y_grid);
123+
const double peak = *std::max_element(half_width.begin(), half_width.end());
124+
if (peak > 0.0) {
125+
for (double& v : half_width) {
126+
v /= peak;
127+
}
128+
}
129+
}
130+
87131
std::vector<double> moving_average(std::span<const double> y, std::size_t window) {
88132
if (y.empty() || window == 0) {
89133
return {};

0 commit comments

Comments
 (0)