Skip to content

Commit 2b14be6

Browse files
authored
Merge pull request #214 from scottshambaugh/xoffsets
Add xoffsets kwarg
2 parents 87dda1a + 087554d commit 2b14be6

File tree

4 files changed

+84
-8
lines changed

4 files changed

+84
-8
lines changed
Loading

labellines/core.py

+32-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import warnings
22
from typing import Optional, Union
3-
3+
from datetime import timedelta
44
import matplotlib.pyplot as plt
55
import numpy as np
66
from matplotlib.container import ErrorbarContainer
7-
from matplotlib.dates import DateConverter, num2date
7+
from matplotlib.dates import (
8+
_SwitchableDateConverter,
9+
ConciseDateConverter,
10+
DateConverter,
11+
num2date,
12+
)
813
from matplotlib.lines import Line2D
914
from more_itertools import always_iterable
1015

@@ -19,6 +24,8 @@ def labelLine(
1924
label: Optional[str] = None,
2025
align: Optional[bool] = None,
2126
drop_label: bool = False,
27+
xoffset: float = 0,
28+
xoffset_logspace: bool = False,
2229
yoffset: float = 0,
2330
yoffset_logspace: bool = False,
2431
outline_color: str = "auto",
@@ -43,6 +50,11 @@ def labelLine(
4350
drop_label : bool, optional
4451
If True, the label is consumed by the function so that subsequent
4552
calls to e.g. legend do not use it anymore.
53+
xoffset : double, optional
54+
Space to add to label's x position
55+
xoffset_logspace : bool, optional
56+
If True, then xoffset will be added to the label's x position in
57+
log10 space
4658
yoffset : double, optional
4759
Space to add to label's y position
4860
yoffset_logspace : bool, optional
@@ -65,6 +77,8 @@ def labelLine(
6577
x,
6678
label=label,
6779
align=align,
80+
xoffset=xoffset,
81+
xoffset_logspace=xoffset_logspace,
6882
yoffset=yoffset,
6983
yoffset_logspace=yoffset_logspace,
7084
outline_color=outline_color,
@@ -97,6 +111,7 @@ def labelLines(
97111
xvals: Optional[Union[tuple[float, float], list[float]]] = None,
98112
drop_label: bool = False,
99113
shrink_factor: float = 0.05,
114+
xoffsets: Union[float, list[float]] = 0,
100115
yoffsets: Union[float, list[float]] = 0,
101116
outline_color: str = "auto",
102117
outline_width: float = 5,
@@ -120,6 +135,9 @@ def labelLines(
120135
calls to e.g. legend do not use it anymore.
121136
shrink_factor : double, optional
122137
Relative distance from the edges to place closest labels. Defaults to 0.05.
138+
xoffsets : number or list, optional.
139+
Distance relative to the line when positioning the labels. If given a number,
140+
the same value is used for all lines.
123141
yoffsets : number or list, optional.
124142
Distance relative to the line when positioning the labels. If given a number,
125143
the same value is used for all lines.
@@ -243,20 +261,29 @@ def labelLines(
243261
converter = ax.xaxis.converter
244262
else:
245263
converter = ax.xaxis.get_converter()
246-
if isinstance(converter, DateConverter):
264+
time_classes = (_SwitchableDateConverter, DateConverter, ConciseDateConverter)
265+
if isinstance(converter, time_classes):
247266
xvals = [
248267
num2date(x).replace(tzinfo=ax.xaxis.get_units())
249268
for x in xvals # type: ignore
250269
]
251270

252271
txts = []
272+
try:
273+
if isinstance(xoffsets, timedelta):
274+
xoffsets = [xoffsets] * len(all_lines) # type: ignore
275+
else:
276+
xoffsets = [float(xoffsets)] * len(all_lines) # type: ignore
277+
except TypeError:
278+
pass
253279
try:
254280
yoffsets = [float(yoffsets)] * len(all_lines) # type: ignore
255281
except TypeError:
256282
pass
257-
for line, x, yoffset, label in zip(
283+
for line, x, xoffset, yoffset, label in zip(
258284
lab_lines,
259285
xvals, # type: ignore
286+
xoffsets, # type: ignore
260287
yoffsets, # type: ignore
261288
labels,
262289
):
@@ -267,6 +294,7 @@ def labelLines(
267294
label=label,
268295
align=align,
269296
drop_label=drop_label,
297+
xoffset=xoffset,
270298
yoffset=yoffset,
271299
outline_color=outline_color,
272300
outline_width=outline_width,

labellines/line_label.py

+30
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
from typing import TYPE_CHECKING
44

5+
import matplotlib.dates as mdates
56
import matplotlib.patheffects as patheffects
67
import numpy as np
8+
from datetime import timedelta
79
from matplotlib.text import Text
810

911
from .utils import normalize_xydata
@@ -35,6 +37,12 @@ class LineLabel(Text):
3537
_auto_align: bool
3638
"""Align text with the line (True) or parallel to x axis (False)"""
3739

40+
_xoffset: float
41+
"""An additional x offset for the label"""
42+
43+
_xoffset_logspace: bool
44+
"""Sets whether to treat _xoffset exponentially"""
45+
3846
_yoffset: float
3947
"""An additional y offset for the label"""
4048

@@ -56,6 +64,8 @@ def __init__(
5664
x: Position,
5765
label: Optional[str] = None,
5866
align: Optional[bool] = None,
67+
xoffset: float = 0,
68+
xoffset_logspace: bool = False,
5969
yoffset: float = 0,
6070
yoffset_logspace: bool = False,
6171
outline_color: Optional[Union[AutoLiteral, ColorLike]] = "auto",
@@ -76,6 +86,11 @@ def __init__(
7686
align : bool, optional
7787
If true, the label is parallel to the line, otherwise horizontal,
7888
by default True.
89+
xoffset : float, optional
90+
An additional x offset for the line label, by default 0.
91+
xoffset_logspace : bool, optional
92+
If true xoffset is applied exponentially to appear linear on a log-axis,
93+
by default False.
7994
yoffset : float, optional
8095
An additional y offset for the line label, by default 0.
8196
yoffset_logspace : bool, optional
@@ -108,6 +123,8 @@ def __init__(
108123
self._target_x = x
109124
self._ax = line.axes
110125
self._auto_align = align
126+
self._xoffset = xoffset
127+
self._xoffset_logspace = xoffset_logspace
111128
self._yoffset = yoffset
112129
self._yoffset_logspace = yoffset_logspace
113130
label = label or line.get_label()
@@ -162,6 +179,13 @@ def _update_anchors(self):
162179
x = self._line.convert_xunits(self._target_x)
163180
xdata, ydata = normalize_xydata(self._line)
164181

182+
# Convert timedelta to float if needed
183+
if isinstance(self._xoffset, timedelta):
184+
xoffset = mdates.date2num(self._xoffset + self._target_x) - x
185+
else:
186+
xoffset = self._xoffset
187+
188+
# Handle nan values
165189
mask = np.isfinite(ydata)
166190
if mask.sum() == 0:
167191
raise ValueError(f"The line {self._line} only contains nan!")
@@ -185,6 +209,12 @@ def _update_anchors(self):
185209
else: # Vertical case
186210
y = (ya + yb) / 2
187211

212+
# Apply x offset
213+
if self._xoffset_logspace:
214+
x *= 10**xoffset
215+
else:
216+
x += xoffset
217+
188218
# Apply y offset
189219
if self._yoffset_logspace:
190220
y *= 10**self._yoffset

labellines/test.py

+22-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from datetime import datetime
1+
from datetime import datetime, timedelta
22

33
import matplotlib.pyplot as plt
44
import numpy as np
@@ -168,6 +168,19 @@ def test_dateaxis_advanced(setup_mpl):
168168
return plt.gcf()
169169

170170

171+
@pytest.mark.mpl_image_compare
172+
def test_dateaxis_timedelta_xoffset(setup_mpl):
173+
dates = [datetime(2018, 11, 1), datetime(2018, 11, 2), datetime(2018, 11, 3)]
174+
dt = timedelta(hours=12)
175+
176+
plt.plot(dates, [0, 1, 2], label="apples")
177+
plt.plot(dates, [3, 4, 5], label="banana")
178+
ax = plt.gca()
179+
180+
labelLines(ax.get_lines(), xoffsets=dt)
181+
return plt.gcf()
182+
183+
171184
@pytest.mark.mpl_image_compare
172185
def test_polar(setup_mpl):
173186
t = np.linspace(0, 2 * np.pi, num=128)
@@ -315,17 +328,22 @@ def test_label_datetime_plot(setup_mpl):
315328
return plt.gcf()
316329

317330

318-
def test_yoffset(setup_mpl):
331+
def test_xyoffset(setup_mpl):
319332
x = np.linspace(0, 1)
320333

321-
for yoffset in ([-0.5, 0.5], 1, 1.2): # try lists # try int # try float
334+
for offset in ([-0.5, 0.5], 1, 1.2): # try lists # try int # try float
322335
plt.clf()
323336
ax = plt.gca()
324337
ax.plot(x, np.sin(x) * 10, label=r"$\sin x$")
325338
ax.plot(x, np.cos(x) * 10, label=r"$\cos x$")
326339
lines = ax.get_lines()
327340
labelLines(
328-
lines, xvals=(0.2, 0.7), align=False, yoffsets=yoffset, bbox={"alpha": 0}
341+
lines,
342+
xvals=(0.2, 0.7),
343+
xoffsets=offset,
344+
yoffsets=offset,
345+
align=False,
346+
bbox={"alpha": 0},
329347
)
330348

331349

0 commit comments

Comments
 (0)