Source code for ladybug.legend

# coding=utf-8
from __future__ import division
import re
try:
    from collections.abc import Iterable  # python < 3.7
except ImportError:
    from collections import Iterable  # python >= 3.8
import sys
if (sys.version_info > (3, 0)):  # python 3
    xrange = range

from ladybug_geometry.geometry3d.pointvector import Point3D, Vector3D
from ladybug_geometry.geometry3d.plane import Plane
from ladybug_geometry.geometry3d.mesh import Mesh3D
from ladybug_geometry.geometry2d.pointvector import Point2D
from ladybug_geometry.geometry2d.mesh import Mesh2D

from .color import Color, Colorset, ColorRange


[docs]class Legend(object): """Ladybug legend used to get legend geometry, legend text, generate colors, etc. Args: values: A List or Tuple of numerical values that will be used to generate the legend and colors. legend_parameters: An Optional LegendParameter object to override default parameters of the legend. Properties: * legend_parameters * values * value_colors * title * title_location * title_location_scene_2d * segment_text * segment_text_location * segment_text_location_scene_2d * segment_mesh * segment_mesh_scene_2d * color_range * segment_numbers * segment_colors * segment_length * is_min_default * is_max_default Usage: .. code-block:: python 1. data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] legend = Legend(data, LegendParameters(segment_count=6)) print(legend.segment_text) print(legend.segment_mesh) print(legend.segment_colors) >> ['0.00', '1.80', '3.60', '5.40', '7.20', '9.00'] >> Mesh3D (6 faces) (14 vertices) >> ((R:75, G:107, B:169), (R:159, G:189, B:238), (R:224, G:229, B:145), (R:247, G:200, B:53), (R:234, G:113, B:0), (R:234, G:38, B:0)) 2. data = [100, 300, 500, 1000, 2000, 3000] leg_colors = [Color(0, 0, 255), Color(0, 255, 0), Color(255, 0, 0)] legend_par = LegendParametersCategorized([300, 2000], leg_colors) legend_par.category_names = ['low', 'desired', 'too much'] legend_par.continuous_colors = False # get data in only 3 colors legend = Legend(data, legend_par) print(legend.segment_text) print(legend.segment_mesh) print(legend.segment_colors) print(legend.value_colors) >> ['low', 'desired', 'too much'] >> Mesh3D (3 faces) (8 vertices) >> ((R:0, G:0, B:255), (R:0, G:255, B:0), (R:255, G:0, B:0)) >> ((R:0, G:0, B:255), (R:0, G:255, B:0), (R:0, G:255, B:0), (R:0, G:255, B:0), (R:0, G:255, B:0), (R:255, G:0, B:0)) 3. data = [-0.5, 0, 0.5] legend_par = LegendParameters(min=-1, max=1, segment_count=3) legend_par.ordinal_dictionary = {-3: 'Cold', -2: 'Cool', -1: 'Slightly Cool', 0: 'Neutral', 1: 'Slightly Warm', 2: 'Warm', 3: 'Hot'} legend = Legend(data, legend_par) print(legend.segment_text) print(legend.segment_mesh) print(legend.segment_colors) legend.legend_parameters.segment_count = 5 print(legend.segment_text) legend.legend_parameters.min = -2 legend.legend_parameters.max = 2 print(legend.segment_text) >> ['Slightly Cool', 'Neutral', 'Slightly Warm'] >> Mesh3D (3 faces) (8 vertices) >> ((R:75, G:107, B:169), (R:249, G:235, B:89), (R:234, G:38, B:0)) >> ['Slightly Cool', '', 'Neutral', '', 'Slightly Warm'] >> ['Cool', 'Slightly Cool', 'Neutral', 'Slightly Warm', 'Warm'] """ __slots__ = ('_values', '_legend_par', '_is_min_default', '_is_max_default') def __init__(self, values, legend_parameters=None): """Initialize Ladybug Legend. """ # check the inputs assert isinstance(values, Iterable) \ and not isinstance(values, (str, dict, bytes, bytearray)), \ 'values should be a list or tuple. Got {}'.format(type(values)) if not isinstance(values, tuple): values = tuple(values) assert len(values) != 0, 'There must be at least one value.' self._values = values if legend_parameters is not None: assert isinstance(legend_parameters, LegendParameters), \ 'Expected LegendParameters. Got {}.'.format(type(legend_parameters)) self._legend_par = legend_parameters.duplicate() else: self._legend_par = LegendParameters() # set default min, max and segment count (if min == max) self._is_min_default = False self._is_max_default = False if self._legend_par.min is None: self._legend_par.min = min(values) self._is_min_default = True if self._legend_par.max is None: self._legend_par.max = max(values) self._is_max_default = True if self._legend_par.min == self._legend_par.max and not \ isinstance(self._legend_par, LegendParametersCategorized) and \ self._legend_par.is_segment_count_default: self._legend_par._segment_count = 1 # set the default segment width if the legend is horizontal if not self._legend_par.vertical and self._legend_par.is_segment_width_default: max_len = len(str(int(self._legend_par.max))) self._legend_par.properties_3d.segment_width = \ self._legend_par.text_height * \ (max_len + self._legend_par.decimal_count + 2) self._legend_par.properties_3d._is_segment_width_default = True
[docs] @classmethod def from_dict(cls, data): """Create a legend from a dictionary. Args: data: A python dictionary in the following format .. code-block:: python { "values": [0, 10], "legend_parameters": None } """ legend_parameters = None if 'legend_parameters' in data and data['legend_parameters'] is not None: if data['legend_parameters']['type'] == 'LegendParametersCategorized': legend_parameters = LegendParametersCategorized.from_dict( data['legend_parameters']) else: legend_parameters = LegendParameters.from_dict(data['legend_parameters']) legend = cls(data['values'], legend_parameters) legend._is_min_default = data['is_min_default'] if 'is_min_default' in data \ else False legend._is_max_default = data['is_max_default'] if 'is_max_default' in data \ else False return legend
@property def legend_parameters(self): """The legend parameters assigned to this legend.""" return self._legend_par @property def values(self): """The data set assigned to the legend.""" return self._values @property def value_colors(self): """A List of colors associated with the assigned values.""" _color_range = self.color_range return tuple(_color_range.color(val) for val in self.values) @property def title(self): """A text string for the title of the legend.""" return self.legend_parameters.title @property def title_location(self): """A Plane for the location and orientation of the legend title.""" _base_pl = self.legend_parameters.base_plane _title_pt = self._title_point_scene_2d() return Plane(_base_pl.n, _base_pl.xy_to_xyz(_title_pt), _base_pl.x) @property def title_location_scene_2d(self): """A Point2D for the location of the title. The Point2D is derived from the 3D plane and properties of the legend parameters, making it useful for output to certain 2D interfaces. """ _base_o = self.legend_parameters.base_plane.o _title_pt = self._title_point_scene_2d() return Point2D(_title_pt.x + _base_o.x, _title_pt.y + _base_o.y) @property def segment_text(self): """A list of text strings for the segment labels of the legend.""" _l_par = self.legend_parameters if isinstance(_l_par, LegendParametersCategorized): return _l_par.category_names else: if _l_par.ordinal_dictionary is None: format_str = '%.{}f'.format(_l_par.decimal_count) seg_txt = [format_str % x for x in self.segment_numbers] if _l_par.include_larger_smaller: seg_txt[0] = '<' + seg_txt[0] seg_txt[-1] = '>' + seg_txt[-1] return seg_txt else: seg_txt = [] for x in self.segment_numbers: try: seg_txt.append(_l_par.ordinal_dictionary[x]) except KeyError: seg_txt.append('') return seg_txt @property def segment_text_location(self): """A list of Plane objects for the location of the legend segment text.""" _base_pl = self.legend_parameters.base_plane _pt_2d = self._segment_point_scene_2d() return [Plane(_base_pl.n, _base_pl.xy_to_xyz(pt), _base_pl.x) for pt in _pt_2d] @property def segment_text_location_scene_2d(self): """A list of Point2D for the location of the legend segment text. The Point2Ds are derived from the 3D plane and properties of the legend parameters, making them useful for output into certain 2D interfaces. """ _base_o = self.legend_parameters.base_plane.o _pt_2d = self._segment_point_scene_2d() return [Point2D(pt.x + _base_o.x, pt.y + _base_o.y) for pt in _pt_2d] @property def segment_mesh(self): """A Ladybug Mesh3D for the legend colors.""" _mesh_2d = self._segment_mesh_2d() return Mesh3D.from_mesh2d(_mesh_2d, self.legend_parameters.base_plane) @property def segment_mesh_scene_2d(self): """A Ladybug Mesh2D for the legend colors. The Mesh2D is derived from the 3D plane and properties of the legend parameters. """ _o = self.legend_parameters.base_plane.o return self._segment_mesh_2d(Point2D(_o.x, _o.y)) @property def color_range(self): """The color range associated with this legend.""" _l_par = self.legend_parameters if isinstance(_l_par, LegendParametersCategorized): return ColorRange(_l_par.colors, _l_par.domain, _l_par.continuous_colors) else: return ColorRange(_l_par.colors, (_l_par.min, _l_par.max)) @property def segment_numbers(self): """Get a list of numbers along a linear scale from the min to max.""" _l_par = self.legend_parameters try: _seg_stp = (_l_par.max - _l_par.min) / (_l_par.segment_count - 1) except ZeroDivisionError: _seg_stp = 0 return tuple(_l_par.min + i * _seg_stp for i in xrange(_l_par.segment_count)) @property def segment_colors(self): """A list of colors associated with the legend segments.""" if isinstance(self.legend_parameters, LegendParametersCategorized): return self.legend_parameters.colors _color_range = self.color_range return tuple(_color_range.color(val) for val in self.segment_numbers) @property def segment_length(self): """An integer for the number of segment lengths in the legend.""" _l_par = self.legend_parameters return _l_par.segment_count if not _l_par.continuous_legend else \ _l_par.segment_count - 1 @property def is_min_default(self): """Boolean noting whether the min is default. Useful when deciding whether to override the legend to properly display an ordinal dictionary. """ return self._is_min_default @property def is_max_default(self): """Boolean noting whether the max is default. Useful when deciding whether to override the legend to properly display an ordinal dictionary. """ return self._is_max_default
[docs] def title_location_2d(self, width=800, height=600): """A Point2D for the location of the title. The Point2D is derived from the 2D properties of the legend parameters. Args: width: The screen width in pixels, which is needed to interpret dimensions specified in the percent of the screen. (Default: 800). height: The screen height in pixels, which is needed to interpret dimensions specified in the percent of the screen. (Default: 600). """ or_x, or_y, sh, sw, th = self._pixel_dims_2d(width, height) offset = sh * 0.5 if self.legend_parameters.vertical else sh * 0.75 base = Point2D(or_x, or_y - offset) ln_break_count = self.legend_parameters.title.count('\n') if ln_break_count != 0: # offset the text so that it's not over the legend offset = ln_break_count * th * 1.5 return Point2D(base.x, base.y + offset) return base
[docs] def segment_text_location_2d(self, width=800, height=600): """A list of Point2D for the location of legend segment text in the 2D screen. The Point2Ds are derived from the 2D properties of the legend parameters. Args: width: The screen width in pixels, which is needed to interpret dimensions specified in the percent of the screen. (Default: 800). height: The screen height in pixels, which is needed to interpret dimensions specified in the percent of the screen. (Default: 600). """ _l_par = self.legend_parameters or_x, or_y, sh, sw, th = self._pixel_dims_2d(width, height) if _l_par.vertical: # vertical pt_x = or_x + sw + int(th * 0.25) if not _l_par.continuous_legend: return tuple( Point2D(pt_x, or_y + (i * sh) - th) for i in range(_l_par.segment_count, 0, -1)) else: return tuple( Point2D(pt_x, or_y + (i * sh) - (th / 2)) for i in range(_l_par.segment_count - 1, -1, -1)) else: # horizontal pt_y = or_y + sh + int(th * 0.75) if not _l_par.continuous_legend: return tuple( Point2D(or_x + (i * sw) + (sw * 0.5), pt_y) for i in range(_l_par.segment_count)) else: return tuple( Point2D(or_x + (i * sw), pt_y) for i in range(_l_par.segment_count))
[docs] def color_map_2d(self, width=800, height=600): """A Matrix of color values for the legend. This is useful for generating images of the colored 2D legend to be output into the plane of the screen. Args: width: The screen width in pixels, which is needed to interpret dimensions specified in the percent of the screen. (Default: 800). height: The screen height in pixels, which is needed to interpret dimensions specified in the percent of the screen. (Default: 600). """ _l_par = self.legend_parameters or_x, or_y, sh, sw, th = self._pixel_dims_2d(width, height) black = Color(0, 0, 0) if _l_par.vertical: # vertical color_mtx = [[black] * sw] if not _l_par.continuous_legend: # discrete colors for s, s_col in enumerate(reversed(self.segment_colors)): col_row = [s_col] * sw col_row[0] = black col_row[-1] = black for h in range(sh): color_mtx.append(col_row) else: # compute continuous colors _color_range = self.color_range seg_num = self.segment_numbers total_h = sh * (len(seg_num) - 1) stn, endn = seg_num[0], seg_num[-1] spn = (endn - stn) / total_h all_cols = [_color_range.color(v) for v in self._frange(stn, endn, spn)] for col in reversed(all_cols): col_row = [col] * sw col_row[0] = black col_row[-1] = black color_mtx.append(col_row) color_mtx.append([black] * sw) else: # horizontal if not _l_par.continuous_legend: # discrete colors s_cols = self.segment_colors total_len = sw * len(s_cols) color_mtx = [[black] * total_len] col_row = [] for col in s_cols: col_row.extend([col] * sw) col_row[0] = black col_row[-1] = black for h in range(sh): color_mtx.append(col_row) color_mtx.append([black] * total_len) else: # compute continuous colors _color_range = self.color_range seg_num = self.segment_numbers total_w = sw * (len(seg_num) - 1) stn, endn = seg_num[0], seg_num[-1] spn = (endn - stn) / total_w color_mtx = [[black] * total_w] all_cols = [_color_range.color(v) for v in self._frange(stn, endn, spn)] if len(all_cols) > total_w: while len(all_cols) > total_w: all_cols.pop(-1) all_cols[0] = black all_cols[-1] = black for h in range(sh): color_mtx.append(all_cols) color_mtx.append([black] * total_w) return color_mtx
[docs] def duplicate(self): """Return a copy of the current legend.""" return self.__copy__()
[docs] def to_dict(self): """Get legend as a dictionary.""" return { 'values': self.values, 'legend_parameters': self.legend_parameters.to_dict(), 'is_min_default': self.is_min_default, 'is_max_default': self.is_max_default, 'type': 'Legend' }
[docs] @staticmethod def parse_dim_2d(dim_str, screen_dim): """Parse a string for a 2D screen dimension into pixel width and height. Args: dim_str: A string for a screen dimension. (eg. 12px, 5%). screen_dim: The total screen dimension in pixels, which is needed to interpret dimensions specified in the percent of the screen. """ if dim_str.endswith('px'): return int(dim_str.replace('px', '')) else: # assume that it's a percentage dimension return int(float(dim_str.replace('%', '')) * screen_dim * 0.01)
def _pixel_dims_2d(self, width, height): """Get the pixel dimensions of the 2D legend given screen width and height.""" l_par = self.legend_parameters or_x = self.parse_dim_2d(l_par.origin_x, width) or_y = self.parse_dim_2d(l_par.origin_y, height) sh = self.parse_dim_2d(l_par.segment_height_2d, height) sw = self.parse_dim_2d(l_par.segment_width_2d, width) th = self.parse_dim_2d(l_par.text_height_2d, height) return or_x, or_y, sh, sw, th def _title_point_scene_2d(self): """Point2D for the title in the 2D space of the legend.""" _l_par = self.legend_parameters if _l_par.vertical: offset = 0.5 if self.legend_parameters.continuous_legend else 0.25 base = Point2D(0, _l_par.segment_height * (self.segment_length + offset)) else: base = Point2D(-_l_par.segment_width * self.segment_length, _l_par.segment_height * 1.25) ln_break_count = self.legend_parameters.title.count('\n') if ln_break_count != 0: # offset the text so that it's not over the legend offset = ln_break_count * self.legend_parameters.text_height * 1.5 return Point2D(base.x, base.y + offset) return base def _segment_point_scene_2d(self): """Point2D for the segment text in the 2D space of the legend.""" _l_par = self.legend_parameters if _l_par.vertical: # vertical return tuple( Point2D(_l_par.segment_width + _l_par.text_height * 0.25, i) for i in Legend._frange( 0, _l_par.segment_height * _l_par.segment_count, _l_par.segment_height)) else: # horizontal _start_val = -_l_par.segment_width * self.segment_length return tuple( Point2D(_start_val + i, -_l_par.text_height * 1.25) for i in Legend._frange( 0, _l_par.segment_width * _l_par.segment_count, _l_par.segment_width)) def _segment_mesh_2d(self, base_pt=Point2D(0, 0)): """Mesh2D for the segments in the 2D space of the legend.""" # get general properties _l_par = self.legend_parameters n_seg = self.segment_length # create the 2D mesh of the legend if _l_par.vertical: mesh2d = Mesh2D.from_grid( base_pt, 1, n_seg, _l_par.segment_width, _l_par.segment_height) else: _base_pt = Point2D(base_pt.x - _l_par.segment_width * n_seg, base_pt.y) mesh2d = Mesh2D.from_grid( _base_pt, n_seg, 1, _l_par.segment_width, _l_par.segment_height) # add colors to the mesh _seg_colors = self.segment_colors if not _l_par.continuous_legend: mesh2d.colors = _seg_colors else: if _l_par.vertical: mesh2d.colors = _seg_colors + _seg_colors else: mesh2d.colors = tuple(col for col in _seg_colors for i in (0, 1)) return mesh2d @staticmethod def _frange(start, stop, step): """Range function capable of yielding float values.""" while start < stop: yield start start += step def __copy__(self): _leg = Legend(self.values, self.legend_parameters) _leg._is_min_default = self._is_min_default _leg._is_max_default = self._is_max_default return _leg def __len__(self): """Return length of values on the object.""" return len(self._values) def __getitem__(self, key): """Return one of the values.""" return self._values[key] def __iter__(self): """Iterate through the values.""" return iter(self._values) def __key(self): return (self._legend_par, self._is_min_default, self._is_max_default) + \ tuple(self._values) def __hash__(self): return hash(self.__key()) def __eq__(self, other): return isinstance(other, Legend) and self.__key() == other.__key() def __ne__(self, value): return not self.__eq__(value)
[docs] def ToString(self): """Overwrite .NET ToString.""" return self.__repr__()
def __repr__(self): """Legend representation.""" return 'Ladybug Legend ({} values)'.format(len(self))
[docs]class LegendParameters(object): """Ladybug legend parameters used to customize legends. All properties of LegendParameters are set-able (except the is_property_default ones). Args: min: A number to set the lower boundary of the legend. If None, the minimum of the values associated with the legend will be used. max: A number to set the upper boundary of the legend. If None, the maximum of the values associated with the legend will be used. segment_count: An integer representing the number of steps between the high and low boundary of the legend. The default is set to 11 and any custom values input in here should always be greater than or equal to 2. colors: An list of color objects. Default is Ladybug's original colorset. title: Text string for Legend title. Typically, the units of the data are used here but the type of data might also be used. Default is an empty string. base_plane: A Ladybug Plane object to note the starting point from where the legend will be generated. The default is the world XY plane at origin (0, 0, 0). Properties: * min * max * segment_count * colors * continuous_legend * title * ordinal_dictionary * decimal_count * include_larger_smaller * vertical * font * user_data * properties_3d * base_plane * segment_height * segment_width * text_height * properties_2d * origin_x * origin_y * segment_height_2d * segment_width_2d * text_height_2d * is_segment_count_default * are_colors_default * is_title_default * is_base_plane_default * is_segment_height_default * is_segment_width_default * is_text_height_default * is_origin_x_default * is_origin_y_default * is_segment_height_2d_default * is_segment_width_2d_default * is_text_height_2d_default Usage: .. code-block:: python lp = LegendParameters(min=0, max=100, segment_count=6) lp.vertical = False lp.segment_width = 5 """ __slots__ = ( '_min', '_max', '_segment_count', '_colors', '_continuous_legend', '_title', '_ordinal_dictionary', '_decimal_count', '_include_larger_smaller', '_vertical', '_font', '_user_data', '_properties_3d', '_properties_2d', '_is_segment_count_default', '_are_colors_default', '_is_title_default') def __init__(self, min=None, max=None, segment_count=None, colors=None, title=None, base_plane=None): """Initialize Ladybug LegendParameters.""" # set the init arguments self._min = None self._max = None self.min = min self.max = max self.segment_count = segment_count self.colors = colors self.title = title # set the other properties to None/default self.continuous_legend = None self.ordinal_dictionary = None self.decimal_count = None self.include_larger_smaller = None self.vertical = None self.font = None self._user_data = None # set the 3D and 2D properties self.properties_3d = Legend3DParameters(base_plane) self.properties_2d = Legend2DParameters()
[docs] @classmethod def from_dict(cls, data): """Create LegendParameters from a dictionary. Args: data: A python dictionary in the following format .. code-block:: python { "type": "LegendParameters", "min": -3, "max": 3, "segment_count": 7 } """ data = data.copy() # copy to avoid mutating the input dictionary assert data['type'] == 'LegendParameters', \ 'Expected LegendParameters. Got {}.'.format(data['type']) default_dict = {'type': 'Default'} optional_keys = ( 'min', 'max', 'segment_count', 'colors', 'continuous_legend', 'title', 'ordinal_dictionary', 'decimal_count', 'include_larger_smaller', 'vertical', 'font', 'properties_3d', 'properties_2d') for key in optional_keys: if key not in data: data[key] = None elif data[key] == default_dict: data[key] = None colors = None if data['colors'] is not None: colors = [Color.from_dict(col) for col in data['colors']] leg_par = cls(data['min'], data['max'], data['segment_count'], colors, data['title']) leg_par.continuous_legend = data['continuous_legend'] if 'ordinal_dictionary' in data and data['ordinal_dictionary'] is not None: leg_par.ordinal_dictionary = \ {int(i): val for i, val in data['ordinal_dictionary'].items()} leg_par.decimal_count = data['decimal_count'] leg_par.include_larger_smaller = data['include_larger_smaller'] leg_par.vertical = data['vertical'] leg_par.font = data['font'] if data['properties_3d'] is not None: leg_par.properties_3d = Legend3DParameters.from_dict(data['properties_3d']) if data['properties_2d'] is not None: leg_par.properties_2d = Legend2DParameters.from_dict(data['properties_2d']) if 'user_data' in data and data['user_data'] is not None: leg_par.user_data = data['user_data'] return leg_par
@property def min(self): """Get or set legend minimum.""" return self._min @min.setter def min(self, minimum): if minimum is not None: assert isinstance(minimum, (float, int)), \ 'Expected number for min. Got {}.'.format(type(minimum)) if self._max is not None: assert minimum <= self._max, \ 'Input min is greater than input max. {} > {}.'.format( minimum, self._max) self._min = minimum @property def max(self): """Get or set legend maximum.""" return self._max @max.setter def max(self, maximum): if maximum is not None: assert isinstance(maximum, (float, int)), \ 'Expected number for max. Got {}.'.format(type(maximum)) if self._min is not None: assert maximum >= self._min, \ 'Input max is less than input min. {} < {}.'.format( maximum, self._min) self._max = maximum @property def segment_count(self): """Get or set the number of segments in the legend.""" return self._segment_count @segment_count.setter def segment_count(self, nos): if nos is not None: assert isinstance(nos, int), \ 'Expected integer for segment_count. Got {}.'.format(type(nos)) assert nos >= 1, 'segment_count must be greater or equal to 1.' \ ' Got {}.'.format(nos) self._segment_count = nos self._is_segment_count_default = False else: self._segment_count = 11 self._is_segment_count_default = True @property def colors(self): """Get or set the colors defining the legend.""" return self._colors @colors.setter def colors(self, cols): if cols is not None: assert isinstance(cols, Iterable) \ and not isinstance(cols, (str, dict, bytes, bytearray)), \ 'Colors should be a list or tuple. Got {}'.format(type(cols)) self._colors = self._convert_colors(cols) assert len(self._colors) > 1, \ 'There must be at least two colors to make a legend.' self._are_colors_default = False else: self._colors = Colorset.original() self._are_colors_default = True @property def continuous_legend(self): """Boolean noting whether legend is drawn as a gradient or discrete segments. If True, the legend mesh will be drawn vertex-by-vertex resulting in a continuous gradient instead of discrete segments. If False, the mesh will be generated with one face for each of the segment_count. Default: False for depicting discrete categories. """ return self._continuous_legend @continuous_legend.setter def continuous_legend(self, cont_leg): if cont_leg is not None: assert isinstance(cont_leg, bool), \ 'Expected boolean for continuous_legend. Got {}.'.format(type(cont_leg)) self._continuous_legend = cont_leg else: self._continuous_legend = False @property def title(self): """Get or set the text for the title of the legend.""" return self._title @title.setter def title(self, title): if title is not None: assert isinstance(title, str), \ 'Expected string for title. Got {}.'.format(type(title)) self._title = title self._is_title_default = False else: self._title = '' self._is_title_default = True @property def ordinal_dictionary(self): """Get or set an optional dictionary that maps values to text categories. If None, numerical values will be used for the legend segments. If not, text categories will be used and the legend will be ordinal. Note that, if the number of items in the dictionary are less than the segment_count, some segments won't receive any label. Examples for possible dictionaries include: {-1: 'Cold', 0: 'Neutral', 1: 'Hot'} {0: 'False', 1: 'True'} """ return self._ordinal_dictionary @ordinal_dictionary.setter def ordinal_dictionary(self, o_dict): if o_dict is not None: assert isinstance(o_dict, dict), \ 'Expected dictionary for ordinal_dictionary. Got {}.'.format( type(o_dict)) for key in o_dict.keys(): assert isinstance(key, int), \ 'Expected integer for ordinal_dictionary key. Got {}.'.format( type(key)) self._ordinal_dictionary = o_dict @property def decimal_count(self): """Get or set an integer for the number of decimal places in the legend text. Default is 2. Note that this input has no bearing on the resulting legend text when an ordinal_dictionary is present. """ return self._decimal_count @decimal_count.setter def decimal_count(self, n_dec): if n_dec is not None: assert isinstance(n_dec, int), \ 'Expected integer for decimal_count. Got {}.'.format(type(n_dec)) self._decimal_count = n_dec else: self._decimal_count = 2 @property def include_larger_smaller(self): """Boolean noting whether > and < should be included in legend segment text.""" return self._include_larger_smaller @include_larger_smaller.setter def include_larger_smaller(self, lgsm): self._include_larger_smaller = bool(lgsm) @property def vertical(self): """Boolean noting whether legend is vertical (True) or horizontal (False). Default: True for a vertically-oriented legend. """ return self._vertical @vertical.setter def vertical(self, vertical): if vertical is not None: assert isinstance(vertical, bool), \ 'Expected boolean for vertical. Got {}.'.format( type(vertical)) self._vertical = vertical else: self._vertical = True @property def font(self): """Get or set the font for the legend text. Examples include "Arial", "Times New Roman", "Courier". Note that this parameter may not have an effect on certain interfaces that have limited access to fonts. Default is "Arial". """ return self._font @font.setter def font(self, font): if font is not None: assert isinstance(font, str), \ 'Expected string for font. Got {}.'.format(type(font)) self._font = font else: self._font = 'Arial' @property def properties_3d(self): """Get or set a Legend3DParameters for the properties of 3D legends.""" return self._properties_3d @properties_3d.setter def properties_3d(self, value): if value is not None: assert isinstance(value, Legend3DParameters), 'Expected Legend3DParameters' \ ' for properties_3d. Got {}.'.format(type(value)) else: value = Legend3DParameters() self._properties_3d = value value._parent = self @property def base_plane(self): """Get or set a Plane for the base point and orientation of the 3D legend. """ return self.properties_3d.base_plane @base_plane.setter def base_plane(self, base_pl): self.properties_3d.base_plane = base_pl @property def segment_height(self): """Get or set the height for each of the legend segments in 3D space.""" return self.properties_3d.segment_height @segment_height.setter def segment_height(self, seg_h): self.properties_3d.segment_height = seg_h @property def segment_width(self): """Get or set the width for each of the legend segments in 3D space.""" return self.properties_3d.segment_width @segment_width.setter def segment_width(self, seg_w): self.properties_3d.segment_width = seg_w @property def text_height(self): """Get or set the height for the legend text in 3D space.""" return self.properties_3d.text_height @text_height.setter def text_height(self, txt_h): self.properties_3d.text_height = txt_h @property def properties_2d(self): """Get or set a Legend2DParameters for the properties of 2D legends.""" return self._properties_2d @properties_2d.setter def properties_2d(self, value): if value is not None: assert isinstance(value, Legend2DParameters), 'Expected Legend2DParameters' \ ' for properties_2d. Got {}.'.format(type(value)) else: value = Legend2DParameters() self._properties_2d = value value._parent = self @property def origin_x(self): """Get or set text for the X coordinate from where the 2D legend will be drawn. """ return self.properties_2d.origin_x @origin_x.setter def origin_x(self, value): self.properties_2d.origin_x = value @property def origin_y(self): """Get or set text for the Y coordinate from where the 2D legend will be drawn. """ return self.properties_2d.origin_y @origin_y.setter def origin_y(self, value): self.properties_2d.origin_y = value @property def segment_height_2d(self): """Get or set the height for each of the legend segments in 2D space.""" return self.properties_2d.segment_height @segment_height_2d.setter def segment_height_2d(self, seg_h): self.properties_2d.segment_height = seg_h @property def segment_width_2d(self): """Get or set the width for each of the legend segments in 2D space.""" return self.properties_2d.segment_width @segment_width_2d.setter def segment_width_2d(self, seg_w): self.properties_2d.segment_width = seg_w @property def text_height_2d(self): """Get or set the height for the legend text in 2D space.""" return self.properties_2d.text_height @text_height_2d.setter def text_height_2d(self, txt_h): self.properties_2d.text_height = txt_h @property def user_data(self): """Get or set an optional dictionary for additional meta data for this object. This will be None until it has been set. All keys and values of this dictionary should be of a standard Python type to ensure correct serialization of the object to/from JSON (eg. str, float, int, list, dict) """ return self._user_data @user_data.setter def user_data(self, value): if value is not None: assert isinstance(value, dict), 'Expected dictionary for ' \ 'object user_data. Got {}.'.format(type(value)) self._user_data = value @property def is_segment_count_default(self): """Boolean noting whether the number of segments is defaulted.""" return self._is_segment_count_default @property def are_colors_default(self): """Boolean noting whether the colors are defaulted.""" return self._are_colors_default @property def is_title_default(self): """Boolean noting whether the title is defaulted.""" return self._is_title_default @property def is_base_plane_default(self): """Boolean noting whether the base plane in 3D space is defaulted.""" return self.properties_3d.is_base_plane_default @property def is_segment_height_default(self): """Boolean noting whether the segment height in 3D space is defaulted.""" return self.properties_3d.is_segment_height_default @property def is_segment_width_default(self): """Boolean noting whether the segment width in 3D space is defaulted.""" return self.properties_3d.is_segment_width_default @property def is_text_height_default(self): """Boolean noting whether the text height in 3D space is defaulted.""" return self.properties_3d.is_text_height_default @property def is_origin_x_default(self): """Boolean noting whether the X coordinate in 2D is defaulted.""" return self.properties_2d.is_origin_x_default @property def is_origin_y_default(self): """Boolean noting whether the Y coordinate in 2D is defaulted.""" return self.properties_2d.is_origin_y_default @property def is_segment_height_2d_default(self): """Boolean noting whether the segment height in 2D is defaulted.""" return self.properties_2d.is_segment_height_default @property def is_segment_width_2d_default(self): """Boolean noting whether the segment width in 2D is defaulted.""" return self.properties_2d.is_segment_width_default @property def is_text_height_2d_default(self): """Boolean noting whether the text height in 3D space is defaulted.""" return self.properties_2d.is_text_height_default
[docs] def colors_by_set(self, colorset_name): """Set the colors of this object using the name of a Colorset. This will also add the name of the color set to this LegendParameter's user_data. Args: colorset_name: The name of a Colorset to dictate the colors of this legend parameter object (eg. ecotect) (eg. benefit_harm). See the the ladybug Colorset object for a complete list of color sets. """ col_method = getattr(Colorset, colorset_name) self.colors = col_method() if self._user_data is None: self.user_data = {'color_set': colorset_name} else: self._user_data['color_set'] = colorset_name
[docs] def duplicate(self): """Return a copy of the current legend parameters.""" return self.__copy__()
[docs] def to_dict(self): """Get legend parameters as a dictionary.""" base = self._base_dict() if self._min is not None: base['min'] = self.min if self._max is not None: base['max'] = self.max if not self.is_segment_count_default: base['segment_count'] = self.segment_count base['ordinal_dictionary'] = self.ordinal_dictionary base['type'] = 'LegendParameters' return base
def _base_dict(self): """Get a dictionary with the base properties shared by all LegendParameters.""" base = { 'continuous_legend': self.continuous_legend, 'decimal_count': self.decimal_count, 'include_larger_smaller': self.include_larger_smaller, 'vertical': self.vertical, 'font': self.font } if not self.are_colors_default: base['colors'] = [c.to_dict() for c in self.colors] if not self.is_title_default: base['title'] = self.title if not self.properties_3d.is_default: base['properties_3d'] = self.properties_3d.to_dict() if not self.properties_2d.is_default: base['properties_2d'] = self.properties_2d.to_dict() if self.user_data is not None: base['user_data'] = self.user_data return base @staticmethod def _convert_colors(cols): """Convert a list of colors into ladybug Color objects.""" try: cols = tuple(col if isinstance(col, Color) else Color( col.R, col.G, col.B) for col in cols) except Exception: try: cols = tuple(Color(col.Red, col.Green, col.Blue) for col in cols) except Exception: raise ValueError("{} is not a valid list of colors".format(cols)) return cols def __copy__(self): new_par = LegendParameters( self.min, self.max, self.segment_count, self.colors, self.title) new_par._continuous_legend = self._continuous_legend new_par._ordinal_dictionary = self._ordinal_dictionary new_par._decimal_count = self._decimal_count new_par._include_larger_smaller = self._include_larger_smaller new_par._vertical = self._vertical new_par._font = self._font new_par.properties_3d = self.properties_3d.duplicate() new_par.properties_2d = self.properties_2d.duplicate() new_par._user_data = None if self.user_data is None else self.user_data.copy() new_par._is_segment_count_default = self._is_segment_count_default new_par._are_colors_default = self._are_colors_default new_par._is_title_default = self._is_title_default return new_par def __key(self): return ( self.min, self.max, self.segment_count, self.title, self._continuous_legend, self._ordinal_dictionary, self._decimal_count, self._include_larger_smaller, self._vertical, self._font, hash(self.properties_3d), hash(self.properties_2d), self._is_segment_count_default, self._are_colors_default, self._is_title_default ) + tuple(hash(col) for col in self.colors) def __hash__(self): return hash(self.__key()) def __eq__(self, other): return isinstance(other, LegendParameters) and self.__key() == other.__key() def __ne__(self, value): return not self.__eq__(value)
[docs] def ToString(self): """Overwrite .NET ToString method.""" return self.__repr__()
def __repr__(self): """Legend parameter representation.""" min = self.min if self.min is not None else '[default]' max = self.max if self.max is not None else '[default]' seg = '[default]' if self.is_segment_count_default \ else self.segment_count title = '[default]' if self.is_title_default else self.title return 'Legend Parameters\n minimum: {}\n maximum: {}\n segments: {}\n' \ ' colors:\n {}\n continuous legend: {}\n' \ ' title: {}\n ordinal text: {}\n number decimals: {}\n' \ ' include < >: {}\n vertical: {}\n font: {}\n {}\n{}'.format( min, max, seg, '\n '.join([str(c) for c in self.colors]), self.continuous_legend, title, self.ordinal_dictionary, self.decimal_count, self.include_larger_smaller, self.vertical, self.font, self.properties_3d, self.properties_2d)
[docs]class LegendParametersCategorized(LegendParameters): """Ladybug legend parameters used to customize legends. These legend parameters have more limitations than the base LegendParameters class. However, these legend parameters will do auto-categorization of data, binning values into groups based on custom ranges. Args: domain: A list of one or more numbers noting the boundaries of the data categories. For example, [100, 2000] creates three categories of (<100, 100-2000, >2000). Values must always be ordered from lowest to highest. colors: An list of color objects with a length equal to the number of items in the domain + 1. These are used to color each of the categories of data. category_names: An optional list of text strings with a length equal to the colors. These will be used to name each of the categories in the legend. If None, the legend text will simply mark the numerical ranges of the categories. (Default: None). title: Text string for Legend title. Typically, the units of the data are used here but the type of data might also be used. Default is an empty string. base_plane: A Ladybug Plane object to note the starting point from where the legend will be generated. The default is the world XY plane at origin (0, 0, 0). Properties: * domain * colors * category_names * continuous_colors * continuous_legend * title * ordinal_dictionary * decimal_count * include_larger_smaller * vertical * font * user_data * properties_3d * base_plane * segment_height * segment_width * text_height * properties_2d * origin_x * origin_y * segment_height_2d * segment_width_2d * text_height_2d * min * max * segment_count * is_title_default * is_base_plane_default * is_segment_height_default * is_segment_width_default * is_text_height_default """ __slots__ = ('_domain', '_category_names', '_continuous_colors') def __init__(self, domain, colors, category_names=None, title=None, base_plane=None): """Initialize Ladybug Legend Parameters Categorized.""" # set the domain after verifying that it is correct assert isinstance(domain, Iterable) \ and not isinstance(domain, (str, dict, bytes, bytearray)), \ 'Domain should be a list or tuple. Got {}'.format(type(domain)) self._domain = tuple(float(x) for x in sorted(domain)) assert len(self._domain) > 0, \ 'LegendParametersCategorized domain must have at least one value.' self._min = self._domain[0] self._max = self._domain[-1] self._segment_count = len(self._domain) + 1 # set the other init arguments self.colors = colors self.category_names = category_names self.title = title # set all of the other inputs to None/default self.continuous_colors = None self.continuous_legend = None self.decimal_count = None self.include_larger_smaller = None self.vertical = None self.font = None self._user_data = None # set the 3D and 2D properties self.properties_3d = Legend3DParameters(base_plane) self.properties_2d = Legend2DParameters() # properties that have no meaning for this class self._ordinal_dictionary = None self._is_segment_count_default = True self._are_colors_default = False
[docs] @classmethod def from_dict(cls, data): """Create LegendParametersCategorized from a dictionary. Args: data: A python dictionary in the following format .. code-block:: python { "type": "LegendParametersCategorized", "domain": [100, 2000], "colors": [{'r': 0, 'g': 0, 'b': 0}, {'r': 0, 'g': 0, 'b': 100}, {'r': 255, 'g': 0, 'b': 0}], "category_names": ["low", "desired", "high"] } """ data = data.copy() # copy to avoid mutating the input dictionary assert data['type'] == 'LegendParametersCategorized', \ 'Expected LegendParametersCategorized. Got {}.'.format(data['type']) default_dict = {'type': 'Default'} optional_keys = ( 'category_names', 'continuous_legend', 'continuous_colors', 'title', 'decimal_count', 'include_larger_smaller', 'vertical', 'font', 'properties_3d', 'properties_2d') for key in optional_keys: if key not in data: data[key] = None elif data[key] == default_dict: data[key] = None colors = [Color.from_dict(col) for col in data['colors']] leg_par = cls(data['domain'], colors, data['category_names'], data['title']) leg_par.continuous_colors = data['continuous_colors'] leg_par.continuous_legend = data['continuous_legend'] leg_par.decimal_count = data['decimal_count'] leg_par.include_larger_smaller = data['include_larger_smaller'] leg_par.vertical = data['vertical'] leg_par.font = data['font'] if data['properties_3d'] is not None: leg_par.properties_3d = Legend3DParameters.from_dict(data['properties_3d']) if data['properties_2d'] is not None: leg_par.properties_2d = Legend2DParameters.from_dict(data['properties_2d']) if 'user_data' in data and data['user_data'] is not None: leg_par.user_data = data['user_data'] return leg_par
@property def domain(self): """Get or set a list of numbers noting the boundaries of the data categories.""" return self._domain @domain.setter def domain(self, dom): assert isinstance(dom, Iterable) \ and not isinstance(dom, (str, dict, bytes, bytearray)), \ 'Domain should be a list or tuple. Got {}'.format(type(dom)) self._domain = tuple(float(x) for x in sorted(dom)) assert len(self._domain) == len(self._colors) - 1, 'The length of domain must' \ 'be one less than length of the colors for a LegendParametersCategorized.' \ '{} != {} - 1'.format(len(self._domain), len(self._colors)) self._min = self._domain[0] self._max = self._domain[-1] self._segment_count = len(self._domain) + 1 @property def colors(self): """Get or set the colors defining the legend.""" return self._colors @colors.setter def colors(self, cols): assert isinstance(cols, Iterable) \ and not isinstance(cols, (str, dict, bytes, bytearray)), \ 'Colors should be a list or tuple. Got {}'.format(type(cols)) self._colors = self._convert_colors(cols) assert len(self._colors) == len(self._domain) + 1, 'The length of colors must ' \ 'be one more than the length of domain for a LegendParametersCategorized.' \ '{} != {} + 1'.format(len(self._colors), len(self._domain)) @property def category_names(self): """Get or set a list of text for the names of the categories.""" if self._category_names: # user-specified category names return self._category_names # generate the category names based on the domain format_str = '%.{}f'.format(self.decimal_count) nums = [format_str % x for x in self.domain] mid_nums = tuple('{} - {}'.format(nums[i], nums[i + 1]) for i in xrange(len(nums) - 1)) if self.include_larger_smaller: return ('<' + nums[0],) + mid_nums + ('>' + nums[-1],) return (nums[0],) + mid_nums + (nums[-1],) @category_names.setter def category_names(self, categories): if categories is not None: assert isinstance(categories, Iterable) \ and not isinstance(categories, (str, dict, bytes, bytearray)), \ 'Category names should be a list or tuple. Got {}'.format( type(categories)) self._category_names = tuple(str(x) for x in categories) assert len(self._category_names) == len(self._domain) + 1, 'The length of ' \ 'category_names must be one more than length of the colors for a ' \ 'LegendParametersCategorized.{} != {} - 1'.format( len(self._domain), len(self._colors)) else: self._category_names = None @property def continuous_colors(self): """Boolean noting whether colors generated are continuous or discrete. If True, the colors generated from the corresponding legend will be in a continuous gradient. If False, they will be categorized in incremental groups according to the segment_count. (Default: False). """ return self._continuous_colors @continuous_colors.setter def continuous_colors(self, cont_cols): if cont_cols is not None: assert isinstance(cont_cols, bool), \ 'Expected boolean for continuous_colors. Got {}.'.format(type(cont_cols)) self._continuous_colors = cont_cols else: self._continuous_colors = False @property def include_larger_smaller(self): """Boolean noting whether > and < should be included in legend segment text.""" return self._include_larger_smaller @include_larger_smaller.setter def include_larger_smaller(self, lg_sm): if lg_sm is not None: assert isinstance(lg_sm, bool), 'Expected boolean for ' \ 'include_larger_smaller. Got {}.'.format(type(lg_sm)) self._include_larger_smaller = lg_sm else: self._include_larger_smaller = True @property def min(self): """Get legend minimum. This is derived from the domain.""" return self._min @property def max(self): """Get legend maximum. This is derived from the domain.""" return self._max @property def segment_count(self): """Get the number of segments in the legend. This is always equal to one more than the length of the domain.""" return self._segment_count @property def ordinal_dictionary(self): """Always None for a LegendParametersCategorized.""" return self._ordinal_dictionary
[docs] def to_dict(self): """Get legend parameters categorized as a dictionary.""" base = self._base_dict() base['type'] = 'LegendParametersCategorized' base['domain'] = self.domain base['category_names'] = self.category_names base['continuous_colors'] = self.continuous_colors return base
def __copy__(self): new_par = LegendParametersCategorized( self._domain, self._colors, self._category_names, self.title) new_par._continuous_colors = self._continuous_colors new_par._continuous_legend = self._continuous_legend new_par._decimal_count = self._decimal_count new_par._include_larger_smaller = self._include_larger_smaller new_par._vertical = self._vertical new_par._font = self._font new_par.properties_3d = self.properties_3d.duplicate() new_par.properties_2d = self.properties_2d.duplicate() new_par._user_data = None if self.user_data is None else self.user_data.copy() new_par._is_title_default = self._is_title_default return new_par def __key(self): return ( self._domain, self._category_names, self.title, self._continuous_colors, self._continuous_legend, self._decimal_count, self._include_larger_smaller, self._vertical, self._font, hash(self.properties_3d), hash(self.properties_2d), self._is_title_default ) + tuple(hash(col) for col in self.colors) def __hash__(self): return hash(self.__key()) def __eq__(self, other): return isinstance(other, LegendParametersCategorized) and \ self.__key() == other.__key() def __ne__(self, value): return not self.__eq__(value) def __repr__(self): """Legend parameter representation.""" title = '[default]' if self.is_title_default else self.title return 'Legend Parameters Categorized\n domain: {}\n colors:\n {}\n' \ ' category names\n {}\n continuous colors: {}\n continuous legend: {}\n' \ ' title: {}\n number decimals: {}\n' \ ' include < >: {}\n vertical: {}\n font: {}\n {}\n{}'.format( self.domain, '\n '.join([str(c) for c in self.colors]), '\n '.join([str(c) for c in self.category_names]), self.continuous_colors, self.continuous_legend, title, self.decimal_count, self.include_larger_smaller, self.vertical, self.font, self.properties_3d, self.properties_2d)
[docs]class Legend3DParameters(object): """Object to customize the properties of legends in the 3D scene. Args: base_plane: A Ladybug Plane object to note the starting point from where the legend will be generated. If None, the default is the world XY plane at origin (0, 0, 0) unless the legend is assigned to a specific geometry, in which case the origin is in the lower right corner of the geometry bounding box for vertical legends and the upper right corner for horizontal legends. segment_height: A number to set the height for each of the legend segments. If None, the default is 1 unless the legend is assigned to a specific geometry, in which case it is automatically set to a value on an appropriate scale (some fraction of the bounding box around the geometry). segment_width: A number to set the width for each of the legend segments. If None, the default is 1 unless the legend is assigned to a specific geometry, in which case it is automatically set to a value on an appropriate scale (some fraction of the bounding box around the geometry). text_height: A number to set the height for the legend text. If None, the default is 1/3 of the segment_height. Properties: * base_plane * segment_height * segment_width * text_height * is_default * is_base_plane_default * is_segment_height_default * is_segment_width_default * is_text_height_default """ __slots__ = ( '_base_plane', '_segment_height', '_segment_width', '_text_height', '_is_base_plane_default', '_is_segment_height_default', '_is_segment_width_default', '_is_text_height_default', '_parent') def __init__(self, base_plane=None, segment_height=None, segment_width=None, text_height=None): """Initialize Legend3DParameters.""" self.base_plane = base_plane self.segment_height = segment_height self.segment_width = segment_width self.text_height = text_height self._parent = None
[docs] @classmethod def from_dict(cls, data): """Create Legend3DParameters from a dictionary. Args: data: A python dictionary in the following format .. code-block:: python { "type": "Legend3DParameters", "base_plane": {"type": "Plane", "o": [11, 0, 0], "n": [0, 0, 1]}, "segment_height": 0.5, "segment_width": 0.25, "text_height": 0.25 } """ data = data.copy() # copy to avoid mutating the input dictionary assert data['type'] == 'Legend3DParameters', \ 'Expected Legend3DParameters. Got {}.'.format(data['type']) default_dict = {'type': 'Default'} optional_keys = ('base_plane', 'segment_height', 'segment_width', 'text_height') for key in optional_keys: if key not in data: data[key] = None elif data[key] == default_dict: data[key] = None base_plane = None if data['base_plane'] is not None: base_plane = Plane.from_dict(data['base_plane']) return cls(base_plane, data['segment_height'], data['segment_width'], data['text_height'])
@property def base_plane(self): """Get or set a Ladybug Point3D for the base point of the legend.""" return self._base_plane @base_plane.setter def base_plane(self, base_pl): if base_pl is not None: assert isinstance(base_pl, Plane), \ 'Expected Ladybug Plane for base_plane. Got {}.'.format(type(base_pl)) self._base_plane = base_pl self._is_base_plane_default = False else: self._base_plane = Plane(Vector3D(0, 0, 1), Point3D(0, 0, 0)) self._is_base_plane_default = True @property def segment_height(self): """Get or set the height for each of the legend segments. The default is 1 unless the legend is assigned to a specific geometry, in which case it is automatically set to a value on an appropriate scale (some fraction of the bounding box around the geometry). """ return self._segment_height @segment_height.setter def segment_height(self, seg_h): if seg_h is not None: assert isinstance(seg_h, (float, int)), \ 'Expected number for segment_height. Got {}.'.format(type(seg_h)) assert seg_h > 0, 'segment_height must be greater than 0.' \ ' Got {}.'.format(seg_h) self._segment_height = seg_h self._is_segment_height_default = False else: self._segment_height = 1 self._is_segment_height_default = True @property def segment_width(self): """Get or set the width for each of the legend segments. Default is 1 when legend is vertical. When horizontal, the default is text_height * (max_number_of_digits + 2) where max_number_of_digits is the number of digits displaying in the legend parameter max. """ if self.is_segment_width_default and self._parent is not None and \ not self._parent.vertical: return self.text_height * 5 return self._segment_width @segment_width.setter def segment_width(self, seg_w): if seg_w is not None: assert isinstance(seg_w, (float, int)), \ 'Expected number for segment_width. Got {}.'.format(type(seg_w)) assert seg_w > 0, 'segment_width must be greater than 0.' \ ' Got {}.'.format(seg_w) self._segment_width = seg_w self._is_segment_width_default = False else: self._segment_width = 1 self._is_segment_width_default = True @property def text_height(self): """Get or set the height for the legend text. Default is 1/3 of the segment_height. """ if self.is_text_height_default: return self.segment_height * 0.33 return self._text_height @text_height.setter def text_height(self, txt_h): if txt_h is not None: assert isinstance(txt_h, (float, int)), \ 'Expected number for text_height. Got {}.'.format(type(txt_h)) assert txt_h > 0, 'text_height must be greater than 0.' \ ' Got {}.'.format(txt_h) self._is_text_height_default = False else: self._is_text_height_default = True self._text_height = txt_h @property def is_base_plane_default(self): """Boolean noting whether the base plane is defaulted.""" return self._is_base_plane_default @property def is_segment_height_default(self): """Boolean noting whether the segment height is defaulted.""" return self._is_segment_height_default @property def is_segment_width_default(self): """Boolean noting whether the segment width is defaulted.""" return self._is_segment_width_default @property def is_text_height_default(self): """Boolean noting whether the text height is defaulted.""" return self._is_text_height_default @property def is_default(self): """Boolean noting whether all properties are defaulted.""" return all(( self._is_base_plane_default, self._is_segment_height_default, self._is_segment_width_default, self._is_text_height_default ))
[docs] def duplicate(self): """Return a copy of the current Legend3DParameters.""" return self.__copy__()
[docs] def to_dict(self): """Get Legend3DParameters as a dictionary.""" base = {'type': 'Legend3DParameters'} if not self.is_base_plane_default: base['base_plane'] = self.base_plane.to_dict() if not self.is_segment_height_default: base['segment_height'] = self.segment_height if not self.is_segment_width_default: base['segment_width'] = self.segment_width if not self.is_text_height_default: base['text_height'] = self.text_height return base
def __copy__(self): new_par = Legend3DParameters( self._base_plane, self._segment_height, self._segment_width, self._text_height) new_par._is_base_plane_default = self._is_base_plane_default new_par._is_segment_height_default = self._is_segment_height_default new_par._is_segment_width_default = self._is_segment_width_default new_par._is_text_height_default = self._is_text_height_default return new_par def __key(self): return ( hash(self.base_plane), self._segment_height, self._segment_width, self._text_height, self._is_base_plane_default, self._is_segment_height_default, self._is_segment_width_default, self._is_text_height_default ) def __hash__(self): return hash(self.__key()) def __eq__(self, other): return isinstance(other, Legend3DParameters) and self.__key() == other.__key() def __ne__(self, value): return not self.__eq__(value)
[docs] def ToString(self): """Overwrite .NET ToString method.""" return self.__repr__()
def __repr__(self): """Legend3DParameters representation.""" base_pt = '[default]' if self.is_base_plane_default else self.base_plane.o seg_h = '[default]' if self.is_segment_height_default else self.segment_height seg_w = '[default]' if self.is_segment_width_default else self.segment_width txt_h = '[default]' if self.is_text_height_default else self.text_height return '3D Parameters\n base point: {}\n' \ ' segment height: {}\n segment width: {}\n' \ ' text height: {}'.format(base_pt, seg_h, seg_w, txt_h)
[docs]class Legend2DParameters(object): """Object to customize the properties of legends in the 2D plane of a screen. Args: origin_x: A text string to note the X coordinate of the base point from where the legend will be generated (assuming an origin in the upper-left corner of the screen with higher positive values of X moving to the right). Text must be formatted as an integer followed by "px" (to denote the number of screen pixels) or a number followed by "%" (to denote the percentage of the screen). Examples include 10px, 5%. The default is set to make the legend clearly visible on the screen (10px). origin_y: A text string to note the Y coordinate of the base point from where the legend will be generated (assuming an origin in the upper-left corner of the screen with higher positive values of Y moving downward). Text must be formatted as an integer followed by "px" (to denote the number of screen pixels) or a number followed by "%" (to denote the percentage of the screen). Examples include 10px, 5%. The default is set to make the legend clearly visible on the screen (50px). segment_height: A text string to note the height for each of the legend segments. Text must be formatted as an integer followed by "px" (to denote the number of screen pixels) or a number followed by "%" (to denote the percentage of the screen). Examples include 10px, 5%. The default is set to make most legends readable (25px for horizontal and 36px for vertical). segment_width: A text string to set the width for each of the legend segments. Text must be formatted as an integer followed by "px" (to denote the number of screen pixels) or a number followed by "%" (to denote the percentage of the screen). Examples include 10px, 5%. The default is set to make most legends readable (36px for horizontal and 25px for vertical). text_height: A text string to set the height for the legend text. Text must be formatted as an integer followed by "px" (to denote the number of screen pixels) or a number followed by "%" (to denote the percentage of the screen). Examples include 10px, 5%. Default is 12px. Properties: * origin_x * origin_y * segment_height * segment_width * text_height * is_default * is_origin_x_default * is_origin_y_default * is_segment_height_default * is_segment_width_default * is_text_height_default """ __slots__ = ( '_origin_x', '_origin_y', '_segment_height', '_segment_width', '_text_height', '_is_origin_x_default', '_is_origin_y_default', '_is_segment_height_default', '_is_segment_width_default', '_is_text_height_default', '_parent') VALID_DIM = re.compile(r'^\d*px|\d*%$|\d*.\d*%$') def __init__(self, origin_x=None, origin_y=None, segment_height=None, segment_width=None, text_height=None): """Initialize Legend2DParameters.""" self.origin_x = origin_x self.origin_y = origin_y self.segment_height = segment_height self.segment_width = segment_width self.text_height = text_height self._parent = None
[docs] @classmethod def from_dict(cls, data): """Create Legend2DParameters from a dictionary. Args: data: A python dictionary in the following format .. code-block:: python { "type": "Legend2DParameters", "origin_x": "20px", "origin_y": "20px", "segment_height": "5%", "segment_width": "2%", "text_height": "3%" } """ data = data.copy() # copy to avoid mutating the input dictionary assert data['type'] == 'Legend2DParameters', \ 'Expected Legend2DParameters. Got {}.'.format(data['type']) default_dict = {'type': 'Default'} optional_keys = ( 'origin_x', 'origin_y', 'segment_height', 'segment_width', 'text_height') for key in optional_keys: if key not in data: data[key] = None elif data[key] == default_dict: data[key] = None return cls(data['origin_x'], data['origin_y'], data['segment_height'], data['segment_width'], data['text_height'])
@property def origin_x(self): """Get or set text to note the X coordinate from where the legend will be drawn. The default is set to make the legend clearly visible on the screen (10px). """ return self._origin_x @origin_x.setter def origin_x(self, value): if value is not None: self.valid_dim_string(value, 'origin_x') self._origin_x = value self._is_origin_x_default = False else: self._origin_x = '10px' self._is_origin_x_default = True @property def origin_y(self): """Get or set text to note the Y coordinate from where the legend will be drawn. The default is set to make the legend clearly visible on the screen (50px). """ return self._origin_y @origin_y.setter def origin_y(self, value): if value is not None: self.valid_dim_string(value, 'origin_y') self._origin_y = value self._is_origin_y_default = False else: self._origin_y = '50px' self._is_origin_y_default = True @property def segment_height(self): """Get or set the height for each of the legend segments. The default is set to make most legends readable (25px for horizontal and 36px for vertical). """ if self.is_segment_height_default and self._parent is not None and \ not self._parent.vertical: return '25px' return self._segment_height @segment_height.setter def segment_height(self, value): if value is not None: self.valid_dim_string(value, 'segment_height') self._segment_height = value self._is_segment_height_default = False else: self._segment_height = '36px' self._is_segment_height_default = True @property def segment_width(self): """Get or set the width for each of the legend segments. The default is set to make most legends readable (36px for horizontal and 25px for vertical). """ if self.is_segment_width_default and self._parent is not None and \ not self._parent.vertical: return '36px' return self._segment_width @segment_width.setter def segment_width(self, value): if value is not None: self.valid_dim_string(value, 'segment_width') self._segment_width = value self._is_segment_width_default = False else: self._segment_width = '25px' self._is_segment_width_default = True @property def text_height(self): """Get or set the height for the legend text.""" return self._text_height @text_height.setter def text_height(self, value): if value is not None: self.valid_dim_string(value, 'text_height') self._text_height = value self._is_text_height_default = False else: self._text_height = '12px' self._is_text_height_default = True @property def is_origin_x_default(self): """Boolean noting whether the origin X coordinate is defaulted.""" return self._is_origin_x_default @property def is_origin_y_default(self): """Boolean noting whether the origin Y coordinate is defaulted.""" return self._is_origin_y_default @property def is_segment_height_default(self): """Boolean noting whether the segment height is defaulted.""" return self._is_segment_height_default @property def is_segment_width_default(self): """Boolean noting whether the segment width is defaulted.""" return self._is_segment_width_default @property def is_text_height_default(self): """Boolean noting whether the text height is defaulted.""" return self._is_text_height_default @property def is_default(self): """Boolean noting whether all properties are defaulted.""" return all(( self._is_origin_x_default, self._is_origin_y_default, self._is_segment_height_default, self._is_segment_width_default, self._is_text_height_default ))
[docs] def duplicate(self): """Return a copy of the current Legend2DParameters.""" return self.__copy__()
[docs] def to_dict(self): """Get Legend2DParameters as a dictionary.""" base = {'type': 'Legend2DParameters'} if not self.is_origin_x_default: base['origin_x'] = self.origin_x if not self.is_origin_y_default: base['origin_y'] = self.origin_y if not self.is_segment_height_default: base['segment_height'] = self.segment_height if not self.is_segment_width_default: base['segment_width'] = self.segment_width if not self.is_text_height_default: base['text_height'] = self.text_height return base
[docs] @staticmethod def valid_dim_string(dim_string, invalid_obj='Legend2DParameters', raise_exception=True): """Check if a string is in a valid format for assigning 2D dimensions. Args: dim_string: Text to check if it fits the format for 2D dimensions. invalid_obj: An optional name of the object to be reported in the error message when raise_exception is True. raise_exception: Boolean to note whether an exception should be raised if the dim_string is not in the correct format. Otherwise, this method will simply return True/False for whether the string meets the format. """ if Legend2DParameters.VALID_DIM.match(dim_string) is None: if not raise_exception: return False msg = 'Invalid specification for {}.\nString "{}" does not match the ' \ 'format expected for 2D dimensions. (eg. 10px, 5%)'.format( invalid_obj, dim_string) raise ValueError(msg) return True
def __copy__(self): new_par = Legend2DParameters( self._origin_x, self._origin_y, self._segment_height, self._segment_width, self._text_height) new_par._is_origin_x_default = self._is_origin_x_default new_par._is_origin_y_default = self._is_origin_y_default new_par._is_segment_height_default = self._is_segment_height_default new_par._is_segment_width_default = self._is_segment_width_default new_par._is_text_height_default = self._is_text_height_default return new_par def __key(self): return ( self._origin_x, self._origin_y, self._segment_height, self._segment_width, self._text_height, self._is_origin_x_default, self._is_origin_y_default, self._is_segment_height_default, self._is_segment_width_default, self._is_text_height_default ) def __hash__(self): return hash(self.__key()) def __eq__(self, other): return isinstance(other, Legend2DParameters) and self.__key() == other.__key() def __ne__(self, value): return not self.__eq__(value)
[docs] def ToString(self): """Overwrite .NET ToString method.""" return self.__repr__()
def __repr__(self): """Legend2DParameters representation.""" origin_x = '[default]' if self.is_origin_x_default else self.origin_x origin_y = '[default]' if self.is_origin_y_default else self.origin_y seg_h = '[default]' if self.is_segment_height_default else self.segment_height seg_w = '[default]' if self.is_segment_width_default else self.segment_width txt_h = '[default]' if self.is_text_height_default else self.text_height return '2D Parameters\n base point: ({}, {})\n' \ ' segment height: {}\n segment width: {}\n' \ ' text height: {}'.format(origin_x, origin_y, seg_h, seg_w, txt_h)