Skip to content

Add LineCollection plot #7173

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

Open
wants to merge 146 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
146 commits
Select commit Hold shift + click to select a range
f01ce23
Add linecollection
Illviljan Oct 16, 2022
7025b99
Update dataarray_plot.py
Illviljan Oct 16, 2022
638a91d
add more lines
Illviljan Oct 16, 2022
2eed4d5
Merge branch 'main' into plot1d_lines
Illviljan Oct 17, 2022
cf0d4ae
Update dataarray_plot.py
Illviljan Oct 18, 2022
147f494
Merge branch 'main' into plot1d_lines
Illviljan Oct 27, 2022
a152fb9
Merge branch 'main' into plot1d_lines
Illviljan May 21, 2023
09c5c66
Merge branch 'main' into plot1d_lines
Illviljan May 21, 2023
8360175
Remove old code
Illviljan May 21, 2023
7a0a739
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 21, 2023
d51bf70
Update accessor.py
Illviljan May 21, 2023
93a132c
keep similar format as scatter
Illviljan May 21, 2023
ce4a806
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 21, 2023
2bd5aac
Update dataarray_plot.py
Illviljan May 21, 2023
dfc3bb8
keep same format as scatter
Illviljan May 21, 2023
079a6a7
Merge branch 'main' into plot1d_lines
Illviljan Sep 16, 2023
2e940c3
Fix lines plot
Illviljan Sep 16, 2023
7c102d5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 16, 2023
789d6af
Update dataarray_plot.py
Illviljan Sep 16, 2023
3c4e442
Merge branch 'plot1d_lines' of https://github.com/Illviljan/xarray in…
Illviljan Sep 16, 2023
51742e2
Merge branch 'main' into plot1d_lines
Illviljan Sep 17, 2023
0cb969a
Update utils.py
Illviljan Sep 17, 2023
d8204e6
Attempt at hanlding coordinates with multiple dimensions
Illviljan Sep 17, 2023
edfa297
Merge branch 'main' into plot1d_lines
Illviljan Dec 10, 2023
4a1e2d6
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 10, 2023
35c402d
Merge branch 'main' into plot1d_lines
Illviljan Mar 10, 2024
baf93ab
Update test_plot.py
Illviljan Mar 10, 2024
d5467a6
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 10, 2024
6044e78
Update test_plot.py
Illviljan Mar 10, 2024
8dc66a9
Merge branch 'plot1d_lines' of https://github.com/Illviljan/xarray in…
Illviljan Mar 10, 2024
d9d0db4
merge tests
Illviljan Mar 10, 2024
0bac33a
Use similar format as scatter
Illviljan Mar 10, 2024
4bb57ca
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 10, 2024
d5fa2c5
Update utils.py
Illviljan Mar 10, 2024
45623a2
Merge branch 'plot1d_lines' of https://github.com/Illviljan/xarray in…
Illviljan Mar 10, 2024
79978a4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 10, 2024
29691d0
Update utils.py
Illviljan Mar 10, 2024
d9f0854
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 10, 2024
afc1c1b
Update utils.py
Illviljan Mar 10, 2024
8ece6a4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 10, 2024
bd11e62
Update utils.py
Illviljan Mar 10, 2024
e5f4b7e
Merge branch 'plot1d_lines' of https://github.com/Illviljan/xarray in…
Illviljan Mar 10, 2024
3e3fd8b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 10, 2024
f3f6554
Update utils.py
Illviljan Mar 10, 2024
6b70602
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 10, 2024
768f68e
Update utils.py
Illviljan Mar 10, 2024
b9af902
Update utils.py
Illviljan Mar 10, 2024
dcabb85
s can be float, no __len__ on those
Illviljan Mar 10, 2024
5690a4b
Update dataarray_plot.py
Illviljan Mar 10, 2024
2ff8742
Update dataarray_plot.py
Illviljan Mar 10, 2024
7bb8c32
Update dataarray_plot.py
Illviljan Mar 10, 2024
a43d685
Update dataarray_plot.py
Illviljan Mar 10, 2024
3ada357
add Hashable vs None ignores
Illviljan Mar 10, 2024
6911b66
Update utils.py
Illviljan Mar 10, 2024
33a151f
clean up
Illviljan Mar 10, 2024
499607b
Merge branch 'main' into plot1d_lines
Illviljan Mar 13, 2024
8910396
Fix color="k"
Illviljan Mar 13, 2024
6def426
cleanup
Illviljan Mar 14, 2024
ae3f623
Add tests for color and linestyle
Illviljan Mar 14, 2024
ae5bc23
Merge branch 'main' into plot1d_lines
Illviljan Mar 14, 2024
164d15e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 14, 2024
207133f
Update test_plot.py
Illviljan Mar 14, 2024
62bbcc8
Update test_plot.py
Illviljan Mar 14, 2024
dac8746
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 14, 2024
3578442
assert not needed
Illviljan Mar 20, 2024
d7b2c0a
Merge branch 'main' into plot1d_lines
Illviljan Mar 20, 2024
07fffb3
Update utils.py
Illviljan Mar 20, 2024
5626d73
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 20, 2024
98edb79
Update utils.py
Illviljan Mar 20, 2024
e90ef58
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 20, 2024
bee8b94
Update utils.py
Illviljan Mar 21, 2024
9f867cd
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 21, 2024
1da316e
Update utils.py
Illviljan Mar 21, 2024
e4aaaa8
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 21, 2024
2917141
Merge branch 'main' into plot1d_lines
Illviljan Mar 22, 2024
fcad6de
Update utils.py
Illviljan Mar 22, 2024
89820a0
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 22, 2024
aef553d
Merge branch 'main' into plot1d_lines
Illviljan Apr 5, 2024
0106bb7
Add some docs examples
Illviljan Apr 5, 2024
f08f8c5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 5, 2024
d57b753
Update plotting.rst
Illviljan Apr 5, 2024
c281454
Merge branch 'plot1d_lines' of https://github.com/Illviljan/xarray in…
Illviljan Apr 5, 2024
532cb9d
Update plotting.rst
Illviljan Apr 5, 2024
50cd419
Update plotting.rst
Illviljan Apr 5, 2024
cc14075
Update plotting.rst
Illviljan Apr 6, 2024
59267ad
Update plotting.rst
Illviljan Apr 6, 2024
1720e0b
Update plotting.rst
Illviljan Apr 6, 2024
d45c819
Update plotting.rst
Illviljan Apr 6, 2024
9f1154a
Update api.rst
Illviljan Apr 6, 2024
659a9fe
improve docs
Illviljan Apr 6, 2024
8009145
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 6, 2024
78e26cb
Update __init__.py
Illviljan Apr 6, 2024
8737a1c
Merge branch 'plot1d_lines' of https://github.com/Illviljan/xarray in…
Illviljan Apr 6, 2024
f4c3c43
fix legend values
Illviljan Apr 6, 2024
bc433ab
Update api-hidden.rst
Illviljan Apr 6, 2024
08965fe
Update utils.py
Illviljan Apr 6, 2024
8b3558a
Add more legend labels tests
Illviljan Apr 6, 2024
7f1780f
Try without fix
Illviljan Apr 6, 2024
8a495b8
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 6, 2024
7684c65
add fix
Illviljan Apr 6, 2024
971fb2e
mypy fixes
Illviljan Apr 6, 2024
c47b1ad
Make sure lines legend is correct
Illviljan Apr 7, 2024
30213b2
Update test_plot.py
Illviljan Apr 7, 2024
2bf943d
Update test_plot.py
Illviljan Apr 7, 2024
422b8da
Update test_plot.py
Illviljan Apr 7, 2024
0b16cf9
Update test_plot.py
Illviljan Apr 7, 2024
6e13194
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 7, 2024
c26233e
Update facetgrid.py
Illviljan Apr 7, 2024
10fac37
Update facetgrid.py
Illviljan Apr 7, 2024
a83b3be
Update dataarray_plot.py
Illviljan Apr 7, 2024
ae2d71f
Merge branch 'main' into plot1d_lines
Illviljan May 17, 2024
c3f461c
remove commented code
Illviljan May 17, 2024
dfdd221
Merge branch 'main' into plot1d_lines
Illviljan May 24, 2024
30e2356
Update xarray/plot/utils.py
Illviljan May 28, 2024
47956b4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 28, 2024
f284a1e
Merge branch 'main' into plot1d_lines
Illviljan Jun 23, 2024
f53da04
Update whats-new.rst
Illviljan Jun 23, 2024
0a20402
Add more docs
Illviljan Jun 23, 2024
b7c0365
Merge branch 'main' into plot1d_lines
Illviljan Jul 9, 2024
e4f5258
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 9, 2024
eda1d74
fix bad merge
Illviljan Jul 9, 2024
381ec22
Merge branch 'plot1d_lines' of https://github.com/Illviljan/xarray in…
Illviljan Jul 9, 2024
d10f79c
Merge branch 'main' into plot1d_lines
Illviljan Dec 20, 2024
77b2c2c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 20, 2024
e0f7e05
make mypy happy again
Illviljan Dec 20, 2024
e39d250
Merge branch 'plot1d_lines' of https://github.com/Illviljan/xarray in…
Illviljan Dec 20, 2024
dec570c
Update whats-new.rst
Illviljan Dec 20, 2024
f06758b
Update whats-new.rst
Illviljan Dec 20, 2024
6a8aea1
Merge branch 'main' into plot1d_lines
Illviljan Jan 10, 2025
5bfbcd8
Merge branch 'main' into plot1d_lines
Illviljan Jul 21, 2025
7e53fe1
Update plotting.rst
Illviljan Jul 21, 2025
95360b4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 21, 2025
093a272
Apply suggestions from code review
Illviljan Jul 21, 2025
9d0fbad
align with ax.scatter
Illviljan Jul 21, 2025
fd58651
align more with ax.scatter
Illviljan Jul 21, 2025
8db8cdd
Update utils.py
Illviljan Jul 21, 2025
195adb6
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 21, 2025
5e52793
Update plotting.rst
Illviljan Jul 21, 2025
797f141
Merge branch 'plot1d_lines' of https://github.com/Illviljan/xarray in…
Illviljan Jul 21, 2025
2a007b8
hide warning with :stderr:
Illviljan Jul 22, 2025
588e457
extra linebreak helps?
Illviljan Jul 22, 2025
8d02fff
2 stderr to catch the 2 warnings?
Illviljan Jul 22, 2025
f6f8dbf
nope didn't work
Illviljan Jul 22, 2025
801b6b9
test all
Illviljan Jul 22, 2025
2a38b0b
remove the unneccessary stderr
Illviljan Jul 22, 2025
1347710
Merge branch 'main' into plot1d_lines
Illviljan Jul 23, 2025
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
1 change: 1 addition & 0 deletions doc/api-hidden.rst
Original file line number Diff line number Diff line change
@@ -393,6 +393,7 @@
plot.imshow
plot.pcolormesh
plot.scatter
plot.lines
plot.surface

