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
|
+
]
|
|
Binary file
|