icplot 0.1.7__tar.gz → 0.2.1__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 (54) hide show
  1. {icplot-0.1.7/src/icplot.egg-info → icplot-0.2.1}/PKG-INFO +2 -2
  2. {icplot-0.1.7 → icplot-0.2.1}/pyproject.toml +3 -3
  3. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/graph/__init__.py +2 -1
  4. icplot-0.2.1/src/icplot/graph/mpl.py +203 -0
  5. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/graph/plot.py +21 -16
  6. icplot-0.2.1/src/icplot/graph/plot_group.py +80 -0
  7. icplot-0.2.1/src/icplot/graph/series.py +129 -0
  8. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/graph/vtk.py +19 -11
  9. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/mesh/mesh.py +23 -17
  10. {icplot-0.1.7 → icplot-0.2.1/src/icplot.egg-info}/PKG-INFO +2 -2
  11. {icplot-0.1.7 → icplot-0.2.1}/src/icplot.egg-info/SOURCES.txt +1 -0
  12. {icplot-0.1.7 → icplot-0.2.1}/src/icplot.egg-info/requires.txt +1 -1
  13. icplot-0.2.1/test/test_plot_group.py +35 -0
  14. icplot-0.1.7/src/icplot/graph/mpl.py +0 -216
  15. icplot-0.1.7/src/icplot/graph/plot_group.py +0 -63
  16. icplot-0.1.7/src/icplot/graph/series.py +0 -63
  17. {icplot-0.1.7 → icplot-0.2.1}/LICENSE +0 -0
  18. {icplot-0.1.7 → icplot-0.2.1}/README.md +0 -0
  19. {icplot-0.1.7 → icplot-0.2.1}/setup.cfg +0 -0
  20. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/__init__.py +0 -0
  21. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/cairo_interface.py +0 -0
  22. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/color.py +0 -0
  23. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/convert.py +0 -0
  24. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/gantt/__init__.py +0 -0
  25. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/gantt/gantt.py +0 -0
  26. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/gantt/gantt_renderer.py +0 -0
  27. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/gantt/pgfgantt.py +0 -0
  28. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/geometry.py +0 -0
  29. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/graph/axis.py +0 -0
  30. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/graph/generator.py +0 -0
  31. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/graph/matplotlib.py +0 -0
  32. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/graph/video.py +0 -0
  33. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/image_utils.py +0 -0
  34. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/main_cli.py +0 -0
  35. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/mermaid.py +0 -0
  36. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/mesh/__init__.py +0 -0
  37. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/mesh/foam.py +0 -0
  38. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/mesh/vtk.py +0 -0
  39. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/py.typed +0 -0
  40. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/scene.py +0 -0
  41. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/tex.py +0 -0
  42. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/trace/__init__.py +0 -0
  43. {icplot-0.1.7 → icplot-0.2.1}/src/icplot/trace/trace.py +0 -0
  44. {icplot-0.1.7 → icplot-0.2.1}/src/icplot.egg-info/dependency_links.txt +0 -0
  45. {icplot-0.1.7 → icplot-0.2.1}/src/icplot.egg-info/entry_points.txt +0 -0
  46. {icplot-0.1.7 → icplot-0.2.1}/src/icplot.egg-info/top_level.txt +0 -0
  47. {icplot-0.1.7 → icplot-0.2.1}/test/test_cairo_interface.py +0 -0
  48. {icplot-0.1.7 → icplot-0.2.1}/test/test_gantt_chart.py +0 -0
  49. {icplot-0.1.7 → icplot-0.2.1}/test/test_geometry.py +0 -0
  50. {icplot-0.1.7 → icplot-0.2.1}/test/test_image_utils.py +0 -0
  51. {icplot-0.1.7 → icplot-0.2.1}/test/test_line_plots.py +0 -0
  52. {icplot-0.1.7 → icplot-0.2.1}/test/test_mermaid.py +0 -0
  53. {icplot-0.1.7 → icplot-0.2.1}/test/test_mesh.py +0 -0
  54. {icplot-0.1.7 → icplot-0.2.1}/test/test_tex.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: icplot
3
- Version: 0.1.7
3
+ Version: 0.2.1
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
@@ -19,7 +19,7 @@ Requires-Dist: matplotlib
19
19
  Requires-Dist: vtk
