plot-io 0.1.0__py3-none-any.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 (65) hide show
  1. plot_io/__init__.py +66 -0
  2. plot_io/_backend.py +114 -0
  3. plot_io/axis.py +574 -0
  4. plot_io/colorbar.py +342 -0
  5. plot_io/decorations.py +496 -0
  6. plot_io/defaults.py +766 -0
  7. plot_io/density.py +97 -0
  8. plot_io/enums.py +63 -0
  9. plot_io/facets.py +184 -0
  10. plot_io/figure.py +331 -0
  11. plot_io/labels.py +63 -0
  12. plot_io/legend.py +498 -0
  13. plot_io/limits.py +92 -0
  14. plot_io/maps/__init__.py +365 -0
  15. plot_io/maps/data.py +333 -0
  16. plot_io/markers.py +117 -0
  17. plot_io/numbers.py +289 -0
  18. plot_io/occupancy.py +219 -0
  19. plot_io/palettes.py +142 -0
  20. plot_io/plot.py +435 -0
  21. plot_io/render.py +1122 -0
  22. plot_io/scales.py +93 -0
  23. plot_io/series/__init__.py +498 -0
  24. plot_io/series/area.py +181 -0
  25. plot_io/series/bar.py +294 -0
  26. plot_io/series/box.py +401 -0
  27. plot_io/series/catdata.py +23 -0
  28. plot_io/series/catpoints.py +182 -0
  29. plot_io/series/contour.py +294 -0
  30. plot_io/series/curves.py +128 -0
  31. plot_io/series/ecdf.py +143 -0
  32. plot_io/series/errorbar.py +111 -0
  33. plot_io/series/errors.py +100 -0
  34. plot_io/series/fill.py +168 -0
  35. plot_io/series/heatmap.py +304 -0
  36. plot_io/series/hexbin.py +216 -0
  37. plot_io/series/hist.py +379 -0
  38. plot_io/series/hist2d.py +163 -0
  39. plot_io/series/kde.py +228 -0
  40. plot_io/series/kde2d.py +153 -0
  41. plot_io/series/line.py +173 -0
  42. plot_io/series/pie.py +304 -0
  43. plot_io/series/points.py +341 -0
  44. plot_io/series/regression.py +250 -0
  45. plot_io/series/scatter.py +103 -0
  46. plot_io/series/stem.py +112 -0
  47. plot_io/series/violin.py +321 -0
  48. plot_io/settings.py +281 -0
  49. plot_io/shapes.py +57 -0
  50. plot_io/sharing.py +160 -0
  51. plot_io/slots.py +52 -0
  52. plot_io/stacking.py +63 -0
  53. plot_io/stats.py +140 -0
  54. plot_io/style.py +71 -0
  55. plot_io/styles/__init__.py +234 -0
  56. plot_io/styles/library.py +732 -0
  57. plot_io/tables.py +141 -0
  58. plot_io/text.py +109 -0
  59. plot_io/ticks.py +284 -0
  60. plot_io/times.py +199 -0
  61. plot_io/values.py +161 -0
  62. plot_io-0.1.0.dist-info/METADATA +102 -0
  63. plot_io-0.1.0.dist-info/RECORD +65 -0
  64. plot_io-0.1.0.dist-info/WHEEL +4 -0
  65. plot_io-0.1.0.dist-info/licenses/LICENSE +28 -0
