1
1
import warnings
2
2
from typing import Optional , Union
3
-
3
+ from datetime import timedelta
4
4
import matplotlib .pyplot as plt
5
5
import numpy as np
6
6
from datetime import datetime
7
7
from matplotlib .container import ErrorbarContainer
8
- from matplotlib .dates import DateConverter , num2date , _SwitchableDateConverter
8
+ from matplotlib .dates import (
9
+ _SwitchableDateConverter ,
10
+ ConciseDateConverter ,
11
+ DateConverter ,
12
+ num2date ,
13
+ )
9
14
from matplotlib .lines import Line2D
10
15
from more_itertools import always_iterable
11
16
@@ -20,6 +25,8 @@ def labelLine(
20
25
label : Optional [str ] = None ,
21
26
align : Optional [bool ] = None ,
22
27
drop_label : bool = False ,
28
+ xoffset : float = 0 ,
29
+ xoffset_logspace : bool = False ,
23
30
yoffset : float = 0 ,
24
31
yoffset_logspace : bool = False ,
25
32
outline_color : str = "auto" ,
@@ -44,6 +51,11 @@ def labelLine(
44
51
drop_label : bool, optional
45
52
If True, the label is consumed by the function so that subsequent
46
53
calls to e.g. legend do not use it anymore.
54
+ xoffset : double, optional
55
+ Space to add to label's x position
56
+ xoffset_logspace : bool, optional
57
+ If True, then xoffset will be added to the label's x position in
58
+ log10 space
47
59
yoffset : double, optional
48
60
Space to add to label's y position
49
61
yoffset_logspace : bool, optional
@@ -66,6 +78,8 @@ def labelLine(
66
78
x ,
67
79
label = label ,
68
80
align = align ,
81
+ xoffset = xoffset ,
82
+ xoffset_logspace = xoffset_logspace ,
69
83
yoffset = yoffset ,
70
84
yoffset_logspace = yoffset_logspace ,
71
85
outline_color = outline_color ,
@@ -98,6 +112,7 @@ def labelLines(
98
112
xvals : Optional [Union [tuple [float , float ], list [float ]]] = None ,
99
113
drop_label : bool = False ,
100
114
shrink_factor : float = 0.05 ,
115
+ xoffsets : Union [float , list [float ]] = 0 ,
101
116
yoffsets : Union [float , list [float ]] = 0 ,
102
117
outline_color : str = "auto" ,
103
118
outline_width : float = 5 ,
@@ -121,6 +136,9 @@ def labelLines(
121
136
calls to e.g. legend do not use it anymore.
122
137
shrink_factor : double, optional
123
138
Relative distance from the edges to place closest labels. Defaults to 0.05.
139
+ xoffsets : number or list, optional.
140
+ Distance relative to the line when positioning the labels. If given a number,
141
+ the same value is used for all lines.
124
142
yoffsets : number or list, optional.
125
143
Distance relative to the line when positioning the labels. If given a number,
126
144
the same value is used for all lines.
@@ -187,18 +205,34 @@ def labelLines(
187
205
if isinstance (xvals , tuple ) and len (xvals ) == 2 :
188
206
xmin , xmax = xvals
189
207
xscale = ax .get_xscale ()
208
+
209
+ # Convert datetime objects to numeric values for linspace/geomspace
210
+ x_is_datetime = isinstance (xmin , datetime ) or isinstance (xmax , datetime )
211
+ if x_is_datetime :
212
+ if not isinstance (xmin , datetime ) or not isinstance (xmax , datetime ):
213
+ raise ValueError (
214
+ f"Cannot mix datetime and numeric values in xvals: { xvals } "
215
+ )
216
+ xmin = plt .matplotlib .dates .date2num (xmin )
217
+ xmax = plt .matplotlib .dates .date2num (xmax )
218
+
190
219
if xscale == "log" :
191
220
xvals = np .geomspace (xmin , xmax , len (all_lines ) + 2 )[1 :- 1 ]
192
221
else :
193
222
xvals = np .linspace (xmin , xmax , len (all_lines ) + 2 )[1 :- 1 ]
194
223
224
+ # Convert numeric values back to datetime objects
225
+ if x_is_datetime :
226
+ xvals = plt .matplotlib .dates .num2date (xvals )
227
+
195
228
# Build matrix line -> xvalue
196
229
ok_matrix = np .zeros ((len (all_lines ), len (all_lines )), dtype = bool )
197
230
198
231
for i , line in enumerate (all_lines ):
199
232
xdata , _ = normalize_xydata (line )
200
233
minx , maxx = min (xdata ), max (xdata )
201
234
for j , xv in enumerate (xvals ): # type: ignore
235
+ xv = line .convert_xunits (xv )
202
236
ok_matrix [i , j ] = minx < xv < maxx
203
237
204
238
# If some xvals do not fall in their corresponding line,
@@ -227,6 +261,8 @@ def labelLines(
227
261
# Move xlabel if it is outside valid range
228
262
xdata , _ = normalize_xydata (line )
229
263
xmin , xmax = min (xdata ), max (xdata )
264
+ xv = line .convert_xunits (xv )
265
+
230
266
if not (xmin <= xv <= xmax ):
231
267
warnings .warn (
232
268
(
@@ -246,7 +282,8 @@ def labelLines(
246
282
converter = ax .xaxis .converter
247
283
else :
248
284
converter = ax .xaxis .get_converter ()
249
- if isinstance (converter , (DateConverter , _SwitchableDateConverter )):
285
+ time_classes = (_SwitchableDateConverter , DateConverter , ConciseDateConverter )
286
+ if isinstance (converter , time_classes ):
250
287
xvals = [
251
288
x # type: ignore
252
289
if isinstance (x , (np .datetime64 , datetime ))
@@ -255,13 +292,21 @@ def labelLines(
255
292
]
256
293
257
294
txts = []
295
+ try :
296
+ if isinstance (xoffsets , timedelta ):
297
+ xoffsets = [xoffsets ] * len (all_lines ) # type: ignore
298
+ else :
299
+ xoffsets = [float (xoffsets )] * len (all_lines ) # type: ignore
300
+ except TypeError :
301
+ pass
258
302
try :
259
303
yoffsets = [float (yoffsets )] * len (all_lines ) # type: ignore
260
304
except TypeError :
261
305
pass
262
- for line , x , yoffset , label in zip (
306
+ for line , x , xoffset , yoffset , label in zip (
263
307
lab_lines ,
264
308
xvals , # type: ignore
309
+ xoffsets , # type: ignore
265
310
yoffsets , # type: ignore
266
311
labels ,
267
312
):
@@ -272,6 +317,7 @@ def labelLines(
272
317
label = label ,
273
318
align = align ,
274
319
drop_label = drop_label ,
320
+ xoffset = xoffset ,
275
321
yoffset = yoffset ,
276
322
outline_color = outline_color ,
277
323
outline_width = outline_width ,
0 commit comments