20
20
  Requires-Dist: moviepy
21
21
  Requires-Dist: scipy
22
- Requires-Dist: iccore==0.1.13
22
+ Requires-Dist: iccore==0.2.1
23
23
  Provides-Extra: test
24
24
  Requires-Dist: pytest; extra == "test"
25
25
  Requires-Dist: pytest-cov; extra == "test"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "icplot"
3
- version = "0.1.7"
3
+ version = "0.2.1"
4
4
  authors = [
5
5
  { name="James Grogan, Irish Centre for High End Computing", email="james.grogan@ichec.ie" },
6
6
  ]
@@ -21,7 +21,7 @@ dependencies = ["wand",
21
21
  "vtk",
22
22
  "moviepy",
23
23
  "scipy",
24
- "iccore==0.1.13"]
24
+ "iccore==0.2.1"]
25
25
 
26
26
  [project.urls]
27
27
  Repository = "https://git.ichec.ie/performance/toolshed/icplot"
@@ -106,7 +106,7 @@ disable = ["raw-checker-failed", "missing-module-docstring", "missing-function-d
106
106
  testpaths = ["test",]
107
107
  log_cli = 1
108
108
  log_cli_level = "debug"
109
- 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"
110
110
 
111
111
  [tool.tox]
112
112
  requires = ["tox"]
@@ -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"
@@ -3,10 +3,9 @@ import logging
3
3
 
4
4
  import numpy as np
5
5
 
6
- from iccore.data.units import DateRange
7
6
  from iccore.data import Series
8
7
 
9
- from .plot_group import PlotGroup
8
+ from .plot_group import PlotGroupPublic, PlotSeriesPublic
10
9
 
11
10
  _HAS_VTK = True
12
11
  try:
@@ -23,7 +22,7 @@ def has_vtk() -> bool:
23
22
  return _HAS_VTK
24
23
 
25
24
 
26
- def to_grid(series: Series, dates: DateRange | None):
25
+ def to_grid(series: PlotSeriesPublic, data_series: Series):
27
26
 
28
27
  if not has_vtk():
29
28
  raise RuntimeError("VTK support failed to load")
@@ -34,37 +33,46 @@ def to_grid(series: Series, dates: DateRange | None):
34
33
  if not series.y:
35
34
  raise ValueError("Expected 2d series for grid plot.")
36
35
 
36
+ x = data_series.get_x_array().data
37
+ y = data_series.get_y_array().data
38
+
37
39
  grid = vtk.vtkStructuredGrid()
38
- grid.SetDimensions([len(series.x.data), len(series.y.data), 1])
40
+ grid.SetDimensions([len(x), len(y), 1])
39
41
 
40
42
  points = vtk.vtkPoints()
41
43
  count = 0
42
44
 
43
- x0 = series.x.data[0]
44
- for y_pt in series.y.data:
45
- for x_pt in series.x.data:
45
+ x0 = x[0]
46
+ for y_pt in y:
47
+ for x_pt in x:
46
48
  delta_x = x_pt.timestamp() - x0.timestamp()
47
49
  points.InsertPoint(count, [delta_x, y_pt, 0])
48
50
  count += 1
49
51
  grid.SetPoints(points)
50
52
 
51
- for v in series.values:
53
+ for v in data_series.values:
52
54
  scalars = numpy_support.numpy_to_vtk(
53
55
  num_array=np.array(v.data[:]).flatten(order="F"),
54
56
  deep=True,
55
57
  array_type=vtk.VTK_FLOAT,
56
58
  )
57
- scalars.SetName(v.quantity.name)
59
+ scalars.SetName(series.measurement.name)
58
60
  grid.GetPointData().AddArray(scalars)
59
61
  return grid
60
62
 
61
63
 
62
- def save(path: Path, config: PlotGroup, series: Series, filename: str = "series"):
64
+ def save(
65
+ path: Path,
66
+ group: PlotGroupPublic,
67
+ series: PlotSeriesPublic,
68
+ data_series: Series,
69
+ filename: str = "series",
70
+ ):
63
71
 
64
72
  if not has_vtk():
65
73
  raise RuntimeError("VTK support failed to load")
66
74
 
67
- grid = to_grid(series, config.date_range)
75
+ grid = to_grid(series, data_series)
68
76
 
69
77
  writer = vtk.vtkStructuredGridWriter()
70
78
  writer.SetFileName(path / f"{filename}.vtk")
@@ -219,49 +219,53 @@ def from_cuboids(
219
219
 
220
220
  def from_cylinder(cylinder: Cylinder, boundary_frac: float = 0.66) -> Mesh:
221
221
 
222
- depth = (cylinder.diameter - boundary_frac * cylinder.diameter) / 2.0
222
+ inner_cube_side = cylinder.diameter * boundary_frac / np.sqrt(2)
223
+ outer_cube_x = cylinder.diameter / np.sqrt(2)
224
+ outer_cube_z = cylinder.diameter * (1.0 - boundary_frac) / (2.0 * np.sqrt(2))
225
+
223
226
  cuboids = [
224
227
  Cuboid(
225
- Point(0.0, 0.0, depth / 2.0),
226
- width=cylinder.diameter,
228
+ Point(0.0, 0.0, -inner_cube_side / 2.0 - outer_cube_z / 2.0),
229
+ width=outer_cube_x,
227
230
  height=cylinder.length,
228
- depth=depth,
231
+ depth=outer_cube_z,
229
232
  top_width_scale=boundary_frac,
230
233
  ),
231
234
  Cuboid(
232
- Point(0.0, 0.0, cylinder.diameter + depth / 2.0),
233
- width=cylinder.diameter,
235
+ Point(0.0, 0.0, inner_cube_side / 2.0 + outer_cube_z / 2.0),
236
+ width=outer_cube_x,
234
237
  height=cylinder.length,
235
- depth=depth,
238
+ depth=outer_cube_z,
236
239
  normal=Vector(0.0, 0.0, -1.0),
237
240
  top_width_scale=boundary_frac,
238
241
  ),
239
242
  Cuboid(
240
- Point(-cylinder.diameter / 2.0, 0.0, cylinder.diameter / 2.0 + depth / 2.0),
241
- width=cylinder.diameter,
243
+ Point(-outer_cube_x / 2.0, 0.0, 0.0),
244
+ width=outer_cube_x,
242
245
  height=cylinder.length,
243
- depth=depth,
246
+ depth=outer_cube_z,
244
247
  normal=Vector(1.0, 0.0, 0.0),
245
248
  top_width_scale=boundary_frac,
246
249
  ),
247
250
  Cuboid(
248
- Point(cylinder.diameter / 2.0, 0.0, cylinder.diameter / 2.0 + depth / 2.0),
249
- width=cylinder.diameter,
251
+ Point(outer_cube_x / 2.0, 0.0, 0.0),
252
+ width=outer_cube_x,
250
253
  height=cylinder.length,
251
- depth=depth,
254
+ depth=outer_cube_z,
252
255
  normal=Vector(-1.0, 0.0, 0.0),
253
256
  top_width_scale=boundary_frac,
254
257
  ),
255
258
  Cuboid(
256
- Point(0.0, 0.0, cylinder.diameter * boundary_frac / 2.0 + depth),
257
- width=cylinder.diameter * boundary_frac,
259
+ Point(0.0, 0.0, 0.0),
260
+ width=inner_cube_side,
258
261
  height=cylinder.length,
259
- depth=boundary_frac * cylinder.diameter,
262
+ depth=inner_cube_side,
260
263
  ),
261
264
  ]
262
265
 
263
266
  mesh = from_cuboids(cuboids)
264
267
 
268
+ """
265
269
  top_front_edge = mesh.get_closest_edge(
266
270
  Point(0.0, -cylinder.length / 2.0, cylinder.diameter + depth / 2.0)
267
271
  )
@@ -269,6 +273,8 @@ def from_cylinder(cylinder: Cylinder, boundary_frac: float = 0.66) -> Mesh:
269
273
  0.0, -cylinder.length / 2.0, cylinder.diameter + depth / 2.0 + 0.15
270
274
  )
271
275
 
272
- edges = (Edge(top_front_edge[0], top_front_edge[1], "arc", (top_front_arc,)),)
276
+ # edges = (Edge(top_front_edge[0], top_front_edge[1], "arc", (top_front_arc,)),)
277
+ """
278
+ edges = ()
273
279
 
274
280
  return Mesh(mesh.vertices, mesh.blocks, edges=edges)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: icplot
3
- Version: 0.1.7
3
+ Version: 0.2.1
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
@@ -19,7 +19,7 @@ Requires-Dist: matplotlib
19
19
  Requires-Dist: vtk
20
20
  Requires-Dist: moviepy
21
21
  Requires-Dist: scipy
22
- Requires-Dist: iccore==0.1.13
22
+ Requires-Dist: iccore==0.2.1
23
23
  Provides-Extra: test
24
24
  Requires-Dist: pytest; extra == "test"
25
25
  Requires-Dist: pytest-cov; extra == "test"
@@ -46,4 +46,5 @@ test/test_image_utils.py
46
46
  test/test_line_plots.py
47
47
  test/test_mermaid.py
48
48
  test/test_mesh.py
49
+ test/test_plot_group.py
49
50
  test/test_tex.py
@@ -3,7 +3,7 @@ matplotlib
3
3
  vtk
4
4
  moviepy
5
5
  scipy
6
- iccore==0.1.13
6
+ iccore==0.2.1
7
7
 
8
8
  [cairo]
9
9
  pycairo
@@ -0,0 +1,35 @@
1
+ from datetime import datetime, timedelta
2
+ from icplot.graph.plot_group import (
3
+ PlotGroupPublic,
4
+ VideoConfig,
5
+ ContourConfig,
6
+ LineConfig,
7
+ )
8
+
9
+
10
+ def test_plot_group():
11
+ now = datetime.utcnow()
12
+ later = now + timedelta(hours=1)
13
+
14
+ video = VideoConfig(fps=10, format="avi", active=False)
15
+ contour = ContourConfig(show_grid=False, colormap="viridis", format="svg")
16
+ line = LineConfig(format="pdf")
17
+
18
+ group = PlotGroupPublic(
19
+ start_datetime=now,
20
+ end_datetime=later,
21
+ video=video,
22
+ contour=contour,
23
+ line=line,
24
+ formats=("mpl", "svg"),
25
+ active=True,
26
+ series=[],
27
+ )
28
+
29
+ assert group.has_date_range
30
+ assert group.date_range == (now, later)
31
+ assert group.video == video
32
+ assert group.contour == contour
33
+ assert group.line == line
34
+ assert group.formats == ("mpl", "svg")
35
+ assert group.active
@@ -1,216 +0,0 @@
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.quantity import Quantity
13
- from iccore.data.units import DateRange, to_date_str
14
- from iccore.data import Series, Array
15
-
16
- from .plot_group import PlotGroup
17
- from .video import images_to_video
18
-
19
-
20
- def get_axis_label_from_quantity(quantity: Quantity) -> str:
21
-
22
- if quantity.name == "time":
23
- return f"{quantity.long_name} (UTC)"
24
-
25
- if quantity.unit.get_symbol():
26
- return f"{quantity.long_name} ({quantity.unit.get_symbol()})"
27
-
28
- return quantity.long_name
29
-
30
-
31
- def get_axis_label(array: Array) -> str:
32
- return get_axis_label_from_quantity(array.quantity)
33
-
34
-
35
- def get_grid(series: Series):
36
-
37
- if not series.x or not series.y:
38
- raise RuntimeError("Series missing expected data")
39
-
40
- return np.meshgrid(series.x.data, series.y.data)
41
-
42
-
43
- def save_plot(fig, path: Path, filename: str, file_prefix: str = ""):
44
- plt.tight_layout()
45
- prefix = f"{file_prefix}_" if file_prefix else ""
46
- os.makedirs(path, exist_ok=True)
47
- plt.savefig(path / f"{prefix}{filename}")
48
- plt.clf()
49
- plt.close(fig)
50
-
51
-
52
- def get_date_suffix(dates: DateRange | None) -> str:
53
- if not dates:
54
- return ""
55
- if not dates.start or not dates.end:
56
- return ""
57
-
58
- return f"_FROM_{to_date_str(dates.start)}_TO_{to_date_str(dates.end)}"
59
-
60
-
61
- def plot_2d(
62
- path: Path, config: PlotGroup, series: Series, quantity: str, file_prefix: str = ""
63
- ):
64
- """
65
- Generate a contour plot for the provided quantity
66
- """
67
-
68
- if not series.x:
69
- raise RuntimeError("Expected series with an x quantity")
70
-
71
- if not series.y:
72
- raise RuntimeError("Expected series with a y quantity.")
73
-
74
- x, y = get_grid(series)
75
-
76
- array = series.get_array(quantity)
77
-
78
- fig, ax = plt.subplots()
79
-
80
- ax.set_ylabel(get_axis_label(series.y))
81
- ax.set_xlabel(get_axis_label(series.x))
82
-
83
- if config.date_range:
84
- start, end = config.date_range.as_tuple()
85
- if start and end:
86
- ax.set_xlim((start, end)) # type: ignore
87
- fig.autofmt_xdate()
88
-
89
- if config.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=config.contour.colormap,
98
- )
99
- else:
100
- cs = ax.contourf(x, y, array.data.T, cmap=config.contour.colormap)
101
-
102
- cbar = fig.colorbar(cs)
103
- cbar.ax.set_ylabel(get_axis_label(array))
104
-
105
- save_plot(
106
- fig,
107
- path,
108
- f"{quantity}{get_date_suffix(config.date_range)}.{config.contour.format}",
109
- file_prefix,
110
- )
111
-
112
-
113
- def plot_1d(
114
- path: Path, config: PlotGroup, series: Series, quantity: str, file_prefix: str = ""
115
- ):
116
- """
117
- Generate a line plot for the provided quantity
118
- """
119
-
120
- if not series.x:
121
- raise RuntimeError("Attempted to plot series without x data")
122
-
123
- array = series.get_array(quantity)
124
-
125
- fig, ax = plt.subplots()
126
- ax.set_xlabel(get_axis_label(series.x))
127
- ax.set_ylabel(get_axis_label(array))
128
-
129
- if config.date_range:
130
- ax.set_xlim(config.date_range.as_tuple()) # type: ignore
131
- fig.autofmt_xdate()
132
-
133
- if array.quantity.has_limits:
134
- ax.set_ylim(array.quantity.limits) # type: ignore
135
-
136
- x = array.data.index.to_numpy(dtype="datetime64[ns]")
137
- y = array.data.values
138
- ax.plot(x, y)
139
-
140
- save_plot(
141
- fig,
142
- path,
143
- f"{quantity}{get_date_suffix(config.date_range)}.{config.line.format}",
144
- file_prefix,
145
- )
146
-
147
-
148
- def plot_array(path: Path, series: Series, array: Array, file_prefix):
149
- """
150
- Plot an array for a single series x value. Useful for
151
- time series video frames.
152
- """
153
-
154
- if not series.x or not series.y:
155
- raise RuntimeError("Attempted to plot series with missing data")
156
-
157
- for idx, (x, values) in enumerate(zip(series.x.data, array.data)):
158
- fig, ax = plt.subplots()
159
-
160
- ax.set_title(x)
161
- ax.set_xlabel(get_axis_label(array))
162
- ax.set_ylabel(get_axis_label(series.y))
163
-
164
- if array.quantity.has_limits:
165
- ax.set_xlim(array.quantity.limits) # type: ignore
166
-
167
- ax.plot(values, series.y.data)
168
-
169
- prefix = f"{file_prefix}_" if file_prefix else ""
170
- save_plot(fig, path / f"{prefix}{array.quantity.name}", f"{idx}.png")
171
-
172
-
173
- def make_video(
174
- path: Path, config: PlotGroup, series: Series, quantity: str, file_prefix: str = ""
175
- ):
176
- plot_array(path, series, series.get_array(quantity), file_prefix)
177
-
178
- prefix = f"{file_prefix}_" if file_prefix else ""
179
-
180
- if not config.video:
181
- return
182
-
183
- images_to_video(
184
- path / f"{prefix}{quantity}",
185
- path,
186
- f"{prefix}{quantity}{get_date_suffix(config.date_range)}",
187
- fps=config.video.fps,
188
- video_format=config.video.format,
189
- )
190
- shutil.rmtree(path / f"{prefix}{quantity}")
191
-
192
-
193
- def plot(
194
- path: Path,
195
- config: PlotGroup,
196
- series: Series,
197
- quantity: Quantity,
198
- file_prefix: str = "",
199
- ):
200
- """
201
- Handle plotting 1-d and 2-d time series and videos for the given quantity
202
- """
203
-
204
- if series.is_compound:
205
- for c in series.components:
206
- plot(path, config, c, quantity, c.name)
207
- return
208
-
209
- if series.y:
210
- plot_2d(path, config, series, quantity.name, file_prefix)
211
-
212
- if config.video and config.video.active:
213
- make_video(path, config, series, quantity.name, file_prefix)
214
- return
215
-
216
- plot_1d(path, config, series, quantity.name, file_prefix)
@@ -1,63 +0,0 @@
1
- """
2
- Module to handle generation of plots
3
- """
4
-
5
- from datetime import date
6
- from dataclasses import dataclass
7
-
8
- from pydantic import BaseModel
9
-
10
- from iccore.data.units import DateRange
11
- from iccore.data.quantity import Quantity
12
-
13
-
14
- @dataclass(frozen=True)
15
- class VideoConfig:
16
- """
17
- Representation of a video
18
- """
19
-
20
- fps: int = 5
21
- format: str = "mp4"
22
- active: bool = True
23
-
24
-
25
- @dataclass(frozen=True)
26
- class ContourConfig:
27
- """
28
- Representation of a contour plot
29
- """
30
-
31
- show_grid: bool = True
32
- colormap: str = "rainbow"
33
- format: str = "png"
34
-
35
-
36
- @dataclass(frozen=True)
37
- class LineConfig:
38
- """
39
- Representatino of a line plot
40
- """
41
-
42
- format: str = "png"
43
-
44
-
45
- class PlotGroup(BaseModel, frozen=True):
46
- """
47
- A group of plots to be generated, one per quantity.
48
- """
49
-
50
- quantities: list[Quantity] = []
51
- start_date: date | None = None
52
- end_date: date | None = None
53
- video: VideoConfig | None = None
54
- contour: ContourConfig = ContourConfig()
55
- line: LineConfig = LineConfig()
56
- formats: tuple[str, ...] = ("mpl", "vtk")
57
- active: bool = True
58
-
59
- @property
60
- def date_range(self) -> DateRange | None:
61
- if not self.start_date:
62
- return None
63
- return DateRange(start=self.start_date, end=self.end_date)
@@ -1,63 +0,0 @@
1
- """
2
- A data series in a plot
3
- """
4
-
5
- import typing
6
-
7
- from pydantic import BaseModel
8
-
9
- from icplot.color import Color
10
-
11
-
12
- class PlotSeries(BaseModel, frozen=True):
13
- """
14
- A data series in a plot, such as a single line in a line-plot
15
-
16
- :param position_right: allows the series to be plotted on the right y-axis.
17
- :type position_right: bool, optional
18
- """
19
-
20
- label: str
21
- color: Color = Color()
22
- series_type: str = ""
23
- highlight: bool = False
24
- position_right: bool = False
25
-
26
-
27
- class ImageSeries(PlotSeries, frozen=True):
28
- """
29
- A plot data series where the elements are images
30
- """
31
-
32
- data: typing.Any
33
- transform: typing.Any
34
- series_type: str = "image"
35
-
36
-
37
- class LinePlotSeries(PlotSeries, frozen=True):
38
- """
39
- A plot series for line plots
40
-
41
- :param drawstyle: Naming comes from matplotlib API, allows for various square plots,
42
- default is a normal point to point line plot.
43
- :type drawstyle: str, optional
44
- """
45
-
46
- x: list
47
- y: list
48
- x_err: float | list[float] | list[list[float]] | None = None
49
- y_err: float | list[float] | list[list[float]] | None = None
50
- err_capsize: float = 2.0
51
- marker: str = "o"
52
- series_type: str = "line"
53
- drawstyle: str = "default"
54
- linestyle: str = "-"
55
-
56
-
57
- class ScatterPlotSeries(PlotSeries, frozen=True):
58
- """
59
- Scatter type plot series
60
- """
61
-
62
- data: typing.Any
63
- series_type: str = "scatter"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes