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.
Files changed (57) hide show
  1. {icplot-0.1.6/src/icplot.egg-info → icplot-0.2.0}/PKG-INFO +3 -2
  2. {icplot-0.1.6 → icplot-0.2.0}/pyproject.toml +6 -5
  3. {icplot-0.1.6 → icplot-0.2.0}/src/icplot/geometry.py +69 -11
  4. {icplot-0.1.6 → icplot-0.2.0}/src/icplot/graph/__init__.py +2 -1
  5. icplot-0.2.0/src/icplot/graph/mpl.py +203 -0
  6. {icplot-0.1.6 → icplot-0.2.0}/src/icplot/graph/plot.py +21 -16
  7. icplot-0.2.0/src/icplot/graph/plot_group.py +80 -0
  8. icplot-0.2.0/src/icplot/graph/series.py +129 -0
  9. icplot-0.2.0/src/icplot/graph/vtk.py +81 -0
  10. {icplot-0.1.6 → icplot-0.2.0}/src/icplot/image_utils.py +35 -4
  11. icplot-0.2.0/src/icplot/mesh/foam.py +140 -0
  12. icplot-0.2.0/src/icplot/mesh/mesh.py +280 -0
  13. icplot-0.2.0/src/icplot/mesh/vtk.py +44 -0
  14. {icplot-0.1.6 → icplot-0.2.0/src/icplot.egg-info}/PKG-INFO +3 -2
  15. {icplot-0.1.6 → icplot-0.2.0}/src/icplot.egg-info/SOURCES.txt +2 -0
  16. {icplot-0.1.6 → icplot-0.2.0}/src/icplot.egg-info/requires.txt +2 -1
  17. icplot-0.2.0/test/test_geometry.py +69 -0
  18. icplot-0.2.0/test/test_mesh.py +36 -0
  19. icplot-0.1.6/src/icplot/graph/mpl.py +0 -216
  20. icplot-0.1.6/src/icplot/graph/plot_group.py +0 -63
  21. icplot-0.1.6/src/icplot/graph/series.py +0 -63
  22. icplot-0.1.6/src/icplot/graph/vtk.py +0 -54
  23. icplot-0.1.6/src/icplot/mesh/foam.py +0 -56
  24. icplot-0.1.6/src/icplot/mesh/mesh.py +0 -79
  25. icplot-0.1.6/test/test_mesh.py +0 -13
  26. {icplot-0.1.6 → icplot-0.2.0}/LICENSE +0 -0
  27. {icplot-0.1.6 → icplot-0.2.0}/README.md +0 -0
  28. {icplot-0.1.6 → icplot-0.2.0}/setup.cfg +0 -0
  29. {icplot-0.1.6 → icplot-0.2.0}/src/icplot/__init__.py +0 -0
  30. {icplot-0.1.6 → icplot-0.2.0}/src/icplot/cairo_interface.py +0 -0
  31. {icplot-0.1.6 → icplot-0.2.0}/src/icplot/color.py +0 -0
  32. {icplot-0.1.6 → icplot-0.2.0}/src/icplot/convert.py +0 -0
  33. {icplot-0.1.6 → icplot-0.2.0}/src/icplot/gantt/__init__.py +0 -0
  34. {icplot-0.1.6 → icplot-0.2.0}/src/icplot/gantt/gantt.py +0 -0
  35. {icplot-0.1.6 → icplot-0.2.0}/src/icplot/gantt/gantt_renderer.py +0 -0
  36. {icplot-0.1.6 → icplot-0.2.0}/src/icplot/gantt/pgfgantt.py +0 -0
  37. {icplot-0.1.6 → icplot-0.2.0}/src/icplot/graph/axis.py +0 -0
  38. {icplot-0.1.6 → icplot-0.2.0}/src/icplot/graph/generator.py +0 -0
  39. {icplot-0.1.6 → icplot-0.2.0}/src/icplot/graph/matplotlib.py +0 -0
  40. {icplot-0.1.6 → icplot-0.2.0}/src/icplot/graph/video.py +0 -0
  41. {icplot-0.1.6 → icplot-0.2.0}/src/icplot/main_cli.py +0 -0
  42. {icplot-0.1.6 → icplot-0.2.0}/src/icplot/mermaid.py +0 -0
  43. {icplot-0.1.6 → icplot-0.2.0}/src/icplot/mesh/__init__.py +0 -0
  44. {icplot-0.1.6 → icplot-0.2.0}/src/icplot/py.typed +0 -0
  45. {icplot-0.1.6 → icplot-0.2.0}/src/icplot/scene.py +0 -0
  46. {icplot-0.1.6 → icplot-0.2.0}/src/icplot/tex.py +0 -0
  47. {icplot-0.1.6 → icplot-0.2.0}/src/icplot/trace/__init__.py +0 -0
  48. {icplot-0.1.6 → icplot-0.2.0}/src/icplot/trace/trace.py +0 -0
  49. {icplot-0.1.6 → icplot-0.2.0}/src/icplot.egg-info/dependency_links.txt +0 -0
  50. {icplot-0.1.6 → icplot-0.2.0}/src/icplot.egg-info/entry_points.txt +0 -0
  51. {icplot-0.1.6 → icplot-0.2.0}/src/icplot.egg-info/top_level.txt +0 -0
  52. {icplot-0.1.6 → icplot-0.2.0}/test/test_cairo_interface.py +0 -0
  53. {icplot-0.1.6 → icplot-0.2.0}/test/test_gantt_chart.py +0 -0
  54. {icplot-0.1.6 → icplot-0.2.0}/test/test_image_utils.py +0 -0
  55. {icplot-0.1.6 → icplot-0.2.0}/test/test_line_plots.py +0 -0
  56. {icplot-0.1.6 → icplot-0.2.0}/test/test_mermaid.py +0 -0
  57. {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.1.6
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: iccore==0.1.13
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.1.6"
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
- "iccore==0.1.13"]
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.33
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=59 --cov-report xml:coverage.xml --cov-report html"
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
- p = copy.deepcopy(self.loc)
62
- return [
63
- p,
64
- p.translate(Vector(self.width)),
65
- p.translate(Vector(self.width, self.height)),
66
- p.translate(Vector(0.0, self.height)),
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(self.loc, self.normal, self.width, self.height)
93
- top = base.translate(self.normal.scale(self.depth))
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 PlotGroup
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 plot_quantity(path: Path, config: PlotGroup, series: Series, quantity: Quantity):
98
+ def plot_series(
99
+ path: Path,
100
+ group: PlotGroupPublic,
101
+ series: PlotSeriesPublic,
102
+ data_series: Series,
103
+ ):
99
104
 
100
- schema_name = "" if quantity.schema_name == "default" else quantity.schema_name
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
- if "mpl" in config.formats:
106
- mpl.plot(output_path, config, series, quantity)
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
- def plot_series(path: Path, config: PlotGroup, series: Series, sensor: str, name: str):
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"