From af91fd485607b4a4ed63ec55d2fcf33247a0c097 Mon Sep 17 00:00:00 2001 From: Jan-Lukas Wynen Date: Tue, 10 Nov 2026 14:54:50 +0100 Subject: [PATCH 1/8] Add marginals_plot --- docs/_static/plotting/marginals-plot.png | Bin 0 -> 37511 bytes docs/plotting/index.md | 7 ++ docs/plotting/marginals-plot.ipynb | 106 +++++++++++++++++++ src/plopp/__init__.py | 1 + src/plopp/plotting/__init__.pyi | 2 + src/plopp/plotting/_marginals.py | 97 +++++++++++++++++ tests/plotting/marginals_test.py | 126 +++++++++++++++++++++++ 7 files changed, 339 insertions(+) create mode 100644 docs/_static/plotting/marginals-plot.png create mode 100644 docs/plotting/marginals-plot.ipynb create mode 100644 src/plopp/plotting/_marginals.py create mode 100644 tests/plotting/marginals_test.py diff --git a/docs/_static/plotting/marginals-plot.png b/docs/_static/plotting/marginals-plot.png new file mode 100644 index 0000000000000000000000000000000000000000..ee338c7944223eaf63d1e890dc8c764d0e83e7a2 GIT binary patch literal 37511 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%0D2i)$VeVio~`r)^!M|`IP4Q# zxMbiI|1#ocXAi`@gyp4gEO#$|Qcwglbib$p5;{expEwYZaG3Y>S!v=yjcNK8Kc&T%KvJ^66FK>7&-}lMM}C z4^4|qgu6Pd1ery{cGl$vBqvo3zdsq_AG25Bp>Dy(yT%oRZujj@&}igtmy&#%nzvx9 zCh3#^OQ(+CIGO9UV|a?bEq{=Pb7A_Li9G=0CiHqfoHePJ^rNZZEAGvY6)8E0mVSIn zz2azmn?|!vDAzVUvD2|i;q;GmhsGZc?Fh(tXB%3$&1)X}j`y#JiV@3&g@pFwp}#9V z{fOxubaWf{oTV*re(PJhN;3bekk}M|hD!D)J5g&=4{oXAUD+WShcG<7H zpZUzsO{o4H>tONxGv}3fhL>~Grf+73@yIrocolF^?u+Wf6Tw|`SX=ubVnu@BM;-Ki zrggnI^@xAp4th7kzal0Nqr&#Y|I6AjVgZ;(kYiqx_Vp6Z`=amDM>Ec_?&!bzqW`(> zM`&1BAkBVPa=dPS;b;Ir9fO{yV5bNE4uyyHFMc&yHKJ z>mN>jDooLfx3Rgsb>?-O(Jh=2LD2LusP85A!LKo59 zMF4=vFS2vSiOds{1kyFsZ`*<4rSU%#GnUMOc!}5@am)RoT@Bn7>PBl|Ko+dpqp!H7 zL$%0n$6cV`$b$+OKFQlj%1>Y|kTlely}=t2x^>uJlJyYcB&H>>bGJK4CquPm`{4;+ z1sya{LT{MArP|9g1Xs1Lj{mD30cf|;6zsaq6X$kB^tw~I=L@Pu_L4nvI3gz8Z zc%zb)E-mNo>h>9Z49RO=O-@P*KUrdazkf1wqklPG4JKSD8J#L2A%O<(9`|Pz-K98T z)T+;jOKPzdjegr_l%jiQ^~MXo?%fV!43Y}2&)pRkG%@Gh5G>~_Vzy0`nI$_|L+uW$ z_iEO%6uBUs9##3sumP+c zB=z>(y4me3PBfU?@XZx0?*gpLYv9$r*7N2|^IDI6H^4rfq`u!_l?8%%!FRmU>Tb1) z)eaWey^~yE%vsm49Is%|(P}LOz3I3ixI-&X(ji{EBG|fA&KbH;7$9KMQE}`g6|Se zKg0EPP=l%8TC)3V9wMP{ZGUckN|Wj+{Dv8(6>U8D2<-+D|F9?*Zeh~Ri zxa(l@yXIAv9f`Y+9@!M`u_iQO_}J)B)0XA53s?Z^1*zqD@dI(SQTW^Q}sT4$Pe z4qr-2e8bY#{BQBq;8F+K`S}h^OZ~IT^O7=NWcZ&iZLK;H1hgZE`sTne=~sjTCL zYak9g7%gf_^w0e$3J`A~YJYSKCRq*^oNN^zoYLQ}@Vcjss~%1&?Fi6jA&PuKa(y^2 zXerT>G0Bt%4@h|inlpon1ZV7j-RF%ri1gm!GbzU?N}oK3ilgGpl`#V4J?70SrKN(R z>d(U7Y+vAr%JBIr@2Pf}WyO$<5v|&#50JzRXyVy);rk zaiH&Wzzkx9TB4a*ONO15Ih&*wG)s>0BuS=AbI1TMS?SYXU`uFtW z)fz95cDJt9_aNkZOLpP|T7`MjDCVG}D(x7I&Xf8~?tl23UiQRC(s29KTn?+|Y}#vr zcGok%i~y{#i-yAFU2AV*g^jQMFAS47XaB~D(ChvOhsl(fG3-3*^QWJqLa0)d(o|Qc zC9GdBk*3B*&ukWQ{7+6!c89@MlAUln)nep7c*@O0nq(QhEd1`>;FcD>F>s-vJ9ic9 zgK+|5fcZ(SQ~#^f&!|k+hIF}1m)dk%-YcmVmHQ798V&=q*27mR?`d&kRX@-NP((?$ z1WNr@&_yO^lNLBJsGx#wEZj6KM8A{3#3Ltfn7r132I?uR=H=R=8D|2Szt(2d-dyvE z5-+;?jgLVp=eX^pug;`LkJPW2FUql-uZ~!+7x$$y|EZ^bkcB|X?+Bhqt&Pm4!zhEx zW@SFDoo@f>uM^f9_ewsNBq9foLKLokJ$Tws_Fo`dI!)T&`OXG zh1HsG>(&Qt31_j8U?R65H$?PFlI#3j*YmMCR3kE!NqYg&mnoTUvh^^32}x~uNd6gJ8ea}eB<&A@m6vHOY?dZ&2tG>FV#`>&FF=T{LTG6^kdBv5 zy!Bs!n9xFbDFCU}TrbO8hN88|Ze#b&cD1>FcYKQ7+Ps4Dnj>pYj#j zFO)lGg#E868=+XE&P7rMtQleng2GIS?eIZ6V01NWfxq`%?y|Yh7fmY z1P2e&GPF$po!Bv_rl{rOF z)p#>P(s7SPr58a#lG|`2WRM=6^5*fFRYXc^IQB_Z)na#zSVo2wYOLZhnxq}?5{o~T zpbB6LC&He`1;e7-gtxXP4m;3oU&=B6~#w?Shb9*4f%I zr^8_uEqY9@yM|}^5M$sXVh5=3_k(gh+VhBx;8&q7Crb_4+RDQ1Fh3E7C|-}H+UmVw zbU!MtB)-SZ%|n<|_ZtPmkMJb|TiNG0&#$tz-l{s>VJdZnE*SDEJ~)0eN*< znVEUb4uR~nA;KLsgeBu>K0*W`3geUwJ3iyn9x3tsma%o)woD?yh6W_b2~mjPx5qc$ zf7Khu(8<;llId`YWxPIdbn#bVIem$-{B~hsU3L+p`vvyxTKPsIc8d#tuXOprZes`V zM<<3y=0i7S2j5EzdS>*c`#eib^$XTa*C)tM%-mbOUo`G#M?97xOZHSg@vUR_1_E}_D4(3j%O`^vdi6t z)JF5$yX3%5K+^Id?IEAwH#9xV9`{^u#{lwa0J`;VhljVok&po)vmH8kEhve5Im|#F zvGf(lZCWtI$m8vOr4b0jF13B{l!WC>|Ka;|!W1(yJ9aDs>bKC04hI7et#B^rp3wxmTqh!DHHSj(|?+)_l z?{@4=c@D`&GvxwQ*$DE(R;)LDef?=?|39&JBO+2hfBHn9SlvmyDpCL${)M3d-)@D~ zJ7eo@HPh$kyfMroAMza!s18O@8)(`4y-1mZ`aCq0wmwAlFYwRl85tSJ9vkE%%e3?H z@u}#Vs$$Vj11Wq2<)18k69=?b>sR;b&tZNx=;-OQi`T^-an+qa#1EIE|R>u>?G0vH(>y6fc^X06!*V03*KP%r?$ z%aL(NZNhATDLArC^A6M7;gzcRiQ1cdBPcr$KXm{YrMb7$r4VDx*aGfGMv^Ov^gxgP zKCDr%$_|^E-LsR7X-Uu9_C0k6TV+iTE1&*VVeL{ZqHHar>LE+kZmVg?uUa0~mbGx5bF z&_ERli!NZu7xSBF@Vq`U9JavTMR;dV@UcpfmJkhB_b93U?04m>6X{eVfo zz?(1Qf?F#Nj-1?di>@tXbCJ{7H;Mjo^zU$g!s7fkf?v$DYh?g(8W0lq>@Ivhn0K%N z4w-qrpl9dh<4ZJ)zx4Ye{ytgXUm`z!{32IA#=hW!UwS`ZqY9@(O3E_)y};<>tJrkX zGK4nOn#HPXXdq`>^TrWw-67p|HMkI0vft;69zhFt*mW$piOp`@-&9AR4dT2?x1%N| zt76|6WS?9?pw9&JZvhqUcZ-_o-u9W%K|BFf2x%tB;RLXW3O3H3QDl6_1te0WS2S>S!xQhRlZuo3uKRed8Nu9ac}cEN zur&xMg7ZA6NPN)~!*!Yk#~yRnYpAPZkl@92^NDh?KQZdKx%(t$8xg`|dkx6cmF+8e z?~jMH?OB~aQ=TOlOnd1C@4m&vdwk;|V2siedK?}hap<9DCWG5mtJ{zFWrd(bz%Drc z?b%31rc0~7p(Qp+#ZBjzUn0gU!e>HRb6|}p!hGVug7qmG^Xix3iggUjG;`et?>4~LoakRpI!p6(%J*ZcEQ1-9>K$w)-x zGGJ(42f_XReD|lJp_VQ;rpA$)-ENyk+Ov7AtgWBgwF=GTka_@oVkkeXwd+zr1NA9v zVMPzldCJBcmHBuaD#+i8FOs$$kqvyeAv7R}Wn6Lct{C%nm)!&`}HiRV^yN8E})q)_~acB&}XPvpO#SDW4(Z1zwuf{0}ps2A}qY@nH zkv2FQ_d7UGeOWZ9lEXUO)@9$1Re$-Nt^3+`PRSGh;M3yd`q=w9w@F?ZgaBBKk3IY*Ahv8T zqd?0%w0I+^(Xb%y93DMGb!r;b4E?Tr`-dRcwkkOm zHem0`Kp>ii9>0#^<727h+_po(zT1YOvKj@SY^TmVwN1O7;yN*y(F1=1`7Lq<#rP{) zaiSgZ8!ww_Au-e*F4s<#i1MMZ)z_)4rrO=nF^ z;(P$u%|y<3bnQl&{@$%ywkSIS_C&VfbnrHC}bqUm>5!Br9OFK8$(ZVbnNRAOG&9zPQ?ILB- z8_tKqcRGpnEi@93NJqpE>Yf(dwhZDF*Sq~Xr4dSG>k_O0^TS$N2IjC;O=IM=9*Gx0 z!H8F~-~D=f#^|=aG@mY+;OH>5nIo=Bf{fht|>5lPUDlVK*;H)kI7P!GcxF|@CN0lnm0{&zhpR%G+TIdJ*d=_Z7Ngt$udvxGYUn;3dEYm0nqD`Sv7O@6AMfHimkdS%<5XHo#sjon&yadM(;NmhWUauqkAtWTIq+&MA@>6sxmHwM1k*T@NV9qQhk}Aws$?QmG zoy!;uN@_2bOZn#i``ZI16_&iace5kRqo+_%G1y*e92=*#4HdzClp-E+GSA~w(_xhh zi>tEC$~yNwZifb8^o_|Im7xvmFRr1(p`?m{o&dU%5d(OEj758gvJn z8%la^5Y-r-vKmAgWbVhZq2i$0y=a-ed-slT&WJ~M9*g?%y9qMRexr;Fg#=7lO>Jev zNwID<64r~#KO)ViCfh2;bdlcc)L9&4%?Yil*NQC9+ z)OfD&2Ok8FAE87E#koaaqy2~|*9+Uh5ddn;3~&;Va}RoQGoDkh#DjIXjEoc_W|89s z4i`pe=E=d11K#W0HYR5MDv1Eg5v;nU6^oHXar=DN6B{tjp-?jKgUl^K(r^p)7zE80 z2rg&q%@+orF5Z>0C)+4hfv7_qKZq0UJ`&6ih0Z)q*g)nHQmksu+>%j_vbih5g#n;l zUjdv;QU4jG5`rAT$1e`Ig4`(bImu-wGv6_Pg2;%-dN)B^a~7NI(mSC2l38ttUeGmY zUq|02(qa4wt*Xus=x!{rE(p);>Dh7Ab#=A(UBRC6VGAX)V@EYq%%9f$F!TPzMl@&u z$kElUh=&?V_E7eor}^Fx?7$)$!8Z;-+Sn7@lzy>#y+W8Zu> zD?lty>VkjAu^HdTFl2aU>A+kLMSxgxNojM&rZBn5e{yniK))svUTU(fz5dJ<9(ye1 zBnI>2Dr(DHEVU8t#T68KeqKas9e|$kQ}Lg5&sl+qf0#- zHwTMvq~ow8{R6l)ERbY;x50N00&&I!J{nfxQY)dWUOac^m{EX`B9AGK`<{AKD=s4*vBGrsLz0;A5s2-GF%#LWD(?APSV9OqgYXre|NV8mu|Y}2<6_?~%OPYimAE&I5!x%I5W!cG zvB}%``7J6hue=r%92}WtgOHi{Q+np`z!~g&Q literal 0 HcmV?d00001 diff --git a/docs/plotting/index.md b/docs/plotting/index.md index c35568dc0..a552305d2 100644 --- a/docs/plotting/index.md +++ b/docs/plotting/index.md @@ -58,6 +58,12 @@ ::: +:::{grid-item-card} Marginals plot +:link: marginals-plot.ipynb +:img-bottom: ../_static/plotting/marginals-plot.png + +::: + :::: ```{toctree} @@ -73,4 +79,5 @@ super-plot scatter-plot scatter3d-plot mesh3d-plot +marginals-plot ``` diff --git a/docs/plotting/marginals-plot.ipynb b/docs/plotting/marginals-plot.ipynb new file mode 100644 index 000000000..4964f9c1b --- /dev/null +++ b/docs/plotting/marginals-plot.ipynb @@ -0,0 +1,106 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Marginals plot\n", + "\n", + "The marginals plot shows a 2-dimensional image together with the 1-dimensional lines obtained from summing over each dimension." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "import plopp as pp" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## Basic marginals plot\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "da = pp.data.data2d()\n", + "pp.marginals_plot(da)" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "## Control the side of the vertical plot\n", + "\n", + "Above, the 1D plot as a function of `y` is placed on the left and its data axis increases from right to left.\n", + "We can alternatively show it on the right. This also results in the axis increasing from left to right as is the more common case:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "pp.marginals_plot(da, side=\"right\")" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "## Extra arguments\n", + "\n", + "`marginals_plot` supports some additional arguments like `title`. It also forward any extra arguments to the underlying plot functions, in this example, `grid`. Note that the arguments are passed to *all* three subplots." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "pp.marginals_plot(da, title=\"My plot\", grid=True)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/plopp/__init__.py b/src/plopp/__init__.py index 57a53f09c..5aaa3a426 100644 --- a/src/plopp/__init__.py +++ b/src/plopp/__init__.py @@ -28,6 +28,7 @@ 'plotting': [ 'DimensionSlicer', 'inspector', + 'marginals_plot', 'mesh3d', 'plot', 'scatter', diff --git a/src/plopp/plotting/__init__.pyi b/src/plopp/plotting/__init__.pyi index 7e82c1cbb..85ad74ca7 100644 --- a/src/plopp/plotting/__init__.pyi +++ b/src/plopp/plotting/__init__.pyi @@ -2,6 +2,7 @@ # Copyright (c) 2025 Scipp contributors (https://github.com/scipp) from ._inspector import inspector +from ._marginals import marginals_plot from ._mesh3d import mesh3d from ._plot import plot from ._scatter import scatter @@ -13,6 +14,7 @@ from ._xyplot import xyplot __all__ = [ 'DimensionSlicer', 'inspector', + 'marginals_plot', 'mesh3d', 'plot', 'scatter', diff --git a/src/plopp/plotting/_marginals.py b/src/plopp/plotting/_marginals.py new file mode 100644 index 000000000..ab95d31a3 --- /dev/null +++ b/src/plopp/plotting/_marginals.py @@ -0,0 +1,97 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 Scipp contributors (https://github.com/scipp) + +from typing import Any, Literal + +import matplotlib.pyplot as plt +import scipp as sc +from mpl_toolkits.axes_grid1 import make_axes_locatable + +from ..backends.matplotlib.utils import make_figure +from ..core import Node +from ..core.typing import FigureLike +from ..graphics import imagefigure, linefigure + + +def marginals_plot( + data: sc.DataArray | Node, + *, + side: Literal["left", "right"] = "left", + title: str = "", + ax: plt.Axes | None = None, + **plot_kwargs: Any, +) -> FigureLike: + """Plot 2D data and the sums over each dim as a 1D plot. + + This function plots ``data`` as a 2D image with a colorbar. + It also sums over each of its dimensions and plots the resulting 1D data + next to and above the main image. + + .. versionadded:: 26.11.0 + + Parameters + ---------- + data: + The data to plot. Must be 2-dimensional. + side: + Which side to show the 1D plot along the y-axis. + Note that when ``side == "left"``, the x-axis of the side plot is inverted, + i.e., it increases from right to left. + title: + Title of the plot. + ax: + Optional axes to plot into. If None, ``marginals_plot`` creates new Axes. + **plot_kwargs: + Forwarded to *all* subplots :func:`plopp.graphics.linefigure`, + and :func:`plopp.graphics.imagefigure`. + + Returns + ------- + : + A figure that contains all axes. + """ + if ax is None: + ax_2d = make_figure().subplots() + else: + ax_2d = ax + + divider = make_axes_locatable(ax_2d) + ax_top = divider.append_axes("top", 1.2, pad=0.1, sharex=ax_2d) + ax_side = divider.append_axes(side, 1.2, pad=0.1, sharey=ax_2d) + cax = divider.append_axes("right", "4%", pad="5%") + + main_data_node = data if isinstance(data, Node) else Node(data) + fig = imagefigure(main_data_node, ax=ax_2d, cax=cax, cbar=True, **plot_kwargs) + + # Main plot (2D) + y_label = ax_2d.get_ylabel() + if side == "left": + ax_2d.yaxis.set_tick_params(labelleft=False) + ax_2d.set_ylabel("") + + # Top Plot + top_node = Node(lambda da: da.sum(da.dims[0]), main_data_node) + linefigure(top_node, ax=ax_top, title=title, **plot_kwargs) + ax_top.xaxis.set_tick_params(labelbottom=False) + ax_top.set_xlabel("") + + # Side plot + def make_side_data(da: sc.DataArray) -> sc.DataArray: + summed = da.sum(da.dims[1]) + return sc.DataArray( + data=summed.coords[summed.dim], + # rename to work around https://github.com/scipp/plopp/issues/616. + coords={summed.dim: summed.data}, + ).rename({summed.dim: " "}) + + side_node = Node(make_side_data, main_data_node) + linefigure(side_node, ax=ax_side, **plot_kwargs) + ax_side.set_xlabel(ax_top.get_ylabel()) + if side == "left": + ax_side.set_ylabel(y_label) + ax_side.invert_xaxis() + else: + ax_side.yaxis.set_tick_params(labelleft=False) + ax_side.set_ylabel("") + + return fig diff --git a/tests/plotting/marginals_test.py b/tests/plotting/marginals_test.py new file mode 100644 index 000000000..6a9914595 --- /dev/null +++ b/tests/plotting/marginals_test.py @@ -0,0 +1,126 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 Scipp contributors (https://github.com/scipp) + +import matplotlib.pyplot as plt +import numpy as np +import pytest +import scipp as sc + +import plopp as pp +from plopp.data.testing import data_array + +pytestmark = pytest.mark.usefixtures("_parametrize_mpl_backends") + + +def test_marginals_plot_creates_all_axes(): + da = data_array(ndim=2) + fig = pp.marginals_plot(da) + main, _, _, cax = fig.canvas.fig.axes + assert fig.canvas.ax is main + assert fig.canvas.cax is cax + + +def test_marginals_plot_from_node(): + da = data_array(ndim=2) + pp.marginals_plot(pp.Node(da)) + + +def test_marginals_plot_marginal_values(): + da = data_array(ndim=2) + fig = pp.marginals_plot(da) + _, top, side, _ = fig.canvas.fig.axes + top_line = top.lines[0] + side_line = side.lines[0] + xdim, ydim = da.dims[1], da.dims[0] + np.testing.assert_allclose(top_line.get_xdata(), da.coords[xdim].values) + np.testing.assert_allclose(top_line.get_ydata(), da.sum(ydim).values) + np.testing.assert_allclose(side_line.get_xdata(), da.sum(xdim).values) + np.testing.assert_allclose(side_line.get_ydata(), da.coords[ydim].values) + + +def test_marginals_plot_updates_when_node_changes(): + da = data_array(ndim=2) + node = pp.Node(da) + fig = pp.marginals_plot(node) + _, top, side, _ = fig.canvas.fig.axes + node.func = lambda: da * 2.0 + node.notify_children('updated') + np.testing.assert_allclose( + top.lines[0].get_ydata(), (da * 2.0).sum(da.dims[0]).values + ) + np.testing.assert_allclose( + side.lines[0].get_xdata(), (da * 2.0).sum(da.dims[1]).values + ) + + +def test_marginals_plot_side_left(): + da = data_array(ndim=2) + fig = pp.marginals_plot(da, side='left') + main, top, side, _ = fig.canvas.fig.axes + assert side.xaxis_inverted() + assert not main.xaxis_inverted() + assert main.get_ylabel() == '' + assert side.get_ylabel() == f'{da.dims[0]} [m]' + assert side.get_xlabel() == top.get_ylabel() + assert top.get_xlabel() == '' + assert main.get_xlabel() == f'{da.dims[1]} [m]' + + +def test_marginals_plot_side_right(): + da = data_array(ndim=2) + fig = pp.marginals_plot(da, side='right') + main, top, side, _ = fig.canvas.fig.axes + assert not side.xaxis_inverted() + assert main.get_ylabel() == f'{da.dims[0]} [m]' + assert side.get_ylabel() == '' + assert side.get_xlabel() == top.get_ylabel() + assert top.get_xlabel() == '' + + +def test_marginals_plot_side_axes_position(): + da = data_array(ndim=2) + fig_left = pp.marginals_plot(da, side='left') + fig_right = pp.marginals_plot(da, side='right') + fig_left.canvas.fig.draw_without_rendering() + fig_right.canvas.fig.draw_without_rendering() + main_l, _, side_l, _ = fig_left.canvas.fig.axes + main_r, _, side_r, _ = fig_right.canvas.fig.axes + assert side_l.get_position().x1 <= main_l.get_position().x0 + assert side_r.get_position().x0 >= main_r.get_position().x1 + + +def test_marginals_plot_title(): + da = data_array(ndim=2) + fig = pp.marginals_plot(da, title='My title') + _, top, _, _ = fig.canvas.fig.axes + assert top.get_title() == 'My title' + + +def test_marginals_plot_with_existing_ax(): + da = data_array(ndim=2) + f, ax = plt.subplots() + fig = pp.marginals_plot(da, ax=ax) + assert fig.canvas.ax is ax + assert len(f.axes) == 4 + + +def test_marginals_plot_forwards_kwargs(): + da = data_array(ndim=2) + fig = pp.marginals_plot(da, grid=True) + main, top, side, _ = fig.canvas.fig.axes + for ax in (main, top, side): + assert ax.xaxis.get_gridlines()[0].get_visible() + + +def test_marginals_plot_small_known_values(): + da = sc.DataArray( + data=sc.array(dims=['a', 'b'], values=np.arange(12.0).reshape(3, 4), unit='K'), + coords={ + 'a': sc.arange('a', 3.0, unit='s'), + 'b': sc.arange('b', 4.0, unit='m'), + }, + ) + fig = pp.marginals_plot(da) + _, top, side, _ = fig.canvas.fig.axes + np.testing.assert_allclose(top.lines[0].get_ydata(), [12.0, 15.0, 18.0, 21.0]) + np.testing.assert_allclose(side.lines[0].get_xdata(), [6.0, 22.0, 38.0]) From d0a89a40b279171de4dee79f3d7340a960e491b6 Mon Sep 17 00:00:00 2001 From: Jan-Lukas Wynen Date: Tue, 10 Nov 2026 17:24:20 +0100 Subject: [PATCH 2/8] Rename marginals_plot to marginals --- docs/plotting/index.md | 4 ++-- docs/plotting/{marginals-plot.ipynb => marginals.ipynb} | 8 ++++---- src/plopp/__init__.py | 2 +- src/plopp/plotting/__init__.pyi | 4 ++-- src/plopp/plotting/_marginals.py | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) rename docs/plotting/{marginals-plot.ipynb => marginals.ipynb} (84%) diff --git a/docs/plotting/index.md b/docs/plotting/index.md index a552305d2..657eebd9f 100644 --- a/docs/plotting/index.md +++ b/docs/plotting/index.md @@ -59,7 +59,7 @@ ::: :::{grid-item-card} Marginals plot -:link: marginals-plot.ipynb +:link: marginals.ipynb :img-bottom: ../_static/plotting/marginals-plot.png ::: @@ -79,5 +79,5 @@ super-plot scatter-plot scatter3d-plot mesh3d-plot -marginals-plot +marginals ``` diff --git a/docs/plotting/marginals-plot.ipynb b/docs/plotting/marginals.ipynb similarity index 84% rename from docs/plotting/marginals-plot.ipynb rename to docs/plotting/marginals.ipynb index 4964f9c1b..ad5ab1f0c 100644 --- a/docs/plotting/marginals-plot.ipynb +++ b/docs/plotting/marginals.ipynb @@ -37,7 +37,7 @@ "outputs": [], "source": [ "da = pp.data.data2d()\n", - "pp.marginals_plot(da)" + "pp.marginals(da)" ] }, { @@ -58,7 +58,7 @@ "metadata": {}, "outputs": [], "source": [ - "pp.marginals_plot(da, side=\"right\")" + "pp.marginals(da, side=\"right\")" ] }, { @@ -68,7 +68,7 @@ "source": [ "## Extra arguments\n", "\n", - "`marginals_plot` supports some additional arguments like `title`. It also forward any extra arguments to the underlying plot functions, in this example, `grid`. Note that the arguments are passed to *all* three subplots." + "`marginals` supports some additional arguments like `title`. It also forward any extra arguments to the underlying plot functions, in this example, `grid`. Note that the arguments are passed to *all* three subplots." ] }, { @@ -78,7 +78,7 @@ "metadata": {}, "outputs": [], "source": [ - "pp.marginals_plot(da, title=\"My plot\", grid=True)" + "pp.marginals(da, title=\"My plot\", grid=True)" ] } ], diff --git a/src/plopp/__init__.py b/src/plopp/__init__.py index 5aaa3a426..def5b8527 100644 --- a/src/plopp/__init__.py +++ b/src/plopp/__init__.py @@ -28,7 +28,7 @@ 'plotting': [ 'DimensionSlicer', 'inspector', - 'marginals_plot', + 'marginals', 'mesh3d', 'plot', 'scatter', diff --git a/src/plopp/plotting/__init__.pyi b/src/plopp/plotting/__init__.pyi index 85ad74ca7..2d3c9c0d2 100644 --- a/src/plopp/plotting/__init__.pyi +++ b/src/plopp/plotting/__init__.pyi @@ -2,7 +2,7 @@ # Copyright (c) 2025 Scipp contributors (https://github.com/scipp) from ._inspector import inspector -from ._marginals import marginals_plot +from ._marginals import marginals from ._mesh3d import mesh3d from ._plot import plot from ._scatter import scatter @@ -14,7 +14,7 @@ from ._xyplot import xyplot __all__ = [ 'DimensionSlicer', 'inspector', - 'marginals_plot', + 'marginals', 'mesh3d', 'plot', 'scatter', diff --git a/src/plopp/plotting/_marginals.py b/src/plopp/plotting/_marginals.py index ab95d31a3..8c650aa7c 100644 --- a/src/plopp/plotting/_marginals.py +++ b/src/plopp/plotting/_marginals.py @@ -13,7 +13,7 @@ from ..graphics import imagefigure, linefigure -def marginals_plot( +def marginals( data: sc.DataArray | Node, *, side: Literal["left", "right"] = "left", From 9060ecf38407d6c5d95be898a3ca09133bebad7d Mon Sep 17 00:00:00 2001 From: Jan-Lukas Wynen Date: Tue, 10 Nov 2026 17:24:29 +0100 Subject: [PATCH 3/8] Add marginals to top level index --- docs/index.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 75a57b64c..f4c31013f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -29,7 +29,6 @@

