Skip to content

Refactor plotting code and update test logic#261

Open
yuli0346 wants to merge 6 commits intoCenterForTheBuiltEnvironment:plots/first_attemptfrom
yuli0346:plots/first_attempt
Open

Refactor plotting code and update test logic#261
yuli0346 wants to merge 6 commits intoCenterForTheBuiltEnvironment:plots/first_attemptfrom
yuli0346:plots/first_attempt

Conversation

@yuli0346
Copy link

Seaborn Testing Framework Overview

1. Framework

Seaborn uses pytest as its primary testing framework.

2. Structure

All test files are organized under the tests/ directory, with submodules corresponding to each functional module (such as test_algorithms.py, test_categorical.py, and tests in the _stats subpackage).

3. Shared Fixtures and Utilities

Common fixtures, setup functions, and helper tools are defined in tests/conftest.py for sharing across multiple test modules.

4. Test Content: Logical Functions and Plotting Functions

Seaborn's tests can be divided into two main categories:

a) Logical / Statistical / Data Transformation Functions

  • These are the parts of the code that perform calculations, data aggregation, or transformations. For this kind of function, Seaborn’s tests follow a simple and transparent process: give a fixed input → run the function → check if the output matches the expected result.
  • To make these checks precise, Seaborn uses NumPy’s testing tools (such as ‘assert_array_equal’ or ‘assert_array_almost_equal’) to compare arrays or numeric outputs.
  • If a function contains randomness, the test will use a fixed random seed (e.g., by setting it uniformly in a fixture) to ensure consistent output across runs.

b) Plotting / Visualization Functions

  • For plotting output, instead of saving the image and comparing it pixel by pixel, Seaborn tests the structure and properties of the plot.
  • After calling a plotting function, the test looks directly at the Matplotlib objects that represent the figure — such as the Axes, lines, patches, legends, and color collections. It then checks that these objects have the expected number, labels, colors, and layout.

5. Parameterized Testing

In some modules, Seaborn uses @pytest.mark.parametrize to efficiently cover multiple input combinations and avoid redundant test code.


Implementation of Seaborn-Style Testing Structure

This PR introduces a Seaborn-inspired testing framework for the pythermalcomfort visualization and computation modules. It aligns the project’s testing logic with Seaborn’s property-based philosophy, ensuring tests are reproducible, modular, and maintainable.

1. conftest.py – Shared Infrastructure

  • Fixed random seed to ensure deterministic results.
  • Disabled GUI backends (matplotlib.use("Agg")) to avoid pop-up graphics during tests.
  • Automatic plot cleanup (plt.close() after each test).
  • Shared fixtures for reusable data and models (e.g., PMV parameters, simple linear model).

2. test_utils.py – Logical / Utility Function Tests

  • Tests numerical and logical correctness for:
    • Temperature and humidity conversions, humidity ratio calculations.
    • Default threshold verification for models.
    • Parameter passing and mapping logic between models.
  • Uses numpy.testing.assert_array_equal for accuracy checks.
  • Includes edge-case handling (negative humidity, reversed intervals) to ensure graceful error messages or warnings instead of crashes.

3. test_generic.py – Core Plotting Tests

  • Calls calc_plot_ranges() to render plots.
  • Inspects returned Matplotlib objects (Axes, artists dictionary).
  • Verifies expected plot components (curves, bands, legends, axis labels).
  • Checks that colors, transparency, and line widths are correctly applied.
  • Ensures consistent behavior under multiple parameter combinations (thresholds, color modes).

4. test_ranges_xxx.py – Specific Plot Function Tests

  • Smoke tests: Basic functionality verification.
  • Parameter-range compatibility: Handles extreme ranges (e.g., -10°C to 50°C) and variable step sizes.
  • Graph property validation: Checks legends, axis labels, and color consistency.
  • Error handling: Raises clear errors for invalid inputs (reversed intervals, negative step sizes).
  • Axes reuse logic: Confirms that plotting functions correctly reuse or create axes as needed.

Summary

By following Seaborn’s mature testing philosophy, it ensures the pythermalcomfort testing suite remains reliable, reproducible, and easy to extend for future development.

- Add complete test suite for plotting functionality
- Include tests for utils, generic, and all range plotting functions
- Improve docstrings for all test classes and methods
- Clean up unnecessary inline comments
- Follow seaborn testing approach with property-level assertions
- All 118 tests pass successfully with expected warnings for unsolvable thresholds
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 16, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • development

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

