plot-io 0.12.0__py3-none-win_amd64.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.
Files changed (130) hide show
  1. plot_io/__init__.py +73 -0
  2. plot_io/_methods.py +1691 -0
  3. plot_io/_native/__init__.py +199 -0
  4. plot_io/_native/__main__.py +197 -0
  5. plot_io/_native/_api.py +457 -0
  6. plot_io/_native/bin/windows-x86_64.dll +0 -0
  7. plot_io/_native/kernels.py +335 -0
  8. plot_io/_native/manifest.json +49 -0
  9. plot_io/axis.py +1347 -0
  10. plot_io/boxes.py +1156 -0
  11. plot_io/canvas.py +389 -0
  12. plot_io/color/__init__.py +3 -0
  13. plot_io/color/data.py +1646 -0
  14. plot_io/color/maps.py +922 -0
  15. plot_io/color/values.py +295 -0
  16. plot_io/color/variants.py +1246 -0
  17. plot_io/colorbar.py +1426 -0
  18. plot_io/columns.py +614 -0
  19. plot_io/decorations.py +920 -0
  20. plot_io/defaults.py +809 -0
  21. plot_io/density.py +113 -0
  22. plot_io/ext.py +68 -0
  23. plot_io/facets.py +240 -0
  24. plot_io/figure.py +788 -0
  25. plot_io/formatters.py +97 -0
  26. plot_io/genome/__init__.py +17 -0
  27. plot_io/genome/adapters.py +356 -0
  28. plot_io/genome/combine.py +123 -0
  29. plot_io/genome/contacts.py +508 -0
  30. plot_io/genome/intervals.py +750 -0
  31. plot_io/genome/lanes.py +191 -0
  32. plot_io/genome/layout.py +184 -0
  33. plot_io/genome/links.py +364 -0
  34. plot_io/genome/pack.py +27 -0
  35. plot_io/genome/readers.py +145 -0
  36. plot_io/genome/regions.py +228 -0
  37. plot_io/genome/scale.py +349 -0
  38. plot_io/genome/series.py +344 -0
  39. plot_io/genome/signal.py +621 -0
  40. plot_io/genome/sites.py +782 -0
  41. plot_io/genome/sources.py +459 -0
  42. plot_io/genome/testing.py +161 -0
  43. plot_io/geometry.py +289 -0
  44. plot_io/groups.py +308 -0
  45. plot_io/guides.py +503 -0
  46. plot_io/helpers/__init__.py +17 -0
  47. plot_io/helpers/clustermap.py +202 -0
  48. plot_io/helpers/dendrogram.py +172 -0
  49. plot_io/helpers/grids.py +304 -0
  50. plot_io/helpers/qq.py +334 -0
  51. plot_io/helpers/raincloud.py +249 -0
  52. plot_io/helpers/tree.py +214 -0
  53. plot_io/helpers/volcano.py +505 -0
  54. plot_io/hints.py +125 -0
  55. plot_io/kernels.py +740 -0
  56. plot_io/labels.py +566 -0
  57. plot_io/layers/__init__.py +100 -0
  58. plot_io/layers/base.py +630 -0
  59. plot_io/layers/context.py +87 -0
  60. plot_io/layers/core.py +446 -0
  61. plot_io/layers/geoms.py +1362 -0
  62. plot_io/layers/grammar.py +586 -0
  63. plot_io/layers/paint.py +614 -0
  64. plot_io/layers/pipeline.py +170 -0
  65. plot_io/layers/positions.py +431 -0
  66. plot_io/layers/stats.py +1088 -0
  67. plot_io/layout.py +1381 -0
  68. plot_io/legend.py +605 -0
  69. plot_io/levels.py +509 -0
  70. plot_io/limits.py +124 -0
  71. plot_io/lines.py +42 -0
  72. plot_io/mapped.py +522 -0
  73. plot_io/markers.py +110 -0
  74. plot_io/markup.py +128 -0
  75. plot_io/numbers.py +354 -0
  76. plot_io/occupancy.py +295 -0
  77. plot_io/plot.py +597 -0
  78. plot_io/presets/__init__.py +51 -0
  79. plot_io/presets/_util.py +153 -0
  80. plot_io/presets/area.py +145 -0
  81. plot_io/presets/bar.py +261 -0
  82. plot_io/presets/box.py +400 -0
  83. plot_io/presets/catpoints.py +177 -0
  84. plot_io/presets/contour.py +353 -0
  85. plot_io/presets/curves.py +30 -0
  86. plot_io/presets/dots.py +355 -0
  87. plot_io/presets/ecdf.py +97 -0
  88. plot_io/presets/errorbar.py +106 -0
  89. plot_io/presets/errors.py +296 -0
  90. plot_io/presets/fill.py +74 -0
  91. plot_io/presets/heatmap.py +322 -0
  92. plot_io/presets/hexbin.py +198 -0
  93. plot_io/presets/hist.py +391 -0
  94. plot_io/presets/hist2d.py +87 -0
  95. plot_io/presets/kde.py +162 -0
  96. plot_io/presets/kde2d.py +147 -0
  97. plot_io/presets/line.py +143 -0
  98. plot_io/presets/pie.py +502 -0
  99. plot_io/presets/regression.py +115 -0
  100. plot_io/presets/scatter.py +194 -0
  101. plot_io/presets/stem.py +100 -0
  102. plot_io/presets/violin.py +323 -0
  103. plot_io/py.typed +0 -0
  104. plot_io/registry.py +82 -0
  105. plot_io/render.py +480 -0
  106. plot_io/rows.py +491 -0
  107. plot_io/scales.py +103 -0
  108. plot_io/seams.py +138 -0
  109. plot_io/settings.py +807 -0
  110. plot_io/shapes.py +66 -0
  111. plot_io/sharing.py +232 -0
  112. plot_io/sizemaps.py +97 -0
  113. plot_io/spec.py +240 -0
  114. plot_io/states.py +216 -0
  115. plot_io/styles/__init__.py +307 -0
  116. plot_io/styles/library.py +397 -0
  117. plot_io/tables.py +138 -0
  118. plot_io/text.py +412 -0
  119. plot_io/ticks.py +400 -0
  120. plot_io/times.py +287 -0
  121. plot_io/titles.py +225 -0
  122. plot_io/transform.py +114 -0
  123. plot_io/types.py +13604 -0
  124. plot_io/units.py +768 -0
  125. plot_io/validators.py +1029 -0
  126. plot_io/values.py +190 -0
  127. plot_io-0.12.0.dist-info/METADATA +215 -0
  128. plot_io-0.12.0.dist-info/RECORD +130 -0
  129. plot_io-0.12.0.dist-info/WHEEL +4 -0
  130. plot_io-0.12.0.dist-info/licenses/LICENSE +28 -0
