plot-io 0.12.0__py3-none-win_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plot_io/__init__.py +73 -0
- plot_io/_methods.py +1691 -0
- plot_io/_native/__init__.py +199 -0
- plot_io/_native/__main__.py +197 -0
- plot_io/_native/_api.py +457 -0
- plot_io/_native/bin/windows-arm64.dll +0 -0
- plot_io/_native/kernels.py +335 -0
- plot_io/_native/manifest.json +49 -0
- plot_io/axis.py +1347 -0
- plot_io/boxes.py +1156 -0
- plot_io/canvas.py +389 -0
- plot_io/color/__init__.py +3 -0
- plot_io/color/data.py +1646 -0
- plot_io/color/maps.py +922 -0
- plot_io/color/values.py +295 -0
- plot_io/color/variants.py +1246 -0
- plot_io/colorbar.py +1426 -0
- plot_io/columns.py +614 -0
- plot_io/decorations.py +920 -0
- plot_io/defaults.py +809 -0
- plot_io/density.py +113 -0
- plot_io/ext.py +68 -0
- plot_io/facets.py +240 -0
- plot_io/figure.py +788 -0
- plot_io/formatters.py +97 -0
- plot_io/genome/__init__.py +17 -0
- plot_io/genome/adapters.py +356 -0
- plot_io/genome/combine.py +123 -0
- plot_io/genome/contacts.py +508 -0
- plot_io/genome/intervals.py +750 -0
- plot_io/genome/lanes.py +191 -0
- plot_io/genome/layout.py +184 -0
- plot_io/genome/links.py +364 -0
- plot_io/genome/pack.py +27 -0
- plot_io/genome/readers.py +145 -0
- plot_io/genome/regions.py +228 -0
- plot_io/genome/scale.py +349 -0
- plot_io/genome/series.py +344 -0
- plot_io/genome/signal.py +621 -0
- plot_io/genome/sites.py +782 -0
- plot_io/genome/sources.py +459 -0
- plot_io/genome/testing.py +161 -0
- plot_io/geometry.py +289 -0
- plot_io/groups.py +308 -0
- plot_io/guides.py +503 -0
- plot_io/helpers/__init__.py +17 -0
- plot_io/helpers/clustermap.py +202 -0
- plot_io/helpers/dendrogram.py +172 -0
- plot_io/helpers/grids.py +304 -0
- plot_io/helpers/qq.py +334 -0
- plot_io/helpers/raincloud.py +249 -0
- plot_io/helpers/tree.py +214 -0
- plot_io/helpers/volcano.py +505 -0
- plot_io/hints.py +125 -0
- plot_io/kernels.py +740 -0
- plot_io/labels.py +566 -0
- plot_io/layers/__init__.py +100 -0
- plot_io/layers/base.py +630 -0
- plot_io/layers/context.py +87 -0
- plot_io/layers/core.py +446 -0
- plot_io/layers/geoms.py +1362 -0
- plot_io/layers/grammar.py +586 -0
- plot_io/layers/paint.py +614 -0
- plot_io/layers/pipeline.py +170 -0
- plot_io/layers/positions.py +431 -0
- plot_io/layers/stats.py +1088 -0
- plot_io/layout.py +1381 -0
- plot_io/legend.py +605 -0
- plot_io/levels.py +509 -0
- plot_io/limits.py +124 -0
- plot_io/lines.py +42 -0
- plot_io/mapped.py +522 -0
- plot_io/markers.py +110 -0
- plot_io/markup.py +128 -0
- plot_io/numbers.py +354 -0
- plot_io/occupancy.py +295 -0
- plot_io/plot.py +597 -0
- plot_io/presets/__init__.py +51 -0
- plot_io/presets/_util.py +153 -0
- plot_io/presets/area.py +145 -0
- plot_io/presets/bar.py +261 -0
- plot_io/presets/box.py +400 -0
- plot_io/presets/catpoints.py +177 -0
- plot_io/presets/contour.py +353 -0
- plot_io/presets/curves.py +30 -0
- plot_io/presets/dots.py +355 -0
- plot_io/presets/ecdf.py +97 -0
- plot_io/presets/errorbar.py +106 -0
- plot_io/presets/errors.py +296 -0
- plot_io/presets/fill.py +74 -0
- plot_io/presets/heatmap.py +322 -0
- plot_io/presets/hexbin.py +198 -0
- plot_io/presets/hist.py +391 -0
- plot_io/presets/hist2d.py +87 -0
- plot_io/presets/kde.py +162 -0
- plot_io/presets/kde2d.py +147 -0
- plot_io/presets/line.py +143 -0
- plot_io/presets/pie.py +502 -0
- plot_io/presets/regression.py +115 -0
- plot_io/presets/scatter.py +194 -0
- plot_io/presets/stem.py +100 -0
- plot_io/presets/violin.py +323 -0
- plot_io/py.typed +0 -0
- plot_io/registry.py +82 -0
- plot_io/render.py +480 -0
- plot_io/rows.py +491 -0
- plot_io/scales.py +103 -0
- plot_io/seams.py +138 -0
- plot_io/settings.py +807 -0
- plot_io/shapes.py +66 -0
- plot_io/sharing.py +232 -0
- plot_io/sizemaps.py +97 -0
- plot_io/spec.py +240 -0
- plot_io/states.py +216 -0
- plot_io/styles/__init__.py +307 -0
- plot_io/styles/library.py +397 -0
- plot_io/tables.py +138 -0
- plot_io/text.py +412 -0
- plot_io/ticks.py +400 -0
- plot_io/times.py +287 -0
- plot_io/titles.py +225 -0
- plot_io/transform.py +114 -0
- plot_io/types.py +13604 -0
- plot_io/units.py +768 -0
- plot_io/validators.py +1029 -0
- plot_io/values.py +190 -0
- plot_io-0.12.0.dist-info/METADATA +215 -0
- plot_io-0.12.0.dist-info/RECORD +130 -0
- plot_io-0.12.0.dist-info/WHEEL +4 -0
- plot_io-0.12.0.dist-info/licenses/LICENSE +28 -0
plot_io/presets/pie.py
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
"""Pie and donut charts, on a plot without axes: wedges (``Polygons``) and labels
|
|
2
|
+
(``Text``)."""
|
|
3
|
+
|
|
4
|
+
import array
|
|
5
|
+
import math
|
|
6
|
+
import warnings
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import vec_io
|
|
10
|
+
|
|
11
|
+
from ..canvas import Canvas
|
|
12
|
+
from .._methods import PieMixin
|
|
13
|
+
from .. import groups
|
|
14
|
+
from ..color.values import ink_for
|
|
15
|
+
from ..numbers import NumberStyle, check_format, format_numbers
|
|
16
|
+
from ..color.values import color
|
|
17
|
+
from ..settings import Setting, boolean, either, literal, number, one_of, table
|
|
18
|
+
from ..units import resolve_raw
|
|
19
|
+
from ..validators import Custom, angle, length
|
|
20
|
+
from ..color.values import with_alpha
|
|
21
|
+
from ..text import Font, font_of, measure
|
|
22
|
+
from ..layers.base import Series, common
|
|
23
|
+
from ..layers.core import Call, Layer, Table
|
|
24
|
+
from ..layers.geoms import GEOMS
|
|
25
|
+
from ..layers.grammar import entry
|
|
26
|
+
from ..layers.paint import Paint, auto_colors, role
|
|
27
|
+
from ..labels import column
|
|
28
|
+
from ._util import given_colors, map_or_color, placed_text
|
|
29
|
+
|
|
30
|
+
__all__ = ["Pie"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
_OFFSET = length(lo=0, percent="radius")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _explode(v: Any) -> Any:
|
|
37
|
+
if isinstance(v, (list, tuple)):
|
|
38
|
+
return [_OFFSET(x) for x in v]
|
|
39
|
+
return _OFFSET(v)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_EXPLODE = Custom(_explode, like=_OFFSET, inner=_OFFSET)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _outline(w: dict[str, Any], cx: float, cy: float, r: float, hole: float, end_seam: bool,
|
|
46
|
+
start_seam: bool) -> Any:
|
|
47
|
+
"""The outline of an unexploded wedge without its seamed radial edges: open paths."""
|
|
48
|
+
a0, a1 = math.radians(w["start"]), math.radians(w["end"])
|
|
49
|
+
point = (lambda a, rr: (cx + rr * math.cos(a), cy - rr * math.sin(a)))
|
|
50
|
+
inner = hole * r
|
|
51
|
+
# edges in order around the wedge: (start point, append to the path, removed)
|
|
52
|
+
edges: list[tuple[tuple[float, float], Any, bool]] = [
|
|
53
|
+
(point(a0, inner), lambda p: p.line_to(*point(a0, r)), start_seam),
|
|
54
|
+
(point(a0, r), lambda p: _arc(p, cx, cy, r, a0, a1), False),
|
|
55
|
+
(point(a1, r), lambda p: p.line_to(*point(a1, inner)), end_seam)]
|
|
56
|
+
if hole:
|
|
57
|
+
edges.append((point(a1, inner), lambda p: _arc(p, cx, cy, inner, a1, a0), False))
|
|
58
|
+
first = next(i for i, e in enumerate(edges) if e[2])
|
|
59
|
+
edges = edges[first + 1:] + edges[:first + 1] # starts after a removed edge
|
|
60
|
+
p = vec_io.Path()
|
|
61
|
+
open_ = False
|
|
62
|
+
for start, add, removed in edges:
|
|
63
|
+
if removed:
|
|
64
|
+
open_ = False
|
|
65
|
+
continue
|
|
66
|
+
if not open_:
|
|
67
|
+
p.move_to(*start)
|
|
68
|
+
open_ = True
|
|
69
|
+
add(p)
|
|
70
|
+
return p
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _arc(path: Any, cx: float, cy: float, r: float, a0: float, a1: float) -> None:
|
|
74
|
+
"""Append an arc of radius ``r`` from angle ``a0`` to ``a1`` (radians, counterclockwise
|
|
75
|
+
on the page: y up) as cubic Béziers of at most 90°."""
|
|
76
|
+
n = max(1, math.ceil(abs(a1 - a0) / (math.pi / 2) - 1e-9))
|
|
77
|
+
step = (a1 - a0) / n
|
|
78
|
+
k = 4.0 / 3.0 * math.tan(step / 4.0)
|
|
79
|
+
for i in range(n):
|
|
80
|
+
t0, t1 = a0 + i * step, a0 + (i + 1) * step
|
|
81
|
+
x0, y0 = cx + r * math.cos(t0), cy - r * math.sin(t0)
|
|
82
|
+
x3, y3 = cx + r * math.cos(t1), cy - r * math.sin(t1)
|
|
83
|
+
x1, y1 = x0 - k * r * math.sin(t0), y0 - k * r * math.cos(t0)
|
|
84
|
+
x2, y2 = x3 + k * r * math.sin(t1), y3 + k * r * math.cos(t1)
|
|
85
|
+
path.curve_to(x1, y1, x2, y2, x3, y3)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Pie(PieMixin, Series):
|
|
89
|
+
"""A pie (or donut, with ``hole``) of ``values``: a sequence, or a dict
|
|
90
|
+
``{label: value}``; the plot is drawn without axes.
|
|
91
|
+
|
|
92
|
+
Wedges start at ``start``; the plot is drawn without axes (``Plot.axes=False``, aspect
|
|
93
|
+
1). ``hole`` makes a donut (``"40%"`` of the radius), ``explode`` moves wedges out
|
|
94
|
+
(``%`` of the radius, one value or one per wedge). Names go outside (beside their
|
|
95
|
+
wedges; a crowded side's in a column beside the pie with elbow leaders, the smallest
|
|
96
|
+
wedges' left out with a warning when the column is taller than the plot), inside, or
|
|
97
|
+
into the legend (``name_position="none"``, the legend then outside right); ``color_map``
|
|
98
|
+
colors them (a map's colors for the wedges); ``value_labels`` writes percents or values inside, or outside with
|
|
99
|
+
a leader line when a wedge is too thin.
|
|
100
|
+
|
|
101
|
+
Example::
|
|
102
|
+
|
|
103
|
+
p = plt.Plot(width=240, height=160)
|
|
104
|
+
p.pie({"apples": 5, "pears": 3, "plums": 2, "figs": 1}, value_labels="percent")
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
SETTINGS = table(
|
|
108
|
+
names=Setting(lambda v: [str(x) for x in v],
|
|
109
|
+
doc="Wedge names (default: the dict's keys)."),
|
|
110
|
+
color_map=Setting(map_or_color,
|
|
111
|
+
doc="Wedge colors: a map (a list of colors or a qualitative map: "
|
|
112
|
+
"taken in turn; another kind: evenly spaced), one color, a "
|
|
113
|
+
"kind (that kind's map) or `\"auto\"` (the plot's next "
|
|
114
|
+
"colors)."),
|
|
115
|
+
start=Setting(angle(), doc="Angle where the first wedge starts (degrees, 0: "
|
|
116
|
+
"3 o'clock, 90: 12 o'clock)."),
|
|
117
|
+
counterclockwise=Setting(boolean, doc="Direction of the wedges."),
|
|
118
|
+
hole=Setting(length(lo=0, percent="radius"), doc="Radius of the hole (`%` of the "
|
|
119
|
+
"radius): a donut."),
|
|
120
|
+
explode=Setting(_EXPLODE, doc="Offset of the wedges from the centre (`%` of the "
|
|
121
|
+
"radius): one value or one per wedge."),
|
|
122
|
+
sort=Setting(boolean, doc="Sort the wedges by decreasing value."),
|
|
123
|
+
name_position=Setting(one_of("outside", "inside", "none"),
|
|
124
|
+
doc="Where the names go: `outside`, `inside` or `none` (in "
|
|
125
|
+
"the legend instead)."),
|
|
126
|
+
value_labels=Setting(either(literal(False), one_of("percent", "value")),
|
|
127
|
+
doc="Write each wedge's `percent` or `value` inside it (outside, "
|
|
128
|
+
"with a leader line, when it does not fit)."),
|
|
129
|
+
label_spacing=Setting(length(positive=True, percent="em"),
|
|
130
|
+
doc="Smallest distance between outside labels on a side."),
|
|
131
|
+
value_label_format=Setting(check_format,
|
|
132
|
+
doc="Number format of the value labels (percentages: of "
|
|
133
|
+
"100)."),
|
|
134
|
+
**groups.Stroke.embed("edge_",
|
|
135
|
+
only=("color", "line_width"),
|
|
136
|
+
checks={"color": groups.series_color},
|
|
137
|
+
docs={"color": "Wedge outline color: a color, `\"series\"` "
|
|
138
|
+
"(each wedge's own color) or `none`.",
|
|
139
|
+
"line_width": "Wedge outline width."}),
|
|
140
|
+
opacity=Setting(number(0, 1), doc="Opacity."),
|
|
141
|
+
**groups.FillStyle.embed(docs={"fill": "Wedge fill: `True` (each wedge's color), a "
|
|
142
|
+
"color, or `False`.",
|
|
143
|
+
"fill_opacity": "Opacity of the fills, times "
|
|
144
|
+
"`opacity` (outlines keep `opacity`)."}),
|
|
145
|
+
seam_color=Setting(color,
|
|
146
|
+
doc="The edges neighbouring wedges share, in this color; `none`: "
|
|
147
|
+
"both outlines as drawn."),
|
|
148
|
+
**common("name", "z", "rasterize", "simplify", "font_size", "font_family", "text_color"),
|
|
149
|
+
**groups.NumberFormat.embed(),
|
|
150
|
+
)
|
|
151
|
+
AXES = False
|
|
152
|
+
|
|
153
|
+
DATA = {"values": "wedges"}
|
|
154
|
+
|
|
155
|
+
def __init__(self, values: Any, plot: Any = None, **settings: Any) -> None:
|
|
156
|
+
if any(not (v >= 0) for v in self.vals if math.isfinite(v)):
|
|
157
|
+
raise ValueError("pie values must be non-negative")
|
|
158
|
+
super().__init__(plot=plot, **settings)
|
|
159
|
+
if not self.plot.is_set("axes"):
|
|
160
|
+
self.plot.set(axes=False)
|
|
161
|
+
if not self.plot.legend.is_placed():
|
|
162
|
+
self.plot.legend.set(side="right")
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def vals(self) -> Any:
|
|
166
|
+
"""The value of each wedge."""
|
|
167
|
+
return self.cols["values"]
|
|
168
|
+
|
|
169
|
+
# -- geometry ---------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
def wedge_names(self) -> list[Any]:
|
|
172
|
+
given = self.resolve("names")
|
|
173
|
+
if given is not None:
|
|
174
|
+
if len(given) != len(self.vals):
|
|
175
|
+
raise ValueError(f"{len(given)} names for {len(self.vals)} wedges")
|
|
176
|
+
return given
|
|
177
|
+
return list(self.cols.categories)
|
|
178
|
+
|
|
179
|
+
def order(self) -> list[int]:
|
|
180
|
+
idx = [i for i in range(len(self.vals)) if math.isfinite(self.vals[i]) and self.vals[i] > 0]
|
|
181
|
+
if self.resolve("sort"):
|
|
182
|
+
idx.sort(key=lambda i: -self.vals[i])
|
|
183
|
+
return idx
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def stats(self) -> list[dict[str, Any]]:
|
|
187
|
+
"""Per wedge (in drawing order): ``index``, ``value``, ``fraction``, ``start`` and
|
|
188
|
+
``end`` angles (degrees, counterclockwise from 3 o'clock)."""
|
|
189
|
+
order = self.order()
|
|
190
|
+
total = math.fsum(self.vals[i] for i in order)
|
|
191
|
+
sign = 1.0 if self.resolve("counterclockwise") else -1.0
|
|
192
|
+
a = self.resolve("start")
|
|
193
|
+
if not isinstance(a, (int, float)):
|
|
194
|
+
raise ValueError(f"Pie: start is not an angle: {self.get('start')!r}")
|
|
195
|
+
out = []
|
|
196
|
+
for i in order:
|
|
197
|
+
frac = self.vals[i] / total if total else 0.0
|
|
198
|
+
b = a + sign * 360.0 * frac
|
|
199
|
+
out.append({"index": i, "value": self.vals[i], "fraction": frac, "start": a, "end": b})
|
|
200
|
+
a = b
|
|
201
|
+
return out
|
|
202
|
+
|
|
203
|
+
def _fraction(self, name: str, raw: Any, r: float | None) -> float:
|
|
204
|
+
"""A length of setting ``name`` as a fraction of the radius ``r`` (px; ``None``:
|
|
205
|
+
unknown, lengths count as 0)."""
|
|
206
|
+
a = resolve_raw(self, name, _OFFSET, raw, of=0.0)
|
|
207
|
+
b = resolve_raw(self, name, _OFFSET, raw, of=1.0)
|
|
208
|
+
if a is None or b is None:
|
|
209
|
+
raise ValueError(f"Pie setting {name!r}: {raw!r} gives no length")
|
|
210
|
+
return b - a + (a / r if r else 0.0)
|
|
211
|
+
|
|
212
|
+
def _explode_of(self, i: int, r: float | None = None) -> float:
|
|
213
|
+
e = self._raw("explode")
|
|
214
|
+
if e is None:
|
|
215
|
+
return 0.0
|
|
216
|
+
if isinstance(e, list):
|
|
217
|
+
if len(e) != len(self.vals):
|
|
218
|
+
raise ValueError(f"{len(e)} explode values for {len(self.vals)} wedges")
|
|
219
|
+
e = e[i]
|
|
220
|
+
return self._fraction("explode", e, r)
|
|
221
|
+
|
|
222
|
+
def _hole(self, r: float) -> float:
|
|
223
|
+
"""``hole`` as a fraction of the radius ``r``."""
|
|
224
|
+
return self._fraction("hole", self._raw("hole"), r)
|
|
225
|
+
|
|
226
|
+
def _reach(self) -> float:
|
|
227
|
+
n = len(self.vals) if self._raw("explode") is not None else 0
|
|
228
|
+
ex = max((self._explode_of(i) for i in range(n)), default=0.0)
|
|
229
|
+
out = self.resolve("name_position") == "outside" or self.resolve("value_labels")
|
|
230
|
+
return 1.0 + ex + (0.45 if out else 0.05)
|
|
231
|
+
|
|
232
|
+
def wedge_colors(self, ctx: Any) -> list[Any]:
|
|
233
|
+
"""Colors in value order (one per value)."""
|
|
234
|
+
n = len(self.vals)
|
|
235
|
+
given = given_colors(self, n)
|
|
236
|
+
if given is not None:
|
|
237
|
+
given = given if isinstance(given, list) else [given]
|
|
238
|
+
cols = [ctx.color(given[i % len(given)]) for i in range(n)]
|
|
239
|
+
else:
|
|
240
|
+
cols = auto_colors(self, ctx)
|
|
241
|
+
cols = (cols * (n // max(len(cols), 1) + 1))[:n]
|
|
242
|
+
a = self._opacity()
|
|
243
|
+
return [with_alpha(c, a) for c in cols]
|
|
244
|
+
|
|
245
|
+
def _opacity(self) -> float:
|
|
246
|
+
a = self.resolve("opacity")
|
|
247
|
+
if not isinstance(a, (int, float)):
|
|
248
|
+
raise ValueError(f"Pie: opacity is not a number: {self.get('opacity')!r}")
|
|
249
|
+
return a
|
|
250
|
+
|
|
251
|
+
def wedge_fills(self, ctx: Any) -> list[Any]:
|
|
252
|
+
"""Fills in value order: the colors at ``fill_opacity``."""
|
|
253
|
+
fc, f = groups.fill_parts(self)
|
|
254
|
+
cols = self.wedge_colors(ctx)
|
|
255
|
+
if fc is not None:
|
|
256
|
+
cols = [with_alpha(ctx.color(fc), self._opacity())] * len(cols)
|
|
257
|
+
return [with_alpha(c, (c[3] if isinstance(c, tuple) and len(c) > 3 else 1.0) * f)
|
|
258
|
+
for c in cols]
|
|
259
|
+
|
|
260
|
+
def wedge_edges(self, ctx: Any) -> list[Any]:
|
|
261
|
+
"""Outline colors in value order (``None``: no outline)."""
|
|
262
|
+
v = self.resolve("edge_color")
|
|
263
|
+
if v == "series":
|
|
264
|
+
return self.wedge_colors(ctx)
|
|
265
|
+
return [ctx.color(v)] * len(self.vals)
|
|
266
|
+
|
|
267
|
+
# -- the preset --------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
def _center(self, ctx: Any) -> tuple[float, float, float]:
|
|
270
|
+
cx, cy = ctx.xmap(0.0), ctx.ymap(0.0)
|
|
271
|
+
r = abs(ctx.xmap(1.0) - cx)
|
|
272
|
+
return cx, cy, r
|
|
273
|
+
|
|
274
|
+
def _wedges(self, ctx: Any) -> dict[str, Any]:
|
|
275
|
+
"""The wedges in page units: paths, fills, outline colors, and with seams the
|
|
276
|
+
outlines (without the seamed radial edges) and the seams."""
|
|
277
|
+
cx, cy, r = self._center(ctx)
|
|
278
|
+
hole = self._hole(r)
|
|
279
|
+
cols, edges = self.wedge_fills(ctx), self.wedge_edges(ctx)
|
|
280
|
+
paths, fills, strokes = [], [], []
|
|
281
|
+
st = self.stats
|
|
282
|
+
for w in st:
|
|
283
|
+
i = w["index"]
|
|
284
|
+
a0, a1 = math.radians(w["start"]), math.radians(w["end"])
|
|
285
|
+
mid = (a0 + a1) / 2
|
|
286
|
+
ex = self._explode_of(i, r) * r
|
|
287
|
+
ox, oy = cx + ex * math.cos(mid), cy - ex * math.sin(mid)
|
|
288
|
+
p = vec_io.Path()
|
|
289
|
+
if w["fraction"] >= 1.0 - 1e-12:
|
|
290
|
+
p.move_to(ox + r, oy)
|
|
291
|
+
_arc(p, ox, oy, r, 0.0, 2 * math.pi)
|
|
292
|
+
p.close()
|
|
293
|
+
if hole:
|
|
294
|
+
p.move_to(ox + hole * r, oy)
|
|
295
|
+
_arc(p, ox, oy, hole * r, 2 * math.pi, 0.0)
|
|
296
|
+
p.close()
|
|
297
|
+
else:
|
|
298
|
+
p.move_to(ox + r * math.cos(a0), oy - r * math.sin(a0))
|
|
299
|
+
_arc(p, ox, oy, r, a0, a1)
|
|
300
|
+
if hole:
|
|
301
|
+
p.line_to(ox + hole * r * math.cos(a1), oy - hole * r * math.sin(a1))
|
|
302
|
+
_arc(p, ox, oy, hole * r, a1, a0)
|
|
303
|
+
else:
|
|
304
|
+
p.line_to(ox, oy)
|
|
305
|
+
p.close()
|
|
306
|
+
paths.append(p)
|
|
307
|
+
fills.append(cols[i])
|
|
308
|
+
strokes.append(edges[i])
|
|
309
|
+
lw = self.resolve("edge_width")
|
|
310
|
+
seam = ctx.color(self.resolve("seam_color"))
|
|
311
|
+
seamed = set() # wedges k whose end touches wedge k + 1
|
|
312
|
+
if seam is not None and lw and len(st) > 1:
|
|
313
|
+
for k, w in enumerate(st):
|
|
314
|
+
nxt = st[(k + 1) % len(st)]
|
|
315
|
+
if not (self._explode_of(w["index"], r) or self._explode_of(nxt["index"], r)):
|
|
316
|
+
seamed.add(k) # exploded wedges do not touch
|
|
317
|
+
out: dict[str, Any] = {"paths": paths, "fill": fills, "stroke": strokes,
|
|
318
|
+
"line_width": lw, "simplify": self.resolve("simplify")}
|
|
319
|
+
if not seamed:
|
|
320
|
+
return out
|
|
321
|
+
n = len(st)
|
|
322
|
+
outlines = []
|
|
323
|
+
for k, w in enumerate(st):
|
|
324
|
+
end, start = k in seamed, (k - 1) % n in seamed
|
|
325
|
+
whole = not (end or start) or w["fraction"] >= 1.0 - 1e-12
|
|
326
|
+
outlines.append(paths[k] if whole else _outline(w, cx, cy, r, hole, end, start))
|
|
327
|
+
segs: list[tuple[float, ...]] = []
|
|
328
|
+
for k in sorted(seamed):
|
|
329
|
+
a = math.radians(st[k]["end"])
|
|
330
|
+
c, s_ = math.cos(a), -math.sin(a)
|
|
331
|
+
segs.append((cx + hole * r * c, cy + hole * r * s_, cx + r * c, cy + r * s_, lw))
|
|
332
|
+
out.update(outlines=outlines, seams=segs, seam=seam)
|
|
333
|
+
return out
|
|
334
|
+
|
|
335
|
+
def layers(self) -> Call:
|
|
336
|
+
call = Call(self)
|
|
337
|
+
key = id(self)
|
|
338
|
+
|
|
339
|
+
t = Table()
|
|
340
|
+
t.lazy["page_paths"] = lambda ctx: _wedges(self, ctx)["paths"]
|
|
341
|
+
|
|
342
|
+
call.layers.append(Layer(GEOMS["polygons"], t, paint=Paint(self, "pie.wedges"),
|
|
343
|
+
stat="identity", key=False))
|
|
344
|
+
|
|
345
|
+
def font(ctx: Any) -> Any:
|
|
346
|
+
return font_of(self, self.resolve("font_size"),
|
|
347
|
+
color=ctx.color(self.resolve("text_color")))
|
|
348
|
+
|
|
349
|
+
def labels(ctx: Any) -> list[Any]:
|
|
350
|
+
cx, cy, r = self._center(ctx)
|
|
351
|
+
f = font(ctx)
|
|
352
|
+
inside, placed = self._labels(ctx.canvas, cx, cy, r, f, self.wedge_fills(ctx))
|
|
353
|
+
return inside + self._outside_labels(placed, f, cx, r, ctx.rect[1], ctx.rect[3],
|
|
354
|
+
True)
|
|
355
|
+
|
|
356
|
+
call.layers.append(placed_text(self, key, labels, font))
|
|
357
|
+
if self.resolve("name_position") == "none":
|
|
358
|
+
names = self.wedge_names()
|
|
359
|
+
if names:
|
|
360
|
+
for k, w in enumerate(self.stats):
|
|
361
|
+
key_layer = Layer(GEOMS["rects"], Table(),
|
|
362
|
+
paint=Paint(self, "pie.key", index=k))
|
|
363
|
+
call.entries.append(entry(self, names[w["index"]], [key_layer],
|
|
364
|
+
fill="symbol", stroke="symbol",
|
|
365
|
+
marker=key_layer.paint))
|
|
366
|
+
call.axes = False
|
|
367
|
+
call.color_count = 0 if given_colors(self, len(self.vals)) is not None \
|
|
368
|
+
else len(self.vals)
|
|
369
|
+
r = self._reach()
|
|
370
|
+
call.extra = {"x": array.array("d", [-r, r]), "y": array.array("d", [-r, r])}
|
|
371
|
+
call.exact = {"x", "y"}
|
|
372
|
+
call.overhang = self.overhang
|
|
373
|
+
return call
|
|
374
|
+
|
|
375
|
+
def overhang(self, fonts: Any, w: float, h: float) -> tuple[float, float]:
|
|
376
|
+
"""How far (px) the outside labels reach past the left and right of axes ``w`` ×
|
|
377
|
+
``h``."""
|
|
378
|
+
r = min(w, h) / (2 * self._reach())
|
|
379
|
+
font = font_of(self, self.resolve("font_size"))
|
|
380
|
+
left = right = 0.0
|
|
381
|
+
placed = self._labels(fonts, w / 2, h / 2, r, font, None)[1]
|
|
382
|
+
for text, tx, _, _, ha, *_ in self._outside_labels(placed, font, w / 2, r, 0.0, h):
|
|
383
|
+
tw = measure(fonts, text, font)[0]
|
|
384
|
+
if ha == "right":
|
|
385
|
+
left = max(left, tw - tx)
|
|
386
|
+
else:
|
|
387
|
+
right = max(right, tx + tw - w)
|
|
388
|
+
return left, right
|
|
389
|
+
|
|
390
|
+
def _labels(self, canvas: Canvas, cx: float, cy: float, r: float, font: Font,
|
|
391
|
+
cols: list[Any] | None) -> tuple[list[Any], list[list[Any]]]:
|
|
392
|
+
"""The labels inside the wedges (``cols``: the wedge fills; ``None``: measure only)
|
|
393
|
+
as ``(text, x, y, color, ha, va)``, and the outside ones, for
|
|
394
|
+
:meth:`_outside_labels`."""
|
|
395
|
+
hole = self._hole(r)
|
|
396
|
+
names = self.wedge_names()
|
|
397
|
+
pos = self.resolve("name_position")
|
|
398
|
+
vl = self.resolve("value_labels")
|
|
399
|
+
stats = self.stats
|
|
400
|
+
texts: list[Any] = [None] * len(stats)
|
|
401
|
+
placed: list[list[Any]] = []
|
|
402
|
+
inside: list[Any] = []
|
|
403
|
+
if vl:
|
|
404
|
+
vals = [100.0 * w["fraction"] if vl == "percent" else w["value"] for w in stats]
|
|
405
|
+
style = NumberStyle.of(self)
|
|
406
|
+
fmt = self.resolve("value_label_format")
|
|
407
|
+
if vl == "percent" and fmt == "auto":
|
|
408
|
+
style.suffix = style.suffix or "%"
|
|
409
|
+
if style.precision is None:
|
|
410
|
+
style.precision = 2
|
|
411
|
+
texts = format_numbers(vals, fmt, style)
|
|
412
|
+
for k, w in enumerate(stats):
|
|
413
|
+
i = w["index"]
|
|
414
|
+
a0, a1 = math.radians(w["start"]), math.radians(w["end"])
|
|
415
|
+
mid = (a0 + a1) / 2
|
|
416
|
+
ex = self._explode_of(i, r) * r
|
|
417
|
+
ox, oy = cx + ex * math.cos(mid), cy - ex * math.sin(mid)
|
|
418
|
+
ux, uy = math.cos(mid), -math.sin(mid)
|
|
419
|
+
side = "left" if ux >= 0 else "right"
|
|
420
|
+
outside_texts = []
|
|
421
|
+
if names and pos == "outside":
|
|
422
|
+
outside_texts.append(names[i])
|
|
423
|
+
if names and pos == "inside" and cols is not None:
|
|
424
|
+
inside.append((names[i], ox + ux * r * (0.5 + hole / 2),
|
|
425
|
+
oy + uy * r * (0.5 + hole / 2), ink_for(cols[i]), "center",
|
|
426
|
+
"bottom" if vl else "center"))
|
|
427
|
+
if texts[k] is not None:
|
|
428
|
+
rin = r * (0.5 + hole / 2) if hole else r * 0.62
|
|
429
|
+
tw, ta, td = measure(canvas, texts[k], font)
|
|
430
|
+
arc_len = abs(a1 - a0) * rin
|
|
431
|
+
fits = arc_len >= 1.2 * max(tw, ta + td) and (1 - hole) * r >= 1.1 * (ta + td)
|
|
432
|
+
if fits:
|
|
433
|
+
if cols is not None:
|
|
434
|
+
inside.append((texts[k], ox + ux * rin, oy + uy * rin,
|
|
435
|
+
ink_for(cols[i]), "center",
|
|
436
|
+
"top" if names and pos == "inside" else "center"))
|
|
437
|
+
else:
|
|
438
|
+
outside_texts.append(texts[k])
|
|
439
|
+
if outside_texts:
|
|
440
|
+
text = " ".join(str(t) for t in outside_texts) if all(
|
|
441
|
+
isinstance(t, str) for t in outside_texts) else outside_texts[0]
|
|
442
|
+
lead = not (names and pos == "outside") or len(outside_texts) > 1
|
|
443
|
+
placed.append([oy + uy * r * 1.12, ox + ux * r * 1.12, side, text, lead,
|
|
444
|
+
(ox + ux * r, oy + uy * r), w["value"]])
|
|
445
|
+
return inside, placed
|
|
446
|
+
|
|
447
|
+
def _outside_labels(self, placed: list[list[Any]], font: Font, cx: float, r: float,
|
|
448
|
+
lo: float, hi: float, warn: bool = False) -> list[Any]:
|
|
449
|
+
"""The outside labels of each side: beside their wedges when they keep
|
|
450
|
+
``label_spacing`` apart within ``lo``–``hi`` (page y), else in a column beside the
|
|
451
|
+
pie with elbow leaders (:func:`~plot_io.labels.column`), the smallest wedges'
|
|
452
|
+
left out (with a warning: ``warn``) when the column is taller than ``lo``–``hi``."""
|
|
453
|
+
gap = self.resolve("label_spacing")
|
|
454
|
+
n = len(self.vals) if self._raw("explode") is not None else 0
|
|
455
|
+
ex = max((self._explode_of(i, r) for i in range(n)), default=0.0)
|
|
456
|
+
out: list[Any] = []
|
|
457
|
+
dropped = 0
|
|
458
|
+
for side in ("left", "right"):
|
|
459
|
+
items = sorted((p for p in placed if p[2] == side), key=lambda p: p[0])
|
|
460
|
+
ys = [p[0] for p in items]
|
|
461
|
+
if all(b - a >= gap - 1e-9 for a, b in zip(ys, ys[1:])) and all(
|
|
462
|
+
lo + gap / 2 <= y <= hi - gap / 2 for y in ys):
|
|
463
|
+
for ty, tx, _, text, lead, (ax, ay), _ in items:
|
|
464
|
+
leader = (ax, ay, tx - (3 if side == "left" else -3), ty) if lead else None
|
|
465
|
+
out.append((text, tx, ty, font.color, side, "center", 0.0, None, leader))
|
|
466
|
+
continue
|
|
467
|
+
x = cx + (1 if side == "left" else -1) * r * (1.12 + ex)
|
|
468
|
+
got = column([p[5] for p in items], ys, [gap] * len(items), x,
|
|
469
|
+
max(0.06 * r, 4.0), lo, hi, [p[6] for p in items])
|
|
470
|
+
for p, g in zip(items, got):
|
|
471
|
+
if g is None:
|
|
472
|
+
dropped += 1
|
|
473
|
+
continue
|
|
474
|
+
out.append((p[3], x, g[0], font.color, side, "center", 0.0, None, g[1]))
|
|
475
|
+
if dropped and warn:
|
|
476
|
+
warnings.warn(f"pie: {dropped} outside labels do not fit beside the pie; the "
|
|
477
|
+
"smallest wedges' are left out", UserWarning, stacklevel=2)
|
|
478
|
+
return out
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _wedges(s: Pie, ctx: Any) -> dict[str, Any]:
|
|
482
|
+
from ._util import memo
|
|
483
|
+
return memo(ctx, (id(s), "wedges"), lambda: s._wedges(ctx))
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
@role("pie.wedges")
|
|
487
|
+
def _wedges_paint(s: Pie, ctx: Any, glyph: bool) -> dict[str, Any]:
|
|
488
|
+
p = dict(_wedges(s, ctx))
|
|
489
|
+
p.pop("paths")
|
|
490
|
+
return p
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
@role("pie.key")
|
|
494
|
+
def _key_paint(s: Pie, ctx: Any, glyph: bool, index: int = 0) -> dict[str, Any]:
|
|
495
|
+
"""The legend rectangle of wedge ``index``."""
|
|
496
|
+
st = s.stats
|
|
497
|
+
if index >= len(st):
|
|
498
|
+
return {"fill": None, "stroke": None, "line_width": None}
|
|
499
|
+
k = st[index]["index"]
|
|
500
|
+
edge = s.wedge_edges(ctx)[k] if s.resolve("edge_width") else None
|
|
501
|
+
return {"fill": s.wedge_fills(ctx)[k], "stroke": edge,
|
|
502
|
+
"line_width": s.resolve("edge_width") if edge is not None else None}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Regression (stat ``regression``): a fitted curve (``Path``, polynomial or LOWESS) with a
|
|
2
|
+
confidence band (``Band``) and the points (``Points``)."""
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .._methods import RegressionMixin
|
|
7
|
+
from .. import columns, groups
|
|
8
|
+
from ..color.values import color
|
|
9
|
+
from ..layers import stats
|
|
10
|
+
from ..layers.base import Series, 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, boolean, either, literal, number, one_of, pair, table
|
|
15
|
+
from ..transform import settings_both as coord_settings
|
|
16
|
+
|
|
17
|
+
__all__ = ["Regression", "t_quantile"]
|
|
18
|
+
|
|
19
|
+
t_quantile = stats.t_quantile
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Regression(RegressionMixin, Series):
|
|
23
|
+
"""A regression of ``y`` on ``x``: a polynomial of ``order`` (least squares) with a
|
|
24
|
+
confidence band of the mean, or a LOWESS curve; ``scatter`` also draws the points
|
|
25
|
+
(numbers per point in ``marker_color`` map through the series' color axis).
|
|
26
|
+
|
|
27
|
+
The band is Student's t. ``lowess=True``: no band; above 10,000 points it fits at
|
|
28
|
+
points 1 % of the x range apart and interpolates between them (as statsmodels'
|
|
29
|
+
``delta``). ``logx`` fits against log x; ``range`` sets the x range of the polynomial
|
|
30
|
+
curve (default the data's), drawn in ``points`` points. With fewer distinct x values
|
|
31
|
+
than ``order`` + 1 the order is lowered (one distinct x: no fit), with a warning. The
|
|
32
|
+
points take marker settings with the ``marker_`` prefix. With ``weights``, weighted
|
|
33
|
+
least squares, the band with the weight total minus the coefficients as degrees of
|
|
34
|
+
freedom (as the repeated rows); ``lowess=True`` takes no weights.
|
|
35
|
+
``regression.stats``: ``coefficients`` (lowest power first), ``r2``, ``se``, and the
|
|
36
|
+
curve.
|
|
37
|
+
|
|
38
|
+
Example::
|
|
39
|
+
|
|
40
|
+
rng = random.Random(1)
|
|
41
|
+
x = [rng.uniform(0, 10) for _ in range(60)]
|
|
42
|
+
p = plt.Plot(width=240, height=160)
|
|
43
|
+
p.regression(x, [0.5 * v + rng.gauss(0, 1) for v in x])
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
SETTINGS = table(
|
|
47
|
+
**coord_settings(),
|
|
48
|
+
order=Setting(number(lo=1, hi=5, integer=True), doc="Polynomial order (1–5)."),
|
|
49
|
+
lowess=Setting(boolean, doc="Fit a LOWESS curve instead (no band)."),
|
|
50
|
+
frac=Setting(number(0, 1), doc="Fraction of the points in each LOWESS fit."),
|
|
51
|
+
weights=Setting(lambda v: columns.numbers(v, "weights"),
|
|
52
|
+
doc="A weight per point, counted as repeated points (weighted least "
|
|
53
|
+
"squares; not with `lowess`)."),
|
|
54
|
+
ci=Setting(either(literal(None), number(0, 100)),
|
|
55
|
+
doc="Confidence level of the band (%); `None`: no band."),
|
|
56
|
+
logx=Setting(boolean, doc="Fit y against log(x) (x > 0)."),
|
|
57
|
+
scatter=Setting(boolean, doc="Draw the points too."),
|
|
58
|
+
range=Setting(either(literal(None), pair(number())),
|
|
59
|
+
doc="x range of the fitted polynomial (and band); default the "
|
|
60
|
+
"data range."),
|
|
61
|
+
points=Setting(number(lo=2, hi=65536, integer=True), doc="Points of the fitted curve."),
|
|
62
|
+
color=Setting(color, doc="Line, band and point color; default the plot's next color."),
|
|
63
|
+
**groups.Stroke.embed(only=("line_width", "line_style"),
|
|
64
|
+
inherited=("line_width",),
|
|
65
|
+
docs={"line_width": "Line width; default 1.4 × the plot's.",
|
|
66
|
+
"line_style": "Line style or dash list."}),
|
|
67
|
+
band_opacity=Setting(number(0, 1), doc="Opacity of the band."),
|
|
68
|
+
**groups.Marker.embed("marker_"),
|
|
69
|
+
**common("color_map"),
|
|
70
|
+
orientation=Setting(one_of("vertical", "horizontal"),
|
|
71
|
+
doc="`vertical` (y on x) or `horizontal` (the fit drawn with x "
|
|
72
|
+
"along the y axis)."),
|
|
73
|
+
**common("name", "opacity", "z", "rasterize", "simplify", "font_size", "font_family",
|
|
74
|
+
"text_color"),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
DATA = {"x": "numbers", "y": "numbers"}
|
|
78
|
+
x = column("x", "x of each point (categories at their index among their own).")
|
|
79
|
+
y = column("y", "y of each point (as x).")
|
|
80
|
+
|
|
81
|
+
def __init__(self, x: Any, y: Any, plot: Any = None, **settings: Any) -> None:
|
|
82
|
+
super().__init__(plot=plot, **settings)
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def stats(self) -> dict[str, Any]:
|
|
86
|
+
"""``coefficients`` (lowest power first, in x or log x), ``r2``, ``se`` (residual
|
|
87
|
+
standard error), and the curve: ``x``, ``y``, ``low``, ``high``."""
|
|
88
|
+
return stats.run(self, "regression")
|
|
89
|
+
|
|
90
|
+
def density_points(self) -> tuple[Any, Any]:
|
|
91
|
+
"""The points' positions on the plot's axes (a density in ``marker_color``)."""
|
|
92
|
+
return xy_density_points(self, self.x, self.y)
|
|
93
|
+
|
|
94
|
+
SPEC = Spec(
|
|
95
|
+
[Lay("points", ("marker", {"prefix": "marker_", "color": "faded", "when": "scatter",
|
|
96
|
+
"n": lambda s: len(s.x)}),
|
|
97
|
+
key=False, occupy=lambda s: bool(s.resolve("scatter"))),
|
|
98
|
+
Lay("band", ("band", {"color": "faded", "opacity": "band_opacity"}), table="band",
|
|
99
|
+
stat="regression", glyph="band"),
|
|
100
|
+
# the curve stays within the data's and the band's extent (it does not widen the
|
|
101
|
+
# axes)
|
|
102
|
+
Lay("path", ("stroke", {"color": "faded", "dash": True}), table="curve",
|
|
103
|
+
aes={"x": "curve_x", "y": "curve_y"}, stat="regression", bounds=False)],
|
|
104
|
+
tables={"band": lambda s, b: _band(s.stats), "curve": lambda s, b: _curve(s.stats)},
|
|
105
|
+
key={"stroke": "faded"}, color_sources="scatter")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _band(st: dict[str, Any]) -> Table | None:
|
|
109
|
+
if not len(st["x"]) or st["low"] is None:
|
|
110
|
+
return None
|
|
111
|
+
return Table(x=st["x"], y1=st["low"], y2=st["high"])
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _curve(st: dict[str, Any]) -> Table | None:
|
|
115
|
+
return Table(curve_x=st["x"], curve_y=st["y"]) if len(st["x"]) else None
|