From c487a2e613ef19a6b87a928aadbf93ca0c03926d Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Thu, 5 Oct 2023 11:03:21 -0700 Subject: [PATCH] fix(extension): Add method to extend the AdaptiveChart --- ladybug_display/_extend_ladybug.py | 13 ++ ladybug_display/extension/adaptivechart.py | 193 +++++++++++++++++++++ ladybug_display/extension/psychchart.py | 4 +- 3 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 ladybug_display/extension/adaptivechart.py diff --git a/ladybug_display/_extend_ladybug.py b/ladybug_display/_extend_ladybug.py index 2a71710..6f2c740 100644 --- a/ladybug_display/_extend_ladybug.py +++ b/ladybug_display/_extend_ladybug.py @@ -50,3 +50,16 @@ RadiationStudy.to_vis_set = radiation_study_to_vis_set except ImportError: pass # ladybug-radiance is not installed + +# try to extend ladybug-comfort +try: + # import the ladybug-comfort modules + from ladybug_comfort.chart.adaptive import AdaptiveChart + + # import the extension functions + from .extension.adaptivechart import adaptive_chart_to_vis_set + + # inject the methods onto the classes + AdaptiveChart.to_vis_set = adaptive_chart_to_vis_set +except ImportError: + pass # ladybug-radiance is not installed diff --git a/ladybug_display/extension/adaptivechart.py b/ladybug_display/extension/adaptivechart.py new file mode 100644 index 0000000..c71fe01 --- /dev/null +++ b/ladybug_display/extension/adaptivechart.py @@ -0,0 +1,193 @@ +"""Method to draw an AdaptiveChart as a VisualizationSet.""" +from ladybug_geometry.geometry2d import Polyline2D +from ladybug_geometry.geometry3d import Vector3D, Point3D, Plane, LineSegment3D, \ + Polyline3D, Mesh3D +from ladybug.datatype.time import Time +from ladybug.legend import LegendParameters + +from ladybug_display.geometry3d import DisplayLineSegment3D, DisplayPolyline3D, \ + DisplayText3D +from ladybug_display.visualization import VisualizationSet, AnalysisGeometry, \ + VisualizationData, ContextGeometry + + +def adaptive_chart_to_vis_set( + adaptive_chart, data=None, legend_parameters=None, z=0): + """Get a Ladybug AdaptiveChart represented as a VisualizationSet. + + Args: + adaptive_chart: A Ladybug AdaptiveChart object. + data: An optional list of data collection objects, which are aligned with + the prevailing and operative temperature values of the chart. and will + generate additional colored AnalysisGeometries on the chart. + legend_parameters: An optional LegendParameter object or list of LegendParameter + objects to customize the display of the data on the adaptive + chart. Note that this relates only to the data supplied as input + for this method and, to customize the display of the time/frequency + mesh, the AdaptiveChart's native legend_parameters should be + edited. If a list is used here, this should align with the input data + (one legend parameter per data collection). + z: A number for the Z-coordinate to be used in translation. (Default: 0). + + Returns: + A VisualizationSet with the adaptive chart represented several + ContextGeometries and an AnalysisGeometry. This includes these objects + in the following order. + + - Title -- A ContextGeometry for the title and border around the + adaptive chart. + + - Prevailing_Axis -- A ContextGeometry with lines and text for the + Prevailing Outdoor Temperature (X) axis of the adaptive chart. + + - Operative_Axis -- A ContextGeometry with lines and text for the + Indoor Operative Temperature (Y) axis of the adaptive chart. + + - Comfort_Polygon -- A ContextGeometry with lines for the comfort polygon + and neutral temperature of the adaptive chart. + + - Analysis_Data -- An AnalysisGeometry for the data on the adaptive + chart. This will include multiple data sets if the data input + is provided. + """ + # establish the VisualizationSet object + vis_set = VisualizationSet('Adaptive_Chart', ()) + vis_set.display_name = 'Adaptive Chart' + + # get values used throughout the translation + txt_hgt = adaptive_chart.legend_parameters.text_height + font = adaptive_chart.legend_parameters.font + bp = Plane(o=Point3D(0, 0, z)) + + # add the title and border + meta_i = adaptive_chart.operative_temperature.header.metadata.items() + title_items = ['Adaptive Chart', 'Time [hr]'] + \ + ['{}: {}'.format(k, v) for k, v in meta_i] + ttl_pl = adaptive_chart.container.lower_title_location.move( + Vector3D(0, -txt_hgt * 3)) + if z != 0: + ttl_pl = Plane(n=ttl_pl.n, o=Point3D(ttl_pl.o.x, ttl_pl.o.y, z), x=ttl_pl.x) + ttl_txt = DisplayText3D( + '\n'.join(title_items), ttl_pl, txt_hgt * 1.5, None, font, 'Left', 'Top') + border_geo = Polyline3D.from_polyline2d( + Polyline2D.from_polygon(adaptive_chart.chart_border), bp) + title_objs = [ttl_txt, DisplayPolyline3D(border_geo, line_width=2)] + title = ContextGeometry('Title', title_objs) + vis_set.add_geometry(title) + + # add the prevailing temperature axis + tm_pl = _plane_from_point(adaptive_chart.x_axis_location, z) + temp_txt = DisplayText3D( + adaptive_chart.x_axis_text, tm_pl, txt_hgt * 1.5, None, font, 'Center', 'Top') + temp_geo = [temp_txt] + for tl in adaptive_chart.prevailing_lines: + tl_geo = LineSegment3D.from_line_segment2d(tl, z) + temp_geo.append(DisplayLineSegment3D(tl_geo, line_type='Dotted')) + tl_pts = adaptive_chart.prevailing_label_points + for txt, pt in zip(adaptive_chart.prevailing_labels, tl_pts): + t_pln = Plane(o=Point3D(pt.x, pt.y, z)) + txt_obj = DisplayText3D(txt, t_pln, txt_hgt, None, font, 'Center', 'Top') + temp_geo.append(txt_obj) + temp_axis = ContextGeometry('Prevailing_Axis', temp_geo) + temp_axis.display_name = 'Prevailing Axis' + vis_set.add_geometry(temp_axis) + + # add the operative temperature axis + op_pl = _plane_from_point(adaptive_chart.y_axis_location, z, Vector3D(0, 1)) + op_txt = DisplayText3D( + adaptive_chart.y_axis_text, op_pl, txt_hgt * 1.5, None, font, 'Center', 'Top') + op_geo = [op_txt] + for hl in adaptive_chart.operative_lines: + hl_geo = LineSegment3D.from_line_segment2d(hl, z) + op_geo.append(DisplayLineSegment3D(hl_geo, line_type='Dotted')) + op_pts = adaptive_chart.operative_label_points + for txt, pt in zip(adaptive_chart.operative_labels, op_pts): + t_pln = Plane(o=Point3D(pt.x, pt.y, z)) + txt_obj = DisplayText3D(txt, t_pln, txt_hgt, None, font, 'Left', 'Middle') + op_geo.append(txt_obj) + op_axis = ContextGeometry('Operative_Axis', op_geo) + op_axis.display_name = 'Operative Axis' + vis_set.add_geometry(op_axis) + + # add the comfort polygon + poly_geo = [] + neutral_geo = Polyline3D.from_polyline2d( + Polyline2D.from_polygon(adaptive_chart.comfort_polygon), bp) + poly_geo.append(DisplayPolyline3D(neutral_geo, line_width=3)) + neutral_geo = Polyline3D.from_polyline2d(adaptive_chart.neutral_polyline, bp) + poly_geo.append(DisplayPolyline3D(neutral_geo, line_width=1)) + comf_poly = ContextGeometry('Comfort_Polygon', poly_geo) + comf_poly.display_name = 'Comfort Polygon' + vis_set.add_geometry(comf_poly) + + # add the analysis geometry + # ensure 3D legend defaults are overridden to make the data readable + l_par = adaptive_chart.legend.legend_parameters.duplicate() + l_par.base_plane = l_par.base_plane + l_par.segment_height = l_par.segment_height + l_par.segment_width = l_par.segment_width + # gather all of the visualization data sets + vis_data = [VisualizationData(adaptive_chart.hour_values, l_par, Time(), 'hr')] + if data is not None and len(data) != 0: + if legend_parameters is None: + l_pars = [LegendParameters()] * len(data) + elif isinstance(legend_parameters, LegendParameters): + l_pars = [legend_parameters] * len(data) + else: # assume it's a list that aligns with the data + l_pars = legend_parameters + for dat, lp in zip(data, l_pars): + # process the legend parameters + lp = lp.duplicate() + if lp.is_base_plane_default: + lp.base_plane = l_par.base_plane + if lp.is_segment_height_default: + lp.segment_height = l_par.segment_height + if lp.is_segment_width_default: + lp.segment_width = l_par.segment_width + # check to be sure the data collection aligns + d_vals = dat.values + _tp_values = adaptive_chart.prevailing_outdoor_temperature.values + _to_values = adaptive_chart.operative_temperature.values + # create a matrix with a tally of the hours for all the data + base_mtx = [[[] for val in adaptive_chart._tp_category] + for rh in adaptive_chart._to_category] + for tp, to, val in zip(_tp_values, _to_values, d_vals): + if tp < adaptive_chart._min_prevailing or \ + tp > adaptive_chart._max_prevailing: + continue # temperature value does not currently fit on the chart + if to < adaptive_chart._min_operative or \ + to > adaptive_chart._max_operative: + continue # temperature value does not currently fit on the chart + for y, to_cat in enumerate(adaptive_chart._to_category): + if to < to_cat: + break + for x, tp_cat in enumerate(adaptive_chart._tp_category): + if tp < tp_cat: + break + base_mtx[y][x].append(val) + # compute average values + avg_values = [sum(val_list) / len(val_list) for rh_l in base_mtx + for val_list in rh_l if len(val_list) != 0] + hd = dat.header + vd = VisualizationData(avg_values, lp, hd.data_type, hd.unit) + vis_data.append(vd) + # create the analysis geometry + mesh_3d = Mesh3D.from_mesh2d(adaptive_chart.colored_mesh, bp) + mesh_geo = AnalysisGeometry( + 'Analysis_Data', [mesh_3d], vis_data, active_data=len(vis_data) - 1) + mesh_geo.display_name = 'Analysis Data' + mesh_geo.display_mode = 'Surface' + vis_set.add_geometry(mesh_geo) + + return vis_set + + +def _plane_from_point(point_2d, z, align_vec=Vector3D(1, 0, 0)): + """Get a Plane from a Point2D. + + Args: + point_2d: A Point2D to serve as the origin of the plane. + z: The Z value for the plane origin. + align_vec: A Vector3D to serve as the X-Axis of the plane. + """ + return Plane(o=Point3D(point_2d.x, point_2d.y, z), x=align_vec) diff --git a/ladybug_display/extension/psychchart.py b/ladybug_display/extension/psychchart.py index e864694..8af3f0f 100644 --- a/ladybug_display/extension/psychchart.py +++ b/ladybug_display/extension/psychchart.py @@ -55,7 +55,7 @@ def psychrometric_chart_to_vis_set( - Wet_Bulb_Lines -- A ContextGeometry with lines and text for the wet bulb temperature of the psychrometric chart. This layer will not be - included if plot_wet_bulb is FAlse. + included if plot_wet_bulb is False. - Analysis_Data -- An AnalysisGeometry for the data on the psychrometric chart. This will include multiple data sets if the data input @@ -189,7 +189,7 @@ def psychrometric_chart_to_vis_set( d_vals = dat.values assert len(d_vals) == psych_chart._calc_length, \ 'Number of data collection values ' \ - 'must match those of the psychometric chart temperature and humidity.' + 'must match those of the psychrometric chart temperature and humidity.' # create a matrix with a tally of the hours for all the data base_mtx = [[[] for val in psych_chart._t_category] for rh in psych_chart._rh_category]