@@ -0,0 +1,103 @@
1
+ """Scatter: markers at points."""
2
+
3
+ import array
4
+ from typing import Any
5
+
6
+ from .. import _backend
7
+ from ..settings import Setting, table
8
+ from . import Series, common
9
+ from .errors import error_value
10
+ from .points import (MarkerStyle, density_points, marker_settings, order_index,
11
+ order_settings, permuted)
12
+
13
+ __all__ = ["Scatter"]
14
+
15
+
16
+ def _check_xy(x: Any, y: Any) -> tuple[array.array, array.array]:
17
+ xs, ys = _backend.doubles(x), _backend.doubles(y)
18
+ if len(xs) != len(ys):
19
+ raise ValueError(f"x has {len(xs)} values and y {len(ys)}")
20
+ return xs, ys
21
+
22
+
23
+ class Scatter(Series):
24
+ """Markers at points ``(x, y)``.
25
+
26
+ ``color``, ``line_color``, ``size``, ``marker`` and ``alpha`` take one value or one per
27
+ point. Numbers per point in ``color``/``line_color`` are mapped through ``color_map``;
28
+ in ``size``, through ``size_map`` when it is set; ``"density"`` maps the points'
29
+ density.
30
+ """
31
+
32
+ SETTINGS = table(
33
+ **marker_settings(),
34
+ **common("color_map", "size_map", "size_legend"),
35
+ **order_settings(),
36
+ x_error=Setting(error_value, doc="Horizontal error bars: a value, a sequence, "
37
+ "`(low, high)` or `ErrorBars`."),
38
+ y_error=Setting(error_value, doc="Vertical error bars."),
39
+ **common("name", "z", "rasterize", "simplify", "font_size", "font_family",
40
+ "text_color"),
41
+ )
42
+
43
+ def __init__(self, x: Any, y: Any, plot: Any = None, **settings: Any) -> None:
44
+ self.x, self.y = _check_xy(x, y)
45
+ super().__init__(plot=plot, **settings)
46
+
47
+ def _error(self, role: str) -> Any:
48
+ e = self.get("x_error" if role == "x" else "y_error")
49
+ if e is not None:
50
+ e.bind(len(self.x))
51
+ return e
52
+
53
+ def values(self, role: str) -> Any:
54
+ v = self.x if role == "x" else self.y
55
+ e = self._error(role)
56
+ if e is None:
57
+ return v
58
+ lo, hi = e.ends(v)
59
+ out = array.array("d", v)
60
+ out.extend(lo)
61
+ out.extend(hi)
62
+ return out
63
+
64
+ def legend_points(self) -> tuple[Any, Any]:
65
+ return self.x, self.y
66
+
67
+ def _style(self, ctx: Any, glyph: bool = False) -> MarkerStyle:
68
+ return MarkerStyle(self, ctx, "", self.base_color(ctx), len(self.x), glyph)
69
+
70
+ def draw(self, ctx: Any) -> None:
71
+ idx = order_index(self, len(self.x))
72
+ style = MarkerStyle(self, ctx, "", self.base_color(ctx), len(self.x), False, idx)
73
+ simplify = self.simplify_arg()
74
+ glyph = self._style(ctx, True)
75
+ base = glyph.first_color("fill") or glyph.first_color("stroke")
76
+ for role in ("x", "y"):
77
+ e = self._error(role)
78
+ if e is not None:
79
+ e.draw(ctx, self.x, self.y, role == "x", base, 0.8, simplify, style.opacity)
80
+ xs = ctx.xmap.many(self.x)
81
+ ys = ctx.ymap.many(self.y)
82
+ if idx is not None:
83
+ xs, ys = permuted(xs, idx), permuted(ys, idx)
84
+ style.draw(ctx.canvas, xs, ys, simplify)
85
+
86
+ @property
87
+ def stats(self) -> dict[str, Any]:
88
+ """Derived data: ``{"density": values}`` when the series maps a density."""
89
+ from .points import density_stats
90
+ return density_stats(self)
91
+
92
+ def density_points(self) -> tuple[Any, Any]:
93
+ return density_points(self, self.x, self.y)
94
+
95
+ def zero_default(self, role: str) -> bool:
96
+ return True
97
+
98
+ def legend_colors(self, ctx: Any) -> tuple[Any, Any]:
99
+ style = self._style(ctx, True)
100
+ return style.first_color("fill"), style.first_color("stroke")
101
+
102
+ def legend_glyph(self, ctx: Any, x: float, y: float, w: float, h: float) -> None:
103
+ self._style(ctx, True).glyph(ctx.canvas, x + w / 2, y + h / 2, h * 1.1)
plot_io/series/stem.py ADDED
@@ -0,0 +1,112 @@
1
+ """Stems: lines from a baseline to each value, ending in a marker (lollipops)."""
2
+
3
+ import array
4
+ from typing import Any
5
+
6
+ from .. import _backend
7
+ from ..settings import Setting, color, number, one_of, table
8
+ from ..style import with_alpha
9
+ from . import Series, common
10
+ from .points import MarkerStyle, marker_settings
11
+
12
+ __all__ = ["Stem"]
13
+
14
+
15
+ class Stem(Series):
16
+ """Stems from ``baseline`` to ``y`` at ``x`` (or at 0, 1, … when ``y`` is omitted)."""
17
+
18
+ SETTINGS = table(
19
+ **marker_settings("marker_"),
20
+ color=Setting(color, doc="Stem and marker color; default: the next palette color."),
21
+ line_width=Setting(number(lo=0), inherited=True, doc="Stem width (pt).", length=True),
22
+ baseline=Setting(number(), doc="Value the stems start from."),
23
+ baseline_color=Setting(color, doc="Baseline color; `none`: no baseline."),
24
+ baseline_width=Setting(number(lo=0), doc="Baseline width (pt).", length=True),
25
+ orientation=Setting(one_of("vertical", "horizontal"),
26
+ doc="`vertical` stems (values on y) or `horizontal`."),
27
+ **common("name", "alpha", "z", "rasterize", "simplify", "font_size", "font_family",
28
+ "text_color"),
29
+ )
30
+
31
+ def __init__(self, x: Any, y: Any = None, plot: Any = None, **settings: Any) -> None:
32
+ if y is None:
33
+ y = x
34
+ x = range(len(_backend.doubles(y)))
35
+ self.x, self.y = _backend.doubles(x), _backend.doubles(y)
36
+ if len(self.x) != len(self.y):
37
+ raise ValueError(f"x has {len(self.x)} values and y {len(self.y)}")
38
+ super().__init__(plot=plot, **settings)
39
+
40
+ def _horizontal(self) -> bool:
41
+ return self.get("orientation") == "horizontal"
42
+
43
+ def values(self, role: str) -> Any:
44
+ val_role = "x" if self._horizontal() else "y"
45
+ if role == val_role:
46
+ out = array.array("d", self.y)
47
+ out.append(self.get("baseline"))
48
+ return out
49
+ return self.x
50
+
51
+ def zero_default(self, role: str) -> bool:
52
+ return role == ("x" if self._horizontal() else "y")
53
+
54
+ def occupy(self, ctx: Any, occ: Any) -> None:
55
+ nan = float("nan")
56
+ b = self.get("baseline")
57
+ t, v = [], []
58
+ for x, y in zip(self.x, self.y):
59
+ t += [x, x, nan]
60
+ v += [b, y, nan]
61
+ px, py = (v, t) if self._horizontal() else (t, v)
62
+ occ.lines(ctx.xmap.many(px), ctx.ymap.many(py))
63
+
64
+ def legend_points(self) -> tuple[Any, Any]:
65
+ return (self.y, self.x) if self._horizontal() else (self.x, self.y)
66
+
67
+ def _color(self, ctx: Any) -> Any:
68
+ return with_alpha(self.base_color(ctx), self.get("alpha"))
69
+
70
+ def _markers(self, ctx: Any, n: int, glyph: bool = False) -> MarkerStyle:
71
+ style = MarkerStyle(self, ctx, "marker_", self._color(ctx), n, glyph)
72
+ return style
73
+
74
+ def draw(self, ctx: Any) -> None:
75
+ c = self._color(ctx)
76
+ horiz = self._horizontal()
77
+ base = self.get("baseline")
78
+ xs, ys = (self.y, self.x) if horiz else (self.x, self.y)
79
+ px, py = ctx.xmap.many(xs), ctx.ymap.many(ys)
80
+ canvas = ctx.canvas
81
+ bc = ctx.color(self.get("baseline_color"))
82
+ if bc is not None and self.get("baseline_width") and len(self.x):
83
+ n, lo, hi = _backend.extent(self.x, None)
84
+ if n:
85
+ if horiz:
86
+ b = ctx.xmap(base)
87
+ canvas.draw_lines([b, b], [ctx.ymap(lo), ctx.ymap(hi)], color=bc,
88
+ width=self.get("baseline_width"))
89
+ else:
90
+ b = ctx.ymap(base)
91
+ canvas.draw_lines([ctx.xmap(lo), ctx.xmap(hi)], [b, b], color=bc,
92
+ width=self.get("baseline_width"))
93
+ lw = self.get("line_width")
94
+ if c is not None and lw:
95
+ if horiz:
96
+ b = ctx.xmap(base)
97
+ canvas.draw_segments(b, py, px, py, color=c, width=lw, simplify=self.simplify_arg())
98
+ else:
99
+ b = ctx.ymap(base)
100
+ canvas.draw_segments(px, b, px, py, color=c, width=lw, simplify=self.simplify_arg())
101
+ self._markers(ctx, len(self.x)).draw(canvas, px, py, self.simplify_arg())
102
+
103
+ def legend_colors(self, ctx: Any) -> tuple[Any, Any]:
104
+ return self._color(ctx), None
105
+
106
+ def legend_glyph(self, ctx: Any, x: float, y: float, w: float, h: float) -> None:
107
+ c = self._color(ctx)
108
+ canvas = ctx.canvas
109
+ if c is not None:
110
+ canvas.draw_lines([x + w / 2, x + w / 2], [y + h, y + h * 0.35], color=c,
111
+ width=self.get("line_width"))
112
+ self._markers(ctx, 1, True).glyph(canvas, x + w / 2, y + h * 0.35, h * 0.7)
@@ -0,0 +1,321 @@
1
+ """Violin plots: a mirrored density per category, with an inner box or quartiles."""
2
+
3
+ import array
4
+ import math
5
+ from typing import Any
6
+
7
+ from .. import _backend, stats
8
+ from ..settings import Setting, boolean, color, either, literal, number, one_of, table
9
+ from ..shapes import fill_polygons
10
+ from ..slots import slots
11
+ from ..style import dash_of, desaturate, with_alpha
12
+ from . import Series, common
13
+ from .catdata import category_values
14
+ from .kde import check_bandwidth
15
+ from ..values import Values, category_paint
16
+
17
+ __all__ = ["Violin"]
18
+
19
+
20
+ def _inner(v: Any) -> Any:
21
+ if v is False or v == "none":
22
+ return False
23
+ return one_of("box", "quartiles", "points")(v)
24
+
25
+
26
+ class Violin(Series):
27
+ """Violins of ``values``: a dict ``{category: values}``, a list of arrays (categories
28
+ ``0, 1, …`` or ``categories=``), or one array."""
29
+
30
+ SETTINGS = table(
31
+ color=Setting(category_paint,
32
+ doc="Fill: a color, one per violin (list or dict by category), or "
33
+ "numbers per violin mapped through `color_map`; default the next "
34
+ "palette color."),
35
+ **common("color_map"),
36
+ line_color=Setting(color, doc="Outline and inner color."),
37
+ line_width=Setting(number(lo=0), doc="Outline width (pt).", length=True),
38
+ saturation=Setting(number(0, 1), doc="Saturation of the fills."),
39
+ alpha=Setting(number(0, 1), doc="Fill opacity."),
40
+ width=Setting(number(positive=True), doc="Width of a category's slot."),
41
+ bandwidth=Setting(check_bandwidth, doc="`scott`, `silverman` or a number."),
42
+ adjust=Setting(number(positive=True), doc="Factor applied to the bandwidth."),
43
+ cut=Setting(number(lo=0), doc="The density extends this many bandwidths past "
44
+ "the data."),
45
+ grid=Setting(number(lo=16, integer=True), doc="Points per violin outline."),
46
+ scale=Setting(one_of("area", "count", "width"),
47
+ doc="`area`: equal areas; `count`: area proportional to the number of "
48
+ "values; `width`: equal widths."),
49
+ inner=Setting(_inner,
50
+ doc="Inside each violin: `box` (a thin box, whiskers and a median dot), "
51
+ "`quartiles` (dashed lines), `points`, or `False` (nothing)."),
52
+ split=Setting(boolean, doc="Two violin series share each slot as facing "
53
+ "halves."),
54
+ orientation=Setting(one_of("vertical", "horizontal"),
55
+ doc="`vertical` (categories on x) or `horizontal`."),
56
+ dodge=Setting(boolean, doc="Violin and box series sharing a category share its "
57
+ "slot side by side; `False` overlays them."),
58
+ gap=Setting(number(0, 1), doc="Fraction of each part of a shared slot left "
59
+ "empty."),
60
+ categories=Setting(either(literal(None), lambda v: list(v)),
61
+ doc="Category names of a list of arrays."),
62
+ **common("name", "z", "rasterize", "simplify", "font_size", "font_family", "text_color"),
63
+ )
64
+ DODGE_GROUP = "box"
65
+ LONG_FORM = True
66
+
67
+ def __init__(self, values: Any, positions: Any = None, plot: Any = None,
68
+ **settings: Any) -> None:
69
+ self.cats, self.data = category_values(values, settings.get("categories"))
70
+ self.positions = None if positions is None else [float(p) for p in positions]
71
+ if self.positions is not None and len(self.positions) != len(self.cats):
72
+ raise ValueError(f"{len(self.positions)} positions for {len(self.cats)} violins")
73
+ super().__init__(plot=plot, **settings)
74
+
75
+ def _horizontal(self) -> bool:
76
+ return self.get("orientation") == "horizontal"
77
+
78
+ def _cat_role(self) -> str:
79
+ return "y" if self._horizontal() else "x"
80
+
81
+ # -- statistics -------------------------------------------------------------------
82
+
83
+ @property
84
+ def stats(self) -> dict[Any, dict[str, Any]]:
85
+ """``{category: {"grid", "density", "count", "box": BoxStats, "bandwidth"}}``."""
86
+ return self.cached("stats", self._compute)
87
+
88
+ def _compute(self) -> dict[Any, dict[str, Any]]:
89
+ out = {}
90
+ n = self.get("grid")
91
+ for k, d in zip(self.cats, self.data):
92
+ bs = _backend.box_stats(d, 1.5)
93
+ if not bs.count:
94
+ out[k] = {"grid": array.array("d"), "density": array.array("d"), "count": 0,
95
+ "box": bs, "bandwidth": 0.0}
96
+ continue
97
+ bw = stats.bandwidth(d, self.get("bandwidth")) * self.get("adjust")
98
+ cnt, lo, hi = _backend.extent(d, None)
99
+ cut = self.get("cut")
100
+ g0, g1 = lo - cut * bw, hi + cut * bw
101
+ if g1 <= g0:
102
+ g0, g1 = g0 - 0.5, g1 + 0.5
103
+ dens = stats.kde(d, g0, g1, n, bw)
104
+ grid = array.array("d", (g0 + (g1 - g0) * i / (n - 1) for i in range(n)))
105
+ out[k] = {"grid": grid, "density": dens, "count": cnt, "box": bs, "bandwidth": bw}
106
+ return out
107
+
108
+ def categories(self, role: str) -> list[Any] | None:
109
+ if role == self._cat_role() and self.positions is None:
110
+ return list(self.cats)
111
+ return None
112
+
113
+ def values(self, role: str) -> Any:
114
+ if role == self._cat_role():
115
+ return array.array("d", self.positions or [])
116
+ out = array.array("d")
117
+ for st in self.stats.values():
118
+ g = st["grid"]
119
+ if len(g):
120
+ out.extend((g[0], g[-1]))
121
+ return out
122
+
123
+ def extent(self, role: str, scale: Any) -> tuple[int, float, float]:
124
+ if role == self._cat_role() and self.positions is not None:
125
+ w = self.get("width") / 2
126
+ n, lo, hi = _backend.extent(self.positions, scale.function)
127
+ return n, lo - w, hi + w
128
+ return super().extent(role, scale)
129
+
130
+ def occupy(self, ctx: Any, occ: Any) -> None:
131
+ lo, hi, c0, c1 = [], [], [], []
132
+ stats = self.stats
133
+ for (center, width), k in zip(slots(self, ctx), self.cats):
134
+ st = stats.get(k)
135
+ if st is None or not len(st["grid"]):
136
+ continue
137
+ lo.append(st["grid"][0])
138
+ hi.append(st["grid"][-1])
139
+ c0.append(center - width / 2)
140
+ c1.append(center + width / 2)
141
+ if self._horizontal():
142
+ occ.rects(ctx.xmap.many(lo), ctx.ymap.many(c0), ctx.xmap.many(hi), ctx.ymap.many(c1))
143
+ else:
144
+ occ.rects(ctx.xmap.many(c0), ctx.ymap.many(lo), ctx.xmap.many(c1), ctx.ymap.many(hi))
145
+
146
+ def legend_points(self) -> tuple[Any, Any] | None:
147
+ xs, ys = array.array("d"), array.array("d")
148
+ pos = self.positions or [float(i) for i in range(len(self.cats))]
149
+ for p, (k, st) in zip(pos, self.stats.items()):
150
+ b = st["box"]
151
+ if b.count:
152
+ for v in (b.low, b.q1, b.median, b.q3, b.high):
153
+ xs.append(p)
154
+ ys.append(v)
155
+ return (ys, xs) if self._horizontal() else (xs, ys)
156
+
157
+ # -- drawing ----------------------------------------------------------------------
158
+
159
+ def _per_violin(self, v: Any) -> list[float]:
160
+ if isinstance(v, dict):
161
+ return [v.get(k, float("nan")) for k in self.cats]
162
+ if len(v) != len(self.cats):
163
+ raise ValueError(f"{len(v)} values for {len(self.cats)} violins")
164
+ return list(v)
165
+
166
+ def color_sources(self) -> list[tuple[Any, Any]]:
167
+ import array
168
+ c = self.get("color")
169
+ if isinstance(c, Values):
170
+ return [(self.color_map_obj(), array.array("d", self._per_violin(c.data)))]
171
+ return []
172
+
173
+ def fills(self, ctx: Any) -> list[Any]:
174
+ c = self.get("color")
175
+ n = len(self.cats)
176
+ if isinstance(c, Values):
177
+ cm = self.color_map_obj()
178
+ flo, fhi = ctx.maps[id(cm)][:2]
179
+ from ..scales import get_scale
180
+ f = get_scale(cm.scale).f
181
+ span = (fhi - flo) or 1.0
182
+ cols = [cm((f(v) - flo) / span) for v in self._per_violin(c.data)]
183
+ elif isinstance(c, dict):
184
+ cols = [ctx.color(c[k]) if c.get(k) is not None else None for k in self.cats]
185
+ elif isinstance(c, list):
186
+ if len(c) != n:
187
+ raise ValueError(f"{len(c)} colors for {n} violins")
188
+ cols = [ctx.color(x) for x in c]
189
+ else:
190
+ cols = [self.base_color(ctx)] * n
191
+ sat = self.get("saturation")
192
+ a = self.get("alpha")
193
+ return [with_alpha(desaturate(x, sat) if sat != 1.0 else x, a) if x is not None else None
194
+ for x in cols]
195
+
196
+ def _half_widths(self) -> dict[Any, float]:
197
+ """Scale factor per category: half-width in category units per unit of density."""
198
+ st = self.stats
199
+ mode = self.get("scale")
200
+ peaks = {k: (max(s["density"]) if len(s["density"]) else 0.0) for k, s in st.items()}
201
+ if mode == "width":
202
+ return {k: (1.0 / p if p else 0.0) for k, p in peaks.items()}
203
+ if mode == "count":
204
+ top = max((peaks[k] * s["count"] for k, s in st.items()), default=0.0)
205
+ return {k: (s["count"] / top if top else 0.0) for k, s in st.items()}
206
+ top = max(peaks.values(), default=0.0)
207
+ return {k: (1.0 / top if top else 0.0) for k in st}
208
+
209
+ def _side(self) -> int:
210
+ """0: both halves; -1: the left (lower) half; 1: the right half (split)."""
211
+ if not self.get("split"):
212
+ return 0
213
+ peers = [s for s in self.plot.series if isinstance(s, Violin) and s.get("split")
214
+ and s._horizontal() == self._horizontal()]
215
+ return -1 if peers.index(self) % 2 == 0 else 1
216
+
217
+ def draw(self, ctx: Any) -> None:
218
+ horiz = self._horizontal()
219
+ cmap, vmap = (ctx.ymap, ctx.xmap) if horiz else (ctx.xmap, ctx.ymap)
220
+ st = self.stats
221
+ scale = self._half_widths()
222
+ side = self._side()
223
+ fills = self.fills(ctx)
224
+ lc = ctx.color(self.get("line_color"))
225
+ lw = self.get("line_width")
226
+ polys, polyfill = [], []
227
+ inner = self.get("inner")
228
+ canvas = ctx.canvas
229
+ geo = list(zip(slots(self, ctx), self.cats, fills))
230
+ for (center, width), k, fill in geo:
231
+ s = st[k]
232
+ if not s["count"]:
233
+ continue
234
+ half = width / 2
235
+ hw = [d * scale[k] * half for d in s["density"]]
236
+ pv = vmap.many(s["grid"])
237
+ right = [cmap(center + w) for w in hw] if side >= 0 else [cmap(center)] * len(hw)
238
+ left = [cmap(center - w) for w in hw] if side <= 0 else [cmap(center)] * len(hw)
239
+ ra = array.array("d", right)
240
+ ra.extend(reversed(left))
241
+ rv = array.array("d", pv)
242
+ rv.extend(reversed(pv))
243
+ polys.append([(rv, ra)] if horiz else [(ra, rv)])
244
+ polyfill.append(fill)
245
+ fill_polygons(canvas, polys, fill=polyfill, stroke=lc if lw else None,
246
+ line_width=lw or None)
247
+ if not inner or lc is None:
248
+ return
249
+ for (center, width), k, fill in geo:
250
+ s = st[k]
251
+ if not s["count"]:
252
+ continue
253
+ b = s["box"]
254
+ off = 0.0 if not side else side * width * 0.06
255
+ c = center + off
256
+ if inner == "box":
257
+ bw = width * 0.06
258
+ a0, a1 = cmap(c - bw / 2), cmap(c + bw / 2)
259
+ q1, q3 = vmap(b.q1), vmap(b.q3)
260
+ ac = cmap(c)
261
+ lo, hi = vmap(b.low), vmap(b.high)
262
+ if horiz:
263
+ canvas.draw_lines([lo, hi], [ac, ac], color=lc, width=max(lw, 0.5))
264
+ canvas.begin_path()
265
+ canvas.rect(min(q1, q3), min(a0, a1), abs(q3 - q1), abs(a1 - a0))
266
+ else:
267
+ canvas.draw_lines([ac, ac], [lo, hi], color=lc, width=max(lw, 0.5))
268
+ canvas.begin_path()
269
+ canvas.rect(min(a0, a1), min(q1, q3), abs(a1 - a0), abs(q3 - q1))
270
+ canvas.fill(lc)
271
+ m = vmap(b.median)
272
+ mx, my = (m, ac) if horiz else (ac, m)
273
+ canvas.draw_markers([mx], [my], size=max(abs(a1 - a0) * 0.9, 2.0),
274
+ fill="white", stroke=None)
275
+ elif inner == "quartiles":
276
+ for v, dash in ((b.q1, "dashed"), (b.median, "dashed"), (b.q3, "dashed")):
277
+ d = _density_at(s, v) * scale[k] * width / 2
278
+ lo_a = c - d if side <= 0 else c
279
+ hi_a = c + d if side >= 0 else c
280
+ pv = vmap(v)
281
+ xs, ys = ([pv, pv], [cmap(lo_a), cmap(hi_a)]) if horiz else \
282
+ ([cmap(lo_a), cmap(hi_a)], [pv, pv])
283
+ canvas.draw_lines(xs, ys, color=lc, width=lw or 0.8,
284
+ dash=dash_of("dashed" if v != b.median else "solid",
285
+ lw or 0.8))
286
+ elif inner == "points":
287
+ d = self.data[self.cats.index(k)]
288
+ pv = vmap.many(d)
289
+ pc = cmap(c)
290
+ xs, ys = (pv, [pc] * len(pv)) if horiz else ([pc] * len(pv), pv)
291
+ canvas.draw_markers(xs, ys, size=2.0, fill=lc, stroke=None)
292
+
293
+ def legend_colors(self, ctx: Any) -> tuple[Any, Any]:
294
+ f = self.fills(ctx)
295
+ return (f[0] if f else None), ctx.color(self.get("line_color"))
296
+
297
+ def legend_glyph(self, ctx: Any, x: float, y: float, w: float, h: float) -> None:
298
+ fill, stroke = self.legend_colors(ctx)
299
+ canvas = ctx.canvas
300
+ bw = min(w, 1.6 * h)
301
+ canvas.begin_path()
302
+ canvas.rect(x + (w - bw) / 2, y, bw, h)
303
+ if fill is not None:
304
+ canvas.fill(fill)
305
+ if stroke is not None and self.get("line_width"):
306
+ canvas.line_width = self.get("line_width")
307
+ canvas.stroke(stroke)
308
+
309
+
310
+ def _density_at(s: dict[str, Any], v: float) -> float:
311
+ g, d = s["grid"], s["density"]
312
+ n = len(g)
313
+ if not n or not (g[0] <= v <= g[-1]):
314
+ return 0.0
315
+ t = (v - g[0]) / (g[-1] - g[0]) * (n - 1) if g[-1] > g[0] else 0.0
316
+ i = min(int(t), n - 2)
317
+ f = t - i
318
+ return d[i] * (1 - f) + d[i + 1] * f
319
+
320
+
321
+ del math