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,163 @@
1
+ """2-D histograms: counts of points in rectangular bins, drawn as a heatmap."""
2
+
3
+ import array
4
+ import math
5
+ from typing import Any
6
+
7
+ from .. import _backend, stats
8
+ from ..scales import get_scale
9
+ from ..settings import Setting, either, literal, number, one_of, pair, table
10
+ from .heatmap import Heatmap, _Coords
11
+
12
+ __all__ = ["Hist2d"]
13
+
14
+
15
+ def _bins2(v: Any) -> Any:
16
+ if isinstance(v, (tuple, list)) and len(v) == 2 and not all(
17
+ isinstance(x, (int, float)) for x in v):
18
+ return (stats.check_bins(v[0]), stats.check_bins(v[1]))
19
+ if isinstance(v, (tuple, list)) and len(v) == 2 and all(
20
+ isinstance(x, int) and not isinstance(x, bool) for x in v):
21
+ return (stats.check_bins(v[0]), stats.check_bins(v[1]))
22
+ b = stats.check_bins(v)
23
+ return (b, b)
24
+
25
+
26
+ def _range2(v: Any) -> Any:
27
+ return (pair(number())(v[0]), pair(number())(v[1]))
28
+
29
+
30
+ class Hist2d(Heatmap):
31
+ """A 2-D histogram of points ``(x, y)``: counts in bins (even on the axis scales),
32
+ drawn as a heatmap; bins with fewer than ``min_count`` points are left empty."""
33
+
34
+ SETTINGS = table(
35
+ **{k: v for k, v in Heatmap.SETTINGS.items() if k not in ("origin",)},
36
+ bins=Setting(_bins2,
37
+ doc="Bins of both axes, or `(x bins, y bins)`: counts, edges or rules "
38
+ "(as `Hist`)."),
39
+ range=Setting(either(literal(None), _range2),
40
+ doc="`((x0, x1), (y0, y1))`; default the data range."),
41
+ weights=Setting(lambda v: _backend.doubles(v), doc="A weight per point."),
42
+ stat=Setting(one_of("count", "density", "probability", "percent"),
43
+ doc="`count`, `density` (volume 1), `probability` or `percent`."),
44
+ min_count=Setting(number(lo=0), doc="Bins below this count are transparent."),
45
+ origin=Setting(one_of("top", "bottom"), doc="Row of the first y bin."),
46
+ )
47
+
48
+ def __init__(self, x: Any, y: Any, plot: Any = None, **settings: Any) -> None:
49
+ self.px, self.py = _backend.doubles(x), _backend.doubles(y)
50
+ if len(self.px) != len(self.py):
51
+ raise ValueError(f"x has {len(self.px)} values and y {len(self.py)}")
52
+ self.k = 1
53
+ self._map: Any = None
54
+ self._map_key: Any = None
55
+ self._bar: Any = None
56
+ from . import Series
57
+ Series.__init__(self, plot=plot, **settings)
58
+ if self.get("colorbar"):
59
+ self._attach_bar()
60
+
61
+ def inverts(self, role: str) -> bool:
62
+ return False
63
+
64
+ def _scales(self) -> tuple[Any, Any]:
65
+ out = []
66
+ for axis in (self.plot.x_axis, self.plot.y_axis):
67
+ name = axis.nominal_scale
68
+ out.append(get_scale("linear" if name == "category" else name))
69
+ return out[0], out[1]
70
+
71
+ @property
72
+ def stats(self) -> dict[str, Any]:
73
+ """``{"x_edges", "y_edges", "counts": rows (first y bin first), "values": rows}``."""
74
+ sx, sy = self._scales()
75
+ return self.cached(("stats", sx.name, sy.name), lambda: self._compute(sx, sy))
76
+
77
+ def _compute(self, sx: Any, sy: Any) -> dict[str, Any]:
78
+ bx, by = self.get("bins")
79
+ rng = self.get("range")
80
+ fx = _backend.scaled(self.px, 1.0, 0.0, sx.function) if sx.function else self.px
81
+ fy = _backend.scaled(self.py, 1.0, 0.0, sy.function) if sy.function else self.py
82
+ w = self.get("weights")
83
+
84
+ def edges(b: Any, v: Any, s: Any, r: Any) -> Any:
85
+ if isinstance(b, array.array):
86
+ return array.array("d", (s.f(e) for e in b))
87
+ fr = None if r is None else (s.f(r[0]), s.f(r[1]))
88
+ return stats.bin_edges(v, b, fr)
89
+
90
+ xe = edges(bx, fx, sx, None if rng is None else rng[0])
91
+ ye = edges(by, fy, sy, None if rng is None else rng[1])
92
+ counts = stats.histogram2d(fx, fy, xe, ye, w)
93
+ total = math.fsum(math.fsum(r) for r in counts)
94
+ st = self.get("stat")
95
+ xd = [sx.inverse(e) for e in xe]
96
+ yd = [sy.inverse(e) for e in ye]
97
+ mc = self.get("min_count")
98
+ rows = []
99
+ for j, r in enumerate(counts):
100
+ out = array.array("d")
101
+ for i, c in enumerate(r):
102
+ if c < mc or c == 0:
103
+ out.append(math.nan)
104
+ elif st == "count":
105
+ out.append(c)
106
+ elif st == "probability":
107
+ out.append(c / total)
108
+ elif st == "percent":
109
+ out.append(100.0 * c / total)
110
+ else:
111
+ area = (xe[i + 1] - xe[i]) * (ye[j + 1] - ye[j])
112
+ out.append(c / (total * area) if area else math.nan)
113
+ rows.append(out)
114
+ return {"x_edges": array.array("d", xd), "y_edges": array.array("d", yd),
115
+ "counts": counts, "values": rows}
116
+
117
+ # the heatmap's grid, from the bins
118
+ @property
119
+ def flat(self) -> Any:
120
+ out = array.array("d")
121
+ for r in self.stats["values"]:
122
+ out.extend(r)
123
+ return out
124
+
125
+ @flat.setter
126
+ def flat(self, v: Any) -> None:
127
+ pass
128
+
129
+ @property
130
+ def h(self) -> int:
131
+ return len(self.stats["values"])
132
+
133
+ @h.setter
134
+ def h(self, v: Any) -> None:
135
+ pass
136
+
137
+ @property
138
+ def w(self) -> int:
139
+ rows = self.stats["values"]
140
+ return len(rows[0]) if rows else 0
141
+
142
+ @w.setter
143
+ def w(self, v: Any) -> None:
144
+ pass
145
+
146
+ @property
147
+ def xc(self) -> _Coords:
148
+ return _Coords(self.stats["x_edges"], self.w, "x")
149
+
150
+ @xc.setter
151
+ def xc(self, v: Any) -> None:
152
+ pass
153
+
154
+ @property
155
+ def yc(self) -> _Coords:
156
+ return _Coords(self.stats["y_edges"], self.h, "y")
157
+
158
+ @yc.setter
159
+ def yc(self, v: Any) -> None:
160
+ pass
161
+
162
+ def legend_points(self) -> tuple[Any, Any]:
163
+ return self.px, self.py
plot_io/series/kde.py ADDED
@@ -0,0 +1,228 @@
1
+ """Kernel density estimates: smooth curves of the distribution of values."""
2
+
3
+ import array
4
+ import math
5
+ from typing import Any
6
+
7
+ from .. import _backend, stats
8
+ from ..scales import get_scale
9
+ from ..settings import Setting, boolean, either, literal, number, one_of, pair, table
10
+ from ..shapes import fill_polygons
11
+ from ..stacking import check_stack, stacked
12
+ from ..style import dash_of, with_alpha
13
+ from . import Series, common
14
+ from .hist import _weights, value_sets
15
+ from .points import colors_only
16
+
17
+ __all__ = ["Kde", "check_bandwidth"]
18
+
19
+
20
+ def check_bandwidth(v: Any) -> Any:
21
+ if isinstance(v, str):
22
+ return one_of("scott", "silverman")(v)
23
+ return number(positive=True)(v)
24
+
25
+
26
+ class Kde(Series):
27
+ """A Gaussian kernel density estimate of ``values`` (one array, or several as a dict or
28
+ a list of arrays, on a common grid)."""
29
+
30
+ SETTINGS = table(
31
+ bandwidth=Setting(check_bandwidth,
32
+ doc="`scott`, `silverman`, or a bandwidth in data units."),
33
+ adjust=Setting(number(positive=True), doc="Factor applied to the bandwidth."),
34
+ cut=Setting(number(lo=0), doc="The curve extends this many bandwidths past the "
35
+ "data."),
36
+ clip=Setting(either(literal(None), pair(number())), doc="Limits of the curve."),
37
+ grid=Setting(number(lo=16, integer=True), doc="Number of points of the curve."),
38
+ cumulative=Setting(boolean, doc="Plot the cumulative distribution."),
39
+ fill=Setting(boolean, doc="Fill under the curve."),
40
+ weights=Setting(_weights, doc="A weight per value (per set for several sets)."),
41
+ orientation=Setting(one_of("vertical", "horizontal"),
42
+ doc="`vertical` (values along x) or `horizontal`."),
43
+ stack=Setting(check_stack, doc="Stack the sets (seaborn's `multiple=\"stack\"`)."),
44
+ color=Setting(colors_only, doc="Color, or one per set; default palette colors."),
45
+ alpha=Setting(number(0, 1), doc="Fill opacity; default 0.25 (1 when stacked)."),
46
+ line_width=Setting(number(lo=0), inherited=True, doc="Line width (pt).", length=True),
47
+ line_style=Setting(one_of("solid", "dashed", "dotted", "dashdot"),
48
+ doc="Line style."),
49
+ **common("name", "z", "rasterize", "simplify", "font_size", "font_family", "text_color"),
50
+ )
51
+ STACK_GROUP = "kde"
52
+
53
+ def __init__(self, values: Any, plot: Any = None, **settings: Any) -> None:
54
+ self.sets, self.named = value_sets(values)
55
+ super().__init__(plot=plot, **settings)
56
+
57
+ def _horizontal(self) -> bool:
58
+ return self.get("orientation") == "horizontal"
59
+
60
+ def _val_role(self) -> str:
61
+ return "y" if self._horizontal() else "x"
62
+
63
+ def _scale(self) -> Any:
64
+ axis = self.plot.y_axis if self._horizontal() else self.plot.x_axis
65
+ name = axis.nominal_scale
66
+ return get_scale("linear" if name == "category" else name)
67
+
68
+ @property
69
+ def stats(self) -> dict[str, Any]:
70
+ """``{"grid": values, "densities": [per set], "bandwidths": [per set]}`` (densities
71
+ per unit of the axis scale on log axes)."""
72
+ scale = self._scale()
73
+ return self.cached(("stats", scale.name), lambda: self._compute(scale))
74
+
75
+ def _compute(self, scale: Any) -> dict[str, Any]:
76
+ fn = scale.function
77
+ weights = self.get("weights")
78
+ fsets, ws, bws = [], [], []
79
+ for i, (key, v) in enumerate(self.sets):
80
+ fv = _backend.scaled(v, 1.0, 0.0, fn) if fn else v
81
+ w = None
82
+ if weights is not None:
83
+ w = (weights[key] if isinstance(weights, dict) else
84
+ weights[i] if isinstance(weights, list) else weights)
85
+ fsets.append(fv)
86
+ ws.append(w)
87
+ bws.append(stats.bandwidth(fv, self.get("bandwidth"), w) * self.get("adjust"))
88
+ cut = self.get("cut")
89
+ lo, hi = math.inf, -math.inf
90
+ for fv, bw in zip(fsets, bws):
91
+ n, a, b = _backend.extent(fv, None)
92
+ if n:
93
+ lo, hi = min(lo, a - cut * bw), max(hi, b + cut * bw)
94
+ if not math.isfinite(lo):
95
+ lo, hi = 0.0, 1.0
96
+ clip = self.get("clip")
97
+ if clip is not None:
98
+ lo, hi = max(lo, scale.f(clip[0])), min(hi, scale.f(clip[1]))
99
+ n = self.get("grid")
100
+ dens = [stats.kde(fv, lo, hi, n, bw, w, self.get("cumulative"))
101
+ for fv, bw, w in zip(fsets, bws, ws)]
102
+ grid = array.array("d", (scale.inverse(lo + (hi - lo) * i / (n - 1)) for i in range(n)))
103
+ return {"grid": grid, "densities": dens, "bandwidths": bws}
104
+
105
+ def stack_layers(self) -> list[tuple[list[Any], Any]]:
106
+ st = self.stats
107
+ keys = [round(g, 12) for g in st["grid"]]
108
+ return [(keys, d) for d in st["densities"]]
109
+
110
+ def _ends(self) -> list[tuple[list[float], list[float]]]:
111
+ if self.get("stack"):
112
+ return stacked(self)
113
+ return [([0.0] * len(d), list(d)) for d in self.stats["densities"]]
114
+
115
+ def values(self, role: str) -> Any:
116
+ if role == self._val_role():
117
+ return self.stats["grid"]
118
+ out = array.array("d", [0.0])
119
+ for _, t in self._ends():
120
+ out.extend(t)
121
+ return out
122
+
123
+ def zero_default(self, role: str) -> bool:
124
+ return role != self._val_role()
125
+
126
+ def zero_padding(self, role: str) -> bool:
127
+ return False
128
+
129
+ def palette_count(self) -> int:
130
+ return len(self.sets)
131
+
132
+ def has_own_color(self) -> bool:
133
+ return self.is_set("color")
134
+
135
+ def legend_labels(self) -> list[Any]:
136
+ if self.named:
137
+ return [str(k) for k, _ in self.sets]
138
+ name = self.get("name")
139
+ return [] if name is None else [name]
140
+
141
+ def occupy(self, ctx: Any, occ: Any) -> None:
142
+ t = self.stats["grid"]
143
+ filled = self.get("fill") or self.get("stack")
144
+ for bottom, top in self._ends():
145
+ if self._horizontal():
146
+ if filled:
147
+ occ.band(ctx.ymap.many(t), ctx.xmap.many(bottom), ctx.xmap.many(top), True)
148
+ else:
149
+ occ.lines(ctx.xmap.many(top), ctx.ymap.many(t))
150
+ elif filled:
151
+ occ.band(ctx.xmap.many(t), ctx.ymap.many(bottom), ctx.ymap.many(top))
152
+ else:
153
+ occ.lines(ctx.xmap.many(t), ctx.ymap.many(top))
154
+
155
+ def legend_points(self) -> tuple[Any, Any] | None:
156
+ g = self.stats["grid"]
157
+ xs, ys = array.array("d"), array.array("d")
158
+ for _, t in self._ends():
159
+ xs.extend(g)
160
+ ys.extend(t)
161
+ return (ys, xs) if self._horizontal() else (xs, ys)
162
+
163
+ def colors(self, ctx: Any) -> list[Any]:
164
+ c = self.get("color")
165
+ n = len(self.sets)
166
+ if isinstance(c, list):
167
+ if len(c) != n:
168
+ raise ValueError(f"{len(c)} colors for {n} sets")
169
+ return [ctx.color(x) for x in c]
170
+ if c is not None:
171
+ return [ctx.color(c)] * n
172
+ return self.auto_colors(ctx)
173
+
174
+ def _fill_alpha(self) -> float:
175
+ return self.get("alpha")
176
+
177
+ def draw(self, ctx: Any) -> None:
178
+ g = self.stats["grid"]
179
+ horiz = self._horizontal()
180
+ vmap, dmap = (ctx.ymap, ctx.xmap) if horiz else (ctx.xmap, ctx.ymap)
181
+ pg = vmap.many(g)
182
+ lw = self.get("line_width")
183
+ dash = dash_of(self.get("line_style"), lw)
184
+ fill = self.get("fill") or self.get("stack")
185
+ for (bot, top), col in zip(self._ends(), self.colors(ctx)):
186
+ if col is None:
187
+ continue
188
+ pt, pb = dmap.many(top), dmap.many(bot)
189
+ if fill:
190
+ rx = array.array("d", pg)
191
+ rx.extend(reversed(pg))
192
+ ry = array.array("d", pt)
193
+ ry.extend(reversed(pb))
194
+ ring = (ry, rx) if horiz else (rx, ry)
195
+ fill_polygons(ctx.canvas, [[ring]], fill=with_alpha(col, self._fill_alpha()))
196
+ xs, ys = (pt, pg) if horiz else (pg, pt)
197
+ if lw:
198
+ ctx.canvas.draw_lines(xs, ys, color=col, width=lw, dash=dash,
199
+ simplify=self.simplify_arg(lines=True))
200
+
201
+ def legend_colors_at(self, ctx: Any, i: int) -> tuple[Any, Any]:
202
+ cols = self.colors(ctx)
203
+ c = cols[i] if i < len(cols) else None
204
+ fill = with_alpha(c, self._fill_alpha()) if c is not None and (
205
+ self.get("fill") or self.get("stack")) else None
206
+ return fill, c
207
+
208
+ def legend_glyph_at(self, ctx: Any, i: int, x: float, y: float, w: float, h: float) -> None:
209
+ fill, c = self.legend_colors_at(ctx, i)
210
+ canvas = ctx.canvas
211
+ if fill is not None:
212
+ canvas.fill_style = fill
213
+ canvas.fill_rect(x, y, w, h)
214
+ if c is not None:
215
+ lw = self.get("line_width")
216
+ canvas.draw_lines([x, x + w], [y + h / 2, y + h / 2] if fill is None else [y, y],
217
+ color=c, width=lw, dash=dash_of(self.get("line_style"), lw))
218
+
219
+ def legend_glyph(self, ctx: Any, x: float, y: float, w: float, h: float) -> None:
220
+ self.legend_glyph_at(ctx, 0, x, y, w, h)
221
+
222
+
223
+ def _kde2d() -> type:
224
+ from .kde2d import Kde2d
225
+ return Kde2d
226
+
227
+
228
+ Kde.with_y = staticmethod(_kde2d) # type: ignore[attr-defined]
@@ -0,0 +1,153 @@
1
+ """2-D kernel density contours (``kde(x, y)``): seaborn's ``kdeplot(x=, y=)``."""
2
+
3
+ import array
4
+ import math
5
+ from typing import Any
6
+
7
+ from .. import _backend, stats
8
+ from ..density import Density, _bandwidth
9
+ from ..settings import Setting, color, number, table
10
+ from .contour import Contour
11
+
12
+ __all__ = ["Kde2d"]
13
+
14
+
15
+ class Kde2d(Contour):
16
+ """Density contours of points ``(x, y)``: the 2-D Gaussian density on a grid over the
17
+ data plus ``cut`` bandwidths, drawn at levels enclosing equal shares of the mass."""
18
+
19
+ SETTINGS = table(
20
+ **{k: v for k, v in Contour.SETTINGS.items() if k not in ("levels", "colorbar")},
21
+ levels=Setting(lambda v: v if isinstance(v, list) else number(lo=1, integer=True)(v), doc="A number of iso-proportion levels (contour k of n encloses "
22
+ "k/n of the mass, above `thresh`), or density values."),
23
+ thresh=Setting(number(0, 1),
24
+ doc="Lowest iso-proportion: the outermost contour encloses 1 − "
25
+ "`thresh` of the mass."),
26
+ bandwidth=Setting(_bandwidth,
27
+ doc="`\"scott\"`, `\"silverman\"`, a number or a 2 × 2 matrix, as "
28
+ "`Density`."),
29
+ adjust=Setting(number(positive=True), doc="Factor of the bandwidth."),
30
+ cut=Setting(number(lo=0), doc="Grid extends this many bandwidths past the data."),
31
+ gridsize=Setting(number(lo=16, hi=1024, integer=True),
32
+ doc="Grid points per side."),
33
+ color=Setting(color, doc="Line color of unfilled contours without a `color_map`; "
34
+ "default the next palette color."),
35
+ colorbar=Setting(lambda v: bool(v), doc="Attach a colorbar to the plot."),
36
+ )
37
+
38
+ def __init__(self, x: Any, y: Any, plot: Any = None, **settings: Any) -> None:
39
+ self.px, self.py = _backend.doubles(x), _backend.doubles(y)
40
+ if len(self.px) != len(self.py):
41
+ raise ValueError(f"x has {len(self.px)} values and y {len(self.py)}")
42
+ self.h = self.w = 2
43
+ self.flat = array.array("d", [0.0] * 4)
44
+ self.xn = self.yn = array.array("d", [0.0, 1.0])
45
+ self._map = None
46
+ self._bar = None
47
+ from . import Series
48
+ Series.__init__(self, plot=plot, **settings)
49
+ if self.get("colorbar"):
50
+ self._bar = self.plot.colorbar(self)
51
+
52
+ @property
53
+ def USES_PALETTE(self) -> bool: # type: ignore[override]
54
+ return not self.get("filled") and not self.is_set("color_map")
55
+
56
+ def has_own_color(self) -> bool:
57
+ return self.is_set("color")
58
+
59
+ def _density(self) -> Density:
60
+ return Density(self.get("bandwidth"), self.get("adjust"))
61
+
62
+ def _grid(self) -> dict[str, Any]:
63
+ sx, sy = self._scales()
64
+ return self.cached(("grid", sx.name, sy.name), lambda: self._compute_grid(sx, sy))
65
+
66
+ def _compute_grid(self, sx: Any, sy: Any) -> dict[str, Any]:
67
+ fx = _backend.scaled(self.px, 1.0, 0.0, sx.function, **sx.options)
68
+ fy = _backend.scaled(self.py, 1.0, 0.0, sy.function, **sy.options)
69
+ d = self._density()
70
+ h = d.covariance(fx, fy)
71
+ n = int(self.get("gridsize"))
72
+ _, x0, x1 = _backend.extent(fx, None)
73
+ _, y0, y1 = _backend.extent(fy, None)
74
+ if not all(math.isfinite(v) for v in (x0, x1, y0, y1)) or \
75
+ not all(math.isfinite(v) for v in h):
76
+ return {"fx": array.array("d", [0.0, 1.0]), "fy": array.array("d", [0.0, 1.0]),
77
+ "flat": array.array("d", [0.0] * 4), "w": 2, "h": 2}
78
+ cut = self.get("cut")
79
+ x0, x1 = x0 - cut * math.sqrt(h[0]), x1 + cut * math.sqrt(h[0])
80
+ y0, y1 = y0 - cut * math.sqrt(h[2]), y1 + cut * math.sqrt(h[2])
81
+ flat = _backend.kde2d_grid(fx, fy, h, x0, x1, n, y0, y1, n)
82
+ gx = array.array("d", (x0 + (x1 - x0) * i / (n - 1) for i in range(n)))
83
+ gy = array.array("d", (y0 + (y1 - y0) * j / (n - 1) for j in range(n)))
84
+ return {"fx": gx, "fy": gy, "flat": flat, "w": n, "h": n}
85
+
86
+ def level_values(self) -> list[float]:
87
+ lv = self.get("levels")
88
+ if isinstance(lv, list):
89
+ return sorted(float(v) for v in lv)
90
+ g = self._grid()
91
+ values = sorted((v for v in g["flat"] if v == v), reverse=True)
92
+ total = sum(values)
93
+ if not total:
94
+ return [0.0]
95
+ thresh = self.get("thresh")
96
+ props = [thresh + (1.0 - thresh) * i / (lv - 1) for i in range(lv)] if lv > 1 \
97
+ else [thresh]
98
+ cum, out, acc, j = [], [], 0.0, 0
99
+ for v in values:
100
+ acc += v
101
+ cum.append(acc / total)
102
+ for p in props: # density whose upper set holds 1 − p
103
+ target = 1.0 - p
104
+ while j < len(cum) - 1 and cum[j] < target:
105
+ j += 1
106
+ out.append(values[min(j, len(values) - 1)])
107
+ j = 0
108
+ return sorted(set(out))
109
+
110
+ def _compute(self, sx: Any, sy: Any) -> dict[str, Any]:
111
+ g = self._grid()
112
+ lv = self.level_values()
113
+ lines: dict[float, Any] = {}
114
+ if self.get("lines"):
115
+ for level in lv:
116
+ lines[level] = stats.contour_lines(g["flat"], g["w"], g["h"], g["fx"], g["fy"],
117
+ level)
118
+ bands = []
119
+ if self.get("filled"):
120
+ tops = lv[1:] + [math.inf]
121
+ for lo, hi in zip(lv, tops):
122
+ top = hi if math.isfinite(hi) else max(g["flat"]) * 1.0001 + 1e-300
123
+ rings = stats.contour_bands(g["flat"], g["w"], g["h"], g["fx"], g["fy"], lo, top)
124
+ if len(rings[0]):
125
+ bands.append((lo, hi if math.isfinite(hi) else top, rings))
126
+ return {"levels": lv, "lines": lines, "bands": bands, "f_space": True}
127
+
128
+ def values(self, role: str) -> Any:
129
+ sx, sy = self._scales()
130
+ g = self._grid()
131
+ f, s = (g["fx"], sx) if role == "x" else (g["fy"], sy)
132
+ return array.array("d", [s.inverse(f[0]), s.inverse(f[-1])])
133
+
134
+ def _level_color(self, ctx: Any, i: int, level: float) -> Any:
135
+ if not self.get("filled") and not self.is_set("color_map") \
136
+ and self.get("colors") is None:
137
+ return self.base_color(ctx)
138
+ return super()._level_color(ctx, i, level)
139
+
140
+ def color_sources(self) -> list[tuple[Any, Any]]:
141
+ if not self.get("filled") and not self.is_set("color_map"):
142
+ return []
143
+ return super().color_sources()
144
+
145
+ def column_role(self, param: str) -> str | None:
146
+ return param if param in ("x", "y") else None
147
+
148
+ def legend_points(self) -> tuple[Any, Any] | None:
149
+ return self.px, self.py
150
+
151
+ def occupy(self, ctx: Any, occ: Any) -> None:
152
+ from . import Series
153
+ Series.occupy(self, ctx, occ)