icplot 0.1.5__tar.gz → 0.1.6__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 (49) hide show
  1. {icplot-0.1.5/src/icplot.egg-info → icplot-0.1.6}/PKG-INFO +4 -2
  2. icplot-0.1.6/pyproject.toml +154 -0
  3. {icplot-0.1.5 → icplot-0.1.6}/src/icplot/cairo_interface.py +1 -1
  4. {icplot-0.1.5 → icplot-0.1.6}/src/icplot/gantt/gantt.py +7 -0
  5. {icplot-0.1.5 → icplot-0.1.6}/src/icplot/gantt/gantt_renderer.py +5 -4
  6. icplot-0.1.6/src/icplot/geometry.py +151 -0
  7. {icplot-0.1.5 → icplot-0.1.6}/src/icplot/graph/axis.py +4 -2
  8. {icplot-0.1.5 → icplot-0.1.6}/src/icplot/graph/matplotlib.py +3 -6
  9. icplot-0.1.6/src/icplot/graph/mpl.py +216 -0
  10. {icplot-0.1.5 → icplot-0.1.6}/src/icplot/graph/plot.py +37 -2
  11. icplot-0.1.6/src/icplot/graph/plot_group.py +63 -0
  12. {icplot-0.1.5 → icplot-0.1.6}/src/icplot/graph/series.py +7 -0
  13. icplot-0.1.6/src/icplot/graph/video.py +35 -0
  14. icplot-0.1.6/src/icplot/graph/vtk.py +54 -0
  15. icplot-0.1.6/src/icplot/mesh/__init__.py +1 -0
  16. icplot-0.1.6/src/icplot/mesh/foam.py +56 -0
  17. icplot-0.1.6/src/icplot/mesh/mesh.py +79 -0
  18. icplot-0.1.5/src/icplot/geometry.py → icplot-0.1.6/src/icplot/scene.py +25 -1
  19. {icplot-0.1.5 → icplot-0.1.6/src/icplot.egg-info}/PKG-INFO +4 -2
  20. {icplot-0.1.5 → icplot-0.1.6}/src/icplot.egg-info/SOURCES.txt +9 -0
  21. {icplot-0.1.5 → icplot-0.1.6}/src/icplot.egg-info/requires.txt +3 -1
  22. {icplot-0.1.5 → icplot-0.1.6}/test/test_cairo_interface.py +1 -1
  23. {icplot-0.1.5 → icplot-0.1.6}/test/test_line_plots.py +1 -1
  24. icplot-0.1.6/test/test_mesh.py +13 -0
  25. icplot-0.1.5/pyproject.toml +0 -108
  26. {icplot-0.1.5 → icplot-0.1.6}/LICENSE +0 -0
  27. {icplot-0.1.5 → icplot-0.1.6}/README.md +0 -0
  28. {icplot-0.1.5 → icplot-0.1.6}/setup.cfg +0 -0
  29. {icplot-0.1.5 → icplot-0.1.6}/src/icplot/__init__.py +0 -0
  30. {icplot-0.1.5 → icplot-0.1.6}/src/icplot/color.py +0 -0
  31. {icplot-0.1.5 → icplot-0.1.6}/src/icplot/convert.py +0 -0
  32. {icplot-0.1.5 → icplot-0.1.6}/src/icplot/gantt/__init__.py +0 -0
  33. {icplot-0.1.5 → icplot-0.1.6}/src/icplot/gantt/pgfgantt.py +0 -0
  34. {icplot-0.1.5 → icplot-0.1.6}/src/icplot/graph/__init__.py +0 -0
  35. {icplot-0.1.5 → icplot-0.1.6}/src/icplot/graph/generator.py +0 -0
  36. {icplot-0.1.5 → icplot-0.1.6}/src/icplot/image_utils.py +0 -0
  37. {icplot-0.1.5 → icplot-0.1.6}/src/icplot/main_cli.py +0 -0
  38. {icplot-0.1.5 → icplot-0.1.6}/src/icplot/mermaid.py +0 -0
  39. {icplot-0.1.5 → icplot-0.1.6}/src/icplot/py.typed +0 -0
  40. {icplot-0.1.5 → icplot-0.1.6}/src/icplot/tex.py +0 -0
  41. {icplot-0.1.5 → icplot-0.1.6}/src/icplot/trace/__init__.py +0 -0
  42. {icplot-0.1.5 → icplot-0.1.6}/src/icplot/trace/trace.py +0 -0
  43. {icplot-0.1.5 → icplot-0.1.6}/src/icplot.egg-info/dependency_links.txt +0 -0
  44. {icplot-0.1.5 → icplot-0.1.6}/src/icplot.egg-info/entry_points.txt +0 -0
  45. {icplot-0.1.5 → icplot-0.1.6}/src/icplot.egg-info/top_level.txt +0 -0
  46. {icplot-0.1.5 → icplot-0.1.6}/test/test_gantt_chart.py +0 -0
  47. {icplot-0.1.5 → icplot-0.1.6}/test/test_image_utils.py +0 -0
  48. {icplot-0.1.5 → icplot-0.1.6}/test/test_mermaid.py +0 -0
  49. {icplot-0.1.5 → icplot-0.1.6}/test/test_tex.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: icplot
