diff --git a/.gitignore b/.gitignore index a69049f253a0485bf1830dab5848aaf02a3ba371..4a776706c427d9eb694a1bffe990f521cf40064d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ # Ignore all pycache folders everywhere __pycache__ +/tests/deserializer_matrix/new_deserializer2d.png +/tests/deserializer_matrix/new_deserializer3d.png + # Don't ignore the following files/folders in the root directory !/.gitlab !/.vscode diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000000000000000000000000000000000..b6c5d28bf40fda1cebef358daadc17f4ec868e95 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Current File", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal" + } + + ] +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ba01a5a57f09090a8035be4816357cea16088b3b..8ccba999bd62567b38762c9f3bd1d4e50518bd5e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -171,13 +171,13 @@ Preferrably write short functions, and [pure functions](https://realpython.com/p ### Running tests The tests compare JSON files created after a contribution to the ones before. Any differences will be marked as errors. -If a model change is part of the contribution the specification needs to be updated under [specification](./doc/static/specification). -To update the test JSON comparison files run: +If a model change or deserilizer change is part of the contribution many test files need to be updated. +This can be automatically done creating three kinds of files: ```cmd pytest tests --update-tests=confirm ``` -This creates the JSON files for the new model inside the tests_updated folder. -Manual testing for the new files is required as there is no true comparison to validate them. After that, one can replace the contents of [tests](./tests) with the ones of tests_updated. -It will also create an updated scheme inside [specification](./doc/static/specification). This will need to be renamed according to the current version of PlotSerializer to be pushed. \ No newline at end of file +The first type of files created being all the tests accomodating the new model under the tests_updated folder. They have to be manually checked for correctness and then moved into the initial [plots](./tests/plots) folder. +The second being the new model under [specification](./doc/static/specification) where it will have to be renamed according to the current version of PlotSerializer to be pushed. +The third being pictures of every plottype deserialized into a figure to broadly confirm the deserializer is still functioning. Once again the contents must me renamed and replace the old files in [folder](./tests/deserializer_matrix) upon confirming correctness. diff --git a/plot_serializer/matplotlib/deserializer.py b/plot_serializer/matplotlib/deserializer.py index ad9a23541dfc25a74f4092c1b48c70a2f92552da..21739d69c6556481667f332848c3e72cadd223cf 100644 --- a/plot_serializer/matplotlib/deserializer.py +++ b/plot_serializer/matplotlib/deserializer.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Optional import matplotlib.pyplot as plt import numpy as np @@ -21,22 +21,26 @@ from plot_serializer.model import ( ScatterTrace3D, SurfaceTrace3D, ) +from plot_serializer.proxy import Proxy -def deserialize_from_json_file(filename: str) -> MplFigure: +def deserialize_from_json_file(filename: str, fig: Any = None, ax: Any = None) -> Optional[MplFigure]: with open(filename, "r") as file: - return deserialize_from_json(file.read()) + return deserialize_from_json(file.read(), fig=fig, ax=ax) -def deserialize_from_json(json: str) -> MplFigure: +def deserialize_from_json(json: str, fig: Optional[MplFigure] = None, ax: Any = None) -> Optional[MplFigure]: model_figure = Figure.model_validate_json(json_data=json) - - if model_figure.plots[0].type == "3d": - fig, ax = plt.subplots(len(model_figure.plots), subplot_kw={"projection": "3d"}) - else: - fig, ax = plt.subplots(len(model_figure.plots)) - if model_figure.title is not None: - fig.suptitle(model_figure.title) + if isinstance(ax, Proxy): + ax = ax.delegate + if ax is None: + if model_figure.plots[0].type == "3d": + fig, ax = plt.subplots(len(model_figure.plots), subplot_kw={"projection": "3d"}) + else: + fig, ax = plt.subplots(len(model_figure.plots)) + if fig is not None: + if model_figure.title is not None: + fig.suptitle(model_figure.title) i = 0 for plot in model_figure.plots: @@ -307,13 +311,13 @@ def _deserialize_linetrace3d(trace: LineTrace3D, ax: MplAxes3D) -> None: def _deserialize_surfacetrace3d(trace: SurfaceTrace3D, ax: MplAxes3D) -> None: - x = np.zeros([trace._length, trace._width]) - y = np.zeros([trace._length, trace._width]) - z = np.zeros([trace._length, trace._width]) + x = np.zeros([trace.length, trace.width]) + y = np.zeros([trace.length, trace.width]) + z = np.zeros([trace.length, trace.width]) i = 0 j = 0 for point in trace.datapoints: - if j == trace._width: + if j == trace.width: j = 0 i = i + 1 x[i][j] = point.x @@ -337,10 +341,13 @@ def _deserialize_pieplot(plot: PiePlot, ax: MplAxes) -> None: # We need to ignore the argument types here, because matplotlib says # it doesn't support None inside of the lists, but it actually does. - ax.pie( - x, - labels=label, # type: ignore[arg-type] - colors=color, # type: ignore[arg-type] - explode=explode, # type: ignore[arg-type] - radius=plot.radius, # type: ignore[arg-type] - ) + if plot.radius is None: + plot.radius = 1 + else: + ax.pie( + x, + labels=label, # type: ignore[arg-type] + colors=color, # type: ignore[arg-type] + explode=explode, # type: ignore[arg-type] + radius=plot.radius, + ) diff --git a/plot_serializer/matplotlib/serializer.py b/plot_serializer/matplotlib/serializer.py index c68af47d624e8f09eb63e54e02fd1187ed528bc4..5af88b57c5701520972f0893e47c7c9cb7996c3a 100644 --- a/plot_serializer/matplotlib/serializer.py +++ b/plot_serializer/matplotlib/serializer.py @@ -101,14 +101,6 @@ PLOTTING_METHODS = [ ] -def is_array_like(x): - try: - np.array(x) - return True - except Exception: - return False - - def _convert_matplotlib_color( self, color_list: Any, length: int, cmap: Any, norm: Any ) -> Tuple[List[str] | None, bool]: @@ -274,6 +266,19 @@ class _AxesProxy(Proxy[MplAxes]): return result + """ + Custom wrapper for ax.plot with additional functionality. + + {plt.Axes.plot.__doc__} + + Parameters + ---------- + *args : tuple + Positional arguments passed to `ax.plot`. + **kwargs : dict + Keyword arguments passed to `ax.plot`. + """ + def plot(self, *args: Any, **kwargs: Any) -> list[Line2D]: try: mpl_lines = self.delegate.plot(*args, **kwargs) @@ -844,8 +849,8 @@ class _AxesProxy3D(Proxy[MplAxes3D]): traces.append( SurfaceTrace3D( type="surface3D", - _length=length, - _width=width, + length=length, + width=width, label=label, datapoints=datapoints, ) diff --git a/plot_serializer/model.py b/plot_serializer/model.py index a8c9e61dae45a4fe759df12795af49fa797d2459..fd6575d33cd765ef896dad37726dbc7262f40571 100644 --- a/plot_serializer/model.py +++ b/plot_serializer/model.py @@ -13,8 +13,7 @@ from pydantic import BaseModel, ConfigDict, Field, model_validator Scale = Union[ScaleBase, str] -MetadataValue = Union[int, float, str] -Metadata = Dict[str, MetadataValue] +Metadata = Dict[Any, Any] Color = Optional[str | Tuple[float, float, float] | Tuple[float, float, float, float]] @@ -254,15 +253,11 @@ class LineTrace3D(BaseModel): class SurfaceTrace3D(BaseModel): type: Literal["surface3D"] metadata: Metadata = {} - _length: int - _width: int + length: int + width: int label: Optional[str] = None datapoints: List[Point3D] - @model_validator(mode="after") - def check_dimension_matches_dataponts(self) -> "SurfaceTrace3D": - return self - def emit_warnings(self) -> None: msg: List[str] = [] @@ -272,6 +267,10 @@ class SurfaceTrace3D(BaseModel): if len(msg) > 0: logging.warning("%s is not set for SurfaceTrace3D.", msg) + @model_validator(mode="after") + def check_dimension_matches_dataponts(self) -> "SurfaceTrace3D": + return self + @model_validator(mode="before") def cast_numpy_types(cls: Any, values: Any) -> Any: # noqa: N805 # noqa: N805 def convert(value: Any) -> Any: diff --git a/requirements.txt b/requirements.txt index ae88b3ffa3b360c9073b55f7e6ffcca0d3e78d87..b5494a836a76085ae4e9a99742d57c33e08a6512 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,3 +22,6 @@ types-docutils # Unit testing pytest + +#Debug +debugpy \ No newline at end of file diff --git a/tests/deserializer_matrix/deserializer2d.png b/tests/deserializer_matrix/deserializer2d.png new file mode 100644 index 0000000000000000000000000000000000000000..c1024aa633a4436b8351635019fd93264692a3ef Binary files /dev/null and b/tests/deserializer_matrix/deserializer2d.png differ diff --git a/tests/deserializer_matrix/deserializer3d.png b/tests/deserializer_matrix/deserializer3d.png new file mode 100644 index 0000000000000000000000000000000000000000..e5e8873e6907646a32b5ce3f23186200fbd286e9 Binary files /dev/null and b/tests/deserializer_matrix/deserializer3d.png differ diff --git a/tests/plots/surface_plot3D_all_features.json b/tests/plots/surface_plot3D_all_features.json index ddf7aaa1e312e8f92a970517c516905f33bbf4dd..3f4f2ff2aa07ac76f21e73638063f3ab8a939a76 100644 --- a/tests/plots/surface_plot3D_all_features.json +++ b/tests/plots/surface_plot3D_all_features.json @@ -18,6 +18,8 @@ "traces": [ { "type": "surface3D", + "length": 20, + "width": 20, "label": "testSurface", "datapoints": [ { diff --git a/tests/plots/surface_plot3D_simple.json b/tests/plots/surface_plot3D_simple.json index 4437a6e02fd984d5721956887d5a88512778ad7a..960ad28dd4d5e6096bbfdc328b56335e67a9fb35 100644 --- a/tests/plots/surface_plot3D_simple.json +++ b/tests/plots/surface_plot3D_simple.json @@ -18,6 +18,8 @@ "traces": [ { "type": "surface3D", + "length": 8, + "width": 8, "label": "_child0", "datapoints": [ { diff --git a/tests/plots/surface_plot3D_test_metadata.json b/tests/plots/surface_plot3D_test_metadata.json index c6c0391896548772b0be23f7672e5497785df3c1..1e1fa1655c9a67a6a24ebd8a7052de97db7c5383 100644 --- a/tests/plots/surface_plot3D_test_metadata.json +++ b/tests/plots/surface_plot3D_test_metadata.json @@ -27,6 +27,8 @@ "traces": [ { "type": "surface3D", + "length": 8, + "width": 8, "label": "_child0", "datapoints": [ { @@ -362,6 +364,8 @@ "metadata": { "key": "value" }, + "length": 8, + "width": 8, "label": "_child1", "datapoints": [ { diff --git a/tests/test_deserializer.py b/tests/test_deserializer.py new file mode 100644 index 0000000000000000000000000000000000000000..3248c18d323601d8cbd02401003f8d4c5cfc4bfc --- /dev/null +++ b/tests/test_deserializer.py @@ -0,0 +1,58 @@ +from typing import Any + +from matplotlib import pyplot as plt + +from plot_serializer.matplotlib.deserializer import deserialize_from_json_file +from plot_serializer.matplotlib.serializer import MatplotlibSerializer + +files = [ + "./tests/plots/bar_plot_all_features.json", + "./tests/plots/box_plot_all_features.json", + "./tests/plots/errorbar_all_features.json", + "./tests/plots/hist_plot_all_features_datasets.json", + "./tests/plots/line_plot_all_features.json", + "./tests/plots/pie_plot_all_features.json", + "./tests/plots/scatter_plot_all_enabled.json", +] + +files3d = [ + "./tests/plots/line_plot3D_all_features.json", + "./tests/plots/scatter3D_plot_marker.json", + "./tests/plots/surface_plot3D_all_features.json", +] + + +def test_deserializer(request: Any) -> None: + rows, columns = 3, 3 + + update_tests = request.config.getoption("--update-tests") + + serializer = MatplotlibSerializer() + fig, ax = serializer.subplots(rows, columns, figsize=(15, 10)) + fig.suptitle("Amount of plots: " + str(len(files))) + + for i in range(len(files)): + deserialize_from_json_file(files[i], ax=ax[i // columns, i % columns]) + + if update_tests == "confirm": + fig.savefig("./tests/deserializer_matrix/new_deserializer2d.png") + + plt.close() + + +def test_deserializer3d(request: Any) -> None: + rows, columns = 2, 2 + + update_tests = request.config.getoption("--update-tests") + + serializer = MatplotlibSerializer() + fig, ax = serializer.subplots(rows, columns, figsize=(15, 10), subplot_kw={"projection": "3d"}) + fig.suptitle("Amount of plots: " + str(len(files3d))) + + for i in range(len(files3d)): + deserialize_from_json_file(files3d[i], ax=ax[i // columns, i % columns]) + + if update_tests == "confirm": + fig.savefig("./tests/deserializer_matrix/new_deserializer3d.png") + + plt.close()