- Expose common visual parameters (cmap, band_alpha, line_color, line_width) directly in function signatures
- Replace plot_kwargs with **kwargs for advanced customization
- Update all 4 ranges plotting functions: ranges_tdb_rh, ranges_tdb_psychrometric, ranges_tdb_v, ranges_to_rh
- Update comprehensive docstrings with detailed examples showing basic, visual, and advanced customization
- Update all test files to use new API design
- Maintain backward compatibility with existing functionality
- Follow seaborn-style API design patterns for better usability
@yuli0346
Copy link
Author

Simplified Plotting API Based on Seaborn Design

We've made the plotting API simpler and easier to use, based on Seaborn's design.

Seaborn Features

  • Explicit Core Parameters:
    Common parameters are directly written in the function signature, allowing users to easily understand what they can change.

  • Other Parameters:
    Non-core styling options are transparently passed to Matplotlib via **kwargs.

Our Improvements

Based on this approach, we have refactored and improved the plotting API in our codebase:

  • Common plotting parameters such as cmap, band_alpha, line_width, and line_color are made explicit, helping users quickly understand which inputs affect the appearance of the chart.

@FedericoTartarini
Copy link
Collaborator

@yuli0346 I do not see any updates since the last two weeks, we discussed improving the tests and removing the ChatGPT-generated code. @KristinaM93 is looking into how they implemented the code in Seaborn. The whole team should have done that, do you have any updates?

t_range: tuple[float, float] = (10.0, 36.0),
v_range: tuple[float, float] = (0.0, 1.5),
v_step: float = 0.05,
# Visual controls (most commonly used)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this suggestion

plot_kwargs : dict[str, Any] or None, optional
Additional keyword arguments forwarded to ``calc_plot_ranges`` for further
customization (e.g., cmap, band_colors, xlabel, ylabel, etc.).
**kwargs : Any
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we remove all the inputs like xlabel, ylabel, title, and figsize since they can be changed later on since the function returns the plot axis. Here we should only pass the necessary inputs that are needed to generated the figure. Kristina is looking into the Seaborn documentation to see how they implemented the functions there. Could you also please look into that as I mentioned a few weeks ago?

plot_kwargs : dict[str, Any] or None, optional
Additional keyword arguments forwarded to ``calc_plot_ranges`` for further
customization (e.g., cmap, band_colors, xlabel, ylabel, etc.).
**kwargs : Any
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. Drop xlabel, ylabel, title, and figsize. Keep an ax parameter, return the Axes, and let callers set labels/size on the returned object. This matches Seaborn’s axis-level pattern: accept ax, return Axes; figure-level APIs control size via height/aspect.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ve taken out the xlabel, ylabel, title, and figsize now.

- Simplify function docstrings in test_ranges_tdb_rh.py
- Remove redundant comments
- Merge exception handling into single except block
- Improve code readability and maintainability
- Keep detailed class docstrings for test scope documentation
@yuli0346
Copy link
Author

Sorry for missing your earlier comment!
We’ve added a centralized model registry in conftest.py so we can easily test different models, not just PMV. It includes a ModelInfo dataclass, a unified MODEL_CONFIGS list, and an all_models fixture. And, the changes mainly focus on test_ranges_tdb_rh.py, and we added six test functions, including the numerical accuracy test you mentioned. It now checks that each plotted curve point matches the direct model output within ±0.1.

