icplot 0.1.6__tar.gz → 0.2.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.
- {icplot-0.1.6/src/icplot.egg-info → icplot-0.2.0}/PKG-INFO +3 -2
- {icplot-0.1.6 → icplot-0.2.0}/pyproject.toml +6 -5
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot/geometry.py +69 -11
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot/graph/__init__.py +2 -1
- icplot-0.2.0/src/icplot/graph/mpl.py +203 -0
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot/graph/plot.py +21 -16
- icplot-0.2.0/src/icplot/graph/plot_group.py +80 -0
- icplot-0.2.0/src/icplot/graph/series.py +129 -0
- icplot-0.2.0/src/icplot/graph/vtk.py +81 -0
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot/image_utils.py +35 -4
- icplot-0.2.0/src/icplot/mesh/foam.py +140 -0
- icplot-0.2.0/src/icplot/mesh/mesh.py +280 -0
- icplot-0.2.0/src/icplot/mesh/vtk.py +44 -0
- {icplot-0.1.6 → icplot-0.2.0/src/icplot.egg-info}/PKG-INFO +3 -2
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot.egg-info/SOURCES.txt +2 -0
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot.egg-info/requires.txt +2 -1
- icplot-0.2.0/test/test_geometry.py +69 -0
- icplot-0.2.0/test/test_mesh.py +36 -0
- icplot-0.1.6/src/icplot/graph/mpl.py +0 -216
- icplot-0.1.6/src/icplot/graph/plot_group.py +0 -63
- icplot-0.1.6/src/icplot/graph/series.py +0 -63
- icplot-0.1.6/src/icplot/graph/vtk.py +0 -54
- icplot-0.1.6/src/icplot/mesh/foam.py +0 -56
- icplot-0.1.6/src/icplot/mesh/mesh.py +0 -79
- icplot-0.1.6/test/test_mesh.py +0 -13
- {icplot-0.1.6 → icplot-0.2.0}/LICENSE +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/README.md +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/setup.cfg +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot/__init__.py +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot/cairo_interface.py +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot/color.py +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot/convert.py +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot/gantt/__init__.py +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot/gantt/gantt.py +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot/gantt/gantt_renderer.py +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot/gantt/pgfgantt.py +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot/graph/axis.py +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot/graph/generator.py +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot/graph/matplotlib.py +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot/graph/video.py +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot/main_cli.py +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot/mermaid.py +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot/mesh/__init__.py +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot/py.typed +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot/scene.py +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot/tex.py +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot/trace/__init__.py +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot/trace/trace.py +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot.egg-info/dependency_links.txt +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot.egg-info/entry_points.txt +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/src/icplot.egg-info/top_level.txt +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/test/test_cairo_interface.py +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/test/test_gantt_chart.py +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/test/test_image_utils.py +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/test/test_line_plots.py +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/test/test_mermaid.py +0 -0
- {icplot-0.1.6 → icplot-0.2.0}/test/test_tex.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: icplot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Utilities for generating plots and graphics for technical reports.
|
|
5
5
|
Author-email: "James Grogan, Irish Centre for High End Computing" <james.grogan@ichec.ie>
|
|
6
6
|
Project-URL: Repository, https://git.ichec.ie/performance/toolshed/icplot
|
|
@@ -18,7 +18,8 @@ Requires-Dist: wand
|
|
|
18
18
|
Requires-Dist: matplotlib
|
|
19
19
|
Requires-Dist: vtk
|
|
20
20
|
Requires-Dist: moviepy
|
|
21
|
-
Requires-Dist:
|
|
21
|
+
Requires-Dist: scipy
|
|
22
|
+
Requires-Dist: iccore==0.2.0
|
|
22
23
|
Provides-Extra: test
|
|
23
24
|
Requires-Dist: pytest; extra == "test"
|
|
24
25
|
Requires-Dist: pytest-cov; extra == "test"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "icplot"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
authors = [
|
|
5
5
|
{ name="James Grogan, Irish Centre for High End Computing", email="james.grogan@ichec.ie" },
|
|
6
6
|
]
|
|
@@ -20,7 +20,8 @@ dependencies = ["wand",
|
|
|
20
20
|
"matplotlib",
|
|
21
21
|
"vtk",
|
|
22
22
|
"moviepy",
|
|
23
|
-
|
|
23
|
+
"scipy",
|
|
24
|
+
"iccore==0.2.0"]
|
|
24
25
|
|
|
25
26
|
[project.urls]
|
|
26
27
|
Repository = "https://git.ichec.ie/performance/toolshed/icplot"
|
|
@@ -50,7 +51,7 @@ build-backend = "setuptools.build_meta"
|
|
|
50
51
|
icplot = "icplot.main_cli:main_cli"
|
|
51
52
|
|
|
52
53
|
[tool.setuptools.package-data]
|
|
53
|
-
"icplot" = ["py.typed"]
|
|
54
|
+
"icplot" = ["py.typed", "controlDict"]
|
|
54
55
|
|
|
55
56
|
[tool.setuptools.packages.find]
|
|
56
57
|
where = ["src"]
|
|
@@ -61,7 +62,7 @@ ignore_missing_imports = true
|
|
|
61
62
|
[tool.pylint.main]
|
|
62
63
|
|
|
63
64
|
source-roots = ["src"]
|
|
64
|
-
fail-under = 9.
|
|
65
|
+
fail-under = 9.2
|
|
65
66
|
|
|
66
67
|
[tool.pylint.basic]
|
|
67
68
|
|
|
@@ -105,7 +106,7 @@ disable = ["raw-checker-failed", "missing-module-docstring", "missing-function-d
|
|
|
105
106
|
testpaths = ["test",]
|
|
106
107
|
log_cli = 1
|
|
107
108
|
log_cli_level = "debug"
|
|
108
|
-
addopts = "--cov=icplot --cov-report term --cov-fail-under=
|
|
109
|
+
addopts = "--cov=icplot --cov-report term --cov-fail-under=55 --cov-report xml:coverage.xml --cov-report html"
|
|
109
110
|
|
|
110
111
|
[tool.tox]
|
|
111
112
|
requires = ["tox"]
|
|
@@ -5,9 +5,11 @@ dependent work.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
|
-
import copy
|
|
9
8
|
from dataclasses import dataclass
|
|
10
9
|
|
|
10
|
+
import numpy as np
|
|
11
|
+
from scipy.spatial.transform import Rotation
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
@dataclass(frozen=True)
|
|
13
15
|
class Vector:
|
|
@@ -22,6 +24,9 @@ class Vector:
|
|
|
22
24
|
def scale(self, factor: float) -> Vector:
|
|
23
25
|
return Vector(self.x * factor, self.y * factor, self.z * factor)
|
|
24
26
|
|
|
27
|
+
def as_array(self) -> np.ndarray:
|
|
28
|
+
return np.array([self.x, self.y, self.z])
|
|
29
|
+
|
|
25
30
|
|
|
26
31
|
@dataclass(frozen=True)
|
|
27
32
|
class Point:
|
|
@@ -33,9 +38,16 @@ class Point:
|
|
|
33
38
|
y: float = 0.0
|
|
34
39
|
z: float = 0.0
|
|
35
40
|
|
|
41
|
+
def as_array(self) -> np.ndarray:
|
|
42
|
+
return np.array([self.x, self.y, self.z])
|
|
43
|
+
|
|
36
44
|
def translate(self, v: Vector) -> Point:
|
|
37
45
|
return Point(self.x + v.x, self.y + v.y, self.z + v.z)
|
|
38
46
|
|
|
47
|
+
@staticmethod
|
|
48
|
+
def from_array(arr: np.ndarray) -> Point:
|
|
49
|
+
return Point(arr[0], arr[1], arr[2])
|
|
50
|
+
|
|
39
51
|
|
|
40
52
|
@dataclass(frozen=True)
|
|
41
53
|
class Shape:
|
|
@@ -43,10 +55,26 @@ class Shape:
|
|
|
43
55
|
A shape in 3d space, defaults to having a normal to Z
|
|
44
56
|
"""
|
|
45
57
|
|
|
46
|
-
loc: Point
|
|
58
|
+
loc: Point = Point(0.0, 0.0, 0.0)
|
|
47
59
|
normal: Vector = Vector(0.0, 0.0, 1.0)
|
|
48
60
|
|
|
49
61
|
|
|
62
|
+
def get_rotation_matrix(vec0, vec1):
|
|
63
|
+
"""get rotation matrix between two vectors using scipy"""
|
|
64
|
+
vec0 = np.reshape(vec0, (1, -1))
|
|
65
|
+
vec1 = np.reshape(vec1, (1, -1))
|
|
66
|
+
r = Rotation.align_vectors(vec0, vec1)
|
|
67
|
+
return r[0].as_matrix()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_normal(p0, p1, p2):
|
|
71
|
+
direction = np.cross(p1 - p0, p2 - p0)
|
|
72
|
+
return direction / np.linalg.norm(direction)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
_GLOBAL_Z = np.array([0.0, 0.0, 1.0])
|
|
76
|
+
|
|
77
|
+
|
|
50
78
|
@dataclass(frozen=True)
|
|
51
79
|
class Quad(Shape):
|
|
52
80
|
"""
|
|
@@ -58,14 +86,32 @@ class Quad(Shape):
|
|
|
58
86
|
|
|
59
87
|
@property
|
|
60
88
|
def points(self) -> list[Point]:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
89
|
+
|
|
90
|
+
points = [
|
|
91
|
+
self.loc.translate(Vector(-self.width / 2.0, -self.height / 2.0)),
|
|
92
|
+
self.loc.translate(Vector(self.width / 2.0, -self.height / 2.0)),
|
|
93
|
+
self.loc.translate(Vector(self.width / 2.0, self.height / 2.0)),
|
|
94
|
+
self.loc.translate(Vector(-self.width / 2.0, self.height / 2.0)),
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
rot = get_rotation_matrix(_GLOBAL_Z, self.normal.as_array())
|
|
98
|
+
rotated_points = [
|
|
99
|
+
Point.from_array(
|
|
100
|
+
rot.dot(p.as_array() - self.loc.as_array()) + self.loc.as_array()
|
|
101
|
+
)
|
|
102
|
+
for p in points
|
|
67
103
|
]
|
|
68
104
|
|
|
105
|
+
plane_normal = get_normal(
|
|
106
|
+
rotated_points[0].as_array(),
|
|
107
|
+
rotated_points[1].as_array(),
|
|
108
|
+
rotated_points[3].as_array(),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if np.dot(plane_normal, self.normal.as_array()) < 0:
|
|
112
|
+
rotated_points.reverse()
|
|
113
|
+
return rotated_points
|
|
114
|
+
|
|
69
115
|
def translate(self, v: Vector) -> Quad:
|
|
70
116
|
return Quad(self.loc.translate(v), self.normal, self.width, self.height)
|
|
71
117
|
|
|
@@ -76,10 +122,10 @@ class Cuboid(Shape):
|
|
|
76
122
|
A regular cuboid
|
|
77
123
|
"""
|
|
78
124
|
|
|
79
|
-
loc: Point
|
|
80
125
|
width: float = 1.0
|
|
81
126
|
height: float = 1.0
|
|
82
127
|
depth: float = 1.0
|
|
128
|
+
top_width_scale: float = 1.0
|
|
83
129
|
|
|
84
130
|
def translate(self, v: Vector) -> Cuboid:
|
|
85
131
|
return Cuboid(
|
|
@@ -89,8 +135,20 @@ class Cuboid(Shape):
|
|
|
89
135
|
@property
|
|
90
136
|
def points(self) -> list[Point]:
|
|
91
137
|
|
|
92
|
-
base = Quad(
|
|
93
|
-
|
|
138
|
+
base = Quad(
|
|
139
|
+
self.loc.translate(Vector(0.0, 0.0, -self.depth / 2.0)),
|
|
140
|
+
self.normal,
|
|
141
|
+
self.width,
|
|
142
|
+
self.height,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
top = Quad(
|
|
146
|
+
self.loc.translate(Vector(0.0, 0.0, -self.depth / 2.0)),
|
|
147
|
+
self.normal,
|
|
148
|
+
self.width * self.top_width_scale,
|
|
149
|
+
self.height,
|
|
150
|
+
)
|
|
151
|
+
top = top.translate(self.normal.scale(self.depth))
|
|
94
152
|
return base.points + top.points
|
|
95
153
|
|
|
96
154
|
|
|
@@ -2,5 +2,6 @@ from .plot import * # NOQA
|
|
|
2
2
|
from .series import * # NOQA
|
|
3
3
|
from .axis import * # NOQA
|
|
4
4
|
from .generator import plot_file
|
|
5
|
+
from .plot_group import PlotGroupCreate, PlotGroupPublic
|
|
5
6
|
|
|
6
|
-
__all__ = ["plot_file"]
|
|
7
|
+
__all__ = ["plot_file", "PlotGroupCreate", "PlotGroupPublic"]
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module to support plotting with matplotlib
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import shutil
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import matplotlib.pyplot as plt
|
|
11
|
+
|
|
12
|
+
from iccore.data.measurement import MeasurementPublicWithUnits
|
|
13
|
+
from iccore.data.units import to_date_str
|
|
14
|
+
from iccore.data import Series, UnitPublic
|
|
15
|
+
|
|
16
|
+
from .plot_group import PlotGroupPublic
|
|
17
|
+
from .series import PlotSeriesPublic
|
|
18
|
+
from .video import images_to_video
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_axis_label(
|
|
22
|
+
measurement: MeasurementPublicWithUnits, unit: UnitPublic | None = None
|
|
23
|
+
) -> str:
|
|
24
|
+
|
|
25
|
+
if unit:
|
|
26
|
+
resolved_unit = unit
|
|
27
|
+
else:
|
|
28
|
+
resolved_unit = measurement.unit
|
|
29
|
+
|
|
30
|
+
if measurement.name == "time":
|
|
31
|
+
return f"{measurement.get_long_name()} (UTC)"
|
|
32
|
+
|
|
33
|
+
if measurement.unit.symbol:
|
|
34
|
+
return f"{measurement.get_long_name()} ({resolved_unit.symbol})"
|
|
35
|
+
|
|
36
|
+
return measurement.long_name
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_grid(series: Series):
|
|
40
|
+
|
|
41
|
+
if not series.x or not series.y:
|
|
42
|
+
raise RuntimeError("Series missing expected data")
|
|
43
|
+
|
|
44
|
+
return np.meshgrid(series.get_x_array().data, series.get_y_array().data)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def save_plot(fig, path: Path, filename: str, file_prefix: str = ""):
|
|
48
|
+
plt.tight_layout()
|
|
49
|
+
prefix = f"{file_prefix}_" if file_prefix else ""
|
|
50
|
+
os.makedirs(path, exist_ok=True)
|
|
51
|
+
plt.savefig(path / f"{prefix}{filename}")
|
|
52
|
+
plt.clf()
|
|
53
|
+
plt.close(fig)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_date_suffix(group: PlotGroupPublic) -> str:
|
|
57
|
+
if not group.has_date_range:
|
|
58
|
+
return ""
|
|
59
|
+
start, end = group.date_range
|
|
60
|
+
return f"_FROM_{to_date_str(start)}_TO_{to_date_str(end)}"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def plot_2d(
|
|
64
|
+
path: Path, group: PlotGroupPublic, series: PlotSeriesPublic, data_series: Series
|
|
65
|
+
):
|
|
66
|
+
"""
|
|
67
|
+
Generate a contour plot for the provided quantity
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
if not data_series.x or not series.x:
|
|
71
|
+
raise RuntimeError("Expected series with an x quantity")
|
|
72
|
+
|
|
73
|
+
if not data_series.y or not series.y:
|
|
74
|
+
raise RuntimeError("Expected series with a y quantity.")
|
|
75
|
+
|
|
76
|
+
x, y = get_grid(data_series)
|
|
77
|
+
|
|
78
|
+
array = data_series.get_array(series.measurement.name)
|
|
79
|
+
|
|
80
|
+
fig, ax = plt.subplots()
|
|
81
|
+
|
|
82
|
+
ax.set_xlabel(get_axis_label(series.x))
|
|
83
|
+
ax.set_ylabel(get_axis_label(series.y))
|
|
84
|
+
|
|
85
|
+
if group.has_date_range:
|
|
86
|
+
ax.set_xlim(group.date_range) # type: ignore
|
|
87
|
+
fig.autofmt_xdate()
|
|
88
|
+
|
|
89
|
+
if group.contour.show_grid:
|
|
90
|
+
ax.grid(c="k", ls="-", alpha=0.3)
|
|
91
|
+
|
|
92
|
+
if False and array.quantity.has_limits:
|
|
93
|
+
cs = ax.contourf(
|
|
94
|
+
x,
|
|
95
|
+
y,
|
|
96
|
+
np.clip(array.data.T, array.quantity.limits[0], array.quantity.limits[1]),
|
|
97
|
+
cmap=group.contour.colormap,
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
cs = ax.contourf(x, y, np.array(array.data).T, cmap=group.contour.colormap)
|
|
101
|
+
|
|
102
|
+
cbar = fig.colorbar(cs)
|
|
103
|
+
cbar.ax.set_ylabel(get_axis_label(series.measurement, series.unit))
|
|
104
|
+
|
|
105
|
+
date_str = get_date_suffix(group)
|
|
106
|
+
save_plot(fig, path, f"{series.measurement.name}{date_str}.{group.contour.format}")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def plot_1d(
|
|
110
|
+
path: Path, group: PlotGroupPublic, series: PlotSeriesPublic, data_series: Series
|
|
111
|
+
):
|
|
112
|
+
"""
|
|
113
|
+
Generate a line plot for the provided quantity
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
if not data_series.x or not series.x:
|
|
117
|
+
raise RuntimeError("Attempted to plot series without x data")
|
|
118
|
+
|
|
119
|
+
fig, ax = plt.subplots()
|
|
120
|
+
ax.set_xlabel(get_axis_label(series.x))
|
|
121
|
+
ax.set_ylabel(get_axis_label(series.measurement, series.unit))
|
|
122
|
+
|
|
123
|
+
if group.has_date_range:
|
|
124
|
+
ax.set_xlim(group.date_range) # type: ignore
|
|
125
|
+
fig.autofmt_xdate()
|
|
126
|
+
|
|
127
|
+
if series.has_limits:
|
|
128
|
+
ax.set_ylim(series.limits) # type: ignore
|
|
129
|
+
|
|
130
|
+
x = data_series.get_array(series.x.name).data
|
|
131
|
+
y = data_series.get_array(series.measurement.name).data
|
|
132
|
+
ax.plot(x, y)
|
|
133
|
+
|
|
134
|
+
date_str = get_date_suffix(group)
|
|
135
|
+
save_plot(
|
|
136
|
+
fig,
|
|
137
|
+
path,
|
|
138
|
+
f"{series.measurement.name}{date_str}.{group.line.format}",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def plot_array(path: Path, series: PlotSeriesPublic, data_series: Series):
|
|
143
|
+
"""
|
|
144
|
+
Plot an array for a single series x value. Useful for
|
|
145
|
+
time series video frames.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
array = data_series.get_array(series.measurement.name)
|
|
149
|
+
|
|
150
|
+
if not data_series.x or not data_series.y or not series.y:
|
|
151
|
+
raise RuntimeError("Attempted to plot series with missing data")
|
|
152
|
+
|
|
153
|
+
for idx, (x, values) in enumerate(
|
|
154
|
+
zip(data_series.get_array(data_series.x).data, array.data)
|
|
155
|
+
):
|
|
156
|
+
fig, ax = plt.subplots()
|
|
157
|
+
|
|
158
|
+
ax.set_title(x)
|
|
159
|
+
ax.set_xlabel(get_axis_label(series.measurement, series.unit))
|
|
160
|
+
ax.set_ylabel(get_axis_label(series.y))
|
|
161
|
+
|
|
162
|
+
if series.has_limits:
|
|
163
|
+
ax.set_xlim(series.limits) # type: ignore
|
|
164
|
+
|
|
165
|
+
ax.plot(values, data_series.get_array(data_series.y).data)
|
|
166
|
+
|
|
167
|
+
save_plot(fig, path / f"{series.measurement.name}", f"{idx}.png")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def make_video(
|
|
171
|
+
path: Path, group: PlotGroupPublic, series: PlotSeriesPublic, data_series: Series
|
|
172
|
+
):
|
|
173
|
+
|
|
174
|
+
plot_array(path, series, data_series)
|
|
175
|
+
|
|
176
|
+
if not group.video:
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
images_to_video(
|
|
180
|
+
path / f"{series.measurement.name}",
|
|
181
|
+
path,
|
|
182
|
+
f"{series.measurement.name}{get_date_suffix(group)}",
|
|
183
|
+
fps=group.video.fps,
|
|
184
|
+
video_format=group.video.format,
|
|
185
|
+
)
|
|
186
|
+
shutil.rmtree(path / f"{series.measurement.name}")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def plot(
|
|
190
|
+
path: Path, group: PlotGroupPublic, series: PlotSeriesPublic, data_series: Series
|
|
191
|
+
):
|
|
192
|
+
"""
|
|
193
|
+
Handle plotting 1-d and 2-d time series and videos for the given quantity
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
if data_series.y:
|
|
197
|
+
plot_2d(path, group, series, data_series)
|
|
198
|
+
|
|
199
|
+
if group.video and group.video.active:
|
|
200
|
+
make_video(path, group, series, data_series)
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
plot_1d(path, group, series, data_series)
|
|
@@ -8,15 +8,15 @@ import os
|
|
|
8
8
|
|
|
9
9
|
from pydantic import BaseModel
|
|
10
10
|
|
|
11
|
-
from iccore.data.quantity import Quantity
|
|
12
11
|
from iccore.data import Series
|
|
12
|
+
from iccore.data.series import convert_units
|
|
13
13
|
|
|
14
14
|
from icplot.color import ColorMap, Color
|
|
15
15
|
from icplot.graph import mpl, vtk
|
|
16
16
|
|
|
17
17
|
from .axis import PlotAxis
|
|
18
18
|
from .series import LinePlotSeries, ScatterPlotSeries, ImageSeries
|
|
19
|
-
from .plot_group import
|
|
19
|
+
from .plot_group import PlotGroupPublic, PlotSeriesPublic
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class Plot(BaseModel, frozen=True):
|
|
@@ -95,23 +95,28 @@ def get_series_colors(cmap: ColorMap, plot: Plot) -> list[Color]:
|
|
|
95
95
|
return colors
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
def
|
|
98
|
+
def plot_series(
|
|
99
|
+
path: Path,
|
|
100
|
+
group: PlotGroupPublic,
|
|
101
|
+
series: PlotSeriesPublic,
|
|
102
|
+
data_series: Series,
|
|
103
|
+
):
|
|
99
104
|
|
|
100
|
-
|
|
101
|
-
output_path = path / quantity.sensor / schema_name
|
|
105
|
+
output_path = path / series.measurement.product_name
|
|
102
106
|
|
|
103
107
|
os.makedirs(output_path, exist_ok=True)
|
|
104
108
|
|
|
105
|
-
|
|
106
|
-
|
|
109
|
+
# Convert units
|
|
110
|
+
if series.measurement.unit != series.unit:
|
|
111
|
+
converted_data_series = convert_units(
|
|
112
|
+
data_series,
|
|
113
|
+
((series.measurement.name, series.measurement.unit, series.unit),),
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
converted_data_series = data_series
|
|
107
117
|
|
|
118
|
+
if "mpl" in group.formats:
|
|
119
|
+
mpl.plot(output_path, group, series, converted_data_series)
|
|
108
120
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
output_path = path / sensor / name
|
|
112
|
-
|
|
113
|
-
os.makedirs(output_path, exist_ok=True)
|
|
114
|
-
|
|
115
|
-
if "vtk" in config.formats:
|
|
116
|
-
if series.y:
|
|
117
|
-
vtk.save(output_path, config, series)
|
|
121
|
+
if "vtk" in group.formats and data_series.y:
|
|
122
|
+
vtk.save(output_path, group, series, converted_data_series)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module to handle generation of plots
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from .series import PlotSeriesCreate, PlotSeriesPublic
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class VideoConfig:
|
|
15
|
+
"""
|
|
16
|
+
Representation of a video
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
fps: int = 5
|
|
20
|
+
format: str = "mp4"
|
|
21
|
+
active: bool = True
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class ContourConfig:
|
|
26
|
+
"""
|
|
27
|
+
Representation of a contour plot
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
show_grid: bool = True
|
|
31
|
+
colormap: str = "rainbow"
|
|
32
|
+
format: str = "png"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class LineConfig:
|
|
37
|
+
"""
|
|
38
|
+
Representatino of a line plot
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
format: str = "png"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class PlotGroupBase(BaseModel, frozen=True):
|
|
45
|
+
|
|
46
|
+
start_datetime: datetime | None = None
|
|
47
|
+
end_datetime: datetime | None = None
|
|
48
|
+
video: VideoConfig | None = None
|
|
49
|
+
contour: ContourConfig = ContourConfig()
|
|
50
|
+
line: LineConfig = LineConfig()
|
|
51
|
+
formats: tuple[str, ...] = ("mpl",)
|
|
52
|
+
active: bool = True
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def has_date_range(self) -> bool:
|
|
56
|
+
return self.start_datetime is not None and self.end_datetime is not None
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def date_range(self) -> tuple[datetime, datetime]:
|
|
60
|
+
|
|
61
|
+
if self.start_datetime is None or self.end_datetime is None:
|
|
62
|
+
raise ValueError("Requested data range but none set")
|
|
63
|
+
|
|
64
|
+
return self.start_datetime, self.end_datetime
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class PlotGroupCreate(PlotGroupBase, frozen=True):
|
|
68
|
+
"""
|
|
69
|
+
A group of plots to be generated, one per quantity.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
series: list[PlotSeriesCreate] = []
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class PlotGroupPublic(PlotGroupBase, frozen=True):
|
|
76
|
+
"""
|
|
77
|
+
A group of plots to be generated, one per quantity.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
series: list[PlotSeriesPublic] = []
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A data series in a plot
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import typing
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from iccore.data import (
|
|
10
|
+
ProductPublicWithMeasurements,
|
|
11
|
+
MeasurementPublicWithUnits,
|
|
12
|
+
Unit,
|
|
13
|
+
UnitPublic,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from icplot.color import Color
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PlotSeriesBase(BaseModel, frozen=True):
|
|
20
|
+
|
|
21
|
+
min_value: float | None = None
|
|
22
|
+
max_value: float | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PlotSeriesCreate(PlotSeriesBase, frozen=True):
|
|
26
|
+
|
|
27
|
+
measurement: str
|
|
28
|
+
unit_name: str = ""
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def product(self) -> str:
|
|
32
|
+
return self.measurement.split(".")[0]
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def base_measurement(self) -> str:
|
|
36
|
+
return self.measurement.split(".")[1]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PlotSeriesPublic(PlotSeriesBase, frozen=True):
|
|
40
|
+
|
|
41
|
+
measurement: MeasurementPublicWithUnits
|
|
42
|
+
unit: UnitPublic
|
|
43
|
+
x: MeasurementPublicWithUnits | None
|
|
44
|
+
y: MeasurementPublicWithUnits | None
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def has_limits(self) -> bool:
|
|
48
|
+
return self.min_value is not None and self.max_value is not None
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def limits(self) -> tuple[float, float]:
|
|
52
|
+
if self.max_value is None or self.min_value is None:
|
|
53
|
+
raise ValueError("Attempted to access limits but none set")
|
|
54
|
+
return self.min_value, self.max_value
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def from_create(
|
|
58
|
+
cls, create: PlotSeriesCreate, product: ProductPublicWithMeasurements
|
|
59
|
+
) -> "PlotSeriesPublic":
|
|
60
|
+
|
|
61
|
+
measurement = product.get_measurement(create.base_measurement)
|
|
62
|
+
|
|
63
|
+
if create.unit_name and create.unit_name != measurement.unit.name:
|
|
64
|
+
unit = Unit.object(create.unit_name, "name", return_t=UnitPublic)
|
|
65
|
+
else:
|
|
66
|
+
unit = measurement.unit
|
|
67
|
+
|
|
68
|
+
return PlotSeriesPublic(
|
|
69
|
+
measurement=measurement,
|
|
70
|
+
unit=unit,
|
|
71
|
+
min_value=create.min_value,
|
|
72
|
+
max_value=create.max_value,
|
|
73
|
+
x=product.x,
|
|
74
|
+
y=product.y,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class PlotSeries(BaseModel, frozen=True):
|
|
79
|
+
"""
|
|
80
|
+
A data series in a plot, such as a single line in a line-plot
|
|
81
|
+
|
|
82
|
+
:param position_right: allows the series to be plotted on the right y-axis.
|
|
83
|
+
:type position_right: bool, optional
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
label: str
|
|
87
|
+
color: Color = Color()
|
|
88
|
+
series_type: str = ""
|
|
89
|
+
highlight: bool = False
|
|
90
|
+
position_right: bool = False
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ImageSeries(PlotSeries, frozen=True):
|
|
94
|
+
"""
|
|
95
|
+
A plot data series where the elements are images
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
data: typing.Any
|
|
99
|
+
transform: typing.Any
|
|
100
|
+
series_type: str = "image"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class LinePlotSeries(PlotSeries, frozen=True):
|
|
104
|
+
"""
|
|
105
|
+
A plot series for line plots
|
|
106
|
+
|
|
107
|
+
:param drawstyle: Naming comes from matplotlib API, allows for various square plots,
|
|
108
|
+
default is a normal point to point line plot.
|
|
109
|
+
:type drawstyle: str, optional
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
x: list
|
|
113
|
+
y: list
|
|
114
|
+
x_err: float | list[float] | list[list[float]] | None = None
|
|
115
|
+
y_err: float | list[float] | list[list[float]] | None = None
|
|
116
|
+
err_capsize: float = 2.0
|
|
117
|
+
marker: str = "o"
|
|
118
|
+
series_type: str = "line"
|
|
119
|
+
drawstyle: str = "default"
|
|
120
|
+
linestyle: str = "-"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class ScatterPlotSeries(PlotSeries, frozen=True):
|
|
124
|
+
"""
|
|
125
|
+
Scatter type plot series
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
data: typing.Any
|
|
129
|
+
series_type: str = "scatter"
|