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
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)
|