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
+ from datetime import datetime
6
7
from matplotlib .container import ErrorbarContainer
7
- from matplotlib .dates import DateConverter , num2date
8
+ from matplotlib .dates import (
9
+ _SwitchableDateConverter ,
10
+ ConciseDateConverter ,
11
+ DateConverter ,
12
+ num2date ,
13
+ )
8
14
from matplotlib .lines import Line2D
9
15
from more_itertools import always_iterable
10
16
@@ -19,6 +25,8 @@ def labelLine(
19
25
label : Optional [str ] = None ,
20
26
align : Optional [bool ] = None ,
21
27
drop_label : bool = False ,
28
+ xoffset : float = 0 ,
29
+ xoffset_logspace : bool = False ,
22
30
yoffset : float = 0 ,
23
31
yoffset_logspace : bool = False ,
24
32
outline_color : str = "auto" ,
@@ -43,6 +51,11 @@ def labelLine(
43
51
drop_label : bool, optional
44
52
If True, the label is consumed by the function so that subsequent
45
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
46
59
yoffset : double, optional
47
60
Space to add to label's y position
48
61
yoffset_logspace : bool, optional
@@ -65,6 +78,8 @@ def labelLine(
65
78
x ,
66
79
label = label ,
67
80
align = align ,
81
+ xoffset = xoffset ,
82
+ xoffset_logspace = xoffset_logspace ,
68
83
yoffset = yoffset ,
69
84
yoffset_logspace = yoffset_logspace ,
70
85
outline_color = outline_color ,
@@ -97,6 +112,7 @@ def labelLines(
97
112
xvals : Optional [Union [tuple [float , float ], list [float ]]] = None ,
98
113
drop_label : bool = False ,
99
114
shrink_factor : float = 0.05 ,
115
+ xoffsets : Union [float , list [float ]] = 0 ,
100
116
yoffsets : Union [float , list [float ]] = 0 ,
101
117
outline_color : str = "auto" ,
102
118
outline_width : float = 5 ,
@@ -120,6 +136,9 @@ def labelLines(
120
136
calls to e.g. legend do not use it anymore.
121
137
shrink_factor : double, optional
122
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.
123
142
yoffsets : number or list, optional.
124
143
Distance relative to the line when positioning the labels. If given a number,
125
144
the same value is used for all lines.
@@ -186,18 +205,34 @@ def labelLines(
186
205
if isinstance (xvals , tuple ) and len (xvals ) == 2 :
187
206
xmin , xmax = xvals
188
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
+
189
219
if xscale == "log" :
190
220
xvals = np .geomspace (xmin , xmax , len (all_lines ) + 2 )[1 :- 1 ]
191
221
else :
192
222
xvals = np .linspace (xmin , xmax , len (all_lines ) + 2 )[1 :- 1 ]
193
223
224
+ # Convert numeric values back to datetime objects
225
+ if x_is_datetime :
226
+ xvals = plt .matplotlib .dates .num2date (xvals )
227
+
194
228
# Build matrix line -> xvalue
195
229
ok_matrix = np .zeros ((len (all_lines ), len (all_lines )), dtype = bool )
196
230
197
231
for i , line in enumerate (all_lines ):
198
232
xdata , _ = normalize_xydata (line )
199
233
minx , maxx = np .nanmin (xdata ), np .nanmax (xdata )
200
234
for j , xv in enumerate (xvals ): # type: ignore
235
+ xv = line .convert_xunits (xv )
201
236
ok_matrix [i , j ] = minx < xv < maxx
202
237
203
238
# If some xvals do not fall in their corresponding line,
@@ -213,6 +248,8 @@ def labelLines(
213
248
xvals [order ] = old_xvals # type: ignore
214
249
else :
215
250
xvals = list (always_iterable (xvals )) # force the creation of a copy
251
+ if len (xvals ) == 1 :
252
+ xvals = [xvals [0 ]] * len (all_lines )
216
253
217
254
lab_lines , labels = [], []
218
255
# Take only the lines which have labels other than the default ones
@@ -224,6 +261,8 @@ def labelLines(
224
261
# Move xlabel if it is outside valid range
225
262
xdata , _ = normalize_xydata (line )
226
263
xmin , xmax = np .nanmin (xdata ), np .nanmax (xdata )
264
+ xv = line .convert_xunits (xv )
265
+
227
266
if not (xmin <= xv <= xmax ):
228
267
warnings .warn (
229
268
(
@@ -243,20 +282,29 @@ def labelLines(
243
282
converter = ax .xaxis .converter
244
283
else :
245
284
converter = ax .xaxis .get_converter ()
246
- if isinstance (converter , DateConverter ):
285
+ time_classes = (_SwitchableDateConverter , DateConverter , ConciseDateConverter )
286
+ if isinstance (converter , time_classes ):
247
287
xvals = [
248
288
num2date (x ).replace (tzinfo = ax .xaxis .get_units ())
249
289
for x in xvals # type: ignore
250
290
]
251
291
252
292
txts = []
293
+ try :
294
+ if isinstance (xoffsets , timedelta ):
295
+ xoffsets = [xoffsets ] * len (all_lines ) # type: ignore
296
+ else :
297
+ xoffsets = [float (xoffsets )] * len (all_lines ) # type: ignore
298
+ except TypeError :
299
+ pass
253
300
try :
254
301
yoffsets = [float (yoffsets )] * len (all_lines ) # type: ignore
255
302
except TypeError :
256
303
pass
257
- for line , x , yoffset , label in zip (
304
+ for line , x , xoffset , yoffset , label in zip (
258
305
lab_lines ,
259
306
xvals , # type: ignore
307
+ xoffsets , # type: ignore
260
308
yoffsets , # type: ignore
261
309
labels ,
262
310
):
@@ -267,6 +315,7 @@ def labelLines(
267
315
label = label ,
268
316
align = align ,
269
317
drop_label = drop_label ,
318
+ xoffset = xoffset ,
270
319
yoffset = yoffset ,
271
320
outline_color = outline_color ,
272
321
outline_width = outline_width ,
0 commit comments