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.
- {icplot-0.1.7/src/icplot.egg-info → icplot-0.2.1}/PKG-INFO +2 -2
- {icplot-0.1.7 → icplot-0.2.1}/pyproject.toml +3 -3
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/graph/__init__.py +2 -1
- icplot-0.2.1/src/icplot/graph/mpl.py +203 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/graph/plot.py +21 -16
- icplot-0.2.1/src/icplot/graph/plot_group.py +80 -0
- icplot-0.2.1/src/icplot/graph/series.py +129 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/graph/vtk.py +19 -11
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/mesh/mesh.py +23 -17
- {icplot-0.1.7 → icplot-0.2.1/src/icplot.egg-info}/PKG-INFO +2 -2
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot.egg-info/SOURCES.txt +1 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot.egg-info/requires.txt +1 -1
- icplot-0.2.1/test/test_plot_group.py +35 -0
- icplot-0.1.7/src/icplot/graph/mpl.py +0 -216
- icplot-0.1.7/src/icplot/graph/plot_group.py +0 -63
- icplot-0.1.7/src/icplot/graph/series.py +0 -63
- {icplot-0.1.7 → icplot-0.2.1}/LICENSE +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/README.md +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/setup.cfg +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/__init__.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/cairo_interface.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/color.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/convert.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/gantt/__init__.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/gantt/gantt.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/gantt/gantt_renderer.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/gantt/pgfgantt.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/geometry.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/graph/axis.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/graph/generator.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/graph/matplotlib.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/graph/video.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/image_utils.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/main_cli.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/mermaid.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/mesh/__init__.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/mesh/foam.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/mesh/vtk.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/py.typed +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/scene.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/tex.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/trace/__init__.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot/trace/trace.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot.egg-info/dependency_links.txt +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot.egg-info/entry_points.txt +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/src/icplot.egg-info/top_level.txt +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/test/test_cairo_interface.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/test/test_gantt_chart.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/test/test_geometry.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/test/test_image_utils.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/test/test_line_plots.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/test/test_mermaid.py +0 -0
- {icplot-0.1.7 → icplot-0.2.1}/test/test_mesh.py +0 -0
- {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
|
|
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
|
|
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
|
|
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
|
|
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=
|
|
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
|
|
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"
|
|
@@ -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
|
|
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:
|
|
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(
|
|
40
|
+
grid.SetDimensions([len(x), len(y), 1])
|
|
39
41
|
|
|
40
42
|
points = vtk.vtkPoints()
|
|
41
43
|
count = 0
|
|
42
44
|
|
|
43
|
-
x0 =
|
|
44
|
-
for y_pt in
|
|
45
|
-
for x_pt in
|
|
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
|
|
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(
|
|
59
|
+
scalars.SetName(series.measurement.name)
|
|
58
60
|
grid.GetPointData().AddArray(scalars)
|
|
59
61
|
return grid
|
|
60
62
|
|
|
61
63
|
|
|
62
|
-
def save(
|
|
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,
|
|
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
|
-
|
|
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,
|
|
226
|
-
width=
|
|
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=
|
|
231
|
+
depth=outer_cube_z,
|
|
229
232
|
top_width_scale=boundary_frac,
|
|
230
233
|
),
|
|
231
234
|
Cuboid(
|
|
232
|
-
Point(0.0, 0.0,
|
|
233
|
-
width=
|
|
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=
|
|
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(-
|
|
241
|
-
width=
|
|
243
|
+
Point(-outer_cube_x / 2.0, 0.0, 0.0),
|
|
244
|
+
width=outer_cube_x,
|
|
242
245
|
height=cylinder.length,
|
|
243
|
-
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(
|
|
249
|
-
width=
|
|
251
|
+
Point(outer_cube_x / 2.0, 0.0, 0.0),
|
|
252
|
+
width=outer_cube_x,
|
|
250
253
|
height=cylinder.length,
|
|
251
|
-
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,
|
|
257
|
-
width=
|
|
259
|
+
Point(0.0, 0.0, 0.0),
|
|
260
|
+
width=inner_cube_side,
|
|
258
261
|
height=cylinder.length,
|
|
259
|
-
depth=
|
|
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
|
|
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
|
|
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"
|
|
@@ -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
|
|
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
|