CFTimeIndex.all
2 changes: 2 additions & 0 deletions doc/api/plotting.rst
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ Dataset
:toctree: ../generated/
:template: autosummary/accessor_method.rst

Dataset.plot.lines
Dataset.plot.scatter
Dataset.plot.quiver
Dataset.plot.streamplot
@@ -32,6 +33,7 @@ DataArray
DataArray.plot.hist
DataArray.plot.imshow
DataArray.plot.line
DataArray.plot.lines
DataArray.plot.pcolormesh
DataArray.plot.step
DataArray.plot.scatter
53 changes: 53 additions & 0 deletions doc/user-guide/plotting.rst
Original file line number Diff line number Diff line change
@@ -742,6 +742,59 @@ And adding the z-axis
For more advanced scatter plots, we recommend converting the relevant data variables
to a pandas DataFrame and using the extensive plotting capabilities of ``seaborn``.

Lines
~~~~~

:py:func:`xarray.plot.lines` calls matplotlib.collections.LineCollection under the hood,
allowing multiple lines being drawn efficiently. It uses similar arguments as
:py:func:`xarray.plot.scatter`.

Let's return to the air temperature dataset:

.. jupyter-execute::

airtemps = xr.tutorial.open_dataset("air_temperature")
air = airtemps.air - 273.15
air.attrs = airtemps.air.attrs
air.attrs["units"] = "deg C"

air.isel(lon=10).plot.lines(x="time", hue="lat")

Make it a little more transparent:

.. jupyter-execute::

air.isel(lon=10).plot.lines(x="time", hue="lat", alpha=0.2)

Zoom in a little on the x-axis, and compare a few latitudes and longitudes,
group them using ``hue`` and ``linewidth``. The ``linewidth`` kwarg works in
a similar way as ``markersize`` kwarg for scatter plots, it lets you vary the
line's size by variable value.

.. jupyter-execute::
:stderr:

air_zoom = air.isel(time=slice(1200, 1500), lat=[5, 10, 15], lon=[10, 15])
air_zoom.plot.lines(x="time", hue="lat", linewidth="lon", add_colorbar=False)

Lines can modify the linestyle but does not allow markers. Instead combine :py:func:`xarray.plot.lines`
with :py:func:`xarray.plot.scatter`:

.. jupyter-execute::

air.isel(lat=10, lon=10)[:200].plot.lines(x="time", color="k", linestyle="dashed")
air.isel(lat=10, lon=10)[:200].plot.scatter(x="time", color="k", marker="^")


Switching to another dataset with more variables we can analyse in similar
fashion as :py:func:`xarray.plot.scatter`:

.. jupyter-execute::
:stderr:

ds = xr.tutorial.scatter_example_dataset(seed=42)
ds.plot.lines(x="A", y="B", hue="y", linewidth="x", row="x", col="w")

Quiver
~~~~~~

3 changes: 3 additions & 0 deletions doc/whats-new.rst
Original file line number Diff line number Diff line change
@@ -722,6 +722,9 @@ New Features
iso8601-parser (:pull:`9885`).
By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_.

- Added new plot method :py:meth:`DataArray.plot.lines` which allows creating line plots efficiently in
a similar manner to :py:meth:`DataArray.plot.scatter`, also available for datasets. (:pull:`7173`)
By `Jimmy Westling <https://github.com/illviljan>`_.