- ## Getting started ::::{grid} 2 @@ -120,6 +119,12 @@ ::: +:::{grid-item-card} Marginals plot +:link: plotting/marginals.ipynb +:img-bottom: _static/plotting/marginals-plot.png + +::: + :::: ## Custom figures From 7fc2473444a2caeb730f820411169d0b60e3b31f Mon Sep 17 00:00:00 2001 From: Jan-Lukas Wynen Date: Tue, 10 Nov 2026 17:29:08 +0100 Subject: [PATCH 4/8] Support cbar argument --- src/plopp/plotting/_marginals.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/plopp/plotting/_marginals.py b/src/plopp/plotting/_marginals.py index 8c650aa7c..1617cc00c 100644 --- a/src/plopp/plotting/_marginals.py +++ b/src/plopp/plotting/_marginals.py @@ -18,6 +18,7 @@ def marginals( *, side: Literal["left", "right"] = "left", title: str = "", + cbar: bool = True, ax: plt.Axes | None = None, **plot_kwargs: Any, ) -> FigureLike: @@ -39,6 +40,8 @@ def marginals( i.e., it increases from right to left. title: Title of the plot. + cbar: + Whether to use a colorbar. ax: Optional axes to plot into. If None, ``marginals_plot`` creates new Axes. **plot_kwargs: @@ -58,10 +61,10 @@ def marginals( divider = make_axes_locatable(ax_2d) ax_top = divider.append_axes("top", 1.2, pad=0.1, sharex=ax_2d) ax_side = divider.append_axes(side, 1.2, pad=0.1, sharey=ax_2d) - cax = divider.append_axes("right", "4%", pad="5%") + cax = divider.append_axes("right", "4%", pad="5%") if cbar else None main_data_node = data if isinstance(data, Node) else Node(data) - fig = imagefigure(main_data_node, ax=ax_2d, cax=cax, cbar=True, **plot_kwargs) + fig = imagefigure(main_data_node, ax=ax_2d, cax=cax, cbar=cbar, **plot_kwargs) # Main plot (2D) y_label = ax_2d.get_ylabel() From ac391600d434a1c006ad7307755fe6ec7a00fe83 Mon Sep 17 00:00:00 2001 From: Jan-Lukas Wynen Date: Wed, 11 Nov 2026 09:02:23 +0100 Subject: [PATCH 5/8] fix --- tests/plotting/marginals_test.py | 46 ++++++++++++++++---------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/tests/plotting/marginals_test.py b/tests/plotting/marginals_test.py index 6a9914595..3cc679ed5 100644 --- a/tests/plotting/marginals_test.py +++ b/tests/plotting/marginals_test.py @@ -12,22 +12,22 @@ pytestmark = pytest.mark.usefixtures("_parametrize_mpl_backends") -def test_marginals_plot_creates_all_axes(): +def test_marginals_creates_all_axes(): da = data_array(ndim=2) - fig = pp.marginals_plot(da) + fig = pp.marginals(da) main, _, _, cax = fig.canvas.fig.axes assert fig.canvas.ax is main assert fig.canvas.cax is cax -def test_marginals_plot_from_node(): +def test_marginals_from_node(): da = data_array(ndim=2) - pp.marginals_plot(pp.Node(da)) + pp.marginals(pp.Node(da)) -def test_marginals_plot_marginal_values(): +def test_marginals_marginal_values(): da = data_array(ndim=2) - fig = pp.marginals_plot(da) + fig = pp.marginals(da) _, top, side, _ = fig.canvas.fig.axes top_line = top.lines[0] side_line = side.lines[0] @@ -38,10 +38,10 @@ def test_marginals_plot_marginal_values(): np.testing.assert_allclose(side_line.get_ydata(), da.coords[ydim].values) -def test_marginals_plot_updates_when_node_changes(): +def test_marginals_updates_when_node_changes(): da = data_array(ndim=2) node = pp.Node(da) - fig = pp.marginals_plot(node) + fig = pp.marginals(node) _, top, side, _ = fig.canvas.fig.axes node.func = lambda: da * 2.0 node.notify_children('updated') @@ -53,9 +53,9 @@ def test_marginals_plot_updates_when_node_changes(): ) -def test_marginals_plot_side_left(): +def test_marginals_side_left(): da = data_array(ndim=2) - fig = pp.marginals_plot(da, side='left') + fig = pp.marginals(da, side='left') main, top, side, _ = fig.canvas.fig.axes assert side.xaxis_inverted() assert not main.xaxis_inverted() @@ -66,9 +66,9 @@ def test_marginals_plot_side_left(): assert main.get_xlabel() == f'{da.dims[1]} [m]' -def test_marginals_plot_side_right(): +def test_marginals_side_right(): da = data_array(ndim=2) - fig = pp.marginals_plot(da, side='right') + fig = pp.marginals(da, side='right') main, top, side, _ = fig.canvas.fig.axes assert not side.xaxis_inverted() assert main.get_ylabel() == f'{da.dims[0]} [m]' @@ -77,10 +77,10 @@ def test_marginals_plot_side_right(): assert top.get_xlabel() == '' -def test_marginals_plot_side_axes_position(): +def test_marginals_side_axes_position(): da = data_array(ndim=2) - fig_left = pp.marginals_plot(da, side='left') - fig_right = pp.marginals_plot(da, side='right') + fig_left = pp.marginals(da, side='left') + fig_right = pp.marginals(da, side='right') fig_left.canvas.fig.draw_without_rendering() fig_right.canvas.fig.draw_without_rendering() main_l, _, side_l, _ = fig_left.canvas.fig.axes @@ -89,30 +89,30 @@ def test_marginals_plot_side_axes_position(): assert side_r.get_position().x0 >= main_r.get_position().x1 -def test_marginals_plot_title(): +def test_marginals_title(): da = data_array(ndim=2) - fig = pp.marginals_plot(da, title='My title') + fig = pp.marginals(da, title='My title') _, top, _, _ = fig.canvas.fig.axes assert top.get_title() == 'My title' -def test_marginals_plot_with_existing_ax(): +def test_marginals_with_existing_ax(): da = data_array(ndim=2) f, ax = plt.subplots() - fig = pp.marginals_plot(da, ax=ax) + fig = pp.marginals(da, ax=ax) assert fig.canvas.ax is ax assert len(f.axes) == 4 -def test_marginals_plot_forwards_kwargs(): +def test_marginals_forwards_kwargs(): da = data_array(ndim=2) - fig = pp.marginals_plot(da, grid=True) + fig = pp.marginals(da, grid=True) main, top, side, _ = fig.canvas.fig.axes for ax in (main, top, side): assert ax.xaxis.get_gridlines()[0].get_visible() -def test_marginals_plot_small_known_values(): +def test_marginals_small_known_values(): da = sc.DataArray( data=sc.array(dims=['a', 'b'], values=np.arange(12.0).reshape(3, 4), unit='K'), coords={ @@ -120,7 +120,7 @@ def test_marginals_plot_small_known_values(): 'b': sc.arange('b', 4.0, unit='m'), }, ) - fig = pp.marginals_plot(da) + fig = pp.marginals(da) _, top, side, _ = fig.canvas.fig.axes np.testing.assert_allclose(top.lines[0].get_ydata(), [12.0, 15.0, 18.0, 21.0]) np.testing.assert_allclose(side.lines[0].get_xdata(), [6.0, 22.0, 38.0]) From eadc040d32600b2e836b4f68f6b036b27bc99352 Mon Sep 17 00:00:00 2001 From: Jan-Lukas Wynen Date: Wed, 11 Nov 2026 09:13:20 +0100 Subject: [PATCH 6/8] More explicit histogram error message --- src/plopp/plotting/_marginals.py | 4 ++++ tests/plotting/marginals_test.py | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/plopp/plotting/_marginals.py b/src/plopp/plotting/_marginals.py index 1617cc00c..4ad68f28b 100644 --- a/src/plopp/plotting/_marginals.py +++ b/src/plopp/plotting/_marginals.py @@ -80,6 +80,10 @@ def marginals( # Side plot def make_side_data(da: sc.DataArray) -> sc.DataArray: + if da.coords.is_edges(da.dims[0]): + raise sc.DimensionError( + "marginals() does not support bin edges along the y-axis." + ) summed = da.sum(da.dims[1]) return sc.DataArray( data=summed.coords[summed.dim], diff --git a/tests/plotting/marginals_test.py b/tests/plotting/marginals_test.py index 3cc679ed5..102661497 100644 --- a/tests/plotting/marginals_test.py +++ b/tests/plotting/marginals_test.py @@ -38,6 +38,27 @@ def test_marginals_marginal_values(): np.testing.assert_allclose(side_line.get_ydata(), da.coords[ydim].values) +def test_marginals_bin_edges_x(): + da = data_array(ndim=2) + da = da.hist({da.dims[1]: 10}) + fig = pp.marginals(da) + _, top, side, _ = fig.canvas.fig.axes + top_line = top.lines[0] + side_line = side.lines[0] + xdim, ydim = da.dims[1], da.dims[0] + np.testing.assert_allclose(top_line.get_xdata(), da.coords[xdim].values) + np.testing.assert_allclose(top_line.get_ydata()[1:], da.sum(ydim).values) + np.testing.assert_allclose(side_line.get_xdata(), da.sum(xdim).values) + np.testing.assert_allclose(side_line.get_ydata(), da.coords[ydim].values) + + +def test_marginals_bin_edges_y_not_supported(): + da = data_array(ndim=2) + da = da.hist({da.dims[0]: 10}).transpose(da.dims) # bin in the first dim + with pytest.raises(sc.DimensionError): + pp.marginals(da) + + def test_marginals_updates_when_node_changes(): da = data_array(ndim=2) node = pp.Node(da) From d45ddbfae647ced035907844d703b602b237f67b Mon Sep 17 00:00:00 2001 From: Jan-Lukas Wynen Date: Thu, 12 Nov 2026 08:53:59 +0100 Subject: [PATCH 7/8] Build data with arange if coord is missing --- src/plopp/plotting/_marginals.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/plopp/plotting/_marginals.py b/src/plopp/plotting/_marginals.py index 4ad68f28b..be3d877c6 100644 --- a/src/plopp/plotting/_marginals.py +++ b/src/plopp/plotting/_marginals.py @@ -85,8 +85,12 @@ def make_side_data(da: sc.DataArray) -> sc.DataArray: "marginals() does not support bin edges along the y-axis." ) summed = da.sum(da.dims[1]) + if (coord := summed.coords.get(summed.dim)) is not None: + side_data = coord + else: + side_data = sc.arange(summed.dim, summed.sizes[summed.dim], unit=None) return sc.DataArray( - data=summed.coords[summed.dim], + data=side_data, # rename to work around https://github.com/scipp/plopp/issues/616. coords={summed.dim: summed.data}, ).rename({summed.dim: " "}) From 17358a8b0215f2a8a7ab040ca0a9baa70e35fc54 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Thu, 12 Nov 2026 09:35:05 +0100 Subject: [PATCH 8/8] use preprocessor inside marginals plot --- src/plopp/plotting/_marginals.py | 32 ++++++++++++++++++++++----- src/plopp/plotting/_plot.py | 11 ++++++++- src/plopp/plotting/common.py | 38 ++++++++++++++++++-------------- tests/plotting/marginals_test.py | 11 +++++++++ 4 files changed, 68 insertions(+), 24 deletions(-) diff --git a/src/plopp/plotting/_marginals.py b/src/plopp/plotting/_marginals.py index be3d877c6..85762db59 100644 --- a/src/plopp/plotting/_marginals.py +++ b/src/plopp/plotting/_marginals.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2026 Scipp contributors (https://github.com/scipp) +from functools import partial from typing import Any, Literal import matplotlib.pyplot as plt @@ -11,6 +12,7 @@ from ..core import Node from ..core.typing import FigureLike from ..graphics import imagefigure, linefigure +from .common import input_to_nodes, preprocess def marginals( @@ -20,6 +22,9 @@ def marginals( title: str = "", cbar: bool = True, ax: plt.Axes | None = None, + ignore_size: bool = False, + ignore_sorted: bool = False, + coords=None, **plot_kwargs: Any, ) -> FigureLike: """Plot 2D data and the sums over each dim as a 1D plot. @@ -44,6 +49,12 @@ def marginals( Whether to use a colorbar. ax: Optional axes to plot into. If None, ``marginals_plot`` creates new Axes. + ignore_size: + If ``True``, skip the check that prevents the rendering of very large data. + ignore_sorted: + Do not check if the coordinates are sorted before plotting. + coords: + If supplied, use these coords instead of the input's dimension coordinates. **plot_kwargs: Forwarded to *all* subplots :func:`plopp.graphics.linefigure`, and :func:`plopp.graphics.imagefigure`. @@ -63,7 +74,20 @@ def marginals( ax_side = divider.append_axes(side, 1.2, pad=0.1, sharey=ax_2d) cax = divider.append_axes("right", "4%", pad="5%") if cbar else None - main_data_node = data if isinstance(data, Node) else Node(data) + main_data_node = input_to_nodes( + data, + processor=partial( + preprocess, + ignore_size=ignore_size, + ignore_sorted=ignore_sorted, + coords=coords, + ), + ) + + if len(main_data_node) > 1: + raise ValueError("The marginals plot accepts only a single data input.") + main_data_node = main_data_node[0] + fig = imagefigure(main_data_node, ax=ax_2d, cax=cax, cbar=cbar, **plot_kwargs) # Main plot (2D) @@ -85,12 +109,8 @@ def make_side_data(da: sc.DataArray) -> sc.DataArray: "marginals() does not support bin edges along the y-axis." ) summed = da.sum(da.dims[1]) - if (coord := summed.coords.get(summed.dim)) is not None: - side_data = coord - else: - side_data = sc.arange(summed.dim, summed.sizes[summed.dim], unit=None) return sc.DataArray( - data=side_data, + data=summed.coords[summed.dim], # rename to work around https://github.com/scipp/plopp/issues/616. coords={summed.dim: summed.data}, ).rename({summed.dim: " "}) diff --git a/src/plopp/plotting/_plot.py b/src/plopp/plotting/_plot.py index 59f4a9ecd..2a47ce6fa 100644 --- a/src/plopp/plotting/_plot.py +++ b/src/plopp/plotting/_plot.py @@ -31,6 +31,7 @@ def plot( figsize: tuple[float, float] | None = None, grid: bool = False, ignore_size: bool = False, + ignore_sorted: bool = False, legend: bool | tuple[float, float] = True, hide_log_buttons: bool = False, logc: bool | None = None, @@ -85,6 +86,8 @@ def plot( If ``True``, the interactive log buttons will be hidden. ignore_size: If ``True``, skip the check that prevents the rendering of very large data. + ignore_sorted: + Do not check if the coordinates are sorted before plotting. legend: Show legend if ``True``. If ``legend`` is a tuple, it should contain the ``(x, y)`` coordinates of the legend's anchor point in axes coordinates. @@ -170,7 +173,13 @@ def plot( ) nodes = input_to_nodes( - obj, processor=partial(preprocess, ignore_size=ignore_size, coords=coords) + obj, + processor=partial( + preprocess, + ignore_size=ignore_size, + ignore_sorted=ignore_sorted, + coords=coords, + ), ) ndims = set() diff --git a/src/plopp/plotting/common.py b/src/plopp/plotting/common.py index 60d18fea1..d226f0cf1 100644 --- a/src/plopp/plotting/common.py +++ b/src/plopp/plotting/common.py @@ -231,28 +231,29 @@ def _handle_coords_with_left_over_dimensions(da: sc.DataArray) -> sc.DataArray: ) -def _check_coord_sanity(da: sc.DataArray) -> None: +def _check_coord_sanity(da: sc.DataArray, ignore_sorted: bool = False) -> None: """ Warn if any coordinate is not sorted. This can lead to unpredictable results when plotting. Also, raise an error if any coordinate is scalar. """ for name, coord in da.coords.items(): - try: - if not ( - _all_dims_sorted(coord, order='ascending') - or _all_dims_sorted(coord, order='descending') - ): - warnings.warn( - 'The input contains a coordinate with unsorted values ' - f'({name}). The results may be unpredictable. ' - 'Coordinates can be sorted using ' - '`scipp.sort(data, key="to_be_sorted", order="ascending")`.', - RuntimeWarning, - stacklevel=2, - ) - except sc.DTypeError: - pass + if not ignore_sorted: + try: + if not ( + _all_dims_sorted(coord, order='ascending') + or _all_dims_sorted(coord, order='descending') + ): + warnings.warn( + 'The input contains a coordinate with unsorted values ' + f'({name}). The results may be unpredictable. ' + 'Coordinates can be sorted using ' + '`scipp.sort(data, key="to_be_sorted", order="ascending")`.', + RuntimeWarning, + stacklevel=2, + ) + except sc.DTypeError: + pass if not coord.dims: raise ValueError( @@ -266,6 +267,7 @@ def preprocess( obj: Plottable | list, name: str | None = None, ignore_size: bool = False, + ignore_sorted: bool = False, coords: Iterable[str] | str | None = None, ) -> sc.DataArray: """ @@ -284,6 +286,8 @@ def preprocess( Override the input's name (when a dict-like structure is to be plotted). ignore_size: Do not perform a size check on the object before plotting it. + ignore_sorted: + Do not check if the coordinates are sorted before plotting. coords: If supplied, use these coords instead of the input's dimension coordinates. """ @@ -304,7 +308,7 @@ def preprocess( out = _add_missing_dimension_coords(out) out = _drop_non_dimension_coords(out) out = _handle_coords_with_left_over_dimensions(out) - _check_coord_sanity(out) + _check_coord_sanity(out, ignore_sorted=ignore_sorted) return out diff --git a/tests/plotting/marginals_test.py b/tests/plotting/marginals_test.py index 102661497..a913875aa 100644 --- a/tests/plotting/marginals_test.py +++ b/tests/plotting/marginals_test.py @@ -145,3 +145,14 @@ def test_marginals_small_known_values(): _, top, side, _ = fig.canvas.fig.axes np.testing.assert_allclose(top.lines[0].get_ydata(), [12.0, 15.0, 18.0, 21.0]) np.testing.assert_allclose(side.lines[0].get_xdata(), [6.0, 22.0, 38.0]) + + +def test_marginals_missing_coords(): + da = data_array(ndim=2) + da.coords.clear() + pp.marginals(da) + + +def test_marginals_numpy_array(): + da = data_array(ndim=2) + pp.marginals(da.values)