Skip to content

Commit

Permalink
Merge pull request #36 from azogue/chore/fix-#28-dbt-wmax-zone
Browse files Browse the repository at this point in the history
✨ Add zone kind 'dbt-wmax' with vapour content limit + bugfixes
  • Loading branch information
azogue authored Jun 13, 2023
2 parents 78d66a3 + 954f29f commit da408b3
Show file tree
Hide file tree
Showing 10 changed files with 279 additions and 24 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ 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.9.1] - ✨ Add zone kind 'dbt-wmax' with vapour content limit - 2023-06-13

##### Changes

- ✨ Add new kind of overlay **zone 'dbt-wmax'**, to define chart areas delimited between db-temps and absolute humidity values, solving #28
- 🐛 Enable zones defined by 2 points (assume a rectangle defined by left-bottom/right-top coords)
- 🐛 Fix logic for plot regeneration, to plot again if config changes _AFTER_ plotting the chart
- 🐛 Fix ZoneStyle definition when linewidth is 0 and linestyle remains the default (passing inconsistent params to matplotlib)

## [0.9.0] - ✨ More kinds of chart zones + CSS for SVG styling - 2023-06-12

Define new enclosed areas in chart between constant RH lines and constant volume or enthalpy values,
Expand Down
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ pip install psychrochart

## Features

- **SI** units (with temperatures in celsius for better readability).
- Easy style customization based on [**pydantic**](https://docs.pydantic.dev/latest/) models and config presets for full customization of colors, line styles, line widths, etc..
- **SI** units (with temperatures in celsius for better readability), with _partial_ compatibility with IP system (imperial units)
- Easy style customization based on [**pydantic**](https://docs.pydantic.dev/latest/) models and config presets for full customization of **chart limits**, included lines and labels, colors, line styles, line widths, etc..
- Psychrometric charts within temperature and humidity ratio ranges, for any pressure\*, with:
- **Saturation line**
- **Constant RH lines**
Expand All @@ -41,10 +41,16 @@ pip install psychrochart
- **Constant specific volume lines**
- **Constant dry-bulb temperature lines** (internal orthogonal grid, vertical)
- **Constant humidity ratio lines** (internal orthogonal grid, horizontal)
- Plot legend for each family of lines
- Plot legend for each family of lines, labeled zones and annotations
- Specify labels for each family of lines
- **Overlay points, zones, convex hulls, and arrows**
- **Export SVG, PNG files**
- Overlay points, arrows, **data-series** (numpy arrays or pandas series), and convex hulls around points
- Define multiple kinds of **zones limited by psychrometric values**:
- 'dbt-rh' for areas between dry-bulb temperature and relative humidity values,
- 'enthalpy-rh' for areas between constant enthalpy and relative humidity values
- 'volume-rh' for areas between constant volume and relative humidity values
- 'dbt-wmax' for an area between dry-bulb temperature and water vapor content values (:= a rectangle cut by the saturation line),
- 'xy-points' to define arbitrary closed paths in plot coordinates (dbt, abs humidity)
- **Export as SVG, PNG files**, or generate dynamic SVGs with extra CSS and <defs> with `chart.make_svg(...)`

> NOTE: The ranges of temperature, humidity and pressure where this library should provide good results are within the normal environments for people to live in.
>
Expand Down
5 changes: 2 additions & 3 deletions psychrochart/chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,7 @@ def rendered(self) -> bool:
@property
def axes(self) -> Axes:
"""Return the Axes object plotting the chart if necessary."""
self.process_chart()
if not self.rendered:
if not self.rendered or self.config.has_changed:
self.plot()
assert isinstance(self._axes, Axes)
return self._axes
Expand Down Expand Up @@ -422,7 +421,7 @@ def make_svg(
svg_definitions: str | None = None,
**params,
) -> str:
"""Generate chart as SVG and return as text."""
"""Generate chart as SVG, with optional styling, and return as text."""
svg_io = StringIO()
self.save(svg_io, canvas_cls=FigureCanvasSVG, **params)
svg_io.seek(0)
Expand Down
129 changes: 127 additions & 2 deletions psychrochart/chartzones.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ def _make_zone_delimited_by_vertical_dbt_and_rh(
x_data=np.array(temps_zone),
y_data=np.array(abs_humid),
style=zone.style,
type_curve="dbt-rh",
type_curve=zone.zone_type,
label=zone.label,
internal_value=random_internal_value() if zone.label is None else None,
)
Expand Down Expand Up @@ -330,6 +330,119 @@ def _points_to_volume(dbt_values, w_values):
)


def _make_zone_delimited_by_dbt_and_wmax(
zone: ChartZone,
pressure: float,
*,
step_temp: float,
dbt_min: float,
dbt_max: float,
w_min: float,
w_max: float,
) -> PsychroCurve | None:
assert zone.zone_type == "dbt-wmax"
dbt_1, dbt_2 = zone.points_x
w_1, w_2 = zone.points_y

if dbt_1 > dbt_max or dbt_2 < dbt_min or w_1 > w_max or w_2 < w_min:
# zone outside limits
return None

w_1 = max(w_1, w_min)
w_2 = min(w_2, w_max)
dbt_1 = max(dbt_1, dbt_min)
dbt_2 = min(dbt_2, dbt_max)

saturation = make_saturation_line(dbt_1, dbt_2, step_temp, pressure)
if saturation.outside_limits(dbt_min, dbt_max, w_min, w_max):
# just make a rectangle
return PsychroCurve(
x_data=np.array([dbt_1, dbt_2]),
y_data=np.array([w_1, w_2]),
style=zone.style,
type_curve=zone.zone_type,
label=zone.label,
internal_value=w_2,
)

# build path clockwise starting in left bottom corner
path_x, path_y = [], []
if saturation.y_data[0] < w_1: # saturation cuts lower w value
idx_start = (saturation.y_data > w_1).argmax()
t_start, t_end = (
saturation.x_data[idx_start - 1],
saturation.x_data[idx_start],
)
w_start, w_end = (
saturation.y_data[idx_start - 1],
saturation.y_data[idx_start],
)
t_cut1, _w_cut1 = _crossing_point_between_rect_lines(
segment_1_x=(dbt_1, dbt_2),
segment_1_y=(w_1, w_1),
segment_2_x=(t_start, t_end),
segment_2_y=(w_start, w_end),
)
path_x.append(t_cut1)
path_y.append(w_1)
else: # saturation cuts left y-axis
idx_start = 0
t_cut1, w_cut1 = saturation.x_data[0], saturation.y_data[0]

path_x.append(dbt_1)
path_y.append(w_1)
path_x.append(t_cut1)
path_y.append(w_cut1)

if saturation.y_data[-1] < w_2: # saturation cuts right dbt_2
path_x += saturation.x_data[idx_start:].tolist()
path_y += saturation.y_data[idx_start:].tolist()

t_cut2, w_cut2 = saturation.x_data[-1], saturation.y_data[-1]
path_x.append(t_cut2)
path_y.append(w_cut2)
else: # saturation cuts top w_2
idx_end = (saturation.y_data < w_2).argmin()
path_x += saturation.x_data[idx_start:idx_end].tolist()
path_y += saturation.y_data[idx_start:idx_end].tolist()

t_start, t_end = (
saturation.x_data[idx_end - 1],
saturation.x_data[idx_end],
)
w_start, w_end = (
saturation.y_data[idx_end - 1],
saturation.y_data[idx_end],
)
t_cut2, _w_cut2 = _crossing_point_between_rect_lines(
segment_1_x=(dbt_1, dbt_2),
segment_1_y=(w_2, w_2),
segment_2_x=(t_start, t_end),
segment_2_y=(w_start, w_end),
)
path_x.append(t_cut2)
path_y.append(w_2)

path_x.append(dbt_2)
path_y.append(w_2)

path_x.append(dbt_2)
path_y.append(w_1)

# repeat 1st point to close path
path_x.append(path_x[0])
path_y.append(path_y[0])

return PsychroCurve(
x_data=np.array(path_x),
y_data=np.array(path_y),
style=zone.style,
type_curve=zone.zone_type,
label=zone.label,
internal_value=w_2,
)


def make_zone_curve(
zone_conf: ChartZone,
*,
Expand Down Expand Up @@ -370,14 +483,26 @@ def make_zone_curve(
w_max=w_max,
)

if zone_conf.zone_type == "dbt-wmax":
# points for zone between abs humid and dbt ranges
return _make_zone_delimited_by_dbt_and_wmax(
zone_conf,
pressure,
step_temp=step_temp,
dbt_min=dbt_min,
dbt_max=dbt_max,
w_min=w_min,
w_max=w_max,
)

# expect points in plot coordinates!
assert zone_conf.zone_type == "xy-points"
zone_value = random_internal_value() if zone_conf.label is None else None
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",
type_curve=zone_conf.zone_type,
label=zone_conf.label,
internal_value=zone_value,
)
Expand Down
1 change: 0 additions & 1 deletion psychrochart/models/annots.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ class ChartPoint(BaseModel):
class ChartSeries(BaseModel):
"""Input model for data-series point array annotation."""

# TODO fusion with PsychroCurve, + pandas ready
x_data: np.ndarray
y_data: np.ndarray
style: dict[str, Any] = Field(default_factory=dict)
Expand Down
4 changes: 3 additions & 1 deletion psychrochart/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@
color=[0.0, 0.125, 0.376], linewidth=0.75, linestyle=":"
)

