From dd663ae38af3884c6bd5872846b7b76fdddb06d1 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Fri, 31 May 2024 17:30:21 -0700 Subject: [PATCH] fix(visualize): Update MonthlyChart for new stacked line visual --- .../json/LB_Monthly_Chart.json | 4 +-- ladybug_grasshopper/src/LB Monthly Chart.py | 33 ++++++++++++++---- .../user_objects/LB Monthly Chart.ghuser | Bin 8592 -> 8788 bytes 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/ladybug_grasshopper/json/LB_Monthly_Chart.json b/ladybug_grasshopper/json/LB_Monthly_Chart.json index 378a393..60bb17d 100644 --- a/ladybug_grasshopper/json/LB_Monthly_Chart.json +++ b/ladybug_grasshopper/json/LB_Monthly_Chart.json @@ -1,5 +1,5 @@ { - "version": "1.8.0", + "version": "1.8.1", "nickname": "MonthlyChart", "outputs": [ [ @@ -141,7 +141,7 @@ } ], "subcategory": "2 :: Visualize Data", - "code": "\ntry:\n from ladybug_geometry.geometry2d.pointvector import Point2D\n from ladybug_geometry.geometry3d.pointvector import Vector3D, Point3D\n from ladybug_geometry.geometry3d.plane import Plane\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug_geometry:\\n\\t{}'.format(e))\n\ntry:\n from ladybug.monthlychart import MonthlyChart\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug:\\n\\t{}'.format(e))\n\ntry:\n from ladybug_{{cad}}.config import conversion_to_meters, tolerance\n from ladybug_{{cad}}.color import color_to_color\n from ladybug_{{cad}}.togeometry import to_point2d\n from ladybug_{{cad}}.fromgeometry import from_mesh2d, from_mesh2d_to_outline, \\\n from_polyline2d, from_linesegment2d\n from ladybug_{{cad}}.text import text_objects\n from ladybug_{{cad}}.colorize import ColoredPolyline\n from ladybug_{{cad}}.fromobjects import legend_objects\n from ladybug_{{cad}}.{{plugin}} import all_required_inputs, objectify_output, \\\n schedule_solution\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug_{{cad}}:\\n\\t{}'.format(e))\n\n\nif all_required_inputs(ghenv.Component) and None not in _data:\n # set default values for the chart dimensions\n z_val = _base_pt_.Z if _base_pt_ is not None else 0\n z_val_tol = z_val + tolerance\n _base_pt_ = to_point2d(_base_pt_) if _base_pt_ is not None else Point2D()\n _x_dim_ = _x_dim_ if _x_dim_ is not None else 10.0 / conversion_to_meters()\n _y_dim_ = _y_dim_ if _y_dim_ is not None else 40.0 / conversion_to_meters()\n stack_ = stack_ if stack_ is not None else False\n percentile_ = percentile_ if percentile_ is not None else 34.0\n lpar = legend_par_[0] if len(legend_par_) != 0 else None\n\n # create the monthly chart object and get the main pieces of geometry\n month_chart = MonthlyChart(_data, lpar, _base_pt_, _x_dim_, _y_dim_,\n stack_, percentile_)\n if len(legend_par_) > 1:\n if legend_par_[1].min is not None:\n month_chart.set_minimum_by_index(legend_par_[1].min, 1)\n if legend_par_[1].max is not None:\n month_chart.set_maximum_by_index(legend_par_[1].max, 1)\n\n # get the main pieces of geometry\n data_lines = []\n d_meshes = month_chart.data_meshes\n if d_meshes is not None:\n data_mesh = [from_mesh2d(msh, z_val_tol) for msh in d_meshes]\n if month_chart.time_interval == 'Monthly':\n data_lines += [l for msh in d_meshes for l in\n from_mesh2d_to_outline(msh, z_val_tol)]\n d_lines = month_chart.data_polylines\n if d_lines is not None:\n data_lines += [from_polyline2d(lin, z_val_tol) for lin in d_lines]\n borders = [from_polyline2d(month_chart.chart_border, z_val)] + \\\n [from_linesegment2d(line, z_val) for line in month_chart.y_axis_lines] + \\\n [from_linesegment2d(line, z_val_tol) for line in month_chart.month_lines]\n leg = month_chart.legend\n if z_val != 0 and leg.legend_parameters.is_base_plane_default:\n nl_par = leg.legend_parameters.duplicate()\n m_vec = Vector3D(0, 0, z_val)\n nl_par.base_plane = nl_par.base_plane.move(m_vec)\n leg._legend_par = nl_par\n legend = legend_objects(leg)\n\n # process all of the text-related outputs\n title_txt = month_chart.title_text if global_title_ is None else global_title_\n txt_hgt = month_chart.legend_parameters.text_height\n font = month_chart.legend_parameters.font\n ttl_tp = month_chart.lower_title_location\n if z_val != 0:\n ttl_tp = Plane(n=ttl_tp.n, o=Point3D(ttl_tp.o.x, ttl_tp.o.y, z_val), x=ttl_tp.x)\n title = text_objects(title_txt, ttl_tp, txt_hgt, font)\n\n # process the first y axis\n y1_txt = month_chart.y_axis_title_text1 if len(y_axis_title_) == 0 else y_axis_title_[0]\n y1_tp = month_chart.y_axis_title_location1\n if z_val != 0:\n y1_tp = Plane(n=y1_tp.n, o=Point3D(y1_tp.o.x, y1_tp.o.y, z_val), x=y1_tp.x)\n y_title = text_objects(y1_txt, y1_tp, txt_hgt, font)\n if time_marks_:\n txt_h = _x_dim_ / 20 if _x_dim_ / 20 < txt_hgt * 0.75 else txt_hgt * 0.75\n label1 = [text_objects(txt, Plane(o=Point3D(pt.x, pt.y, z_val)), txt_h, font, 1, 0)\n for txt, pt in zip(month_chart.time_labels, month_chart.time_label_points)]\n borders.extend([from_linesegment2d(line, z_val_tol) for line in month_chart.time_ticks])\n else:\n label1 = [text_objects(txt, Plane(o=Point3D(pt.x, pt.y, z_val)), txt_hgt, font, 1, 0)\n for txt, pt in zip(month_chart.month_labels, month_chart.month_label_points)]\n label2 = [text_objects(txt, Plane(o=Point3D(pt.x, pt.y, z_val)), txt_hgt, font, 2, 3)\n for txt, pt in zip(month_chart.y_axis_labels1, month_chart.y_axis_label_points1)]\n labels = label1 + label2\n\n # process the second y axis if it exists\n if month_chart.y_axis_title_text2 is not None:\n y2_txt = month_chart.y_axis_title_text2 if len(y_axis_title_) <= 1 else y_axis_title_[1]\n y2_tp = month_chart.y_axis_title_location2\n if z_val != 0:\n y2_tp = Plane(n=y2_tp.n, o=Point3D(y2_tp.o.x, y2_tp.o.y, z_val), x=y2_tp.x)\n y_title2 = text_objects(y2_txt, y2_tp, txt_hgt, font)\n y_title = [y_title, y_title2]\n label3 = [text_objects(txt, Plane(o=Point3D(pt.x, pt.y, z_val)), txt_hgt, font, 0, 3)\n for txt, pt in zip(month_chart.y_axis_labels2, month_chart.y_axis_label_points2)]\n labels = labels + label3\n\n # if there are colored lines, then process them to be output from the component\n if month_chart.time_interval == 'MonthlyPerHour':\n cols = [color_to_color(col) for col in month_chart.colors]\n col_lines, month_count = [], len(data_lines) / len(_data)\n for i, pline in enumerate(data_lines):\n col_line = ColoredPolyline(pline)\n col_line.color = cols[int(i / month_count)]\n col_line.thickness = 3\n col_lines.append(col_line)\n # CWM: I don't know why we have to re-schedule the solution but this is the\n # only way I found to get the colored polylines to appear (redraw did not work).\n schedule_solution(ghenv.Component, 2)\n\n # output arguments for the visualization set\n vis_set = [month_chart, z_val, time_marks_, global_title_, y_axis_title_]\n vis_set = objectify_output('VisualizationSet Aruments [MonthlyChart]', vis_set)\n", + "code": "\ntry:\n from ladybug_geometry.geometry2d.pointvector import Point2D\n from ladybug_geometry.geometry3d.pointvector import Vector3D, Point3D\n from ladybug_geometry.geometry3d.plane import Plane\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug_geometry:\\n\\t{}'.format(e))\n\ntry:\n from ladybug.monthlychart import MonthlyChart\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug:\\n\\t{}'.format(e))\n\ntry:\n from ladybug_{{cad}}.config import conversion_to_meters, tolerance\n from ladybug_{{cad}}.color import color_to_color\n from ladybug_{{cad}}.togeometry import to_point2d\n from ladybug_{{cad}}.fromgeometry import from_mesh2d, from_mesh2d_to_outline, \\\n from_polyline2d, from_linesegment2d\n from ladybug_{{cad}}.text import text_objects\n from ladybug_{{cad}}.colorize import ColoredPolyline\n from ladybug_{{cad}}.fromobjects import legend_objects\n from ladybug_{{cad}}.{{plugin}} import all_required_inputs, objectify_output, \\\n schedule_solution\nexcept ImportError as e:\n raise ImportError('\\nFailed to import ladybug_{{cad}}:\\n\\t{}'.format(e))\n\n\nif all_required_inputs(ghenv.Component) and None not in _data:\n # set default values for the chart dimensions\n z_val = _base_pt_.Z if _base_pt_ is not None else 0\n z_val_tol = z_val + tolerance\n _base_pt_ = to_point2d(_base_pt_) if _base_pt_ is not None else Point2D()\n _x_dim_ = _x_dim_ if _x_dim_ is not None else 10.0 / conversion_to_meters()\n _y_dim_ = _y_dim_ if _y_dim_ is not None else 40.0 / conversion_to_meters()\n stack_ = stack_ if stack_ is not None else False\n percentile_ = percentile_ if percentile_ is not None else 34.0\n lpar = legend_par_[0] if len(legend_par_) != 0 else None\n\n # create the monthly chart object and get the main pieces of geometry\n month_chart = MonthlyChart(_data, lpar, _base_pt_, _x_dim_, _y_dim_,\n stack_, percentile_)\n if len(legend_par_) > 1:\n if legend_par_[1].min is not None:\n month_chart.set_minimum_by_index(legend_par_[1].min, 1)\n if legend_par_[1].max is not None:\n month_chart.set_maximum_by_index(legend_par_[1].max, 1)\n\n # get the main pieces of geometry\n data_lines = []\n d_meshes = month_chart.data_meshes\n if d_meshes is not None:\n data_mesh = [from_mesh2d(msh, z_val_tol) for msh in d_meshes]\n if month_chart.time_interval == 'Monthly':\n data_lines += [l for msh in d_meshes for l in\n from_mesh2d_to_outline(msh, z_val_tol)]\n d_lines = month_chart.data_polylines\n if d_lines is not None:\n data_lines += [from_polyline2d(lin, z_val_tol) for lin in d_lines]\n borders = [from_polyline2d(month_chart.chart_border, z_val)] + \\\n [from_linesegment2d(line, z_val) for line in month_chart.y_axis_lines] + \\\n [from_linesegment2d(line, z_val_tol) for line in month_chart.month_lines]\n leg = month_chart.legend\n if z_val != 0 and leg.legend_parameters.is_base_plane_default:\n nl_par = leg.legend_parameters.duplicate()\n m_vec = Vector3D(0, 0, z_val)\n nl_par.base_plane = nl_par.base_plane.move(m_vec)\n leg._legend_par = nl_par\n legend = legend_objects(leg)\n\n # process all of the text-related outputs\n title_txt = month_chart.title_text if global_title_ is None else global_title_\n txt_hgt = month_chart.legend_parameters.text_height\n font = month_chart.legend_parameters.font\n ttl_tp = month_chart.lower_title_location\n if z_val != 0:\n ttl_tp = Plane(n=ttl_tp.n, o=Point3D(ttl_tp.o.x, ttl_tp.o.y, z_val), x=ttl_tp.x)\n title = text_objects(title_txt, ttl_tp, txt_hgt, font)\n\n # process the first y axis\n y1_txt = month_chart.y_axis_title_text1 if len(y_axis_title_) == 0 else y_axis_title_[0]\n y1_tp = month_chart.y_axis_title_location1\n if z_val != 0:\n y1_tp = Plane(n=y1_tp.n, o=Point3D(y1_tp.o.x, y1_tp.o.y, z_val), x=y1_tp.x)\n y_title = text_objects(y1_txt, y1_tp, txt_hgt, font)\n if time_marks_:\n txt_h = _x_dim_ / 20 if _x_dim_ / 20 < txt_hgt * 0.75 else txt_hgt * 0.75\n label1 = [text_objects(txt, Plane(o=Point3D(pt.x, pt.y, z_val)), txt_h, font, 1, 0)\n for txt, pt in zip(month_chart.time_labels, month_chart.time_label_points)]\n borders.extend([from_linesegment2d(line, z_val_tol) for line in month_chart.time_ticks])\n else:\n label1 = [text_objects(txt, Plane(o=Point3D(pt.x, pt.y, z_val)), txt_hgt, font, 1, 0)\n for txt, pt in zip(month_chart.month_labels, month_chart.month_label_points)]\n label2 = [text_objects(txt, Plane(o=Point3D(pt.x, pt.y, z_val)), txt_hgt, font, 2, 3)\n for txt, pt in zip(month_chart.y_axis_labels1, month_chart.y_axis_label_points1)]\n labels = label1 + label2\n\n # process the second y axis if it exists\n if month_chart.y_axis_title_text2 is not None:\n y2_txt = month_chart.y_axis_title_text2 if len(y_axis_title_) <= 1 else y_axis_title_[1]\n y2_tp = month_chart.y_axis_title_location2\n if z_val != 0:\n y2_tp = Plane(n=y2_tp.n, o=Point3D(y2_tp.o.x, y2_tp.o.y, z_val), x=y2_tp.x)\n y_title2 = text_objects(y2_txt, y2_tp, txt_hgt, font)\n y_title = [y_title, y_title2]\n label3 = [text_objects(txt, Plane(o=Point3D(pt.x, pt.y, z_val)), txt_hgt, font, 0, 3)\n for txt, pt in zip(month_chart.y_axis_labels2, month_chart.y_axis_label_points2)]\n labels = labels + label3\n\n # if there are colored lines, then process them to be output from the component\n if month_chart.time_interval == 'MonthlyPerHour':\n cols = [color_to_color(col) for col in month_chart.colors]\n if stack_:\n mc = int(len(_data[0]) / 24)\n chunk_lines = [data_lines[i:i + mc] for i in range(0, len(data_lines), mc)]\n col_lines, col_count, wait = [], 0, False\n for chunk in chunk_lines:\n for pline in chunk:\n col_line = ColoredPolyline(pline)\n col_line.color = cols[col_count]\n col_line.thickness = 3\n col_lines.append(col_line)\n data = _data[col_count]\n if all(v >= 0 for v in data.values) or all(v <= 0 for v in data.values):\n col_count += 1\n elif wait:\n col_count += 1\n wait = False\n else:\n wait = True\n else:\n col_lines, month_count = [], len(data_lines) / len(_data)\n for i, pline in enumerate(data_lines):\n col_line = ColoredPolyline(pline)\n col_line.color = cols[int(i / month_count)]\n col_line.thickness = 3\n col_lines.append(col_line)\n # CWM: I don't know why we have to re-schedule the solution but this is the\n # only way I found to get the colored polylines to appear (redraw did not work).\n schedule_solution(ghenv.Component, 2)\n\n # output arguments for the visualization set\n vis_set = [month_chart, z_val, time_marks_, global_title_, y_axis_title_]\n vis_set = objectify_output('VisualizationSet Aruments [MonthlyChart]', vis_set)\n", "category": "Ladybug", "name": "LB Monthly Chart", "description": "Create a chart in the Rhino scene with data organized by month.\n_\nData will display as a bar chart if the input data is monthly or daily. If the\ndata is hourly or sub-hourly, it will be plotted with lines and/or a colored\nmesh that shows the range of the data within specific percentiles.\n-" diff --git a/ladybug_grasshopper/src/LB Monthly Chart.py b/ladybug_grasshopper/src/LB Monthly Chart.py index 7fe590e..3d50816 100644 --- a/ladybug_grasshopper/src/LB Monthly Chart.py +++ b/ladybug_grasshopper/src/LB Monthly Chart.py @@ -77,7 +77,7 @@ ghenv.Component.Name = 'LB Monthly Chart' ghenv.Component.NickName = 'MonthlyChart' -ghenv.Component.Message = '1.8.0' +ghenv.Component.Message = '1.8.1' ghenv.Component.Category = 'Ladybug' ghenv.Component.SubCategory = '2 :: Visualize Data' ghenv.Component.AdditionalHelpFromDocStrings = '1' @@ -193,12 +193,31 @@ # if there are colored lines, then process them to be output from the component if month_chart.time_interval == 'MonthlyPerHour': cols = [color_to_color(col) for col in month_chart.colors] - col_lines, month_count = [], len(data_lines) / len(_data) - for i, pline in enumerate(data_lines): - col_line = ColoredPolyline(pline) - col_line.color = cols[int(i / month_count)] - col_line.thickness = 3 - col_lines.append(col_line) + if stack_: + mc = int(len(_data[0]) / 24) + chunk_lines = [data_lines[i:i + mc] for i in range(0, len(data_lines), mc)] + col_lines, col_count, wait = [], 0, False + for chunk in chunk_lines: + for pline in chunk: + col_line = ColoredPolyline(pline) + col_line.color = cols[col_count] + col_line.thickness = 3 + col_lines.append(col_line) + data = _data[col_count] + if all(v >= 0 for v in data.values) or all(v <= 0 for v in data.values): + col_count += 1 + elif wait: + col_count += 1 + wait = False + else: + wait = True + else: + col_lines, month_count = [], len(data_lines) / len(_data) + for i, pline in enumerate(data_lines): + col_line = ColoredPolyline(pline) + col_line.color = cols[int(i / month_count)] + col_line.thickness = 3 + col_lines.append(col_line) # CWM: I don't know why we have to re-schedule the solution but this is the # only way I found to get the colored polylines to appear (redraw did not work). schedule_solution(ghenv.Component, 2) diff --git a/ladybug_grasshopper/user_objects/LB Monthly Chart.ghuser b/ladybug_grasshopper/user_objects/LB Monthly Chart.ghuser index 6f9c249e70c49f292a00fbe0a2733a42905e1c4a..33585b078b2cc2221788a8e6bb541f85e89b38d5 100644 GIT binary patch literal 8788 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 K81T=1kN*Qk=uZ*= literal 8592 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