11import warnings
22from typing import Optional , Union
3-
3+ from datetime import timedelta
44import matplotlib .pyplot as plt
55import numpy as np
66from datetime import datetime
77from 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+ )
914from matplotlib .lines import Line2D
1015from more_itertools import always_iterable
1116
@@ -20,6 +25,8 @@ def labelLine(
2025 label : Optional [str ] = None ,
2126 align : Optional [bool ] = None ,
2227 drop_label : bool = False ,
28+ xoffset : float = 0 ,
29+ xoffset_logspace : bool = False ,
2330 yoffset : float = 0 ,
2431 yoffset_logspace : bool = False ,
2532 outline_color : str = "auto" ,
@@ -44,6 +51,11 @@ def labelLine(
4451 drop_label : bool, optional
4552 If True, the label is consumed by the function so that subsequent
4653 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
4759 yoffset : double, optional
4860 Space to add to label's y position
4961 yoffset_logspace : bool, optional
@@ -66,6 +78,8 @@ def labelLine(
6678 x ,
6779 label = label ,
6880 align = align ,
81+ xoffset = xoffset ,
82+ xoffset_logspace = xoffset_logspace ,
6983 yoffset = yoffset ,
7084 yoffset_logspace = yoffset_logspace ,
7185 outline_color = outline_color ,
@@ -98,6 +112,7 @@ def labelLines(
98112 xvals : Optional [Union [tuple [float , float ], list [float ]]] = None ,
99113 drop_label : bool = False ,
100114 shrink_factor : float = 0.05 ,
115+ xoffsets : Union [float , list [float ]] = 0 ,
101116 yoffsets : Union [float , list [float ]] = 0 ,
102117 outline_color : str = "auto" ,
103118 outline_width : float = 5 ,
@@ -121,6 +136,9 @@ def labelLines(
121136 calls to e.g. legend do not use it anymore.
122137 shrink_factor : double, optional
123138 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.
124142 yoffsets : number or list, optional.
125143 Distance relative to the line when positioning the labels. If given a number,
126144 the same value is used for all lines.
@@ -187,18 +205,34 @@ def labelLines(
187205 if isinstance (xvals , tuple ) and len (xvals ) == 2 :
188206 xmin , xmax = xvals
189207 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+
190219 if xscale == "log" :
191220 xvals = np .geomspace (xmin , xmax , len (all_lines ) + 2 )[1 :- 1 ]
192221 else :
193222 xvals = np .linspace (xmin , xmax , len (all_lines ) + 2 )[1 :- 1 ]
194223
224+ # Convert numeric values back to datetime objects
225+ if x_is_datetime :
226+ xvals = plt .matplotlib .dates .num2date (xvals )
227+
195228 # Build matrix line -> xvalue
196229 ok_matrix = np .zeros ((len (all_lines ), len (all_lines )), dtype = bool )
197230
198231 for i , line in enumerate (all_lines ):
199232 xdata , _ = normalize_xydata (line )
200233 minx , maxx = min (xdata ), max (xdata )
201234 for j , xv in enumerate (xvals ): # type: ignore
235+ xv = line .convert_xunits (xv )
202236 ok_matrix [i , j ] = minx < xv < maxx
203237
204238 # If some xvals do not fall in their corresponding line,
@@ -227,6 +261,8 @@ def labelLines(
227261 # Move xlabel if it is outside valid range
228262 xdata , _ = normalize_xydata (line )
229263 xmin , xmax = min (xdata ), max (xdata )
264+ xv = line .convert_xunits (xv )
265+
230266 if not (xmin <= xv <= xmax ):
231267 warnings .warn (
232268 (
@@ -246,7 +282,8 @@ def labelLines(
246282 converter = ax .xaxis .converter
247283 else :
248284 converter = ax .xaxis .get_converter ()
249- if isinstance (converter , (DateConverter , _SwitchableDateConverter )):
285+ time_classes = (_SwitchableDateConverter , DateConverter , ConciseDateConverter )
286+ if isinstance (converter , time_classes ):
250287 xvals = [
251288 x # type: ignore
252289 if isinstance (x , (np .datetime64 , datetime ))
@@ -255,13 +292,21 @@ def labelLines(
255292 ]
256293
257294 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
258302 try :
259303 yoffsets = [float (yoffsets )] * len (all_lines ) # type: ignore
260304 except TypeError :
261305 pass
262- for line , x , yoffset , label in zip (
306+ for line , x , xoffset , yoffset , label in zip (
263307 lab_lines ,
264308 xvals , # type: ignore
309+ xoffsets , # type: ignore
265310 yoffsets , # type: ignore
266311 labels ,
267312 ):
@@ -272,6 +317,7 @@ def labelLines(
272317 label = label ,
273318 align = align ,
274319 drop_label = drop_label ,
320+ xoffset = xoffset ,
275321 yoffset = yoffset ,
276322 outline_color = outline_color ,
277323 outline_width = outline_width ,
0 commit comments