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.
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-arm64.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
@@ -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))
@@ -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)