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.
- plot_io/__init__.py +66 -0
- plot_io/_backend.py +114 -0
- plot_io/axis.py +574 -0
- plot_io/colorbar.py +342 -0
- plot_io/decorations.py +496 -0
- plot_io/defaults.py +766 -0
- plot_io/density.py +97 -0
- plot_io/enums.py +63 -0
- plot_io/facets.py +184 -0
- plot_io/figure.py +331 -0
- plot_io/labels.py +63 -0
- plot_io/legend.py +498 -0
- plot_io/limits.py +92 -0
- plot_io/maps/__init__.py +365 -0
- plot_io/maps/data.py +333 -0
- plot_io/markers.py +117 -0
- plot_io/numbers.py +289 -0
- plot_io/occupancy.py +219 -0
- plot_io/palettes.py +142 -0
- plot_io/plot.py +435 -0
- plot_io/render.py +1122 -0
- plot_io/scales.py +93 -0
- plot_io/series/__init__.py +498 -0
- plot_io/series/area.py +181 -0
- plot_io/series/bar.py +294 -0
- plot_io/series/box.py +401 -0
- plot_io/series/catdata.py +23 -0
- plot_io/series/catpoints.py +182 -0
- plot_io/series/contour.py +294 -0
- plot_io/series/curves.py +128 -0
- plot_io/series/ecdf.py +143 -0
- plot_io/series/errorbar.py +111 -0
- plot_io/series/errors.py +100 -0
- plot_io/series/fill.py +168 -0
- plot_io/series/heatmap.py +304 -0
- plot_io/series/hexbin.py +216 -0
- plot_io/series/hist.py +379 -0
- plot_io/series/hist2d.py +163 -0
- plot_io/series/kde.py +228 -0
- plot_io/series/kde2d.py +153 -0
- plot_io/series/line.py +173 -0
- plot_io/series/pie.py +304 -0
- plot_io/series/points.py +341 -0
- plot_io/series/regression.py +250 -0
- plot_io/series/scatter.py +103 -0
- plot_io/series/stem.py +112 -0
- plot_io/series/violin.py +321 -0
- plot_io/settings.py +281 -0
- plot_io/shapes.py +57 -0
- plot_io/sharing.py +160 -0
- plot_io/slots.py +52 -0
- plot_io/stacking.py +63 -0
- plot_io/stats.py +140 -0
- plot_io/style.py +71 -0
- plot_io/styles/__init__.py +234 -0
- plot_io/styles/library.py +732 -0
- plot_io/tables.py +141 -0
- plot_io/text.py +109 -0
- plot_io/ticks.py +284 -0
- plot_io/times.py +199 -0
- plot_io/values.py +161 -0
- plot_io-0.1.0.dist-info/METADATA +102 -0
- plot_io-0.1.0.dist-info/RECORD +65 -0
- plot_io-0.1.0.dist-info/WHEEL +4 -0
- 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)
|
plot_io/series/violin.py
ADDED
|
@@ -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
|