diff --git a/src/cowpatch/__init__.py b/src/cowpatch/__init__.py index 6296d08..7c75fb4 100644 --- a/src/cowpatch/__init__.py +++ b/src/cowpatch/__init__.py @@ -4,6 +4,7 @@ from .layout_elements import layout, area from .base_elements import patch -#from .text_elements import text +from .text_elements import text +from .annotation_elements import annotation #from .wrappers import wrapper_plotnine, wrapper_matplotlib, wrapper_seaborn from .config import rcParams diff --git a/src/cowpatch/annotation_elements.py b/src/cowpatch/annotation_elements.py new file mode 100644 index 0000000..ee5b42e --- /dev/null +++ b/src/cowpatch/annotation_elements.py @@ -0,0 +1,1156 @@ +import copy +from .text_elements import text +from .utils import inherits, _to_alphabet_representation, _to_roman_numerials,\ + _string_tag_format +import re +import numpy as np +import copy + +import pdb + +# TODO: potentially allow addition of theme to pass info standard formats... + +# annotation +# +# internal functions: +# 1. pass tags farther down (to another annotation) +# 2. create tag of requested size +# 3. + +class annotation: + def __init__(self, title=None, subtitle=None, caption=None, + tags=None, tags_format=None, tags_order="auto", + tags_loc=None, tags_inherit="fix"): + """ + annotation tools for adding titles and tags to cow.patch + arangement of plots. + + Arguments + --------- + title : str, cow.text, or dictionary + This contains information for the top level patch's titles, which + can appear on the top (dictionary key: "top"/"t"), left + ("left"/"l"), right ("right"/"r"), or bottom ("bottom"/"b"). One + can have multiple titles (e.g. one on top and one on the left). + subtitle : str, cow.text, or dictionary + This contains information for the top level patch's subtitles + (and appear below the titles). Like `title`, these can appear on + the top (dictionary key: "top"/"t"), left ("left"/"l"), + right ("right"/"r"), or bottom ("bottom"/"b"). One can have + multiple subtitles (e.g. one on top and one on the left). + caption : str or cow.text + Each patch can have 1 caption which is located below all other + elements when the patch is configured (i.e. below all plots, + titles, subtitles and tags). + tags : list or tuple + list or tuple. If one wants to define each labels name by hand, + use a list. If you would like to define labels in an automated + manner relative to the the hierachical storage structure, use a + tuple. The length of the tuple, at max should be the max depth of + the arangement of plots. Each value in the tuple can either be + a string specifying the general auto-index structure ("1" or "0", + "a", "A", "i", "I") OR it can be a list that acts like the auto + ordering for the given level. + tags_format : str/text objects or tuple of str/text objects + Format of strings relative to levels of inserts. For example, if + we were using the automated labels and had a depth of 2, one might + see something like "Fig {0}.{1}" for this paramter. We could then + see the output of "Fig 2.1" if both tags liked like ("1","1") + or ("0","0"). The default, if tags on auto-constructed, is + ("{0}", "{0}.{1}", "{0}.{1}.{2}", "{0}.{1}.{2}.{3}", ...) + tags_order : str ["auto", "input", "yokogaki"] + How we orderthe tags. If auto, the default is by "input" if you + provide your own labels and "yokogaki" if you don't. "Input" means + that the tags ordering will be assoicated with the grobs ordering. + "Yokogaki" means that the tags will be associated with the + top-to-bottom, left-to-right ordering of the grobs. + tags_loc : str ["top", "left", "right", "bottom"] + Location of the tags relative to grob they are tagging. + tags_inherit : str ["fix", "override"] + Indicates how tagging should behave relative to plotential plot + hierachy. If "fix", then the associated cowpatch object and inner + objects won't inherits tags from parents. If "override", + parent tagging structure will pass through *and* override tagging + desires for this level. + + Notes + ----- + TODO (update docstring): + Due to "tags_inherit", we need to allow updates. Potential solution + to removing values would be to have additions say "np.nan" instead of + None to clean something out. Annoyingly this would force updates of + of a dictionary - which will also need this functionality. + + Note this function doesn't track if the text object is the correct + class... maybe it should override it if it isn't the correct class? + """ + + # prep definition + self.title = dict() + self.subtitle = dict() + self.caption = None + self.tags = None + self.tags_order = None + self.tags_loc = None + self.tags_inherit = None + self._tags_format = None + #self.tags_depth = -1 + + self._update_all_attributes(title=title, + subtitle=subtitle, + caption=caption, + tags=tags, + tags_format=tags_format, + tags_order=tags_order, + tags_loc=tags_loc, + tags_inherit=tags_inherit) + + @property + def tags_format(self): + if self._tags_format is not None and self._tags_format is not np.nan: + _tags_format = self._tags_format + elif self.tags is None or self.tags is np.nan: # both tags_format and tags are None + _tags_format = self._tags_format + elif inherits(self.tags, list): + _tags_format = ("{0}", ) + else: + _tags_format = tuple(_string_tag_format(x) + for x in np.arange(len(self.tags))) + + ## making tags_format text (same code in _update_all_attributes...) + if _tags_format is not None and _tags_format is not np.nan: # if None/np.nan, then tags is also None + new_tags_format = [] + for e in _tags_format: + if inherits(e, text): + e2 = copy.deepcopy(e) + e2._define_type(_type = "cow_tag") + new_tags_format.append(e2) + else: #e is string + e = text(label = e, _type = "cow_tag") + new_tags_format.append(e) + new_tags_format = tuple(new_tags_format) + else: + new_tags_format = _tags_format + + return new_tags_format + + @property + def tags_depth(self): + """ + tags_depth definition + """ + if inherits(self.tags, list): + tags_depth = 1 + elif (self.tags_format is None or self.tags_format is np.nan) and \ + (self.tags is None or self.tags is np.nan): + tags_depth = -1 + elif self.tags_format is None or self.tags_format is np.nan: + tags_depth = len(self.tags) + else: + tags_depth = len(self.tags_format) + + return tags_depth + + def _clean_up_attributes(self): + """ + Examines all attributes and those that are np.nan are converted to None + """ + attributes = copy.deepcopy(self.__dict__) + + for key, value in attributes.items(): + if value is np.nan: + if key in ["title", "subtitle"]: + self.__dict__[key] = {} + else: + self.__dict__[key] = None + + + + + return None + + def _update_all_attributes(self, title=None, subtitle=None, caption=None, + tags=None, tags_format=None, tags_order="auto", + tags_loc=None, tags_inherit="fix"): + """ + Updates all attributes of self related to input values + + Arguments + --------- + title : str, cow.text, or dictionary + This contains information for the top level patch's titles, which + can appear on the top (dictionary key: "top"/"t"), left + ("left"/"l"), right ("right"/"r"), or bottom ("bottom"/"b"). One + can have multiple titles (e.g. one on top and one on the left). + subtitle : str, cow.text, or dictionary + This contains information for the top level patch's subtitles + (and appear below the titles). Like `title`, these can appear on + the top (dictionary key: "top"/"t"), left ("left"/"l"), + right ("right"/"r"), or bottom ("bottom"/"b"). One can have + multiple subtitles (e.g. one on top and one on the left). + caption : str or cow.text + Each patch can have 1 caption which is located below all other + elements when the patch is configured (i.e. below all plots, + titles, subtitles and tags). + tags : list or tuple + list or tuple. If one wants to define each labels name by hand, + use a list. If you would like to define labels in an automated + manner relative to the the hierachical storage structure, use a + tuple. The length of the tuple, at max should be the max depth of + the arangement of plots. Each value in the tuple can either be + a string specifying the general auto-index structure ("1" or "0", + "a", "A", "i", "I") OR it can be a list that acts like the auto + ordering for the given level. + tags_format : str or text + Format of strings relative to levels of inserts. For example, if + we were using the automated labels and had a depth of 2, one might + see something like "Fig {0}.{1}" for this paramter. We could then + see the output of "Fig 2.1" if both tags liked like ("1","1") + or ("0","0"). The default, if tags on auto-constructed, is + ("{0}", "{0}.{1}", "{0}.{1}.{2}", "{0}.{1}.{2}.{3}", ...) + tags_order : str ["auto", "input", "yokogaki"] + How we the tags. If auto, the default is by "input" if you provide + your own labels and "yokogaki" if you don't. "Input" means that the + tags ordering will be assoicated with the grobs ordering. + "Yokogaki" means that the tags will be associated with the + top-to-bottom, left-to-right ordering of the grobs. + tags_loc : str ["top", "left", "right", "bottom"] + Location of the tags relative to grob they are tagging. + tags_inherit : str ["fix", "override"] + Indicates how tagging should behave relative to plotential plot + hierachy. If "fix", then the associated cowpatch object and inner + objects won't inherits tags from parents. If "override", + parent tagging structure will pass through *and* override tagging + desires for this level. + + Returns + ------- + None - internally updates self + + Note + ---- + Value of np.nan removes current value and none doesn't update + """ + + # process title ------------- + self.title = self._update_tdict_info(t = title, + current_t = self.title, + _type = "title") + + # process subtitle ------------ + self.subtitle = self._update_tdict_info(t = subtitle, + current_t = self.subtitle, + _type = "subtitle") + + # process caption ---------- + # basic update + if caption is np.nan: + self.caption = np.nan + elif caption is not None: + if inherits(caption, str): + caption = text(label = caption) + caption._define_type(_type = "cow_caption") + + self.caption = caption + + # process tag information ----------- + ## tags + if tags is np.nan: + self.tags = np.nan + elif not inherits(tags, list) and not inherits(tags, tuple) and tags is not None: + raise ValueError("tags should be either a list or tuple") + elif inherits(tags, list): + self.tags = (tags,) + elif tags is not None: + self.tags = tags + + ## tags_order + if tags_order is np.nan: + self.tags_order = "auto" + elif tags_order not in ["auto", "input", "yokogaki"]: + raise ValueError("tags_order can only take values None, \"auto\", "+\ + "\"yokogaki\", or \"input\" (or np.nan if you "+\ + "wish to revert to None with an update)") + else: + self.tags_order = tags_order + + if self.tags_order is None: + self.tags_order = "auto" + + ## tags_loc + if tags_loc is np.nan: + self.tags_loc = np.nan + elif tags_loc is not None: + self.tags_loc = tags_loc + + ## tags_inherit + if tags_inherit is np.nan: + self.tags_inherit = np.nan + elif tags_inherit is not None and (tags_inherit not in ["fix", "override"]): + raise ValueError("tags_inherit can only take values None, \"fix\" "+\ + "or \"override\" (or np.nan if you wish to revert "+\ + "to None with an update)") + elif tags_inherit is not None: + self.tags_inherit = tags_inherit + + if self.tags_inherit is None: # default value is fix + self.tags_inherit = "fix" + + # tags_format processing --------------------- + ## default tags_format + if tags_format is not None: # then we will be overriding... + if tags_format is np.nan: + if self.tags is None: + tags_format = np.nan + elif inherits(self.tags, list): + tags_format = ("{0}",) + else: + tags_format = tuple(_string_tag_format(x) + for x in np.arange(len(self.tags))) + elif (inherits(tags_format, str) or \ + inherits(tags_format, text)): + tags_format = (tags_format, ) + + ## making tags_format text objects + if tags_format is not None and tags_format is not np.nan: # if None/np.nan, then tags is also None + new_tags_format = [] + for e in tags_format: + if inherits(e, text): + e2 = copy.deepcopy(e) + e2._define_type(_type = "cow_tag") + new_tags_format.append(e2) + else: #e is string + e = text(label = e) + e._define_type(_type = "cow_tag") + new_tags_format.append(e) + new_tags_format = tuple(new_tags_format) + else: + new_tags_format = tags_format + + self._tags_format = new_tags_format + + # # tags_depth definition -------------------- + # if inherits(self.tags, list): + # self.tags_depth = 1 + # elif (self.tags_format is None or self.tags_format is np.nan) and \ + # (self.tags is None or self.tags is np.nan): + # self.tags_depth = -1 + # elif self.tags_format is None or self.tags_format is np.nan: + # self.tags_depth = len(self.tags) + # else: + # self.tags_depth = len(self.tags_format) + + + def _get_title(self, _type=["title", "subtitle", "caption"][0], + location="top"): + """ + obtain title/subtitle/caption with proper rotation + + Arguments + --------- + _type : str + type of title/subtitle/caption we're trying to return + location : str + if _type = "caption", then this isn't looked at, else + selected from ["top", "bottom", "left", "right"] + + Returns + ------- + cow.text object + cow.text object that has the desired rotation for the location + """ + if _type == "title": + et = self.title.get(location) + elif _type == "subtitle": + et = self.subtitle.get(location) + else: + return self.caption + + if location in ["left","right"] and et is not None: + et = et._additional_rotation(angle=90) + + return et + + + def _get_tag_full(self, index=(0,)): + """ + Create text of tag for given level and index (fully goes down) + + Arguments + --------- + index : tuple + tuple of integers that contain the relative level indices of the + desired tag. + + Returns + ------- + cow.text object for tag + + Notes + ----- + *IMPORTANT*: the `_get_tag` function is likely the function you actually + want to use! + + This function does process rotation expectation from `self.tag_loc` + attribute. + """ + if inherits(index, int): + index = (index,) + + if len(self.tags_format) < len(index): + raise ValueError("tags_format tuple has less indices than _get_tag index suggests") + + indices_used = [int(re.findall("[0-9]+", x)[0]) + for x in re.findall("\{[0-9]+\}", + self.tags_format[len(index)-1].label)] + if np.max(indices_used) > len(index)-1: + raise ValueError("tags_format has more indices than the tag hierarchy has.") + + if np.all([True if not inherits(self.tags[i], list) + else True if len(self.tags[i]) > x + else False for i, x in enumerate(index)]): + et = copy.deepcopy(self.tags_format[len(index)-1]) + et.label = et.label.format( + *[self._get_index_value(i,x) if not inherits(self.tags[i], list) + else self.tags[i][x] if len(self.tags[i]) > x + else "" for i,x in enumerate(index) + ]) + else: + et = text(label = "", _type = "cow_tag") + + + if self.tags_loc in ["left", "right"]: + et = et._additional_rotation(angle=90) + + + return et + + def _get_tag(self, index=0): + """ + Create text of tag for given level and index + + Arguments + --------- + index : integer + current index for desired tag + + Returns + ------- + cow.text object for tag + + Notes + ----- + If the annotation level is described with a list, then we + return an emtpy string, even if tags_format suggests otherwise. + + This function does process rotation expectation from `self.tag_loc` + attribute. + """ + + level_format = self.tags_format[0].label + indices_used = [int(re.findall("[0-9]+", x)[0]) + for x in re.findall("\{[0-9]+\}", + level_format)] + + if np.max(indices_used) > 0: + raise ValueError("current tags_format structure %s has more "+ + "indices than the level expected" % level_format) + + if self.tags is None: + return text(label = "", _type = "cow_tag") + + if inherits(self.tags[0], list): + if index < len(self.tags[0]): + index_string = self.tags[0][index] + else: + #index_string = "" + # in doesn't have index - return empty element + return text(label = "", _type = "cow_tag") + + else: + index_string = self._get_index_value(level=0,index=index) + + et = copy.deepcopy(self.tags_format[0]) + et.label = et.label.format(index_string) + + if self.tags_loc in ["left", "right"]: + et = et._additional_rotation(angle=90) + + return et + + + def _get_index_value(self, level=0, index=0): + """ + provide index level of a tag + + Arguments + --------- + level : int + level of depth of tag (0 is top level, 1 is first level below, etc.) + index : tuple + tuple of integers that contain the relative level indices of the + desired tag. + + Note + ---- + For developers: this function is overkill as we now create child + annotation objects so "level=0" should always be the case + """ + if len(self.tags) < level: + return "" + if inherits(self.tags[level], list): + if len(self.tags[level]) < index: + return "" + else: + return self.tags[level][index] + else: + return self._get_auto_index_value(index=index, + _type = self.tags[level]) + + def _get_auto_index_value(self, index=0, _type = ["0","1", "a", "A", "i","I"][0]): + """ + (Internal) get the index of a particular type of auto-index + + Arguments + --------- + index : int + 0-based index + _type : str + type of indexing + + Returns + ------- + out : str + index related to requested style + """ + + if _type == "0": + return str(index) + elif _type == "1": + return str(index+1) + elif _type == "a": + return _to_alphabet_representation(index+1) + elif _type == "A": + return _to_alphabet_representation(index+1, caps=True) + elif _type == "i": + return _to_roman_numerials(index+1) + elif _type == "I": + return _to_roman_numerials(index+1, caps=True) + else: + raise ValueError('type of auto-tags must be in '+\ + '["0","1", "a", "A", "i", "I"]') + + def _calculate_tag_margin_sizes(self, index=0, full_index=None, + fundamental=False, + to_inches=False): + """ + (Internal) calculate tag's margin sizes (all zeros if not actually + creating object) + + Arguments + --------- + index : int + integer of current level index (the last value of the full_index) + full_index : int or tuple + tuple of indices relative to the hierarchical ordering of the tag + fundamental : boolean + if the associated object being "tagged" is a fundamental object, + if not, a tag is only made if the tags_depth is at the final level. + to_inches : boolean + if the output should be converted to inches before returned + + Returns + ------- + dictionary with following keys/ objects + min_inner_width : float + minimum width required for title & subtitles on top or bottom + min_full_width : float + minium width required for caption (spans all of width). This + will always be zero for the tag structure + extra_used_width : float + extra width required for title & subtitles on left or right + min_inner_height : float + minimum height required for title & subtitles on left or right + extra_used_height : float + extra height required for title & subtitles on top or bottom + top_left_loc : tuple + tuple of top left corner of inner image relative to title text + + Notes + ----- + For developers: this function also returns the above dictionary with + all zeros if there will be no tag created (which is a function of the + `fundamental` argument and `full_index` argument) + + """ + # clean-up + if full_index is None: + full_index = (index, ) + + if not inherits(full_index, tuple): + full_index = (full_index, ) + + if (index is not None) and (index != full_index[-1]): + raise ValueError("structure between arguments `index` and "+ + "`full_index` disagree.") + + # if we shouldn't actually make the tag + if index is None or \ + (self.tags_depth != len(full_index) and not fundamental): + return {"min_inner_width": 0, + "min_full_width": 0, # not able to be nonzero for tag + "extra_used_width": 0, + "min_inner_height": 0, + "extra_used_height": 0, + "top_left_loc": (0,0) + } + + + # getting tag ------------------- + tag = self._get_tag(index=index) + tag_sizes = tag._min_size(to_inches=to_inches) + + min_inner_width = tag_sizes[0] * \ + (self.tags_loc in ["top", "bottom", "t", "b"]) + min_inner_height = tag_sizes[1] * \ + (self.tags_loc in ["left", "right", "l", "r"]) + + + extra_used_width = tag_sizes[0] * \ + (self.tags_loc in ["left", "right", "l", "r"]) + extra_used_height = tag_sizes[1] * \ + (self.tags_loc in ["top", "bottom", "t", "b"]) + + top_left_loc = ( + tag_sizes[0] * (self.tags_loc in ["left", "l"]), + tag_sizes[1] * (self.tags_loc in ["top", "t"]) + ) + + + return {"min_inner_width": min_inner_width, + "min_full_width": 0, # not able to be nonzero for tag + "extra_used_width": extra_used_width, + "min_inner_height": min_inner_height, + "extra_used_height": extra_used_height, + "top_left_loc": top_left_loc + } + + def _get_tag_and_location(self, width, height, + index=0, + full_index=None, + fundamental=False): + """ + create desired tag and identify location to place tag and associated + image + + Arguments + --------- + width : float + width in pt. If to small, request will auto-correct as this is + an internal function - the assessment should probably happen before + if you want to throw a warning. + height : float + height in pt. If to small, request will auto-correct as this is + an internal function - the assessment should probably happen before + if you want to throw a warning. + index : int + index of the tag. The annotation object should already capture + depth. + full_index : int or tuple + tuple of indices relative to the hierarchical ordering of the tag + fundamental : boolean + if the associated object being "tagged" is a fundamental object, + if not, a tag is only made if the tags_depth is at the final level. + + Return + ------ + tag_loc : tuple + upper left corner location for tag + image_loc : tuple + upper left corner location for image (associated with tag - NOT + THE TAG). If the tag is on the top, this means where the corner of + the image should be placed to correctly be below the tag. + tag_image : + tag text svg object + + Notes + ----- + TODO: This function does process rotation expectation from `self.tag_loc` + attribute. + """ + # clean-up + if full_index is None: + full_index = (index, ) + + if not inherits(full_index, tuple): + full_index = (full_index, ) + + if index != full_index[-1]: + raise ValueError("structure between arguments `index` and "+ + "`full_index` disagree.") + + + # if we shouldn't actually make the tag + if index is None or \ + (self.tags_depth != len(full_index) and not fundamental): + return None, None, None + + tag_image = self._get_tag(index=index) + + # if we shouldn't make the tag since it is an empty string + if tag_image.label == '': + return None, None, None + + if self.tags_loc in ["top", "bottom"]: + inner_width_pt = width + inner_height_pt = None + else: + inner_width_pt = None + inner_height_pt = height + + tag_image, size_pt = \ + tag_image._svg(width_pt=inner_width_pt, + height_pt=inner_height_pt) + + if self.tags_loc == "top": + tag_loc = (0,0) + image_loc = (0, size_pt[1]) + elif self.tags_loc == "left": + tag_loc = (0,0) + image_loc = (size_pt[0], 0) + elif self.tags_loc == "bottom": + tag_loc = (0, height - size_pt[1]) + image_loc = (0,0) + else: # self.tags_loc == "right": + tag_loc = (width - size_pt[0],0) + image_loc = (0,0) + + return tag_loc, image_loc, tag_image + + + + + def _calculate_margin_sizes(self, to_inches=False): + """ + (Internal) calculates marginal sizes needed to be displayed for titles + + Arguments + --------- + to_inches : boolean + if the output should be converted to inches before returned + + Returns + ------- + dictatiory with following keys/ objects + min_inner_width : float + minimum width required for title & subtitles on top or bottom + min_full_width : float + minium width required for caption (spans all of width) + extra_used_width : float + extra width required for title & subtitles on left or right + min_inner_height : float + minimum height required for title & subtitles on left or right + extra_used_height : float + extra height required for title & subtitles on top or bottom + top_left_loc : tuple + tuple of top left corner of inner image relative to title text + """ + min_inner_width = \ + np.sum([t._min_size(to_inches=to_inches)[0] + for t in + [self._get_title(_type="title", location="top"), + self._get_title(_type="title", location="bottom"), + self._get_title(_type="subtitle", location="top"), + self._get_title(_type="subtitle", location="bottom")] + if t is not None] + [0]) + + min_full_width = \ + np.sum([t._min_size(to_inches=to_inches)[0] + for t in + [self._get_title(_type="caption")] + if t is not None] + [0]) + + extra_used_width = \ + np.sum([t._min_size(to_inches=to_inches)[0] + for t in + [self._get_title(_type="title",location="left"), + self._get_title(_type="title",location="right"), + self._get_title(_type="subtitle", location="left"), + self._get_title(_type="subtitle", location="right")] + if t is not None]+ [0]) + + min_inner_height = \ + np.sum([t._min_size(to_inches=to_inches)[1] + for t in + [self._get_title(_type="title",location="left"), + self._get_title(_type="title",location="right"), + self._get_title(_type="subtitle",location="left"), + self._get_title(_type="subtitle",location="right")] + if t is not None] +[0]) + + extra_used_height = \ + np.sum([t._min_size(to_inches=to_inches)[1] + for t in + [self._get_title(_type="title",location="top"), + self._get_title(_type="title",location="bottom"), + self._get_title(_type="subtitle",location="top"), + self._get_title(_type="subtitle",location="bottom"), + self._get_title(_type="caption")] + if t is not None] + [0]) + + top_left_loc = ( + np.sum([t._min_size(to_inches=to_inches)[0] + for t in + [self._get_title(_type="title",location="left"), + self._get_title(_type="subtitle",location="left")] + if t is not None] + [0]), + np.sum([t._min_size(to_inches=to_inches)[1] + for t in + [self._get_title(_type="title",location="top"), + self._get_title(_type="subtitle",location="top")] + if t is not None] + [0]) + + ) + + return {"min_inner_width": min_inner_width, + "min_full_width": min_full_width, + "extra_used_width": extra_used_width, + "min_inner_height": min_inner_height, + "extra_used_height": extra_used_height, + "top_left_loc": top_left_loc + } + + + def _get_titles_and_locations(self, width, height): + """ + (Internal) Create title objects and locations to be placed in image + + Arguments + --------- + width : float + width of overall image (in pt) + height : float + height of overall image (in pt) + + Returns + ------- + out_list : list + list of tuples of the location to place the title (top left corner) + and the image of the title itself. The list has entries for any + titles, then any subtitles and then the caption (if any). Each + entry is a tuple with (1) a tuple of the top left corner location + for the title and (2) a tuple of (a) the svg object of the title + itself and (b) a tuple of the size of the svg object in pt + + Notes + ----- + Here's a visual diagram that helped define this function + + + ~~~~ width ~~~~~ + =====b'===== (width - b) + =b= (width of 7 and 8) + =a= (width of 1 and 2) + a1 (width of 1) + + b_1 (width of 7) + ~/*' | 33333 | + ~/* | 44444 | + ~/ ---+-------+--- + ~/ 1 2| |7 8 + ~/ 1 2| |7 8 + ~/ 1 2| |7 8 + ~/ ---+-------+--- + ~ %$ | 55555 | + ~ % | 66666 | + ~ & 999999999999999 + + ~ : height + / : ii' (height - ii - iii_1) + * : i (height of 3 and 4) + ' : i_1 (height of 3) + % : ii (height of 5 and 6) + $ : ii_1 (height of 5) + & : iii_1 (height of 9) + + + + where + 1 : title, left + 2 : subtitle, left + 3 : title, top + 4 : subtitle, top + 5 : title, bottom + 6 : subtitle, bottom + 7 : title, right + 8 : subtitle, right + 9 : caption + """ + # TODO: testing (likely just do a complex image) + # TODO: make sure the pt vs inch question is settled + + # minimum size of each object + title_min_size_dict = {key : [t._min_size() if t is not None else (0,0) + for t in [self._get_title(_type="title",location=key), + self._get_title(_type="subtitle",location=key)]] + for key in ["top", "bottom", "left", "right"]} + + if self.caption is not None: + title_min_size_dict["caption"] = self._get_title(_type="caption")._min_size() + else: + title_min_size_dict["caption"] = (0,0) + + + # shifts for top left positioning + shift_horizontal = { + # same value + ("title", "top") : np.sum([tu[0] for tu in title_min_size_dict["left"]]), + ("subtitle", "top") : np.sum([tu[0] for tu in title_min_size_dict["left"]]), + ("title", "bottom") : np.sum([tu[0] for tu in title_min_size_dict["left"]]), + ("subtitle", "bottom") : np.sum([tu[0] for tu in title_min_size_dict["left"]]), + + "caption" : 0, + + ("title", "left") : 0, + ("subtitle", "left") : title_min_size_dict["left"][0][0], + ("title", "right") : width - np.sum([tu[0] for tu in title_min_size_dict["right"]]), + ("subtitle", "right") : width - title_min_size_dict["right"][1][0] + } + + shift_vertical = { + # same value + ("title", "left") : np.sum([tu[1] for tu in title_min_size_dict["top"]]), + ("subtitle", "left") : np.sum([tu[1] for tu in title_min_size_dict["top"]]), + ("title", "right") : np.sum([tu[1] for tu in title_min_size_dict["top"]]), + ("subtitle", "right") : np.sum([tu[1] for tu in title_min_size_dict["top"]]), + + "caption" : height - title_min_size_dict["caption"][1], + + ("title", "top") : 0, + ("subtitle", "top") : title_min_size_dict["top"][0][1], + ("title", "bottom") : width - np.sum([tu[1] for tu in title_min_size_dict["bottom"]]) -\ + title_min_size_dict["caption"][1], + ("subtitle", "bottom") : width - title_min_size_dict["bottom"][1][1] -\ + title_min_size_dict["caption"][1] + } + + # sizes to create each title element with + inner_width = width - np.sum([tu[0] for tu in title_min_size_dict["left"] +\ + title_min_size_dict["right"]]) + inner_height = height - np.sum([tu[1] for tu in title_min_size_dict["top"] +\ + title_min_size_dict["bottom"]]) + + size_request = { + ("title","top") : (inner_width, None), + ("title","bottom") : (inner_width, None), + ("title","left") : (None, inner_height), + ("title","right") : (None, inner_height), + ("subtitle","top") : (inner_width, None), + ("subtitle","bottom") : (inner_width, None), + ("subtitle","left") : (None, inner_height), + ("subtitle","right") : (None, inner_height), + "caption" : (width, None) + } + + + out_list = [] + out_list += [ ( (shift_horizontal[("title",key)], \ + shift_vertical[("title",key)]), \ + self._get_title(_type="title", location=key).\ + _svg(width_pt = size_request[("title", key)][0], + height_pt = size_request[("title", key)][1]) + ) for key in ["top", "bottom", "left", "right"] + if self._get_title(_type="title",location=key) is not None] + + out_list += [ ( (shift_horizontal[("subtitle",key)], \ + shift_vertical[("subtitle",key)]), \ + self._get_title(_type="subtitle",location=key).\ + _svg(width_pt = size_request[("subtitle", key)][0], + height_pt = size_request[("subtitle", key)][1]) + ) for key in ["top", "bottom", "left", "right"] + if self._get_title(_type="subtitle",location=key) is not None] + + out_list += [((shift_horizontal[key], shift_vertical[key]), \ + self._get_title(_type="caption").\ + _svg(width_pt = size_request[key][0], \ + height_pt = size_request[key][1]) + ) for key in ["caption"] if + self._get_title(_type="caption") is not None] + + + return out_list + + + + + + + def _update_tdict_info(self, t, current_t = dict(), _type = "title"): + """ + Update or define title/subtitle + + Arguments + --------- + t : str, cow.text, or dictionary + This contains information for the top level patch's titles or subtitles, + which can appear on the top (dictionary key: "top"/"t"), left + ("left"/"l"), right ("right"/"r"), or bottom ("bottom"/"b"). One can + have multiple titles (e.g. one on top and one on the left). + current_t : dictionary + current title/subtitle dictionary to be updated. + _type : str ["title", "subtitle"] + string indicates if the t is a title or subtitle. + + Returns + ------- + an updated version of `current_t`. + + Notes: + ------ + A value of "np.nan" with remove any existing values, and a value of "None" + with do nothing to the current value. If one is trying to remove the "top" + title, but not all titles, they must use the dictionary structure (i.e. + "t = {top: np.nan}". + """ + + current_t = copy.deepcopy(current_t) + + if inherits(t, str): + t = {"top": copy.deepcopy(t)} + + if inherits(t, text): + t = {"top": copy.deepcopy(t)} + + if t is np.nan: + current_t = np.nan + t = None + + if t is not None: + if not inherits(t, dict): + raise ValueError(_type + " type isn't one of the expected types") + else: + text_keys = {"t":"top", "l":"left", "r":"right", "b":"bottom"} + for key, el in t.items(): + if key in text_keys.keys(): + key = text_keys[key] + if key not in text_keys.values(): + raise ValueError(_type +\ + (" dictionary key is not as expected (%s)" % key)) + + if el is np.nan: # remove current element if new element value is np.nan + current_val = current_t.get(key) + if current_val is not None: + current_t.pop(key) + elif el is not None: + if inherits(el, str): + el = text(label = el) + el._define_type(_type = "cow_" + _type) + + current_t[key] = el + + + return current_t + + + + def _step_down_tags_info(self, parent_index): + """ + Create an updated version of tags_info for children + + Arguments + --------- + parent_index : int + integer associated with the child's tag index + + Returns + ------- + annotation with all tag attributes to update for children's + annotation. + + Notes + ----- + For developers: we step down both the the `tags` and `tags_format` + parameters of the annotation object. + """ + if self.tags is None or \ + len(self.tags) <= 1 or \ + len(self.tags_format) <= 1: + return annotation() + + tags = self.tags[1:] + + # updating tags_format + old_tags_format = self.tags_format[1:] + + new_tags_format = [] + new_fillers = ["{"+str(i)+"}" for i in range(len(tags))] + + for t in old_tags_format: + new_t = copy.deepcopy(t) + current_index = self._get_index_value(0, parent_index) + new_t.label = new_t.label.format(current_index, *new_fillers) + + new_tags_format.append(new_t) + + tags_format = tuple(new_tags_format) + + inner_annotation = annotation(tags = tags, + tags_format = tags_format, + tags_order = self.tags_order, + tags_loc = self.tags_loc) + return inner_annotation + + + def inheritance_type(self): + return self.tags_inherit + + + def __add__(self, other): + """ + update annotations through addition + + Arguments + --------- + other : annotation object + object to update current annotation object with + + Returns + ------- + updated self (annotation object) + """ + if not inherits(other, annotation): + raise ValueError("annotation can be only added with "+\ + "another annotation") + + self._update_all_attributes(title = other.title, + subtitle = other.subtitle, + caption = other.caption, + tags=other.tags, + tags_format=other._tags_format, + tags_order=other.tags_order, + tags_loc=other.tags_loc, + tags_inherit=other.tags_inherit) + + return self + + def __eq__(self, other): + """ + checks if two annotation objects are equal + + Arguments + --------- + other : annotation object + object to update current annotation object with + + Returns + ------- + boolean for equality (if second object is not an annotation object, + will return False) + """ + if not inherits(other, annotation): + return False + + return self.__dict__ == other.__dict__ + + + diff --git a/src/cowpatch/base_elements.py b/src/cowpatch/base_elements.py index 0fceb67..3cdaf22 100644 --- a/src/cowpatch/base_elements.py +++ b/src/cowpatch/base_elements.py @@ -2,16 +2,20 @@ import plotnine as p9 import matplotlib.pyplot as plt import svgutils.transform as sg +import copy from .svg_utils import gg_to_svg, _save_svg_wrapper, _show_image, \ _raw_gg_to_svg, _select_correcting_size_svg, \ - _uniquify_svg_safe + _add_to_base_image, _uniquify_svg_safe from .utils import to_inches, from_inches, inherits_plotnine, inherits, \ - _flatten_nested_list + _flatten_nested_list, _overall_scale_recommendation_patch from .layout_elements import layout +from .annotation_elements import annotation from .config import rcParams +from .text_elements import text import copy +import pdb class patch: def __init__(self, *args, grobs=None): @@ -115,7 +119,8 @@ def __init__(self, *args, grobs=None): else: self.grobs = grobs - self.__layout = "patch" # this is different than None... + self._layout = "patch" # this is different than None... + self._annotation = None @property def layout(self): @@ -124,24 +129,42 @@ def layout(self): object or the default ``layout`` if no layout has been explicitly defined """ - if self.__layout == "patch": + if self._layout == "patch": if len(self.grobs) < 4: return layout(nrow = len(self.grobs), ncol = 1) else: num_grobs = len(self.grobs) nrow = int(np.ceil(np.sqrt(num_grobs))) - ncol = int(np.ceil(len(self.grobs) / nrow)) + ncol = int(np.ceil(num_grobs / nrow)) return layout(nrow=nrow, ncol=ncol) else: - return self.__layout + return self._layout + + @property + def annotation(self): + """ + defines ``annotation`` that either returns the most up-to-date + ``cow.annotation`` object or the default ``annotation`` if no + annotation has been explicitly defined + + Details + ------- + if no annotation is provide, we do make sure that + "tags_inherit='override'" + """ + if self._annotation is None: + return annotation(tags_inherit="override") + else: + return self._annotation + def _check_layout(self): """ checks layout if design matrix is fulled defined """ - if self.layout.num_grobs is not None: - if self.layout.num_grobs != len(self.grobs): + if (self.layout.num_grobs is not None) and \ + (self.layout.num_grobs != len(self.grobs)): raise AttributeError("layout's number of patches does not "+ "matches number of patches in arangement") @@ -199,286 +222,1026 @@ def __add__(self, other): raise ValueError("currently not implimented addition with another patch object") elif inherits(other, layout): # combine with layout ------------- - new_obj = copy.deepcopy(self) - new_obj.__layout = other - return new_obj + object_copy = copy.deepcopy(self) + object_copy._layout = other + + return object_copy elif inherits(other, annotation): - raise ValueError("currently not implimented addition with annotation") + object_copy = copy.deepcopy(self) + + if self.annotation is None: + other_copy = copy.deepcopy(other) + other_copy._clean_up_attributes() + object_copy._annotation = other_copy + else: + final_copy = copy.deepcopy(self.annotation + other) + final_copy._clean_up_attributes() + object_copy._annotation = final_copy - return self + return object_copy def __mul__(self, other): raise ValueError("currently not implimented *") - def __and__(self, other): - raise ValueError("currently not implimented &") - def _svg(self, width_pt, height_pt, sizes=None, num_attempts=None, _u_idx=None): + def _get_grob_tag_ordering(self, cur_annotation=None): """ - Internal function to create an svg representation of the patch + get ordering of tags related to grob index Arguments --------- - width_pt : float - desired width of svg object in points - height_pt : float - desired height of svg object in points + cur_annotation : annotation object + annotation object if we are not going to use the patch's own + annotation object but some alternation from a parent. If this + is None, we shall be using the internal annotation object + associated with the patch. Returns ------- - svg_object : ``svgutils.transforms`` object + numpy array of tag order for each grob - See also - -------- - svgutils.transforms : pythonic svg object + Note + ---- + This function leverages the patch's layout and annotation objects """ - self._check_layout() - - if num_attempts is None: - num_attempts = rcParams["num_attempts"] + if cur_annotation is None: + cur_annotation = self.annotation - # prep index tracking - # ------------------- - if _u_idx is None: - _u_idx = str(self.__hash__()) + self._check_layout() + if cur_annotation.tags is None or cur_annotation.tags[0] is None: + return np.array([None]*len(self.grobs)) - # examine if sizing is possible and update or error if not - # -------------------------------------------------------- - if sizes is None: # top layer - #pdb.set_trace() - while num_attempts > 0: - sizes, logics = self._svg_get_sizes(width_pt=width_pt, - height_pt=height_pt) - out_info = self._process_sizes(sizes, logics) + tags_order = cur_annotation.tags_order + if tags_order == "auto": + if inherits(cur_annotation.tags[0],list): + tags_order = "input" + else: + tags_order = "yokogaki" - if type(out_info) is list: - num_attempts = -412 # strictly less than 0 - else: # out_info is a scaling - width_pt = width_pt*out_info - height_pt = height_pt*out_info + if tags_order == "yokogaki": + out_array = self.layout._yokogaki_ordering(num_grobs = len(self.grobs)) + elif tags_order == "input": + out_array = np.arange(len(self.grobs)) + else: + raise ValueError("patch's annotation's tags_order is not an expected option") + + # list correction (length of output) + if cur_annotation is not None and \ + inherits(cur_annotation.tags[0], list) and \ + len(cur_annotation.tags[0]) < len(self.grobs): + out_array = np.array(out_array, dtype = object) # to allow for None + out_array[out_array > len(cur_annotation.tags[0])-1] = None + + return out_array + + + + # def _svg(self, width_pt, height_pt, sizes=None, num_attempts=None): + # """ + # Internal function to create an svg representation of the patch + + # Arguments + # --------- + # width_pt : float + # desired width of svg object in points + # height_pt : float + # desired height of svg object in points + # sizes: TODO: write description + # num_attempts : TODO: write description + + # Returns + # ------- + # svg_object : ``svgutils.transforms`` object + + # See also + # -------- + # svgutils.transforms : pythonic svg object + # """ + + # self._check_layout() + + # # examine if sizing is possible and update or error if not + # # -------------------------------------------------------- + # if sizes is None: # top layer + # sizes = self._svg_get_sizes(width_pt, height_pt) + + + # layout = self.layout + + # areas = layout._element_locations(width_pt=width_pt, + # height_pt=height_pt, + # num_grobs=len(self.grobs)) + + # base_image = sg.SVGFigure() + # base_image.set_size((str(width_pt)+"pt", str(height_pt)+"pt")) + # # add a view box... (set_size doesn't correctly update this...) + # # maybe should have used px instead of px.... + # base_image.root.set("viewBox", "0 0 %s %s" % (str(width_pt), str(height_pt))) + + # # TODO: way to make decisions about the base image... + # # TODO: this should only be on the base layer... + # base_image.append( + # sg.fromstring("")) + + # for p_idx in np.arange(len(self.grobs)): + # inner_area = areas[p_idx] + # # TODO: how to deal with ggplot objects vs patch objects + # if inherits(self.grobs[p_idx], patch): + # inner_width_pt, inner_height_pt = inner_area.width, inner_area.height + # inner_svg, _ = self.grobs[p_idx]._svg(width_pt = inner_width_pt, + # height_pt = inner_height_pt, + # sizes = sizes[p_idx]) + # elif inherits_plotnine(self.grobs[p_idx]): + # inner_gg_width_in, inner_gg_height_in = sizes[p_idx] + # inner_svg = _raw_gg_to_svg(self.grobs[p_idx], + # width = inner_gg_width_in, + # height = inner_gg_height_in, + # dpi = 96) + # else: + # raise ValueError("grob idx %i is not a patch object nor"+\ + # "a ggplot object within patch with hash %i" % p_idx, self.__hash__) + + + # inner_root = inner_svg.getroot() + + # inner_root.moveto(x=inner_area.x_left, + # y=inner_area.y_top) + # base_image.append(inner_root) + + # return base_image, (width_pt, height_pt) + + # def _estimate_default_min_desired_size(self, parent_index=()): + # """ + # (Internal) Estimate default height and width of full image so that + # inner objects (plots, text, etc.) have a minimum desirable size + # and all titles are fully seen. + + # Arguments + # --------- + # parent_index : int + # parent level index information + + # Returns + # ------- + # suggested_width : float + # proposed width for overall size (inches) + # suggested_height : float + # proposed height for overall size (inches) + + # Notes + # ----- + # ***Function's logic***: + # In order to propose a minimum sizing to an arrangement we take two + # things into account. Specially, + + # 1. we assure that each plot's height & width are greater then or equal + # to a minimum plot height and width (For text objects treated as plots, + # we just assure they are not truncated, with no minimize sizing is + # enforced). + # 2. and for titles (,subtitles, captions,) and tags, we ensure that + # they will not be truncated based on the sizing of the plot/arrangement + # they are associated with. + + # To accomplish this second goal, we track different dimensions of these + # text labeling objects. For clarity, define (1) a text object’s + # *typographic length* as the maximum width of all lines of and with + # each line’s text’s baseline being horizontal and a text object’s + # *typographic overall height* as the distance between the top “line” of + # text’s ascender line and the bottom "line" of text's descender line + # `reference `_, + # and (2) a *bounding box* for any text object is the minimum sized box + # that can contain the text object. This bounding box has a *height* and + # *width* which is naturally impacted by the orientation of baseline of + # the text. + + + # For titles (,subtitles, captions,) and tags of tag_type = 0 (aka the + # tag is drawn outside the underlying plot/arrangement's space), we do + # the following: + + # A. we make sure the associated plot/arangement’s requested height and + # weight ensures that the text object's *typographic length* is not + # truncated. To do so we assure that the plot-specific height and width + # being larger than max(image_min_height_from_objective_1, select_bbox_heights)) + # and max(image_min_width_from_objective_1,select_bbox_widths)). A text + # element contributes to the select_bbox_heights if it has left or right + # alignment and to select_bbox_widths if it has top or bottom alignment. + # B. we make sure the "global" location for said inner plot/arrangement + # and text greater than or equal to the the plot’s minimum sizing + # defined in (A) plus a selected_cumulative_extra_bbox_height and _width. + # A text element contributes to the selected_cumulative_extra_bbox_height + # if it has top or bottom alignment and to + # selected_cumulative_extra_bbox_width if it has left or right + # alignment. Note that different types of titles and tags contribute to + # this valve in an addition sense. + + # If a tag has tag_type=1, then all it's bounding box’s attributes + # contribute to the dimensions described in (A). + + # ***Additional info:*** + # The default rcParams are (in inches): + # base_height = 3.71, + # base_aspect_ratio = 1.618 # the golden ratio + + # The idea of a minimum size for a subplot follows ideas proposed in + # cowplot: `wilkelab.org/cowplot/reference/save_plot.html `_. + + # """ + + # min_image_height = rcParams["base_height"] + # min_image_width = rcParams["base_aspect_ratio"] * min_image_height + + # raise ValueError("TODO: correct update to _calculate_margin_sizes") + # ## dealing with titles + # if self.annotation is None: + # min_desired_widths = [] + # extra_desired_widths = [] + # min_desired_heights = [] + # extra_desired_heights = [] + # else: + # min_desired_widths, extra_desired_widths, \ + # min_desired_heights, extra_desired_heights = \ + # self.annotation._calculate_margin_sizes(to_inches=True) + + # # inner objects + # areas = self.layout._element_locations(width_pt=1, + # height_pt=1, + # num_grobs = len(self.grobs)) + + # if (self.annotation is not None) and \ + # (self.annotation.order_type() == "yokogaki"): + # grob_tag_ordering = self.layout._yokogaki_ordering( + # num_grobs=len(self.grobs)) + # else: + # grob_tag_ordering = np.arange(len(self.grobs)) + + + # for g_idx in np.arange(len(self.grobs)): + # inner_area = areas[g_idx] + # current_index = tuple(list(parent_index) + \ + # [grob_tag_ordering[g_idx]]) + # ### tag sizing ----------- + # if self.annotation is not None: + # tag_min_desired_widths, tag_extra_desired_widths, \ + # tag_min_desired_heights, tag_extra_desired_heights = \ + # self.annotation._calculate_tag_margin_sizes( + # index = current_index, + # fundamental = inherits(self.grobs[g_idx], patch), + # to_inches=True) + # else: + # tag_min_desired_widths, tag_extra_desired_widths, \ + # tag_min_desired_heights, tag_extra_desired_heights = [0],[0],[0],[0] + + + # ### object sizing ------------- + # if inherits_plotnine(self.grobs[g_idx]): + # inner_width = min_image_width + # inner_height = min_image_height + # elif inherits(self.grobs[g_idx], text): + # inner_width, inner_height = self.grobs[g_idx]._min_size(to_inches=True) + # else: # patch object... + # inner_width, inner_height = \ + # self.grobs[g_idx]._estimate_default_min_desired_size(parent_index=current_index) + + + # min_desired_widths.append((np.max([inner_width]+tag_min_desired_widths) +\ + # tag_extra_desired_widths)*\ + # 1/inner_area.width) + # min_desired_heights.append((np.max([inner_height]+tag_min_desired_heights) +\ + # tag_extra_desired_heights) *\ + # 1/inner_area.height) + + + # # wrap it all up (zeros added to allow for empty sets) + # min_desired_width = np.max(min_desired_widths+[0]) +\ + # np.sum(extra_desired_widths+[0]) + # min_desired_height = np.max(min_desired_heights+[0]) +\ + # np.sum(extra_desired_heights+[0]) + + # return min_desired_width, min_desired_height + + # def _default_size(self, width, height): + # """ + # (Internal) obtain default recommended size of overall image if width or + # height is None + + # Arguments + # --------- + # width : float + # width of output image in inches (this should actually be associated + # with the svg...) + # height : float + # height of svg in inches (this should actually be associated + # with the svg...) + + # Returns + # ------- + # width : float + # returns default width for given object if not provided (else just + # returns provided value). If only height is provided then width + # proposed is relative to a default aspect ratio for the object. + # height : float + # returns default height for given object if not provided (else just + # returns provided value). If only width is provided then height + # proposed is relative to a default aspect ratio for the object. + # """ + # both_none = False + # if width is None or height is None: + # _width, _height = self._estimate_default_min_desired_size()#self._size_dive() + # if width is None and height is None: + # both_none = True + # width = _width + # height = _height + # elif width is not None: + # height = _height / _width * width + # else: + # width = _width / _height * height + # return width, height + + # def _inner_svg_get_sizes(self, width_pt, height_pt, tag_parent_index=()): + # """ + # (Internal) Calculates required sizes for plot objects to meet required + # sizes and logics if the requested sizing was possible + + # Arguments + # --------- + # width_pt : float + # overall width of the image in points + # height_pt : float + # overall height of the image in points + # tags_parent_index : tuple + # tuple of integers that contain the relative level indices of the + # desired tag for parent. + + # Returns + # ------- + # sizes : nested list + # For each element in the patch (with nesting structure in the list), + # this contains width, height tuples that either capture the size + # to request the ggplot object to be to return the actual desired + # size (see gg_to_svg notes), OR it contains the fraction defined by + # the requested width (or height) w.r.t. the returned width + # (or height) from saving the plotnine object. The later option + # occurs when when the ggplot's sizing didn't converge to the + # desired size. These sizing do not contain information about + # annotations (but implicitly reflect them existing) + # logics : nested list + # For each element in the patch (with nesting structure in the list), + # this contains a boolean value if the ggplot object was able to + # be correctly size. + # sizes_text : nested list + # logics_text : nested list + + # Notes + # ----- + # Internally this function uses rcParams's ``eps``, ``mini_size_px`` and + # ``maxIter`` to determine the parameters to be put in + # .svg_utils._select_correcting_size_svg. + # """ + # layout = self.layout + + # areas = layout._element_locations(width_pt=width_pt, + # height_pt=height_pt, + # num_grobs=len(self.grobs)) + + # sizes = [] + # logics = [] + # sizes_annotation = [] + # logics_annotation = [] + + # for p_idx in np.arange(len(self.grobs)): + # inner_area = areas[p_idx] + # inner_width_pt = inner_area.width + # inner_height_pt = inner_area.height + + # if inherits(self.grobs[p_idx], patch): + # inner_sizes_list, logic_list = \ + # self.grobs[p_idx]._inner_svg_get_sizes(width_pt = inner_width_pt, + # height_pt = inner_height_pt) + # sizes.append(inner_sizes_list) + # logics.append(logic_list) + # elif inherits_plotnine(self.grobs[p_idx]): + # inner_w, inner_h, inner_logic = \ + # _select_correcting_size_svg(self.grobs[p_idx], + # width=to_inches(inner_width_pt, + # units="pt", + # dpi=96), + # height=to_inches(inner_height_pt, + # units="pt", + # dpi=96), + # dpi=96, + # eps=rcParams["eps"], + # min_size_px=rcParams["min_size_px"], + # maxIter=rcParams["maxIter"], + # throw_error=False) + # sizes.append((inner_w,inner_h)) + # logics.append(inner_logic) + # elif inherits(self.grobs[p_idx], text): + # raise ValueError("TODO!") + # else: + # raise ValueError("grob idx %i is not a patch object nor"+\ + # "a ggplot object" % p_idx) + + # return sizes, logics + + # def _svg_get_sizes(self, width_pt, height_pt): + # """ + # captures backend process of making sure the sizes requested can be + # provided with the actual images... + # """ + + # if num_attempts is None: + # num_attempts = rcParams["num_attempts"] + + # while num_attempts > 0: + # sizes, logics = self._svg_get_sizes(width_pt=width_pt, + # height_pt=height_pt) + # out_info = self._process_sizes(sizes, logics) + + # if type(out_info) is list: + # num_attempts = -412 # strictly less than 0 + # else: # out_info is a scaling + # width_pt = width_pt*out_info + # height_pt = height_pt*out_info + + # num_attempts -= 1 + + # if num_attempts == 0: + # raise StopIteration("Attempts to find the correct sizing of inner"+\ + # "plots failed with provided parameters") + + # return sizes + + # def _process_sizes(self, sizes, logics): + # """ + # (Internal) draw conclusions about the output of _svg_get_sizes. + + # This function assesses if any internal ggplot object failed to + # produce an image of the requested size (w.r.t. to given interation + # parameters) + + # Arguments + # --------- + # sizes : nested list + # sizes output of _svg_get_sizes. This is a nested list with + # (width,height) or (requested_width/actual_width, + # requested_height/actual_width), with the different being if the + # ggplot object was able to be presented in the requested size + # logics : nested list + # logics output of _svg_get_sizes. This is a nested list of boolean + # values w.r.t. the previous different. + + # Returns + # ------- + # sizes : nested list + # Returned if no logics values are False. This object is the sizes + # nested list that was input. + # max_scaling : float + # Returned if at least one of the logics values are False. This is + # the scaling of the original ``width_pt`` and ``height_pt`` that + # defined the sizes and logics that could make the requested sizes + # for all ggplots that failed to be correctly sized be at least as + # large as the returned size from a basic ggplot saving. + # """ + # flatten_logics = _flatten_nested_list(logics) + # if np.all(flatten_logics): + # return sizes + + # # else suggest a rescale: + # flatten_sizes = _flatten_nested_list(sizes) + # bad_sizing_info = [flatten_sizes[i] for i in range(len(flatten_sizes)) + # if not flatten_logics[i]] + + # width_scaling = 1/np.min([sizing[0] for sizing in bad_sizing_info]) + # height_scaling = 1/np.min([sizing[1] for sizing in bad_sizing_info]) + + # max_scaling = np.max([width_scaling, height_scaling]) + # # ^ keeping aspect ratio with a single scaling + + # return max_scaling + + + def _svg(self, width=None, height=None, data_dict=None): + return self._hierarchical_general_process(width, height, data_dict, + approach="create") + + def _default_size(self, data_dict=None): + """ + calculate the default size for this patch arrangement based on + cow.rcParams that define minimum sizing of figures. This also takes + into account the true required sizes to present titles & tags - num_attempts -= 1 + Arguments + --------- + data_dict : dictionary + This dictionary is used to pass information from parents to + children to help with the overall calculation of the full default + size. + + Specifically, `data_dict` can have an attribute + `"default-size-proportion"`. If this attribute exists, it should + be a tuple with values between (0,1]. These values capture the + relative size of this child's shape to the overall parent we + are getting the default size for. + + Additionally, this dictionary can store things for the tag creation + and sizing, including the attribute + `"parent-guided-annotation-update"`. This is the parent's + annotation object. If the current level's `annotation` has + `tags_inherit="override"` the annotation tag structure will be + taken from the parent. - if num_attempts == 0: - raise StopIteration("Attempts to find the correct sizing of inner"+ - "plots failed with provided parameters") + Returns + ------- + default_size : tuple + default size of the image (in inches) to respect minimum sizing + desires as defined by cow.rcParams & title & tag sizes. - layout = self.layout + Details + ------- + *For code explorers*: this function wraps around + `_hierarchical_general_process`. To understand the underlying code + see that function (and potentially read the docstring before you do + a deep dive). - areas = layout._element_locations(width_pt=width_pt, - height_pt=height_pt, - num_grobs=len(self.grobs)) + """ + return self._hierarchical_general_process(width=None, + height=None, + data_dict=data_dict, + approach="default-size") - base_image = sg.SVGFigure() - base_image.set_size((str(width_pt)+"pt", str(height_pt)+"pt")) - # add a view box... (set_size doesn't correctly update this...) - # maybe should have used px instead of px.... - base_image.root.set("viewBox", "0 0 %s %s" % (str(width_pt), str(height_pt))) + def _doable_size(self, width=None, height=None, data_dict=None): + """ + calculates a doable size for all grobs, including nested grobs + within inner patch objects & assesses "doablity" at the give size - # TODO: way to make decisions about the base image... - base_image.append( - sg.fromstring("")) + Arguments + --------- + width : float + inches (TODO - write description) + height : float + inches (TODO - write description) + data_dict : dict + dictionary of inner storage (TODO - write description) - for p_idx in np.arange(len(self.grobs)): - inner_area = areas[p_idx] - inner_u_idx = _u_idx + "_" + str(p_idx) - # TODO: how to deal with ggplot objects vs patch objects - if inherits(self.grobs[p_idx], patch): - inner_width_pt, inner_height_pt = inner_area.width, inner_area.height - inner_svg, _ = self.grobs[p_idx]._svg(width_pt = inner_width_pt, - height_pt = inner_height_pt, - sizes = sizes[p_idx], - _u_idx = inner_u_idx) - elif inherits_plotnine(self.grobs[p_idx]): - inner_gg_width_in, inner_gg_height_in = sizes[p_idx] - inner_svg = _raw_gg_to_svg(self.grobs[p_idx], - width = inner_gg_width_in, - height = inner_gg_height_in, - dpi = 96) - inner_svg = _uniquify_svg_safe(inner_svg, inner_u_idx) + Additionally, this dictionary can store things for the tag creation + and sizing, including the attribute + `"parent-guided-annotation-update"`. This is the parent's + annotation object. If the current level's `annotation` has + `tags_inherit="override"` the annotation tag structure will be + taken from the parent. - else: - raise ValueError("grob idx %i is not a patch object nor"+ - "a ggplot object within patch with hash %i" % p_idx, self.__hash__()) + Returns + ------- + sizes_list : (potentially nested) list + if doablity == 1, then this provides tuples of sizes (in pt) for + each element base element, which nested structure for patch + wrapping. This doesn't contain sizing for titles & tags but does + take them into consideration. + if doablity == 1, TODO - write description + doability: boolean + 0/1 boolean if requested width and height are doable for the + overarching image (TODO: make this a true boolean?) - inner_root = inner_svg.getroot() - inner_root.moveto(x=inner_area.x_left, - y=inner_area.y_top) - base_image.append(inner_root) + """ + return self._hierarchical_general_process(width, height, data_dict, + approach="size") - return base_image, (width_pt, height_pt) - def _size_dive(self, parents_areas=None): + def _hierarchical_general_process(self, + width=None, height=None, data_dict=None, + approach=["size", "create", "default-size"][0]): """ - (Internal) calculate a suggested overall size that ensures a minimum - width and height of the smallest inner plot + (Internal) Process hierarchical patchwork structure to (1) save/show, + (2) estimate needed size input, (3) and define default size Arguments --------- - parents_areas : list - list of parent's/parents' areas. If value is None it means element - has no parents - - Returns + width: float + outcome svg's width requested (in inches). Can be None. + height: float + outcome svg's height requested (in inches). Can be None. + data_dict : dictionary + dictionary of data to pass to children patches + approach : str + string of which approach should be used + + Details ------- - suggested_width : float or list - proposed width for overall size if parents_areas=None, else this - is a list of relative proportion of widths of images (relative to - the global width taking a value of 1) - suggested_height : float or - proposed height for overall size if parents_areas=None, else this - is a list of relative proportion of height of images (relative to - the global height taking a value of 1) - depth : int or list - maximum depth of structure if parents_areas=None, else this - is a list of inner elements. + This internal function is likely hard to read, and can be used to + accomplish 3 different tasks that require depth-search style progression + through grobs. For easier read, we would recommend selecting one of the + approaches and then folding / code-collapsing all the conditional + statements that refer to the other approaches. Below are details about + each of the approaches including information about how the parameters + are used. + + "default-size": + + This approach calculates a default-size for the patch to observe a + minimum width and height of a images (as stored in rcParams) and for all + titles, subtitles, captions, and tags to have enough space to be + correctly presented. For the + arguments, (width, height) are not at all, and the data_dict may have the + attribute "default-size-proportion" which should be a tuple of positive + floats (less than or equal to 1) that captures the relative size of this + patch to the overall. + + The math for the "default-size" is pretty basic, and based on the idea + that for each plot and associated captions/titles one could describe + the associated `smallest_overall_size` would be the maximum of (the + required minimum size of the plot + `required_size_of_text`) times + the scaling factory to get from the overall patch to the interior plot. + Note that this is does for height and width separately. + + all: + + The data_dict can include an attribute "parent-index" which should be + a tuple of the parent's index (used for tag tracking). The data_dict can + include an attribute "parent-guided-annotation-update" which contains + an annotation object that the parent is providing. + + - data_dict["parent-guided-annotation-update"] + """ - Notes - ----- - The default rcParams are: - base_height = 3.71, - base_aspect_ratio = 1.618 # the golden ratio + # initialization section ---------------------------------------------- - This follows ideas proposed in cowplot: `wilkelab.org/cowplot/reference/save_plot.html `_. - """ - # basically following: https://wilkelab.org/cowplot/reference/save_plot.html - min_image_height = rcParams["base_height"] - min_image_width = rcParams["base_aspect_ratio"] * min_image_height + cur_annotation = copy.deepcopy(self.annotation) + if data_dict is not None and \ + data_dict.get("parent-guided-annotation-update") is not None: - image_rel_widths = [] - image_rel_heights = [] - depth = [] + # Note: this addition allows for the keeping of titles but the + # update of tags (as only the tag information is passed) + if cur_annotation.inheritance_type() == "override": + cur_annotation += data_dict["parent-guided-annotation-update"] - areas = self.layout._element_locations(width_pt=1, - height_pt=1, - num_grobs = len(self.grobs)) - if parents_areas is None: - parents_areas = [] - rel_width_to_parents = 1 - rel_height_to_parents = 1 - else: - rel_width_to_parents = np.prod([a.width for a in parents_areas]) - rel_height_to_parents = np.prod([a.height for a in parents_areas]) - parent_depth = len(parents_areas) + # prep section: annotation global size corrections -------------------- - for g_idx in np.arange(len(self.grobs)): - inner_area = areas[g_idx] + ## global annotation sizing - if inherits_plotnine(self.grobs[g_idx]): - inner_rw = [inner_area.width * rel_width_to_parents] - inner_rh = [inner_area.height * rel_height_to_parents] - inner_d = [parent_depth + 1] - else: - inner_rw, inner_rh, inner_d =\ - self.grobs[g_idx]._size_dive(parents_areas=parents_areas+ - [inner_area]) - image_rel_widths += inner_rw - image_rel_heights += inner_rh - depth += inner_d - - if len(parents_areas) == 0: - return (min_image_width/np.min(image_rel_widths), - min_image_height/np.min(image_rel_heights), - np.max(depth)) - else: - return (image_rel_widths, - image_rel_heights, - depth) + ### default size estimation + if approach == "default-size": + if data_dict is None or \ + data_dict.get("default-size-proportion") is None: - def _default_size(self, width, height): - """ - (Internal) obtain default recommended size of overall image if width or - height is None + if data_dict is None: + data_dict = dict() + data_dict["default-size-proportion"] = (1,1) - Arguments - --------- - width : float - width of output image in inches (this should actually be associated - with the svg...) - height : float - height of svg in inches (this should actually be associated - with the svg...) + # data_dict check + if not inherits(data_dict["default-size-proportion"], tuple) or \ + len(data_dict["default-size-proportion"]) != 2 or \ + not np.all(np.array(data_dict["default-size-proportion"]) > 0) or \ + not np.all(np.array(data_dict["default-size-proportion"]) <= 1): - Returns - ------- - width : float - returns default width for given object if not provided (else just - returns provided value). If only height is provided then width - proposed is relative to a default aspect ratio for the object. - height : float - returns default height for given object if not provided (else just - returns provided value). If only width is provided then height - proposed is relative to a default aspect ratio for the object. - """ - both_none = False - if width is None or height is None: - _width, _height, _ = self._size_dive() + raise ValueError("data_dict[\"default-size-proportion\"] must " +\ + "be a 2 length tuple of non-negative floats (less " +\ + "than or equal to 1") + + overall_default_size = (0,0) # starting storage for overall_default_size + else: # using default sizings + if width is None or height is None: + default_width, default_height = \ + self._hierarchical_general_process(width=1, height=1, + approach="default-size") + if width is None and height is not None: + width = default_width/default_height * height + if width is not None and height is None: + height = default_height/default_width * width if width is None and height is None: - both_none = True - width = _width - height = _height - elif width is not None: - height = _height / _width * width - else: - width = _width / _height * height - return width, height + width = default_width + height = default_height - def _svg_get_sizes(self, width_pt, height_pt): - """ - (Internal) Calculates required sizes for plot objects to meet required - sizes and logics if the requested sizing was possible - Arguments - --------- - width_pt : float - overall width of the image in points - height_pt : float - overall height of the image in points + if True: # all approaches + title_margin_sizes_dict = \ + cur_annotation._calculate_margin_sizes(to_inches=True) + text_used_width, text_used_height, = \ + title_margin_sizes_dict["extra_used_width"], \ + title_margin_sizes_dict["extra_used_height"] - Returns - ------- - sizes : nested list - For each element in the patch (with nesting structure in the list), - this contains width, height tuples that either capture the size - to request the ggplot object to be to return the actual desired - size (see gg_to_svg notes), OR it contains the fraction defined by - the requested width (or height) w.r.t. the returned width - (or height) from saving the plotnine object. The later option - occurs when when the ggplot's sizing didn't converge to the - desired size. - logics : nested list - For each element in the patch (with nesting structure in the list), - this contains a boolean value if the ggplot object was able to - be correctly size. + tl_loc = title_margin_sizes_dict["top_left_loc"] - Notes - ----- - Internally this function uses rcParams's ``eps``, ``mini_size_px`` and - ``maxIter`` to determine the parameters to be put in - .svg_utils._select_correcting_size_svg. - """ - layout = self.layout - areas = layout._element_locations(width_pt=width_pt, - height_pt=height_pt, - num_grobs=len(self.grobs)) + if approach == "default-size": # then we want to track relative sizing + width_inner, height_inner = \ + to_inches(1, units="pt"), to_inches(1, units="pt") + else: + width_inner, height_inner = \ + width - text_used_width, height - text_used_height + + ### sizing estimation + if approach == "size": + if data_dict is None: + data_dict = {} + if data_dict.get("size-num-attempts") is None: + data_dict["size-num-attempts"] = rcParams["num_attempts"] + if data_dict["size-num-attempts"] == 0: + raise StopIteration("Attempts to find the correct sizing of "+\ + "inner plots failed with provided "+\ + "parameters") + if data_dict.get("size-node-level") is None: + data_dict["size-node-level"] = 0 + + size_multiplier = [] + sizes_list = [] + ### saving / showing + if approach == "create": + #### create base image + base_image = sg.SVGFigure() + base_image.set_size((str(width_pt)+"pt", str(height_pt)+"pt")) + # add a view box... (set_size doesn't correctly update this...) + base_image.root.set("viewBox", "0 0 %s %s" % (str(width_pt), str(height_pt))) + + #### process titles/subtitles/captions + titles_and_locs = \ + cur_annotation._get_titles_and_locations( + width = from_inches(width, "pt"), + height = from_inches(height, "pt")) + + for loc_tuple, (title, _) in titles_and_locs: + _add_to_base_image(base_image, title, loc_tuple) + + #### sizes of inner grobs: + if data_dict is None or data_dict.get("sizes") is None: + sizes = self._hierarchical_general_process(width = 1, + height = 1, + approach = "size") + else: + sizes = data_dict["sizes"] - sizes = [] - logics = [] + #### prep index tracking (for uniquification of svg objections): + if data_dict is None or data_dict.get("u_idx") is None: + _u_idx = str(self.__hash__()) + else: + _u_idx = data_dict["u_idx"] + + #### TODO?: maybe track the time it takes to calculate sizes and if it takes to + #### long provide user with progressbar for the actual plot creation? + + + ### prep for grob processing + if True: # all approaches + areas = self.layout._element_locations( + width_pt=from_inches(width_inner, units="pt"), # TODO: need to check all pt vs inch assumptions + height_pt=from_inches(height_inner, units="pt"), + num_grobs=len(self.grobs)) + tag_index_array = self._get_grob_tag_ordering( + cur_annotation=cur_annotation + ) + + # process section: loop through heirarchical structure ---------------- + for p_idx, image in enumerate(self.grobs): + ## prep tag annotation + if True: # all approaches + inner_area = areas[p_idx] + + if tag_index_array[p_idx] is not None: + grob_tag_index = tag_index_array[p_idx] + + fundamental_tag = not inherits(image, patch) + + if data_dict is None or data_dict.get("parent-index") is None: + current_index = (grob_tag_index,) + else: + current_index = tuple(list(data_dict["parent-index"])+\ + [grob_tag_index]) + + + else: # no tag required + fundamental_tag = False + grob_tag_index = None + current_index = () + + tag_margin_dict = cur_annotation._calculate_tag_margin_sizes( + fundamental=fundamental_tag, + index=grob_tag_index, + full_index=current_index, + to_inches=True) + + + if approach in ["create", "size"]: + # in pt... + grob_width, grob_height = \ + inner_area.width - tag_margin_dict["extra_used_width"],\ + inner_area.height - tag_margin_dict["extra_used_height"] + + ### create tag + if approach == "create": + inner_u_idx = _u_idx + "_" + str(p_idx) + tag_loc, image_loc, tag_image = \ + cur_annotation._get_tag_and_location( + width=inner_area.width, + height=inner_area.height, + index=grob_tag_index, + full_index=current_index, + fundamental=fundamental_tag) + + _add_to_base_image(base_image, tag_image, tag_loc) + + ## grob processing + if inherits(image, patch): + + data_dict_pass_through = data_dict.copy() + data_dict_pass_through["parent-index"] = current_index + data_dict_pass_through["parent-guided-annotation-update"] = \ + cur_annotation._step_down_tags_info(parent_index=grob_tag_index) + + ### default sizing + if approach == "default-size": + # (TODO: remove comment once done with pt vs in stuff) + # no need to look at width & height as (relative to PT & IN) + # is actually in the "pure-relative" format (relative to + # overall size of 1 x 1) + default_size_prop = (inner_area.width, inner_area.height) + data_dict_pass_through["default-size-proportion"] = \ + default_size_prop + + default_inner_size = self.grobs[p_idx]._hierarchical_general_process( + data_dict=data_dict_pass_through, + approach="default-size") + # ^same as calling ._default_size(data_dict=data_dict_pass_through) + ### sizing estimation + if approach == "size": + data_dict_pass_through["size-node-level"] = \ + data_dict["size-node-level"] + 1 + + inner_sizes, inner_size_multiplier = \ + image._hierarchical_general_process( + width=to_inches(grob_width, "pt"), + height=to_inches(grob_height, "pt"), + data_dict=data_dict_pass_through, + approach="size") + sizes_list.append(inner_sizes) + size_multiplier.append(inner_size_multiplier) + + ### saving / showing + if approach == "create": + data_dict_pass_through["u_idx"] = inner_u_idx + data_dict_pass_through["size"] = sizes[p_idx] + grob_image = image._hierarchical_general_process( + width=grob_width, + height=grob_height, + data_dict=data_dict_pass_through, + approach="create") + + elif inherits_plotnine(image): + ### default sizing + if approach == "default-size": + default_inner_size = ( + rcParams["base_aspect_ratio"] * rcParams["base_height"], + rcParams["base_height"]) + ### sizing estimation + if approach == "size": + v0, v1, successful_logic = \ + _select_correcting_size_svg( + self.grobs[p_idx], + width=grob_width, + height=grob_height, + dpi=96, + eps=rcParams["eps"], + min_size_px=rcParams["min_size_px"], + maxIter=rcParams["maxIter"], + throw_error=False) + + if successful_logic is False: # track needed size change + _, inner_size_multiplier = \ + _overall_scale_recommendation_single( + image_new_size=(v0*grob_width, + v1*grob_height), + text_inner_size=(tag_margin_dict["min_inner_width"], + tag_margin_dict["min_inner_height"]), + text_extra_size=(tag_margin_dict["extra_used_width"], + tag_margin_dict["extra_used_height"]), + original_overall_size=(grob_width, grob_height)) + sizes_list.append((-1,-1)) + size_multiplier.append(inner_size_multiplier) + else: # collect creation size requirement + sizes_list.append((v0,v1)) + size_multiplier.append(1) + ### saving / showing + if approach == "create": + grob_image = _raw_gg_to_svg(image, + width = sizes[p_idx][0], + height = sizes[p_idx][1], + dpi = 96) + grob_image = _uniquify_svg_safe(grob_image, inner_u_idx) + + elif inherits(image, text): + ### default sizing + if approach == "default-size": + m_w,m_h = self.grobs[p_idx]._min_size(to_inches=True) + + default_inner_size = ( + np.max([rcParams["base_aspect_ratio"] *\ + rcParams["base_height"], m_w]), + np.max([m_h,rcParams["base_height"]]) + ) + ### sizing estimation + if approach == "size": + m_w, m_h = image._min_size(to_inches=False) # PT vs IN + #pdb.set_trace() + if m_w > grob_width or m_h > grob_height: # track needed size change + inner_size_multiplier = np.max([m_w/grob_width, + m_h/grob_height]) + sizes_list.append((-1,-1)) + size_multiplier.append(inner_size_multiplier) + else: + sizes_list.append((grob_width, grob_height)) + size_multiplier.append(1) + ### saving / showing + if approach == "create": + grob_image = image._svg( + width_pt=sizes[p_idx][0], + height_pt=sizes[p_idx][1]) + else: + raise ValueError("cowpatch currently only processes native "+\ + "classes patch and text as well as "+\ + "plotnine's ggplot objects") + + ## post processing inner loop + ### sizing estimation + if approach == "default-size": + #### we need to deal with mins from tag info + default_inner_size = ( + np.max([default_inner_size[0], + tag_margin_dict["min_inner_width"]]), + np.max([default_inner_size[1], + tag_margin_dict["min_inner_height"]]) + ) + + grob_default_size = ( + default_inner_size[0] + tag_margin_dict["extra_used_width"], + default_inner_size[1] + tag_margin_dict["extra_used_height"] + ) + + overall_default_size = ( + np.max([1/inner_area.width * grob_default_size[0], + overall_default_size[0]]), + np.max([1/inner_area.height * grob_default_size[1], + overall_default_size[1]]) + ) + + ### saving / showing + if approach == "create": + #### append to base image @ correct location + _add_to_base_image(base_image, grob_image, image_loc) + + # post processing section: cleaning up + ### sizing estimation + if approach == "default-size": + default_inner_size = ( + np.max([overall_default_size[0], + title_margin_sizes_dict["min_inner_width"], + title_margin_sizes_dict["min_full_width"] - \ + title_margin_sizes_dict["extra_used_width"]]), + np.max([overall_default_size[1], + title_margin_sizes_dict["min_inner_height"]]) + ) + + return ( + default_inner_size[0] + title_margin_sizes_dict["extra_used_width"], + default_inner_size[1] + title_margin_sizes_dict["extra_used_height"] + ) + ### sizing estimation + if approach == "size": + if not np.allclose(size_multiplier, np.ones(len(size_multiplier))): # track needed size change + pdb.set_trace() + _, inner_size_multiplier = \ + _overall_scale_recommendation_patch(size_multiplier, + text_inner_size=( + np.max([title_margin_sizes_dict["min_inner_width"], + title_margin_sizes_dict["min_full_width"] - \ + title_margin_sizes_dict["extra_used_width"]]), + title_margin_sizes_dict["min_inner_height"]), + text_extra_size=(title_margin_sizes_dict["extra_used_width"], + title_margin_sizes_dict["extra_used_height"]), + original_overall_size=(width_inner, height_inner)) + + ### repeat + # if sizing incorrect and num-attempts haven't run out, + # repeat process with updated sizing + if data_dict["size-node-level"] == 0: + return self._hierarchical_general_process(self, + width=inner_size_multiplier*width, + height=inner_size_multiplier*height, + data_dict={"size-num-attempts": + data_dict["size-num-attempts"]-1}, + approach="size") + else: + return (-1,-1), inner_size_multiplier + else: # collect sizes + return sizes_list, 1 - for p_idx in np.arange(len(self.grobs)): - inner_area = areas[p_idx] - inner_width_pt = inner_area.width - inner_height_pt = inner_area.height # TODO: how to deal with ggplot objects vs patch objects if inherits(self.grobs[p_idx], patch): @@ -507,55 +1270,8 @@ def _svg_get_sizes(self, width_pt, height_pt): raise ValueError("grob idx %i is not a patch object nor"+ "a ggplot object" % p_idx) - return sizes, logics - - def _process_sizes(self, sizes, logics): - """ - (Internal) draw conclusions about the output of _svg_get_sizes. - - This function assesses if any internal ggplot object failed to - produce an image of the requested size (w.r.t. to given interation - parameters) - - Arguments - --------- - sizes : nested list - sizes output of _svg_get_sizes. This is a nested list with - (width,height) or (requested_width/actual_width, - requested_height/actual_width), with the different being if the - ggplot object was able to be presented in the requested size - logics : nested list - logics output of _svg_get_sizes. This is a nested list of boolean - values w.r.t. the previous different. - - Returns - ------- - sizes : nested list - Returned if no logics values are False. This object is the sizes - nested list that was input. - max_scaling : float - Returned if at least one of the logics values are False. This is - the scaling of the original ``width_pt`` and ``height_pt`` that - defined the sizes and logics that could make the requested sizes - for all ggplots that failed to be correctly sized be at least as - large as the returned size from a basic ggplot saving. - """ - flatten_logics = _flatten_nested_list(logics) - if np.all(flatten_logics): - return sizes - - # else suggest a rescale: - flatten_sizes = _flatten_nested_list(sizes) - bad_sizing_info = [flatten_sizes[i] for i in range(len(flatten_sizes)) - if not flatten_logics[i]] - - width_scaling = 1/np.min([sizing[0] for sizing in bad_sizing_info]) - height_scaling = 1/np.min([sizing[1] for sizing in bad_sizing_info]) - max_scaling = np.max([width_scaling, height_scaling]) - # ^ keeping aspect ratio with a single scaling - return max_scaling def save(self, filename, width=None, height=None, dpi=96, _format=None, verbose=None): @@ -603,15 +1319,20 @@ def save(self, filename, width=None, height=None, dpi=96, _format=None, io.BytesIO : object that acts like a reading in of bytes """ # updating width and height if necessary (some combine is none) - width, height = self._default_size(width=width,height=height) + #width, height = self._default_size(width=width, height=height) # global default for verbose (if not provided by the user) if verbose is None: verbose = rcParams["save_verbose"] svg_obj, (actual_width_pt, actual_height_pt) = \ - self._svg(width_pt = from_inches(width, "pt", dpi=dpi), - height_pt = from_inches(height, "pt", dpi=dpi)) + self._hierarchical_general_process( + width=from_inches(width, "pt", dpi=dpi), + height=from_inches(height, "pt", dpi=dpi), + data_dict=None, + approach="create") + # self._svg(width_pt = from_inches(width, "pt", dpi=dpi), + # height_pt = from_inches(height, "pt", dpi=dpi)) _save_svg_wrapper(svg_obj, filename=filename, @@ -655,15 +1376,20 @@ def show(self, width=None, height=None, dpi=96, verbose=None): representation. """ # updating width and height if necessary (some combine is none) - width, height = self._default_size(width=width,height=height) + #width, height = self._default_size(width=width, height=height) # global default for verbose (if not provided by the user) if verbose is None: verbose = rcParams["show_verbose"] svg_obj, (actual_width_pt, actual_height_pt) = \ - self._svg(width_pt = from_inches(width, "pt", dpi=dpi), - height_pt = from_inches(height, "pt", dpi=dpi)) + self._hierarchical_general_process( + width=from_inches(width, "pt", dpi=dpi), + height=from_inches(height, "pt", dpi=dpi), + data_dict=None, + approach="create") + # self._svg(width_pt = from_inches(width, "pt", dpi=dpi), + # height_pt = from_inches(height, "pt", dpi=dpi)) _show_image(svg_obj, width=to_inches(actual_width_pt, "pt", dpi=dpi), @@ -677,6 +1403,7 @@ def __str__(self): def __repr__(self): out = "num_grobs: " + str(len(self.grobs)) +\ - "\n---\nlayout:\n" + self.layout.__repr__() + "\n---\nlayout:\n" + self.layout.__repr__()+\ + "\n---\nannotation:\n" + self.annotation.__repr__() return "" % self.__hash__() + "\n" + out diff --git a/src/cowpatch/config.py b/src/cowpatch/config.py index 68e98f5..3dd958f 100644 --- a/src/cowpatch/config.py +++ b/src/cowpatch/config.py @@ -1,3 +1,5 @@ +import plotnine as p9 + rcParams = dict(maxIter=20, min_size_px=10, eps=1e-2, @@ -8,7 +10,26 @@ num_attempts=2, base_height=3.71, - base_aspect_ratio=1.618 # the golden ratio + base_aspect_ratio=1.618, # the golden ratio + + cow_text=p9.element_text( + family=p9.options.get_option("base_family"), + style='normal', + color='black', + size=1.2*11, + linespacing=0.9, + ha='center', + va='center', + rotation=0, + margin={}), + cow_tag=p9.element_text(size=1.2*11, + ha="left"), + cow_title=p9.element_text(size=1.2*11, + ha="left"), + cow_subtitle=p9.element_text(size=11, + ha="left"), + cow_caption=p9.element_text(size=.8*11, + ha="right"), ) """ diff --git a/src/cowpatch/layout_elements.py b/src/cowpatch/layout_elements.py index cd7761b..6feef42 100644 --- a/src/cowpatch/layout_elements.py +++ b/src/cowpatch/layout_elements.py @@ -517,7 +517,7 @@ def _yokogaki_ordering(self, num_grobs=None): "with self.num_grobs") num_grobs = self.num_grobs - areas = self._element_locations(1,1) # basically getting relative positions (doesn't matter) - nor does it matter about rel_height and width, but ah well + areas = self._element_locations(1,1, num_grobs=num_grobs) # basically getting relative positions (doesn't matter) - nor does it matter about rel_height and width, but ah well all_x_left = np.array([a.x_left for a in areas]) all_y_top = np.array([a.y_top for a in areas]) @@ -624,8 +624,16 @@ def __eq__(self, value): return design_logic and \ self.ncol == value.ncol and \ self.nrow == value.nrow and \ - np.unique(self.rel_heights/value.rel_heights).shape[0] == 1 and \ - np.unique(self.rel_widths/value.rel_widths).shape[0] == 1 + ( + ((self.rel_heights is None) and (value.rel_heights is None)) + or + (np.unique(self.rel_heights/value.rel_heights).shape[0] == 1) + ) and \ + ( + ((self.rel_widths is None) and (value.rel_widths is None)) + or + (np.unique(self.rel_widths/value.rel_widths).shape[0] == 1) + ) class area: diff --git a/src/cowpatch/svg_utils.py b/src/cowpatch/svg_utils.py index 42fc2e4..4760c98 100644 --- a/src/cowpatch/svg_utils.py +++ b/src/cowpatch/svg_utils.py @@ -468,6 +468,27 @@ def _show_image(svg, width, height, dpi=300, verbose=True): +def _add_to_base_image(base_image, current_image, loc = (0,0)): + """ + append current image into base_image at location loc + + Arguments + --------- + base_image : svg object + base image object + current_image : svg object + new image object to be added to the base image + loc : tuple + top left corner location for the new image to be placed (within the + base_image) + """ + inner_root = current_image.getroot() + inner_root.moveto(x=loc[0], + y=loc[1]) + base_image.append(inner_root) + + return None + def _uniquify_svg_safe(svg_obj, str_update): """ Update svg code to 'uniquify' svg but by making sure 'url(#___)' @@ -544,4 +565,3 @@ def _uniquify_svg_str_safe(svg_str, str_update): return svg_str - diff --git a/src/cowpatch/text_elements.py b/src/cowpatch/text_elements.py index 0a1eda7..f166279 100644 --- a/src/cowpatch/text_elements.py +++ b/src/cowpatch/text_elements.py @@ -1,8 +1,26 @@ import plotnine as p9 +from plotnine.themes.themeable import themeable +from plotnine.themes.theme import theme import numpy as np +import copy +import matplotlib.pyplot as plt +import io +import svgutils.transform as sg +import xml.etree.ElementTree as ET +import re + +from contextlib import suppress # suppress kwargs that are incorrect + +import pdb + +from .utils import to_pt, from_pt, to_inches, from_inches, \ + _transform_size_to_pt, inherits +from .utils import to_inches as _to_inches +from .svg_utils import _show_image, _save_svg_wrapper +from .config import rcParams class text: - def __init__(self, label, element_text=None, theme=None): + def __init__(self, label, element_text=None, _type = "cow_text"): """ create a new text object @@ -11,30 +29,25 @@ def __init__(self, label, element_text=None, theme=None): label : string text label with desirable format (e.g. sympy, etc.) element_text : plotnine.themes.elements.element_text - element object from plotnine, can also be added later - theme : plotnine.theme object - theme object from plotnine, can be associated later - should only provide a theme or element_text (as a theme - contains an element_text) + element object from plotnine + _type : string + string of which cowpatch text object should inherit attributes + from, if element_text argument doesn't competely define the text + attributes Notes ----- - This class leverages matplotlib to create the text (given that - matplotlib can create path-style objects for text if the individual - is worried that the svg text visually won't be preserved - this is also - annoying that this is the default). - - Note the function use the ``text`` attribute - NOT the - ``plot_title`` attribute if a theme is provided. + https://stackoverflow.com/questions/34387893/output-matplotlib-figure-to-svg-with-text-as-text-not-curves + ^ on text vs paths for text in mathplotlib """ self.label = label - - if element_text is not None and theme is not None: - raise ValueError("please provide only a theme or element_text, "+ - "not both") + self._type = _type self.element_text = None # prep initialization + self._theme = p9.theme() self._clean_element_text(element_text) - self.theme = theme + + def _define_type(self, _type): + self._type = _type def _clean_element_text(self, element_text): """ @@ -49,7 +62,7 @@ def _clean_element_text(self, element_text): Notes ----- - updates i place + updates in place """ if element_text is None: element_text = None @@ -66,98 +79,288 @@ def _clean_element_text(self, element_text): else: self.element_text = element_text + # keeping theme update tracking + if self.element_text is not None: + self._theme += p9.theme( + **{self._type: self.element_text.theme_element}) + return None # just a reminder def __add__(self, other): """ - add element_text or theme to update format + add element_text update format + + Arguments + --------- + other : plotnine.themes.elements.element_text + element_text to define the attributes of the text. + + Returns + ------- + updated text object + + Notes + ----- + The way we allow for alterations when text objects are used in + `annotate` function only allows direct alterations of the text's style + with the addition of a p9.element_text object. + """ - TODO: make this work w.r.t patchwork approaches (edge cases #8) + if isinstance(other, p9.theme): + # "a text object's information should not be updated with p9.theme + # directly due to uncertainity in which theme key parameter will + # actually be used. + raise ValueError("a text object's presentation information is not "+\ + "able to be directly updated with a p9.theme, " + + "use a p9.element_text object instead. See Notes "+\ + "for more information") + + if not isinstance(other, p9.themes.themeable.element_text): + raise ValueError("text objects are only allowed to be combined "+\ + "with p9.element_text objects.") + + new_object = copy.deepcopy(self) + + new_object._clean_element_text(other) + + # # need to update theme or element_text... + # if isinstance(other, p9.themes.themeable.element_text): + # self._clean_element_text(other) + + # # update theme if it already exists + # # (if not we'll update it when it's required) + # if self.theme is not None: + # self.theme += p9.theme(text = self.element_text.theme_element) + + # if isinstance(other, p9.theme): + # if self.theme is None: + # self.theme = other + # else: + # self.theme.add_theme(other) + + # new_element_text = self.theme.themeables.get("text") + # self._clean_element_text(new_element_text) + return new_object + + def __eq__(self, other): + """ + Check if current object is equal to another Arguments --------- - other : plotnine.themes.elements.element_text or theme - theme or element_text to define the attributes of the text. + other : other object, assumed to be text object + + Returns + ------- + boolean if equivalent + """ + + if not inherits(other, text): + return False + + return self.__dict__ == other.__dict__ + + def _additional_rotation(self, angle=0): + """ + (internal function) to create a rotation of the original object + + Arguments + --------- + angle : float + angle in degrees (0-360) to rotate the current text + + Returns + ------- + new text object that is a rotation of the current one + """ + + new_text_object = copy.deepcopy(self) + + if angle == 0: # not alterations done + return new_text_object + + if new_text_object.element_text is None: + rotation = (((angle) / 360.) - + np.floor(((angle) / 360.))) * 360 + new_text_object += p9.element_text(angle=rotation) + else: + # grab the elment_text from text object + et = new_text_object.element_text.theme_element + current_angle = et.properties.get("rotation") + if current_angle is not None: + et.properties["rotation"] = \ + (((current_angle + angle) / 360.) - + np.floor(((current_angle + angle) / 360.))) * 360 + else: # if no rotation has currently been set + et.properties["rotation"] = \ + (((angle) / 360.) - + np.floor(((angle) / 360.))) * 360 + + new_text_object += et + + return new_text_object + + + + + def _update_element_text_from_theme(self, theme, key=None): + """ + Internal function to update .element_text description from a theme + + Arguments + --------- + theme : plotnine.theme object + theme object from plotnine + key : str + string associated with which of theme's internal parameter keys + the element_text will be updated from. If key is None, then we + use the internal ._type value Notes ----- - Note the function use the ``text`` attribute - NOT the ``plot_title`` - attribute if a theme is provided. + updates element_text inplace. """ + if key is None: + key = self._type - if not isinstance(other, p9.themes.themeable.element_text) and \ - not isinstance(other, p9.theme): - raise ValueError("text objects are only allowed to be combined "+ - "with element_text objects.") - # need to update theme or element_text... - if isinstance(other, p9.themes.themeable.element_text): - self._clean_element_text(other) + # 1. check that theme has desired key + if not key in theme.themeables.keys(): + raise ValueError("key parameter in _update_element_text_from_theme " +\ + "function call needs to be a key in the provided " +\ + "theme's themeables.") - # update theme if it already exists - # (if not we'll update it when it's required) - if self.theme is not None: - self.theme += p9.theme(text = self.element_text.theme_element) + # update element_text with new element_text + new_et = theme.themeables.get(key) + if self.element_text is not None: + self.element_text.merge(new_et) + else: + self.element_text = new_et - if isinstance(other, p9.theme): - if self.theme is None: - self.theme = other - else: - self.theme.add_theme(other) + self._theme += theme + self._clean_element_text(self.element_text) - new_element_text = self.theme.themeables.get("text") - self._clean_element_text(new_element_text) - return self + return None # just a reminder - def _provide_complete_theme(self): + def _get_full_element_text(self): """ - It should be here that the current global theme is accessed, and thing are completed... + create a "full" element_text from base p9 theme + + Notes + ----- + This function will update the element_text *only with* attributes + that aren't currently defined (using the cow.theme_get() function) """ - if self.theme is None: - current_theme = p9.theme_get() - # and update with our element_text - if self.element_text is not None: - # problem here... (need to update themeable.get("text") instead) - current_theme += p9.theme(text=self.element_text.theme_element) + new_self = copy.deepcopy(self) + new_self._update_element_text_from_theme(theme_get() + self._theme, # don't want to actually update self.element_text or self._theme + key=self._type) + if self.element_text is not None: + new_self.element_text.merge(self.element_text) + + # full set of structure inherited from cow_text - TODO: smarter approach mirroring p9 functionality? + if self._type != "cow_text": # assume inherits properties + cow_text_et = new_self._theme.themeables.get("cow_text") + cow_text_et.merge(new_self.element_text) + return cow_text_et else: - current_theme = self.theme + return new_self.element_text - return current_theme - def _inner_prep(self): + def _min_size(self, to_inches=False): """ - Internal function to create matplotlib figure with text and - provide a bounding box for the location in the plot + calculate minimum size of bounding box around self in pt and + creates base image of text object + + + Arguments + --------- + to_inches : boolean + if the output should be converted to inches before returned Returns ------- - fig : matplotlib.figure.Figure - figure with text at (0,0), no axes - bbox : matplotlib.transforms.Bbox - bbox location of text in figure + min_width : float + minimum width for text in pt (or inches if `to_inches` is True) + min_height: float + minimum height for text in pt (or inches if `to_inches` is True) + + Note + ---- + this code is simlar to that in _base_text_image """ + current_element_text = self._get_full_element_text() - # apply theme/element_text correctly ------ + properties = current_element_text.properties.copy() + + # create text and apply ---------- + # https://stackoverflow.com/questions/24581194/matplotlib-text-bounding-box-dimensions - # mirrors code in p9.ggplot.draw_title() and - # p9.themeable.plot_title.apply_figure() - # code references: - # - https://github.com/has2k1/plotnine/blob/9fbb5f77c8a8fb8c522eb88d4274cd2fa4f8dd88/plotnine/ggplot.py#L545 - # - https://github.com/has2k1/plotnine/blob/6c82cdc20d6f81c96772da73fc07a672a0a0a6ef/plotnine/themes/themeable.py#L361 - # + fig, ax = plt.subplots() + fig.set_dpi(96) # used for bbox in pixel sizes... + txt = fig.text(x=0.000, y=0.000, s=self.label) + with suppress(KeyError): + del properties['margin'] + with suppress(KeyError): + txt.set(**properties) - # collect desirable theme and properties -------- - theme = self._provide_complete_theme() + bbox = txt.get_window_extent(fig.canvas.get_renderer()) + min_width_pt = to_pt(bbox.width, 'px') + min_height_pt = to_pt(bbox.height, 'px') + + margin_dict = current_element_text.properties.get("margin") + if margin_dict is not None: + margin_dict_pt = {"t": to_pt(margin_dict["t"], margin_dict["units"]), + "b": to_pt(margin_dict["b"], margin_dict["units"]), + "l": to_pt(margin_dict["l"], margin_dict["units"]), + "r": to_pt(margin_dict["r"], margin_dict["units"])} + else: + margin_dict_pt = {"t":0, "b":0, "l":0, "r":0} - text_themeable = theme.themeables.get('text') - properties = text_themeable.properties.copy() + min_width_pt += (margin_dict_pt["l"] + margin_dict_pt["r"]) + min_height_pt += (margin_dict_pt["t"] + margin_dict_pt["b"]) + plt.close() + + if to_inches: + min_width = _to_inches(min_width_pt, units = "pt") + min_height = _to_inches(min_height_pt, units = "pt") + else: + min_width = min_width_pt + min_height = min_height_pt + + + return min_width, min_height + + def _base_text_image(self, close=True): + """ + Create base figure and obtain bbox structure that contains + the image in the figure + + Arguments + --------- + close : boolean + if we should close the matplotlib plt instance (default True) + + Returns + ------- + fig : matplotlib figure + figure of the text (in the smallest area possible) + bbox : matplotlib bbox object + bbox for the associated figure (describing bounding box + of text object in the image) + + Note + ---- + this code is simlar to that in _min_size + """ + current_element_text = self._get_full_element_text() + properties = current_element_text.properties.copy() # create text and apply ---------- # https://stackoverflow.com/questions/24581194/matplotlib-text-bounding-box-dimensions fig, ax = plt.subplots() - fig.set_dpi(96) + fig.set_dpi(96) # used for bbox in pixel sizes... txt = fig.text(x=0.000, y=0.000, s=self.label) with suppress(KeyError): del properties['margin'] @@ -171,35 +374,103 @@ def _inner_prep(self): fig.axes.pop() plt.axis('off') - # bbox aids in cutting all but the desired image + if close: + plt.close() return fig, bbox + def _default_size(self, width=None, height=None): + """ + (Internal) obtain default recommended size of overall text object if + width or height is None - def _create_svg_object(self, width=None, height=None): + Arguments + --------- + width : float + width of output image in inches (this should actually be associated + with the svg...) + height : float + height of svg in inches (this should actually be associated + with the svg...) + + Returns + ------- + width : float + returns default width for given object if not provided (else just + returns provided value). If only height is provided then width + proposed is the minimum width (including margins). THIS IS IN INCHES + height : float + returns default height for given object if not provided (else just + returns provided value). If only width is provided then height + proposed is the minimum height (including margins). THIS IS IN INCHES + + Notes + ----- + This function is internal, it does assume that in width/height is not + None that you've done your homework and not set width or height as too + small. + """ + if width is not None and height is not None: + return width, height + + min_width_pt, min_height_pt = self._min_size() + + if width is None: + width = from_pt(min_width_pt, "inches") + if height is None: + height = from_pt(min_height_pt, "inches") + + return width, height + + def _svg(self, width_pt=None, height_pt=None, sizes=None, num_attempts=None): """ Internal to create svg object (with text in correct location and correct image size) Arguments --------- - width : float - width of desired output (in inches) - height : float - height of desired output (in inches) + width_pt : float + width of desired output (in pt) + height_pt : float + height of desired output (in pt) + sizes: TODO: write description & code up + num_attempts : TODO: write description & code up Returns ------- svg_obj : svgutils.transform.SVGFigure svg representation of text with correct format and image size + (width_pt, height_pt) : tuple + tuple of width and height (in pt) of image out (given inputs + can be None) + + See also + -------- + patch._svg : similar functionality for a patch object + svgutils.transforms : pythonic svg object """ - fig, bbox = self._inner_prep() + fig, bbox = self._base_text_image(close=False) + min_width_pt, min_height_pt = self._min_size() + + # TODO: update to the "correction proposal approach" + if width_pt is not None: + if width_pt < min_width_pt - 1e-10: # eps needed + plt.close() + raise ValueError("requested width of text object isn't "+\ + "large enough for text") + else: #if width is None + width_pt = min_width_pt + + + if height_pt is not None: + if height_pt < min_height_pt - 1e-10: # eps needed + plt.close() + raise ValueError("requested height of text object isn't "+\ + "large enough for text") + else: #if height is None + height_pt = min_height_pt - if width is not None: - width = to_pt(width, "in") - if height is not None: - height = to_pt(height, "in") # get original matplotlib image ------------ @@ -208,37 +479,35 @@ def _create_svg_object(self, width=None, height=None): fid.seek(0) image_string = fid.read() img = sg.fromstring(image_string) - img_size = transform_size((img.width, img.height)) - # location correction for alignment and margins ------- - - current_theme = self._provide_complete_theme() - ha_str = current_theme.themeables.get("text").properties.get("ha") - va_str = current_theme.themeables.get("text").properties.get("va") - margin_dict = current_theme.themeables.get("text").properties.get("margin") - margin_dict_pt = {"t": to_pt(margin_dict["t"], margin_dict["units"]), - "b": to_pt(margin_dict["b"], margin_dict["units"]), - "l": to_pt(margin_dict["l"], margin_dict["units"]), - "r": to_pt(margin_dict["r"], margin_dict["units"])} - + img_size = _transform_size_to_pt((img.width, img.height)) + # ^this won't be width or min_width_pt related - if width is None: - width = to_pt(bbox.width, 'px') + margin_dict_pt["l"] + margin_dict_pt["r"] + # location correction for alignment and margins ------- + current_element_text = self._get_full_element_text() + ha_str = current_element_text.properties.get("ha") + va_str = current_element_text.properties.get("va") + margin_dict = current_element_text.properties.get("margin") + + if margin_dict is not None: + margin_dict_pt = {"t": to_pt(margin_dict["t"], margin_dict["units"]), + "b": to_pt(margin_dict["b"], margin_dict["units"]), + "l": to_pt(margin_dict["l"], margin_dict["units"]), + "r": to_pt(margin_dict["r"], margin_dict["units"])} + else: + margin_dict_pt = {"t":0, "b":0, "l":0, "r":0} - if height is None: - height = to_pt(bbox.height, 'px') + margin_dict_pt["t"] + margin_dict_pt["b"] if ha_str == "center": - x_shift = width/2 - to_pt(bbox.width, "px") /2 + x_shift = width_pt/2 - to_pt(bbox.width, "px")/2 elif ha_str == "right": - x_shift = width - to_pt(bbox.width, "px") - margin_dict_pt["r"] + x_shift = width_pt - to_pt(bbox.width, "px") - margin_dict_pt["r"] else: # ha_str == "left" x_shift = margin_dict_pt["l"] - if va_str == "center": - y_shift = height/2 - to_pt(bbox.height, "px") /2 + y_shift = height_pt/2 - to_pt(bbox.height, "px")/2 elif va_str == "bottom": - y_shift = height - to_pt(bbox.height, "px") - margin_dict_pt["b"] + y_shift = height_pt - to_pt(bbox.height, "px") - margin_dict_pt["b"] else: # va_str = "top" y_shift = margin_dict_pt["t"] @@ -251,13 +520,11 @@ def _create_svg_object(self, width=None, height=None): img_size[1] - to_pt(bbox.y1, units = "px")) # height - y_max - new_image_size = (str(width)+"pt", - str(height)+"pt") + new_image_size_string_val = (str(width_pt), str(height_pt)) + new_image_size = (new_image_size_string_val[0] + "pt", + new_image_size_string_val[1] + "pt") # need to declare the viewBox for some reason... - new_image_size_string_val = [re.sub("pt", "", val) - for val in new_image_size] - new_viewBox = "0 0 %s %s" % (new_image_size_string_val[0], new_image_size_string_val[1]) @@ -266,21 +533,13 @@ def _create_svg_object(self, width=None, height=None): y = -1*svg_bb_top_left_corner[1] + y_shift) img_root_str = img_root.tostr() - new_image = sg.SVGFigure() new_image.set_size(new_image_size) new_image.root.set("viewBox", new_viewBox) - - # remove patch (lxml.etree) ---------- - # img_root2_lxml = etree.fromstring(img_root_str) - # parent = img_root2_lxml.findall(".//{http://www.w3.org/2000/svg}g[@id=\"patch_1\"]")[0] - # to_remove = parent.getchildren()[0] - # parent.remove(to_remove) - # img_root2_str = etree.tostring(img_root2_lxml) - # img2 = sg.fromstring(img_root2_str.decode("utf-8")) - - # remove path (xml.etree.ElementTree) --------- + ### remove path (xml.etree.ElementTree) --------- + # removing the background behind the text object (allows text to be + # above other objects) img_root2_xml = ET.fromstring(img_root_str) parent = img_root2_xml.findall(".//{http://www.w3.org/2000/svg}g[@id=\"patch_1\"]")[0] to_remove = img_root2_xml.findall(".//{http://www.w3.org/2000/svg}path")[0] @@ -293,23 +552,248 @@ def _create_svg_object(self, width=None, height=None): # closing plot plt.close() - return new_image - + return new_image, (width_pt, height_pt) - def save(self, filename, width=None, height=None): + def save(self, filename, width=None, height=None, dpi=96, _format=None, + verbose=None): """ - save text object as image in minimal size object + save text object image to file Arguments --------- filename : str + local string to save the file to (this can also be at a + ``io.BytesIO``) width : float - in inches (if None, then tight (w.r.t. to margins)) + width of output image in inches (this should actually be associated + with the svg...) height : float - in inches (if None, then tight (w.r.t. to margins)) + height of svg in inches (this should actually be associated + with the svg...) + dpi : int or float + dots per square inch, default is 96 (standard) + _format : str + string of format (error tells options). If provided this is the + format used, if None, then we'll try to use the ``filename`` + extension. + verbose : bool + If ``True``, print the saving information. The package default + is defined by cowpatch's own rcParams (the base default is + ``True``), which is used if verbose is ``None``. See Notes. + + Returns + ------- + None + saves to a file + + Notes + ----- + If width and/or height is None, the approach will attempt to define + acceptable width and height. + + The ``verbose`` parameter can be changed either directly with defining + ``verbose`` input parameter or changing + ``cow.rcParams["save_verbose"]``. + + See also + -------- + io.BytesIO : object that acts like a reading in of bytes + cow.patch.save : same function but for a cow.patch object + """ + # updating width and height if necessary (some combine is none) + width, height = self._default_size(width=width, height=height) + + # global default for verbose (if not provided by the user) + if verbose is None: + verbose = rcParams["save_verbose"] + + svg_obj, (actual_width_pt, actual_height_pt) = \ + self._svg(width_pt = from_inches(width, "pt", dpi=dpi), + height_pt = from_inches(height, "pt", dpi=dpi)) + + _save_svg_wrapper(svg_obj, + filename=filename, + width=to_inches(actual_width_pt, "pt", dpi=dpi), + height=to_inches(actual_height_pt, "pt", dpi=dpi), + dpi=dpi, + _format=_format, + verbose=verbose) + + def show(self, width=None, height=None, dpi=96, verbose=None): + """ + display text object from the command line or in a jupyter notebook + + Arguments + --------- + width : float + width of output image in inches (this should actually be associated + with the svg...) + height : float + height of svg in inches (this should actually be associated + with the svg...) + dpi : int or float + dots per square inch, default is 96 (standard) + verbose : bool + If ``True``, print the saving information. The package default + is defined by cowpatch's own rcParams (the base default is + ``True``), which is used if verbose is ``None``. See Notes. + + Notes + ----- + If width and/or height is None, the approach will attempt to define + acceptable width and height. + + The ``verbose`` parameter can be changed either directly with defining + ``verbose`` input parameter or changing + ``cow.rcParams["show_verbose"]``. + + If run from the command line, this approach leverage matplotlib's + plot render to show a static png version of the image. If run inside + a jupyter notebook, this approache presents the actual svg + representation. + + See also + -------- + cow.patch.show : same function but for a cow.patch object """ + # updating width and height if necessary (some combine is none) + width, height = self._default_size(width=width, height=height) + + # global default for verbose (if not provided by the user) + if verbose is None: + verbose = rcParams["show_verbose"] + + svg_obj, (actual_width_pt, actual_height_pt) = \ + self._svg(width_pt=from_inches(width, "pt", dpi=dpi), + height_pt=from_inches(height, "pt", dpi=dpi)) + + _show_image(svg_obj, + width=to_inches(actual_width_pt, "pt", dpi=dpi), + height=to_inches(actual_height_pt, "pt", dpi=dpi), + dpi=dpi, + verbose=verbose) + + def __str__(self): + self.show() + return "" % self.__hash__() + + def __repr__(self): + out = "label:\n" + " |" + re.sub("\n", "\n |", self.label) +\ + "\nelement_text:\n |" + self.element_text.__repr__() +\ + "\n_type:\n |\"" + self._type +"\"" + + return "" % self.__hash__() + "\n" + out + + def __hash__(self): + return hash(tuple(self.__dict__)) + +class cow_title(themeable): + """ + text class for cow.patch titles + + Parameters + ---------- + theme_element : element_text + """ + pass + +class cow_subtitle(themeable): + """ + text class for cow.patch subtitles + + Parameters + ---------- + theme_element : element_text + """ + pass + +class cow_caption(themeable): + """ + text class for cow.patch captions + + Parameters + ---------- + theme_element : element_text + """ + pass + +class cow_tag(themeable): + """ + text class for cow.patch tags + + Parameters + ---------- + theme_element : element_text + """ + pass + +class cow_text(cow_title, cow_subtitle, cow_caption, cow_tag): + """ + default text for cow.text object + + Parameters + ---------- + theme_element : element_text + """ + + @property + def rcParams(self): + rcParams = super().rcParams + + family = self.properties.get('family') + style = self.properties.get('style') + weight = self.properties.get('weight') + size = self.properties.get('size') + color = self.properties.get('color') + + if family: + rcParams['font.family'] = family + if style: + rcParams['font.style'] = style + if weight: + rcParams['font.weight'] = weight + if size: + rcParams['font.size'] = size + rcParams['xtick.labelsize'] = size + rcParams['ytick.labelsize'] = size + rcParams['legend.fontsize'] = size + if color: + rcParams['text.color'] = color + + return rcParams + + + +def theme_get(): + """ + Creates a theme that contains defaults for cow related themeables + added to a p9.theme_get if needed. + + From plotnine: + + The default theme is the one set (using :func:`theme_set`) by + the user. If none has been set, then :class:`theme_gray` is + the default. + """ + return theme_complete(p9.theme_get()) + + +def theme_complete(other): + """ + a object that can be right added to another p9.theme and + will complete this p9.theme (relative to cow's default rcParms) + + """ + base = copy.deepcopy(other) + for cow_key in ["cow_text", "cow_title", "cow_subtitle", + "cow_caption", "cow_tag"]: + if cow_key not in base.themeables.keys() and \ + cow_key in rcParams.keys(): + base += theme(**{cow_key: rcParams[cow_key]}) + else: + current_et = base.themables[cow_key] + updated_et = rcParams[cow_key].merge(current_et) + base += theme(**{cow_key: updated_et}) + return base - svg_obj = self._create_svg_object(width=width, height=height) - svg_obj.save(filename) - plt.close() # do we need this? diff --git a/src/cowpatch/utils.py b/src/cowpatch/utils.py index ce74457..a423856 100644 --- a/src/cowpatch/utils.py +++ b/src/cowpatch/utils.py @@ -169,6 +169,7 @@ def inherits_plotnine(other): return "plotnine" == parent + def _transform_size_to_pt(size_string_tuple): """ takes string with unit and converts it to a float w.r.t pt @@ -238,3 +239,183 @@ def _flatten_nested_list(x): else: out.append(xi) return out + + +_arabic_roman_map = [(1000, 'M'), (900, 'CM'), (500, 'D'), (400, 'CD'), (100, 'C'), (90, 'XC'), + (50, 'L'), (40, 'XL'), (10, 'X'), (9, 'IX'), (5, 'V'), (4, 'IV'), (1, 'I')] +def _to_roman_numerials(x, caps = False): + """ + Arguments + --------- + x : int + arabic numeral + caps : boolean + if output should be capitalized + + Returns + ------- + out : str + roman numeral + """ + # from https://stackoverflow.com/a/40274588 + out = '' + + if x > 3999: + raise ValueError("current implimentation only works for input <= 3999") + + while x > 0: + for i, r in _arabic_roman_map: + while x >= i: + out += r + x -= i + + if caps: + return out + else: + return out.lower() + +_alphabet = "abcdefghijklmnopqrstuvwxyz" +def _to_alphabet_representation(x, caps = False): + """ + Arguments + --------- + x : int + arabic numeral + caps : boolean + if output should be capitalized + + Returns + ------- + out : str + alphabet numeric representation + """ + out = '' + while x > 0: + r = x % 26 + out = _alphabet[r-1] + out + x = x // 26 + + if caps: + return out.upper() + else: + return out + +def _string_tag_format(max_level=0): + out = '{0}' + level = 1 + while level <= max_level: + out += ".{"+str(level)+"}" + level +=1 + + return out + + + +def _overall_scale_recommendation_single(image_new_size, + text_inner_size, + text_extra_size, + original_overall_size): + """ + proposal an overall size based on inner image needs and text annotation + sizes + + Arguments + --------- + image_new_size : tuple + size 2 float tuple with the requested minimum size the inner image can + be (relative to the image) + text_inner_size : tuple + size 2 float tuple with the requested minimum size the inner image can + be (relative to the surrounding text) + text_extra_size : tuple + size 2 float tuple with extra width and height for surrounding text, + beyond the inner image + original_overall_size : tuple + size 2 float tuple of original size provided for the overall image + + Returns + ------- + out1 : tuple + size 2 length tuple of newly requested size + out2 : float + fraction scaling of new size to old size + + Notes + ----- + This function assumes at least one of the new image_new_size values are + greater than the original requested size (original_overall_size - + text_extra_sizes) + """ + + min_inner_size_needed = tuple([np.max([image_new_size[i], text_inner_size[i]]) + for i in [0,1]]) + + min_overall_size_needed = tuple(np.array(min_inner_size_needed) +\ + np.array(text_extra_size)) + + size_ratio = original_overall_size[0] / original_overall_size[1] + + out_array = np.zeros(2) + + if min_overall_size_needed[1] > 1/size_ratio * min_overall_size_needed[0]: + out_array[1] = min_overall_size_needed[1] + out_array[0] = size_ratio * min_overall_size_needed[1] + else: + out_array[0] = min_overall_size_needed[0] + out_array[1] = 1/size_ratio * min_overall_size_needed[0] + + return tuple(out_array), out_array[0]/original_overall_size[0] + +def _overall_scale_recommendation_patch(interior_image_scalings, + text_inner_size, + text_extra_size, + original_overall_size): + """ + proposal an overall size for an arangement based on information about + interior image and overall text + + Arguments + --------- + interior_image_scalings : list + list of floats. Each value is the requested scaling of a inner image + (as a function of the image and the associated tag information) + text_inner_size : tuple + size 2 float tuple with the requested minimum size the inner image can + be (relative to the surrounding text) + text_extra_size : tuple + size 2 float tuple with extra width and height for surrounding text, + beyond the inner image + original_overall_size : tuple + size 2 float tuple of original size provided for the overall image + + Returns + ------- + out1 : tuple + size 2 length tuple of newly requested size + out2 : float + fraction scaling of new size to old size + + Notes + ----- + This function assumes at least one of the new image_new_size values are + greater than the original requested size (original_overall_size - + text_extra_sizes) + """ + original_inner_size = tuple(np.array(original_overall_size) - \ + np.array(text_extra_size)) + + inner_new_size_request = \ + np.array(original_inner_size) * np.max(interior_image_scalings + [1]) + + out = \ + _overall_scale_recommendation_single(inner_new_size_request, + text_inner_size, + text_extra_size, + original_overall_size) + + return out + + + + + diff --git a/tests/test_annotation_elements.py b/tests/test_annotation_elements.py new file mode 100644 index 0000000..2b146de --- /dev/null +++ b/tests/test_annotation_elements.py @@ -0,0 +1,1638 @@ +import cowpatch as cow +from cowpatch.svg_utils import _save_svg_wrapper, _add_to_base_image +from cowpatch.utils import to_inches, inherits +import numpy as np +import pandas as pd +import copy +import plotnine as p9 +import plotnine.data as p9_data + +import pytest +from hypothesis import given, strategies as st, settings +from pytest_regressions import image_regression, data_regression +import itertools +import io +import svgutils.transform as sg + +def test_annotation__update_tdict_info(): + """ + tests annotation._update_tdict_info function + """ + random_an = cow.annotation() + # without a current value -------------------------------------------------- + ## title + + out_dict = random_an._update_tdict_info(t = "banana", + _type = "title") + + el_expected = cow.text("banana") + el_expected._define_type("cow_title") + + top_title = out_dict.pop("top") + + assert len(out_dict) == 0 and \ + top_title == el_expected, \ + "addition of just a top title does not return a title dictionary as "+\ + "expected (no current value)" + + ## subtitle + out_dict = random_an._update_tdict_info(t = "banana", + _type = "subtitle") + + el_expected = cow.text("banana") + el_expected._define_type("cow_subtitle") + + top_subtitle = out_dict.pop("top") + + assert len(out_dict) == 0 and \ + top_subtitle == el_expected, \ + "addition of just a top subtitle does not return a title dictionary "+\ + "as expected (no current value)" + + + # updating with np.nan vs None ------------------------- + current_t = {"top": "banana"} + bottom_title_start = cow.text("apple") + + ## title ----------- + ### base + bottom_title_start._define_type("cow_title") + current_t2 = {"top": "banana", "bottom": bottom_title_start, "left": "left", + "right": "right"} + + out_dict = random_an._update_tdict_info(t = "pear", + current_t = current_t, + _type = "title") + out_dict2 = random_an._update_tdict_info(t = {"bottom":"pear"}, + current_t = current_t2, + _type = "title") + el_expected = cow.text("pear") + el_expected._define_type("cow_title") + + top_title = out_dict.pop("top") + + assert len(out_dict) == 0 and \ + top_title == el_expected, \ + "addition of just a top title does not return a title dictionary "+\ + "as expected (override current value)" + + bottom_title = out_dict2.pop("bottom") + + assert len(out_dict2) == 3 and \ + bottom_title == el_expected, \ + "addition of just a bottom title does not return a title dictionary "+\ + "as expected (override current value, other dimensions exist)" + + + ### None + out_dict = random_an._update_tdict_info(t = None, + current_t = current_t, + _type = "title") + + assert out_dict == current_t, \ + "if t is None, then we should expect no change to current_t" + + out_dict2 = random_an._update_tdict_info(t = {"top": None, + "bottom":"pear"}, + current_t = current_t2, + _type = "title") + current_t2_copy = current_t2.copy() + bottom_title = out_dict2.pop("bottom") + top_title = out_dict2.pop("top") + el_top_expected = current_t2_copy.pop("top") + + assert bottom_title == el_expected and \ + top_title == el_top_expected, \ + "if a title in dictionary is None, then we should expect no change "+\ + "to relative to current_t" + + ### np.nan + out_dict = random_an._update_tdict_info(t = np.nan, + current_t = current_t, + _type = "title") + + assert out_dict is np.nan, \ + "if title is np.nan, we expect the title to be stored as np.nan" + + + out_dict2 = random_an._update_tdict_info(t = {"top": np.nan, + "bottom":"pear"}, + current_t = current_t2, + _type = "title") + bottom_title = out_dict2.pop("bottom") + top_title = out_dict2.get("top") + + assert top_title is None and \ + bottom_title == el_expected and \ + len(out_dict2) == 2,\ + "if title in dictionary is np.nan, we expect the title to be erased "+\ + "(other updates should still happen)" + + ## subtitle ----- + ### base + bottom_title_start._define_type("cow_subtitle") + current_t2 = {"top": "banana", "bottom": bottom_title_start, "left": "left", + "right": "right"} + + out_dict = random_an._update_tdict_info(t = "pear", + current_t = current_t, + _type = "subtitle") + out_dict2 = random_an._update_tdict_info(t = {"bottom":"pear"}, + current_t = current_t2, + _type = "subtitle") + el_expected = cow.text("pear") + el_expected._define_type("cow_subtitle") + + top_title = out_dict.pop("top") + + assert len(out_dict) == 0 and \ + top_title == el_expected, \ + "addition of just a top subtitle does not return a subtitle dictionary "+\ + "as expected (override current value)" + + bottom_title = out_dict2.pop("bottom") + + assert len(out_dict2) == 3 and \ + bottom_title == el_expected, \ + "addition of just a bottom subtitle does not return a subtitle dictionary "+\ + "as expected (override current value, other dimensions exist)" + + + ### None + out_dict = random_an._update_tdict_info(t = None, + current_t = current_t, + _type = "subtitle") + + assert out_dict == current_t, \ + "if t is None, then we should expect no change to current_t" + + out_dict2 = random_an._update_tdict_info(t = {"top": None, + "bottom":"pear"}, + current_t = current_t2, + _type = "subtitle") + current_t2_copy = current_t2.copy() + bottom_title = out_dict2.pop("bottom") + top_title = out_dict2.pop("top") + el_top_expected = current_t2_copy.pop("top") + + assert bottom_title == el_expected and \ + top_title == el_top_expected, \ + "if a subtitle in dictionary is None, then we should expect no change "+\ + "to relative to current_t" + + ### np.nan + out_dict = random_an._update_tdict_info(t = np.nan, + current_t = current_t, + _type = "subtitle") + assert out_dict is np.nan, \ + "if subtitle is np.nan, we expect the subtitle to be np.nan" + + + out_dict2 = random_an._update_tdict_info(t = {"top": np.nan, + "bottom":"pear"}, + current_t = current_t2, + _type = "subtitle") + bottom_title = out_dict2.pop("bottom") + top_title = out_dict2.get("top") + + assert top_title is None and \ + bottom_title == el_expected and \ + len(out_dict2) == 2,\ + "if subtitle in dictionary is np.nan, we expect the subtitle to be erased "+\ + "(other updates should still happen)" + +def test_annotation__add__(): + """ + test addition update for annotation objects + """ + + a1_input_full = dict(title = "banana", + subtitle = {"bottom":cow.text("apple")}, + caption = "peeler", + tags = ["core", "slice"], + tags_format = ("Fig {0}"), + tags_order = "auto", + tags_loc = "top", + tags_inherit="fix") + + a_update_options = dict(title = {"top":"alpha"}, + subtitle = {"left":cow.text("beta")}, + caption = "gamma", + tags = ["delta", "epsilon"], + tags_format = ("Figure {0}"), + tags_order = "input", + tags_loc = "bottom", + tags_inherit="fix") + + a1_full = cow.annotation(**a1_input_full) + + for inner_key in a_update_options.keys(): + if inner_key != "subtitle": + a2_input = a1_input_full.copy() + a2_input[inner_key] = a_update_options[inner_key] + else: + a2_input = a1_input_full.copy() + a2_input[inner_key] = {"bottom":cow.text("apple"), + "left":cow.text("beta")} + + update_dict = {inner_key: a_update_options[inner_key]} + + a1_inner = copy.deepcopy(a1_full) + + a2_expected = cow.annotation(**a2_input) + + a1_updated = a1_inner + cow.annotation(**update_dict) + + assert a2_expected == a1_updated, \ + ("updating with addition failed to perform as expected (attribute %s)" % inner_key) + +def test_annotation__get_tag_full(): + """ + test annotation's _get_tag_full + """ + mya = cow.annotation(title = {"top":"my plot", "bottom":"my plot's bottom"}, + subtitle = {"top":"my very special plot", + "bottom":"below my plot's bottom is the subtitle"}, + caption = "this is an example figure", + tags_format = ("Fig {0}", "Fig {0}.{1}"), tags = ("1", "a"), + tags_loc = "top") + + assert mya._get_tag_full(0) == cow.text("Fig 1", _type = "cow_tag"), \ + "expect tag creation to match tags_format structure (level 0), int" + assert mya._get_tag_full((0,)) == cow.text("Fig 1", _type = "cow_tag"), \ + "expect tag creation to match tags_format structure (level 0), tuple" + + assert mya._get_tag_full((1,2)) == cow.text("Fig 2.c", _type = "cow_tag"), \ + "expect tag creation to match tags_format structure (level 1)" + + with pytest.raises(Exception) as e_info: + mya._get_tag_full((1,2,3)) + # can't obtain a tag when we don't have formats that far down + +def test_annotation__get_tag(): + """ + test annotation's _get_tag function + """ + mya = cow.annotation(title = {"top":"my plot", "bottom":"my plot's bottom"}, + subtitle = {"top":"my very special plot", + "bottom":"below my plot's bottom is the subtitle"}, + caption = "this is an example figure", + tags_format = ("Fig {0}", "Fig {0}.{1}"), tags = ("1", "a"), + tags_loc = "top") + + assert mya._get_tag(0) == cow.text("Fig 1", _type = "cow_tag"), \ + "expect tag creation to match tags_format structure (level 0), int" + + index = np.random.choice(100) + assert mya._get_tag(index) == cow.text("Fig %i" % (index+1), _type = "cow_tag"), \ + "expect tag creation to match tags_format structure (level 0), int" + + + # can't obtain a tag when we don't have formats that far down + with pytest.raises(Exception) as e_info: + mya._get_tag((0,)) + + with pytest.raises(Exception) as e_info: + mya._get_tag((1,2)) + + with pytest.raises(Exception) as e_info: + mya._get_tag((1,2,3)) + + # list structure + mya2 = cow.annotation(title = {"top":"my plot", "bottom":"my plot's bottom"}, + subtitle = {"top":"my very special plot", + "bottom":"below my plot's bottom is the subtitle"}, + caption = "this is an example figure", + tags = (["banana", "apple"],), + tags_loc = "top") + + assert (mya2._get_tag(0) == cow.text("banana", _type = "cow_tag")) & \ + (mya2._get_tag(1) == cow.text("apple", _type = "cow_tag")), \ + "expect tag creation to match list structure (level 0), int" + + assert mya2._get_tag(3) == cow.text("", _type = "cow_tag"), \ + "expected tag of index beyond list length to be an empty tag "+\ + "(but not raise an error)" + + with pytest.raises(Exception) as e_info: + mya2._get_tag((0,)) + + with pytest.raises(Exception) as e_info: + mya2._get_tag((0,0)) + + +def test_annotation__get_tag_full_rotations(): + """ + test that annotation's _get_tag works correctly with rotation informatin + + """ + + mya = cow.annotation(title = {"top":"my plot", "bottom":"my plot's bottom"}, + subtitle = {"top":"my very special plot", + "bottom":"below my plot's bottom is the subtitle"}, + caption = "this is an example figure", + tags_format = ("Fig {0}", "Fig {0}.{1}"), tags = ("1", "a"), + tags_loc = "left") + + assert mya._get_tag_full(0) == \ + cow.text("Fig 1", _type = "cow_tag")._additional_rotation(angle=90), \ + ("expect tag creation to match tags_format structure (level 0), int "+ + "(left rotation)") + assert mya._get_tag_full((0,)) == \ + cow.text("Fig 1", _type = "cow_tag")._additional_rotation(angle=90), \ + ("expect tag creation to match tags_format structure (level 0), tuple "+ + "(left rotation)") + + assert mya._get_tag_full((1,2)) == \ + cow.text("Fig 2.c", _type = "cow_tag")._additional_rotation(angle=90), \ + ("expect tag creation to match tags_format structure (level 1) "+ + "(left rotation)") + + with pytest.raises(Exception) as e_info: + mya._get_tag_full((1,2,3)) + # can't obtain a tag when we don't have formats that far down + + +def test_annotation__get_tag_rotations(): + """ + test that annotation's _get_tag works correctly with rotation informatin + """ + + mya = cow.annotation(title = {"top":"my plot", "bottom":"my plot's bottom"}, + subtitle = {"top":"my very special plot", + "bottom":"below my plot's bottom is the subtitle"}, + caption = "this is an example figure", + tags_format = ("Fig {0}", "Fig {0}.{1}"), tags = ("1", "a"), + tags_loc = "left") + + assert mya._get_tag(0) == \ + cow.text("Fig 1", _type = "cow_tag")._additional_rotation(angle=90), \ + ("expect tag creation to match tags_format structure (level 0), int "+ + "(left rotation)") + + + index = np.random.choice(100) + assert mya._get_tag(index) == \ + cow.text("Fig %i" % (index+1), _type = "cow_tag")._additional_rotation(angle=90), \ + ("expect tag creation to match tags_format structure (level 0), int "+ + "(left rotation)") + + + # can't obtain a tag when we don't have formats that far down + with pytest.raises(Exception) as e_info: + mya._get_tag((0,)) + + with pytest.raises(Exception) as e_info: + mya._get_tag((1,2)) + + with pytest.raises(Exception) as e_info: + mya._get_tag((1,2,3)) + + # list structure + mya2 = cow.annotation(title = {"top":"my plot", "bottom":"my plot's bottom"}, + subtitle = {"top":"my very special plot", + "bottom":"below my plot's bottom is the subtitle"}, + caption = "this is an example figure", + tags = (["banana", "apple"],), + tags_loc = "left") + + assert (mya2._get_tag(0) == + cow.text("banana", _type = "cow_tag")._additional_rotation(angle=90)) & \ + (mya2._get_tag(1) == + cow.text("apple", _type = "cow_tag")._additional_rotation(angle=90)), \ + ("expect tag creation to match list structure (level 0), int "+ + "(left rotation)") + + assert mya2._get_tag(3) == \ + cow.text("", _type = "cow_tag"), \ + ("expected tag of index beyond list length to be an empty tag "+ + "(but not raise an error) "+ + "(left rotation)") + + with pytest.raises(Exception) as e_info: + mya2._get_tag((0,)) + + with pytest.raises(Exception) as e_info: + mya2._get_tag((0,0)) + +def test_annotation__step_down_tags_info(): + """ + test for annnotation's _step_down_tags_info functionality + """ + junk_dict = dict(title = {"top":cow.text("banana", _type = "cow_title")}, + caption = "minion") + tag_dict = dict(tags = (["apple", "pear"], "i", "a"), + tags_format = ("", "{0}: {1}", "{0}: {1}.{2}"), + tags_order="auto", tags_loc="top", + tags_inherit="override") + + partial_a = cow.annotation(**tag_dict) + full_a = cow.annotation(**junk_dict) + partial_a + + stepdown_ann1 = full_a._step_down_tags_info(parent_index = 1) + stepdown_ann2 = partial_a._step_down_tags_info(parent_index = 1) + + assert stepdown_ann1 == stepdown_ann2, \ + "stepdown should pass only tag information" + + stepdown_tag_dict = dict(tags = ("i", "a"), + tags_format = ("pear: {0}", "pear: {0}.{1}"), + tags_order = "auto", + tags_loc = "top") + + stepdown_tag_expected = cow.annotation(**stepdown_tag_dict) + + assert stepdown_tag_expected == stepdown_ann2, \ + "stepdown with list for first layer incorrect" + + stepdown_ann_second = stepdown_ann2._step_down_tags_info(parent_index=5) + + stepdown_tag_dict_second = dict(tags = ("a",), + tags_format = ("pear: vi.{0}",), + tags_order = "auto", + tags_loc = "top") + + stepdown_tag_second_expected = cow.annotation(**stepdown_tag_dict_second) + + assert stepdown_tag_second_expected == stepdown_ann_second, \ + "stepdown with roman numeral iteration for second layer incorrect" + +def test_annotation_passing(): + """ + tests that annotation tag's pass annotations correctly + + these are stastic test-driven development tests + """ + g0 = p9.ggplot(p9_data.mpg) +\ + p9.geom_bar(p9.aes(x="hwy")) +\ + p9.labs(title = 'Plot 0') + + g1 = p9.ggplot(p9_data.mpg) +\ + p9.geom_point(p9.aes(x="hwy", y = "displ")) +\ + p9.labs(title = 'Plot 1') + + g2 = p9.ggplot(p9_data.mpg) +\ + p9.geom_point(p9.aes(x="hwy", y = "displ", color="class")) +\ + p9.labs(title = 'Plot 2') + + g3 = p9.ggplot(p9_data.mpg[p9_data.mpg["class"].isin(["compact", + "suv", + "pickup"])]) +\ + p9.geom_histogram(p9.aes(x="hwy"),bins=10) +\ + p9.facet_wrap("class") + + # vis 0 + + vis_inner0 = cow.patch([g2, g3]) +\ + cow.layout(nrow = 1) +\ + cow.annotation(tags = ["banana", "apple"]) + + vis0 = cow.patch([g0,g1, vis_inner0]) +\ + cow.layout(design = np.array([[0,1], + [2,2]])) +\ + cow.annotation(tags = ("Fig {0}", "Fig {0}.{1}"), + tags_format = ("1", "a"), + title = "no passing through") + # see docs for expectation... + + # vis 1 + + vis_inner1 = vis_inner0 + cow.annotation(tags_inherit = "override") + + vis1 = cow.patch([g0,g1, vis_inner1]) +\ + cow.layout(design = np.array([[0,1], + [2,2]])) +\ + cow.annotation(tags = ("Fig {0}", "Fig {0}.{1}"), + tags_format = ("1", "a"), + title = "passing through") + # see docs for expectation... + + # vis 2 + + vis_inner2 = vis_inner0 + cow.annotation(tags_inherit = "fix", + tags = ("Fig 3.{0}",), + tags_format = ("a",)) + + vis2 = cow.patch([g0,g1, vis_inner2]) +\ + cow.layout(design = np.array([[0,1], + [2,2]])) +\ + cow.annotation(tags = ["Fig 1", "Fig 2"], + tags_format = ("1", "a"), + title = "no pass through, top defined, bottom generated") + # see docs for expectation... + +def test_annotation__clean_up_attributes(): + """ + test annotation's _clean_up_attributes + """ + + + a = cow.annotation(tags = np.nan, tags_order = "input") + b = cow.annotation(tags = None, tags_order = "input") + + a._clean_up_attributes() + + assert a == b, \ + "we expect that np.nans are cleaned up after running the "+\ + "_clean_up_attributes function" + + for key in a.__dict__.keys(): + if key not in ["tags_inherit", "tags_order", "tags_depth", + "_tags_format"]: + + a2 = cow.annotation(**{key: np.nan}) + b2 = cow.annotation(**{key: None}) + + assert a2 != b2 and a2.__dict__[key] != b2.__dict__[key], \ + "before running _clean_up_attributes function, we expect "+\ + "the difference in specific key value to be not equal... " +\ + "(key = %s)" % key + + a2._clean_up_attributes() + + assert a2 == b2, \ + "we expect that np.nans are cleaned up after running the "+\ + "_clean_up_attributes function " +\ + "(key = %s)" % key + + if key == "_tags_format": + a2 = cow.annotation(**{"tags_format": np.nan}) + b2 = cow.annotation(**{"tags_format": None}) + + assert a2 != b2 and a2.__dict__[key] != b2.__dict__[key], \ + "before running _clean_up_attributes function, we expect "+\ + "the difference in specific key value to be not equal... " +\ + "(key = %s)" % key + + a2._clean_up_attributes() + + assert a2 == b2, \ + "we expect that np.nans are cleaned up after running the "+\ + "_clean_up_attributes function " +\ + "(key = %s)" % key + +@pytest.mark.parametrize("location", ["top", "left", "right", "bottom"]) +@pytest.mark.parametrize("_type", ["title", "subtitle", "caption"]) +def test_annotation__get_title(location, _type): + """ + test annotation's _get_title function + """ + full_ann = cow.annotation(title = {"top": cow.text(label="top title", + element_text=p9.element_text(angle=0)), + "bottom": cow.text(label="bottom title", + element_text=p9.element_text(angle=0)), + "left": cow.text(label="left title", + element_text=p9.element_text(angle=0)), + "right": cow.text(label="right title", + element_text=p9.element_text(angle=0))}, + subtitle = {"top": cow.text(label="top subtitle", + element_text=p9.element_text(angle=0)), + "bottom": cow.text(label="bottom subtitle", + element_text=p9.element_text(angle=0)), + "left": cow.text(label="left subtitle", + element_text=p9.element_text(angle=0)), + "right": cow.text(label="right subtitle", + element_text=p9.element_text(angle=0))}, + caption = "caption", + tags = ("0",)) + if _type in ["title", "subtitle"]: + attributes_dict = dict(tags = ("0",)) + attributes_dict[_type] = {"top": "top level", + "bottom": "bottom level", + "left": "left level", + "right": "right level"} + all_single_level_ann = cow.annotation(**attributes_dict) + + attributes_dict2 = {_type: {location: "current (sub)title"} } + single_ann = cow.annotation(**attributes_dict2) + else: + all_single_level_ann = cow.annotation(caption = "caption", + tags = ("0",)) + single_ann = cow.annotation(caption = "caption") + + + # without value ---------- + no_ann = cow.annotation(tags = ("0",)) + + if _type in ["title", "subtitle"]: + n_element = no_ann._get_title(_type=_type, location=location) + + assert n_element is None, \ + "no element returned if none defined, type %s, loc %s" % (_type, location) + else: + n_element1 = no_ann._get_title(_type=_type, location=location) + + assert n_element1 is None, \ + "no element returned if none defined, type %s, loc %s" % (_type, location) + + n_element2 = no_ann._get_title(_type=_type) + + assert n_element2 is None, \ + "no element returned if none defined, type %s, loc: default" % (_type) + + # with values ---------- + et_full = full_ann._get_title(_type=_type, location=location) + et_level_full = all_single_level_ann._get_title(_type=_type, location=location) + et_single = single_ann._get_title(_type=_type, location=location) + + # label check + if _type in ["title", "subtitle"]: + assert et_full.label == ("%s %s" % (location, _type)), \ + "expected %s element's label grabbed doesn't match static type (full)" % _type + assert et_level_full.label == ("%s level" % (location)), \ + "expected %s element's label grabbed doesn't match static type (level full)" % _type + assert et_single.label == "current (sub)title", \ + "expected %s element's label grabbed doesn't match static type (single)" % _type + else: + assert et_full.label == ("%s" % (_type)), \ + "expected %s element's label grabbed doesn't match static type (full)" % _type + assert et_level_full.label == ("%s" % (_type)), \ + "expected %s element's label grabbed doesn't match static type (level full)" % _type + assert et_single.label == ("%s" % (_type)), \ + "expected %s element's label grabbed doesn't match static type (single)" % _type + # text._type check + for et_i, et in enumerate([et_full, et_level_full, et_single]): + assert et._type == "cow_" + _type, \ + ("expected %s element grabbed to inherit "+ + "text _type cow_%s, et_i: %i" % (_type, _type, et_i)) + + # rotation angle check + for et_i, et in enumerate([et_full, et_level_full, et_single]): + if _type in ["title", "subtitle"]: + if location in ["left", "right"]: + assert np.allclose(et.element_text.theme_element.properties["rotation"], 90), \ + ("if type: %s, expect location %s to "+ + "have 90 degree rotation (et_i %i)" % (_type, location, et_i)) + else: + # None if no element_text has been defined + assert (et.element_text is None) or \ + np.allclose(et.element_text.theme_element.properties["rotation"], 0), \ + ("if type: %s, expect location %s to "+ + "have 90 degree rotation (et_i %i)" % (_type, location, et_i)) + else: + # None if no element_text has been defined + assert (et.element_text is None) or \ + np.allclose(et.element_text.theme_element.properties["rotation"], 0), \ + ("if type: caption, expect location %s show not alter "+ + "rotation as caption is always on bottom (et_i %i)" % (location, et_i)) + + + # save image + +@pytest.mark.parametrize("location", ["top", "left", "right", "bottom"]) +@pytest.mark.parametrize("_type", ["title", "subtitle", "caption"]) +@pytest.mark.parametrize("et_i", [0,1,2]) +def test_annotation__get_title_ir(image_regression, location, _type, et_i): + """ + test annotation _get_title -- image regression tests + """ + + # data set-up --- + + full_ann = cow.annotation(title = {"top": "top title", + "bottom": "bottom title", + "left": "left title", + "right": "right title"}, + subtitle = {"top": "top subtitle", + "bottom": "bottom subtitle", + "left": "left subtitle", + "right": "right subtitle"}, + caption = "caption", + tags = ("0",)) + if _type in ["title", "subtitle"]: + attributes_dict = dict(tags = ("0",)) + attributes_dict[_type] = {"top": "top level", + "bottom": "bottom level", + "left": "left level", + "right": "right level"} + all_single_level_ann = cow.annotation(**attributes_dict) + + attributes_dict2 = {_type: {location: "current (sub)title"} } + single_ann = cow.annotation(**attributes_dict2) + else: + all_single_level_ann = cow.annotation(caption = "caption", + tags = ("0",)) + single_ann = cow.annotation(caption = "caption") + + + ann_options = [full_ann, all_single_level_ann, single_ann] + + + # regression test ---------- + ann = ann_options[et_i] + et = ann._get_title(_type=_type, location=location) + + with io.BytesIO() as fid2: + et.save(filename=fid2, _format = "png", verbose=False) + image_regression.check(fid2.getvalue(), diff_threshold=.1) + + + + +@pytest.mark.parametrize("location", ["title", "subtitle", "caption"]) +def test_annontation__calculate_margin_sizes_basic(location): + """ + test annotation's _calculate_margin_sizes, static / basic + + Details + ------- + loops over a single type (title, subtitle, caption) - which means + everything focuses on *top* functionality. Additionally, not a one + after actual numberical assessments, more comparisons with different + sizes for default cow.text types. + """ + a0 = cow.annotation(**{location:"example title"}) + a0_size_dict = a0._calculate_margin_sizes(to_inches=False) + + # check that tags don't impact __calculate_margin_sizes + a1 = a0 + cow.annotation(tags = ["banana", "apple"]) + a2 = a0 + cow.annotation(tags_format = ("Fig {0}", "Fig {0}.{1}"), + tags = ("1", "a")) + a1_size_dict = a1._calculate_margin_sizes(to_inches=False) + a2_size_dict = a2._calculate_margin_sizes(to_inches=False) + + assert a0_size_dict == a1_size_dict and \ + a0_size_dict == a2_size_dict, \ + "tag alterations shouldn't change %s margin sizes" % location + + + # top or bottom structure for basic text input: + assert a0_size_dict["extra_used_width"] == 0 and \ + a0_size_dict["min_inner_height"] == 0 and \ + a0_size_dict["top_left_loc"][0] == 0, \ + ("top or bottom structure text (only) with %s should have a "+\ + "clear set of zeros for margins") % location + + # title/subtitle vs caption sizing structure + if location in ["title", "subtitle"]: + assert a0_size_dict["min_full_width"] == 0, \ + ("top or bottom structure text (only) with %s should only " +\ + "population min_inner_width, not min_full_width based as " +\ + "the %s should be directly over the image") % (location, location) + else: + assert a0_size_dict["min_inner_width"] == 0, \ + ("top or bottom structure text (only) with %s should only " +\ + "population min_full_width, not min_inner_width based as " +\ + "the %s be able to span all other margins") % (location, location) + + + # text height (relative to default rcParams) + # https://stackoverflow.com/questions/41336177/font-size-vs-line-height-vs-actual-height + if location in ["title", "subtitle"]: + # caption doesn't meet this expectation and the world says implimentation like this can happen + assert a0_size_dict["extra_used_height"]/13.5 == \ + cow.rcParams["cow_%s"%location].properties["size"]/\ + cow.rcParams["cow_title"].properties["size"], \ + ("font size for %s correctly aligns with expection relative to "+\ + "output size for title") % location + + # text width (relative to height) + if location in ["title", "subtitle"]: + assert a0_size_dict["min_inner_width"]/a0_size_dict["extra_used_height"] > 5,\ + ("expected the length of the %s text to be > 5x the height of "+\ + "the text") % location + else: + assert a0_size_dict["min_full_width"]/a0_size_dict["extra_used_height"] > 5,\ + ("expected the length of the %s text to be > 5x the height of "+\ + "the text") % location + +@pytest.mark.parametrize("types, location1, location2", + list(itertools.product( + list(itertools.combinations(["title", "subtitle", "caption"], r=2)), + ["top", "bottom", "left", "right"], + ["top", "bottom", "left", "right"])) + ) +def test_annotation__calculate_margin_sizes_non_basic(types, location1, location2): + """ + test annnotation's _calculate_margin_sizes (non-basic) + + Details + ------- + types : tuple + of title, subtitle, caption (no dupplicates) + location1, location2 : str + different location decisions, can duplicate. + + The checks across these options don't examine static values but more + how alterations are reflected into the code + """ + # lets of left, right, top, bottom + some other option. + # allow for overrides and combinations + + # force all sizes to be the same (to deal with different structures) + locations = (location1, location2) + input_dict = dict() + + for t_idx, _type in enumerate(types): + if _type != "caption": + input_dict[_type] = \ + {locations[t_idx]: cow.text("text", + element_text=p9.element_text(size=12))} + else: + input_dict[_type] = cow.text("text", + element_text=p9.element_text(size=12)) + + + a0 = cow.annotation(**input_dict) + + a0_size_dict = a0._calculate_margin_sizes(to_inches=False) + + # check that tags don't impact __calculate_margin_sizes + a1 = a0 + cow.annotation(tags = ["banana", "apple"]) + a2 = a0 + cow.annotation(tags_format = ("Fig {0}", "Fig {0}.{1}"), + tags = ("1", "a")) + a1_size_dict = a1._calculate_margin_sizes(to_inches=False) + a2_size_dict = a2._calculate_margin_sizes(to_inches=False) + + assert a0_size_dict == a1_size_dict and \ + a0_size_dict == a2_size_dict, \ + "tag alterations shouldn't change %s margin sizes" % location + + # image location matches left & top shifts + count_left = np.sum([locations[l_idx] == "left" for l_idx in [0,1] + if types[l_idx] != "caption"]) + count_width_plus = np.sum([locations[l_idx] in ["left", "right"] + for l_idx in [0,1] if types[l_idx] != "caption"]) + count_top = np.sum([locations[l_idx] == "top" for l_idx in [0,1] + if types[l_idx] != "caption"]) + count_height_plus = np.sum([(locations[l_idx] in ["top", "bottom"]) or\ + (types[l_idx] == "caption") for l_idx in [0,1]]) + + if count_width_plus > 0: + left_side = a0_size_dict["extra_used_width"] * \ + count_left/count_width_plus + else: + left_side = 0 + + if count_height_plus > 0: + top_side = a0_size_dict["extra_used_height"] *\ + count_top/count_height_plus + else: + top_side = 0 + + assert a0_size_dict["top_left_loc"] == (left_side, top_side), \ + ("expected image starting location doesn't match expectation "+\ + "relative to text on top and left ({t1}:{l1}, {t2}:{l2}").\ + format(t1=types[0], t2=types[1], + l1=locations[0], l2=locations[1]) + +@pytest.mark.parametrize("_type", ["title", "subtitle", "caption"]) +@pytest.mark.parametrize("location", ["top", "bottom", "left", "right"]) +def test_annotation__calculate_margin_sizes_static(data_regression, _type, location): + """ + test annotation's _calculate_margin_sizes - focuses on locational rotation + differences by using data_regression + + Details: + -------- + we also check the relative scaling of all attributes to ensure to_inches + parameter is working correctly + """ + if _type in ["title", "subtitle"]: + parameter_dict = {_type: + {location: "type: %s, location: %s" % (_type, location)}} + else: + parameter_dict = {_type: "type: %s, location: %s" % (_type, location)} + + ann = cow.annotation(**parameter_dict) + + + out_info = ann._calculate_margin_sizes(to_inches=False) + out_info_in = ann._calculate_margin_sizes(to_inches=True) + + # to_inches comparison ---------- + for key in out_info.keys(): + if key != "top_left_loc": + if out_info_in[key] == 0: + assert out_info_in[key] == out_info[key], \ + ("to_inches =T/F should be a expicit scaling of 72 between "+ + "inches and pt, type: %s, location %s, key: %s" % (_type, location, key)) + else: + assert np.allclose(out_info_in[key]*72, out_info[key]), \ + ("to_inches =T/F should be a expicit scaling of 72 between "+ + "inches and pt, type: %s, location %s, key: %s" % (_type, location, key)) + else: + for idx in [0,1]: + if out_info_in[key] == 0: + assert out_info_in[key][idx] == out_info[key][idx], \ + ("to_inches =T/F should be a expicit scaling of 72 between "+ + "inches and pt, type: %s, location %s, key: %s, idx: %i" % (_type, location, key, idx)) + else: + assert np.allclose(out_info_in[key][idx]*72, out_info[key][idx]), \ + ("to_inches =T/F should be a expicit scaling of 72 between "+ + "inches and pt, type: %s, location %s, key: %s, idx: %i" % (_type, location, key, idx)) + + data_dict_out = {} + for key in out_info.keys(): + if key != "top_left_loc": + data_dict_out[key] = str(out_info[key]) + else: + data_dict_out[key] = \ + {str(i): str(value) for i, value in enumerate(out_info[key])} + + data_regression.check(data_dict_out) + + +@pytest.mark.parametrize("location", ["top", "bottom", "left", "right"]) +def test_annotation__calculate_tag_margin_sizes(data_regression, location): + """ + test annotation's_calculate_tag_margin_sizes + + Details + ------- + Deals with + (1) if a tag should be created, + (2) nested versus non-nested + (3) different location types [TODO] rotation structures + """ + + a0_list = cow.annotation(tags = ["banana", "apple"], + tags_loc = location) + a0_list_nest = cow.annotation(tags = (["young", "old"], + ["harry", "hermione", "ron"]), + tags_loc = location, + tags_format = ("{0}", "{0}{1}")) + + a0_tuple_nest = cow.annotation(tags_format = ("Fig {0}", "Fig {0}.{1}"), + tags = ("1", "a"), + tags_loc = location) + + # check that titles don't impact __calculate_tag_margin_sizes + a0_all = [a0_list, a0_list_nest, a0_tuple_nest] + a_info_str = ["list", "nested-list", "nested-tuple"] + + data_reg_dict = {} + for a_idx, a0 in enumerate(a0_all): + a0_title = a0 + cow.annotation(title = "Conformal Inference") + + + base = a0._calculate_tag_margin_sizes(index = 1) + base_plus_title = a0_title._calculate_tag_margin_sizes(index = 1) + + assert base == base_plus_title, \ + ("title attributes shouldn't impact the sizing structure for a "+\ + "tag (structure: %s, loc %s)") % (a_info_str[a_idx], location) + + # data regression tracking + data_reg_dict[str(a_idx)+"_base"] = \ + {key:str(value) for key, value in base.items()} + + if a0.tags_depth == 2: + # not title + a0_s = a0._step_down_tags_info(1) + a0_title_s = a0_title._step_down_tags_info(1) + + base_s = a0_s._calculate_tag_margin_sizes(index = 0) + base_plus_title_s = a0_title_s._calculate_tag_margin_sizes(index = 0) + + assert base_s == base_plus_title_s, \ + ("title attributes shouldn't impact the sizing structure for a "+\ + "tag - stepdown (structure: %s, loc %s)") % (a_info_str[a_idx], location) + + # data regression tracking + data_reg_dict[str(a_idx)+"_base_s"] = \ + {key:str(value) for key, value in base_s.items()} + + + # fundamental + base_f = a0._calculate_tag_margin_sizes(index = 1, + fundamental=True) + base_plus_title_f = a0_title._calculate_tag_margin_sizes(index = 1, + fundamental=True) + + assert base_f == base_plus_title_f and \ + base_f != {'min_inner_width': 0, + 'min_full_width': 0, + 'extra_used_width': 0, + 'min_inner_height': 0, + 'extra_used_height': 0, + 'top_left_loc': (0.0, 0)}, \ + ("title attributes shouldn't impact the sizing structure for "+\ + "a tag (fundamental), and fundamental works correctly... "+\ + "(structure: %s, loc %s)") % (a_info_str[a_idx], location) + + # when a tag shouldn't be created + if a0.tags_depth == 1: + a0_s = a0._step_down_tags_info(1) + a0_title_s = a0_title._step_down_tags_info(1) + base_e = a0_s._calculate_tag_margin_sizes(index = 0) + base_plus_title_e = a0_title_s._calculate_tag_margin_sizes(index = 0) + else: + a0_s = a0._step_down_tags_info(1)._step_down_tags_info(0) + a0_title_s = a0_title._step_down_tags_info(1)._step_down_tags_info(0) + base_e = a0._calculate_tag_margin_sizes(index = 1) + base_plus_title_e = a0_title._calculate_tag_margin_sizes(index = 1) + + assert base_e == base_plus_title_e and \ + base_e == {'min_inner_width': 0, + 'min_full_width': 0, + 'extra_used_width': 0, + 'min_inner_height': 0, + 'extra_used_height': 0, + 'top_left_loc': (0.0, 0)}, \ + ("if requesting tag for depth below max depth, no tag would be "+\ + "created - aka margins == 0, (with and without title) " +\ + "(structure: %s, loc %s)") % (a_info_str[a_idx], location) + + # not fundamental + if a0.tags_depth == 2: + base_nf = a0._calculate_tag_margin_sizes(index = 1, + fundamental=False) + base_plus_title_nf = a0_title._calculate_tag_margin_sizes(index = 1, + fundamental=False) + + assert base_nf == base_plus_title_nf and \ + base_nf == {'min_inner_width': 0, + 'min_full_width': 0, + 'extra_used_width': 0, + 'min_inner_height': 0, + 'extra_used_height': 0, + 'top_left_loc': (0.0, 0)}, \ + ("if not a fundamental tag, no tag should be created "+\ + "(title or not) "+\ + "(structure: %s, loc %s)") % (a_info_str[a_idx], location) + + + # list_nest vs tuple_nest + a0 = a0_list_nest + a0_title = a0 + cow.annotation(title = "Conformal Inference") + + a0_s = a0._step_down_tags_info(1) + a0_title_s = a0_title._step_down_tags_info(1) + base_le = a0_s._calculate_tag_margin_sizes(index = 5) + base_plus_title_le = a0_title_s._calculate_tag_margin_sizes(index = 5) + + assert base_le == base_plus_title_le and \ + base_le == {'min_inner_width': 0, + 'min_full_width': 0, + 'extra_used_width': 0, + 'min_inner_height': 0, + 'extra_used_height': 0, + 'top_left_loc': (0.0, 0)}, \ + ("tags based in lists should have finite length of non-zero tags " +\ + "(structure: %s, loc %s)") % (a_info_str[a_idx], location) + + + a0 = a0_tuple_nest + a0_title = a0 + cow.annotation(title = "Conformal Inference") + + for t_idx in np.random.choice(100,2): + a0_s = a0._step_down_tags_info(1) + a0_title_s = a0_title._step_down_tags_info(1) + base_t = a0_s._calculate_tag_margin_sizes(index = t_idx) + base_plus_title_t = a0_title_s._calculate_tag_margin_sizes(index = t_idx) + + assert base_t == base_plus_title_t and \ + base_t != {'min_inner_width': 0, + 'min_full_width': 0, + 'extra_used_width': 0, + 'min_inner_height': 0, + 'extra_used_height': 0, + 'top_left_loc': (0.0, 0)}, \ + ("tags based in auto creation shouldn't have finite length of "+\ + "non-zero tags (structure: %s, loc %s, t_idx: %i)") % (a_info_str[a_idx], location, t_idx) + + data_regression.check(data_reg_dict) + + +def test_annotation__get_tag_and_location(): + """ + test annotation's _get_tag_and_location + + Details + ------- + This group of tests is static, but examines a range of cases: + 1. nested versus single depth tags + 2. list versus infinite length tags + 3. all 4 location descriptions (and correctly deals with 90 degree rotation) + 4. across fundamental & non-fundamental tags + + We also look at 4 specical cases: + 1. [Non-error] full_index is None (fundamental & not) + 2. [Non-error] full_index type is integer (fundamental & not) + + 3. [Error] index != full_index[-1] + 4. [Error] width and/or height is too small + + Additional Notes + ---------------- + We also check manual regression tests on some min_sizing of text objects + """ + + a0_list_t = cow.text("banana", _type = "cow_tag") + a0_list_nest_t = cow.text("Age: old, Name: harry", _type = "cow_tag") + a0_tuple_nest_t = cow.text("Fig 5.b", _type = "cow_tag") + + ms_a0_list_t = a0_list_t._min_size() + ms_a0_list_nest_t = a0_list_nest_t._min_size() + ms_a0_tuple_nest_t = a0_tuple_nest_t._min_size() + + # "regression check" for default tag elements + assert np.allclose(ms_a0_list_t, (49.5, 13.5)) and \ + np.allclose(ms_a0_list_nest_t, (146.25, 13.5)) and \ + np.allclose(ms_a0_tuple_nest_t, (43.96875, 13.5)), \ + ("min_sizes of static examples should be fixed against " + + "current value (simple regression check)") + + # rotationed objects for left/right structure definition + a0_list_t_r = a0_list_t._additional_rotation(angle=90) + a0_list_nest_t_r = a0_list_nest_t._additional_rotation(angle=90) + a0_tuple_nest_t_r = a0_tuple_nest_t._additional_rotation(angle=90) + + ms_a0_list_t_r = a0_list_t_r._min_size() + ms_a0_list_nest_t_r = a0_list_nest_t_r._min_size() + ms_a0_tuple_nest_t_r = a0_tuple_nest_t_r._min_size() + + # another check on rotation sizes + assert np.allclose(ms_a0_list_t, ms_a0_list_t_r[::-1]) and \ + np.allclose(ms_a0_list_nest_t, ms_a0_list_nest_t_r[::-1]) and \ + np.allclose(ms_a0_tuple_nest_t, ms_a0_tuple_nest_t_r[::-1]), \ + ("static examples sizes post 90 degree rotation should just be" + + "flipped") + + + # actual checks set-up + standard_width, standard_height = 175, 175 + + tag_loc_d = { + "a0_list_d": {"top": (0,0), "left": (0,0), + "right": (standard_width - ms_a0_list_t_r[0],0), + "bottom": (0, standard_height - ms_a0_list_t[1])}, + "a0_list_nest_d": {"top": (0,0), "left": (0,0), + "right": (standard_width - ms_a0_list_nest_t_r[0],0), + "bottom": (0, standard_height - ms_a0_list_nest_t[1])}, + "a0_tuple_nest_d": {"top": (0,0), "left": (0,0), + "right": (standard_width - ms_a0_tuple_nest_t_r[0],0), + "bottom": (0, standard_height - ms_a0_tuple_nest_t[1])} + } + + image_loc_d = { + "a0_list_d": {"bottom": (0,0), "right": (0,0), + "left": (ms_a0_list_t_r[0], 0), + "top": (0, ms_a0_list_t[1])}, + "a0_list_nest_d": {"bottom": (0,0), "right": (0,0), + "left": (ms_a0_list_nest_t_r[0], 0), + "top": (0, ms_a0_list_nest_t[1])}, + "a0_tuple_nest_d": {"bottom": (0,0), "right": (0,0), + "left": (ms_a0_tuple_nest_t_r[0], 0), + "top": (0, ms_a0_tuple_nest_t[1])} + } + + # real output (tag_loc and image_loc) + for location in ["top", "bottom", "left", "right"]: + a0_list = cow.annotation(tags = ["banana", "apple"], + tags_loc = location) + a0_list_nest = cow.annotation(tags = (["young", "old"], + ["harry", "hermione", "ron"]), + tags_loc = location, + tags_format = ("Age: {0}", "Age: {0}, Name: {1}")) + + a0_tuple_nest = cow.annotation(tags_format = ("Fig {0}", "Fig {0}.{1}"), + tags = ("1", "a"), + tags_loc = location) + + ann_options = [a0_list, a0_list_nest, a0_tuple_nest] + ann_strings = ["a0_list_d", "a0_list_nest_d", "a0_tuple_nest_d"] + full_index_options = [(1,), (1,0), (5,1)] + + # full input is provided (no None) + for ann_index in [0,1,2]: + ann = ann_options[ann_index] + ann_str = ann_strings[ann_index] + full_index = full_index_options[ann_index] + index = full_index[-1] + + tag_loc, image_loc, _ = ann._get_tag_and_location(index=index, + full_index=full_index, + width=standard_width, + height=standard_height, + fundamental=False) + + assert np.allclose(tag_loc, tag_loc_d[ann_str][location]), \ + ("tag loc (%s, %s) doesn't match expected as a function " + + "of location and tag min_size") % (ann_str, location) + + assert np.allclose(image_loc, image_loc_d[ann_str][location]), \ + ("image loc (%s, %s) doesn't match expected as a function " + + "of location and tag min_size") % (ann_str, location) + + # full_index = None (no error), + # would be seen as non-fundamental for nested items (why we only look at case 0) + for ann_index in [0]: + ann = ann_options[ann_index] + ann_str = ann_strings[ann_index] + index = full_index_options[ann_index][-1] + + tag_loc, image_loc, _ = ann._get_tag_and_location(index=index, + full_index=None, + width=standard_width, + height=standard_height, + fundamental=False) + + assert np.allclose(tag_loc, tag_loc_d[ann_str][location]), \ + ("tag loc (%s, %s) doesn't match expected as a function " + + "of location and tag min_size, "+ + "even when full_index is None") % (ann_str, location) + + assert np.allclose(image_loc, image_loc_d[ann_str][location]), \ + ("image loc (%s, %s) doesn't match expected as a function " + + "of location and tag min_size, "+ + "even when full_index is None") % (ann_str, location) + + # type(full_index) = integer (no error), + # would be seen as non-fundamental for nested items (why we only look at case 0) + for ann_index in [0]: + ann = ann_options[ann_index] + ann_str = ann_strings[ann_index] + index = full_index_options[ann_index][-1] + full_index = index + + tag_loc, image_loc, _ = ann._get_tag_and_location(index=index, + full_index=full_index, + width=standard_width, + height=standard_height, + fundamental=False) + + assert np.allclose(tag_loc, tag_loc_d[ann_str][location]), \ + ("tag loc (%s, %s) doesn't match expected as a function " + + "of location and tag min_size, "+ + "even when full_index is None") % (ann_str, location) + + assert np.allclose(image_loc, image_loc_d[ann_str][location]), \ + ("image loc (%s, %s) doesn't match expected as a function " + + "of location and tag min_size, "+ + "even when full_index is None") % (ann_str, location) + + + # special cases - no real output + for location in ["top", "bottom", "left", "right"]: + + a0_list = cow.annotation(tags = ["banana", "apple"], + tags_loc = location) + a0_list_nest = cow.annotation(tags = (["young", "old"], + ["harry", "hermione", "ron"]), + tags_loc = location, + tags_format = ("Age: {0}", "Age: {0}, Name: {1}")) + + a0_tuple_nest = cow.annotation(tags_format = ("Fig {0}", "Fig {0}.{1}"), + tags = ("1", "a"), + tags_loc = location) + + # returns (None)^3 --------------- + # fundamental = False + out_list_fF = a0_list_nest._get_tag_and_location(index=0, + full_index=(0,), + width=100, # really large + height=100, # really large + fundamental=False) + + assert np.all(out_list_fF == (None,None,None)), \ + ("if fundamental is False, return None^3 is expected from " + + "_get_tag_and_location, loc: %s, type = list" % location) + + out_tuple_fF = a0_tuple_nest._get_tag_and_location(index =0, + full_index = (0,), + width=100, # really large + height=100, # really large + fundamental=False) + assert np.all(out_tuple_fF == (None,None,None)), \ + ("if fundamental is False, return None^3 is expected from " + + "_get_tag_and_location, loc: %s, type = tuple" % location) + + + # outside list size + out_list_l1 = a0_list_nest._get_tag_and_location(index=3, + full_index=(3,), + width=100, # really large + height=100) # really large + assert np.all(out_tuple_fF == (None,None,None)), \ + ("if index > list length, return None^3 is expected from " + + "_get_tag_and_location, loc: %s, type = list, level 1" % location) + + out_list_l2 = a0_list_nest._get_tag_and_location(index=5, + full_index=(0,5), + width=100, # really large + height=100) # really large + + assert np.all(out_tuple_fF == (None,None,None)), \ + ("if index > list length, return None^3 is expected from " + + "_get_tag_and_location, loc: %s, type = list, level 2" % location) + + # None for full index + fundamental=False with nested structure + # leads to (None)^2 output + for ann_index in [1,2]: + ann = ann_options[ann_index] + ann_str = ann_strings[ann_index] + index = full_index_options[ann_index][-1] + + info_out = ann._get_tag_and_location(index=index, + full_index=None, + width=standard_width, + height=standard_height, + fundamental=False) + + assert np.all(info_out == (None,None,None)), \ + ("tag loc (%s, %s) with full_index=None, fundamental=False" + + "should not produce any tags, leading to all output of "+ + "_get_tag_and_location being None") % (ann_str, location) + + # type(full_index) = integer + fundamental=False with nested structure + # leads to (None)^2 output + for ann_index in [1,2]: + ann = ann_options[ann_index] + ann_str = ann_strings[ann_index] + index = full_index_options[ann_index][-1] + full_index = index + + info_out = ann._get_tag_and_location(index=index, + full_index=full_index, + width=standard_width, + height=standard_height, + fundamental=False) + assert np.all(info_out == (None,None,None)), \ + ("tag loc (%s, %s) with type(full_index)=int, fundamental=False" + + "should not produce any tags, leading to all output of "+ + "_get_tag_and_location being None") % (ann_str, location) + + # errors ----------- + # index != full_index[-1] (error) + with pytest.raises(Exception) as e_info: + a0_list._get_tag_and_location(index=5, + full_index=(4), + width=100, # really large + height=100) + with pytest.raises(Exception) as e_info: + a0_list_nest._get_tag_and_location(index=5, + full_index=(0,4), + width=100, # really large + height=100) + with pytest.raises(Exception) as e_info: + a0_tuple_nest._get_tag_and_location(index=5, + full_index=(0,4), + width=100, # really large + height=100) + + # width or height too small (error) + + with pytest.raises(Exception) as e_info: + a0_list._get_tag_and_location(index=0, + full_index=(0), + width=1, # really small + height=1) + with pytest.raises(Exception) as e_info: + a0_list_nest._get_tag_and_location(index=0, + full_index=(0,0), + width=1, # really small + height=1) + with pytest.raises(Exception) as e_info: + a0_tuple_nest._get_tag_and_location(index=0, + full_index=(0,0), + width=1, # really small + height=1) + + + +@pytest.mark.parametrize("location", ["top", "bottom", "left", "right"]) +@pytest.mark.parametrize("ann_index", [0,1,2]) +def test_annotation__get_tag_and_location2(image_regression, location, ann_index): + """ + regression tests for tag images for annotation's _get_tag_and_location + + Details + ------- + Includes implicit rotations, different nesting, different types + of tags (lists or types) + """ + + a0_list = cow.annotation(tags = ["banana", "apple"], + tags_loc = location) + a0_list_nest = cow.annotation(tags = (["young", "old"], + ["harry", "hermione", "ron"]), + tags_loc = location, + tags_format = ("Age: {0}", "Age: {0}, Name: {1}")) + + a0_tuple_nest = cow.annotation(tags_format = ("Fig {0}", "Fig {0}.{1}"), + tags = ("1", "a"), + tags_loc = location) + + # tag names for manual checks + tag_names = ["banana", "Age: old, Name: harry", "Fig 5.b"] + + + ann_options = [a0_list, a0_list_nest, a0_tuple_nest] + full_index_options = [(1,), (1,0), (5,1)] + + # actual assessment + ann = ann_options[ann_index] + full_index = full_index_options[ann_index] + index = full_index[-1] + + _,_,tag_image = ann._get_tag_and_location(width=175, + height=175, + index=index, + full_index = full_index) + + + with io.BytesIO() as fid2: + _save_svg_wrapper(tag_image, fid2, + width=to_inches(175, "pt"), + height=to_inches(175, "pt"), + _format="png", verbose=False) + image_regression.check(fid2.getvalue(), diff_threshold=.1) + + + + +def _create_blank_image_with_titles_helper(out, width, height): + """ + takes in list from annotation's _get_titles_and_locations and + creates an image + + Arguments + --------- + out : list + list of output from annotation's _get_titles_and_locations + width : float + width (in pt) used to generate `out` + height : float + height (in pt) used to generate `out` + + Returns + ------- + svg object with titles/subtitles/captions inserted + """ + base_image = sg.SVGFigure() + base_image.set_size((str(width)+"pt", str(height)+"pt")) + # add a view box... (set_size doesn't correctly update this...) + base_image.root.set("viewBox", "0 0 %s %s" % (str(width), str(height))) + + for loc_tuple, (svg_obj, _) in out: + _add_to_base_image(base_image, svg_obj, loc_tuple) + + return base_image + + +def test_annotation__get_titles_and_locations_basic_full(image_regression): + + + full_ann = cow.annotation(title = {"top": "top title", + "bottom": "bottom title", + "left": "left title", + "right": "right title"}, + subtitle = {"top": "top subtitle", + "bottom": "bottom subtitle", + "left": "left subtitle", + "right": "right subtitle"}, + caption = "caption", + tags = ("0",)) + + width_pt = 400 + height_pt = 400 + + out_full = full_ann._get_titles_and_locations(width=width_pt, + height=height_pt) + + # basic correct outcome sizes ---------- + assert (len(out_full) == 9) and \ + np.all([len(x) == 2 for x in out_full]) and \ + np.all([len(x[1]) == 2 for x in out_full]) and \ + np.all([inherits(x[1][0], sg.SVGFigure) for x in out_full]) and \ + np.all([inherits(x[1][1], tuple) for x in out_full]) and \ + np.all([inherits(x[0], tuple) for x in out_full]), \ + ("if all title/subtitle/caption element is used " + + "outcome of _get_titles_and_locations structure " + + "should be length 9, tuples of correct structure") + + full_svg = _create_blank_image_with_titles_helper(out_full, + width=width_pt, + height=height_pt) + + with io.BytesIO() as fid2: + _save_svg_wrapper(full_svg, fid2, + width=to_inches(width_pt, "pt"), + height=to_inches(height_pt, "pt"), + _format="png", verbose=False) + image_regression.check(fid2.getvalue(), diff_threshold=.1) + + + +@pytest.mark.parametrize("_type", ["title", "subtitle", "caption"]) +def test_annotation__get_titles_and_locations_basic_level(image_regression,_type): + # create a set of static combinations of + # titles, subtitles, captions (in different locations) + # identify expected locations and potentially try to create + # svg objects of the image themselves to compare the output too + + if _type in ["title", "subtitle"]: + attributes_dict = dict(tags = ("0",)) + attributes_dict[_type] = {"top": "top level", + "bottom": "bottom level", + "left": "left level", + "right": "right level"} + all_single_level_ann = cow.annotation(**attributes_dict) + else: + all_single_level_ann = cow.annotation(caption = "caption", + tags = ("0",)) + + + width_pt = 400 + height_pt = 400 + + out_level = all_single_level_ann._get_titles_and_locations(width=width_pt, + height=height_pt) + + + # basic correct outcome sizes ---------- + if _type in ["title", "subtitle"]: + assert (len(out_level) == 4) and \ + np.all([len(x) == 2 for x in out_level]) and \ + np.all([len(x[1]) == 2 for x in out_level]) and \ + np.all([inherits(x[1][0], sg.SVGFigure) for x in out_level]) and \ + np.all([inherits(x[1][1], tuple) for x in out_level]) and \ + np.all([inherits(x[0], tuple) for x in out_level]), \ + ("if all %s element is used " + + "outcome of _get_titles_and_locations structure " + + "should be length 9, tuples of correct structure") % (_type) + else: + assert (len(out_level) == 1) and \ + np.all([len(x) == 2 for x in out_level]) and \ + np.all([len(x[1]) == 2 for x in out_level]) and \ + np.all([inherits(x[1][0], sg.SVGFigure) for x in out_level]) and \ + np.all([inherits(x[1][1], tuple) for x in out_level]) and \ + np.all([inherits(x[0], tuple) for x in out_level]), \ + ("if only %s element is used " + + "outcome of _get_titles_and_locations structure " + + "should be length 9, tuples of correct structure") % (_type) + + + level_svg = _create_blank_image_with_titles_helper(out_level, + width=width_pt, + height=height_pt) + + with io.BytesIO() as fid2: + _save_svg_wrapper(level_svg, fid2, + width=to_inches(width_pt, "pt"), + height=to_inches(height_pt, "pt"), + _format="png", verbose=False) + image_regression.check(fid2.getvalue(), diff_threshold=.1) + + +@pytest.mark.parametrize("location", ["top", "left", "right", "bottom"]) +@pytest.mark.parametrize("_type", ["title", "subtitle", "caption"]) +def test_annotation__get_titles_and_locations_basic_single(image_regression, + location, _type): + # create a set of static combinations of + # titles, subtitles, captions (in different locations) + # identify expected locations and potentially try to create + # svg objects of the image themselves to compare the output too + + if _type in ["title", "subtitle"]: + attributes_dict2 = {_type: {location: "current (sub)title"} } + single_ann = cow.annotation(**attributes_dict2) + else: + single_ann = cow.annotation(caption = "caption") + + + width_pt = 400 + height_pt = 400 + + out_single = single_ann._get_titles_and_locations(width = width_pt, + height = height_pt) + + + # basic correct outcome sizes ---------- + if _type in ["title", "subtitle"]: + assert (len(out_single) == 1) and \ + np.all([len(x) == 2 for x in out_single]) and \ + np.all([len(x[1]) == 2 for x in out_single]) and \ + np.all([inherits(x[1][0], sg.SVGFigure) for x in out_single]) and \ + np.all([inherits(x[1][1], tuple) for x in out_single]) and \ + np.all([inherits(x[0], tuple) for x in out_single]), \ + ("if single %s, %s element is used " + + "outcome of _get_titles_and_locations structure " + + "should be length 9, tuples of correct structure") % (_type, location) + else: + assert (len(out_single) == 1) and \ + np.all([len(x) == 2 for x in out_single]) and \ + np.all([len(x[1]) == 2 for x in out_single]) and \ + np.all([inherits(x[1][0], sg.SVGFigure) for x in out_single]) and \ + np.all([inherits(x[1][1], tuple) for x in out_single]) and \ + np.all([inherits(x[0], tuple) for x in out_single]), \ + ("if only %s, (pointless location: %s) element is used " + + "outcome of _get_titles_and_locations structure " + + "should be length 9, tuples of correct structure") % (_type, location) + + + single_svg = _create_blank_image_with_titles_helper(out_single, + width=width_pt, + height=height_pt) + + with io.BytesIO() as fid2: + _save_svg_wrapper(single_svg, fid2, + width=to_inches(width_pt, "pt"), + height=to_inches(height_pt, "pt"), + _format="png", verbose=False) + image_regression.check(fid2.getvalue(), diff_threshold=.1) + + + diff --git a/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_bottom_caption_.yml b/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_bottom_caption_.yml new file mode 100644 index 0000000..1c94501 --- /dev/null +++ b/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_bottom_caption_.yml @@ -0,0 +1,8 @@ +extra_used_height: '9.75' +extra_used_width: '0' +min_full_width: '137.25' +min_inner_height: '0' +min_inner_width: '0' +top_left_loc: + '0': '0' + '1': '0' diff --git a/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_bottom_subtitle_.yml b/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_bottom_subtitle_.yml new file mode 100644 index 0000000..ee910a2 --- /dev/null +++ b/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_bottom_subtitle_.yml @@ -0,0 +1,8 @@ +extra_used_height: '11.25' +extra_used_width: '0' +min_full_width: '0' +min_inner_height: '0' +min_inner_width: '170.34375' +top_left_loc: + '0': '0' + '1': '0' diff --git a/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_bottom_title_.yml b/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_bottom_title_.yml new file mode 100644 index 0000000..daa7564 --- /dev/null +++ b/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_bottom_title_.yml @@ -0,0 +1,8 @@ +extra_used_height: '13.5' +extra_used_width: '0' +min_full_width: '0' +min_inner_height: '0' +min_inner_width: '182.0625' +top_left_loc: + '0': '0' + '1': '0' diff --git a/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_left_caption_.yml b/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_left_caption_.yml new file mode 100644 index 0000000..9b79e2a --- /dev/null +++ b/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_left_caption_.yml @@ -0,0 +1,8 @@ +extra_used_height: '9.75' +extra_used_width: '0' +min_full_width: '119.625' +min_inner_height: '0' +min_inner_width: '0' +top_left_loc: + '0': '0' + '1': '0' diff --git a/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_left_subtitle_.yml b/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_left_subtitle_.yml new file mode 100644 index 0000000..9da60b3 --- /dev/null +++ b/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_left_subtitle_.yml @@ -0,0 +1,8 @@ +extra_used_height: '0' +extra_used_width: '11.250000000000009' +min_full_width: '0' +min_inner_height: '148.59375' +min_inner_width: '0' +top_left_loc: + '0': '11.250000000000009' + '1': '0' diff --git a/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_left_title_.yml b/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_left_title_.yml new file mode 100644 index 0000000..a2c070d --- /dev/null +++ b/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_left_title_.yml @@ -0,0 +1,8 @@ +extra_used_height: '0' +extra_used_width: '13.50000000000001' +min_full_width: '0' +min_inner_height: '155.53125' +min_inner_width: '0' +top_left_loc: + '0': '13.50000000000001' + '1': '0' diff --git a/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_right_caption_.yml b/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_right_caption_.yml new file mode 100644 index 0000000..34070e7 --- /dev/null +++ b/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_right_caption_.yml @@ -0,0 +1,8 @@ +extra_used_height: '9.75' +extra_used_width: '0' +min_full_width: '126.28125' +min_inner_height: '0' +min_inner_width: '0' +top_left_loc: + '0': '0' + '1': '0' diff --git a/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_right_subtitle_.yml b/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_right_subtitle_.yml new file mode 100644 index 0000000..9865c9e --- /dev/null +++ b/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_right_subtitle_.yml @@ -0,0 +1,8 @@ +extra_used_height: '0' +extra_used_width: '11.250000000000009' +min_full_width: '0' +min_inner_height: '156.75' +min_inner_width: '0' +top_left_loc: + '0': '0' + '1': '0' diff --git a/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_right_title_.yml b/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_right_title_.yml new file mode 100644 index 0000000..69f048f --- /dev/null +++ b/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_right_title_.yml @@ -0,0 +1,8 @@ +extra_used_height: '0' +extra_used_width: '13.50000000000001' +min_full_width: '0' +min_inner_height: '165.28125' +min_inner_width: '0' +top_left_loc: + '0': '0' + '1': '0' diff --git a/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_top_caption_.yml b/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_top_caption_.yml new file mode 100644 index 0000000..d508a7a --- /dev/null +++ b/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_top_caption_.yml @@ -0,0 +1,8 @@ +extra_used_height: '9.75' +extra_used_width: '0' +min_full_width: '119.71875' +min_inner_height: '0' +min_inner_width: '0' +top_left_loc: + '0': '0' + '1': '0' diff --git a/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_top_subtitle_.yml b/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_top_subtitle_.yml new file mode 100644 index 0000000..67e41df --- /dev/null +++ b/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_top_subtitle_.yml @@ -0,0 +1,8 @@ +extra_used_height: '11.25' +extra_used_width: '0' +min_full_width: '0' +min_inner_height: '0' +min_inner_width: '148.6875' +top_left_loc: + '0': '0' + '1': '11.25' diff --git a/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_top_title_.yml b/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_top_title_.yml new file mode 100644 index 0000000..211e991 --- /dev/null +++ b/tests/test_annotation_elements/test_annotation__calculate_margin_sizes_static_top_title_.yml @@ -0,0 +1,8 @@ +extra_used_height: '13.5' +extra_used_width: '0' +min_full_width: '0' +min_inner_height: '0' +min_inner_width: '155.8125' +top_left_loc: + '0': '0' + '1': '13.5' diff --git a/tests/test_annotation_elements/test_annotation__calculate_tag_margin_sizes_bottom_.yml b/tests/test_annotation_elements/test_annotation__calculate_tag_margin_sizes_bottom_.yml new file mode 100644 index 0000000..a236229 --- /dev/null +++ b/tests/test_annotation_elements/test_annotation__calculate_tag_margin_sizes_bottom_.yml @@ -0,0 +1,35 @@ +0_base: + extra_used_height: '13.5' + extra_used_width: '0.0' + min_full_width: '0' + min_inner_height: '0.0' + min_inner_width: '36.84375' + top_left_loc: (0.0, 0.0) +1_base: + extra_used_height: '0' + extra_used_width: '0' + min_full_width: '0' + min_inner_height: '0' + min_inner_width: '0' + top_left_loc: (0, 0) +1_base_s: + extra_used_height: '13.5' + extra_used_width: '0.0' + min_full_width: '0' + min_inner_height: '0.0' + min_inner_width: '55.40625' + top_left_loc: (0.0, 0.0) +2_base: + extra_used_height: '0' + extra_used_width: '0' + min_full_width: '0' + min_inner_height: '0' + min_inner_width: '0' + top_left_loc: (0, 0) +2_base_s: + extra_used_height: '13.5' + extra_used_width: '0.0' + min_full_width: '0' + min_inner_height: '0.0' + min_inner_width: '43.59375' + top_left_loc: (0.0, 0.0) diff --git a/tests/test_annotation_elements/test_annotation__calculate_tag_margin_sizes_left_.yml b/tests/test_annotation_elements/test_annotation__calculate_tag_margin_sizes_left_.yml new file mode 100644 index 0000000..b9eb820 --- /dev/null +++ b/tests/test_annotation_elements/test_annotation__calculate_tag_margin_sizes_left_.yml @@ -0,0 +1,35 @@ +0_base: + extra_used_height: '0.0' + extra_used_width: '13.500000000000004' + min_full_width: '0' + min_inner_height: '36.84375' + min_inner_width: '0.0' + top_left_loc: (13.500000000000004, 0.0) +1_base: + extra_used_height: '0' + extra_used_width: '0' + min_full_width: '0' + min_inner_height: '0' + min_inner_width: '0' + top_left_loc: (0, 0) +1_base_s: + extra_used_height: '0.0' + extra_used_width: '13.500000000000004' + min_full_width: '0' + min_inner_height: '55.40625' + min_inner_width: '0.0' + top_left_loc: (13.500000000000004, 0.0) +2_base: + extra_used_height: '0' + extra_used_width: '0' + min_full_width: '0' + min_inner_height: '0' + min_inner_width: '0' + top_left_loc: (0, 0) +2_base_s: + extra_used_height: '0.0' + extra_used_width: '13.500000000000004' + min_full_width: '0' + min_inner_height: '43.59375' + min_inner_width: '0.0' + top_left_loc: (13.500000000000004, 0.0) diff --git a/tests/test_annotation_elements/test_annotation__calculate_tag_margin_sizes_right_.yml b/tests/test_annotation_elements/test_annotation__calculate_tag_margin_sizes_right_.yml new file mode 100644 index 0000000..c59c534 --- /dev/null +++ b/tests/test_annotation_elements/test_annotation__calculate_tag_margin_sizes_right_.yml @@ -0,0 +1,35 @@ +0_base: + extra_used_height: '0.0' + extra_used_width: '13.500000000000004' + min_full_width: '0' + min_inner_height: '36.84375' + min_inner_width: '0.0' + top_left_loc: (0.0, 0.0) +1_base: + extra_used_height: '0' + extra_used_width: '0' + min_full_width: '0' + min_inner_height: '0' + min_inner_width: '0' + top_left_loc: (0, 0) +1_base_s: + extra_used_height: '0.0' + extra_used_width: '13.500000000000004' + min_full_width: '0' + min_inner_height: '55.40625' + min_inner_width: '0.0' + top_left_loc: (0.0, 0.0) +2_base: + extra_used_height: '0' + extra_used_width: '0' + min_full_width: '0' + min_inner_height: '0' + min_inner_width: '0' + top_left_loc: (0, 0) +2_base_s: + extra_used_height: '0.0' + extra_used_width: '13.500000000000004' + min_full_width: '0' + min_inner_height: '43.59375' + min_inner_width: '0.0' + top_left_loc: (0.0, 0.0) diff --git a/tests/test_annotation_elements/test_annotation__calculate_tag_margin_sizes_top_.yml b/tests/test_annotation_elements/test_annotation__calculate_tag_margin_sizes_top_.yml new file mode 100644 index 0000000..8fd63b9 --- /dev/null +++ b/tests/test_annotation_elements/test_annotation__calculate_tag_margin_sizes_top_.yml @@ -0,0 +1,35 @@ +0_base: + extra_used_height: '13.5' + extra_used_width: '0.0' + min_full_width: '0' + min_inner_height: '0.0' + min_inner_width: '36.84375' + top_left_loc: (0.0, 13.5) +1_base: + extra_used_height: '0' + extra_used_width: '0' + min_full_width: '0' + min_inner_height: '0' + min_inner_width: '0' + top_left_loc: (0, 0) +1_base_s: + extra_used_height: '13.5' + extra_used_width: '0.0' + min_full_width: '0' + min_inner_height: '0.0' + min_inner_width: '55.40625' + top_left_loc: (0.0, 13.5) +2_base: + extra_used_height: '0' + extra_used_width: '0' + min_full_width: '0' + min_inner_height: '0' + min_inner_width: '0' + top_left_loc: (0, 0) +2_base_s: + extra_used_height: '13.5' + extra_used_width: '0.0' + min_full_width: '0' + min_inner_height: '0.0' + min_inner_width: '43.59375' + top_left_loc: (0.0, 13.5) diff --git a/tests/test_annotation_elements/test_annotation__get_tag_and_location2_0_bottom_.png b/tests/test_annotation_elements/test_annotation__get_tag_and_location2_0_bottom_.png new file mode 100644 index 0000000..858804f Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_tag_and_location2_0_bottom_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_tag_and_location2_0_left_.png b/tests/test_annotation_elements/test_annotation__get_tag_and_location2_0_left_.png new file mode 100644 index 0000000..aa1dc0f Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_tag_and_location2_0_left_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_tag_and_location2_0_right_.png b/tests/test_annotation_elements/test_annotation__get_tag_and_location2_0_right_.png new file mode 100644 index 0000000..aa1dc0f Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_tag_and_location2_0_right_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_tag_and_location2_0_top_.png b/tests/test_annotation_elements/test_annotation__get_tag_and_location2_0_top_.png new file mode 100644 index 0000000..858804f Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_tag_and_location2_0_top_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_tag_and_location2_1_bottom_.png b/tests/test_annotation_elements/test_annotation__get_tag_and_location2_1_bottom_.png new file mode 100644 index 0000000..202bfaf Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_tag_and_location2_1_bottom_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_tag_and_location2_1_left_.png b/tests/test_annotation_elements/test_annotation__get_tag_and_location2_1_left_.png new file mode 100644 index 0000000..7c87e37 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_tag_and_location2_1_left_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_tag_and_location2_1_right_.png b/tests/test_annotation_elements/test_annotation__get_tag_and_location2_1_right_.png new file mode 100644 index 0000000..7c87e37 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_tag_and_location2_1_right_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_tag_and_location2_1_top_.png b/tests/test_annotation_elements/test_annotation__get_tag_and_location2_1_top_.png new file mode 100644 index 0000000..202bfaf Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_tag_and_location2_1_top_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_tag_and_location2_2_bottom_.png b/tests/test_annotation_elements/test_annotation__get_tag_and_location2_2_bottom_.png new file mode 100644 index 0000000..e653453 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_tag_and_location2_2_bottom_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_tag_and_location2_2_left_.png b/tests/test_annotation_elements/test_annotation__get_tag_and_location2_2_left_.png new file mode 100644 index 0000000..5815b3b Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_tag_and_location2_2_left_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_tag_and_location2_2_right_.png b/tests/test_annotation_elements/test_annotation__get_tag_and_location2_2_right_.png new file mode 100644 index 0000000..5815b3b Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_tag_and_location2_2_right_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_tag_and_location2_2_top_.png b/tests/test_annotation_elements/test_annotation__get_tag_and_location2_2_top_.png new file mode 100644 index 0000000..e653453 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_tag_and_location2_2_top_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_0_caption_bottom_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_0_caption_bottom_.png new file mode 100644 index 0000000..c78790a Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_0_caption_bottom_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_0_caption_left_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_0_caption_left_.png new file mode 100644 index 0000000..c78790a Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_0_caption_left_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_0_caption_right_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_0_caption_right_.png new file mode 100644 index 0000000..c78790a Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_0_caption_right_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_0_caption_top_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_0_caption_top_.png new file mode 100644 index 0000000..c78790a Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_0_caption_top_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_0_subtitle_bottom_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_0_subtitle_bottom_.png new file mode 100644 index 0000000..1b01d4d Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_0_subtitle_bottom_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_0_subtitle_left_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_0_subtitle_left_.png new file mode 100644 index 0000000..35c2a16 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_0_subtitle_left_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_0_subtitle_right_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_0_subtitle_right_.png new file mode 100644 index 0000000..378c80e Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_0_subtitle_right_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_0_subtitle_top_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_0_subtitle_top_.png new file mode 100644 index 0000000..4b56f97 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_0_subtitle_top_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_0_title_bottom_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_0_title_bottom_.png new file mode 100644 index 0000000..7ebb9bd Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_0_title_bottom_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_0_title_left_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_0_title_left_.png new file mode 100644 index 0000000..8164503 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_0_title_left_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_0_title_right_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_0_title_right_.png new file mode 100644 index 0000000..fb74eae Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_0_title_right_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_0_title_top_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_0_title_top_.png new file mode 100644 index 0000000..9743afd Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_0_title_top_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_1_caption_bottom_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_1_caption_bottom_.png new file mode 100644 index 0000000..c78790a Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_1_caption_bottom_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_1_caption_left_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_1_caption_left_.png new file mode 100644 index 0000000..c78790a Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_1_caption_left_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_1_caption_right_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_1_caption_right_.png new file mode 100644 index 0000000..c78790a Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_1_caption_right_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_1_caption_top_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_1_caption_top_.png new file mode 100644 index 0000000..c78790a Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_1_caption_top_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_1_subtitle_bottom_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_1_subtitle_bottom_.png new file mode 100644 index 0000000..9b12035 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_1_subtitle_bottom_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_1_subtitle_left_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_1_subtitle_left_.png new file mode 100644 index 0000000..6651fb4 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_1_subtitle_left_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_1_subtitle_right_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_1_subtitle_right_.png new file mode 100644 index 0000000..4953f5f Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_1_subtitle_right_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_1_subtitle_top_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_1_subtitle_top_.png new file mode 100644 index 0000000..7eebc78 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_1_subtitle_top_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_1_title_bottom_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_1_title_bottom_.png new file mode 100644 index 0000000..b8d5ac7 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_1_title_bottom_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_1_title_left_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_1_title_left_.png new file mode 100644 index 0000000..bc68ac0 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_1_title_left_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_1_title_right_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_1_title_right_.png new file mode 100644 index 0000000..8dfa44c Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_1_title_right_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_1_title_top_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_1_title_top_.png new file mode 100644 index 0000000..035c3d2 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_1_title_top_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_2_caption_bottom_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_2_caption_bottom_.png new file mode 100644 index 0000000..c78790a Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_2_caption_bottom_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_2_caption_left_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_2_caption_left_.png new file mode 100644 index 0000000..c78790a Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_2_caption_left_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_2_caption_right_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_2_caption_right_.png new file mode 100644 index 0000000..c78790a Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_2_caption_right_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_2_caption_top_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_2_caption_top_.png new file mode 100644 index 0000000..c78790a Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_2_caption_top_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_2_subtitle_bottom_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_2_subtitle_bottom_.png new file mode 100644 index 0000000..cb5ba16 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_2_subtitle_bottom_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_2_subtitle_left_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_2_subtitle_left_.png new file mode 100644 index 0000000..891fd40 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_2_subtitle_left_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_2_subtitle_right_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_2_subtitle_right_.png new file mode 100644 index 0000000..891fd40 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_2_subtitle_right_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_2_subtitle_top_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_2_subtitle_top_.png new file mode 100644 index 0000000..cb5ba16 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_2_subtitle_top_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_2_title_bottom_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_2_title_bottom_.png new file mode 100644 index 0000000..7aef7f8 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_2_title_bottom_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_2_title_left_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_2_title_left_.png new file mode 100644 index 0000000..c3fc740 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_2_title_left_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_2_title_right_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_2_title_right_.png new file mode 100644 index 0000000..c3fc740 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_2_title_right_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_title_ir_2_title_top_.png b/tests/test_annotation_elements/test_annotation__get_title_ir_2_title_top_.png new file mode 100644 index 0000000..7aef7f8 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_title_ir_2_title_top_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_full.png b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_full.png new file mode 100644 index 0000000..9c346a1 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_full.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_level_caption_.png b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_level_caption_.png new file mode 100644 index 0000000..693fd8e Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_level_caption_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_level_subtitle_.png b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_level_subtitle_.png new file mode 100644 index 0000000..64673ef Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_level_subtitle_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_level_title_.png b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_level_title_.png new file mode 100644 index 0000000..64f287a Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_level_title_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_caption_bottom_.png b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_caption_bottom_.png new file mode 100644 index 0000000..693fd8e Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_caption_bottom_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_caption_left_.png b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_caption_left_.png new file mode 100644 index 0000000..693fd8e Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_caption_left_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_caption_right_.png b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_caption_right_.png new file mode 100644 index 0000000..693fd8e Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_caption_right_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_caption_top_.png b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_caption_top_.png new file mode 100644 index 0000000..693fd8e Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_caption_top_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_subtitle_bottom_.png b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_subtitle_bottom_.png new file mode 100644 index 0000000..9cbbcc9 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_subtitle_bottom_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_subtitle_left_.png b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_subtitle_left_.png new file mode 100644 index 0000000..9afdba8 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_subtitle_left_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_subtitle_right_.png b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_subtitle_right_.png new file mode 100644 index 0000000..737dc49 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_subtitle_right_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_subtitle_top_.png b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_subtitle_top_.png new file mode 100644 index 0000000..a133e68 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_subtitle_top_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_title_bottom_.png b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_title_bottom_.png new file mode 100644 index 0000000..6b11fa0 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_title_bottom_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_title_left_.png b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_title_left_.png new file mode 100644 index 0000000..4302098 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_title_left_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_title_right_.png b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_title_right_.png new file mode 100644 index 0000000..51e050f Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_title_right_.png differ diff --git a/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_title_top_.png b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_title_top_.png new file mode 100644 index 0000000..6da8e49 Binary files /dev/null and b/tests/test_annotation_elements/test_annotation__get_titles_and_locations_basic_single_title_top_.png differ diff --git a/tests/test_base_elements.py b/tests/test_base_elements.py index 798d1a4..e729788 100644 --- a/tests/test_base_elements.py +++ b/tests/test_base_elements.py @@ -4,9 +4,12 @@ import numpy as np import cowpatch as cow from cowpatch.utils import inherits, _flatten_nested_list, \ - _transform_size_to_pt + _transform_size_to_pt, to_inches +from cowpatch.config import rcParams + import pytest import io +import itertools import plotnine as p9 import plotnine.data as p9_data @@ -64,7 +67,162 @@ def test_patch__init__(): assert len(mypatch_args2.grobs) == 3, \ "grobs can be passed through the grobs parameter indirectly" -def test_patch__size_dive(): +def test_patch_layout(): + """ + tests when default layout appears and that it's correct + + static tests + """ + + p0 = p9.ggplot(p9_data.mpg) +\ + p9.geom_bar(p9.aes(x="hwy")) +\ + p9.facet_wrap("cyl") +\ + p9.labs(title = 'Plot 0') + + p1 = p9.ggplot(p9_data.mpg) +\ + p9.geom_point(p9.aes(x="hwy", y = "displ", color="class")) +\ + p9.labs(title = 'Plot 1') + + p2 = p9.ggplot(p9_data.mpg) +\ + p9.geom_point(p9.aes(x="hwy", y = "displ")) +\ + p9.labs(title = 'Plot 2') + + + # 3 elements + base = cow.patch(grobs=[p0,p1,p2]) + out_layout = base.layout + + assert base._layout == "patch" and \ + out_layout == cow.layout(nrow=3, ncol=1), \ + "if only 3 grobs & no layout, expect presented in a single column display" + + base1 = base + cow.layout(nrow=2, ncol=2) + out_layout1 = base1.layout + + assert base1._layout == out_layout1 and \ + out_layout1 == cow.layout(nrow=2, ncol=2), \ + "layout added to patch should be the one returned (complete layout)" + + + base2 = base + cow.layout(nrow=2) + out_layout2 = base2.layout + + assert base2._layout == out_layout2 and \ + out_layout2 == cow.layout(nrow=2), \ + "layout added to patch should be the one returned (partical layout)" + + # 4 elements + base = cow.patch(grobs=[p0,p1,p2,p0]) + out_layout = base.layout + + + assert base._layout == "patch" and \ + out_layout == cow.layout(nrow=2, ncol=2), \ + "if only 4 grobs & no layout, expect presented in a 2 x 2 display" + + + # > 4 elements + ngrobs = np.random.choice(np.arange(5,31)) + nrow = int(np.ceil(np.sqrt(ngrobs))) + ncol = int(np.ceil(ngrobs / nrow)) + base_large = cow.patch(grobs=[p0 for i in range(ngrobs)]) + out_layout_large = base_large.layout + + assert base_large._layout == "patch" and \ + out_layout_large == cow.layout(nrow=nrow, ncol=ncol) and \ + nrow*ncol >= ngrobs, \ + ("if only %i grobs & no layout, expect presented in a "+ + "sqrt(n) x sqrt(n)-ish display") % ngrobs + +def test_patch_annotation(): + """ + tests when default annotation appears and that it's correct + + static tests + """ + + p0 = p9.ggplot(p9_data.mpg) +\ + p9.geom_bar(p9.aes(x="hwy")) +\ + p9.facet_wrap("cyl") +\ + p9.labs(title = 'Plot 0') + + p1 = p9.ggplot(p9_data.mpg) +\ + p9.geom_point(p9.aes(x="hwy", y = "displ", color="class")) +\ + p9.labs(title = 'Plot 1') + + p2 = p9.ggplot(p9_data.mpg) +\ + p9.geom_point(p9.aes(x="hwy", y = "displ")) +\ + p9.labs(title = 'Plot 2') + + + # none, 3 grobs + base = cow.patch(grobs=[p0,p1,p2]) + out_ann = base.annotation + + assert base._annotation == None and \ + out_ann == cow.annotation(tags_inherit="override"), \ + "if no annotation set, basic one defined (base has grobs)" + + # none, no grobs + base0 = cow.patch() + out_ann0 = base0.annotation + + + assert base0._annotation == None and \ + out_ann0 == cow.annotation(tags_inherit="override"), \ + "if no annotation set, basic one defined (base has no grobs)" + + + # with annotation object + for title, subtitle, caption in \ + itertools.product(["title", cow.text("title"), {"bottom":"title"}, + {"bottom": cow.text("title")}, + {"left": 'ltitle', "bottom":"btitle"}, + {"left": cow.text('ltitle'), "bottom": cow.text("btitle")}, + None], + ["subtitle", cow.text("subtitle"), {"bottom":"subtitle"}, + {"bottom": cow.text("subtitle")}, + {"left": 'lsubtitle', "bottom":"bsubtitle"}, + {"left": cow.text('lsubtitle'), "bottom": cow.text("bsubtitle")}, + None], + [None, "caption", cow.text("caption")]): + + base_full1 = base + cow.annotation(title=title,subtitle=subtitle, + caption=caption) + out_ann_full1 = base_full1.annotation + + assert base_full1._annotation == out_ann_full1 and \ + out_ann_full1 == cow.annotation(title=title,subtitle=subtitle, + caption=caption), \ + ("annotation properties shouldn't be updated by patch, \n" + + "title: {title}, subtitle: {subtitle}, caption: {caption}".\ + format(title=title,subtitle=subtitle,caption=caption)) + + for tags, tags_loc, tags_inherit in \ + itertools.product( + [('1',), ("0", "a"), ["banana", "apple"], + (["banana", "apple"], "a") , None], + ["top", "bottom", "left", "right", None], + ["fix", "override"]): + + base_full2 = base + cow.annotation(tags=tags, + tags_loc=tags_loc, + tags_inherit=tags_inherit) + out_annotation_full2 = base_full2.annotation + + assert base_full2._annotation == out_annotation_full2 and \ + out_annotation_full2 == cow.annotation(tags=tags, + tags_loc=tags_loc, + tags_inherit=tags_inherit), \ + ("annotation properties shouldn't be updated by patch, \n" + + "tags: {tags}, tags_loc: {tags_loc}, tags_inherit: {tags_inherit}".\ + format(tags=tags,tags_loc=tags_loc, tags_inherit=tags_inherit)) + + +def test_patch__get_grob_tag_ordering(): + """ + test patch's internal _get_grob_tag_ordering + """ g0 = p9.ggplot(p9_data.mpg) +\ p9.geom_bar(p9.aes(x="hwy")) +\ p9.labs(title = 'Plot 0') @@ -84,153 +242,167 @@ def test_patch__size_dive(): p9.facet_wrap("class") # basic option ---------- - vis1 = cow.patch(g0,g1,g2) +\ - cow.layout(design = np.array([[0,1], - [0,2]]), + vis0 = cow.patch(g1,g0,g2) + + + # with layout ------------ + visl1 = vis0 +\ + cow.layout(design = np.array([[1,0], + [1,2]]), rel_heights = [1,2]) + o_l1 = visl1._get_grob_tag_ordering() - sug_width, sug_height, max_depth = \ - vis1._size_dive() + assert np.all(np.array([None]*3) == o_l1), \ + "if no tags, we expect tag ordering to be a array of Nones for "+\ + "each grob (with defined layout - l1)" - assert np.allclose(sug_width, - (2 * # 1/ rel width of smallest width of images - cow.rcParams["base_height"] * - cow.rcParams["base_aspect_ratio"])), \ - "suggested width incorrectly sizes the smallest width of the images (v1)" + visl2 = visl1 +\ + cow.annotation(title = "Combination") - assert np.allclose(sug_height, - (3 * # 1/ rel width of smallest width of images - cow.rcParams["base_height"])), \ - "suggested height incorrectly sizes the smallest height of the images (v1)" + o_l2 = visl2._get_grob_tag_ordering() - assert max_depth == 1, \ - "expected depth for basic cow.patch (of depth 1) is incorrect (v1)" + assert np.all(np.array([None]*3) == o_l2), \ + "if no tags, we expect tag ordering to be a array of Nones for "+\ + "each grob (with defined layout, some annotation - l2)" + visl3 = visl2 + cow.annotation(tags = ["Figure 0"]) # tag_order = "auto" (input) - # of note: the internal uses "pt", but they're actually defined relatively... - image_rel_widths, image_rel_heights, depths = \ - vis1._size_dive(parents_areas=[cow.area(width=1/6, - height=1/6, - x_left=0, - y_top=0, - _type="pt")]) + o_l3 = visl3._get_grob_tag_ordering() - assert np.allclose(image_rel_widths, [.5*1/6]*3), \ - "expected widths if input into a smaller image incorrect "+\ - "(v1.1, rel width to top 1/6)" + assert np.all(np.array([0, None, None]) == o_l3), \ + "with tag_order =\"auto\" (leads to input) and a shorter list of "+\ + "tags than the full set observations (and with some early order "+\ + "observations without tags, one should expect some nones and so "+\ + "non-nones in the grob_tag_ordering (with defined layout - l3)" - assert np.allclose(image_rel_heights, [1/6, 1/6*1/3, 1/6*2/3]), \ - "expected heights if input into a smaller image incorrect "+\ - "(v1.1, rel heights to top 1/6)" - assert np.allclose(depths, [1+1]*3), \ - "expected depths in basic cow.patch (all of depth 1) input into a "+\ - "1 level deep smaller image is incorrect (v1.1)" + visl3_1 = visl2 + cow.annotation(tags = ["Figure 0"], tags_order="input") + o_l3_1 = visl3_1._get_grob_tag_ordering() - image_rel_widths2, image_rel_heights2, depths2 = \ - vis1._size_dive(parents_areas=[cow.area(width=1/3, - height=1/2, - x_left=0, - y_top=0, - _type="pt"), - cow.area(width=1/2, - height=1/3, - x_left=1/2, - y_top=0, - _type="pt")]) - assert np.allclose(image_rel_widths2, [.5*1/6]*3), \ - "expected widths if input into a smaller image incorrect "+\ - "(v1.2, rel width to top 1/6)" + assert np.all(np.array([0, None, None]) == o_l3_1), \ + "with tag_order =\"input\" and a shorter list of "+\ + "tags than the full set observations (and with some early order "+\ + "observations without tags, one should expect some nones and so "+\ + "non-nones in the grob_tag_ordering (with defined layout - l3_1)" - assert np.allclose(image_rel_heights2, [1/6, 1/6*1/3, 1/6*2/3]), \ - "expected heights if input into a smaller image incorrect "+\ - "(v1.2, rel heights to top 1/6)" + visl3_2 = visl2 + cow.annotation(tags = ["Figure 0"], tags_order="yokogaki") - assert np.allclose(depths2, [1+2]*3), \ - "expected depths in basic cow.patch (all of depth 1) input into a "+\ - "2 levels deep smaller image is incorrect (v1.2)" + o_l3_2 = visl3_2._get_grob_tag_ordering() + assert np.all(np.array([None, 0, None]) == o_l3_2), \ + "with tag_order =\"yokogaki\" and a shorter list of "+\ + "tags than the full set observations (and with some early order "+\ + "observations without tags, one should expect some nones and so "+\ + "non-nones in the grob_tag_ordering (with defined layout - l3_2)" - # nested option -------- - vis_nested = cow.patch(g0,cow.patch(g1,g2)+\ - cow.layout(ncol=1, rel_heights = [1,2])) +\ - cow.layout(nrow=1) + visl4 = visl2 + cow.annotation(tags_format = ("Figure {0}",), + tags = ("0", )) # tag_order = "auto" (yokogaki) - sug_width_n, sug_height_n, max_depth_n = \ - vis_nested._size_dive() + o_l4 = visl4._get_grob_tag_ordering() - assert np.allclose(sug_width_n, - (2 * # 1/ rel width of smallest width of images - cow.rcParams["base_height"] * - cow.rcParams["base_aspect_ratio"])), \ - "suggested width incorrectly sizes the smallest width of the images "+\ - "(v2 - nested)" + assert np.all(np.array([1,0,2]) == o_l4), \ + "with tag_order =\"auto\" (leads to yokogaki) and tuple for tags, "+\ + "in the grob_tag_ordering (with defined layout - o_l4)" + + + visl4_1 = visl2 + cow.annotation(tags_format = ("Figure {0}",), + tags = ("0", ), tags_order="input") + + o_l4_1 = visl4_1._get_grob_tag_ordering() + + assert np.all(np.array([0,1,2]) == o_l4_1), \ + "with tag_order =\"input\" and tuple for tags, "+\ + "in the grob_tag_ordering (with defined layout - o_l4_1)" + + + visl4_2 = visl2 + cow.annotation(tags_format = ("Figure {0}",), + tags = ("0", ), tags_order="yokogaki") + + o_l4_2 = visl4_2._get_grob_tag_ordering() + + assert np.all(np.array([1,0,2]) == o_l4_2), \ + "with tag_order =\"yokogaki\" and tuple for tags, "+\ + "in the grob_tag_ordering (with defined layout - o_l4_2)" + + + + # without layout ------------ + + vis_nol_2 = vis0 +\ + cow.annotation(title = "Combination") + + o_nol_2 = vis_nol_2._get_grob_tag_ordering() + + assert np.all(np.array([None]*3) == o_nol_2), \ + "if no tags, we expect tag ordering to be a array of Nones for "+\ + "each grob (with no defined layout - nol_2)" - assert np.allclose(sug_height_n, - (3 * # 1/ rel width of smallest width of images - cow.rcParams["base_height"])), \ - "suggested height incorrectly sizes the smallest height of the images "+\ - "(v2 - nested)" - assert max_depth_n == 2, \ - "expected depth for nested cow.patch (of depth 1) is incorrect "+\ - "(v2 - nested)" + vis_nol_3 = vis_nol_2 + cow.annotation(tags = ["Figure 0"]) # tag_order = "auto" (input) + o_nol_3 = vis_nol_3._get_grob_tag_ordering() - # of note: the internal uses "pt", but they're actually defined relatively... - image_rel_widths_n, image_rel_heights_n, depths_n = \ - vis_nested._size_dive(parents_areas=[cow.area(width=1/6, - height=1/6, - x_left=0, - y_top=0, - _type="pt")]) + assert np.all(np.array([0,None,None]) == o_nol_3), \ + "with tag_order =\"auto\" (leads to input) and a shorter list of "+\ + "tags than the full set observations (with no defined layout - nol_3)" - assert np.allclose(image_rel_widths_n, [.5*1/6]*3), \ - "expected widths if input into a smaller image incorrect "+\ - "(v2.1 - nested, rel width to top 1/6)" + vis_nol_3_1 = vis_nol_2 + cow.annotation(tags = ["Figure 0"], + tags_order="input") - assert np.allclose(image_rel_heights_n, [1/6, 1/6*1/3, 1/6*2/3]), \ - "expected heights if input into a smaller image incorrect "+\ - "(v2.1 - nested, rel heights to top 1/6)" + o_nol_3_1 = vis_nol_3_1._get_grob_tag_ordering() - assert np.allclose(depths_n, list(np.array([1,2,2])+1)), \ - "expected depths in nested cow.patch (all of depth 1) input into a "+\ - "1 level deep smaller image is incorrect (v2.1 - nested)" + assert np.all(np.array([0,None,None]) == o_nol_3_1), \ + "with tag_order =\"input\" and a shorter list of "+\ + "tags than the full set observations (with no defined layout - nol_3_1)" - image_rel_widths2_n, image_rel_heights2_n, depths2_n = \ - vis_nested._size_dive(parents_areas=[cow.area(width=1/3, - height=1/2, - x_left=0, - y_top=0, - _type="pt"), - cow.area(width=1/2, - height=1/3, - x_left=1/2, - y_top=0, - _type="pt")]) + vis_nol_3_2 = vis_nol_2 + cow.annotation(tags = ["Figure 0"], + tags_order="yokogaki") + o_nol_3_2 = vis_nol_3_2._get_grob_tag_ordering() - assert np.allclose(image_rel_widths2_n, [.5*1/6]*3), \ - "expected widths if input into a smaller image incorrect "+\ - "(v2.2 - nested, rel width to top 1/6)" + assert np.all(np.array([0,None,None]) == o_nol_3_2), \ + "with tag_order =\"yokogaki\" and a shorter list of "+\ + "tags than the full set observations (with no defined layout - nol_3_2)" - assert np.allclose(image_rel_heights2_n, [1/6, 1/6*1/3, 1/6*2/3]), \ - "expected heights if input into a smaller image incorrect "+\ - "(v2.2 - nested, rel heights to top 1/6)" - assert np.allclose(depths2_n, list(np.array([1,2,2])+2)), \ - "expected depths in nested cow.patch (all of depth 1) input into a "+\ - "1 levels deep smaller image is incorrect (v2.2 - nested)" -def test_patch__default_size__both_none(): + vis_nol_4 = vis_nol_2 + cow.annotation(tags_format = ("Figure {0}",), + tags = ("0", )) # tag_order = "auto" (yokogaki) + + o_nol_4 = vis_nol_4._get_grob_tag_ordering() + + assert np.all(np.array([0,1,2]) == o_nol_4), \ + "with tag_order =\"auto\" and tuple for tags, "+\ + "in the grob_tag_ordering (with no defined layout - o_nol_4)" + + + vis_nol_4_1 = vis_nol_2 + cow.annotation(tags_format = ("Figure {0}",), + tags = ("0", ), tags_order="input") + + o_nol_4_1 = vis_nol_4_1._get_grob_tag_ordering() + + assert np.all(np.array([0,1,2]) == o_l4_1), \ + "with tag_order =\"input\" and tuple for tags, "+\ + "in the grob_tag_ordering (with no defined layout - o_nol_4_1)" + + vis_nol_4_2 = vis_nol_2 + cow.annotation(tags_format = ("Figure {0}",), + tags = ("0", ), tags_order="yokogaki") + + o_nol_4_2 = vis_nol_4_2._get_grob_tag_ordering() + + assert np.all(np.array([0,1,2]) == o_nol_4_2), \ + "with tag_order =\"yokogaki\" and tuple for tags, "+\ + "in the grob_tag_ordering (with no defined layout - o_nol_4_2)" + +def test_patch__get_grob_tag_ordering2(): """ - this test passes none for both parameters + static testing of _get_grob_tag_ordering """ g0 = p9.ggplot(p9_data.mpg) +\ p9.geom_bar(p9.aes(x="hwy")) +\ @@ -244,6 +416,154 @@ def test_patch__default_size__both_none(): p9.geom_point(p9.aes(x="hwy", y = "displ", color="class")) +\ p9.labs(title = 'Plot 2') + g3 = p9.ggplot(p9_data.mpg[p9_data.mpg["class"].isin(["compact", + "suv", + "pickup"])]) +\ + p9.geom_histogram(p9.aes(x="hwy"), bins=10) +\ + p9.facet_wrap("class") + + my_patch = cow.patch(g0,g1,g2,g3) + my_layout = cow.layout(design = np.array([[0,0,0,3,3,3], + [0,0,0,2,2,2], + [1,1,1,2,2,2]])) + + my_annotation1 = cow.annotation(tags = ("Fig {0}",), tags_format = ("a",), + tags_order = "auto") + my_annotation2 = cow.annotation(tags = ["Fig 1", "Fig 2", "Fig 3"], # notice this isn't complete... + tags_order = "auto") + # yokogaki ---- + my_a_y1 = my_annotation1 + cow.annotation(tags_order = "yokogaki") + vis_y1 = my_patch + my_layout + my_a_y1 + + out_y1 = vis_y1._get_grob_tag_ordering() + + assert np.all(out_y1 == np.array([0,3,2,1])), \ + "yokogaki ordering incorrect (static example 1, tuple tags)" + + my_a_y1_auto = my_annotation1 + vis_y1_auto = my_patch + my_layout + my_a_y1_auto + + out_y1_auto = vis_y1_auto._get_grob_tag_ordering() + + assert np.all(out_y1_auto == np.array([0,3,2,1])), \ + "yokogaki ordering incorrect - auto tags_order (static example "+\ + "1_auto, tuple tags)" + + my_a_y2 = my_annotation2 + cow.annotation(tags_order = "yokogaki") + vis_y2 = my_patch + my_layout + my_a_y2 + + out_y2 = vis_y2._get_grob_tag_ordering() + + assert np.all(out_y2 == np.array([0,None,2,1])), \ + "yokogaki ordering incorrect (static example 2, list tags)" + + # input ---- + my_a_i1 = my_annotation1 + cow.annotation(tags_order = "input") + vis_i1 = my_patch + my_layout + my_a_i1 + + out_i1 = vis_i1._get_grob_tag_ordering() + + assert np.all(out_i1 == np.array([0,1,2,3])), \ + "input ordering incorrect (static example 1, tuple tags)" + + my_a_i2 = my_annotation2 + cow.annotation(tags_order = "input") + vis_i2 = my_patch + my_layout + my_a_i2 + + out_i2 = vis_i2._get_grob_tag_ordering() + + assert np.all(out_i2 == np.array([0,1,2,None])), \ + "input ordering incorrect (static example 2, list tags)" + + my_a_i2_auto = my_annotation2 + cow.annotation(tags_order = "auto") + vis_i2_auto = my_patch + my_layout + my_a_i2_auto + + out_i2_auto = vis_i2_auto._get_grob_tag_ordering() + + assert np.all(out_i2_auto == np.array([0,1,2,None])), \ + "input ordering incorrect - auto tag ordering (static example 2, list tags)" + + # no tags (different ways) ----- + + my_n1_a = my_annotation1 + \ + cow.annotation(tags = np.nan, tags_order = "auto") + vis_n1_a = my_patch + my_layout + my_n1_a + out_n1_a = vis_n1_a._get_grob_tag_ordering() + + assert np.all(out_n1_a == np.array([None]*4)), \ + "if tags are missing, we expect the _get_grob_tag_ordering to "+\ + "return an empty array (static example 1 - np.nan override, auto)" + + my_n1_y = my_annotation1 + \ + cow.annotation(tags = np.nan, tags_order = "yokogaki") + vis_n1_y = my_patch + my_layout + my_n1_y + out_n1_y = vis_n1_y._get_grob_tag_ordering() + + assert np.all(out_n1_y == np.array([None]*4)), \ + "if tags are missing, we expect the _get_grob_tag_ordering to "+\ + "return an empty array (static example 1 - np.nan override, yokogaki)" + + my_n1_i = my_annotation1 + \ + cow.annotation(tags = np.nan, tags_order = "input") + vis_n1_i = my_patch + my_layout + my_n1_i + out_n1_i = vis_n1_i._get_grob_tag_ordering() + + assert np.all(out_n1_i == np.array([None]*4)), \ + "if tags are missing, we expect the _get_grob_tag_ordering to "+\ + "return an empty array (static example 1 - np.nan override, input)" + + + + + my_n2_a = my_annotation2 + \ + cow.annotation(tags = np.nan, tags_order = "auto") + vis_n2_a = my_patch + my_layout + my_n2_a + out_n2_a = vis_n2_a._get_grob_tag_ordering() + + assert np.all(out_n2_a == np.array([None]*4)), \ + "if tags are missing, we expect the _get_grob_tag_ordering to "+\ + "return an empty array (static example 2 - np.nan override, auto)" + + my_n2_y = my_annotation2 + \ + cow.annotation(tags = np.nan, tags_order = "yokogaki") + vis_n2_y = my_patch + my_layout + my_n2_y + out_n2_y = vis_n2_y._get_grob_tag_ordering() + + assert np.all(out_n2_y == np.array([None]*4)), \ + "if tags are missing, we expect the _get_grob_tag_ordering to "+\ + "return an empty array (static example 2 - np.nan override, yokogaki)" + + my_n2_i = my_annotation2 + \ + cow.annotation(tags = np.nan, tags_order = "input") + vis_n2_i = my_patch + my_layout + my_n2_i + out_n2_i = vis_n2_i._get_grob_tag_ordering() + + assert np.all(out_n2_i == np.array([None]*4)), \ + "if tags are missing, we expect the _get_grob_tag_ordering to "+\ + "return an empty array (static example 2 - np.nan override, input)" + + + vis_n_n = my_patch + my_layout + out_n_a = vis_n_n._get_grob_tag_ordering() + + + assert np.all(out_n_a == np.array([None]*4)), \ + "if annotation object itelf is missing, we expect the "+\ + "_get_grob_tag_ordering to return an empty array" + + +def test_patch__default_size_NoAnnotation(): + g0 = p9.ggplot(p9_data.mpg) +\ + p9.geom_bar(p9.aes(x="hwy")) +\ + p9.labs(title = 'Plot 0') + + g1 = p9.ggplot(p9_data.mpg) +\ + p9.geom_point(p9.aes(x="hwy", y = "displ")) +\ + p9.labs(title = 'Plot 1') + + g2 = p9.ggplot(p9_data.mpg) +\ + p9.geom_point(p9.aes(x="hwy", y = "displ", color="class")) +\ + p9.labs(title = 'Plot 2') + g3 = p9.ggplot(p9_data.mpg[p9_data.mpg["class"].isin(["compact", "suv", "pickup"])]) +\ @@ -256,18 +576,21 @@ def test_patch__default_size__both_none(): [0,2]]), rel_heights = [1,2]) - out_w, out_h = vis1._default_size(height=None,width=None) + sug_width, sug_height = \ + vis1._default_size() - assert np.allclose(out_w, + assert np.allclose(sug_width, (2 * # 1/ rel width of smallest width of images cow.rcParams["base_height"] * cow.rcParams["base_aspect_ratio"])), \ - "_default_size incorrectly connects with _size_dive output - width (v1)" + "suggested width incorrectly sizes the smallest width of the images "+\ + "(v1, no annotation)" - assert np.allclose(out_h, + assert np.allclose(sug_height, (3 * # 1/ rel width of smallest width of images cow.rcParams["base_height"])), \ - "_default_size incorrectly connects with _size_dive output - height (v1)" + "suggested height incorrectly sizes the smallest height of the images "+\ + "- rel_heights example (v1, no annotation)" # nested option -------- @@ -275,22 +598,24 @@ def test_patch__default_size__both_none(): cow.layout(ncol=1, rel_heights = [1,2])) +\ cow.layout(nrow=1) - out_w_n, out_h_n = vis_nested._default_size(height=None,width=None) - assert np.allclose(out_w_n, + sug_width_n, sug_height_n = \ + vis_nested._default_size() + + assert np.allclose(sug_width_n, (2 * # 1/ rel width of smallest width of images cow.rcParams["base_height"] * cow.rcParams["base_aspect_ratio"])), \ - "_default_size incorrectly connects with _size_dive output - width (v2-nested)" + "suggested width incorrectly sizes the smallest width of the images "+\ + "(v2 - nested, no annotation)" - assert np.allclose(out_h_n, + assert np.allclose(sug_height_n, (3 * # 1/ rel width of smallest width of images cow.rcParams["base_height"])), \ - "_default_size incorrectly connects with _size_dive output - height (v2-nested)" + "suggested height incorrectly sizes the smallest height of the images "+\ + "(v2 - nested, no annotation)" -@given(st.floats(min_value=.5, max_value=49), - st.floats(min_value=.5, max_value=49)) -def test_patch__default_size__both_not_none(height,width): +def test_patch__default_size_Annotation(): g0 = p9.ggplot(p9_data.mpg) +\ p9.geom_bar(p9.aes(x="hwy")) +\ p9.labs(title = 'Plot 0') @@ -303,303 +628,672 @@ def test_patch__default_size__both_not_none(height,width): p9.geom_point(p9.aes(x="hwy", y = "displ", color="class")) +\ p9.labs(title = 'Plot 2') - g3 = p9.ggplot(p9_data.mpg[p9_data.mpg["class"].isin(["compact", - "suv", - "pickup"])]) +\ - p9.geom_histogram(p9.aes(x="hwy"),bins=10) +\ - p9.facet_wrap("class") - # basic option ---------- vis1 = cow.patch(g0,g1,g2) +\ cow.layout(design = np.array([[0,1], [0,2]]), - rel_heights = [1,2]) + rel_heights = [1,2]) +\ + cow.annotation(title = "My title") + + sug_width, sug_height = \ + vis1._default_size() + + assert np.allclose(sug_width, + (2 * # 1/ rel width of smallest width of images + cow.rcParams["base_height"] * + cow.rcParams["base_aspect_ratio"])), \ + "suggested width incorrectly sizes the smallest width of the images (v1, annotation)" + + assert np.allclose(sug_height, + (3 * # 1/ rel width of smallest width of images + cow.rcParams["base_height"] +\ + cow.text("My title", _type="cow_title").\ + _min_size(to_inches=True)[1] + ) + ), \ + "suggested height incorrectly sizes the smallest height of the images (v1, annotation)" - out_w, out_h = vis1._default_size(height=height,width=width) - assert out_w == width and out_h == height, \ - "if height and width are provided, they shouldn't be changed by "+\ - "default size function (v1 - no nesting)" # nested option -------- vis_nested = cow.patch(g0,cow.patch(g1,g2)+\ cow.layout(ncol=1, rel_heights = [1,2])) +\ - cow.layout(nrow=1) + cow.layout(nrow=1) +\ + cow.annotation(subtitle = "My subtitle") - out_w_n, out_h_n = vis_nested._default_size(height=height,width=width) - assert out_w_n == width and out_h_n == height, \ - "if height and width are provided, they shouldn't be changed by "+\ - "default size function (v2 - nesting)" -@given(st.floats(min_value=.5, max_value=49)) -def test_patch__default_size__one_none(height_or_width): - g0 = p9.ggplot(p9_data.mpg) +\ - p9.geom_bar(p9.aes(x="hwy")) +\ - p9.labs(title = 'Plot 0') + sug_width_n, sug_height_n = \ + vis_nested._default_size() - g1 = p9.ggplot(p9_data.mpg) +\ - p9.geom_point(p9.aes(x="hwy", y = "displ")) +\ - p9.labs(title = 'Plot 1') + assert np.allclose(sug_width_n, + (2 * # 1/ rel width of smallest width of images + cow.rcParams["base_height"] * + cow.rcParams["base_aspect_ratio"])), \ + "suggested width incorrectly sizes the smallest width of the images "+\ + "(v2 - nested, annotation)" - g2 = p9.ggplot(p9_data.mpg) +\ - p9.geom_point(p9.aes(x="hwy", y = "displ", color="class")) +\ - p9.labs(title = 'Plot 2') + assert np.allclose(sug_height_n, + (3 * # 1/ rel width of smallest width of images + cow.rcParams["base_height"] +\ + cow.text("My subtitle", _type="cow_subtitle").\ + _min_size(to_inches=True)[1] + )), \ + "suggested height incorrectly sizes the smallest height of the images "+\ + "(v2 - nested, annotation)" - g3 = p9.ggplot(p9_data.mpg[p9_data.mpg["class"].isin(["compact", - "suv", - "pickup"])]) +\ - p9.geom_histogram(p9.aes(x="hwy"),bins=10) +\ - p9.facet_wrap("class") + # tag nested option (explicit tags_inherit="override")----------- + vis_nested_tag = cow.patch(g0,cow.patch(g1,g2)+\ + cow.layout(ncol=1, rel_heights = [1,2]) +\ + cow.annotation(tags_inherit="override")) +\ + cow.layout(nrow=1) +\ + cow.annotation(caption = "My caption") +\ + cow.annotation(tags=("0", "a"), tags_format=("Fig {0}", "Fig {0}.{1}"), + tags_loc="top") + sug_width_nt, sug_height_nt = \ + vis_nested_tag._default_size() - # basic option ---------- - vis1 = cow.patch(g0,g1,g2) +\ + assert np.allclose(sug_width_nt, + (2 * # 1/ rel width of smallest width of images + cow.rcParams["base_height"] * + cow.rcParams["base_aspect_ratio"])), \ + "suggested width incorrectly sizes the smallest width of the images "+\ + "(v2 - nested + tagged, annotation - explicit tags_inherit=\"override\")" + + assert np.allclose(sug_height_nt, + (3 * # 1/ rel width of smallest width of images (and include the caption and 1 tag) + (cow.rcParams["base_height"] +\ + cow.text("Fig 01ab", _type="cow_tag").\ + _min_size(to_inches=True)[1]) +\ + cow.text("My caption", _type="cow_caption").\ + _min_size(to_inches=True)[1])), \ + "suggested height incorrectly sizes the smallest height of the images "+\ + "(v2 - nested + tagged, annotation - explicit tags_inherit=\"override\")" + + # tag nested option (implicit tags_inherit="override")----------- + vis_nested_tag_i = cow.patch(g0,cow.patch(g1,g2)+\ + cow.layout(ncol=1, rel_heights = [1,2])) +\ + cow.layout(nrow=1) +\ + cow.annotation(caption = "My caption") +\ + cow.annotation(tags=("0", "a"), tags_format=("Fig {0}", "Fig {0}.{1}"), + tags_loc="top") + + sug_width_nt_i, sug_height_nt_i = \ + vis_nested_tag_i._default_size() + + assert np.allclose(sug_width_nt_i, + (2 * # 1/ rel width of smallest width of images + cow.rcParams["base_height"] * + cow.rcParams["base_aspect_ratio"])), \ + "suggested width incorrectly sizes the smallest width of the images "+\ + "(v2 - nested + tagged, annotation - implicit tags_inherit=\"override\")" + + assert np.allclose(sug_height_nt_i, + (3 * # 1/ rel width of smallest width of images (and include the caption and 1 tag) + (cow.rcParams["base_height"] +\ + cow.text("Fig 01ab", _type="cow_tag").\ + _min_size(to_inches=True)[1]) +\ + cow.text("My caption", _type="cow_caption").\ + _min_size(to_inches=True)[1])), \ + "suggested height incorrectly sizes the smallest height of the images "+\ + "(v2 - nested + tagged, annotation - implicit tags_inherit=\"override\")" + +def test_patch_default_size_TextNoAnnotation(): + """ + test of patch's _default_size + + Static test to ensure sizes of text objects minimum sizing correctly + alter default_size output if needed. Also, actual examples ensure that + there any error in `_min_size` call relative to `to_inches` parameter + is not occuring. + + Note: + this test doesn't combine text objects with annotation (so it fits + in the NoAnnotation approach). For now we're not going to test that + option. + """ + # basic option (text fits in desired box) ---------- + + t0 = cow.text("An approximate answer to the right problem\n"+ + "is worth a good deal more than an exact\n"+ + "answer to an approximate problem. ~John Tukey") +\ + p9.element_text(size = 15) + + t1 = cow.text("If I can’t picture it,\n"+ + "I can’t understand it. ~Albert Einstein") +\ + p9.element_text(size = 17, ha="left") + + t2 = cow.text("Tables usually outperform graphics in\n"+ + "reporting on small data sets of 20\n"+ + "numbers or less. ~John Tukey") +\ + p9.element_text(size=12, ha="left") + + vis1 = cow.patch(t0, t1, t2) +\ cow.layout(design = np.array([[0,1], [0,2]]), rel_heights = [1,2]) - default_w, default_h = vis1._default_size(None,None) - static_aspect_ratio = default_h / default_w + sug_width, sug_height = vis1._default_size() - # provide width ---- - out_w, out_h = vis1._default_size(height=None,width=height_or_width) - assert np.allclose(out_w, height_or_width) and \ - np.allclose(out_h, height_or_width * static_aspect_ratio), \ - "if *only width* is provided, suggested height is relative to aspect "+\ - "ratio that would be suggested if neither provided (v1)" + assert np.allclose(sug_width, + (2 * # 1/ rel width of smallest width of images + cow.rcParams["base_height"] * + cow.rcParams["base_aspect_ratio"])), \ + ("when text size elements in image *don't require* larger-than rcParams "+ + "sizing, width sizing should match rcParam related expectation "+ + "(no annotation)") - # provide height ---- - out_w, out_h = vis1._default_size(height=height_or_width,width=None) - assert np.allclose(out_h, height_or_width) and \ - np.allclose(out_w, height_or_width / static_aspect_ratio), \ - "if *only height* is provided, suggested width is relative to aspect "+\ - "ratio that would be suggested if neither provided (v1)" + assert np.allclose(sug_height, + (3 * # 1/ rel width of smallest width of images + cow.rcParams["base_height"])), \ + ("when text size elements in image *don't require* larger-than rcParams "+ + "sizing, height sizing should match rcParam related expectation "+ + "(no annotation)") + + m_w0, m_h0 = t0._min_size(to_inches=True) + m_w1, m_h1 = t1._min_size(to_inches=True) + m_w2, m_h2 = t2._min_size(to_inches=True) + + assert ((sug_height >= m_h0) and + (2/3 * sug_height >= m_h2) and + (1/3 * sug_height >= m_h1)) and \ + ((1/2 * sug_width >= m_w0) and + (1/2 * sug_width >= m_w2) and + (1/2 * sug_width >= m_w1)), \ + ("when text size elements in image *don't require* larger-than rcParams "+ + "the min size of text objects should be less than or equal to allocated "+ + "sizing for the image (no annotation)") + + + # second option (text doesn't fits in desired box) ---------- + + t0_2 = cow.text("An approximate answer to the right problem\n"+ + "is worth a good deal more than an exact\n"+ + "answer to an approximate problem. ~John Tukey") +\ + p9.element_text(size = 25) + + t1_2 = cow.text("If I can’t picture it,\n"+ + "I can’t understand it. ~Albert Einstein") +\ + p9.element_text(size = 20, ha="left") + + t2_2 = cow.text("Tables usually outperform graphics in\n"+ + "reporting on small data sets of 20\n"+ + "numbers or less. ~John Tukey") +\ + p9.element_text(size=25, ha="left") + + vis2 = cow.patch(t0_2, t1_2, t2_2) +\ + cow.layout(design = np.array([[0,1], + [0,2]]), + rel_heights = [1,2]) - # nested option -------- - vis_nested = cow.patch(g0,cow.patch(g1,g2)+\ - cow.layout(ncol=1, rel_heights = [1,2])) +\ - cow.layout(nrow=1) - default_w_n, default_h_n = vis_nested._default_size(None,None) - static_aspect_ratio_n = default_h_n / default_w_n + sug_width_2, sug_height_2 = vis2._default_size() - # provide width ---- - out_w, out_h = vis_nested._default_size(height=None,width=height_or_width) - assert np.allclose(out_w, height_or_width) and \ - np.allclose(out_h, height_or_width * static_aspect_ratio_n), \ - "if *only width* is provided, suggested height is relative to aspect "+\ - "ratio that would be suggested if neither provided (v1)" + assert (sug_width_2 >= + (2 * # 1/ rel width of smallest width of images + cow.rcParams["base_height"] * + cow.rcParams["base_aspect_ratio"])), \ + ("when text size elements in image *require* larger-than rcParams "+ + "sizing, width sizing be greater or equal to rcParam related expectation "+ + "(no annotation)") - # provide height ---- - out_w, out_h = vis_nested._default_size(height=height_or_width,width=None) - assert np.allclose(out_h, height_or_width) and \ - np.allclose(out_w, height_or_width / static_aspect_ratio_n), \ - "if *only height* is provided, suggested width is relative to aspect "+\ - "ratio that would be suggested if neither provided (v1)" + assert (sug_height_2 >= + (3 * # 1/ rel width of smallest width of images + cow.rcParams["base_height"])), \ + ("when text size elements in image *require* larger-than rcParams "+ + "sizing, height sizing be greater or equal to rcParam related expectation "+ + "(no annotation)") + + m_w0_2, m_h0_2 = t0_2._min_size(to_inches=True) + m_w1_2, m_h1_2 = t1_2._min_size(to_inches=True) + m_w2_2, m_h2_2 = t2_2._min_size(to_inches=True) + + assert ((sug_height_2 >= m_h0_2) and + (2/3 * sug_height_2 >= m_h2_2) and + (1/3 * sug_height_2 >= m_h1_2)) and \ + ((1/2 * sug_width_2 >= m_w0_2) and + (1/2 * sug_width_2 >= m_w2_2) and + (1/2 * sug_width_2 >= m_w1_2)), \ + ("when text size elements in image *require* larger-than rcParams "+ + "the min size of text objects should be less than or equal to allocated "+ + "sizing for the image (no annotation)") + + assert ((2 * # 1/ rel width of smallest width of images + cow.rcParams["base_height"] * + cow.rcParams["base_aspect_ratio"]) < + np.max([2 * m_w0_2, 2 * m_w1_2, 2 * m_w2_2])) or \ + ((3 * # 1/ rel width of smallest width of images + cow.rcParams["base_height"]) < + np.max([m_h0_2, 3/2 * m_h2_2, 3 * m_h1_2])), \ + ("inner test to, ensuring that second example provides case where "+ + "text size element in image *require* larger-than rcParams "+ + "default sizing (specifically width, but code allows for either)") -def test_patch__svg_get_sizes(): - g0 = p9.ggplot(p9_data.mpg) +\ - p9.geom_bar(p9.aes(x="hwy")) +\ - p9.labs(title = 'Plot 0') - g1 = p9.ggplot(p9_data.mpg) +\ - p9.geom_point(p9.aes(x="hwy", y = "displ")) +\ - p9.labs(title = 'Plot 1') - g2 = p9.ggplot(p9_data.mpg) +\ - p9.geom_point(p9.aes(x="hwy", y = "displ", color="class")) +\ - p9.labs(title = 'Plot 2') - g3 = p9.ggplot(p9_data.mpg[p9_data.mpg["class"].isin(["compact", - "suv", - "pickup"])]) +\ - p9.geom_histogram(p9.aes(x="hwy"),bins=10) +\ - p9.facet_wrap("class") +def test_patch__doable_size_SimpleText(): + """ + testing patch's _doable_size functionality - # basic option ---------- - vis1 = cow.patch(g0,g1,g2) +\ + test focuses on only text objects that would correctly fit inside + the suggested size + """ + # overall tests should deal with (1) annotations (title & tags) + # (2) nested & not nested (3) vary with too small and acceptably sized + # figures. + + # 1. test only text attributes (start with default_size) + # should see sizing directly relate to default_size + + # basic option (text fits in desired box) ---------- + + t0 = cow.text("An approximate answer to the right problem\n"+ + "is worth a good deal more than an exact\n"+ + "answer to an approximate problem. ~John Tukey") +\ + p9.element_text(size = 15) + + t1 = cow.text("If I can’t picture it,\n"+ + "I can’t understand it. ~Albert Einstein") +\ + p9.element_text(size = 17, ha="left") + + t2 = cow.text("Tables usually outperform graphics in\n"+ + "reporting on small data sets of 20\n"+ + "numbers or less. ~John Tukey") +\ + p9.element_text(size=12, ha="left") + + t3 = cow.text(label="Pardon my French.\n"+ + "~Non-French speaking person") +\ + p9.element_text(size = 20) + + vis1 = cow.patch(t0, t2, t3) +\ cow.layout(design = np.array([[0,1], [0,2]]), - rel_heights = [4,1]) + rel_heights = [1,2]) - # successful sizings ---- - sizes, logics = vis1._svg_get_sizes(width_pt = 20 * 72, - height_pt = 20 * 72) + sug_width, sug_height = vis1._default_size() - requested_sizes = [(10,20), (10,16), (10,4)] + sizes_list, doable = vis1._doable_size(width=sug_width, height=sug_height) - assert np.all(logics), \ - "expected all plotnine objects to be able to be sized correctly "+\ - "in very large output (v1)" + sizes_list_in = np.array([[to_inches(x[0], "pt"), to_inches(x[1], "pt")] + for x in sizes_list]) - assert type(sizes) is list and \ - np.all([len(s) == 2 and type(s) is tuple for s in sizes]), \ - "expected structure of sizes list is incorrect (v1)" + assert doable, \ + ("if patch's width & height for doable_size is much higher than "+ + "minimum size possible, _doable_size should return doable") - assert np.all([2/3 < (sizes[s_idx][0]/requested_sizes[s_idx][0]) < 1.5 and \ - 2/3 < (sizes[s_idx][1]/requested_sizes[s_idx][1]) < 1.5 - for s_idx in [0,1,2]]), \ - "suggested sizing in sizes isn't too extreme relative to true "+\ - "requested sizes- this is just a sanity check, "+\ - "not a robust test (v1)" + assert np.allclose([sug_width/2]*3, sizes_list_in[:,0]) and \ + np.allclose(sizes_list_in[:,0].min(), + rcParams["base_height"] * rcParams["base_aspect_ratio"]) and \ + ("if static patch uses default size, expected width sizing "+ + "should be met (both relative to rcParams & structure mult)") - # failed sizings ------ - sizes_f, logics_f = vis1._svg_get_sizes(width_pt = 10 * 72, - height_pt = 10 * 72) + assert np.allclose(sug_height * np.array([1,1/3,2/3]), sizes_list_in[:,1]) and \ + np.allclose(sizes_list_in[:,1].min(), + rcParams["base_height"]) and \ + ("if static patch uses default size, expected height sizing "+ + "should be met (both relative to rcParams & structure mult)") - requested_sizes_f = [(5,10), (5,8), (5,2)] # final one should fail... - assert not np.all(logics_f) and (logics_f == [True, True, False]), \ - "expected not all plotnine objects to be able to be sized correctly "+\ - "in small output (v1.1 - failed)" + # non-minimum _default_sizing used: - assert type(sizes_f) is list and \ - np.all([len(s) == 2 and type(s) is tuple for s in sizes_f]), \ - "expected structure of sizes list is incorrect (v1.1 - failed)" + sug_width2, sug_height2 = 15,30 + sizes_list2, doable2 = vis1._doable_size(width=sug_width2, height=sug_height2) - assert np.all([2/3 < (sizes_f[s_idx][0]/requested_sizes_f[s_idx][0]) < 1.5 and \ - 2/3 < (sizes_f[s_idx][1]/requested_sizes_f[s_idx][1]) < 1.5 - for s_idx in [0,1]]), \ - "suggested sizing in sizes (that didn't fail) isn't too extreme "+\ - "relative to true "+\ - "requested sizes- this is just a sanity check, "+\ - "not a robust test (v1.1 - failed)" + sizes_list_in2 = np.array([[to_inches(x[0], "pt"), to_inches(x[1], "pt")] + for x in sizes_list2]) - assert sizes_f[2][0] < 1 and sizes_f[2][1] < 1, \ - "expected failed sizing (due to being too small, to return a scaling" +\ - "below 1 (note the correction to scaling should be 1/suggested scaling))," +\ - "(v1.1 - failed)" + assert doable2, \ + ("if patch's width & height for doable_size is much higher than "+ + "minimum size possible, size > _doable_size should return doable") - # nested option -------- - vis_nested = cow.patch(g0,cow.patch(g1, g2)+\ - cow.layout(ncol=1, rel_heights = [4,1])) +\ - cow.layout(nrow=1) + assert np.allclose([sug_width2/2]*3, sizes_list_in2[:,0]) and \ + (sizes_list_in2[:,0].min() > + rcParams["base_height"] * rcParams["base_aspect_ratio"]) and \ + ("if static patch uses sizes > default size, expected width sizing "+ + "should be met relative to input sizing") - # successful sizings ---- - sizes_n, logics_n = vis_nested._svg_get_sizes(width_pt = 20 * 72, - height_pt = 20 * 72) - requested_sizes_n = [(10,20), (10,16), (10,4)] - - assert np.all(_flatten_nested_list(logics_n)), \ - "expected all plotnine objects to be able to be sized correctly "+\ - "in very large output (v2 - nested)" - - - assert type(sizes_n) is list and len(sizes_n) == 2 and \ - type(sizes_n[0]) is tuple and type(sizes_n[1]) is list and \ - len(sizes_n[0]) == 2 and len(sizes_n[1]) == 2 and \ - np.all([len(s) == 2 and type(s) is tuple for s in sizes_n[1]]), \ - "expected structure of sizes list is incorrect (v2 - nested)" - - sizes_n_flattened = _flatten_nested_list(sizes_n) - - assert np.all([2/3 < (sizes_n_flattened[s_idx][0]/requested_sizes[s_idx][0]) < 1.5 and \ - 2/3 < (sizes_n_flattened[s_idx][1]/requested_sizes[s_idx][1]) < 1.5 - for s_idx in [0,1,2]]), \ - "suggested sizing in sizes isn't too extreme relative to true "+\ - "requested sizes- this is just a sanity check, "+\ - "not a robust test (v2 - nested)" - - assert np.allclose(sizes_n_flattened, sizes), \ - "expected nested and non-nested suggested sizes to be equal (v1 vs v2)" - - # failed sizings ------ - sizes_f_n, logics_f_n = vis_nested._svg_get_sizes(width_pt = 10 * 72, - height_pt = 10 * 72) - - requested_sizes_f = [(5,10), (5,8), (5,2)] # final one should fail ... - - logic_f_n_flat = _flatten_nested_list(logics_f_n) - sizes_f_n_flat = _flatten_nested_list(sizes_f_n) - - assert not np.all(logic_f_n_flat) and \ - (logic_f_n_flat == [True, True, False]), \ - "expected not all plotnine objects to be able to be sized correctly "+\ - "in smaller output (v2.1 - nested, failed)" - - assert type(sizes_f_n) is list and len(sizes_f_n) == 2 and \ - type(sizes_f_n[0]) is tuple and type(sizes_f_n[1]) is list and \ - len(sizes_f_n[0]) == 2 and len(sizes_f_n[1]) == 2 and \ - np.all([len(s) == 2 and type(s) is tuple for s in sizes_f_n[1]]), \ - "expected structure of sizes list is incorrect (v2.1 - nested, failed)" - - assert np.all([2/3 < (sizes_f_n_flat[s_idx][0]/requested_sizes_f[s_idx][0]) < 1.5 and \ - 2/3 < (sizes_f_n_flat[s_idx][1]/requested_sizes_f[s_idx][1]) < 1.5 - for s_idx in [0,1]]), \ - "suggested sizing in sizes (that didn't fail) isn't too extreme "+\ - "relative to true "+\ - "requested sizes- this is just a sanity check, "+\ - "not a robust test (v2.1 - nested, failed)" - - assert sizes_f_n_flat[2][0] < 1 and sizes_f_n_flat[2][1] < 1, \ - "expected failed sizing (due to being too small, to return a scaling" +\ - "below 1 (note the correction to scaling should be 1/suggested scaling))," +\ - "(v2.1 - nested, failed)" - - assert np.allclose(sizes_f_n_flat, sizes_f), \ - "expected nested and non-nested suggested sizes to be equal (v1.1 vs v2.1 - failed)" - -@given(st.floats(min_value=.5, max_value=49), - st.floats(min_value=.5, max_value=49), - st.floats(min_value=.5, max_value=49), - st.floats(min_value=.5, max_value=49), - st.floats(min_value=.5, max_value=49), - st.floats(min_value=.5, max_value=49)) -def test_patch__process_sizes(w1,h1,w2,h2,w3,h3): - # default patch (not needed) - empty_patch = cow.patch() - - # not nested ------- - sizes = [(w1,h1),(w2,h2),(w3,h3)] - - # all true --- - logics = [True, True, True] - - out_s = empty_patch._process_sizes(sizes = sizes, logics = logics) - assert out_s == sizes, \ - "expected sizes to return if all logics true" - - # not all true ---- - logics_f = [True, True, False] - out_s1 = empty_patch._process_sizes(sizes = sizes, logics = logics_f) - - assert np.allclose(out_s1, 1/np.min(sizes[2])), \ - "expected max_scaling should be the max of 1/width_scale and "+\ - "1/height_scale assoicated with failed plot(s) (v1.1 - 1 plot failed)" - - logics_f2 = [True, False, False] - out_s2 = empty_patch._process_sizes(sizes = sizes, logics = logics_f2) - - assert np.allclose(out_s2, 1/np.min([w2,h2,w3,h3])), \ - "expected max_scaling should be the max of 1/width_scale and "+\ - "1/height_scale assoicated with failed plot(s) (v1.2 - 2 plot failed)" - - # nested --------- - sizes_n = [(w1,h1),[(w2,h2),(w3,h3)]] - - # all true --- - logics_n = [True, [True, True]] - out_s_n = empty_patch._process_sizes(sizes = sizes_n, logics = logics_n) - assert out_s_n == sizes_n, \ - "expected unflatted sizes to return if all logics true (v2 - nested)" - - # not all true ---- - logics_n_f = [True, [True, False]] - out_s1 = empty_patch._process_sizes(sizes = sizes_n, logics = logics_n_f) - - assert np.allclose(out_s1, 1/np.min(sizes_n[1][1])), \ - "expected max_scaling should be the max of 1/width_scale and "+\ - "1/height_scale assoicated with failed plot(s) (v2.1 - 1 plot failed)" - - logics_f2 = [True, [False, False]] - out_s2 = empty_patch._process_sizes(sizes = sizes, logics = logics_f2) - - assert np.allclose(out_s2, 1/np.min([w2,h2,w3,h3])), \ - "expected max_scaling should be the max of 1/width_scale and "+\ - "1/height_scale assoicated with failed plot(s) (v2.2 - 2 plot failed)" + assert np.allclose(sug_height2 * np.array([1,1/3,2/3]), sizes_list_in2[:,1]) and \ + (sizes_list_in2[:,1].min() > + rcParams["base_height"]) and \ + ("if static patch uses sizes > default size, expected height sizing "+ + "should be met relative to input sizing") + + + + # nested structure, _default_sizing used: + vis1_nested = cow.patch(grobs = [t0, + cow.patch(t2, t3) + cow.layout(design = np.array([[0],[1],[1]]))]) +\ + cow.layout(design = np.array([[0,1]])) + + + sug_width_n, sug_height_n = vis1_nested._default_size() + + sizes_list_n, doable_n = vis1_nested.\ + _doable_size(width=sug_width_n, height=sug_height_n) + + sizes_list_n_in = np.array([[to_inches(x[0], "pt"), to_inches(x[1], "pt")] + for x in sizes_list_n[:1]] + + [[to_inches(x[0], "pt"), to_inches(x[1], "pt")] + for x in sizes_list_n[1]]) + + assert doable_n, \ + ("if patch's width & height for doable_size is much higher than "+ + "minimum size possible, _doable_size should return doable - nested") + + assert np.allclose([sug_width_n/2]*3, sizes_list_n_in[:,0]) and \ + np.allclose(sizes_list_n_in[:,0].min(), + rcParams["base_height"] * rcParams["base_aspect_ratio"]) and \ + ("if static patch uses default size, expected width sizing "+ + "should be met (both relative to rcParams & structure mult) - nested") + + assert np.allclose(sug_height_n * np.array([1,1/3,2/3]), + sizes_list_n_in[:,1]) and \ + np.allclose(sizes_list_n_in[:,1].min(), + rcParams["base_height"]) and \ + ("if static patch uses default size, expected height sizing "+ + "should be met (both relative to rcParams & structure mult) - nested") + + + # nested structure, non-minimum _default_sizing used: + + + sug_width2, sug_height2 = 15,30 + sizes_list_n2, doable_n2 = vis1_nested.\ + _doable_size(width=sug_width2, height=sug_height2) + + sizes_list_n_in2 = np.array([[to_inches(x[0], "pt"), to_inches(x[1], "pt")] + for x in sizes_list_n2[:1]] + + [[to_inches(x[0], "pt"), to_inches(x[1], "pt")] + for x in sizes_list_n2[1]]) + + assert doable_n2, \ + ("if patch's width & height for doable_size is much higher than "+ + "minimum size possible, size > _doable_size should return doable - nested") + + assert np.allclose([sug_width2/2]*3, sizes_list_n_in2[:,0]) and \ + (sizes_list_n_in2[:,0].min() > + rcParams["base_height"] * rcParams["base_aspect_ratio"]) and \ + ("if static patch uses sizes > default size, expected width sizing "+ + "should be met relative to input sizing - nested") + + assert np.allclose(sug_height2 * np.array([1,1/3,2/3]), + sizes_list_n_in2[:,1]) and \ + (sizes_list_n_in2[:,1].min() > + rcParams["base_height"]) and \ + ("if static patch uses sizes > default size, expected height sizing "+ + "should be met relative to input sizing - nested") + + + + + # second option (text doesn't fits in desired box) ---------- + + t0_2 = cow.text("An approximate answer to the right problem\n"+ + "is worth a good deal more than an exact\n"+ + "answer to an approximate problem. ~John Tukey") +\ + p9.element_text(size = 25) + + t1_2 = cow.text("If I can’t picture it,\n"+ + "I can’t understand it. ~Albert Einstein") +\ + p9.element_text(size = 20, ha="left") + + t2_2 = cow.text("Tables usually outperform graphics in\n"+ + "reporting on small data sets of 20\n"+ + "numbers or less. ~John Tukey") +\ + p9.element_text(size=25, ha="left") + + vis_nf = cow.patch(t0_2, t1_2, t2_2) +\ + cow.layout(design = np.array([[0,1], + [0,2]]), + rel_heights = [1,2]) + + sug_width_nf = 2 * rcParams["base_height"] * rcParams["base_aspect_ratio"] + sug_height_nf = 3 * rcParams["base_height"] + + sug_width_nf_min, sug_height_nf_min = vis_nf._default_size() + + assert (sug_width_nf < sug_width_nf_min) and \ + (sug_height_nf <= sug_height_nf_min), \ + ("test structure (that rcParams default size is too small for " + + "figure creation) was incorrectly defined (needed width is larger)") + + sizes_list_nf, doable_nf = vis_nf.\ + _doable_size(width=sug_width_nf, height=sug_height_nf, + data_dict = {"size-num-attempts": 1}) + + sizes_list_nf_in = np.array([[to_inches(x[0], "pt"), to_inches(x[1], "pt")] + for x in sizes_list_nf]) + + vis_nf._doable_size(width=sug_width_nf_min, height=sug_height_nf_min) -# global savings and showing and creating ------ + + # nested + + +def test_patch_doable_size_StaticNonSimple(): + """ + testing patch's _doable_size functionality + + test focuses on varability in acutal requested size + based on plotnine's varying correct input size required + """ + # overall tests should deal with (1) annotations (title & tags) + # (2) nested & not nested (3) vary with too small and acceptably sized + # figures. + + # 2. write a second test with ggplot images and save the size + # for a regression test (data_regression) + + + + + +# def test_patch__svg_get_sizes(): +# g0 = p9.ggplot(p9_data.mpg) +\ +# p9.geom_bar(p9.aes(x="hwy")) +\ +# p9.labs(title = 'Plot 0') + +# g1 = p9.ggplot(p9_data.mpg) +\ +# p9.geom_point(p9.aes(x="hwy", y = "displ")) +\ +# p9.labs(title = 'Plot 1') + +# g2 = p9.ggplot(p9_data.mpg) +\ +# p9.geom_point(p9.aes(x="hwy", y = "displ", color="class")) +\ +# p9.labs(title = 'Plot 2') + +# g3 = p9.ggplot(p9_data.mpg[p9_data.mpg["class"].isin(["compact", +# "suv", +# "pickup"])]) +\ +# p9.geom_histogram(p9.aes(x="hwy"),bins=10) +\ +# p9.facet_wrap("class") + + +# # basic option ---------- +# vis1 = cow.patch(g0,g1,g2) +\ +# cow.layout(design = np.array([[0,1], +# [0,2]]), +# rel_heights = [4,1]) + +# # successful sizings ---- +# sizes, logics = vis1._svg_get_sizes(width_pt = 20 * 72, +# height_pt = 20 * 72) + +# requested_sizes = [(10,20), (10,16), (10,4)] + +# assert np.all(logics), \ +# "expected all plotnine objects to be able to be sized correctly "+\ +# "in very large output (v1)" + +# assert type(sizes) is list and \ +# np.all([len(s) == 2 and type(s) is tuple for s in sizes]), \ +# "expected structure of sizes list is incorrect (v1)" + +# assert np.all([2/3 < (sizes[s_idx][0]/requested_sizes[s_idx][0]) < 1.5 and \ +# 2/3 < (sizes[s_idx][1]/requested_sizes[s_idx][1]) < 1.5 +# for s_idx in [0,1,2]]), \ +# "suggested sizing in sizes isn't too extreme relative to true "+\ +# "requested sizes- this is just a sanity check, "+\ +# "not a robust test (v1)" + +# # failed sizings ------ +# sizes_f, logics_f = vis1._svg_get_sizes(width_pt = 10 * 72, +# height_pt = 10 * 72) + +# requested_sizes_f = [(5,10), (5,8), (5,2)] # final one should fail... + +# assert not np.all(logics_f) and (logics_f == [True, True, False]), \ +# "expected not all plotnine objects to be able to be sized correctly "+\ +# "in small output (v1.1 - failed)" + +# assert type(sizes_f) is list and \ +# np.all([len(s) == 2 and type(s) is tuple for s in sizes_f]), \ +# "expected structure of sizes list is incorrect (v1.1 - failed)" + +# assert np.all([2/3 < (sizes_f[s_idx][0]/requested_sizes_f[s_idx][0]) < 1.5 and \ +# 2/3 < (sizes_f[s_idx][1]/requested_sizes_f[s_idx][1]) < 1.5 +# for s_idx in [0,1]]), \ +# "suggested sizing in sizes (that didn't fail) isn't too extreme "+\ +# "relative to true "+\ +# "requested sizes- this is just a sanity check, "+\ +# "not a robust test (v1.1 - failed)" + +# assert sizes_f[2][0] < 1 and sizes_f[2][1] < 1, \ +# "expected failed sizing (due to being too small, to return a scaling" +\ +# "below 1 (note the correction to scaling should be 1/suggested scaling))," +\ +# "(v1.1 - failed)" + +# # nested option -------- +# vis_nested = cow.patch(g0,cow.patch(g1, g2)+\ +# cow.layout(ncol=1, rel_heights = [4,1])) +\ +# cow.layout(nrow=1) + +# # successful sizings ---- +# sizes_n, logics_n = vis_nested._svg_get_sizes(width_pt = 20 * 72, +# height_pt = 20 * 72) +# requested_sizes_n = [(10,20), (10,16), (10,4)] + +# assert np.all(_flatten_nested_list(logics_n)), \ +# "expected all plotnine objects to be able to be sized correctly "+\ +# "in very large output (v2 - nested)" + + +# assert type(sizes_n) is list and len(sizes_n) == 2 and \ +# type(sizes_n[0]) is tuple and type(sizes_n[1]) is list and \ +# len(sizes_n[0]) == 2 and len(sizes_n[1]) == 2 and \ +# np.all([len(s) == 2 and type(s) is tuple for s in sizes_n[1]]), \ +# "expected structure of sizes list is incorrect (v2 - nested)" + +# sizes_n_flattened = _flatten_nested_list(sizes_n) + +# assert np.all([2/3 < (sizes_n_flattened[s_idx][0]/requested_sizes[s_idx][0]) < 1.5 and \ +# 2/3 < (sizes_n_flattened[s_idx][1]/requested_sizes[s_idx][1]) < 1.5 +# for s_idx in [0,1,2]]), \ +# "suggested sizing in sizes isn't too extreme relative to true "+\ +# "requested sizes- this is just a sanity check, "+\ +# "not a robust test (v2 - nested)" + +# assert np.allclose(sizes_n_flattened, sizes), \ +# "expected nested and non-nested suggested sizes to be equal (v1 vs v2)" + +# # failed sizings ------ +# sizes_f_n, logics_f_n = vis_nested._svg_get_sizes(width_pt = 10 * 72, +# height_pt = 10 * 72) + +# requested_sizes_f = [(5,10), (5,8), (5,2)] # final one should fail ... + +# logic_f_n_flat = _flatten_nested_list(logics_f_n) +# sizes_f_n_flat = _flatten_nested_list(sizes_f_n) + +# assert not np.all(logic_f_n_flat) and \ +# (logic_f_n_flat == [True, True, False]), \ +# "expected not all plotnine objects to be able to be sized correctly "+\ +# "in smaller output (v2.1 - nested, failed)" + +# assert type(sizes_f_n) is list and len(sizes_f_n) == 2 and \ +# type(sizes_f_n[0]) is tuple and type(sizes_f_n[1]) is list and \ +# len(sizes_f_n[0]) == 2 and len(sizes_f_n[1]) == 2 and \ +# np.all([len(s) == 2 and type(s) is tuple for s in sizes_f_n[1]]), \ +# "expected structure of sizes list is incorrect (v2.1 - nested, failed)" + +# assert np.all([2/3 < (sizes_f_n_flat[s_idx][0]/requested_sizes_f[s_idx][0]) < 1.5 and \ +# 2/3 < (sizes_f_n_flat[s_idx][1]/requested_sizes_f[s_idx][1]) < 1.5 +# for s_idx in [0,1]]), \ +# "suggested sizing in sizes (that didn't fail) isn't too extreme "+\ +# "relative to true "+\ +# "requested sizes- this is just a sanity check, "+\ +# "not a robust test (v2.1 - nested, failed)" + +# assert sizes_f_n_flat[2][0] < 1 and sizes_f_n_flat[2][1] < 1, \ +# "expected failed sizing (due to being too small, to return a scaling" +\ +# "below 1 (note the correction to scaling should be 1/suggested scaling))," +\ +# "(v2.1 - nested, failed)" + +# assert np.allclose(sizes_f_n_flat, sizes_f), \ +# "expected nested and non-nested suggested sizes to be equal (v1.1 vs v2.1 - failed)" + +# @given(st.floats(min_value=.5, max_value=49), +# st.floats(min_value=.5, max_value=49), +# st.floats(min_value=.5, max_value=49), +# st.floats(min_value=.5, max_value=49), +# st.floats(min_value=.5, max_value=49), +# st.floats(min_value=.5, max_value=49)) +# def test_patch__process_sizes(w1,h1,w2,h2,w3,h3): +# # default patch (not needed) +# empty_patch = cow.patch() + +# # not nested ------- +# sizes = [(w1,h1),(w2,h2),(w3,h3)] + +# # all true --- +# logics = [True, True, True] + +# out_s = empty_patch._process_sizes(sizes = sizes, logics = logics) +# assert out_s == sizes, \ +# "expected sizes to return if all logics true" + +# # not all true ---- +# logics_f = [True, True, False] +# out_s1 = empty_patch._process_sizes(sizes = sizes, logics = logics_f) + +# assert np.allclose(out_s1, 1/np.min(sizes[2])), \ +# "expected max_scaling should be the max of 1/width_scale and "+\ +# "1/height_scale assoicated with failed plot(s) (v1.1 - 1 plot failed)" + +# logics_f2 = [True, False, False] +# out_s2 = empty_patch._process_sizes(sizes = sizes, logics = logics_f2) + +# assert np.allclose(out_s2, 1/np.min([w2,h2,w3,h3])), \ +# "expected max_scaling should be the max of 1/width_scale and "+\ +# "1/height_scale assoicated with failed plot(s) (v1.2 - 2 plot failed)" + +# # nested --------- +# sizes_n = [(w1,h1),[(w2,h2),(w3,h3)]] + +# # all true --- +# logics_n = [True, [True, True]] +# out_s_n = empty_patch._process_sizes(sizes = sizes_n, logics = logics_n) +# assert out_s_n == sizes_n, \ +# "expected unflatted sizes to return if all logics true (v2 - nested)" + +# # not all true ---- +# logics_n_f = [True, [True, False]] +# out_s1 = empty_patch._process_sizes(sizes = sizes_n, logics = logics_n_f) + +# assert np.allclose(out_s1, 1/np.min(sizes_n[1][1])), \ +# "expected max_scaling should be the max of 1/width_scale and "+\ +# "1/height_scale assoicated with failed plot(s) (v2.1 - 1 plot failed)" + +# logics_f2 = [True, [False, False]] +# out_s2 = empty_patch._process_sizes(sizes = sizes, logics = logics_f2) + +# assert np.allclose(out_s2, 1/np.min([w2,h2,w3,h3])), \ +# "expected max_scaling should be the max of 1/width_scale and "+\ +# "1/height_scale assoicated with failed plot(s) (v2.2 - 2 plot failed)" + + +# global savings and showing and creating ------ def _layouts_and_patches_patch_plus_layout(idx): # creation of some some ggplot objects g0 = p9.ggplot(p9_data.mpg) +\ @@ -779,9 +1473,9 @@ def test_patch__svg(): # printing ---------- -def test_patch__repr__(monkeypatch,capsys): +def test_patch__str__(monkeypatch,capsys): """ - test patch .__repr__, static + test patch .__str__, static print(.) also creates the figure """ @@ -811,7 +1505,7 @@ def test_patch__repr__(monkeypatch,capsys): re_cap.start() == 0 and re_cap.end() == len(captured.out),\ "expected __str__ expression for patch to be of format" -def test_patch__str__(capsys): +def test_patch__repr__(capsys): g0 = p9.ggplot(p9_data.mpg) +\ p9.geom_bar(p9.aes(x="hwy")) +\ @@ -839,40 +1533,7 @@ def test_patch__str__(capsys): " # grobs and layout" -# grammar ----------- - -def test_patch__and__(image_regression): - # # creation of some some ggplot objects - # g0 = p9.ggplot(p9_data.mpg) +\ - # p9.geom_bar(p9.aes(x="hwy")) +\ - # p9.labs(title = 'Plot 0') - - # g1 = p9.ggplot(p9_data.mpg) +\ - # p9.geom_point(p9.aes(x="hwy", y = "displ")) +\ - # p9.labs(title = 'Plot 1') - - # g2 = p9.ggplot(p9_data.mpg) +\ - # p9.geom_point(p9.aes(x="hwy", y = "displ", color="class")) +\ - # p9.labs(title = 'Plot 2') - - # g3 = p9.ggplot(p9_data.mpg[p9_data.mpg["class"].isin(["compact", - # "suv", - # "pickup"])]) +\ - # p9.geom_histogram(p9.aes(x="hwy")) +\ - # p9.facet_wrap("class") - - # g0p = cow.patch(g0) - # g1p = cow.patch(g1) - # g2p = cow.patch(g2) - # g3p = cow.patch(g3) - - - # g01 = g0p + g1p - # g02 = g0p + g2p - # g012 = g0p + g1p + g2p - # g012_2 = g01 + g2p - pass - +# uniquify ----------- def test_patch_svg_uniquify1(image_regression): """ @@ -938,7 +1599,6 @@ def test_patch_svg_uniquify2(image_regression): image_regression.check(fid.getvalue(), diff_threshold=.1) - def test_patch_svg_uniquify3(image_regression): """ test 3: patch _svg creation use of svg_utils._unquify_svg_str diff --git a/tests/test_layout_elements.py b/tests/test_layout_elements.py index 3487d2b..e33c863 100644 --- a/tests/test_layout_elements.py +++ b/tests/test_layout_elements.py @@ -230,7 +230,7 @@ def test_layout__yokogaki_ordering(): assert np.all(yorder2 == np.array([0,2,3,1])), \ "yokogaki ordering with second simple example failed (2)" -def test_layout__eq__(): +def test_layout__not_eq__(): """ test layout equal for things not equal... (basic test) """ @@ -250,6 +250,24 @@ def test_layout__eq__(): assert l1 != "example string", \ "layout doesn't equal a string..." +def test_layout__eq__(): + """ + test for equal layout even if not complete (nrow, ncol) + """ + l1 = cow.layout(nrow = 1) + l2 = cow.layout(nrow=1) + + assert l1 == l2, \ + "expected equality of layout even if not complete (nrow, but no ncol)" + + l1 = cow.layout(ncol = 1) + l2 = cow.layout(ncol=1) + + assert l1 == l2, \ + "expected equality of layout even if not complete (ncol, but no nrow)" + + + def test_layout__element_locations2(): """ test assumes default relative widths and heights diff --git a/tests/test_text_elements.py b/tests/test_text_elements.py new file mode 100644 index 0000000..6152487 --- /dev/null +++ b/tests/test_text_elements.py @@ -0,0 +1,728 @@ +import cowpatch as cow +import plotnine as p9 + +import pytest +import copy +import numpy as np + +from pytest_regressions import data_regression, image_regression +from hypothesis import given, strategies as st, settings +from datetime import timedelta + +import io +import re +import matplotlib.pyplot as plt + +import pdb + +# text object --------------------- + +def test_text__clean_element_text(): + """ + test that element_text stored inside is always a p9.themes.themeable (static) + + Both through initialization and addition + + tests __init__ and _clean_element_text + """ + + et_simple = p9.element_text(size = 12) + + + # initialization + mytitle2 = cow.text("title", element_text = et_simple) + + assert isinstance(mytitle2.element_text, p9.themes.themeable.text), \ + "internal saving of element_text is expected to be a themable.text "+\ + "(not a element_text) - through initalization" + + # addition + mytitle = cow.text("title") + + mytitle_simple = mytitle + et_simple + + assert isinstance(mytitle_simple.element_text, p9.themes.themeable.text), \ + "internal saving of element_text is expected to be a themable.text "+\ + "(not a element_text) - updated with addition" + +def test_text__add__(): + """ + test addition (static) + + checks + 1. theme cannot be added directly + 2. does not update self, but updates a new creation + """ + et_simple = p9.element_text(size = 12) + mytitle = cow.text("title", element_text = et_simple) + + # theme cannot be added + with pytest.raises(Exception) as e_info: + mytitle + p9.theme_bw() + # cannot add theme to text object directly + # would need to use internal _update_element_text_from_theme + + # addition doesn't update old object + + mytitle2 = mytitle + p9.element_text(size = 9) + + assert mytitle.element_text.properties.get("size") == 12 and \ + mytitle2.element_text.properties.get("size") == 9, \ + "expected that the __add__ updates and creates a new object and " +\ + "doesn't update the previous text object directly" + +def test_text__update_element_text_from_theme(): + """ + test of _update_element_text_from_theme (static test) + """ + mytitle = cow.text("title") + mytitle2 = mytitle + p9.element_text(size = 13) + mytitle_c = copy.deepcopy(mytitle) + mytitle2_c = copy.deepcopy(mytitle2) + + # test error with key + with pytest.raises(Exception) as e_info: + mytitle._update_element_text_from_theme(p9.theme_bw(), + key = "new_name") + with pytest.raises(Exception) as e_info: + mytitle2._update_element_text_from_theme(p9.theme_bw(), + key = "new_name") + + # test update + mytheme = p9.theme_bw() + p9.theme(text = p9.element_text(size = 9)) + + mytitle2._update_element_text_from_theme(mytheme, "text") + mytitle._update_element_text_from_theme(mytheme, "text") + + assert mytitle.element_text.properties.get('size') == 9 and \ + mytitle_c.element_text is None,\ + "expected theme to update element_text of text object "+\ + "(with element_text originally being none)" + + + assert mytitle2.element_text.properties.get('size') == 9 and \ + mytitle2_c.element_text.properties.get('size') == 13,\ + "expected theme to update element_text of text object "+\ + "(with element_text default)" + +def test_text__update_element_text_from_theme2(): + """ + test _update_element_text_from_theme + + """ + + my_ets = [p9.element_text(size = 8), + p9.element_text(angle = 45, size = 12)] + my_themes = [# just text size + p9.theme(cow_tag = my_ets[0], + cow_caption = my_ets[0], + cow_text = my_ets[0], + cow_title = my_ets[0], + cow_subtitle = my_ets[0]), + # new angle + p9.theme(cow_tag = my_ets[1], + cow_caption = my_ets[1], + cow_text = my_ets[1], + cow_title = my_ets[1], + cow_subtitle = my_ets[1]), + p9.theme_bw() # no update information for cow stuff + ] + + + _mytheme = my_themes[0] + for _type in ["cow_tag", "cow_caption", "cow_text", + "cow_title", "cow_subtitle"]: + # test if object already has element_text + # test that override does the correct connection to _type + + a = cow.text("Fig 1", _type = _type) + a = a + p9.element_text(angle = 100) + a._update_element_text_from_theme(_mytheme) + a_et_expected = dict(rotation = 100, size = 8, visible = True) + + assert a.element_text.properties == a_et_expected, \ + "error updating element_text from theme preserving other "+\ + "attributes (a, theme 0)" + + b = cow.text("Fig 1", _type = _type) + b = b + p9.element_text(size = 8) + b._update_element_text_from_theme(_mytheme) + b_et_expected = dict(size = 8, visible = True) + + assert b.element_text.properties == b_et_expected, \ + "error updating element_text from theme preserving other "+\ + "attributes (b, theme 0)" + + c = cow.text("Fig 1", _type = _type) + c._update_element_text_from_theme(_mytheme) + c_et_expected = dict(size = 8, visible = True) + + assert c.element_text.properties == c_et_expected, \ + "error updating element_text from theme preserving other "+\ + "attributes (c, theme 0)" + + _mytheme = my_themes[1] + for _type in ["cow_tag", "cow_caption", "cow_text", + "cow_title", "cow_subtitle"]: + # test if object already has element_text + # test that override does the correct connection to _type + + a = cow.text("Fig 1", _type = _type) + a = a + p9.element_text(angle = 90) + a._update_element_text_from_theme(_mytheme) + a_et_expected = dict(rotation = 45, size = 12, visible = True) + + assert a.element_text.properties == a_et_expected, \ + "error updating element_text from theme preserving other "+\ + "attributes (a, theme 1)" + + b = cow.text("Fig 1", _type = _type) + b = b + p9.element_text(size = 8) + b._update_element_text_from_theme(_mytheme) + b_et_expected = dict(rotation = 45, size = 12, visible = True) + + assert b.element_text.properties == b_et_expected, \ + "error updating element_text from theme preserving other "+\ + "attributes (b, theme 1)" + + c = cow.text("Fig 1", _type = _type) + c._update_element_text_from_theme(_mytheme) + c_et_expected = dict(rotation = 45, size = 12, visible = True) + + assert c.element_text.properties == c_et_expected, \ + "error updating element_text from theme preserving other "+\ + "attributes (c, theme 1)" + + + _mytheme = my_themes[2] + for _type in ["cow_tag", "cow_caption", "cow_text", + "cow_title", "cow_subtitle"]: + # test if object already has element_text + # test that override does the correct connection to _type + + a = cow.text("Fig 1", _type = _type) + a = a + p9.element_text(angle = 90) + + with pytest.raises(Exception) as e_info: + a._update_element_text_from_theme(_mytheme) + assert e_info.typename == "ValueError" and \ + e_info.value.args[0] == "key parameter in "+\ + "_update_element_text_from_theme function call needs to "+\ + "be a key in the provided theme's themeables.", \ + "if theme doesn't have cow_... attributes cannot be used to update"+\ + "text object, so we expect an error (a, theme 2)" + + + + + b = cow.text("Fig 1", _type = _type) + b = b + p9.element_text(size = 8) + + with pytest.raises(Exception) as e_info: + b._update_element_text_from_theme(_mytheme) + assert e_info.typename == "ValueError" and \ + e_info.value.args[0] == "key parameter in "+\ + "_update_element_text_from_theme function call needs to "+\ + "be a key in the provided theme's themeables.", \ + "if theme doesn't have cow_... attributes cannot be used to update"+\ + "text object, so we expect an error (b, theme 2)" + + + c = cow.text("Fig 1", _type = _type) + with pytest.raises(Exception) as e_info: + c._update_element_text_from_theme(_mytheme) + assert e_info.typename == "ValueError" and \ + e_info.value.args[0] == "key parameter in "+\ + "_update_element_text_from_theme function call needs to "+\ + "be a key in the provided theme's themeables.", \ + "if theme doesn't have cow_... attributes cannot be used to update"+\ + "text object, so we expect an error (c, theme 2)" + + +def test_text__additional_rotation0(image_regression): + """ + test text's _additional_rotation function + + ensure that 0 angle rotation returns the same object + """ + # no angle change means that same object + myt = cow.text(label="Portland", + element_text=p9.element_text(size=15,angle=15), + _type="cow_text") + + myt2 = myt._additional_rotation() + + assert myt2 == myt, \ + "default rotation of 0 should return the same text element" + + myt2_2 = myt._additional_rotation(angle=0) + + assert myt2_2 == myt, \ + "explicit rotation of 0 should return the same text element" + + # no element text pre-defined (this highlights the function shouldn't + # be used outside internal functions + + myt_b = cow.text(label="Portland", + _type="cow_text") + + myt_b2 = myt_b._additional_rotation() + + assert myt_b2 == myt_b, \ + ("default rotation of 0 should return the same text element, " + + "initial has non element_text defined") + + myt_b2_2 = myt_b._additional_rotation(angle=0) + + assert myt_b2_2 == myt_b, \ + ("explicit rotation of 0 should return the same text element, " + + "initial has non element_text defined") + + # image regression for partially rotated object + with io.BytesIO() as fid2: + myt.save(filename=fid2, _format = "png") + image_regression.check(fid2.getvalue(), diff_threshold=.1) + + # some attribute is added but not angle + myt_e = cow.text(label="Portland", + element_text=p9.element_text(size=15), + _type="cow_text") + myt_e_2 = myt_e._additional_rotation(angle=0) + + assert myt_e_2 == myt_e_2, \ + ("default rotation of 0 should return the same text element, " + + "intial has element_text without angle defined") + + myt_e_3 = myt_e._additional_rotation(angle=15) + expected_attributes_e_3 = myt_e.element_text.theme_element.properties + expected_attributes_e_3["rotation"] = 15 + assert myt_e_3.element_text.theme_element.properties == expected_attributes_e_3, \ + ("for intial has element_text without angle defined, non-zero angle "+ + "should update it from zero") + + + +def test_text__additional_rotation(image_regression): + """ + test text's _additional_rotation function + + ensures correct addition of rotation without overwriting other + parmeters + """ + # additional parameters not lost + myt = cow.text(label="Portland", + element_text=p9.element_text(size=15,angle=15,color="blue"), + _type="cow_text") + + myt2 = myt._additional_rotation(angle = 20) + + old = myt.element_text.theme_element.properties + new_expected = old.copy() + new_expected["rotation"] = 35 + + assert myt2.element_text.theme_element.properties == new_expected, \ + ("_additional_rotation should only impact the angle, not other "+ + "element attributes") + + # beyond 360 only does a max of 360 degrees + myt2_b = myt._additional_rotation(angle = 350) + + assert np.allclose(myt2_b.element_text.theme_element.properties["rotation"], 5), \ + "_additional_rotation should keep angle betwen [0, 360)" + + myt2_b2 = myt._additional_rotation(angle = 345) + + assert (myt2_b2.element_text.theme_element.properties["rotation"] < 360) and \ + (np.allclose(myt2_b2.element_text.theme_element.properties["rotation"], 0) or + np.allclose(myt2_b2.element_text.theme_element.properties["rotation"], 360)), \ + "_additional_rotation should keep angle betwen [0, 360), 360 strict" + + + # image regression for partially rotated object + with io.BytesIO() as fid2: + myt2.save(filename=fid2, _format = "png") + image_regression.check(fid2.getvalue(), diff_threshold=.1) + + + + +def test_text__get_full_element_text(): + """ + test _get_full_element_text (static test - using _type text not cow_text, etc.) + """ + mytitle = cow.text("title") + mytitle._define_type("text") + + et_full_base = mytitle._get_full_element_text() + + mytitle_size9 = mytitle + p9.element_text(size = 9) + et_full_size9 = mytitle_size9._get_full_element_text() + + mytitle_size13 = mytitle + p9.element_text(size = 13) + et_full_size13 = mytitle_size13._get_full_element_text() + + assert et_full_size9.properties.get("size") == 9 and \ + et_full_size13.properties.get("size") == 13, \ + "expected specified element_text attributes to be perserved (size)" + + for key in et_full_base.properties: + if key != "size": + assert (et_full_base.properties.get(key) == \ + et_full_size9.properties.get(key)) and \ + (et_full_base.properties.get(key) == \ + et_full_size13.properties.get(key)), \ + (("expected all properties but key=size (key = %s) to match "+\ + "if all inherits properties from global theme") % key) + +def test_text__get_full_element_text2(): + """ + test _get_full_element_text, static + + tests that nothing is override but additional attributes are provided + relative to the _type parameter and default theme structure + + """ + for _type in ["cow_tag", "cow_caption", "cow_text", + "cow_title", "cow_subtitle"]: + # test if object already has element_text + # test that override does the correct connection to _type + + a = cow.text("Fig 1", _type = _type) + a = a + p9.element_text(angle = 90) + + b = cow.text("Fig 1", _type = _type) + b = b + p9.element_text(size = 8) + + c = cow.text("Fig 1", _type = _type) + + a_et = a._get_full_element_text() + a_et_properties_expected = \ + {'visible': True, + 'rotation': 90, + 'size': cow.rcParams[_type].properties["size"]} + + b_et = b._get_full_element_text() + b_et_properties_expected = \ + {'visible': True, + 'size': 8} + + c_et = c._get_full_element_text() + c_et_properties_expected = cow.rcParams[_type].properties + + for key, v_expected in a_et_properties_expected.items(): + assert a_et.properties[key] == v_expected, \ + "text element with rotation should preserve rotation " +\ + "but update size relative to default (_type :%s, key: %s)" % (_type, key) + + for key, v_expected in b_et_properties_expected.items(): + assert b_et.properties[key] == v_expected, \ + "text element with size should not be updated with base "+\ + "size information (_type :%s, key: %s)" % (_type, key) + + for key, v_expected in c_et_properties_expected.items(): + assert c_et.properties[key] == v_expected, \ + "text object without element_text attribute should fully update "+\ + "with default size information (_type :%s, key: %s)" % (_type, key) + + +def test_text__min_size(): + """ + test _min_size (static) + + This function only checks relative sizes reported from _min_size + """ + + for _type in ["cow_tag", "cow_caption", "cow_text", + "cow_title", "cow_subtitle"]: + + # base option + a = cow.text("Fig 1", _type = _type) + + out = a._min_size() + out_in = a._min_size(to_inches=True) + + assert np.all([np.allclose(cow.utils.to_inches(out[i], "pt"), out_in[i]) + for i in [0,1]]), \ + ("error in _min_size's application of `to_inches` parameter "+ + "type: %s, test 1" % _type) + + a_long = cow.text("Fig 1 Fig 1", _type = _type) + out_long = a_long._min_size() + + assert (3 * out[0]> out_long[0] > 2 * out[0]) and \ + np.allclose(out_long[1], out[1]), \ + ("if text size 2x+ in length, relative sizes should match, " + + "type: %s, text 1" % _type) + + a_tall = cow.text("Fig 1\nFig 1", _type = _type) + out_tall = a_tall._min_size() + + assert (2.5 * out[1] > out_tall[1] > 1.5 * out[1]) and \ + np.allclose(out_tall[0], out[0]), \ + ("if text size ~2x in height, relative sizes should match, " + + "type: %s, text 1" % _type) + + # rotated text 90 degrees + a2 = a + p9.element_text(angle = 90) + + out2 = a2._min_size() + out2_in = a2._min_size(to_inches=True) + + assert np.all([np.allclose(cow.utils.to_inches(out2[i], "pt"), out2_in[i]) + for i in [0,1]]), \ + ("error in _min_size's application of `to_inches` parameter "+ + "type: %s, test 2" % _type) + + assert np.allclose(out, out2[::-1]), \ + ("expected a rotation of 90 degrees to text to directly flip " + + "required size, type : %s, test 1+2" % _type) + + + a2_long = cow.text("Fig 1 Fig 1", _type = _type) +\ + p9.element_text(angle = 90) + out2_long = a2_long._min_size() + + assert (3 * out2[1]> out2_long[1] > 2 * out2[1]) and \ + np.allclose(out2_long[0], out2[0]), \ + ("if text size 2x+ in length (and rotated 90 degrees), " + + "relative sizes should match, type: %s, text 2" % _type) + + a2_tall = cow.text("Fig 1\nFig 1", _type = _type) +\ + p9.element_text(angle = 90) + out2_tall = a2_tall._min_size() + + assert (2.5 * out2[0] > out2_tall[0] > 1.5 * out2[0]) and \ + np.allclose(out2_tall[1], out2[1]), \ + ("if text size ~2x in height (and rotated 90 degrees), "+ + "relative sizes should match, type: %s, text 2" % _type) + +def test_text__min_size2(data_regression): + """ + _min_size data regression test on sizing for different base types + + Notes + ----- + the usage of data_regression requires ensuring the saved + dictionary is "yaml"-ifable (which is why we do things like + converting the values to strings). + """ + static_min_size_data = {} + + for _type in ["cow_tag", "cow_caption", "cow_text", + "cow_title", "cow_subtitle"]: + + a = cow.text("Fig 1", _type = _type) + out = a._min_size() + inner_dict_list = [str(x) for x in out] + static_min_size_data[_type] = inner_dict_list + + data_regression.check(static_min_size_data) + + +@pytest.mark.parametrize("_type", ["cow_tag", "cow_caption", "cow_text", + "cow_title", "cow_subtitle"]) +def test_text__base_text_image(image_regression, _type): + """ + tests for _base_text_image (regression for image, + and bbox versus _min_size check) + """ + # base option + a = cow.text("Fig 1", _type = _type) + fig, bbox = a._base_text_image() + + # matchin with min_size + ms_out = a._min_size() + assert (bbox.width > ms_out[0] > .5 * bbox.width) and \ + (bbox.height > ms_out[1] > .5 * bbox.height), \ + ("expect that bbox is slightly bigger than min_sizing, " + + "type = %s, test 1" % _type) + + with io.BytesIO() as fid2: + fig.savefig(fname=fid2, format = "png") + image_regression.check(fid2.getvalue(), diff_threshold=.1) + + +@pytest.mark.parametrize("_type", ["cow_tag", "cow_caption", "cow_text", + "cow_title", "cow_subtitle"]) +def test_text__base_text_image2(image_regression, _type): + """ + tests for _base_text_image (regression for image, + and bbox versus _min_size check) + + test rotated 90 degrees + """ + # rotated option + a = cow.text("Fig 1", _type = _type) +\ + p9.element_text(angle = 90) + fig, bbox = a._base_text_image() + + # matchin with min_size + ms_out = a._min_size() + assert (bbox.width > ms_out[0] > .5 * bbox.width) and \ + (bbox.height > ms_out[1] > .5 * bbox.height), \ + ("expect that bbox is slightly bigger than min_sizing, " + + "type = %s, test 1" % _type) + + with io.BytesIO() as fid2: + fig.savefig(fname=fid2, format = "png") + image_regression.check(fid2.getvalue(), diff_threshold=.1) + +@pytest.mark.parametrize("_type", ["cow_tag", "cow_caption", "cow_text", + "cow_title", "cow_subtitle"]) +@given(st.floats(min_value=.5, max_value=49), + st.floats(min_value=.5, max_value=49)) +@settings(max_examples=4, deadline=timedelta(milliseconds=1000)) +def test_text__default_size(_type, height, width): + """ + test for _default_size function + """ + # base option + a = cow.text("Fig 1", _type = _type) + ms_out = a._min_size(to_inches = True) + + assert np.allclose(a._default_size(), ms_out), \ + ("if width & height input are none, expect to return " + + "_min_size(to_inches=True), type = %s, test 1" % _type) + + ds_out = a._default_size(width=width) + + assert np.allclose(ds_out, (width, ms_out[1])), \ + ("if width is non-none, height none, expected return " + + "(width, min_size(to_inches=True)[1]), type = %s, test 1" % _type) + + ds_out2 = a._default_size(height=height) + + assert np.allclose(ds_out2, (ms_out[0], height)), \ + ("if height is non-none, width none, expected return " + + "(min_size(to_inches=True)[0], height), type = %s, test 1" % _type) + + ds_out3 = a._default_size(height=height, width=width) + + assert np.allclose(ds_out3, (width, height)), \ + ("if height is non-none, width non-none, expected return " + + "(width, height), type = %s, test 1" % _type) + + + # long option + a_long = cow.text("Fig 1 Fig 1", _type = _type) + ms_out_long = a_long._min_size(to_inches = True) + + assert np.allclose(a_long._default_size(), ms_out_long), \ + ("if width & height input are none, expect to return " + + "_min_size(to_inches=True), type = %s, test 2" % _type) + + assert np.allclose(a_long._default_size(), ms_out_long), \ + ("if width & height input are none, expect to return " + + "_min_size(to_inches=True), type = %s, test 2" % _type) + + ds_out_long = a_long._default_size(width=width) + + assert np.allclose(ds_out_long, (width, ms_out_long[1])), \ + ("if width is non-none, height none, expected return " + + "(width, min_size(to_inches=True)[1]), type = %s, test 2" % _type) + + ds_out2_long = a_long._default_size(height=height) + + assert np.allclose(ds_out2_long, (ms_out_long[0], height)), \ + ("if height is non-none, width none, expected return " + + "(min_size(to_inches=True)[0], height), type = %s, test 2" % _type) + + ds_out3_long = a_long._default_size(height=height, width=width) + + assert np.allclose(ds_out3_long, (width, height)), \ + ("if height is non-none, width non-none, expected return " + + "(width, height), type = %s, test 2" % _type) + + + + + # 2x height option + a_tall = cow.text("Fig 1\nFig 1", _type = _type) + ms_out_tall = a_tall._min_size(to_inches = True) + + assert np.allclose(a_tall._default_size(), ms_out_tall), \ + ("if width & height input are none, expect to return " + + "_min_size(to_inches=True), type = %s, test 3" % _type) + + ds_out_tall = a_tall._default_size(width=width) + + assert np.allclose(ds_out_tall, (width, ms_out_tall[1])), \ + ("if width is non-none, height none, expected return " + + "(width, min_size(to_inches=True)[1]), type = %s, test 3" % _type) + + ds_out2_tall = a_tall._default_size(height=height) + + assert np.allclose(ds_out2_tall, (ms_out_tall[0], height)), \ + ("if height is non-none, width none, expected return " + + "(min_size(to_inches=True)[0], height), type = %s, test 3" % _type) + + ds_out3_tall = a_tall._default_size(height=height, width=width) + + assert np.allclose(ds_out3_tall, (width, height)), \ + ("if height is non-none, width non-none, expected return " + + "(width, height), type = %s, test 3" % _type) + + +def test_text__svg(): + raise ValueError("Not Tested") + + +def test_text_save(): + raise ValueError("Not Tested") + + +def test_text_show(): + raise ValueError("Not Tested") + + +# printing ------------- +@pytest.mark.parametrize("_type", ["cow_tag", "cow_caption", "cow_text", + "cow_title", "cow_subtitle"]) +def test_text__str__(monkeypatch, capsys, _type): + """ + test text.__str__, static + + print(.) also creates the figure + """ + monkeypatch.setattr(plt, "show", lambda:None) + + a = cow.text("Fig 1", _type = _type) + + print(a) + captured = capsys.readouterr() + + re_cap = re.search("\\n", captured.out) + assert re_cap is not None and \ + re_cap.start() == 0 and re_cap.end() == len(captured.out),\ + "expected __str__ expression for text to be of format" + + +@pytest.mark.parametrize("_type", ["cow_tag", "cow_caption", "cow_text", + "cow_title", "cow_subtitle"]) +def test_text__repr__(monkeypatch, capsys, _type): + """ + test text.__repr__, static + """ + monkeypatch.setattr(plt, "show", lambda:None) + + a = cow.text("Fig 1", _type = _type) + + print(repr(a)) + captured = capsys.readouterr() + lines = re.split("\\n", captured.out) + + re_cap = re.search("\\n", captured.out) + assert re_cap is not None and \ + re_cap.start() == 0 and re_cap.end() == (len(lines[0]) + 1),\ + "expected __repr__ first line expression for text to be "+\ + "of format" + + + assert ((lines[1] == "label:") and + (lines[2] == " |" + a.label) and + (lines[3] == "element_text:") and + (lines[5] == "_type:") and + (lines[6] == " |\"%s\"" % _type)), \ + "expected overall __repr__ expression to follow a specific format" + + diff --git a/tests/test_text_elements/test__base_text_image2_cow_caption_.png b/tests/test_text_elements/test__base_text_image2_cow_caption_.png new file mode 100644 index 0000000..927f006 Binary files /dev/null and b/tests/test_text_elements/test__base_text_image2_cow_caption_.png differ diff --git a/tests/test_text_elements/test__base_text_image2_cow_subtitle_.png b/tests/test_text_elements/test__base_text_image2_cow_subtitle_.png new file mode 100644 index 0000000..4ad282b Binary files /dev/null and b/tests/test_text_elements/test__base_text_image2_cow_subtitle_.png differ diff --git a/tests/test_text_elements/test__base_text_image2_cow_tag_.png b/tests/test_text_elements/test__base_text_image2_cow_tag_.png new file mode 100644 index 0000000..b0c8db2 Binary files /dev/null and b/tests/test_text_elements/test__base_text_image2_cow_tag_.png differ diff --git a/tests/test_text_elements/test__base_text_image2_cow_text_.png b/tests/test_text_elements/test__base_text_image2_cow_text_.png new file mode 100644 index 0000000..04d36b2 Binary files /dev/null and b/tests/test_text_elements/test__base_text_image2_cow_text_.png differ diff --git a/tests/test_text_elements/test__base_text_image2_cow_title_.png b/tests/test_text_elements/test__base_text_image2_cow_title_.png new file mode 100644 index 0000000..b0c8db2 Binary files /dev/null and b/tests/test_text_elements/test__base_text_image2_cow_title_.png differ diff --git a/tests/test_text_elements/test__base_text_image_cow_caption_.png b/tests/test_text_elements/test__base_text_image_cow_caption_.png new file mode 100644 index 0000000..927f006 Binary files /dev/null and b/tests/test_text_elements/test__base_text_image_cow_caption_.png differ diff --git a/tests/test_text_elements/test__base_text_image_cow_subtitle_.png b/tests/test_text_elements/test__base_text_image_cow_subtitle_.png new file mode 100644 index 0000000..33bc3ce Binary files /dev/null and b/tests/test_text_elements/test__base_text_image_cow_subtitle_.png differ diff --git a/tests/test_text_elements/test__base_text_image_cow_tag_.png b/tests/test_text_elements/test__base_text_image_cow_tag_.png new file mode 100644 index 0000000..14f5542 Binary files /dev/null and b/tests/test_text_elements/test__base_text_image_cow_tag_.png differ diff --git a/tests/test_text_elements/test__base_text_image_cow_text_.png b/tests/test_text_elements/test__base_text_image_cow_text_.png new file mode 100644 index 0000000..98b09d0 Binary files /dev/null and b/tests/test_text_elements/test__base_text_image_cow_text_.png differ diff --git a/tests/test_text_elements/test__base_text_image_cow_title_.png b/tests/test_text_elements/test__base_text_image_cow_title_.png new file mode 100644 index 0000000..14f5542 Binary files /dev/null and b/tests/test_text_elements/test__base_text_image_cow_title_.png differ diff --git a/tests/test_text_elements/test__min_size2.yml b/tests/test_text_elements/test__min_size2.yml new file mode 100644 index 0000000..ab2997d --- /dev/null +++ b/tests/test_text_elements/test__min_size2.yml @@ -0,0 +1,15 @@ +cow_caption: +- '20.90625' +- '9.75' +cow_subtitle: +- '25.78125' +- '11.25' +cow_tag: +- '31.3125' +- '13.5' +cow_text: +- '31.3125' +- '13.5' +cow_title: +- '31.3125' +- '13.5' diff --git a/tests/test_text_elements/test_text__additional_rotation.png b/tests/test_text_elements/test_text__additional_rotation.png new file mode 100644 index 0000000..970b40c Binary files /dev/null and b/tests/test_text_elements/test_text__additional_rotation.png differ diff --git a/tests/test_text_elements/test_text__additional_rotation0.png b/tests/test_text_elements/test_text__additional_rotation0.png new file mode 100644 index 0000000..fc3123a Binary files /dev/null and b/tests/test_text_elements/test_text__additional_rotation0.png differ diff --git a/tests/test_text_elements/test_text__base_text_image2_cow_caption_.png b/tests/test_text_elements/test_text__base_text_image2_cow_caption_.png new file mode 100644 index 0000000..927f006 Binary files /dev/null and b/tests/test_text_elements/test_text__base_text_image2_cow_caption_.png differ diff --git a/tests/test_text_elements/test_text__base_text_image2_cow_subtitle_.png b/tests/test_text_elements/test_text__base_text_image2_cow_subtitle_.png new file mode 100644 index 0000000..4ad282b Binary files /dev/null and b/tests/test_text_elements/test_text__base_text_image2_cow_subtitle_.png differ diff --git a/tests/test_text_elements/test_text__base_text_image2_cow_tag_.png b/tests/test_text_elements/test_text__base_text_image2_cow_tag_.png new file mode 100644 index 0000000..b0c8db2 Binary files /dev/null and b/tests/test_text_elements/test_text__base_text_image2_cow_tag_.png differ diff --git a/tests/test_text_elements/test_text__base_text_image2_cow_text_.png b/tests/test_text_elements/test_text__base_text_image2_cow_text_.png new file mode 100644 index 0000000..04d36b2 Binary files /dev/null and b/tests/test_text_elements/test_text__base_text_image2_cow_text_.png differ diff --git a/tests/test_text_elements/test_text__base_text_image2_cow_title_.png b/tests/test_text_elements/test_text__base_text_image2_cow_title_.png new file mode 100644 index 0000000..b0c8db2 Binary files /dev/null and b/tests/test_text_elements/test_text__base_text_image2_cow_title_.png differ diff --git a/tests/test_text_elements/test_text__base_text_image_cow_caption_.png b/tests/test_text_elements/test_text__base_text_image_cow_caption_.png new file mode 100644 index 0000000..927f006 Binary files /dev/null and b/tests/test_text_elements/test_text__base_text_image_cow_caption_.png differ diff --git a/tests/test_text_elements/test_text__base_text_image_cow_subtitle_.png b/tests/test_text_elements/test_text__base_text_image_cow_subtitle_.png new file mode 100644 index 0000000..33bc3ce Binary files /dev/null and b/tests/test_text_elements/test_text__base_text_image_cow_subtitle_.png differ diff --git a/tests/test_text_elements/test_text__base_text_image_cow_tag_.png b/tests/test_text_elements/test_text__base_text_image_cow_tag_.png new file mode 100644 index 0000000..14f5542 Binary files /dev/null and b/tests/test_text_elements/test_text__base_text_image_cow_tag_.png differ diff --git a/tests/test_text_elements/test_text__base_text_image_cow_text_.png b/tests/test_text_elements/test_text__base_text_image_cow_text_.png new file mode 100644 index 0000000..98b09d0 Binary files /dev/null and b/tests/test_text_elements/test_text__base_text_image_cow_text_.png differ diff --git a/tests/test_text_elements/test_text__base_text_image_cow_title_.png b/tests/test_text_elements/test_text__base_text_image_cow_title_.png new file mode 100644 index 0000000..14f5542 Binary files /dev/null and b/tests/test_text_elements/test_text__base_text_image_cow_title_.png differ diff --git a/tests/test_text_elements/test_text__min_size2.yml b/tests/test_text_elements/test_text__min_size2.yml new file mode 100644 index 0000000..ab2997d --- /dev/null +++ b/tests/test_text_elements/test_text__min_size2.yml @@ -0,0 +1,15 @@ +cow_caption: +- '20.90625' +- '9.75' +cow_subtitle: +- '25.78125' +- '11.25' +cow_tag: +- '31.3125' +- '13.5' +cow_text: +- '31.3125' +- '13.5' +cow_title: +- '31.3125' +- '13.5' diff --git a/tests/test_utils.py b/tests/test_utils.py index 5c52810..390deaf 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -171,3 +171,150 @@ def test__flatten_nested_list(): x3_flat = cowpatch.utils._flatten_nested_list(x3) assert x3_flat == x3, \ "_flatten_nested_list failed to flatten correctly (v3 - no nesting)" + + +@given(st.integers(min_value=1, max_value=100000), + st.floats(min_value = 1.001, max_value=5), + st.floats(min_value = .2, max_value=.9), + st.floats(min_value = 5, max_value=25), + st.floats(min_value = 5, max_value=25)) +def test__overall_scale_recommendation_patch_NoAnnotation(seed, + upscale, + lesser_upscale, + width, height): + """ + basic test _overall_scale_recommendation_patch, without annotation + """ + np.random.seed(seed) + size_multiplier = list(np.array([1, upscale, 1, + (upscale * lesser_upscale + (1-lesser_upscale)) + ])[np.random.choice(4,4, replace=False)]) + text_inner_size = (0,0) + text_extra_size = (0,0) + original_overall_size = (width, height) + + out = cowpatch.utils._overall_scale_recommendation_patch(size_multiplier, + text_inner_size, + text_extra_size, + original_overall_size) + + assert np.allclose(out[0], + np.array([width,height])*upscale) and \ + np.allclose(out[1], upscale) and \ + np.allclose(np.max(size_multiplier), upscale), \ + "overall upscaling matches needs of largest upscale grob" + + +@given(st.integers(min_value=1, max_value=100000), + st.floats(min_value = 1.001, max_value=5), + st.floats(min_value = .2, max_value=.9), + st.floats(min_value = 5, max_value=25), + st.floats(min_value = 5, max_value=25), + + st.floats(min_value = .1, max_value=10), + st.floats(min_value = .1, max_value=10), + st.floats(min_value = .1, max_value=2), + st.floats(min_value = .1, max_value=2)) +def test__overall_scale_recommendation_patch_Annotation(seed, + upscale, + lesser_upscale, + width, height, + text_inner_w, + text_inner_h, + text_extra_w, + text_extra_h): + """ + basic test _overall_scale_recommendation_patch, with annotation + + solid checks with/without extra_size, with/without inner_size + not really any checks on both non-zero + """ + np.random.seed(seed) + size_multiplier = list(np.array([1, upscale, 1, + (upscale * lesser_upscale + (1-lesser_upscale)) + ])[np.random.choice(4,4, replace=False)]) + text_inner_size = (text_inner_w,text_inner_h) + text_extra_size = (text_extra_w,text_extra_h) + original_overall_size = (width, height) + + out_no_ann = cowpatch.utils._overall_scale_recommendation_patch(size_multiplier, + (0,0), + (0,0), + original_overall_size) + + + # both non-zero --------------- + + out = cowpatch.utils._overall_scale_recommendation_patch(size_multiplier, + text_inner_size, + text_extra_size, + original_overall_size) + + + # no extra size ------------------ + + out_no_extra_size = cowpatch.utils._overall_scale_recommendation_patch(size_multiplier, + text_inner_size, + (0,0), + original_overall_size) + + # text_inner_size < original_overall_size, and text_extra_size = (0,0)) + if (text_inner_size[0] < width) and (text_inner_size[1] < height): + assert np.allclose(out_no_extra_size[0], np.array([width,height])*upscale) and \ + np.allclose(out_no_extra_size[1], upscale) and \ + np.allclose(np.max(size_multiplier), upscale), \ + ("overall upscaling matches needs of largest upscale grob " + + "(text_inner_size < original_overall_size, and " + + "text_extra_size = (0,0))") + elif (text_inner_size[0] > out_no_ann[0][0]) or \ + (text_inner_size[1] > out_no_ann[0][1]): + # text inner size is larger than normal upscale required + assert out_no_extra_size[1] > np.max(size_multiplier), \ + ("if text_inner_size is larger than the base scale-up sizing " + + "the scaling will be larger then for just the default") + + if text_inner_size[0] > out_no_ann[0][0]: + assert np.allclose(text_inner_size[0], out_no_extra_size[0][0]), \ + ("if text_inner_size is larger than the base scale-up sizing " + + "outputted sizing should be relative to it (width)") + + if text_inner_size[1] > out_no_ann[0][1]: + assert np.allclose(text_inner_size[1], out_no_extra_size[0][1]), \ + ("if text_inner_size is larger than the base scale-up sizing " + + "outputted sizing should be relative to it (height)") + + # no inner size ------------------ + + # TODO: see comments + # Comment: TODO: look at the equation on overleaf (page 2 & 3) and + # inspect the ipad notes on why we thought it was $<$ not $>$ for the + # logic.) + + out_no_inner_size = cowpatch.utils._overall_scale_recommendation_patch(size_multiplier, + (0,0), + text_extra_size, + original_overall_size) + + # minimum size of height and width + expected_size_out_no_inner_size_no_ratio = \ + (np.array(original_overall_size) - np.array(text_extra_size)) *\ + np.max(size_multiplier) + np.array(text_extra_size) + + scaling = np.max([expected_size_out_no_inner_size_no_ratio[i]/original_overall_size[i] + for i in [0,1]]) + + # calculation to get height and width that obey min sizing and keep height/width ratio + # TODO: the idea that you can use an argmin isn't correct - this is relative ratio even now. + min_val = np.argmin(expected_size_out_no_inner_size_no_ratio) + expected_size_out_no_inner_size =\ + expected_size_out_no_inner_size_no_ratio[min_val] *\ + [np.array([1, height/width]), np.array([width,height,1])][min_val] + + + assert np.allclose(expected_size_out_no_inner_size, out_no_inner_size[0]) and \ + np.allclose(scaling, out_no_inner_size[1]), \ + ("expected (with inner_size = 0), that extra size impacted both the "+ + "initial scaling estimate and the final estimated size") + + +