icplot 0.0.8__tar.gz → 0.0.10__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.0.8/src/icplot.egg-info → icplot-0.0.10}/PKG-INFO +2 -2
- {icplot-0.0.8 → icplot-0.0.10}/pyproject.toml +2 -2
- {icplot-0.0.8 → icplot-0.0.10}/src/icplot/cairo_interface.py +4 -24
- icplot-0.0.10/src/icplot/color.py +56 -0
- icplot-0.0.10/src/icplot/gantt/__init__.py +1 -0
- icplot-0.0.10/src/icplot/gantt/gantt.py +19 -0
- icplot-0.0.10/src/icplot/gantt/gantt_renderer.py +59 -0
- icplot-0.0.10/src/icplot/gantt/pgfgantt_interface.py +26 -0
- icplot-0.0.10/src/icplot/graph/__init__.py +2 -0
- icplot-0.0.10/src/icplot/graph/axis.py +44 -0
- icplot-0.0.10/src/icplot/graph/matplotlib.py +101 -0
- icplot-0.0.10/src/icplot/graph/plot.py +92 -0
- icplot-0.0.10/src/icplot/graph/plotter.py +22 -0
- icplot-0.0.10/src/icplot/graph/series.py +111 -0
- {icplot-0.0.8 → icplot-0.0.10}/src/icplot/image_utils.py +19 -4
- icplot-0.0.10/src/icplot/py.typed +0 -0
- {icplot-0.0.8 → icplot-0.0.10/src/icplot.egg-info}/PKG-INFO +2 -2
- {icplot-0.0.8 → icplot-0.0.10}/src/icplot.egg-info/SOURCES.txt +8 -3
- {icplot-0.0.8 → icplot-0.0.10}/src/icplot.egg-info/requires.txt +1 -1
- icplot-0.0.10/test/test_cairo_interface.py +29 -0
- {icplot-0.0.8 → icplot-0.0.10}/test/test_gantt_chart.py +0 -5
- {icplot-0.0.8 → icplot-0.0.10}/test/test_line_plots.py +8 -2
- icplot-0.0.8/src/icplot/color.py +0 -13
- icplot-0.0.8/src/icplot/gantt.py +0 -64
- icplot-0.0.8/src/icplot/graph/__init__.py +0 -1
- icplot-0.0.8/src/icplot/graph/matplotlib_plot_backend.py +0 -69
- icplot-0.0.8/src/icplot/graph/plot.py +0 -163
- icplot-0.0.8/src/icplot/graph/series.py +0 -54
- icplot-0.0.8/src/icplot/project_elements.py +0 -4
- icplot-0.0.8/test/test_cairo_interface.py +0 -6
- {icplot-0.0.8 → icplot-0.0.10}/LICENSE +0 -0
- {icplot-0.0.8 → icplot-0.0.10}/README.md +0 -0
- {icplot-0.0.8 → icplot-0.0.10}/setup.cfg +0 -0
- {icplot-0.0.8 → icplot-0.0.10}/src/icplot/__init__.py +0 -0
- {icplot-0.0.8 → icplot-0.0.10}/src/icplot/geometry.py +0 -0
- {icplot-0.0.8 → icplot-0.0.10}/src/icplot/main_cli.py +0 -0
- {icplot-0.0.8 → icplot-0.0.10}/src/icplot/tex_interface.py +0 -0
- {icplot-0.0.8 → icplot-0.0.10}/src/icplot.egg-info/dependency_links.txt +0 -0
- {icplot-0.0.8 → icplot-0.0.10}/src/icplot.egg-info/entry_points.txt +0 -0
- {icplot-0.0.8 → icplot-0.0.10}/src/icplot.egg-info/top_level.txt +0 -0
- {icplot-0.0.8 → icplot-0.0.10}/test/test_image_utils.py +0 -0
- {icplot-0.0.8 → icplot-0.0.10}/test/test_tex_interface.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: icplot
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.10
|
|
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,7 @@ 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.0
|
|
19
|
+
Requires-Dist: iccore==0.1.0
|
|
20
20
|
Provides-Extra: test
|
|
21
21
|
Requires-Dist: pytest; extra == "test"
|
|
22
22
|
Requires-Dist: pytest-cov; extra == "test"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "icplot"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.10"
|
|
4
4
|
authors = [
|
|
5
5
|
{ name="James Grogan, Irish Centre for High End Computing", email="james.grogan@ichec.ie" },
|
|
6
6
|
]
|
|
@@ -18,7 +18,7 @@ keywords = ["Publishing", "Technical Reports", "Graphics"]
|
|
|
18
18
|
|
|
19
19
|
dependencies = ["wand",
|
|
20
20
|
"matplotlib",
|
|
21
|
-
"iccore==0.0
|
|
21
|
+
"iccore==0.1.0"]
|
|
22
22
|
|
|
23
23
|
[project.urls]
|
|
24
24
|
Repository = "https://git.ichec.ie/performance/toolshed/icplot"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import os
|
|
2
1
|
from pathlib import Path
|
|
2
|
+
|
|
3
3
|
import cairo
|
|
4
4
|
|
|
5
|
-
from .geometry import Scene, Rectangle
|
|
5
|
+
from .geometry import Scene, Rectangle
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class CairoInterface:
|
|
@@ -52,29 +52,9 @@ class CairoInterface:
|
|
|
52
52
|
if item.item_type == "shape":
|
|
53
53
|
self.draw_shape(cr, item)
|
|
54
54
|
elif item.item_type == "text":
|
|
55
|
-
self.draw_text(cr,
|
|
55
|
+
self.draw_text(cr, item)
|
|
56
56
|
|
|
57
|
-
def draw_svg(self, scene, path):
|
|
57
|
+
def draw_svg(self, scene: Scene, path: Path):
|
|
58
58
|
with cairo.SVGSurface(path, scene.size[0], scene.size[1]) as surface:
|
|
59
59
|
cr = cairo.Context(surface)
|
|
60
60
|
self.draw_scene(cr, scene)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if __name__ == "__main__":
|
|
64
|
-
|
|
65
|
-
cairo_interface = CairoInterface()
|
|
66
|
-
|
|
67
|
-
scene = Scene()
|
|
68
|
-
|
|
69
|
-
rect = Rectangle(20, 20)
|
|
70
|
-
rect.location = (10, 10)
|
|
71
|
-
rect.fill = Color(0.5, 0.5, 1, 0.5)
|
|
72
|
-
rect.stroke = Color(0.5, 0.0, 0.0, 0.5)
|
|
73
|
-
scene.items.append(rect)
|
|
74
|
-
|
|
75
|
-
text = TextPath("Hello World")
|
|
76
|
-
text.location = (5, 5)
|
|
77
|
-
scene.items.append(text)
|
|
78
|
-
|
|
79
|
-
output_path = Path(os.getcwd()) / "output.svg"
|
|
80
|
-
cairo_interface.draw_svg(scene, output_path)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module handles colors
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Callable
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Color(BaseModel):
|
|
11
|
+
"""
|
|
12
|
+
A color class, internal storage is rgba as float.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
r: float = 0.0
|
|
16
|
+
g: float = 0.0
|
|
17
|
+
b: float = 0.0
|
|
18
|
+
a: float = 1.0
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def from_rgba(r: float = 0.0, b: float = 0.0, g: float = 0.0, a: float = 1.0):
|
|
22
|
+
return Color(**{"r": r, "g": g, "b": b, "a": a})
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def from_list(data: list):
|
|
26
|
+
return Color(**{"r": data[0], "g": data[1], "b": data[2]})
|
|
27
|
+
|
|
28
|
+
def as_list(self) -> list:
|
|
29
|
+
return [self.r, self.g, self.b]
|
|
30
|
+
|
|
31
|
+
def is_black(self) -> bool:
|
|
32
|
+
return self.r == 0.0 and self.g == 0.0 and self.b == 0.0 and self.a == 1.0
|
|
33
|
+
|
|
34
|
+
def serialize(self) -> str:
|
|
35
|
+
return f"r: {self.r}, g: {self.g}, b: {self.b}, a: {self.a}"
|
|
36
|
+
|
|
37
|
+
class ColorMap:
|
|
38
|
+
"""
|
|
39
|
+
A mapping from a flat index to a color
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, label: str, data_func: Callable):
|
|
43
|
+
self.label = label
|
|
44
|
+
self.data_func = data_func
|
|
45
|
+
self.start_offset = 0.25
|
|
46
|
+
self.scale = 2
|
|
47
|
+
|
|
48
|
+
def get_color(self, cursor: int, values: list) -> Color:
|
|
49
|
+
"""
|
|
50
|
+
Returns a colour based on a cmap and how far across
|
|
51
|
+
the datasets you are
|
|
52
|
+
"""
|
|
53
|
+
position = cursor / len(values)
|
|
54
|
+
return Color.from_list(
|
|
55
|
+
self.data_func(self.start_offset + (position / self.scale))
|
|
56
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .gantt import * # NOQA
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from iccore.project import Milestone
|
|
6
|
+
|
|
7
|
+
from icplot.color import Color
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GanttChart(BaseModel):
|
|
11
|
+
|
|
12
|
+
milestones: list[Milestone] = []
|
|
13
|
+
title: str = ""
|
|
14
|
+
start_date: datetime | None = None
|
|
15
|
+
end_date: datetime | None = None
|
|
16
|
+
bar_max_height: float = 0.1
|
|
17
|
+
height: int = 100
|
|
18
|
+
width: int = 500
|
|
19
|
+
bar_color: Color = Color.from_rgba(0.7, 0, 0)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from iccore.project import Milestone
|
|
2
|
+
from icplot.geometry import Scene, Rectangle
|
|
3
|
+
|
|
4
|
+
from .gantt import GanttChart
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class GanttRenderer:
|
|
8
|
+
|
|
9
|
+
def _add_milestone(
|
|
10
|
+
self, gantt: GanttChart, scene: Scene, milestone: Milestone, chart_range, yloc
|
|
11
|
+
):
|
|
12
|
+
# chart_delta = chart_range[1] - chart_range[0]
|
|
13
|
+
chart_delta = 0
|
|
14
|
+
start_delta = milestone.start_date - chart_range[0]
|
|
15
|
+
# milestone_delta = milestone.due_date - milestone.start_date
|
|
16
|
+
milestone_delta = 0
|
|
17
|
+
|
|
18
|
+
start_frac = float(start_delta / chart_delta)
|
|
19
|
+
milestone_frac = float(milestone_delta / chart_delta)
|
|
20
|
+
|
|
21
|
+
w = milestone_frac * gantt.width
|
|
22
|
+
h = gantt.bar_max_height * gantt.height
|
|
23
|
+
x = start_frac * gantt.width
|
|
24
|
+
y = yloc
|
|
25
|
+
|
|
26
|
+
rect = Rectangle(w, h)
|
|
27
|
+
rect.location = (x, y)
|
|
28
|
+
rect.fill = gantt.bar_color
|
|
29
|
+
scene.items.append(rect)
|
|
30
|
+
|
|
31
|
+
def _add_milestones(self, gantt: GanttChart, scene: Scene):
|
|
32
|
+
|
|
33
|
+
if not gantt.milestones:
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
milestones = gantt.milestones
|
|
37
|
+
# milestones.sort(key=lambda x: x.start_date, reverse=True)
|
|
38
|
+
|
|
39
|
+
start_date = gantt.start_date
|
|
40
|
+
end_date = gantt.end_date
|
|
41
|
+
if not start_date:
|
|
42
|
+
start_date = milestones[0].start_date
|
|
43
|
+
|
|
44
|
+
# if not end_date:
|
|
45
|
+
# end_date = max(m.due_date for m in milestones)
|
|
46
|
+
|
|
47
|
+
chart_range = (start_date, end_date)
|
|
48
|
+
yloc = 0.0
|
|
49
|
+
bar_height = gantt.bar_max_height * gantt.height
|
|
50
|
+
for milestone in milestones:
|
|
51
|
+
self._add_milestone(gantt, scene, milestone, chart_range, yloc)
|
|
52
|
+
yloc += bar_height
|
|
53
|
+
|
|
54
|
+
def render(self, gantt: GanttChart) -> Scene:
|
|
55
|
+
scene = Scene()
|
|
56
|
+
|
|
57
|
+
self._add_milestones(gantt, scene)
|
|
58
|
+
|
|
59
|
+
return scene
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from .gantt import GanttChart
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PgfGanttRenderer:
|
|
7
|
+
|
|
8
|
+
def __init__(self):
|
|
9
|
+
self.template: str = ""
|
|
10
|
+
|
|
11
|
+
def _load_template(self):
|
|
12
|
+
if self.template:
|
|
13
|
+
return
|
|
14
|
+
|
|
15
|
+
with open(Path(__file__).parent / "pgfgantt.tex", "r", encoding="utf-8") as f:
|
|
16
|
+
self.template = f.read()
|
|
17
|
+
|
|
18
|
+
def render(self, gantt: GanttChart) -> str:
|
|
19
|
+
|
|
20
|
+
output = self.template
|
|
21
|
+
|
|
22
|
+
if gantt.title:
|
|
23
|
+
title_str = f"\\title{{{gantt.title}}}"
|
|
24
|
+
output.replace("%%TITLE%%", title_str)
|
|
25
|
+
|
|
26
|
+
return output
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module has a plot's axis
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from iccore.serialization import Serializable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Range(Serializable):
|
|
9
|
+
"""
|
|
10
|
+
This is a sequence of numbers specified by lower and upper
|
|
11
|
+
bounds and a step size. Eg (1, 5, 1) gives: [1, 2, 3, 4]
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, lower, upper, step):
|
|
15
|
+
self.lower = lower
|
|
16
|
+
self.upper = upper
|
|
17
|
+
self.step = step
|
|
18
|
+
|
|
19
|
+
def eval(self) -> list:
|
|
20
|
+
return list(range(self.lower, self.upper, self.step))
|
|
21
|
+
|
|
22
|
+
def serialize(self):
|
|
23
|
+
return {"lower": self.lower, "upper": self.upper, "step": self.step}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PlotAxis(Serializable):
|
|
27
|
+
"""
|
|
28
|
+
This is a plot's axis
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, label: str = ""):
|
|
32
|
+
self.label = label
|
|
33
|
+
self.ticks: Range | None = None
|
|
34
|
+
|
|
35
|
+
def set_ticks(self, lower: int, upper: int, step: int):
|
|
36
|
+
self.ticks = Range(lower, upper + 1, step)
|
|
37
|
+
|
|
38
|
+
def serialize(self):
|
|
39
|
+
return {"label": self.label, "ticks": self.ticks.serialize()}
|
|
40
|
+
|
|
41
|
+
def get_resolved_ticks(self) -> list:
|
|
42
|
+
if self.ticks:
|
|
43
|
+
return self.ticks.eval()
|
|
44
|
+
return []
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import logging
|
|
3
|
+
from typing import cast
|
|
4
|
+
|
|
5
|
+
logging.getLogger("matplotlib").setLevel(logging.WARNING)
|
|
6
|
+
import matplotlib as mpl # NOQA
|
|
7
|
+
|
|
8
|
+
default_backend = mpl.get_backend()
|
|
9
|
+
mpl.use("Agg")
|
|
10
|
+
import matplotlib.pyplot as plt # NOQA
|
|
11
|
+
|
|
12
|
+
from icplot.color import ColorMap # NOQA
|
|
13
|
+
from .series import PlotSeries, LinePlotSeries, ScatterPlotSeries, ImageSeries # NOQA
|
|
14
|
+
from .plot import Plot, GridPlot # NOQA
|
|
15
|
+
from .plotter import Plotter # NOQA
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MatplotlibColorMap(ColorMap):
|
|
19
|
+
"""
|
|
20
|
+
A matplotlib based colormap
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, label: str):
|
|
24
|
+
super().__init__(label, mpl.colormaps[label])
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MatplotlibPlotter(Plotter):
|
|
28
|
+
"""
|
|
29
|
+
A plot backend for matplotlib
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, cmap_label="viridis"):
|
|
33
|
+
super().__init__(MatplotlibColorMap(cmap_label))
|
|
34
|
+
|
|
35
|
+
def set_decorations(self, ax, plot: Plot):
|
|
36
|
+
ax.legend(loc="upper left")
|
|
37
|
+
if plot.x_axis.label:
|
|
38
|
+
ax.set_xlabel(plot.x_axis.label)
|
|
39
|
+
if plot.y_axis.label:
|
|
40
|
+
ax.set_ylabel(plot.y_axis.label)
|
|
41
|
+
if plot.x_axis.ticks:
|
|
42
|
+
ax.set_xticks(plot.x_axis.get_resolved_ticks())
|
|
43
|
+
if plot.y_axis.ticks:
|
|
44
|
+
ax.set_yticks(plot.y_axis.get_resolved_ticks())
|
|
45
|
+
if plot.title:
|
|
46
|
+
ax.set_title(plot.title)
|
|
47
|
+
|
|
48
|
+
def plot_line(self, ax, series: LinePlotSeries):
|
|
49
|
+
ax.plot(
|
|
50
|
+
series.x,
|
|
51
|
+
series.y,
|
|
52
|
+
label=series.label,
|
|
53
|
+
color=series.get_color().as_list(),
|
|
54
|
+
marker=series.marker,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def plot_scatter(self, ax, series: ScatterPlotSeries):
|
|
58
|
+
ax.scatter(series.data, label=series.label, color=series.get_color().as_list())
|
|
59
|
+
|
|
60
|
+
def plot_image(self, ax, series: ImageSeries):
|
|
61
|
+
ax.imshow(series.data)
|
|
62
|
+
ax.axis("off")
|
|
63
|
+
|
|
64
|
+
def plot_series(self, ax, series: PlotSeries):
|
|
65
|
+
if series.series_type == "line":
|
|
66
|
+
self.plot_line(ax, cast(LinePlotSeries, series))
|
|
67
|
+
elif series.series_type == "scatter":
|
|
68
|
+
self.plot_scatter(ax, cast(ScatterPlotSeries, series))
|
|
69
|
+
elif series.series_type == "image":
|
|
70
|
+
self.plot_image(ax, cast(ImageSeries, series))
|
|
71
|
+
|
|
72
|
+
def plot(self, plot: Plot, path: Path | None = None):
|
|
73
|
+
|
|
74
|
+
self.apply_cmap_colors(plot)
|
|
75
|
+
|
|
76
|
+
fig, ax = plt.subplots()
|
|
77
|
+
|
|
78
|
+
for series in plot.series:
|
|
79
|
+
self.plot_series(ax, series)
|
|
80
|
+
|
|
81
|
+
self.set_decorations(ax, plot)
|
|
82
|
+
|
|
83
|
+
self.render(fig, path)
|
|
84
|
+
|
|
85
|
+
def plot_grid(self, plot: GridPlot, path: Path | None = None, num_samples: int = 0):
|
|
86
|
+
|
|
87
|
+
rows, cols, series = plot.get_subplots(num_samples)
|
|
88
|
+
fig, axs = plt.subplots(rows, cols)
|
|
89
|
+
|
|
90
|
+
for ax, series_item in zip(axs, series):
|
|
91
|
+
self.plot_series(ax, series_item)
|
|
92
|
+
|
|
93
|
+
self.render(fig, path)
|
|
94
|
+
|
|
95
|
+
def render(self, fig, path: Path | None = None):
|
|
96
|
+
if path:
|
|
97
|
+
fig.savefig(path)
|
|
98
|
+
else:
|
|
99
|
+
plt.switch_backend(default_backend)
|
|
100
|
+
fig.show()
|
|
101
|
+
plt.switch_backend("Agg")
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module has content for generating plots
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import random
|
|
6
|
+
|
|
7
|
+
from iccore.serialization import Serializable
|
|
8
|
+
|
|
9
|
+
from .axis import PlotAxis
|
|
10
|
+
from .series import PlotSeries
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Plot(Serializable):
|
|
14
|
+
"""
|
|
15
|
+
A generic plot with optional axis ticks
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
title: str = "",
|
|
21
|
+
x_label: str = "",
|
|
22
|
+
y_label: str = "",
|
|
23
|
+
legend_label: str = "",
|
|
24
|
+
) -> None:
|
|
25
|
+
self.series: list[PlotSeries] = []
|
|
26
|
+
self.title = title
|
|
27
|
+
self.size: tuple | None = None
|
|
28
|
+
self.plot_type = ""
|
|
29
|
+
self.x_axis = PlotAxis(x_label)
|
|
30
|
+
self.y_axis = PlotAxis(y_label)
|
|
31
|
+
self.legend_label = legend_label
|
|
32
|
+
|
|
33
|
+
def set_x_ticks(self, lower, upper, step):
|
|
34
|
+
self.x_axis.set_ticks(lower, upper, step)
|
|
35
|
+
|
|
36
|
+
def set_y_ticks(self, lower, upper, step):
|
|
37
|
+
self.y_axis.set_ticks(lower, upper, step)
|
|
38
|
+
|
|
39
|
+
def add_series(self, series):
|
|
40
|
+
self.series.append(series)
|
|
41
|
+
|
|
42
|
+
def serialize(self):
|
|
43
|
+
return {
|
|
44
|
+
"series": [s.serialize() for s in self.series],
|
|
45
|
+
"title": self.title,
|
|
46
|
+
"type": self.plot_type,
|
|
47
|
+
"x_axis": self.x_axis.serialize(),
|
|
48
|
+
"y_axis": self.y_axis.serialize(),
|
|
49
|
+
"legend_label": self.legend_label,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class GridPlot(Plot):
|
|
54
|
+
"""
|
|
55
|
+
Make a grid of plots
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self, data, title: str = "", stride: int = 4, size: tuple = (25, 20)
|
|
60
|
+
) -> None:
|
|
61
|
+
super().__init__(title)
|
|
62
|
+
self.data = data
|
|
63
|
+
self.stride = stride
|
|
64
|
+
self.size = size
|
|
65
|
+
|
|
66
|
+
def get_series_indices(self, num_samples: int = 0):
|
|
67
|
+
rows = num_samples // self.stride
|
|
68
|
+
cols = num_samples // rows
|
|
69
|
+
len_data = len(self.data)
|
|
70
|
+
|
|
71
|
+
if num_samples == 0:
|
|
72
|
+
indices = list(range(0, len_data))
|
|
73
|
+
else:
|
|
74
|
+
indices = [random.randint(0, len_data - 1) for _ in range(num_samples)]
|
|
75
|
+
return rows, cols, indices
|
|
76
|
+
|
|
77
|
+
def get_subplots(self, num_samples: int = 0):
|
|
78
|
+
rows, cols, indices = self.get_series_indices(num_samples)
|
|
79
|
+
|
|
80
|
+
subplots = []
|
|
81
|
+
count = 1
|
|
82
|
+
for index in indices:
|
|
83
|
+
if num_samples > 0 and count == num_samples + 1:
|
|
84
|
+
break
|
|
85
|
+
if isinstance(self.data[index], list):
|
|
86
|
+
for series in self.data[index]:
|
|
87
|
+
subplots.append(series)
|
|
88
|
+
count += 1
|
|
89
|
+
else:
|
|
90
|
+
subplots.append(self.data[index])
|
|
91
|
+
count += 1
|
|
92
|
+
return rows, cols, subplots
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from icplot.color import ColorMap
|
|
4
|
+
|
|
5
|
+
from .plot import Plot
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Plotter:
|
|
9
|
+
|
|
10
|
+
def __init__(self, cmap: ColorMap):
|
|
11
|
+
self.cmap = cmap
|
|
12
|
+
|
|
13
|
+
def apply_cmap_colors(self, plot: Plot):
|
|
14
|
+
skipped_color = 0
|
|
15
|
+
for idx, series in enumerate(plot.series):
|
|
16
|
+
if not series.color:
|
|
17
|
+
series.color = self.cmap.get_color(idx - skipped_color, plot.series)
|
|
18
|
+
else:
|
|
19
|
+
skipped_color += 1
|
|
20
|
+
|
|
21
|
+
def plot(self, plot: Plot, path: Path | None = None):
|
|
22
|
+
pass
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from iccore.serialization import Serializable
|
|
2
|
+
|
|
3
|
+
from icplot.color import Color
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PlotSeries(Serializable):
|
|
7
|
+
"""
|
|
8
|
+
A data series in a plot, such as a single line in a line-plot
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, label: str = "", color: Color | None = None) -> None:
|
|
12
|
+
self.label = label
|
|
13
|
+
self.color = color
|
|
14
|
+
self.series_type = ""
|
|
15
|
+
|
|
16
|
+
def get_color(self) -> Color:
|
|
17
|
+
if self.color:
|
|
18
|
+
return self.color
|
|
19
|
+
return Color()
|
|
20
|
+
|
|
21
|
+
def serialize(self):
|
|
22
|
+
|
|
23
|
+
color_val = {}
|
|
24
|
+
if self.color:
|
|
25
|
+
color_val = self.color.serialize()
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
"label": self.label,
|
|
29
|
+
"color": color_val,
|
|
30
|
+
"type": self.series_type,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ImageSeries(PlotSeries):
|
|
35
|
+
"""
|
|
36
|
+
A plot data series where the elements are images
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
data,
|
|
42
|
+
label: str = "",
|
|
43
|
+
transform=None,
|
|
44
|
+
):
|
|
45
|
+
super().__init__(label)
|
|
46
|
+
self.data = data
|
|
47
|
+
self.series_type = "image"
|
|
48
|
+
self.transform = transform
|
|
49
|
+
|
|
50
|
+
def serialize(self):
|
|
51
|
+
ret = super().serialize()
|
|
52
|
+
ret["data"] = self.data
|
|
53
|
+
return ret
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class LinePlotSeries(PlotSeries):
|
|
57
|
+
"""
|
|
58
|
+
A plot series for line plots
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
x: list | None = None,
|
|
64
|
+
y: list | None = None,
|
|
65
|
+
label: str = "",
|
|
66
|
+
color: Color | None = None,
|
|
67
|
+
marker: str = "o",
|
|
68
|
+
) -> None:
|
|
69
|
+
super().__init__(label, color)
|
|
70
|
+
self.marker = marker
|
|
71
|
+
self.series_type = "line"
|
|
72
|
+
if x:
|
|
73
|
+
self.x = x
|
|
74
|
+
else:
|
|
75
|
+
self.x = []
|
|
76
|
+
if y:
|
|
77
|
+
self.y = y
|
|
78
|
+
else:
|
|
79
|
+
self.y = []
|
|
80
|
+
|
|
81
|
+
def add_entry(self, x_point: float, y_point: float):
|
|
82
|
+
self.x.append(x_point)
|
|
83
|
+
self.y.append(y_point)
|
|
84
|
+
|
|
85
|
+
def sort(self):
|
|
86
|
+
"""
|
|
87
|
+
Sorts by x values
|
|
88
|
+
"""
|
|
89
|
+
tuples = list(zip(self.x, self.y))
|
|
90
|
+
tuples.sort(key=lambda tup: tup[0])
|
|
91
|
+
for idx, _ in enumerate(tuples):
|
|
92
|
+
self.x[idx] = tuples[idx][0]
|
|
93
|
+
self.y[idx] = tuples[idx][1]
|
|
94
|
+
|
|
95
|
+
def serialize(self):
|
|
96
|
+
ret = super().serialize()
|
|
97
|
+
ret["x"] = self.x
|
|
98
|
+
ret["y"] = self.y
|
|
99
|
+
return ret
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ScatterPlotSeries(PlotSeries):
|
|
103
|
+
def __init__(self, data, label: str = "", color: Color | None = None):
|
|
104
|
+
super().__init__(label, color)
|
|
105
|
+
self.data = data
|
|
106
|
+
self.series_type = "scatter"
|
|
107
|
+
|
|
108
|
+
def serialize(self):
|
|
109
|
+
ret = super().serialize()
|
|
110
|
+
ret["data"] = self.data
|
|
111
|
+
return ret
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module has functions for converting between image
|
|
3
|
+
formats
|
|
4
|
+
"""
|
|
5
|
+
|
|
1
6
|
import logging
|
|
2
7
|
from pathlib import Path
|
|
3
8
|
|
|
@@ -12,14 +17,16 @@ logger = logging.getLogger(__name__)
|
|
|
12
17
|
def _get_out_filename(source: Path, target: Path | None, extension: str) -> Path:
|
|
13
18
|
if target:
|
|
14
19
|
return target
|
|
15
|
-
|
|
16
|
-
return source.parent / f"{source.stem}.{extension}"
|
|
20
|
+
return source.parent / f"{source.stem}.{extension}"
|
|
17
21
|
|
|
18
22
|
|
|
19
|
-
def pdf_to_png(source: Path, target: Path | None = None):
|
|
23
|
+
def pdf_to_png(source: Path, target: Path | None = None, resolution: int = 300):
|
|
24
|
+
"""
|
|
25
|
+
Convert a pdf to png with white background
|
|
26
|
+
"""
|
|
20
27
|
|
|
21
28
|
outfile = _get_out_filename(source, target, "png")
|
|
22
|
-
with Image(filename=source, resolution=
|
|
29
|
+
with Image(filename=source, resolution=resolution) as img:
|
|
23
30
|
img.format = "png"
|
|
24
31
|
img.background_color = Color("white")
|
|
25
32
|
img.alpha_channel = "remove"
|
|
@@ -27,10 +34,18 @@ def pdf_to_png(source: Path, target: Path | None = None):
|
|
|
27
34
|
|
|
28
35
|
|
|
29
36
|
def svg_to_png(source: Path, target: Path | None = None):
|
|
37
|
+
"""
|
|
38
|
+
Convert an svg to png
|
|
39
|
+
"""
|
|
40
|
+
|
|
30
41
|
outfile = _get_out_filename(source, target, "png")
|
|
31
42
|
cairosvg.svg2png(url=str(source), write_to=str(outfile))
|
|
32
43
|
|
|
33
44
|
|
|
34
45
|
def svg_to_pdf(source: Path, target: Path | None = None):
|
|
46
|
+
"""
|
|
47
|
+
Convert an svg to pdf
|
|
48
|
+
"""
|
|
49
|
+
|
|
35
50
|
outfile = _get_out_filename(source, target, "pdf")
|
|
36
51
|
cairosvg.svg2pdf(url=str(source), write_to=str(outfile))
|
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: icplot
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.10
|
|
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,7 @@ 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.0
|
|
19
|
+
Requires-Dist: iccore==0.1.0
|
|
20
20
|
Provides-Extra: test
|
|
21
21
|
Requires-Dist: pytest; extra == "test"
|
|
22
22
|
Requires-Dist: pytest-cov; extra == "test"
|
|
@@ -5,11 +5,10 @@ setup.cfg
|
|
|
5
5
|
src/icplot/__init__.py
|
|
6
6
|
src/icplot/cairo_interface.py
|
|
7
7
|
src/icplot/color.py
|
|
8
|
-
src/icplot/gantt.py
|
|
9
8
|
src/icplot/geometry.py
|
|
10
9
|
src/icplot/image_utils.py
|
|
11
10
|
src/icplot/main_cli.py
|
|
12
|
-
src/icplot/
|
|
11
|
+
src/icplot/py.typed
|
|
13
12
|
src/icplot/tex_interface.py
|
|
14
13
|
src/icplot.egg-info/PKG-INFO
|
|
15
14
|
src/icplot.egg-info/SOURCES.txt
|
|
@@ -17,9 +16,15 @@ src/icplot.egg-info/dependency_links.txt
|
|
|
17
16
|
src/icplot.egg-info/entry_points.txt
|
|
18
17
|
src/icplot.egg-info/requires.txt
|
|
19
18
|
src/icplot.egg-info/top_level.txt
|
|
19
|
+
src/icplot/gantt/__init__.py
|
|
20
|
+
src/icplot/gantt/gantt.py
|
|
21
|
+
src/icplot/gantt/gantt_renderer.py
|
|
22
|
+
src/icplot/gantt/pgfgantt_interface.py
|
|
20
23
|
src/icplot/graph/__init__.py
|
|
21
|
-
src/icplot/graph/
|
|
24
|
+
src/icplot/graph/axis.py
|
|
25
|
+
src/icplot/graph/matplotlib.py
|
|
22
26
|
src/icplot/graph/plot.py
|
|
27
|
+
src/icplot/graph/plotter.py
|
|
23
28
|
src/icplot/graph/series.py
|
|
24
29
|
test/test_cairo_interface.py
|
|
25
30
|
test/test_gantt_chart.py
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
|
|
5
|
+
from icplot.cairo_interface import CairoInterface
|
|
6
|
+
from icplot.geometry import Scene, Rectangle, TextPath
|
|
7
|
+
from icplot.color import Color
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_cairo_interface():
|
|
11
|
+
|
|
12
|
+
cairo_interface = CairoInterface()
|
|
13
|
+
scene = Scene()
|
|
14
|
+
|
|
15
|
+
rect = Rectangle(20, 20)
|
|
16
|
+
rect.location = (10, 10)
|
|
17
|
+
rect.fill = Color.from_rgba(0.5, 0.5, 1, 0.5)
|
|
18
|
+
rect.stroke = Color.from_rgba(0.5, 0.0, 0.0, 0.5)
|
|
19
|
+
scene.items.append(rect)
|
|
20
|
+
|
|
21
|
+
text = TextPath("Hello World")
|
|
22
|
+
text.location = (5, 5)
|
|
23
|
+
scene.items.append(text)
|
|
24
|
+
|
|
25
|
+
output_dir = Path(os.getcwd()) / "test_cairo_interface"
|
|
26
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
27
|
+
cairo_interface.draw_svg(scene, output_dir / "output.svg")
|
|
28
|
+
shutil.rmtree(output_dir)
|
|
29
|
+
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
3
|
from icplot.graph import Plot, LinePlotSeries
|
|
4
|
+
from icplot.graph.matplotlib import MatplotlibPlotter
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
def test_line_plot():
|
|
7
8
|
|
|
8
9
|
plot = Plot(
|
|
9
|
-
title="test",
|
|
10
|
+
title="test",
|
|
11
|
+
x_label="test_x",
|
|
12
|
+
y_label="test_y",
|
|
13
|
+
legend_label="test_legend"
|
|
10
14
|
)
|
|
11
15
|
|
|
12
16
|
data = [([0, 5, 10], [1, 2, 3]), ([0, 5, 10], [3, 6, 9]), ([0, 5, 10], [4, 8, 12])]
|
|
@@ -17,7 +21,9 @@ def test_line_plot():
|
|
|
17
21
|
plot.set_x_ticks(0, 10, 5)
|
|
18
22
|
|
|
19
23
|
output_path = Path() / "output.svg"
|
|
20
|
-
|
|
24
|
+
|
|
25
|
+
plotter = MatplotlibPlotter()
|
|
26
|
+
plotter.plot(plot, output_path)
|
|
21
27
|
|
|
22
28
|
assert output_path.exists()
|
|
23
29
|
output_path.unlink()
|
icplot-0.0.8/src/icplot/color.py
DELETED
icplot-0.0.8/src/icplot/gantt.py
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
from .cairo_interface import CairoInterface
|
|
3
|
-
from .geometry import Scene, Color, Rectangle
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class GanttChart:
|
|
7
|
-
|
|
8
|
-
def __init__(self):
|
|
9
|
-
self.milestones = []
|
|
10
|
-
self.start_date = None
|
|
11
|
-
self.end_date = None
|
|
12
|
-
self.milestone_bar_max_height = 0.1
|
|
13
|
-
self.height = 100
|
|
14
|
-
self.width = 500
|
|
15
|
-
self.milestone_color = Color(0.7, 0, 0)
|
|
16
|
-
|
|
17
|
-
def _add_milestone(self, scene, milestone, chart_range, yloc):
|
|
18
|
-
chart_delta = chart_range[1] - chart_range[0]
|
|
19
|
-
start_delta = milestone.start_date - chart_range[0]
|
|
20
|
-
milestone_delta = milestone.due_date - milestone.start_date
|
|
21
|
-
|
|
22
|
-
start_frac = float(start_delta / chart_delta)
|
|
23
|
-
milestone_frac = float(milestone_delta / chart_delta)
|
|
24
|
-
|
|
25
|
-
w = milestone_frac * self.width
|
|
26
|
-
h = self.milestone_bar_max_height * self.height
|
|
27
|
-
x = start_frac * self.width
|
|
28
|
-
y = yloc
|
|
29
|
-
|
|
30
|
-
rect = Rectangle(w, h)
|
|
31
|
-
rect.location = (x, y)
|
|
32
|
-
rect.fill = self.milestone_color
|
|
33
|
-
scene.items.append(rect)
|
|
34
|
-
|
|
35
|
-
def _add_milestones(self, scene):
|
|
36
|
-
|
|
37
|
-
if not self.milestones:
|
|
38
|
-
return
|
|
39
|
-
|
|
40
|
-
milestones = self.milestones
|
|
41
|
-
milestones.sort(key=lambda x: x.start_date, reverse=True)
|
|
42
|
-
|
|
43
|
-
start_date = self.start_date
|
|
44
|
-
end_date = self.end_date
|
|
45
|
-
if not start_date:
|
|
46
|
-
start_date = milestones[0].start_date
|
|
47
|
-
|
|
48
|
-
if not end_date:
|
|
49
|
-
end_date = max(m.due_date for m in milestones)
|
|
50
|
-
|
|
51
|
-
chart_range = (start_date, end_date)
|
|
52
|
-
yloc = 0
|
|
53
|
-
bar_height = self.milestone_bar_max_height * self.height
|
|
54
|
-
for milestone in milestones:
|
|
55
|
-
self._add_milestone(scene, milestone, chart_range, yloc)
|
|
56
|
-
yloc += bar_height
|
|
57
|
-
|
|
58
|
-
def plot(self, path: Path):
|
|
59
|
-
scene = Scene()
|
|
60
|
-
|
|
61
|
-
self._add_milestones(scene)
|
|
62
|
-
|
|
63
|
-
renderer = CairoInterface()
|
|
64
|
-
renderer.draw_svg(scene, path)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from .plot import * # NOQA
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
import logging
|
|
3
|
-
|
|
4
|
-
logging.getLogger("matplotlib").setLevel(logging.WARNING)
|
|
5
|
-
import matplotlib # NOQA
|
|
6
|
-
|
|
7
|
-
default_backend = matplotlib.get_backend()
|
|
8
|
-
matplotlib.use("Agg")
|
|
9
|
-
import matplotlib.pyplot as plt # NOQA
|
|
10
|
-
import matplotlib as mpl # NOQA
|
|
11
|
-
|
|
12
|
-
from icplot.color import ColorMap # NOQA
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class MatplotlibColorMap(ColorMap):
|
|
16
|
-
|
|
17
|
-
def __init__(self, label: str):
|
|
18
|
-
super().__init__(label)
|
|
19
|
-
self.c_map = mpl.colormaps[label]
|
|
20
|
-
self.cmap_start_offset = 0.25
|
|
21
|
-
self.cmap_scale = 2
|
|
22
|
-
|
|
23
|
-
def get_color(self, cursor: int, values: list):
|
|
24
|
-
"""
|
|
25
|
-
Returns a colour based on a cmap and how far across
|
|
26
|
-
the datasets you are
|
|
27
|
-
"""
|
|
28
|
-
position = cursor / len(values)
|
|
29
|
-
return self.c_map(self.cmap_start_offset + (position / self.cmap_scale))
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class MatplotlibPlotBackend:
|
|
33
|
-
|
|
34
|
-
def __init__(self):
|
|
35
|
-
self.ax = plt.subplot(111)
|
|
36
|
-
|
|
37
|
-
def set_decorations(self, x_label, y_label, title, x_ticks, y_ticks):
|
|
38
|
-
self.ax.legend(loc="upper left")
|
|
39
|
-
if x_label:
|
|
40
|
-
self.ax.set_xlabel(x_label)
|
|
41
|
-
if y_label:
|
|
42
|
-
self.ax.set_ylabel(y_label)
|
|
43
|
-
if title:
|
|
44
|
-
self.ax.set_title(title)
|
|
45
|
-
if x_ticks:
|
|
46
|
-
self.ax.set_xticks(x_ticks)
|
|
47
|
-
if y_ticks:
|
|
48
|
-
self.ax.set_yticks(y_ticks)
|
|
49
|
-
|
|
50
|
-
def plot_line(self, x, y, label, color):
|
|
51
|
-
self.ax.plot(x, y, label=label, color=color)
|
|
52
|
-
|
|
53
|
-
def plot_scatter(self, data, label, color):
|
|
54
|
-
self.ax.plot(data, label=label, color=color)
|
|
55
|
-
|
|
56
|
-
def make_subplot(self, rows, cols, count):
|
|
57
|
-
plt.subplot(rows, cols, count)
|
|
58
|
-
|
|
59
|
-
def plot_image(self, data):
|
|
60
|
-
plt.imshow(data)
|
|
61
|
-
plt.axis("off")
|
|
62
|
-
|
|
63
|
-
def render(self, path: Path | None = None):
|
|
64
|
-
if path:
|
|
65
|
-
plt.savefig(path)
|
|
66
|
-
else:
|
|
67
|
-
plt.switch_backend(default_backend)
|
|
68
|
-
plt.show()
|
|
69
|
-
plt.switch_backend("Agg")
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
This module has content for generating plots
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import random
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import cast
|
|
8
|
-
import json
|
|
9
|
-
|
|
10
|
-
from .series import PlotSeries, LinePlotSeries, ScatterPlotSeries, ImageSeries
|
|
11
|
-
from .matplotlib_plot_backend import MatplotlibPlotBackend, MatplotlibColorMap
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class Range:
|
|
15
|
-
|
|
16
|
-
def __init__(self, lower, upper, step):
|
|
17
|
-
self.lower = lower
|
|
18
|
-
self.upper = upper
|
|
19
|
-
self.step = step
|
|
20
|
-
|
|
21
|
-
def eval(self):
|
|
22
|
-
return range(self.lower, self.upper, self.step)
|
|
23
|
-
|
|
24
|
-
def serialize(self):
|
|
25
|
-
return {"lower": self.lower, "upper": self.upper, "step": self.step}
|
|
26
|
-
|
|
27
|
-
def __str__(self):
|
|
28
|
-
return json.dumps(self.serialize(), ident=4)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class Plot:
|
|
32
|
-
"""
|
|
33
|
-
A generic plot with optional axis ticks
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
def __init__(
|
|
37
|
-
self,
|
|
38
|
-
title: str = "",
|
|
39
|
-
x_label: str = "",
|
|
40
|
-
y_label: str = "",
|
|
41
|
-
legend_label: str = "",
|
|
42
|
-
) -> None:
|
|
43
|
-
self.series: list[PlotSeries] = []
|
|
44
|
-
self.title = title
|
|
45
|
-
self.size: tuple | None = None
|
|
46
|
-
self.plot_type = ""
|
|
47
|
-
self.x_label = x_label
|
|
48
|
-
self.y_label = y_label
|
|
49
|
-
self.legend_label = legend_label
|
|
50
|
-
self.x_ticks: Range | None = None
|
|
51
|
-
self.y_ticks: Range | None = None
|
|
52
|
-
self.backend = MatplotlibPlotBackend()
|
|
53
|
-
self.cmap = MatplotlibColorMap("viridis")
|
|
54
|
-
|
|
55
|
-
def set_colour_map(self, cmap):
|
|
56
|
-
self.c_map = MatplotlibColorMap(cmap)
|
|
57
|
-
|
|
58
|
-
def set_x_ticks(self, lower, upper, step):
|
|
59
|
-
self.x_ticks = Range(lower, upper + 1, step)
|
|
60
|
-
|
|
61
|
-
def set_y_ticks(self, lower, upper, step):
|
|
62
|
-
self.y_ticks = Range(lower, upper + 1, step)
|
|
63
|
-
|
|
64
|
-
def set_decorations(self):
|
|
65
|
-
|
|
66
|
-
xtick_range = None
|
|
67
|
-
ytick_range = None
|
|
68
|
-
if self.x_ticks:
|
|
69
|
-
xtick_range = self.x_ticks.eval()
|
|
70
|
-
if self.y_ticks:
|
|
71
|
-
ytick_range = self.y_ticks.eval()
|
|
72
|
-
|
|
73
|
-
self.backend.set_decorations(
|
|
74
|
-
self.x_label,
|
|
75
|
-
self.y_label,
|
|
76
|
-
self.title,
|
|
77
|
-
xtick_range,
|
|
78
|
-
ytick_range,
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
def add_series(self, series):
|
|
82
|
-
self.series.append(series)
|
|
83
|
-
|
|
84
|
-
def plot_series(self, series, label):
|
|
85
|
-
if series.series_type == "line":
|
|
86
|
-
line_series = cast(LinePlotSeries, series)
|
|
87
|
-
self.backend.plot_line(
|
|
88
|
-
line_series.x, line_series.y, label=label, color=series.color
|
|
89
|
-
)
|
|
90
|
-
elif series.series_type == "scatter":
|
|
91
|
-
scatter_series = cast(ScatterPlotSeries, series)
|
|
92
|
-
self.backend.plot_scatter(
|
|
93
|
-
scatter_series.data, label=label, color=series.color
|
|
94
|
-
)
|
|
95
|
-
elif series.series_type == "image":
|
|
96
|
-
image_series = cast(ImageSeries, series)
|
|
97
|
-
self.backend.plot_image(image_series.data)
|
|
98
|
-
|
|
99
|
-
def plot(self, path: Path | None = None):
|
|
100
|
-
for idx, series in enumerate(self.series):
|
|
101
|
-
series.color = self.cmap.get_color(idx, self.series)
|
|
102
|
-
|
|
103
|
-
first = True
|
|
104
|
-
for series in self.series:
|
|
105
|
-
label = series.label
|
|
106
|
-
if first and self.legend_label:
|
|
107
|
-
label = f"{label} {self.legend_label}"
|
|
108
|
-
first = False
|
|
109
|
-
self.plot_series(series, label)
|
|
110
|
-
|
|
111
|
-
self.set_decorations()
|
|
112
|
-
self.backend.render(path)
|
|
113
|
-
|
|
114
|
-
def serialize(self):
|
|
115
|
-
return {
|
|
116
|
-
"series": [s.serialize() for s in self.series],
|
|
117
|
-
"title": self.title,
|
|
118
|
-
"type": self.plot_type,
|
|
119
|
-
"xlabel": self.x_label,
|
|
120
|
-
"ylabel": self.y_label,
|
|
121
|
-
"legend_label": self.legend_label,
|
|
122
|
-
"xticks": self.x_ticks.serialize(),
|
|
123
|
-
"yticks": self.y_ticks.serialize(),
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
class GridPlot(Plot):
|
|
128
|
-
"""
|
|
129
|
-
Make a grid of plots
|
|
130
|
-
"""
|
|
131
|
-
|
|
132
|
-
def __init__(
|
|
133
|
-
self, title: str = "", stride: int = 4, size: tuple = (25, 20)
|
|
134
|
-
) -> None:
|
|
135
|
-
super().__init__(title)
|
|
136
|
-
self.stride = stride
|
|
137
|
-
self.size = size
|
|
138
|
-
|
|
139
|
-
def _subplot(self, rows: int, cols: int, count: int, series):
|
|
140
|
-
|
|
141
|
-
self.backend.make_subplot(rows, cols, count)
|
|
142
|
-
self.plot_series(series, "")
|
|
143
|
-
return count + 1
|
|
144
|
-
|
|
145
|
-
def render(self, data, path: Path, num_samples: int = 0):
|
|
146
|
-
rows = num_samples // self.stride
|
|
147
|
-
cols = num_samples // rows
|
|
148
|
-
count = 1
|
|
149
|
-
if num_samples == 0:
|
|
150
|
-
indices = [random.randint(0, len(data) - 1) for _ in range(num_samples)]
|
|
151
|
-
else:
|
|
152
|
-
indices = list(range(0, len(data)))
|
|
153
|
-
|
|
154
|
-
for index in indices:
|
|
155
|
-
if num_samples > 0 and count == num_samples + 1:
|
|
156
|
-
break
|
|
157
|
-
if isinstance(data[index], list):
|
|
158
|
-
for series in data[index]:
|
|
159
|
-
count = self._subplot(rows, cols, count, series)
|
|
160
|
-
else:
|
|
161
|
-
count = self._subplot(rows, cols, count, data[index])
|
|
162
|
-
|
|
163
|
-
self.backend.render(path)
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class PlotSeries:
|
|
5
|
-
|
|
6
|
-
def __init__(self, label: str = ""):
|
|
7
|
-
self.label = label
|
|
8
|
-
self.color: list[float] = [0.0, 0.0, 0.0]
|
|
9
|
-
self.series_type = ""
|
|
10
|
-
|
|
11
|
-
def serialize(self):
|
|
12
|
-
return {"label": self.label, "color": self.color, "type": self.series_type}
|
|
13
|
-
|
|
14
|
-
def __str__(self):
|
|
15
|
-
return json.dumps(self.serialize(), indent=4)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class ImageSeries(PlotSeries):
|
|
19
|
-
def __init__(self, data, label: str = "", transform=None):
|
|
20
|
-
super().__init__(label)
|
|
21
|
-
self.data = data
|
|
22
|
-
self.series_type = "image"
|
|
23
|
-
self.transform = transform
|
|
24
|
-
|
|
25
|
-
def serialize(self):
|
|
26
|
-
ret = super().serialize()
|
|
27
|
-
ret["data"] = self.data
|
|
28
|
-
return ret
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class LinePlotSeries(PlotSeries):
|
|
32
|
-
def __init__(self, x: list[float], y: list[float], label: str = "") -> None:
|
|
33
|
-
super().__init__(label)
|
|
34
|
-
self.series_type = "line"
|
|
35
|
-
self.x = x
|
|
36
|
-
self.y = y
|
|
37
|
-
|
|
38
|
-
def serialize(self):
|
|
39
|
-
ret = super().serialize()
|
|
40
|
-
ret["x"] = self.x
|
|
41
|
-
ret["y"] = self.y
|
|
42
|
-
return ret
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class ScatterPlotSeries(PlotSeries):
|
|
46
|
-
def __init__(self, data, label: str = ""):
|
|
47
|
-
super().__init__(label)
|
|
48
|
-
self.data = data
|
|
49
|
-
self.series_type = "scatter"
|
|
50
|
-
|
|
51
|
-
def serialize(self):
|
|
52
|
-
ret = super().serialize()
|
|
53
|
-
ret["data"] = self.data
|
|
54
|
-
return ret
|
|
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
|