Skip to content

Commit 72d53a5

Browse files
committed
add per-series opacity/color controls and monte-carlo alpha example
1 parent 2d1b4d6 commit 72d53a5

9 files changed

Lines changed: 225 additions & 9 deletions

File tree

CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ if(BUILD_EXAMPLES)
6565

6666
add_executable(three_line_ieee_example examples/three_line_ieee_example.cpp)
6767
target_link_libraries(three_line_ieee_example PRIVATE gnuplotpp::gnuplotpp)
68+
69+
add_executable(monte_carlo_alpha_example examples/monte_carlo_alpha_example.cpp)
70+
target_link_libraries(monte_carlo_alpha_example PRIVATE gnuplotpp::gnuplotpp)
6871
endif()
6972

7073
if(BUILD_TESTING)

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,10 @@ flowchart LR
6161
When using `IEEE_SingleColumn` or `IEEE_DoubleColumn`, renderer behavior is tightened to print-safe defaults:
6262

6363
- 8.5 pt Times-style text defaults
64-
- monochrome plotting (`set monochrome`)
64+
- monochrome plotting by default (`set monochrome`)
6565
- dashed line-style differentiation per series (`dt 1..N`)
66-
- SciencePlots-like axis defaults: inward mirrored ticks, visible minor ticks, thin 0.5 axis/grid strokes
66+
- per-series color/opacity overrides disable global monochrome automatically
67+
- SciencePlots-like axis defaults: visible minor ticks, thin 0.5 axis/grid strokes
6768
- vector-first outputs (`PDF`, `SVG`, `EPS`)
6869

6970
## Examples
@@ -72,6 +73,7 @@ When using `IEEE_SingleColumn` or `IEEE_DoubleColumn`, renderer behavior is tigh
7273
./build/dev-debug/two_window_example --out out/two_window
7374
./build/dev-debug/layout_2x2_example --out out/layout_2x2
7475
./build/dev-debug/three_line_ieee_example --out out/three_line_ieee
76+
./build/dev-debug/monte_carlo_alpha_example --out out/monte_carlo_alpha
7577
```
7678

7779
### Annotated IEEE Example

docs/PLOT_CONTROLS.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ s.type = gnuplotpp::SeriesType::Line; // Line, Scatter, ErrorBars, Band
7373
s.label = "SRIF";
7474
s.has_line_width = true;
7575
s.line_width_pt = 1.6;
76+
s.has_color = true;
77+
s.color = "#000000"; // RGB hex
78+
s.has_opacity = true;
79+
s.opacity = 0.15; // [0,1], converted to ARGB for gnuplot
7680
```
7781

7882
## Data and Rendering
@@ -91,6 +95,28 @@ auto result = fig.save("out/my_run/figures");
9195
9296
## Figure Recipes
9397
98+
### 0) Monte-Carlo Density (Many Lines, No Legend, Low Opacity)
99+
100+
```cpp
101+
gnuplotpp::AxesSpec ax;
102+
ax.legend = false;
103+
ax.grid = true;
104+
fig.axes(0).set(ax);
105+
106+
for (int k = 0; k < 1000; ++k) {
107+
gnuplotpp::SeriesSpec s;
108+
s.type = gnuplotpp::SeriesType::Line;
109+
s.label = "";
110+
s.has_line_width = true;
111+
s.line_width_pt = 0.5;
112+
s.has_color = true;
113+
s.color = "#000000";
114+
s.has_opacity = true;
115+
s.opacity = 0.08;
116+
fig.axes(0).add_series(s, t, y_mc[k]);
117+
}
118+
```
119+
94120
### 1) Single IEEE Plot (1x1)
95121

