# some functions defined here to avoid numpy import def _mean(x): if len(x) == 0: raise ValueError("x must have positive length") return float(sum(x)) / len(x) def _argmin(x): return sorted(enumerate(x), key=lambda t: t[1])[0][0] def _argmax(x): return sorted(enumerate(x), key=lambda t: t[1], reverse=True)[0][0] def _df_anno(xanchor, yanchor, x, y): """ Default annotation parameters """ return dict(xanchor=xanchor, yanchor=yanchor, x=x, y=y, showarrow=False) def _add_inside_to_position(pos): if not ("inside" in pos or "outside" in pos): pos.add("inside") return pos def _prepare_position(position, prepend_inside=False): if position is None: position = "top right" pos_str = position position = set(position.split(" ")) if prepend_inside: position = _add_inside_to_position(position) return position, pos_str def annotation_params_for_line(shape_type, shape_args, position): # all x0, x1, y0, y1 are used to place the annotation, that way it could # work with a slanted line # even with a slanted line, there are the horizontal and vertical # conventions of placing a shape x0 = shape_args["x0"] x1 = shape_args["x1"] y0 = shape_args["y0"] y1 = shape_args["y1"] X = [x0, x1] Y = [y0, y1] R = "right" T = "top" L = "left" C = "center" B = "bottom" M = "middle" aY = max(Y) iY = min(Y) eY = _mean(Y) aaY = _argmax(Y) aiY = _argmin(Y) aX = max(X) iX = min(X) eX = _mean(X) aaX = _argmax(X) aiX = _argmin(X) position, pos_str = _prepare_position(position) if shape_type == "vline": if position == set(["top", "left"]): return _df_anno(R, T, X[aaY], aY) if position == set(["top", "right"]): return _df_anno(L, T, X[aaY], aY) if position == set(["top"]): return _df_anno(C, B, X[aaY], aY) if position == set(["bottom", "left"]): return _df_anno(R, B, X[aiY], iY) if position == set(["bottom", "right"]): return _df_anno(L, B, X[aiY], iY) if position == set(["bottom"]): return _df_anno(C, T, X[aiY], iY) if position == set(["left"]): return _df_anno(R, M, eX, eY) if position == set(["right"]): return _df_anno(L, M, eX, eY) elif shape_type == "hline": if position == set(["top", "left"]): return _df_anno(L, B, iX, Y[aiX]) if position == set(["top", "right"]): return _df_anno(R, B, aX, Y[aaX]) if position == set(["top"]): return _df_anno(C, B, eX, eY) if position == set(["bottom", "left"]): return _df_anno(L, T, iX, Y[aiX]) if position == set(["bottom", "right"]): return _df_anno(R, T, aX, Y[aaX]) if position == set(["bottom"]): return _df_anno(C, T, eX, eY) if position == set(["left"]): return _df_anno(R, M, iX, Y[aiX]) if position == set(["right"]): return _df_anno(L, M, aX, Y[aaX]) raise ValueError('Invalid annotation position "%s"' % (pos_str,)) def annotation_params_for_rect(shape_type, shape_args, position): x0 = shape_args["x0"] x1 = shape_args["x1"] y0 = shape_args["y0"] y1 = shape_args["y1"] position, pos_str = _prepare_position(position, prepend_inside=True) if position == set(["inside", "top", "left"]): return _df_anno("left", "top", min([x0, x1]), max([y0, y1])) if position == set(["inside", "top", "right"]): return _df_anno("right", "top", max([x0, x1]), max([y0, y1])) if position == set(["inside", "top"]): return _df_anno("center", "top", _mean([x0, x1]), max([y0, y1])) if position == set(["inside", "bottom", "left"]): return _df_anno("left", "bottom", min([x0, x1]), min([y0, y1])) if position == set(["inside", "bottom", "right"]): return _df_anno("right", "bottom", max([x0, x1]), min([y0, y1])) if position == set(["inside", "bottom"]): return _df_anno("center", "bottom", _mean([x0, x1]), min([y0, y1])) if position == set(["inside", "left"]): return _df_anno("left", "middle", min([x0, x1]), _mean([y0, y1])) if position == set(["inside", "right"]): return _df_anno("right", "middle", max([x0, x1]), _mean([y0, y1])) if position == set(["inside"]): # TODO: Do we want this? return _df_anno("center", "middle", _mean([x0, x1]), _mean([y0, y1])) if position == set(["outside", "top", "left"]): return _df_anno( "right" if shape_type == "vrect" else "left", "bottom" if shape_type == "hrect" else "top", min([x0, x1]), max([y0, y1]), ) if position == set(["outside", "top", "right"]): return _df_anno( "left" if shape_type == "vrect" else "right", "bottom" if shape_type == "hrect" else "top", max([x0, x1]), max([y0, y1]), ) if position == set(["outside", "top"]): return _df_anno("center", "bottom", _mean([x0, x1]), max([y0, y1])) if position == set(["outside", "bottom", "left"]): return _df_anno( "right" if shape_type == "vrect" else "left", "top" if shape_type == "hrect" else "bottom", min([x0, x1]), min([y0, y1]), ) if position == set(["outside", "bottom", "right"]): return _df_anno( "left" if shape_type == "vrect" else "right", "top" if shape_type == "hrect" else "bottom", max([x0, x1]), min([y0, y1]), ) if position == set(["outside", "bottom"]): return _df_anno("center", "top", _mean([x0, x1]), min([y0, y1])) if position == set(["outside", "left"]): return _df_anno("right", "middle", min([x0, x1]), _mean([y0, y1])) if position == set(["outside", "right"]): return _df_anno("left", "middle", max([x0, x1]), _mean([y0, y1])) raise ValueError("Invalid annotation position %s" % (pos_str,)) def axis_spanning_shape_annotation(annotation, shape_type, shape_args, kwargs): """ annotation: a go.layout.Annotation object, a dict describing an annotation, or None shape_type: one of 'vline', 'hline', 'vrect', 'hrect' and determines how the x, y, xanchor, and yanchor values are set. shape_args: the parameters used to draw the shape, which are used to place the annotation kwargs: a dictionary that was the kwargs of a _process_multiple_axis_spanning_shapes spanning shapes call. Items in this dict whose keys start with 'annotation_' will be extracted and the keys with the 'annotation_' part stripped off will be used to assign properties of the new annotation. Property precedence: The annotation's x, y, xanchor, and yanchor properties are set based on the shape_type argument. Each property already specified in the annotation or through kwargs will be left as is (not replaced by the value computed using shape_type). Note that the xref and yref properties will in general get overwritten if the result of this function is passed to an add_annotation called with the row and col parameters specified. Returns an annotation populated with fields based on the annotation_position, annotation_ prefixed kwargs or the original annotation passed in to this function. """ # set properties based on annotation_ prefixed kwargs prefix = "annotation_" len_prefix = len(prefix) annotation_keys = list(filter(lambda k: k.startswith(prefix), kwargs.keys())) # If no annotation or annotation-key is specified, return None as we don't # want an annotation in this case if annotation is None and len(annotation_keys) == 0: return None # TODO: Would it be better if annotation were initialized to an instance of # go.layout.Annotation ? if annotation is None: annotation = dict() for k in annotation_keys: if k == "annotation_position": # don't set so that Annotation constructor doesn't complain continue subk = k[len_prefix:] annotation[subk] = kwargs[k] # set x, y, xanchor, yanchor based on shape_type and position annotation_position = None if "annotation_position" in kwargs.keys(): annotation_position = kwargs["annotation_position"] if shape_type.endswith("line"): shape_dict = annotation_params_for_line( shape_type, shape_args, annotation_position ) elif shape_type.endswith("rect"): shape_dict = annotation_params_for_rect( shape_type, shape_args, annotation_position ) for k in shape_dict.keys(): # only set property derived from annotation_position if it hasn't already been set # see above: this would be better as a go.layout.Annotation then the key # would be checked for validity here (otherwise it is checked later, # which I guess is ok too) if (k not in annotation) or (annotation[k] is None): annotation[k] = shape_dict[k] return annotation def split_dict_by_key_prefix(d, prefix): """ Returns two dictionaries, one containing all the items whose keys do not start with a prefix and another containing all the items whose keys do start with the prefix. Note that the prefix is not removed from the keys. """ no_prefix = dict() with_prefix = dict() for k in d.keys(): if k.startswith(prefix): with_prefix[k] = d[k] else: no_prefix[k] = d[k] return (no_prefix, with_prefix)