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.
- {icplot-0.1.5/src/icplot.egg-info → icplot-0.1.6}/PKG-INFO +4 -2
- icplot-0.1.6/pyproject.toml +154 -0
- {icplot-0.1.5 → icplot-0.1.6}/src/icplot/cairo_interface.py +1 -1
- {icplot-0.1.5 → icplot-0.1.6}/src/icplot/gantt/gantt.py +7 -0
- {icplot-0.1.5 → icplot-0.1.6}/src/icplot/gantt/gantt_renderer.py +5 -4
- icplot-0.1.6/src/icplot/geometry.py +151 -0
- {icplot-0.1.5 → icplot-0.1.6}/src/icplot/graph/axis.py +4 -2
- {icplot-0.1.5 → icplot-0.1.6}/src/icplot/graph/matplotlib.py +3 -6
- icplot-0.1.6/src/icplot/graph/mpl.py +216 -0
- {icplot-0.1.5 → icplot-0.1.6}/src/icplot/graph/plot.py +37 -2
- icplot-0.1.6/src/icplot/graph/plot_group.py +63 -0
- {icplot-0.1.5 → icplot-0.1.6}/src/icplot/graph/series.py +7 -0
- icplot-0.1.6/src/icplot/graph/video.py +35 -0
- icplot-0.1.6/src/icplot/graph/vtk.py +54 -0
- icplot-0.1.6/src/icplot/mesh/__init__.py +1 -0
- icplot-0.1.6/src/icplot/mesh/foam.py +56 -0
- icplot-0.1.6/src/icplot/mesh/mesh.py +79 -0
- icplot-0.1.5/src/icplot/geometry.py → icplot-0.1.6/src/icplot/scene.py +25 -1
- {icplot-0.1.5 → icplot-0.1.6/src/icplot.egg-info}/PKG-INFO +4 -2
- {icplot-0.1.5 → icplot-0.1.6}/src/icplot.egg-info/SOURCES.txt +9 -0
- {icplot-0.1.5 → icplot-0.1.6}/src/icplot.egg-info/requires.txt +3 -1
- {icplot-0.1.5 → icplot-0.1.6}/test/test_cairo_interface.py +1 -1
- {icplot-0.1.5 → icplot-0.1.6}/test/test_line_plots.py +1 -1
- icplot-0.1.6/test/test_mesh.py +13 -0
- icplot-0.1.5/pyproject.toml +0 -108
- {icplot-0.1.5 → icplot-0.1.6}/LICENSE +0 -0
- {icplot-0.1.5 → icplot-0.1.6}/README.md +0 -0
- {icplot-0.1.5 → icplot-0.1.6}/setup.cfg +0 -0
- {icplot-0.1.5 → icplot-0.1.6}/src/icplot/__init__.py +0 -0
- {icplot-0.1.5 → icplot-0.1.6}/src/icplot/color.py +0 -0
- {icplot-0.1.5 → icplot-0.1.6}/src/icplot/convert.py +0 -0
- {icplot-0.1.5 → icplot-0.1.6}/src/icplot/gantt/__init__.py +0 -0
- {icplot-0.1.5 → icplot-0.1.6}/src/icplot/gantt/pgfgantt.py +0 -0
- {icplot-0.1.5 → icplot-0.1.6}/src/icplot/graph/__init__.py +0 -0
- {icplot-0.1.5 → icplot-0.1.6}/src/icplot/graph/generator.py +0 -0
- {icplot-0.1.5 → icplot-0.1.6}/src/icplot/image_utils.py +0 -0
- {icplot-0.1.5 → icplot-0.1.6}/src/icplot/main_cli.py +0 -0
- {icplot-0.1.5 → icplot-0.1.6}/src/icplot/mermaid.py +0 -0
- {icplot-0.1.5 → icplot-0.1.6}/src/icplot/py.typed +0 -0
- {icplot-0.1.5 → icplot-0.1.6}/src/icplot/tex.py +0 -0
- {icplot-0.1.5 → icplot-0.1.6}/src/icplot/trace/__init__.py +0 -0
- {icplot-0.1.5 → icplot-0.1.6}/src/icplot/trace/trace.py +0 -0
- {icplot-0.1.5 → icplot-0.1.6}/src/icplot.egg-info/dependency_links.txt +0 -0
- {icplot-0.1.5 → icplot-0.1.6}/src/icplot.egg-info/entry_points.txt +0 -0
- {icplot-0.1.5 → icplot-0.1.6}/src/icplot.egg-info/top_level.txt +0 -0
- {icplot-0.1.5 → icplot-0.1.6}/test/test_gantt_chart.py +0 -0
- {icplot-0.1.5 → icplot-0.1.6}/test/test_image_utils.py +0 -0
- {icplot-0.1.5 → icplot-0.1.6}/test/test_mermaid.py +0 -0
- {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.
|
|
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:
|
|
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"]]
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
80
|
+
if key in ("bottom", "ymin"):
|
|
84
81
|
axs[idx].set_ylim(ymin=item)
|
|
85
|
-
elif key
|
|
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)
|