3
- Version: 0.1.5
3
+ Version: 0.1.6
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
@@ -16,7 +16,9 @@ Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
17
  Requires-Dist: wand
18
18
  Requires-Dist: matplotlib
19
- Requires-Dist: iccore==0.1.9
19
+ Requires-Dist: vtk
20
+ Requires-Dist: moviepy
21
+ Requires-Dist: iccore==0.1.13
20
22
  Provides-Extra: test
21
23
  Requires-Dist: pytest; extra == "test"
22
24
  Requires-Dist: pytest-cov; extra == "test"
@@ -0,0 +1,154 @@
1
+ [project]
2
+ name = "icplot"
3
+ version = "0.1.6"
4
+ authors = [
5
+ { name="James Grogan, Irish Centre for High End Computing", email="james.grogan@ichec.ie" },
6
+ ]
7
+ description = "Utilities for generating plots and graphics for technical reports."
8
+ readme = "README.md"
9
+ requires-python = ">=3.10"
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Programming Language :: Python :: 3",
13
+ "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
14
+ "Operating System :: OS Independent",
15
+ "Topic :: System :: Distributed Computing"
16
+ ]
17
+ keywords = ["Publishing", "Technical Reports", "Graphics"]
18
+
19
+ dependencies = ["wand",
20
+ "matplotlib",
21
+ "vtk",
22
+ "moviepy",
23
+ "iccore==0.1.13"]
24
+
25
+ [project.urls]
26
+ Repository = "https://git.ichec.ie/performance/toolshed/icplot"
27
+ Homepage = "https://git.ichec.ie/performance/toolshed/icplot"
28
+
29
+ [project.optional-dependencies]
30
+ test = [
31
+ "pytest",
32
+ "pytest-cov",
33
+ "pytest-sugar",
34
+ "black",
35
+ "mypy",
36
+ "flake8",
37
+ "pylint"
38
+ ]
39
+
40
+ cairo = [
41
+ "pycairo",
42
+ "CairoSVG"
43
+ ]
44
+
45
+ [build-system]
46
+ requires = ["setuptools", "wheel"]
47
+ build-backend = "setuptools.build_meta"
48
+
49
+ [project.scripts]
50
+ icplot = "icplot.main_cli:main_cli"
51
+
52
+ [tool.setuptools.package-data]
53
+ "icplot" = ["py.typed"]
54
+
55
+ [tool.setuptools.packages.find]
56
+ where = ["src"]
57
+
58
+ [tool.mypy]
59
+ ignore_missing_imports = true
60
+
61
+ [tool.pylint.main]
62
+
63
+ source-roots = ["src"]
64
+ fail-under = 9.33
65
+
66
+ [tool.pylint.basic]
67
+
68
+ class-const-naming-style = "UPPER_CASE"
69
+ class-naming-style = "PascalCase"
70
+ const-naming-style = "UPPER_CASE"
71
+ docstring-min-length = -1
72
+
73
+ function-naming-style = "snake_case"
74
+ method-naming-style = "snake_case"
75
+ module-naming-style = "snake_case"
76
+ variable-naming-style = "snake_case"
77
+
78
+ no-docstring-rgx = "^_"
79
+
80
+ [tool.pylint.design]
81
+ # Maximum number of arguments for function / method.
82
+ max-args = 6
83
+ # Maximum number of attributes for a class (see R0902).
84
+ max-attributes = 7
85
+ # Minimum number of public methods for a class (see R0903).
86
+ min-public-methods = 1
87
+
88
+ [tool.pylint.logging]
89
+ # The type of string formatting that logging methods do. `old` means using %
90
+ # formatting, `new` is for `{}` formatting.
91
+ logging-format-style = "old"
92
+
93
+ # Disable the message, report, category or checker with the given id(s). You can
94
+ # either give multiple identifiers separated by comma (,) or put this option
95
+ # multiple times (only on the command line, not in the configuration file where
96
+ # it should appear only once). You can also use "--disable=all" to disable
97
+ # everything first and then re-enable specific checks. For example, if you want
98
+ # to run only the similarities checker, you can use "--disable=all
99
+ # --enable=similarities". If you want to run only the classes checker, but have
100
+ # no Warning level messages displayed, use "--disable=all --enable=classes
101
+ # --disable=W".
102
+ disable = ["raw-checker-failed", "missing-module-docstring", "missing-function-docstring", "bad-inline-option", "locally-disabled", "file-ignored", "suppressed-message", "useless-suppression", "deprecated-pragma", "use-symbolic-message-instead", "use-implicit-booleaness-not-comparison-to-string", "use-implicit-booleaness-not-comparison-to-zero"]
103
+
104
+ [tool.pytest.ini_options]
105
+ testpaths = ["test",]
106
+ log_cli = 1
107
+ log_cli_level = "debug"
108
+ addopts = "--cov=icplot --cov-report term --cov-fail-under=59 --cov-report xml:coverage.xml --cov-report html"
109
+
110
+ [tool.tox]
111
+ requires = ["tox"]
112
+ env_list = ["format_check", "lint", "style", "type", "3.10", "3.13", "docs"]
113
+ skip_missing_interpreters = true
114
+
115
+ [tool.tox.env_run_base]
116
+ description = "Run Unit and Integration Tests"
117
+ deps = [".[test, cairo]"]
118
+ commands = [["pytest", "test"]]
119
+
120
+ [tool.tox.env.format_apply]
121
+ description = "Run Format Apply"
122
+ skip_install = true
123
+ deps = ["black"]
124
+ commands = [["black", "src"], ["black", "test"]]
125
+
126
+ [tool.tox.env.format_check]
127
+ description = "Run Format Check"
128
+ skip_install = true
129
+ deps = ["black"]
130
+ commands = [
131
+ ["black", "--check", "--diff", "src"],
132
+ ["black", "--check", "--diff", "test"]]
133
+
134
+ [tool.tox.env.lint]
135
+ description = "Run Linter"
136
+ deps = ["pylint"]
137
+ commands = [["pylint", "src"]]
138
+
139
+ [tool.tox.env.style]
140
+ description = "Run Style Check"
141
+ skip_install = true
142
+ deps = ["flake8"]
143
+ commands = [["flake8", "src"]]
144
+
145
+ [tool.tox.env.type]
146
+ description = "Run Type Checks"
147
+ deps = ["mypy", ".[types]"]
148
+ commands = [["mypy", "src"]]
149
+
150
+ [tool.tox.env.docs]
151
+ description = "Generate Documentation"
152
+ changedir = "docs"
153
+ deps = ["sphinx"]
154
+ commands = [["sphinx-build","-W", "-b","html","-d","{envtmpdir}/doctrees","."," {envtmpdir}/html"]]
@@ -2,7 +2,7 @@ from pathlib import Path
2
2
 
