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.
Files changed (42) hide show
  1. {icplot-0.0.8/src/icplot.egg-info → icplot-0.0.10}/PKG-INFO +2 -2
  2. {icplot-0.0.8 → icplot-0.0.10}/pyproject.toml +2 -2
  3. {icplot-0.0.8 → icplot-0.0.10}/src/icplot/cairo_interface.py +4 -24
  4. icplot-0.0.10/src/icplot/color.py +56 -0
  5. icplot-0.0.10/src/icplot/gantt/__init__.py +1 -0
  6. icplot-0.0.10/src/icplot/gantt/gantt.py +19 -0
  7. icplot-0.0.10/src/icplot/gantt/gantt_renderer.py +59 -0
  8. icplot-0.0.10/src/icplot/gantt/pgfgantt_interface.py +26 -0
  9. icplot-0.0.10/src/icplot/graph/__init__.py +2 -0
  10. icplot-0.0.10/src/icplot/graph/axis.py +44 -0
  11. icplot-0.0.10/src/icplot/graph/matplotlib.py +101 -0
  12. icplot-0.0.10/src/icplot/graph/plot.py +92 -0
  13. icplot-0.0.10/src/icplot/graph/plotter.py +22 -0
  14. icplot-0.0.10/src/icplot/graph/series.py +111 -0
  15. {icplot-0.0.8 → icplot-0.0.10}/src/icplot/image_utils.py +19 -4
  16. icplot-0.0.10/src/icplot/py.typed +0 -0
  17. {icplot-0.0.8 → icplot-0.0.10/src/icplot.egg-info}/PKG-INFO +2 -2
  18. {icplot-0.0.8 → icplot-0.0.10}/src/icplot.egg-info/SOURCES.txt +8 -3
  19. {icplot-0.0.8 → icplot-0.0.10}/src/icplot.egg-info/requires.txt +1 -1
  20. icplot-0.0.10/test/test_cairo_interface.py +29 -0
  21. {icplot-0.0.8 → icplot-0.0.10}/test/test_gantt_chart.py +0 -5
  22. {icplot-0.0.8 → icplot-0.0.10}/test/test_line_plots.py +8 -2
  23. icplot-0.0.8/src/icplot/color.py +0 -13
  24. icplot-0.0.8/src/icplot/gantt.py +0 -64
  25. icplot-0.0.8/src/icplot/graph/__init__.py +0 -1
  26. icplot-0.0.8/src/icplot/graph/matplotlib_plot_backend.py +0 -69
  27. icplot-0.0.8/src/icplot/graph/plot.py +0 -163
  28. icplot-0.0.8/src/icplot/graph/series.py +0 -54
  29. icplot-0.0.8/src/icplot/project_elements.py +0 -4
  30. icplot-0.0.8/test/test_cairo_interface.py +0 -6
  31. {icplot-0.0.8 → icplot-0.0.10}/LICENSE +0 -0
  32. {icplot-0.0.8 → icplot-0.0.10}/README.md +0 -0
  33. {icplot-0.0.8 → icplot-0.0.10}/setup.cfg +0 -0
  34. {icplot-0.0.8 → icplot-0.0.10}/src/icplot/__init__.py +0 -0
  35. {icplot-0.0.8 → icplot-0.0.10}/src/icplot/geometry.py +0 -0
  36. {icplot-0.0.8 → icplot-0.0.10}/src/icplot/main_cli.py +0 -0
  37. {icplot-0.0.8 → icplot-0.0.10}/src/icplot/tex_interface.py +0 -0
  38. {icplot-0.0.8 → icplot-0.0.10}/src/icplot.egg-info/dependency_links.txt +0 -0
  39. {icplot-0.0.8 → icplot-0.0.10}/src/icplot.egg-info/entry_points.txt +0 -0
  40. {icplot-0.0.8 → icplot-0.0.10}/src/icplot.egg-info/top_level.txt +0 -0
  41. {icplot-0.0.8 → icplot-0.0.10}/test/test_image_utils.py +0 -0
  42. {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.8
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.9
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.8"
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.9"]
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, Color, TextPath
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, text)
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,2 @@
1
+ from .plot import * # NOQA
2
+ from .series import * # NOQA
@@ -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
- else:
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=300) as img:
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.8
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.9
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/project_elements.py
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/matplotlib_plot_backend.py
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
@@ -1,6 +1,6 @@
1
1
  wand
2
2
  matplotlib
3
- iccore==0.0.9
3
+ iccore==0.1.0
4
4
 
5
5
  [cairo]
6
6
  pycairo
@@ -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
+
@@ -24,8 +24,3 @@ def test_gantt_chart():
24
24
  gantt = GanttChart()
25
25
  gantt.milestones = [milestone0, milestone1]
26
26
 
27
- output_path = Path(os.getcwd()) / "gantt.svg"
28
-
29
- gantt.plot(output_path)
30
-
31
- output_path.unlink()
@@ -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", x_label="test_x", y_label="test_y", legend_label="test_legend"
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
- plot.plot(output_path)
24
+
25
+ plotter = MatplotlibPlotter()
26
+ plotter.plot(plot, output_path)
21
27
 
22
28
  assert output_path.exists()
23
29
  output_path.unlink()
@@ -1,13 +0,0 @@
1
- class Color:
2
-
3
- def __init__(self, r=1, g=1, b=1, a=1):
4
- self.r = r
5
- self.g = g
6
- self.b = b
7
- self.a = a
8
-
9
-
10
- class ColorMap:
11
-
12
- def __init__(self, label: str):
13
- self.label = label
@@ -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
@@ -1,4 +0,0 @@
1
- class GitlabUser:
2
-
3
- def __init__(self):
4
- self.name = ""
@@ -1,6 +0,0 @@
1
- from icplot.cairo_interface import CairoInterface
2
-
3
-
4
- def test_cairo_interface():
5
-
6
- cairo_interface = CairoInterface()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes