Skip to content

Commit f29025c

Browse files
committed
core: add pre-render validation and DataTable column checks
1 parent d39381b commit f29025c

4 files changed

Lines changed: 120 additions & 1 deletion

File tree

include/gnuplotpp/data.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "gnuplotpp/plot.hpp"
44

55
#include <filesystem>
6+
#include <span>
67
#include <string>
78
#include <unordered_map>
89
#include <vector>
@@ -21,6 +22,13 @@ struct DataTable {
2122
/** @brief Returns true when a named column exists. */
2223
bool has_column(const std::string& name) const;
2324

25+
/**
26+
* @brief Validate that all requested columns exist.
27+
* @param names Required column names.
28+
* @throws std::out_of_range with available column list when missing.
29+
*/
30+
void require_columns(std::span<const std::string> names) const;
31+
2432
/** @brief Returns row count; throws when columns have inconsistent lengths. */
2533
std::size_t row_count() const;
2634

include/gnuplotpp/plot.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,13 @@ class IPlotBackend {
445445
const std::filesystem::path& out_dir) = 0;
446446
};
447447

448+
/**
449+
* @brief Validate figure/series invariants before backend rendering.
450+
* @param fig Figure to validate.
451+
* @return `ok=true` when valid, otherwise InvalidInput with message.
452+
*/
453+
RenderResult validate_figure_for_render(const Figure& fig);
454+
448455
/**
449456
* @brief Figure container with a fixed subplot grid and backend delegation.
450457
*/

src/data.cpp

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,32 @@
77

88
namespace gnuplotpp {
99

10+
namespace {
11+
12+
std::string available_columns(const std::unordered_map<std::string, std::vector<double>>& cols) {
13+
std::vector<std::string> names;
14+
names.reserve(cols.size());
15+
for (const auto& [name, _] : cols) {
16+
names.push_back(name);
17+
}
18+
std::sort(names.begin(), names.end());
19+
std::string out;
20+
for (std::size_t i = 0; i < names.size(); ++i) {
21+
out += names[i];
22+
if (i + 1 < names.size()) {
23+
out += ", ";
24+
}
25+
}
26+
return out;
27+
}
28+
29+
} // namespace
30+
1031
const std::vector<double>& DataTable::column(const std::string& name) const {
1132
const auto it = columns.find(name);
1233
if (it == columns.end()) {
13-
throw std::out_of_range("column not found: " + name);
34+
throw std::out_of_range("column not found: " + name + " (available: " +
35+
available_columns(columns) + ")");
1436
}
1537
return it->second;
1638
}
@@ -19,6 +41,15 @@ bool DataTable::has_column(const std::string& name) const {
1941
return columns.find(name) != columns.end();
2042
}
2143

44+
void DataTable::require_columns(std::span<const std::string> names) const {
45+
for (const auto& name : names) {
46+
if (!has_column(name)) {
47+
throw std::out_of_range("required column missing: " + name + " (available: " +
48+
available_columns(columns) + ")");
49+
}
50+
}
51+
}
52+
2253
std::size_t DataTable::row_count() const {
2354
if (columns.empty()) {
2455
return 0;

src/figure.cpp

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,50 @@ AxesSpec normalize_axes_spec(AxesSpec spec) {
2626
return spec;
2727
}
2828

29+
RenderResult invalid_input(std::string msg) {
30+
return RenderResult{
31+
.ok = false, .status = RenderStatus::InvalidInput, .message = std::move(msg)};
32+
}
33+
34+
RenderResult validate_series(const SeriesData& s,
35+
const std::size_t axis_idx,
36+
const std::size_t series_idx) {
37+
if (s.spec.has_opacity && (s.spec.opacity < 0.0 || s.spec.opacity > 1.0)) {
38+
return invalid_input("axis " + std::to_string(axis_idx) + " series " +
39+
std::to_string(series_idx) + ": opacity must be in [0,1]");
40+
}
41+
if (s.x.size() != s.y.size()) {
42+
return invalid_input("axis " + std::to_string(axis_idx) + " series " +
43+
std::to_string(series_idx) + ": x/y size mismatch");
44+
}
45+
switch (s.spec.type) {
46+
case SeriesType::Band:
47+
if (s.y2.size() != s.x.size()) {
48+
return invalid_input("axis " + std::to_string(axis_idx) + " series " +
49+
std::to_string(series_idx) + ": band y2 size mismatch");
50+
}
51+
break;
52+
case SeriesType::ErrorBars:
53+
if (s.yerr_low.size() != s.x.size() || s.yerr_high.size() != s.x.size()) {
54+
return invalid_input("axis " + std::to_string(axis_idx) + " series " +
55+
std::to_string(series_idx) +
56+
": errorbar low/high size mismatch");
57+
}
58+
break;
59+
case SeriesType::Heatmap:
60+
if (s.z.size() != s.x.size()) {
61+
return invalid_input("axis " + std::to_string(axis_idx) + " series " +
62+
std::to_string(series_idx) + ": heatmap z size mismatch");
63+
}
64+
break;
65+
case SeriesType::Line:
66+
case SeriesType::Scatter:
67+
case SeriesType::Histogram:
68+
break;
69+
}
70+
return RenderResult{};
71+
}
72+
2973
} // namespace
3074

3175
void Axes::set(AxesSpec spec) { spec_ = normalize_axes_spec(std::move(spec)); }
@@ -155,11 +199,40 @@ RenderResult Figure::save(const std::filesystem::path& out_dir) const {
155199
.message =
156200
"No backend configured. Call set_backend() before save()."};
157201
}
202+
const auto valid = validate_figure_for_render(*this);
203+
if (!valid.ok) {
204+
return valid;
205+
}
158206
return backend_->render(*this, out_dir);
159207
}
160208

161209
void Figure::set_backend(std::shared_ptr<IPlotBackend> backend) {
162210
backend_ = std::move(backend);
163211
}
164212

213+
RenderResult validate_figure_for_render(const Figure& fig) {
214+
const auto& spec = fig.spec();
215+
if (spec.rows <= 0 || spec.cols <= 0) {
216+
return invalid_input("rows and cols must be positive");
217+
}
218+
if (spec.formats.empty()) {
219+
return invalid_input("no output formats requested");
220+
}
221+
const auto& axes = fig.all_axes();
222+
const auto expected = static_cast<std::size_t>(spec.rows * spec.cols);
223+
if (axes.size() != expected) {
224+
return invalid_input("axes count does not match rows*cols");
225+
}
226+
for (std::size_t ai = 0; ai < axes.size(); ++ai) {
227+
const auto& axis = axes[ai];
228+
for (std::size_t si = 0; si < axis.series().size(); ++si) {
229+
const auto sres = validate_series(axis.series()[si], ai, si);
230+
if (!sres.ok) {
231+
return sres;
232+
}
233+
}
234+
}
235+
return RenderResult{};
236+
}
237+
165238
} // namespace gnuplotpp

0 commit comments

Comments
 (0)