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.
- 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-x86_64.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/presets/_util.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""What the presets share: per-category colors, legend entries, axis titles."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
from ..color.values import desaturate
|
|
6
|
+
from ..layers.core import Call, Entry
|
|
7
|
+
from ..layers.paint import faded, memo
|
|
8
|
+
from ..validators import typed
|
|
9
|
+
|
|
10
|
+
__all__ = ["keyed", "columns_meta", "set_color_value", "fill_colors", "faded",
|
|
11
|
+
"per_category", "memo", "placed_text", "gradient_entry", "map_or_color",
|
|
12
|
+
"given_colors"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def keyed(call: Call, keys: dict[str, Any], flip: bool, tables: list[Any]) -> dict[str, Any]:
|
|
16
|
+
"""Positions given as categories (``Columns.keys``): the categories of
|
|
17
|
+
the plot's axes, and the columns of ``tables`` placed at them once the axes are known
|
|
18
|
+
(``flip``: the data's x on the plot's y). Returns the column functions by data
|
|
19
|
+
argument."""
|
|
20
|
+
out = {}
|
|
21
|
+
for name, k in keys.items():
|
|
22
|
+
role = name if not flip else ("y" if name == "x" else "x")
|
|
23
|
+
call.cats[role] = list(k.levels)
|
|
24
|
+
|
|
25
|
+
def column(ctx: Any, k: Any = k, role: str = role) -> Any:
|
|
26
|
+
return k.on((ctx.x if role == "x" else ctx.y).categories)
|
|
27
|
+
|
|
28
|
+
out[name] = column
|
|
29
|
+
for t in tables:
|
|
30
|
+
if name in t:
|
|
31
|
+
t.lazy[name] = column
|
|
32
|
+
return out
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def columns_meta(call: Call, roles: dict[str, str]) -> None:
|
|
36
|
+
"""Axis titles from data column names and date axes from date data: ``roles`` maps
|
|
37
|
+
data arguments to the plot axis they are drawn on (``x`` and ``y`` map to
|
|
38
|
+
themselves unless given)."""
|
|
39
|
+
s = call.series
|
|
40
|
+
|
|
41
|
+
def role_of(param: str) -> str | None:
|
|
42
|
+
if param in roles:
|
|
43
|
+
return roles[param]
|
|
44
|
+
return param if param in ("x", "y") else None
|
|
45
|
+
|
|
46
|
+
for param, name in s.cols.titles.items():
|
|
47
|
+
role = role_of(param)
|
|
48
|
+
if role is not None:
|
|
49
|
+
call.titles.setdefault(role, name)
|
|
50
|
+
for param, tz in s.cols.times.items():
|
|
51
|
+
role = role_of(param)
|
|
52
|
+
if role is not None and role not in call.times:
|
|
53
|
+
call.times[role] = (True, tz)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@typed("ColorLike | Sequence[ColorLike] | Mapping[Any, ColorLike]")
|
|
57
|
+
def set_color_value(v: Any) -> Any:
|
|
58
|
+
"""``color`` of a series of value sets: a color, one per set (a list), or a dict
|
|
59
|
+
``{set name: color}``."""
|
|
60
|
+
from ..color.values import color
|
|
61
|
+
from ..layers.paint import colors_only
|
|
62
|
+
if isinstance(v, dict):
|
|
63
|
+
return {k: color(c) for k, c in v.items()}
|
|
64
|
+
return colors_only(v)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def fill_colors(s: Any, ctx: Any, cols: list[Any]) -> list[Any]:
|
|
68
|
+
"""The fills of items of colors ``cols`` (before saturation): ``fill``'s color in place
|
|
69
|
+
of each, ``saturation`` when the series has it, at ``fill_opacity`` times
|
|
70
|
+
``opacity``."""
|
|
71
|
+
from .. import groups
|
|
72
|
+
fc, fa = groups.fill_parts(s)
|
|
73
|
+
if fc is not None:
|
|
74
|
+
cols = [ctx.color(fc)] * len(cols)
|
|
75
|
+
sat = s.resolve("saturation") if "saturation" in s.SETTINGS else 1.0
|
|
76
|
+
if sat != 1.0:
|
|
77
|
+
cols = [desaturate(x, sat) if x is not None else None for x in cols]
|
|
78
|
+
opacity = s.resolve("opacity")
|
|
79
|
+
return faded(cols, (1.0 if opacity is None else opacity) * fa)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def per_category(s: Any, cats: list[Any], v: Any, what: str) -> list[Any]:
|
|
83
|
+
"""Values per category of ``color``: a dict ``{category: value}`` or a list."""
|
|
84
|
+
if isinstance(v, dict):
|
|
85
|
+
return [v.get(k, float("nan")) for k in cats]
|
|
86
|
+
if len(v) != len(cats):
|
|
87
|
+
raise ValueError(f"{s.kind} setting 'color': {len(v)} values for {len(cats)} {what}")
|
|
88
|
+
return list(v)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def placed_text(series: Any, key: Any, items: Callable[[Any], list[tuple[Any, ...]]],
|
|
92
|
+
font: Callable[[Any], Any], overlay: bool = True, **extra: Any) -> Any:
|
|
93
|
+
"""A ``Text`` layer of labels placed once the axes are known: ``items(ctx)`` gives
|
|
94
|
+
``(text, x, y, color, ha, va)`` in page units, optionally followed by ``angle``,
|
|
95
|
+
``box`` and ``leader`` (see :class:`~plot_io.layers.geoms.Text`); its paint is the
|
|
96
|
+
``text`` role (``font(ctx)`` and ``extra``)."""
|
|
97
|
+
from ..layers.core import Layer, Table
|
|
98
|
+
from ..layers.geoms import GEOMS
|
|
99
|
+
from ..layers.paint import Paint
|
|
100
|
+
names = ("text", "page_x", "page_y", "color", "ha", "va", "angle", "boxes", "leaders")
|
|
101
|
+
t = Table()
|
|
102
|
+
|
|
103
|
+
def column(k: int) -> Callable[[Any], Any]:
|
|
104
|
+
def get(ctx: Any) -> Any:
|
|
105
|
+
rows = memo(ctx, (key, "text"), lambda: items(ctx))
|
|
106
|
+
if all(len(r) <= k for r in rows):
|
|
107
|
+
return None
|
|
108
|
+
return [r[k] if len(r) > k else None for r in rows]
|
|
109
|
+
return get
|
|
110
|
+
|
|
111
|
+
for k, name in enumerate(names):
|
|
112
|
+
t.lazy[name] = column(k)
|
|
113
|
+
return Layer(GEOMS["text"], t, paint=Paint(series, "text", font=font, **extra),
|
|
114
|
+
overlay=overlay, occupy=False, key=False, stat="identity",
|
|
115
|
+
position="identity")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def gradient_entry(series: Any, name: Any, cmap: Callable[[], Any]) -> Any:
|
|
119
|
+
"""A legend entry whose symbol is the color map ``cmap()`` as a left-to-right gradient
|
|
120
|
+
(no symbol when it gives ``None``)."""
|
|
121
|
+
from ..layers.paint import Paint
|
|
122
|
+
return Entry(name, (("gradient", ()),), (Paint(series, "gradient", cmap=cmap),),
|
|
123
|
+
Paint(series, "key"))
|
|
124
|
+
|
|
125
|
+
@typed("ColorMap | str | Sequence[ColorLike] | ArrayLike | Callable[..., Any] | "
|
|
126
|
+
"Mapping[Any, ColorLike]")
|
|
127
|
+
def map_or_color(v: Any) -> Any:
|
|
128
|
+
"""The ``color_map`` of a pie or contour: a ``color_map`` value, or one color."""
|
|
129
|
+
from ..color.maps import check_color_map
|
|
130
|
+
from ..color.values import color
|
|
131
|
+
try:
|
|
132
|
+
return check_color_map(v)
|
|
133
|
+
except (TypeError, ValueError) as e:
|
|
134
|
+
try:
|
|
135
|
+
return color(v)
|
|
136
|
+
except (TypeError, ValueError):
|
|
137
|
+
raise e from None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def given_colors(series: Any, n: int, kinds: tuple[str, ...] | None = None) -> Any:
|
|
141
|
+
"""What a series' own ``color_map`` (:func:`map_or_color`) gives ``n`` items: one color,
|
|
142
|
+
or a list, the map's colors (``kinds``: only of a map of one of those kinds); ``None``:
|
|
143
|
+
``"auto"``, or a map of another kind."""
|
|
144
|
+
from ..color.maps import MAP_WORDS, ColorMap, chosen_map, cycle_of
|
|
145
|
+
v = series.resolve("color_map")
|
|
146
|
+
if v is None or v == "auto":
|
|
147
|
+
return None
|
|
148
|
+
if not isinstance(v, ColorMap) and not (isinstance(v, str) and v in MAP_WORDS):
|
|
149
|
+
return v
|
|
150
|
+
cm = chosen_map(series, v, "qualitative")
|
|
151
|
+
if kinds is not None and cm.kind not in kinds:
|
|
152
|
+
return None
|
|
153
|
+
return list(cycle_of(cm, n))
|
plot_io/presets/area.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Stacked areas: layers piled on a baseline (zero, symmetric or streamgraph), each a
|
|
2
|
+
``Band`` with its top as a ``Path``."""
|
|
3
|
+
|
|
4
|
+
import array
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .._methods import AreaMixin
|
|
8
|
+
from .. import groups
|
|
9
|
+
from ..color.values import with_alpha
|
|
10
|
+
from ..layers import positions
|
|
11
|
+
from ..layers.base import Series, common
|
|
12
|
+
from ..layers.core import Table
|
|
13
|
+
from ..layers.grammar import Lay, Spec
|
|
14
|
+
from ..settings import Setting, boolean, one_of, table, text
|
|
15
|
+
from ..transform import settings_both as coord_settings
|
|
16
|
+
from ..layers.paint import base_colors
|
|
17
|
+
from ._util import fill_colors, set_color_value
|
|
18
|
+
from .curves import curve_settings
|
|
19
|
+
|
|
20
|
+
__all__ = ["Area"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _names(v: Any) -> Any:
|
|
24
|
+
"""One text, or a tuple of texts (one per layer) from a list."""
|
|
25
|
+
if isinstance(v, (list, tuple)):
|
|
26
|
+
return tuple(text(n) for n in v)
|
|
27
|
+
return text(v)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Area(AreaMixin, Series):
|
|
31
|
+
"""Areas of the layers ``y`` (one column, or several: a dict ``{name: values}``, a
|
|
32
|
+
data frame, a 2-D buffer's columns, a list of arrays) along ``x``, stacked on ``baseline``; ``orientation="horizontal"`` runs them along y.
|
|
33
|
+
|
|
34
|
+
Named layers get a legend entry each. ``baseline``: ``zero``, ``sym``, ``wiggle`` or
|
|
35
|
+
``weighted_wiggle`` (matplotlib's stackplot baselines). ``fill_opacity`` is the fills'
|
|
36
|
+
opacity, ``opacity`` the whole series'. ``interpolation`` and ``missing`` as
|
|
37
|
+
:class:`Fill`'s.
|
|
38
|
+
|
|
39
|
+
Example::
|
|
40
|
+
|
|
41
|
+
p = plt.Plot(width=240, height=160)
|
|
42
|
+
p.area(range(8), {"a": [1, 2, 3, 3, 4, 4, 5, 5], "b": [2, 2, 2, 3, 3, 2, 2, 3],
|
|
43
|
+
"c": [1, 1, 2, 2, 1, 2, 3, 3]})
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
SETTINGS = table(
|
|
47
|
+
**coord_settings(),
|
|
48
|
+
baseline=Setting(one_of("zero", "sym", "wiggle", "weighted_wiggle"),
|
|
49
|
+
doc="`zero`, `sym` (symmetric around 0), `wiggle` or "
|
|
50
|
+
"`weighted_wiggle` (streamgraphs, as matplotlib's stackplot)."),
|
|
51
|
+
stack=Setting(boolean, doc="Stack the layers; `False` overlays them from 0."),
|
|
52
|
+
color=Setting(set_color_value,
|
|
53
|
+
doc="A color, one per layer (a list, or a dict by layer name); default "
|
|
54
|
+
"the plot's colors."),
|
|
55
|
+
**groups.FillStyle.embed(docs={"fill": "Fill: `True` (each layer's color), a color, or "
|
|
56
|
+
"`False`.",
|
|
57
|
+
"fill_opacity": "Opacity of the fill, times `opacity` "
|
|
58
|
+
"(outlines keep `opacity`)."}),
|
|
59
|
+
**groups.Stroke.embed("edge_",
|
|
60
|
+
only=("color", "line_width"),
|
|
61
|
+
checks={"color": groups.series_color},
|
|
62
|
+
docs={"color": "Outline color of each layer's top: a color, "
|
|
63
|
+
"`\"series\"` (each layer's own color) or none.",
|
|
64
|
+
"line_width": "Outline width; default the line width."}),
|
|
65
|
+
**curve_settings(),
|
|
66
|
+
orientation=Setting(one_of("vertical", "horizontal"),
|
|
67
|
+
doc="`vertical` (layers along x) or `horizontal` (along y)."),
|
|
68
|
+
name=Setting(_names, doc="Legend entry: a text, or one per layer (a list); unset: "
|
|
69
|
+
"none (the keys of a dict of layers are entries)."),
|
|
70
|
+
**common("plain_color", "opacity", "z", "rasterize", "simplify", "font_size",
|
|
71
|
+
"font_family", "text_color"),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
DATA = {"x": "position", "y": "sets"}
|
|
75
|
+
|
|
76
|
+
def __init__(self, x: Any, y: Any, plot: Any = None, **settings: Any) -> None:
|
|
77
|
+
super().__init__(plot=plot, **settings)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def ys(self) -> list[array.array]:
|
|
81
|
+
"""The layers' heights: one column each."""
|
|
82
|
+
return self.cols.values
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def names(self) -> list[Any]:
|
|
86
|
+
"""The layers' names (a dict's keys, a frame's columns, else 0, 1, …)."""
|
|
87
|
+
return [k for k, _ in self.cols.sets]
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def stats(self) -> list[tuple[array.array, array.array]]:
|
|
91
|
+
"""``(bottom, top)`` of each layer."""
|
|
92
|
+
return self.cached("stats", self._compute, ("stack", "baseline"))
|
|
93
|
+
|
|
94
|
+
def _compute(self) -> list[tuple[array.array, array.array]]:
|
|
95
|
+
tables = _layers(self, None)
|
|
96
|
+
return [(t["bottom"], t["top"]) for t in tables]
|
|
97
|
+
|
|
98
|
+
def _colors(self, ctx: Any) -> list[Any]:
|
|
99
|
+
return base_colors(self, ctx, self.names, "layers")
|
|
100
|
+
|
|
101
|
+
def _fills(self, ctx: Any) -> list[Any]:
|
|
102
|
+
return fill_colors(self, ctx, self._colors(ctx))
|
|
103
|
+
|
|
104
|
+
def _strokes(self, ctx: Any) -> list[Any]:
|
|
105
|
+
"""Each layer's outline color."""
|
|
106
|
+
ec = self.resolve("edge_color")
|
|
107
|
+
opacity = self.resolve("opacity")
|
|
108
|
+
out = []
|
|
109
|
+
for c in self._colors(ctx):
|
|
110
|
+
c = c if ec == "series" else ctx.color(ec)
|
|
111
|
+
out.append(with_alpha(c, 1.0 if opacity is None else opacity) if c is not None
|
|
112
|
+
else None)
|
|
113
|
+
return out
|
|
114
|
+
|
|
115
|
+
SPEC = Spec(
|
|
116
|
+
[Lay("band", ("band", {"color": "_fills()",
|
|
117
|
+
"extra": ("simplify", "interpolation", "missing")}),
|
|
118
|
+
table="layers", per_set=True, aes={"y1": "top", "y2": "bottom"},
|
|
119
|
+
position="stack"),
|
|
120
|
+
Lay("path", ("stroke", {"color": "_strokes()", "width": "edge_width",
|
|
121
|
+
"extra": ("interpolation", "missing")}),
|
|
122
|
+
table="layers", per_set=True, aes={"y": "top"}, occupy=False, glyph="top")],
|
|
123
|
+
tables={"layers": lambda s, b: _layers(s, b)},
|
|
124
|
+
entries="sets", key={"fill": "_fills()", "stroke": "_strokes()", "when": "edge_width"},
|
|
125
|
+
color_count="sets", items="layers", zero=lambda s: ("y",) if s.resolve("baseline") == "zero" else ())
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _layers(s: Area, b: Any) -> list[Table]:
|
|
129
|
+
"""A table per layer: ``x`` and its ``heights``, stacked on the baseline (in the
|
|
130
|
+
plot's positions; at once without a build) or from 0."""
|
|
131
|
+
x = s.cols["x"]
|
|
132
|
+
n = len(x)
|
|
133
|
+
out = []
|
|
134
|
+
for r in s.ys:
|
|
135
|
+
t = Table(x=x, heights=r)
|
|
136
|
+
if not s.resolve("stack"):
|
|
137
|
+
t["bottom"], t["top"] = array.array("d", bytes(8 * n)), array.array("d", r)
|
|
138
|
+
out.append(t)
|
|
139
|
+
if s.resolve("stack"):
|
|
140
|
+
if b is None:
|
|
141
|
+
positions.stack(out, baseline=s.resolve("baseline"))
|
|
142
|
+
else:
|
|
143
|
+
for t in out:
|
|
144
|
+
t.stack = (("area", id(s)), True, s.resolve("baseline"))
|
|
145
|
+
return out
|
plot_io/presets/bar.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""Bars: one rectangle per category or x value (``Rects``), grouped (dodge) or stacked
|
|
2
|
+
(stack), with error bars (``Segments``) and value labels (``Text``)."""
|
|
3
|
+
|
|
4
|
+
import math
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from vec_io.arrays import doubles
|
|
8
|
+
|
|
9
|
+
from .._methods import BarMixin
|
|
10
|
+
from .. import groups
|
|
11
|
+
from ..labels import bar_labels, check_labels, label_size
|
|
12
|
+
from ..layers.base import common
|
|
13
|
+
from ..layers.core import Layer, Table
|
|
14
|
+
from ..layers.geoms import RECTS
|
|
15
|
+
from ..layers.grammar import Lay, Spec, flipped
|
|
16
|
+
from ..layers.paint import Paint, base_colors, role
|
|
17
|
+
from ..layers.positions import Dodge, check_stack, slot_edges, slots
|
|
18
|
+
from ..numbers import NumberStyle, check_format, format_numbers
|
|
19
|
+
from ..color.values import color
|
|
20
|
+
from ..settings import Setting, boolean, number, one_of, table
|
|
21
|
+
from ..text import font_of
|
|
22
|
+
from ..transform import settings_both as coord_settings
|
|
23
|
+
from ..validators import length
|
|
24
|
+
from ..values import category_paint
|
|
25
|
+
from ._util import (faded, fill_colors, memo, placed_text)
|
|
26
|
+
from .errors import Summarized, error_layers, error_value, errors_of
|
|
27
|
+
|
|
28
|
+
__all__ = ["Bar"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _bottom(v: Any) -> Any:
|
|
32
|
+
if isinstance(v, (int, float)) and not isinstance(v, bool):
|
|
33
|
+
return float(v)
|
|
34
|
+
return doubles(v)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
_width = length(positive=True, data=True, percent="slot")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Bar(BarMixin, Summarized):
|
|
41
|
+
"""Bars of height ``y`` at ``x`` (categories or numbers), or of a dict
|
|
42
|
+
``{category: height}``, or of heights alone (at 0, 1, …).
|
|
43
|
+
|
|
44
|
+
Categories make a category axis (``None`` among them: no bar there); numbers centre the
|
|
45
|
+
bars on them, ``width`` in data units. ``value_labels`` writes the values outside,
|
|
46
|
+
inside or at the centre of the bars, formatted by ``value_label_format``; ``y_error``
|
|
47
|
+
adds error bars at the bar ends (along the value axis; ``error_color``, ``error_width``
|
|
48
|
+
style them). Value axes include 0 (``include_zero="auto"``). With replicates,
|
|
49
|
+
``estimate`` and ``interval`` draw bars of their estimate with error bars of their
|
|
50
|
+
interval (`summaries <docs/api.md#summaries-estimate-interval>`_). Bars share their category's slot with the plot's other bar
|
|
51
|
+
series (``dodge``, ``stack``: `series <docs/api.md#5-series>`_).
|
|
52
|
+
|
|
53
|
+
Example::
|
|
54
|
+
|
|
55
|
+
p = plt.Plot(width=240, height=160)
|
|
56
|
+
p.bar(["a", "b", "c", "d"], [3, 5, 2, 4], value_labels=True)
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
SETTINGS = table(
|
|
60
|
+
**coord_settings(),
|
|
61
|
+
color=Setting(category_paint,
|
|
62
|
+
doc="Bar fill: a color, one per bar (list or dict by category), or "
|
|
63
|
+
"numbers per bar (list or dict) mapped through `color_map`; "
|
|
64
|
+
"default: the plot's next color."),
|
|
65
|
+
**common("color_map"),
|
|
66
|
+
**groups.Stroke.embed("edge_",
|
|
67
|
+
only=("color", "line_width"),
|
|
68
|
+
checks={"color": groups.series_color},
|
|
69
|
+
docs={"color": "Outline color: a color, `\"series\"` (each "
|
|
70
|
+
"bar's own color, at `opacity`) or none.",
|
|
71
|
+
"line_width": "Outline width."}),
|
|
72
|
+
**groups.FillStyle.embed(docs={"fill": "Fill: `True` (each bar's color), a color, or "
|
|
73
|
+
"`False`.",
|
|
74
|
+
"fill_opacity": "Opacity of the fill, times `opacity` "
|
|
75
|
+
"(outlines keep `opacity`)."}),
|
|
76
|
+
seam_color=Setting(color,
|
|
77
|
+
doc="Where outlined bars touch (dodged or adjacent), the shared edge in "
|
|
78
|
+
"this color; `none`: both outlines as drawn. Between the parts "
|
|
79
|
+
"of a stack, the line is the outline of the part nearer the "
|
|
80
|
+
"baseline."),
|
|
81
|
+
max_width=Setting(length(positive=True, data=True, percent="slot"),
|
|
82
|
+
doc="Largest width of the series' part of a slot (as `width`); "
|
|
83
|
+
"default none."),
|
|
84
|
+
width=Setting(_width,
|
|
85
|
+
doc="Bar width: `%` of the slot, a length (bare numbers: px), `d` "
|
|
86
|
+
"data units, or a duration (or `timedelta`) on time axes."),
|
|
87
|
+
bottom=Setting(_bottom, doc="Base of the bars: a value or one per bar."),
|
|
88
|
+
orientation=Setting(one_of("vertical", "horizontal"),
|
|
89
|
+
doc="`vertical` (bars rise from the x axis) or `horizontal`."),
|
|
90
|
+
stack=Setting(check_stack,
|
|
91
|
+
doc="`True`: on top of the previous stacked bars of the plot; "
|
|
92
|
+
"`\"normalize\"`: stacks scaled to 1."),
|
|
93
|
+
dodge=Setting(boolean, doc="Bar series sharing a category share its slot side "
|
|
94
|
+
"by side; `False` overlays them."),
|
|
95
|
+
gap=Setting(length(lo=0, percent="slot_part"), doc="Fraction of each bar's part of a shared slot "
|
|
96
|
+
"left empty."),
|
|
97
|
+
saturation=Setting(number(0, 1), doc="Saturation of the fills (seaborn: 0.75)."),
|
|
98
|
+
y_error=Setting(error_value, doc="Error bars at the bar ends, along the value axis: "
|
|
99
|
+
"a value, a sequence, `(low, high)` or `Errors`."),
|
|
100
|
+
**common("estimate"),
|
|
101
|
+
**groups.Stroke.embed("error_",
|
|
102
|
+
only=("color", "line_width"),
|
|
103
|
+
docs={"color": "Error bar color.",
|
|
104
|
+
"line_width": "Error bar line width."}),
|
|
105
|
+
value_labels=Setting(check_labels,
|
|
106
|
+
doc="Value labels: `True`/`\"outside\"`, `\"inside\"`, "
|
|
107
|
+
"`\"center\"`. Outside labels widen automatic limits so they "
|
|
108
|
+
"fit in the axes; a label too large for its bar goes outside, "
|
|
109
|
+
"or is left out on a stacked bar; a label over an earlier one "
|
|
110
|
+
"is left out."),
|
|
111
|
+
value_label_format=Setting(check_format, doc="Number format of the value labels."),
|
|
112
|
+
**common("name", "opacity", "z", "rasterize", "simplify", "font_size", "font_family",
|
|
113
|
+
"text_color"),
|
|
114
|
+
**groups.NumberFormat.embed(),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
DATA = {"x": "position", "y": "heights"}
|
|
118
|
+
ITEMS = "bars"
|
|
119
|
+
|
|
120
|
+
def __init__(self, x: Any, y: Any = None, plot: Any = None, **settings: Any) -> None:
|
|
121
|
+
super().__init__(plot=plot, **settings)
|
|
122
|
+
|
|
123
|
+
# -- paint -----------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
def _colors(self, ctx: Any) -> list[Any]:
|
|
126
|
+
"""Each bar's color, saturation applied, before opacity."""
|
|
127
|
+
return base_colors(self, ctx, self.cols.categories, "bars", items=True)
|
|
128
|
+
|
|
129
|
+
def fills(self, ctx: Any) -> list[Any]:
|
|
130
|
+
return fill_colors(self, ctx, base_colors(self, ctx, self.cols.categories, "bars",
|
|
131
|
+
items=True, saturation=False))
|
|
132
|
+
|
|
133
|
+
def edges(self, ctx: Any) -> Any:
|
|
134
|
+
"""Outline color: one, or one per bar (``edge_color="series"``); ``None``."""
|
|
135
|
+
if not self.resolve("edge_width"):
|
|
136
|
+
return None
|
|
137
|
+
v = self.resolve("edge_color")
|
|
138
|
+
if v != "series":
|
|
139
|
+
return ctx.color(v)
|
|
140
|
+
opacity = self.resolve("opacity")
|
|
141
|
+
opacity = 1.0 if opacity is None else opacity
|
|
142
|
+
return faded(self._colors(ctx), opacity)
|
|
143
|
+
|
|
144
|
+
# -- the preset --------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
SPEC = Spec(
|
|
147
|
+
[Lay("rects", ("bar.rects", {"table": "#data"}), name="bars",
|
|
148
|
+
aes={"y0": "bottom", "y1": "top"},
|
|
149
|
+
position=lambda s: "stack" if s.resolve("stack") else "dodge", seams=True)],
|
|
150
|
+
tables={"data": lambda s, b: _bars(s, b)},
|
|
151
|
+
key={"fill": "symbol", "stroke": "symbol", "marker": "@bars"},
|
|
152
|
+
zero=("y",), titles={"x": "x", "y": "y", "values": "y"}, slots="x",
|
|
153
|
+
color_sources="items", finish=lambda s, b: _finish(s, b))
|
|
154
|
+
|
|
155
|
+
def _label_texts(self, t: Table) -> list[Any]:
|
|
156
|
+
vals = [b - a for a, b in zip(t["bottom"], t["top"])]
|
|
157
|
+
return format_numbers(vals, self.resolve("value_label_format"), NumberStyle.of(self))
|
|
158
|
+
|
|
159
|
+
def _label_items(self, ctx: Any, bars: Layer) -> list[Any]:
|
|
160
|
+
lay = bars.columns(ctx)
|
|
161
|
+
rects = RECTS.page_rects(ctx, lay)
|
|
162
|
+
fills = self.fills(ctx)
|
|
163
|
+
keep = [i for i, r in enumerate(rects) if all(math.isfinite(v) for v in r[:4])]
|
|
164
|
+
texts = self._label_texts(bars.table)
|
|
165
|
+
font = font_of(self, self.resolve("font_size"),
|
|
166
|
+
color=ctx.color(self.resolve("text_color")))
|
|
167
|
+
return bar_labels(ctx.canvas, [rects[i] for i in keep], [texts[i] for i in keep],
|
|
168
|
+
[fills[i] for i in keep], self.resolve("value_labels"), bars.flip, font,
|
|
169
|
+
stacked=bool(self.resolve("stack")))
|
|
170
|
+
|
|
171
|
+
def _label_room(self, fonts: Any, role: str, t: Table,
|
|
172
|
+
val: str) -> list[tuple[float, float, int]]:
|
|
173
|
+
"""``(value, px, direction)`` per outside label on the value axis: the label needs
|
|
174
|
+
``px`` past ``value``, toward higher (1) or lower (-1) values."""
|
|
175
|
+
if self.resolve("value_labels") != "outside" or role != val:
|
|
176
|
+
return []
|
|
177
|
+
texts = self._label_texts(t)
|
|
178
|
+
font = font_of(self, self.resolve("font_size"))
|
|
179
|
+
horizontal = flipped(self)
|
|
180
|
+
return [(top, label_size(fonts, text, font, horizontal), -1 if top < b else 1)
|
|
181
|
+
for b, top, text in zip(t["bottom"], t["top"], texts)
|
|
182
|
+
if text not in (None, "") and math.isfinite(top) and math.isfinite(b)]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@role("bar.rects")
|
|
186
|
+
def _rects_paint(s: Bar, ctx: Any, glyph: bool, table: Any = None) -> dict[str, Any]:
|
|
187
|
+
"""The bars' fills and outlines (the legend's: the first bar's)."""
|
|
188
|
+
fills = memo(ctx, (id(table), "fills"), lambda: s.fills(ctx))
|
|
189
|
+
lc = memo(ctx, (id(table), "edges"), lambda: s.edges(ctx))
|
|
190
|
+
if glyph:
|
|
191
|
+
return {"fill": fills[0] if fills else None,
|
|
192
|
+
"stroke": lc[0] if isinstance(lc, list) and lc else
|
|
193
|
+
(None if isinstance(lc, list) else lc),
|
|
194
|
+
"line_width": s.resolve("edge_width")}
|
|
195
|
+
return {"fill": fills, "stroke": lc, "line_width": s.resolve("edge_width"),
|
|
196
|
+
"seam": ctx.color(s.resolve("seam_color")), "stack": bool(s.resolve("stack")),
|
|
197
|
+
"simplify": s.resolve("simplify")}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _bars(s: Bar, b: Any) -> Table:
|
|
201
|
+
"""The bars: heights per category (stacked with the plot's stacked bars, or from
|
|
202
|
+
``bottom``), in slots shared with the plot's dodged bars."""
|
|
203
|
+
flip = b.flip
|
|
204
|
+
time = "x" in s.cols.times
|
|
205
|
+
stack = s.resolve("stack")
|
|
206
|
+
t = Table(keys=list(s.cols.categories), heights=s.cols["y"])
|
|
207
|
+
d = Dodge(s, "bar", flip, list(s.cols.categories), s.cols["x"],
|
|
208
|
+
"stack" if stack else None, time)
|
|
209
|
+
t.dodge = d
|
|
210
|
+
if stack:
|
|
211
|
+
t.stack = (("bar", flip), stack)
|
|
212
|
+
else:
|
|
213
|
+
bottom = s.resolve("bottom")
|
|
214
|
+
n = len(s.cols["y"])
|
|
215
|
+
bottoms = [bottom] * n if isinstance(bottom, float) else list(bottom)
|
|
216
|
+
if len(bottoms) != n:
|
|
217
|
+
raise ValueError(f"{len(bottoms)} bottoms for {n} bars")
|
|
218
|
+
t["bottom"] = bottoms
|
|
219
|
+
t["top"] = [lo + h for lo, h in zip(bottoms, s.cols["y"])]
|
|
220
|
+
|
|
221
|
+
def ends(ctx: Any) -> list[tuple[float, float]]:
|
|
222
|
+
return memo(ctx, (id(t), "slot_edges"), lambda: slot_edges(d, ctx))
|
|
223
|
+
|
|
224
|
+
def x0(ctx: Any) -> list[float]:
|
|
225
|
+
return [a for a, _ in ends(ctx)]
|
|
226
|
+
|
|
227
|
+
def x1(ctx: Any) -> list[float]:
|
|
228
|
+
return [b for _, b in ends(ctx)]
|
|
229
|
+
|
|
230
|
+
t.lazy["x0"], t.lazy["x1"] = x0, x1
|
|
231
|
+
return t
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _finish(s: Bar, b: Any) -> None:
|
|
235
|
+
"""Error bars at the tops (after the plot's positions), value labels."""
|
|
236
|
+
call, flip = b.call, b.flip
|
|
237
|
+
t = b.tables["data"]
|
|
238
|
+
d = t.dodge
|
|
239
|
+
val = b.role("y")
|
|
240
|
+
|
|
241
|
+
def centers(ctx: Any) -> list[float]:
|
|
242
|
+
return [c for c, _ in memo(ctx, (id(t), "slots"), lambda: slots(d, ctx))]
|
|
243
|
+
|
|
244
|
+
def errors() -> None:
|
|
245
|
+
e = errors_of(s, "y_error")
|
|
246
|
+
if e is not None:
|
|
247
|
+
tops = t["top"]
|
|
248
|
+
e.bind(len(tops), s, "y_error")
|
|
249
|
+
source = Paint(s, "error_source", color="error_color", width="error_width",
|
|
250
|
+
simplify=False, opacity=1.0)
|
|
251
|
+
call.layers[1:1] = error_layers(s, e, centers, tops, False, source, flip)
|
|
252
|
+
call.sticky_values[val] = sorted({v for v in t["bottom"] if math.isfinite(v)})
|
|
253
|
+
|
|
254
|
+
call.after.append(errors)
|
|
255
|
+
bars = call.layers[0]
|
|
256
|
+
if s.resolve("value_labels"):
|
|
257
|
+
call.layers.append(placed_text(s, id(s), lambda ctx: s._label_items(ctx, bars),
|
|
258
|
+
lambda ctx: font_of(
|
|
259
|
+
s, s.resolve("font_size"),
|
|
260
|
+
color=ctx.color(s.resolve("text_color")))))
|
|
261
|
+
call.room = lambda fonts, role: s._label_room(fonts, role, t, val)
|