diff --git a/docs/notebook_requirements.txt b/docs/notebook_requirements.txt index f3c5dc95..b47fa02e 100644 --- a/docs/notebook_requirements.txt +++ b/docs/notebook_requirements.txt @@ -15,7 +15,7 @@ ipython==8.26.0 ipython-genutils==0.2.0 ipywidgets==8.1.3 jedi==0.19.1 -Jinja2==3.1.5 +Jinja2==3.1.6 jsonschema==4.23.0 jupyter==1.0.0 jupyter-client==8.6.2 @@ -29,7 +29,7 @@ nbclient==0.10.0 nbconvert==7.16.4 nbformat==5.10.4 nest-asyncio==1.6.0 -notebook==7.2.1 +notebook==7.2.2 numexpr==2.10.1 pandocfilters==1.5.1 parso==0.8.4 @@ -48,7 +48,7 @@ soupsieve==2.6 terminado==0.18.1 testpath==0.6.0 tinycss2==1.3.0 -tornado==6.4.2 +tornado==6.5.1 traitlets==5.14.3 wcwidth==0.2.13 webencodings==0.5.1 diff --git a/docs/sphinx/source/changelog.rst b/docs/sphinx/source/changelog.rst index fc3d805a..8c279d97 100644 --- a/docs/sphinx/source/changelog.rst +++ b/docs/sphinx/source/changelog.rst @@ -1,5 +1,7 @@ RdTools Change Log ================== +.. include:: changelog/pending.rst +.. include:: changelog/v3.0.1.rst .. include:: changelog/v3.0.0.rst .. include:: changelog/v2.1.8.rst .. include:: changelog/v2.1.7.rst diff --git a/docs/sphinx/source/changelog/pending.rst b/docs/sphinx/source/changelog/pending.rst new file mode 100644 index 00000000..59ee736a --- /dev/null +++ b/docs/sphinx/source/changelog/pending.rst @@ -0,0 +1,16 @@ +************************* +v3.0.x (X, X, 2025) +************************* + +Enhancements +------------ +* :py:func:`~rdtools.plotting.degradation_timeseries_plot` has new parameter ``label=`` + to allow the timeseries plot to have right labeling (default), center or left labeling. + (:issue:`455`) + + + +Contributors +------------ +* Chris Deline (:ghuser:`cdeline`) + diff --git a/docs/sphinx/source/changelog/v3.0.1.rst b/docs/sphinx/source/changelog/v3.0.1.rst new file mode 100644 index 00000000..9bfaaaa3 --- /dev/null +++ b/docs/sphinx/source/changelog/v3.0.1.rst @@ -0,0 +1,11 @@ +************************* +v3.0.1 (August 21, 2025) +************************* + +Requirements +------------ +* Updated Jinja2==3.1.6 in ``notebook_requirements.txt`` (:pull:`465`) +* Updated tornado==6.5.1 in ``notebook_requirements.txt`` (:pull:`465`) +* Updated requests==2.32.4 in ``requirements.txt`` (:pull:`465`) +* Updated urllib3==2.5.0 in ``requirements.txt`` (:pull:`465`) +* Removed constraint that scipy<1.16.0 (:pull:`465`) \ No newline at end of file diff --git a/rdtools/plotting.py b/rdtools/plotting.py index 93a07bac..b59e2b8c 100644 --- a/rdtools/plotting.py +++ b/rdtools/plotting.py @@ -5,6 +5,7 @@ import plotly.express as px import numpy as np import warnings +import datetime def degradation_summary_plots(yoy_rd, yoy_ci, yoy_info, normalized_yield, @@ -431,7 +432,7 @@ def availability_summary_plots(power_system, power_subsystem, loss_total, return fig -def degradation_timeseries_plot(yoy_info, rolling_days=365, include_ci=True, +def degradation_timeseries_plot(yoy_info, rolling_days=365, include_ci=True, label='right', fig=None, plot_color=None, ci_color=None, **kwargs): ''' Plot resampled time series of degradation trend with time @@ -447,6 +448,13 @@ def degradation_timeseries_plot(yoy_info, rolling_days=365, include_ci=True, at least 50% of datapoints to be included in rolling plot. include_ci : bool, default True calculate and plot 2-sigma confidence intervals along with rolling median + label : {'right', 'left', 'center'}, default 'right' + A combination of 1) which Year-on-Year slope edge to label, + and 2) which rolling median edge to label. + + * ``right`` : label right edge of YoY slope and right edge of rolling median interval. + * ``center``: label center of YoY slope interval and center of rolling median interval. + * ``left`` : label left edge of YoY slope and center of rolling median interval. fig : matplotlib, optional fig object to add new plot to (first set of axes only) plot_color : str, optional @@ -475,7 +483,6 @@ def _bootstrap(x, percentile, reps): try: results_values = yoy_info['YoY_values'] - except KeyError: raise KeyError("yoy_info input dictionary does not contain key `YoY_values`.") @@ -484,7 +491,22 @@ def _bootstrap(x, percentile, reps): if ci_color is None: ci_color = 'C0' - roller = results_values.rolling(f'{rolling_days}d', min_periods=rolling_days//2) + if label not in {None, "left", "right", "center"}: + raise ValueError(f"Unsupported value {label} for `label`") + if label is None: + label = "right" + + if label == "right": + center = False + offset_days = 0 + elif label == "center": + center = True + offset_days = 182 + elif label == "left": + center = True + offset_days = 365 + + roller = results_values.rolling(f'{rolling_days}d', min_periods=rolling_days//2, center=center) # unfortunately it seems that you can't return multiple values in the rolling.apply() kernel. # TODO: figure out some workaround to return both percentiles in a single pass if include_ci: @@ -495,8 +517,10 @@ def _bootstrap(x, percentile, reps): else: ax = fig.axes[0] if include_ci: - ax.fill_between(ci_lower.index, ci_lower, ci_upper, color=ci_color) - ax.plot(roller.median(), color=plot_color, **kwargs) + ax.fill_between(ci_lower.index - datetime.timedelta(days=offset_days), + ci_lower, ci_upper, color=ci_color) + ax.plot(roller.median().index - datetime.timedelta(days=offset_days), + roller.median(), color=plot_color, **kwargs) ax.axhline(results_values.median(), c='k', ls='--') plt.ylabel('Degradation trend (%/yr)') fig.autofmt_xdate() diff --git a/rdtools/test/plotting_test.py b/rdtools/test/plotting_test.py index cb4639cb..f4eb83f6 100644 --- a/rdtools/test/plotting_test.py +++ b/rdtools/test/plotting_test.py @@ -252,7 +252,49 @@ def test_availability_summary_plots_empty(availability_analysis_object): def test_degradation_timeseries_plot(degradation_info): power, yoy_rd, yoy_ci, yoy_info = degradation_info - # test defaults - result = degradation_timeseries_plot(yoy_info) - assert_isinstance(result, plt.Figure) + # test defaults (label='right') + result_right = degradation_timeseries_plot(yoy_info) + assert_isinstance(result_right, plt.Figure) + xlim_right = result_right.get_axes()[0].get_xlim()[0] + + # test label='center' + result_center = degradation_timeseries_plot(yoy_info=yoy_info, include_ci=False, + label='center', fig=result_right) + assert_isinstance(result_center, plt.Figure) + xlim_center = result_center.get_axes()[0].get_xlim()[0] + + # test label='left' + result_left = degradation_timeseries_plot(yoy_info=yoy_info, include_ci=False, label='left') + assert_isinstance(result_left, plt.Figure) + xlim_left = result_left.get_axes()[0].get_xlim()[0] + + # test label=None (should default to 'right') + result_none = degradation_timeseries_plot(yoy_info=yoy_info, include_ci=False, label=None) + assert_isinstance(result_none, plt.Figure) + xlim_none = result_none.get_axes()[0].get_xlim()[0] + + # Check that the xlim values are offset as expected + # right > center > left (since offset_days increases) + assert xlim_right > xlim_center > xlim_left + assert xlim_right == xlim_none # label=None defaults to 'right' + + # The expected difference from right to left is 548 days (1.5 yrs), allow 5% tolerance + expected_diff = 548 + actual_diff = (xlim_right - xlim_left) + tolerance = expected_diff * 0.05 + assert abs(actual_diff - expected_diff) <= tolerance, \ + f"difference of right-left xlim {actual_diff} not within 5% of 1.5 yrs." + + # The expected difference from right to center is 365 days, allow 5% tolerance + expected_diff2 = 365 + actual_diff2 = (xlim_right - xlim_center) + tolerance2 = expected_diff2 * 0.05 + assert abs(actual_diff2 - expected_diff2) <= tolerance2, \ + f"difference of right-center xlim {actual_diff2} not within 5% of 1 yr." + + with pytest.raises(KeyError): + degradation_timeseries_plot({'a': 1}, include_ci=False) + with pytest.raises(ValueError): + degradation_timeseries_plot(yoy_info, include_ci=False, label='CENTER') + plt.close('all') diff --git a/requirements.txt b/requirements.txt index 387589a4..5ce3b27f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ python-dateutil==2.9.0 pytz==2024.1 arch==7.0.0 filterpy==1.4.5 -requests==2.32.3 +requests==2.32.4 retrying==1.3.4 scikit-learn==1.5.1 scipy==1.13.1 @@ -30,6 +30,6 @@ statsmodels==0.14.2 threadpoolctl==3.5.0 tomli==2.0.1 typing_extensions==4.12.2 -urllib3==2.2.2 +urllib3==2.5.0 xgboost==2.1.1