96122
```cpp
@@ -152,8 +178,20 @@ For each render:
152178
- `tmp/ax*_series*.dat`
153179
- `tmp/gnuplot.log`
154180
181+
## Object Transparency (Native gnuplot Commands)
182+
183+
Use `AxesSpec::gnuplot_commands` for transparent filled objects:
184+
185+
```cpp
186+
ax.gnuplot_commands = {
187+
"set object 1 rect from graph 0.05,0.80 to graph 0.35,0.95 "
188+
"fc rgb '#000000' fs transparent solid 0.10 noborder"
189+
};
190+
```
191+
155192
## Existing End-to-End Examples
156193

157194
- `examples/two_window_example.cpp`
158195
- `examples/layout_2x2_example.cpp`
159196
- `examples/three_line_ieee_example.cpp`
197+
- `examples/monte_carlo_alpha_example.cpp`
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#include "gnuplotpp/gnuplot_backend.hpp"
2+
#include "gnuplotpp/logging.hpp"
3+
#include "gnuplotpp/plot.hpp"
4+
#include "gnuplotpp/presets.hpp"
5+
6+
#include <filesystem>
7+
#include <random>
8+
#include <string>
9+
#include <vector>
10+
11+
int main(int argc, char** argv) {
12+
using namespace gnuplotpp;
13+
14+
std::filesystem::path out_dir = "out/monte_carlo_alpha_example";
15+
for (int i = 1; i < argc; ++i) {
16+
const std::string arg = argv[i];
17+
if (arg == "--out" && i + 1 < argc) {
18+
out_dir = argv[++i];
19+
}
20+
}
21+
22+
FigureSpec fs;
23+
fs.preset = Preset::IEEE_DoubleColumn;
24+
apply_preset_defaults(fs);
25+
fs.rows = 1;
26+
fs.cols = 1;
27+
fs.formats = {OutputFormat::Pdf, OutputFormat::Svg, OutputFormat::Eps};
28+
fs.title = "Monte Carlo Ensemble (N=1000)";
29+
30+
Figure fig(fs);
31+
32+
AxesSpec ax;
33+
ax.xlabel = "t [s]";
34+
ax.ylabel = "x(t)";
35+
ax.grid = true;
36+
ax.legend = false;
37+
fig.axes(0).set(ax);
38+
39+
std::vector<double> t;
40+
t.reserve(220);
41+
for (int i = 0; i < 220; ++i) {
42+
t.push_back(0.1 * static_cast<double>(i));
43+
}
44+
45+
std::mt19937_64 rng(123456ULL);
46+
std::normal_distribution<double> noise(0.0, 0.12);
47+
for (int k = 0; k < 1000; ++k) {
48+
std::vector<double> y;
49+
y.reserve(t.size());
50+
51+
double state = 0.0;
52+
for (double x_sample : t) {
53+
(void)x_sample;
54+
state = 0.985 * state + noise(rng);
55+
y.push_back(state);
56+
}
57+
58+
SeriesSpec s;
59+
s.type = SeriesType::Line;
60+
s.has_line_width = true;
61+
s.line_width_pt = 0.5;
62+
s.has_color = true;
63+
s.color = "#000000";
64+
s.has_opacity = true;
65+
s.opacity = 0.08;
66+
fig.axes(0).add_series(s, t, y);
67+
}
68+
69+
fig.set_backend(make_gnuplot_backend());
70+
const auto result = fig.save(out_dir / "figures");
71+
if (!result.ok) {
72+
gnuplotpp::log::Error("plot render incomplete: ", result.message);
73+
gnuplotpp::log::Error("script: ", result.script_path.string());
74+
return 1;
75+
}
76+
for (const auto& output : result.outputs) {
77+
gnuplotpp::log::Info("output: ", output.string());
78+
}
79+
return 0;
80+
}

include/gnuplotpp/plot.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,15 @@ struct SeriesSpec {
8484
std::string label;
8585
bool has_line_width = false;
8686
double line_width_pt = 1.0;
87+
/** @brief Optional explicit line/point color (e.g., "#112233"). */
88+
bool has_color = false;
89+
std::string color = "#000000";
90+
/**
91+
* @brief Optional opacity in [0, 1].
92+
* @note Backends map this to gnuplot ARGB color strings when possible.
93+
*/
94+
bool has_opacity = false;
95+
double opacity = 1.0;
8796
};
8897

8998
/** @brief In-memory series samples used by backends for emission/rendering. */

src/figure.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ void Axes::add_series(const SeriesSpec& spec,
1212
if (x.size() != y.size()) {
1313
throw std::invalid_argument("x and y must have the same length");
1414
}
15+
if (spec.has_opacity && (spec.opacity < 0.0 || spec.opacity > 1.0)) {
16+
throw std::invalid_argument("series opacity must be in [0, 1]");
17+
}
1518

1619
SeriesData data;
1720
data.spec = spec;

src/gnuplot_backend.cpp

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <filesystem>
77
#include <fstream>
88
#include <iomanip>
9+
#include <cmath>
910
#include <sstream>
1011
#include <string>
1112
#include <utility>
@@ -72,6 +73,46 @@ bool is_ieee_preset(const Preset preset) {
7273
return preset == Preset::IEEE_SingleColumn || preset == Preset::IEEE_DoubleColumn;
7374
}
7475

76+
bool is_hex_digit(const char c) {
77+
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
78+
}
79+
80+
bool is_hex_rgb(const std::string& color) {
81+
if (color.size() != 7 || color.front() != '#') {
82+
return false;
83+
}
84+
for (std::size_t i = 1; i < color.size(); ++i) {
85+
if (!is_hex_digit(color[i])) {
86+
return false;
87+
}
88+
}
89+
return true;
90+
}
91+
92+
std::string clamp_opacity_color(const std::string& color, const double opacity) {
93+
const double clamped = std::clamp(opacity, 0.0, 1.0);
94+
if (!is_hex_rgb(color)) {
95+
return color;
96+
}
97+
const int alpha =
98+
static_cast<int>(std::lround(clamped * 255.0)); // gnuplot expects AARRGGBB
99+
std::ostringstream os;
100+
os << "#" << std::hex << std::setfill('0') << std::nouppercase << std::setw(2) << alpha
101+
<< color.substr(1);
102+
return os.str();
103+
}
104+
105+
bool has_custom_color_or_opacity(const Figure& fig) {
106+
for (const auto& axis : fig.all_axes()) {
107+
for (const auto& series : axis.series()) {
108+
if (series.spec.has_color || series.spec.has_opacity) {
109+
return true;
110+
}
111+
}
112+
}
113+
return false;
114+
}
115+
75116
std::string terminal_for(OutputFormat format, const FigureSpec& spec) {
76117
std::ostringstream os;
77118
os << std::fixed << std::setprecision(3);
@@ -109,32 +150,50 @@ std::string with_clause(const SeriesData& series,
109150
series.spec.has_line_width ? series.spec.line_width_pt : style.line_width_pt;
110151
const bool ieee = is_ieee_preset(preset);
111152
const int dt = static_cast<int>(series_idx % 7U) + 1;
153+
std::string color;
154+
if (series.spec.has_color) {
155+
color = series.spec.color;
156+
} else if (ieee || series.spec.has_opacity) {
157+
color = "#000000";
158+
}
159+
if (series.spec.has_opacity) {
160+
color = clamp_opacity_color(color, series.spec.opacity);
161+
}
112162

113163
std::ostringstream os;
114164
os << std::fixed << std::setprecision(3);
115165
switch (series.spec.type) {
116166
case SeriesType::Line:
117167
os << "with lines lw " << std::max(0.5, line_width);
168+
if (!color.empty()) {
169+
os << " lc rgb '" << color << "'";
170+
}
118171
if (ieee) {
119-
os << " lc rgb '#000000' dt " << dt;
172+
os << " dt " << dt;
120173
}
121174
break;
122175
case SeriesType::Scatter:
123176
os << "with points pt 7 ps " << std::max(0.5, style.point_size);
124-
if (ieee) {
125-
os << " lc rgb '#000000'";
177+
if (!color.empty()) {
178+
os << " lc rgb '" << color << "'";
126179
}
127180
break;
128181
case SeriesType::ErrorBars:
129182
os << "with yerrorbars lw " << std::max(0.5, line_width);
183+
if (!color.empty()) {
184+
os << " lc rgb '" << color << "'";
185+
}
130186
if (ieee) {
131-
os << " lc rgb '#000000' dt " << dt;
187+
os << " dt " << dt;
132188
}
133189
break;
134190
case SeriesType::Band:
135191
os << "with lines lw " << std::max(0.5, line_width);
192+
if (!color.empty()) {
193+
os << " lc rgb '" << color << "'";
194+
}
136195
if (ieee) {
137-
os << " lc rgb '#000000' dt " << dt;
196+
os << " dt " << dt;
138197
}
139198
break;
140199
}
@@ -147,6 +206,7 @@ void emit_plot_body(std::ostream& os,
147206
const auto& spec = fig.spec();
148207
const auto& all_axes = fig.all_axes();
149208
const bool ieee = is_ieee_preset(spec.preset);
209+
const bool custom_color_or_opacity = has_custom_color_or_opacity(fig);
150210
const double tick_font_pt = ieee ? 8.0 : spec.style.font_pt;
151211
const double label_font_pt = ieee ? 8.5 : spec.style.font_pt;
152212
const double title_font_pt = ieee ? 8.5 : spec.style.font_pt;
@@ -161,7 +221,7 @@ void emit_plot_body(std::ostream& os,
161221
os << "set ytics font '" << esc(spec.style.font) << "," << tick_font_pt << "'\n";
162222
os << "set format x '%.2g'\n";
163223
os << "set format y '%.2g'\n";
164-
if (ieee) {
224+
if (ieee && !custom_color_or_opacity) {
165225
os << "set monochrome\n";
166226
}
167227
os << "set key noopaque\n";

tests/api_test.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#include <cassert>
55
#include <memory>
6+
#include <stdexcept>
67
#include <vector>
78

89
namespace {
@@ -37,6 +38,15 @@ int main() {
3738
const std::vector<double> x{0.0, 1.0, 2.0};
3839
const std::vector<double> y{1.0, 2.0, 3.0};
3940
fig.axes(0, 0).add_series(SeriesSpec{.label = "line"}, x, y);
41+
bool threw = false;
42+
try {
43+
fig.axes(0, 0).add_series(SeriesSpec{.label = "bad", .has_opacity = true, .opacity = 1.2},
44+
x,
45+
y);
46+
} catch (const std::invalid_argument&) {
47+
threw = true;
48+
}
49+
assert(threw);
4050

4151
assert(fig.axes(0, 0).series().size() == 1U);
4252
const auto no_backend = fig.save("out");

tests/gnuplot_backend_test.cpp

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,18 @@ int main() {
4545
const std::vector<double> t{0.0, 1.0, 2.0};
4646
const std::vector<double> ep{2.0, 1.0, 0.5};
4747
const std::vector<double> ev{0.4, 0.3, 0.2};
48+
const std::vector<double> ep_mc{1.8, 1.1, 0.6};
4849

4950
fig.axes(0, 0).add_series(SeriesSpec{.label = "SRIF"}, t, ep);
51+
fig.axes(0, 0).add_series(SeriesSpec{.label = "MC",
52+
.has_line_width = true,
53+
.line_width_pt = 0.6,
54+
.has_color = true,
55+
.color = "#000000",
56+
.has_opacity = true,
57+
.opacity = 0.25},
58+
t,
59+
ep_mc);
5060
fig.axes(0, 1).add_series(SeriesSpec{.label = "SRIF"}, t, ev);
5161

5262
const auto out_dir = std::filesystem::temp_directory_path() / "gnuplotpp_backend_test";
@@ -62,8 +72,9 @@ int main() {
6272
const auto script = read_file(result.script_path);
6373
assert(script.find("set multiplot layout 1,2") != std::string::npos);
6474
assert(script.find("set terminal pdfcairo") != std::string::npos);
65-
assert(script.find("set monochrome") != std::string::npos);
75+
assert(script.find("set monochrome") == std::string::npos);
6676
assert(script.find("dt 1") != std::string::npos);
77+
assert(script.find("lc rgb '#40000000'") != std::string::npos);
6778
assert(script.find("plot") != std::string::npos);
6879

6980
const auto data0 = out_dir / "tmp" / "ax0_series0.dat";

0 commit comments

Comments
 (0)