Skip to content

Commit b0ccb00

Browse files
committed
Merge remote-tracking branch 'origin/develop'
2 parents e122f81 + e495967 commit b0ccb00

File tree

7 files changed

+288
-29
lines changed

7 files changed

+288
-29
lines changed

docs/header.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
</head>
2828
<body>
2929
<!-- https://tholman.com/github-corners/ -->
30-
<a href="https://github.com/yhisaki/plotly.cpp" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:#151513; color:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"/><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"/><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"/></svg></a><style>.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}</style>
30+
<a href="https://github.com/yhisaki/plotly.cpp" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:#151513; color:#fff; position: fixed; z-index: 2147483647; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"/><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"/><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"/></svg></a><style>.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}</style>
3131

3232
<!--BEGIN FULL_SIDEBAR-->
3333
<div id="side-nav" class="ui-resizable side-nav-resizable"><!-- do not remove this div, it is closed by doxygen! -->

src/detail/http_server.cpp

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,6 @@ HttpServer::HttpServer(const std::filesystem::path &directory) : _server() {
2626
setupCommon();
2727
}
2828

29-
HttpServer::HttpServer(const std::string_view htmlContent) : _server() {
30-
_server.Get(
31-
"/",
32-
[htmlContent](const httplib::Request &, httplib::Response &res) -> void {
33-
res.set_content(std::string(htmlContent), "text/html");
34-
});
35-
setupCommon();
36-
}
37-
3829
HttpServer::~HttpServer() { stop(); }
3930

