Skip to content

Commit f80e2a0

Browse files
Position of annotation on lines and rects
Making sure that position is for any orientation of the shape.
1 parent 3a52321 commit f80e2a0

File tree

2 files changed

+207
-26
lines changed

2 files changed

+207
-26
lines changed

packages/python/plotly/plotly/basedatatypes.py

+19-3
Original file line numberDiff line numberDiff line change
@@ -3604,7 +3604,14 @@ def _make_axis_spanning_shape(self, direction, shape, none_if_no_trace=True):
36043604
return shape
36053605

36063606
def _process_multiple_axis_spanning_shapes(
3607-
self, shape_args, row, col, direction, exclude_empty_subplots=True, **kwargs
3607+
self,
3608+
shape_args,
3609+
row,
3610+
col,
3611+
direction,
3612+
exclude_empty_subplots=True,
3613+
annotation=None,
3614+
**kwargs
36083615
):
36093616
"""
36103617
Add a shape or multiple shapes and call _make_axis_spanning_shape on
@@ -3619,7 +3626,7 @@ def _process_multiple_axis_spanning_shapes(
36193626
# this was called intending to add to a single plot (and
36203627
# self.add_shape succeeded)
36213628
# however, in the case of a single plot, xref and yref are not
3622-
# specified, so we specify them here to the following routines can work
3629+
# specified, so we specify them here so the following routines can work
36233630
# (they need to append " domain" to xref or yref)
36243631
self.layout["shapes"][-1].update(xref="x", yref="y")
36253632
n_shapes_after = len(self.layout["shapes"])
@@ -3638,7 +3645,15 @@ def _process_multiple_axis_spanning_shapes(
36383645
)
36393646
self.layout["shapes"] = self.layout["shapes"][:n_shapes_before] + new_shapes
36403647

3641-
def add_vline(self, x, row=None, col=None, exclude_empty_subplots=True, **kwargs):
3648+
def add_vline(
3649+
self,
3650+
x,
3651+
row=None,
3652+
col=None,
3653+
exclude_empty_subplots=True,
3654+
annotation=None,
3655+
**kwargs
3656+
):
36423657
"""
36433658
Add a vertical line to a plot or subplot that extends infinitely in the
36443659
y-dimension.
@@ -3665,6 +3680,7 @@ def add_vline(self, x, row=None, col=None, exclude_empty_subplots=True, **kwargs
36653680
col,
36663681
"vertical",
36673682
exclude_empty_subplots=exclude_empty_subplots,
3683+
annotation=annotation,
36683684
**kwargs
36693685
)
36703686
return self
+188-23
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,213 @@
1-
def compute_anchors(position, vertical):
1+
import plotly.graph_objects as go
2+
from numpy import argmax, argmin, mean
3+
4+
5+
def compute_anchors_for_line(position, vertical):
26
xanchor = None
37
yanchor = None
48
if vertical:
5-
if position in ["n", "sw", "se"]:
9+
if position in ["n", "top", "sw", "bottom left", "se", "bottom right"]:
610
yanchor = "bottom"
7-
if position in ["nw", "ne", "s"]:
11+
if position in ["nw", "top left", "ne", "top right", "s", "bottom"]:
812
yanchor = "top"
9-
if position in ["w", "e"]:
13+
if position in ["w", "left", "e", "right"]:
1014
yanchor = "middle"
11-
if position in ["ne", "e", "se"]:
15+
if position in ["ne", "top right", "e", "right", "se", "bottom right"]:
1216
xanchor = "left"
13-
if position in ["sw", "w", "nw"]:
17+
if position in ["sw", "bottom left", "w", "left", "nw", "top left"]:
1418
xanchor = "right"
15-
if position in ["n", "s"]:
19+
if position in ["n", "top", "s", "bottom"]:
1620
xanchor = "center"
1721
else:
18-
if position in ["nw", "n", "ne"]:
22+
if position in ["nw", "top left", "n", "top", "ne", "top right"]:
1923
yanchor = "bottom"
20-
if position in ["se", "s", "sw"]:
24+
if position in ["se", "bottom right", "s", "bottom", "sw", "bottom left"]:
2125
yanchor = "top"
22-
if position in ["w", "e"]:
26+
if position in ["w", "left", "e", "right"]:
2327
yanchor = "middle"
24-
if position in ["e", "sw", "nw"]:
28+
if position in ["e", "right", "sw", "bottom left", "nw", "top left"]:
2529
xanchor = "left"
26-
if position in ["ne", "se", "w"]:
30+
if position in ["ne", "top right", "se", "bottom right", "w", "left"]:
2731
xanchor = "right"
28-
if position in ["n", "s"]:
32+
if position in ["n", "top", "s", "bottom"]:
2933
xanchor = "center"
3034
return xanchor, yanchor
3135