plot_io/density.py ADDED
@@ -0,0 +1,113 @@
1
+ """Point density: a 2-D Gaussian kernel density estimate at each point of a series, as
2
+ scipy's ``gaussian_kde(np.array([x, y]))(np.array([x, y]))`` computes it.
3
+
4
+ The kernel covariance is the data covariance scaled by Scott's factor n^(-1/6) (or
5
+ Silverman's), and the density is computed on the axis positions (log axes in log space).
6
+ plot_io's C library does the work: an exact sum for small inputs, a binned and blurred grid for
7
+ large ones.
8
+ """
9
+
10
+ import math
11
+ from typing import Any
12
+
13
+ from vec_io.arrays import doubles
14
+
15
+ from . import kernels
16
+ from .settings import number, one_of
17
+ from .validators import real
18
+
19
+ __all__ = ["Density"]
20
+
21
+ _check_normalize = one_of("pdf", "count", "max")
22
+ _check_method = one_of("auto", "exact", "grid")
23
+
24
+
25
+ def _bandwidth(v: Any) -> Any:
26
+ if isinstance(v, str):
27
+ return one_of("scott", "silverman")(v)
28
+ if real(v) is None and hasattr(v, "__iter__"):
29
+ m = [list(map(float, row)) for row in v]
30
+ if len(m) != 2 or any(len(r) != 2 for r in m):
31
+ raise ValueError("a bandwidth matrix is 2 × 2")
32
+ if abs(m[0][1] - m[1][0]) > 1e-12 * max(1.0, abs(m[0][1])):
33
+ raise ValueError("a bandwidth matrix is symmetric")
34
+ if not (m[0][0] > 0 and m[1][1] > 0 and m[0][0] * m[1][1] - m[0][1] ** 2 > 0):
35
+ raise ValueError("a bandwidth matrix is positive definite")
36
+ return (m[0][0], m[0][1], m[1][1])
37
+ return number(positive=True)(v)
38
+
39
+
40
+ class Density:
41
+ """The local density of a series' points, as values for ``color``, ``size``, ``opacity``
42
+ or ``draw_order``.
43
+
44
+ ``bandwidth``: ``"scott"`` (default), ``"silverman"``, a number (isotropic standard
45
+ deviation in axis units) or a 2 × 2 covariance matrix; ``adjust`` multiplies it.
46
+ ``normalize``: ``"pdf"`` (scipy's values), ``"count"`` (points per unit area) or
47
+ ``"max"`` (0–1). ``method``: ``"exact"``, ``"grid"`` or ``"auto"`` (exact up to
48
+ 20,000 points); ``grid``: grid size of the grid method.
49
+
50
+ As scipy's ``gaussian_kde(np.array([x, y]))(np.array([x, y]))``: the kernel covariance
51
+ is the data covariance times Scott's factor n^(-1/6), on the axis positions (log axes in
52
+ log space); NaN points get NaN. ``"exact"`` sums over all points (multithreaded, equal
53
+ to scipy's); ``"grid"`` bins, blurs and reads back (about 1e-4 relative). Accepted by
54
+ ``color``, ``edge_color``, ``size`` (through the size axis), ``opacity`` (mapped to
55
+ ``opacity_range``), ``draw_order`` (``"density"``: densest last), and the marker colors
56
+ of Line, Errorbar, Stem and Regression; computed per series, cached until a setting
57
+ changes, in ``series.stats["density"]``.
58
+ """
59
+
60
+ def __init__(self, bandwidth: Any = "scott", adjust: float = 1.0, normalize: str = "pdf",
61
+ method: str = "auto", grid: int = 512) -> None:
62
+ self.bandwidth = _bandwidth(bandwidth)
63
+ self.adjust = number(positive=True)(adjust)
64
+ self.normalize = _check_normalize(normalize)
65
+ self.method = _check_method(method)
66
+ self.grid = int(number(lo=16, hi=4096, integer=True)(grid))
67
+
68
+ def __repr__(self) -> str:
69
+ return f"Density({self.bandwidth!r}, normalize={self.normalize!r})"
70
+
71
+ def label(self) -> str:
72
+ return {"pdf": "density", "count": "points per unit area",
73
+ "max": "relative density"}[self.normalize]
74
+
75
+ def compute(self, series: Any) -> Any:
76
+ """The density at each point of ``series`` (its ``density_points()``)."""
77
+ xs, ys = series.density_points()
78
+ return self.at(xs, ys)
79
+
80
+ def at(self, xs: Any, ys: Any) -> Any:
81
+ """The density at each point ``(xs[i], ys[i])`` (positions, not data)."""
82
+ xs, ys = doubles(xs), doubles(ys)
83
+ h = self.covariance(xs, ys)
84
+ if not all(math.isfinite(v) for v in h): # fewer than 2 distinct points
85
+ import array
86
+ import warnings
87
+ warnings.warn("density of fewer than 2 distinct points: drawn in one color",
88
+ stacklevel=2)
89
+ return array.array("d", (1.0 if math.isfinite(x) and math.isfinite(y)
90
+ else math.nan for x, y in zip(xs, ys)))
91
+ dens = kernels.kde2d_at(xs, ys, h, self.method, self.grid)
92
+ return self._normalized(dens, xs, ys)
93
+
94
+ def covariance(self, xs: Any, ys: Any, weights: Any = None) -> tuple[float, float, float]:
95
+ """The kernel covariance ``(h11, h12, h22)`` (``weights``: as repeated points)."""
96
+ a = self.adjust * self.adjust
97
+ if isinstance(self.bandwidth, tuple):
98
+ h11, h12, h22 = self.bandwidth
99
+ return (h11 * a, h12 * a, h22 * a)
100
+ if isinstance(self.bandwidth, float):
101
+ s = self.bandwidth * self.bandwidth * a
102
+ return (s, 0.0, s)
103
+ h11, h12, h22 = kernels.kde2d_bandwidth(xs, ys, self.bandwidth, weights)
104
+ return (h11 * a, h12 * a, h22 * a)
105
+
106
+ def _normalized(self, dens: Any, xs: Any, ys: Any) -> Any:
107
+ if self.normalize == "pdf":
108
+ return dens
109
+ if self.normalize == "count":
110
+ n = sum(1 for x, y in zip(xs, ys) if x == x and y == y)
111
+ return kernels.scaled(dens, float(n))
112
+ _, _, hi = kernels.extent(dens, None)
113
+ return kernels.scaled(dens, 1.0 / hi if hi and math.isfinite(hi) and hi > 0 else 1.0)
plot_io/ext.py ADDED
@@ -0,0 +1,68 @@
1
+ """Plot types of your own. Subject to change while plot_io is 0.x.
2
+
3
+ A type is a :class:`Series` subclass: its data arguments (``DATA``: argument → slot kind,
4
+ :data:`KINDS`), its settings (``SETTINGS``: :func:`table` of :class:`Setting`, with
5
+ ``default=``; :func:`common` for the shared ones) and its declaration (``SPEC``:
6
+ :class:`Spec` of :class:`Lay` layers drawing :class:`Table` columns with a geom of
7
+ :data:`GEOMS` and a paint role). :func:`register` adds ``plot.<name>(…)``. Paint roles and
8
+ layer options are checked at the type's first drawing. A layer's ``stat`` (a name of
9
+ :data:`STATS`; :func:`stat` registers one, with ``defaults`` for the settings it reads that a
10
+ type does not have) runs once per call: table functions ``fn(series, build)`` read its output
11
+ in ``build.stats[name]``. A layer's ``position`` (``"stack"``, ``"dodge"``…) is asked of its
12
+ tables holding ``keys`` and ``heights`` (``bottom`` and ``top`` written). Unknown stats and
13
+ positions raise. :func:`auto_colors` gives the
14
+ colors of the plot's a series took (``call.color_count`` of them, set in a spec's ``finish``);
15
+ :func:`base_colors` each set's color (``color`` given as :func:`set_colors` takes it, else
16
+ the plot's colors); ``register(…, levels="split")`` lets ``color=``, ``marker=``… take a
17
+ column of levels (one call per level, as ``group=``).
18
+ :func:`norm_quantile` and :func:`beta_quantile` are quantile functions for stats;
19
+ :func:`axis_of` and :func:`axis_scale` give the axis a data role is drawn on and its scale
20
+ (stats computed in the axis' space); :func:`group` embeds a setting group (``"Marker"``);
21
+ :func:`with_alpha` scales a color's opacity; :class:`PanelSet` (``plot_io.facets``) calls a
22
+ type on every panel of a figure with the panel's arguments. ``docs/api.md``,
23
+ "Plot types of your own", has an example.
24
+ """
25
+
26
+ from typing import Any
27
+
28
+ from .color.values import color, with_alpha
29
+ from .columns import KINDS, Columns
30
+ from .facets import PanelSet
31
+ from .layers.base import Series, common
32
+ from .layers.core import Table
33
+ from .layers.geoms import GEOMS, LAYER_OPTIONS
34
+ from .layers.grammar import Lay, Spec, axis_of
35
+ from .layers.paint import ROLES, Paint, auto_colors, base_colors, role
36
+ from .layers.stats import STATS, axis_scale, beta_quantile, compute, norm_quantile, stat
37
+ from .presets._util import set_color_value as set_colors
38
+ from .settings import Setting, table
39
+ from .validators import angle, boolean, either, length, literal, number, one_of, pair, text
40
+
41
+ __all__ = ["Series", "Spec", "Lay", "Table", "Columns", "KINDS", "GEOMS", "LAYER_OPTIONS",
42
+ "ROLES", "Paint", "role", "auto_colors", "base_colors", "set_colors", "STATS",
43
+ "stat", "compute",
44
+ "norm_quantile", "beta_quantile", "axis_of", "axis_scale", "group", "with_alpha",
45
+ "PanelSet", "Setting", "table", "common",
46
+ "color", "number", "length", "angle", "one_of", "literal", "either", "pair",
47
+ "boolean", "text", "register"]
48
+
49
+
50
+ def register(name: str, cls: Any, levels: str | None = None) -> None:
51
+ """Add ``plot.<name>(…)``, ``figure.<name>(…)`` and ``facets.<name>(…)`` drawing
52
+ ``cls`` (``plt.registry.layers.add``); ``levels``: ``"split"`` (one call per level
53
+ combination, as ``group=`` members) or ``"items"`` (each item by its levels) makes the
54
+ type's ``color``, ``marker``, ``line_style`` and ``size`` take levels."""
55
+ from .levels import adopt
56
+ from .registry import layers
57
+ if levels is not None:
58
+ if levels not in ("split", "items"):
59
+ raise ValueError(f"levels: expected 'split' or 'items', got {levels!r}")
60
+ adopt(cls, levels)
61
+ layers.add(name, cls)
62
+
63
+
64
+ def group(name: str, prefix: str = "", **options: Any) -> dict[str, Setting]:
65
+ """The settings of group ``name`` (``"Marker"``, ``"Stroke"``, ``"FillStyle"``,
66
+ ``"TextStyle"``, ``"Font"``) with ``prefix`` (``groups.<name>.embed``)."""
67
+ from . import groups
68
+ return getattr(groups, name).embed(prefix, **options)
plot_io/facets.py ADDED
@@ -0,0 +1,240 @@
1
+ """Facets: small multiples of one table, split by the values of one or two columns.
2
+
3
+ ``fig, facets = plt.facet(data, col=…, row=…)`` makes an ordinary :class:`~plot_io.Figure`
4
+ with one plot per combination of values, and a :class:`FacetSet` of those panels. A plot
5
+ type called on the set (``facets.scatter("x", "y")``) adds one layer per panel, on that
6
+ panel's rows, and returns a :class:`LayerSet`; group levels keep their colors across panels.
7
+ Axes are shared through the figure's ``share``, color and size axes through its
8
+ ``share_color`` and ``share_size`` (``"all"`` by default), as on any figure; the panels'
9
+ legend entries go into one figure legend (``share_legend="all"`` by default).
10
+ """
11
+
12
+ from typing import Any, Callable
13
+
14
+ from ._methods import PanelSetMixin
15
+ from .figure import Figure
16
+ from .tables import column, group_keys, take
17
+
18
+ __all__ = ["facet", "PanelSet", "FacetSet", "LayerSet"]
19
+
20
+ #: Decorations a facet set adds to every panel.
21
+ _DECORATIONS = ("hline", "vline", "axline", "hspan", "vspan", "text", "annotate",
22
+ "bracket", "brackets")
23
+ _SIDES = ("right", "left", "top", "bottom")
24
+
25
+
26
+ def subset(data: Any, idx: list[int]) -> Any:
27
+ """The rows ``idx`` of a table."""
28
+ if hasattr(data, "iloc"): # pandas
29
+ return data.iloc[idx]
30
+ if isinstance(data, dict):
31
+ return {k: take(v, idx) for k, v in data.items()}
32
+ if getattr(getattr(data, "dtype", None), "names", None) is not None:
33
+ return data[idx] # numpy structured array
34
+ try:
35
+ return data[idx] # polars and others
36
+ except (TypeError, KeyError, IndexError):
37
+ raise ValueError("facets need a dict, a data frame or a structured array") from None
38
+
39
+
40
+ def _levels(data: Any, name: str | None, order: Any) -> tuple[list[Any], list[list[int]]]:
41
+ if name is None:
42
+ n = len(next(iter(data.values()))) if isinstance(data, dict) else len(data)
43
+ return [None], [list(range(n))]
44
+ values, cat_order = column(data, name)
45
+ if hasattr(values, "tolist"):
46
+ values = values.tolist()
47
+ given = order.get(name) if isinstance(order, dict) else None
48
+ return group_keys(values, cat_order, given, "drop")
49
+
50
+
51
+ class LayerSet(list):
52
+ """The layers (or decorations) one facet-set call added, one per panel."""
53
+
54
+ def set(self, **settings: Any) -> "LayerSet":
55
+ """Set settings on every member."""
56
+ for s in self:
57
+ s.set(**settings)
58
+ return self
59
+
60
+ @property
61
+ def plots(self) -> list[Any]:
62
+ return [getattr(s, "plot", None) for s in self]
63
+
64
+
65
+ class PanelSet(PanelSetMixin):
66
+ """Panels of a figure: plot types and decorations called on the set are added to every
67
+ panel, with the panel's arguments (:meth:`arguments`). Base of :class:`FacetSet`
68
+ (each panel's rows of one table) and of the panel sets of ``plt.pairs`` (each panel's
69
+ variables)."""
70
+
71
+ def __init__(self, figure: Figure, data: Any, panels: dict[Any, Any],
72
+ colorbar: Any = None) -> None:
73
+ self.figure = figure
74
+ self.data = data
75
+ #: panel key → plot
76
+ self.panels = panels
77
+ self._colorbar_side = colorbar
78
+ self._group_levels: dict[str, list[Any]] = {}
79
+ #: the colorbar of the first call mapping colors (``colorbar=`` given)
80
+ self.colorbar: Any = None
81
+
82
+ def __repr__(self) -> str:
83
+ return f"{type(self).__name__}({len(self.panels)} panels)"
84
+
85
+ def __iter__(self) -> Any:
86
+ return iter(self.panels.values())
87
+
88
+ def __len__(self) -> int:
89
+ return len(self.panels)
90
+
91
+ def plot(self, key: Any) -> Any:
92
+ """The panel of key ``key``."""
93
+ return self.panels[key]
94
+
95
+ def part(self, key: Any) -> Any:
96
+ """What panel ``key`` draws (its key here; a facet's rows, a pair's variables)."""
97
+ return key
98
+
99
+ def arguments(self, key: Any, args: tuple, kw: dict[str, Any]) -> tuple[tuple, dict]:
100
+ """The arguments of a call on panel ``key``: the call's, with ``data=`` the set's
101
+ table."""
102
+ if self.data is not None:
103
+ kw = {**kw, "data": self.data}
104
+ return args, kw
105
+
106
+ def map(self, func: Callable[[Any, Any], Any]) -> list[Any]:
107
+ """``func(plot, part)`` for every panel (:meth:`part`); returns the results."""
108
+ return [func(p, self.part(k)) for k, p in self.panels.items()]
109
+
110
+ def call(self, method: str, args: tuple, kw: dict[str, Any]) -> LayerSet:
111
+ """``plot.<method>(*args, **kw)`` on every panel, with the panel's arguments."""
112
+ kw = dict(kw)
113
+ if self.data is not None:
114
+ from .levels import facet_orders
115
+ facet_orders(self.data, kw)
116
+ group = kw.get("group")
117
+ if isinstance(group, str) and self.data is not None:
118
+ levels = self._group_levels.get(group)
119
+ if levels is None:
120
+ values, cat = column(self.data, group)
121
+ if hasattr(values, "tolist"):
122
+ values = values.tolist()
123
+ levels = group_keys(values, cat, kw.get("group_order"),
124
+ kw.get("group_missing", "keep"))[0]
125
+ self._group_levels[group] = levels
126
+ kw.setdefault("group_order", levels)
127
+ colors = {lv: f"C{i}" for i, lv in enumerate(levels)}
128
+ colors.update(kw.get("group_colors") or {})
129
+ kw["group_colors"] = colors
130
+ out = LayerSet()
131
+ for k, p in self.panels.items():
132
+ a, w = self.arguments(k, args, kw)
133
+ out.append(getattr(p, method)(*a, **w))
134
+ if self._colorbar_side and self.colorbar is None and out:
135
+ self._add_colorbar(out[0])
136
+ return out
137
+
138
+ def _add_colorbar(self, first: Any) -> None:
139
+ """The colorbar of the panels' color axes, in the lanes beside them, for the first
140
+ call that maps colors."""
141
+ from .layers.paint import color_sources
142
+ members = list(first) if isinstance(first, list) else [first]
143
+ mapped = next((s for s in members if hasattr(s, "SETTINGS") and color_sources(s)),
144
+ None)
145
+ if mapped is None:
146
+ return
147
+ self.colorbar = self.figure.add_colorbar(mapped, side=self._colorbar_side)
148
+
149
+
150
+ class FacetSet(PanelSet):
151
+ """The panels of a facet figure (``plt.facet``), keyed by facet value (or ``(row
152
+ value, col value)``): plot types and decorations called on it are added to every panel
153
+ (on its rows); :meth:`map` passes each panel's rows."""
154
+
155
+ def __init__(self, figure: Figure, data: Any, panels: dict[Any, Any],
156
+ rows: dict[Any, list[int]], colorbar: Any) -> None:
157
+ super().__init__(figure, data, panels, colorbar)
158
+ self._rows = rows
159
+
160
+ def part(self, key: Any) -> Any:
161
+ """The rows of panel ``key``."""
162
+ return subset(self.data, self._rows[key])
163
+
164
+ def arguments(self, key: Any, args: tuple, kw: dict[str, Any]) -> tuple[tuple, dict]:
165
+ return args, {**kw, "data": self.part(key)}
166
+
167
+ def subsets(self) -> list[tuple[Any, Any, Any]]:
168
+ """``(key, plot, rows)`` of every panel."""
169
+ return [(k, p, self.part(k)) for k, p in self.panels.items()]
170
+
171
+
172
+ def _decoration(name: str) -> Any:
173
+ def method(self: PanelSet, *args: Any, **settings: Any) -> LayerSet:
174
+ return LayerSet(getattr(p, name)(*args, **settings) for p in self.panels.values())
175
+ method.__name__ = name
176
+ method.__qualname__ = f"PanelSet.{name}"
177
+ method.__doc__ = f"``plot.{name}(…)`` on every panel."
178
+ return method
179
+
180
+
181
+ for _name in _DECORATIONS:
182
+ setattr(PanelSet, _name, _decoration(_name))
183
+
184
+
185
+ def facet(data: Any, col: str | None = None, row: str | None = None, wrap: int | None = None,
186
+ share: Any = "grid", order: Any = None, title_format: str = "{name} = {value}",
187
+ colorbar: Any = None, **figure_settings: Any) -> tuple[Figure, FacetSet]:
188
+ """Small multiples of ``data``: a figure with one plot per value of ``col`` (and
189
+ ``row``), and the :class:`FacetSet` of its panels. ``wrap`` wraps ``col`` into rows of
190
+ that many panels; ``order``: ``{column: [values]}``; ``colorbar``: ``"right"``,
191
+ ``"left"``, ``"top"`` or ``"bottom"``, one colorbar beside the panels (in the lanes,
192
+ ``figure.add_colorbar``) for their color axes (shared: ``share_color="all"`` unless
193
+ given; size axes likewise, ``share_size="all"``, with one size key; one legend for the
194
+ panels' entries on the right: ``share_legend="all"`` unless given).
195
+
196
+ Example::
197
+
198
+ rng = random.Random(1)
199
+ df = {"x": [rng.gauss(0, 1) for _ in range(150)], "condition": ["a", "b", "c"] * 50}
200
+ df["y"] = [v + rng.gauss(0, 0.5) for v in df["x"]]
201
+ fig, panels = plt.facet(df, col="condition", width=120, height=120)
202
+ panels.scatter("x", "y")
203
+ """
204
+ if col is None and row is None:
205
+ raise ValueError("facet needs col, row or both")
206
+ if wrap is not None and (row is not None or col is None):
207
+ raise ValueError("wrap applies to col alone")
208
+ if colorbar not in (None, False) and colorbar not in _SIDES:
209
+ raise ValueError(f"colorbar is None or one of {', '.join(map(repr, _SIDES))}")
210
+ cols, col_rows = _levels(data, col, order)
211
+ rows, row_rows = _levels(data, row, order)
212
+ panels: list[tuple[Any, Any, list[int]]] = []
213
+ for rk, ri in zip(rows, row_rows):
214
+ rset = set(ri)
215
+ for ck, ci in zip(cols, col_rows):
216
+ panels.append((rk, ck, [i for i in ci if i in rset]))
217
+ if wrap:
218
+ ncol = wrap
219
+ nrow = -(-len(panels) // ncol)
220
+ else:
221
+ nrow, ncol = len(rows), len(cols)
222
+ grid: list[list[Any]] = [[None] * ncol for _ in range(nrow)]
223
+ ids = []
224
+ for k in range(len(panels)):
225
+ r, c = divmod(k, ncol)
226
+ grid[r][c] = f"panel{k}"
227
+ ids.append(f"panel{k}")
228
+ figure_settings.setdefault("share_color", "all")
229
+ figure_settings.setdefault("share_size", "all")
230
+ figure_settings.setdefault("share_legend", "all")
231
+ fig = Figure(layout=grid, share=share, **figure_settings)
232
+ by_key: dict[Any, Any] = {}
233
+ idx_of: dict[Any, list[int]] = {}
234
+ for pid, (rk, ck, idx) in zip(ids, panels):
235
+ key = ck if row is None else (rk if col is None else (rk, ck))
236
+ parts = [(n, v) for n, v in ((row, rk), (col, ck)) if n is not None]
237
+ title = ", ".join(title_format.format(name=n, value=v) for n, v in parts)
238
+ by_key[key] = fig.plot(pid, title=title)
239
+ idx_of[key] = idx
240
+ return fig, FacetSet(fig, data, by_key, idx_of, colorbar or None)