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
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)
|