3
3
  import cairo
4
4
 
5
- from .geometry import Scene, Rectangle
5
+ from .scene import Scene, Rectangle
6
6
 
7
7
 
8
8
  def draw_rect(cr, rect: Rectangle):
@@ -1,3 +1,7 @@
1
+ """
2
+ This module represents a gantt chard
3
+ """
4
+
1
5
  from datetime import datetime
2
6
 
3
7
  from pydantic import BaseModel
@@ -8,6 +12,9 @@ from icplot.color import Color
8
12
 
9
13
 
10
14
  class GanttChart(BaseModel, frozen=True):
15
+ """
16
+ A gantt chart
17
+ """
11
18
 
12
19
  milestones: list[Milestone] = []
13
20
  title: str = ""
@@ -1,5 +1,5 @@
1
1
  from iccore.project import Milestone
2
- from icplot.geometry import Scene, SceneItem, Rectangle
2
+ from icplot.scene import Scene, SceneItem, Rectangle
3
3
 
4
4
  from .gantt import GanttChart
5
5
 
@@ -25,10 +25,11 @@ def _render_milestone(
25
25
  return rect
26
26
 
27
27
 
28
- def _render_milestones(gantt: GanttChart):
28
+ def _render_milestones(gantt: GanttChart) -> list[SceneItem]:
29
29
 
30
+ scene_items: list = []
30
31
  if not gantt.milestones:
31
- return
32
+ return scene_items
32
33
 
33
34
  milestones = gantt.milestones
34
35
  # milestones.sort(key=lambda x: x.start_date, reverse=True)
@@ -44,7 +45,7 @@ def _render_milestones(gantt: GanttChart):
44
45
  chart_range = (start_date, end_date)
45
46
  yloc = 0.0
46
47
  bar_height = gantt.bar_max_height * gantt.height
47
- scene_items = []
48
+
48
49
  for milestone in milestones:
49
50
  item = _render_milestone(gantt, milestone, chart_range, yloc)
50
51
  scene_items.append(item)
@@ -0,0 +1,151 @@
1
+ """
2
+ Module for primitive geometries. This is used to keep dependencies simple,
3
+ consider a real geometry library like cgal for more complex or performance
4
+ dependent work.
5
+ """
6
+
7
+ from __future__ import annotations
8
+ import copy
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class Vector:
14
+ """
15
+ A 3d spatial vector
16
+ """
17
+
18
+ x: float
19
+ y: float = 0.0
20
+ z: float = 0.0
21
+
22
+ def scale(self, factor: float) -> Vector:
23
+ return Vector(self.x * factor, self.y * factor, self.z * factor)
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class Point:
28
+ """
29
+ A location in 3d space
30
+ """
31
+
32
+ x: float
33
+ y: float = 0.0
34
+ z: float = 0.0
35
+
36
+ def translate(self, v: Vector) -> Point:
37
+ return Point(self.x + v.x, self.y + v.y, self.z + v.z)
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class Shape:
42
+ """
43
+ A shape in 3d space, defaults to having a normal to Z
44
+ """
45
+
46
+ loc: Point
47
+ normal: Vector = Vector(0.0, 0.0, 1.0)
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class Quad(Shape):
52
+ """
53
+ A regular quadrilateral in 3d space, defaults to be normal to Z
54
+ """
55
+
56
+ width: float = 1.0
57
+ height: float = 1.0
58
+
59
+ @property
60
+ 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)),
67
+ ]
68
+
69
+ def translate(self, v: Vector) -> Quad:
70
+ return Quad(self.loc.translate(v), self.normal, self.width, self.height)
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class Cuboid(Shape):
75
+ """
76
+ A regular cuboid
77
+ """
78
+
79
+ loc: Point
80
+ width: float = 1.0
81
+ height: float = 1.0
82
+ depth: float = 1.0
83
+
84
+ def translate(self, v: Vector) -> Cuboid:
85
+ return Cuboid(
86
+ self.loc.translate(v), self.normal, self.width, self.height, self.depth
87
+ )
88
+
89
+ @property
90
+ def points(self) -> list[Point]:
91
+
92
+ base = Quad(self.loc, self.normal, self.width, self.height)
93
+ top = base.translate(self.normal.scale(self.depth))
94
+ return base.points + top.points
95
+
96
+
97
+ @dataclass(frozen=True)
98
+ class Cylinder(Shape):
99
+ """
100
+ A cylinder
101
+ """
102
+
103
+ diameter: float = 1.0
104
+ length: float = 1.0
105
+
106
+ @property
107
+ def start(self) -> Point:
108
+ return self.loc
109
+
110
+ @property
111
+ def end(self) -> Point:
112
+ return self.loc.translate(self.normal.scale(self.length))
113
+
114
+
115
+ @dataclass(frozen=True)
116
+ class Revolution(Shape):
117
+ """
118
+ A revolved profile sitting on the plane given by the loc
119
+ and normal and revolved about the normal.
120
+ """
121
+
122
+ diameter: float = 1.0
123
+ length: float = 1.0
124
+ profile: str = "arc"
125
+
126
+
127
+ @dataclass(frozen=True)
128
+ class CuboidGrid:
129
+ """
130
+ A irregular grid composed of cuboids. Can be useful for
131
+ generating topological meshes like OpenFoam's blockMesh.
132
+ """
133
+
134
+ x_locs: list[float]
135
+ y_locs: list[float]
136
+ z_locs: list[float]
137
+
138
+ @property
139
+ def cuboids(self) -> list[Cuboid]:
140
+ ret = []
141
+
142
+ for kdx, z in enumerate(self.z_locs[:-1]):
143
+ for jdx, y in enumerate(self.y_locs[:-1]):
144
+ for idx, x in enumerate(self.x_locs[:-1]):
145
+ width = self.x_locs[idx + 1] - x
146
+ height = self.y_locs[jdx + 1] - y
147
+ depth = self.z_locs[kdx + 1] - z
148
+ ret.append(
149
+ Cuboid(Point(x, y, z), width=width, height=height, depth=depth)
150
+ )
151
+ return ret
@@ -28,7 +28,7 @@ class PlotAxis(BaseModel, frozen=True):
28
28
  """
29
29
 
30
30
  label: str = ""
31
- ticks: Range | None = None
31
+ ticks: Range | list[int] | None = None
32
32
  scale: str = "linear"
33
33
  text_fontsize: int = 10
34
34
  tick_fontsize: int = 10
@@ -38,5 +38,7 @@ class PlotAxis(BaseModel, frozen=True):
38
38
  @property
39
39
  def resolved_ticks(self) -> list:
40
40
  if self.ticks:
41
- return self.ticks.eval()
41
+ if isinstance(self.ticks, Range):
42
+ return self.ticks.eval()
43
+ return self.ticks
42
44
  return []
@@ -31,10 +31,7 @@ def str_exact_values_formatter(x, pos):
31
31
  then when using plain formatter values less than 1 come out as 0. A way around
32
32
  this is this custom formatter.
33
33
  """
34
- if x.is_integer():
35
- return str(int(x))
36
- else:
37
- return str(x)
34
+ return str(int(x)) if x.is_integer else str(x)
38
35
 
39
36
 
40
37
  def _set_legend_decorations(axs, plot):
@@ -80,9 +77,9 @@ def _set_y_axes_decorations(y_axes: list[PlotAxis], axs):
80
77
  if y_axis.max_ticks:
81
78
  axs[idx].yaxis.set_major_locator(plt.MaxNLocator(y_axis.max_ticks))
82
79
  for key, item in y_axis.limits.items():
83
- if key == "bottom" or key == "ymin":
80
+ if key in ("bottom", "ymin"):
84
81
  axs[idx].set_ylim(ymin=item)
85
- elif key == "top" or key == "ymax":
82
+ elif key in ("top", "ymax"):
86
83
  axs[idx].set_ylim(ymax=item)
87
84
 
88
85
 
@@ -0,0 +1,216 @@
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)