FlowPlotPy 0.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,23 @@
1
+ cmake_minimum_required(VERSION 3.18)
2
+
3
+ project(${SKBUILD_PROJECT_NAME} VERSION ${SKBUILD_PROJECT_VERSION} LANGUAGES CXX)
4
+
5
+ set(PYBIND11_FINDPYTHON ON)
6
+ find_package(pybind11 CONFIG REQUIRED)
7
+
8
+ pybind11_add_module(_flowplot flowplot_py.cpp)
9
+
10
+ target_compile_features(_flowplot PRIVATE cxx_std_20)
11
+
12
+ target_include_directories(_flowplot PRIVATE
13
+ ${CMAKE_CURRENT_SOURCE_DIR}
14
+ ${CMAKE_CURRENT_SOURCE_DIR}/..
15
+ )
16
+
17
+ target_compile_definitions(_flowplot PRIVATE
18
+ FLOW_PLOT_IMPLEMENTATION
19
+ FLOW_PLOT_RENDERER
20
+ FLOW_PLOT_COMPLETE_JSON
21
+ )
22
+
23
+ install(TARGETS _flowplot LIBRARY DESTINATION flowplot)
@@ -0,0 +1,336 @@
1
+ Metadata-Version: 2.2
2
+ Name: FlowPlotPy
3
+ Version: 0.9.0
4
+ Summary: Python bindings for the FlowPlot plotting library
5
+ Author: FlowPlot contributors
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Manwe314
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Classifier: Development Status :: 3 - Alpha
29
+ Classifier: Intended Audience :: Developers
30
+ Classifier: Intended Audience :: Science/Research
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Programming Language :: C++
33
+ Classifier: Programming Language :: Python :: 3
34
+ Classifier: Programming Language :: Python :: 3.10
35
+ Classifier: Programming Language :: Python :: 3.11
36
+ Classifier: Programming Language :: Python :: 3.12
37
+ Classifier: Programming Language :: Python :: 3.13
38
+ Classifier: Topic :: Scientific/Engineering :: Visualization
39
+ Requires-Python: >=3.10
40
+ Provides-Extra: numpy
41
+ Requires-Dist: numpy>=1.23; extra == "numpy"
42
+ Description-Content-Type: text/markdown
43
+
44
+ # FlowPlot
45
+
46
+ FlowPlot is a header-only plotting library that:
47
+
48
+ - compiles a JSON template into an internal spec
49
+ - binds runtime data (`withData("dataset.field", ...)`)
50
+ - resolves layout/axes/layers into a render command stream
51
+ - optionally rasterizes to PNG with the built-in CPU renderer
52
+
53
+ It supports scatter and histogram layers, automatic axis-domain inference, legends/titles, and pluggable text measurement/layout through `ITextEngine`.
54
+ Text styling supports font family, numeric weight, and style (`normal`, `italic`, `oblique`).
55
+
56
+ ## Quick Start
57
+
58
+ ### 1. Create a template (`ScatterTemplate.json`)
59
+
60
+ ```json
61
+ {
62
+ "version": "1.0",
63
+ "fonts": [
64
+ {
65
+ "family": "Inter",
66
+ "weight": 400,
67
+ "style": "normal",
68
+ "path": "/absolute/path/to/Inter-Regular.ttf"
69
+ },
70
+ {
71
+ "family": "Inter",
72
+ "weight": 700,
73
+ "style": "normal",
74
+ "path": "/absolute/path/to/Inter-Bold.ttf"
75
+ }
76
+ ],
77
+ "figure": {
78
+ "width": 1200,
79
+ "height": 800,
80
+ "title": {
81
+ "visible": true,
82
+ "text": "Scatter Plot",
83
+ "fontFamily": "Inter",
84
+ "fontWeight": 700,
85
+ "fontStyle": "normal"
86
+ }
87
+ },
88
+ "datasets": [
89
+ {
90
+ "name": "main",
91
+ "schema": {
92
+ "x": "number",
93
+ "y": "number"
94
+ }
95
+ }
96
+ ],
97
+ "panels": [
98
+ {
99
+ "layers": [
100
+ {
101
+ "type": "scatter",
102
+ "dataset": "main",
103
+ "mapping": {
104
+ "x": { "field": "x" },
105
+ "y": { "field": "y" }
106
+ }
107
+ }
108
+ ]
109
+ }
110
+ ]
111
+ }
112
+ ```
113
+
114
+ ### 2. Render from C++
115
+
116
+ ```cpp
117
+ #define FLOW_PLOT_RENDERER
118
+ #define FLOW_PLOT_IMPLEMENTATION
119
+ #define FLOW_PLOT_DEFAULT_FONT_PATH "./FacultyGlyphic-Regular.ttf"
120
+ #include "FlowPlot_Mega.hpp"
121
+
122
+ #include <vector>
123
+
124
+ int main()
125
+ {
126
+ std::vector<int> x{1, 2, 3, 4, 5};
127
+ std::vector<int> y{3, 4, 1, 4, 6};
128
+
129
+ FlowPlot::plot("./ScatterTemplate.json")
130
+ .withData("main.x", x)
131
+ .withData("main.y", y)
132
+ .writePng("./out.png");
133
+ }
134
+ ```
135
+
136
+ ### 3. Compile
137
+
138
+ ```bash
139
+ g++ -std=c++20 -I. -IFlowPlot main.cpp -o plot
140
+ ```
141
+
142
+ ## Public API
143
+
144
+ - `FlowPlot::plot(path)` loads template JSON (`.json` extension optional).
145
+ - `PlotBuilder::set("path.to.prop", value)` mutates template JSON before compile.
146
+ - `value` supports: `int`, `float`, `double`, `bool`, `const char*`, `std::string`, `std::string_view`.
147
+ - `PlotBuilder::setJsonRaw("path.to.prop", jsonText)` sets a property from raw JSON text (object/array/primitive).
148
+ - `PlotBuilder::withData("dataset.field", std::span<const T>)`
149
+ - `PlotBuilder::withData("dataset.field", const std::vector<T>&)`
150
+ - `PlotBuilder::useTextEngine(engine)` injects a custom text engine.
151
+ - `PlotBuilder::getCommands()` returns `RenderPlot` (resolved command stream).
152
+ - `PlotBuilder::writePng(path)` (only when `FLOW_PLOT_RENDERER` is enabled).
153
+ - `FlowPlot::registerFonts(textEngine, templateJsonText)` registers every root-level `fonts[]` entry with an `ITextEngine`.
154
+ - `FlowPlot::getCompleteJson(templateJsonText, pretty = true)` (only when `FLOW_PLOT_COMPLETE_JSON` is enabled).
155
+
156
+ ## Defines
157
+
158
+ ### Required in specific modes
159
+
160
+ - `FLOW_PLOT_RENDERER`
161
+ - Enables renderer integration (`CpuRenderer`, `StbTextEngine`, `PlotBuilder::writePng`).
162
+ - Define before including headers (or pass as compiler define).
163
+
164
+ - `FLOW_PLOT_IMPLEMENTATION`
165
+ - Required in exactly one translation unit if you use built-in stb implementations.
166
+ - Without this (and without external stb implementation), renderer builds can compile but will fail to link when stb symbols are needed.
167
+
168
+ ### Optional
169
+
170
+ - `FLOW_PLOT_DEFAULT_FONT_PATH`
171
+ - String literal path to a default TTF file auto-registered by `StbTextEngine`.
172
+ - `PlotBuilder::writePng` and renderer-enabled `PlotBuilder::getCommands` auto-create a fallback `StbTextEngine` if none is set; this default font path makes that work out of the box.
173
+
174
+ - `FLOW_PLOT_COMPLETE_JSON`
175
+ - Enables full-template normalization helper: `getCompleteJson(templateJsonText, pretty)`.
176
+
177
+ - `FLOW_PLOT_STB_EXTERNAL_IMPLEMENTATION`
178
+ - Use when you provide stb implementations externally.
179
+ - Disables FlowPlot’s internal stb implementation emission.
180
+
181
+ ### Internal (do not set manually)
182
+
183
+ Examples: `FLOW_PLOT_HPP_INCLUDED`, `FLOW_PLOT_RENDERER_HPP_INCLUDED`, `FLOW_PLOT_DEFINED_STBTT_IMPLEMENTATION`, etc.
184
+
185
+ ## Template Schema (Overview)
186
+
187
+ Root object:
188
+
189
+ - `version` (`"1.0"`)
190
+ - `fonts` optional array of font variants
191
+ - `figure`
192
+ - `datasets`
193
+ - `layout`
194
+ - `panels`
195
+
196
+ ### `fonts`
197
+
198
+ Optional root-level font manifest. Each entry describes one concrete font face:
199
+
200
+ ```json
201
+ {
202
+ "family": "Inter",
203
+ "weight": 400,
204
+ "style": "normal",
205
+ "path": "/absolute/path/to/Inter-Regular.ttf"
206
+ }
207
+ ```
208
+
209
+ - `family`: family name referenced by text specs.
210
+ - `weight`: numeric font weight, usually `100..900`.
211
+ - `style`: `normal`, `italic`, or `oblique`.
212
+ - `path`: absolute path to a `.ttf`/`.ttc` font file.
213
+
214
+ The built-in fallback `StbTextEngine` automatically registers these entries for `writePng()` and renderer-enabled `getCommands()`. Custom text engines can opt in by calling `FlowPlot::registerFonts(engine, templateJsonText)`.
215
+
216
+ ### `figure`
217
+
218
+ Includes width/height/dpi/background/padding/title/legends.
219
+
220
+ ### `datasets`
221
+
222
+ Each dataset has:
223
+
224
+ - `name`
225
+ - `schema` map where values are `number`, `string`, or `boolean`
226
+
227
+ Runtime data binding uses:
228
+
229
+ - `withData("datasetName.fieldName", data)`
230
+
231
+ ### `panels`
232
+
233
+ Each panel has:
234
+
235
+ - style/frame/title
236
+ - axes: `xAxis`, `yAxis`, `xSecondary`, `ySecondary`
237
+ - `layers`
238
+
239
+ ### `layers`
240
+
241
+ Common layer fields:
242
+
243
+ - `type`: `scatter` or `histogram`
244
+ - `dataset`: dataset name
245
+ - `axisData`: `{ "x": "primary|secondary|null", "y": "primary|secondary|null" }`
246
+ - `mapping`, `style`, `stats`, `config`
247
+
248
+ Scatter mapping (important fields):
249
+
250
+ - `mapping.x.field` (required)
251
+ - `mapping.y.field` (required)
252
+ - optional: `mapping.color.field`, `mapping.size.field`, `mapping.label.field`
253
+
254
+ Histogram mapping (important fields):
255
+
256
+ - `mapping.data.field` (required)
257
+ - `mapping.data.axis`: `"x"` or `"y"` (which axis receives input data)
258
+ - optional color field/mapping
259
+
260
+ Notes:
261
+
262
+ - If no layer contributes data to an axis and no explicit `min/max` is provided, axis domain falls back to `0..1`.
263
+ - You must define panels/layers/mappings if you want automatic domain inference from data.
264
+
265
+ ## Renderer Pipeline
266
+
267
+ High-level flow:
268
+
269
+ 1. Template JSON -> compiled spec
270
+ 2. Spec + data views -> bound IR
271
+ 3. Bound IR + text metrics -> resolved IR
272
+ 4. Resolved IR -> `RenderPlot` command stream
273
+ 5. `CpuRenderer` rasterizes commands to RGBA8 image / PNG
274
+
275
+ Command variants include:
276
+
277
+ - `BoxCommand`
278
+ - `PolylineCommand`
279
+ - `TextCommand`
280
+ - `MarkersCommand`
281
+ - clip stack commands (`PushClipRectCommand`, `PopClipRectCommand`)
282
+
283
+ ## Text Engine Pluggability
284
+
285
+ `ITextEngine` is pluggable for text metrics/layout:
286
+
287
+ - `registerFont(...)`
288
+ - `hasFont(...)`
289
+ - `measureText(...)`
290
+ - `layoutText(...)`
291
+
292
+ Built-in `StbTextEngine` provides UTF-8 layout and glyph bitmap raster support used by the CPU renderer.
293
+
294
+ Important current behavior:
295
+
296
+ - `resolvePlotIR` needs a text engine for auto-sized text boxes.
297
+ - `PlotBuilder::writePng` and renderer-enabled `PlotBuilder::getCommands` auto-fall back to `StbTextEngine` when no engine is explicitly set.
298
+ - The fallback `StbTextEngine` automatically registers root `fonts[]` entries before resolving text.
299
+ - If no default font is available, `writePng` throws with guidance (`useTextEngine(...)` or set `FLOW_PLOT_DEFAULT_FONT_PATH`).
300
+ - Font lookup uses `family + weight + style`, with fallback to normal style/default weight/default family where possible.
301
+ - Non-`StbTextEngine` custom engines can drive measurement/layout, but glyph rasterization in the built-in renderer is currently specialized for `StbTextEngine`.
302
+
303
+ ## Header Options
304
+
305
+ You can use modular headers (`FlowPlot/FlowPlot.hpp`) or one of four generated amalgamated headers.
306
+
307
+ Regenerate all amalgamated variants with:
308
+
309
+ ```bash
310
+ python3 tools/generate_flowplot_mega.py
311
+ ```
312
+
313
+ Generated outputs:
314
+
315
+ - `FlowPlot_Mega_Core.hpp`
316
+ - Inlines FlowPlot headers only.
317
+ - Leaves RapidJSON and stb external (`-IFlowPlot` needed for bundled deps).
318
+ - `FlowPlot_Mega_Stb.hpp`
319
+ - Inlines FlowPlot + stb headers.
320
+ - Leaves RapidJSON external (`-IFlowPlot` needed for bundled deps).
321
+ - `FlowPlot_Mega_Json.hpp`
322
+ - Inlines FlowPlot + used RapidJSON subset.
323
+ - Leaves stb external (`-IFlowPlot` needed only if renderer/stb paths are used).
324
+ - `FlowPlot_Mega.hpp`
325
+ - Inlines FlowPlot + used RapidJSON subset + stb.
326
+ - Fully self-contained single header (copy one file and include it).
327
+
328
+ All variants support the same feature macros and runtime API.
329
+
330
+ ## JSON Backend
331
+
332
+ FlowPlot uses RapidJSON. The JSON-inlined mega variants include only the RapidJSON subset reachable from FlowPlot's actual include usage.
333
+
334
+ ## Documentation Status
335
+
336
+ This is a temporary README. FlowPlot will get full documentation and a pre-v1.0.0 release soon.
@@ -0,0 +1,421 @@
1
+ #include <pybind11/numpy.h>
2
+ #include <pybind11/pybind11.h>
3
+ #include <pybind11/stl.h>
4
+
5
+ #include "FlowPlot_Mega.hpp"
6
+
7
+ #include <cstddef>
8
+ #include <cstdint>
9
+ #include <filesystem>
10
+ #include <optional>
11
+ #include <span>
12
+ #include <stdexcept>
13
+ #include <string>
14
+ #include <string_view>
15
+ #include <utility>
16
+ #include <vector>
17
+
18
+ namespace py = pybind11;
19
+
20
+ namespace
21
+ {
22
+ py::tuple colorToTuple(const FlowPlot::Color& color)
23
+ {
24
+ return py::make_tuple(color.r, color.g, color.b, color.a);
25
+ }
26
+
27
+ py::tuple pointToTuple(const FlowPlot::PointF& point)
28
+ {
29
+ return py::make_tuple(point.x, point.y);
30
+ }
31
+
32
+ py::tuple rectToTuple(const FlowPlot::RectF& rect)
33
+ {
34
+ return py::make_tuple(rect.x, rect.y, rect.w, rect.h);
35
+ }
36
+
37
+ const char* horizontalAlignName(FlowPlot::HorizontalAlign align) noexcept
38
+ {
39
+ switch (align)
40
+ {
41
+ case FlowPlot::HorizontalAlign::Left:
42
+ return "left";
43
+ case FlowPlot::HorizontalAlign::Center:
44
+ return "center";
45
+ case FlowPlot::HorizontalAlign::Right:
46
+ return "right";
47
+ }
48
+ return "left";
49
+ }
50
+
51
+ const char* verticalAlignName(FlowPlot::VerticalAlign align) noexcept
52
+ {
53
+ switch (align)
54
+ {
55
+ case FlowPlot::VerticalAlign::Top:
56
+ return "top";
57
+ case FlowPlot::VerticalAlign::Middle:
58
+ return "middle";
59
+ case FlowPlot::VerticalAlign::Bottom:
60
+ return "bottom";
61
+ }
62
+ return "top";
63
+ }
64
+
65
+ const char* markerShapeName(FlowPlot::MarkerShape shape) noexcept
66
+ {
67
+ switch (shape)
68
+ {
69
+ case FlowPlot::MarkerShape::Circle:
70
+ return "circle";
71
+ case FlowPlot::MarkerShape::Square:
72
+ return "square";
73
+ case FlowPlot::MarkerShape::Diamond:
74
+ return "diamond";
75
+ case FlowPlot::MarkerShape::Triangle:
76
+ return "triangle";
77
+ }
78
+ return "circle";
79
+ }
80
+
81
+ const char* lineCapName(FlowPlot::LineCap cap) noexcept
82
+ {
83
+ switch (cap)
84
+ {
85
+ case FlowPlot::LineCap::Butt:
86
+ return "butt";
87
+ case FlowPlot::LineCap::Square:
88
+ return "square";
89
+ case FlowPlot::LineCap::Round:
90
+ return "round";
91
+ }
92
+ return "butt";
93
+ }
94
+
95
+ const char* lineJoinName(FlowPlot::LineJoin join) noexcept
96
+ {
97
+ switch (join)
98
+ {
99
+ case FlowPlot::LineJoin::Miter:
100
+ return "miter";
101
+ case FlowPlot::LineJoin::Bevel:
102
+ return "bevel";
103
+ case FlowPlot::LineJoin::Round:
104
+ return "round";
105
+ }
106
+ return "bevel";
107
+ }
108
+
109
+ py::list pointsToList(const std::vector<FlowPlot::PointF>& points)
110
+ {
111
+ py::list out;
112
+ for (const FlowPlot::PointF& point : points)
113
+ out.append(pointToTuple(point));
114
+ return out;
115
+ }
116
+
117
+ py::list colorsToList(const std::vector<FlowPlot::Color>& colors)
118
+ {
119
+ py::list out;
120
+ for (const FlowPlot::Color& color : colors)
121
+ out.append(colorToTuple(color));
122
+ return out;
123
+ }
124
+
125
+ py::list floatsToList(const std::vector<float>& values)
126
+ {
127
+ py::list out;
128
+ for (float value : values)
129
+ out.append(value);
130
+ return out;
131
+ }
132
+
133
+ py::dict commandToDict(const FlowPlot::BoxCommand& command)
134
+ {
135
+ py::dict out;
136
+ out["type"] = "box";
137
+ out["rect"] = rectToTuple(command.rect);
138
+ out["fill"] = colorToTuple(command.fill);
139
+ out["stroke"] = colorToTuple(command.stroke);
140
+ out["stroke_width"] = command.strokeWidth;
141
+ return out;
142
+ }
143
+
144
+ py::dict commandToDict(const FlowPlot::PolylineCommand& command)
145
+ {
146
+ py::dict out;
147
+ out["type"] = "polyline";
148
+ out["points"] = pointsToList(command.points);
149
+ out["color"] = colorToTuple(command.color);
150
+ out["width"] = command.width;
151
+ out["cap"] = lineCapName(command.cap);
152
+ out["join"] = lineJoinName(command.join);
153
+ return out;
154
+ }
155
+
156
+ py::dict commandToDict(const FlowPlot::TextCommand& command)
157
+ {
158
+ py::dict out;
159
+ out["type"] = "text";
160
+ out["box"] = rectToTuple(command.box);
161
+ out["text"] = command.text;
162
+ out["color"] = colorToTuple(command.color);
163
+ out["font_family"] = command.fontFamily;
164
+ out["font_size"] = command.fontSize;
165
+ out["font_weight"] = command.fontWeight;
166
+ out["font_style"] = FlowPlot::fontStyleName(command.fontStyle);
167
+ out["h_align"] = horizontalAlignName(command.hAlign);
168
+ out["v_align"] = verticalAlignName(command.vAlign);
169
+ out["clip_to_box"] = command.clipToBox;
170
+ return out;
171
+ }
172
+
173
+ py::dict commandToDict(const FlowPlot::MarkersCommand& command)
174
+ {
175
+ py::dict out;
176
+ out["type"] = "markers";
177
+ out["shape"] = markerShapeName(command.shape);
178
+ out["positions"] = pointsToList(command.positions);
179
+ out["fills"] = colorsToList(command.fills);
180
+ out["stroke"] = colorToTuple(command.stroke);
181
+ out["sizes"] = floatsToList(command.sizes);
182
+ out["stroke_width"] = command.strokeWidth;
183
+ return out;
184
+ }
185
+
186
+ py::dict commandToDict(const FlowPlot::PushClipRectCommand& command)
187
+ {
188
+ py::dict out;
189
+ out["type"] = "push_clip_rect";
190
+ out["rect"] = rectToTuple(command.rect);
191
+ return out;
192
+ }
193
+
194
+ py::dict commandToDict(const FlowPlot::PopClipRectCommand&)
195
+ {
196
+ py::dict out;
197
+ out["type"] = "pop_clip_rect";
198
+ return out;
199
+ }
200
+
201
+ py::dict renderPlotToDict(const FlowPlot::RenderPlot& plot)
202
+ {
203
+ py::list commands;
204
+ for (const FlowPlot::RenderCommand& command : plot.commands)
205
+ {
206
+ commands.append(std::visit(
207
+ [](const auto& concreteCommand)
208
+ {
209
+ return commandToDict(concreteCommand);
210
+ },
211
+ command));
212
+ }
213
+
214
+ py::dict out;
215
+ out["width"] = plot.width;
216
+ out["height"] = plot.height;
217
+ out["background"] = colorToTuple(plot.background);
218
+ out["commands"] = std::move(commands);
219
+ return out;
220
+ }
221
+
222
+ class PyPlot
223
+ {
224
+ public:
225
+ explicit PyPlot(const std::string& path)
226
+ : builder_(FlowPlot::makePlot(path))
227
+ {
228
+ }
229
+
230
+ PyPlot& set(const std::string& property, py::object value)
231
+ {
232
+ if (py::isinstance<py::bool_>(value))
233
+ builder_.set(property, value.cast<bool>());
234
+ else if (py::isinstance<py::int_>(value))
235
+ builder_.set(property, value.cast<int>());
236
+ else if (py::isinstance<py::float_>(value))
237
+ builder_.set(property, value.cast<double>());
238
+ else if (py::isinstance<py::str>(value))
239
+ builder_.set(property, value.cast<std::string>());
240
+ else if (value.is_none())
241
+ throw py::type_error("set() does not accept None; use set_json_raw(property, \"null\")");
242
+ else
243
+ throw py::type_error("set() value must be bool, int, float, or str");
244
+
245
+ return *this;
246
+ }
247
+
248
+ PyPlot& setJsonRaw(const std::string& property, const std::string& jsonText)
249
+ {
250
+ builder_.setJsonRaw(property, jsonText);
251
+ return *this;
252
+ }
253
+
254
+ PyPlot& setDefaultFont(const std::string& fontPath)
255
+ {
256
+ textEngine_.emplace(std::filesystem::path(fontPath));
257
+ builder_.useTextEngine(*textEngine_);
258
+ return *this;
259
+ }
260
+
261
+ PyPlot& withData(const std::string& datasetField, py::object data)
262
+ {
263
+ if (py::isinstance<py::array>(data))
264
+ return withNumpyData(datasetField, data);
265
+
266
+ if (py::isinstance<py::sequence>(data) && !py::isinstance<py::str>(data))
267
+ return withSequenceData(datasetField, data.cast<py::sequence>());
268
+
269
+ throw py::type_error("with_data() expects a 1D sequence or NumPy array");
270
+ }
271
+
272
+ py::dict getCommands() const
273
+ {
274
+ prepareTextEngine("get_commands()");
275
+ return renderPlotToDict(builder_.getCommands());
276
+ }
277
+
278
+ void writePng(const std::string& outputPath) const
279
+ {
280
+ prepareTextEngine("write_png()");
281
+ builder_.writePng(outputPath);
282
+ }
283
+
284
+ private:
285
+ void prepareTextEngine(const char* caller) const
286
+ {
287
+ if (!textEngine_.has_value())
288
+ {
289
+ throw py::value_error(
290
+ std::string(caller)
291
+ + " requires a default font in Python; call set_default_font(path_to_ttf) first");
292
+ }
293
+
294
+ builder_.registerTemplateFonts(*textEngine_);
295
+ }
296
+
297
+ PyPlot& withNumpyData(const std::string& datasetField, py::object data)
298
+ {
299
+ using DoubleArray = py::array_t<double, py::array::c_style | py::array::forcecast>;
300
+
301
+ DoubleArray array = DoubleArray::ensure(data);
302
+ if (!array)
303
+ throw py::type_error("with_data() NumPy arrays must contain numeric data");
304
+
305
+ py::buffer_info info = array.request();
306
+ if (info.ndim != 1)
307
+ throw py::value_error("with_data() NumPy arrays must be 1D");
308
+
309
+ keptArrays_.push_back(array);
310
+ builder_.withData(
311
+ datasetField,
312
+ std::span<const double>(
313
+ static_cast<const double*>(info.ptr),
314
+ static_cast<std::size_t>(info.shape[0])));
315
+ return *this;
316
+ }
317
+
318
+ PyPlot& withSequenceData(const std::string& datasetField, py::sequence sequence)
319
+ {
320
+ if (sequence.empty())
321
+ {
322
+ ownedDoubles_.emplace_back();
323
+ builder_.withData(datasetField, ownedDoubles_.back());
324
+ return *this;
325
+ }
326
+
327
+ py::object first = sequence[0];
328
+ if (py::isinstance<py::str>(first))
329
+ return withStringSequenceData(datasetField, sequence);
330
+
331
+ if (py::isinstance<py::bool_>(first))
332
+ throw py::type_error("with_data() boolean sequences are not supported yet");
333
+
334
+ if (py::isinstance<py::int_>(first) || py::isinstance<py::float_>(first))
335
+ return withNumericSequenceData(datasetField, sequence);
336
+
337
+ throw py::type_error("with_data() sequence items must be numbers or strings");
338
+ }
339
+
340
+ PyPlot& withNumericSequenceData(const std::string& datasetField, py::sequence sequence)
341
+ {
342
+ std::vector<double>& values = ownedDoubles_.emplace_back();
343
+ values.reserve(static_cast<std::size_t>(sequence.size()));
344
+
345
+ for (py::handle item : sequence)
346
+ {
347
+ if (py::isinstance<py::bool_>(item) || !(py::isinstance<py::int_>(item) || py::isinstance<py::float_>(item)))
348
+ throw py::type_error("numeric data sequences cannot contain non-numeric values");
349
+
350
+ values.push_back(py::cast<double>(item));
351
+ }
352
+
353
+ builder_.withData(datasetField, values);
354
+ return *this;
355
+ }
356
+
357
+ PyPlot& withStringSequenceData(const std::string& datasetField, py::sequence sequence)
358
+ {
359
+ std::vector<std::string>& values = ownedStrings_.emplace_back();
360
+ values.reserve(static_cast<std::size_t>(sequence.size()));
361
+
362
+ for (py::handle item : sequence)
363
+ {
364
+ if (!py::isinstance<py::str>(item))
365
+ throw py::type_error("string data sequences cannot contain non-string values");
366
+
367
+ values.push_back(py::cast<std::string>(item));
368
+ }
369
+
370
+ builder_.withData(datasetField, values);
371
+ return *this;
372
+ }
373
+
374
+ FlowPlot::PlotBuilder builder_;
375
+ mutable std::optional<FlowPlot::StbTextEngine> textEngine_{};
376
+ std::vector<py::array> keptArrays_{};
377
+ std::vector<std::vector<double>> ownedDoubles_{};
378
+ std::vector<std::vector<std::string>> ownedStrings_{};
379
+ };
380
+
381
+ PyPlot plot(const std::string& path)
382
+ {
383
+ return PyPlot(path);
384
+ }
385
+ } // namespace
386
+
387
+ PYBIND11_MODULE(_flowplot, module)
388
+ {
389
+ module.doc() = "Python bindings for FlowPlot";
390
+
391
+ py::class_<PyPlot>(module, "Plot")
392
+ .def("set", &PyPlot::set, py::arg("property"), py::arg("value"), py::return_value_policy::reference_internal)
393
+ .def("set_json_raw", &PyPlot::setJsonRaw, py::arg("property"), py::arg("json"), py::return_value_policy::reference_internal)
394
+ .def("setJsonRaw", &PyPlot::setJsonRaw, py::arg("property"), py::arg("json"), py::return_value_policy::reference_internal)
395
+ .def("set_default_font", &PyPlot::setDefaultFont, py::arg("font_path"), py::return_value_policy::reference_internal)
396
+ .def("setDefaultFont", &PyPlot::setDefaultFont, py::arg("font_path"), py::return_value_policy::reference_internal)
397
+ .def("with_data", &PyPlot::withData, py::arg("dataset_field"), py::arg("data"), py::return_value_policy::reference_internal)
398
+ .def("withData", &PyPlot::withData, py::arg("dataset_field"), py::arg("data"), py::return_value_policy::reference_internal)
399
+ .def("get_commands", &PyPlot::getCommands)
400
+ .def("getCommands", &PyPlot::getCommands)
401
+ .def("write_png", &PyPlot::writePng, py::arg("output_path"))
402
+ .def("writePng", &PyPlot::writePng, py::arg("output_path"));
403
+
404
+ module.def("plot", &plot, py::arg("path"));
405
+ module.def(
406
+ "get_complete_json",
407
+ [](const std::string& templateJson, bool pretty)
408
+ {
409
+ return FlowPlot::getCompleteJson(templateJson, pretty);
410
+ },
411
+ py::arg("template_json"),
412
+ py::arg("pretty") = true);
413
+ module.def(
414
+ "getCompleteJson",
415
+ [](const std::string& templateJson, bool pretty)
416
+ {
417
+ return FlowPlot::getCompleteJson(templateJson, pretty);
418
+ },
419
+ py::arg("template_json"),
420
+ py::arg("pretty") = true);
421
+ }
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["scikit-build-core>=0.10", "pybind11>=2.12"]
3
+ build-backend = "scikit_build_core.build"
4
+
5
+ [project]
6
+ name = "FlowPlotPy"
7
+ version = "0.9.0"
8
+ description = "Python bindings for the FlowPlot plotting library"
9
+ readme = "../README.md"
10
+ requires-python = ">=3.10"
11
+ license = { file = "../LICENSE" }
12
+ authors = [
13
+ { name = "FlowPlot contributors" }
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Intended Audience :: Science/Research",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: C++",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Topic :: Scientific/Engineering :: Visualization",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ numpy = ["numpy>=1.23"]
31
+
32
+ [tool.scikit-build]
33
+ cmake.version = ">=3.18"
34
+ build-dir = "build/{wheel_tag}"
35
+
36
+ [tool.scikit-build.wheel]
37
+ packages = ["src/flowplot"]
@@ -0,0 +1,22 @@
1
+ from importlib import resources
2
+
3
+ from . import _flowplot
4
+ from ._flowplot import Plot, getCompleteJson, get_complete_json
5
+
6
+
7
+ def _default_font_resource():
8
+ return resources.files(__package__).joinpath("fonts", "Inter.ttf")
9
+
10
+
11
+ def plot(path):
12
+ plot_obj = _flowplot.plot(path)
13
+ with resources.as_file(_default_font_resource()) as font_path:
14
+ plot_obj.set_default_font(str(font_path))
15
+ return plot_obj
16
+
17
+ __all__ = [
18
+ "Plot",
19
+ "getCompleteJson",
20
+ "get_complete_json",
21
+ "plot",
22
+ ]