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
@@ -0,0 +1,194 @@
1
+ """Scatter: markers at points (``Points``), with optional error bars (``Segments``) and
2
+ point labels (``Text``, placed by :func:`plot_io.labels.around`)."""
3
+
4
+ import math
5
+ from typing import Any
6
+
7
+ from .._methods import ScatterMixin
8
+ from .. import groups
9
+ from ..layers.base import column, common
10
+ from ..layers.grammar import Lay, Spec, flipped
11
+ from ..layers.paint import density_points, density_stats, order_settings
12
+ from ..settings import Setting, table
13
+ from ..validators import boolean, length
14
+ from ..transform import settings_both as coord_settings
15
+ from .errors import Summarized, error_value
16
+
17
+ __all__ = ["Scatter"]
18
+
19
+
20
+ class Scatter(ScatterMixin, Summarized):
21
+ """Markers at points ``(x, y)``.
22
+
23
+ ``color``, ``edge_color``, ``size``, ``marker`` and ``opacity`` take one value or one per
24
+ point. Numbers per point in ``color``/``edge_color`` are mapped through ``color_map``;
25
+ in ``size``, through ``size_map`` when it is set; ``"density"`` maps the points'
26
+ density. ``orientation="horizontal"`` draws ``x`` on the y axis.
27
+
28
+ ``size`` is a diameter; with ``size_basis="area"`` every shape has the area of the circle
29
+ of that diameter (a square of size 4 is drawn 3.54 px wide). See
30
+ `values in color and size <docs/api.md#values-in-color-and-size>`_.
31
+
32
+ Error bars: ``x_error``/``y_error`` take a value, a sequence, ``(low, high)`` sequences
33
+ or ``Errors(values, color=None, line_width=None, cap_length=None)`` (cap: the series'
34
+ ``cap_length``, else 4 px). With replicates, ``estimate`` draws a point per ``x`` and
35
+ ``interval`` its error bars (`summaries <docs/api.md#summaries-estimate-interval>`_).
36
+
37
+ Point labels (``labels``) go beside their point (8 places, at ``0.25em``) or further out
38
+ on rings up to ``label_reach``, with a leader line to the point; a label meets no other
39
+ label, no point of the series, no leader and no inside legend placed before it, and
40
+ stays inside the axes. Labels are placed in the order given; one without room is
41
+ dropped (``series.labels_dropped`` counts them). The placement is deterministic.
42
+
43
+ Example::
44
+
45
+ rng = random.Random(1)
46
+ p = plt.Plot(width=240, height=160)
47
+ p.scatter([rng.gauss(0, 1) for _ in range(100)], [rng.gauss(0, 1) for _ in range(100)])
48
+ """
49
+
50
+ SETTINGS = table(
51
+ **coord_settings(),
52
+ **groups.Marker.embed(),
53
+ **common("color_map", "size_map", "size_key"),
54
+ **order_settings(),
55
+ x_error=Setting(error_value, doc="Horizontal error bars: a value, a sequence, "
56
+ "`(low, high)` or `Errors`."),
57
+ y_error=Setting(error_value, doc="Vertical error bars."),
58
+ **common("estimate"),
59
+ orientation=Setting(lambda v: _orientation(v),
60
+ doc="`vertical` (x along the x axis) or `horizontal` (x along "
61
+ "the y axis)."),
62
+ labels=Setting(lambda v: _labels(v),
63
+ doc="A text per point (a sequence or a `data=` column; `None` and "
64
+ "`\"\"`: none), placed beside its point or further out with a "
65
+ "leader, meeting no other label, no point and an inside legend; "
66
+ "a label without room is dropped (`labels_dropped`)."),
67
+ **groups.TextStyle.embed("label_", only=("font_size", "color"),
68
+ docs={"font_size": "Font size of the point labels.",
69
+ "color": "Color of the point labels (unset: the "
70
+ "text color)."}),
71
+ label_reach=Setting(length(lo=0, percent="em", em="label_font_size"),
72
+ doc="Farthest a point label goes from its point."),
73
+ label_leaders=Setting(boolean, doc="A line from each label placed away from its "
74
+ "point to the point."),
75
+ **common("name", "z", "rasterize", "simplify", "font_size", "font_family",
76
+ "text_color"),
77
+ )
78
+
79
+ DATA = {"x": "position", "y": "position"}
80
+ x = column("x", "x of each point (categories at their index among their own).")
81
+ y = column("y", "y of each point (as x).")
82
+
83
+ def __init__(self, x: Any, y: Any, plot: Any = None, **settings: Any) -> None:
84
+ super().__init__(plot=plot, **settings)
85
+
86
+ @property
87
+ def stats(self) -> dict[str, Any]:
88
+ """Derived data: ``{"density": values}`` when the series maps a density."""
89
+ return density_stats(self)
90
+
91
+ @property
92
+ def labels_dropped(self) -> int:
93
+ """Point labels left out for want of room at the last drawing."""
94
+ return self.__dict__.get("_labels_dropped", 0)
95
+
96
+ def _label_picks(self) -> list[tuple[int, Any]]:
97
+ """``(point index, text)`` of the labels, in priority order."""
98
+ texts = self.resolve("labels")
99
+ if not texts:
100
+ return []
101
+ return [(i, t if isinstance(t, str) else str(t)) for i, t in enumerate(texts)
102
+ if t is not None and t != "" and not (isinstance(t, float) and t != t)]
103
+
104
+ def density_points(self) -> tuple[Any, Any]:
105
+ if flipped(self):
106
+ return density_points(self, self.y, self.x)
107
+ return density_points(self, self.x, self.y)
108
+
109
+ SPEC = Spec(
110
+ [Lay("errors", ("error_source", {"color": "marker", "opacity": "marker",
111
+ "marker": "@points"})),
112
+ Lay("points", ("marker", {"order": True, "once": True, "n": lambda s: len(s.x)}),
113
+ name="points", radius=lambda s: _radius(s.resolve("size")))],
114
+ key={"fill": "marker", "stroke": "marker", "marker": "@points"},
115
+ zero=("x", "y"), color_sources=True, size_sources=True,
116
+ finish=lambda s, b: point_labels(s, b))
117
+
118
+
119
+ def _labels(v: Any) -> Any:
120
+ if v is None:
121
+ return None
122
+ if isinstance(v, (str, bytes)) or not hasattr(v, "__len__"):
123
+ raise ValueError(f"expected a text per point (a column of data= by name), got {v!r}")
124
+ return list(v.tolist() if hasattr(v, "tolist") else v)
125
+
126
+
127
+ def point_labels(s: Any, b: Any) -> None:
128
+ """The text layer of ``s``' point labels (a series of Scatter's settings whose spec has
129
+ a ``points`` layer of its data's points), placed when drawn."""
130
+ from .. import labels
131
+ from ..layers.geoms import GEOMS
132
+ from ..text import font_of
133
+ from ._util import memo, placed_text
134
+ pts = [la for la in b.call.layers if la.geom is GEOMS["points"]]
135
+ if not s.resolve("labels") or not pts:
136
+ return
137
+ layer = pts[-1]
138
+ radius = layer.options.get("radius", 0.0) or 3.0
139
+
140
+ def font(ctx: Any) -> Any:
141
+ c = s.resolve("label_color")
142
+ return font_of(s, s.resolve("label_font_size"),
143
+ color=ctx.color(s.resolve("text_color") if c is None else c))
144
+
145
+ def items(ctx: Any) -> list[tuple[Any, ...]]:
146
+ picks = s._label_picks()
147
+ s.__dict__["_labels_dropped"] = 0
148
+ if not picks:
149
+ return []
150
+ lay = layer.columns(ctx)
151
+ xs, ys = GEOMS["points"]._xy(ctx, lay) # type: ignore[attr-defined]
152
+ if isinstance(xs, float):
153
+ xs, ys = [xs], [ys]
154
+ f = font(ctx)
155
+ sizes, shift = [], []
156
+ for _, t in picks:
157
+ q = labels.quad(ctx.canvas, t, f, 0.0, 0.0, "center", "center")
158
+ us, vs = [c[0] for c in q], [c[1] for c in q] # type: ignore[union-attr]
159
+ sizes.append((max(us) - min(us), max(vs) - min(vs)))
160
+ shift.append(((max(us) + min(us)) / 2, (max(vs) + min(vs)) / 2))
161
+ taken = memo(ctx, "point_labels", lambda: list(ctx.root.avoid))
162
+ n = len(xs)
163
+ keep = [(k, i) for k, (i, _) in enumerate(picks) if i < n]
164
+ got = labels.around((xs, ys), radius, [i for _, i in keep], [sizes[k] for k, _ in keep],
165
+ ctx.rect, labels.PAD * f.size, s.resolve("label_reach"), taken)
166
+ leaders = s.resolve("label_leaders")
167
+ out: list[tuple[Any, ...]] = []
168
+ for (k, i), g in zip(keep, got):
169
+ if g is None:
170
+ continue
171
+ cx, cy, ring = g
172
+ line = None
173
+ if ring and leaders:
174
+ w, h = sizes[k]
175
+ px, py = float(xs[i]), float(ys[i])
176
+ qx = min(max(px, cx - w / 2), cx + w / 2)
177
+ qy = min(max(py, cy - h / 2), cy + h / 2)
178
+ dist = math.hypot(qx - px, qy - py) or 1.0
179
+ line = (px + (qx - px) * radius / dist, py + (qy - py) * radius / dist, qx, qy)
180
+ out.append((picks[k][1], cx - shift[k][0], cy - shift[k][1], f.color, "center",
181
+ "center", None, None, line))
182
+ s.__dict__["_labels_dropped"] = len(picks) - len(out)
183
+ return out
184
+
185
+ b.call.layers.append(placed_text(s, (id(s), "point labels"), items, font))
186
+
187
+
188
+ def _radius(size: Any) -> float:
189
+ return size / 2 if isinstance(size, (int, float)) else 0.0
190
+
191
+
192
+ def _orientation(v: Any) -> Any:
193
+ from ..settings import one_of
194
+ return one_of("vertical", "horizontal")(v)
@@ -0,0 +1,100 @@
1
+ """Stems: segments from a baseline to each value (``Segments``), ending in a marker
2
+ (``Points``), over a baseline (``Path``): lollipops."""
3
+
4
+ from typing import Any
5
+
6
+ from .._methods import StemMixin
7
+ from .. import groups
8
+ from .. import kernels
9
+ from ..color.values import color
10
+ from ..layers.base import COMMON, column, common
11
+ from ..layers.core import Table
12
+ from ..layers.grammar import Lay, Spec
13
+ from ..layers.paint import xy_density_points
14
+ from ..settings import Setting, number, one_of, table
15
+ from ..transform import settings_both as coord_settings
16
+ from ..validators import length
17
+ from .errors import Summarized
18
+
19
+ __all__ = ["Stem"]
20
+
21
+
22
+ class Stem(StemMixin, Summarized):
23
+ """Stems from ``baseline`` to ``y`` at ``x`` (or at 0, 1, … when ``y`` is omitted);
24
+ numbers per point in ``marker_color`` map through the series' color axis.
25
+
26
+ Stems from ``baseline`` to each value with a marker (lollipops), and a baseline line.
27
+ With replicates, ``estimate`` draws a stem per ``x``.
28
+
29
+ Example::
30
+
31
+ import math
32
+ p = plt.Plot(width=240, height=160)
33
+ p.stem(range(16), [math.sin(i / 2) for i in range(16)])
34
+ """
35
+
36
+ SETTINGS = table(
37
+ **coord_settings(),
38
+ **groups.Marker.embed("marker_"),
39
+ **common("color_map"),
40
+ color=Setting(color, doc="Stem and marker color; default: the plot's next color."),
41
+ line_width=Setting(length(lo=0), inherited=True, doc="Stem width."),
42
+ baseline=Setting(number(), doc="Value the stems start from."),
43
+ **groups.Stroke.embed("baseline_",
44
+ only=("color", "line_width"),
45
+ docs={"color": "Baseline color; `none`: no baseline.",
46
+ "line_width": "Baseline width."}),
47
+ orientation=Setting(one_of("vertical", "horizontal"),
48
+ doc="`vertical` stems (values on y) or `horizontal`."),
49
+ estimate=COMMON["estimate"](),
50
+ **common("name", "opacity", "z", "rasterize", "simplify", "font_size", "font_family",
51
+ "text_color"),
52
+ )
53
+
54
+ DATA = {"x": "position", "y": "numbers"}
55
+ x = column("x", "x of each point (categories at their index among their own).")
56
+ y = column("y", "y of each point (as x).")
57
+
58
+ def __init__(self, x: Any, y: Any = None, plot: Any = None, **settings: Any) -> None:
59
+ super().__init__(plot=plot, **settings)
60
+
61
+ def density_points(self) -> tuple[Any, Any]:
62
+ """The markers' positions on the plot's axes (a density in ``marker_color``)."""
63
+ return xy_density_points(self, self.x, self.y)
64
+
65
+ SPEC = Spec(
66
+ [Lay("path", ("stroke", {"color": "baseline_color", "width": "baseline_width"}),
67
+ table="baseline", key=False, occupy=False),
68
+ Lay("segments", ("stroke", {"color": "faded", "drop_zero": True,
69
+ "extra": ("simplify",)}),
70
+ table="stems", glyph="stem"),
71
+ Lay("points", ("marker", {"prefix": "marker_", "color": "faded",
72
+ "n": lambda s: len(s.x)}),
73
+ occupy=False, glyph_at=0.35, glyph_max=0.7)],
74
+ tables={"stems": lambda s, b: _stems(s, b), "baseline": lambda s, b: _baseline(s, b)},
75
+ key={"fill": "faded"}, zero=("y",), color_sources=True)
76
+
77
+
78
+ def _stems(s: Stem, b: Any) -> Table:
79
+ t = Table(x0=s.x, x1=s.x, y0=s.resolve("baseline"), y1=s.y)
80
+ if "x" in b.at:
81
+ t.lazy["x0"] = t.lazy["x1"] = b.at["x"]
82
+ return t
83
+
84
+
85
+ def _baseline(s: Stem, b: Any) -> Table | None:
86
+ """The baseline under the stems, from the first to the last."""
87
+ if not len(s.x):
88
+ return None
89
+ cnt, lo, hi = kernels.extent(s.x, None)
90
+ if not cnt:
91
+ return None
92
+ base = s.resolve("baseline")
93
+ t = Table(x=[lo, hi], y=[base, base])
94
+ at = b.at.get("x")
95
+ if at is not None:
96
+ def ends(ctx: Any) -> list[float]:
97
+ _, a, b = kernels.extent(at(ctx), None)
98
+ return [a, b]
99
+ t.lazy["x"] = ends
100
+ return t
@@ -0,0 +1,323 @@
1
+ """Violin plots (stat ``violin``): a mirrored density per category (``Band``, along the
2
+ value axis), with an inner box (``Path``, ``Rects``, ``Points``), quartile lines
3
+ (``Path``) or points (``Points``)."""
4
+
5
+ import array
6
+ from typing import Any
7
+
8
+ from .._methods import ViolinMixin
9
+ from .. import columns, groups
10
+ from ..color.values import desaturate, with_alpha
11
+ from ..layers import stats
12
+ from ..layers.base import Series, common
13
+ from ..layers.core import Layer, Table
14
+ from ..layers.geoms import GEOMS, Marks
15
+ from ..layers.grammar import Lay, Spec, axis_of
16
+ from ..layers.paint import Paint, role
17
+ from ..layers.positions import Dodge, placed
18
+ from ..lines import dash_of
19
+ from ..settings import Setting, boolean, either, literal, number, one_of, table
20
+ from ..transform import settings_both as coord_settings
21
+ from ..validators import length
22
+ from ..values import category_paint
23
+ from ..layers.paint import base_colors
24
+ from ._util import fill_colors
25
+ from .kde import check_bandwidth
26
+
27
+ __all__ = ["Violin"]
28
+
29
+
30
+ def _inner(v: Any) -> Any:
31
+ if v is False or v == "none":
32
+ return False
33
+ return one_of("box", "quartiles", "points")(v)
34
+
35
+
36
+ class Violin(ViolinMixin, Series):
37
+ """Violins of ``values``: a dict ``{category: values}``, a list of arrays (categories
38
+ ``0, 1, …`` or ``categories=``), or one array.
39
+
40
+ ``values`` as :class:`Box` takes them. Each violin is the density of its category,
41
+ mirrored, in the value axis' space (log axes: in log space); ``width_scale`` gives equal
42
+ areas (default), areas proportional to the counts, or equal widths. A category of one
43
+ value (or all equal) is a line at it, with a warning. ``inner`` draws a thin box with
44
+ whiskers and a median dot, dashed quartiles, the points, or nothing (``False``). With
45
+ ``split=True``, two violin series share each slot as facing halves. ``side="low"`` or
46
+ ``"high"`` draws one half of each violin (toward lower or higher positions), alone in
47
+ its slot.
48
+
49
+ Example::
50
+
51
+ rng = random.Random(1)
52
+ p = plt.Plot(width=240, height=160)
53
+ p.violin({k: [rng.gauss(m, 1) for _ in range(80)] for k, m in (("A", 0), ("B", 1), ("C", 0.5))})
54
+ """
55
+
56
+ SETTINGS = table(
57
+ **coord_settings(),
58
+ color=Setting(category_paint,
59
+ doc="Fill: a color, one per violin (list or dict by category), or "
60
+ "numbers per violin mapped through `color_map`; default the next "
61
+ "plot's color."),
62
+ **common("color_map"),
63
+ **groups.Stroke.embed("edge_",
64
+ only=("color", "line_width"),
65
+ checks={"color": groups.series_color},
66
+ docs={"color": "Outline and inner color: a color or "
67
+ "`\"series\"` (each violin's own color, at "
68
+ "`opacity`).",
69
+ "line_width": "Outline width."}),
70
+ saturation=Setting(number(0, 1), doc="Saturation of the fills."),
71
+ **groups.FillStyle.embed(docs={"fill": "Fill: `True` (each violin's color), a color, or "
72
+ "`False`.",
73
+ "fill_opacity": "Opacity of the fill, times `opacity` "
74
+ "(outlines keep `opacity`)."}),
75
+ max_width=Setting(length(positive=True, data=True, percent="slot"),
76
+ doc="Largest width of the series' part of a slot (as `width`); "
77
+ "default none."),
78
+ width=Setting(length(positive=True, data=True, percent="slot"),
79
+ doc="Violin width: `%` of the slot, a length (bare numbers: px), `d` "
80
+ "data units, or a duration on time axes."),
81
+ bandwidth=Setting(check_bandwidth, doc="`scott`, `silverman` or a number."),
82
+ weights=Setting(columns.weights,
83
+ doc="A weight per value, counted as repeated values (a dict or a list "
84
+ "per category, as the values; long data: a weight per row)."),
85
+ adjust=Setting(number(positive=True), doc="Factor applied to the bandwidth."),
86
+ cut=Setting(number(lo=0), doc="The density extends this many bandwidths past "
87
+ "the data."),
88
+ points=Setting(number(lo=16, hi=65536, integer=True),
89
+ doc="Points per violin outline."),
90
+ width_scale=Setting(one_of("area", "count", "width"),
91
+ doc="`area`: equal areas; `count`: area proportional to the "
92
+ "number of values; `width`: equal widths."),
93
+ inner=Setting(_inner,
94
+ doc="Inside each violin: `box` (a thin box, whiskers and a median dot), "
95
+ "`quartiles` (dashed lines), `points`, or `False` (nothing)."),
96
+ split=Setting(boolean, doc="Two violin series share each slot as facing "
97
+ "halves."),
98
+ side=Setting(one_of("both", "low", "high"),
99
+ doc="`both` halves, or only the `low` or `high` one (toward lower or "
100
+ "higher positions)."),
101
+ orientation=Setting(one_of("vertical", "horizontal"),
102
+ doc="`vertical` (categories on x) or `horizontal`."),
103
+ dodge=Setting(boolean, doc="Violin and box series sharing a category share its "
104
+ "slot side by side; `False` overlays them."),
105
+ gap=Setting(length(lo=0, percent="slot_part"), doc="Fraction of each part of a shared slot left "
106
+ "empty."),
107
+ categories=Setting(either(literal(None), lambda v: list(v)),
108
+ doc="Category names of a list of arrays."),
109
+ **common("name", "opacity", "z", "rasterize", "simplify", "font_size", "font_family",
110
+ "text_color"),
111
+ )
112
+ DATA = {"values": "categories", "positions": "slots"}
113
+ ITEMS = "violins"
114
+
115
+ def __init__(self, values: Any, positions: Any = None, plot: Any = None,
116
+ **settings: Any) -> None:
117
+ super().__init__(plot=plot, **settings)
118
+
119
+ def value_scale(self) -> Any:
120
+ """The scale of the value axis (densities are computed in its space)."""
121
+ return stats.axis_scale(axis_of(self, "y"))
122
+
123
+ @property
124
+ def stats(self) -> dict[Any, dict[str, Any]]:
125
+ """``{category: {"grid", "density", "count", "box": BoxStats, "bandwidth",
126
+ "line"}}``: densities per unit of the value axis' scale; ``line``: a category
127
+ without a bandwidth, drawn as a line at its value."""
128
+ return stats.run(self, "violin")
129
+
130
+ # -- paint -----------------------------------------------------------------------
131
+
132
+ def _colors(self, ctx: Any) -> list[Any]:
133
+ return base_colors(self, ctx, self.cols.categories, "violins", items=True,
134
+ saturation=False)
135
+
136
+ def fills(self, ctx: Any) -> list[Any]:
137
+ return fill_colors(self, ctx, self._colors(ctx))
138
+
139
+ def edges(self, ctx: Any) -> list[Any]:
140
+ """Outline and inner color of each violin (``None``: none)."""
141
+ v = self.resolve("edge_color")
142
+ if v != "series":
143
+ return [ctx.color(v)] * len(self.cols.categories)
144
+ sat, a = self.resolve("saturation"), self.resolve("opacity")
145
+ return [with_alpha(desaturate(x, sat) if sat != 1.0 else x, a) if x is not None else None
146
+ for x in self._colors(ctx)]
147
+
148
+ def _half_widths(self) -> dict[Any, float]:
149
+ """Scale factor per category: half-width in category units per unit of density."""
150
+ st = self.stats
151
+ mode = self.resolve("width_scale")
152
+ peaks = {k: (max(s["density"]) if len(s["density"]) else 0.0) for k, s in st.items()
153
+ if not s["line"]}
154
+ lines = {k: 1.0 for k, s in st.items() if s["line"]} # lines: the full width
155
+ if mode == "width":
156
+ return {**{k: (1.0 / p if p else 0.0) for k, p in peaks.items()}, **lines}
157
+ if mode == "count":
158
+ top = max((peaks[k] * st[k]["count"] for k in peaks), default=0.0)
159
+ return {**{k: (st[k]["count"] / top if top else 0.0) for k in peaks}, **lines}
160
+ top = max(peaks.values(), default=0.0)
161
+ return {**{k: (1.0 / top if top else 0.0) for k in peaks}, **lines}
162
+
163
+ # -- the preset --------------------------------------------------------------------
164
+
165
+ SPEC = Spec(
166
+ [Lay("band", ("violin.band", {"table": "#bodies"}), table="bodies", stat="violin",
167
+ position="dodge", key=False, turn=True),
168
+ Lay("rects", "violin.key", table="key", name="key", drawn=False)],
169
+ tables={"bodies": lambda s, b: _bodies(s, b), "key": lambda s, b: Table()},
170
+ key={"fill": "symbol", "stroke": "symbol", "marker": "@key"},
171
+ titles={"values": "y", "categories": "x"}, slots="positions",
172
+ color_sources="items", finish=lambda s, b: _finish(s, b))
173
+
174
+
175
+ def _bodies(s: Violin, b: Any) -> Table | None:
176
+ """The violins with values (``drawn``), in slots shared with the plot's dodged boxes
177
+ and violins; ``scale``: half-width per unit of density."""
178
+ st = s.stats
179
+ cats = s.cols.categories
180
+ drawn = [i for i, k in enumerate(cats) if st[k]["count"]]
181
+ if not drawn:
182
+ return None
183
+ scale = s._half_widths()
184
+ d = Dodge(s, "box", b.flip, list(cats), s.cols["positions"],
185
+ "split" if s.resolve("split") else None)
186
+ if not s.resolve("split"):
187
+ d.side = {"low": -1, "high": 1}.get(s.resolve("side"), 0)
188
+ t: Any = Table(x=[st[cats[i]]["grid"] for i in drawn])
189
+ t.dodge, t.drawn, t.scale = d, drawn, scale
190
+
191
+ def halves(ctx: Any, sign: int) -> list[Any]:
192
+ pl = placed(d, ctx)
193
+ out = []
194
+ for i in drawn:
195
+ k = cats[i]
196
+ center, width = pl.geo[i]
197
+ half = width / 2
198
+ if sign * d.side < 0: # the other half of a split violin
199
+ out.append(pl.back(array.array("d", [center] * len(st[k]["density"]))))
200
+ else:
201
+ out.append(pl.back(array.array("d", (center + sign * v * scale[k] * half
202
+ for v in st[k]["density"]))))
203
+ return out
204
+
205
+ t.lazy["y1"] = lambda ctx: halves(ctx, 1)
206
+ t.lazy["y2"] = lambda ctx: halves(ctx, -1)
207
+ return t
208
+
209
+
210
+ @role("violin.band")
211
+ def _band_paint(s: Violin, ctx: Any, glyph: bool, table: Any = None) -> dict[str, Any]:
212
+ fills, lcs = s.fills(ctx), s.edges(ctx)
213
+ p: dict[str, Any] = {"color": [fills[i] for i in table.drawn],
214
+ "simplify": s.resolve("simplify")}
215
+ lw = s.resolve("edge_width")
216
+ if lw:
217
+ p["stroke"] = [lcs[i] for i in table.drawn]
218
+ p["width"] = lw
219
+ return p
220
+
221
+
222
+ @role("violin.key")
223
+ def _key_paint(s: Violin, ctx: Any, glyph: bool) -> dict[str, Any]:
224
+ f, e = s.fills(ctx), s.edges(ctx)
225
+ lw = s.resolve("edge_width")
226
+ return {"fill": f[0] if f else None, "stroke": e[0] if e else None,
227
+ "line_width": lw if lw else None}
228
+
229
+
230
+ def _finish(s: Violin, b: Any) -> None:
231
+ """Dates on the category axis; the inner marks of each violin."""
232
+ t = b.tables.get("bodies")
233
+ if t is None:
234
+ return
235
+ t.dodge.time = b.role("x") in b.call.times
236
+ inner = s.resolve("inner")
237
+ if inner:
238
+ for i in t.drawn:
239
+ b.call.layers += _Inner(s, i, b.flip, t).layers(inner)
240
+
241
+
242
+ class _Inner:
243
+ """The inner marks of violin ``i`` (layers in the violin's frame)."""
244
+
245
+ def __init__(self, v: Violin, i: int, flip: bool, bodies: Any) -> None:
246
+ self.v, self.i, self.flip, self.d = v, i, flip, bodies.dodge
247
+ self.scale, self.lw = bodies.scale, v.resolve("edge_width")
248
+ self.k = v.cols.categories[i]
249
+ self.st = v.stats[self.k]
250
+
251
+ def centre(self, ctx: Any) -> tuple[float, float]:
252
+ """The violin's centre (a split half's inner marks shift to its side) and width,
253
+ in slot space."""
254
+ c, w = placed(self.d, ctx).geo[self.i]
255
+ return c + (0.0 if not self.d.side else self.d.side * w * 0.06), w
256
+
257
+ def at(self, ctx: Any, xs: list[float]) -> Any:
258
+ """Slot-space values in data units."""
259
+ return placed(self.d, ctx).back(xs)
260
+
261
+ def _layer(self, geom: str, t: Table, part: str, **options: Any) -> Layer:
262
+ return Layer(GEOMS[geom], t, paint=Paint(self.v, "violin.inner", at=self, part=part,
263
+ **options),
264
+ flip=self.flip, key=False, occupy=False, each=geom == "rects")
265
+
266
+ def layers(self, inner: str) -> list[Layer]:
267
+ b = self.st["box"]
268
+ if inner == "box":
269
+ return self._box(b)
270
+ if inner == "quartiles":
271
+ return [self._quartile(q, "solid" if i == 1 else "dashed")
272
+ for i, q in enumerate((b.q1, b.median, b.q3))]
273
+ pts = Table(y=self.v.cols.values[self.i])
274
+ pts.lazy["x"] = lambda ctx: self.at(ctx, [self.centre(ctx)[0]] *
275
+ len(self.v.cols.values[self.i]))
276
+ return [self._layer("points", pts, "points")]
277
+
278
+ def _box(self, b: Any) -> list[Layer]:
279
+ line = Table(y=[b.low, b.high])
280
+ line.lazy["x"] = lambda ctx: self.at(ctx, [self.centre(ctx)[0]] * 2)
281
+ body = Table(y0=[b.q1], y1=[b.q3])
282
+ body.lazy["x0"] = lambda ctx: self.at(ctx, [self.centre(ctx)[0] -
283
+ self.centre(ctx)[1] * 0.03])
284
+ body.lazy["x1"] = lambda ctx: self.at(ctx, [self.centre(ctx)[0] +
285
+ self.centre(ctx)[1] * 0.03])
286
+ dot = Table(y=[b.median])
287
+ dot.lazy["x"] = lambda ctx: self.at(ctx, [self.centre(ctx)[0]])
288
+ return [self._layer("path", line, "whisker"), self._layer("rects", body, "body"),
289
+ self._layer("points", dot, "median")]
290
+
291
+ def _quartile(self, q: float, dash: str) -> Layer:
292
+ t = Table(y=[q, q])
293
+ side = self.d.side
294
+
295
+ def ends(ctx: Any) -> list[float]:
296
+ c, w = self.centre(ctx)
297
+ dd = stats.density_at(self.st, q, self.v.value_scale()) * self.scale[self.k] * w / 2
298
+ return self.at(ctx, [c - dd if side <= 0 else c, c + dd if side >= 0 else c])
299
+ t.lazy["x"] = ends
300
+ return self._layer("path", t, "quartile", dash=dash)
301
+
302
+
303
+ @role("violin.inner")
304
+ def _inner_paint(s: Violin, ctx: Any, glyph: bool, at: Any = None, part: str = "",
305
+ dash: str = "solid") -> Any:
306
+ """The inner marks of a violin: its whisker line, body, median dot, quartile lines or
307
+ points, in its outline color."""
308
+ c = s.edges(ctx)[at.i]
309
+ if part == "whisker":
310
+ return {"color": c, "width": max(at.lw, 0.5)}
311
+ if part == "body":
312
+ return {"fill": c, "stroke": None, "line_width": None}
313
+ if part == "quartile":
314
+ width = at.lw or 0.8
315
+ return {"color": c, "width": width, "dash": dash_of(dash, width)}
316
+ if c is None:
317
+ return None
318
+ if part == "points":
319
+ return Marks(None, 3.0, c, None)
320
+ m, w = at.centre(ctx)
321
+ pl = placed(at.d, ctx)
322
+ bw = abs(pl.px(m + w * 0.03) - pl.px(m - w * 0.03))
323
+ return Marks(None, max(bw * 0.9, 2.0), "white", None)
plot_io/py.typed ADDED
File without changes