diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b01bdeb..e0282fa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ exclude: > )$ repos: - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.270 + rev: v0.0.272 hooks: - id: ruff args: @@ -40,6 +40,8 @@ repos: - id: "mypy" name: "Check type hints (mypy)" verbose: true + additional_dependencies: + - types-python-slugify ci: autofix_commit_msg: | [pre-commit.ci] auto fixes from pre-commit.com hooks diff --git a/CHANGELOG.md b/CHANGELOG.md index fe90843..c14c3ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.7.0] - ♻️ Testing suite refactor + model changes - 2023-06-09 + +Maintenance-only update, without new features. + +##### Changes + +- 💥 Remove plotting methods from `PsychroCurve`/`PsychroCurves` for a more functional approach, using methods in `plot_logic.py` +- 💥 Add `PsychroCurve.internal_value` field, to identify the trigger value for each curve (constant H/V/RH...), and evolve validation of data arrays +- ✅ tests: Increase test coverage and optimize tests for faster run (~2x) +- 📦️ env: Add slugify to deps and bump version + ## [0.6.0] - ✨ Chart config auto-refresh + bugfixes - 2023-06-07 Before, chart customize was done by creating a new `Psychrochart` object based on some modified chart configuration, diff --git a/poetry.lock b/poetry.lock index bf9a3d5..e5d8f05 100644 --- a/poetry.lock +++ b/poetry.lock @@ -729,6 +729,23 @@ files = [ [package.dependencies] six = ">=1.5" +[[package]] +name = "python-slugify" +version = "8.0.1" +description = "A Python slugify application that also handles Unicode" +optional = false +python-versions = ">=3.7" +files = [ + {file = "python-slugify-8.0.1.tar.gz", hash = "sha256:ce0d46ddb668b3be82f4ed5e503dbc33dd815d83e2eb6824211310d3fb172a27"}, + {file = "python_slugify-8.0.1-py2.py3-none-any.whl", hash = "sha256:70ca6ea68fe63ecc8fa4fcf00ae651fc8a5d02d93dcd12ae6d4fc7ca46c4d395"}, +] + +[package.dependencies] +text-unidecode = ">=1.3" + +[package.extras] +unidecode = ["Unidecode (>=1.1.1)"] + [[package]] name = "pyyaml" version = "6.0" @@ -843,6 +860,17 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "text-unidecode" +version = "1.3" +description = "The most basic Text::Unidecode port" +optional = false +python-versions = "*" +files = [ + {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, + {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, +] + [[package]] name = "tomli" version = "2.0.1" @@ -888,4 +916,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.12" -content-hash = "ea49069acdb0b9fb2c7ae1c59290d1fb860de9fafdd35f2c278ce79c56b1fa97" +content-hash = "f361d46b0132a81521ad766f92a8cf11d0da4f461b27be609d6ff230a6b4147f" diff --git a/psychrochart/chart.py b/psychrochart/chart.py index f53ceb5..648756e 100644 --- a/psychrochart/chart.py +++ b/psychrochart/chart.py @@ -2,7 +2,7 @@ import gc from io import StringIO from pathlib import Path -from typing import Any, Iterable, Mapping, Type +from typing import Any, Iterable, Type from matplotlib import figure from matplotlib.artist import Artist @@ -27,9 +27,11 @@ ) from psychrochart.models.styles import CurveStyle from psychrochart.plot_logic import ( + add_label_to_curve, apply_axis_styling, plot_annots_dbt_rh, plot_chart, + plot_curve, ) from psychrochart.process_logic import ( append_zones_to_chart, @@ -263,10 +265,12 @@ def plot_vertical_dry_bulb_temp_line( temp, self.pressure, style=style_curve, + type_curve="constant-dbt", reverse=reverse, ) - if curve.plot_curve(self.axes) and label is not None: - curve.add_label(self.axes, label, **label_params) + + if plot_curve(curve, self.axes) and label is not None: + add_label_to_curve(curve, self.axes, label, **label_params) def plot_legend( self, @@ -326,7 +330,7 @@ def save( self, path_dest: Any, canvas_cls: Type[FigureCanvasBase] | None = None, - **params: Mapping[str, Any], + **params, ) -> None: """Write the chart to disk.""" # ensure destination path if folder does not exist @@ -342,7 +346,7 @@ def save( canvas_use(self._fig).print_figure(path_dest, **params) gc.collect() - def make_svg(self, **params: Mapping[str, Any]) -> str: + def make_svg(self, **params) -> str: """Generate chart as SVG and return as text.""" svg_io = StringIO() self.save(svg_io, canvas_cls=FigureCanvasSVG, **params) diff --git a/psychrochart/chart_entities.py b/psychrochart/chart_entities.py new file mode 100644 index 0000000..ac58f23 --- /dev/null +++ b/psychrochart/chart_entities.py @@ -0,0 +1,6 @@ +from uuid import uuid4 + + +def random_internal_value() -> float: + """Generate random 'internal_value' for unnamed curves.""" + return float(int(f"0x{str(uuid4())[-4:]}", 16)) diff --git a/psychrochart/chartdata.py b/psychrochart/chartdata.py index 126e086..59a2af3 100644 --- a/psychrochart/chartdata.py +++ b/psychrochart/chartdata.py @@ -19,9 +19,10 @@ ) from scipy.interpolate import interp1d +from psychrochart.chart_entities import random_internal_value from psychrochart.models.annots import ChartZone from psychrochart.models.curves import PsychroCurve, PsychroCurves -from psychrochart.models.styles import CurveStyle, ZoneStyle +from psychrochart.models.styles import CurveStyle from psychrochart.util import solve_curves_with_iteration f_vec_hum_ratio_from_vap_press = np.vectorize(GetHumRatioFromVapPres) @@ -134,6 +135,7 @@ def make_constant_relative_humidity_lines( type_curve="constant_rh_data", label_loc=label_loc, label=f"RH {rh:g} %" if rh in rh_label_values else None, + internal_value=float(rh), ) for rh, curve_ct_rh in zip(rh_values, curves_ct_rh) ], @@ -162,6 +164,7 @@ def make_constant_dry_bulb_v_line( y_data=np.array(path_y), style=style, type_curve=type_curve, + internal_value=temp, ) @@ -181,6 +184,7 @@ def make_constant_dry_bulb_v_lines( y_data=np.array([w_humidity_ratio_min, w_max]), style=style, type_curve="constant_dry_temp_data", + internal_value=temp, ) for temp, w_max in zip(temps_vl, w_max_vec) ], @@ -207,6 +211,7 @@ def make_constant_humidity_ratio_h_lines( y_data=np.array([w, w]), style=style, type_curve="constant_humidity_data", + internal_value=w, ) for w, t_dp in zip(ws_hl, dew_points) ], @@ -231,6 +236,7 @@ def make_saturation_line( y_data=w_sat, style=style, type_curve="saturation", + internal_value=100.0, ) return PsychroCurves(curves=[sat_c]) @@ -308,6 +314,7 @@ def make_constant_enthalpy_lines( if round(h, 3) in h_label_values else None ), + internal_value=round(h, 3), ) for t_sat, w_sat, t_max, h in zip( t_sat_points, w_in_sat, temps_max_constant_h, h_objective @@ -386,6 +393,7 @@ def make_constant_specific_volume_lines( if round(vol, 3) in v_label_values else None ), + internal_value=round(vol, 3), ) for t_sat, w_sat, t_max, vol in zip( t_sat_points, w_in_sat, temps_max_constant_v, v_objective @@ -474,25 +482,40 @@ def make_constant_wet_bulb_temperature_lines( type_curve="constant_wbt_data", label_loc=label_loc, label=(_make_temp_label(wbt) if wbt in wbt_label_values else None), + internal_value=wbt, ) curves.append(c) return PsychroCurves(curves=curves, family_label=family_label) -def _make_zone_dbt_rh( - t_min: float, - t_max: float, - increment: float, - rh_min: float, - rh_max: float, - pressure: float, - style: ZoneStyle, - label: str | None = None, +def make_zone_curve( + zone_conf: ChartZone, increment: float, pressure: float ) -> PsychroCurve: - """Generate points for zone between constant dry bulb temps and RH.""" - temps = np.arange(t_min, t_max + increment, increment) + """Generate plot-points for zone.""" + # todo better id for overlay zones if no label + zone_value = random_internal_value() if zone_conf.label is None else None + if zone_conf.zone_type == "xy-points": + # expect points in plot coordinates! + return PsychroCurve( + x_data=np.array(zone_conf.points_x), + y_data=np.array(zone_conf.points_y), + style=zone_conf.style, + type_curve="xy-points", + label=zone_conf.label, + internal_value=zone_value, + ) + + assert zone_conf.zone_type == "dbt-rh" + # points for zone between constant dry bulb temps and RH + t_min = zone_conf.points_x[0] + t_max = zone_conf.points_x[-1] + rh_min = zone_conf.points_y[0] + rh_max = zone_conf.points_y[-1] assert rh_min >= 0.0 and rh_max <= 100.0 + assert t_min < t_max + + temps = np.arange(t_min, t_max + increment, increment) curve_rh_up = gen_points_in_constant_relative_humidity( temps, rh_max, pressure ) @@ -506,39 +529,8 @@ def _make_zone_dbt_rh( return PsychroCurve( x_data=np.array(temps_zone), y_data=np.array(abs_humid), - style=style, - type_curve="constant_rh_data", - label=label, + style=zone_conf.style, + type_curve="dbt-rh", + label=zone_conf.label, + internal_value=zone_value, ) - - -def make_zone_curve( - zone_conf: ChartZone, increment: float, pressure: float -) -> PsychroCurve: - """Generate points for zone between constant dry bulb temps and RH.""" - # TODO make conversion rh -> w and new zone_type: "dbt-rh-points" - assert isinstance(zone_conf.style, ZoneStyle) - if zone_conf.zone_type == "dbt-rh": - t_min = zone_conf.points_x[0] - t_max = zone_conf.points_x[-1] - rh_min = zone_conf.points_y[0] - rh_max = zone_conf.points_y[-1] - return _make_zone_dbt_rh( - t_min, - t_max, - increment, - rh_min, - rh_max, - pressure, - zone_conf.style, - label=zone_conf.label, - ) - else: - # zone_type: 'xy-points' - return PsychroCurve( - x_data=np.array(zone_conf.points_x), - y_data=np.array(zone_conf.points_y), - style=zone_conf.style, - type_curve="custom path", - label=zone_conf.label, - ) diff --git a/psychrochart/models/curves.py b/psychrochart/models/curves.py index 82f48dd..d1151a1 100644 --- a/psychrochart/models/curves.py +++ b/psychrochart/models/curves.py @@ -1,55 +1,10 @@ -import logging -from math import atan2, degrees -from typing import AbstractSet, Any, AnyStr, Mapping +from typing import AbstractSet, Any, Mapping -from matplotlib import patches -from matplotlib.axes import Axes -from matplotlib.path import Path import numpy as np from pydantic import BaseModel, Field, root_validator from psychrochart.models.styles import CurveStyle, ZoneStyle from psychrochart.models.validators import parse_curve_arrays -from psychrochart.util import mod_color - - -def _between_limits( - x_data: np.ndarray, - y_data: np.ndarray, - xmin: float, - xmax: float, - ymin: float, - ymax: float, -) -> bool: - data_xmin = min(x_data) - data_xmax = max(x_data) - data_ymin = min(y_data) - data_ymax = max(y_data) - if ( - (data_ymax < ymin) - or (data_xmax < xmin) - or (data_ymin > ymax) - or (data_xmin > xmax) - ): - return False - return True - - -def _annotate_label( - ax: Axes, - label: AnyStr, - text_x: float, - text_y: float, - rotation: float, - text_style: dict[str, Any], -) -> None: - if abs(rotation) > 0: - text_loc = np.array((text_x, text_y)) - text_style["rotation"] = ax.transData.transform_angles( - np.array((rotation,)), text_loc.reshape((1, 2)) - )[0] - text_style["rotation_mode"] = "anchor" - ax.annotate(label, (text_x, text_y), **text_style) class PsychroCurve(BaseModel): @@ -61,6 +16,7 @@ class PsychroCurve(BaseModel): type_curve: str | None = None label: str | None = None label_loc: float = 0.75 + internal_value: float | None = None class Config: arbitrary_types_allowed = True @@ -68,8 +24,23 @@ class Config: @root_validator(pre=True) def _parse_curve_data(cls, values): + if ( + values.get("label") is None + and values.get("internal_value") is None + ): + raise ValueError( + "PsychroCurve should have a 'label' or an 'internal_value'" + ) return parse_curve_arrays(values) + @property + def curve_id(self) -> str: + """Get Curve identifier (value or label).""" + if self.internal_value is not None: + return f"{self.internal_value:g}" + assert self.label is not None + return self.label + def dict( self, *, @@ -109,134 +80,6 @@ def __repr__(self) -> str: extra = f" (label: {self.label})" if self.label else "" return f"<{name} {len(self.x_data)} values{extra}>" - def plot_curve(self, ax: Axes) -> bool: - """Plot the curve, if it's between chart limits.""" - xmin, xmax = ax.get_xlim() - ymin, ymax = ax.get_ylim() - if ( - self.x_data is None - or self.y_data is None - or not _between_limits( - self.x_data, self.y_data, xmin, xmax, ymin, ymax - ) - ): - logging.info( - "%s (label:%s) not between limits ([%.2g, %.2g, %.2g, %.2g]) " - "-> x:%s, y:%s", - self.type_curve, - self.label or "unnamed", - xmin, - xmax, - ymin, - ymax, - self.x_data, - self.y_data, - ) - return False - - if isinstance(self.style, ZoneStyle): - assert len(self.y_data) > 2 - verts = list(zip(self.x_data, self.y_data)) - codes = ( - [Path.MOVETO] - + [Path.LINETO] * (len(self.y_data) - 2) - + [Path.CLOSEPOLY] - ) - path = Path(verts, codes) - patch = patches.PathPatch(path, **self.style.dict()) - ax.add_patch(patch) - - if self.label is not None: - bbox_p = path.get_extents() - text_x = 0.5 * (bbox_p.x0 + bbox_p.x1) - text_y = 0.5 * (bbox_p.y0 + bbox_p.y1) - style_params = { - "ha": "center", - "va": "center", - "backgroundcolor": [1, 1, 1, 0.4], - } - assert isinstance(self.style, ZoneStyle) - style_params["color"] = mod_color(self.style.edgecolor, -25) - _annotate_label( - ax, self.label, text_x, text_y, 0, style_params - ) - else: - ax.plot(self.x_data, self.y_data, **self.style.dict()) - if self.label is not None: - self.add_label(ax) - return True - - def add_label( - self, - ax: Axes, - text_label: str | None = None, - va: str | None = None, - ha: str | None = None, - loc: float | None = None, - **params, - ) -> Axes: - """Annotate the curve with its label.""" - num_samples = len(self.x_data) - assert num_samples > 1 - text_style = {"va": "bottom", "ha": "left", "color": [0.0, 0.0, 0.0]} - loc_f: float = self.label_loc if loc is None else loc - label: str = ( - (self.label if self.label is not None else "") - if text_label is None - else text_label - ) - - def _tilt_params(x_data, y_data, idx_0, idx_f): - delta_x = x_data[idx_f] - self.x_data[idx_0] - delta_y = y_data[idx_f] - self.y_data[idx_0] - rotation_deg = degrees(atan2(delta_y, delta_x)) - if delta_x == 0: - tilt_curve = 1e12 - else: - tilt_curve = delta_y / delta_x - return rotation_deg, tilt_curve - - if num_samples == 2: - xmin, xmax = ax.get_xlim() - rotation, tilt = _tilt_params(self.x_data, self.y_data, 0, 1) - if abs(rotation) == 90: - text_x = self.x_data[0] - text_y = self.y_data[0] + loc_f * ( - self.y_data[1] - self.y_data[0] - ) - elif loc_f == 1.0: - if self.x_data[1] > xmax: - text_x = xmax - text_y = self.y_data[0] + tilt * (xmax - self.x_data[0]) - else: - text_x, text_y = self.x_data[1], self.y_data[1] - label += " " - text_style["ha"] = "right" - else: - text_x = self.x_data[0] + loc_f * (xmax - xmin) - if text_x < xmin: - text_x = xmin + loc_f * (xmax - xmin) - text_y = self.y_data[0] + tilt * (text_x - self.x_data[0]) - else: - idx = min(num_samples - 2, int(num_samples * loc_f)) - rotation, tilt = _tilt_params( - self.x_data, self.y_data, idx, idx + 1 - ) - text_x, text_y = self.x_data[idx], self.y_data[idx] - text_style["ha"] = "center" - - text_style["color"] = mod_color(self.style.color, -25) - if ha is not None: - text_style["ha"] = ha - if va is not None: - text_style["va"] = va - if params: - text_style.update(params) - - _annotate_label(ax, label, text_x, text_y, rotation, text_style) - - return ax - class PsychroCurves(BaseModel): """Pydantic model to store a list of psychrometric curves for plotting.""" @@ -249,23 +92,6 @@ def __repr__(self) -> str: extra = f" (label: {self.family_label})" if self.family_label else "" return f"<{len(self.curves)} PsychroCurves{extra}>" - def plot(self, ax: Axes) -> Axes: - """Plot the family curves.""" - [curve.plot_curve(ax) for curve in self.curves] - - # Curves family labelling - if self.curves and self.family_label is not None: - ax.plot( - [-1], - [-1], - label=self.family_label, - marker="D", - markersize=10, - **self.curves[0].style.dict(), - ) - - return ax - class PsychroChartModel(BaseModel): """Pydantic model to store all psychrometric curves for PsychroChart.""" diff --git a/psychrochart/models/validators.py b/psychrochart/models/validators.py index c3c05e5..fc5fa7c 100644 --- a/psychrochart/models/validators.py +++ b/psychrochart/models/validators.py @@ -49,4 +49,7 @@ def parse_curve_arrays(values): values["x_data"] = np.array(values["x_data"]) if "y_data" in values and not isinstance(values["y_data"], np.ndarray): values["y_data"] = np.array(values["y_data"]) + shape = values["x_data"].shape[0], values["y_data"].shape[0] + if shape[0] == 0 or shape[1] == 0 or shape[0] != shape[1]: + raise ValueError(f"Invalid shape: {shape}") return values diff --git a/psychrochart/plot_logic.py b/psychrochart/plot_logic.py index eeb49f9..12611df 100644 --- a/psychrochart/plot_logic.py +++ b/psychrochart/plot_logic.py @@ -1,14 +1,205 @@ """A library to make psychrometric charts and overlay information in them.""" import logging +from math import atan2, degrees +from typing import Any, AnyStr +from matplotlib import patches from matplotlib.artist import Artist from matplotlib.axes import Axes +from matplotlib.path import Path +from matplotlib.text import Annotation import numpy as np from scipy.spatial import ConvexHull, QhullError from psychrochart.models.annots import ChartAnnots from psychrochart.models.config import ChartConfig -from psychrochart.models.curves import PsychroChartModel +from psychrochart.models.curves import ( + PsychroChartModel, + PsychroCurve, + PsychroCurves, +) +from psychrochart.models.styles import ZoneStyle +from psychrochart.util import mod_color + + +def _annotate_label( + ax: Axes, + label: AnyStr, + text_x: float, + text_y: float, + rotation: float, + text_style: dict[str, Any], +) -> Annotation: + if abs(rotation) > 0: + text_loc = np.array((text_x, text_y)) + text_style["rotation"] = ax.transData.transform_angles( + np.array((rotation,)), text_loc.reshape((1, 2)) + )[0] + text_style["rotation_mode"] = "anchor" + return ax.annotate(label, (text_x, text_y), **text_style) + + +def add_label_to_curve( + curve: PsychroCurve, + ax: Axes, + text_label: str | None = None, + va: str | None = None, + ha: str | None = None, + loc: float | None = None, + **params, +) -> Annotation: + """Annotate the curve with its label.""" + num_samples = len(curve.x_data) + assert num_samples > 1 + text_style = {"va": "bottom", "ha": "left", "color": [0.0, 0.0, 0.0]} + loc_f: float = curve.label_loc if loc is None else loc + label: str = ( + (curve.label if curve.label is not None else "") + if text_label is None + else text_label + ) + + def _tilt_params(x_data, y_data, idx_0, idx_f): + delta_x = x_data[idx_f] - curve.x_data[idx_0] + delta_y = y_data[idx_f] - curve.y_data[idx_0] + rotation_deg = degrees(atan2(delta_y, delta_x)) + if delta_x == 0: + tilt_curve = 1e12 + else: + tilt_curve = delta_y / delta_x + return rotation_deg, tilt_curve + + if num_samples == 2: + xmin, xmax = ax.get_xlim() + rotation, tilt = _tilt_params(curve.x_data, curve.y_data, 0, 1) + if abs(rotation) == 90: + text_x = curve.x_data[0] + text_y = curve.y_data[0] + loc_f * ( + curve.y_data[1] - curve.y_data[0] + ) + elif loc_f == 1.0: + if curve.x_data[1] > xmax: + text_x = xmax + text_y = curve.y_data[0] + tilt * (xmax - curve.x_data[0]) + else: + text_x, text_y = curve.x_data[1], curve.y_data[1] + label += " " + text_style["ha"] = "right" + else: + text_x = curve.x_data[0] + loc_f * (xmax - xmin) + if text_x < xmin: + text_x = xmin + loc_f * (xmax - xmin) + text_y = curve.y_data[0] + tilt * (text_x - curve.x_data[0]) + else: + idx = min(num_samples - 2, int(num_samples * loc_f)) + rotation, tilt = _tilt_params(curve.x_data, curve.y_data, idx, idx + 1) + text_x, text_y = curve.x_data[idx], curve.y_data[idx] + text_style["ha"] = "center" + + text_style["color"] = mod_color(curve.style.color, -25) + if ha is not None: + text_style["ha"] = ha + if va is not None: + text_style["va"] = va + if params: + text_style.update(params) + + return _annotate_label(ax, label, text_x, text_y, rotation, text_style) + + +def plot_curve( + curve: PsychroCurve, ax: Axes, label_prefix: str | None = None +) -> list[Artist]: + """Plot the curve, if it's between chart limits.""" + xmin, xmax = ax.get_xlim() + ymin, ymax = ax.get_ylim() + if ( + curve.x_data is None + or curve.y_data is None + or max(curve.y_data) < ymin + or max(curve.x_data) < xmin + or min(curve.y_data) > ymax + or min(curve.x_data) > xmax + ): + logging.info( + "%s (label:%s) not between limits ([%.2g, %.2g, %.2g, %.2g]) " + "-> x:%s, y:%s", + curve.type_curve, + curve.label or "unnamed", + xmin, + xmax, + ymin, + ymax, + curve.x_data, + curve.y_data, + ) + return [] + + artists = [] + if isinstance(curve.style, ZoneStyle): + assert len(curve.y_data) > 2 + verts = list(zip(curve.x_data, curve.y_data)) + codes = ( + [Path.MOVETO] + + [Path.LINETO] * (len(curve.y_data) - 2) + + [Path.CLOSEPOLY] + ) + path = Path(verts, codes) + patch = patches.PathPatch(path, **curve.style.dict()) + ax.add_patch(patch) + artists.append(patch) + + if curve.label is not None: + bbox_p = path.get_extents() + text_x = 0.5 * (bbox_p.x0 + bbox_p.x1) + text_y = 0.5 * (bbox_p.y0 + bbox_p.y1) + style_params = { + "ha": "center", + "va": "center", + "backgroundcolor": [1, 1, 1, 0.4], + } + assert isinstance(curve.style, ZoneStyle) + style_params["color"] = mod_color(curve.style.edgecolor, -25) + artist_label = _annotate_label( + ax, curve.label, text_x, text_y, 0, style_params + ) + artists.append(artist_label) + else: + artist_line = ax.plot(curve.x_data, curve.y_data, **curve.style.dict()) + artists.append(artist_line) + if curve.label is not None: + artists.append(add_label_to_curve(curve, ax)) + + return artists + + +def plot_curves_family(family: PsychroCurves | None, ax: Axes) -> list[Artist]: + """Plot all curves in the family.""" + artists: list[Artist] = [] + if family is None: + return artists + + [ + plot_curve(curve, ax, label_prefix=family.family_label) + for curve in family.curves + ] + # Curves family labelling + if family.curves and family.family_label is not None: + artist_fam_label = ax.plot( + [-1], + [-1], + label=family.family_label, + marker="D", + markersize=10, + **family.curves[0].style.dict(), + ) + artists.append(artist_fam_label) + + # return [ + # art for art in artist_curves + artist_labels if art is not None + # ] + # TODO collect artists from plot_curve + return [] def _apply_spines_style(axes, style, location="right") -> None: @@ -28,8 +219,7 @@ def apply_axis_styling(config: ChartConfig, ax: Axes) -> None: ax.yaxis.set_label_position("right") ax.set_xlim(config.dbt_min, config.dbt_max) ax.set_ylim(config.w_min, config.w_max) - ax.grid(False) - ax.grid(False, which="minor") + ax.grid(False, which="both") # Apply axis styles if config.figure.x_label is not None: style_axis = config.figure.x_axis_labels.dict() @@ -101,23 +291,17 @@ def apply_axis_styling(config: ChartConfig, ax: Axes) -> None: def plot_chart(chart: PsychroChartModel, ax: Axes) -> Axes: """Plot the psychrochart curves on given Axes.""" # Plot curves: - if chart.constant_dry_temp_data is not None: - chart.constant_dry_temp_data.plot(ax) - if chart.constant_humidity_data is not None: - chart.constant_humidity_data.plot(ax) - if chart.constant_h_data is not None: - chart.constant_h_data.plot(ax) - if chart.constant_v_data is not None: - chart.constant_v_data.plot(ax) - if chart.constant_rh_data is not None: - chart.constant_rh_data.plot(ax) - if chart.constant_wbt_data is not None: - chart.constant_wbt_data.plot(ax) - chart.saturation.plot(ax) + plot_curves_family(chart.constant_dry_temp_data, ax) + plot_curves_family(chart.constant_humidity_data, ax) + plot_curves_family(chart.constant_h_data, ax) + plot_curves_family(chart.constant_v_data, ax) + plot_curves_family(chart.constant_rh_data, ax) + plot_curves_family(chart.constant_wbt_data, ax) + plot_curves_family(chart.saturation, ax) # Plot zones: for zone in chart.zones: - zone.plot(ax=ax) + plot_curves_family(zone, ax) return ax @@ -140,14 +324,13 @@ def plot_annots_dbt_rh( **d_con.style.dict(), ) ) - # TODO document fix for issue #14: removing marker by default if d_con.outline_marker_width: _handlers_annotations.append( ax.plot( x_line, y_line, color=[*d_con.style.color[:3], 0.15], - lw=d_con.outline_marker_width, # lw=50, + lw=d_con.outline_marker_width, solid_capstyle="round", ) ) diff --git a/psychrochart/process_logic.py b/psychrochart/process_logic.py index bce2351..1da6f3e 100644 --- a/psychrochart/process_logic.py +++ b/psychrochart/process_logic.py @@ -56,18 +56,15 @@ def append_zones_to_chart( zones: ChartZones | dict[str, Any] | str | None = None, ) -> None: """Append zones as patches to the psychrometric chart data-container.""" - chart.zones.append( - PsychroCurves( + zones_use = obj_loader(ChartZones, zones, default_obj=DEFAULT_ZONES).zones + if zones_use: + curves = PsychroCurves( curves=[ - make_zone_curve( - zone_conf, config.limits.step_temp, chart.pressure - ) - for zone_conf in obj_loader( - ChartZones, zones, default_obj=DEFAULT_ZONES - ).zones + make_zone_curve(zone, config.limits.step_temp, chart.pressure) + for zone in zones_use ] ) - ) + chart.zones.append(curves) def _generate_chart_curves( diff --git a/pyproject.toml b/pyproject.toml index ea8e6c4..2aaf10c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ log_date_format = "%Y-%m-%d %H:%M:%S" [tool.poetry] name = "psychrochart" -version = "0.6.0" +version = "0.7.0" description = "A python 3 library to make psychrometric charts and overlay information on them" authors = ["Eugenio Panadero "] packages = [ @@ -74,6 +74,7 @@ matplotlib = ">=3.7" scipy = ">=1.10" psychrolib = ">=2.5" pydantic = ">=1.8" +python-slugify = ">=8.0.1" [tool.poetry.group.dev.dependencies] pre-commit = ">=2.10.0" diff --git a/tests/conftest.py b/tests/conftest.py index b411595..c56e075 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,11 +7,29 @@ from matplotlib import rcParams +from psychrochart import PsychroChart + _RG_SVGDATE = re.compile(r"(\s+?.*\s+?)") RSC_EXAMPLES = Path(__file__).parent / "example-charts" TEST_BASEDIR = Path(__file__).parent / "generated" +def store_test_chart( + chart: PsychroChart, + name_svg: str, + png: bool = False, + svg_rsc: bool = False, +) -> None: + """Helper method to store test charts.""" + p_svg = TEST_BASEDIR / name_svg + if png: + chart.save(p_svg.with_suffix(".png"), facecolor="none") + if svg_rsc: + chart.save(RSC_EXAMPLES / name_svg, metadata={"Date": None}) + else: + chart.save(p_svg, metadata={"Date": None}) + + def timeit(msg_log: str) -> Callable: """Wrap a method to print the execution time of a method call.""" diff --git a/tests/test_annotations.py b/tests/test_annotations.py index ece6394..3305bf8 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -1,5 +1,5 @@ from psychrochart import PsychroChart -from tests.conftest import TEST_BASEDIR +from tests.conftest import store_test_chart _TEST_POINTS = { "exterior": { @@ -121,6 +121,7 @@ def _make_base_chart(): chart = PsychroChart.create("minimal") + chart.config.figure.dpi = 72 chart.append_zones(_TEST_ZONES) chart.plot() @@ -156,31 +157,34 @@ def _store_chart( chart.plot_legend( markerscale=0.7, frameon=False, fontsize=10, labelspacing=1.2 ) - chart.save(TEST_BASEDIR / chart_name) + # store_test_chart(chart, chart_name, png=True) + store_test_chart(chart, chart_name) + # chart.save(TEST_BASEDIR / chart_name) + # store_test_chart(custom_2, "custom-chart-2.svg") def test_base_chart(): chart = _make_base_chart() - _store_chart(chart, "testchart_overlay_base.png", add_legend=False) + _store_chart(chart, "testchart_overlay_base.svg", add_legend=False) def test_overlay_single_points(): chart = _make_base_chart() chart.plot_points_dbt_rh(_TEST_POINTS) - _store_chart(chart, "testchart_plot_points.png") + _store_chart(chart, "testchart_plot_points.svg") chart = _make_base_chart() chart.plot_points_dbt_rh( _TEST_POINTS, scatter_style={"s": 300, "alpha": 0.7, "color": "darkorange"}, ) - _store_chart(chart, "testchart_scatter_points.png") + _store_chart(chart, "testchart_scatter_points.svg") def test_connected_points(): chart = _make_base_chart() chart.plot_points_dbt_rh(_TEST_POINTS, _TEST_CONNECTORS) - _store_chart(chart, "testchart_connected_points.png", add_legend=False) + _store_chart(chart, "testchart_connected_points.svg", add_legend=False) def test_marked_connected_points(): @@ -189,13 +193,13 @@ def test_marked_connected_points(): connectors[0]["outline_marker_width"] = 50 connectors[1]["outline_marker_width"] = 50 chart.plot_points_dbt_rh(_TEST_POINTS, connectors) - _store_chart(chart, "testchart_marked_connected_points.png") + _store_chart(chart, "testchart_marked_connected_points.svg") def test_connected_array(): chart = _make_base_chart() chart.plot_points_dbt_rh(_TEST_SERIES) - _store_chart(chart, "testchart_connected_array.png", add_legend=False) + _store_chart(chart, "testchart_connected_array.svg", add_legend=False) def test_convex_hull(): @@ -205,7 +209,7 @@ def test_convex_hull(): points=_TEST_POINTS | _TEST_SERIES, convex_groups=[_TEST_AREA], ) - _store_chart(chart, "testchart_convex_hull.png", add_legend=False) + _store_chart(chart, "testchart_convex_hull.svg", add_legend=False) def test_all_annots(): @@ -215,4 +219,4 @@ def test_all_annots(): connectors=[_TEST_CONNECTORS[0] | {"outline_marker_width": 25}], convex_groups=[_TEST_AREA], ) - _store_chart(chart, "testchart_all_annots.png") + _store_chart(chart, "testchart_all_annots.svg") diff --git a/tests/test_axes_limits.py b/tests/test_axes_limits.py new file mode 100644 index 0000000..6875813 --- /dev/null +++ b/tests/test_axes_limits.py @@ -0,0 +1,85 @@ +import logging + +from psychrochart import PsychroChart +from psychrochart.models.annots import DEFAULT_ZONES +from psychrochart.process_logic import set_unit_system +from tests.conftest import store_test_chart + + +def test_constant_wetbulb_temp_lines(caplog): + set_unit_system() + caplog.clear() + with caplog.at_level(logging.INFO): + chart = PsychroChart.create("minimal") + assert len(caplog.messages) == 0, caplog.messages + + chart.config.figure.figsize = (8, 6) + chart.config.limits.range_temp_c = (-10, 50) + chart.config.chart_params.with_constant_v = False + chart.config.chart_params.constant_rh_labels = [] + chart.config.chart_params.constant_h_step = 2 + chart.config.constant_h.linewidth = 0.5 + chart.config.constant_rh.linewidth = 1 + chart.config.constant_h.linestyle = ":" + chart.config.chart_params.constant_wet_temp_step = 1 + chart.config.chart_params.with_constant_humidity = True + chart.config.chart_params.constant_humid_step = 2 + chart.config.chart_params.range_wet_temp = [-25, 60] + chart.config.chart_params.constant_wet_temp_labels = list( + range(-25, 60) + ) + store_test_chart(chart, "chart-wbt-layout-normal.svg") + + # example of saturation crossing x-axis + chart.config.limits.range_humidity_g_kg = (10, 50) + store_test_chart(chart, "chart-wbt-layout-cut-xaxis.svg") + + # example of saturation crossing both axis + chart.config.limits.range_humidity_g_kg = (15, 120) + store_test_chart(chart, "chart-wbt-layout-cut-both.svg") + + assert len(caplog.messages) == 0, caplog.messages + + # check plot in limits with 2 zones outside visible limits + chart.close_fig() + chart.append_zones(DEFAULT_ZONES) + store_test_chart(chart, "chart-wbt-layout-cut-both-no-zones.svg") + + assert len(caplog.messages) == 2, caplog.messages + assert "not between limits" in caplog.messages[0] + assert "not between limits" in caplog.messages[1] + + +def test_wetbulb_temp_slope_change(caplog): + set_unit_system() + caplog.clear() + with caplog.at_level(logging.INFO): + chart = PsychroChart.create("minimal") + assert len(caplog.messages) == 0, caplog.messages + + # detail wbt discontinuity + chart.config.figure.figsize = (12, 12) + chart.config.saturation.linewidth = 1 + chart.config.constant_rh.linewidth = 1 + + chart.config.limits.range_temp_c = (-3, 12) + chart.config.limits.range_humidity_g_kg = (0, 6) + chart.config.chart_params.constant_rh_labels = [] + + chart.config.chart_params.with_constant_v = False + chart.config.chart_params.with_constant_h = False + chart.config.chart_params.with_constant_humidity = True + chart.config.chart_params.constant_humid_step = 2 + chart.config.chart_params.constant_temp_step = 1 + chart.config.chart_params.constant_temp_label_step = 1 + chart.config.chart_params.range_wet_temp = [-25, 60] + chart.config.chart_params.constant_wet_temp_labels = list( + range(-25, 60) + ) + chart.config.chart_params.constant_wet_temp_step = 0.25 + wbt_labels = [-8, -7, -6, -2, -1, -0.5, 0, 0.5, 1, 2, 6, 7, 8] + chart.config.chart_params.constant_wet_temp_labels = wbt_labels + chart.config.chart_params.constant_wet_temp_labels_loc = 0.01 + + store_test_chart(chart, "chart-wbt-discontinuity.svg") + assert len(caplog.messages) == 0, caplog.messages diff --git a/tests/test_curves_models.py b/tests/test_curves_models.py index 7b4ba03..0dda111 100644 --- a/tests/test_curves_models.py +++ b/tests/test_curves_models.py @@ -1,10 +1,53 @@ """Tests objects to handle psychrometric curves.""" import matplotlib.pyplot as plt import numpy as np +import pytest from psychrochart import PsychroChart from psychrochart.models.curves import PsychroCurve from psychrochart.models.styles import CurveStyle +from psychrochart.plot_logic import add_label_to_curve, plot_curve + + +def test_curve_validation(): + x_data = np.arange(0, 50, 1) + y_data = np.arange(0, 50, 1) + raw_style = {"color": "blue", "lw": 0.5} + + # pydantic validation for raw dicts + curve = PsychroCurve.validate( + {"x_data": x_data, "y_data": y_data, "style": raw_style, "label": "T1"} + ) + assert (curve.x_data == x_data).all() + assert curve.label == "T1" + assert isinstance(curve.style, CurveStyle) + assert isinstance(curve.style, CurveStyle) + assert curve.curve_id == "T1" + + # also for styling + style = CurveStyle.validate(raw_style) + assert style.color[2] == 1.0 + assert style.linewidth == 0.5 + + with pytest.raises(ValueError): + # curves need a label or an internal value + PsychroCurve(x_data=x_data, y_data=y_data, style=style) + + with pytest.raises(ValueError): + # data should have the same length + PsychroCurve(x_data=x_data[:-1], y_data=y_data, style=style, label="T") + + with pytest.raises(ValueError): + # and not be empty! + PsychroCurve( + x_data=np.array([]), y_data=np.array([]), style=style, label="T" + ) + + # no label (:=no presence on legend if enabled), but an internal value + curve = PsychroCurve( + x_data=x_data, y_data=y_data, style=style, internal_value=42 + ) + assert curve.curve_id == "42" def test_curve_serialization(): @@ -12,8 +55,9 @@ def test_curve_serialization(): x_data = np.arange(0, 50, 1) y_data = np.arange(0, 50, 1) style = CurveStyle(color="k", linewidth=0.5) - - curve = PsychroCurve(x_data=x_data, y_data=y_data, style=style) + curve = PsychroCurve( + x_data=x_data, y_data=y_data, style=style, internal_value=2 + ) # Dict export and import: d_curve = curve.dict() @@ -33,18 +77,20 @@ def test_plot_single_curves(): x_data = np.arange(0, 50, 1) y_data = np.arange(0, 50, 1) style = CurveStyle(color="k", linewidth=0.5) - curve = PsychroCurve(x_data=x_data, y_data=y_data, style=style) + curve = PsychroCurve(x_data=x_data, y_data=y_data, style=style, label="T1") # Plotting ax = plt.subplot() - curve.plot_curve(ax) + plot_curve(curve, ax) # Vertical line - vertical_curve = PsychroCurve(x_data=[25, 25], y_data=[2, 48], style=style) - vertical_curve.plot_curve(ax) + vertical_curve = PsychroCurve( + x_data=[25, 25], y_data=[2, 48], style=style, internal_value=25 + ) + plot_curve(vertical_curve, ax) # Add label - vertical_curve.add_label(ax, "TEST", va="baseline", ha="center") + add_label_to_curve(vertical_curve, ax, "TEST", va="baseline", ha="center") def test_string_representation_for_psychrochart_objs(): diff --git a/tests/test_generate_rsc.py b/tests/test_generate_rsc.py index ccc140c..46d1aea 100644 --- a/tests/test_generate_rsc.py +++ b/tests/test_generate_rsc.py @@ -1,7 +1,7 @@ import pytest from psychrochart import PsychroChart -from tests.conftest import RSC_EXAMPLES, TEST_BASEDIR +from tests.conftest import store_test_chart @pytest.mark.parametrize( @@ -21,10 +21,7 @@ def test_generate_rsc_default_charts( chart.plot_legend() # generate SVG as text, save PNG image - path_svg = RSC_EXAMPLES / svg_dest_file - path_png = (TEST_BASEDIR / svg_dest_file).with_suffix(".png") - path_svg.write_text(chart.make_svg(metadata={"Date": None})) - chart.save(path_png, facecolor="none") + store_test_chart(chart, svg_dest_file, svg_rsc=True) def test_generate_rsc_splash_chart(): @@ -147,10 +144,4 @@ def test_generate_rsc_splash_chart(): ) # Save to disk - path_svg = RSC_EXAMPLES / "chart_overlay_style_minimal.svg" - chart.save(path_svg, metadata={"Date": None}) - - # generate PNG variant - chart.save( - TEST_BASEDIR / path_svg.with_suffix(".png").name, facecolor="none" - ) + store_test_chart(chart, "chart_overlay_style_minimal.svg", svg_rsc=True) diff --git a/tests/test_overlay_info.py b/tests/test_overlay_info.py index 20b1cfe..47cf4bd 100644 --- a/tests/test_overlay_info.py +++ b/tests/test_overlay_info.py @@ -5,7 +5,7 @@ from psychrochart import PsychroChart from psychrochart.models.annots import ChartZone from psychrochart.models.parsers import load_config -from tests.conftest import TEST_BASEDIR +from tests.conftest import store_test_chart, TEST_BASEDIR def test_chart_overlay_points_and_zones(): @@ -72,8 +72,7 @@ def test_chart_overlay_points_and_zones(): chart.plot_legend(markerscale=1.0, fontsize=11, labelspacing=1.3) # Save to disk - path_svg = TEST_BASEDIR / "chart_overlay_test.svg" - chart.save(path_svg) + store_test_chart(chart, "chart_overlay_test.svg") def test_chart_overlay_minimal(): @@ -110,8 +109,7 @@ def test_chart_overlay_minimal(): chart.plot_points_dbt_rh(points, convex_groups=convex_groups) # Save to disk - path_svg = TEST_BASEDIR / "chart_overlay_minimal.svg" - chart.save(path_svg) + store_test_chart(chart, "chart_overlay_minimal.svg") def test_chart_overlay_arrows(): @@ -133,8 +131,7 @@ def test_chart_overlay_arrows(): chart.plot_legend(markerscale=1.0, fontsize=11, labelspacing=1.3) # Save to disk - path_svg = TEST_BASEDIR / "test_chart_overlay_arrows_1.svg" - chart.save(path_svg) + store_test_chart(chart, "test_chart_overlay_arrows_1.svg") chart.remove_annotations() points_arrows = { @@ -169,8 +166,7 @@ def test_chart_overlay_arrows(): chart.plot_arrows_dbt_rh(points_arrows) # Save to disk - path_svg = TEST_BASEDIR / "test_chart_overlay_arrows_2.svg" - chart.save(path_svg) + store_test_chart(chart, "test_chart_overlay_arrows_2.svg") def test_chart_overlay_convexhull(): @@ -215,46 +211,14 @@ def test_chart_overlay_convexhull(): chart.plot_legend(markerscale=1.0, fontsize=11, labelspacing=1.3) # Save to disk - path_svg = TEST_BASEDIR / "chart_overlay_test_convexhull.svg" - chart.save(path_svg) + store_test_chart(chart, "chart_overlay_test_convexhull.svg") -def test_overlay_a_lot_of_points_1(): - """Customize a chart with group of points.""" - # Load config & customize chart - chart = PsychroChart.create("minimal") - chart.config.limits.pressure_kpa = 90.5 - assert 90500.0 != chart.pressure - - # Plotting - chart.plot() - assert 90500.0 == chart.pressure - - # Create a lot of points - num_samples = 50000 - theta = np.linspace(0, 2 * np.pi, num_samples) - r = np.random.rand(num_samples) - x, y = 7 * r * np.cos(theta) + 25, 20 * r * np.sin(theta) + 50 - - points = {"test_series_1": (x, y)} - scatter_style = { - "s": 5, - "alpha": 0.1, - "color": "darkorange", - "marker": "+", - } - - chart.plot_points_dbt_rh(points, scatter_style=scatter_style) - - # Save to disk - path_png = TEST_BASEDIR / "chart_overlay_test_lot_of_points_1.png" - chart.save(path_png) - - -def test_overlay_a_lot_of_points_2(): +def test_overlay_a_lot_of_points(): """Customize a chart with two cloud of points.""" # Load config & customize chart chart = PsychroChart.create("minimal") + chart.config.figure.dpi = 100 chart.config.limits.pressure_kpa = 90.5 assert 90500.0 != chart.pressure @@ -288,11 +252,12 @@ def test_overlay_a_lot_of_points_2(): "style": scatter_style_1, "xy": (x, y), }, - "test_displaced": {"label": "Displaced", "xy": (x2, y2)}, + "test_displaced": (x2, y2), } chart.plot_points_dbt_rh(points, scatter_style=scatter_style_2) chart.plot_legend(markerscale=1.0, fontsize=11, labelspacing=1.3) # Save to disk - path_png = TEST_BASEDIR / "chart_overlay_test_lot_of_points.png" - chart.save(path_png) + chart.save(TEST_BASEDIR / "chart_overlay_test_lot_of_points.png") + # uncomment to generate a BIG SVG file + # store_test_chart(chart, "chart_overlay_test_lot_of_points.svg", png=True) diff --git a/tests/test_plot_chart.py b/tests/test_plot_chart.py index ec2b791..2ae58d1 100644 --- a/tests/test_plot_chart.py +++ b/tests/test_plot_chart.py @@ -2,7 +2,7 @@ import numpy as np from psychrochart import PsychroChart -from tests.conftest import TEST_BASEDIR +from tests.conftest import store_test_chart # fmt: off TEST_EXAMPLE_ZONES = [ @@ -101,10 +101,7 @@ def test_custom_style_psychrochart(): chart = PsychroChart.create(custom_style) chart.plot() chart.plot_legend() - - path_png = TEST_BASEDIR / "test_custom_psychrochart.png" - chart.save(path_png, facecolor="none") - chart.close_fig() + store_test_chart(chart, "test_custom_psychrochart.svg") def test_custom_style_psychrochart_2(): @@ -188,11 +185,7 @@ def test_custom_style_psychrochart_2(): }, } chart = PsychroChart.create(custom_style) - chart.plot() - - path_png = TEST_BASEDIR / "test_custom_psychrochart_2.png" - chart.save(path_png, facecolor="none") - chart.close_fig() + store_test_chart(chart, "test_custom_psychrochart_2.svg") for p in np.arange(90.0, 105.0): custom_style["limits"]["pressure_kpa"] = p @@ -281,11 +274,7 @@ def test_custom_style_psychrochart_3(): "zones": TEST_EXAMPLE_ZONES, } chart = PsychroChart.create(custom_style) - chart.plot() - - path_png = TEST_BASEDIR / "test_custom_psychrochart_3.png" - chart.save(path_png, facecolor="none") - chart.close_fig() + store_test_chart(chart, "test_custom_psychrochart_3.svg") for p in np.arange(90.0, 105.0): custom_style["limits"]["pressure_kpa"] = p @@ -294,11 +283,9 @@ def test_custom_style_psychrochart_3(): def test_default_styles_psychrochart(): """Test the plot custom styling with other preset styles.""" - path_svg = TEST_BASEDIR / "test_interior_psychrochart.svg" chart = PsychroChart.create("interior") chart.plot() chart.plot_legend( markerscale=0.7, frameon=False, fontsize=10, labelspacing=1.2 ) - chart.save(path_svg) - chart.close_fig() + store_test_chart(chart, "test_interior_psychrochart.svg") diff --git a/tests/test_profile_mem_reuse_chart.py b/tests/test_profile_mem_reuse_chart.py deleted file mode 100644 index 0712870..0000000 --- a/tests/test_profile_mem_reuse_chart.py +++ /dev/null @@ -1,185 +0,0 @@ -"""Tests plotting.""" - -from psychrochart import PsychroChart -from tests.conftest import TEST_BASEDIR, timeit - - -@timeit("make_chart") -def _make_chart(path_save=None): - chart = PsychroChart.create("minimal") - # Zones: - zones_conf = { - "zones": [ - { - "zone_type": "dbt-rh", - "style": { - "edgecolor": [1.0, 0.749, 0.0, 0.8], - "facecolor": [1.0, 0.749, 0.0, 0.2], - "linewidth": 2, - "linestyle": "--", - }, - "points_x": [23, 28], - "points_y": [40, 60], - "label": "Summer", - }, - { - "zone_type": "dbt-rh", - "style": { - "edgecolor": [0.498, 0.624, 0.8], - "facecolor": [0.498, 0.624, 1.0, 0.2], - "linewidth": 2, - "linestyle": "--", - }, - "points_x": [18, 23], - "points_y": [35, 55], - "label": "Winter", - }, - ] - } - chart.append_zones(zones_conf) - # Plotting - chart.plot() - # Vertical lines - t_min, t_opt, t_max = 16, 23, 30 - chart.plot_vertical_dry_bulb_temp_line( - t_min, - {"color": [0.0, 0.125, 0.376], "lw": 2, "ls": ":"}, - f" TOO COLD ({t_min}°C)", - ha="left", - loc=0.0, - fontsize=14, - ) - chart.plot_vertical_dry_bulb_temp_line( - t_opt, {"color": [0.475, 0.612, 0.075], "lw": 2, "ls": ":"} - ) - chart.plot_vertical_dry_bulb_temp_line( - t_max, - {"color": [1.0, 0.0, 0.247], "lw": 2, "ls": ":"}, - f"TOO HOT ({t_max}°C) ", - ha="right", - loc=1, - reverse=True, - fontsize=14, - ) - # Save to disk the base chart - if path_save is not None: - path_svg = TEST_BASEDIR / path_save - chart.save(path_svg) - return chart - - -@timeit("add_points") -def _add_points(chart, with_connectors=True, path_save=None): - if with_connectors: - # Append points and connectors - points = { - "exterior": { - "label": "Exterior", - "style": { - "color": [0.855, 0.004, 0.278, 0.8], - "marker": "X", - "markersize": 15, - }, - "xy": (31.06, 32.9), - }, - "exterior_estimated": { - "label": "Estimated (Weather service)", - "style": { - "color": [0.573, 0.106, 0.318, 0.5], - "marker": "x", - "markersize": 10, - }, - "xy": (36.7, 25.0), - }, - "interior": { - "label": "Interior", - "style": { - "color": [0.592, 0.745, 0.051, 0.9], - "marker": "o", - "markersize": 30, - }, - "xy": (29.42, 52.34), - }, - } - connectors = [ - { - "start": "exterior", - "end": "exterior_estimated", - "style": { - "color": [0.573, 0.106, 0.318, 0.7], - "linewidth": 2, - "linestyle": "-.", - }, - }, - { - "start": "exterior", - "end": "interior", - "style": { - "color": [0.855, 0.145, 0.114, 0.8], - "linewidth": 2, - "linestyle": ":", - }, - }, - ] - - convex_groups = [ - ( - ["exterior", "exterior_estimated", "interior"], - {"color": "darkgreen", "lw": 2, "alpha": 0.5, "ls": ":"}, - {"color": "darkgreen", "lw": 0, "alpha": 0.3}, - ), - ] - chart.plot_points_dbt_rh( - points, connectors, convex_groups=convex_groups - ) - else: - points = { - "exterior": (31.06, 32.9), - "exterior_estimated": (36.7, 25.0), - "interior": (29.42, 52.34), - } - chart.plot_points_dbt_rh(points) - # Save to disk - if path_save is not None: - path_svg = TEST_BASEDIR / path_save - chart.save(path_svg) - - -@timeit("MAKE_CHARTS") -def _make_charts(with_reuse=False): - name = "test_reuse_chart_" if with_reuse else "test_noreuse_chart_" - chart = _make_chart() - _add_points(chart, True, f"{name}_1.svg") - - if with_reuse: - chart.remove_annotations() - else: - chart = _make_chart() - _add_points(chart, False, f"{name}_2.svg") - - -def _draw_chart(path_img, chart=None): - if chart is None: - chart = _make_chart() - chart.plot_legend() - _add_points(chart, True, path_img) - chart.close_fig() - - -def test_reuse_psychrochart(): - """Customize a chart with some additions""" - _make_charts(with_reuse=True) - - -def test_no_reuse_psychrochart(): - """Customize a chart with some additions""" - _make_charts(with_reuse=False) - - -def test_redraw_psychrochart(): - """Test the workflow to redraw a chart.""" - counter = 0 - chart = _make_chart() - while counter < 10: - _draw_chart("test_redraw_chart_1.svg", chart) - counter += 1 diff --git a/tests/test_reuse_chart.py b/tests/test_reuse_chart.py index 92c591f..d4be4dd 100644 --- a/tests/test_reuse_chart.py +++ b/tests/test_reuse_chart.py @@ -164,4 +164,3 @@ def test_redraw_psychrochart(): chart.close_fig() _add_points(chart, False, "test_redraw_chart_2.svg") - chart.close_fig() diff --git a/tests/test_serialization.py b/tests/test_serialization.py index 5c82a85..2be0013 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -2,7 +2,7 @@ import pickle from psychrochart import load_config, PsychroChart -from tests.conftest import TEST_BASEDIR +from tests.conftest import store_test_chart, TEST_BASEDIR def test_0_serialize_psychrochart(): @@ -10,7 +10,6 @@ def test_0_serialize_psychrochart(): path_svg = TEST_BASEDIR / "test_to_pickle.svg" chart = PsychroChart.create() chart.save(path_svg) - chart.close_fig() (TEST_BASEDIR / "chart.pickle").write_bytes(pickle.dumps(chart)) (TEST_BASEDIR / "chart.json").write_text(chart.json(indent=2)) @@ -21,14 +20,12 @@ def test_1_unpickle_psychrochart(): chart = pickle.loads((TEST_BASEDIR / "chart.pickle").read_bytes()) path_svg = TEST_BASEDIR / "test_from_pickle.svg" chart.save(path_svg) - chart.close_fig() def test_1_load_json_psychrochart(): chart = PsychroChart.parse_file(TEST_BASEDIR / "chart.json") path_svg = TEST_BASEDIR / "test_from_json.svg" chart.save(path_svg) - chart.close_fig() def test_2_compare_psychrocharts(): @@ -41,15 +38,13 @@ def test_2_compare_psychrocharts(): def test_workflow_with_json_serializing(): # Get a preconfigured style model and customize it chart_config = load_config("interior") + chart_config.figure.dpi = 72 chart_config.limits.range_temp_c = (18.0, 32.0) chart_config.limits.range_humidity_g_kg = (1.0, 40.0) chart_config.limits.altitude_m = 3000 custom_chart = PsychroChart.create(chart_config) - custom_chart.save( - TEST_BASEDIR / "custom-chart-1.svg", metadata={"Date": None} - ) - custom_chart.save(TEST_BASEDIR / "custom-chart-1.png") + store_test_chart(custom_chart, "custom-chart-1.svg", png=True) # serialize the config for future uses assert chart_config.json() == custom_chart.config.json() @@ -61,13 +56,11 @@ def test_workflow_with_json_serializing(): custom_2 = PsychroChart.create( (TEST_BASEDIR / "custom-chart-config.json").as_posix() ) - custom_2.save(TEST_BASEDIR / "custom-chart-2.svg", metadata={"Date": None}) - custom_2.save(TEST_BASEDIR / "custom-chart-2.png") + store_test_chart(custom_2, "custom-chart-2.svg", png=True) # or reload chart from disk custom_3 = PsychroChart.parse_file(TEST_BASEDIR / "custom-chart.json") - custom_3.save(TEST_BASEDIR / "custom-chart-3.svg", metadata={"Date": None}) - custom_3.save(TEST_BASEDIR / "custom-chart-3.png") + store_test_chart(custom_3, "custom-chart-3.svg", png=True) # anyway it produces the same psychrochart svg1 = (TEST_BASEDIR / "custom-chart-1.svg").read_text()