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/scales.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Scales: a monotone function from data to a position line.
|
|
2
|
+
|
|
3
|
+
A numeric scale maps a value ``v`` to ``f(v)``; an axis maps ``f(lo)…f(hi)`` linearly onto
|
|
4
|
+
its length. ``f`` is one of the functions pdf_io's ``arrays.scaled`` applies in C, so bulk
|
|
5
|
+
data never goes through a Python loop.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
|
|
11
|
+
__all__ = ["Scale", "SCALES", "get_scale", "NUMERIC_SCALES", "AXIS_SCALES"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Scale:
|
|
15
|
+
"""``forward``/``inverse`` on single values; ``function`` is pdf_io's name for it."""
|
|
16
|
+
|
|
17
|
+
__slots__ = ("name", "function", "forward", "inverse", "lo", "hi", "options")
|
|
18
|
+
|
|
19
|
+
def __init__(self, name: str, function: str | None, forward: Callable[[float], float],
|
|
20
|
+
inverse: Callable[[float], float], lo: float = -math.inf,
|
|
21
|
+
hi: float = math.inf, options: dict | None = None) -> None:
|
|
22
|
+
self.name = name
|
|
23
|
+
self.function = function
|
|
24
|
+
self.forward = forward
|
|
25
|
+
self.inverse = inverse
|
|
26
|
+
self.lo = lo # open domain bounds for log, closed for sqrt (see contains)
|
|
27
|
+
self.hi = hi
|
|
28
|
+
self.options = options or {} # extra arguments of pdf_io's scaled (linthresh)
|
|
29
|
+
|
|
30
|
+
def contains(self, v: float) -> bool:
|
|
31
|
+
if not math.isfinite(v):
|
|
32
|
+
return False
|
|
33
|
+
if self.name in ("log", "log2", "log10"):
|
|
34
|
+
return v > 0
|
|
35
|
+
if self.name == "sqrt":
|
|
36
|
+
return v >= 0
|
|
37
|
+
if self.name == "exp":
|
|
38
|
+
return v < 709.78
|
|
39
|
+
if self.name == "logit":
|
|
40
|
+
return 0.0 < v < 1.0
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
def f(self, v: float) -> float:
|
|
44
|
+
return self.forward(v) if self.contains(v) else math.nan
|
|
45
|
+
|
|
46
|
+
def __repr__(self) -> str:
|
|
47
|
+
return f"Scale({self.name})"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _safe_exp(t: float) -> float:
|
|
51
|
+
return math.exp(t) if t < 709.78 else math.inf
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
SCALES = {
|
|
55
|
+
"linear": Scale("linear", None, lambda v: v, lambda t: t),
|
|
56
|
+
"log": Scale("log", "log", math.log, _safe_exp),
|
|
57
|
+
"log2": Scale("log2", "log2", math.log2, lambda t: 2.0 ** t if t < 1024 else math.inf),
|
|
58
|
+
"log10": Scale("log10", "log10", math.log10, lambda t: 10.0 ** t if t < 308 else math.inf),
|
|
59
|
+
"sqrt": Scale("sqrt", "sqrt", math.sqrt, lambda t: t * t),
|
|
60
|
+
"exp": Scale("exp", "exp", _safe_exp, lambda t: math.log(t) if t > 0 else -math.inf),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
def _symlog(c: float) -> Scale:
|
|
64
|
+
def f(v: float) -> float:
|
|
65
|
+
return math.copysign(math.log10(1.0 + abs(v) / c), v)
|
|
66
|
+
|
|
67
|
+
def inv(t: float) -> float:
|
|
68
|
+
return math.copysign(c * (10.0 ** abs(t) - 1.0), t) if abs(t) < 308 else \
|
|
69
|
+
math.copysign(math.inf, t)
|
|
70
|
+
return Scale("symlog", "symlog", f, inv, options={"linthresh": c})
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _logit_inverse(t: float) -> float:
|
|
74
|
+
if t > 300:
|
|
75
|
+
return 1.0
|
|
76
|
+
return 1.0 / (1.0 + 10.0 ** -t)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
SCALES["symlog"] = _symlog(1.0)
|
|
80
|
+
SCALES["logit"] = Scale("logit", "logit", lambda p: math.log10(p / (1.0 - p)), _logit_inverse)
|
|
81
|
+
|
|
82
|
+
NUMERIC_SCALES = tuple(SCALES)
|
|
83
|
+
#: Scales an axis takes: the numeric ones, ``time`` (set by date data) and ``category``.
|
|
84
|
+
AXIS_SCALES = NUMERIC_SCALES + ("time", "category")
|
|
85
|
+
|
|
86
|
+
SCALES["time"] = Scale("time", None, lambda v: v, lambda t: t)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_scale(name: str, linthresh: float | None = None) -> Scale:
|
|
90
|
+
"""The scale ``name`` (``linthresh``: the linear region of ``symlog``)."""
|
|
91
|
+
if name == "symlog" and linthresh is not None and linthresh != 1.0:
|
|
92
|
+
return _symlog(float(linthresh))
|
|
93
|
+
return SCALES[name]
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
"""Series: data plus style, drawn in a plot.
|
|
2
|
+
|
|
3
|
+
A series reports what the axes need (extent, categories, values for quantile limits),
|
|
4
|
+
the color and size maps it uses, and draws itself and its legend glyph. New plot types
|
|
5
|
+
are new subclasses.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .. import _backend
|
|
12
|
+
from ..settings import Setting, Settings, color, dpi_or_bool, number, route, simplify_value, table, text
|
|
13
|
+
|
|
14
|
+
__all__ = ["Series", "COMMON", "DrawContext"]
|
|
15
|
+
|
|
16
|
+
COMMON = dict(
|
|
17
|
+
name=lambda: Setting(text, doc="Legend entry; unset: not in the legend."),
|
|
18
|
+
alpha=lambda: Setting(number(0, 1), inherited=True, doc="Opacity, multiplying the colors' own."),
|
|
19
|
+
z=lambda: Setting(number(), doc="Drawing order (higher on top); default: call order."),
|
|
20
|
+
rasterize=lambda: Setting(dpi_or_bool, inherited=True,
|
|
21
|
+
doc="`True` or a dpi: drawn as an image inside the PDF."),
|
|
22
|
+
simplify=lambda: Setting(simplify_value, inherited=True,
|
|
23
|
+
doc="`\"auto\"`, `True`, a dpi or `False`: pdf_io's `simplify`."),
|
|
24
|
+
font_size=lambda: Setting(number(positive=True), inherited=True, doc="Base font size (pt).", length=True),
|
|
25
|
+
font_family=lambda: Setting(str, inherited=True, doc="Font family."),
|
|
26
|
+
text_color=lambda: Setting(color, inherited=True, doc="Text color."),
|
|
27
|
+
line_width=lambda: Setting(number(lo=0), inherited=True, doc="Line width (pt).", length=True),
|
|
28
|
+
color_map=lambda: Setting(_check_color_map, inherited=True,
|
|
29
|
+
doc="How numbers in `color` become colors: a `ColorMap`, a map "
|
|
30
|
+
"name or colors (alias `cmap`); default: the style's."),
|
|
31
|
+
size_legend=lambda: Setting(_size_legend,
|
|
32
|
+
doc="With a `size_map`: a size key in the legend (`True`), "
|
|
33
|
+
"none (`False`), or the values to show (a list)."),
|
|
34
|
+
size_map=lambda: Setting(_check_size_map,
|
|
35
|
+
doc="How numbers in `size` become sizes: a `SizeMap` (alias "
|
|
36
|
+
"`smap`); without it, numbers are sizes in pt."),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _size_legend(v: Any) -> Any:
|
|
41
|
+
if isinstance(v, bool):
|
|
42
|
+
return v
|
|
43
|
+
if isinstance(v, (list, tuple)) and v:
|
|
44
|
+
return [float(x) for x in v]
|
|
45
|
+
raise ValueError(f"expected True, False or a list of values, got {v!r}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _check_color_map(v: Any) -> Any:
|
|
49
|
+
from ..values import check_color_map
|
|
50
|
+
return check_color_map(v)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _check_size_map(v: Any) -> Any:
|
|
54
|
+
from ..values import check_size_map
|
|
55
|
+
return check_size_map(v)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
#: Data arguments whose strings name columns when ``data`` is given (others than the
|
|
59
|
+
#: positional ones).
|
|
60
|
+
DATA_KEYS = ("color", "line_color", "marker_color", "size", "alpha", "group", "order",
|
|
61
|
+
"x_error", "y_error", "weights", "positions", "x", "y", "labels")
|
|
62
|
+
#: Of those, the ones whose strings must be columns.
|
|
63
|
+
MUST_BE_COLUMNS = ("group", "weights", "positions")
|
|
64
|
+
#: Per-point keyword arguments split among the members of a group.
|
|
65
|
+
SPLIT_KEYS = ("color", "line_color", "marker_color", "size", "alpha", "marker", "x_error",
|
|
66
|
+
"y_error", "order", "weights", "x", "y", "labels")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _positional(cls: type) -> list[str]:
|
|
70
|
+
import inspect
|
|
71
|
+
names = cls.__dict__.get("_POSITIONAL")
|
|
72
|
+
if names is None:
|
|
73
|
+
sig = inspect.signature(getattr(cls, "__init__"))
|
|
74
|
+
names = [n for n, p in sig.parameters.items()
|
|
75
|
+
if n not in ("self", "plot") and p.kind in (p.POSITIONAL_ONLY,
|
|
76
|
+
p.POSITIONAL_OR_KEYWORD)]
|
|
77
|
+
setattr(cls, "_POSITIONAL", names)
|
|
78
|
+
return names
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _length(v: Any) -> int | None:
|
|
82
|
+
if v is None or isinstance(v, (str, bytes, dict, int, float)):
|
|
83
|
+
return None
|
|
84
|
+
try:
|
|
85
|
+
return len(v)
|
|
86
|
+
except TypeError:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class SeriesMeta(type):
|
|
91
|
+
"""Resolves ``data`` columns and splits ``group`` calls before a series is made."""
|
|
92
|
+
|
|
93
|
+
def __call__(cls, *args: Any, **kw: Any) -> Any:
|
|
94
|
+
from .. import tables
|
|
95
|
+
from ..settings import canonical_kwargs
|
|
96
|
+
with_y = getattr(cls, "with_y", None)
|
|
97
|
+
if with_y is not None and (len(args) == 2 or "y" in kw):
|
|
98
|
+
return with_y()(*args, **kw)
|
|
99
|
+
names = _positional(cls)
|
|
100
|
+
kw = canonical_kwargs(kw, getattr(cls, "SETTINGS", {}), cls.__name__)
|
|
101
|
+
if len(args) > len(names):
|
|
102
|
+
raise TypeError(f"{cls.__name__}() takes {len(names)} positional arguments")
|
|
103
|
+
pos = dict(zip(names, args))
|
|
104
|
+
for n in names:
|
|
105
|
+
if n in kw:
|
|
106
|
+
if n in pos:
|
|
107
|
+
raise TypeError(f"{cls.__name__}() got two values for {n!r}")
|
|
108
|
+
pos[n] = kw.pop(n)
|
|
109
|
+
plot = kw.get("plot")
|
|
110
|
+
data = kw.pop("data", None)
|
|
111
|
+
if data is None and plot is not None:
|
|
112
|
+
data = plot.get("data")
|
|
113
|
+
columns: dict[str, Any] = {}
|
|
114
|
+
orders: dict[str, Any] = {}
|
|
115
|
+
if data is not None:
|
|
116
|
+
for n, v in list(pos.items()):
|
|
117
|
+
if isinstance(v, str):
|
|
118
|
+
pos[n], orders[n] = tables.column(data, v)
|
|
119
|
+
columns[n] = v
|
|
120
|
+
for n in DATA_KEYS:
|
|
121
|
+
v = kw.get(n)
|
|
122
|
+
if isinstance(v, str) and n not in pos:
|
|
123
|
+
if tables.has_column(data, v):
|
|
124
|
+
if n in ("color", "line_color", "marker_color"):
|
|
125
|
+
from ..settings import color
|
|
126
|
+
try:
|
|
127
|
+
color(v)
|
|
128
|
+
tables.color_name_warning(v)
|
|
129
|
+
except ValueError:
|
|
130
|
+
pass
|
|
131
|
+
kw[n], orders[n] = tables.column(data, v)
|
|
132
|
+
columns[n] = v
|
|
133
|
+
elif n in MUST_BE_COLUMNS:
|
|
134
|
+
raise ValueError(f"no column {v!r} in data")
|
|
135
|
+
from .. import times as _times
|
|
136
|
+
time_params: dict[str, Any] = {}
|
|
137
|
+
for store in (pos, kw):
|
|
138
|
+
for n in ("x", "y", "positions"):
|
|
139
|
+
v = store.get(n)
|
|
140
|
+
if v is not None and _times.is_time_like(v):
|
|
141
|
+
store[n], time_params[n] = _times.to_seconds(v)
|
|
142
|
+
if "group" in kw:
|
|
143
|
+
group = cls._grouped(pos, kw, columns, orders)
|
|
144
|
+
for m in group:
|
|
145
|
+
m.times = dict(time_params)
|
|
146
|
+
return group
|
|
147
|
+
for k in ("group_order", "group_missing", "group_colors"):
|
|
148
|
+
if k in kw:
|
|
149
|
+
raise ValueError(f"{k} applies with group=")
|
|
150
|
+
if getattr(cls, "LONG_FORM", False):
|
|
151
|
+
cls._long_form(pos, kw, columns, orders)
|
|
152
|
+
s = super().__call__(**pos, **kw)
|
|
153
|
+
s.columns = columns
|
|
154
|
+
s.times = time_params
|
|
155
|
+
return s
|
|
156
|
+
|
|
157
|
+
def _long_form(cls, pos: dict[str, Any], kw: dict[str, Any], columns: dict[str, Any],
|
|
158
|
+
orders: dict[str, Any]) -> None:
|
|
159
|
+
"""``values`` flat with categories per point in ``x`` (``y`` when horizontal):
|
|
160
|
+
a dict ``{category: values}`` in first-appearance (or categorical) order."""
|
|
161
|
+
key = "y" if kw.get("orientation") == "horizontal" else "x"
|
|
162
|
+
cats = kw.get(key)
|
|
163
|
+
vals = pos.get("values")
|
|
164
|
+
n = _length(vals)
|
|
165
|
+
if cats is None or n is None or isinstance(vals, dict) or _length(cats) != n:
|
|
166
|
+
return
|
|
167
|
+
kw.pop(key)
|
|
168
|
+
from .. import tables
|
|
169
|
+
out: dict[Any, list[Any]] = {}
|
|
170
|
+
for c in orders.get(key) or []:
|
|
171
|
+
out[c] = []
|
|
172
|
+
from .. import _backend
|
|
173
|
+
d = _backend.doubles(vals)
|
|
174
|
+
for c, v in zip(cats, d):
|
|
175
|
+
if tables.is_missing(c):
|
|
176
|
+
continue
|
|
177
|
+
out.setdefault(c, []).append(v)
|
|
178
|
+
pos["values"] = {k: v for k, v in out.items() if v}
|
|
179
|
+
if key in columns:
|
|
180
|
+
columns["categories"] = columns.pop(key)
|
|
181
|
+
|
|
182
|
+
def _grouped(cls, pos: dict[str, Any], kw: dict[str, Any], columns: dict[str, Any],
|
|
183
|
+
orders: dict[str, Any]) -> Any:
|
|
184
|
+
from .. import tables
|
|
185
|
+
labels = kw.pop("group")
|
|
186
|
+
given = kw.pop("group_order", None)
|
|
187
|
+
missing = kw.pop("group_missing", "keep")
|
|
188
|
+
if missing not in ("keep", "drop"):
|
|
189
|
+
raise ValueError("group_missing is 'keep' or 'drop'")
|
|
190
|
+
colors = kw.pop("group_colors", None) or {}
|
|
191
|
+
n = _length(labels)
|
|
192
|
+
if n is None:
|
|
193
|
+
raise ValueError("group needs one label per point")
|
|
194
|
+
if hasattr(labels, "tolist"):
|
|
195
|
+
labels = labels.tolist()
|
|
196
|
+
keys, rows = tables.group_keys(labels, orders.get("group"), given, missing)
|
|
197
|
+
auto_marker = kw.get("marker") == "auto"
|
|
198
|
+
auto_style = kw.get("line_style") == "auto"
|
|
199
|
+
title = columns.get("group")
|
|
200
|
+
members: list[Any] = []
|
|
201
|
+
for k, (key, idx) in enumerate(zip(keys, rows)):
|
|
202
|
+
pk = {nm: (tables.take(v, idx) if _length(v) == n else v) for nm, v in pos.items()}
|
|
203
|
+
kk = {}
|
|
204
|
+
for nm, v in kw.items():
|
|
205
|
+
if nm in SPLIT_KEYS and _length(v) == n and not (
|
|
206
|
+
isinstance(v, tuple) and len(v) in (3, 4)):
|
|
207
|
+
v = tables.take(v, idx)
|
|
208
|
+
kk[nm] = v
|
|
209
|
+
if auto_marker:
|
|
210
|
+
kk["marker"] = tables.MARKER_CYCLE[k % len(tables.MARKER_CYCLE)]
|
|
211
|
+
if auto_style:
|
|
212
|
+
kk["line_style"] = tables.LINE_STYLE_CYCLE[k % len(tables.LINE_STYLE_CYCLE)]
|
|
213
|
+
if key in colors:
|
|
214
|
+
kk["color"] = colors[key]
|
|
215
|
+
kk.setdefault("name", str(key))
|
|
216
|
+
if k > 0 and kk.get("plot") is None and members:
|
|
217
|
+
kk["plot"] = members[0].plot
|
|
218
|
+
s = cls(**pk, **kk)
|
|
219
|
+
s.columns = dict(columns)
|
|
220
|
+
members.append(s)
|
|
221
|
+
group = tables.SeriesGroup(members, keys, title)
|
|
222
|
+
if members and title is not None:
|
|
223
|
+
members[0].plot._legend_title = title
|
|
224
|
+
return group
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
#: Settings that may hold colors to map (numbers or a ``Density``).
|
|
228
|
+
COLOR_SETTINGS = ("color", "line_color", "marker_color")
|
|
229
|
+
#: Settings that may hold sizes to map.
|
|
230
|
+
SIZE_SETTINGS = ("size",)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def common(*names: str) -> dict[str, Setting]:
|
|
234
|
+
return {n: COMMON[n]() for n in names}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class DrawContext:
|
|
238
|
+
"""What a series needs while drawing: the canvas, the axes rectangle and mappers."""
|
|
239
|
+
|
|
240
|
+
def __init__(self, canvas: Any, plot: Any, rect: tuple[float, float, float, float],
|
|
241
|
+
x: Any, y: Any, maps: dict[int, tuple[float, float]], palette: tuple) -> None:
|
|
242
|
+
self.canvas = canvas
|
|
243
|
+
self.plot = plot
|
|
244
|
+
self.rect = rect
|
|
245
|
+
self.x = x
|
|
246
|
+
self.y = y
|
|
247
|
+
x0, y0, x1, y1 = rect
|
|
248
|
+
self.xmap = x.mapper(x0, x1)
|
|
249
|
+
self.ymap = y.mapper(y1, y0)
|
|
250
|
+
self.maps = maps
|
|
251
|
+
self.palette = palette
|
|
252
|
+
self.figure_size = (x1, y1)
|
|
253
|
+
|
|
254
|
+
def color(self, value: Any) -> Any:
|
|
255
|
+
from ..style import resolve_color
|
|
256
|
+
return resolve_color(value, self.palette)
|
|
257
|
+
|
|
258
|
+
def map_limits(self, m: Any) -> tuple[float, float]:
|
|
259
|
+
return self.maps[id(m)]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class Series(Settings, metaclass=SeriesMeta):
|
|
263
|
+
"""Base of every series. ``plot=None`` creates a plot (and its figure)."""
|
|
264
|
+
|
|
265
|
+
SETTINGS: dict[str, Setting] = table(**common("name", "alpha", "z", "rasterize",
|
|
266
|
+
"simplify"))
|
|
267
|
+
#: Whether the series takes the next palette color when it has no color of its own.
|
|
268
|
+
USES_PALETTE = True
|
|
269
|
+
#: Whether the series is drawn on axes (``False``: pie charts, on plots without axes).
|
|
270
|
+
AXES = True
|
|
271
|
+
#: Dodge group of categorical series sharing category slots (``"box"``, ``"bar"``,
|
|
272
|
+
#: ``"points"``); ``None``: not dodged.
|
|
273
|
+
DODGE_GROUP: str | None = None
|
|
274
|
+
#: Stack group of series that stack with ``stack=True`` (``"bar"``, ``"area"``).
|
|
275
|
+
STACK_GROUP: str | None = None
|
|
276
|
+
|
|
277
|
+
#: Whether ``values`` may be flat with categories per point in ``x`` (long data).
|
|
278
|
+
LONG_FORM = False
|
|
279
|
+
|
|
280
|
+
def __init__(self, *, plot: Any = None, **settings: Any) -> None:
|
|
281
|
+
from ..figure import Figure
|
|
282
|
+
from ..plot import Plot
|
|
283
|
+
|
|
284
|
+
own, plot_kw, fig_kw = route(settings, [type(self), Plot, Figure],
|
|
285
|
+
extra={1: Plot.EXTRA})
|
|
286
|
+
if plot is None:
|
|
287
|
+
plot = Plot(**plot_kw, **fig_kw)
|
|
288
|
+
else:
|
|
289
|
+
if plot_kw:
|
|
290
|
+
plot.set(**plot_kw)
|
|
291
|
+
if fig_kw:
|
|
292
|
+
plot.figure.set(**fig_kw)
|
|
293
|
+
self.plot = plot
|
|
294
|
+
self._auto_color: Any = None
|
|
295
|
+
super().__init__(**own)
|
|
296
|
+
plot._add_series(self)
|
|
297
|
+
|
|
298
|
+
def _parent(self) -> Settings:
|
|
299
|
+
return self.plot
|
|
300
|
+
|
|
301
|
+
def set(self, **settings: Any) -> Any:
|
|
302
|
+
super().set(**settings)
|
|
303
|
+
self._cache: dict[Any, Any] = {}
|
|
304
|
+
return self
|
|
305
|
+
|
|
306
|
+
def cached(self, key: Any, compute: Any) -> Any:
|
|
307
|
+
"""``compute()`` once until a setting changes (derived data: bins, densities…)."""
|
|
308
|
+
cache = self.__dict__.setdefault("_cache", {})
|
|
309
|
+
if key not in cache:
|
|
310
|
+
cache[key] = compute()
|
|
311
|
+
return cache[key]
|
|
312
|
+
|
|
313
|
+
def __repr__(self) -> str:
|
|
314
|
+
name = self.get("name")
|
|
315
|
+
return f"{self.kind}({name!r})" if name else f"{self.kind}()"
|
|
316
|
+
|
|
317
|
+
# -- what the axes need --------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
def has_axis(self, role: str) -> bool:
|
|
320
|
+
return True
|
|
321
|
+
|
|
322
|
+
def time_role(self, role: str) -> Any:
|
|
323
|
+
"""``(True, tz)`` when the data on axis ``role`` were dates, else ``None``."""
|
|
324
|
+
for param, tz in getattr(self, "times", {}).items():
|
|
325
|
+
if self.column_role(param) == role:
|
|
326
|
+
return (True, tz)
|
|
327
|
+
return None
|
|
328
|
+
|
|
329
|
+
def column_role(self, param: str) -> str | None:
|
|
330
|
+
"""The axis a data argument named ``param`` is drawn on (for axis titles taken from
|
|
331
|
+
column names)."""
|
|
332
|
+
if param in ("x", "y"):
|
|
333
|
+
return param
|
|
334
|
+
if param in ("values", "height", "categories"):
|
|
335
|
+
cat = getattr(self, "_cat_role", None)
|
|
336
|
+
if cat is not None:
|
|
337
|
+
role = cat()
|
|
338
|
+
return role if param == "categories" else ("y" if role == "x" else "x")
|
|
339
|
+
horizontal = "orientation" in self.SETTINGS and \
|
|
340
|
+
self.get("orientation") == "horizontal"
|
|
341
|
+
return "y" if horizontal else "x"
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
def axis_title(self, role: str) -> Any:
|
|
345
|
+
"""The column name drawn on axis ``role``, if the series was given a column."""
|
|
346
|
+
for param, name in getattr(self, "columns", {}).items():
|
|
347
|
+
if self.column_role(param) == role:
|
|
348
|
+
return name
|
|
349
|
+
return None
|
|
350
|
+
|
|
351
|
+
def categories(self, role: str) -> list[Any] | None:
|
|
352
|
+
"""Categories on axis ``role``, or ``None`` for numeric data."""
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
def values(self, role: str) -> Any:
|
|
356
|
+
"""All data values on axis ``role`` (for quantile limits)."""
|
|
357
|
+
return None
|
|
358
|
+
|
|
359
|
+
def extent(self, role: str, scale: Any) -> tuple[int, float, float]:
|
|
360
|
+
v = self.values(role)
|
|
361
|
+
if v is None:
|
|
362
|
+
return 0, math.nan, math.nan
|
|
363
|
+
return _backend.extent(v, scale.function)
|
|
364
|
+
|
|
365
|
+
def inverts(self, role: str) -> bool:
|
|
366
|
+
"""Whether axis ``role`` is inverted by default for this series."""
|
|
367
|
+
return False
|
|
368
|
+
|
|
369
|
+
def zero_default(self, role: str) -> bool:
|
|
370
|
+
"""Whether axis ``role`` includes 0 under ``include_zero="auto"`` (Vega-Lite)."""
|
|
371
|
+
return False
|
|
372
|
+
|
|
373
|
+
def zero_padding(self, role: str) -> bool:
|
|
374
|
+
"""Whether automatic limits on axis ``role`` take no padding."""
|
|
375
|
+
return False
|
|
376
|
+
|
|
377
|
+
def color_sources(self) -> list[tuple[Any, Any]]:
|
|
378
|
+
"""``(ColorMap, values)`` pairs the series maps: its color settings holding numbers
|
|
379
|
+
or a ``Density``, through ``color_map_obj()``."""
|
|
380
|
+
from ..values import Values, is_computed
|
|
381
|
+
out = []
|
|
382
|
+
for name in COLOR_SETTINGS:
|
|
383
|
+
if name in self.SETTINGS:
|
|
384
|
+
v = self.get(name)
|
|
385
|
+
if isinstance(v, Values) or is_computed(v):
|
|
386
|
+
out.append((self.color_map_obj(), self.point_values(v)))
|
|
387
|
+
return out
|
|
388
|
+
|
|
389
|
+
def size_sources(self) -> list[tuple[Any, Any]]:
|
|
390
|
+
out = []
|
|
391
|
+
for name in SIZE_SETTINGS:
|
|
392
|
+
m = self.size_map_obj(name)
|
|
393
|
+
if m is not None:
|
|
394
|
+
out.append((m, self.point_values(self.get(name))))
|
|
395
|
+
return out
|
|
396
|
+
|
|
397
|
+
def color_map_obj(self) -> Any:
|
|
398
|
+
"""The ``ColorMap`` numbers in ``color`` go through: ``color_map`` when it is a
|
|
399
|
+
``ColorMap`` (shared), else one made from it for this series."""
|
|
400
|
+
from ..maps import ColorMap
|
|
401
|
+
m = self.get("color_map") if "color_map" in self.SETTINGS else self.plot.get("color_map")
|
|
402
|
+
if isinstance(m, ColorMap):
|
|
403
|
+
return m
|
|
404
|
+
return self.cached(("color_map", repr(m)), lambda: ColorMap(m))
|
|
405
|
+
|
|
406
|
+
def size_map_obj(self, name: str = "size") -> Any:
|
|
407
|
+
"""The ``SizeMap`` of a size setting holding values, or ``None`` (sizes in pt)."""
|
|
408
|
+
from ..maps import SizeMap
|
|
409
|
+
from ..values import is_computed
|
|
410
|
+
if name not in self.SETTINGS or "size_map" not in self.SETTINGS:
|
|
411
|
+
return None
|
|
412
|
+
v = self.get(name)
|
|
413
|
+
m = self.get("size_map")
|
|
414
|
+
if is_computed(v):
|
|
415
|
+
return m if m is not None else self.cached("size_map", SizeMap)
|
|
416
|
+
if m is not None and not isinstance(v, (int, float)):
|
|
417
|
+
return m
|
|
418
|
+
return None
|
|
419
|
+
|
|
420
|
+
def point_values(self, v: Any) -> Any:
|
|
421
|
+
"""The numbers of a value setting: given ``Values``, sizes, or a computed
|
|
422
|
+
``Density``."""
|
|
423
|
+
from ..values import Values, is_computed
|
|
424
|
+
if isinstance(v, Values):
|
|
425
|
+
return v.data
|
|
426
|
+
if is_computed(v):
|
|
427
|
+
return self.cached(("density", id(v)), lambda: v.compute(self))
|
|
428
|
+
return v
|
|
429
|
+
|
|
430
|
+
def palette_count(self) -> int:
|
|
431
|
+
"""Palette colors the series takes when it has no color (one per value set of
|
|
432
|
+
multi-set series)."""
|
|
433
|
+
return 1
|
|
434
|
+
|
|
435
|
+
def auto_colors(self, ctx: Any) -> list[Any]:
|
|
436
|
+
"""The palette colors given to the series (``palette_count()`` of them)."""
|
|
437
|
+
cols = getattr(self, "_auto_colors", None) or [self._auto_color]
|
|
438
|
+
return [ctx.color(c) for c in cols]
|
|
439
|
+
|
|
440
|
+
def has_own_color(self) -> bool:
|
|
441
|
+
return self.is_set("color")
|
|
442
|
+
|
|
443
|
+
def base_color(self, ctx: DrawContext) -> Any:
|
|
444
|
+
"""The series color: its own, or the palette color it was given."""
|
|
445
|
+
c = self.get("color") if "color" in self.SETTINGS else None
|
|
446
|
+
if c is None:
|
|
447
|
+
c = self._auto_color
|
|
448
|
+
return ctx.color(c)
|
|
449
|
+
|
|
450
|
+
# -- drawing ---------------------------------------------------------------------
|
|
451
|
+
|
|
452
|
+
def draw(self, ctx: DrawContext) -> None:
|
|
453
|
+
raise NotImplementedError
|
|
454
|
+
|
|
455
|
+
def draw_overlay(self, ctx: DrawContext) -> None:
|
|
456
|
+
"""Drawn after every series, outside the axes clip (value labels)."""
|
|
457
|
+
|
|
458
|
+
def legend_glyph(self, ctx: DrawContext, x: float, y: float, w: float, h: float) -> None:
|
|
459
|
+
"""Draw the legend symbol in the box ``(x, y, w, h)`` (y: top)."""
|
|
460
|
+
|
|
461
|
+
def legend_colors(self, ctx: DrawContext) -> tuple[Any, Any]:
|
|
462
|
+
"""``(fill, stroke)`` of simple legend symbols (``Legend.glyph``)."""
|
|
463
|
+
return self.base_color(ctx), None
|
|
464
|
+
|
|
465
|
+
def legend_labels(self) -> list[Any]:
|
|
466
|
+
"""Legend entries of the object: its ``name``, or none."""
|
|
467
|
+
name = self.get("name")
|
|
468
|
+
return [] if name is None else [name]
|
|
469
|
+
|
|
470
|
+
def legend_glyph_at(self, ctx: Any, i: int, x: float, y: float, w: float, h: float) -> None:
|
|
471
|
+
"""The symbol of legend entry ``i`` (series with several entries override this)."""
|
|
472
|
+
self.legend_glyph(ctx, x, y, w, h)
|
|
473
|
+
|
|
474
|
+
def legend_colors_at(self, ctx: Any, i: int) -> tuple[Any, Any]:
|
|
475
|
+
return self.legend_colors(ctx)
|
|
476
|
+
|
|
477
|
+
def occupy(self, ctx: DrawContext, occ: Any) -> None:
|
|
478
|
+
"""Marks what the series draws on ``occ`` (an :class:`~plot_io.occupancy.Occupancy`),
|
|
479
|
+
for legend placement; by default its legend points."""
|
|
480
|
+
pts = self.legend_points()
|
|
481
|
+
if pts is not None:
|
|
482
|
+
from ..occupancy import thin
|
|
483
|
+
xs, ys = thin(*(_backend.doubles(v) for v in pts))
|
|
484
|
+
size = self.get("size") if "size" in self.SETTINGS else None
|
|
485
|
+
radius = size / 2 if isinstance(size, (int, float)) else 0.0
|
|
486
|
+
occ.points(ctx.xmap.many(xs), ctx.ymap.many(ys), radius)
|
|
487
|
+
|
|
488
|
+
def occupy_all(self, ctx: DrawContext, occ: Any) -> None:
|
|
489
|
+
"""Marks the whole axes (series that fill them: heatmaps, contours)."""
|
|
490
|
+
occ.rects([ctx.rect[0]], [ctx.rect[1]], [ctx.rect[2]], [ctx.rect[3]])
|
|
491
|
+
|
|
492
|
+
def legend_points(self) -> tuple[Any, Any] | None:
|
|
493
|
+
"""``(xs, ys)`` data points for the legend's ``auto`` placement."""
|
|
494
|
+
return None
|
|
495
|
+
|
|
496
|
+
def simplify_arg(self, lines: bool = False) -> Any:
|
|
497
|
+
"""pdf_io's ``simplify`` for this series' calls (``lines``: lines and areas)."""
|
|
498
|
+
return _backend.simplify_arg(self.get("simplify"), lines)
|