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
plot_io/series/area.py ADDED
@@ -0,0 +1,181 @@
1
+ """Stacked areas: layers piled on a baseline (zero, symmetric or streamgraph)."""
2
+
3
+ import array
4
+ import math
5
+ from typing import Any
6
+
7
+ from pdf_io import arrays as pdf_arrays
8
+
9
+ from .. import _backend
10
+ from ..settings import Setting, boolean, color, number, one_of, table
11
+ from ..style import with_alpha
12
+ from . import Series, common
13
+ from .curves import curve_settings, line_args
14
+
15
+ __all__ = ["Area"]
16
+
17
+
18
+ def _layers(ys: Any) -> tuple[list[Any], list[array.array]]:
19
+ if isinstance(ys, dict):
20
+ return list(ys.keys()), [_backend.doubles(v) for v in ys.values()]
21
+ rows = pdf_arrays.rows(ys)
22
+ if rows is None:
23
+ rows = [_backend.doubles(r) for r in ys]
24
+ return list(range(len(rows))), rows
25
+
26
+
27
+ class Area(Series):
28
+ """Areas of the layers ``ys`` (rows of a 2-D sequence, or a dict ``{name: values}``)
29
+ along ``x``, stacked on ``baseline``."""
30
+
31
+ SETTINGS = table(
32
+ baseline=Setting(one_of("zero", "sym", "wiggle", "weighted_wiggle"),
33
+ doc="`zero`, `sym` (symmetric around 0), `wiggle` or "
34
+ "`weighted_wiggle` (streamgraphs, as matplotlib's stackplot)."),
35
+ stack=Setting(boolean, doc="Stack the layers; `False` overlays them from 0."),
36
+ color=Setting(lambda v: [color(c) for c in v] if isinstance(v, (list, tuple))
37
+ and not (len(v) in (3, 4) and all(isinstance(c, (int, float)) for c in v))
38
+ else color(v),
39
+ doc="A color, or one per layer; default palette colors."),
40
+ alpha=Setting(number(0, 1), doc="Opacity of the fills."),
41
+ line_color=Setting(color, doc="Outline color of each layer's top; default none."),
42
+ line_width=Setting(number(lo=0), inherited=True, doc="Outline width (pt).", length=True),
43
+ **curve_settings(),
44
+ **common("name", "z", "rasterize", "simplify", "font_size", "font_family", "text_color"),
45
+ )
46
+
47
+ def __init__(self, x: Any, ys: Any, plot: Any = None, **settings: Any) -> None:
48
+ self.x = _backend.doubles(x)
49
+ self.names, self.ys = _layers(ys)
50
+ self.named = isinstance(ys, dict)
51
+ for r in self.ys:
52
+ if len(r) != len(self.x):
53
+ raise ValueError(f"x has {len(self.x)} values and a layer {len(r)}")
54
+ super().__init__(plot=plot, **settings)
55
+
56
+ def _horizontal(self) -> bool:
57
+ return False
58
+
59
+ @property
60
+ def stats(self) -> list[tuple[array.array, array.array]]:
61
+ """``(bottom, top)`` of each layer."""
62
+ return self.cached("stats", self._compute)
63
+
64
+ def _compute(self) -> list[tuple[array.array, array.array]]:
65
+ n = len(self.x)
66
+ ys = [[v if math.isfinite(v) else 0.0 for v in r] for r in self.ys]
67
+ if not self.get("stack"):
68
+ return [(array.array("d", bytes(8 * n)), array.array("d", r)) for r in self.ys]
69
+ m = len(ys)
70
+ total = [math.fsum(r[j] for r in ys) for j in range(n)]
71
+ base = self.get("baseline")
72
+ if base == "zero":
73
+ first = [0.0] * n
74
+ elif base == "sym":
75
+ first = [-t / 2 for t in total]
76
+ elif base == "wiggle":
77
+ first = [-math.fsum(ys[i][j] * (m - 0.5 - i) for i in range(m)) / m
78
+ for j in range(n)]
79
+ else:
80
+ inv = [1.0 / t if t else 0.0 for t in total]
81
+ center = [0.0] * n
82
+ acc = 0.0
83
+ for j in range(n):
84
+ s = 0.0
85
+ cum = 0.0
86
+ for i in range(m):
87
+ cum += ys[i][j]
88
+ inc = ys[i][j] - (ys[i][j - 1] if j else 0.0) if j else ys[i][j]
89
+ below = total[j] - cum + 0.5 * ys[i][j]
90
+ move_up = 0.5 if j == 0 else below * inv[j]
91
+ s += (move_up - 0.5) * inc
92
+ acc += s
93
+ center[j] = acc
94
+ first = [c - 0.5 * t for c, t in zip(center, total)]
95
+ out = []
96
+ cur = first
97
+ for r, raw in zip(ys, self.ys):
98
+ top = [a + b for a, b in zip(cur, r)]
99
+ bot = array.array("d", cur)
100
+ tp = array.array("d", (t if math.isfinite(v) else math.nan
101
+ for t, v in zip(top, raw)))
102
+ out.append((bot, tp))
103
+ cur = top
104
+ return out
105
+
106
+ def values(self, role: str) -> Any:
107
+ if role == "x":
108
+ return self.x
109
+ out = array.array("d")
110
+ for b, t in self.stats:
111
+ out.extend(b)
112
+ out.extend(t)
113
+ return out
114
+
115
+ def occupy(self, ctx: Any, occ: Any) -> None:
116
+ from ..occupancy import MAX_VERTICES, thin
117
+ for b, t in self.stats:
118
+ x, b, t = thin(self.x, b, t, limit=MAX_VERTICES)
119
+ occ.band(ctx.xmap.many(x), ctx.ymap.many(b), ctx.ymap.many(t))
120
+
121
+ def legend_points(self) -> tuple[Any, Any] | None:
122
+ xs, ys = array.array("d"), array.array("d")
123
+ for b, t in self.stats:
124
+ xs.extend(self.x)
125
+ ys.extend(t)
126
+ xs.extend(self.x)
127
+ ys.extend(b)
128
+ return xs, ys
129
+
130
+ def zero_default(self, role: str) -> bool:
131
+ return role == "y" and self.get("baseline") == "zero"
132
+
133
+ def palette_count(self) -> int:
134
+ return len(self.ys)
135
+
136
+ def has_own_color(self) -> bool:
137
+ return self.is_set("color")
138
+
139
+ def legend_labels(self) -> list[Any]:
140
+ if self.named:
141
+ return [str(k) for k in self.names]
142
+ name = self.get("name")
143
+ return [] if name is None else [name]
144
+
145
+ def _colors(self, ctx: Any) -> list[Any]:
146
+ c = self.get("color")
147
+ n = len(self.ys)
148
+ if isinstance(c, list):
149
+ if len(c) != n:
150
+ raise ValueError(f"{len(c)} colors for {n} layers")
151
+ return [ctx.color(x) for x in c]
152
+ if c is not None:
153
+ return [ctx.color(c)] * n
154
+ return self.auto_colors(ctx)
155
+
156
+ def draw(self, ctx: Any) -> None:
157
+ xs = ctx.xmap.many(self.x)
158
+ args = line_args(self)
159
+ lc = ctx.color(self.get("line_color"))
160
+ lw = self.get("line_width")
161
+ for (b, t), col in zip(self.stats, self._colors(ctx)):
162
+ pb, pt = ctx.ymap.many(b), ctx.ymap.many(t)
163
+ if col is not None:
164
+ ctx.canvas.fill_between(xs, pt, pb, color=col, opacity=self.get("alpha"),
165
+ simplify=self.simplify_arg(lines=True), **args)
166
+ if lc is not None and lw:
167
+ ctx.canvas.draw_lines(xs, pt, color=lc, width=lw, **args)
168
+
169
+ def legend_colors_at(self, ctx: Any, i: int) -> tuple[Any, Any]:
170
+ cols = self._colors(ctx)
171
+ c = cols[i] if i < len(cols) else None
172
+ return (with_alpha(c, self.get("alpha")) if c is not None else None), None
173
+
174
+ def legend_glyph_at(self, ctx: Any, i: int, x: float, y: float, w: float, h: float) -> None:
175
+ fill, _ = self.legend_colors_at(ctx, i)
176
+ if fill is not None:
177
+ ctx.canvas.fill_style = fill
178
+ ctx.canvas.fill_rect(x, y, w, h)
179
+
180
+ def legend_glyph(self, ctx: Any, x: float, y: float, w: float, h: float) -> None:
181
+ self.legend_glyph_at(ctx, 0, x, y, w, h)
plot_io/series/bar.py ADDED
@@ -0,0 +1,294 @@
1
+ """Bars: one rectangle per category or x value, grouped or stacked."""
2
+
3
+ import array
4
+ import math
5
+ from typing import Any
6
+
7
+ from .. import _backend
8
+ from ..labels import bar_labels, check_labels
9
+ from ..numbers import NumberStyle, check_format, format_numbers, number_settings
10
+ from ..scales import get_scale
11
+ from ..settings import Setting, boolean, color, number, one_of, table
12
+ from ..shapes import draw_rects
13
+ from ..slots import slots
14
+ from ..stacking import check_stack, stacked
15
+ from ..style import desaturate, with_alpha
16
+ from ..text import Font
17
+ from . import Series, common
18
+ from .errors import error_value
19
+ from ..values import Values, category_paint
20
+
21
+ __all__ = ["Bar", "category_items"]
22
+
23
+
24
+ def category_items(x: Any, values: Any) -> tuple[list[Any], Any, bool]:
25
+ """``(keys, values, categorical)`` of bar-like input: ``x`` and values, a dict, or
26
+ values alone (positions 0, 1, …)."""
27
+ if values is None:
28
+ if isinstance(x, dict):
29
+ keys = list(x.keys())
30
+ return keys, _backend.doubles(list(x.values())), all(isinstance(k, str) for k in keys)
31
+ vals = _backend.doubles(x)
32
+ return [float(i) for i in range(len(vals))], vals, False
33
+ vals = _backend.doubles(values)
34
+ if isinstance(x, (list, tuple)) and x and all(isinstance(k, str) for k in x):
35
+ keys = list(x)
36
+ categorical = True
37
+ else:
38
+ keys = list(_backend.doubles(x))
39
+ categorical = False
40
+ if len(keys) != len(vals):
41
+ raise ValueError(f"{len(keys)} positions for {len(vals)} values")
42
+ return keys, vals, categorical
43
+
44
+
45
+ def _bottom(v: Any) -> Any:
46
+ if isinstance(v, (int, float)) and not isinstance(v, bool):
47
+ return float(v)
48
+ return _backend.doubles(v)
49
+
50
+
51
+ def _width(v: Any) -> float:
52
+ if hasattr(v, "total_seconds"):
53
+ v = v.total_seconds()
54
+ return number(positive=True)(v)
55
+
56
+
57
+ class Bar(Series):
58
+ """Bars of ``height`` at ``x`` (categories or numbers), or of a dict
59
+ ``{category: height}``, or of heights alone (at 0, 1, …)."""
60
+
61
+ SETTINGS = table(
62
+ color=Setting(category_paint,
63
+ doc="Bar fill: a color, one per bar (list or dict by category), or "
64
+ "numbers per bar (list or dict) mapped through `color_map`; "
65
+ "default: the next palette color."),
66
+ **common("color_map"),
67
+ line_color=Setting(color, doc="Outline color; default none."),
68
+ line_width=Setting(number(lo=0), doc="Outline width (pt).", length=True),
69
+ width=Setting(_width,
70
+ doc="Bar width: a category's slot (category units) or data units (a "
71
+ "`timedelta` on time axes)."),
72
+ bottom=Setting(_bottom, doc="Base of the bars: a value or one per bar."),
73
+ orientation=Setting(one_of("vertical", "horizontal"),
74
+ doc="`vertical` (bars rise from the x axis) or `horizontal`."),
75
+ stack=Setting(check_stack,
76
+ doc="`True`: on top of the previous stacked bars of the plot; "
77
+ "`\"normalize\"`: stacks scaled to 1."),
78
+ dodge=Setting(boolean, doc="Bar series sharing a category share its slot side "
79
+ "by side; `False` overlays them."),
80
+ gap=Setting(number(0, 1), doc="Fraction of each bar's part of a shared slot "
81
+ "left empty."),
82
+ saturation=Setting(number(0, 1), doc="Saturation of the fills (seaborn: 0.75)."),
83
+ error=Setting(error_value, doc="Error bars at the bar ends: a value, a sequence, "
84
+ "`(low, high)` or `ErrorBars`."),
85
+ error_color=Setting(color, doc="Error bar color."),
86
+ labels=Setting(check_labels,
87
+ doc="Value labels: `True`/`\"outside\"`, `\"inside\"`, `\"center\"`."),
88
+ label_format=Setting(check_format, doc="Number format of the labels."),
89
+ **common("name", "alpha", "z", "rasterize", "simplify", "font_size", "font_family",
90
+ "text_color"),
91
+ **number_settings(),
92
+ )
93
+ DODGE_GROUP = "bar"
94
+ STACK_GROUP = "bar"
95
+
96
+ def __init__(self, x: Any, height: Any = None, plot: Any = None, **settings: Any) -> None:
97
+ keys, vals, categorical = category_items(x, height)
98
+ self.cats = keys
99
+ self.heights = vals
100
+ self.positions = None if categorical else [float(k) for k in keys]
101
+ super().__init__(plot=plot, **settings)
102
+
103
+ def column_role(self, param: str) -> str | None:
104
+ if param == "x":
105
+ return self._cat_role()
106
+ return super().column_role(param)
107
+
108
+ def _horizontal(self) -> bool:
109
+ return self.get("orientation") == "horizontal"
110
+
111
+ def _cat_role(self) -> str:
112
+ return "y" if self._horizontal() else "x"
113
+
114
+ def stack_layers(self) -> list[tuple[list[Any], Any]]:
115
+ return [(self.cats, self.heights)]
116
+
117
+ def _ends(self) -> tuple[list[float], list[float]]:
118
+ """Bottoms and tops of the bars (data units)."""
119
+ if self.get("stack"):
120
+ return stacked(self)[0]
121
+ b = self.get("bottom")
122
+ n = len(self.heights)
123
+ bottoms = [b] * n if isinstance(b, float) else list(b)
124
+ if len(bottoms) != n:
125
+ raise ValueError(f"{len(bottoms)} bottoms for {n} bars")
126
+ return bottoms, [lo + h for lo, h in zip(bottoms, self.heights)]
127
+
128
+ # -- axes -------------------------------------------------------------------------
129
+
130
+ def categories(self, role: str) -> list[Any] | None:
131
+ if role == self._cat_role() and self.positions is None:
132
+ return list(self.cats)
133
+ return None
134
+
135
+ def values(self, role: str) -> Any:
136
+ if role == self._cat_role():
137
+ return array.array("d", self.positions or [])
138
+ bottoms, tops = self._ends()
139
+ out = array.array("d", bottoms)
140
+ out.extend(tops)
141
+ e = self.get("error")
142
+ if e is not None:
143
+ e.bind(len(tops))
144
+ lo, hi = e.ends(tops)
145
+ out.extend(lo)
146
+ out.extend(hi)
147
+ return out
148
+
149
+ def extent(self, role: str, scale: Any) -> tuple[int, float, float]:
150
+ if role == self._cat_role() and self.positions is not None:
151
+ w = self.get("width") / 2
152
+ n, lo, hi = _backend.extent(self.positions, scale.function)
153
+ return n, lo - w, hi + w
154
+ return super().extent(role, scale)
155
+
156
+ def zero_default(self, role: str) -> bool:
157
+ return role != self._cat_role()
158
+
159
+ def zero_padding(self, role: str) -> bool:
160
+ return False
161
+
162
+ def _per_bar(self, v: Any) -> list[float]:
163
+ if isinstance(v, dict):
164
+ if self.cats is None:
165
+ raise ValueError("a dict of colors needs categorical bars")
166
+ return [v.get(k, float("nan")) for k in self.cats]
167
+ if len(v) != len(self.heights):
168
+ raise ValueError(f"{len(v)} values for {len(self.heights)} bars")
169
+ return list(v)
170
+
171
+ def color_sources(self) -> list[tuple[Any, Any]]:
172
+ c = self.get("color")
173
+ if isinstance(c, Values):
174
+ return [(self.color_map_obj(), array.array("d", self._per_bar(c.data)))]
175
+ return []
176
+
177
+ def has_own_color(self) -> bool:
178
+ return self.is_set("color")
179
+
180
+ def occupy(self, ctx: Any, occ: Any) -> None:
181
+ bottoms, tops = self._ends()
182
+ pos = self.positions or [float(i) for i in range(len(self.cats))]
183
+ w = self.get("width") / 2
184
+ lo = [p - w for p in pos]
185
+ hi = [p + w for p in pos]
186
+ if self._horizontal():
187
+ occ.rects(ctx.xmap.many(bottoms), ctx.ymap.many(lo), ctx.xmap.many(tops),
188
+ ctx.ymap.many(hi))
189
+ else:
190
+ occ.rects(ctx.xmap.many(lo), ctx.ymap.many(bottoms), ctx.xmap.many(hi),
191
+ ctx.ymap.many(tops))
192
+
193
+ def legend_points(self) -> tuple[Any, Any] | None:
194
+ bottoms, tops = self._ends()
195
+ pos = self.positions or [float(i) for i in range(len(self.cats))]
196
+ if self._horizontal():
197
+ return array.array("d", tops), array.array("d", pos)
198
+ return array.array("d", pos), array.array("d", tops)
199
+
200
+ # -- drawing ----------------------------------------------------------------------
201
+
202
+ def fills(self, ctx: Any) -> list[Any]:
203
+ c = self.get("color")
204
+ n = len(self.heights)
205
+ if isinstance(c, Values):
206
+ cm = self.color_map_obj()
207
+ flo, fhi = ctx.maps[id(cm)][:2]
208
+ f = get_scale(cm.scale).f
209
+ span = (fhi - flo) or 1.0
210
+ cols = [cm((f(v) - flo) / span) for v in self._per_bar(c.data)]
211
+ elif isinstance(c, dict):
212
+ if self.cats is None:
213
+ raise ValueError("a dict of colors needs categorical bars")
214
+ cols = [ctx.color(c[k]) if c.get(k) is not None else None for k in self.cats]
215
+ elif isinstance(c, list):
216
+ if len(c) != n:
217
+ raise ValueError(f"{len(c)} colors for {n} bars")
218
+ cols = [ctx.color(x) for x in c]
219
+ else:
220
+ cols = [self.base_color(ctx)] * n
221
+ sat = self.get("saturation")
222
+ alpha = self.get("alpha")
223
+ alpha = 1.0 if alpha is None else alpha
224
+ return [with_alpha(desaturate(x, sat) if sat != 1.0 else x, alpha)
225
+ if x is not None else None for x in cols]
226
+
227
+ def rects(self, ctx: Any) -> list[tuple[float, float, float, float, bool]]:
228
+ """Page rectangles ``(x0, y0, x1, y1, negative)`` of the bars."""
229
+ horiz = self._horizontal()
230
+ cmap, vmap = (ctx.ymap, ctx.xmap) if horiz else (ctx.xmap, ctx.ymap)
231
+ bottoms, tops = self._ends()
232
+ out = []
233
+ for (center, width), b, t in zip(slots(self, ctx), bottoms, tops):
234
+ a0, a1 = cmap(center - width / 2), cmap(center + width / 2)
235
+ v0, v1 = vmap(b), vmap(t)
236
+ neg = t < b
237
+ if horiz:
238
+ out.append((min(v0, v1), min(a0, a1), max(v0, v1), max(a0, a1), neg))
239
+ else:
240
+ out.append((min(a0, a1), min(v0, v1), max(a0, a1), max(v0, v1), neg))
241
+ return out
242
+
243
+ def draw(self, ctx: Any) -> None:
244
+ rects = self.rects(ctx)
245
+ fills = self.fills(ctx)
246
+ lc = ctx.color(self.get("line_color"))
247
+ lw = self.get("line_width")
248
+ keep = [i for i, r in enumerate(rects) if all(math.isfinite(v) for v in r[:4])]
249
+ draw_rects(ctx.canvas, [rects[i][0] for i in keep], [rects[i][1] for i in keep],
250
+ [rects[i][2] - rects[i][0] for i in keep],
251
+ [rects[i][3] - rects[i][1] for i in keep],
252
+ fill=[fills[i] for i in keep], stroke=lc if lw else None,
253
+ line_width=lw or None, simplify=self.simplify_arg())
254
+ e = self.get("error")
255
+ if e is not None:
256
+ bottoms, tops = self._ends()
257
+ e.bind(len(tops))
258
+ centers = [c for c, _ in slots(self, ctx)]
259
+ ec = ctx.color(self.get("error_color"))
260
+ if self._horizontal():
261
+ e.draw(ctx, tops, centers, True, ec, 0.8, None, 1.0)
262
+ else:
263
+ e.draw(ctx, centers, tops, False, ec, 0.8, None, 1.0)
264
+
265
+ def draw_overlay(self, ctx: Any) -> None:
266
+ where = self.get("labels")
267
+ if not where:
268
+ return
269
+ rects = self.rects(ctx)
270
+ fills = self.fills(ctx)
271
+ keep = [i for i, r in enumerate(rects) if all(math.isfinite(v) for v in r[:4])]
272
+ bottoms, tops = self._ends()
273
+ vals = [t - b for b, t in zip(bottoms, tops)]
274
+ texts = format_numbers(vals, self.get("label_format"), NumberStyle.of(self))
275
+ font = Font(self.get("font_size"), self.get("font_family"),
276
+ color=ctx.color(self.get("text_color")))
277
+ bar_labels(ctx.canvas, [rects[i] for i in keep], [texts[i] for i in keep],
278
+ [fills[i] for i in keep], where, self._horizontal(), font)
279
+
280
+ def legend_colors(self, ctx: Any) -> tuple[Any, Any]:
281
+ fills = self.fills(ctx)
282
+ lc = ctx.color(self.get("line_color")) if self.get("line_width") else None
283
+ return (fills[0] if fills else None), lc
284
+
285
+ def legend_glyph(self, ctx: Any, x: float, y: float, w: float, h: float) -> None:
286
+ fill, stroke = self.legend_colors(ctx)
287
+ canvas = ctx.canvas
288
+ canvas.begin_path()
289
+ canvas.rect(x, y, w, h)
290
+ if fill is not None:
291
+ canvas.fill(fill)
292
+ if stroke is not None:
293
+ canvas.line_width = self.get("line_width")
294
+ canvas.stroke(stroke)