icplot 0.0.10__tar.gz → 0.0.11__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.10/src/icplot.egg-info → icplot-0.0.11}/PKG-INFO +8 -2
- {icplot-0.0.10 → icplot-0.0.11}/README.md +6 -0
- {icplot-0.0.10 → icplot-0.0.11}/pyproject.toml +2 -2
- {icplot-0.0.10 → icplot-0.0.11}/src/icplot/color.py +3 -6
- icplot-0.0.11/src/icplot/graph/axis.py +33 -0
- {icplot-0.0.10 → icplot-0.0.11}/src/icplot/graph/matplotlib.py +3 -3
- icplot-0.0.11/src/icplot/graph/plot.py +64 -0
- {icplot-0.0.10 → icplot-0.0.11}/src/icplot/graph/plotter.py +6 -5
- icplot-0.0.11/src/icplot/graph/series.py +43 -0
- {icplot-0.0.10 → icplot-0.0.11}/src/icplot/main_cli.py +48 -8
- icplot-0.0.11/src/icplot/tex_interface.py +83 -0
- {icplot-0.0.10 → icplot-0.0.11/src/icplot.egg-info}/PKG-INFO +8 -2
- {icplot-0.0.10 → icplot-0.0.11}/src/icplot.egg-info/requires.txt +1 -1
- icplot-0.0.11/test/test_line_plots.py +32 -0
- icplot-0.0.11/test/test_tex_interface.py +24 -0
- icplot-0.0.10/src/icplot/graph/axis.py +0 -44
- icplot-0.0.10/src/icplot/graph/plot.py +0 -92
- icplot-0.0.10/src/icplot/graph/series.py +0 -111
- icplot-0.0.10/src/icplot/tex_interface.py +0 -92
- icplot-0.0.10/test/test_line_plots.py +0 -29
- icplot-0.0.10/test/test_tex_interface.py +0 -18
- {icplot-0.0.10 → icplot-0.0.11}/LICENSE +0 -0
- {icplot-0.0.10 → icplot-0.0.11}/setup.cfg +0 -0
- {icplot-0.0.10 → icplot-0.0.11}/src/icplot/__init__.py +0 -0
- {icplot-0.0.10 → icplot-0.0.11}/src/icplot/cairo_interface.py +0 -0
- {icplot-0.0.10 → icplot-0.0.11}/src/icplot/gantt/__init__.py +0 -0
- {icplot-0.0.10 → icplot-0.0.11}/src/icplot/gantt/gantt.py +0 -0
- {icplot-0.0.10 → icplot-0.0.11}/src/icplot/gantt/gantt_renderer.py +0 -0
- {icplot-0.0.10 → icplot-0.0.11}/src/icplot/gantt/pgfgantt_interface.py +0 -0
- {icplot-0.0.10 → icplot-0.0.11}/src/icplot/geometry.py +0 -0
- {icplot-0.0.10 → icplot-0.0.11}/src/icplot/graph/__init__.py +0 -0
- {icplot-0.0.10 → icplot-0.0.11}/src/icplot/image_utils.py +0 -0
- {icplot-0.0.10 → icplot-0.0.11}/src/icplot/py.typed +0 -0
- {icplot-0.0.10 → icplot-0.0.11}/src/icplot.egg-info/SOURCES.txt +0 -0
- {icplot-0.0.10 → icplot-0.0.11}/src/icplot.egg-info/dependency_links.txt +0 -0
- {icplot-0.0.10 → icplot-0.0.11}/src/icplot.egg-info/entry_points.txt +0 -0
- {icplot-0.0.10 → icplot-0.0.11}/src/icplot.egg-info/top_level.txt +0 -0
- {icplot-0.0.10 → icplot-0.0.11}/test/test_cairo_interface.py +0 -0
- {icplot-0.0.10 → icplot-0.0.11}/test/test_gantt_chart.py +0 -0
- {icplot-0.0.10 → icplot-0.0.11}/test/test_image_utils.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.11
|
|
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.1.
|
|
19
|
+
Requires-Dist: iccore==0.1.1
|
|
20
20
|
Provides-Extra: test
|
|
21
21
|
Requires-Dist: pytest; extra == "test"
|
|
22
22
|
Requires-Dist: pytest-cov; extra == "test"
|
|
@@ -62,6 +62,12 @@ To covert between image formats you can do:
|
|
|
62
62
|
icplot convert --source my_image.svg --target my_image.png
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
To render a Tex tikz image as pdf and png you can do:
|
|
66
|
+
|
|
67
|
+
``` shell
|
|
68
|
+
icplot convert --source my_tikz.tex
|
|
69
|
+
```
|
|
70
|
+
|
|
65
71
|
# Copyright #
|
|
66
72
|
|
|
67
73
|
Copyright 2024 Irish Centre for High End Computing
|
|
@@ -31,6 +31,12 @@ To covert between image formats you can do:
|
|
|
31
31
|
icplot convert --source my_image.svg --target my_image.png
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
To render a Tex tikz image as pdf and png you can do:
|
|
35
|
+
|
|
36
|
+
``` shell
|
|
37
|
+
icplot convert --source my_tikz.tex
|
|
38
|
+
```
|
|
39
|
+
|
|
34
40
|
# Copyright #
|
|
35
41
|
|
|
36
42
|
Copyright 2024 Irish Centre for High End Computing
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "icplot"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.11"
|
|
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.1.
|
|
21
|
+
"iccore==0.1.1"]
|
|
22
22
|
|
|
23
23
|
[project.urls]
|
|
24
24
|
Repository = "https://git.ichec.ie/performance/toolshed/icplot"
|
|
@@ -34,6 +34,7 @@ class Color(BaseModel):
|
|
|
34
34
|
def serialize(self) -> str:
|
|
35
35
|
return f"r: {self.r}, g: {self.g}, b: {self.b}, a: {self.a}"
|
|
36
36
|
|
|
37
|
+
|
|
37
38
|
class ColorMap:
|
|
38
39
|
"""
|
|
39
40
|
A mapping from a flat index to a color
|
|
@@ -42,15 +43,11 @@ class ColorMap:
|
|
|
42
43
|
def __init__(self, label: str, data_func: Callable):
|
|
43
44
|
self.label = label
|
|
44
45
|
self.data_func = data_func
|
|
45
|
-
self.start_offset = 0.25
|
|
46
|
-
self.scale = 2
|
|
47
46
|
|
|
48
47
|
def get_color(self, cursor: int, values: list) -> Color:
|
|
49
48
|
"""
|
|
50
49
|
Returns a colour based on a cmap and how far across
|
|
51
50
|
the datasets you are
|
|
52
51
|
"""
|
|
53
|
-
position = cursor / len(values)
|
|
54
|
-
return Color.from_list(
|
|
55
|
-
self.data_func(self.start_offset + (position / self.scale))
|
|
56
|
-
)
|
|
52
|
+
position = cursor / (len(values) - 1)
|
|
53
|
+
return Color.from_list(self.data_func(position))
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module has a plot's axis
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Range(BaseModel):
|
|
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
|
+
lower: int
|
|
15
|
+
upper: int
|
|
16
|
+
step: int
|
|
17
|
+
|
|
18
|
+
def eval(self) -> list:
|
|
19
|
+
return list(range(self.lower, self.upper, self.step))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PlotAxis(BaseModel):
|
|
23
|
+
"""
|
|
24
|
+
This is a plot's axis
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
label: str = ""
|
|
28
|
+
ticks: Range | None = None
|
|
29
|
+
|
|
30
|
+
def get_resolved_ticks(self) -> list:
|
|
31
|
+
if self.ticks:
|
|
32
|
+
return self.ticks.eval()
|
|
33
|
+
return []
|
|
@@ -29,7 +29,7 @@ class MatplotlibPlotter(Plotter):
|
|
|
29
29
|
A plot backend for matplotlib
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
|
-
def __init__(self, cmap_label="
|
|
32
|
+
def __init__(self, cmap_label="gist_rainbow"):
|
|
33
33
|
super().__init__(MatplotlibColorMap(cmap_label))
|
|
34
34
|
|
|
35
35
|
def set_decorations(self, ax, plot: Plot):
|
|
@@ -50,12 +50,12 @@ class MatplotlibPlotter(Plotter):
|
|
|
50
50
|
series.x,
|
|
51
51
|
series.y,
|
|
52
52
|
label=series.label,
|
|
53
|
-
color=series.
|
|
53
|
+
color=series.color.as_list(),
|
|
54
54
|
marker=series.marker,
|
|
55
55
|
)
|
|
56
56
|
|
|
57
57
|
def plot_scatter(self, ax, series: ScatterPlotSeries):
|
|
58
|
-
ax.scatter(series.data, label=series.label, color=series.
|
|
58
|
+
ax.scatter(series.data, label=series.label, color=series.color.as_list())
|
|
59
59
|
|
|
60
60
|
def plot_image(self, ax, series: ImageSeries):
|
|
61
61
|
ax.imshow(series.data)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module has content for generating plots
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import random
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from .axis import PlotAxis
|
|
10
|
+
from .series import PlotSeries
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Plot(BaseModel):
|
|
14
|
+
"""
|
|
15
|
+
A generic plot with optional axis ticks
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
title: str = ""
|
|
19
|
+
legend_label: str = ""
|
|
20
|
+
x_axis: PlotAxis = PlotAxis()
|
|
21
|
+
y_axis: PlotAxis = PlotAxis()
|
|
22
|
+
plot_type: str = ""
|
|
23
|
+
series: list[PlotSeries] = []
|
|
24
|
+
|
|
25
|
+
def add_series(self, series: PlotSeries):
|
|
26
|
+
self.series.append(series)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class GridPlot(Plot):
|
|
30
|
+
"""
|
|
31
|
+
Make a grid of plots
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
stride: int = 4
|
|
35
|
+
size: tuple = (25, 20)
|
|
36
|
+
data: list = []
|
|
37
|
+
|
|
38
|
+
def get_series_indices(self, num_samples: int = 0):
|
|
39
|
+
rows = num_samples // self.stride
|
|
40
|
+
cols = num_samples // rows
|
|
41
|
+
len_data = len(self.data)
|
|
42
|
+
|
|
43
|
+
if num_samples == 0:
|
|
44
|
+
indices = list(range(0, len_data))
|
|
45
|
+
else:
|
|
46
|
+
indices = [random.randint(0, len_data - 1) for _ in range(num_samples)]
|
|
47
|
+
return rows, cols, indices
|
|
48
|
+
|
|
49
|
+
def get_subplots(self, num_samples: int = 0):
|
|
50
|
+
rows, cols, indices = self.get_series_indices(num_samples)
|
|
51
|
+
|
|
52
|
+
subplots = []
|
|
53
|
+
count = 1
|
|
54
|
+
for index in indices:
|
|
55
|
+
if num_samples > 0 and count == num_samples + 1:
|
|
56
|
+
break
|
|
57
|
+
if isinstance(self.data[index], list):
|
|
58
|
+
for series in self.data[index]:
|
|
59
|
+
subplots.append(series)
|
|
60
|
+
count += 1
|
|
61
|
+
else:
|
|
62
|
+
subplots.append(self.data[index])
|
|
63
|
+
count += 1
|
|
64
|
+
return rows, cols, subplots
|
|
@@ -11,12 +11,13 @@ class Plotter:
|
|
|
11
11
|
self.cmap = cmap
|
|
12
12
|
|
|
13
13
|
def apply_cmap_colors(self, plot: Plot):
|
|
14
|
-
|
|
14
|
+
non_highlight = []
|
|
15
15
|
for idx, series in enumerate(plot.series):
|
|
16
|
-
if not series.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
if not series.highlight:
|
|
17
|
+
non_highlight.append(series)
|
|
18
|
+
|
|
19
|
+
for idx, series in enumerate(non_highlight):
|
|
20
|
+
series.color = self.cmap.get_color(idx, non_highlight)
|
|
20
21
|
|
|
21
22
|
def plot(self, plot: Plot, path: Path | None = None):
|
|
22
23
|
pass
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from icplot.color import Color
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PlotSeries(BaseModel):
|
|
9
|
+
"""
|
|
10
|
+
A data series in a plot, such as a single line in a line-plot
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
label: str
|
|
14
|
+
color: Color = Color()
|
|
15
|
+
series_type: str = ""
|
|
16
|
+
highlight: bool = False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ImageSeries(PlotSeries):
|
|
20
|
+
"""
|
|
21
|
+
A plot data series where the elements are images
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
data: typing.Any
|
|
25
|
+
transform: typing.Any
|
|
26
|
+
series_type: str = "image"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class LinePlotSeries(PlotSeries):
|
|
30
|
+
"""
|
|
31
|
+
A plot series for line plots
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
x: list
|
|
35
|
+
y: list
|
|
36
|
+
marker: str = "o"
|
|
37
|
+
series_type: str = "line"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ScatterPlotSeries(PlotSeries):
|
|
41
|
+
|
|
42
|
+
data: typing.Any
|
|
43
|
+
series_type: str = "scatter"
|
|
@@ -2,24 +2,23 @@
|
|
|
2
2
|
import argparse
|
|
3
3
|
import logging
|
|
4
4
|
from pathlib import Path
|
|
5
|
+
import os
|
|
5
6
|
|
|
6
|
-
from iccore import
|
|
7
|
-
from iccore import
|
|
7
|
+
from iccore.cli_utils import launch_common
|
|
8
|
+
from iccore.serialization import read_yaml
|
|
8
9
|
|
|
10
|
+
from icplot.graph import Plot
|
|
11
|
+
from icplot.graph.matplotlib import MatplotlibPlotter
|
|
9
12
|
from icplot.image_utils import pdf_to_png, svg_to_png, svg_to_pdf
|
|
13
|
+
from icplot.tex_interface import TexBuildSettings, TexInterface
|
|
10
14
|
|
|
11
15
|
logger = logging.getLogger(__name__)
|
|
12
16
|
|
|
13
17
|
|
|
14
|
-
def launch_common(args):
|
|
15
|
-
runtime.ctx.set_is_dry_run(args.dry_run)
|
|
16
|
-
logging_utils.setup_default_logger()
|
|
17
|
-
|
|
18
|
-
|
|
19
18
|
def convert(args):
|
|
20
19
|
launch_common(args)
|
|
21
20
|
|
|
22
|
-
logger.info("
|
|
21
|
+
logger.info("Startubg conversion between %s and %s", args.source, args.target)
|
|
23
22
|
|
|
24
23
|
if args.target:
|
|
25
24
|
target = Path(args.target).resolve()
|
|
@@ -36,9 +35,35 @@ def convert(args):
|
|
|
36
35
|
svg_to_pdf(args.source.resolve(), target)
|
|
37
36
|
else:
|
|
38
37
|
svg_to_png(args.source)
|
|
38
|
+
elif args.source.suffix == ".tex":
|
|
39
|
+
if args.target:
|
|
40
|
+
target = Path(args.target).resolve()
|
|
41
|
+
else:
|
|
42
|
+
target = Path(os.getcwd())
|
|
43
|
+
|
|
44
|
+
settings = TexBuildSettings(
|
|
45
|
+
args.source.resolve(), args.build_dir.resolve(), args.target
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
tex = TexInterface()
|
|
49
|
+
tex.build(settings)
|
|
50
|
+
|
|
39
51
|
logger.info("Finished conversion")
|
|
40
52
|
|
|
41
53
|
|
|
54
|
+
def plot(args):
|
|
55
|
+
|
|
56
|
+
launch_common(args)
|
|
57
|
+
|
|
58
|
+
config = read_yaml(args.config.resolve())
|
|
59
|
+
|
|
60
|
+
plotter = MatplotlibPlotter()
|
|
61
|
+
if ["plots"] in config:
|
|
62
|
+
for eachplot in config["plots"]:
|
|
63
|
+
plot = Plot(**eachplot)
|
|
64
|
+
plotter.plot(plot, args.output_dir.resolve())
|
|
65
|
+
|
|
66
|
+
|
|
42
67
|
def main_cli():
|
|
43
68
|
parser = argparse.ArgumentParser()
|
|
44
69
|
parser.add_argument(
|
|
@@ -61,8 +86,23 @@ def main_cli():
|
|
|
61
86
|
default="",
|
|
62
87
|
help="Path to file to be converted to",
|
|
63
88
|
)
|
|
89
|
+
convert_parser.add_argument(
|
|
90
|
+
"--build_dir",
|
|
91
|
+
default=Path(os.getcwd()) / "_build/tikz",
|
|
92
|
+
help="Path for build output",
|
|
93
|
+
)
|
|
64
94
|
convert_parser.set_defaults(func=convert)
|
|
65
95
|
|
|
96
|
+
plot_parser = subparsers.add_parser("plot")
|
|
97
|
+
plot_parser.add_argument("--config", type=Path, help="Path to the plot config")
|
|
98
|
+
plot_parser.add_argument(
|
|
99
|
+
"--output_dir",
|
|
100
|
+
type=Path,
|
|
101
|
+
default=Path(os.getcwd()),
|
|
102
|
+
help="Path to the plot output directory",
|
|
103
|
+
)
|
|
104
|
+
plot_parser.set_defaults(func=plot)
|
|
105
|
+
|
|
66
106
|
args = parser.parse_args()
|
|
67
107
|
args.func(args)
|
|
68
108
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module has functionality for publishing Tex documents
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from typing import NamedTuple
|
|
7
|
+
import shutil
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
from .image_utils import pdf_to_png
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TexBuildSettings(NamedTuple):
|
|
18
|
+
"""
|
|
19
|
+
This holds settings for the Tex build
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
source: Path
|
|
23
|
+
build_dir: Path
|
|
24
|
+
output_dir: Path | None = None
|
|
25
|
+
build_engine: str = "pdflatex"
|
|
26
|
+
output_png: bool = True
|
|
27
|
+
clear_workdir: bool = True
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TexInterface:
|
|
31
|
+
"""
|
|
32
|
+
This is an interface to Tex for building Tex doucments.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def build_pdf(self, settings: TexBuildSettings, work_dir: Path):
|
|
36
|
+
|
|
37
|
+
cmd = f"{settings.build_engine} {settings.source}"
|
|
38
|
+
|
|
39
|
+
with open(work_dir / "stdout.txt", "w", encoding="utf-8") as f:
|
|
40
|
+
subprocess.run(
|
|
41
|
+
cmd, shell=True, check=True, cwd=work_dir, stdout=f, stderr=f
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def build_single(self, settings: TexBuildSettings):
|
|
45
|
+
logger.info("Building source: %s", settings.source)
|
|
46
|
+
|
|
47
|
+
# Make a working dir
|
|
48
|
+
work_dir = settings.build_dir / settings.source.stem
|
|
49
|
+
os.makedirs(work_dir, exist_ok=True)
|
|
50
|
+
|
|
51
|
+
self.build_pdf(settings, work_dir)
|
|
52
|
+
|
|
53
|
+
tex_path = work_dir / settings.source.name
|
|
54
|
+
pdf_path = tex_path.parent / f"{tex_path.stem}.pdf"
|
|
55
|
+
if settings.output_png:
|
|
56
|
+
pdf_to_png(pdf_path)
|
|
57
|
+
|
|
58
|
+
# If output dir is different to build dir copy final content there
|
|
59
|
+
if settings.output_dir and settings.output_dir != work_dir:
|
|
60
|
+
os.makedirs(settings.output_dir, exist_ok=True)
|
|
61
|
+
shutil.copy(pdf_path, settings.output_dir)
|
|
62
|
+
|
|
63
|
+
if settings.output_png:
|
|
64
|
+
png_path = pdf_path.parent / f"{tex_path.stem}.png"
|
|
65
|
+
shutil.copy(png_path, settings.output_dir)
|
|
66
|
+
|
|
67
|
+
if settings.clear_workdir:
|
|
68
|
+
shutil.rmtree(work_dir)
|
|
69
|
+
|
|
70
|
+
def build(self, settings: TexBuildSettings):
|
|
71
|
+
if settings.source.is_dir():
|
|
72
|
+
for tex_file in settings.source.glob("*.tex"):
|
|
73
|
+
single_build = TexBuildSettings(
|
|
74
|
+
tex_file,
|
|
75
|
+
settings.build_dir,
|
|
76
|
+
settings.output_dir,
|
|
77
|
+
settings.build_engine,
|
|
78
|
+
)
|
|
79
|
+
self.build_single(single_build)
|
|
80
|
+
else:
|
|
81
|
+
self.build_single(settings)
|
|
82
|
+
|
|
83
|
+
logger.info("Finished building sources")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: icplot
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.11
|
|
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.1.
|
|
19
|
+
Requires-Dist: iccore==0.1.1
|
|
20
20
|
Provides-Extra: test
|
|
21
21
|
Requires-Dist: pytest; extra == "test"
|
|
22
22
|
Requires-Dist: pytest-cov; extra == "test"
|
|
@@ -62,6 +62,12 @@ To covert between image formats you can do:
|
|
|
62
62
|
icplot convert --source my_image.svg --target my_image.png
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
To render a Tex tikz image as pdf and png you can do:
|
|
66
|
+
|
|
67
|
+
``` shell
|
|
68
|
+
icplot convert --source my_tikz.tex
|
|
69
|
+
```
|
|
70
|
+
|
|
65
71
|
# Copyright #
|
|
66
72
|
|
|
67
73
|
Copyright 2024 Irish Centre for High End Computing
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from icplot.graph import Plot, LinePlotSeries
|
|
4
|
+
from icplot.graph.matplotlib import MatplotlibPlotter
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_line_plot():
|
|
8
|
+
|
|
9
|
+
data = [([0, 5, 10], [1, 2, 3]), ([0, 5, 10], [3, 6, 9]), ([0, 5, 10], [4, 8, 12])]
|
|
10
|
+
|
|
11
|
+
series = []
|
|
12
|
+
for idx, [x, y] in enumerate(data):
|
|
13
|
+
series_config = {"label" : f"Series {idx}", "x" : x, "y" : y}
|
|
14
|
+
series.append(LinePlotSeries(**series_config))
|
|
15
|
+
|
|
16
|
+
plot_config = {"title": "test",
|
|
17
|
+
"legend_label" : "test_legend",
|
|
18
|
+
"series" : series,
|
|
19
|
+
"x_axis" : {"label" : "test_x",
|
|
20
|
+
"ticks" : {"lower" : 0,
|
|
21
|
+
"upper" : 10,
|
|
22
|
+
"step" : 5}}}
|
|
23
|
+
|
|
24
|
+
plot = Plot(**plot_config)
|
|
25
|
+
|
|
26
|
+
output_path = Path() / "output.svg"
|
|
27
|
+
|
|
28
|
+
plotter = MatplotlibPlotter()
|
|
29
|
+
plotter.plot(plot, output_path)
|
|
30
|
+
|
|
31
|
+
assert output_path.exists()
|
|
32
|
+
output_path.unlink()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from iccore.test_utils import get_test_data_dir, get_test_output_dir
|
|
5
|
+
|
|
6
|
+
from icplot.tex_interface import TexBuildSettings, TexInterface
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_tex_interface():
|
|
10
|
+
|
|
11
|
+
data_dir = get_test_data_dir()
|
|
12
|
+
build_dir = get_test_output_dir()
|
|
13
|
+
source = data_dir / "test.tex"
|
|
14
|
+
|
|
15
|
+
settings = TexBuildSettings(source, build_dir, build_dir)
|
|
16
|
+
|
|
17
|
+
tex = TexInterface()
|
|
18
|
+
|
|
19
|
+
tex.build(settings)
|
|
20
|
+
|
|
21
|
+
assert Path(build_dir / "test.pdf").exists()
|
|
22
|
+
assert Path(build_dir / "test.png").exists()
|
|
23
|
+
|
|
24
|
+
shutil.rmtree(build_dir)
|
|
@@ -1,44 +0,0 @@
|
|
|
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 []
|
|
@@ -1,92 +0,0 @@
|
|
|
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
|
|
@@ -1,111 +0,0 @@
|
|
|
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,92 +0,0 @@
|
|
|
1
|
-
import argparse
|
|
2
|
-
import subprocess
|
|
3
|
-
import shutil
|
|
4
|
-
import os
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
import logging
|
|
7
|
-
|
|
8
|
-
from .image_utils import pdf_to_png
|
|
9
|
-
|
|
10
|
-
logger = logging.getLogger(__name__)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class TexInterface:
|
|
14
|
-
def __init__(self, build_dir: Path | None, output_dir: Path | None):
|
|
15
|
-
|
|
16
|
-
if build_dir:
|
|
17
|
-
self.build_dir: Path = build_dir.resolve()
|
|
18
|
-
else:
|
|
19
|
-
self.build_dir = Path()
|
|
20
|
-
if not output_dir:
|
|
21
|
-
self.output_dir: Path = self.build_dir
|
|
22
|
-
else:
|
|
23
|
-
self.output_dir = output_dir.resolve()
|
|
24
|
-
self.build_engine = "pdflatex"
|
|
25
|
-
|
|
26
|
-
def build_pdf(self, source_path: Path, build_dir: Path):
|
|
27
|
-
cmd = f"{self.build_engine} {source_path}"
|
|
28
|
-
subprocess.run(cmd, shell=True, check=True, cwd=build_dir)
|
|
29
|
-
|
|
30
|
-
def build_single(self, source_path: Path):
|
|
31
|
-
logging.info("Building source: %s", source_path)
|
|
32
|
-
|
|
33
|
-
# Make a working dir
|
|
34
|
-
work_dir = self.build_dir / source_path.stem
|
|
35
|
-
os.makedirs(work_dir, exist_ok=True)
|
|
36
|
-
|
|
37
|
-
tex_path = work_dir / source_path.name
|
|
38
|
-
shutil.copy(source_path, tex_path)
|
|
39
|
-
|
|
40
|
-
self.build_pdf(tex_path, work_dir)
|
|
41
|
-
|
|
42
|
-
pdf_path = tex_path.parent / f"{tex_path.stem}.pdf"
|
|
43
|
-
pdf_to_png(pdf_path)
|
|
44
|
-
|
|
45
|
-
# If output dir is different to build dir copy final content there
|
|
46
|
-
if self.output_dir != self.build_dir:
|
|
47
|
-
png_path = pdf_path.parent / f"{tex_path.stem}.png"
|
|
48
|
-
shutil.copy(pdf_path, self.output_dir)
|
|
49
|
-
shutil.copy(png_path, self.output_dir)
|
|
50
|
-
|
|
51
|
-
def build(self, search_path: Path):
|
|
52
|
-
|
|
53
|
-
if not search_path.is_absolute():
|
|
54
|
-
search_path = Path(os.getcwd()) / search_path
|
|
55
|
-
|
|
56
|
-
os.makedirs(self.output_dir, exist_ok=True)
|
|
57
|
-
|
|
58
|
-
if search_path.is_dir():
|
|
59
|
-
for tex_file in search_path.glob("*.tex"):
|
|
60
|
-
self.build_single(tex_file)
|
|
61
|
-
else:
|
|
62
|
-
self.build_single(search_path)
|
|
63
|
-
|
|
64
|
-
logger.info("Finished building sources")
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if __name__ == "__main__":
|
|
68
|
-
|
|
69
|
-
parser = argparse.ArgumentParser()
|
|
70
|
-
parser.add_argument(
|
|
71
|
-
"--source",
|
|
72
|
-
type=Path,
|
|
73
|
-
help="Path to the tikz source",
|
|
74
|
-
)
|
|
75
|
-
parser.add_argument(
|
|
76
|
-
"--build_dir",
|
|
77
|
-
type=Path,
|
|
78
|
-
default=Path(os.getcwd()) / "_build/tikz",
|
|
79
|
-
help="Path for build output",
|
|
80
|
-
)
|
|
81
|
-
parser.add_argument(
|
|
82
|
-
"--output_dir",
|
|
83
|
-
type=Path,
|
|
84
|
-
default="",
|
|
85
|
-
help="Optional dir to collect build output",
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
logging.basicConfig(encoding="utf-8", level=logging.INFO)
|
|
89
|
-
args = parser.parse_args()
|
|
90
|
-
|
|
91
|
-
tex = TexInterface(args.build_dir, args.output_dir)
|
|
92
|
-
tex.build(args.source)
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
|
|
3
|
-
from icplot.graph import Plot, LinePlotSeries
|
|
4
|
-
from icplot.graph.matplotlib import MatplotlibPlotter
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def test_line_plot():
|
|
8
|
-
|
|
9
|
-
plot = Plot(
|
|
10
|
-
title="test",
|
|
11
|
-
x_label="test_x",
|
|
12
|
-
y_label="test_y",
|
|
13
|
-
legend_label="test_legend"
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
data = [([0, 5, 10], [1, 2, 3]), ([0, 5, 10], [3, 6, 9]), ([0, 5, 10], [4, 8, 12])]
|
|
17
|
-
|
|
18
|
-
for idx, [x, y] in enumerate(data):
|
|
19
|
-
plot.add_series(LinePlotSeries(x, y, label=f"Series {idx}"))
|
|
20
|
-
|
|
21
|
-
plot.set_x_ticks(0, 10, 5)
|
|
22
|
-
|
|
23
|
-
output_path = Path() / "output.svg"
|
|
24
|
-
|
|
25
|
-
plotter = MatplotlibPlotter()
|
|
26
|
-
plotter.plot(plot, output_path)
|
|
27
|
-
|
|
28
|
-
assert output_path.exists()
|
|
29
|
-
output_path.unlink()
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import shutil
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from icplot.tex_interface import TexInterface
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def test_tex_interface():
|
|
8
|
-
|
|
9
|
-
build_dir = Path(os.getcwd()) / "tmp_build"
|
|
10
|
-
tex = TexInterface(build_dir, build_dir)
|
|
11
|
-
|
|
12
|
-
data_dir = Path(__file__).parent / "data"
|
|
13
|
-
tex_file = data_dir / "test.tex"
|
|
14
|
-
|
|
15
|
-
tex = TexInterface(build_dir, build_dir)
|
|
16
|
-
tex.build(tex_file)
|
|
17
|
-
|
|
18
|
-
shutil.rmtree(build_dir)
|
|
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
|