plot-io 0.12.0__py3-none-win_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plot_io/__init__.py +73 -0
- plot_io/_methods.py +1691 -0
- plot_io/_native/__init__.py +199 -0
- plot_io/_native/__main__.py +197 -0
- plot_io/_native/_api.py +457 -0
- plot_io/_native/bin/windows-arm64.dll +0 -0
- plot_io/_native/kernels.py +335 -0
- plot_io/_native/manifest.json +49 -0
- plot_io/axis.py +1347 -0
- plot_io/boxes.py +1156 -0
- plot_io/canvas.py +389 -0
- plot_io/color/__init__.py +3 -0
- plot_io/color/data.py +1646 -0
- plot_io/color/maps.py +922 -0
- plot_io/color/values.py +295 -0
- plot_io/color/variants.py +1246 -0
- plot_io/colorbar.py +1426 -0
- plot_io/columns.py +614 -0
- plot_io/decorations.py +920 -0
- plot_io/defaults.py +809 -0
- plot_io/density.py +113 -0
- plot_io/ext.py +68 -0
- plot_io/facets.py +240 -0
- plot_io/figure.py +788 -0
- plot_io/formatters.py +97 -0
- plot_io/genome/__init__.py +17 -0
- plot_io/genome/adapters.py +356 -0
- plot_io/genome/combine.py +123 -0
- plot_io/genome/contacts.py +508 -0
- plot_io/genome/intervals.py +750 -0
- plot_io/genome/lanes.py +191 -0
- plot_io/genome/layout.py +184 -0
- plot_io/genome/links.py +364 -0
- plot_io/genome/pack.py +27 -0
- plot_io/genome/readers.py +145 -0
- plot_io/genome/regions.py +228 -0
- plot_io/genome/scale.py +349 -0
- plot_io/genome/series.py +344 -0
- plot_io/genome/signal.py +621 -0
- plot_io/genome/sites.py +782 -0
- plot_io/genome/sources.py +459 -0
- plot_io/genome/testing.py +161 -0
- plot_io/geometry.py +289 -0
- plot_io/groups.py +308 -0
- plot_io/guides.py +503 -0
- plot_io/helpers/__init__.py +17 -0
- plot_io/helpers/clustermap.py +202 -0
- plot_io/helpers/dendrogram.py +172 -0
- plot_io/helpers/grids.py +304 -0
- plot_io/helpers/qq.py +334 -0
- plot_io/helpers/raincloud.py +249 -0
- plot_io/helpers/tree.py +214 -0
- plot_io/helpers/volcano.py +505 -0
- plot_io/hints.py +125 -0
- plot_io/kernels.py +740 -0
- plot_io/labels.py +566 -0
- plot_io/layers/__init__.py +100 -0
- plot_io/layers/base.py +630 -0
- plot_io/layers/context.py +87 -0
- plot_io/layers/core.py +446 -0
- plot_io/layers/geoms.py +1362 -0
- plot_io/layers/grammar.py +586 -0
- plot_io/layers/paint.py +614 -0
- plot_io/layers/pipeline.py +170 -0
- plot_io/layers/positions.py +431 -0
- plot_io/layers/stats.py +1088 -0
- plot_io/layout.py +1381 -0
- plot_io/legend.py +605 -0
- plot_io/levels.py +509 -0
- plot_io/limits.py +124 -0
- plot_io/lines.py +42 -0
- plot_io/mapped.py +522 -0
- plot_io/markers.py +110 -0
- plot_io/markup.py +128 -0
- plot_io/numbers.py +354 -0
- plot_io/occupancy.py +295 -0
- plot_io/plot.py +597 -0
- plot_io/presets/__init__.py +51 -0
- plot_io/presets/_util.py +153 -0
- plot_io/presets/area.py +145 -0
- plot_io/presets/bar.py +261 -0
- plot_io/presets/box.py +400 -0
- plot_io/presets/catpoints.py +177 -0
- plot_io/presets/contour.py +353 -0
- plot_io/presets/curves.py +30 -0
- plot_io/presets/dots.py +355 -0
- plot_io/presets/ecdf.py +97 -0
- plot_io/presets/errorbar.py +106 -0
- plot_io/presets/errors.py +296 -0
- plot_io/presets/fill.py +74 -0
- plot_io/presets/heatmap.py +322 -0
- plot_io/presets/hexbin.py +198 -0
- plot_io/presets/hist.py +391 -0
- plot_io/presets/hist2d.py +87 -0
- plot_io/presets/kde.py +162 -0
- plot_io/presets/kde2d.py +147 -0
- plot_io/presets/line.py +143 -0
- plot_io/presets/pie.py +502 -0
- plot_io/presets/regression.py +115 -0
- plot_io/presets/scatter.py +194 -0
- plot_io/presets/stem.py +100 -0
- plot_io/presets/violin.py +323 -0
- plot_io/py.typed +0 -0
- plot_io/registry.py +82 -0
- plot_io/render.py +480 -0
- plot_io/rows.py +491 -0
- plot_io/scales.py +103 -0
- plot_io/seams.py +138 -0
- plot_io/settings.py +807 -0
- plot_io/shapes.py +66 -0
- plot_io/sharing.py +232 -0
- plot_io/sizemaps.py +97 -0
- plot_io/spec.py +240 -0
- plot_io/states.py +216 -0
- plot_io/styles/__init__.py +307 -0
- plot_io/styles/library.py +397 -0
- plot_io/tables.py +138 -0
- plot_io/text.py +412 -0
- plot_io/ticks.py +400 -0
- plot_io/times.py +287 -0
- plot_io/titles.py +225 -0
- plot_io/transform.py +114 -0
- plot_io/types.py +13604 -0
- plot_io/units.py +768 -0
- plot_io/validators.py +1029 -0
- plot_io/values.py +190 -0
- plot_io-0.12.0.dist-info/METADATA +215 -0
- plot_io-0.12.0.dist-info/RECORD +130 -0
- plot_io-0.12.0.dist-info/WHEEL +4 -0
- plot_io-0.12.0.dist-info/licenses/LICENSE +28 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Clustered heatmaps: a matrix with its rows and columns in the leaf order of their
|
|
2
|
+
clustering trees, the dendrograms beside it, and annotation strips between them."""
|
|
3
|
+
|
|
4
|
+
import array
|
|
5
|
+
import math
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .. import Figure, kernels
|
|
9
|
+
from .grids import _split
|
|
10
|
+
from .tree import Tree, _frame, linkage
|
|
11
|
+
|
|
12
|
+
__all__ = ["clustermap", "ClusterMap"]
|
|
13
|
+
|
|
14
|
+
_SIDES = ("right", "left", "top", "bottom")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ClusterMap:
|
|
18
|
+
"""A clustered heatmap (``plt.clustermap``): ``heatmap`` (the series) on ``plot``,
|
|
19
|
+
``row_tree`` and ``col_tree`` (``None`` when not clustered), ``row_order`` and
|
|
20
|
+
``col_order`` (the matrix's row and column indices, top to bottom and left to right),
|
|
21
|
+
``row_dendrogram`` and ``col_dendrogram`` (series or ``None``), ``row_colors`` and
|
|
22
|
+
``col_colors`` (the strips' series or ``None``), ``colorbar`` (the heatmap's colorbar,
|
|
23
|
+
a :class:`~plot_io.colorbar.FigureColorbar` whose ``.set`` changes its label, ticks or
|
|
24
|
+
side; ``None`` with ``colorbar=None``)."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, figure: Figure) -> None:
|
|
27
|
+
self.figure = figure
|
|
28
|
+
self.heatmap: Any = None
|
|
29
|
+
self.plot: Any = None
|
|
30
|
+
self.row_tree: Tree | None = None
|
|
31
|
+
self.col_tree: Tree | None = None
|
|
32
|
+
self.row_order: list[int] = []
|
|
33
|
+
self.col_order: list[int] = []
|
|
34
|
+
self.row_dendrogram: Any = None
|
|
35
|
+
self.col_dendrogram: Any = None
|
|
36
|
+
self.row_colors: Any = None
|
|
37
|
+
self.col_colors: Any = None
|
|
38
|
+
self.colorbar: Any = None
|
|
39
|
+
|
|
40
|
+
def __repr__(self) -> str:
|
|
41
|
+
return f"ClusterMap({len(self.row_order)} × {len(self.col_order)})"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _matrix(values: Any) -> list[array.array]:
|
|
45
|
+
g = kernels.grid(values)
|
|
46
|
+
if g is not None:
|
|
47
|
+
h, w, flat = g[0], g[1], g[3]
|
|
48
|
+
return [array.array("d", flat[i * w:(i + 1) * w]) for i in range(h)]
|
|
49
|
+
rows = [kernels.doubles(r) for r in values]
|
|
50
|
+
if rows and any(len(r) != len(rows[0]) for r in rows):
|
|
51
|
+
raise ValueError("clustermap: rows of different lengths")
|
|
52
|
+
return rows
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _scaled(m: list[array.array], axis: str, how: str) -> list[array.array]:
|
|
56
|
+
"""``how="z"``: z-scores (sample standard deviation); ``"01"``: (v − min) / (max −
|
|
57
|
+
min); of each row (``axis="rows"``) or column."""
|
|
58
|
+
if axis == "cols":
|
|
59
|
+
w = len(m[0]) if m else 0
|
|
60
|
+
cols = _scaled([array.array("d", (r[j] for r in m)) for j in range(w)], "rows", how)
|
|
61
|
+
return [array.array("d", (c[i] for c in cols)) for i in range(len(m))]
|
|
62
|
+
out = []
|
|
63
|
+
for r in m:
|
|
64
|
+
v = [x for x in r if not math.isnan(x)]
|
|
65
|
+
if how == "z":
|
|
66
|
+
mean = sum(v) / len(v) if v else 0.0
|
|
67
|
+
sd = math.sqrt(sum((x - mean) ** 2 for x in v) / (len(v) - 1)) if len(v) > 1 else 0.0
|
|
68
|
+
a, b = mean, sd
|
|
69
|
+
else:
|
|
70
|
+
a = min(v) if v else 0.0
|
|
71
|
+
b = (max(v) - a) if v else 0.0
|
|
72
|
+
out.append(array.array("d", (((x - a) / b if b else math.nan) for x in r)))
|
|
73
|
+
return out
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _axis_value(name: str, v: Any) -> Any:
|
|
77
|
+
if v not in (None, "rows", "cols"):
|
|
78
|
+
raise ValueError(f"clustermap: {name} is 'rows', 'cols' or None, got {v!r}")
|
|
79
|
+
return v
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _tree(given: Any, m: list[array.array], axis: str, labels: list[str], method: str,
|
|
83
|
+
metric: str) -> Tree | None:
|
|
84
|
+
if given is False or given is None:
|
|
85
|
+
return None
|
|
86
|
+
if given is True:
|
|
87
|
+
return linkage(m, method=method, metric=metric, axis=axis, labels=labels)
|
|
88
|
+
t = given if isinstance(given, Tree) else Tree(given)
|
|
89
|
+
if t.n != len(labels):
|
|
90
|
+
raise ValueError(f"clustermap: a tree of {t.n} leaves for {len(labels)} {axis}")
|
|
91
|
+
return t
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _hidden(plot: Any) -> Any:
|
|
95
|
+
plot.x_axis(visible=False)
|
|
96
|
+
plot.y_axis(visible=False)
|
|
97
|
+
return plot
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def clustermap(matrix: Any, x: Any = None, y: Any = None, method: str = "average",
|
|
101
|
+
metric: str = "euclidean", rows: Any = True, cols: Any = True,
|
|
102
|
+
scale: Any = None, standard_scale: Any = None, row_colors: Any = None,
|
|
103
|
+
col_colors: Any = None, tree_size: Any = "15%", colorbar: Any = "right",
|
|
104
|
+
**heatmap_settings: Any) -> tuple[Figure, ClusterMap]:
|
|
105
|
+
"""A heatmap of ``matrix`` (rows first: a 2-D buffer, a sequence of rows or a data
|
|
106
|
+
frame, whose index and columns name them) with its rows and columns in the leaf order
|
|
107
|
+
of their trees (``rows``, ``cols``: ``True`` clusters them with ``method`` and
|
|
108
|
+
``metric``, as ``plt.linkage``; a :class:`Tree` or a scipy ``Z``; ``False`` keeps the
|
|
109
|
+
order), the dendrograms left and top, ``tree_size`` wide. ``x``, ``y``: the column and
|
|
110
|
+
row labels (default: the frame's, else the indices). ``scale``: z-scores of each row
|
|
111
|
+
(``"rows"``) or column (``"cols"``); ``standard_scale``: each scaled to 0…1; both before
|
|
112
|
+
clustering and drawing. ``row_colors``, ``col_colors``: an annotation strip between
|
|
113
|
+
the dendrogram and the heatmap, one value per row (column): colours drawn as given, or
|
|
114
|
+
categories as levels (one colour each, in the legend). ``colorbar``: its side, or
|
|
115
|
+
``None`` (it is ``ClusterMap.colorbar``). ``heatmap_settings`` go to the heatmap (the
|
|
116
|
+
figure's to the figure; default 480 × 480 px). Returns the figure and its :class:`ClusterMap`.
|
|
117
|
+
|
|
118
|
+
Example::
|
|
119
|
+
|
|
120
|
+
rng = random.Random(1)
|
|
121
|
+
matrix = [[rng.gauss(r % 3, 1) + c % 2 for c in range(8)] for r in range(12)]
|
|
122
|
+
fig, cm = plt.clustermap(matrix, width=260, height=240)
|
|
123
|
+
"""
|
|
124
|
+
if scale is not None and standard_scale is not None:
|
|
125
|
+
raise ValueError("clustermap: give scale or standard_scale, not both")
|
|
126
|
+
_axis_value("scale", scale)
|
|
127
|
+
_axis_value("standard_scale", standard_scale)
|
|
128
|
+
if colorbar not in (None, False) and colorbar not in _SIDES:
|
|
129
|
+
raise ValueError(f"colorbar is None or one of {', '.join(map(repr, _SIDES))}")
|
|
130
|
+
values, row_names, col_names = _frame(matrix)
|
|
131
|
+
m = _matrix(values)
|
|
132
|
+
if not m or not m[0]:
|
|
133
|
+
raise ValueError("clustermap: an empty matrix")
|
|
134
|
+
nr, nc = len(m), len(m[0])
|
|
135
|
+
ylab = [str(v) for v in (y if y is not None else row_names or range(nr))]
|
|
136
|
+
xlab = [str(v) for v in (x if x is not None else col_names or range(nc))]
|
|
137
|
+
if len(ylab) != nr or len(xlab) != nc:
|
|
138
|
+
raise ValueError(f"clustermap: {len(ylab)} row and {len(xlab)} column labels for a "
|
|
139
|
+
f"{nr} × {nc} matrix")
|
|
140
|
+
if scale is not None:
|
|
141
|
+
m = _scaled(m, scale, "z")
|
|
142
|
+
elif standard_scale is not None:
|
|
143
|
+
m = _scaled(m, standard_scale, "01")
|
|
144
|
+
fig_kw = _split("heatmap", heatmap_settings)
|
|
145
|
+
fig_kw.setdefault("reference", "figure")
|
|
146
|
+
sized = [k for k in ("width", "height") if k in fig_kw]
|
|
147
|
+
if not sized:
|
|
148
|
+
fig_kw["width"] = fig_kw["height"] = 480
|
|
149
|
+
elif len(sized) == 1:
|
|
150
|
+
fig_kw.setdefault("aspect_ratio", 1)
|
|
151
|
+
for k, v in (("share_legend", "all"), ("spacing", 2)):
|
|
152
|
+
fig_kw.setdefault(k, v)
|
|
153
|
+
rt = _tree(rows, m, "rows", ylab, method, metric)
|
|
154
|
+
ct = _tree(cols, m, "cols", xlab, method, metric)
|
|
155
|
+
left = (["rt"] if rt else []) + (["rc"] if row_colors is not None else [])
|
|
156
|
+
above = (["ct"] if ct else []) + (["cc"] if col_colors is not None else [])
|
|
157
|
+
strip = "1em"
|
|
158
|
+
widths = [tree_size if k == "rt" else strip for k in left] + [1]
|
|
159
|
+
heights = [tree_size if k == "ct" else strip for k in above] + [1]
|
|
160
|
+
grid = [[0] * len(left) + [k] for k in above] + [left + ["hm"]]
|
|
161
|
+
fig = Figure(layout=grid, widths=widths, heights=heights, **fig_kw)
|
|
162
|
+
cm = ClusterMap(fig)
|
|
163
|
+
cm.row_tree, cm.col_tree = rt, ct
|
|
164
|
+
cm.row_order = list(rt.leaves) if rt else list(range(nr))
|
|
165
|
+
cm.col_order = list(ct.leaves) if ct else list(range(nc))
|
|
166
|
+
ro, co = cm.row_order, cm.col_order
|
|
167
|
+
hm = cm.plot = fig.plot("hm")
|
|
168
|
+
hm.y_axis(side="right")
|
|
169
|
+
heatmap_settings.setdefault("origin", "top")
|
|
170
|
+
cm.heatmap = hm.heatmap([[r[j] for j in co] for r in (m[i] for i in ro)],
|
|
171
|
+
x=[xlab[j] for j in co], y=[ylab[i] for i in ro],
|
|
172
|
+
**heatmap_settings)
|
|
173
|
+
if rt:
|
|
174
|
+
p = _hidden(fig.plot("rt", share_y=hm, spines="none"))
|
|
175
|
+
cm.row_dendrogram = p.dendrogram(rt, labels=ylab, orientation="horizontal")
|
|
176
|
+
if ct:
|
|
177
|
+
p = _hidden(fig.plot("ct", share_x=hm, spines="none"))
|
|
178
|
+
cm.col_dendrogram = p.dendrogram(ct, labels=xlab)
|
|
179
|
+
bar = {"width": "100%", "fill_opacity": 1, "edge_width": 0}
|
|
180
|
+
if row_colors is not None:
|
|
181
|
+
p = _hidden(fig.plot("rc", share_y=hm, spines="none"))
|
|
182
|
+
p.x_axis(limits="0/1", padding=0)
|
|
183
|
+
cm.row_colors = p.bar([ylab[i] for i in ro], [1.0] * nr,
|
|
184
|
+
orientation="horizontal",
|
|
185
|
+
**_strip(row_colors, ro, "row_colors"), **bar)
|
|
186
|
+
if col_colors is not None:
|
|
187
|
+
p = _hidden(fig.plot("cc", share_x=hm, spines="none"))
|
|
188
|
+
p.y_axis(limits="0/1", padding=0)
|
|
189
|
+
cm.col_colors = p.bar([xlab[j] for j in co], [1.0] * nc,
|
|
190
|
+
**_strip(col_colors, co, "col_colors"), **bar)
|
|
191
|
+
if colorbar:
|
|
192
|
+
cm.colorbar = fig.add_colorbar(cm.heatmap, plots=[hm], side=colorbar)
|
|
193
|
+
return fig, cm
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _strip(values: Any, order: list[int], name: str) -> dict[str, Any]:
|
|
197
|
+
"""A strip's ``color`` in the drawn order; categories keep their order of appearance
|
|
198
|
+
in the matrix (``level_order``)."""
|
|
199
|
+
v = values.tolist() if hasattr(values, "tolist") else list(values)
|
|
200
|
+
if len(v) != len(order):
|
|
201
|
+
raise ValueError(f"clustermap: {len(v)} {name} for {len(order)}")
|
|
202
|
+
return {"color": [v[i] for i in order], "level_order": list(dict.fromkeys(v))}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Dendrograms: the merges of a :class:`~plot_io.helpers.tree.Tree` as U-shaped links."""
|
|
2
|
+
|
|
3
|
+
import array
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .. import ext
|
|
7
|
+
from .._methods import DendrogramMixin
|
|
8
|
+
from .tree import Tree
|
|
9
|
+
|
|
10
|
+
__all__ = ["Dendrogram"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Dendrogram(DendrogramMixin, ext.Series):
|
|
14
|
+
"""The merges of ``tree`` (a :class:`Tree`, or a scipy ``Z``) as U-shaped links: the
|
|
15
|
+
leaves at slots 0, 1, … in ``tree.leaves`` order on the leaf axis, the heights on the
|
|
16
|
+
value axis, the root at the top (``orientation="horizontal"``: at the left, heights
|
|
17
|
+
along x, the x axis inverted unless set). ``labels`` (default: the tree's) make the
|
|
18
|
+
leaf axis categorical, in leaf order, so a heatmap with the same categories shares it;
|
|
19
|
+
without labels each leaf is ticked with its observation's index. Every leaf is
|
|
20
|
+
labelled: turned 90° when the labels do not fit flat, then smaller, down to the axis'
|
|
21
|
+
``label_min_size`` (6 pt), and every k-th only below it.
|
|
22
|
+
|
|
23
|
+
``clusters=k`` draws the links inside each of the ``k`` flat clusters in the next
|
|
24
|
+
plot's colors, the others in ``color`` (default: the text color). Trees come from
|
|
25
|
+
:func:`linkage`, or ``plt.Tree(Z, labels=None)`` of a scipy linkage matrix.
|
|
26
|
+
|
|
27
|
+
Example::
|
|
28
|
+
|
|
29
|
+
rng = random.Random(1)
|
|
30
|
+
rows = [[rng.gauss(c, 1) for _ in range(4)] for c in (0, 0, 0, 4, 4, 8, 8, 8)]
|
|
31
|
+
p = plt.Plot(width=240, height=160)
|
|
32
|
+
p.dendrogram(plt.linkage(rows), labels=list("abcdefgh"), clusters=3)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
SETTINGS = ext.table(
|
|
36
|
+
color=ext.Setting(ext.color, doc="Line color; default: the text color."),
|
|
37
|
+
clusters=ext.Setting(ext.number(lo=1, integer=True),
|
|
38
|
+
doc="Cut into this many flat clusters (`Tree.cut`): the links "
|
|
39
|
+
"within each drawn in the plot's next color."),
|
|
40
|
+
orientation=ext.Setting(ext.one_of("vertical", "horizontal"),
|
|
41
|
+
doc="`vertical`: leaves along x, heights on y; `horizontal`: "
|
|
42
|
+
"leaves along y, heights on x."),
|
|
43
|
+
**ext.common("line_width", "name", "opacity", "z", "text_color"),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def __init__(self, tree: Any, labels: Any = None, plot: Any = None,
|
|
47
|
+
**settings: Any) -> None:
|
|
48
|
+
t = tree if isinstance(tree, Tree) else Tree(tree)
|
|
49
|
+
labels = t.labels if labels is None else list(labels)
|
|
50
|
+
if labels is not None:
|
|
51
|
+
if len(labels) != t.n:
|
|
52
|
+
raise ValueError(f"Dendrogram: {len(labels)} labels for {t.n} leaves")
|
|
53
|
+
if len(set(labels)) != len(labels):
|
|
54
|
+
raise ValueError("Dendrogram: labels must differ (they are categories)")
|
|
55
|
+
#: the tree drawn
|
|
56
|
+
self.tree = t
|
|
57
|
+
#: the names of the leaves (``None``: their indices)
|
|
58
|
+
self.labels = labels
|
|
59
|
+
super().__init__(plot=plot, **settings)
|
|
60
|
+
|
|
61
|
+
def line_color(self, ctx: Any) -> Any:
|
|
62
|
+
"""The color of the links outside the clusters."""
|
|
63
|
+
c = self.resolve("color")
|
|
64
|
+
return ctx.color(self.resolve("text_color") if c is None else c)
|
|
65
|
+
|
|
66
|
+
def cluster_colors(self, ctx: Any) -> list[Any]:
|
|
67
|
+
"""The links' colors: outside the clusters, then each cluster's."""
|
|
68
|
+
k = self._clusters()
|
|
69
|
+
return [self.line_color(ctx)] + (ext.auto_colors(self, ctx)[:k] if k else [])
|
|
70
|
+
|
|
71
|
+
def _clusters(self) -> int:
|
|
72
|
+
k = self.resolve("clusters")
|
|
73
|
+
return min(int(k), self.tree.n) if k is not None and self.tree.n > 1 else 0
|
|
74
|
+
|
|
75
|
+
def _flip(self) -> bool:
|
|
76
|
+
return self.resolve("orientation") == "horizontal"
|
|
77
|
+
|
|
78
|
+
def _groups(self) -> list[list[int]]:
|
|
79
|
+
"""The merges drawn in each color: outside the clusters, then per cluster."""
|
|
80
|
+
t, k = self.tree, self._clusters()
|
|
81
|
+
n = t.n
|
|
82
|
+
if not k:
|
|
83
|
+
return [list(range(n - 1))]
|
|
84
|
+
label = t.cut(k=k) + [0] * (n - 1)
|
|
85
|
+
out: list[list[int]] = [[] for _ in range(k + 1)]
|
|
86
|
+
for i, r in enumerate(t.merges):
|
|
87
|
+
a, b = label[int(r[0])], label[int(r[1])]
|
|
88
|
+
label[n + i] = a if a == b else 0
|
|
89
|
+
out[label[n + i]].append(i)
|
|
90
|
+
return out
|
|
91
|
+
|
|
92
|
+
def _positions(self, categories: Any) -> list[float]:
|
|
93
|
+
"""Each observation's slot: its leaf's index, or its label's on the axis."""
|
|
94
|
+
t = self.tree
|
|
95
|
+
pos = [0.0] * t.n
|
|
96
|
+
if categories is None or self.labels is None:
|
|
97
|
+
for i, leaf in enumerate(t.leaves):
|
|
98
|
+
pos[leaf] = float(i)
|
|
99
|
+
else:
|
|
100
|
+
index: dict[Any, float] = {}
|
|
101
|
+
for i, c in enumerate(categories):
|
|
102
|
+
index.setdefault(c, float(i))
|
|
103
|
+
pos = [index.get(c, float("nan")) for c in self.labels]
|
|
104
|
+
return pos
|
|
105
|
+
|
|
106
|
+
def _links(self, pos: list[float], merges: list[int]) -> dict[str, array.array]:
|
|
107
|
+
"""The three segments of each merge in ``merges``: ``x0, x1, y0, y1``."""
|
|
108
|
+
t = self.tree
|
|
109
|
+
n = t.n
|
|
110
|
+
x = pos + [0.0] * (n - 1)
|
|
111
|
+
h = [0.0] * n + [r[2] for r in t.merges]
|
|
112
|
+
for i, r in enumerate(t.merges):
|
|
113
|
+
x[n + i] = (x[int(r[0])] + x[int(r[1])]) / 2
|
|
114
|
+
out = {k: array.array("d") for k in ("x0", "x1", "y0", "y1")}
|
|
115
|
+
for i in merges:
|
|
116
|
+
a, b, top = int(t.merges[i][0]), int(t.merges[i][1]), t.merges[i][2]
|
|
117
|
+
out["x0"].extend((x[a], x[b], x[a]))
|
|
118
|
+
out["x1"].extend((x[a], x[b], x[b]))
|
|
119
|
+
out["y0"].extend((h[a], h[b], top))
|
|
120
|
+
out["y1"].extend((top, top, top))
|
|
121
|
+
return out
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _tables(s: Dendrogram, b: Any) -> list[Any] | None:
|
|
125
|
+
if s.tree.n < 2:
|
|
126
|
+
return None
|
|
127
|
+
groups = s._groups()
|
|
128
|
+
role = "y" if s._flip() else "x"
|
|
129
|
+
seen: dict[str, Any] = {}
|
|
130
|
+
|
|
131
|
+
def links(ctx: Any) -> list[dict[str, array.array]]:
|
|
132
|
+
cats = (ctx.y if role == "y" else ctx.x).categories
|
|
133
|
+
if seen.get("cats") is not cats:
|
|
134
|
+
pos = s._positions(cats)
|
|
135
|
+
seen["cats"], seen["links"] = cats, [s._links(pos, g) for g in groups]
|
|
136
|
+
return seen["links"]
|
|
137
|
+
|
|
138
|
+
def column(k: int, c: str) -> Any:
|
|
139
|
+
return lambda ctx: links(ctx)[k][c]
|
|
140
|
+
|
|
141
|
+
pos = s._positions(None)
|
|
142
|
+
out = []
|
|
143
|
+
for k, g in enumerate(groups):
|
|
144
|
+
t = ext.Table(**s._links(pos, g))
|
|
145
|
+
if s.labels is not None:
|
|
146
|
+
for c in ("x0", "x1"):
|
|
147
|
+
t.lazy[c] = column(k, c)
|
|
148
|
+
out.append(t)
|
|
149
|
+
return out
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _finish(s: Dendrogram, b: Any) -> None:
|
|
153
|
+
call, n = b.call, s.tree.n
|
|
154
|
+
leaf = "y" if s._flip() else "x"
|
|
155
|
+
call.color_count = s._clusters()
|
|
156
|
+
call.leaves.add(leaf)
|
|
157
|
+
if s._flip():
|
|
158
|
+
call.invert = {"x"}
|
|
159
|
+
if s.labels is not None:
|
|
160
|
+
call.cats[leaf] = [s.labels[i] for i in s.tree.leaves]
|
|
161
|
+
return
|
|
162
|
+
call.extra[leaf] = array.array("d", [-0.5, n - 0.5])
|
|
163
|
+
call.exact.add(leaf)
|
|
164
|
+
call.ticks[leaf] = [(float(i), str(o)) for i, o in enumerate(s.tree.leaves)]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
Dendrogram.SPEC = ext.Spec(
|
|
168
|
+
[ext.Lay("segments", ("stroke", {"color": "cluster_colors()", "opacity": True}),
|
|
169
|
+
table="links", per_set=True, key="first")],
|
|
170
|
+
tables={"links": _tables}, key={"stroke": "line_color()"}, zero=("y",), finish=_finish)
|
|
171
|
+
|
|
172
|
+
ext.register("dendrogram", Dendrogram)
|
plot_io/helpers/grids.py
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Joint plots and pair grids: figures of plots sharing axes, made by one call.
|
|
2
|
+
|
|
3
|
+
``fig, joint = plt.joint("a", "b", data=df)``: a main plot with marginal distributions on
|
|
4
|
+
top and on the right. ``fig, pairs = plt.pairs(df)``: every pair of variables, with each
|
|
5
|
+
variable's distribution on the diagonal. Both own their figure (as ``plt.facet``); level
|
|
6
|
+
aesthetics (``color="group"``) take one colour per level in every panel and one legend.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numbers
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .. import Figure, ext
|
|
13
|
+
|
|
14
|
+
__all__ = ["joint", "pairs", "Joint", "Pairs"]
|
|
15
|
+
|
|
16
|
+
_MAIN = ("scatter", "hexbin", "hist2d", "kde2d", "regression")
|
|
17
|
+
#: kinds drawn by another type's method (``kde`` of x and y is the 2-D density)
|
|
18
|
+
_METHOD = {"kde2d": "kde"}
|
|
19
|
+
_MARGINAL = ("hist", "kde", "box", "violin", "strip")
|
|
20
|
+
_DIAGONAL = ("hist", "kde")
|
|
21
|
+
#: the call keywords marginal and diagonal calls take from the main call
|
|
22
|
+
_SHARED = ("color", "color_map", "level_order", "level_missing")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _is_number(v: Any) -> bool:
|
|
26
|
+
return isinstance(v, numbers.Real) and not isinstance(v, bool)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _column(data: Any, name: str) -> Any:
|
|
30
|
+
try:
|
|
31
|
+
return data[name]
|
|
32
|
+
except (KeyError, IndexError, TypeError, ValueError):
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _numeric(values: Any) -> bool:
|
|
37
|
+
"""Whether a column holds numbers (not text, booleans or categories)."""
|
|
38
|
+
dtype = getattr(values, "dtype", None)
|
|
39
|
+
if dtype is not None:
|
|
40
|
+
if callable(getattr(dtype, "is_numeric", None)): # polars
|
|
41
|
+
return bool(dtype.is_numeric())
|
|
42
|
+
kind = getattr(dtype, "kind", None)
|
|
43
|
+
if kind is not None:
|
|
44
|
+
return kind in "iuf"
|
|
45
|
+
first = next((v for v in values if v is not None), None)
|
|
46
|
+
return _is_number(first)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _color_kind(color: Any, data: Any) -> str | None:
|
|
50
|
+
"""``"levels"`` (a column of categories), ``"plain"`` (one colour), ``"numbers"`` (a
|
|
51
|
+
column of numbers through a color map) or ``None``."""
|
|
52
|
+
if color is None:
|
|
53
|
+
return None
|
|
54
|
+
if isinstance(color, str):
|
|
55
|
+
values = _column(data, color) if data is not None else None
|
|
56
|
+
if values is None:
|
|
57
|
+
return "plain"
|
|
58
|
+
return "numbers" if _numeric(values) else "levels"
|
|
59
|
+
if isinstance(color, tuple) and len(color) in (3, 4) and all(map(_is_number, color)):
|
|
60
|
+
return "plain"
|
|
61
|
+
return "numbers" if _numeric(color) else "levels"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _split(kind: str, settings: dict[str, Any]) -> dict[str, Any]:
|
|
65
|
+
"""Take the figure's settings out of ``settings`` (those the plot type lacks)."""
|
|
66
|
+
from .. import registry
|
|
67
|
+
own = registry.layers.get(_METHOD.get(kind, kind)).SETTINGS
|
|
68
|
+
return {k: settings.pop(k) for k in list(settings)
|
|
69
|
+
if k not in own and k in Figure.SETTINGS}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _shared(settings: dict[str, Any], data: Any) -> tuple[dict[str, Any], bool]:
|
|
73
|
+
"""The keywords the distributions take from the main call, and whether ``color`` maps
|
|
74
|
+
levels."""
|
|
75
|
+
kind = _color_kind(settings.get("color"), data)
|
|
76
|
+
out = {k: settings[k] for k in _SHARED if settings.get(k) is not None}
|
|
77
|
+
if kind == "numbers":
|
|
78
|
+
out.pop("color", None)
|
|
79
|
+
if kind != "levels":
|
|
80
|
+
out.pop("color_map", None)
|
|
81
|
+
return out, kind == "levels"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _check(name: str, value: Any, allowed: tuple[str, ...]) -> Any:
|
|
85
|
+
if value is not None and value != "auto" and value not in allowed:
|
|
86
|
+
raise ValueError(f"{name} is one of {', '.join(map(repr, allowed))}, 'auto' or None, "
|
|
87
|
+
f"got {value!r}")
|
|
88
|
+
return value
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _figure(grid: list[list[Any]], fig_kw: dict[str, Any]) -> Figure:
|
|
92
|
+
for k, v in (("share_color", "all"), ("share_size", "all"), ("share_legend", "all"),
|
|
93
|
+
("label_outer", True)):
|
|
94
|
+
fig_kw.setdefault(k, v)
|
|
95
|
+
return Figure(layout=grid, **fig_kw)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class Joint:
|
|
99
|
+
"""A joint plot (``plt.joint``): ``main`` and the marginal plots ``top`` (sharing x)
|
|
100
|
+
and ``right`` (sharing y); ``series`` (the main call's), ``marginals`` (the marginal
|
|
101
|
+
calls')."""
|
|
102
|
+
|
|
103
|
+
def __init__(self, figure: Figure, x: Any, y: Any, data: Any) -> None:
|
|
104
|
+
self.figure = figure
|
|
105
|
+
self.x, self.y, self.data = x, y, data
|
|
106
|
+
self.main = figure.plot("main")
|
|
107
|
+
self.top = figure.plot("top", share_x=self.main, legend=False, spines="bottom")
|
|
108
|
+
self.right = figure.plot("right", share_y=self.main, legend=False, spines="left")
|
|
109
|
+
self.top.y_axis(visible=False)
|
|
110
|
+
self.right.x_axis(visible=False)
|
|
111
|
+
self.series: Any = None
|
|
112
|
+
self.marginals: list[Any] = []
|
|
113
|
+
|
|
114
|
+
def __repr__(self) -> str:
|
|
115
|
+
return f"Joint({len(self.marginals)} marginal calls)"
|
|
116
|
+
|
|
117
|
+
def marginals_call(self, method: str, **settings: Any) -> list[Any]:
|
|
118
|
+
"""``top.<method>(x, …)`` and ``right.<method>(y, …)`` (along y: box, violin and
|
|
119
|
+
strip vertical, the others ``orientation="horizontal"``); returns both series."""
|
|
120
|
+
if self.data is not None:
|
|
121
|
+
settings.setdefault("data", self.data)
|
|
122
|
+
across = method in ("box", "violin", "strip")
|
|
123
|
+
out = []
|
|
124
|
+
for plot, values, flip in ((self.top, self.x, across), (self.right, self.y, not across)):
|
|
125
|
+
kw = dict(settings)
|
|
126
|
+
if flip:
|
|
127
|
+
kw.setdefault("orientation", "horizontal")
|
|
128
|
+
out.append(getattr(plot, method)(values, **kw))
|
|
129
|
+
self.marginals += out
|
|
130
|
+
return out
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def joint(x: Any, y: Any, data: Any = None, kind: str = "scatter", marginal: Any = "auto",
|
|
134
|
+
ratio: float = 5, gap: Any = "0.5em", **settings: Any) -> tuple[Figure, Joint]:
|
|
135
|
+
"""A joint plot: ``kind`` (``scatter``, ``hexbin``, ``hist2d``, ``kde2d``,
|
|
136
|
+
``regression``) of ``x`` and ``y`` in the main plot, ``marginal`` distributions
|
|
137
|
+
(``hist``, ``kde``, ``box``, ``violin``, ``strip``, ``None``; ``"auto"``: ``kde`` when
|
|
138
|
+
``color`` maps levels, else ``hist``) above it and on its right, ``ratio`` times
|
|
139
|
+
narrower, ``gap`` apart. ``settings`` go to the main call (the figure's to the
|
|
140
|
+
figure); ``color``, ``level_order``, ``level_missing`` and (with levels) ``color_map`` to the
|
|
141
|
+
marginals too. ``width`` and ``height`` measure the whole figure (``reference="figure"``).
|
|
142
|
+
Returns the figure and its :class:`Joint`.
|
|
143
|
+
|
|
144
|
+
Example::
|
|
145
|
+
|
|
146
|
+
rng = random.Random(1)
|
|
147
|
+
x = [rng.gauss(0, 1) for _ in range(200)]
|
|
148
|
+
fig, joint = plt.joint(x, [v + rng.gauss(0, 0.6) for v in x], width=240, height=240)
|
|
149
|
+
"""
|
|
150
|
+
if kind not in _MAIN:
|
|
151
|
+
raise ValueError(f"kind is one of {', '.join(map(repr, _MAIN))}, got {kind!r}")
|
|
152
|
+
_check("marginal", marginal, _MARGINAL)
|
|
153
|
+
if not _is_number(ratio) or ratio <= 0:
|
|
154
|
+
raise ValueError(f"ratio is a positive number, got {ratio!r}")
|
|
155
|
+
fig_kw = _split(kind, settings)
|
|
156
|
+
fig_kw.setdefault("reference", "figure")
|
|
157
|
+
fig_kw.setdefault("spacing", gap)
|
|
158
|
+
fig_kw.setdefault("widths", [ratio, 1])
|
|
159
|
+
fig_kw.setdefault("heights", [1, ratio])
|
|
160
|
+
fig = _figure([["top", 0], ["main", "right"]], fig_kw)
|
|
161
|
+
j = Joint(fig, x, y, data)
|
|
162
|
+
shared, levels = _shared(settings, data)
|
|
163
|
+
if data is not None:
|
|
164
|
+
settings["data"] = data
|
|
165
|
+
j.series = getattr(j.main, _METHOD.get(kind, kind))(x, y, **settings)
|
|
166
|
+
if marginal == "auto":
|
|
167
|
+
marginal = "kde" if levels else "hist"
|
|
168
|
+
if marginal is not None:
|
|
169
|
+
j.marginals_call(marginal, **shared)
|
|
170
|
+
return fig, j
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class _Cells(ext.PanelSet):
|
|
174
|
+
"""Cells of a pair grid: a call draws each cell's variables (``(x,)`` on the diagonal,
|
|
175
|
+
``(x, y)`` elsewhere) before its own arguments."""
|
|
176
|
+
|
|
177
|
+
def __init__(self, figure: Figure, data: Any, panels: dict[Any, Any],
|
|
178
|
+
names: list[str]) -> None:
|
|
179
|
+
super().__init__(figure, data, panels)
|
|
180
|
+
self._names = names
|
|
181
|
+
|
|
182
|
+
def part(self, key: Any) -> tuple[str, ...]:
|
|
183
|
+
"""The variables of cell ``key`` (row, column): ``(x,)`` or ``(x, y)``."""
|
|
184
|
+
i, j = key
|
|
185
|
+
return (self._names[j],) if i == j else (self._names[j], self._names[i])
|
|
186
|
+
|
|
187
|
+
def arguments(self, key: Any, args: tuple, kw: dict[str, Any]) -> tuple[tuple, dict]:
|
|
188
|
+
return self.part(key) + tuple(args), {**kw, "data": self.data}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _cell_method(name: str) -> Any:
|
|
192
|
+
def method(self: _Cells, *args: Any, **settings: Any) -> Any:
|
|
193
|
+
return self.call(name, args, settings)
|
|
194
|
+
method.__name__ = name
|
|
195
|
+
method.__qualname__ = f"_Cells.{name}"
|
|
196
|
+
method.__doc__ = f"``plot.{name}(<the cell's variables>, …)`` on every cell."
|
|
197
|
+
return method
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _cell_methods() -> None:
|
|
201
|
+
"""The plot types on cells take the data arguments from the cell (the typed methods
|
|
202
|
+
of a panel set require them)."""
|
|
203
|
+
from .. import registry
|
|
204
|
+
for name in registry.layers.names():
|
|
205
|
+
setattr(_Cells, name, _cell_method(name))
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class Pairs:
|
|
209
|
+
"""A pair grid (``plt.pairs``): ``vars``, and the :class:`~plot_io.facets.PanelSet`\\ s
|
|
210
|
+
``lower``, ``upper`` (empty with ``corner``), ``all`` (both) and ``diagonal`` (each
|
|
211
|
+
diagonal cell's hidden ``twin_y``); keys ``(row, column)``. ``plot(i, j)``: a cell."""
|
|
212
|
+
|
|
213
|
+
def __init__(self, figure: Figure, data: Any, names: list[str], corner: bool) -> None:
|
|
214
|
+
self.figure = figure
|
|
215
|
+
self.vars = names
|
|
216
|
+
n = len(names)
|
|
217
|
+
plots: dict[tuple[int, int], Any] = {}
|
|
218
|
+
diag: dict[tuple[int, int], Any] = {}
|
|
219
|
+
for i in range(n):
|
|
220
|
+
for j in range(i + 1 if corner else n):
|
|
221
|
+
p = figure.plot(f"{i},{j}", legend=False if i == j else None)
|
|
222
|
+
p.x_axis(label=names[j])
|
|
223
|
+
p.y_axis(label=names[i])
|
|
224
|
+
plots[i, j] = p
|
|
225
|
+
if i == j:
|
|
226
|
+
t = p.twin_y(legend=False, spines="none")
|
|
227
|
+
t.y_axis(visible=False)
|
|
228
|
+
diag[i, j] = t
|
|
229
|
+
if corner:
|
|
230
|
+
plots[0, 0].y_axis(visible=False)
|
|
231
|
+
self._plots = plots
|
|
232
|
+
lower = {k: p for k, p in plots.items() if k[0] > k[1]}
|
|
233
|
+
upper = {k: p for k, p in plots.items() if k[0] < k[1]}
|
|
234
|
+
self.lower = _Cells(figure, data, lower, names)
|
|
235
|
+
self.upper = _Cells(figure, data, upper, names)
|
|
236
|
+
self.all = _Cells(figure, data, {k: p for k, p in plots.items() if k[0] != k[1]},
|
|
237
|
+
names)
|
|
238
|
+
self.diagonal = _Cells(figure, data, diag, names)
|
|
239
|
+
|
|
240
|
+
def __repr__(self) -> str:
|
|
241
|
+
return f"Pairs({self.vars!r})"
|
|
242
|
+
|
|
243
|
+
def plot(self, i: int, j: int) -> Any:
|
|
244
|
+
"""The cell of row ``i`` (y: ``vars[i]``) and column ``j`` (x: ``vars[j]``)."""
|
|
245
|
+
return self._plots[i, j]
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _names(data: Any) -> list[str]:
|
|
249
|
+
if isinstance(data, dict):
|
|
250
|
+
return list(data)
|
|
251
|
+
cols = getattr(data, "columns", None)
|
|
252
|
+
if cols is not None:
|
|
253
|
+
return [str(c) for c in cols]
|
|
254
|
+
names = getattr(getattr(data, "dtype", None), "names", None)
|
|
255
|
+
if names:
|
|
256
|
+
return list(names)
|
|
257
|
+
raise ValueError("pairs needs a dict, a data frame or a structured array")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def pairs(data: Any, vars: Any = None, kind: str = "scatter", diagonal: Any = "auto",
|
|
261
|
+
corner: bool = False, **settings: Any) -> tuple[Figure, Pairs]:
|
|
262
|
+
"""Every pair of the columns ``vars`` of ``data`` (default: its numeric columns but
|
|
263
|
+
``color``'s): an n × n grid, row i plotting ``vars[i]`` on y, column j ``vars[j]`` on
|
|
264
|
+
x, x shared per column and y per row; ``kind`` (``scatter``, ``hexbin``, ``hist2d``,
|
|
265
|
+
``kde2d``, ``regression``) off the diagonal, ``diagonal`` (``hist``, ``kde``, ``None``;
|
|
266
|
+
``"auto"``: ``kde`` when ``color`` maps levels, else ``hist``) on it, on a hidden
|
|
267
|
+
``twin_y`` of each diagonal cell. ``corner=True``: no upper triangle. ``settings`` go
|
|
268
|
+
to the off-diagonal calls (the figure's to the figure); ``color``,
|
|
269
|
+
``level_order``, ``level_missing`` and (with levels) ``color_map`` to the diagonal too. Returns the figure and its
|
|
270
|
+
:class:`Pairs`.
|
|
271
|
+
|
|
272
|
+
Example::
|
|
273
|
+
|
|
274
|
+
rng = random.Random(1)
|
|
275
|
+
a = [rng.gauss(0, 1) for _ in range(80)]
|
|
276
|
+
data = {"a": a, "b": [v + rng.gauss(0, 0.5) for v in a], "c": [rng.gauss(0, 1) for _ in a]}
|
|
277
|
+
fig, grid = plt.pairs(data, width=90, height=90)
|
|
278
|
+
"""
|
|
279
|
+
if kind not in _MAIN:
|
|
280
|
+
raise ValueError(f"kind is one of {', '.join(map(repr, _MAIN))}, got {kind!r}")
|
|
281
|
+
_check("diagonal", diagonal, _DIAGONAL)
|
|
282
|
+
color = settings.get("color")
|
|
283
|
+
if vars is None:
|
|
284
|
+
vars = [c for c in _names(data) if c != color and _numeric(_column(data, c))]
|
|
285
|
+
names = [str(v) for v in vars]
|
|
286
|
+
if not names:
|
|
287
|
+
raise ValueError("pairs: no variables (vars=, or numeric columns in data)")
|
|
288
|
+
fig_kw = _split(kind, settings)
|
|
289
|
+
n = len(names)
|
|
290
|
+
grid = [[f"{i},{j}" if j <= i or not corner else 0 for j in range(n)] for i in range(n)]
|
|
291
|
+
fig_kw.setdefault("share", "grid")
|
|
292
|
+
fig = _figure(grid, fig_kw)
|
|
293
|
+
p = Pairs(fig, data, names, corner)
|
|
294
|
+
shared, levels = _shared(settings, data)
|
|
295
|
+
if n > 1:
|
|
296
|
+
(p.lower if corner else p.all).call(_METHOD.get(kind, kind), (), settings)
|
|
297
|
+
if diagonal == "auto":
|
|
298
|
+
diagonal = "kde" if levels else "hist"
|
|
299
|
+
if diagonal is not None:
|
|
300
|
+
p.diagonal.call(diagonal, (), shared)
|
|
301
|
+
return fig, p
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
_cell_methods()
|