3236

33-
def compute_coord(annotype, position):
34-
# If annotype is vline, this is y value, if hline, this is x value
35-
if annotype == "vline":
36-
if position in ["nw", "n", "ne"]:
37+
def compute_coord_for_line(position, vertical):
38+
# If vertical, this is y value, otherwise, this is x value
39+
if vertical:
40+
if position in ["nw", "top left", "n", "top", "ne", "top right"]:
3741
return 1
38-
if position in ["w", "e"]:
42+
if position in ["w", "left", "e", "right"]:
3943
return 0.5
40-
if position in ["sw", "s", "se"]:
44+
if position in ["sw", "bottom left", "s", "bottom", "se", "bottom right"]:
4145
return 0
42-
if annotype == "hline":
43-
if position in ["nw", "sw", "w", "nw"]:
46+
else:
47+
if position in [
48+
"nw",
49+
"top left",
50+
"sw",
51+
"bottom left",
52+
"w",
53+
"left",
54+
"nw",
55+
"top left",
56+
]:
4457
return 0
45-
if position in ["n", "s"]:
58+
if position in ["n", "top", "s", "bottom"]:
4659
return 0.5
47-
if position in ["ne", "e", "se"]:
60+
if position in ["ne", "top right", "e", "right", "se", "bottom right"]:
4861
return 1
62+
63+
64+
def annotation_params_for_line(shape_type, shape_args, position):
65+
# all x0, x1, y0, y1 are used to place the annotation, that way it could
66+
# work with a slanted line
67+
# even with a slanted line, there are the horizontal and vertical
68+
# conventions of placing a shape
69+
x0 = shape_args["x0"]
70+
x1 = shape_args["x1"]
71+
y0 = shape_args["y0"]
72+
y1 = shape_args["y1"]
73+
X = [x0, x1]
74+
Y = [y0, y1]
75+
R = "right"
76+
T = "top"
77+
L = "left"
78+
C = "center"
79+
B = "bottom"
80+
M = "middle"
81+
aY = max(Y)
82+
iY = min(Y)
83+
mY = mean(Y)
84+
aaY = argmax(Y)
85+
aiY = argmin(Y)
86+
aX = max(X)
87+
iX = min(X)
88+
mX = mean(X)
89+
aaX = argmax(X)
90+
aiX = argmin(X)
91+
92+
def _d(xanchor, yanchor, x, y):
93+
return dict(xanchor=xanchor, yanchor=yanchor, x=x, y=y)
94+
95+
if shape_type == "vline":
96+
if position == "top left":
97+
return _d(R, T, X[aaY], aY)
98+
if position == "top right":
99+
return _d(L, T, X[aaY], aY)
100+
if position == "top":
101+
return _d(C, B, X[aaY], aY)
102+
if position == "bottom left":
103+
return _d(R, B, X[aiY], iY)
104+
if position == "bottom right":
105+
return _d(L, B, X[aiY], iY)
106+
if position == "bottom":
107+
return _d(C, T, X[aiY], iY)
108+
if position == "left":
109+
return _d(R, M, eX, eY)
110+
if position == "right":
111+
return _d(L, M, eX, eY)
112+
return _d(L, T, X[aaY], aY)
113+
if shape_type == "hline":
114+
if position == "top left":
115+
return _d(L, B, iX, Y[aiX])
116+
if position == "top right":
117+
return _d(R, B, aX, Y[aaX])
118+
if position == "top":
119+
return _d(C, B, mX, mY)
120+
if position == "bottom left":
121+
return _d(L, T, iX, Y[aiX])
122+
if position == "bottom right":
123+
return _d(R, T, aX, Y[aaX])
124+
if position == "bottom":
125+
return _d(C, T, mX, mY)
126+
if position == "left":
127+
return _d(R, M, iX, Y[aiX])
128+
if position == "right":
129+
return _d(L, M, aX, Y[aaX])
130+
return _d(R, B, aX, Y[aaX])
131+
132+
133+
def annotation_params_for_rect(shape_type, shape_args, position):
134+
x0 = shape_args["x0"]
135+
x1 = shape_args["x1"]
136+
y0 = shape_args["y0"]
137+
y1 = shape_args["y1"]
138+
139+
def _d(xanchor, yanchor, x, y):
140+
return dict(xanchor=xanchor, yanchor=yanchor, x=x, y=y)
141+
142+
if position == "inside top left":
143+
return _d("left", "top", min([x0, x1]), max([y0, y1]))
144+
if position == "inside top right":
145+
return _d("right", "top", max([x0, x1]), max([y0, y1]))
146+
if position == "inside top":
147+
return _d("center", "top", mean([x0, x1]), max([y0, y1]))
148+
if position == "inside bottom left":
149+
return _d("left", "bottom", min([x0, x1]), min([y0, y1]))
150+
if position == "inside bottom right":
151+
return _d("right", "bottom", max([x0, x1]), min([y0, y1]))
152+
if position == "inside bottom":
153+
return _d("center", "bottom", mean([x0, x1]), min([y0, y1]))
154+
if position == "inside left":
155+
return _d("left", "middle", min([x0, x1]), mean([y0, y1]))
156+
if position == "inside right":
157+
return _d("right", "middle", max([x0, x1]), mean([y0, y1]))
158+
if position == "inside":
159+
# TODO: Do we want this?
160+
return _d("center", "middle", mean([x0, x1]), mean([y0, y1]))
161+
if position == "outside top left":
162+
return _d("right", "bottom", min([x0, x1]), max([y0, y1]))
163+
if position == "outside top right":
164+
return _d("left", "bottom", max([x0, x1]), max([y0, y1]))
165+
if position == "outside top":
166+
return _d("center", "bottom", mean([x0, x1]), max([y0, y1]))
167+
if position == "outside bottom left":
168+
return _d("right", "top", min([x0, x1]), min([y0, y1]))
169+
if position == "outside bottom right":
170+
return _d("left", "top", max([x0, x1]), min([y0, y1]))
171+
if position == "outside bottom":
172+
return _d("center", "top", mean([x0, x1]), min([y0, y1]))
173+
if position == "outside left":
174+
return _d("right", "middle", min([x0, x1]), mean([y0, y1]))
175+
if position == "outside right":
176+
return _d("left", "middle", max([x0, x1]), mean([y0, y1]))
177+
# default is inside top right
178+
return _d("right", "top", max([x0, x1]), max([y0, y1]))
179+
180+
181+
def axis_spanning_shape_annotation(annotation, shape_type, shape_args, kwargs):
182+
"""
183+
annotation: a go.layout.Annotation object, a dict describing an annotation, or None
184+
shape_type: one of 'vline', 'hline', 'vrect', 'hrect' and determines how the
185+
x, y, xanchor, and yanchor values are set.
186+
shape_args: the parameters used to draw the shape, which are used to place the annotation
187+
kwargs: a dictionary that was the kwargs of a
188+
_process_multiple_axis_spanning_shapes spanning shapes call. Items in this
189+
dict whose keys start with 'annotation_' will be extracted and the keys with
190+
the 'annotation_' part stripped off will be used to assign properties of the
191+
new annotation.
192+
193+
Property precedence:
194+
The annotation's x, y, xanchor, and yanchor properties are set based on the
195+
shape_type argument. Each property already specified in the annotation or
196+
through kwargs will be left as is (not replaced by the value computed using
197+
shape_type). Note that the xref and yref properties will in general get
198+
overwritten if the result of this function is passed to an add_annotation
199+
called with the row and col parameters specified.
200+
"""
201+
# Force to go.layout.Annotation, no matter if it is that already, a dict or None
202+
annotation = go.layout.Annotation(annotation)
203+
# set properties based on annotation_ prefixed kwargs
204+
prefix = "annotation_"
205+
len_prefix = len(prefix)
206+
annotation_keys = filter(lambda k: k.startswith(prefix), kwargs.keys())
207+
for k in annotation_keys:
208+
subk = k[len_prefix:]
209+
annotation[subk] = kwargs[k]
210+
# set x, y, xanchor, yanchor based on shape_type and position
211+
annotation_position = None
212+
if "annotation_position" in kwargs.keys():
213+
annotation_position = kwargs["annotation_position"]

0 commit comments

Comments
 (0)