16
16
from .grid_finder import GridFinder
17
17
18
18
19
- def _value_and_jacobian (func , xs , ys , xlims , ylims ):
19
+ def _value_and_jac_angle (func , xs , ys , xlim , ylim ):
20
20
"""
21
- Compute *func* and its derivatives along x and y at positions *xs*, *ys*,
22
- while ensuring that finite difference calculations don't try to evaluate
23
- values outside of *xlims*, *ylims*.
21
+ Parameters
22
+ ----------
23
+ func : callable
24
+ A function that transforms the coordinates of a point (x, y) to a new coordinate
25
+ system (u, v), and which can also take x and y as arrays of shape *shape* and
26
+ returns (u, v) as a ``(2, shape)`` array.
27
+ xs, ys : array-likes
28
+ Points where *func* and its derivatives will be evaluated.
29
+ xlim, ylim : pairs of floats
30
+ (min, max) beyond which *func* should not be evaluated.
31
+
32
+ Returns
33
+ -------
34
+ val
35
+ Value of *func* at each point of ``(xs, ys)``.
36
+ thetas_dx
37
+ Angles (in radians) defined by the (u, v) components of the numerically
38
+ differentiated df/dx vector, at each point of ``(xs, ys)``. If needed, the
39
+ differentiation step size is increased until at least one component of df/dx
40
+ is nonzero, under the constraint of not going out of the *xlims*, *ylims*
41
+ bounds. If the gridline at a point is actually null (and the angle is thus not
42
+ well defined), the derivatives are evaluated after taking a small step along y;
43
+ this ensures e.g. that the tick at r=0 on a radial axis of a polar plot is
44
+ parallel with the ticks at r!=0.
45
+ thetas_dy
46
+ Like *thetas_dx*, but for df/dy.
24
47
"""
25
- eps = np .finfo (float ).eps ** (1 / 2 ) # see e.g. scipy.optimize.approx_fprime
48
+
49
+ shape = np .broadcast_shapes (np .shape (xs ), np .shape (ys ))
26
50
val = func (xs , ys )
27
- # Take the finite difference step in the direction where the bound is the
28
- # furthest; the step size is min of epsilon and distance to that bound.
29
- xlo , xhi = sorted (xlims )
30
- dxlo = xs - xlo
31
- dxhi = xhi - xs
32
- xeps = (np .take ([- 1 , 1 ], dxhi >= dxlo )
33
- * np .minimum (eps , np .maximum (dxlo , dxhi )))
34
- val_dx = func (xs + xeps , ys )
35
- ylo , yhi = sorted (ylims )
36
- dylo = ys - ylo
37
- dyhi = yhi - ys
38
- yeps = (np .take ([- 1 , 1 ], dyhi >= dylo )
39
- * np .minimum (eps , np .maximum (dylo , dyhi )))
40
- val_dy = func (xs , ys + yeps )
41
- return (val , (val_dx - val ) / xeps , (val_dy - val ) / yeps )
51
+
52
+ # Take finite difference steps towards the furthest bound; the step size will be the
53
+ # min of epsilon and the distance to that bound.
54
+ eps0 = np .finfo (float ).eps ** (1 / 2 ) # cf. scipy.optimize.approx_fprime
55
+
56
+ def calc_eps (vals , lim ):
57
+ lo , hi = sorted (lim )
58
+ dlo = vals - lo
59
+ dhi = hi - vals
60
+ eps_max = np .maximum (dlo , dhi )
61
+ eps = np .where (dhi >= dlo , 1 , - 1 ) * np .minimum (eps0 , eps_max )
62
+ return eps , eps_max
63
+
64
+ xeps , xeps_max = calc_eps (xs , xlim )
65
+ yeps , yeps_max = calc_eps (ys , ylim )
66
+
67
+ def calc_thetas (dfunc , ps , eps_p0 , eps_max , eps_q ):
68
+ thetas_dp = np .full (shape , np .nan )
69
+ missing = np .full (shape , True )
70
+ eps_p = eps_p0
71
+ for it , eps_q in enumerate ([0 , eps_q ]):
72
+ while missing .any () and (abs (eps_p ) < eps_max ).any ():
73
+ if it == 0 and (eps_p > 1 ).any ():
74
+ break # Degenerate derivative, move a bit along the other coord.
75
+ eps_p = np .minimum (eps_p , eps_max )
76
+ df_x , df_y = (dfunc (eps_p , eps_q ) - dfunc (0 , eps_q )) / eps_p
77
+ good = missing & ((df_x != 0 ) | (df_y != 0 ))
78
+ thetas_dp [good ] = np .arctan2 (df_y , df_x )[good ]
79
+ missing &= ~ good
80
+ eps_p *= 2
81
+ return thetas_dp
82
+
83
+ thetas_dx = calc_thetas (lambda eps_p , eps_q : func (xs + eps_p , ys + eps_q ),
84
+ xs , xeps , xeps_max , yeps )
85
+ thetas_dy = calc_thetas (lambda eps_p , eps_q : func (xs + eps_q , ys + eps_p ),
86
+ ys , yeps , yeps_max , xeps )
87
+ return (val , thetas_dx , thetas_dy )
42
88
43
89
44
90
class FixedAxisArtistHelper (_FixedAxisArtistHelperBase ):
@@ -167,12 +213,11 @@ def trf_xy(x, y):
167
213
elif self .nth_coord == 1 :
168
214
xx0 = (xmin + xmax ) / 2
169
215
yy0 = self .value
170
- xy1 , dxy1_dx , dxy1_dy = _value_and_jacobian (
216
+ xy1 , angle_dx , angle_dy = _value_and_jac_angle (
171
217
trf_xy , xx0 , yy0 , (xmin , xmax ), (ymin , ymax ))
172
218
p = axes .transAxes .inverted ().transform (xy1 )
173
219
if 0 <= p [0 ] <= 1 and 0 <= p [1 ] <= 1 :
174
- d = [dxy1_dy , dxy1_dx ][self .nth_coord ]
175
- return xy1 , np .rad2deg (np .arctan2 (* d [::- 1 ]))
220
+ return xy1 , np .rad2deg ([angle_dy , angle_dx ][self .nth_coord ])
176
221
else :
177
222
return None , None
178
223
@@ -197,23 +242,17 @@ def trf_xy(x, y):
197
242
# find angles
198
243
if self .nth_coord == 0 :
199
244
mask = (e0 <= yy0 ) & (yy0 <= e1 )
200
- (xx1 , yy1 ), ( dxx1 , dyy1 ), ( dxx2 , dyy2 ) = _value_and_jacobian (
245
+ (xx1 , yy1 ), angle_normal , angle_tangent = _value_and_jac_angle (
201
246
trf_xy , self .value , yy0 [mask ], (- np .inf , np .inf ), (e0 , e1 ))
202
247
labels = self ._grid_info ["lat_labels" ]
203
248
204
249
elif self .nth_coord == 1 :
205
250
mask = (e0 <= xx0 ) & (xx0 <= e1 )
206
- (xx1 , yy1 ), ( dxx2 , dyy2 ), ( dxx1 , dyy1 ) = _value_and_jacobian (
251
+ (xx1 , yy1 ), angle_tangent , angle_normal = _value_and_jac_angle (
207
252
trf_xy , xx0 [mask ], self .value , (- np .inf , np .inf ), (e0 , e1 ))
208
253
labels = self ._grid_info ["lon_labels" ]
209
254
210
255
labels = [l for l , m in zip (labels , mask ) if m ]
211
-
212
- angle_normal = np .arctan2 (dyy1 , dxx1 )
213
- angle_tangent = np .arctan2 (dyy2 , dxx2 )
214
- mm = (dyy1 == 0 ) & (dxx1 == 0 ) # points with degenerate normal
215
- angle_normal [mm ] = angle_tangent [mm ] + np .pi / 2
216
-
217
256
tick_to_axes = self .get_tick_transform (axes ) - axes .transAxes
218
257
in_01 = functools .partial (
219
258
mpl .transforms ._interval_contains_close , (0 , 1 ))
0 commit comments