Skip to content

Commit f1ecd32

Browse files
committed
timeseries plot functionality
1 parent a08b67a commit f1ecd32

File tree

4 files changed

+267
-3
lines changed

4 files changed

+267
-3
lines changed

CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ if(Python3_NumPy_FOUND)
110110
target_compile_features(ranges PRIVATE cxx_std_20)
111111
set_target_properties(ranges PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
112112

113+
add_executable(timeseries examples/timeseries.cpp)
114+
target_link_libraries(timeseries PRIVATE matplotlib_cpp)
115+
target_compile_features(timeseries PRIVATE cxx_std_20)
116+
set_target_properties(timeseries PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
117+
113118
endif()
114119

115120

datetime_utils.h

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#pragma once
2+
3+
#include "matplotlibcpp.h"
4+
5+
#include <Python.h>
6+
#include <datetime.h>
7+
8+
#include <string>
9+
#include <cstdlib>
10+
#include <map>
11+
#include <ranges>
12+
13+
// Convenience functions for converting C/C++ time objects to datetime.datetime
14+
// objects. These are outside the matplotlibcpp namespace because they do not
15+
// exist in matplotlib.pyplot.
16+
template<class TimePoint>
17+
PyObject* toPyDateTime(const TimePoint& t, int dummy=0)
18+
{
19+
using namespace std::chrono;
20+
auto tsec=time_point_cast<seconds>(t);
21+
auto us=duration_cast<microseconds>(t-tsec);
22+
23+
time_t tt=system_clock::to_time_t(t);
24+
PyObject* obj=toPyDateTime(tt, us.count());
25+
26+
return obj;
27+
}
28+
29+
template <>
30+
PyObject* toPyDateTime(const time_t& t, int us)
31+
{
32+
tm tm {};
33+
gmtime_r(&t,&tm); // compatible with matlab, inverse of datenum.
34+
35+
if (!PyDateTimeAPI) {
36+
PyDateTime_IMPORT;
37+
}
38+
39+
PyObject* obj=PyDateTime_FromDateAndTime(tm.tm_year+1900,
40+
tm.tm_mon+1,
41+
tm.tm_mday,
42+
tm.tm_hour,
43+
tm.tm_min,
44+
tm.tm_sec,
45+
us);
46+
if (obj) {
47+
PyDateTime_Check(obj);
48+
Py_INCREF(obj);
49+
}
50+
51+
return obj;
52+
}
53+
54+
template <class Time_t>
55+
PyObject* toPyDateTimeList(const Time_t* t, size_t nt)
56+
{
57+
PyObject* tlist=PyList_New(nt);
58+
if (tlist==nullptr)
59+
return nullptr;
60+
61+
// Py_INCREF(tlist);
62+
63+
if (!PyDateTimeAPI) {
64+
PyDateTime_IMPORT;
65+
}
66+
67+
for (size_t i=0; i<nt; i++) {
68+
PyObject* ti=toPyDateTime(t[i], 0);
69+
PyList_SET_ITEM(tlist, i, ti);
70+
}
71+
72+
return tlist;
73+
}
74+
75+
template <class Time_t>
76+
class DateTimeList
77+
{
78+
public:
79+
80+
DateTimeList(const Time_t* t, size_t nt) {
81+
tlist=(PyListObject*) toPyDateTimeList(t, nt);
82+
}
83+
84+
~DateTimeList() { if (tlist) Py_DECREF((PyObject*) tlist); }
85+
86+
PyListObject* get() const { return tlist; }
87+
size_t size() const { return tlist ? PyList_Size((PyObject*)tlist) : 0; }
88+
89+
private:
90+
mutable PyListObject* tlist=nullptr;
91+
};
92+
93+
94+
95+
namespace matplotlibcpp {
96+
97+
// special purpose function to plot against python datetime objects.
98+
template <class Time_t, std::ranges::contiguous_range ContainerY>
99+
bool plot(const DateTimeList<Time_t>& t, const ContainerY& y,
100+
const std::string& fmt="")
101+
{
102+
detail::_interpreter::get();
103+
104+
// DECREF decrements the ref counts of all objects in the plot_args tuple,
105+
// In particular, it decreasesthe ref count of the time array x.
106+
// We want to maintain that unchanged though, so we can reuse it.
107+
PyListObject* tarray=t.get();
108+
Py_INCREF(tarray);
109+
110+
NPY_TYPES ytype=detail::select_npy_type<typename ContainerY::value_type>::type;
111+
112+
npy_intp tsize=PyList_Size((PyObject*)tarray);
113+
assert(y.size()%tsize == 0 && "length of y must be a multiple of length of x!");
114+
115+
npy_intp yrows=tsize, ycols=y.size()/yrows;
116+
npy_intp ysize[]={yrows, ycols}; // ysize[0] must equal tsize
117+
118+
PyObject* yarray = PyArray_New(&PyArray_Type,
119+
2, ysize, ytype, nullptr, (void*) y.data(),
120+
0, NPY_ARRAY_FARRAY, nullptr); // col major
121+
122+
PyObject* pystring = PyString_FromString(fmt.c_str());
123+
124+
PyObject* plot_args = PyTuple_New(3);
125+
PyTuple_SetItem(plot_args, 0, (PyObject*)tarray);
126+
PyTuple_SetItem(plot_args, 1, yarray);
127+
PyTuple_SetItem(plot_args, 2, pystring);
128+
129+
PyObject* res = PyObject_CallObject(detail::_interpreter::get().s_python_function_plot, plot_args);
130+
131+
Py_DECREF(plot_args);
132+
if(res) Py_DECREF(res);
133+
134+
return true;
135+
}
136+
137+
}

examples/timeseries.cpp

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
//
2+
// g++ -std=c++20 -g -Wall -o timeseries $(python-config --includes) timeseries.cpp $(python-config --ldflags --embed)
3+
// g++ -std=c++20 -g -Wall -fsanitize=address -o timeseries $(python-config --includes) timeseries.cpp $(python-config --ldflags --embed) -lasan
4+
//
5+
#include "../matplotlibcpp.h"
6+
#include "../datetime_utils.h"
7+
8+
#include <cassert>
9+
#include <iostream>
10+
#include <cstdlib>
11+
#include <vector>
12+
#include <string>
13+
14+
using namespace std;
15+
namespace plt = matplotlibcpp;
16+
17+
// In python2.7 there was a dedicated function PyString_AsString, but no more.
18+
string tostring(PyObject* obj)
19+
{
20+
string out;
21+
PyObject* repr=PyObject_Repr(obj); // unicode object
22+
if (repr) {
23+
PyObject* str=PyUnicode_AsEncodedString(repr, 0, 0);
24+
if (str) {
25+
const char* bytes=PyBytes_AS_STRING(str);
26+
out=bytes;
27+
Py_DECREF(str);
28+
}
29+
30+
Py_DECREF(repr);
31+
}
32+
33+
return out;
34+
}
35+
36+
int main()
37+
{
38+
using namespace std::chrono;
39+
using clk=std::chrono::high_resolution_clock;
40+
41+
plt::detail::_interpreter::get(); // initialize everything
42+
43+
// Test toPyDateTime functions.
44+
PyObject* now=toPyDateTime(time(0));
45+
cout << tostring(now) << endl;
46+
Py_DECREF(now);
47+
48+
now=toPyDateTime(clk::now());
49+
cout << tostring(now) << endl;
50+
Py_DECREF(now);
51+
52+
53+
// Time series plot
54+
55+
// We have a time array (e.g. from a csv file):
56+
size_t n=10;
57+
vector<time_t> tvec;
58+
time_t tstart=time(0);
59+
for (size_t i=0; i<n; i++, tstart+=24*3600)
60+
tvec.push_back(tstart);
61+
62+
// Create the python array of times - ONCE!
63+
DateTimeList<time_t> tarray(tvec.data(), tvec.size());
64+
65+
// Create 2-column data and plot it against tarray:
66+
vector<double> data(2*n);
67+
for (size_t i=0; i<2*n; i++) data[i]=2.0*i+5;
68+
69+
plt::plot(tarray, data);
70+
plt::xticks({{"rotation", "20"}});
71+
plt::title("Linear");
72+
plt::grid(true);
73+
plt::show();
74+
75+
// Modify some data and plot again, reusing the time array:
76+
for (size_t i=0; i<n; i++) data[i]=i*i-3.0;
77+
78+
// plt::plot((PyListObject*) tarray, data);
79+
plt::plot(tarray, data);
80+
plt::xticks({{"rotation", "20"}});
81+
plt::title("Quadratic");
82+
plt::grid(true);
83+
plt::show();
84+
85+
// Unfortunately, we have to call the DateTimeList destructor explicitly
86+
// for now, because the destructor needs an interpreter, which we kill
87+
// on the next line. In turn, that's due to the singleton implementation
88+
// of the interpreter using a static object.
89+
//
90+
// TODO: implement the singleton using a pointer. The pointer
91+
// (to _interpreter::ctx) is still static, but ctx can be deleted at the
92+
// right time (before Py_Finalize); it is not at the mercy of the OS.
93+
tarray.~DateTimeList<time_t>();
94+
plt::detail::_interpreter::kill();
95+
96+
return 0;
97+
}

matplotlibcpp.h

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// Python headers must be included before any system headers, since
44
// they define _POSIX_C_SOURCE
55
#include <Python.h>
6+
#include <datetime.h>
67

78
#include <vector>
89
#include <map>
@@ -11,11 +12,10 @@
1112
#include <algorithm>
1213
#include <stdexcept>
1314
#include <iostream>
14-
#include <cstdint> // <cstdint> requires c++11 support
1515
#include <functional>
16-
#include <string> // std::stod
17-
#include <span>
1816
#include <ranges>
17+
#include <chrono>
18+
#include <string>
1919

2020
#ifndef WITHOUT_NUMPY
2121
# define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
@@ -596,6 +596,7 @@ bool plot(const ContainerY& y, const std::map<std::string, std::string>& keyword
596596
return plot(x,y,keywords);
597597
}
598598

599+
599600
#else
600601

601602
// For the less fortunate ones who can't or won't use C++ 20, we stick with
@@ -2307,6 +2308,27 @@ inline void xticks(const std::vector<Numeric> &ticks, const std::map<std::string
23072308
xticks(ticks, {}, keywords);
23082309
}
23092310

2311+
// options only, e.g. plt::xticks(rotation=20)
2312+
inline void xticks(const std::map<std::string, std::string>& keywords)
2313+
{
2314+
detail::_interpreter::get();
2315+
2316+
PyObject* args = PyTuple_New(0);
2317+
2318+
// construct keyword args
2319+
PyObject* kwargs = PyDict_New();
2320+
for (const auto& [k, v] : keywords)
2321+
PyDict_SetItemString(kwargs, k.c_str(), PyString_FromString(v.c_str()));
2322+
2323+
PyObject* res = PyObject_Call(detail::_interpreter::get().s_python_function_xticks, args, kwargs);
2324+
2325+
Py_DECREF(kwargs);
2326+
if(!res) throw std::runtime_error("Call to xticks() failed");
2327+
2328+
Py_DECREF(res);
2329+
}
2330+
2331+
23102332
template<typename Numeric>
23112333
inline void yticks(const std::vector<Numeric> &ticks, const std::vector<std::string> &labels = {}, const std::map<std::string, std::string>& keywords = {})
23122334
{
@@ -3227,4 +3249,7 @@ class Plot
32273249
PyObject* set_data_fct = nullptr;
32283250
};
32293251

3252+
32303253
} // end namespace matplotlibcpp
3254+
3255+

0 commit comments

Comments
 (0)