if plot_kwargs:
kwargs.update(plot_kwargs)
# Allow additional matplotlib parameters via **kwargs
calc_kwargs.update(kwargs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think we need
if plot_kwargs:
calc_kwargs.update(plot_kwargs)
calc_kwargs.update(kwargs)

- Pass correct metric_attr through plot_kwargs in test_numerical_accuracy
- Skip validation for boundary points that indicate unsolved thresholds
- Boundary points are excluded when they have large errors, as they represent cases where the solver couldn't find a solution within the specified range
@yuli0346 yuli0346 changed the title Add tests following Seaborn test logic Refactor plotting code and update test logic Nov 2, 2025
- Remove xlabel, ylabel, title, figsize from function signatures
- Update functions to use **kwargs with if kwargs check
- Update docstrings to guide users to set labels on returned Axes
- Align with Seaborn's axis-level pattern

Files modified:
- ranges_to_rh.py
- ranges_tdb_v.py
- ranges_tdb_psychrometric.py
@LianqiWang111
Copy link

We evaluated how Seaborn's core features, such as theme management system, perceptually uniform color palettes, DataFrame-aware plotting API, and advanced grid layout extensions and how to improve the performance, interpretability and usability of thermal comfort visualization results. Through a systematic review of Seaborn official documents (Seaborn Development Team, 2024), we have identified a number of components that can be directly applied to this project, including sns.set_theme, color_palette, lineplot, and FacetGrid. These characteristics enable Pythermalcomfort to generate high-quality graphics that meet modern scientific research display standards.

1. Style and Theme Management
We found that Seaborn's global theme system (sns.set_theme) is very suitable for unifying the chart style in PyThermalComfort. It provides preset themes such as whitegrid and darkgrid, which can automatically adjust the background, font and grid lines without complicated settings. By parameterizing seaborn_style and seaborn_palette, the project can flexibly switch between different scenes and maintain visual consistency.

Before modification:

if ax is None:
    plt.style.use("seaborn-v0_8-whitegrid")
    _, ax = plt.subplots(figsize=(7, 4), dpi=300, constrained_layout=True)

After modification:

if use_seaborn is not False:
    sns.set_theme(style=seaborn_style, palette=seaborn_palette)

if ax is None:
    _, ax = plt.subplots(figsize=(7, 4), dpi=300, constrained_layout=True)

Improvement Notes:
The original version only supported hard-coded visual styles.
The new implementation introduces a configurable use_seaborn switch and integrates sns.set_theme() to enable parameterized theme control (e.g., "whitegrid", "darkgrid", "ticks"), allowing users to maintain consistent styling across plots.

Reference:
Seaborn Development Team. (2024). seaborn.set_theme — seaborn 0.13.2 documentation. PyData. Retrieved November 3, 2025, from https://seaborn.pydata.org/generated/seaborn.set_theme.html

2. Color Palettes and Colormaps
Seaborn’s color_palette() and as_cmap=True options enable perceptually balanced gradients that outperform default Matplotlib colormaps. In thermal-comfort plots, these palettes make temperature-humidity regions blend smoothly while preserving metric contrast. The system also supports named palettes ("coolwarm", "crest", "flare") that align with scientific visualization standards.

Before modification:

if band_colors is not None:
    if len(band_colors) != needed:
        raise ValueError("band_colors must have length equal to number of regions")
    band_colors = band_colors
else:
    cmap_obj = plt.get_cmap(cmap)
    band_colors = [cmap_obj(i / (needed - 1)) for i in range(needed)]

After modification:

if use_seaborn:
    cmap_obj = sns.color_palette(seaborn_palette, as_cmap=True)
else:
    cmap_obj = plt.get_cmap(cmap)
band_colors = [cmap_obj(i / (needed - 1)) for i in range(needed)]

Improvement Notes:
Added Seaborn’s sns.color_palette() for scientifically curated palettes (e.g., "coolwarm", "crest", "flare").
The gradient transitions are now smoother and more perceptually balanced, and the palette can be customized to ensure a unified aesthetic across all diagrams.

Reference:
Seaborn Development Team. (2024). seaborn.color_palette — seaborn 0.13.2 documentation. PyData. Retrieved November 3, 2025, from https://seaborn.pydata.org/generated/seaborn.color_palette.html

3. Data-aware Plotting API

Seaborn’s DataFrame-oriented API (e.g., sns.lineplot, sns.scatterplot) is well suited to PyThermalComfort, where each curve represents model outputs over continuous variables. By converting NumPy arrays into Pandas DataFrames, the project can label thresholds, associate colors automatically, and simplify multi-curve rendering.

Before modification:

curve_artists = []
for curve in curves:
    m = np.isfinite(curve)
    if clip_arr is not None:
        m = m & (curve >= clip_arr)
    if m.any():
        (ln,) = ax.plot(curve[m], y_arr[m], color=line_color, linewidth=line_width)
        curve_artists.append(ln)

After modification:

curve_artists = []
curve_colors = band_colors[:-1] 
for idx, curve in enumerate(curves):
    m = np.isfinite(curve)
    if clip_arr is not None:
        m = m & (curve >= clip_arr)
    if m.any():
        df_curve = pd.DataFrame({
            "x": curve[m],
            "y": y_arr[m],
            "threshold": [thr_list[idx]] * np.count_nonzero(m),
        })
        ln = sns.lineplot(
            data=df_curve,
            x="x", y="y",
            ax=ax,
            color=curve_colors[idx],
            linewidth=line_width + 0.5,
            legend=False,
            palette=seaborn_palette if use_seaborn else None,
        )
        if len(ax.lines):
            curve_artists.append(ax.lines[-1])

Improvement Notes:
Replaced ax.plot() with sns.lineplot() to enable automatic color mapping and smoother line rendering.
Lines are now visually softer and harmonized with the surrounding color bands.
Additionally, the data are now structured using pandas.DataFrame, improving scalability and enabling data-aware styling for future extensions.

Reference:
Seaborn Development Team. (2024). seaborn.lineplot — seaborn 0.13.2 documentation. PyData. Retrieved November 3, 2025, from https://seaborn.pydata.org/generated/seaborn.lineplot.html

4. Legend Synchronization
Automatically generate legend entries that are synchronized with the band color. The labels follow the rules of the beginning/middle/end interval and reuse the same color sequence as the drawing band, eliminating the need for manual synchronization and enhancing readability.

Before modification:

legend_elements = []
for i in range(needed):
    if i == 0 and len(thr_list) > 0:
        label = f"< {thr_list[0]:.1f}"
    elif i == needed - 1 and len(thr_list) > 0:
        label = f"> {thr_list[-1]:.1f}"
    else:
        label = f"{thr_list[i - 1]:.1f} to {thr_list[i]:.1f}"

    legend_elements.append(
        plt.Rectangle(
            (0, 0), 1, 1,
            facecolor=band_colors[i],
            alpha=band_alpha,
            label=label,
        )
    )

legend_artist = ax.legend(
    handles=legend_elements,
    loc="lower center",
    bbox_to_anchor=(0.5, 1),
    ncol=min(6, len(legend_elements)),
    framealpha=0.8,
    markerscale=0.6,
)

After modification:

legend_elements = []
for i in range(needed):
    if i == 0 and len(thr_list) > 0:
        label = f"< {thr_list[0]:.1f}"
    elif i == needed - 1 and len(thr_list) > 0:
        label = f"> {thr_list[-1]:.1f}"
    elif len(thr_list) == 0:
        label = "Region"
    else:
        label = f"{thr_list[i - 1]:.1f} to {thr_list[i]:.1f}"
    legend_elements.append(
        plt.Rectangle(
            (0, 0),
            1,
            1,
            facecolor=band_colors[i],
            alpha=band_alpha,
            label=label,
        )
    )

legend_artist = ax.legend(
    handles=legend_elements,
    loc="lower center",
    bbox_to_anchor=(0.5, 1),
    ncol=min(6, len(legend_elements)),
    framealpha=0.8,
    markerscale=0.6,
)

Improvement notes:
Legend colors are directly inherited from the Seaborn palette;
This ensures complete color consistency between the legend and the zone colors, eliminating the need for manual synchronization.

Reference:
Seaborn Development Team. (2025). seaborn.color_palette — Seaborn 0.13.x Documentation. Retrieved November 3, 2025, from https://seaborn.pydata.org/generated/seaborn.color_palette.html

5. DataFrame Integration

We replace array-loop plotting with a Pandas DataFrame + Seaborn API workflow. Explicit column semantics (x / y / threshold) improve readability and create a scalable path for grouping, faceting, and multivariate extensions.

Before modification:

for curve in curves:
    m = np.isfinite(curve)
    if clip_arr is not None:
        m = m & (curve >= clip_arr)
    if m.any():
        (ln,) = ax.plot(curve[m], y_arr[m], color=line_color, linewidth=line_width)
        curve_artists.append(ln)

After modification:

df_curve = pd.DataFrame({
    "x": curve[m],
    "y": y_arr[m],
    "threshold": [thr_list[idx]] * np.count_nonzero(m),
})
ln = sns.lineplot(
    data=df_curve,
    x="x", y="y",
    ax=ax,
    color=curve_colors[idx],
    linewidth=line_width + 0.5,
    legend=False,
    palette=seaborn_palette if use_seaborn else None,
)
if len(ax.lines):
    curve_artists.append(ax.lines[-1])

Improvement Notes:
Use Pandas DataFrame instead of raw arrays to make column semantics explicit;
Align with Seaborn’s data-oriented API, reducing boilerplate loops;
Provide a unified interface for future expansion

Reference:
Seaborn Development Team. (2025). seaborn.lineplot — Seaborn 0.13.x Documentation. Retrieved November 3, 2025, from https://seaborn.pydata.org/generated/seaborn.lineplot.html

6. Configurable Visualization Framework

We introduce a toggleable visualization layer via use_seaborn and style/palette parameters. Seaborn is enabled by default for consistent themes and colors, while a pure-Matplotlib fallback preserves backward compatibility and works without the Seaborn dependency.

Before modification:
fixed style; no toggle and no palette parameterization

After modification:

use_seaborn: bool = True,
seaborn_style: str = "whitegrid",
seaborn_palette: str = "coolwarm",

if use_seaborn is not False:
    sns.set_theme(style=seaborn_style, palette=seaborn_palette)

Improvement Notes:
use_seaborn toggles the Seaborn enhancement layer on/off;
Pure-Matplotlib fallback ensures backward compatibility and no hard dependency;
Style and palette are parameterized to enforce consistent visuals and reuse.

Reference:
Seaborn Development Team. (2024). seaborn.set_theme — Seaborn 0.13.x Documentation. PyData. Retrieved November 3, 2025, from https://seaborn.pydata.org/generated/seaborn.set_theme.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants