Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add xoffsets kwarg #214

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 32 additions & 4 deletions labellines/core.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import warnings
from typing import Optional, Union

from datetime import timedelta
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.container import ErrorbarContainer
from matplotlib.dates import DateConverter, num2date
from matplotlib.dates import (
_SwitchableDateConverter,
ConciseDateConverter,
DateConverter,
num2date,
)
from matplotlib.lines import Line2D
from more_itertools import always_iterable

Expand All @@ -19,6 +24,8 @@ def labelLine(
label: Optional[str] = None,
align: Optional[bool] = None,
drop_label: bool = False,
xoffset: float = 0,
xoffset_logspace: bool = False,
yoffset: float = 0,
yoffset_logspace: bool = False,
outline_color: str = "auto",
Expand All @@ -43,6 +50,11 @@ def labelLine(
drop_label : bool, optional
If True, the label is consumed by the function so that subsequent
calls to e.g. legend do not use it anymore.
xoffset : double, optional
Space to add to label's x position
xoffset_logspace : bool, optional
If True, then xoffset will be added to the label's x position in
log10 space
yoffset : double, optional
Space to add to label's y position
yoffset_logspace : bool, optional
Expand All @@ -65,6 +77,8 @@ def labelLine(
x,
label=label,
align=align,
xoffset=xoffset,
xoffset_logspace=xoffset_logspace,
yoffset=yoffset,
yoffset_logspace=yoffset_logspace,
outline_color=outline_color,
Expand Down Expand Up @@ -97,6 +111,7 @@ def labelLines(
xvals: Optional[Union[tuple[float, float], list[float]]] = None,
drop_label: bool = False,
shrink_factor: float = 0.05,
xoffsets: Union[float, list[float]] = 0,
yoffsets: Union[float, list[float]] = 0,
outline_color: str = "auto",
outline_width: float = 5,
Expand All @@ -120,6 +135,9 @@ def labelLines(
calls to e.g. legend do not use it anymore.
shrink_factor : double, optional
Relative distance from the edges to place closest labels. Defaults to 0.05.
xoffsets : number or list, optional.
Distance relative to the line when positioning the labels. If given a number,
the same value is used for all lines.
yoffsets : number or list, optional.
Distance relative to the line when positioning the labels. If given a number,
the same value is used for all lines.
Expand Down Expand Up @@ -243,20 +261,29 @@ def labelLines(
converter = ax.xaxis.converter
else:
converter = ax.xaxis.get_converter()
if isinstance(converter, DateConverter):
time_classes = (_SwitchableDateConverter, DateConverter, ConciseDateConverter)
if isinstance(converter, time_classes):
xvals = [
num2date(x).replace(tzinfo=ax.xaxis.get_units())
for x in xvals # type: ignore
]

txts = []
try:
if isinstance(xoffsets, timedelta):
xoffsets = [xoffsets] * len(all_lines) # type: ignore
else:
xoffsets = [float(xoffsets)] * len(all_lines) # type: ignore
except TypeError:
pass
try:
yoffsets = [float(yoffsets)] * len(all_lines) # type: ignore
except TypeError:
pass
for line, x, yoffset, label in zip(
for line, x, xoffset, yoffset, label in zip(
lab_lines,
xvals, # type: ignore
xoffsets, # type: ignore
yoffsets, # type: ignore
labels,
):
Expand All @@ -267,6 +294,7 @@ def labelLines(
label=label,
align=align,
drop_label=drop_label,
xoffset=xoffset,
yoffset=yoffset,
outline_color=outline_color,
outline_width=outline_width,
Expand Down
30 changes: 30 additions & 0 deletions labellines/line_label.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

from typing import TYPE_CHECKING

import matplotlib.dates as mdates
import matplotlib.patheffects as patheffects
import numpy as np
from datetime import timedelta
from matplotlib.text import Text

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

_xoffset: float
"""An additional x offset for the label"""

_xoffset_logspace: bool
"""Sets whether to treat _xoffset exponentially"""

_yoffset: float
"""An additional y offset for the label"""

Expand All @@ -56,6 +64,8 @@ def __init__(
x: Position,
label: Optional[str] = None,
align: Optional[bool] = None,
xoffset: float = 0,
xoffset_logspace: bool = False,
yoffset: float = 0,
yoffset_logspace: bool = False,
outline_color: Optional[Union[AutoLiteral, ColorLike]] = "auto",
Expand All @@ -76,6 +86,11 @@ def __init__(
align : bool, optional
If true, the label is parallel to the line, otherwise horizontal,
by default True.
xoffset : float, optional
An additional x offset for the line label, by default 0.
xoffset_logspace : bool, optional
If true xoffset is applied exponentially to appear linear on a log-axis,
by default False.
yoffset : float, optional
An additional y offset for the line label, by default 0.
yoffset_logspace : bool, optional
Expand Down Expand Up @@ -108,6 +123,8 @@ def __init__(
self._target_x = x
self._ax = line.axes
self._auto_align = align
self._xoffset = xoffset
self._xoffset_logspace = xoffset_logspace
self._yoffset = yoffset
self._yoffset_logspace = yoffset_logspace
label = label or line.get_label()
Expand Down Expand Up @@ -162,6 +179,13 @@ def _update_anchors(self):
x = self._line.convert_xunits(self._target_x)
xdata, ydata = normalize_xydata(self._line)

# Convert timedelta to float if needed
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self, can update when matplotlib/matplotlib#8869 is resolved.

if isinstance(self._xoffset, timedelta):
xoffset = mdates.date2num(self._xoffset + self._target_x) - x
else:
xoffset = self._xoffset

# Handle nan values
mask = np.isfinite(ydata)
if mask.sum() == 0:
raise ValueError(f"The line {self._line} only contains nan!")
Expand All @@ -185,6 +209,12 @@ def _update_anchors(self):
else: # Vertical case
y = (ya + yb) / 2

# Apply x offset
if self._xoffset_logspace:
x *= 10**xoffset
else:
x += xoffset

# Apply y offset
if self._yoffset_logspace:
y *= 10**self._yoffset
Expand Down
26 changes: 22 additions & 4 deletions labellines/test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime
from datetime import datetime, timedelta

import matplotlib.pyplot as plt
import numpy as np
Expand Down Expand Up @@ -168,6 +168,19 @@ def test_dateaxis_advanced(setup_mpl):
return plt.gcf()


@pytest.mark.mpl_image_compare
def test_dateaxis_timedelta_xoffset(setup_mpl):
dates = [datetime(2018, 11, 1), datetime(2018, 11, 2), datetime(2018, 11, 3)]
dt = timedelta(hours=12)

plt.plot(dates, [0, 1, 2], label="apples")
plt.plot(dates, [3, 4, 5], label="banana")
ax = plt.gca()

labelLines(ax.get_lines(), xoffsets=dt)
return plt.gcf()


@pytest.mark.mpl_image_compare
def test_polar(setup_mpl):
t = np.linspace(0, 2 * np.pi, num=128)
Expand Down Expand Up @@ -315,17 +328,22 @@ def test_label_datetime_plot(setup_mpl):
return plt.gcf()


def test_yoffset(setup_mpl):
def test_xyoffset(setup_mpl):
x = np.linspace(0, 1)

for yoffset in ([-0.5, 0.5], 1, 1.2): # try lists # try int # try float
for offset in ([-0.5, 0.5], 1, 1.2): # try lists # try int # try float
plt.clf()
ax = plt.gca()
ax.plot(x, np.sin(x) * 10, label=r"$\sin x$")
ax.plot(x, np.cos(x) * 10, label=r"$\cos x$")
lines = ax.get_lines()
labelLines(
lines, xvals=(0.2, 0.7), align=False, yoffsets=yoffset, bbox={"alpha": 0}
lines,
xvals=(0.2, 0.7),
xoffsets=offset,
yoffsets=offset,
align=False,
bbox={"alpha": 0},
)


Expand Down