Breaking changes
~~~~~~~~~~~~~~~~
2 changes: 2 additions & 0 deletions xarray/plot/__init__.py
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@
hist,
imshow,
line,
lines,
pcolormesh,
plot,
step,
@@ -28,6 +29,7 @@
"hist",
"imshow",
"line",
"lines",
"pcolormesh",
"plot",
"scatter",
248 changes: 248 additions & 0 deletions xarray/plot/accessor.py
Original file line number Diff line number Diff line change
@@ -135,6 +135,130 @@ def line(
def line(self, *args, **kwargs) -> list[Line3D] | FacetGrid[DataArray]:
return dataarray_plot.line(self._da, *args, **kwargs)

@overload
def lines( # type: ignore[misc,unused-ignore] # None is hashable :(
self,
*args: Any,
x: Hashable | None = None,
y: Hashable | None = None,
z: Hashable | None = None,
hue: Hashable | None = None,
hue_style: HueStyleOptions = None,
markersize: Hashable | None = None,
linewidth: Hashable | None = None,
figsize: Iterable[float] | None = None,
size: float | None = None,
aspect: float | None = None,
ax: Axes | None = None,
row: None = None, # no wrap -> primitive
col: None = None, # no wrap -> primitive
col_wrap: int | None = None,
xincrease: bool | None = True,
yincrease: bool | None = True,
add_legend: bool | None = None,
add_colorbar: bool | None = None,
add_labels: bool | Iterable[bool] = True,
add_title: bool = True,
subplot_kws: dict[str, Any] | None = None,
xscale: ScaleOptions = None,
yscale: ScaleOptions = None,
xticks: ArrayLike | None = None,
yticks: ArrayLike | None = None,
xlim: ArrayLike | None = None,
ylim: ArrayLike | None = None,
cmap=None,
vmin: float | None = None,
vmax: float | None = None,
norm: Normalize | None = None,
extend=None,
levels=None,
**kwargs: Any,
) -> LineCollection: ...

@overload
def lines(
self,
*args: Any,
x: Hashable | None = None,
y: Hashable | None = None,
z: Hashable | None = None,
hue: Hashable | None = None,
hue_style: HueStyleOptions = None,
markersize: Hashable | None = None,
linewidth: Hashable | None = None,
figsize: Iterable[float] | None = None,
size: float | None = None,
aspect: float | None = None,
ax: Axes | None = None,
row: Hashable | None = None,
col: Hashable, # wrap -> FacetGrid
col_wrap: int | None = None,
xincrease: bool | None = True,
yincrease: bool | None = True,
add_legend: bool | None = None,
add_colorbar: bool | None = None,
add_labels: bool | Iterable[bool] = True,
add_title: bool = True,
subplot_kws: dict[str, Any] | None = None,
xscale: ScaleOptions = None,
yscale: ScaleOptions = None,
xticks: ArrayLike | None = None,
yticks: ArrayLike | None = None,
xlim: ArrayLike | None = None,
ylim: ArrayLike | None = None,
cmap=None,
vmin: float | None = None,
vmax: float | None = None,
norm: Normalize | None = None,
extend=None,
levels=None,
**kwargs: Any,
) -> FacetGrid[DataArray]: ...

@overload
def lines(
self,
*args: Any,
x: Hashable | None = None,
y: Hashable | None = None,
z: Hashable | None = None,
hue: Hashable | None = None,
hue_style: HueStyleOptions = None,
markersize: Hashable | None = None,
linewidth: Hashable | None = None,
figsize: Iterable[float] | None = None,
size: float | None = None,
aspect: float | None = None,
ax: Axes | None = None,
row: Hashable, # wrap -> FacetGrid
col: Hashable | None = None,
col_wrap: int | None = None,
xincrease: bool | None = True,
yincrease: bool | None = True,
add_legend: bool | None = None,
add_colorbar: bool | None = None,
add_labels: bool | Iterable[bool] = True,
add_title: bool = True,
subplot_kws: dict[str, Any] | None = None,
xscale: ScaleOptions = None,
yscale: ScaleOptions = None,
xticks: ArrayLike | None = None,
yticks: ArrayLike | None = None,
xlim: ArrayLike | None = None,
ylim: ArrayLike | None = None,
cmap=None,
vmin: float | None = None,
vmax: float | None = None,
norm: Normalize | None = None,
extend=None,
levels=None,
**kwargs: Any,
) -> FacetGrid[DataArray]: ...

@functools.wraps(dataarray_plot.lines)
def lines(self, *args, **kwargs) -> LineCollection | FacetGrid[DataArray]:
return dataarray_plot.lines(self._da, *args, **kwargs)

@overload
def step( # type: ignore[misc,unused-ignore] # None is hashable :(
self,
@@ -923,6 +1047,130 @@ def __call__(self, *args, **kwargs) -> NoReturn:
"an explicit plot method, e.g. ds.plot.scatter(...)"
)

@overload
def lines( # type: ignore[misc,unused-ignore] # None is hashable :(
self,
*args: Any,
x: Hashable | None = None,
y: Hashable | None = None,
z: Hashable | None = None,
hue: Hashable | None = None,
hue_style: HueStyleOptions = None,
markersize: Hashable | None = None,
linewidth: Hashable | None = None,
figsize: Iterable[float] | None = None,
size: float | None = None,
aspect: float | None = None,
ax: Axes | None = None,
row: None = None, # no wrap -> primitive
col: None = None, # no wrap -> primitive
col_wrap: int | None = None,
xincrease: bool | None = True,
yincrease: bool | None = True,
add_legend: bool | None = None,
add_colorbar: bool | None = None,
add_labels: bool | Iterable[bool] = True,
add_title: bool = True,
subplot_kws: dict[str, Any] | None = None,
xscale: ScaleOptions = None,
yscale: ScaleOptions = None,
xticks: ArrayLike | None = None,
yticks: ArrayLike | None = None,
xlim: ArrayLike | None = None,
ylim: ArrayLike | None = None,
cmap=None,
vmin: float | None = None,
vmax: float | None = None,
norm: Normalize | None = None,
extend=None,
levels=None,
**kwargs: Any,
) -> LineCollection: ...

@overload
def lines(
self,
*args: Any,
x: Hashable | None = None,
y: Hashable | None = None,
z: Hashable | None = None,
hue: Hashable | None = None,
hue_style: HueStyleOptions = None,
markersize: Hashable | None = None,
linewidth: Hashable | None = None,
figsize: Iterable[float] | None = None,
size: float | None = None,
aspect: float | None = None,
ax: Axes | None = None,
row: Hashable | None = None,
col: Hashable, # wrap -> FacetGrid
col_wrap: int | None = None,
xincrease: bool | None = True,
yincrease: bool | None = True,
add_legend: bool | None = None,
add_colorbar: bool | None = None,
add_labels: bool | Iterable[bool] = True,
add_title: bool = True,
subplot_kws: dict[str, Any] | None = None,
xscale: ScaleOptions = None,
yscale: ScaleOptions = None,
xticks: ArrayLike | None = None,
yticks: ArrayLike | None = None,
xlim: ArrayLike | None = None,
ylim: ArrayLike | None = None,
cmap=None,
vmin: float | None = None,
vmax: float | None = None,
norm: Normalize | None = None,
extend=None,
levels=None,
**kwargs: Any,
) -> FacetGrid[DataArray]: ...

@overload
def lines(
self,
*args: Any,
x: Hashable | None = None,
y: Hashable | None = None,
z: Hashable | None = None,
hue: Hashable | None = None,
hue_style: HueStyleOptions = None,
markersize: Hashable | None = None,
linewidth: Hashable | None = None,
figsize: Iterable[float] | None = None,
size: float | None = None,
aspect: float | None = None,
ax: Axes | None = None,
row: Hashable, # wrap -> FacetGrid
col: Hashable | None = None,
col_wrap: int | None = None,
xincrease: bool | None = True,
yincrease: bool | None = True,
add_legend: bool | None = None,
add_colorbar: bool | None = None,
add_labels: bool | Iterable[bool] = True,
add_title: bool = True,
subplot_kws: dict[str, Any] | None = None,
xscale: ScaleOptions = None,
yscale: ScaleOptions = None,
xticks: ArrayLike | None = None,
yticks: ArrayLike | None = None,
xlim: ArrayLike | None = None,
ylim: ArrayLike | None = None,
cmap=None,
vmin: float | None = None,
vmax: float | None = None,
norm: Normalize | None = None,
extend=None,
levels=None,
**kwargs: Any,
) -> FacetGrid[DataArray]: ...

@functools.wraps(dataset_plot.lines)
def lines(self, *args, **kwargs) -> LineCollection | FacetGrid[DataArray]:
return dataset_plot.lines(self._ds, *args, **kwargs)

@overload
def scatter( # type: ignore[misc,unused-ignore] # None is hashable :(
self,
212 changes: 201 additions & 11 deletions xarray/plot/dataarray_plot.py
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@
_guess_coords_to_plot,
_infer_interval_breaks,
_infer_xy_labels,
_line,
_Normalize,
_process_cmap_cbar_kwargs,
_rescale_imshow_rgb,
@@ -36,7 +37,7 @@

if TYPE_CHECKING:
from matplotlib.axes import Axes
from matplotlib.collections import PathCollection, QuadMesh
from matplotlib.collections import LineCollection, PathCollection, QuadMesh
from matplotlib.colors import Colormap, Normalize
from matplotlib.container import BarContainer
from matplotlib.contour import QuadContourSet
@@ -186,18 +187,32 @@ def _prepare_plot1d_data(
"""
# If there are more than 1 dimension in the array than stack all the
# dimensions so the plotter can plot anything:
if darray.ndim > 1:
if darray.ndim >= 2:
# When stacking dims the lines will continue connecting. For floats
# this can be solved by adding a nan element in between the flattening
# points:
dims_T = []
if np.issubdtype(darray.dtype, np.floating):
for v in ["z", "x"]:
dim = coords_to_plot.get(v, None)
if (dim is not None) and (dim in darray.dims):
darray_nan = np.nan * darray.isel({dim: -1})
darray = concat([darray, darray_nan], dim=dim)
dims_T.append(coords_to_plot[v])
dims_T: list[Hashable] = []
if plotfunc_name == "lines" and np.issubdtype(darray.dtype, np.floating):
i = 0
for v in ("z", "x"):
coord = coords_to_plot.get(v, None)
if coord is not None:
if coord in darray.dims:
# Dimension coordinate:
d = coord
else:
# Coordinate with multiple dimensions:
c = darray[coord]
dims_filt = dict.fromkeys(c.dims)
for k in dims_filt.keys() & set(dims_T):
dims_filt.pop(k)

d = tuple(dims_filt.keys())[i]

darray_nan = np.nan * darray.isel({d: -1})
darray = concat([darray, darray_nan], dim=d)
dims_T.append(d)
# i += 1

# Lines should never connect to the same coordinate when stacked,
# transpose to avoid this as much as possible:
@@ -472,6 +487,10 @@ def line(
primitive : list of Line3D or FacetGrid
When either col or row is given, returns a FacetGrid, otherwise
a list of matplotlib Line3D objects.
See also
--------
Use :py:func:`xarray.plot.lines` for efficient plotting of many lines.
"""
# Handle facetgrids first
if row or col:
@@ -1038,7 +1057,7 @@ def newplotfunc(
)

if add_legend_:
if plotfunc.__name__ in ["scatter", "line"]:
if plotfunc.__name__ in ["scatter", "lines"]:
_add_legend(
(
hueplt_norm
@@ -1108,6 +1127,177 @@ def _add_labels(
_set_concise_date(ax, axis=axis)


@overload
def lines( # type: ignore[misc,unused-ignore] # None is hashable :(
darray: DataArray,
*args: Any,
x: Hashable | None = None,
y: Hashable | None = None,
z: Hashable | None = None,
hue: Hashable | None = None,
hue_style: HueStyleOptions = None,
markersize: Hashable | None = None,
linewidth: Hashable | None = None,
figsize: Iterable[float] | None = None,
size: float | None = None,
aspect: float | None = None,
ax: Axes | None = None,
row: None = None, # no wrap -> primitive
col: None = None, # no wrap -> primitive
col_wrap: int | None = None,
xincrease: bool | None = True,
yincrease: bool | None = True,
add_legend: bool | None = None,
add_colorbar: bool | None = None,
add_labels: bool | Iterable[bool] = True,
add_title: bool = True,
subplot_kws: dict[str, Any] | None = None,
xscale: ScaleOptions = None,
yscale: ScaleOptions = None,
xticks: ArrayLike | None = None,
yticks: ArrayLike | None = None,
xlim: ArrayLike | None = None,
ylim: ArrayLike | None = None,
cmap: str | Colormap | None = None,
vmin: float | None = None,
vmax: float | None = None,
norm: Normalize | None = None,
extend: ExtendOptions = None,
levels: ArrayLike | None = None,
**kwargs,
) -> LineCollection: ...


@overload
def lines(
darray: T_DataArray,
*args: Any,
x: Hashable | None = None,
y: Hashable | None = None,
z: Hashable | None = None,
hue: Hashable | None = None,
hue_style: HueStyleOptions = None,
markersize: Hashable | None = None,
linewidth: Hashable | None = None,
figsize: Iterable[float] | None = None,
size: float | None = None,
aspect: float | None = None,
ax: Axes | None = None,
row: Hashable | None = None,
col: Hashable, # wrap -> FacetGrid
col_wrap: int | None = None,
xincrease: bool | None = True,
yincrease: bool | None = True,
add_legend: bool | None = None,
add_colorbar: bool | None = None,
add_labels: bool | Iterable[bool] = True,
add_title: bool = True,
subplot_kws: dict[str, Any] | None = None,
xscale: ScaleOptions = None,
yscale: ScaleOptions = None,
xticks: ArrayLike | None = None,
yticks: ArrayLike | None = None,
xlim: ArrayLike | None = None,
ylim: ArrayLike | None = None,
cmap: str | Colormap | None = None,
vmin: float | None = None,
vmax: float | None = None,
norm: Normalize | None = None,
extend: ExtendOptions = None,
levels: ArrayLike | None = None,
**kwargs,
) -> FacetGrid[DataArray]: ...


@overload
def lines(
darray: T_DataArray,
*args: Any,
x: Hashable | None = None,
y: Hashable | None = None,
z: Hashable | None = None,
hue: Hashable | None = None,
hue_style: HueStyleOptions = None,
markersize: Hashable | None = None,
linewidth: Hashable | None = None,
figsize: Iterable[float] | None = None,
size: float | None = None,
aspect: float | None = None,
ax: Axes | None = None,
row: Hashable, # wrap -> FacetGrid
col: Hashable | None = None,
col_wrap: int | None = None,
xincrease: bool | None = True,
yincrease: bool | None = True,
add_legend: bool | None = None,
add_colorbar: bool | None = None,
add_labels: bool | Iterable[bool] = True,
add_title: bool = True,
subplot_kws: dict[str, Any] | None = None,
xscale: ScaleOptions = None,
yscale: ScaleOptions = None,
xticks: ArrayLike | None = None,
yticks: ArrayLike | None = None,
xlim: ArrayLike | None = None,
ylim: ArrayLike | None = None,
cmap: str | Colormap | None = None,
vmin: float | None = None,
vmax: float | None = None,
norm: Normalize | None = None,
extend: ExtendOptions = None,
levels: ArrayLike | None = None,
**kwargs,
) -> FacetGrid[DataArray]: ...


@_plot1d
def lines(
xplt: DataArray | None,
yplt: DataArray | None,
ax: Axes,
add_labels: bool | Iterable[bool] = True,
**kwargs,
) -> LineCollection:
"""
Line plot of DataArray values.
Wraps :func:`matplotlib:matplotlib.collections.LineCollection` which allows
efficient plotting of many lines in a similar fashion to
:py:func:`xarray.plot.scatter`.
"""
if "u" in kwargs or "v" in kwargs:
raise ValueError("u, v are not allowed in lines plots.")

zplt: DataArray | None = kwargs.pop("zplt", None)
hueplt: DataArray | None = kwargs.pop("hueplt", None)
sizeplt: DataArray | None = kwargs.pop("sizeplt", None)

if hueplt is not None:
kwargs.update(c=hueplt.to_numpy().ravel())

if sizeplt is not None:
kwargs.update(s=sizeplt.to_numpy().ravel())

plts_or_none = (xplt, yplt, zplt)
_add_labels(add_labels, plts_or_none, ("", "", ""), ax)

xplt_np = None if xplt is None else xplt.to_numpy().ravel()
yplt_np = None if yplt is None else yplt.to_numpy().ravel()
zplt_np = None if zplt is None else zplt.to_numpy().ravel()
plts_np = tuple(p for p in (xplt_np, yplt_np, zplt_np) if p is not None)

if len(plts_np) == 3:
import mpl_toolkits

assert isinstance(ax, mpl_toolkits.mplot3d.axes3d.Axes3D)
return _line(ax, *plts_np, **kwargs)

if len(plts_np) == 2:
return _line(ax, *plts_np, **kwargs)

raise ValueError("At least two variables required for a lines plot.")


@overload
def scatter( # type: ignore[misc,unused-ignore] # None is hashable :(
darray: DataArray,
177 changes: 177 additions & 0 deletions xarray/plot/dataset_plot.py
Original file line number Diff line number Diff line change
@@ -918,3 +918,180 @@ def scatter(
da = _temp_dataarray(ds, y, locals_)

return da.plot.scatter(*locals_.pop("args", ()), **locals_)


@overload
def lines( # type: ignore[misc,unused-ignore] # None is hashable :(
ds: Dataset,
*args: Any,
x: Hashable | None = None,
y: Hashable | None = None,
z: Hashable | None = None,
hue: Hashable | None = None,
hue_style: HueStyleOptions = None,
markersize: Hashable | None = None,
linewidth: Hashable | None = None,
figsize: Iterable[float] | None = None,
size: float | None = None,
aspect: float | None = None,
ax: Axes | None = None,
row: None = None, # no wrap -> primitive
col: None = None, # no wrap -> primitive
col_wrap: int | None = None,
xincrease: bool | None = True,
yincrease: bool | None = True,
add_legend: bool | None = None,
add_colorbar: bool | None = None,
add_labels: bool | Iterable[bool] = True,
add_title: bool = True,
subplot_kws: dict[str, Any] | None = None,
xscale: ScaleOptions = None,
yscale: ScaleOptions = None,
xticks: ArrayLike | None = None,
yticks: ArrayLike | None = None,
xlim: ArrayLike | None = None,
ylim: ArrayLike | None = None,
cmap: str | Colormap | None = None,
vmin: float | None = None,
vmax: float | None = None,
norm: Normalize | None = None,
extend: ExtendOptions = None,
levels: ArrayLike | None = None,
**kwargs: Any,
) -> LineCollection: ...


@overload
def lines(
ds: Dataset,
*args: Any,
x: Hashable | None = None,
y: Hashable | None = None,
z: Hashable | None = None,
hue: Hashable | None = None,
hue_style: HueStyleOptions = None,
markersize: Hashable | None = None,
linewidth: Hashable | None = None,
figsize: Iterable[float] | None = None,
size: float | None = None,
aspect: float | None = None,
ax: Axes | None = None,
row: Hashable | None = None,
col: Hashable, # wrap -> FacetGrid
col_wrap: int | None = None,
xincrease: bool | None = True,
yincrease: bool | None = True,
add_legend: bool | None = None,
add_colorbar: bool | None = None,
add_labels: bool | Iterable[bool] = True,
add_title: bool = True,
subplot_kws: dict[str, Any] | None = None,
xscale: ScaleOptions = None,
yscale: ScaleOptions = None,
xticks: ArrayLike | None = None,
yticks: ArrayLike | None = None,
xlim: ArrayLike | None = None,
ylim: ArrayLike | None = None,
cmap: str | Colormap | None = None,
vmin: float | None = None,
vmax: float | None = None,
norm: Normalize | None = None,
extend: ExtendOptions = None,
levels: ArrayLike | None = None,
**kwargs: Any,
) -> FacetGrid[DataArray]: ...


@overload
def lines(
ds: Dataset,
*args: Any,
x: Hashable | None = None,
y: Hashable | None = None,
z: Hashable | None = None,
hue: Hashable | None = None,
hue_style: HueStyleOptions = None,
markersize: Hashable | None = None,
linewidth: Hashable | None = None,
figsize: Iterable[float] | None = None,
size: float | None = None,
aspect: float | None = None,
ax: Axes | None = None,
row: Hashable, # wrap -> FacetGrid
col: Hashable | None = None,
col_wrap: int | None = None,
xincrease: bool | None = True,
yincrease: bool | None = True,
add_legend: bool | None = None,
add_colorbar: bool | None = None,
add_labels: bool | Iterable[bool] = True,
add_title: bool = True,
subplot_kws: dict[str, Any] | None = None,
xscale: ScaleOptions = None,
yscale: ScaleOptions = None,
xticks: ArrayLike | None = None,
yticks: ArrayLike | None = None,
xlim: ArrayLike | None = None,
ylim: ArrayLike | None = None,
cmap: str | Colormap | None = None,
vmin: float | None = None,
vmax: float | None = None,
norm: Normalize | None = None,
extend: ExtendOptions = None,
levels: ArrayLike | None = None,
**kwargs: Any,
) -> FacetGrid[DataArray]: ...


@_update_doc_to_dataset(dataarray_plot.lines)
def lines(
ds: Dataset,
*args: Any,
x: Hashable | None = None,
y: Hashable | None = None,
z: Hashable | None = None,
hue: Hashable | None = None,
hue_style: HueStyleOptions = None,
markersize: Hashable | None = None,
linewidth: Hashable | None = None,
figsize: Iterable[float] | None = None,
size: float | None = None,
aspect: float | None = None,
ax: Axes | None = None,
row: Hashable | None = None,
col: Hashable | None = None,
col_wrap: int | None = None,
xincrease: bool | None = True,
yincrease: bool | None = True,
add_legend: bool | None = None,
add_colorbar: bool | None = None,
add_labels: bool | Iterable[bool] = True,
add_title: bool = True,
subplot_kws: dict[str, Any] | None = None,
xscale: ScaleOptions = None,
yscale: ScaleOptions = None,
xticks: ArrayLike | None = None,
yticks: ArrayLike | None = None,
xlim: ArrayLike | None = None,
ylim: ArrayLike | None = None,
cmap: str | Colormap | None = None,
vmin: float | None = None,
vmax: float | None = None,
norm: Normalize | None = None,
extend: ExtendOptions = None,
levels: ArrayLike | None = None,
**kwargs: Any,
) -> LineCollection | FacetGrid[DataArray]:
"""
Line plot Dataset data variables against each other.
Wraps :func:`matplotlib:matplotlib.collections.LineCollection` which allows
efficient plotting of many lines in a similar fashion to
:py:func:`xarray.plot.scatter`.
"""
locals_ = locals()
del locals_["ds"]
locals_.update(locals_.pop("kwargs", {}))
da = _temp_dataarray(ds, y, locals_)

return da.plot.lines(*locals_.pop("args", ()), **locals_)
12 changes: 6 additions & 6 deletions xarray/plot/facetgrid.py
Original file line number Diff line number Diff line change
@@ -486,7 +486,7 @@ def map_plot1d(
func_kwargs["add_title"] = False

add_labels_ = np.zeros(self.axs.shape + (3,), dtype=bool)
if kwargs.get("z") is not None:
if coords_to_plot["z"] is not None:
# 3d plots looks better with all labels. 3d plots can't sharex either so it
# is easy to get lost while rotating the plots:
add_labels_[:] = True
@@ -499,10 +499,10 @@ def map_plot1d(
# Set up the lists of names for the row and column facet variables:
if self._single_group:
full = tuple(
{self._single_group: x}
for x in range(self.data[self._single_group].size)
{self._single_group: v}
for v in range(self.data[self._single_group].size)
)
empty = tuple(None for x in range(self._nrow * self._ncol - len(full)))
empty = (None,) * (self._nrow * self._ncol - len(full))
name_d = full + empty
else:
rowcols = itertools.product(
@@ -525,8 +525,8 @@ def map_plot1d(
subset = self.data.isel(d)
mappable = func(
subset,
x=x,
y=y,
x=coords_to_plot["x"],
z=coords_to_plot["z"],
ax=ax,
hue=hue,
_size=size_,
279 changes: 268 additions & 11 deletions xarray/plot/utils.py
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@
)
from datetime import date, datetime
from inspect import getfullargspec
from typing import TYPE_CHECKING, Any, Literal, cast, overload
from typing import TYPE_CHECKING, Any, Literal, overload

import numpy as np
import pandas as pd
@@ -38,8 +38,13 @@

if TYPE_CHECKING:
from matplotlib.axes import Axes
from matplotlib.colors import Normalize
from matplotlib.collections import LineCollection
from matplotlib.colorizer import Colorizer
from matplotlib.colors import Colormap, Normalize
from matplotlib.lines import Line2D
from matplotlib.ticker import FuncFormatter
from matplotlib.typing import ColorType, DrawStyleType, LineStyleType
from mpl_toolkits.mplot3d.art3d import Line3DCollection
from numpy.typing import ArrayLike

from xarray.core.dataarray import DataArray
@@ -1071,9 +1076,9 @@ def _get_color_and_size(value):

elif prop == "sizes":
if isinstance(self, mpl.collections.LineCollection):
arr = self.get_linewidths()
arr = np.ma.asarray(self.get_linewidths())
else:
arr = self.get_sizes()
arr = np.ma.asarray(self.get_sizes())
_color = kwargs.pop("color", "k")

def _get_color_and_size(value):
@@ -1086,7 +1091,7 @@ def _get_color_and_size(value):
)

# Get the unique values and their labels:
values = np.unique(arr)
values = np.unique(arr[~arr.mask])
label_values = np.asarray(func(values))
label_values_are_numeric = np.issubdtype(label_values.dtype, np.number)

@@ -1729,17 +1734,20 @@ def _add_legend(
# values correctly. Order might be different because
# legend_elements uses np.unique instead of pd.unique,
# FacetGrid.add_legend might have troubles with this:
hdl, lbl = [], []
hdl: list[Line2D] = []
lbl: list[str] = []
for p in primitive:
hdl_, lbl_ = legend_elements(p, prop, num="auto", func=huesizeplt.func)
hdl += hdl_
lbl += lbl_

# Only save unique values:
u, ind = np.unique(lbl, return_index=True)
ind = np.argsort(ind)
lbl = cast(list, u[ind].tolist())
hdl = cast(list, np.array(hdl)[ind].tolist())
# Only save unique values, don't sort values as it was already sort in
# legend_elements:
lbl_ = np.array(lbl)
_, ind = np.unique(lbl_, return_index=True)
ind = np.sort(ind)
lbl = lbl_[ind].tolist()
hdl = np.array(hdl)[ind].tolist()

# Add a subtitle:
hdl, lbl = _legend_add_subtitle(hdl, lbl, label_from_attrs(huesizeplt.data))
@@ -1842,6 +1850,255 @@ def _guess_coords_to_plot(
return coords_to_plot


@overload
def _line(
self, # Axes,
x: float | ArrayLike,
y: float | ArrayLike,
z: None = ...,
s: float | ArrayLike | None = ...,
c: Sequence[ColorType] | ColorType | None = ...,
*,
linestyle: LineStyleType | None = ...,
cmap: str | Colormap | None = ...,
norm: str | Normalize | None = ...,
vmin: float | None = ...,
vmax: float | None = ...,
alpha: float | None = ...,
edgecolors: Literal["face", "none"] | ColorType | Sequence[ColorType] | None = ...,
plotnonfinite: bool = ...,
data=...,
**kwargs,
) -> LineCollection: ...


@overload
def _line(
self, # Axes3D,
x: float | ArrayLike,
y: float | ArrayLike,
z: float | ArrayLike = ...,
s: float | ArrayLike | None = ...,
c: Sequence[ColorType] | ColorType | None = ...,
*,
linestyle: LineStyleType | None = ...,
cmap: str | Colormap | None = ...,
norm: str | Normalize | None = ...,
vmin: float | None = ...,
vmax: float | None = ...,
alpha: float | None = ...,
edgecolors: Literal["face", "none"] | ColorType | Sequence[ColorType] | None = ...,
plotnonfinite: bool = ...,
data=...,
drawstyle: DrawStyleType = ...,
**kwargs,
) -> Line3DCollection: ...


def _line(
self, # Axes | Axes3D
x: float | ArrayLike,
y: float | ArrayLike,
z: float | ArrayLike | None = None,
s: float | ArrayLike | None = None,
c: ArrayLike | Sequence[ColorType] | ColorType | None = None,
*,
linestyle: LineStyleType | None = None,
cmap: str | Colormap | None = None,
norm: str | Normalize | None = None,
vmin: float | None = None,
vmax: float | None = None,
alpha: float | None = None,
edgecolors: Literal["face", "none"] | ColorType | Sequence[ColorType] | None = None,
colorizer: Colorizer | None = None,
plotnonfinite: bool = False,
data=None,
drawstyle: DrawStyleType = "default",
**kwargs,
) -> LineCollection | Line3DCollection:
"""
ax.scatter-like wrapper for LineCollection.
This function helps the handling of datetimes since Linecollection doesn't
support it directly, just like PatchCollection doesn't either.
The function attempts to be as similar to the scatter version as possible.
"""
import matplotlib.collections as mcoll
import matplotlib.pyplot as plt
from matplotlib import _api, cbook

rcParams = plt.matplotlib.rcParams

def _parse_lines_color_args(
self, c, edgecolors, kwargs, xsize, get_next_color_func
):
if edgecolors is None:
# Use "face" instead of rcParams['scatter.edgecolors']
edgecolors = "face"

c, colors, edgecolors = self._parse_scatter_color_args(
c,
edgecolors,
kwargs,
x_.size,
get_next_color_func=self._get_patches_for_fill.get_next_color,
)

return c, colors, edgecolors

linewidths = s # Can be different in scatter, but same in line plots.

# add edgecolors and linewidths to kwargs so they
# can be processed by normailze_kwargs
if edgecolors is not None:
kwargs.update({"edgecolors": edgecolors})
if linewidths is not None:
kwargs.update({"linewidths": linewidths})

kwargs = cbook.normalize_kwargs(kwargs, mcoll.Collection)
# re direct linewidth and edgecolor so it can be
# further processed by the rest of the function
linewidths = kwargs.pop("linewidth", None)
edgecolors = kwargs.pop("edgecolor", None)

# Process **kwargs to handle aliases, conflicts with explicit kwargs:
x_: np.ndarray
y_: np.ndarray
x_, y_ = self._process_unit_info(
[("x", x), ("y", y)], kwargs
) # type ignore[union-attr]

# Handle z inputs:
if z is not None:
from mpl_toolkits.mplot3d.art3d import Line3DCollection

LineCollection_ = Line3DCollection
add_collection_ = self.add_collection3d
auto_scale = self.auto_scale_xyz
auto_scale_args: tuple[Any, ...] = (x_, y_, z, self.has_data())
else:
LineCollection_ = plt.matplotlib.collections.LineCollection
add_collection_ = self.add_collection
auto_scale = self._request_autoscale_view
auto_scale_args = tuple()

if s is None:
s = np.array([rcParams["lines.linewidth"]])

s_: np.ndarray = np.ma.ravel(s)
if len(s_) not in (1, x_.size) or (
not np.issubdtype(s_.dtype, np.floating)
and not np.issubdtype(s_.dtype, np.integer)
):
raise ValueError(
"s must be a scalar, or float array-like with the same size as x and y"
)

# get the original edgecolor the user passed before we normalize
orig_edgecolor = edgecolors
if edgecolors is None:
orig_edgecolor = kwargs.get("edgecolor", None)
c, colors, edgecolors = _parse_lines_color_args(
self,
c,
edgecolors,
kwargs,
x_.size,
get_next_color_func=self._get_patches_for_fill.get_next_color,
)

if plotnonfinite and colors is None:
c = np.ma.masked_invalid(c)
(
x_,
y_,
s_,
edgecolors,
linewidths,
) = cbook._combine_masks( # type: ignore[attr-defined] # non-public?
x_, y_, s_, edgecolors, linewidths
)
else:
(
x_,
y_,
s_,
c,
colors,
edgecolors,
linewidths,
) = cbook._combine_masks( # type: ignore[attr-defined] # non-public?
x_, y_, s_, c, colors, edgecolors, linewidths
)

# Unmask edgecolors if it was actually a single RGB or RGBA.
if (
x_.size in (3, 4)
and isinstance(edgecolors, np.ma.MaskedArray)
and not np.ma.is_masked(orig_edgecolor)
):
edgecolors = edgecolors.data

# load default linestyle from rcParams
if linestyle is None:
linestyle = rcParams["lines.linestyle"]

if drawstyle == "default":
# Draw linear lines:
xyz = list(v for v in (x_, y_, z) if v is not None)
else:
# Draw stepwise lines:
from matplotlib.cbook import STEP_LOOKUP_MAP

step_func = STEP_LOOKUP_MAP[drawstyle]
xyz = step_func(*tuple(v for v in (x_, y_, z) if v is not None))

# Broadcast arrays to correct format:
# https://stackoverflow.com/questions/42215777/matplotlib-line-color-in-3d
points = np.stack(np.broadcast_arrays(*xyz), axis=-1).reshape(-1, 1, len(xyz))
segments = np.concatenate([points[:-1], points[1:]], axis=1)

collection = LineCollection_(
segments,
linewidths=s_,
linestyles=linestyle,
facecolors=colors,
edgecolors=edgecolors,
alpha=alpha,
# offset_transform=kwargs.pop("transform", self.transData),
)
# collection.set_transform(plt.matplotlib.transforms.IdentityTransform())
collection.update(kwargs)

if colors is None:
if colorizer:
collection._set_colorizer_check_keywords(
colorizer, cmap=cmap, norm=norm, vmin=vmin, vmax=vmax
)
else:
collection.set_cmap(cmap)
collection.set_norm(norm)
collection.set_array(c)
collection._scale_norm(norm, vmin, vmax)
else:
extra_kwargs = {"cmap": cmap, "norm": norm, "vmin": vmin, "vmax": vmax}
extra_keys = [k for k, v in extra_kwargs.items() if v is not None]
if any(extra_keys):
keys_str = ", ".join(f"'{k}'" for k in extra_keys)
_api.warn_external(
"No data for colormapping provided via 'c'. "
f"Parameters {keys_str} will be ignored"
)
collection._internal_update(kwargs)

add_collection_(collection)

auto_scale(*auto_scale_args)

return collection


def _set_concise_date(ax: Axes, axis: Literal["x", "y", "z"] = "x") -> None:
"""
Use ConciseDateFormatter which is meant to improve the
190 changes: 173 additions & 17 deletions xarray/tests/test_plot.py
Original file line number Diff line number Diff line change
@@ -2936,6 +2936,57 @@ def test_legend_labels_facetgrid(self) -> None:
)
assert actual == expected

def test_legend_labels_facegrid2(self) -> None:
ds = xr.tutorial.scatter_example_dataset(seed=42)

g = ds.plot.scatter(
x="A", y="B", hue="y", markersize="x", row="x", col="w", add_colorbar=False
)

legend = g.figlegend
assert legend is not None
actual_text = [t.get_text() for t in legend.texts]
expected_text = [
"y [yunits]",
"$\\mathdefault{0.0}$",
"$\\mathdefault{0.1}$",
"$\\mathdefault{0.2}$",
"$\\mathdefault{0.3}$",
"$\\mathdefault{0.4}$",
"$\\mathdefault{0.5}$",
"$\\mathdefault{0.6}$",
"$\\mathdefault{0.7}$",
"$\\mathdefault{0.8}$",
"$\\mathdefault{0.9}$",
"$\\mathdefault{1.0}$",
"x [xunits]",
"$\\mathdefault{0}$",
"$\\mathdefault{1}$",
"$\\mathdefault{2}$",
]
assert actual_text == expected_text

actual_size = [v.get_markersize() for v in legend.get_lines()]
expected_size = [
6.0,
6.0,
6.0,
6.0,
6.0,
6.0,
6.0,
6.0,
6.0,
6.0,
6.0,
6.0,
6.0,
4.242640687119285,
6.708203932499369,
8.48528137423857,
]
np.testing.assert_allclose(expected_size, actual_size)

def test_add_legend_by_default(self) -> None:
sc = self.ds.plot.scatter(x="A", y="B", hue="hue")
fig = sc.figure
@@ -3305,8 +3356,9 @@ def test_maybe_gca() -> None:


@requires_matplotlib
@pytest.mark.parametrize("plotfunc", ["scatter", "lines"])
@pytest.mark.parametrize(
"x, y, z, hue, markersize, row, col, add_legend, add_colorbar",
"x, y, z, hue, _size, row, col, add_legend, add_colorbar",
[
("A", "B", None, None, None, None, None, None, None),
("B", "A", None, "w", None, None, None, True, None),
@@ -3315,30 +3367,33 @@ def test_maybe_gca() -> None:
("B", "A", "z", "w", None, None, None, True, None),
("A", "B", "z", "y", "x", None, None, True, True),
("A", "B", "z", "y", "x", "w", None, True, True),
("A", "B", "z", "y", "x", "w", "x", True, True),
],
)
def test_datarray_scatter(
x, y, z, hue, markersize, row, col, add_legend, add_colorbar
def test_plot1d_functions(
x: Hashable,
y: Hashable,
z: Hashable,
hue: Hashable,
_size: Hashable,
row: Hashable,
col: Hashable,
add_legend: bool | None,
add_colorbar: bool | None,
plotfunc: str,
) -> None:
"""Test datarray scatter. Merge with TestPlot1D eventually."""
ds = xr.tutorial.scatter_example_dataset()

extra_coords = [v for v in [x, hue, markersize] if v is not None]

# Base coords:
coords = dict(ds.coords)

# Add extra coords to the DataArray:
coords.update({v: ds[v] for v in extra_coords})

darray = xr.DataArray(ds[y], coords=coords)
"""Test plot1d function. Merge with TestPlot1D eventually."""
ds = xr.tutorial.scatter_example_dataset(seed=42)

with figure_context():
darray.plot.scatter(
getattr(ds.plot, plotfunc)(
x=x,
y=y,
z=z,
hue=hue,
markersize=markersize,
_size=_size,
row=row,
col=col,
add_legend=add_legend,
add_colorbar=add_colorbar,
)
@@ -3466,6 +3521,107 @@ def test_plot1d_filtered_nulls() -> None:
assert expected == actual


@requires_matplotlib
@pytest.mark.parametrize("plotfunc", ["lines"])
def test_plot1d_lines_color(plotfunc: str, x="z", color="b") -> None:
from matplotlib.colors import to_rgba_array

ds = xr.tutorial.scatter_example_dataset(seed=42)

darray = ds.A.sel(x=0, y=0)

with figure_context():
fig, ax = plt.subplots()
getattr(darray.plot, plotfunc)(x=x, color=color)
coll = ax.collections[0]

# Make sure color is respected:
expected_color = np.asarray(to_rgba_array(color))
actual_color = np.asarray(coll.get_edgecolor())
np.testing.assert_allclose(expected_color, actual_color)


@requires_matplotlib
@pytest.mark.parametrize("plotfunc", ["lines"])
def test_plot1d_lines_linestyle(plotfunc: str, x="z", linestyle="dashed") -> None:
# TODO: Is there a public function that converts linestyle to dash pattern?
from matplotlib.lines import ( # type: ignore[attr-defined]
_get_dash_pattern,
_scale_dashes,
)

ds = xr.tutorial.scatter_example_dataset(seed=42)

darray = ds.A.sel(x=0, y=0)

with figure_context():
fig, ax = plt.subplots()
getattr(darray.plot, plotfunc)(x=x, linestyle=linestyle)
coll = ax.collections[0]

# Make sure linestyle is respected:
w = np.atleast_1d(coll.get_linewidth())[0]
expected_linestyle = [_scale_dashes(*_get_dash_pattern(linestyle), w)]
actual_linestyle = coll.get_linestyle()
assert expected_linestyle == actual_linestyle


@requires_matplotlib
def test_plot1d_lines_facetgrid_legend() -> None:
# asserts that order is correct, only unique values, no nans/masked values.

ds = xr.tutorial.scatter_example_dataset(seed=42)

with figure_context():
g = ds.plot.lines(
x="A", y="B", hue="y", linewidth="x", row="x", col="w", add_colorbar=False
)

legend = g.figlegend
assert legend is not None
actual_text = [t.get_text() for t in legend.texts]
expected_text = [
"y [yunits]",
"$\\mathdefault{0.0}$",
"$\\mathdefault{0.1}$",
"$\\mathdefault{0.2}$",
"$\\mathdefault{0.3}$",
"$\\mathdefault{0.4}$",
"$\\mathdefault{0.5}$",
"$\\mathdefault{0.6}$",
"$\\mathdefault{0.7}$",
"$\\mathdefault{0.8}$",
"$\\mathdefault{0.9}$",
"$\\mathdefault{1.0}$",
"x [xunits]",
"$\\mathdefault{0}$",
"$\\mathdefault{1}$",
"$\\mathdefault{2}$",
]
assert expected_text == actual_text

actual_size = [v.get_linewidth() for v in legend.get_lines()]
expected_size = [
1.5,
6.0,
6.0,
6.0,
6.0,
6.0,
6.0,
6.0,
6.0,
6.0,
6.0,
6.0,
1.5,
1.224744871391589,
1.9364916731037085,
2.449489742783178,
]
np.testing.assert_allclose(expected_size, actual_size)


@requires_matplotlib
def test_9155() -> None:
# A test for types from issue #9155

Unchanged files with check annotations Beta

# otherwise numpy unsigned ints will silently cast to the signed counterpart
fill_value = fill_value.item()
# passes if provided fill value fits in encoded on-disk type
new_fill = encoded_dtype.type(fill_value)

Check warning on line 248 in xarray/coding/variables.py

GitHub Actions / ubuntu-latest py3.11 bare-min-and-scipy

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype) will give the desired result (the cast overflows).

Check warning on line 248 in xarray/coding/variables.py

GitHub Actions / ubuntu-latest py3.11 bare-min-and-scipy

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype) will give the desired result (the cast overflows).

Check warning on line 248 in xarray/coding/variables.py

GitHub Actions / ubuntu-latest py3.11 bare-min-and-scipy

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype) will give the desired result (the cast overflows).

Check warning on line 248 in xarray/coding/variables.py

GitHub Actions / ubuntu-latest py3.11 bare-min-and-scipy

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype) will give the desired result (the cast overflows).

Check warning on line 248 in xarray/coding/variables.py

GitHub Actions / ubuntu-latest py3.11 bare-min-and-scipy

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype) will give the desired result (the cast overflows).

Check warning on line 248 in xarray/coding/variables.py

GitHub Actions / ubuntu-latest py3.11 bare-min-and-scipy

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype) will give the desired result (the cast overflows).

Check warning on line 248 in xarray/coding/variables.py

GitHub Actions / ubuntu-latest py3.11 bare-min-and-scipy

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype) will give the desired result (the cast overflows).

Check warning on line 248 in xarray/coding/variables.py

GitHub Actions / ubuntu-latest py3.11 bare-min-and-scipy

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype) will give the desired result (the cast overflows).

Check warning on line 248 in xarray/coding/variables.py

GitHub Actions / ubuntu-latest py3.11 bare-min-and-scipy

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype) will give the desired result (the cast overflows).

Check warning on line 248 in xarray/coding/variables.py

GitHub Actions / ubuntu-latest py3.11 bare-min-and-scipy

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype) will give the desired result (the cast overflows).

Check warning on line 248 in xarray/coding/variables.py

GitHub Actions / ubuntu-latest py3.11 min-all-deps

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype) will give the desired result (the cast overflows).

Check warning on line 248 in xarray/coding/variables.py

GitHub Actions / ubuntu-latest py3.11 min-all-deps

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype) will give the desired result (the cast overflows).

Check warning on line 248 in xarray/coding/variables.py

GitHub Actions / ubuntu-latest py3.11 min-all-deps

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype) will give the desired result (the cast overflows).

Check warning on line 248 in xarray/coding/variables.py

GitHub Actions / ubuntu-latest py3.11 min-all-deps

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype) will give the desired result (the cast overflows).

Check warning on line 248 in xarray/coding/variables.py

GitHub Actions / ubuntu-latest py3.11 min-all-deps

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype) will give the desired result (the cast overflows).

Check warning on line 248 in xarray/coding/variables.py

GitHub Actions / ubuntu-latest py3.11 min-all-deps

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype) will give the desired result (the cast overflows).

Check warning on line 248 in xarray/coding/variables.py

GitHub Actions / ubuntu-latest py3.11 min-all-deps

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype) will give the desired result (the cast overflows).

Check warning on line 248 in xarray/coding/variables.py

GitHub Actions / ubuntu-latest py3.11 min-all-deps

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype) will give the desired result (the cast overflows).

Check warning on line 248 in xarray/coding/variables.py

GitHub Actions / ubuntu-latest py3.11 min-all-deps

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype) will give the desired result (the cast overflows).

Check warning on line 248 in xarray/coding/variables.py

GitHub Actions / ubuntu-latest py3.11 min-all-deps

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype) will give the desired result (the cast overflows).
except OverflowError:
encoded_kind_str = "signed" if encoded_dtype.kind == "i" else "unsigned"
warnings.warn(