ZoneKind = Literal["dbt-rh", "xy-points", "enthalpy-rh", "volume-rh"]
ZoneKind = Literal[
"dbt-rh", "xy-points", "enthalpy-rh", "volume-rh", "dbt-wmax"
]


class ChartFigure(BaseConfig):
Expand Down
5 changes: 4 additions & 1 deletion psychrochart/models/styles.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,8 @@ def _color_arr(cls, v, values):
return parse_color(v)

@root_validator(pre=True)
def _remove_aliases(cls, values):
def _remove_aliases_and_fix_defaults(cls, values):
if values.get("linewidth", 2) == 0:
# avoid matplotlib error with inconsistent line parameters
values["linestyle"] = "-"
return reduce_field_abrs(values)
29 changes: 19 additions & 10 deletions psychrochart/plot_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,15 +135,25 @@ def plot_curve(
return {}

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())
if len(curve.y_data) == 2: # draw a rectangle!
patch = patches.Rectangle(
(curve.x_data[0], curve.y_data[0]),
width=curve.x_data[1] - curve.x_data[0],
height=curve.y_data[1] - curve.y_data[0],
**curve.style.dict(),
)
bbox_p = patch.get_extents()
else:
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())
bbox_p = path.get_extents()
ax.add_patch(patch)
gid_zone = make_item_gid(
"zone",
Expand All @@ -156,7 +166,6 @@ def plot_curve(
artists,
)
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 = {
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ log_date_format = "%Y-%m-%d %H:%M:%S"

[tool.poetry]
name = "psychrochart"
version = "0.9.0"
version = "0.9.1"
description = "A python 3 library to make psychrometric charts and overlay information on them"
authors = ["Eugenio Panadero <[email protected]>"]
packages = [
Expand Down
Loading

0 comments on commit da408b3

Please sign in to comment.