diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/__init__.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/__init__.py index 432d8577..8fcb8af3 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/__init__.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/__init__.py @@ -19,10 +19,6 @@ # override "HOME" in case this is set to something other than default for windows os.environ["HOME"] = (Path("C:/Users/") / getpass.getuser()).as_posix() -import python_toolkit -# set plotting style for modules within this toolkit -plt.style.use(Path(list(python_toolkit.__path__)[0]).absolute() / "bhom" / "bhom.mplstyle") - # get dataset paths SRI_DATA = DATA_DIRECTORY / "sri_data.csv" KOEPPEN_DATA = DATA_DIRECTORY / "koeppen.csv" diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/directional_solar_radiation.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/directional_solar_radiation.py index 071b22b0..900d0169 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/directional_solar_radiation.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/directional_solar_radiation.py @@ -79,6 +79,8 @@ def directional_solar_radiation(epw_file, directions, tilts, irradiance_type, analysis_period, cmap, title, save_path) -> str: try: + style = os.environ.get("BHOM_style_context", "python_toolkit.bhom") + if cmap not in plt.colormaps(): cmap = "YlOrRd" @@ -93,11 +95,12 @@ def directional_solar_radiation(epw_file, directions, tilts, irradiance_type, an elif irradiance_type == "Reflected": irradiance_type = IrradianceType.REFLECTED - fig, ax = plt.subplots(1, 1, figsize=(22.8/2, 7.6/2)) - values, dirs, tts = create_radiation_matrix(Path(epw_file), rad_type=irradiance_type, analysis_period=analysis_period, directions=directions, tilts=tilts) - tilt_orientation_factor(Path(epw_file), ax=ax, rad_type=irradiance_type, analysis_period=analysis_period, directions=directions, tilts=tilts, cmap=cmap) - if not (title == "" or title is None): - ax.set_title(title) + with plt.style.context(style): + fig, ax = plt.subplots(1, 1, figsize=(22.8/2, 7.6/2)) + values, dirs, tts = create_radiation_matrix(Path(epw_file), rad_type=irradiance_type, analysis_period=analysis_period, directions=directions, tilts=tilts) + tilt_orientation_factor(Path(epw_file), ax=ax, rad_type=irradiance_type, analysis_period=analysis_period, directions=directions, tilts=tilts, cmap=cmap, style_context=style) + if not (title == "" or title is None): + ax.set_title(title) return_dict = {} diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/diurnal.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/diurnal.py index d44e5577..df99c6f0 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/diurnal.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/diurnal.py @@ -2,6 +2,7 @@ # pylint: disable=C0415,E0401,W0703 import argparse import json +import os import sys import traceback from pathlib import Path @@ -66,17 +67,19 @@ def diurnal(epw_file, data_type_key="Dry Bulb Temperature", colour="#000000", title=None, period="monthly", save_path = None) -> str: try: + style = os.environ.get("BHOM_style_context", "python_toolkit.bhom") epw = EPW(epw_file) if data_type_key == "Wet Bulb Temperature": coll = wet_bulb_temperature(epw) else: coll = HourlyContinuousCollection.from_dict([a for a in epw.to_dict()["data_collections"] if a["header"]["data_type"]["name"] == data_type_key][0]) - - fig, ax = plt.subplots() - dnal(collection_to_series(coll), ax=ax, title=title, period=period, color=colour) - return_dict = {"data": collection_metadata(coll)} + with plt.style.context(style): + fig, ax = plt.subplots() + + dnal(collection_to_series(coll), ax=ax, title=title, period=period, color=colour, style_context=style) + return_dict = {"data": collection_metadata(coll)} if save_path == None or save_path == "": base64 = figure_to_base64(fig, html=False) diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/facade_condensation_risk_chart.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/facade_condensation_risk_chart.py index 81df1bbe..95e9f55d 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/facade_condensation_risk_chart.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/facade_condensation_risk_chart.py @@ -1,6 +1,7 @@ """Method to wrap creation of diurnal plots""" # pylint: disable=C0415,E0401,W0703 import argparse +import os import textwrap from pathlib import Path @@ -50,10 +51,12 @@ def facade_condensation_risk_chart(epw_file: str, thresholds: list[float], save_path: str = None) -> None: + style = os.environ.get("BHOM_style_context", "python_toolkit.bhom") + epw = EPW(epw_file) hcc = epw.dry_bulb_temperature - fig = facade_condensation_risk_chart_table(epw_file, thresholds) + fig = facade_condensation_risk_chart_table(epw_file, thresholds, style_context=style) return_dict = {"data": collection_metadata(hcc)} diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/facade_condensation_risk_heatmap.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/facade_condensation_risk_heatmap.py index fba0706e..231641eb 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/facade_condensation_risk_heatmap.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/facade_condensation_risk_heatmap.py @@ -1,6 +1,7 @@ """Method to wrap creation of diurnal plots""" # pylint: disable=C0415,E0401,W0703 import argparse +import os import textwrap from pathlib import Path @@ -49,10 +50,12 @@ ) def facade_condensation_risk_heatmap(epw_file: str, thresholds: list[float], save_path: str = None) -> None: + style = os.environ.get("BHOM_style_context", "python_toolkit.bhom") + epw = EPW(epw_file) hcc = epw.dry_bulb_temperature - fig = facade_condensation_risk_heatmap_histogram(epw_file, thresholds) + fig = facade_condensation_risk_heatmap_histogram(epw_file, thresholds, style_context=style) return_dict = {"data": collection_metadata(hcc)} diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/heatmap.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/heatmap.py index 14ff11da..a1368c60 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/heatmap.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/heatmap.py @@ -1,6 +1,7 @@ """Method to wrap for conversion of EPW to CSV file.""" # pylint: disable=C0415,E0401,W0703 import argparse +import os from pathlib import Path import json import sys @@ -54,19 +55,21 @@ def heatmap(epw_file: str, data_type_key: str, colour_map: str, save_path:str = None) -> str: """Create a CSV file version of an EPW.""" try: + style = os.environ.get("BHOM_style_context", "python_toolkit.bhom") if colour_map not in plt.colormaps(): colour_map = "YlGnBu" - fig, ax = plt.subplots() + with plt.style.context(style): + fig, ax = plt.subplots() - epw = EPW(epw_file) + epw = EPW(epw_file) - if data_type_key == "Wet Bulb Temperature": - coll = wet_bulb_temperature(epw) - else: - coll = HourlyContinuousCollection.from_dict([a for a in epw.to_dict()["data_collections"] if a["header"]["data_type"]["name"] == data_type_key][0]) + if data_type_key == "Wet Bulb Temperature": + coll = wet_bulb_temperature(epw) + else: + coll = HourlyContinuousCollection.from_dict([a for a in epw.to_dict()["data_collections"] if a["header"]["data_type"]["name"] == data_type_key][0]) - hmap(collection_to_series(coll), ax=ax, cmap=colour_map) + hmap(collection_to_series(coll), ax=ax, cmap=colour_map, style_context=style) return_dict = {} diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/sunpath.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/sunpath.py index 9bdbfe8f..f0155acd 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/sunpath.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/sunpath.py @@ -1,6 +1,7 @@ """Method to wrap creation of sunpath plots""" # pylint: disable=C0415,E0401,W0703 import argparse +import os import sys import traceback from pathlib import Path @@ -52,11 +53,13 @@ def sunpath(epw_file, analysis_period, size, save_path) -> str: try: - fig, ax = plt.subplots() + style = os.environ.get("BHOM_style_context", "python_toolkit.bhom") + with plt.style.context(style): + fig, ax = plt.subplots() - analysis_period = AnalysisPeriod.from_dict(json.loads(analysis_period)) - epw = EPW(epw_file) - spath(location=epw.location, analysis_period=analysis_period, sun_size=size, ax=ax) + analysis_period = AnalysisPeriod.from_dict(json.loads(analysis_period)) + epw = EPW(epw_file) + spath(location=epw.location, analysis_period=analysis_period, sun_size=size, ax=ax, style_context=style) return_dict = {"data": sunpath_metadata(Sunpath.from_location(epw.location))} diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/utci_heatmap.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/utci_heatmap.py index fad831cb..410e2950 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/utci_heatmap.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/utci_heatmap.py @@ -1,6 +1,7 @@ """Method to wrap UTCI plots""" # pylint: disable=C0415,E0401,W0703 import argparse +import os import sys import traceback import matplotlib @@ -42,6 +43,7 @@ def utci_heatmap(input_json:str, save_path = None, epw_file:str = None) -> str: try: + style = os.environ.get("BHOM_style_context", "python_toolkit.bhom") if not input_json.startswith("{"): #assume it's a path with open(input_json, "r") as f: @@ -61,14 +63,15 @@ def utci_heatmap(input_json:str, save_path = None, epw_file:str = None) -> str: colors=(bin_colours), name="UTCI") - fig, ax = plt.subplots(1, 1, figsize=(10, 4)) - ec.plot_utci_heatmap(utci_categories = custom_bins, ax=ax) + with plt.style.context(style): + fig, ax = plt.subplots(1, 1, figsize=(10, 4)) + ec.plot_utci_heatmap(utci_categories = custom_bins, ax=ax, style_context=style) - utci_collection = ec.universal_thermal_climate_index + utci_collection = ec.universal_thermal_climate_index - return_dict = {"data": utci_metadata(utci_collection), "external_comfort": ec.to_dict()} + return_dict = {"data": utci_metadata(utci_collection), "external_comfort": ec.to_dict()} - plt.tight_layout() + plt.tight_layout() if save_path == None or save_path == "": base64 = figure_to_base64(fig,html=False) diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/walkability_heatmap.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/walkability_heatmap.py index 82ebfbab..5c82b84f 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/walkability_heatmap.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/walkability_heatmap.py @@ -1,4 +1,5 @@ import argparse +import os import sys import matplotlib import traceback @@ -38,18 +39,21 @@ def walkability_heatmap(input_json: str, save_path: str, epw_file:str = None) -> str: try: + style = os.environ.get("BHOM_style_context", "python_toolkit.bhom") argsDict = json.loads(input_json) ec = ExternalComfort.from_dict(json.loads(argsDict["external_comfort"])) - fig, ax = plt.subplots(1, 1, figsize=(10, 4)) - ec.plot_walkability_heatmap(ax=ax) - #TODO: create walkability collection metadata - utci_collection = ec.universal_thermal_climate_index + with plt.style.context(style): + fig, ax = plt.subplots(1, 1, figsize=(10, 4)) + ec.plot_walkability_heatmap(ax=ax, style_context=style) - return_dict = {"data": utci_metadata(utci_collection), "external_comfort": ec.to_dict()} + #TODO: create walkability collection metadata + utci_collection = ec.universal_thermal_climate_index - plt.tight_layout() + return_dict = {"data": utci_metadata(utci_collection), "external_comfort": ec.to_dict()} + + plt.tight_layout() if save_path == None or save_path == "": base64 = figure_to_base64(fig,html=False) diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/windrose.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/windrose.py index 5b624843..b1282c70 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/windrose.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/bhom/wrapped/plot/windrose.py @@ -1,6 +1,7 @@ """Method to wrap for creating wind roses from epw files.""" # pylint: disable=C0415,E0401,W0703 import argparse +import os import sys import traceback from pathlib import Path @@ -59,6 +60,7 @@ def windrose(epw_file: str, analysis_period: str, colour_map: str, bins: int, save_path: str = None) -> str: """Method to wrap for creating wind roses from epw files.""" try: + style = os.environ.get("BHOM_style_context", "python_toolkit.bhom") if colour_map not in plt.colormaps(): colour_map = "YlGnBu" @@ -66,15 +68,16 @@ def windrose(epw_file: str, analysis_period: str, colour_map: str, bins: int, sa analysis_period = AnalysisPeriod.from_dict(json.loads(analysis_period)) w_epw = Wind.from_epw(epw_file) - fig, ax = plt.subplots(1, 1, figsize=(6, 6), subplot_kw={"projection": "polar"}) + with plt.style.context(style): + fig, ax = plt.subplots(1, 1, figsize=(6, 6), subplot_kw={"projection": "polar"}) - wind_filtered = w_epw.filter_by_analysis_period(analysis_period=analysis_period) + wind_filtered = w_epw.filter_by_analysis_period(analysis_period=analysis_period) - w_epw.filter_by_analysis_period(analysis_period=analysis_period).plot_windrose(ax=ax, directions=bins, ylim=(0, 3.6/bins), colors=colour_map) + wind_filtered.plot_windrose(ax=ax, directions=bins, ylim=(0, 3.6/bins), colors=colour_map, style_context=style) - return_dict = {"data": wind_metadata(wind_filtered, directions=bins)} + return_dict = {"data": wind_metadata(wind_filtered, directions=bins)} - plt.tight_layout() + plt.tight_layout() if save_path == None or save_path == "": return_dict["figure"] = figure_to_base64(fig,html=False) else: diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/categorical/categorical.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/categorical/categorical.py index bc2b7524..e5c1fb6b 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/categorical/categorical.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/categorical/categorical.py @@ -500,6 +500,8 @@ def annual_monthly_histogram( Whether to show the labels on the bars. Defaults to False. **kwargs: Additional keyword arguments to pass to plt.bar. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: @@ -507,55 +509,58 @@ def annual_monthly_histogram( """ validate_timeseries(series) + + style_context = kwargs.pop("style_context", "python_toolkit.bhom") - if ax is None: - ax = plt.gca() + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() - color_lookup = dict(zip(self.bin_names, self.colors)) + color_lookup = dict(zip(self.bin_names, self.colors)) - t = self.timeseries_summary_monthly(series, density=True) + t = self.timeseries_summary_monthly(series, density=True) - t.plot( - ax=ax, - kind="bar", - stacked=True, - color=[color_lookup[i] for i in t.columns], - width=kwargs.pop("width", 1), - legend=False, - **kwargs, - ) - ax.set_xlim(-0.5, len(t) - 0.5) - ax.set_ylim(0, 1) - ax.set_xticklabels( - [calendar.month_abbr[int(i._text)] for i in ax.get_xticklabels()], - ha="center", - rotation=0, - ) - for spine in ["top", "right", "left", "bottom"]: - ax.spines[spine].set_visible(False) - ax.yaxis.set_major_formatter(mticker.PercentFormatter(1)) - - if show_legend: - ax.legend( - bbox_to_anchor=(1, 1), - loc="upper left", - borderaxespad=0.0, - frameon=False, - title=self.name, + t.plot( + ax=ax, + kind="bar", + stacked=True, + color=[color_lookup[i] for i in t.columns], + width=kwargs.pop("width", 1), + legend=False, + **kwargs, ) - - if show_labels: - for i, c in enumerate(ax.containers): - label_colors = [contrasting_color(i.get_facecolor()) for i in c.patches] - labels = [f"{v.get_height():0.1%}" if v.get_height() > 0.15 else "" for v in c] - ax.bar_label( - c, - labels=labels, - label_type="center", - color=label_colors[i], - fontsize="x-small", + ax.set_xlim(-0.5, len(t) - 0.5) + ax.set_ylim(0, 1) + ax.set_xticklabels( + [calendar.month_abbr[int(i._text)] for i in ax.get_xticklabels()], + ha="center", + rotation=0, + ) + for spine in ["top", "right", "left", "bottom"]: + ax.spines[spine].set_visible(False) + ax.yaxis.set_major_formatter(mticker.PercentFormatter(1)) + + if show_legend: + ax.legend( + bbox_to_anchor=(1, 1), + loc="upper left", + borderaxespad=0.0, + frameon=False, + title=self.name, ) + if show_labels: + for i, c in enumerate(ax.containers): + label_colors = [contrasting_color(i.get_facecolor()) for i in c.patches] + labels = [f"{v.get_height():0.1%}" if v.get_height() > 0.15 else "" for v in c] + ax.bar_label( + c, + labels=labels, + label_type="center", + color=label_colors[i], + fontsize="x-small", + ) + return ax @bhom_analytics() @@ -575,6 +580,8 @@ def annual_monthly_table( which creates a new plt.Axes object. **kwargs: Additional keyword arguments to pass to plt.bar. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: @@ -582,23 +589,26 @@ def annual_monthly_table( """ validate_timeseries(series) + + style_context = kwargs.pop("style_context", "python_toolkit.bhom") - if ax is None: - ax = plt.gca() + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() - t = self.timeseries_summary_monthly(series, density=False) - #t = t.iloc[:,:-1] - t = t.transpose().iloc[::-1] + t = self.timeseries_summary_monthly(series, density=False) + #t = t.iloc[:,:-1] + t = t.transpose().iloc[::-1] - # Hide axes - ax.axis('off') + # Hide axes + ax.axis('off') - # Create table - colors = self.colors[::-1] - table = ax.table(cellText=t.values, rowLabels = t.index, colLabels = t.columns, rowColours = colors, loc='center', **kwargs) - for (row, col), cell in table.get_celld().items(): - if (row == 0) or (col == -1): - cell.set_text_props(fontproperties=FontProperties(weight='bold')) + # Create table + colors = self.colors[::-1] + table = ax.table(cellText=t.values, rowLabels = t.index, colLabels = t.columns, rowColours = colors, loc='center', **kwargs) + for (row, col), cell in table.get_celld().items(): + if (row == 0) or (col == -1): + cell.set_text_props(fontproperties=FontProperties(weight='bold')) return ax @@ -613,6 +623,8 @@ def annual_heatmap(self, series: pd.Series, ax: plt.Axes = None, **kwargs) -> pl A matplotlib Axes object to plot on. Defaults to None. **kwargs: Additional keyword arguments to pass to the heatmap function. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: @@ -620,11 +632,8 @@ def annual_heatmap(self, series: pd.Series, ax: plt.Axes = None, **kwargs) -> pl """ validate_timeseries(series) - - if ax is None: - ax = plt.gca() - - heatmap( + + ax = heatmap( series, ax=ax, cmap=self.cmap, @@ -668,6 +677,8 @@ def annual_heatmap_histogram( The number of columns in the legend. Defaults to 5. show_labels (bool, optional): Whether to show the labels on the bars. Defaults to True. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Figure: @@ -681,50 +692,53 @@ def annual_heatmap_histogram( title = kwargs.pop("title", None) ti = self.name + ("" if title is None else f"\n{title}") + + style_context = kwargs.get("style_context", "python_toolkit.bhom") - if show_legend: - fig, (hmap_ax, legend_ax, bar_ax) = plt.subplots( - 3, 1, figsize=figsize, height_ratios=(7, 0.5, 2) - ) - legend_ax.set_axis_off() - handles, labels = self.create_legend_handles_labels(label_type="name") - legend_ax.legend( - handles, - labels, - loc="center", - bbox_to_anchor=(0.5, 0), - fontsize="small", - ncol=ncol, - borderpad=2.5, - ) - else: - fig, (hmap_ax, bar_ax) = plt.subplots(2, 1, figsize=figsize, height_ratios=(7, 2)) - - self.annual_heatmap(series, show_colorbar=False, ax=hmap_ax) - - self.annual_monthly_histogram(ax=bar_ax, series=series, show_labels=show_labels) - - hmap_ax.set_title(ti) - - # add sun up indicator lines - if location is not None: - ylimz = hmap_ax.get_ylim() - xlimz = hmap_ax.get_xlim() - ymin = min(hmap_ax.get_ylim()) - sun_rise_set = sunrise_sunset(location=location) - sunrise = [ - ymin + (((i.time().hour * 60) + (i.time().minute)) / (60 * 24)) - for i in sun_rise_set.sunrise - ] - sunset = [ - ymin + (((i.time().hour * 60) + (i.time().minute)) / (60 * 24)) - for i in sun_rise_set.sunset - ] - xx = np.arange(min(hmap_ax.get_xlim()), max(hmap_ax.get_xlim()) + 1, 1) - hmap_ax.plot(xx, sunrise, zorder=9, c=sunrise_color, lw=1) - hmap_ax.plot(xx, sunset, zorder=9, c=sunset_color, lw=1) - hmap_ax.set_xlim(xlimz) - hmap_ax.set_ylim(ylimz) + with plt.style.context(style_context): + if show_legend: + fig, (hmap_ax, legend_ax, bar_ax) = plt.subplots( + 3, 1, figsize=figsize, height_ratios=(7, 0.5, 2) + ) + legend_ax.set_axis_off() + handles, labels = self.create_legend_handles_labels(label_type="name") + legend_ax.legend( + handles, + labels, + loc="center", + bbox_to_anchor=(0.5, 0), + fontsize="small", + ncol=ncol, + borderpad=2.5, + ) + else: + fig, (hmap_ax, bar_ax) = plt.subplots(2, 1, figsize=figsize, height_ratios=(7, 2)) + + self.annual_heatmap(series, show_colorbar=False, ax=hmap_ax, **kwargs) + + self.annual_monthly_histogram(ax=bar_ax, series=series, show_labels=show_labels, **kwargs) + + hmap_ax.set_title(ti) + + # add sun up indicator lines + if location is not None: + ylimz = hmap_ax.get_ylim() + xlimz = hmap_ax.get_xlim() + ymin = min(hmap_ax.get_ylim()) + sun_rise_set = sunrise_sunset(location=location) + sunrise = [ + ymin + (((i.time().hour * 60) + (i.time().minute)) / (60 * 24)) + for i in sun_rise_set.sunrise + ] + sunset = [ + ymin + (((i.time().hour * 60) + (i.time().minute)) / (60 * 24)) + for i in sun_rise_set.sunset + ] + xx = np.arange(min(hmap_ax.get_xlim()), max(hmap_ax.get_xlim()) + 1, 1) + hmap_ax.plot(xx, sunrise, zorder=9, c=sunrise_color, lw=1) + hmap_ax.plot(xx, sunset, zorder=9, c=sunset_color, lw=1) + hmap_ax.set_xlim(xlimz) + hmap_ax.set_ylim(ylimz) return fig @@ -741,6 +755,8 @@ def annual_threshold_chart(self, series: pd.Series, ax: plt.Axes = None, **kwarg Additional keyword arguments to pass to the heatmap function. show_legend (bool, optional): Whether to show the legend. Defaults to True. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: @@ -748,22 +764,22 @@ def annual_threshold_chart(self, series: pd.Series, ax: plt.Axes = None, **kwarg """ validate_timeseries(series) + + style_context = kwargs.get("style_context", "python_toolkit.bhom") + + with plt.style.context(style_context): + ax = timeseries( + series, + ax=ax, + **kwargs, + ) - if ax is None: - ax = plt.gca() - - plt = timeseries( - series, - ax=ax, - **kwargs, - ) - - for bin_name, interval in list(zip(*[self.bin_names, self.interval_index])): - val = interval.right - if np.isfinite(val) and val>ax.get_ylim()[0]: - col = self.color_from_bin_name(bin_name) - ax.axhline(val, 0, 1, color = col, ls="--", lw=2) - ax.text(0.5, val, val, va='center', ha='center', backgroundcolor='w', color = col, transform=ax.get_yaxis_transform()) + for bin_name, interval in list(zip(*[self.bin_names, self.interval_index])): + val = interval.right + if np.isfinite(val) and val>ax.get_ylim()[0]: + col = self.color_from_bin_name(bin_name) + ax.axhline(val, 0, 1, color = col, ls="--", lw=2) + ax.text(0.5, val, val, va='center', ha='center', backgroundcolor='w', color = col, transform=ax.get_yaxis_transform()) return ax @@ -840,7 +856,7 @@ def simplify(self) -> "CategoricalComfort": ) def heatmap_histogram( - self, series: pd.Series, show_colorbar: bool = True, figsize: tuple[float] = (15, 5) + self, series: pd.Series, show_colorbar: bool = True, figsize: tuple[float] = (15, 5), style_context:str = "python_toolkit.bhom" ) -> Figure: """Create a heatmap histogram. This combines the heatmap and histogram. @@ -851,55 +867,58 @@ def heatmap_histogram( Whether to show the colorbar in the plot. Defaults to True. figsize (tuple[float], optional): Change the figsize. Defaults to (15, 5). + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: Figure: A figure! """ - - fig = plt.figure(figsize=figsize, constrained_layout=True) - spec = fig.add_gridspec( - ncols=1, nrows=2, width_ratios=[1], height_ratios=[5, 2], hspace=0.0 - ) - heatmap_ax = fig.add_subplot(spec[0, 0]) - histogram_ax = fig.add_subplot(spec[1, 0]) - - # Add heatmap - self.annual_heatmap(series, ax=heatmap_ax, show_colorbar=False) - # Add stacked plot - self.annual_monthly_histogram(series=series, ax=histogram_ax, show_labels=True) - - if show_colorbar: - # add colorbar - divider = make_axes_locatable(histogram_ax) - colorbar_ax = divider.append_axes("bottom", size="20%", pad=0.7) - cb = fig.colorbar( - mappable=heatmap_ax.get_children()[0], - cax=colorbar_ax, - orientation="horizontal", - drawedges=False, - extend="both", + + with plt.style.context(style_context): + fig = plt.figure(figsize=figsize, constrained_layout=True) + spec = fig.add_gridspec( + ncols=1, nrows=2, width_ratios=[1], height_ratios=[5, 2], hspace=0.0 ) - cb.outline.set_visible(False) - for bin_name, interval in list(zip(*[self.bin_names, self.interval_index])): - if np.isinf(interval.left): - ha = "right" - position = interval.right - elif np.isinf(interval.right): - ha = "left" - position = interval.left - else: - ha = "center" - position = np.mean([interval.left, interval.right]) - colorbar_ax.text( - position, - 1.05, - textwrap.fill(bin_name, 11), - ha=ha, - va="bottom", - fontsize="x-small", + heatmap_ax = fig.add_subplot(spec[0, 0]) + histogram_ax = fig.add_subplot(spec[1, 0]) + + # Add heatmap + self.annual_heatmap(series, ax=heatmap_ax, show_colorbar=False, style_context=style_context) + # Add stacked plot + self.annual_monthly_histogram(series=series, ax=histogram_ax, show_labels=True, style_context=style_context) + + if show_colorbar: + # add colorbar + divider = make_axes_locatable(histogram_ax) + colorbar_ax = divider.append_axes("bottom", size="20%", pad=0.7) + cb = fig.colorbar( + mappable=heatmap_ax.get_children()[0], + cax=colorbar_ax, + orientation="horizontal", + drawedges=False, + extend="both", ) - - heatmap_ax.set_title(f"{self.name}\n{series.name}", y=1, ha="left", va="bottom", x=0) + cb.outline.set_visible(False) + for bin_name, interval in list(zip(*[self.bin_names, self.interval_index])): + if np.isinf(interval.left): + ha = "right" + position = interval.right + elif np.isinf(interval.right): + ha = "left" + position = interval.left + else: + ha = "center" + position = np.mean([interval.left, interval.right]) + colorbar_ax.text( + position, + 1.05, + textwrap.fill(bin_name, 11), + ha=ha, + va="bottom", + fontsize="x-small", + ) + + heatmap_ax.set_title(f"{self.name}\n{series.name}", y=1, ha="left", va="bottom", x=0) return fig diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/external_comfort/_externalcomfortbase.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/external_comfort/_externalcomfortbase.py index 9632177f..9bbd4897 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/external_comfort/_externalcomfortbase.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/external_comfort/_externalcomfortbase.py @@ -278,7 +278,7 @@ def to_dataframe(self) -> pd.DataFrame: return pd.concat([simulation_result_df, obj_df], axis=1) def plot_utci_day_comfort_metrics( - self, ax: plt.Axes = None, month: int = 3, day: int = 21 + self, ax: plt.Axes = None, month: int = 3, day: int = 21, style_context:str = "python_toolkit.bhom" ) -> plt.Axes: """Plot a single day UTCI and composite components @@ -304,12 +304,14 @@ def plot_utci_day_comfort_metrics( month=month, day=day, title=self.description(), + style_context=style_context ) def plot_utci_heatmap( self, ax: plt.Axes = None, utci_categories: Categorical = UTCI_DEFAULT_CATEGORIES, + style_context:str = "python_toolkit.bhom" ) -> plt.Axes: """Create a heatmap showing the annual hourly UTCI values associated with this Typology. @@ -317,6 +319,8 @@ def plot_utci_heatmap( ax (plt.Axes, optional): A matplotlib Axes object to plot on. Defaults to None. utci_categories (Categorical, optional): The UTCI categories to use. Defaults to UTCI_DEFAULT_CATEGORIES. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: A matplotlib Axes object. @@ -326,6 +330,7 @@ def plot_utci_heatmap( series=collection_to_series(self.universal_thermal_climate_index), ax=ax, title=self.description(), + style_context=style_context ) def walkability_time_limits(self): @@ -365,6 +370,10 @@ def plot_walkability_heatmap( ax (plt.Axes, optional): A matplotlib Axes object to plot on. Defaults to None. utci_categories (Categorical, optional): The UTCI categories to use. Defaults to UTCI_DEFAULT_CATEGORIES. + **kwargs + Additional keyword arguments to pass to the heatmap function. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: A matplotlib Axes object. """ @@ -393,6 +402,8 @@ def plot_utci_heatmap_histogram( UTCI_DEFAULT_CATEGORIES. **kwargs: Additional keyword arguments to pass to the heatmap function. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: Figure: A matplotlib Figure object. """ @@ -426,6 +437,8 @@ def plot_utci_histogram( Typology and AnalysisPeriod. **kwargs: Additional keyword arguments to pass to the histogram function. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: A matplotlib Axes object. @@ -452,6 +465,7 @@ def plot_utci_distance_to_comfortable( ax: plt.Axes = None, comfort_thresholds: tuple[float] = (9, 26), distance_to_comfort_band_centroid: bool = True, + style_context:str = "python_toolkit.bhom" ) -> Figure: """Create a heatmap showing the "distance" in C from the "no thermal stress" UTCI comfort band. @@ -462,6 +476,8 @@ def plot_utci_distance_to_comfortable( Defaults to [9, 26]. distance_to_comfort_band_centroid (bool, optional): Set to True to calculate the distance to the centroid of the comfort band. Defaults to True. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: Axes: A matplotlib Axes object. @@ -480,6 +496,7 @@ def plot_utci_distance_to_comfortable( title=f"{self.description()}\nDistance to comfortable", vmin=-10, vmax=10, + style_context=style_context ) def plot_dbt_heatmap(self, **kwargs) -> plt.Axes: @@ -488,6 +505,8 @@ def plot_dbt_heatmap(self, **kwargs) -> plt.Axes: Args: **kwargs: Additional keyword arguments to pass to the heatmap function. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: Figure: A matplotlib Figure object. @@ -506,6 +525,8 @@ def plot_rh_heatmap(self, **kwargs) -> plt.Axes: Args: **kwargs: Additional keyword arguments to pass to the heatmap function. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: Figure: A matplotlib Figure object. @@ -524,6 +545,8 @@ def plot_ws_heatmap(self, **kwargs) -> plt.Axes: Args: **kwargs: Additional keyword arguments to pass to the heatmap function. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: Figure: A matplotlib Figure object. @@ -542,6 +565,8 @@ def plot_mrt_heatmap(self, **kwargs) -> plt.Axes: Args: **kwargs: Additional keyword arguments to pass to the heatmap function. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: Figure: A matplotlib Figure object. diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/external_comfort/_shelterbase.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/external_comfort/_shelterbase.py index a7e2e7a6..a7c7ac37 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/external_comfort/_shelterbase.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/external_comfort/_shelterbase.py @@ -750,79 +750,83 @@ def annual_wind_speed( @bhom_analytics() def visualise( self, - ax: plt.Axes = None, + ax: mplot3d.Axes3D = None, tri_kwargs: dict[str, Any] = None, lim_kwargs: dict[str, tuple[float]] = None, - ) -> Figure: + style_context: str = "python_toolkit.bhom" + ) -> mplot3d.Axes3D: """Visualise this shelter to check validity and that it exists where you think it should! Args: - ax (plt.Axes, optional): - A matplotlib axes object. Defaults to None. + ax (mplot3d.Axes3D, optional): + A matplotlib3D axes object. Defaults to None. tri_kwargs: Additional keyword arguments to pass to the Poly3DCollection (shelter) render object. lim_kwargs: Additional keyword arguments to pass to the x/y/z lims of the axes. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom """ - if ax is None: - fig = plt.figure() - ax = mplot3d.Axes3D(fig) - fig.add_axes(ax) - if not isinstance(ax, mplot3d.Axes3D): - raise ValueError("ax must be a 3D matplotlib axes object") + with plt.style.context(style_context): + if ax is None: + fig = plt.figure() + ax = mplot3d.Axes3D(fig) + fig.add_axes(ax) + if not isinstance(ax, mplot3d.Axes3D): + raise ValueError("ax must be a 3D matplotlib axes object") - if tri_kwargs is None: - tri_kwargs = {} - if lim_kwargs is None: - lim_kwargs = {} + if tri_kwargs is None: + tri_kwargs = {} + if lim_kwargs is None: + lim_kwargs = {} - # TODO - make this use the Ladybug-Matplotlib renderer when it is ready + # TODO - make this use the Ladybug-Matplotlib renderer when it is ready - ax.scatter(*SENSOR_LOCATION.to_array(), c="red") + ax.scatter(*SENSOR_LOCATION.to_array(), c="red") - # add shelter as a polygon - vtx = np.array([i.to_array() for i in self.face3d.vertices]) - tri = mplot3d.art3d.Poly3DCollection([vtx]) - tri.set_color( - tri_kwargs.get( - "color", + # add shelter as a polygon + vtx = np.array([i.to_array() for i in self.face3d.vertices]) + tri = mplot3d.art3d.Poly3DCollection([vtx]) + tri.set_color( tri_kwargs.get( - "c", tri_kwargs.get("fc", tri_kwargs.get("facecolor", "grey")) - ), + "color", + tri_kwargs.get( + "c", tri_kwargs.get("fc", tri_kwargs.get("facecolor", "grey")) + ), + ) ) - ) - tri.set_alpha( - tri_kwargs.get( - "alpha", - tri_kwargs.get("a", 0.5), + tri.set_alpha( + tri_kwargs.get( + "alpha", + tri_kwargs.get("a", 0.5), + ) ) - ) - tri.set_edgecolor( - tri_kwargs.get( - "edgecolor", - tri_kwargs.get("ec", "k"), + tri.set_edgecolor( + tri_kwargs.get( + "edgecolor", + tri_kwargs.get("ec", "k"), + ) ) - ) - ax.add_collection3d(tri) + ax.add_collection3d(tri) - # format axes - ax.set_xlabel("x") - ax.set_ylabel("y") - ax.set_zlabel("z") + # format axes + ax.set_xlabel("x") + ax.set_ylabel("y") + ax.set_zlabel("z") - # set lims - ax.set_xlim(lim_kwargs.get("xlim", (-10, 10))) - ax.set_ylim(lim_kwargs.get("ylim", (-10, 10))) + # set lims + ax.set_xlim(lim_kwargs.get("xlim", (-10, 10))) + ax.set_ylim(lim_kwargs.get("ylim", (-10, 10))) - # pylint: disable=no-member - ax.set_zlim(lim_kwargs.get("zlim", (0, 20))) - # pylint: enable=no-member + # pylint: disable=no-member + ax.set_zlim(lim_kwargs.get("zlim", (0, 20))) + # pylint: enable=no-member - ax.set_aspect("equal") + ax.set_aspect("equal") - return fig + return ax @bhom_analytics() diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_condensation_potential.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_condensation_potential.py index fdfff6ce..048b4a85 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_condensation_potential.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_condensation_potential.py @@ -39,6 +39,8 @@ def condensation_potential( condensation potential calculation. Defaults to 0.9. **kwargs: A set of kwargs to pass to plt.plot. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: The plt.Axes object populated with the plot. @@ -60,110 +62,113 @@ def condensation_potential( raise ValueError( "The dry bulb temperature and dew point temperature must be time series" ) - - if ax is None: - ax = plt.gca() - - dbt_color = "red" if "dbt_color" not in kwargs else kwargs["dbt_color"] - kwargs.pop("dbt_color", None) - dpt_color = "blue" if "dpt_color" not in kwargs else kwargs["dpt_color"] - kwargs.pop("dpt_color", None) - potential_color = ( - "orange" if "risk_color" not in kwargs else kwargs["potential_color"] - ) - kwargs.pop("potential_color", None) - - ax.set_title( - create_title( - kwargs.pop("title", None), - "Condensation potential", - ) - ) - - # prepare data - df = pd.concat( - [dry_bulb_temperature, dew_point_temperature], axis=1, keys=["dbt", "dpt"] - ) - dbt = df.dbt.groupby([df.index.month, df.index.hour], axis=0).quantile(dbt_quantile) - dpt = df.dpt.groupby([df.index.month, df.index.hour], axis=0).quantile(dpt_quantile) - - # plot values - for n, i in enumerate(range(len(dbt) + 1)[::24]): - if n == len(range(len(dbt) + 1)[::24]) - 1: - continue - - # get local values - x = np.array(range(len(dbt) + 1)[i : i + 25]) - dbt_y = np.array( - (dbt.values.tolist() + [dbt.values[0]])[i : i + 24] - + [(dbt.values.tolist() + [dbt.values[0]])[i : i + 24][0]] + + style_context = kwargs.pop("style_context", "python_toolkit.bhom") + + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() + + dbt_color = "red" if "dbt_color" not in kwargs else kwargs["dbt_color"] + kwargs.pop("dbt_color", None) + dpt_color = "blue" if "dpt_color" not in kwargs else kwargs["dpt_color"] + kwargs.pop("dpt_color", None) + potential_color = ( + "orange" if "risk_color" not in kwargs else kwargs["potential_color"] ) - dpt_y = np.array( - (dpt.values.tolist() + [dpt.values[0]])[i : i + 24] - + [(dpt.values.tolist() + [dpt.values[0]])[i : i + 24][0]] + kwargs.pop("potential_color", None) + + ax.set_title( + create_title( + kwargs.pop("title", None), + "Condensation potential", + ) ) - # dbt line - ax.plot( - x, - dbt_y, - c=dbt_color, - label=f"Dry bulb temperature ({dbt_quantile:0.0%}-ile)" - if n == 0 - else "_nolegend_", + # prepare data + df = pd.concat( + [dry_bulb_temperature, dew_point_temperature], axis=1, keys=["dbt", "dpt"] ) - # dpt line - ax.plot( - x, - dpt_y, - c=dpt_color, - label=f"Dew-point temperature ({dpt_quantile:0.0%}-ile)" - if n == 0 - else "_nolegend_", + dbt = df.dbt.groupby([df.index.month, df.index.hour], axis=0).quantile(dbt_quantile) + dpt = df.dpt.groupby([df.index.month, df.index.hour], axis=0).quantile(dpt_quantile) + + # plot values + for n, i in enumerate(range(len(dbt) + 1)[::24]): + if n == len(range(len(dbt) + 1)[::24]) - 1: + continue + + # get local values + x = np.array(range(len(dbt) + 1)[i : i + 25]) + dbt_y = np.array( + (dbt.values.tolist() + [dbt.values[0]])[i : i + 24] + + [(dbt.values.tolist() + [dbt.values[0]])[i : i + 24][0]] + ) + dpt_y = np.array( + (dpt.values.tolist() + [dpt.values[0]])[i : i + 24] + + [(dpt.values.tolist() + [dpt.values[0]])[i : i + 24][0]] + ) + + # dbt line + ax.plot( + x, + dbt_y, + c=dbt_color, + label=f"Dry bulb temperature ({dbt_quantile:0.0%}-ile)" + if n == 0 + else "_nolegend_", + ) + # dpt line + ax.plot( + x, + dpt_y, + c=dpt_color, + label=f"Dew-point temperature ({dpt_quantile:0.0%}-ile)" + if n == 0 + else "_nolegend_", + ) + # potential ranges + ax.fill_between( + x, + dbt_y, + dpt_y, + where=dbt_y < dpt_y, + color=potential_color, + label="Highest condensation potential" if n == 0 else "_nolegend_", + ) + + ax.text( + 1, + 1, + ( + f"{(dbt.values < dpt.values).sum() / len(dbt):0.1%} of annual hours " + f"with potential for condensation\n(using {dbt_quantile:0.0%}-ile DBT " + f"and {dpt_quantile:0.0%}-ile DPT)" + ), + ha="right", + va="top", + transform=ax.transAxes, ) - # potential ranges - ax.fill_between( - x, - dbt_y, - dpt_y, - where=dbt_y < dpt_y, - color=potential_color, - label="Highest condensation potential" if n == 0 else "_nolegend_", + major_ticks = range(len(dbt))[::12] + minor_ticks = range(len(dbt))[::6] + major_ticklabels = [] + for i in dbt.index: + if i[1] == 0: + major_ticklabels.append(f"{calendar.month_abbr[i[0]]}") + elif i[1] == 12: + major_ticklabels.append("") + + ax.set_xlim(0, len(dbt)) + ax.xaxis.set_major_locator(mticker.FixedLocator(major_ticks)) + ax.xaxis.set_minor_locator(mticker.FixedLocator(minor_ticks)) + ax.set_xticklabels( + major_ticklabels, + minor=False, + ha="left", ) - ax.text( - 1, - 1, - ( - f"{(dbt.values < dpt.values).sum() / len(dbt):0.1%} of annual hours " - f"with potential for condensation\n(using {dbt_quantile:0.0%}-ile DBT " - f"and {dpt_quantile:0.0%}-ile DPT)" - ), - ha="right", - va="top", - transform=ax.transAxes, - ) - major_ticks = range(len(dbt))[::12] - minor_ticks = range(len(dbt))[::6] - major_ticklabels = [] - for i in dbt.index: - if i[1] == 0: - major_ticklabels.append(f"{calendar.month_abbr[i[0]]}") - elif i[1] == 12: - major_ticklabels.append("") - - ax.set_xlim(0, len(dbt)) - ax.xaxis.set_major_locator(mticker.FixedLocator(major_ticks)) - ax.xaxis.set_minor_locator(mticker.FixedLocator(minor_ticks)) - ax.set_xticklabels( - major_ticklabels, - minor=False, - ha="left", - ) - - # print - ax.set_ylabel("Temperature (°C)") - - ax.legend(loc="upper left", bbox_to_anchor=(0, 1)) + # print + ax.set_ylabel("Temperature (°C)") + + ax.legend(loc="upper left", bbox_to_anchor=(0, 1)) return ax diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_degree_days.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_degree_days.py index 1b917d5b..3f6ab274 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_degree_days.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_degree_days.py @@ -31,6 +31,8 @@ def cooling_degree_days( Whether to show the labels on the bars. Defaults to True. **kwargs: Additional keyword arguments to pass to the matplotlib bar plot. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: Figure: @@ -39,63 +41,66 @@ def cooling_degree_days( if not isinstance(epw, EPW): raise ValueError("epw is not an EPW object.") - - if ax is None: - ax = plt.gca() - - temp = degree_time([epw], return_type="days", cool_base=cool_base) - - temp = temp.droplevel(0, axis=1).resample("MS").sum() - temp.index = [calendar.month_abbr[i] for i in temp.index.month] - clg = temp.filter(regex="Cooling") - - color = kwargs.pop("color", "#00A9E0") - - clg.plot(ax=ax, kind="bar", color=color, **kwargs) - ax.set_ylabel(clg.columns[0]) - plt.setp(ax.get_xticklabels(), rotation=0, ha="center") - ax.grid(visible=True, which="major", axis="both", ls="--", lw=1, alpha=0.2) - - ax.text( - 1, - 1, - f"Annual: {sum(rect.get_height() for rect in ax.patches):0.0f} days", - transform=ax.transAxes, - ha="right", - ) - - if show_labels: - max_height = max(v.get_height() for v in ax.containers[0]) - for i, c in enumerate(ax.containers): - label_colors = [contrasting_color(i.get_facecolor()) for i in c.patches] - labels = [ - f"{v.get_height():0.0f}" if v.get_height() > 0.1 * max_height else "" - for v in c - ] - ax.bar_label( - c, - labels=labels, - label_type="edge", - color=label_colors[i], - # fontsize="small", - fontweight="bold", - padding=-15, - ) - labels = [ - f"{v.get_height():0.0f}" if v.get_height() <= 0.1 * max_height else "" - for v in c - ] - ax.bar_label( - c, - labels=labels, - label_type="edge", - # color=label_colors[i], - # fontsize="small", - fontweight="bold", - # padding=-15, - ) - - plt.tight_layout() + + style_context = kwargs.pop("style_context", "python_toolkit.bhom") + + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() + + temp = degree_time([epw], return_type="days", cool_base=cool_base) + + temp = temp.droplevel(0, axis=1).resample("MS").sum() + temp.index = [calendar.month_abbr[i] for i in temp.index.month] + clg = temp.filter(regex="Cooling") + + color = kwargs.pop("color", "#00A9E0") + + clg.plot(ax=ax, kind="bar", color=color, **kwargs) + ax.set_ylabel(clg.columns[0]) + plt.setp(ax.get_xticklabels(), rotation=0, ha="center") + ax.grid(visible=True, which="major", axis="both", ls="--", lw=1, alpha=0.2) + + ax.text( + 1, + 1, + f"Annual: {sum(rect.get_height() for rect in ax.patches):0.0f} days", + transform=ax.transAxes, + ha="right", + ) + + if show_labels: + max_height = max(v.get_height() for v in ax.containers[0]) + for i, c in enumerate(ax.containers): + label_colors = [contrasting_color(i.get_facecolor()) for i in c.patches] + labels = [ + f"{v.get_height():0.0f}" if v.get_height() > 0.1 * max_height else "" + for v in c + ] + ax.bar_label( + c, + labels=labels, + label_type="edge", + color=label_colors[i], + # fontsize="small", + fontweight="bold", + padding=-15, + ) + labels = [ + f"{v.get_height():0.0f}" if v.get_height() <= 0.1 * max_height else "" + for v in c + ] + ax.bar_label( + c, + labels=labels, + label_type="edge", + # color=label_colors[i], + # fontsize="small", + fontweight="bold", + # padding=-15, + ) + + plt.tight_layout() return ax @@ -122,6 +127,8 @@ def heating_degree_days( **kwargs: Additional keyword arguments to pass to the matplotlib bar plot. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: Figure: @@ -130,63 +137,66 @@ def heating_degree_days( if not isinstance(epw, EPW): raise ValueError("epw is not an EPW object.") - - if ax is None: - ax = plt.gca() - - temp = degree_time([epw], return_type="days", heat_base=heat_base) - - temp = temp.droplevel(0, axis=1).resample("MS").sum() - temp.index = [calendar.month_abbr[i] for i in temp.index.month] - data = temp.filter(regex="Heating") - - color = kwargs.pop("color", "#D50032") - - data.plot(ax=ax, kind="bar", color=color, **kwargs) - ax.set_ylabel(data.columns[0]) - plt.setp(ax.get_xticklabels(), rotation=0, ha="center") - ax.grid(visible=True, which="major", axis="both", ls="--", lw=1, alpha=0.2) - - ax.text( - 1, - 1, - f"Annual: {sum(rect.get_height() for rect in ax.patches):0.0f} days", - transform=ax.transAxes, - ha="right", - ) - - if show_labels: - max_height = max(v.get_height() for v in ax.containers[0]) - for i, c in enumerate(ax.containers): - label_colors = [contrasting_color(i.get_facecolor()) for i in c.patches] - labels = [ - f"{v.get_height():0.0f}" if v.get_height() > 0.1 * max_height else "" - for v in c - ] - ax.bar_label( - c, - labels=labels, - label_type="edge", - color=label_colors[i], - # fontsize="small", - fontweight="bold", - padding=-15, - ) - labels = [ - f"{v.get_height():0.0f}" if v.get_height() <= 0.1 * max_height else "" - for v in c - ] - ax.bar_label( - c, - labels=labels, - label_type="edge", - # color=label_colors[i], - # fontsize="small", - fontweight="bold", - # padding=-15, - ) - - plt.tight_layout() + + style_context = kwargs.pop("style_context", "python_toolkit.bhom") + + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() + + temp = degree_time([epw], return_type="days", heat_base=heat_base) + + temp = temp.droplevel(0, axis=1).resample("MS").sum() + temp.index = [calendar.month_abbr[i] for i in temp.index.month] + data = temp.filter(regex="Heating") + + color = kwargs.pop("color", "#D50032") + + data.plot(ax=ax, kind="bar", color=color, **kwargs) + ax.set_ylabel(data.columns[0]) + plt.setp(ax.get_xticklabels(), rotation=0, ha="center") + ax.grid(visible=True, which="major", axis="both", ls="--", lw=1, alpha=0.2) + + ax.text( + 1, + 1, + f"Annual: {sum(rect.get_height() for rect in ax.patches):0.0f} days", + transform=ax.transAxes, + ha="right", + ) + + if show_labels: + max_height = max(v.get_height() for v in ax.containers[0]) + for i, c in enumerate(ax.containers): + label_colors = [contrasting_color(i.get_facecolor()) for i in c.patches] + labels = [ + f"{v.get_height():0.0f}" if v.get_height() > 0.1 * max_height else "" + for v in c + ] + ax.bar_label( + c, + labels=labels, + label_type="edge", + color=label_colors[i], + # fontsize="small", + fontweight="bold", + padding=-15, + ) + labels = [ + f"{v.get_height():0.0f}" if v.get_height() <= 0.1 * max_height else "" + for v in c + ] + ax.bar_label( + c, + labels=labels, + label_type="edge", + # color=label_colors[i], + # fontsize="small", + fontweight="bold", + # padding=-15, + ) + + plt.tight_layout() return ax @@ -209,6 +219,8 @@ def degree_days(epw: EPW, heat_base: float = 18, cool_base: float = 23, **kwargs The color of the cooling degree days bars. figsize (Tuple[float]): The size of the figure. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: Figure: @@ -218,10 +230,13 @@ def degree_days(epw: EPW, heat_base: float = 18, cool_base: float = 23, **kwargs figsize = kwargs.pop("figsize", (15, 6)) heat_color = kwargs.pop("heat_color", "#D50032") cool_color = kwargs.pop("cool_color", "#00A9E0") + + style_context = kwargs.get("style_context", "python_toolkit.bhom") - fig, ax = plt.subplots(nrows=2, figsize=figsize) - heating_degree_days(epw, ax=ax[0], heat_base=heat_base, color=heat_color, **kwargs) - cooling_degree_days(epw, ax=ax[1], cool_base=cool_base, color=cool_color, **kwargs) + with plt.style.context(style_context): + fig, ax = plt.subplots(nrows=2, figsize=figsize) + heating_degree_days(epw, ax=ax[0], heat_base=heat_base, color=heat_color, **kwargs) + cooling_degree_days(epw, ax=ax[1], cool_base=cool_base, color=cool_color, **kwargs) - ax[0].set_title(f"{location_to_string(epw.location)} degree days", x=0, ha="left") + ax[0].set_title(f"{location_to_string(epw.location)} degree days", x=0, ha="left") return fig diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_evaporative_cooling_potential.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_evaporative_cooling_potential.py index ad05fe38..7baac9e6 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_evaporative_cooling_potential.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_evaporative_cooling_potential.py @@ -29,6 +29,8 @@ def evaporative_cooling_potential( The matplotlib axes to plot the figure on. Defaults to None. **kwargs: Additional keyword arguments to pass to the heatmap function. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: The matplotlib axes. @@ -39,35 +41,38 @@ def evaporative_cooling_potential( if any(dbt.index != dpt.index): raise ValueError("The indices of the two series must be the same.") - - if ax is None: - ax = plt.gca() - - ecp = (dbt - dpt).clip(lower=0).rename("Evaporative Cooling Potential (C)") - - if agg_year: - # check for presence of Feb 29 in ecp index - if len(ecp[(ecp.index.month == 2) & (ecp.index.day == 29)]) != 0: - idx = pd.date_range(start="2016-01-01", periods=8784, freq="h") - else: - idx = pd.date_range(start="2017-01-01", periods=8760, freq="h") - - ecp = ecp.groupby([ecp.index.month, ecp.index.day, ecp.index.hour]).agg(agg) - ecp.index = idx - - if "cmap" not in kwargs: - kwargs["cmap"] = "GnBu" - - heatmap(series=ecp, ax=ax, **kwargs) - ax.text( - 1, - 1, - "*values shown indicate cooling effect from saturating air with moisture (DBT - DPT)", - transform=ax.transAxes, - ha="right", - va="top", - fontsize="small", - ) + + style_context = kwargs.get("style_context", "python_toolkit.bhom") + + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() + + ecp = (dbt - dpt).clip(lower=0).rename("Evaporative Cooling Potential (C)") + + if agg_year: + # check for presence of Feb 29 in ecp index + if len(ecp[(ecp.index.month == 2) & (ecp.index.day == 29)]) != 0: + idx = pd.date_range(start="2016-01-01", periods=8784, freq="h") + else: + idx = pd.date_range(start="2017-01-01", periods=8760, freq="h") + + ecp = ecp.groupby([ecp.index.month, ecp.index.day, ecp.index.hour]).agg(agg) + ecp.index = idx + + if "cmap" not in kwargs: + kwargs["cmap"] = "GnBu" + + heatmap(series=ecp, ax=ax, **kwargs) + ax.text( + 1, + 1, + "*values shown indicate cooling effect from saturating air with moisture (DBT - DPT)", + transform=ax.transAxes, + ha="right", + va="top", + fontsize="small", + ) return ax @@ -82,21 +87,18 @@ def evaporative_cooling_potential_epw(epw: EPW, ax: plt.Axes = None, **kwargs) - The matplotlib axes to plot the figure on. Defaults to None. **kwargs: Additional keyword arguments to pass to the heatmap function. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: The matplotlib axes. """ - - if ax is None: - ax = plt.gca() - evaporative_cooling_potential( dbt=collection_to_series(epw.dry_bulb_temperature), dpt=collection_to_series(epw.dew_point_temperature), ax=ax, + title=kwargs.pop("title", Path(epw.file_path).name) **kwargs ) - ax.set_title(Path(epw.file_path).name) - return ax diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_misc.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_misc.py index e34ee6a3..2da3a5cb 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_misc.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_misc.py @@ -17,7 +17,7 @@ from ..ladybug_extension.datacollection import collection_to_series -def cloud_cover_categories(epw: EPW, ax: plt.Axes = None) -> plt.Axes: +def cloud_cover_categories(epw: EPW, ax: plt.Axes = None, style_context:str = "python_tookit.bhom") -> plt.Axes: """Plot cloud cover categories from an EPW file. Args: @@ -25,54 +25,57 @@ def cloud_cover_categories(epw: EPW, ax: plt.Axes = None) -> plt.Axes: The EPW file to plot. ax (plt.Axes, optional): A matploltib axes to plot on. Defaults to None. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: The matplotlib axes. """ - if ax is None: - ax = plt.gca() - - s = collection_to_series(epw.opaque_sky_cover) - mtx = s.groupby([s.index.month, s.index.day]).value_counts().unstack().fillna(0) - mapper = { - 0: "clear", - 1: "clear", - 2: "mostly clear", - 3: "mostly clear", - 4: "partly cloudy", - 5: "partly cloudy", - 6: "mostly cloudy", - 7: "mostly cloudy", - 8: "overcast", - 9: "overcast", - 10: "overcast", - } - mtx = mtx.T.groupby(mapper).sum()[ - ["clear", "mostly clear", "partly cloudy", "mostly cloudy", "overcast"] - ] - mtx = (mtx.T / mtx.sum(axis=1)).T - - mtx.plot.area(ax=ax, color=["#95B5DF", "#B1C4DD", "#C9D0D9", "#ACB0B6", "#989CA1"]) - ax.yaxis.set_major_formatter(mtick.PercentFormatter(1)) - ax.set_xticks( - ticks=[n for n, i in enumerate(mtx.index) if i[1] == 1], - labels=[month_abbr[i] for i in range(1, 13, 1)], - ha="left", - ) - ax.set_xlim(0, len(mtx)) - ax.set_ylim(0, 1) - ax.legend( - bbox_to_anchor=(0.5, -0.05), - loc="upper center", - ncol=5, - title="Cloud cover categories", - ) + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() + + s = collection_to_series(epw.opaque_sky_cover) + mtx = s.groupby([s.index.month, s.index.day]).value_counts().unstack().fillna(0) + mapper = { + 0: "clear", + 1: "clear", + 2: "mostly clear", + 3: "mostly clear", + 4: "partly cloudy", + 5: "partly cloudy", + 6: "mostly cloudy", + 7: "mostly cloudy", + 8: "overcast", + 9: "overcast", + 10: "overcast", + } + mtx = mtx.T.groupby(mapper).sum()[ + ["clear", "mostly clear", "partly cloudy", "mostly cloudy", "overcast"] + ] + mtx = (mtx.T / mtx.sum(axis=1)).T + + mtx.plot.area(ax=ax, color=["#95B5DF", "#B1C4DD", "#C9D0D9", "#ACB0B6", "#989CA1"]) + ax.yaxis.set_major_formatter(mtick.PercentFormatter(1)) + ax.set_xticks( + ticks=[n for n, i in enumerate(mtx.index) if i[1] == 1], + labels=[month_abbr[i] for i in range(1, 13, 1)], + ha="left", + ) + ax.set_xlim(0, len(mtx)) + ax.set_ylim(0, 1) + ax.legend( + bbox_to_anchor=(0.5, -0.05), + loc="upper center", + ncol=5, + title="Cloud cover categories", + ) return ax -def hours_sunlight(location: Location, ax: plt.Axes = None) -> plt.Axes: +def hours_sunlight(location: Location, ax: plt.Axes = None, style_context:str = "python_toolkit.bhom") -> plt.Axes: """Plot the hours of sunlight for a location. Args: @@ -80,6 +83,8 @@ def hours_sunlight(location: Location, ax: plt.Axes = None) -> plt.Axes: The location to plot. ax (plt.Axes, optional): A matploltib axes to plot on. Defaults to None. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: @@ -125,72 +130,76 @@ def hours_sunlight(location: Location, ax: plt.Axes = None) -> plt.Axes: [daylight, civil_twilight, nautical_twilight, astronomical_twilight, night], axis=1, ) - ax = plt.gca() - ax.stackplot( - daylight.index, - temp.values.T, - colors=["#FCE49D", "#B9AC86", "#908A7A", "#817F76", "#717171"], - labels=temp.columns, - ) - ax.set_title("Hours of daylight and twilight") - ax.set_ylim(0, 24) - ax.set_xlim(temp.index.min(), temp.index.max()) - ax.set_ylabel("Hours") - - # plot min/max/med days - ax.plot(daylight, c="k") - - ax.scatter(temp.daylight.idxmax(), temp.daylight.max(), c="k", s=10) - ax.text( - temp.daylight.idxmax(), - temp.daylight.max() - 0.5, - f"Summer solstice\n{np.floor(temp.daylight.max()):0.0f} hrs, {(temp.daylight.max() % 1) * 60:0.0f} mins\n{temp.daylight.idxmax():%d %b}", - ha="center", - va="top", - ) - ax.scatter(temp.daylight.idxmin(), temp.daylight.min(), c="k", s=10) - ax.text( - temp.daylight.idxmin(), - temp.daylight.min() - 0.5, - f"Winter solstice\n{np.floor(temp.daylight.min()):0.0f} hrs, {(temp.daylight.min() % 1) * 60:0.0f} mins\n{temp.daylight.idxmin():%d %b}", - ha="right" if temp.daylight.idxmin().month > 6 else "left", - va="top", - ) - - equionoosss = abs((temp.daylight - temp.daylight.median())).sort_values() - ax.scatter(equionoosss.index[0], temp.daylight[equionoosss.index[0]], c="k", s=10) - ax.text( - equionoosss.index[0], - temp.daylight[equionoosss.index[0]] - 0.5, - f"Equinox\n{np.floor(temp.daylight[equionoosss.index[0]]):0.0f} hrs, {(temp.daylight[equionoosss.index[0]] % 1) * 60:0.0f} mins\n{equionoosss.index[0]:%d %b}", - ha="right" if equionoosss.index[0].month > 6 else "left", - va="top", - ) - - ix = None - for ix in equionoosss.index: - if ix.month != equionoosss.index[0].month: - break - ax.scatter(ix, temp.daylight[ix], c="k", s=10) - ax.text( - ix, - temp.daylight[ix] - 0.5, - f"Equinox\n{np.floor(temp.daylight[ix]):0.0f} hrs, {(temp.daylight[ix] % 1) * 60:0.0f} mins\n{ix:%d %b}", - ha="right" if ix.month > 6 else "left", - va="top", - ) - ax.legend( - bbox_to_anchor=(0.5, -0.05), - loc="upper center", - ncol=5, - title="Day period", - ) + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() + + ax.stackplot( + daylight.index, + temp.values.T, + colors=["#FCE49D", "#B9AC86", "#908A7A", "#817F76", "#717171"], + labels=temp.columns, + ) + ax.set_title("Hours of daylight and twilight") + ax.set_ylim(0, 24) + ax.set_xlim(temp.index.min(), temp.index.max()) + ax.set_ylabel("Hours") + + # plot min/max/med days + ax.plot(daylight, c="k") + + ax.scatter(temp.daylight.idxmax(), temp.daylight.max(), c="k", s=10) + ax.text( + temp.daylight.idxmax(), + temp.daylight.max() - 0.5, + f"Summer solstice\n{np.floor(temp.daylight.max()):0.0f} hrs, {(temp.daylight.max() % 1) * 60:0.0f} mins\n{temp.daylight.idxmax():%d %b}", + ha="center", + va="top", + ) + + ax.scatter(temp.daylight.idxmin(), temp.daylight.min(), c="k", s=10) + ax.text( + temp.daylight.idxmin(), + temp.daylight.min() - 0.5, + f"Winter solstice\n{np.floor(temp.daylight.min()):0.0f} hrs, {(temp.daylight.min() % 1) * 60:0.0f} mins\n{temp.daylight.idxmin():%d %b}", + ha="right" if temp.daylight.idxmin().month > 6 else "left", + va="top", + ) + + equionoosss = abs((temp.daylight - temp.daylight.median())).sort_values() + ax.scatter(equionoosss.index[0], temp.daylight[equionoosss.index[0]], c="k", s=10) + ax.text( + equionoosss.index[0], + temp.daylight[equionoosss.index[0]] - 0.5, + f"Equinox\n{np.floor(temp.daylight[equionoosss.index[0]]):0.0f} hrs, {(temp.daylight[equionoosss.index[0]] % 1) * 60:0.0f} mins\n{equionoosss.index[0]:%d %b}", + ha="right" if equionoosss.index[0].month > 6 else "left", + va="top", + ) + + ix = None + for ix in equionoosss.index: + if ix.month != equionoosss.index[0].month: + break + ax.scatter(ix, temp.daylight[ix], c="k", s=10) + ax.text( + ix, + temp.daylight[ix] - 0.5, + f"Equinox\n{np.floor(temp.daylight[ix]):0.0f} hrs, {(temp.daylight[ix] % 1) * 60:0.0f} mins\n{ix:%d %b}", + ha="right" if ix.month > 6 else "left", + va="top", + ) + ax.legend( + bbox_to_anchor=(0.5, -0.05), + loc="upper center", + ncol=5, + title="Day period", + ) return ax -def hours_sunrise_sunset(location: Location, ax: plt.Axes = None) -> plt.Axes: +def hours_sunrise_sunset(location: Location, ax: plt.Axes = None, style_context:str = "python_toolkit.bhom") -> plt.Axes: """Plot the hours of sunrise and sunset for a location. Args: @@ -198,202 +207,205 @@ def hours_sunrise_sunset(location: Location, ax: plt.Axes = None) -> plt.Axes: The location to plot. ax (plt.Axes, optional): A matploltib axes to plot on. Defaults to None. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: The matplotlib axes. """ - if ax is None: - ax = plt.gca() - - srss_df = sunrise_sunset(location) - seconds = srss_df.map(lambda a: ((a - a.normalize()) / pd.Timedelta("1 second"))) - hours = seconds / (60 * 60) + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() + + srss_df = sunrise_sunset(location) + seconds = srss_df.map(lambda a: ((a - a.normalize()) / pd.Timedelta("1 second"))) + hours = seconds / (60 * 60) + + ## hours of daylight + daylight = pd.Series( + [i.seconds / (60 * 60) for i in (srss_df["sunset"] - srss_df["sunrise"])], + name="daylight", + index=srss_df.index, + ) + civil_twilight = ( + [ + i.seconds / (60 * 60) + for i in (srss_df["civil twilight end"] - srss_df["civil twilight start"]) + ] + - daylight + ).rename("civil twilight") + nautical_twilight = ( + [ + i.seconds / (60 * 60) + for i in (srss_df["nautical twilight end"] - srss_df["nautical twilight start"]) + ] + - daylight + - civil_twilight + ).rename("nautical twilight") + astronomical_twilight = ( + [ + i.seconds / (60 * 60) + for i in (srss_df["astronomical twilight end"] - srss_df["astronomical twilight start"]) + ] + - daylight + - civil_twilight + - nautical_twilight + ).rename("astronomical twilight") + night = (24 - (daylight + civil_twilight + nautical_twilight + astronomical_twilight)).rename( + "night" + ) + + temp = pd.concat( + [daylight, civil_twilight, nautical_twilight, astronomical_twilight, night], + axis=1, + ) - ## hours of daylight - daylight = pd.Series( - [i.seconds / (60 * 60) for i in (srss_df["sunset"] - srss_df["sunrise"])], - name="daylight", - index=srss_df.index, - ) - civil_twilight = ( - [ - i.seconds / (60 * 60) - for i in (srss_df["civil twilight end"] - srss_df["civil twilight start"]) - ] - - daylight - ).rename("civil twilight") - nautical_twilight = ( - [ - i.seconds / (60 * 60) - for i in (srss_df["nautical twilight end"] - srss_df["nautical twilight start"]) - ] - - daylight - - civil_twilight - ).rename("nautical twilight") - astronomical_twilight = ( - [ - i.seconds / (60 * 60) - for i in (srss_df["astronomical twilight end"] - srss_df["astronomical twilight start"]) - ] - - daylight - - civil_twilight - - nautical_twilight - ).rename("astronomical twilight") - night = (24 - (daylight + civil_twilight + nautical_twilight + astronomical_twilight)).rename( - "night" - ) - - temp = pd.concat( - [daylight, civil_twilight, nautical_twilight, astronomical_twilight, night], - axis=1, - ) - - ax = plt.gca() - ax.fill_between( - hours.index, - np.zeros_like(hours["astronomical twilight start"]), - hours["astronomical twilight start"], - fc="#717171", - label="Night", - ) - ax.fill_between( - hours.index, - hours["astronomical twilight start"], - hours["nautical twilight start"], - fc="#817F76", - label="Astonomical twilight", - ) - ax.fill_between( - hours.index, - hours["nautical twilight start"], - hours["civil twilight start"], - fc="#908A7A", - label="Nautical twilight", - ) - ax.fill_between( - hours.index, - hours["civil twilight start"], - hours["sunrise"], - fc="#B9AC86", - label="Civil twilight", - ) - ax.fill_between(hours.index, hours["sunrise"], hours["sunset"], fc="#FCE49D", label="Day") - ax.fill_between( - hours.index, - hours["sunset"], - hours["civil twilight end"], - fc="#B9AC86", - label="__nolegend__", - ) - ax.fill_between( - hours.index, - hours["civil twilight end"], - hours["nautical twilight end"], - fc="#908A7A", - label="__nolegend__", - ) - ax.fill_between( - hours.index, - hours["nautical twilight end"], - hours["astronomical twilight end"], - fc="#817F76", - label="__nolegend__", - ) - ax.fill_between( - hours.index, - hours["astronomical twilight end"], - np.ones_like(hours["astronomical twilight start"]) * 24, - fc="#717171", - label="__nolegend__", - ) - - [ax.plot(hours.index, hours[i], c="k", lw=1) for i in ["sunrise", "noon", "sunset"]] - - ax.axvline(temp.daylight.idxmax(), c="k", ls=":", alpha=0.5) - ax.text( - temp.daylight.idxmax(), - hours.noon.loc[temp.daylight.idxmax()] + 0.5, - f"Summer solstice ({temp.daylight.idxmax():%d %b})\n{np.floor(temp.daylight.max()):02.0f} hrs, {(temp.daylight.max() % 1) * 60:02.0f} mins", - ha="center", - va="bottom", - ) - ax.text( - temp.daylight.idxmax(), - hours.sunrise.loc[temp.daylight.idxmax()] + 0.5, - f"Sunrise\n{int(np.floor((hours.sunrise.loc[temp.daylight.idxmax()]))):02.0f}:{(hours.sunrise.loc[temp.daylight.idxmax()] % 1) * 60:02.0f}", - ha="center", - va="bottom", - ) - ax.text( - temp.daylight.idxmax(), - hours.sunset.loc[temp.daylight.idxmax()] + 0.5, - f"Sunset\n{int(np.floor((hours.sunset.loc[temp.daylight.idxmax()]))):02.0f}:{(hours.sunset.loc[temp.daylight.idxmax()] % 1) * 60:02.0f}", - ha="center", - va="bottom", - ) - - ax.axvline(temp.daylight.idxmin(), c="k", ls=":", alpha=0.5) - ax.text( - temp.daylight.idxmin(), - hours.noon.loc[temp.daylight.idxmin()] + 0.5, - f"Winter solstice ({temp.daylight.idxmin():%d %b})\n{np.floor(temp.daylight.min()):02.0f} hrs, {(temp.daylight.min() % 1) * 60:02.0f} mins", - ha="right" if temp.daylight.idxmin().month > 6 else "left", - va="bottom", - ) - ax.text( - temp.daylight.idxmin(), - hours.sunrise.loc[temp.daylight.idxmin()] + 0.5, - f"Sunrise\n{int(np.floor((hours.sunrise.loc[temp.daylight.idxmin()]))):02.0f}:{(hours.sunrise.loc[temp.daylight.idxmin()] % 1) * 60:02.0f}", - ha="right" if temp.daylight.idxmin().month > 6 else "left", - va="bottom", - ) - ax.text( - temp.daylight.idxmin(), - hours.sunset.loc[temp.daylight.idxmin()] + 0.5, - f"Sunset\n{int(np.floor((hours.sunset.loc[temp.daylight.idxmin()]))):02.0f}:{(hours.sunset.loc[temp.daylight.idxmin()] % 1) * 60:02.0f}", - ha="right" if temp.daylight.idxmin().month > 6 else "left", - va="bottom", - ) - - equionoosss = abs((temp.daylight - temp.daylight.median())).sort_values() - ax.axvline(equionoosss.index[0], c="k", ls=":", alpha=0.5) - ax.text( - equionoosss.index[0], - hours.noon.loc[temp.daylight.idxmin()] + 0.5, - f"Equinox ({equionoosss.index[0]:%d %b})\n{np.floor(temp.daylight[equionoosss.index[0]]):0.0f} hrs, {(temp.daylight[equionoosss.index[0]] % 1) * 60:0.0f} mins", - ha="right" if equionoosss.index[0].month > 6 else "left", - va="bottom", - ) - - ix = None - for ix in equionoosss.index: - if ix.month != equionoosss.index[0].month: - break - ax.axvline(ix, c="k", ls=":", alpha=0.5) - ax.text( - ix, - hours.noon.loc[ix] + 0.5, - f"Equinox ({ix:%d %b})\n{np.floor(temp.daylight[ix]):0.0f} hrs, {(temp.daylight[ix] % 1) * 60:0.0f} mins", - ha="right" if ix.month > 6 else "left", - va="bottom", - ) - - ax.set_title("Sunrise and sunset") - ax.set_ylim(0, 24) - ax.set_xlim(hours.index.min(), hours.index.max()) - ax.set_ylabel("Hours") - ax.legend( - bbox_to_anchor=(0.5, -0.05), - loc="upper center", - ncol=5, - title="Day period", - ) + ax = plt.gca() + ax.fill_between( + hours.index, + np.zeros_like(hours["astronomical twilight start"]), + hours["astronomical twilight start"], + fc="#717171", + label="Night", + ) + ax.fill_between( + hours.index, + hours["astronomical twilight start"], + hours["nautical twilight start"], + fc="#817F76", + label="Astonomical twilight", + ) + ax.fill_between( + hours.index, + hours["nautical twilight start"], + hours["civil twilight start"], + fc="#908A7A", + label="Nautical twilight", + ) + ax.fill_between( + hours.index, + hours["civil twilight start"], + hours["sunrise"], + fc="#B9AC86", + label="Civil twilight", + ) + ax.fill_between(hours.index, hours["sunrise"], hours["sunset"], fc="#FCE49D", label="Day") + ax.fill_between( + hours.index, + hours["sunset"], + hours["civil twilight end"], + fc="#B9AC86", + label="__nolegend__", + ) + ax.fill_between( + hours.index, + hours["civil twilight end"], + hours["nautical twilight end"], + fc="#908A7A", + label="__nolegend__", + ) + ax.fill_between( + hours.index, + hours["nautical twilight end"], + hours["astronomical twilight end"], + fc="#817F76", + label="__nolegend__", + ) + ax.fill_between( + hours.index, + hours["astronomical twilight end"], + np.ones_like(hours["astronomical twilight start"]) * 24, + fc="#717171", + label="__nolegend__", + ) + + [ax.plot(hours.index, hours[i], c="k", lw=1) for i in ["sunrise", "noon", "sunset"]] + + ax.axvline(temp.daylight.idxmax(), c="k", ls=":", alpha=0.5) + ax.text( + temp.daylight.idxmax(), + hours.noon.loc[temp.daylight.idxmax()] + 0.5, + f"Summer solstice ({temp.daylight.idxmax():%d %b})\n{np.floor(temp.daylight.max()):02.0f} hrs, {(temp.daylight.max() % 1) * 60:02.0f} mins", + ha="center", + va="bottom", + ) + ax.text( + temp.daylight.idxmax(), + hours.sunrise.loc[temp.daylight.idxmax()] + 0.5, + f"Sunrise\n{int(np.floor((hours.sunrise.loc[temp.daylight.idxmax()]))):02.0f}:{(hours.sunrise.loc[temp.daylight.idxmax()] % 1) * 60:02.0f}", + ha="center", + va="bottom", + ) + ax.text( + temp.daylight.idxmax(), + hours.sunset.loc[temp.daylight.idxmax()] + 0.5, + f"Sunset\n{int(np.floor((hours.sunset.loc[temp.daylight.idxmax()]))):02.0f}:{(hours.sunset.loc[temp.daylight.idxmax()] % 1) * 60:02.0f}", + ha="center", + va="bottom", + ) + + ax.axvline(temp.daylight.idxmin(), c="k", ls=":", alpha=0.5) + ax.text( + temp.daylight.idxmin(), + hours.noon.loc[temp.daylight.idxmin()] + 0.5, + f"Winter solstice ({temp.daylight.idxmin():%d %b})\n{np.floor(temp.daylight.min()):02.0f} hrs, {(temp.daylight.min() % 1) * 60:02.0f} mins", + ha="right" if temp.daylight.idxmin().month > 6 else "left", + va="bottom", + ) + ax.text( + temp.daylight.idxmin(), + hours.sunrise.loc[temp.daylight.idxmin()] + 0.5, + f"Sunrise\n{int(np.floor((hours.sunrise.loc[temp.daylight.idxmin()]))):02.0f}:{(hours.sunrise.loc[temp.daylight.idxmin()] % 1) * 60:02.0f}", + ha="right" if temp.daylight.idxmin().month > 6 else "left", + va="bottom", + ) + ax.text( + temp.daylight.idxmin(), + hours.sunset.loc[temp.daylight.idxmin()] + 0.5, + f"Sunset\n{int(np.floor((hours.sunset.loc[temp.daylight.idxmin()]))):02.0f}:{(hours.sunset.loc[temp.daylight.idxmin()] % 1) * 60:02.0f}", + ha="right" if temp.daylight.idxmin().month > 6 else "left", + va="bottom", + ) + + equionoosss = abs((temp.daylight - temp.daylight.median())).sort_values() + ax.axvline(equionoosss.index[0], c="k", ls=":", alpha=0.5) + ax.text( + equionoosss.index[0], + hours.noon.loc[temp.daylight.idxmin()] + 0.5, + f"Equinox ({equionoosss.index[0]:%d %b})\n{np.floor(temp.daylight[equionoosss.index[0]]):0.0f} hrs, {(temp.daylight[equionoosss.index[0]] % 1) * 60:0.0f} mins", + ha="right" if equionoosss.index[0].month > 6 else "left", + va="bottom", + ) + + ix = None + for ix in equionoosss.index: + if ix.month != equionoosss.index[0].month: + break + ax.axvline(ix, c="k", ls=":", alpha=0.5) + ax.text( + ix, + hours.noon.loc[ix] + 0.5, + f"Equinox ({ix:%d %b})\n{np.floor(temp.daylight[ix]):0.0f} hrs, {(temp.daylight[ix] % 1) * 60:0.0f} mins", + ha="right" if ix.month > 6 else "left", + va="bottom", + ) + + ax.set_title("Sunrise and sunset") + ax.set_ylim(0, 24) + ax.set_xlim(hours.index.min(), hours.index.max()) + ax.set_ylabel("Hours") + ax.legend( + bbox_to_anchor=(0.5, -0.05), + loc="upper center", + ncol=5, + title="Day period", + ) return ax -def solar_elevation_azimuth(location: Location, ax: plt.Axes = None) -> plt.Axes: +def solar_elevation_azimuth(location: Location, ax: plt.Axes = None, style_context:str = "python_toolkit.bhom") -> plt.Axes: """Plot the solar elevation and azimuth for a location. Args: @@ -401,6 +413,8 @@ def solar_elevation_azimuth(location: Location, ax: plt.Axes = None) -> plt.Axes The location to plot. ax (plt.Axes, optional): A matploltib axes to plot on. Defaults to None. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: @@ -456,39 +470,40 @@ def solar_elevation_azimuth(location: Location, ax: plt.Axes = None) -> plt.Axes ], name="directions", ) + + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() - if ax is None: - ax = plt.gca() + sp = Sunpath.from_location(location) + idx = pd.date_range("2017-01-01 00:00:00", "2018-01-01 00:00:00", freq="10min") + suns = [sp.calculate_sun_from_date_time(i) for i in idx] + a = pd.DataFrame(index=idx) + a["altitude"] = [i.altitude for i in suns] + a["azimuth"] = [i.azimuth for i in suns] - sp = Sunpath.from_location(location) - idx = pd.date_range("2017-01-01 00:00:00", "2018-01-01 00:00:00", freq="10min") - suns = [sp.calculate_sun_from_date_time(i) for i in idx] - a = pd.DataFrame(index=idx) - a["altitude"] = [i.altitude for i in suns] - a["azimuth"] = [i.azimuth for i in suns] - - ax = plt.gca() - heatmap( - a.azimuth, - ax=ax, - cmap=cat.cmap, - norm=cat.norm, - title="Solar Elevation and Azimuth", - ) - cb = ax.collections[-1].colorbar - cb.set_ticks( - [0, 45, 90, 135, 180, 225, 270, 315, 360], - labels=["N", "NE", "E", "SE", "S", "SW", "W", "NW", "N"], - ) - # create matrix of monthday/hour for pcolormesh - pvt = a.pivot_table(columns=a.index.date, index=a.index.time) - - # plot the contours for sun positions - x = mdates.date2num(pvt["altitude"].columns) - y = mdates.date2num(pd.to_datetime([f"2017-01-01 {i}" for i in pvt.index])) - z = pvt["altitude"].values - # z = np.ma.masked_array(z, mask=z < 0) - ct = ax.contour(x, y, z, colors="k", levels=np.arange(0, 91, 10)) - ax.clabel(ct, inline=1, fontsize="small") + ax = plt.gca() + heatmap( + a.azimuth, + ax=ax, + cmap=cat.cmap, + norm=cat.norm, + title="Solar Elevation and Azimuth", + ) + cb = ax.collections[-1].colorbar + cb.set_ticks( + [0, 45, 90, 135, 180, 225, 270, 315, 360], + labels=["N", "NE", "E", "SE", "S", "SW", "W", "NW", "N"], + ) + # create matrix of monthday/hour for pcolormesh + pvt = a.pivot_table(columns=a.index.date, index=a.index.time) + + # plot the contours for sun positions + x = mdates.date2num(pvt["altitude"].columns) + y = mdates.date2num(pd.to_datetime([f"2017-01-01 {i}" for i in pvt.index])) + z = pvt["altitude"].values + # z = np.ma.masked_array(z, mask=z < 0) + ct = ax.contour(x, y, z, colors="k", levels=np.arange(0, 91, 10)) + ax.clabel(ct, inline=1, fontsize="small") return ax diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_monthly_histogram_proportion.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_monthly_histogram_proportion.py index 2eda84b9..9988bbc8 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_monthly_histogram_proportion.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_monthly_histogram_proportion.py @@ -40,6 +40,8 @@ def monthly_histogram_proportion( Whether to show the legend. Defaults to False. **kwargs: Additional keyword arguments to pass to plt.bar. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: @@ -47,56 +49,59 @@ def monthly_histogram_proportion( """ validate_timeseries(series) + + style_context = kwargs.pop("style_context", "python_toolkit.bhom") - if ax is None: - ax = plt.gca() + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() - t = pd.cut(series, bins=bins, labels=labels) - t = t.groupby([t.index.year, t.index.month, t], observed=True).count().unstack().T - t = t / t.sum() + t = pd.cut(series, bins=bins, labels=labels) + t = t.groupby([t.index.year, t.index.month, t], observed=True).count().unstack().T + t = t / t.sum() - # adjust column labels - if show_year_in_label: - t.columns = [ - f"{year}\n{calendar.month_abbr[month]}" for year, month in t.columns.values - ] - else: - t.columns = [f"{calendar.month_abbr[month]}" for _, month in t.columns.values] - - t.T.plot.bar( - ax=ax, - stacked=True, - legend=False, - width=1, - **kwargs, - ) - ax.set_xlim(-0.5, len(t.columns) - 0.5) - ax.set_ylim(0, 1) - plt.setp(ax.get_xticklabels(), ha="center", rotation=0) - for spine in ["top", "right", "left", "bottom"]: - ax.spines[spine].set_visible(False) - ax.yaxis.set_major_formatter(mticker.PercentFormatter(1)) + # adjust column labels + if show_year_in_label: + t.columns = [ + f"{year}\n{calendar.month_abbr[month]}" for year, month in t.columns.values + ] + else: + t.columns = [f"{calendar.month_abbr[month]}" for _, month in t.columns.values] - if show_legend: - ax.legend( - bbox_to_anchor=(1, 1), - loc="upper left", - borderaxespad=0.0, - frameon=False, + t.T.plot.bar( + ax=ax, + stacked=True, + legend=False, + width=1, + **kwargs, ) + ax.set_xlim(-0.5, len(t.columns) - 0.5) + ax.set_ylim(0, 1) + plt.setp(ax.get_xticklabels(), ha="center", rotation=0) + for spine in ["top", "right", "left", "bottom"]: + ax.spines[spine].set_visible(False) + ax.yaxis.set_major_formatter(mticker.PercentFormatter(1)) - if show_labels: - for i, c in enumerate(ax.containers): - label_colors = [contrasting_color(i.get_facecolor()) for i in c.patches] - labels = [ - f"{v.get_height():0.1%}" if v.get_height() > 0.1 else "" for v in c - ] - ax.bar_label( - c, - labels=labels, - label_type="center", - color=label_colors[i], - fontsize="x-small", + if show_legend: + ax.legend( + bbox_to_anchor=(1, 1), + loc="upper left", + borderaxespad=0.0, + frameon=False, ) + if show_labels: + for i, c in enumerate(ax.containers): + label_colors = [contrasting_color(i.get_facecolor()) for i in c.patches] + labels = [ + f"{v.get_height():0.1%}" if v.get_height() > 0.1 else "" for v in c + ] + ax.bar_label( + c, + labels=labels, + label_type="center", + color=label_colors[i], + fontsize="x-small", + ) + return ax diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_psychrometric.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_psychrometric.py index e9d6ce18..1c98a4e6 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_psychrometric.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_psychrometric.py @@ -151,6 +151,7 @@ def psychrometric( wet_bulb: bool = False, psychro_polygons: PsychrometricPolygons = None, figsize: tuple[float, float] = (10, 7), + style_context: str = "python_toolkit.bhom" ) -> plt.Figure: """Create a psychrometric chart using a LB backend. @@ -172,6 +173,8 @@ def psychrometric( figsize (tuple[float, float], optional): A tuple of floats for the figure size. Default is (10, 7). + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Figure: @@ -209,545 +212,547 @@ def lb_mesh_to_patch_collection( return patch_collection p = lb_mesh_to_patch_collection(psychart.colored_mesh, psychart.hour_values) - fig, ax = plt.subplots(1, 1, figsize=figsize) - collections = ax.add_collection(p) - if wet_bulb: - # add wet-bulb lines - for i in psychart.wb_lines: + with plt.style.context(style_context): + fig, ax = plt.subplots(1, 1, figsize=figsize) + collections = ax.add_collection(p) + + if wet_bulb: + # add wet-bulb lines + for i in psychart.wb_lines: + ax.plot(*np.array(i.to_array()).T, c="k", ls=":", lw=0.5, alpha=0.5) + for pt, txt in list(zip(*[psychart.wb_label_points, psychart.wb_labels])): + _x, _y = pt.to_array() + ax.text(_x, _y, txt, ha="right", va="bottom", fontsize="x-small") + else: + # add enthalpy lines + for i in psychart.enthalpy_lines: + ax.plot(*np.array(i.to_array()).T, c="k", ls=":", lw=0.5, alpha=0.5) + for pt, txt in list( + zip(*[psychart.enthalpy_label_points, psychart.enthalpy_labels]) + ): + _x, _y = pt.to_array() + ax.text(_x, _y, txt, ha="right", va="bottom", fontsize="x-small") + + # add hr lines + for i in psychart.hr_lines: ax.plot(*np.array(i.to_array()).T, c="k", ls=":", lw=0.5, alpha=0.5) - for pt, txt in list(zip(*[psychart.wb_label_points, psychart.wb_labels])): + for pt, txt in list(zip(*[psychart.hr_label_points, psychart.hr_labels])): _x, _y = pt.to_array() - ax.text(_x, _y, txt, ha="right", va="bottom", fontsize="x-small") - else: - # add enthalpy lines - for i in psychart.enthalpy_lines: + ax.text(_x, _y, txt, ha="left", va="center", fontsize="small") + + # add rh lines + for i in psychart.rh_lines: + ax.plot(*np.array(i.to_array()).T, c="k", ls=":", lw=0.5, alpha=0.5) + for n, (pt, txt) in enumerate( + list(zip(*[psychart.rh_label_points, psychart.rh_labels])) + ): + if n % 2 == 0: + continue + _x, _y = pt.to_array() + ax.text(_x, _y, txt, ha="right", va="center", fontsize="x-small") + + # add dbt lines + for i in psychart.temperature_lines: ax.plot(*np.array(i.to_array()).T, c="k", ls=":", lw=0.5, alpha=0.5) for pt, txt in list( - zip(*[psychart.enthalpy_label_points, psychart.enthalpy_labels]) + zip(*[psychart.temperature_label_points, psychart.temperature_labels]) ): _x, _y = pt.to_array() - ax.text(_x, _y, txt, ha="right", va="bottom", fontsize="x-small") - - # add hr lines - for i in psychart.hr_lines: - ax.plot(*np.array(i.to_array()).T, c="k", ls=":", lw=0.5, alpha=0.5) - for pt, txt in list(zip(*[psychart.hr_label_points, psychart.hr_labels])): - _x, _y = pt.to_array() - ax.text(_x, _y, txt, ha="left", va="center", fontsize="small") - - # add rh lines - for i in psychart.rh_lines: - ax.plot(*np.array(i.to_array()).T, c="k", ls=":", lw=0.5, alpha=0.5) - for n, (pt, txt) in enumerate( - list(zip(*[psychart.rh_label_points, psychart.rh_labels])) - ): - if n % 2 == 0: - continue - _x, _y = pt.to_array() - ax.text(_x, _y, txt, ha="right", va="center", fontsize="x-small") - - # add dbt lines - for i in psychart.temperature_lines: - ax.plot(*np.array(i.to_array()).T, c="k", ls=":", lw=0.5, alpha=0.5) - for pt, txt in list( - zip(*[psychart.temperature_label_points, psychart.temperature_labels]) - ): - _x, _y = pt.to_array() - ax.text(_x, _y, txt, ha="center", va="top", fontsize="small") - - # add x axis label - _x, _y = psychart.x_axis_location.to_array() - ax.text(_x, _y - 1, psychart.x_axis_text, ha="left", va="top", fontsize="large") - - # add y axis label - _x, _y = psychart.y_axis_location.to_array() - ax.text( - _x + 2, - _y, - psychart.y_axis_text, - ha="right", - va="top", - fontsize="large", - rotation=90, - ) + ax.text(_x, _y, txt, ha="center", va="top", fontsize="small") - # set limits to align - ax.set_xlim(0, 76) - ax.set_ylim(-0.01, 50) + # add x axis label + _x, _y = psychart.x_axis_location.to_array() + ax.text(_x, _y - 1, psychart.x_axis_text, ha="left", va="top", fontsize="large") - ax.axis("off") + # add y axis label + _x, _y = psychart.y_axis_location.to_array() + ax.text( + _x + 2, + _y, + psychart.y_axis_text, + ha="right", + va="top", + fontsize="large", + rotation=90, + ) - ax.set_title( - f"{location_to_string(epw.location)}\n{describe_analysis_period(analysis_period)}" - ) + # set limits to align + ax.set_xlim(0, 76) + ax.set_ylim(-0.01, 50) - # Generate peak cooling summary - clg_vals = df.loc[df.idxmax()["Dry Bulb Temperature (C)"]] - max_dbt_table = ( - f"Peak cooling {clg_vals.name:%b %d %H:%M}\n" - f'WS: {clg_vals["Wind Speed (m/s)"]:>6.1f} m/s\n' - f'WD: {clg_vals["Wind Direction (degrees)"]:>6.1f} deg\n' - f'DBT: {clg_vals["Dry Bulb Temperature (C)"]:>6.1f} °C\n' - f'WBT: {clg_vals["Wet Bulb Temperature (C)"]:>6.1f} °C\n' - f'RH: {clg_vals["Relative Humidity (%)"]:>6.1f} %\n' - f'DPT: {clg_vals["Dew Point Temperature (C)"]:>6.1f} °C\n' - f'h: {clg_vals["Enthalpy (kJ/kg)"]:>6.1f} kJ/kg\n' - f'HR: {clg_vals["Humidity Ratio (fraction)"]:<5.4f} kg/kg' - ) - ax.text( - 0, - 0.98, - max_dbt_table, - transform=ax.transAxes, - ha="left", - va="top", - zorder=8, - fontsize="x-small", - color="#555555", - **{"fontname": "monospace"}, - ) + ax.axis("off") - # Generate peak heating summary - htg_vals = df.loc[df.idxmin()["Dry Bulb Temperature (C)"]] - min_dbt_table = ( - f"Peak heating {htg_vals.name:%b %d %H:%M}\n" - f'WS: {htg_vals["Wind Speed (m/s)"]:>6.1f} m/s\n' - f'WD: {htg_vals["Wind Direction (degrees)"]:>6.1f} deg\n' - f'DBT: {htg_vals["Dry Bulb Temperature (C)"]:>6.1f} °C\n' - f'WBT: {htg_vals["Wet Bulb Temperature (C)"]:>6.1f} °C\n' - f'RH: {htg_vals["Relative Humidity (%)"]:>6.1f} %\n' - f'DPT: {htg_vals["Dew Point Temperature (C)"]:>6.1f} °C\n' - f'h: {htg_vals["Enthalpy (kJ/kg)"]:>6.1f} kJ/kg\n' - f'HR: {htg_vals["Humidity Ratio (fraction)"]:<5.4f} kg/kg' - ) - ax.text( - 0, - 0.8, - min_dbt_table, - transform=ax.transAxes, - ha="left", - va="top", - zorder=8, - fontsize="x-small", - color="#555555", - **{"fontname": "monospace"}, - ) + ax.set_title( + f"{location_to_string(epw.location)}\n{describe_analysis_period(analysis_period)}" + ) - # Generate max HumidityRatio summary - hr_vals = df.loc[df.idxmin()["Humidity Ratio (fraction)"]] - max_hr_table = ( - f"Peak heating {hr_vals.name:%b %d %H:%M}\n" - f'WS: {hr_vals["Wind Speed (m/s)"]:>6.1f} m/s\n' - f'WD: {hr_vals["Wind Direction (degrees)"]:>6.1f} deg\n' - f'DBT: {hr_vals["Dry Bulb Temperature (C)"]:>6.1f} °C\n' - f'WBT: {hr_vals["Wet Bulb Temperature (C)"]:>6.1f} °C\n' - f'RH: {hr_vals["Relative Humidity (%)"]:>6.1f} %\n' - f'DPT: {hr_vals["Dew Point Temperature (C)"]:>6.1f} °C\n' - f'h: {hr_vals["Enthalpy (kJ/kg)"]:>6.1f} kJ/kg\n' - f'HR: {hr_vals["Humidity Ratio (fraction)"]:<5.4f} kg/kg' - ) - ax.text( - 0, - 0.62, - max_hr_table, - transform=ax.transAxes, - ha="left", - va="top", - zorder=8, - fontsize="x-small", - color="#555555", - **{"fontname": "monospace"}, - ) + # Generate peak cooling summary + clg_vals = df.loc[df.idxmax()["Dry Bulb Temperature (C)"]] + max_dbt_table = ( + f"Peak cooling {clg_vals.name:%b %d %H:%M}\n" + f'WS: {clg_vals["Wind Speed (m/s)"]:>6.1f} m/s\n' + f'WD: {clg_vals["Wind Direction (degrees)"]:>6.1f} deg\n' + f'DBT: {clg_vals["Dry Bulb Temperature (C)"]:>6.1f} °C\n' + f'WBT: {clg_vals["Wet Bulb Temperature (C)"]:>6.1f} °C\n' + f'RH: {clg_vals["Relative Humidity (%)"]:>6.1f} %\n' + f'DPT: {clg_vals["Dew Point Temperature (C)"]:>6.1f} °C\n' + f'h: {clg_vals["Enthalpy (kJ/kg)"]:>6.1f} kJ/kg\n' + f'HR: {clg_vals["Humidity Ratio (fraction)"]:<5.4f} kg/kg' + ) + ax.text( + 0, + 0.98, + max_dbt_table, + transform=ax.transAxes, + ha="left", + va="top", + zorder=8, + fontsize="x-small", + color="#555555", + **{"fontname": "monospace"}, + ) - # Generate max enthalpy summary - enth_vals = df.loc[df.idxmin()["Enthalpy (kJ/kg)"]] - max_enthalpy_table = ( - f"Peak enthalpy {enth_vals.name:%b %d %H:%M}\n" - f'WS: {enth_vals["Wind Speed (m/s)"]:>6.1f} m/s\n' - f'WD: {enth_vals["Wind Direction (degrees)"]:>6.1f} deg\n' - f'DBT: {enth_vals["Dry Bulb Temperature (C)"]:>6.1f} °C\n' - f'WBT: {enth_vals["Wet Bulb Temperature (C)"]:>6.1f} °C\n' - f'RH: {enth_vals["Relative Humidity (%)"]:>6.1f} %\n' - f'DPT: {enth_vals["Dew Point Temperature (C)"]:>6.1f} °C\n' - f'h: {enth_vals["Enthalpy (kJ/kg)"]:>6.1f} kJ/kg\n' - f'HR: {enth_vals["Humidity Ratio (fraction)"]:<5.4f} kg/kg' - ) - ax.text( - 0, - 0.44, - max_enthalpy_table, - transform=ax.transAxes, - ha="left", - va="top", - zorder=8, - fontsize="x-small", - color="#555555", - **{"fontname": "monospace"}, - ) + # Generate peak heating summary + htg_vals = df.loc[df.idxmin()["Dry Bulb Temperature (C)"]] + min_dbt_table = ( + f"Peak heating {htg_vals.name:%b %d %H:%M}\n" + f'WS: {htg_vals["Wind Speed (m/s)"]:>6.1f} m/s\n' + f'WD: {htg_vals["Wind Direction (degrees)"]:>6.1f} deg\n' + f'DBT: {htg_vals["Dry Bulb Temperature (C)"]:>6.1f} °C\n' + f'WBT: {htg_vals["Wet Bulb Temperature (C)"]:>6.1f} °C\n' + f'RH: {htg_vals["Relative Humidity (%)"]:>6.1f} %\n' + f'DPT: {htg_vals["Dew Point Temperature (C)"]:>6.1f} °C\n' + f'h: {htg_vals["Enthalpy (kJ/kg)"]:>6.1f} kJ/kg\n' + f'HR: {htg_vals["Humidity Ratio (fraction)"]:<5.4f} kg/kg' + ) + ax.text( + 0, + 0.8, + min_dbt_table, + transform=ax.transAxes, + ha="left", + va="top", + zorder=8, + fontsize="x-small", + color="#555555", + **{"fontname": "monospace"}, + ) - # add legend - keys = ( - "WS: Wind speed | WD: Wind direction | DBT: Dry-bulb temperature | WBT: Wet-bulb temperature\n" - "RH: Relative humidity | DPT: Dew-point temperature | h: Enthalpy | HR: Humidity ratio" - ) - ax.text( - 1, - -0.05, - keys, - transform=ax.transAxes, - ha="right", - va="top", - zorder=8, - fontsize="xx-small", - color="#555555", - **{"fontname": "monospace"}, - ) + # Generate max HumidityRatio summary + hr_vals = df.loc[df.idxmin()["Humidity Ratio (fraction)"]] + max_hr_table = ( + f"Peak heating {hr_vals.name:%b %d %H:%M}\n" + f'WS: {hr_vals["Wind Speed (m/s)"]:>6.1f} m/s\n' + f'WD: {hr_vals["Wind Direction (degrees)"]:>6.1f} deg\n' + f'DBT: {hr_vals["Dry Bulb Temperature (C)"]:>6.1f} °C\n' + f'WBT: {hr_vals["Wet Bulb Temperature (C)"]:>6.1f} °C\n' + f'RH: {hr_vals["Relative Humidity (%)"]:>6.1f} %\n' + f'DPT: {hr_vals["Dew Point Temperature (C)"]:>6.1f} °C\n' + f'h: {hr_vals["Enthalpy (kJ/kg)"]:>6.1f} kJ/kg\n' + f'HR: {hr_vals["Humidity Ratio (fraction)"]:<5.4f} kg/kg' + ) + ax.text( + 0, + 0.62, + max_hr_table, + transform=ax.transAxes, + ha="left", + va="top", + zorder=8, + fontsize="x-small", + color="#555555", + **{"fontname": "monospace"}, + ) - cbar = plt.colorbar(collections) - cbar.outline.set_visible(False) - cbar.set_label("Hours") - - # add polygon if polgyon passed - if psychro_polygons is not None: - polygon_data = [] - polygon_names = [] - - def line_objs_to_vertices( - lines: list[Polyline2D | LineSegment2D], - ) -> list[list[float]]: - # ensure input list is flat - lines = [ - v - for item in lines - for v in (item if isinstance(item, list) else [item]) - ] - - # iterate list to obtain point2d objects - point_2ds = [] - for line_obj in lines: - if isinstance(line_obj, LineSegment2D): - point_2ds.extend(i.to_array() for i in line_obj.vertices) - if isinstance(line_obj, Polyline2D): - point_2ds.extend(i.to_array() for i in line_obj.vertices) - - # remove duplicates - vvertices = [] - for v in point_2ds: - if v not in vvertices: - vvertices.append(v) - - # obtain any LineSegment2D objects - return vvertices - - def process_polygon(polygon_name, polygon): - """Process a strategy polygon that does not require any special treatment.""" - - if polygon is not None: - strategy_poly = line_objs_to_vertices(polygon) - dat = poly_obj.evaluate_polygon(polygon, 0.01) - dat = ( - dat[0] - if len(dat) == 1 - else poly_obj.create_collection(dat, polygon_name) - ) - else: - strategy_warning(polygon_name) - return None, None, None - - return polygon_name, strategy_poly, dat - - def polygon_area(xs: list[float], ys: list[float]) -> list[float]: - """https://en.wikipedia.org/wiki/Centroid#Of_a_polygon""" - # https://stackoverflow.com/a/30408825/7128154 - return 0.5 * (np.dot(xs, np.roll(ys, 1)) - np.dot(ys, np.roll(xs, 1))) - - def polygon_centroid(xs: list[float], ys: list[float]) -> list[float]: - """https://en.wikipedia.org/wiki/Centroid#Of_a_polygon""" - xy = np.array([xs, ys]) - c = np.dot( - xy + np.roll(xy, 1, axis=1), xs * np.roll(ys, 1) - np.roll(xs, 1) * ys - ) / (6 * polygon_area(xs, ys)) - return c - - def merge_polygon_data(poly_data): - """Merge an array of polygon comfort conditions into a single data list.""" - val_mtx = [dat.values for dat in poly_data] - merged_values = [] - for hr_data in zip(*val_mtx): - hr_val = 1 if 1 in hr_data else 0 - merged_values.append(hr_val) - return merged_values - - poly_obj = PolygonPMV( - psychart, - rad_temperature=[psychro_polygons.mean_radiant_temperature], - air_speed=[psychro_polygons.air_speed], - met_rate=[psychro_polygons.metabolic_rate], - clo_value=[psychro_polygons.clo_value], + # Generate max enthalpy summary + enth_vals = df.loc[df.idxmin()["Enthalpy (kJ/kg)"]] + max_enthalpy_table = ( + f"Peak enthalpy {enth_vals.name:%b %d %H:%M}\n" + f'WS: {enth_vals["Wind Speed (m/s)"]:>6.1f} m/s\n' + f'WD: {enth_vals["Wind Direction (degrees)"]:>6.1f} deg\n' + f'DBT: {enth_vals["Dry Bulb Temperature (C)"]:>6.1f} °C\n' + f'WBT: {enth_vals["Wet Bulb Temperature (C)"]:>6.1f} °C\n' + f'RH: {enth_vals["Relative Humidity (%)"]:>6.1f} %\n' + f'DPT: {enth_vals["Dew Point Temperature (C)"]:>6.1f} °C\n' + f'h: {enth_vals["Enthalpy (kJ/kg)"]:>6.1f} kJ/kg\n' + f'HR: {enth_vals["Humidity Ratio (fraction)"]:<5.4f} kg/kg' + ) + ax.text( + 0, + 0.44, + max_enthalpy_table, + transform=ax.transAxes, + ha="left", + va="top", + zorder=8, + fontsize="x-small", + color="#555555", + **{"fontname": "monospace"}, ) - # add generic comfort polygon - poly = line_objs_to_vertices(poly_obj.merged_comfort_polygon) - dat = poly_obj.merged_comfort_data - name = "Comfort" - polygon_names.append(name) - polygon_data.append(dat) - ax.add_collection( - PatchCollection( - [Polygon(poly)], - fc="none", - ec="black", - zorder=8, - lw=1, - alpha=0.5, - ls="--", - ) + # add legend + keys = ( + "WS: Wind speed | WD: Wind direction | DBT: Dry-bulb temperature | WBT: Wet-bulb temperature\n" + "RH: Relative humidity | DPT: Dew-point temperature | h: Enthalpy | HR: Humidity ratio" ) - xx, yy = polygon_centroid(*np.array(poly).T) ax.text( - xx, - yy, - "\n".join(textwrap.wrap("Comfort", 12)), - fontsize="xx-small", - c="black", - ha="center", - va="center", + 1, + -0.05, + keys, + transform=ax.transAxes, + ha="right", + va="top", zorder=8, + fontsize="xx-small", + color="#555555", + **{"fontname": "monospace"}, ) - # add strategy polygons - if PassiveStrategy.EVAPORATIVE_COOLING in psychro_polygons.strategies: - ec_poly = poly_obj.evaporative_cooling_polygon() - name, poly, dat = process_polygon( - PassiveStrategy.EVAPORATIVE_COOLING.value, ec_poly - ) - if not all(i is None for i in [name, poly, dat]): - polygon_data.append(dat) - polygon_names.append(name) - ax.add_collection( - PatchCollection( - [Polygon(poly)], - fc="none", - ec="blue", - zorder=8, - lw=1, - alpha=0.5, - ls="--", + cbar = plt.colorbar(collections) + cbar.outline.set_visible(False) + cbar.set_label("Hours") + + # add polygon if polgyon passed + if psychro_polygons is not None: + polygon_data = [] + polygon_names = [] + + def line_objs_to_vertices( + lines: list[Polyline2D | LineSegment2D], + ) -> list[list[float]]: + # ensure input list is flat + lines = [ + v + for item in lines + for v in (item if isinstance(item, list) else [item]) + ] + + # iterate list to obtain point2d objects + point_2ds = [] + for line_obj in lines: + if isinstance(line_obj, LineSegment2D): + point_2ds.extend(i.to_array() for i in line_obj.vertices) + if isinstance(line_obj, Polyline2D): + point_2ds.extend(i.to_array() for i in line_obj.vertices) + + # remove duplicates + vvertices = [] + for v in point_2ds: + if v not in vvertices: + vvertices.append(v) + + # obtain any LineSegment2D objects + return vvertices + + def process_polygon(polygon_name, polygon): + """Process a strategy polygon that does not require any special treatment.""" + + if polygon is not None: + strategy_poly = line_objs_to_vertices(polygon) + dat = poly_obj.evaluate_polygon(polygon, 0.01) + dat = ( + dat[0] + if len(dat) == 1 + else poly_obj.create_collection(dat, polygon_name) ) - ) - xx, yy = polygon_centroid(*np.array(poly).T) - ax.text( - xx, - yy, - "\n".join(textwrap.wrap(name, 12)), - fontsize="xx-small", - c="blue", - ha="center", - va="center", + else: + strategy_warning(polygon_name) + return None, None, None + + return polygon_name, strategy_poly, dat + + def polygon_area(xs: list[float], ys: list[float]) -> list[float]: + """https://en.wikipedia.org/wiki/Centroid#Of_a_polygon""" + # https://stackoverflow.com/a/30408825/7128154 + return 0.5 * (np.dot(xs, np.roll(ys, 1)) - np.dot(ys, np.roll(xs, 1))) + + def polygon_centroid(xs: list[float], ys: list[float]) -> list[float]: + """https://en.wikipedia.org/wiki/Centroid#Of_a_polygon""" + xy = np.array([xs, ys]) + c = np.dot( + xy + np.roll(xy, 1, axis=1), xs * np.roll(ys, 1) - np.roll(xs, 1) * ys + ) / (6 * polygon_area(xs, ys)) + return c + + def merge_polygon_data(poly_data): + """Merge an array of polygon comfort conditions into a single data list.""" + val_mtx = [dat.values for dat in poly_data] + merged_values = [] + for hr_data in zip(*val_mtx): + hr_val = 1 if 1 in hr_data else 0 + merged_values.append(hr_val) + return merged_values + + poly_obj = PolygonPMV( + psychart, + rad_temperature=[psychro_polygons.mean_radiant_temperature], + air_speed=[psychro_polygons.air_speed], + met_rate=[psychro_polygons.metabolic_rate], + clo_value=[psychro_polygons.clo_value], + ) + + # add generic comfort polygon + poly = line_objs_to_vertices(poly_obj.merged_comfort_polygon) + dat = poly_obj.merged_comfort_data + name = "Comfort" + polygon_names.append(name) + polygon_data.append(dat) + ax.add_collection( + PatchCollection( + [Polygon(poly)], + fc="none", + ec="black", zorder=8, + lw=1, + alpha=0.5, + ls="--", ) - - if PassiveStrategy.MASS_NIGHT_VENTILATION in psychro_polygons.strategies: - nf_poly = poly_obj.night_flush_polygon( - psychro_polygons.strategy_parameters.day_above_comfort ) - if nf_poly is not None: - name = PassiveStrategy.MASS_NIGHT_VENTILATION.value - poly = line_objs_to_vertices(nf_poly) - dat = poly_obj.evaluate_night_flush_polygon( - nf_poly, - epw.dry_bulb_temperature, - psychro_polygons.strategy_parameters.night_below_comfort, - psychro_polygons.strategy_parameters.time_constant, - 0.01, + xx, yy = polygon_centroid(*np.array(poly).T) + ax.text( + xx, + yy, + "\n".join(textwrap.wrap("Comfort", 12)), + fontsize="xx-small", + c="black", + ha="center", + va="center", + zorder=8, + ) + + # add strategy polygons + if PassiveStrategy.EVAPORATIVE_COOLING in psychro_polygons.strategies: + ec_poly = poly_obj.evaporative_cooling_polygon() + name, poly, dat = process_polygon( + PassiveStrategy.EVAPORATIVE_COOLING.value, ec_poly ) - dat = dat[0] if len(dat) == 1 else poly_obj.create_collection(dat, name) - polygon_data.append(dat) - polygon_names.append(name) - ax.add_collection( - PatchCollection( - [Polygon(poly)], - fc="none", - ec="purple", + if not all(i is None for i in [name, poly, dat]): + polygon_data.append(dat) + polygon_names.append(name) + ax.add_collection( + PatchCollection( + [Polygon(poly)], + fc="none", + ec="blue", + zorder=8, + lw=1, + alpha=0.5, + ls="--", + ) + ) + xx, yy = polygon_centroid(*np.array(poly).T) + ax.text( + xx, + yy, + "\n".join(textwrap.wrap(name, 12)), + fontsize="xx-small", + c="blue", + ha="center", + va="center", zorder=8, - lw=1, - alpha=0.5, - ls="--", ) - ) - xx, yy = polygon_centroid(*np.array(poly).T) - ax.text( - xx, - yy, - "\n".join(textwrap.wrap(name, 12)), - fontsize="xx-small", - c="purple", - ha="center", - va="center", - zorder=8, - ) - else: - strategy_warning(name) - if PassiveStrategy.INTERNAL_HEAT_CAPTURE in psychro_polygons.strategies: - iht_poly = poly_obj.internal_heat_polygon( - psychro_polygons.strategy_parameters.balance_temperature - ) - name, poly, dat = process_polygon( - PassiveStrategy.INTERNAL_HEAT_CAPTURE.value, iht_poly - ) - if not all(i is None for i in [name, poly, dat]): - polygon_data.append(dat) - polygon_names.append(name) - ax.add_collection( - PatchCollection( - [Polygon(poly)], - fc="none", - ec="orange", + if PassiveStrategy.MASS_NIGHT_VENTILATION in psychro_polygons.strategies: + nf_poly = poly_obj.night_flush_polygon( + psychro_polygons.strategy_parameters.day_above_comfort + ) + if nf_poly is not None: + name = PassiveStrategy.MASS_NIGHT_VENTILATION.value + poly = line_objs_to_vertices(nf_poly) + dat = poly_obj.evaluate_night_flush_polygon( + nf_poly, + epw.dry_bulb_temperature, + psychro_polygons.strategy_parameters.night_below_comfort, + psychro_polygons.strategy_parameters.time_constant, + 0.01, + ) + dat = dat[0] if len(dat) == 1 else poly_obj.create_collection(dat, name) + polygon_data.append(dat) + polygon_names.append(name) + ax.add_collection( + PatchCollection( + [Polygon(poly)], + fc="none", + ec="purple", + zorder=8, + lw=1, + alpha=0.5, + ls="--", + ) + ) + xx, yy = polygon_centroid(*np.array(poly).T) + ax.text( + xx, + yy, + "\n".join(textwrap.wrap(name, 12)), + fontsize="xx-small", + c="purple", + ha="center", + va="center", zorder=8, - lw=1, - alpha=0.5, - ls="--", ) + else: + strategy_warning(name) + + if PassiveStrategy.INTERNAL_HEAT_CAPTURE in psychro_polygons.strategies: + iht_poly = poly_obj.internal_heat_polygon( + psychro_polygons.strategy_parameters.balance_temperature ) - xx, yy = polygon_centroid(*np.array(poly).T) - ax.text( - xx, - yy, - "\n".join(textwrap.wrap(name, 12)), - fontsize="xx-small", - c="orange", - ha="center", - va="center", - zorder=8, + name, poly, dat = process_polygon( + PassiveStrategy.INTERNAL_HEAT_CAPTURE.value, iht_poly ) - - if PassiveStrategy.OCCUPANT_FAN_USE in psychro_polygons.strategies: - fan_poly = poly_obj.fan_use_polygon( - psychro_polygons.strategy_parameters.balance_temperature - ) - name, poly, dat = process_polygon( - PassiveStrategy.OCCUPANT_FAN_USE.value, fan_poly - ) - if not all(i is None for i in [name, poly, dat]): - polygon_data.append(dat) - polygon_names.append(name) - ax.add_collection( - PatchCollection( - [Polygon(poly)], - fc="none", - ec="cyan", + if not all(i is None for i in [name, poly, dat]): + polygon_data.append(dat) + polygon_names.append(name) + ax.add_collection( + PatchCollection( + [Polygon(poly)], + fc="none", + ec="orange", + zorder=8, + lw=1, + alpha=0.5, + ls="--", + ) + ) + xx, yy = polygon_centroid(*np.array(poly).T) + ax.text( + xx, + yy, + "\n".join(textwrap.wrap(name, 12)), + fontsize="xx-small", + c="orange", + ha="center", + va="center", zorder=8, - lw=1, - alpha=0.5, - ls="--", ) + + if PassiveStrategy.OCCUPANT_FAN_USE in psychro_polygons.strategies: + fan_poly = poly_obj.fan_use_polygon( + psychro_polygons.strategy_parameters.balance_temperature ) - xx, yy = polygon_centroid(*np.array(poly).T) - ax.text( - xx, - yy, - "\n".join(textwrap.wrap(name, 12)), - fontsize="xx-small", - c="cyan", - ha="center", - va="center", - zorder=8, + name, poly, dat = process_polygon( + PassiveStrategy.OCCUPANT_FAN_USE.value, fan_poly ) - - if PassiveStrategy.PASSIVE_SOLAR_HEATING in psychro_polygons.strategies: - warn( - f"{PassiveStrategy.PASSIVE_SOLAR_HEATING} assumes radiation from " - "skylights only, using global horizontal radiation." - ) - bal_t = ( - psychro_polygons.strategy_parameters.balance_temperature - if PassiveStrategy.INTERNAL_HEAT_CAPTURE in psychro_polygons.strategies - else None - ) - dat, delta = poly_obj.evaluate_passive_solar( - epw.global_horizontal_radiation, - psychro_polygons.strategy_parameters.solar_heat_capacity, - psychro_polygons.strategy_parameters.time_constant, - bal_t, - ) - sol_poly = poly_obj.passive_solar_polygon(delta, bal_t) - if sol_poly is not None: - name = PassiveStrategy.PASSIVE_SOLAR_HEATING.value - poly = line_objs_to_vertices(sol_poly) - dat = dat[0] if len(dat) == 1 else poly_obj.create_collection(dat, name) - polygon_data.append(dat) - polygon_names.append(name) - ax.add_collection( - PatchCollection( - [Polygon(poly)], - fc="none", - ec="red", + if not all(i is None for i in [name, poly, dat]): + polygon_data.append(dat) + polygon_names.append(name) + ax.add_collection( + PatchCollection( + [Polygon(poly)], + fc="none", + ec="cyan", + zorder=8, + lw=1, + alpha=0.5, + ls="--", + ) + ) + xx, yy = polygon_centroid(*np.array(poly).T) + ax.text( + xx, + yy, + "\n".join(textwrap.wrap(name, 12)), + fontsize="xx-small", + c="cyan", + ha="center", + va="center", zorder=8, - lw=1, - alpha=0.5, - ls="--", ) + + if PassiveStrategy.PASSIVE_SOLAR_HEATING in psychro_polygons.strategies: + warn( + f"{PassiveStrategy.PASSIVE_SOLAR_HEATING} assumes radiation from " + "skylights only, using global horizontal radiation." ) - xx, yy = polygon_centroid(*np.array(poly).T) - ax.text( - xx, - yy, - "\n".join(textwrap.wrap(name, 12)), - fontsize="xx-small", - c="red", - ha="center", - va="center", - zorder=8, + bal_t = ( + psychro_polygons.strategy_parameters.balance_temperature + if PassiveStrategy.INTERNAL_HEAT_CAPTURE in psychro_polygons.strategies + else None + ) + dat, delta = poly_obj.evaluate_passive_solar( + epw.global_horizontal_radiation, + psychro_polygons.strategy_parameters.solar_heat_capacity, + psychro_polygons.strategy_parameters.time_constant, + bal_t, ) + sol_poly = poly_obj.passive_solar_polygon(delta, bal_t) + if sol_poly is not None: + name = PassiveStrategy.PASSIVE_SOLAR_HEATING.value + poly = line_objs_to_vertices(sol_poly) + dat = dat[0] if len(dat) == 1 else poly_obj.create_collection(dat, name) + polygon_data.append(dat) + polygon_names.append(name) + ax.add_collection( + PatchCollection( + [Polygon(poly)], + fc="none", + ec="red", + zorder=8, + lw=1, + alpha=0.5, + ls="--", + ) + ) + xx, yy = polygon_centroid(*np.array(poly).T) + ax.text( + xx, + yy, + "\n".join(textwrap.wrap(name, 12)), + fontsize="xx-small", + c="red", + ha="center", + va="center", + zorder=8, + ) + else: + strategy_warning(name) + + # compute total comfort values + polygon_comfort = ( + [dat.average for dat in polygon_data] + if isinstance(polygon_data[0], BaseCollection) + else polygon_data + ) + if isinstance(polygon_data[0], BaseCollection): + merged_vals = merge_polygon_data(polygon_data) + total_comf_data = poly_obj.create_collection(merged_vals, "Total Comfort") + total_comfort = total_comf_data.average else: - strategy_warning(name) - - # compute total comfort values - polygon_comfort = ( - [dat.average for dat in polygon_data] - if isinstance(polygon_data[0], BaseCollection) - else polygon_data - ) - if isinstance(polygon_data[0], BaseCollection): - merged_vals = merge_polygon_data(polygon_data) - total_comf_data = poly_obj.create_collection(merged_vals, "Total Comfort") - total_comfort = total_comf_data.average - else: - total_comf_data = 1 if sum(polygon_data) > 0 else 0 - total_comfort = total_comf_data - polygon_names.insert(0, "Total Comfort") - polygon_comfort.insert(0, total_comfort) - - # add total comfort to chart - comfort_text = [] - for strat, val in list(zip(*[polygon_names, polygon_comfort])): - comfort_text.append(f"{strat+':':<22} {val:>6.1%}") - comfort_text = "\n".join(comfort_text) - settings_text = "\n".join( - [ - # pylint: disable=C0301 - f'MRT: {"DBT" if psychro_polygons.mean_radiant_temperature is None else psychro_polygons.mean_radiant_temperature}', - f" WS: {psychro_polygons.air_speed}m/s", - f"CLO: {psychro_polygons.clo_value}", - f"MET: {psychro_polygons.metabolic_rate}", - # pylint: enable=C0301 - ] - ) - ax.text( - 0.3, - 0.98, - "\n\n".join([settings_text, comfort_text]), - transform=ax.transAxes, - ha="left", - va="top", - zorder=8, - fontsize="x-small", - color="#555555", - **{"fontname": "monospace"}, - ) + total_comf_data = 1 if sum(polygon_data) > 0 else 0 + total_comfort = total_comf_data + polygon_names.insert(0, "Total Comfort") + polygon_comfort.insert(0, total_comfort) + + # add total comfort to chart + comfort_text = [] + for strat, val in list(zip(*[polygon_names, polygon_comfort])): + comfort_text.append(f"{strat+':':<22} {val:>6.1%}") + comfort_text = "\n".join(comfort_text) + settings_text = "\n".join( + [ + # pylint: disable=C0301 + f'MRT: {"DBT" if psychro_polygons.mean_radiant_temperature is None else psychro_polygons.mean_radiant_temperature}', + f" WS: {psychro_polygons.air_speed}m/s", + f"CLO: {psychro_polygons.clo_value}", + f"MET: {psychro_polygons.metabolic_rate}", + # pylint: enable=C0301 + ] + ) + ax.text( + 0.3, + 0.98, + "\n\n".join([settings_text, comfort_text]), + transform=ax.transAxes, + ha="left", + va="top", + zorder=8, + fontsize="x-small", + color="#555555", + **{"fontname": "monospace"}, + ) - plt.tight_layout() + plt.tight_layout() return fig diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_radiant_cooling_potential.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_radiant_cooling_potential.py index 9a6d3870..bcea281f 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_radiant_cooling_potential.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_radiant_cooling_potential.py @@ -19,75 +19,80 @@ def radiant_cooling_potential( The matplotlib axes to plot the figure on. Defaults to None. **kwargs: Additional keyword arguments to pass to matplotlib.pyplot.plot. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: The matplotlib axes. """ + + style_context = kwargs.pop("style_context", "python_toolkit.bhom") - if ax is None: - ax = plt.gca() + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() - diurnal(series=dpt, ax=ax, period="monthly", zorder=2, **kwargs) + diurnal(series=dpt, ax=ax, period="monthly", zorder=2, **kwargs) - # ad vertical lines - for i in range(12): - ax.axvline(i * 24, ls=":", c="k") + # ad vertical lines + for i in range(12): + ax.axvline(i * 24, ls=":", c="k") - _temp = ( - dpt.groupby([dpt.index.month, dpt.index.day, dpt.index.hour]) - .mean() - .reorder_levels([0, 2, 1]) - .unstack() - .reset_index(drop=True) - .T - ) - ax.scatter( - [_temp.columns.values] * 31, - _temp.values, - s=0.5, - c=kwargs.get("color", "#907090"), - zorder=1, - alpha=0.3, - ) + _temp = ( + dpt.groupby([dpt.index.month, dpt.index.day, dpt.index.hour]) + .mean() + .reorder_levels([0, 2, 1]) + .unstack() + .reset_index(drop=True) + .T + ) + ax.scatter( + [_temp.columns.values] * 31, + _temp.values, + s=0.5, + c=kwargs.get("color", "#907090"), + zorder=1, + alpha=0.3, + ) - ax.axhline(20, ls="--", c="k", lw=1) - ax.annotate( - textwrap.fill("Underfloor cooling (20°C)", 16), - xy=(288, 20), - xycoords="data", - xytext=(10, 10), - textcoords="offset points", - arrowprops={ - "arrowstyle": "->", - "connectionstyle": "angle,angleA=0,angleB=90,rad=10", - }, - ) + ax.axhline(20, ls="--", c="k", lw=1) + ax.annotate( + textwrap.fill("Underfloor cooling (20°C)", 16), + xy=(288, 20), + xycoords="data", + xytext=(10, 10), + textcoords="offset points", + arrowprops={ + "arrowstyle": "->", + "connectionstyle": "angle,angleA=0,angleB=90,rad=10", + }, + ) - ax.axhline(18, ls="--", c="k", lw=1) - ax.annotate( - textwrap.fill("Thermally activated building structure (TABS) (18°C)", 20), - xy=(288, 18), - xycoords="data", - xytext=(10, -45), - textcoords="offset points", - arrowprops={ - "arrowstyle": "->", - "connectionstyle": "angle,angleA=90,angleB=0,rad=10", - }, - ) + ax.axhline(18, ls="--", c="k", lw=1) + ax.annotate( + textwrap.fill("Thermally activated building structure (TABS) (18°C)", 20), + xy=(288, 18), + xycoords="data", + xytext=(10, -45), + textcoords="offset points", + arrowprops={ + "arrowstyle": "->", + "connectionstyle": "angle,angleA=90,angleB=0,rad=10", + }, + ) - ax.axhline(17, ls="--", c="k", lw=1) - ax.annotate( - textwrap.fill("Chilled beams/ ceiling (17°C)", 16), - xy=(288, 17), - xycoords="data", - xytext=(10, -90), - textcoords="offset points", - arrowprops={ - "arrowstyle": "->", - "connectionstyle": "angle,angleA=0,angleB=90,rad=10", - }, - ) - ax.set_title("Radiant cooling potential") + ax.axhline(17, ls="--", c="k", lw=1) + ax.annotate( + textwrap.fill("Chilled beams/ ceiling (17°C)", 16), + xy=(288, 17), + xycoords="data", + xytext=(10, -90), + textcoords="offset points", + arrowprops={ + "arrowstyle": "->", + "connectionstyle": "angle,angleA=0,angleB=90,rad=10", + }, + ) + ax.set_title("Radiant cooling potential") return ax diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_seasonality.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_seasonality.py index 59d943e9..95dc52d4 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_seasonality.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_seasonality.py @@ -30,6 +30,8 @@ def seasonality_comparison( **kwargs: title (str): The title of the plot. If not provided, then the name of the EPW file is used. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom """ from_day_length = seasonality_from_day_length(epw=epw).rename("From day-length", inplace=False) @@ -56,106 +58,109 @@ def seasonality_comparison( title_str = kwargs.pop("title", str(epw)) ywidth = 0.9 - - if ax is None: - ax = plt.gca() - - season_df = pd.concat( - [ - from_month, - from_day_length, - from_temperature, - ], - axis=1, - ) - season_df.index = [mdates.date2num(i) for i in season_df.index] - - y = (1 - ywidth) / 2 - patches = [] - for col in season_df.columns: - for season in seasons: - local = season_df[col][season_df[col] == season] - if any(local.index.diff().unique() > 1): - # get the points at which the values change - shiftpt = local.index.diff().argmax() - patches.append( - mpatches.Rectangle( - xy=(local.index[0], y), - height=ywidth, - width=local.index[shiftpt - 1] - local.index[0], - facecolor=color_config[season], - edgecolor="w", + + style_context = kwargs.pop("style_context", "python_toolkit.bhom") + + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() + + season_df = pd.concat( + [ + from_month, + from_day_length, + from_temperature, + ], + axis=1, + ) + season_df.index = [mdates.date2num(i) for i in season_df.index] + + y = (1 - ywidth) / 2 + patches = [] + for col in season_df.columns: + for season in seasons: + local = season_df[col][season_df[col] == season] + if any(local.index.diff().unique() > 1): + # get the points at which the values change + shiftpt = local.index.diff().argmax() + patches.append( + mpatches.Rectangle( + xy=(local.index[0], y), + height=ywidth, + width=local.index[shiftpt - 1] - local.index[0], + facecolor=color_config[season], + edgecolor="w", + ) ) - ) - patches.append( - mpatches.Rectangle( - xy=(local.index[shiftpt], y), - height=ywidth, - width=local.index[-1], - facecolor=color_config[season], - edgecolor="w", + patches.append( + mpatches.Rectangle( + xy=(local.index[shiftpt], y), + height=ywidth, + width=local.index[-1], + facecolor=color_config[season], + edgecolor="w", + ) ) - ) - else: - patches.append( - mpatches.Rectangle( - xy=(local.index[0], y), - height=ywidth, - width=local.index[-1] - local.index[0], - facecolor=color_config[season], - edgecolor="w", + else: + patches.append( + mpatches.Rectangle( + xy=(local.index[0], y), + height=ywidth, + width=local.index[-1] - local.index[0], + facecolor=color_config[season], + edgecolor="w", + ) ) - ) - y += 1 - pc = PatchCollection(patches=patches, match_original=True, zorder=3) - ax.add_collection(pc) - - # add annotations - y = (1 - ywidth) / 2 - for n, col in enumerate(season_df.columns): - for season in seasons: - local = season_df[col][season_df[col] == season] - if any(local.index.diff().unique() > 1): - # get the points at which the values change - shiftpt = local.index.diff().argmax() - ax.text( - local.index[shiftpt] + 1, - n + 0.5, - f"{mdates.num2date(local.index[shiftpt]):%b %d}", - ha="left", - va="top", - c=contrasting_color(color_config[season]), - ) - else: - ax.text( - local.index[0] + 1, - n + 0.5, - f"{mdates.num2date(local.index[0]):%b %d}", - ha="left", - va="top", - c=contrasting_color(color_config[season]), - ) - y += 1 - - ax.set_xlim(season_df.index[0], season_df.index[-1]) - ax.set_ylim(0, 3) - - ax.set_yticks([0.5, 1.5, 2.5]) - ax.set_yticklabels(season_df.columns, rotation=0, ha="right") - - ax.xaxis.set_major_locator(mdates.MonthLocator()) - ax.xaxis.set_major_formatter(mdates.DateFormatter("%B")) - plt.setp(ax.get_xticklabels(), rotation=0, ha="left") - - # create and add legend - new_handles = [] - for _, color in color_config.items(): - new_handles.append(mpatches.Patch(color=color, edgecolor=None)) - plt.legend( - new_handles, color_config.keys(), bbox_to_anchor=(0.5, -0.12), loc="upper center", ncol=4 - ) + y += 1 + pc = PatchCollection(patches=patches, match_original=True, zorder=3) + ax.add_collection(pc) + + # add annotations + y = (1 - ywidth) / 2 + for n, col in enumerate(season_df.columns): + for season in seasons: + local = season_df[col][season_df[col] == season] + if any(local.index.diff().unique() > 1): + # get the points at which the values change + shiftpt = local.index.diff().argmax() + ax.text( + local.index[shiftpt] + 1, + n + 0.5, + f"{mdates.num2date(local.index[shiftpt]):%b %d}", + ha="left", + va="top", + c=contrasting_color(color_config[season]), + ) + else: + ax.text( + local.index[0] + 1, + n + 0.5, + f"{mdates.num2date(local.index[0]):%b %d}", + ha="left", + va="top", + c=contrasting_color(color_config[season]), + ) + y += 1 + + ax.set_xlim(season_df.index[0], season_df.index[-1]) + ax.set_ylim(0, 3) + + ax.set_yticks([0.5, 1.5, 2.5]) + ax.set_yticklabels(season_df.columns, rotation=0, ha="right") + + ax.xaxis.set_major_locator(mdates.MonthLocator()) + ax.xaxis.set_major_formatter(mdates.DateFormatter("%B")) + plt.setp(ax.get_xticklabels(), rotation=0, ha="left") + + # create and add legend + new_handles = [] + for _, color in color_config.items(): + new_handles.append(mpatches.Patch(color=color, edgecolor=None)) + plt.legend( + new_handles, color_config.keys(), bbox_to_anchor=(0.5, -0.12), loc="upper center", ncol=4 + ) - # add title - ax.set_title(title_str) + # add title + ax.set_title(title_str) return ax diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_skymatrix.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_skymatrix.py index 240fa24c..be90a1ef 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_skymatrix.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_skymatrix.py @@ -49,78 +49,83 @@ def skymatrix( Show the colorbar. Defaults to True. **kwargs: Additional keyword arguments to pass to the plotting function. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: Figure: A matplotlib Figure object. """ - - if ax is None: - ax = plt.gca() - - cmap = kwargs.get("cmap", "viridis") - - # create wea - wea = Wea.from_epw_file( - epw.file_path, analysis_period.timestep - ).filter_by_analysis_period(analysis_period) - wea_duration = len(wea) / wea.timestep - wea_folder = Path(tempfile.gettempdir()) - wea_path = wea_folder / "skymatrix.wea" - wea_file = wea.write(wea_path.as_posix()) - - # run gendaymtx - gendaymtx_exe = (Path(hbr_folders.radbin_path) / "gendaymtx.exe").as_posix() - cmds = [gendaymtx_exe, "-m", str(density), "-d", "-O1", "-A", wea_file] - with subprocess.Popen(cmds, stdout=subprocess.PIPE, shell=True) as process: - stdout = process.communicate() - dir_data_str = stdout[0].decode("ascii") - cmds = [gendaymtx_exe, "-m", str(density), "-s", "-O1", "-A", wea_file] - with subprocess.Popen(cmds, stdout=subprocess.PIPE, shell=True) as process: - stdout = process.communicate() - diff_data_str = stdout[0].decode("ascii") - - def _broadband_rad(data_str: str) -> list[float]: - _ = data_str.split("\r\n")[:8] - data = np.array( - [[float(j) for j in i.split()] for i in data_str.split("\r\n")[8:]][1:-1] - ) - patch_values = (np.array([0.265074126, 0.670114631, 0.064811243]) * data).sum( - axis=1 - ) - patch_steradians = np.array(ViewSphere().dome_patch_weights(density)) - broadband_radiation = patch_values * patch_steradians * wea_duration / 1000 - return broadband_radiation - - dir_vals = _broadband_rad(dir_data_str) - diff_vals = _broadband_rad(diff_data_str) - - # create patches to plot - patches = [] - for face in ViewSphere().dome_patches(density)[0].face_vertices: - patches.append(mpatches.Polygon(np.array([i.to_array() for i in face])[:, :2])) - p = PatchCollection(patches, alpha=1, cmap=cmap) - - p.set_array(dir_vals + diff_vals) # SET DIR/DIFF/TOTAL VALUES HERE - - # plot! - ax.add_collection(p) - ax.set_xlim(-1, 1) - ax.set_ylim(-1, 1) - if show_colorbar: - cbar = plt.colorbar(p, ax=ax) - cbar.outline.set_visible(False) - cbar.set_label("Cumulative irradiance (W/m$^{2}$)") - ax.set_aspect("equal") - ax.axis("off") - - if show_title: - ax.set_title( - f"{location_to_string(epw.location)}\n{describe_analysis_period(analysis_period)}", - ha="left", - x=0, - ) - - plt.tight_layout() + + style_context = kwargs.pop("style_context", "python_toolkit.bhom") + + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() + + cmap = kwargs.get("cmap", "viridis") + + # create wea + wea = Wea.from_epw_file( + epw.file_path, analysis_period.timestep + ).filter_by_analysis_period(analysis_period) + wea_duration = len(wea) / wea.timestep + wea_folder = Path(tempfile.gettempdir()) + wea_path = wea_folder / "skymatrix.wea" + wea_file = wea.write(wea_path.as_posix()) + + # run gendaymtx + gendaymtx_exe = (Path(hbr_folders.radbin_path) / "gendaymtx.exe").as_posix() + cmds = [gendaymtx_exe, "-m", str(density), "-d", "-O1", "-A", wea_file] + with subprocess.Popen(cmds, stdout=subprocess.PIPE, shell=True) as process: + stdout = process.communicate() + dir_data_str = stdout[0].decode("ascii") + cmds = [gendaymtx_exe, "-m", str(density), "-s", "-O1", "-A", wea_file] + with subprocess.Popen(cmds, stdout=subprocess.PIPE, shell=True) as process: + stdout = process.communicate() + diff_data_str = stdout[0].decode("ascii") + + def _broadband_rad(data_str: str) -> list[float]: + _ = data_str.split("\r\n")[:8] + data = np.array( + [[float(j) for j in i.split()] for i in data_str.split("\r\n")[8:]][1:-1] + ) + patch_values = (np.array([0.265074126, 0.670114631, 0.064811243]) * data).sum( + axis=1 + ) + patch_steradians = np.array(ViewSphere().dome_patch_weights(density)) + broadband_radiation = patch_values * patch_steradians * wea_duration / 1000 + return broadband_radiation + + dir_vals = _broadband_rad(dir_data_str) + diff_vals = _broadband_rad(diff_data_str) + + # create patches to plot + patches = [] + for face in ViewSphere().dome_patches(density)[0].face_vertices: + patches.append(mpatches.Polygon(np.array([i.to_array() for i in face])[:, :2])) + p = PatchCollection(patches, alpha=1, cmap=cmap) + + p.set_array(dir_vals + diff_vals) # SET DIR/DIFF/TOTAL VALUES HERE + + # plot! + ax.add_collection(p) + ax.set_xlim(-1, 1) + ax.set_ylim(-1, 1) + if show_colorbar: + cbar = plt.colorbar(p, ax=ax) + cbar.outline.set_visible(False) + cbar.set_label("Cumulative irradiance (W/m$^{2}$)") + ax.set_aspect("equal") + ax.axis("off") + + if show_title: + ax.set_title( + f"{location_to_string(epw.location)}\n{describe_analysis_period(analysis_period)}", + ha="left", + x=0, + ) + + plt.tight_layout() return ax diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_sunpath.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_sunpath.py index 9c628057..0ac92d7f 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_sunpath.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_sunpath.py @@ -29,6 +29,7 @@ def sunpath( sun_size: float = 10, show_grid: bool = True, show_legend: bool = True, + style_context: str = "python_toolkit.bhom" ) -> plt.Axes: """Plot a sun-path for the given Location and analysis period. Args: @@ -51,110 +52,112 @@ def sunpath( Set to True to show the grid. Defaults to True. show_legend (bool, optional): Set to True to include a legend in the plot if data_collection passed. Defaults to True. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: A matplotlib Axes object. """ + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() - if ax is None: - ax = plt.gca() - - sunpath_obj = Sunpath.from_location(location) - all_suns = [ - sunpath_obj.calculate_sun_from_date_time(i) for i in analysis_period.datetimes - ] - suns = [i for i in all_suns if i.altitude > 0] - suns_x, suns_y = np.array([sun.position_2d().to_array() for sun in suns]).T + sunpath_obj = Sunpath.from_location(location) + all_suns = [ + sunpath_obj.calculate_sun_from_date_time(i) for i in analysis_period.datetimes + ] + suns = [i for i in all_suns if i.altitude > 0] + suns_x, suns_y = np.array([sun.position_2d().to_array() for sun in suns]).T - day_suns = [] - for month in [6, 9, 12]: - date = pd.to_datetime(f"2017-{month:02d}-21") - day_idx = pd.date_range(date, date + pd.Timedelta(hours=24), freq="1min") - _ = [] - for idx in day_idx: - s = sunpath_obj.calculate_sun_from_date_time(idx) - if s.altitude > 0: - _.append(np.array(s.position_2d().to_array())) - day_suns.append(np.array(_)) + day_suns = [] + for month in [6, 9, 12]: + date = pd.to_datetime(f"2017-{month:02d}-21") + day_idx = pd.date_range(date, date + pd.Timedelta(hours=24), freq="1min") + _ = [] + for idx in day_idx: + s = sunpath_obj.calculate_sun_from_date_time(idx) + if s.altitude > 0: + _.append(np.array(s.position_2d().to_array())) + day_suns.append(np.array(_)) - ax.set_aspect("equal") - ax.set_xlim(-101, 101) - ax.set_ylim(-101, 101) - ax.axis("off") + ax.set_aspect("equal") + ax.set_xlim(-101, 101) + ax.set_ylim(-101, 101) + ax.axis("off") - if show_grid: - compass = Compass() - ax.add_patch( - plt.Circle( - (0, 0), - 100, - zorder=1, - lw=0.5, - ec="#555555", - fc=(0, 0, 0, 0), - ls="-", + if show_grid: + compass = Compass() + ax.add_patch( + plt.Circle( + (0, 0), + 100, + zorder=1, + lw=0.5, + ec="#555555", + fc=(0, 0, 0, 0), + ls="-", + ) ) - ) - for pt, lab in list(zip(*[compass.major_azimuth_points, compass.MAJOR_TEXT])): - _x, _y = np.array([[0, 0]] + [pt.to_array()]).T - ax.plot(_x, _y, zorder=1, lw=0.5, ls="-", c="#555555", alpha=0.5) - t = ax.text(_x[1], _y[1], lab, ha="center", va="center", fontsize="medium") - t.set_bbox( - {"facecolor": "white", "alpha": 1, "edgecolor": None, "linewidth": 0} + for pt, lab in list(zip(*[compass.major_azimuth_points, compass.MAJOR_TEXT])): + _x, _y = np.array([[0, 0]] + [pt.to_array()]).T + ax.plot(_x, _y, zorder=1, lw=0.5, ls="-", c="#555555", alpha=0.5) + t = ax.text(_x[1], _y[1], lab, ha="center", va="center", fontsize="medium") + t.set_bbox( + {"facecolor": "white", "alpha": 1, "edgecolor": None, "linewidth": 0} + ) + for pt, lab in list(zip(*[compass.minor_azimuth_points, compass.MINOR_TEXT])): + _x, _y = np.array([[0, 0]] + [pt.to_array()]).T + ax.plot(_x, _y, zorder=1, lw=0.5, ls="-", c="#555555", alpha=0.5) + t = ax.text(_x[1], _y[1], lab, ha="center", va="center", fontsize="small") + t.set_bbox( + {"facecolor": "white", "alpha": 1, "edgecolor": None, "linewidth": 0} + ) + + if data_collection is not None: + new_idx = analysis_period_to_datetimes(analysis_period) + series = collection_to_series(data_collection) + vals = ( + series.reindex(new_idx) + .interpolate() + .values[[i.altitude > 0 for i in all_suns]] ) - for pt, lab in list(zip(*[compass.minor_azimuth_points, compass.MINOR_TEXT])): - _x, _y = np.array([[0, 0]] + [pt.to_array()]).T - ax.plot(_x, _y, zorder=1, lw=0.5, ls="-", c="#555555", alpha=0.5) - t = ax.text(_x[1], _y[1], lab, ha="center", va="center", fontsize="small") - t.set_bbox( - {"facecolor": "white", "alpha": 1, "edgecolor": None, "linewidth": 0} + dat = ax.scatter( + suns_x, suns_y, c=vals, s=sun_size, cmap=cmap, norm=norm, zorder=3 ) - if data_collection is not None: - new_idx = analysis_period_to_datetimes(analysis_period) - series = collection_to_series(data_collection) - vals = ( - series.reindex(new_idx) - .interpolate() - .values[[i.altitude > 0 for i in all_suns]] - ) - dat = ax.scatter( - suns_x, suns_y, c=vals, s=sun_size, cmap=cmap, norm=norm, zorder=3 - ) + if show_legend: + cb = ax.figure.colorbar( + dat, + pad=0.09, + shrink=0.8, + aspect=30, + label=f"{series.name}", + ) + cb.outline.set_visible(False) + else: + ax.scatter(suns_x, suns_y, c="#FFCF04", s=sun_size, zorder=3) - if show_legend: - cb = ax.figure.colorbar( - dat, - pad=0.09, - shrink=0.8, - aspect=30, - label=f"{series.name}", + # add equinox/solstice curves + for day_sun in day_suns: + _x, _y = day_sun.T + ax.plot( + _x, + _y, + c="black", + alpha=0.6, + zorder=1, + ls=":", + lw=0.75, ) - cb.outline.set_visible(False) - else: - ax.scatter(suns_x, suns_y, c="#FFCF04", s=sun_size, zorder=3) - # add equinox/solstice curves - for day_sun in day_suns: - _x, _y = day_sun.T - ax.plot( - _x, - _y, - c="black", - alpha=0.6, - zorder=1, - ls=":", - lw=0.75, + title_string = "\n".join( + [ + location_to_string(location), + describe_analysis_period(analysis_period), + ] ) + ax.set_title(title_string, ha="left", x=0, y=1.05) - title_string = "\n".join( - [ - location_to_string(location), - describe_analysis_period(analysis_period), - ] - ) - ax.set_title(title_string, ha="left", x=0, y=1.05) - - plt.tight_layout() + plt.tight_layout() return ax diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_utci.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_utci.py index 1af39633..b4056ced 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_utci.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/_utci.py @@ -56,6 +56,8 @@ def utci_comfort_band_comparison( If True, then show percentage, otherwise show count. Defaults to True. **kwargs: Additional keyword arguments to pass to the function. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: @@ -69,71 +71,74 @@ def utci_comfort_band_comparison( ) if any(len(i) != len(utci_collections[0]) for i in utci_collections): raise ValueError("All collections must be the same length.") + + style_context = kwargs.pop("style_context", "python_toolkit.bhom") - if ax is None: - ax = plt.gca() + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() - # set the title - ax.set_title(kwargs.pop("title", None)) + # set the title + ax.set_title(kwargs.pop("title", None)) - if identifiers is None: - identifiers = [f"{n}" for n in range(len(utci_collections))] - if len(identifiers) != len(utci_collections): - raise ValueError( - "The number of identifiers given does not match the number of UTCI collections given!" - ) - - counts = pd.concat( - [utci_categories.value_counts(i, density=density) for i in utci_collections], - axis=1, - keys=identifiers, - ) - counts.T.plot( - ax=ax, - kind="bar", - stacked=True, - color=utci_categories.colors, - width=0.8, - legend=False, - ) + if identifiers is None: + identifiers = [f"{n}" for n in range(len(utci_collections))] + if len(identifiers) != len(utci_collections): + raise ValueError( + "The number of identifiers given does not match the number of UTCI collections given!" + ) - if kwargs.pop("legend", True): - handles, labels = ax.get_legend_handles_labels() - ax.legend( - handles[::-1], - labels[::-1], - title=utci_categories.name, - bbox_to_anchor=(1, 0.5), - loc="center left", + counts = pd.concat( + [utci_categories.value_counts(i, density=density) for i in utci_collections], + axis=1, + keys=identifiers, + ) + counts.T.plot( + ax=ax, + kind="bar", + stacked=True, + color=utci_categories.colors, + width=0.8, + legend=False, ) - for spine in ["top", "right", "bottom", "left"]: - ax.spines[spine].set_visible(False) - - # add labels to bars - - # get bar total heights - height = np.array([[i.get_height() for i in c] for c in ax.containers]).T.sum(axis=1)[0] - for c in ax.containers: - labels = [] - for v in c: - label = f"{v.get_height():0.1%}" if density else f"{v.get_height():0.0f}" - if v.get_height() / height > 0.04: - labels.append(label) - else: - labels.append("") + if kwargs.pop("legend", True): + handles, labels = ax.get_legend_handles_labels() + ax.legend( + handles[::-1], + labels[::-1], + title=utci_categories.name, + bbox_to_anchor=(1, 0.5), + loc="center left", + ) - ax.bar_label( - c, - labels=labels, - label_type="center", - color=contrasting_color(v.get_facecolor()), - ) + for spine in ["top", "right", "bottom", "left"]: + ax.spines[spine].set_visible(False) + + # add labels to bars + + # get bar total heights + height = np.array([[i.get_height() for i in c] for c in ax.containers]).T.sum(axis=1)[0] + for c in ax.containers: + labels = [] + for v in c: + label = f"{v.get_height():0.1%}" if density else f"{v.get_height():0.0f}" + if v.get_height() / height > 0.04: + labels.append(label) + else: + labels.append("") + + ax.bar_label( + c, + labels=labels, + label_type="center", + color=contrasting_color(v.get_facecolor()), + ) - ax.tick_params(axis="both", which="both", length=0) - ax.grid(False) - plt.xticks(rotation=0) - ax.yaxis.set_major_locator(plt.NullLocator()) + ax.tick_params(axis="both", which="both", length=0) + ax.grid(False) + plt.xticks(rotation=0) + ax.yaxis.set_major_locator(plt.NullLocator()) return ax @@ -163,6 +168,8 @@ def utci_comfort_band_comparison_series( If True, then show percentage, otherwise show count. Defaults to True. **kwargs: Additional keyword arguments to pass to the function. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: @@ -174,72 +181,75 @@ def utci_comfort_band_comparison_series( if any(len(i) != len(utci_series[0]) for i in utci_series): raise ValueError("All collections must be the same length.") + + style_context = kwargs.pop("style_context", "python_toolkit.bhom") - if ax is None: - ax = plt.gca() - - # set the title - ax.set_title(kwargs.pop("title", None)) + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() - if identifiers is None: - identifiers = [f"{n}" for n in range(len(utci_series))] + # set the title + ax.set_title(kwargs.pop("title", None)) - if len(identifiers) != len(utci_series): - raise ValueError( - "The number of identifiers given does not match the number of UTCI collections given!" - ) + if identifiers is None: + identifiers = [f"{n}" for n in range(len(utci_series))] - counts = pd.concat( - [utci_categories.value_counts(i, density=density) for i in utci_series], - axis=1, - keys=identifiers, - ) - counts.T.plot( - ax=ax, - kind="bar", - stacked=True, - color=utci_categories.colors, - width=0.8, - legend=False, - ) + if len(identifiers) != len(utci_series): + raise ValueError( + "The number of identifiers given does not match the number of UTCI collections given!" + ) - if kwargs.pop("legend", True): - handles, labels = ax.get_legend_handles_labels() - ax.legend( - handles[::-1], - labels[::-1], - title=utci_categories.name, - bbox_to_anchor=(1, 0.5), - loc="center left", + counts = pd.concat( + [utci_categories.value_counts(i, density=density) for i in utci_series], + axis=1, + keys=identifiers, + ) + counts.T.plot( + ax=ax, + kind="bar", + stacked=True, + color=utci_categories.colors, + width=0.8, + legend=False, ) - for spine in ["top", "right", "bottom", "left"]: - ax.spines[spine].set_visible(False) - - # add labels to bars - - # get bar total heights - height = np.array([[i.get_height() for i in c] for c in ax.containers]).T.sum(axis=1)[0] - for c in ax.containers: - labels = [] - for v in c: - label = f"{v.get_height():0.1%}" if density else f"{v.get_height():0.0f}" - if v.get_height() / height > 0.04: - labels.append(label) - else: - labels.append("") + if kwargs.pop("legend", True): + handles, labels = ax.get_legend_handles_labels() + ax.legend( + handles[::-1], + labels[::-1], + title=utci_categories.name, + bbox_to_anchor=(1, 0.5), + loc="center left", + ) - ax.bar_label( - c, - labels=labels, - label_type="center", - color=contrasting_color(v.get_facecolor()), - ) + for spine in ["top", "right", "bottom", "left"]: + ax.spines[spine].set_visible(False) + + # add labels to bars + + # get bar total heights + height = np.array([[i.get_height() for i in c] for c in ax.containers]).T.sum(axis=1)[0] + for c in ax.containers: + labels = [] + for v in c: + label = f"{v.get_height():0.1%}" if density else f"{v.get_height():0.0f}" + if v.get_height() / height > 0.04: + labels.append(label) + else: + labels.append("") + + ax.bar_label( + c, + labels=labels, + label_type="center", + color=contrasting_color(v.get_facecolor()), + ) - ax.tick_params(axis="both", which="both", length=0) - ax.grid(False) - plt.xticks(rotation=0) - ax.yaxis.set_major_locator(plt.NullLocator()) + ax.tick_params(axis="both", which="both", length=0) + ax.grid(False) + plt.xticks(rotation=0) + ax.yaxis.set_major_locator(plt.NullLocator()) return ax @@ -277,6 +287,8 @@ def utci_day_comfort_metrics( The day to plot. Default is 21. kwargs: Additional keyword arguments to pass to the matplotlib plot function. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: @@ -286,60 +298,63 @@ def utci_day_comfort_metrics( if any(all(utci.index != i.index) for i in [dbt, mrt, rh, ws]): raise ValueError("All series must have the same index") + + style_context = kwargs.get("style_context", "python_toolkit.bhom") + + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() + + try: + dt = f"{utci.index.year[0]}-{month}-{day}" + date = utci.loc[dt].index[0] + except KeyError as e: + raise e + + axes = [] + for i in range(5): + if i == 0: + axes.append(ax) + else: + temp_ax = ax.twinx() + rspine = temp_ax.spines["right"] + rspine.set_position(("axes", 1 + (i / 20))) + temp_ax.set_frame_on(True) + temp_ax.patch.set_visible(False) + rspine.set_visible(True) + axes.append(temp_ax) + + (a,) = axes[0].plot(utci.loc[dt], c="black", label="UTCI", lw=1.5) + axes[0].set_ylabel("UTCI") + (b,) = axes[1].plot(dbt.loc[dt], c="red", alpha=0.75, label="DBT", ls="--") + axes[1].set_ylabel("DBT") + axes[1].grid(False) + (c,) = axes[2].plot(mrt.loc[dt], c="orange", alpha=0.75, label="MRT", ls="--") + axes[2].set_ylabel("MRT") + axes[2].grid(False) + (d,) = axes[3].plot(rh.loc[dt], c="blue", alpha=0.75, label="RH", ls="--") + axes[3].set_ylabel("RH") + axes[3].grid(False) + (e,) = axes[4].plot(ws.loc[dt], c="green", alpha=0.75, label="WS", ls="--") + axes[4].set_ylabel("WS") + axes[4].grid(False) + + axes[0].spines["right"].set_visible(False) + + axes[0].xaxis.set_major_formatter(mdates.DateFormatter("%H:%M")) + axes[0].set_xlim(utci.loc[dt].index.min(), utci.loc[dt].index.max()) + + axes[0].legend( + handles=[a, b, c, d, e], + loc="lower center", + ncol=5, + bbox_to_anchor=[0.5, -0.15], + frameon=False, + ) - if ax is None: - ax = plt.gca() - - try: - dt = f"{utci.index.year[0]}-{month}-{day}" - date = utci.loc[dt].index[0] - except KeyError as e: - raise e - - axes = [] - for i in range(5): - if i == 0: - axes.append(ax) - else: - temp_ax = ax.twinx() - rspine = temp_ax.spines["right"] - rspine.set_position(("axes", 1 + (i / 20))) - temp_ax.set_frame_on(True) - temp_ax.patch.set_visible(False) - rspine.set_visible(True) - axes.append(temp_ax) - - (a,) = axes[0].plot(utci.loc[dt], c="black", label="UTCI", lw=1.5) - axes[0].set_ylabel("UTCI") - (b,) = axes[1].plot(dbt.loc[dt], c="red", alpha=0.75, label="DBT", ls="--") - axes[1].set_ylabel("DBT") - axes[1].grid(False) - (c,) = axes[2].plot(mrt.loc[dt], c="orange", alpha=0.75, label="MRT", ls="--") - axes[2].set_ylabel("MRT") - axes[2].grid(False) - (d,) = axes[3].plot(rh.loc[dt], c="blue", alpha=0.75, label="RH", ls="--") - axes[3].set_ylabel("RH") - axes[3].grid(False) - (e,) = axes[4].plot(ws.loc[dt], c="green", alpha=0.75, label="WS", ls="--") - axes[4].set_ylabel("WS") - axes[4].grid(False) - - axes[0].spines["right"].set_visible(False) - - axes[0].xaxis.set_major_formatter(mdates.DateFormatter("%H:%M")) - axes[0].set_xlim(utci.loc[dt].index.min(), utci.loc[dt].index.max()) - - axes[0].legend( - handles=[a, b, c, d, e], - loc="lower center", - ncol=5, - bbox_to_anchor=[0.5, -0.15], - frameon=False, - ) - - # set the title - title = [kwargs.pop("title", None), f"{date:%B %d}"] - ax.set_title("\n".join([i for i in title if i is not None])) + # set the title + title = [kwargs.pop("title", None), f"{date:%B %d}"] + ax.set_title("\n".join([i for i in title if i is not None])) return ax @@ -379,6 +394,8 @@ def utci_comparison_diurnal_day( Additional keyword arguments to pass to the matplotlib plot function. ylims (list[float], optional): The y-axis limits. Defaults to None which just uses the min/ax of the fiven collections. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: Figure: @@ -391,78 +408,81 @@ def utci_comparison_diurnal_day( raise ValueError( f"Collection {n} data type is not UTCI and cannot be used in this plot." ) + + style_context = kwargs.pop("style_context", "python_toolkit.bhom") + + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() + + if collection_ids is None: + collection_ids = [f"{i:02d}" for i in range(len(utci_collections))] + assert len(utci_collections) == len( + collection_ids + ), "The length of collections_ids must match the number of collections." + + # set the title + title = [ + kwargs.pop("title", None), + f"{calendar.month_name[month]} typical day ({agg})", + ] + ax.set_title("\n".join([i for i in title if i is not None])) + + # combine utcis and add names to columns + df = pd.concat([collection_to_series(i) for i in utci_collections], axis=1, keys=collection_ids) + ylim = kwargs.pop("ylim", [df.min().min(), df.max().max()]) + df_agg = df.groupby([df.index.month, df.index.hour]).agg(agg).loc[month] + df_agg.index = range(24) + # add a final value to close the day + df_agg.loc[24] = df_agg.loc[0] + + df_agg.plot(ax=ax, legend=True, zorder=3, **kwargs) + + # Fill between ranges + for cat, color, name in list( + zip( + *[ + utci_categories.interval_index, + utci_categories.colors, + utci_categories.bin_names, + ] + ) + ): + ax.axhspan( + max([cat.left, -100]), + min([cat.right, 100]), + color=lighten_color(color, 0.2), + zorder=2, + label="_nolegend_" if not categories_in_legend else name, + ) - if ax is None: - ax = plt.gca() - - if collection_ids is None: - collection_ids = [f"{i:02d}" for i in range(len(utci_collections))] - assert len(utci_collections) == len( - collection_ids - ), "The length of collections_ids must match the number of collections." - - # set the title - title = [ - kwargs.pop("title", None), - f"{calendar.month_name[month]} typical day ({agg})", - ] - ax.set_title("\n".join([i for i in title if i is not None])) - - # combine utcis and add names to columns - df = pd.concat([collection_to_series(i) for i in utci_collections], axis=1, keys=collection_ids) - ylim = kwargs.pop("ylim", [df.min().min(), df.max().max()]) - df_agg = df.groupby([df.index.month, df.index.hour]).agg(agg).loc[month] - df_agg.index = range(24) - # add a final value to close the day - df_agg.loc[24] = df_agg.loc[0] - - df_agg.plot(ax=ax, legend=True, zorder=3, **kwargs) - - # Fill between ranges - for cat, color, name in list( - zip( - *[ - utci_categories.interval_index, - utci_categories.colors, - utci_categories.bin_names, - ] - ) - ): - ax.axhspan( - max([cat.left, -100]), - min([cat.right, 100]), - color=lighten_color(color, 0.2), - zorder=2, - label="_nolegend_" if not categories_in_legend else name, - ) - - # Format plots - ax.set_xlim(0, 24) - ax.set_ylim(ylim) - ax.xaxis.set_major_locator(plt.FixedLocator([0, 6, 12, 18])) - ax.xaxis.set_minor_locator(plt.FixedLocator([3, 9, 15, 21])) - ax.yaxis.set_major_locator(plt.MaxNLocator(8)) - ax.set_xticklabels(["00:00", "06:00", "12:00", "18:00"], minor=False, ha="left") - ax.set_ylabel("Universal Thermal Climate Index (°C)") - ax.set_xlabel("Time of day") - - # add grid using a hacky fix - for i in ax.get_xticks(): - ax.axvline(i, color=ax.xaxis.label.get_color(), ls=":", lw=0.5, alpha=0.1, zorder=5) - for i in ax.get_yticks(): - ax.axhline(i, color=ax.yaxis.label.get_color(), ls=":", lw=0.5, alpha=0.1, zorder=5) - - if show_legend: - handles, labels = ax.get_legend_handles_labels() - ax.legend( - handles[::-1], - labels[::-1], - loc="upper left", - bbox_to_anchor=[1, 1], - frameon=False, - fontsize="small", - ncol=1, - ) + # Format plots + ax.set_xlim(0, 24) + ax.set_ylim(ylim) + ax.xaxis.set_major_locator(plt.FixedLocator([0, 6, 12, 18])) + ax.xaxis.set_minor_locator(plt.FixedLocator([3, 9, 15, 21])) + ax.yaxis.set_major_locator(plt.MaxNLocator(8)) + ax.set_xticklabels(["00:00", "06:00", "12:00", "18:00"], minor=False, ha="left") + ax.set_ylabel("Universal Thermal Climate Index (°C)") + ax.set_xlabel("Time of day") + + # add grid using a hacky fix + for i in ax.get_xticks(): + ax.axvline(i, color=ax.xaxis.label.get_color(), ls=":", lw=0.5, alpha=0.1, zorder=5) + for i in ax.get_yticks(): + ax.axhline(i, color=ax.yaxis.label.get_color(), ls=":", lw=0.5, alpha=0.1, zorder=5) + + if show_legend: + handles, labels = ax.get_legend_handles_labels() + ax.legend( + handles[::-1], + labels[::-1], + loc="upper left", + bbox_to_anchor=[1, 1], + frameon=False, + fontsize="small", + ncol=1, + ) return ax @@ -486,6 +506,8 @@ def utci_heatmap_difference( **kwargs: Additional keyword arguments to pass to the heatmap function. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: @@ -496,10 +518,7 @@ def utci_heatmap_difference( raise ValueError("Input collection 1 is not a UTCI collection.") if not isinstance(utci_collection2.header.data_type, LB_UniversalThermalClimateIndex): raise ValueError("Input collection 2 is not a UTCI collection.") - - if ax is None: - ax = plt.gca() - + vmin = kwargs.pop("vmin", -10) vmax = kwargs.pop("vmax", 10) @@ -538,6 +557,8 @@ def utci_pie( The UTCI categories to use. Defaults to UTCI_DEFAULT_CATEGORIES. **kwargs: Additional keyword arguments to pass to the plotting function. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: A matplotlib Axes object. @@ -545,47 +566,50 @@ def utci_pie( if not isinstance(utci_collection.header.data_type, LB_UniversalThermalClimateIndex): raise ValueError("Input collection is not a UTCI collection.") + + style_context = kwargs.pop("style_context", "python_toolkit.bhom") - if ax is None: - ax = plt.gca() + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() - title = kwargs.pop("title", None) - ax.set_title(title) - - series = collection_to_series(utci_collection) + title = kwargs.pop("title", None) + ax.set_title(title) - sizes = utci_categories.value_counts(series, density=True) - - def func(pct, _): - if pct <= 0.05: - return "" - return f"{pct:.1f}%" - - wedges, _, autotexts = ax.pie( - sizes, - colors=utci_categories.colors, - startangle=90, - counterclock=False, - wedgeprops={"edgecolor": "w", "linewidth": 1}, - autopct=lambda pct: func(pct, sizes) if show_values else None, - pctdistance=0.8, - ) + series = collection_to_series(utci_collection) - if show_values: - plt.setp(autotexts, weight="bold", color="w") + sizes = utci_categories.value_counts(series, density=True) - centre_circle = plt.Circle((0, 0), 0.60, fc="white") - ax.add_artist(centre_circle) + def func(pct, _): + if pct <= 0.05: + return "" + return f"{pct:.1f}%" - if show_legend: - ax.legend( - wedges[::-1], - sizes.index[::-1], - title=utci_categories.name, - bbox_to_anchor=(1, 0.5), - loc="center left", + wedges, _, autotexts = ax.pie( + sizes, + colors=utci_categories.colors, + startangle=90, + counterclock=False, + wedgeprops={"edgecolor": "w", "linewidth": 1}, + autopct=lambda pct: func(pct, sizes) if show_values else None, + pctdistance=0.8, ) + if show_values: + plt.setp(autotexts, weight="bold", color="w") + + centre_circle = plt.Circle((0, 0), 0.60, fc="white") + ax.add_artist(centre_circle) + + if show_legend: + ax.legend( + wedges[::-1], + sizes.index[::-1], + title=utci_categories.name, + bbox_to_anchor=(1, 0.5), + loc="center left", + ) + return ax @@ -620,6 +644,8 @@ def utci_journey( The UTCI categories to use. Defaults to UTCI_DEFAULT_CATEGORIES. **kwargs: Additional keyword arguments to pass to the plotting function. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: A matplotlib Axes object. @@ -630,96 +656,99 @@ def utci_journey( raise ValueError("Number of values and names must be equal.") else: names = [str(i) for i in range(len(utci_values))] + + style_context = kwargs.pop("style_context", "python_toolkit.bhom") - if ax is None: - ax = plt.gca() - - # Convert collections into series and combine - df_pit = pd.Series(utci_values, index=names) - - # Add UTCI background colors to the canvas - for cat, color, name in list( - zip( - *[ - utci_categories.interval_index, - utci_categories.colors, - utci_categories.bin_names, - ] - ) - ): - ax.axhspan( - max([cat.left, -100]), - min([cat.right, 100]), - color=lighten_color(color, 0.2), - zorder=2, - label=name, - ) - - # add UTCI instance values to canvas - for n, (idx, val) in enumerate(df_pit.items()): - ax.scatter(n, val, c="white", s=400, zorder=9) - ax.text(n, val, idx, zorder=10, ha="center", va="center", fontsize="medium") + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() - if show_grid: - # Major ticks every 20, minor ticks every 5 - major_ticks = np.arange(-100, 101, 10) - minor_ticks = np.arange(-100, 101, 5) + # Convert collections into series and combine + df_pit = pd.Series(utci_values, index=names) - ax.set_yticks(major_ticks) - ax.set_yticks(minor_ticks, minor=True) - ax.grid(which="major", c="w", alpha=0.25, ls="--", axis="y") - ax.grid(which="minor", c="w", alpha=0.75, ls=":", axis="y") - - # set ylims - ylim = kwargs.pop("ylim", (min(df_pit) - 5, max(df_pit) + 5)) - title = kwargs.pop("title", None) - ax.set_title(title) - - if curve: - # Smooth values - if len(utci_values) < 3: - k = 1 - else: - k = 2 - x = np.arange(len(utci_values)) - y = df_pit.values - xnew = np.linspace(min(x), max(x), 300) - bspl = make_interp_spline(x, y, k=k) - ynew = bspl(xnew) - - # Plot the smoothed values - ax.plot(xnew, ynew, c="#B30202", ls="--", **kwargs) - - ax.set_ylim(ylim) - - for spine in ["top", "right", "bottom"]: - ax.spines[spine].set_visible(False) - - plt.tick_params( - axis="x", - which="both", - bottom=False, - top=False, - labelbottom=False, - ) + # Add UTCI background colors to the canvas + for cat, color, name in list( + zip( + *[ + utci_categories.interval_index, + utci_categories.colors, + utci_categories.bin_names, + ] + ) + ): + ax.axhspan( + max([cat.left, -100]), + min([cat.right, 100]), + color=lighten_color(color, 0.2), + zorder=2, + label=name, + ) - ax.set_ylabel("UTCI (°C)") - - if show_legend: - handles, labels = ax.get_legend_handles_labels() - lgd = ax.legend( - handles[::-1], - labels[::-1], - bbox_to_anchor=(1, 1), - loc=2, - ncol=1, - borderaxespad=0, - frameon=False, - fontsize="small", + # add UTCI instance values to canvas + for n, (idx, val) in enumerate(df_pit.items()): + ax.scatter(n, val, c="white", s=400, zorder=9) + ax.text(n, val, idx, zorder=10, ha="center", va="center", fontsize="medium") + + if show_grid: + # Major ticks every 20, minor ticks every 5 + major_ticks = np.arange(-100, 101, 10) + minor_ticks = np.arange(-100, 101, 5) + + ax.set_yticks(major_ticks) + ax.set_yticks(minor_ticks, minor=True) + ax.grid(which="major", c="w", alpha=0.25, ls="--", axis="y") + ax.grid(which="minor", c="w", alpha=0.75, ls=":", axis="y") + + # set ylims + ylim = kwargs.pop("ylim", (min(df_pit) - 5, max(df_pit) + 5)) + title = kwargs.pop("title", None) + ax.set_title(title) + + if curve: + # Smooth values + if len(utci_values) < 3: + k = 1 + else: + k = 2 + x = np.arange(len(utci_values)) + y = df_pit.values + xnew = np.linspace(min(x), max(x), 300) + bspl = make_interp_spline(x, y, k=k) + ynew = bspl(xnew) + + # Plot the smoothed values + ax.plot(xnew, ynew, c="#B30202", ls="--", **kwargs) + + ax.set_ylim(ylim) + + for spine in ["top", "right", "bottom"]: + ax.spines[spine].set_visible(False) + + plt.tick_params( + axis="x", + which="both", + bottom=False, + top=False, + labelbottom=False, ) - lgd.get_frame().set_facecolor((1, 1, 1, 0)) - plt.tight_layout() + ax.set_ylabel("UTCI (°C)") + + if show_legend: + handles, labels = ax.get_legend_handles_labels() + lgd = ax.legend( + handles[::-1], + labels[::-1], + bbox_to_anchor=(1, 1), + loc=2, + ncol=1, + borderaxespad=0, + frameon=False, + fontsize="small", + ) + lgd.get_frame().set_facecolor((1, 1, 1, 0)) + + plt.tight_layout() return ax @@ -742,6 +771,8 @@ def utci_heatmap_histogram( Set to True to show the colorbar. Defaults to True. **kwargs: Additional keyword arguments to pass. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: Figure: @@ -757,54 +788,57 @@ def utci_heatmap_histogram( figsize = kwargs.pop("figsize", (15, 5)) # Instantiate figure - fig = plt.figure(figsize=figsize, constrained_layout=True) - spec = fig.add_gridspec(ncols=1, nrows=2, width_ratios=[1], height_ratios=[5, 2], hspace=0.0) - heatmap_ax = fig.add_subplot(spec[0, 0]) - histogram_ax = fig.add_subplot(spec[1, 0]) - - # Add heatmap - utci_categories.annual_heatmap(series, ax=heatmap_ax, show_colorbar=False, **kwargs) - - # Add stacked plot - utci_categories.annual_monthly_histogram(series=series, ax=histogram_ax, show_labels=True) - - if show_colorbar: - # add colorbar - divider = make_axes_locatable(histogram_ax) - colorbar_ax = divider.append_axes("bottom", size="20%", pad=0.7) - cb = fig.colorbar( - mappable=heatmap_ax.get_children()[0], - cax=colorbar_ax, - orientation="horizontal", - drawedges=False, - extend="both", - ) - cb.outline.set_visible(False) - for bin_name, interval in list( - zip(*[utci_categories.bin_names, utci_categories.interval_index]) - ): - if np.isinf(interval.left): - ha = "right" - position = interval.right - elif np.isinf(interval.right): - ha = "left" - position = interval.left - else: - ha = "center" - position = np.mean([interval.left, interval.right]) - - colorbar_ax.text( - position, - 1.05, - textwrap.fill(bin_name, 11), - ha=ha, - va="bottom", - fontsize="x-small", - # transform=colorbar_ax.transAxes, + style_context = kwargs.get("style_context", "python_toolkit.bhom") + + with plt.style.context(style_context): + fig = plt.figure(figsize=figsize, constrained_layout=True) + spec = fig.add_gridspec(ncols=1, nrows=2, width_ratios=[1], height_ratios=[5, 2], hspace=0.0) + heatmap_ax = fig.add_subplot(spec[0, 0]) + histogram_ax = fig.add_subplot(spec[1, 0]) + + # Add heatmap + utci_categories.annual_heatmap(series, ax=heatmap_ax, show_colorbar=False, **kwargs) + + # Add stacked plot + utci_categories.annual_monthly_histogram(series=series, ax=histogram_ax, show_labels=True, **kwargs) + + if show_colorbar: + # add colorbar + divider = make_axes_locatable(histogram_ax) + colorbar_ax = divider.append_axes("bottom", size="20%", pad=0.7) + cb = fig.colorbar( + mappable=heatmap_ax.get_children()[0], + cax=colorbar_ax, + orientation="horizontal", + drawedges=False, + extend="both", ) - - title = f"{series.name} - {title}" if title is not None else series.name - heatmap_ax.set_title(title, y=1, ha="left", va="bottom", x=0) + cb.outline.set_visible(False) + for bin_name, interval in list( + zip(*[utci_categories.bin_names, utci_categories.interval_index]) + ): + if np.isinf(interval.left): + ha = "right" + position = interval.right + elif np.isinf(interval.right): + ha = "left" + position = interval.left + else: + ha = "center" + position = np.mean([interval.left, interval.right]) + + colorbar_ax.text( + position, + 1.05, + textwrap.fill(bin_name, 11), + ha=ha, + va="bottom", + fontsize="x-small", + # transform=colorbar_ax.transAxes, + ) + + title = f"{series.name} - {title}" if title is not None else series.name + heatmap_ax.set_title(title, y=1, ha="left", va="bottom", x=0) return fig @@ -830,110 +864,115 @@ def utci_histogram( Set to True to show the UTCI category labels on the plot. Defaults to False. **kwargs: Additional keyword arguments to pass to the plotting function. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: A matplotlib Axes object. """ - if ax is None: - ax = plt.gca() + style_context = kwargs.pop("style_context", "python_toolkit.bhom") - if not isinstance(utci_collection.header.data_type, LB_UniversalThermalClimateIndex): - raise ValueError("Collection data type is not UTCI and cannot be used in this plot.") + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() - ti = kwargs.pop("title", None) - if ti is not None: - ax.set_title(ti) - - color = kwargs.pop("color", "white") - bg_lighten = kwargs.pop("bg_lighten", 0.5) - alpha = kwargs.pop("alpha", 0.5) - - # Fill between ranges - for interval, bin_color, name in list( - zip( - *[ - utci_categories.interval_index, - utci_categories.colors, - utci_categories.bin_names, - ] - ) - ): - ax.axvspan( - max([interval.left, -100]), - min([interval.right, 100]), - facecolor=lighten_color(bin_color, bg_lighten), - label=name, - ) + if not isinstance(utci_collection.header.data_type, LB_UniversalThermalClimateIndex): + raise ValueError("Collection data type is not UTCI and cannot be used in this plot.") - # get the bins - bins = kwargs.pop( - "bins", - np.linspace( - utci_categories.bins[1] - 100, - utci_categories.bins[-2] + 100, - int((utci_categories.bins[-2] + 100) - (utci_categories.bins[1] - 100)) + 1, - ), - ) - density = kwargs.pop("density", True) + ti = kwargs.pop("title", None) + if ti is not None: + ax.set_title(ti) - # get the binned data within categories - series = collection_to_series(utci_collection) - - # plot data - series.plot(kind="hist", ax=ax, bins=bins, color=color, alpha=alpha, density=density) + color = kwargs.pop("color", "white") + bg_lighten = kwargs.pop("bg_lighten", 0.5) + alpha = kwargs.pop("alpha", 0.5) - # set xlims - xlim = kwargs.pop( - "xlim", - (series.min() - 5, series.max() + 5), - ) - ax.set_xticks(utci_categories.bins[1:-1]) - ax.set_xlim(xlim) - ax.set_xlabel(series.name) - - # set ylims - ylim = kwargs.pop( - "ylim", - ax.get_ylim(), - ) - ax.set_ylim(ylim) - - # get positions for percentage labels - if show_labels: - counts = utci_categories.value_counts(series, density=False) - densities = counts / sum(counts) - _ylow, _yhigh = ax.get_ylim() - _xlow, _xhigh = ax.get_xlim() - for cnt, dens, interval, coll in list( + # Fill between ranges + for interval, bin_color, name in list( zip( *[ - counts, - densities, utci_categories.interval_index, utci_categories.colors, + utci_categories.bin_names, ] ) ): - if np.isinf(interval.left): - midpt = (interval.right + _xlow) / 2 - elif np.isinf(interval.right): - midpt = (interval.left + _xhigh) / 2 - else: - midpt = interval.mid - if midpt < _xlow or midpt > _xhigh: - continue - ax.text( - midpt, - _yhigh * 0.99, - f"{cnt}\n{dens:0.1%}", - ha="center", - va="top", - color=contrasting_color(lighten_color(coll, bg_lighten)), - fontsize="small", + ax.axvspan( + max([interval.left, -100]), + min([interval.right, 100]), + facecolor=lighten_color(bin_color, bg_lighten), + label=name, ) - ax.yaxis.set_major_formatter(mticker.PercentFormatter(xmax=1, decimals=2)) + # get the bins + bins = kwargs.pop( + "bins", + np.linspace( + utci_categories.bins[1] - 100, + utci_categories.bins[-2] + 100, + int((utci_categories.bins[-2] + 100) - (utci_categories.bins[1] - 100)) + 1, + ), + ) + density = kwargs.pop("density", True) + + # get the binned data within categories + series = collection_to_series(utci_collection) + + # plot data + series.plot(kind="hist", ax=ax, bins=bins, color=color, alpha=alpha, density=density) + + # set xlims + xlim = kwargs.pop( + "xlim", + (series.min() - 5, series.max() + 5), + ) + ax.set_xticks(utci_categories.bins[1:-1]) + ax.set_xlim(xlim) + ax.set_xlabel(series.name) + + # set ylims + ylim = kwargs.pop( + "ylim", + ax.get_ylim(), + ) + ax.set_ylim(ylim) + + # get positions for percentage labels + if show_labels: + counts = utci_categories.value_counts(series, density=False) + densities = counts / sum(counts) + _ylow, _yhigh = ax.get_ylim() + _xlow, _xhigh = ax.get_xlim() + for cnt, dens, interval, coll in list( + zip( + *[ + counts, + densities, + utci_categories.interval_index, + utci_categories.colors, + ] + ) + ): + if np.isinf(interval.left): + midpt = (interval.right + _xlow) / 2 + elif np.isinf(interval.right): + midpt = (interval.left + _xhigh) / 2 + else: + midpt = interval.mid + if midpt < _xlow or midpt > _xhigh: + continue + ax.text( + midpt, + _yhigh * 0.99, + f"{cnt}\n{dens:0.1%}", + ha="center", + va="top", + color=contrasting_color(lighten_color(coll, bg_lighten)), + fontsize="small", + ) + + ax.yaxis.set_major_formatter(mticker.PercentFormatter(xmax=1, decimals=2)) return ax @@ -967,6 +1006,8 @@ def utci_shade_benefit( Additional keyword arguments to pass to the plotting function. title (str, optional): The title of the plot. Defaults to None. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Figure: @@ -1028,6 +1069,7 @@ def utci_shade_benefit( title=title, sunrise_color="w", sunset_color="w", + **kwargs ) return fig diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/facades/condensation_risk/heatmap.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/facades/condensation_risk/heatmap.py index fc5ad636..97626d8c 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/facades/condensation_risk/heatmap.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/facades/condensation_risk/heatmap.py @@ -58,6 +58,8 @@ def facade_condensation_risk_chart_table(epw_file: str, thresholds: list[float] The filepath to save the resulting image file of the heatmap to. **kwargs: Additional keyword arguments to pass to the heatmap function. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: Figure: A matplotlib Figure object. @@ -71,20 +73,23 @@ def facade_condensation_risk_chart_table(epw_file: str, thresholds: list[float] figsize = kwargs.pop("figsize", (15, 8)) # Instantiate figure - fig = plt.figure(figsize=figsize, constrained_layout=True) - spec = fig.add_gridspec(ncols=1, nrows=2, width_ratios=[1], height_ratios=[6, 5], hspace=0) - chart_ax = fig.add_subplot(spec[0, 0]) - table_ax = fig.add_subplot(spec[1, 0]) + style_context = kwargs.get("style_context", "python_toolkit.bhom") - # Add Thresholds Chart - CATEGORIES.annual_threshold_chart(series, chart_ax, color = 'slategrey', **kwargs) + with plt.style.context(style_context): + fig = plt.figure(figsize=figsize, constrained_layout=True) + spec = fig.add_gridspec(ncols=1, nrows=2, width_ratios=[1], height_ratios=[6, 5], hspace=0) + chart_ax = fig.add_subplot(spec[0, 0]) + table_ax = fig.add_subplot(spec[1, 0]) + + # Add Thresholds Chart + CATEGORIES.annual_threshold_chart(series, chart_ax, color = 'slategrey', **kwargs) - # Add table - CATEGORIES.annual_monthly_table(series, table_ax, **kwargs) + # Add table + CATEGORIES.annual_monthly_table(series, table_ax, **kwargs) - title = f"{title}" if title is not None else series.name - chart_ax.set_title(title, y=1, ha="left", va="bottom", x=0) - chart_ax.set_anchor('W') + title = f"{title}" if title is not None else series.name + chart_ax.set_title(title, y=1, ha="left", va="bottom", x=0) + chart_ax.set_anchor('W') return fig @@ -104,6 +109,8 @@ def facade_condensation_risk_heatmap_histogram(epw_file: str, thresholds: list[f The filepath to save the resulting image file of the heatmap to. **kwargs: Additional keyword arguments to pass to the heatmap function. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: Figure: A matplotlib Figure object. @@ -117,18 +124,21 @@ def facade_condensation_risk_heatmap_histogram(epw_file: str, thresholds: list[f figsize = kwargs.pop("figsize", (15, 8)) # Instantiate figure - fig = plt.figure(figsize=figsize, constrained_layout=True) - spec = fig.add_gridspec(ncols=1, nrows=2, width_ratios=[1], height_ratios=[5, 3], hspace=0.0) - heatmap_ax = fig.add_subplot(spec[0, 0]) - histogram_ax = fig.add_subplot(spec[1, 0]) + style_context = kwargs.get("style_context", "python_toolkit.bhom") + + with plt.style.context(style_context): + fig = plt.figure(figsize=figsize, constrained_layout=True) + spec = fig.add_gridspec(ncols=1, nrows=2, width_ratios=[1], height_ratios=[5, 3], hspace=0.0) + heatmap_ax = fig.add_subplot(spec[0, 0]) + histogram_ax = fig.add_subplot(spec[1, 0]) - # Add heatmap - CATEGORIES.annual_heatmap(series, heatmap_ax, **kwargs) + # Add heatmap + CATEGORIES.annual_heatmap(series, heatmap_ax, **kwargs) - # Add stacked plot - CATEGORIES.annual_monthly_histogram(series, histogram_ax, False, True, **kwargs) + # Add stacked plot + CATEGORIES.annual_monthly_histogram(series, histogram_ax, False, True, **kwargs) - title = f"{series.name} - {title}" if title is not None else series.name - heatmap_ax.set_title(title, y=1, ha="left", va="bottom", x=0) + title = f"{series.name} - {title}" if title is not None else series.name + heatmap_ax.set_title(title, y=1, ha="left", va="bottom", x=0) return fig \ No newline at end of file diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/lb_geometry.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/lb_geometry.py index 911e676d..9fcae560 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/lb_geometry.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/lb_geometry.py @@ -42,6 +42,8 @@ def plot_lb_geo_2d( A matplotlib Axes object to plot on. Defaults to None. **kwargs: Keyword arguments to pass to the matplotlib plotting methods. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: @@ -50,55 +52,58 @@ def plot_lb_geo_2d( warnings.warn( "This method is undeveloped and needs splitting into multiple methods." ) - if ax is None: - ax = plt.gca() + style_context = kwargs.pop("style_context", "python_toolkit.bhom") - for geo in lb_geometry: - if isinstance(geo, (Point2D, Vector2D)): - ax.scatter(geo.x, geo.y) - continue - if isinstance(geo, LineSegment2D): - x_1, y_1 = geo.endpoints[0].to_array() - x_2, y_2 = geo.endpoints[1].to_array() - ax.plot([x_1, x_2], [y_1, y_2]) - continue - if isinstance(geo, Ray2D): - x_1, y_1 = geo.p.to_array() - x_2, y_2 = geo.v.to_array() - ax.plot([x_1, x_2], [y_1, y_2]) - continue - if isinstance(geo, Polyline2D): - xs, ys = list(zip(*geo.to_array())) - ax.plot(xs, ys) - continue - if isinstance(geo, Polygon2D): - xs, ys = [list(i) for i in zip(*geo.to_array())] - xs.append(xs[0]) - ys.append(ys[0]) - ax.plot(xs, ys) - continue - if isinstance(geo, Arc2D): - xs, ys = list( - zip( - *[ - geo.reflect(origin=geo.c, normal=Vector2D(1, 0)) - .rotate(origin=geo.c, angle=np.pi * 1.5) - .point_at(i) - .to_array() - for i in np.linspace(0, 1, 100) - ] - ) - ) - ax.plot(xs, ys) - continue - if isinstance(geo, Mesh2D): - polygons = [Polygon2D.from_array(i) for i in geo.face_vertices] - for polygon in polygons: - xs, ys = [list(i) for i in zip(*polygon.to_array())] + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() + + for geo in lb_geometry: + if isinstance(geo, (Point2D, Vector2D)): + ax.scatter(geo.x, geo.y) + continue + if isinstance(geo, LineSegment2D): + x_1, y_1 = geo.endpoints[0].to_array() + x_2, y_2 = geo.endpoints[1].to_array() + ax.plot([x_1, x_2], [y_1, y_2]) + continue + if isinstance(geo, Ray2D): + x_1, y_1 = geo.p.to_array() + x_2, y_2 = geo.v.to_array() + ax.plot([x_1, x_2], [y_1, y_2]) + continue + if isinstance(geo, Polyline2D): + xs, ys = list(zip(*geo.to_array())) + ax.plot(xs, ys) + continue + if isinstance(geo, Polygon2D): + xs, ys = [list(i) for i in zip(*geo.to_array())] xs.append(xs[0]) ys.append(ys[0]) ax.plot(xs, ys) - else: - print(f"{type(geo)} not yet supported") + continue + if isinstance(geo, Arc2D): + xs, ys = list( + zip( + *[ + geo.reflect(origin=geo.c, normal=Vector2D(1, 0)) + .rotate(origin=geo.c, angle=np.pi * 1.5) + .point_at(i) + .to_array() + for i in np.linspace(0, 1, 100) + ] + ) + ) + ax.plot(xs, ys) + continue + if isinstance(geo, Mesh2D): + polygons = [Polygon2D.from_array(i) for i in geo.face_vertices] + for polygon in polygons: + xs, ys = [list(i) for i in zip(*polygon.to_array())] + xs.append(xs[0]) + ys.append(ys[0]) + ax.plot(xs, ys) + else: + print(f"{type(geo)} not yet supported") return ax diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/spatial_heatmap.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/spatial_heatmap.py deleted file mode 100644 index ac757ad3..00000000 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/plot/spatial_heatmap.py +++ /dev/null @@ -1,169 +0,0 @@ -"""Methods for plotting spatial heatmaps.""" - -import matplotlib.pyplot as plt -import numpy as np -from matplotlib.colors import BoundaryNorm, Colormap -from matplotlib.figure import Figure -from matplotlib.tri import Triangulation -from mpl_toolkits.axes_grid1 import make_axes_locatable - -from python_toolkit.bhom.analytics import bhom_analytics - - -@bhom_analytics() -def spatial_heatmap( - triangulations: list[Triangulation], - values: list[list[float]], - levels: list[float] | int = None, - contours: list[float] = None, - contour_colors: list[str] = None, - contour_widths: list[float] = None, - cmap: Colormap = "viridis", - extend: str = "neither", - norm: BoundaryNorm = None, - xlims: list[float] = None, - ylims: list[float] = None, - colorbar_label: str = "", - title: str = "", - highlight_pts: dict[str, tuple[int]] = None, - show_legend_title: bool = True, - clabels: bool = False, -) -> Figure: - """Plot a spatial map of a variable using a triangulation and associated values. - - Args: - triangulations (list[Triangulation]): - A list of triangulations to plot. - values (list[list[float]]): - A list of values, corresponding with the triangulations and their respective indices. - levels (list[float] | int, optional): - The number of levels to include in the colorbar. Defaults to None which will use - 10-steps between the min/max for all given values. - contours (list[float], optional): - Add contours at the given values to the spatial plot. Defaults to None. - contour_colors (list[str], optional): - Color each of the listed contours. Defaults to None. - contour_widths (list[float], optional): - The width each of the listed contours. Defaults to None. - cmap (Colormap, optional): - The colormap to use for this plot. Defaults to "viridis". - extend (str, optional): - Define how to handle the end-points of the colorbar. Defaults to "neither". - norm (BoundaryNorm, optional): - A matplotlib BoundaryNorm object containing colormap boundary mapping information. - Defaults to None. - xlims (list[float], optional): - The x-limit for the plot. Defaults to None. - ylims (list[float], optional): - The y-limit for the plot. Defaults to None. - colorbar_label (str, optional): - A label to be placed next to the colorbar. Defaults to "". - title (str, optional): - The title to be placed on the plot. Defaults to "". - highlight_pts (dict[str, int], optional): - A set of points (and their names) to indicate on the spatial plot. Value is the int - index of the highlighted point. - show_legend_title (bool, optional): - A convenient flag to hide the legend and title. - clabels (bool, optional): - A flag to show contour labels. Defaults to False. - - Returns: - Figure: A matplotlib Figure object. - """ - for tri, zs in list(zip(*[triangulations, values])): - if len(tri.x) != len(zs): - raise ValueError( - "The shape of the triangulations and values given do not match." - ) - - if levels is None: - levels = np.linspace( - min(np.amin(i) for i in values), max(np.amax(i) for i in values), 10 - ) - - if xlims is None: - xlims = [ - min(i.x.min() for i in triangulations), - max(i.x.max() for i in triangulations), - ] - - if ylims is None: - ylims = [ - min(i.y.min() for i in triangulations), - max(i.y.max() for i in triangulations), - ] - - fig, ax = plt.subplots(1, 1, figsize=(8, 8)) - - ax.set_aspect("equal") - ax.axis("off") - - ax.set_xlim(xlims) - ax.set_ylim(ylims) - - tcls = [] - for tri, zs in list(zip(*[triangulations, values])): - tcf = ax.tricontourf( - tri, zs, extend=extend, cmap=cmap, levels=levels, norm=norm - ) - # add contour lines - if contours is not None: - if not ( - all(i < np.amin(zs) for i in contours) - or all(i > np.amax(zs) for i in contours) - ): - if contour_widths is None: - contour_widths = [1.5] * len(contours) - if contour_colors is None: - contour_colors = ["k"] * len(contours) - if len(contour_colors) != len(contours) != len(contour_widths): - raise ValueError("contour vars must be same length") - tcl = ax.tricontour( - tri, - zs, - levels=contours, - colors=contour_colors, - linewidths=contour_widths, - ) - if clabels: - ax.clabel(tcl, inline=1, fontsize="small", colors=contour_colors) - tcls.append(tcl) - - if highlight_pts is not None: - if len(triangulations) > 1: - raise ValueError( - "Point highlighting is only possible for 1-length triangulations." - ) - pt_size = (xlims[1] - xlims[0]) / 5 - for k, v in highlight_pts.items(): - ax.scatter( - triangulations[0].x[v], triangulations[0].y[v], s=pt_size, c="red" - ) - ax.text( - triangulations[0].x[v] + (pt_size / 10), - triangulations[0].y[v], - k, - ha="left", - va="center", - ) - - if show_legend_title: - # Plot colorbar - divider = make_axes_locatable(ax) - cax = divider.append_axes("right", size="5%", pad=0.1, aspect=20) - - cbar = plt.colorbar( - tcf, cax=cax # , format=mticker.StrMethodFormatter("{x:04.1f}") - ) - cbar.outline.set_visible(False) - cbar.set_label(colorbar_label) - - for tcl in tcls: - cbar.add_lines(tcl) - - ax.set_title(title, ha="left", va="bottom", x=0) - - plt.tight_layout() - - return fig diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/solar.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/solar.py index 2329c22a..acc6f25f 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/solar.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/solar.py @@ -917,6 +917,7 @@ def radiation_rose( label: bool = True, bar_width: float = 1, lims: tuple[float, float] = None, + style_context:str = "python_toolkit.bhom" ) -> plt.Axes: """Create a solar radiation rose @@ -950,132 +951,135 @@ def radiation_rose( lims (tuple[float, float], optional): Set the limits of the plot. Defaults to None. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: The matplotlib axes. """ - if ax is None: - _, ax = plt.subplots(subplot_kw={"projection": "polar"}) - - if ax.name != "polar": - raise ValueError("ax must be a polar axis.") - - match rad_type: - case IrradianceType.TOTAL: - rad_type = "total" - case IrradianceType.DIRECT: - rad_type = "direct" - case IrradianceType.DIFFUSE: - rad_type = "diffuse" - case IrradianceType.REFLECTED: - raise NotImplementedError("Reflected irradiance not yet supported.") - case _: - raise ValueError("rad_type must be IrradianceType.") - - # create sky conditions - smx = SkyMatrix.from_epw(epw_file=epw_file, high_density=True, hoys=analysis_period.hoys) - rr = RadiationRose(sky_matrix=smx, direction_count=directions, tilt_angle=tilt_angle) - - # get properties to plot - angles = np.deg2rad( - [angle_from_north(j) for j in [Vector2D(*i[:2]) for i in rr.direction_vectors]] - ) - values = getattr(rr, f"{rad_type}_values") - if lims is None: - norm = Normalize(vmin=0, vmax=max(values)) - else: - norm = Normalize(vmin=lims[0], vmax=lims[1]) - cmap = plt.get_cmap(cmap) - colors = [cmap(i) for i in [norm(v) for v in values]] - - # generate plot - rects = ax.bar( - x=angles, - height=values, - width=((np.pi / directions) * 2) * bar_width, - color=colors, - ) - format_polar_plot(ax) - - # add colormap - sm = ScalarMappable(cmap=cmap, norm=norm) - sm.set_array([]) - cbar = plt.colorbar( - sm, - ax=ax, - orientation="vertical", - label="Cumulative irradiance (W/m$^2$)", - fraction=0.046, - pad=0.04, - ) - cbar.outline.set_visible(False) - - # add labels - if label: - offset_distance = max(values) / 10 - if directions > 36: - max_angle = angles[np.argmax(values)] - max_val = max(values) - ax.text( - max_angle, - max_val + offset_distance, - f"{max_val:0.0f}W/m$^2$\n{np.rad2deg(max_angle):0.0f}°", - fontsize="xx-small", - ha="center", - va="center", - rotation=0, - rotation_mode="anchor", - color="k", - ) + with plt.style.context(style_context): + if ax is None: + _, ax = plt.subplots(subplot_kw={"projection": "polar"}) + + if ax.name != "polar": + raise ValueError("ax must be a polar axis.") + + match rad_type: + case IrradianceType.TOTAL: + rad_type = "total" + case IrradianceType.DIRECT: + rad_type = "direct" + case IrradianceType.DIFFUSE: + rad_type = "diffuse" + case IrradianceType.REFLECTED: + raise NotImplementedError("Reflected irradiance not yet supported.") + case _: + raise ValueError("rad_type must be IrradianceType.") + + # create sky conditions + smx = SkyMatrix.from_epw(epw_file=epw_file, high_density=True, hoys=analysis_period.hoys) + rr = RadiationRose(sky_matrix=smx, direction_count=directions, tilt_angle=tilt_angle) + + # get properties to plot + angles = np.deg2rad( + [angle_from_north(j) for j in [Vector2D(*i[:2]) for i in rr.direction_vectors]] + ) + values = getattr(rr, f"{rad_type}_values") + if lims is None: + norm = Normalize(vmin=0, vmax=max(values)) else: - for rect, color in list(zip(*[rects, colors])): - theta = rect.get_x() + (rect.get_width() / 2) - theta_deg = np.rad2deg(theta) - val = rect.get_height() - - if theta_deg < 180: - if val < max(values) / 2: - ha = "left" - anchor = val + offset_distance - else: - ha = "right" - anchor = val - offset_distance - ax.text( - theta, - anchor, - f"{val:0.0f}", - fontsize="xx-small", - ha=ha, - va="center", - rotation=90 - theta_deg, - rotation_mode="anchor", - color=contrasting_color(color), - ) - else: - if val < max(values) / 2: - ha = "right" - anchor = val + offset_distance + norm = Normalize(vmin=lims[0], vmax=lims[1]) + cmap = plt.get_cmap(cmap) + colors = [cmap(i) for i in [norm(v) for v in values]] + + # generate plot + rects = ax.bar( + x=angles, + height=values, + width=((np.pi / directions) * 2) * bar_width, + color=colors, + ) + format_polar_plot(ax) + + # add colormap + sm = ScalarMappable(cmap=cmap, norm=norm) + sm.set_array([]) + cbar = plt.colorbar( + sm, + ax=ax, + orientation="vertical", + label="Cumulative irradiance (W/m$^2$)", + fraction=0.046, + pad=0.04, + ) + cbar.outline.set_visible(False) + + # add labels + if label: + offset_distance = max(values) / 10 + if directions > 36: + max_angle = angles[np.argmax(values)] + max_val = max(values) + ax.text( + max_angle, + max_val + offset_distance, + f"{max_val:0.0f}W/m$^2$\n{np.rad2deg(max_angle):0.0f}°", + fontsize="xx-small", + ha="center", + va="center", + rotation=0, + rotation_mode="anchor", + color="k", + ) + else: + for rect, color in list(zip(*[rects, colors])): + theta = rect.get_x() + (rect.get_width() / 2) + theta_deg = np.rad2deg(theta) + val = rect.get_height() + + if theta_deg < 180: + if val < max(values) / 2: + ha = "left" + anchor = val + offset_distance + else: + ha = "right" + anchor = val - offset_distance + ax.text( + theta, + anchor, + f"{val:0.0f}", + fontsize="xx-small", + ha=ha, + va="center", + rotation=90 - theta_deg, + rotation_mode="anchor", + color=contrasting_color(color), + ) else: - ha = "left" - anchor = val - offset_distance - ax.text( - theta, - anchor, - f"{val:0.0f}", - fontsize="xx-small", - ha=ha, - va="center", - rotation=-theta_deg - 90, - rotation_mode="anchor", - color=contrasting_color(color), - ) - - ax.set_title( - f"{epw_file.name}\n{rad_type.title()} irradiance ({tilt_angle}° altitude)\n{describe_analysis_period(analysis_period)}" - ) - - plt.tight_layout() + if val < max(values) / 2: + ha = "right" + anchor = val + offset_distance + else: + ha = "left" + anchor = val - offset_distance + ax.text( + theta, + anchor, + f"{val:0.0f}", + fontsize="xx-small", + ha=ha, + va="center", + rotation=-theta_deg - 90, + rotation_mode="anchor", + color=contrasting_color(color), + ) + + ax.set_title( + f"{epw_file.name}\n{rad_type.title()} irradiance ({tilt_angle}° altitude)\n{describe_analysis_period(analysis_period)}" + ) + + plt.tight_layout() return ax @@ -1146,6 +1150,7 @@ def tilt_orientation_factor( tilts: int = 9, quantiles: tuple[float] = (0.05, 0.25, 0.5, 0.75, 0.95), lims: tuple[float, float] = None, + style_context:str = "python_toolkit.bhom" ) -> plt.Axes: """Create a tilt-orientation factor plot. @@ -1176,105 +1181,108 @@ def tilt_orientation_factor( lims (tuple[float, float], optional): The limits of the plot. Defaults to None. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: The matplotlib axes. """ - if ax is None: - ax = plt.gca() - - cmap = plt.get_cmap(cmap) - - values, _directions, _tilts = create_radiation_matrix(epw_file=epw_file, rad_type=rad_type, analysis_period=analysis_period, directions=directions, tilts=tilts) - - # create x, y coordinates per result value - __directions, __tilts = np.meshgrid(_directions, _tilts) - - # get location of max - _max = values.flatten().max() - if _max == 0: - raise ValueError(f"No solar radiation within {analysis_period}.") - - _max_idx = values.flatten().argmax() - _max_alt = __tilts.flatten()[_max_idx] - _max_az = __directions.flatten()[_max_idx] - - # create colormap - if lims is None: - norm = Normalize(vmin=0, vmax=_max) - else: - norm = Normalize(vmin=lims[0], vmax=lims[1]) - - # create triangulation - tri = Triangulation(x=__directions.flatten(), y=__tilts.flatten()) - - # create quantile lines - quantiles = [0.05, 0.25, 0.5, 0.75, 0.95] - levels = [np.quantile(a=values.flatten(), q=i) for i in quantiles] - quant_colors = [cmap(i) for i in [norm(v) for v in levels]] - quant_colors_inv = [contrasting_color(i) for i in quant_colors] - max_color_inv = contrasting_color(cmap(norm(_max))) - - # plot data - tcf = ax.tricontourf(tri, values.flatten(), levels=100, cmap=cmap, norm=norm) - tcl = ax.tricontour( - tri, - values.flatten(), - levels=levels, - colors=quant_colors_inv, - linestyles=":", - alpha=0.5, - ) - - # add contour labels - def cl_fmt(x): - return f"{x:,.0f}W/m$^2$" - - _ = ax.clabel(tcl, fontsize="small", fmt=cl_fmt) - - # add colorbar - cb = plt.colorbar( - tcf, - ax=ax, - orientation="vertical", - drawedges=False, - fraction=0.05, - aspect=25, - pad=0.02, - label="Cumulative irradiance (W/m$^2$)", - ) - cb.outline.set_visible(False) - for quantile_val in levels: - cb.ax.plot([0, 1], [quantile_val] * 2, color="k", ls="-", alpha=0.5) - - # add max-location - ax.scatter(_max_az, _max_alt, c=max_color_inv, s=10, marker="x") - alt_offset = (90 / 100) * 0.5 if _max_alt <= 45 else -(90 / 100) * 0.5 - az_offset = (360 / 100) * 0.5 if _max_az <= 180 else -(360 / 100) * 0.5 - ha = "left" if _max_az <= 180 else "right" - va = "bottom" if _max_alt <= 45 else "top" - ax.text( - _max_az + az_offset, - _max_alt + alt_offset, - f"{_max:,.0f}W/m$^2$\n({_max_az:0.0f}°, {_max_alt:0.0f}°)", - ha=ha, - va=va, - c=max_color_inv, - weight="bold", - size="small", - ) - - ax.set_xlim(0, 360) - ax.set_ylim(0, 90) - ax.xaxis.set_major_locator(MultipleLocator(base=30)) - ax.yaxis.set_major_locator(MultipleLocator(base=10)) - ax.set_xlabel("Panel orientation (clockwise from North at 0°)") - ax.set_ylabel("Panel tilt (0° facing the horizon, 90° facing the sky)") - - ax.set_title( - f"{epw_file.name}\n{rad_type.to_string()} irradiance (cumulative)\n{describe_analysis_period(analysis_period)}" - ) + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() + + cmap = plt.get_cmap(cmap) + + values, _directions, _tilts = create_radiation_matrix(epw_file=epw_file, rad_type=rad_type, analysis_period=analysis_period, directions=directions, tilts=tilts) + + # create x, y coordinates per result value + __directions, __tilts = np.meshgrid(_directions, _tilts) + + # get location of max + _max = values.flatten().max() + if _max == 0: + raise ValueError(f"No solar radiation within {analysis_period}.") + + _max_idx = values.flatten().argmax() + _max_alt = __tilts.flatten()[_max_idx] + _max_az = __directions.flatten()[_max_idx] + + # create colormap + if lims is None: + norm = Normalize(vmin=0, vmax=_max) + else: + norm = Normalize(vmin=lims[0], vmax=lims[1]) + + # create triangulation + tri = Triangulation(x=__directions.flatten(), y=__tilts.flatten()) + + # create quantile lines + quantiles = [0.05, 0.25, 0.5, 0.75, 0.95] + levels = [np.quantile(a=values.flatten(), q=i) for i in quantiles] + quant_colors = [cmap(i) for i in [norm(v) for v in levels]] + quant_colors_inv = [contrasting_color(i) for i in quant_colors] + max_color_inv = contrasting_color(cmap(norm(_max))) + + # plot data + tcf = ax.tricontourf(tri, values.flatten(), levels=100, cmap=cmap, norm=norm) + tcl = ax.tricontour( + tri, + values.flatten(), + levels=levels, + colors=quant_colors_inv, + linestyles=":", + alpha=0.5, + ) + + # add contour labels + def cl_fmt(x): + return f"{x:,.0f}W/m$^2$" + + _ = ax.clabel(tcl, fontsize="small", fmt=cl_fmt) + + # add colorbar + cb = plt.colorbar( + tcf, + ax=ax, + orientation="vertical", + drawedges=False, + fraction=0.05, + aspect=25, + pad=0.02, + label="Cumulative irradiance (W/m$^2$)", + ) + cb.outline.set_visible(False) + for quantile_val in levels: + cb.ax.plot([0, 1], [quantile_val] * 2, color="k", ls="-", alpha=0.5) + + # add max-location + ax.scatter(_max_az, _max_alt, c=max_color_inv, s=10, marker="x") + alt_offset = (90 / 100) * 0.5 if _max_alt <= 45 else -(90 / 100) * 0.5 + az_offset = (360 / 100) * 0.5 if _max_az <= 180 else -(360 / 100) * 0.5 + ha = "left" if _max_az <= 180 else "right" + va = "bottom" if _max_alt <= 45 else "top" + ax.text( + _max_az + az_offset, + _max_alt + alt_offset, + f"{_max:,.0f}W/m$^2$\n({_max_az:0.0f}°, {_max_alt:0.0f}°)", + ha=ha, + va=va, + c=max_color_inv, + weight="bold", + size="small", + ) + + ax.set_xlim(0, 360) + ax.set_ylim(0, 90) + ax.xaxis.set_major_locator(MultipleLocator(base=30)) + ax.yaxis.set_major_locator(MultipleLocator(base=10)) + ax.set_xlabel("Panel orientation (clockwise from North at 0°)") + ax.set_ylabel("Panel tilt (0° facing the horizon, 90° facing the sky)") + + ax.set_title( + f"{epw_file.name}\n{rad_type.to_string()} irradiance (cumulative)\n{describe_analysis_period(analysis_period)}" + ) return ax diff --git a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/wind.py b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/wind.py index 85d5f69e..7d7734c8 100644 --- a/LadybugTools_Engine/Python/src/ladybugtools_toolkit/wind.py +++ b/LadybugTools_Engine/Python/src/ladybugtools_toolkit/wind.py @@ -1620,21 +1620,26 @@ def plot_timeseries(self, ax: plt.Axes = None, **kwargs) -> plt.Axes: # type: i Additional keyword arguments to pass to the function. These include: title (str, optional): A title for the plot. Defaults to None. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: A matplotlib Axes object. """ + + style_context = kwargs.get("style_context", "python_toolkit.bhom") - if ax is None: - ax = plt.gca() + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() - ax.set_title(textwrap.fill(f"{self.source}", 75)) + ax.set_title(textwrap.fill(f"{self.source}", 75)) - timeseries(self.ws, ax=ax, **kwargs) + timeseries(self.ws, ax=ax, **kwargs) - ax.set_ylabel(self.ws.name) + ax.set_ylabel(self.ws.name) return ax @@ -1663,105 +1668,110 @@ def plot_windmatrix( Additional keyword arguments to pass to the pcolor function. title (str, optional): A title for the plot. Defaults to None. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: A matplotlib Axes object. """ + + style_context = kwargs.pop("style_context", "python_toolkit.bhom") - if ax is None: - ax = plt.gca() - - if other_data is None: - other_data = self.ws - - title = self.source if self.source is not None else "" - title += f'\n{kwargs.pop("title", None)}' - ax.set_title(textwrap.fill(f"{title}", 75)) - - df = self.wind_matrix(other_data=other_data) - _other_data = df["other"] - _wind_directions = df["direction"] + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() - if any( - [ - _other_data.shape != (24, 12), - _wind_directions.shape != (24, 12), - _wind_directions.shape != _other_data.shape, - not _wind_directions.index.equals(_other_data.index), - not _wind_directions.columns.equals(_other_data.columns), - # not np.array_equal(_wind_directions.index, _other_data.index), - # not np.array_equal(_wind_directions.columns, _other_data.columns), - ] - ): - raise ValueError( - "The other_data and wind_directions must cover all months of the " - "year, and all hours of the day, and align with each other." - ) - - cmap = kwargs.pop("cmap", "YlGnBu") - vmin = kwargs.pop("vmin", _other_data.values.min()) - vmax = kwargs.pop("vmax", _other_data.values.max()) - cbar_title = kwargs.pop("cbar_title", None) - unit = kwargs.pop("unit", None) - norm = kwargs.pop("norm", Normalize(vmin=vmin, vmax=vmax, clip=True)) - mapper = kwargs.pop("mapper", ScalarMappable(norm=norm, cmap=cmap)) - - pc = ax.pcolor(_other_data, cmap=cmap, vmin=vmin, vmax=vmax, **kwargs) - _x = -np.sin(np.deg2rad(_wind_directions.values)) - _y = -np.cos(np.deg2rad(_wind_directions.values)) - direction_matrix = angle_from_north([_x, _y]) - if show_arrows: - arrow_scale = 0.8 - ax.quiver( - np.arange(1, 13, 1) - 0.5, - np.arange(0, 24, 1) + 0.5, - (_x * _other_data.values / 2) * arrow_scale, - (_y * _other_data.values / 2) * arrow_scale, - pivot="mid", - fc="white", - ec="black", - lw=0.5, - alpha=0.5, - ) - - if show_values: - for _xx, col in enumerate(_wind_directions.values.T): - for _yy, _ in enumerate(col.T): - local_value = _other_data.values[_yy, _xx] - cell_color = mapper.to_rgba(local_value) - text_color = contrasting_color(cell_color) - # direction text - ax.text( - _xx, - _yy, - f"{direction_matrix[_yy][_xx]:0.0f}°", - color=text_color, - ha="left", - va="bottom", - fontsize="xx-small", - ) - # other_data text - ax.text( - _xx + 1, - _yy + 1, - f"{_other_data.values[_yy][_xx]:0.1f}{unit}", - color=text_color, - ha="right", - va="top", - fontsize="xx-small", - ) + if other_data is None: + other_data = self.ws + + title = self.source if self.source is not None else "" + title += f'\n{kwargs.pop("title", None)}' + ax.set_title(textwrap.fill(f"{title}", 75)) + + df = self.wind_matrix(other_data=other_data) + _other_data = df["other"] + _wind_directions = df["direction"] + + if any( + [ + _other_data.shape != (24, 12), + _wind_directions.shape != (24, 12), + _wind_directions.shape != _other_data.shape, + not _wind_directions.index.equals(_other_data.index), + not _wind_directions.columns.equals(_other_data.columns), + # not np.array_equal(_wind_directions.index, _other_data.index), + # not np.array_equal(_wind_directions.columns, _other_data.columns), + ] + ): + raise ValueError( + "The other_data and wind_directions must cover all months of the " + "year, and all hours of the day, and align with each other." + ) - ax.set_xticks(np.arange(1, 13, 1) - 0.5) - ax.set_xticklabels([calendar.month_abbr[i] for i in np.arange(1, 13, 1)]) - ax.set_yticks(np.arange(0, 24, 1) + 0.5) - ax.set_yticklabels([f"{i:02d}:00" for i in np.arange(0, 24, 1)]) - for label in ax.yaxis.get_ticklabels()[1::2]: - label.set_visible(False) + cmap = kwargs.pop("cmap", "YlGnBu") + vmin = kwargs.pop("vmin", _other_data.values.min()) + vmax = kwargs.pop("vmax", _other_data.values.max()) + cbar_title = kwargs.pop("cbar_title", None) + unit = kwargs.pop("unit", None) + norm = kwargs.pop("norm", Normalize(vmin=vmin, vmax=vmax, clip=True)) + mapper = kwargs.pop("mapper", ScalarMappable(norm=norm, cmap=cmap)) + + pc = ax.pcolor(_other_data, cmap=cmap, vmin=vmin, vmax=vmax, **kwargs) + _x = -np.sin(np.deg2rad(_wind_directions.values)) + _y = -np.cos(np.deg2rad(_wind_directions.values)) + direction_matrix = angle_from_north([_x, _y]) + if show_arrows: + arrow_scale = 0.8 + ax.quiver( + np.arange(1, 13, 1) - 0.5, + np.arange(0, 24, 1) + 0.5, + (_x * _other_data.values / 2) * arrow_scale, + (_y * _other_data.values / 2) * arrow_scale, + pivot="mid", + fc="white", + ec="black", + lw=0.5, + alpha=0.5, + ) - cb = plt.colorbar(pc, label=cbar_title, pad=0.01) - cb.outline.set_visible(False) + if show_values: + for _xx, col in enumerate(_wind_directions.values.T): + for _yy, _ in enumerate(col.T): + local_value = _other_data.values[_yy, _xx] + cell_color = mapper.to_rgba(local_value) + text_color = contrasting_color(cell_color) + # direction text + ax.text( + _xx, + _yy, + f"{direction_matrix[_yy][_xx]:0.0f}°", + color=text_color, + ha="left", + va="bottom", + fontsize="xx-small", + ) + # other_data text + ax.text( + _xx + 1, + _yy + 1, + f"{_other_data.values[_yy][_xx]:0.1f}{unit}", + color=text_color, + ha="right", + va="top", + fontsize="xx-small", + ) + + ax.set_xticks(np.arange(1, 13, 1) - 0.5) + ax.set_xticklabels([calendar.month_abbr[i] for i in np.arange(1, 13, 1)]) + ax.set_yticks(np.arange(0, 24, 1) + 0.5) + ax.set_yticklabels([f"{i:02d}:00" for i in np.arange(0, 24, 1)]) + for label in ax.yaxis.get_ticklabels()[1::2]: + label.set_visible(False) + + cb = plt.colorbar(pc, label=cbar_title, pad=0.01) + cb.outline.set_visible(False) return ax @@ -1772,6 +1782,7 @@ def plot_densityfunction( percentiles: tuple[float] = (0.5, 0.95), function: str = "pdf", ylim: tuple[float] = None, + style_context:str = "python_toolkit.bhom" ) -> plt.Axes: """Create a histogram showing wind speed frequency. @@ -1786,6 +1797,8 @@ def plot_densityfunction( The function to use. Either "pdf" or "cdf". Defaults to "pdf". ylim (tuple[float], optional): The y-axis limits. Defaults to None. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: The axes object. @@ -1794,43 +1807,44 @@ def plot_densityfunction( if function not in ["pdf", "cdf"]: raise ValueError('function must be either "pdf" or "cdf".') - if ax is None: - ax = plt.gca() - - ax.set_title( - f"{str(self)}\n{'Probability Density Function' if function == 'pdf' else 'Cumulative Density Function'}" - ) + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() - self.ws.plot.hist( - ax=ax, - density=True, - bins=speed_bins, - cumulative=True if function == "cdf" else False, - ) + ax.set_title( + f"{str(self)}\n{'Probability Density Function' if function == 'pdf' else 'Cumulative Density Function'}" + ) - for percentile in percentiles: - x = np.quantile(self.ws, percentile) - ax.axvline(x, 0, 1, ls="--", lw=1, c="black", alpha=0.5) - ax.text( - x + 0.05, - 0, - f"{percentile:0.0%}\n{x:0.2f}m/s", - ha="left", - va="bottom", + self.ws.plot.hist( + ax=ax, + density=True, + bins=speed_bins, + cumulative=True if function == "cdf" else False, ) - ax.set_xlim(0, ax.get_xlim()[-1]) - if ylim: - ax.set_ylim(ylim) + for percentile in percentiles: + x = np.quantile(self.ws, percentile) + ax.axvline(x, 0, 1, ls="--", lw=1, c="black", alpha=0.5) + ax.text( + x + 0.05, + 0, + f"{percentile:0.0%}\n{x:0.2f}m/s", + ha="left", + va="bottom", + ) - ax.set_xlabel("Wind Speed (m/s)") - ax.set_ylabel("Frequency") + ax.set_xlim(0, ax.get_xlim()[-1]) + if ylim: + ax.set_ylim(ylim) - for spine in ["top", "right"]: - ax.spines[spine].set_visible(False) - ax.grid(visible=True, which="major", axis="both", ls="--", lw=1, alpha=0.25) + ax.set_xlabel("Wind Speed (m/s)") + ax.set_ylabel("Frequency") - ax.yaxis.set_major_formatter(mticker.PercentFormatter(1, decimals=1)) + for spine in ["top", "right"]: + ax.spines[spine].set_visible(False) + ax.grid(visible=True, which="major", axis="both", ls="--", lw=1, alpha=0.25) + + ax.yaxis.set_major_formatter(mticker.PercentFormatter(1, decimals=1)) return ax @@ -1845,6 +1859,7 @@ def plot_windrose( legend: bool = True, ylim: tuple[float] = None, label: bool = False, + style_context:str = "python_toolkit.bhom" ) -> plt.Axes: """Create a wind rose showing wind speed and direction frequency. @@ -1871,108 +1886,111 @@ def plot_windrose( The y-axis limits. Defaults to None. label (bool, optional): Set to False to remove the bin labels. Defaults to False. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: The axes object. """ + + with plt.style.context(style_context): + if ax is None: + _, ax = plt.subplots(subplot_kw={"projection": "polar"}) + + # create grouped data for plotting + binned = self.histogram( + directions=directions, + other_data=other_data, + other_bins=other_bins, + density=True, + remove_calm=True, + ) - if ax is None: - _, ax = plt.subplots(subplot_kw={"projection": "polar"}) + # set colors + if colors is None: + if other_data is None: + colors = [ + to_hex(BEAUFORT_CATEGORIES.cmap(i)) + for i in np.linspace(0, 1, len(binned.columns)) + ] + else: + colors = [ + to_hex(plt.get_cmap("viridis")(i)) + for i in np.linspace(0, 1, len(binned.columns)) + ] + if isinstance(colors, str): + colors = plt.get_cmap(colors) + if isinstance(colors, Colormap): + colors = [to_hex(colors(i)) for i in np.linspace(0, 1, len(binned.columns))] + if isinstance(colors, list | tuple): + if len(colors) != len(binned.columns): + raise ValueError( + f"colors must be a list of length {len(binned.columns)}, or a colormap." + ) - # create grouped data for plotting - binned = self.histogram( - directions=directions, - other_data=other_data, - other_bins=other_bins, - density=True, - remove_calm=True, - ) + # HACK to ensure that bar ends are curved when using a polar plot. + fig = plt.figure() + rect = [0.1, 0.1, 0.8, 0.8] + hist_ax = plt.Axes(fig, rect) + hist_ax.bar(np.array([1]), np.array([1])) - # set colors - if colors is None: - if other_data is None: - colors = [ - to_hex(BEAUFORT_CATEGORIES.cmap(i)) - for i in np.linspace(0, 1, len(binned.columns)) - ] + if title is None or title == "": + ax.set_title(textwrap.fill(f"{self.source}", 75)) else: - colors = [ - to_hex(plt.get_cmap("viridis")(i)) - for i in np.linspace(0, 1, len(binned.columns)) - ] - if isinstance(colors, str): - colors = plt.get_cmap(colors) - if isinstance(colors, Colormap): - colors = [to_hex(colors(i)) for i in np.linspace(0, 1, len(binned.columns))] - if isinstance(colors, list | tuple): - if len(colors) != len(binned.columns): - raise ValueError( - f"colors must be a list of length {len(binned.columns)}, or a colormap." - ) - - # HACK to ensure that bar ends are curved when using a polar plot. - fig = plt.figure() - rect = [0.1, 0.1, 0.8, 0.8] - hist_ax = plt.Axes(fig, rect) - hist_ax.bar(np.array([1]), np.array([1])) - - if title is None or title == "": - ax.set_title(textwrap.fill(f"{self.source}", 75)) - else: - ax.set_title(title) - - theta_width = np.deg2rad(360 / directions) - patches = [] - color_list = [] - x = theta_width / 2 - for _, data_values in binned.iterrows(): - y = 0 - for n, val in enumerate(data_values.values): - patches.append( - Rectangle( - xy=(x, y), - width=theta_width, - height=val, - alpha=1, + ax.set_title(title) + + theta_width = np.deg2rad(360 / directions) + patches = [] + color_list = [] + x = theta_width / 2 + for _, data_values in binned.iterrows(): + y = 0 + for n, val in enumerate(data_values.values): + patches.append( + Rectangle( + xy=(x, y), + width=theta_width, + height=val, + alpha=1, + ) ) + color_list.append(colors[n]) + y += val + if label: + ax.text(x, y, f"{y:0.1%}", ha="center", va="center", fontsize="x-small") + x += theta_width + local_cmap = ListedColormap(np.array(color_list).flatten()) + pc = PatchCollection(patches, cmap=local_cmap) + pc.set_array(np.arange(len(color_list))) + ax.add_collection(pc) + + # construct legend + if legend: + handles = [ + mpatches.Patch(color=colors[n], label=f"{i} to {j}") + for n, (i, j) in enumerate(binned.columns.values) + ] + _ = ax.legend( + handles=handles, + bbox_to_anchor=(1.1, 0.5), + loc="center left", + ncol=1, + borderaxespad=0, + frameon=False, + fontsize="small", + title=binned.columns.name, + title_fontsize="small", ) - color_list.append(colors[n]) - y += val - if label: - ax.text(x, y, f"{y:0.1%}", ha="center", va="center", fontsize="x-small") - x += theta_width - local_cmap = ListedColormap(np.array(color_list).flatten()) - pc = PatchCollection(patches, cmap=local_cmap) - pc.set_array(np.arange(len(color_list))) - ax.add_collection(pc) - - # construct legend - if legend: - handles = [ - mpatches.Patch(color=colors[n], label=f"{i} to {j}") - for n, (i, j) in enumerate(binned.columns.values) - ] - _ = ax.legend( - handles=handles, - bbox_to_anchor=(1.1, 0.5), - loc="center left", - ncol=1, - borderaxespad=0, - frameon=False, - fontsize="small", - title=binned.columns.name, - title_fontsize="small", - ) - # set y-axis limits - if ylim is None: - ylim = (0, max(binned.sum(axis=1))) - if len(ylim) != 2: - raise ValueError("ylim must be a tuple of length 2.") - ax.set_ylim(ylim) - ax.yaxis.set_major_formatter(mticker.PercentFormatter(xmax=1)) + # set y-axis limits + if ylim is None: + ylim = (0, max(binned.sum(axis=1))) + if len(ylim) != 2: + raise ValueError("ylim must be a tuple of length 2.") + ax.set_ylim(ylim) + ax.yaxis.set_major_formatter(mticker.PercentFormatter(xmax=1)) - format_polar_plot(ax, yticklabels=True) + format_polar_plot(ax, yticklabels=True) return ax @@ -1987,6 +2005,7 @@ def plot_windhistogram( show_values: bool = True, vmin: float = None, vmax: float = None, + style_context:str = "python_toolkit.bhom" ) -> plt.Axes: """Plot a 2D-histogram for a collection of wind speeds and directions. @@ -2009,60 +2028,63 @@ def plot_windhistogram( The minimum value for the colormap. Defaults to None. vmax (float, optional): The maximum value for the colormap. Defaults to None. + style_context (string, optional): + The matplotlib style to use. Defaults to python_toolkit.bhom Returns: plt.Axes: A matplotlib Axes object. """ + + with plt.style.context(style_context): + if ax is None: + ax = plt.gca() + + hist = self.histogram( + directions=directions, + other_data=other_data, + other_bins=other_bins, + density=density, + ) - if ax is None: - ax = plt.gca() + vmin = hist.values.min() if vmin is None else vmin + vmax = hist.values.max() if vmax is None else vmax + cmap = plt.get_cmap(cmap) + norm = Normalize(vmin=vmin, vmax=vmax, clip=True) + mapper = ScalarMappable(norm=norm, cmap=cmap) - hist = self.histogram( - directions=directions, - other_data=other_data, - other_bins=other_bins, - density=density, - ) + _xticks = np.roll(hist.index, 1) + _values = np.roll(hist.values, 1, axis=0).T - vmin = hist.values.min() if vmin is None else vmin - vmax = hist.values.max() if vmax is None else vmax - cmap = plt.get_cmap(cmap) - norm = Normalize(vmin=vmin, vmax=vmax, clip=True) - mapper = ScalarMappable(norm=norm, cmap=cmap) + pc = ax.pcolor(_values, cmap=cmap, vmin=vmin, vmax=vmax) + ax.set_xticks(np.arange(0.5, len(hist.index), 1), labels=_xticks, rotation=90) + ax.set_xlabel(hist.index.name) + ax.set_yticks(np.arange(0.5, len(hist.columns), 1), labels=hist.columns) + ax.set_ylabel(hist.columns.name) - _xticks = np.roll(hist.index, 1) - _values = np.roll(hist.values, 1, axis=0).T + cb = plt.colorbar(pc, pad=0.01, label="Density" if density else "Count") + if density: + cb.ax.yaxis.set_major_formatter(mticker.PercentFormatter(1, decimals=1)) + cb.outline.set_visible(False) - pc = ax.pcolor(_values, cmap=cmap, vmin=vmin, vmax=vmax) - ax.set_xticks(np.arange(0.5, len(hist.index), 1), labels=_xticks, rotation=90) - ax.set_xlabel(hist.index.name) - ax.set_yticks(np.arange(0.5, len(hist.columns), 1), labels=hist.columns) - ax.set_ylabel(hist.columns.name) + ax.set_title(textwrap.fill(f"{self.source}", 75)) - cb = plt.colorbar(pc, pad=0.01, label="Density" if density else "Count") - if density: - cb.ax.yaxis.set_major_formatter(mticker.PercentFormatter(1, decimals=1)) - cb.outline.set_visible(False) - - ax.set_title(textwrap.fill(f"{self.source}", 75)) - - if show_values: - for _xx, row in enumerate(_values): - for _yy, col in enumerate(row): - if (col * 100).round(1) == 0: - continue - cell_color = mapper.to_rgba(col) - text_color = contrasting_color(cell_color) - ax.text( - _yy + 0.5, - _xx + 0.5, - f"{col:0.2%}" if density else col, - color=text_color, - ha="center", - va="center", - fontsize="xx-small", - ) + if show_values: + for _xx, row in enumerate(_values): + for _yy, col in enumerate(row): + if (col * 100).round(1) == 0: + continue + cell_color = mapper.to_rgba(col) + text_color = contrasting_color(cell_color) + ax.text( + _yy + 0.5, + _xx + 0.5, + f"{col:0.2%}" if density else col, + color=text_color, + ha="center", + va="center", + fontsize="xx-small", + ) return ax