forked from plotly/plotly.py
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathshapeannotation.py
246 lines (223 loc) · 9.59 KB
/
shapeannotation.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# 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)