ograph 1.0.0__py3-none-any.whl

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.
docs/source/conf.py ADDED
@@ -0,0 +1,78 @@
1
+ import sys
2
+ import os
3
+ from importlib.metadata import metadata
4
+ # https://www.sphinx-doc.org/en/master/usage/configuration.html
5
+
6
+ # -- Project information -----------------------------------------------------
7
+ # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
8
+
9
+ project = metadata('ograph')['Name']
10
+ copyright = f"2024-2025, {metadata('ograph')['Author-email']}"
11
+ description = f"{metadata('ograph')['Summary']}"
12
+
13
+ # -- General configuration ---------------------------------------------------
14
+ # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
15
+
16
+ # Add path to source directory
17
+ print(f"Append path: {os.path.abspath(os.path.join('..', '..'))}")
18
+
19
+ sys.path.append(os.path.abspath(os.path.join('..', '..')))
20
+
21
+
22
+ extensions = ['nbsphinx',
23
+ 'sphinx.ext.todo',
24
+ 'sphinx.ext.viewcode',
25
+ 'sphinx.ext.autodoc',
26
+ 'sphinx.ext.napoleon',
27
+ 'sphinx.ext.autosummary']
28
+
29
+ # -- Options for Typing ------------------------------------------------------
30
+
31
+ language = 'en-uk'
32
+
33
+ autoclass_content = 'class'
34
+ autosummary_generate = True
35
+
36
+ autodoc_default_options = {
37
+ 'undoc-members': True,
38
+ # Note: `autodoc_class_signature='separated'` causes `ClassDocumenter` to
39
+ # register both `__init__` and `__new__` as special members.
40
+ # This overrides the default behaviour of not documenting private
41
+ # members -- even if `__new__` is marked as private, Sphinx still
42
+ # documents it.
43
+ # Override the override with `'exclude-members'`, so that `__new__`
44
+ # is ABSOLUTELY not be documented ... until another patch breaks it.
45
+ 'exclude-members': '__new__',
46
+
47
+ }
48
+
49
+
50
+ autodoc_class_signature = 'separated'
51
+ autodoc_inherit_docstrings = False
52
+
53
+ autodoc_member_order = 'bysource'
54
+ autodoc_typehints = 'signature'
55
+ autodoc_typehints_description_target = 'all'
56
+
57
+ napoleon_use_rtype = False
58
+
59
+ # -- Options for HTML output -------------------------------------------------
60
+ # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
61
+
62
+ html_theme = 'sphinx_rtd_theme'
63
+ html_static_path = ['_static']
64
+ html_css_files = ['styles.css',]
65
+ templates_path = ['_templates']
66
+ exclude_patterns: list[str] = []
67
+
68
+ napoleon_include_special_with_doc = True
69
+
70
+ rst_prolog = """
71
+ .. role:: python(code)
72
+ :language: python
73
+ :class: highlight
74
+
75
+ .. role:: arg(code)
76
+ :class: highlight
77
+
78
+ """
ograph/__init__.py ADDED
File without changes
@@ -0,0 +1,3 @@
1
+ """Utilities for plotting clusters of points.
2
+ Initially designed to visualise swarm-based algorithms.
3
+ """
@@ -0,0 +1,131 @@
1
+ from typing import Optional, Callable
2
+ import numpy as np
3
+ from ..oplot import contour
4
+ from ..oplot import Vec2D, Vec3D
5
+ import matplotlib as mpl
6
+ import matplotlib.pyplot as plt
7
+
8
+
9
+ def plot_positions(data: Vec2D | Vec3D,
10
+ objective: Optional[Callable[[float, float], float]] = None,
11
+ *,
12
+ override_region: Optional[Vec2D] = None,
13
+ override_margin: Optional[float] = None):
14
+ """Plot a sequence of collections of points as they
15
+ change over time.
16
+
17
+ Args:
18
+ data: Either a 2-D tensor or a 3-D tensor.
19
+ * If :arg:`data` is 2-D: data[t] is a single point. Plot
20
+ its change over time.
21
+ * If :arg:`data` is 3-D: data[t] is a collection oof
22
+ points. Plot the change of these points over
23
+ time.
24
+
25
+ objective: An objective function to be plotted as background.
26
+
27
+ override_region: Only plot points that fall in this region.
28
+
29
+ override_margin: A small margin to add to override_region.
30
+ Might make the plot prettier.
31
+ """
32
+ mort = np.array(data)
33
+
34
+ margin: float = 1 if override_margin is None\
35
+ else override_margin
36
+
37
+ x_min: float
38
+ x_max: float
39
+ y_min: float
40
+ y_max: float
41
+
42
+ x_min = np.min(mort.T[0])
43
+ x_max = np.max(mort.T[0])
44
+ y_min = np.min(mort.T[1])
45
+ y_max = np.max(mort.T[1])
46
+
47
+ if (override_region is not None):
48
+ x_min = override_region[0][0]
49
+ x_max = override_region[0][1]
50
+ y_min = override_region[1][0]
51
+ y_max = override_region[1][1]
52
+
53
+ mort =\
54
+ np.apply_along_axis(func1d=lambda arr: [
55
+ arr[0] if x_min < arr[0] < x_max else np.nan,
56
+ arr[1] if y_min < arr[1] < y_max else np.nan],
57
+ axis=2 if len(mort.shape) == 3 else 1,
58
+ arr=mort)
59
+
60
+ # Adjust margins. Matplotlib does not plot ticks that intersect
61
+ # with borders of the graph; this trick makes plots prettier.
62
+ x_min -= margin
63
+ y_min -= margin
64
+ x_max += margin
65
+ y_max += margin
66
+
67
+ if objective is not None:
68
+ contour(fun=lambda x, y: objective(x, y),
69
+ x_range=(x_min, x_max),
70
+ y_range=(y_min, y_max)) # type: ignore
71
+
72
+ for i in range(len(mort)):
73
+ _xs: float
74
+ _ys: float
75
+ _xs, _ys = np.array(mort[i]).T
76
+ _intensity: float = i / len(mort)
77
+ plt.scatter(_xs, _ys, s=9,
78
+ color=mpl.colormaps['RdYlGn'](_intensity),
79
+ alpha=0.5) # type: ignore
80
+
81
+ plt.xlim((x_min + margin, x_max - margin))
82
+ plt.ylim((y_min + margin, y_max - margin))
83
+
84
+
85
+ def plot_bests(data: Vec2D,
86
+ best_selector: Callable[[Vec2D],
87
+ np.float64] = np.max,
88
+ best_label: str = "Best value",
89
+ all_label: str = "All values",) -> None:
90
+ """Plot a sequence of numbers against the best one.
91
+ The "best value" is plotted as a horizontal line.
92
+
93
+
94
+ Args:
95
+ data: A sequence of points to be plotted. Each
96
+ data[i] should be a 2-D point.
97
+
98
+ best_selector: A :class:`Callable` that returns the best
99
+ value in :arg:`data`. To plot a pre-determined, constant
100
+ best value, let :arg:`best_selector` always return that value.
101
+
102
+ best_label: Label for the best value.
103
+
104
+ all_label: Label for other values.
105
+ """
106
+ mort = data
107
+
108
+ plt.scatter(range(len(mort)),
109
+ mort,
110
+ s=12,
111
+ color="tab:blue",
112
+ facecolors="white",)
113
+
114
+ plt.vlines(x=range(len(mort)),
115
+ ymin=np.repeat(a=np.min(mort), repeats=len(mort)),
116
+ ymax=mort,
117
+ zorder=-9,
118
+ linewidth=0.8)
119
+
120
+ plt.hlines(best_selector(mort),
121
+ xmin=0,
122
+ xmax=len(mort),
123
+ color="tab:green",
124
+ label=best_label)
125
+
126
+ plt.plot([],
127
+ [],
128
+ color="tab:blue",
129
+ label=all_label)
130
+
131
+ plt.legend()
ograph/oconfig.py ADDED
@@ -0,0 +1,95 @@
1
+ """Utilities that configure the current plot
2
+ or all plots in the current session.
3
+ """
4
+
5
+ from typing import Optional
6
+
7
+ import matplotlib.pyplot as plt
8
+ import matplotlib as mlp
9
+ import matplotlib.pylab as pylab
10
+ from mpl_toolkits.mplot3d.axes3d import Axes3D # type: ignore[import-untyped]
11
+
12
+ from .ofig import ensure_axes_dimension
13
+
14
+
15
+ def clear_axis_labels(enable: bool = False) -> None:
16
+ plt.gca().get_yaxis().set_ticks([])
17
+ plt.gca().get_xaxis().set_ticks([])
18
+
19
+
20
+ def use_axis_lines(enable: bool = True) -> None:
21
+ plt.gca().axhline(y=0, color='DimGrey', linestyle="--")
22
+ plt.gca().axvline(x=0, color='DimGrey', linestyle="--")
23
+
24
+
25
+ def set_font_size(size: int) -> None:
26
+ """Set the font size to :arg:`size`.
27
+
28
+ Effect:
29
+ Configure the current plot.
30
+ """
31
+ mlp.rc("font", size=size)
32
+
33
+
34
+ def use_high_res() -> None:
35
+ """Set figures to render in higher resolution.
36
+
37
+ Set runtime configuration ``figure.dpi`` to 200.
38
+
39
+ Effect:
40
+ Configure all plots in the current session.
41
+ """
42
+ pylab.rcParams.update({'figure.dpi': 200})
43
+
44
+
45
+ def use_low_res() -> None:
46
+ """Set figures to render in the default resolution.
47
+
48
+ Set runtime configuration ``figure.dpi`` to 100, the default value.
49
+
50
+ Effect:
51
+ Configure all plots in the current session.
52
+ """
53
+ pylab.rcParams.update({'figure.dpi': 100})
54
+
55
+
56
+ def view_rotate(h_rotate: float, v_rotate: float) -> None:
57
+ """Rotate the current `Axes3D`.
58
+
59
+ @param h_rotate the degree to rotate vertically
60
+ @param v_rotate the degree to rotate horizontally
61
+ """
62
+ ax: Axes3D = plt.gca() # type: ignore[no-any-unimported]
63
+ ensure_axes_dimension(ax, 3)
64
+ if (isinstance(ax, Axes3D)): # Make mypy happy
65
+ ax.view_init(h_rotate, v_rotate)
66
+ else:
67
+ raise Exception("This should not happen."
68
+ "The exception has been checked.")
69
+
70
+
71
+ def view_axis_pos(pos: Optional[str]) -> None:
72
+ """Position labels and ticks of the current Axes3D.
73
+
74
+ Args:
75
+ pos: The position, one of :python:`'lower'`,
76
+ :python:`'upper'`,
77
+ :python:`'default'`,
78
+ :python:`'both'`,
79
+ :python:`'none'`,
80
+ and :python:`None`.
81
+ """
82
+ accepted_values: list[str] = ['lower', 'upper', 'default', 'both', 'none']
83
+ ax: Axes3D = plt.gca() # type: ignore[no-any-unimported]
84
+ ensure_axes_dimension(ax, 3)
85
+ match pos:
86
+ case None:
87
+ ax.axis('off')
88
+ case other:
89
+ if other in accepted_values:
90
+ for axis in ax.xaxis, ax.yaxis, ax.zaxis:
91
+ axis.set_label_position(other)
92
+ axis.set_ticks_position(other)
93
+ else:
94
+ raise ValueError(f"The input {other} is not one of"
95
+ f"{str(accepted_values)}.")
ograph/ofig.py ADDED
@@ -0,0 +1,180 @@
1
+ """Utilities that create and check figures
2
+ of different dimensions.
3
+ """
4
+
5
+ import matplotlib.pyplot as plt
6
+ from matplotlib.axes import Axes
7
+ from matplotlib.figure import Figure
8
+ import matplotlib.pylab as pylab
9
+ from mpl_toolkits.mplot3d.axes3d import Axes3D # type: ignore[import-untyped]
10
+ from typing import Tuple
11
+ from typing import Optional
12
+ from typing import Any
13
+ import logging
14
+
15
+
16
+ def fig2(xlims: Optional[Tuple[float, float]] = None,
17
+ ylims: Optional[Tuple[float, float]] = None,
18
+ arg: None | tuple[float, float, float, float] = None,
19
+ **kwargs: Any) -> Tuple[Figure, Axes]:
20
+ """Create then return a figure with a 2-dimensional axis.
21
+
22
+ Invoke :meth:`.pyplot.figure` to create figure, then
23
+ invoke :meth:`.pyplot.axes` to create an :class:`Axes`.
24
+ Return both objects in a tuple.
25
+
26
+ Args:
27
+ arg: :python:`None` or 4-tuple.
28
+
29
+ - :python:`None`: A new full window Axes is added using
30
+ ``subplot(**kwargs)``.
31
+
32
+ - 4-tuple of floats *rect* = ``(left, bottom, width, height)``:
33
+ Add a new Axes with dimensions *rect* in normalized
34
+ (0, 1) units, using :meth:`Figure.add_axes` on the current
35
+ figure.
36
+
37
+ xlims: Size of figure along the X axis
38
+ ylims: Size of figure along the Y axis
39
+ *args: Positional arguments to pass to :meth:`.pyplot.axes`
40
+ *kwargs: Keyword arguments to pass to :meth:`.pyplot.axes`
41
+ """
42
+ fig = plt.figure()
43
+ ax = plt.axes(arg, projection='rectilinear', **kwargs)
44
+ if (xlims is not None):
45
+ ax.set_xlim(*xlims)
46
+ if (ylims is not None):
47
+ ax.set_ylim(*ylims)
48
+ return (fig, ax)
49
+
50
+
51
+ def fig3(xlims: Optional[Tuple[float, float]] = None, # type: ignore[no-any-unimported] # noqa: E501
52
+ ylims: Optional[Tuple[float, float]] = None,
53
+ zlims: Optional[Tuple[float, float]] = None,
54
+ arg: None | tuple[float, float, float, float] = None,
55
+ **kwargs: Any) \
56
+ -> Tuple[Figure, Axes3D]:
57
+ """Create then return a figure with a 3-dimensional axis.
58
+
59
+ Invoke :meth:`.pyplot.figure` to create figure, then
60
+ invoke :meth:`.pyplot.axes` to create an :class:`Axes3D`.
61
+ Return both objects in a tuple.
62
+
63
+ Args:
64
+ arg: :python:`None` or 4-tuple.
65
+
66
+ - :python:`None`: A new full window Axes is added using
67
+ ``subplot(**kwargs)``.
68
+
69
+ - 4-tuple of floats *rect* = ``(left, bottom, width, height)``:
70
+ Add a new Axes with dimensions *rect* in normalized
71
+ (0, 1) units, using :meth:`Figure.add_axes` on the current
72
+ figure.
73
+
74
+ xlims: Size of figure along the X axis
75
+ ylims: Size of figure along the Y axis
76
+ ylims: Size of figure along the Y axis
77
+ *kwargs: Keyword arguments to pass to :meth:`.pyplot.axes`
78
+ """
79
+ fig = plt.figure()
80
+ ax: Axes3D = plt.axes(arg, # type: ignore[no-any-unimported]
81
+ projection='3d',
82
+ **kwargs)
83
+
84
+ if (xlims is not None):
85
+ ax.set_xlim(*xlims)
86
+ if (ylims is not None):
87
+ ax.set_ylim(*ylims)
88
+ if (zlims is not None):
89
+ ax.set_zlim(*zlims)
90
+ return (fig, ax)
91
+
92
+
93
+ class DimensionError(ValueError):
94
+ """Raised when the dimension of a figure does not agree
95
+ with the plot.
96
+
97
+ When this happens, something is going seriously wrong.
98
+ """
99
+ def __init__(self, expected: str, actual: str):
100
+ """
101
+ Args:
102
+ expected: Dimensions of the plot
103
+ actual: Dimensions of the figure
104
+ """
105
+ super().__init__(f"Dimension mismatch: expected {expected},"
106
+ f" got {actual}")
107
+
108
+
109
+ def ensure_axes_dimension(axes: Axes | Axes3D, # type: ignore[no-any-unimported] # noqa: E501
110
+ dim: int) -> None:
111
+ """Assert if the given axes is of the specified dimension.
112
+
113
+ Raises:
114
+ :class:`DimensionError`: if :arg:`dim` does not agree with the
115
+ dimension of :arg:`axes:.`
116
+ """
117
+ dimension_to_name = {2: "rectilinear", 3: "3d"}
118
+ if dim in dimension_to_name:
119
+ if (axes.name != dimension_to_name[dim]):
120
+ raise DimensionError(dimension_to_name[dim], axes.name)
121
+ else:
122
+ fig3() if dim == 3 else fig2()
123
+ logging.warning(f"The current projection is not `{dim}d`."
124
+ f"A new {"Axes" if dim == 2 else "Axes3D"}"
125
+ " is created instead.")
126
+
127
+
128
+ def annotate(title: str,
129
+ xlabel: str,
130
+ ylabel: str,
131
+ zlabel: Optional[str] = None) -> None:
132
+ """Annotate the current figure.
133
+
134
+ Set :arg:`title`, :arg:`xlabel`, and :arg:`ylabel` of the current axes.
135
+ Also set runtime configurations that style annotations.
136
+
137
+ To reset the parameters, call:
138
+ :python:`matplotlib.rcParams.update(matplotlib.rcParamsDefault)`
139
+
140
+ Args:
141
+ title: Title of the figure
142
+ xlabel: Labels for the x axis
143
+ ylabel: Labels for the y axis
144
+ zlabel: Labels for the z axis
145
+
146
+ Effect:
147
+ Plot to the current active figure.
148
+ Change runtime configurations.
149
+ """
150
+
151
+ axes_label_size: str = "x-large"
152
+ plot_title_size: str = "x-large"
153
+
154
+ font = {'legend.fontsize': 'x-large',
155
+ 'axes.titlesize': plot_title_size,
156
+ 'axes.labelsize': axes_label_size,
157
+ 'xtick.labelsize': axes_label_size,
158
+ 'ytick.labelsize': axes_label_size,
159
+ 'text.usetex': False,
160
+ 'font.family': 'Open Sans',
161
+ 'axes.titlepad': 15, }
162
+
163
+ ax: Axes | Axes3D = plt.gca() # type: ignore[no-any-unimported]
164
+
165
+ pylab.rcParams.update(font)
166
+ ax.set_xlabel(xlabel, fontname='PT Serif')
167
+ ax.set_ylabel(ylabel, fontname='PT Serif')
168
+ if (zlabel is not None):
169
+ ensure_axes_dimension(ax, 3)
170
+ if (isinstance(ax, Axes3D)):
171
+ ax.set_zlabel(zlabel, fontname='PT Serif')
172
+ else:
173
+ raise Exception("This should not happen.")
174
+
175
+ my_fig = ax.get_figure()
176
+
177
+ if (my_fig is not None):
178
+ my_fig.suptitle(title, fontname='PT Serif')
179
+ else:
180
+ raise Exception("Somehow the Axes is not attached to a Figure. How?")
ograph/ofunc.py ADDED
@@ -0,0 +1,48 @@
1
+ """Test functions and matrices. Examples are
2
+ the Himmelblau function and the unit cube.
3
+ """
4
+ from numpy import array
5
+
6
+
7
+ def rosenbrock(*args: float) -> float:
8
+ """Rosenbrock function.
9
+
10
+ Multi-dimensional test function where the global minimum is located
11
+ inside a flat valley.
12
+ """
13
+ result: float = 0
14
+ for i in range(len(args) - 1):
15
+ result += 100 * (args[i + 1] - args[i]**2)**2 + (1 - args[i])**2
16
+ return result
17
+
18
+
19
+ def himmelblau(x: float, y: float) -> float:
20
+ """Himmelblau function.
21
+
22
+ 2-dimensional test function with several minimum.
23
+ """
24
+ return (x ** 2 + y - 11) ** 2 + (x + y ** 2 - 7) ** 2
25
+
26
+
27
+ #! Immutable 2-dimensional unit square.
28
+ unit_square = array(
29
+ [[0, 0],
30
+ [0, 1],
31
+ [1, 0],
32
+ [1, 1]]
33
+ )
34
+ unit_square.flags.writeable = False
35
+
36
+
37
+ #! Immutable 3-dimensional unit cube. Fancy!
38
+ unit_cube = array(
39
+ [[0, 0, 0],
40
+ [1, 0, 0],
41
+ [0, 1, 0],
42
+ [0, 0, 1],
43
+ [1, 1, 0],
44
+ [0, 1, 1],
45
+ [1, 0, 1],
46
+ [1, 1, 1],]
47
+ )
48
+ unit_cube.flags.writeable = False
ograph/oplot.py ADDED
@@ -0,0 +1,563 @@
1
+ """Utilities that plot to the current Axes.
2
+ """
3
+ import matplotlib.pyplot as plt
4
+ import matplotlib.colors as colors
5
+ import numpy as np
6
+ import matplotlib as mpl
7
+ from typing import overload
8
+
9
+ from .ofig import ensure_axes_dimension
10
+
11
+ from matplotlib.typing import ColorType
12
+
13
+ from typing import Any, Self
14
+ import math
15
+ from numpy.typing import ArrayLike
16
+
17
+ from typing import Optional, Sequence, Callable
18
+
19
+ from scipy.spatial import ConvexHull, distance # type: ignore[import-untyped]
20
+
21
+ from matplotlib.patches import FancyArrowPatch
22
+
23
+ from mpl_toolkits.mplot3d.art3d import Poly3DCollection # type: ignore[import-untyped] # noqa: E501
24
+ from mpl_toolkits.mplot3d.axes3d import Axes3D # type: ignore[import-untyped]
25
+ from mpl_toolkits.mplot3d.art3d import Line3DCollection
26
+ from mpl_toolkits.mplot3d.proj3d import proj_transform # type: ignore[import-untyped] # noqa: E501
27
+
28
+ from numpy import ndarray
29
+
30
+ from matplotlib.patches import Patch
31
+ from matplotlib.text import Text
32
+ from matplotlib.legend import Legend
33
+
34
+ type Array1D = np.ndarray[tuple[Any],
35
+ np.dtype[np.float64]]
36
+ type Array2D = np.ndarray[tuple[Any, Any],
37
+ np.dtype[np.float64]]
38
+ type Array3D = np.ndarray[tuple[Any, Any, Any],
39
+ np.dtype[np.float64]]
40
+
41
+ type Sequence1D = Sequence[float]
42
+ type Sequence2D = Sequence[Sequence[float]]
43
+ type Sequence3D = Sequence[Sequence[Sequence[float]]]
44
+
45
+ type Vec1D = Array1D | Sequence1D
46
+ type Vec2D = Array2D | Sequence2D
47
+ type Vec3D = Array3D | Sequence3D
48
+
49
+ type Point2D = Vec1D
50
+ type Point3D = Vec1D
51
+
52
+ type Points2D = Vec2D
53
+ type Points3D = Vec2D
54
+
55
+ FILL_CONFIG: dict[str, Any] = {"alpha": 0.5}
56
+ EDGE_CONFIG: dict[str, Any] = {"color": "black", "alpha": 1}
57
+ NODE_CONFIG: dict[str, Any] = {"color": "black", "alpha": 0.5}
58
+ CONTOUR_CMAP: str = "viridis"
59
+
60
+
61
+ def heatmap(data: Vec2D,
62
+ xlabels: Optional[Sequence[str]] = None,
63
+ ylabels: Optional[Sequence[str]] = None) -> None:
64
+ """Plot a matrix to the current active axis as a heatmap.
65
+
66
+ Args:
67
+ data: A Numpy matrix.
68
+ xlabels: labels for cells along the x axis.
69
+ ylabels: labels for cells along the x axis.
70
+
71
+ Effects:
72
+ Plot at the current active axis.
73
+ """
74
+ if not isinstance(data, ndarray):
75
+ # If the input is not an numpy array, attempt to cast it into one.
76
+ data = np.array(data)
77
+
78
+ # Configure the size of the plot to accommodate the size of each cell
79
+ plt.rcParams["figure.figsize"] = [len(data) * math.sqrt(len(data)),
80
+ len(data[0]) * math.sqrt(len(data[0]))]
81
+
82
+ xlabels = xlabels if (xlabels is not None)\
83
+ else ["X" + str(i) for i, _ in enumerate(data[0])]
84
+ ylabels = ylabels if (ylabels is not None)\
85
+ else ["Y" + str(i) for i, _ in enumerate(data)]
86
+
87
+ ax = plt.gca()
88
+
89
+ ensure_axes_dimension(ax, 2)
90
+ ax.imshow(data, cmap="Greys")
91
+
92
+ ax.set_xticks(np.arange(len(xlabels)), labels=xlabels)
93
+ ax.set_yticks(np.arange(len(ylabels)), labels=ylabels)
94
+
95
+ plt.setp(ax.get_xticklabels(),
96
+ rotation=45,
97
+ ha="right",
98
+ rotation_mode="anchor")
99
+
100
+ max_cell_value = data.max()
101
+ min_cell_value = data.min()
102
+
103
+ for i in range(len(ylabels)):
104
+ for j in range(len(xlabels)):
105
+ cell_value_scale = max_cell_value - min_cell_value
106
+ ratio = (data[i, j] - min_cell_value) / cell_value_scale
107
+ ax.text(j, i, "{:.2f}".format(data[i, j]),
108
+ ha="center",
109
+ va="center",
110
+ color="w" if ratio > .6 else "k")
111
+
112
+ my_fig = ax.get_figure()
113
+ if (my_fig is not None):
114
+ my_fig.tight_layout() # type: ignore[union-attr]
115
+ # (When called on an `Axes`, ->get_figure(.) always returns a `Figure`)
116
+
117
+
118
+ def chull(shape: Points2D | Points3D) -> None:
119
+ """Plot a convex hull to the current active axes.
120
+
121
+ Detect the shape of points by inspecting the first in the sequence.
122
+
123
+ Args:
124
+ shape: A sequence of 2- or 3-dimensional points.
125
+
126
+ Effects:
127
+ Plot at the current active axis.
128
+ """
129
+ # The checker only checks if the first element has the correct dimension.
130
+ # This is bad, but performant.
131
+ match len(shape[0]):
132
+ case 2:
133
+ _chull_2d(np.array(shape)) # type: ignore[arg-type]
134
+ case 3:
135
+ _chull_3d(np.array(shape)) # type: ignore
136
+ case _:
137
+ raise ValueError("Input must be either 2 or 3")
138
+
139
+
140
+ def _chull_3d(shape: Array2D) -> None:
141
+ ax: Axes3D = plt.gca() # type: ignore[no-any-unimported]
142
+ ensure_axes_dimension(ax, 3)
143
+
144
+ hull = ConvexHull(shape)
145
+ for s in hull.simplices:
146
+ tri = Poly3DCollection([shape[s]])
147
+
148
+ if "alpha" in FILL_CONFIG:
149
+ tri.set_alpha(FILL_CONFIG["alpha"])
150
+ if "color" in FILL_CONFIG:
151
+ tri.set_color(FILL_CONFIG["color"])
152
+
153
+ tri.set_edgecolor('none')
154
+ ax.add_collection3d(tri)
155
+ edges = []
156
+ if distance.euclidean(shape[s[0]], shape[s[1]])\
157
+ < distance.euclidean(shape[s[1]], shape[s[2]]):
158
+ edges.append((s[0], s[1]))
159
+ if distance.euclidean(shape[s[1]], shape[s[2]])\
160
+ < distance.euclidean(shape[s[2]], shape[s[0]]):
161
+ edges.append((s[1], s[2]))
162
+ else:
163
+ edges.append((s[2], s[0]))
164
+ else:
165
+ edges.append((s[1], s[2]))
166
+ if distance.euclidean(shape[s[0]], shape[s[1]]) <\
167
+ distance.euclidean(shape[s[2]], shape[s[0]]):
168
+ edges.append((s[0], s[1]))
169
+ else:
170
+ edges.append((s[2], s[0]))
171
+ for v0, v1 in edges:
172
+ ax.plot(xs=shape[[v0, v1], 0],
173
+ ys=shape[[v0, v1], 1],
174
+ zs=shape[[v0, v1], 2],
175
+ **EDGE_CONFIG)
176
+
177
+ ax.scatter(shape[:, 0],
178
+ shape[:, 1],
179
+ shape[:, 2],
180
+ marker='o',
181
+ **NODE_CONFIG)
182
+
183
+
184
+ def _chull_2d(points: Array2D) -> None:
185
+ ax = plt.gca()
186
+ ensure_axes_dimension(ax, 2)
187
+ hull = ConvexHull(points)
188
+ ax.plot(points[:, 0], points[:, 1], 'o', **NODE_CONFIG) # type: ignore[arg-type] # noqa: E501
189
+ for simplex in hull.simplices:
190
+ ax.plot(points[simplex, 0],
191
+ points[simplex, 1],
192
+ **EDGE_CONFIG) # type: ignore[arg-type]
193
+
194
+ ax.fill(points[hull.vertices, 0],
195
+ points[hull.vertices, 1],
196
+ lw=2,
197
+ **FILL_CONFIG)
198
+
199
+
200
+ class BigArrow(FancyArrowPatch):
201
+ """An 2- or 3-dimensional arrow.
202
+
203
+ :meta private:
204
+ """
205
+ def __init__(self,
206
+ start: Vec1D,
207
+ end: Vec1D,
208
+ *args: Any,
209
+ **kwargs: Any):
210
+ default_styles = {
211
+ "mutation_scale": 30,
212
+ "arrowstyle": "-|>",
213
+ "linestyle": "--"
214
+ }
215
+ # start[i] for example can be either a number or a vector.
216
+ super().__init__((start[0], start[1]), # type: ignore[arg-type]
217
+ (end[0], end[1]),
218
+ *args,
219
+ **(kwargs | default_styles))
220
+ # Note that two copies of `start` and `end` are preserved:
221
+ # One copy is passed to ->_posA_posB of the parent class; this copy
222
+ # is used in `draw`.
223
+ # the other copy is passed to ->start and ->end of this class; this
224
+ # copt is used in do_3d_projections.
225
+ self.start = start
226
+ self.end = end
227
+
228
+ def draw(self: Self, renderer: Any) -> None:
229
+ super().draw(renderer)
230
+
231
+ def do_3d_projection(self: Self, renderer: Any = None) -> Any:
232
+ # The reference
233
+ # https://github.com/matplotlib/matplotlib/blob/v3.8.2/lib/
234
+ # mpl_toolkits/mplot3d/art3d.py#L998-L1065
235
+ # appears to return np.min(tzs).
236
+ # Removing it does not seem to change anything. Still, just to be safe.
237
+ if self.axes is None or not isinstance(self.axes, Axes3D):
238
+ raise Exception("Rendered without axes")
239
+ else:
240
+ txs, tys, tzs = proj_transform(*zip(self.start, self.end),
241
+ self.axes.M)
242
+ self.set_positions((txs[0], tys[0]), (txs[1], tys[1]))
243
+ return np.min(tzs)
244
+
245
+
246
+ @overload
247
+ def arrow(start: Point2D, end: Point2D,
248
+ *args: Any,
249
+ **kwargs: Any) -> None:
250
+ pass
251
+
252
+
253
+ # Suppress the error. This overload will never be matched
254
+ # since :class:`Point3D` and :class:`Point2D` alias
255
+ # the same thing. The overload is there for the user's knowledge.
256
+ @overload
257
+ def arrow(start: Point3D, # type: ignore[overload-cannot-match]
258
+ end: Point3D,
259
+ *args: Any,
260
+ **kwargs: Any) -> None:
261
+ pass
262
+
263
+
264
+ def arrow(start: Point2D | Point3D,
265
+ end: Point2D | Point3D,
266
+ *args: Any,
267
+ **kwargs: Any) -> None:
268
+ '''Plot an arrow to the current Axes or Axes3D.
269
+
270
+ Args:
271
+ start: The starting point of the arrow.
272
+ end: The ending point of the arrow.
273
+
274
+ '''
275
+ ax = plt.gca()
276
+ # Type checking `ax` is necessary, since the arrow
277
+ # class can handle the difference.
278
+ # Plotting to 2D (projection='rectilinear') Axes calls `draw`.
279
+ # Plotting to 3D (projection='3d') calls do_3d_projection.
280
+ # Still, this function might not work for other projections.
281
+ arrow = BigArrow(start, end, *args, **kwargs)
282
+ ax.add_artist(arrow)
283
+
284
+
285
+ def plot(fun: Callable[[float], float],
286
+ x_range: tuple[float, float],
287
+ density: int = 1000,
288
+ *args: ArrayLike,
289
+ **kwargs: Any) -> None:
290
+ '''Plot a contour map to the current Axes or Axes3D.
291
+
292
+ Args:
293
+ fun: The function to plot.
294
+ x_range: A tuple of the beginning and end of the x axis.
295
+ density: Number of data point to plot. These points are
296
+ evenly spaced over (:arg:`x_range` * :arg:`y_range`).
297
+ '''
298
+ ax = plt.gca()
299
+ x_max: float = max(x_range)
300
+ x_min: float = min(x_range)
301
+ xs, ys = _make_ys(fun=fun,
302
+ x_range=(x_min, x_max),
303
+ density=density)
304
+ ax.plot(xs, ys, *args, **kwargs)
305
+
306
+
307
+ def _make_zs(fun: Callable[[float, float], float],
308
+ x_range: tuple[float, float],
309
+ y_range: tuple[float, float],
310
+ density: int = 100,) -> tuple[ndarray, ndarray, ndarray]:
311
+
312
+ x_max: float = max(x_range)
313
+ x_min: float = min(x_range)
314
+ y_max: float = max(y_range)
315
+ y_min: float = min(y_range)
316
+
317
+ xs = np.arange(x_min, x_max, step=(x_max - x_min) / density)
318
+ ys = np.arange(y_min, y_max, step=(y_max - y_min) / density)
319
+
320
+ xs, ys = np.meshgrid(xs, ys)
321
+ zs = np.vectorize(fun)(xs, ys)
322
+
323
+ # This code is for functions that cannot be properly vectorised.
324
+ # zs = np.empty(shape=(len(ys), len(xs)))
325
+ # for i, x in enumerate(xs):
326
+ # for j, y in enumerate(ys):
327
+ # zs[j][i] = fun(x, y)
328
+
329
+ return (xs, ys, zs)
330
+
331
+
332
+ def contour(fun: Callable[[float, float], float],
333
+ x_range: tuple[float, float],
334
+ y_range: tuple[float, float],
335
+ density: int = 100,
336
+ levels: int = 50,
337
+ cmap: str = CONTOUR_CMAP,
338
+ colorbar: bool = True,
339
+ alpha: float = 0.5) -> None:
340
+ '''Plot a contour map to the current Axes3D.
341
+
342
+ Args:
343
+ fun: The function to plot.
344
+ x_range: A tuple of the beginning and end of the x axis.
345
+ y_range: A tuple of the beginning and end of the y axis.
346
+ density: Number of point to plot. These points are
347
+ evenly spaced over (:arg:`x_range` * :arg:`y_range`).
348
+ levels: The number of contour lines.
349
+ cmap: The colour map used by the contour map.
350
+ colorbar: If True, draw the colour bar.
351
+ '''
352
+ ax = plt.gca()
353
+ xs, ys, zs = _make_zs(fun, x_range, y_range, density)
354
+ cs = ax.contour(xs, ys, zs, levels=levels, cmap=cmap,
355
+ norm=colors.Normalize(vmin=zs.min(),
356
+ vmax=zs.max()),
357
+ alpha=alpha)
358
+
359
+ current_figure = ax.get_figure()
360
+ if colorbar and current_figure is not None:
361
+ current_figure.colorbar(cs)
362
+
363
+
364
+ def wireframe(fun: # type: ignore[no-any-unimported]
365
+ # Reason: I'm not sure which import is causing this.
366
+ Callable[[float, float], float],
367
+ x_range: tuple[float, float],
368
+ y_range: tuple[float, float],
369
+ density: int = 100,
370
+ cmap: str = CONTOUR_CMAP,
371
+ alpha: float = 0.9,
372
+ **kwargs: dict[str, Any]) -> Line3DCollection:
373
+ '''Plot a wireframe map to the current Axes or Axes3D.
374
+
375
+ Args:
376
+ fun: The function to plot.
377
+ x_range: A tuple of the beginning and end of the x axis.
378
+ y_range: A tuple of the beginning and end of the y axis.
379
+ density: Number of point to plot. These points are
380
+ evenly spaced over :arg:`x_range`.
381
+ cmap: The colour map used by the contour map.
382
+ alpha: Alpha value (transparency) of the frame.
383
+ '''
384
+ ax: Axes3D = plt.gca() # type: ignore[no-any-unimported]
385
+ xs, ys, zs = _make_zs(fun, x_range, y_range, density)
386
+ return ax.plot_wireframe(xs, ys, zs,
387
+ cmap=cmap,
388
+ norm=colors.Normalize(vmin=zs.min(),
389
+ vmax=zs.max()),
390
+ alpha=alpha,
391
+ **kwargs)
392
+
393
+
394
+ def surface(fun: Callable[[float, float], float], # type: ignore[no-any-unimported] # noqa: E501
395
+ x_range: tuple[float, float],
396
+ y_range: tuple[float, float],
397
+ density: int = 100,
398
+ cmap: str = CONTOUR_CMAP,
399
+ colorbar: bool = True,
400
+ alpha: float = 0.9,) -> Line3DCollection:
401
+ ax: Axes3D = plt.gca() # type: ignore[no-any-unimported]
402
+ xs, ys, zs = _make_zs(fun, x_range, y_range, density)
403
+ cs = ax.plot_surface(xs, ys, zs,
404
+ cmap=cmap,
405
+ norm=colors.Normalize(vmin=zs.min(), vmax=zs.max()),
406
+ alpha=alpha,
407
+ rstride=1,
408
+ cstride=1,
409
+ edgecolor='none')
410
+
411
+ if colorbar:
412
+ ax.get_figure().colorbar(cs)
413
+ return cs
414
+
415
+
416
+ def _make_ys(fun: Callable[[float], float],
417
+ x_range: tuple[float, float],
418
+ density: int = 20) -> tuple[Vec1D, Vec1D]:
419
+ x_max: float = max(x_range)
420
+ x_min: float = min(x_range)
421
+ xs = np.linspace(x_min, x_max, num=density, dtype=np.float64)
422
+ ys = np.array([fun(x) for x in xs])
423
+ return xs, ys
424
+
425
+
426
+ def stems(fun: Callable[[float], float],
427
+ x_range: tuple[float, float],
428
+ density: int = 30,
429
+ plot_links: bool = False,
430
+ *,
431
+ edge_args: dict[str, Any] = {},
432
+ node_args: dict[str, Any] = {},
433
+ fill_args: dict[str, Any] = {}) -> None:
434
+ """PLot a stem plot to the current Axes.
435
+
436
+ Args:
437
+ x_range: A tuple of the beginning and end of the x axis.
438
+ density: Number of point to plot. These points are
439
+ evenly spaced over :arg:`x_range`.
440
+ plot_links: If ``True``, then plot links.
441
+ """
442
+
443
+ xs, ys = _make_ys(fun, x_range, density)
444
+
445
+ stem(xs=xs,
446
+ ys=ys,
447
+ plot_links=plot_links,
448
+ edge_args=edge_args,
449
+ node_args=node_args,
450
+ fill_args=fill_args)
451
+
452
+
453
+ def stem(xs: Vec1D, ys: Vec1D,
454
+ plot_links: bool = False,
455
+ *,
456
+ edge_args: dict[str, Any] = {},
457
+ node_args: dict[str, Any] = {},
458
+ fill_args: dict[str, Any] = {}) -> None:
459
+ """Plot a stem plot to the current Axes.
460
+ Similar to :meth:`Axes.stem`, but offers more control over style.
461
+ """
462
+
463
+ edge_args = EDGE_CONFIG | edge_args
464
+ # The default node style now borrows from edge style.
465
+ node_args = node_args
466
+ fill_args = FILL_CONFIG | fill_args
467
+
468
+ ax = plt.gca()
469
+
470
+ # Incorporate alpha into edge color. Because `->scatter(.)` does not allow
471
+ # alpha to be specified for `edgecolors`, this is necessary.
472
+
473
+ # Suppressing this error because `.get` is maltyped to return
474
+ # (Any | None). This is not very helpful.
475
+ edge_color: Optional[ColorType]\
476
+ = edge_args.get("color") # type: ignore[assignment]
477
+ edge_alpha: Optional[float]\
478
+ = edge_args.get("alpha") # type: ignore[assignment]
479
+ if edge_color is not None and edge_alpha is not None:
480
+ line_color = mpl.colors.colorConverter.to_rgba(
481
+ edge_color,
482
+ edge_alpha
483
+ )
484
+
485
+ old_x = old_y = None
486
+
487
+ if (plot_links):
488
+ ax.plot((min(xs), max(xs)), (0, 0),
489
+ color=line_color,
490
+ linewidth=1,
491
+ zorder=-1)
492
+
493
+ for (x, y) in zip(xs, ys):
494
+ ax.plot([x, x], [0, y],
495
+ zorder=0,
496
+ linewidth=1,
497
+ **edge_args)
498
+
499
+ if (plot_links):
500
+ if old_x is not None and old_y is not None:
501
+ #Hull it
502
+ ax.plot((old_x, x), (old_y, y), color="#696969", linewidth=0.5,
503
+ zorder=-1)
504
+ ax.fill_between((old_x, x), (old_y, y),
505
+ zorder=-2,
506
+ color="#F5F5F5", **fill_args)
507
+ old_x = x
508
+ old_y = y
509
+
510
+ ax.scatter(xs, ys, zorder=4,
511
+ facecolor=fill_args.get("color", "white"),
512
+ edgecolors=line_color,
513
+ linewidth=1.5,
514
+ **node_args)
515
+
516
+
517
+ #: Legacy alias for :meth:`stem`.
518
+ shatter = stem
519
+
520
+
521
+ def _add_patch_to_current_legend(patch: Patch, label: str) -> None:
522
+ ax = plt.gca()
523
+ legend = [c for c in ax.get_children() if isinstance(c, Legend)][0]
524
+
525
+ handles = legend.legend_handles
526
+ labels = legend.texts
527
+
528
+ handles.append(patch)
529
+ labels.append(Text(0, 0, label))
530
+
531
+ plt.legend(handles)
532
+
533
+
534
+ def patch(facecolor: ColorType,
535
+ label: str,
536
+ alpha: float = 1,
537
+ width: float = 1,
538
+ height: float = 0.8) -> None:
539
+ """Add and label a rectangular colour patch to the current
540
+ plot.
541
+
542
+ Args:
543
+ label: Label to the patch.
544
+ alpha: Alpha value of the patch.
545
+ width: Width of the patch.
546
+ height: Height of the patch.
547
+ """
548
+ new_patch = Patch(facecolor=facecolor,
549
+ edgecolor=facecolor,
550
+ label=label,
551
+ alpha=alpha)
552
+
553
+ if plt.gca().get_legend() is None:
554
+ plt.gca().legend(handles=[new_patch],
555
+ handlelength=width,
556
+ handleheight=height,
557
+ loc="lower left")
558
+ else:
559
+ _add_patch_to_current_legend(Patch(facecolor=facecolor,
560
+ edgecolor=facecolor,
561
+ label=label,
562
+ alpha=alpha),
563
+ label=label)
ograph/py.typed ADDED
@@ -0,0 +1 @@
1
+ Whatever
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: ograph
3
+ Version: 1.0.0
4
+ Summary: Experimental Matplotlib API
5
+ Author-email: Lyodine <lyodine@github.com>
6
+ Project-URL: Documentation, https://yidingli.com/docs/ograph/
7
+ Project-URL: Repository, https://yidingli.com/projects/ograph/
8
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: Implementation :: CPython
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.12.0
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE.md
18
+ Requires-Dist: matplotlib>=3.8.4
19
+ Dynamic: license-file
20
+
21
+ <p align="center">
22
+ <img src="./media/alternative_logo.png" width=100>
23
+ </p>
24
+
25
+
26
+ # OGraph
27
+
28
+ A lightweight Matplotlib wrapper.
29
+
30
+ ## Installation
31
+
32
+ Install from PyPI:
33
+
34
+ ```shell
35
+ pip install ograph
36
+ ```
37
+
38
+ Install from source:
39
+
40
+ ```
41
+ pip install .
42
+ ```
43
+
44
+ Please see [documentation](https://yidingli.com/projects/ograph/docs/install-and-build.html) for detailed instructions.
45
+
46
+ ## Components
47
+
48
+ The library have the following modules:
49
+
50
+ | Component | Description |
51
+ | -------------------------------------------------- | -------------------------- |
52
+ | [ofig](./ograph/ofig/) | Create and check the dimensions of plots |
53
+ | [oplot](./ograph/oplot/) | Plot to the current axes |
54
+ | [oconfig](./ograph/oconfig/) | Configure plots |
55
+ | [ofunc](./ograph/ofunc/) | Supply test functions and matrices |
56
+ | [applications.swarm](./ograph/applications/swarm/) | Plot point clusters |
57
+
58
+ The ograph.oplot library contains the following plotters:
59
+
60
+ | Plotter | Dimension of Plot | Data |
61
+ | --------- | ----------------- | ------------------ |
62
+ | heatmap | 2 | $M\times N$ matrix |
63
+ | chull | 2, 3 | $R^2$ or $R^3$ |
64
+ | arrow | 2, 3 | $R^2$ or $R^3$ |
65
+ | plot | 2 | $R\rightarrow R$ |
66
+ | wireframe | 3 | $R^2\rightarrow R$ |
67
+ | surface | 3 | $R^2\rightarrow R$ |
68
+ | stems | 2 | $R\rightarrow R$ |
69
+ | stem | 2 | `xs` and `ys |
70
+ | patch | Any | Colours and labels |
71
+
72
+ The `application.swarm` module contains the following plotters:
73
+
74
+
75
+ plot_positions 2 list[R^2] or list[list[R^2]]
76
+ plot_bests 2 list[R^2]
@@ -0,0 +1,14 @@
1
+ docs/source/conf.py,sha256=_UfMQZptjyv84FrdYGo3yI6ED5DdtmKD6xnP9PG4CuQ,2473
2
+ ograph/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ ograph/oconfig.py,sha256=AegLHVKt9yOzTaSG-7qARmUinISJsLh3FjdUcxcF8ck,2883
4
+ ograph/ofig.py,sha256=fPw6fuWsY5rh5xnds0GP0jZrA199GASl1w5rHN3bisc,6325
5
+ ograph/ofunc.py,sha256=Y8yrnU8KSIe82vHRJZXnHuYig9n3lDuyOgcDBRjwcuc,1068
6
+ ograph/oplot.py,sha256=yGxyR2X9kZRRsXaHkpAyakvo-hEMZzcMu3f3Uu9gW1k,19435
7
+ ograph/py.typed,sha256=5JcTXlyUgcObw15iknvFO3ytTtMZPxgx5j7maXO5cLE,8
8
+ ograph/applications/__init__.py,sha256=TTgep3kTCzWGUQL1Bg2rprpyoP8tT7Ru5emWw5tgYUc,109
9
+ ograph/applications/swarm.py,sha256=R5UyhGPehEhCq3sdT99DTPhw4r1QI7dYtZT2TC6yW78,4165
10
+ ograph-1.0.0.dist-info/licenses/LICENSE.md,sha256=nQuuPcBjE5l5-ARuWaTQBbL0AIqq5-P_5myIcxqajfE,1086
11
+ ograph-1.0.0.dist-info/METADATA,sha256=0Rzj6DYTnMwZ7c4GEHoM59r_FYvKA7_aI34sJuM8Wjo,2590
12
+ ograph-1.0.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
13
+ ograph-1.0.0.dist-info/top_level.txt,sha256=cPMrKsahSsD8lnF-ud1xpxH8vbs22gfyrTWq6EH1fk0,12
14
+ ograph-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2025 Lyodine
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,2 @@
1
+ docs
2
+ ograph