11import warnings
22from typing import Optional , Union
3-
3+ from datetime import timedelta
44import matplotlib .pyplot as plt
55import numpy as np
6+ from datetime import datetime
67from 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+ )
814from matplotlib .lines import Line2D
915from more_itertools import always_iterable
1016
@@ -19,6 +25,8 @@ def labelLine(
1925 label : Optional [str ] = None ,
2026 align : Optional [bool ] = None ,
2127 drop_label : bool = False ,
28+ xoffset : float = 0 ,
29+ xoffset_logspace : bool = False ,
2230 yoffset : float = 0 ,
2331 yoffset_logspace : bool = False ,
2432 outline_color : str = "auto" ,
@@ -43,6 +51,11 @@ def labelLine(
4351 drop_label : bool, optional
4452 If True, the label is consumed by the function so that subsequent
4553 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
4659 yoffset : double, optional
4760 Space to add to label's y position
4861 yoffset_logspace : bool, optional
@@ -65,6 +78,8 @@ def labelLine(
6578 x ,
6679 label = label ,
6780 align = align ,
81+ xoffset = xoffset ,
82+ xoffset_logspace = xoffset_logspace ,
6883 yoffset = yoffset ,
6984 yoffset_logspace = yoffset_logspace ,
7085 outline_color = outline_color ,
@@ -97,6 +112,7 @@ def labelLines(
97112 xvals : Optional [Union [tuple [float , float ], list [float ]]] = None ,
98113 drop_label : bool = False ,
99114 shrink_factor : float = 0.05 ,
115+ xoffsets : Union [float , list [float ]] = 0 ,
100116 yoffsets : Union [float , list [float ]] = 0 ,
101117 outline_color : str = "auto" ,
102118 outline_width : float = 5 ,
@@ -120,6 +136,9 @@ def labelLines(
120136 calls to e.g. legend do not use it anymore.
121137 shrink_factor : double, optional
122138 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.
123142 yoffsets : number or list, optional.
124143 Distance relative to the line when positioning the labels. If given a number,
125144 the same value is used for all lines.
@@ -186,18 +205,34 @@ def labelLines(
186205 if isinstance (xvals , tuple ) and len (xvals ) == 2 :
187206 xmin , xmax = xvals
188207 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+
189219 if xscale == "log" :
190220 xvals = np .geomspace (xmin , xmax , len (all_lines ) + 2 )[1 :- 1 ]
191221 else :
192222 xvals = np .linspace (xmin , xmax , len (all_lines ) + 2 )[1 :- 1 ]
193223
224+ # Convert numeric values back to datetime objects
225+ if x_is_datetime :
226+ xvals = plt .matplotlib .dates .num2date (xvals )
227+
194228 # Build matrix line -> xvalue
195229 ok_matrix = np .zeros ((len (all_lines ), len (all_lines )), dtype = bool )
196230
197231 for i , line in enumerate (all_lines ):
198232 xdata , _ = normalize_xydata (line )
199233 minx , maxx = np .nanmin (xdata ), np .nanmax (xdata )
200234 for j , xv in enumerate (xvals ): # type: ignore
235+ xv = line .convert_xunits (xv )
201236 ok_matrix [i , j ] = minx < xv < maxx
202237
203238 # If some xvals do not fall in their corresponding line,
@@ -213,6 +248,8 @@ def labelLines(
213248 xvals [order ] = old_xvals # type: ignore
214249 else :
215250 xvals = list (always_iterable (xvals )) # force the creation of a copy
251+ if len (xvals ) == 1 :
252+ xvals = [xvals [0 ]] * len (all_lines )
216253
217254 lab_lines , labels = [], []
218255 # Take only the lines which have labels other than the default ones
@@ -224,6 +261,8 @@ def labelLines(
224261 # Move xlabel if it is outside valid range
225262 xdata , _ = normalize_xydata (line )
226263 xmin , xmax = np .nanmin (xdata ), np .nanmax (xdata )
264+ xv = line .convert_xunits (xv )
265+
227266 if not (xmin <= xv <= xmax ):
228267 warnings .warn (
229268 (
@@ -243,20 +282,29 @@ def labelLines(
243282 converter = ax .xaxis .converter
244283 else :
245284 converter = ax .xaxis .get_converter ()
246- if isinstance (converter , DateConverter ):
285+ time_classes = (_SwitchableDateConverter , DateConverter , ConciseDateConverter )
286+ if isinstance (converter , time_classes ):
247287 xvals = [
248288 num2date (x ).replace (tzinfo = ax .xaxis .get_units ())
249289 for x in xvals # type: ignore
250290 ]
251291
252292 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
253300 try :
254301 yoffsets = [float (yoffsets )] * len (all_lines ) # type: ignore
255302 except TypeError :
256303 pass
257- for line , x , yoffset , label in zip (
304+ for line , x , xoffset , yoffset , label in zip (
258305 lab_lines ,
259306 xvals , # type: ignore
307+ xoffsets , # type: ignore
260308 yoffsets , # type: ignore
261309 labels ,
262310 ):
@@ -267,6 +315,7 @@ def labelLines(
267315 label = label ,
268316 align = align ,
269317 drop_label = drop_label ,
318+ xoffset = xoffset ,
270319 yoffset = yoffset ,
271320 outline_color = outline_color ,
272321 outline_width = outline_width ,
0 commit comments