4031
void HttpServer::start() {

src/detail/http_server.hpp

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,6 @@ class HttpServer {
4141
*/
4242
explicit HttpServer(const std::filesystem::path &directory);
4343

44-
/**
45-
* @brief Construct a server serving a single HTML content
46-
* @param htmlContent The HTML content to serve
47-
*/
48-
explicit HttpServer(std::string_view htmlContent);
49-
5044
/**
5145
* @brief Destructor
5246
*/

src/detail/websockets_client.cpp

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@ namespace plotly::detail {
1717
WebsocketClient::WebsocketClient() {
1818
_client.set_access_channels(websocketpp::log::alevel::none);
1919
_client.clear_access_channels(websocketpp::log::alevel::none);
20-
// Only log serious errors, exclude recoverable errors like EOF
21-
_client.set_error_channels(websocketpp::log::elevel::warn |
22-
websocketpp::log::elevel::fatal);
20+
_client.set_error_channels(websocketpp::log::elevel::none);
2321
_client.init_asio();
2422

2523
setupClientHandlers();

src/detail/websockets_server.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ WebsocketServer::WebsocketServer() {
2121

2222
_server.set_access_channels(websocketpp::log::alevel::none);
2323
_server.clear_access_channels(websocketpp::log::alevel::none);
24-
_server.set_error_channels(websocketpp::log::elevel::warn |
25-
websocketpp::log::elevel::fatal);
24+
_server.set_error_channels(websocketpp::log::elevel::none);
2625
_server.init_asio();
2726
_server.set_reuse_addr(true);
2827

src/plotly.cpp

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -128,17 +128,18 @@ class Figure::Impl {
128128
return true;
129129
}
130130

131-
auto callPlotly(const std::string &method, const Object &params) const
131+
auto callPlotly(const std::string &method, const Object &params,
132+
const std::chrono::duration<double> &timeout =
133+
RPC_CALL_TIMEOUT_SECONDS) const
132134
-> std::optional<plotly::Object> {
133135
waitConnection();
134136
auto [future, cancel] = jsonRpc->call(method, params);
135137

136-
// Wait for 1 second for the call
137-
if (future.wait_for(std::chrono::duration<double>(
138-
RPC_CALL_TIMEOUT_SECONDS)) == std::future_status::ready) {
138+
if (future.wait_for(timeout) == std::future_status::ready) {
139139
return future.get();
140140
}
141-
141+
plotly::logInfo("[Figure] Call timed out: %s, timeout: %f[s]",
142+
method.c_str(), timeout.count());
142143
// Call timed out, cancel it and retry
143144
cancel();
144145
return std::nullopt;
@@ -326,9 +327,11 @@ class Figure::Impl {
326327
/// @param opts Animation options
327328
auto animate(const Object &frameOrGroupNameOrFrameList,
328329
const Object &opts) const -> bool {
329-
auto result = callPlotly("Plotly.animate", {{"frameOrGroupNameOrFrameList",
330-
frameOrGroupNameOrFrameList},
331-
{"opts", opts}});
330+
auto result = callPlotly(
331+
"Plotly.animate",
332+
{{"frameOrGroupNameOrFrameList", frameOrGroupNameOrFrameList},
333+
{"opts", opts}},
334+
24h);
332335
return result.has_value();
333336
}
334337

test/test_figure.cpp

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
#include "plotly/plotly.hpp"
2+
#include <atomic>
3+
#include <condition_variable>
24
#include <gtest/gtest.h>
5+
#include <mutex>
36

47
namespace plotly {
58

@@ -14,6 +17,277 @@ TEST_F(FigureTest, Construction) {
1417
EXPECT_NO_THROW({ Figure fig; });
1518
}
1619

20+
// Test browser operations
21+
TEST_F(FigureTest, BrowserOperations) {
22+
Figure fig;
23+
24+
// Test initial state
25+
EXPECT_FALSE(fig.isOpen());
26+
27+
// Test opening browser in headless mode
28+
bool opened = fig.openBrowser(true);
29+
if (opened) {
30+
EXPECT_TRUE(fig.isOpen());
31+
}
32+
}
33+
34+
// Test plot creation and manipulation
35+
TEST_F(FigureTest, PlotCreationAndManipulation) {
36+
Figure fig;
37+
38+
bool browserOpened = fig.openBrowser(true);
39+
if (!browserOpened) {
40+
GTEST_SKIP() << "Browser could not be opened, skipping plot tests";
41+
}
42+
43+
// Create simple plot data
44+
Object trace = {{"x", Object::array({1, 2, 3, 4})},
45+
{"y", Object::array({10, 11, 12, 13})},
46+
{"type", "scatter"},
47+
{"mode", "lines+markers"}};
48+
49+
Object data = Object::array({trace});
50+
Object layout = Object{{"title", "Test Plot"}};
51+
52+
// Test newPlot
53+
EXPECT_TRUE(fig.newPlot(data, layout));
54+
55+
// Test update
56+
Object traceUpdate = {{"marker.color", "red"}};
57+
EXPECT_TRUE(fig.update(traceUpdate));
58+
59+
// Test relayout
60+
Object layoutUpdate = {{"title", "Updated Test Plot"}};
61+
EXPECT_TRUE(fig.relayout(layoutUpdate));
62+
63+
// Test redraw
64+
EXPECT_TRUE(fig.redraw());
65+
}
66+
67+
// Test trace operations
68+
TEST_F(FigureTest, TraceOperations) {
69+
Figure fig;
70+
71+
bool browserOpened = fig.openBrowser(true);
72+
if (!browserOpened) {
73+
GTEST_SKIP() << "Browser could not be opened, skipping trace tests";
74+
}
75+
76+
// Create initial plot
77+
Object trace1 = {{"x", Object::array({1, 2, 3})},
78+
{"y", Object::array({1, 4, 9})},
79+
{"type", "scatter"},
80+
{"name", "trace1"}};
81+
82+
Object data = Object::array({trace1});
83+
EXPECT_TRUE(fig.newPlot(data));
84+
85+
// Test addTraces
86+
Object trace2 = {{"x", Object::array({1, 2, 3})},
87+
{"y", Object::array({2, 5, 10})},
88+
{"type", "scatter"},
89+
{"name", "trace2"}};
90+
91+
EXPECT_TRUE(fig.addTraces(Object::array({trace2})));
92+
93+
// Test restyle
94+
Object styleUpdate = {{"marker.color", "blue"}};
95+
EXPECT_TRUE(fig.restyle(styleUpdate, Object::array({0})));
96+
97+
// Test extendTraces
98+
Object extendData = {{"x", Object::array({4})}, {"y", Object::array({16})}};
99+
EXPECT_TRUE(fig.extendTraces(extendData, Object::array({0})));
100+
101+
// Test moveTraces
102+
EXPECT_TRUE(fig.moveTraces(Object::array({0}), Object::array({1})));
103+
104+
// Test deleteTraces
105+
EXPECT_TRUE(fig.deleteTraces(Object::array({1})));
106+
}
107+
108+
// Test animation operations
109+
TEST_F(FigureTest, AnimationOperations) {
110+
Figure fig;
111+
112+
bool browserOpened = fig.openBrowser(true);
113+
if (!browserOpened) {
114+
GTEST_SKIP() << "Browser could not be opened, skipping animation tests";
115+
}
116+
117+
// Simple test data
118+
Object initialTrace = {{"x", Object::array({1, 2, 3})},
119+
{"y", Object::array({1, 2, 3})},
120+
{"type", "scatter"}};
121+
122+
Object data = Object::array({initialTrace});
123+
EXPECT_TRUE(fig.newPlot(data));
124+
125+
// Test addFrames with simple frame data
126+
std::vector<Object> frames;
127+
for (int i = 0; i < 3; i++) {
128+
Object frameObj = {{"name", std::to_string(i)},
129+
{"data", std::vector<Object>{Object{
130+
{"x", Object::array({1, 2, 3})},
131+
{"y", Object::array({i + 1, i + 2, i + 3})},
132+
{"type", "scatter"}}}}};
133+
frames.push_back(frameObj);
134+
}
135+
136+
EXPECT_TRUE(fig.addFrames(frames));
137+
138+
// Test animate - basic functionality
139+
EXPECT_TRUE(fig.animate(Object(), Object{}));
140+
EXPECT_TRUE(fig.animate(Object::array({"0", "1"}), Object{}));
141+
EXPECT_TRUE(fig.animate(Object::array({}), Object{})); // pause
142+
143+
// Test deleteFrames
144+
EXPECT_TRUE(fig.deleteFrames(Object::array({"0", "1", "2"})));
145+
}
146+
147+
// Test react operation
148+
TEST_F(FigureTest, ReactOperation) {
149+
Figure fig;
150+
151+
bool browserOpened = fig.openBrowser(true);
152+
if (!browserOpened) {
153+
GTEST_SKIP() << "Browser could not be opened, skipping react tests";
154+
}
155+
156+
// Test react with new data
157+
Object trace = {{"x", Object::array({1, 2, 3})},
158+
{"y", Object::array({1, 4, 9})},
159+
{"type", "bar"}};
160+
161+
Object data = Object::array({trace});
162+
Object layout = {{"title", "React Test"}};
163+
Object config = {{"displayModeBar", false}};
164+
165+
EXPECT_TRUE(fig.react(data, layout, config));
166+
}
167+
168+
// Test purge operation
169+
TEST_F(FigureTest, PurgeOperation) {
170+
Figure fig;
171+
172+
bool browserOpened = fig.openBrowser(true);
173+
if (!browserOpened) {
174+
GTEST_SKIP() << "Browser could not be opened, skipping purge tests";
175+
}
176+
177+
// Create a plot first
178+
Object trace = {{"x", Object::array({1, 2, 3})},
179+
{"y", Object::array({1, 4, 9})},
180+
{"type", "scatter"}};
181+
182+
Object data = Object::array({trace});
183+
EXPECT_TRUE(fig.newPlot(data));
184+
185+
// Test purge
186+
EXPECT_TRUE(fig.purge());
187+
}
188+
189+
// Test event handling
190+
TEST_F(FigureTest, EventHandling) {
191+
Figure fig;
192+
193+
bool browserOpened = fig.openBrowser(true);
194+
if (!browserOpened) {
195+
GTEST_SKIP() << "Browser could not be opened, skipping event tests";
196+
}
197+
198+
// Create a plot to generate events
199+
Object trace = {{"x", Object::array({1, 2, 3})},
200+
{"y", Object::array({1, 4, 9})},
201+
{"type", "scatter"}};
202+
203+
Object data = Object::array({trace});
204+
EXPECT_TRUE(fig.newPlot(data));
205+
206+
// Test event listener registration
207+
std::atomic<bool> eventReceived{false};
208+
std::mutex eventMutex;
209+
std::condition_variable eventCv;
210+
211+
bool registered =
212+
fig.on("plotly_click", [&](const plotly::Object & /* eventData */) {
213+
std::lock_guard<std::mutex> lock(eventMutex);
214+
eventReceived = true;
215+
eventCv.notify_all();
216+
});
217+
218+
EXPECT_TRUE(registered);
219+
220+
// Test removing event listeners
221+
EXPECT_TRUE(fig.removeAllListeners("plotly_click"));
222+
}
223+
224+
// Test download directory setting
225+
TEST_F(FigureTest, DownloadDirectoryOperation) {
226+
Figure fig;
227+
228+
bool browserOpened = fig.openBrowser(true);
229+
if (!browserOpened) {
230+
GTEST_SKIP()
231+
<< "Browser could not be opened, skipping download directory tests";
232+
}
233+
234+
// Test setting download directory
235+
std::filesystem::path downloadPath = "/tmp";
236+
// Note: This might fail if Chrome DevTools is not available, which is okay
237+
// We're just testing that the method doesn't crash
238+
EXPECT_NO_THROW({ fig.setDownloadDirectory(downloadPath); });
239+
}
240+
241+
// Test image download
242+
TEST_F(FigureTest, ImageDownload) {
243+
Figure fig;
244+
245+
bool browserOpened = fig.openBrowser(true);
246+
if (!browserOpened) {
247+
GTEST_SKIP() << "Browser could not be opened, skipping download tests";
248+
}
249+
250+
// Create a plot to download
251+
Object trace = {{"x", Object::array({1, 2, 3, 4})},
252+
{"y", Object::array({10, 11, 12, 13})},
253+
{"type", "scatter"},
254+
{"mode", "lines+markers"}};
255+
256+
Object data = Object::array({trace});
257+
Object layout = Object{{"title", "Download Test Plot"}};
258+
259+
EXPECT_TRUE(fig.newPlot(data, layout));
260+
261+
// Test different download options
262+
Object pngOpts = Object{{"format", "png"},
263+
{"width", 800},
264+
{"height", 600},
265+
{"filename", "test_plot_png"}};
266+
267+
EXPECT_NO_THROW({ fig.downloadImage(pngOpts); });
268+
269+
Object svgOpts = Object{{"format", "svg"},
270+
{"width", 800},
271+
{"height", 600},
272+
{"filename", "test_plot_svg"}};
273+
274+
EXPECT_NO_THROW({ fig.downloadImage(svgOpts); });
275+
276+
Object jpegOpts = Object{{"format", "jpeg"},
277+
{"width", 800},
278+
{"height", 600},
279+
{"filename", "test_plot_jpeg"}};
280+
281+
EXPECT_NO_THROW({ fig.downloadImage(jpegOpts); });
282+
283+
Object pdfOpts = Object{{"format", "pdf"},
284+
{"width", 800},
285+
{"height", 600},
286+
{"filename", "test_plot_pdf"}};
287+
288+
EXPECT_NO_THROW({ fig.downloadImage(pdfOpts); });
289+
}
290+
17291
// Test basic plotting workflow with headless browser
18292
TEST_F(FigureTest, BasicPlottingWorkflow) {
19293
Figure fig;

0 commit comments

Comments
 (0)