1616from .grid_finder import GridFinder
1717
1818
19- def _value_and_jacobian (func , xs , ys , xlims , ylims ):
19+ def _value_and_jac_angle (func , xs , ys , xlim , ylim ):
2020 """
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.
2447 """
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 ))
2650 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 )
4288
4389
4490class FixedAxisArtistHelper (_FixedAxisArtistHelperBase ):
@@ -167,12 +213,11 @@ def trf_xy(x, y):
167213 elif self .nth_coord == 1 :
168214 xx0 = (xmin + xmax ) / 2
169215 yy0 = self .value
170- xy1 , dxy1_dx , dxy1_dy = _value_and_jacobian (
216+ xy1 , angle_dx , angle_dy = _value_and_jac_angle (
171217 trf_xy , xx0 , yy0 , (xmin , xmax ), (ymin , ymax ))
172218 p = axes .transAxes .inverted ().transform (xy1 )
173219 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 ])
176221 else :
177222 return None , None
178223
@@ -197,23 +242,17 @@ def trf_xy(x, y):
197242 # find angles
198243 if self .nth_coord == 0 :
199244 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 (
201246 trf_xy , self .value , yy0 [mask ], (- np .inf , np .inf ), (e0 , e1 ))
202247 labels = self ._grid_info ["lat_labels" ]
203248
204249 elif self .nth_coord == 1 :
205250 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 (
207252 trf_xy , xx0 [mask ], self .value , (- np .inf , np .inf ), (e0 , e1 ))
208253 labels = self ._grid_info ["lon_labels" ]
209254
210255 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-
217256 tick_to_axes = self .get_tick_transform (axes ) - axes .transAxes
218257 in_01 = functools .partial (
219258 mpl .transforms ._interval_contains_close , (0 , 1 ))
0 commit comments