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/times.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Dates and times on axes.
|
|
2
|
+
|
|
3
|
+
Values (``datetime``, ``date``, numpy ``datetime64`` arrays, pandas timestamps) become
|
|
4
|
+
float seconds since 1970-01-01 UTC, so scales, limits and the C mapping are unchanged; the
|
|
5
|
+
axis (``scale="time"``, set by date data) chooses calendar-aware ticks and concise labels.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import array
|
|
9
|
+
import calendar
|
|
10
|
+
import datetime as dt
|
|
11
|
+
import math
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
__all__ = ["is_time_like", "to_seconds", "seconds", "time_ticks", "from_seconds"]
|
|
15
|
+
|
|
16
|
+
_UTC = dt.timezone.utc
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _first(v: Any) -> Any:
|
|
20
|
+
for x in v:
|
|
21
|
+
if x is not None and not (isinstance(x, float) and math.isnan(x)):
|
|
22
|
+
return x
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def is_time_like(v: Any) -> bool:
|
|
27
|
+
"""Whether ``v`` is a date or time value, or a sequence or array of them."""
|
|
28
|
+
if isinstance(v, (dt.date, dt.datetime)):
|
|
29
|
+
return True
|
|
30
|
+
kind = getattr(getattr(v, "dtype", None), "kind", None)
|
|
31
|
+
if kind == "M":
|
|
32
|
+
return True
|
|
33
|
+
if isinstance(v, (list, tuple)) and v:
|
|
34
|
+
return isinstance(_first(v), (dt.date, dt.datetime))
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def seconds(x: Any) -> float:
|
|
39
|
+
"""Seconds since the epoch of one value (naive datetimes are UTC)."""
|
|
40
|
+
if x is None or (isinstance(x, float) and math.isnan(x)):
|
|
41
|
+
return math.nan
|
|
42
|
+
if isinstance(x, dt.datetime):
|
|
43
|
+
if x.tzinfo is not None:
|
|
44
|
+
return x.timestamp()
|
|
45
|
+
return calendar.timegm(x.timetuple()) + x.microsecond / 1e6
|
|
46
|
+
if isinstance(x, dt.date):
|
|
47
|
+
return float(calendar.timegm(x.timetuple()))
|
|
48
|
+
if hasattr(x, "astype") and getattr(getattr(x, "dtype", None), "kind", None) == "M":
|
|
49
|
+
n = int(x.astype("datetime64[ns]").astype("int64"))
|
|
50
|
+
return math.nan if n == -(2 ** 63) else n / 1e9
|
|
51
|
+
if isinstance(x, (int, float)):
|
|
52
|
+
return float(x)
|
|
53
|
+
raise ValueError(f"not a date or time: {x!r}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def to_seconds(v: Any) -> tuple[array.array, Any]:
|
|
57
|
+
"""Seconds since the epoch of every value, and the time zone of the first aware
|
|
58
|
+
value (``None``: naive, shown as UTC)."""
|
|
59
|
+
kind = getattr(getattr(v, "dtype", None), "kind", None)
|
|
60
|
+
if kind == "M":
|
|
61
|
+
ns = v.astype("datetime64[ns]").astype("int64").tolist()
|
|
62
|
+
nat = -(2 ** 63)
|
|
63
|
+
return array.array("d", (math.nan if n == nat else n / 1e9 for n in ns)), None
|
|
64
|
+
if isinstance(v, (dt.date, dt.datetime)):
|
|
65
|
+
v = [v]
|
|
66
|
+
first = _first(v)
|
|
67
|
+
tz = first.tzinfo if isinstance(first, dt.datetime) else None
|
|
68
|
+
return array.array("d", (seconds(x) for x in v)), tz
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def from_seconds(s: float, tz: Any) -> dt.datetime:
|
|
72
|
+
"""The datetime of ``s`` seconds in zone ``tz`` (naive UTC when ``tz`` is ``None``)."""
|
|
73
|
+
d = dt.datetime.fromtimestamp(s, tz or _UTC)
|
|
74
|
+
return d if tz is not None else d.replace(tzinfo=None)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _epoch(d: dt.datetime, tz: Any) -> float:
|
|
78
|
+
return d.timestamp() if tz is not None else seconds(d)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
#: (unit, step, approximate seconds) candidates, finest first.
|
|
82
|
+
_STEPS = ([("microsecond", s, s * 1e-6) for s in (1, 2, 5, 10, 20, 50, 100, 200, 500, 1000,
|
|
83
|
+
2000, 5000, 10000, 20000, 50000, 100000,
|
|
84
|
+
200000, 500000)]
|
|
85
|
+
+ [("second", s, s) for s in (1, 2, 5, 10, 15, 30)]
|
|
86
|
+
+ [("minute", s, 60 * s) for s in (1, 2, 5, 10, 15, 30)]
|
|
87
|
+
+ [("hour", s, 3600 * s) for s in (1, 2, 3, 6, 12)]
|
|
88
|
+
+ [("day", s, 86400 * s) for s in (1, 2, 7, 14)]
|
|
89
|
+
+ [("month", s, 2629746 * s) for s in (1, 2, 3, 6)]
|
|
90
|
+
+ [("year", s, 31556952 * s) for s in (1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500,
|
|
91
|
+
1000)])
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _floor(d: dt.datetime, unit: str, step: int) -> dt.datetime:
|
|
95
|
+
if unit == "year":
|
|
96
|
+
return d.replace(year=d.year - d.year % step, month=1, day=1, hour=0, minute=0,
|
|
97
|
+
second=0, microsecond=0)
|
|
98
|
+
if unit == "month":
|
|
99
|
+
m = (d.month - 1) - (d.month - 1) % step + 1
|
|
100
|
+
return d.replace(month=m, day=1, hour=0, minute=0, second=0, microsecond=0)
|
|
101
|
+
if unit == "day":
|
|
102
|
+
return d.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
|
103
|
+
if unit == "hour":
|
|
104
|
+
return d.replace(hour=d.hour - d.hour % step, minute=0, second=0, microsecond=0)
|
|
105
|
+
if unit == "minute":
|
|
106
|
+
return d.replace(minute=d.minute - d.minute % step, second=0, microsecond=0)
|
|
107
|
+
if unit == "second":
|
|
108
|
+
return d.replace(second=d.second - d.second % step, microsecond=0)
|
|
109
|
+
return d.replace(microsecond=0)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _add(d: dt.datetime, unit: str, step: int) -> dt.datetime:
|
|
113
|
+
if unit == "year":
|
|
114
|
+
return d.replace(year=d.year + step)
|
|
115
|
+
if unit == "month":
|
|
116
|
+
m = d.month - 1 + step
|
|
117
|
+
return d.replace(year=d.year + m // 12, month=m % 12 + 1)
|
|
118
|
+
delta = {"hour": dt.timedelta(hours=step), "minute": dt.timedelta(minutes=step),
|
|
119
|
+
"second": dt.timedelta(seconds=step),
|
|
120
|
+
"microsecond": dt.timedelta(microseconds=step),
|
|
121
|
+
"day": dt.timedelta(days=1)}[unit]
|
|
122
|
+
return d + delta
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _candidates(lo: float, hi: float, unit: str, step: int, tz: Any) -> list[float]:
|
|
126
|
+
start = _floor(from_seconds(lo, tz), unit, step)
|
|
127
|
+
out: list[float] = []
|
|
128
|
+
d = start
|
|
129
|
+
guard = 0
|
|
130
|
+
while guard < 100000:
|
|
131
|
+
guard += 1
|
|
132
|
+
s = _epoch(d, tz)
|
|
133
|
+
if s > hi + 1e-9:
|
|
134
|
+
break
|
|
135
|
+
keep = True
|
|
136
|
+
if unit == "day":
|
|
137
|
+
keep = (d.day - 1) % step == 0 and (step < 7 or d.day <= 29 - step)
|
|
138
|
+
if keep and s >= lo - 1e-9:
|
|
139
|
+
out.append(s)
|
|
140
|
+
d = _add(d, unit, step)
|
|
141
|
+
return out
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def time_ticks(lo: float, hi: float, max_ticks: int, tz: Any,
|
|
145
|
+
fmt: Any = "auto") -> tuple[list[tuple[float, str]], str | None]:
|
|
146
|
+
"""Ticks between ``lo`` and ``hi`` seconds: ``[(value, label)]`` and the offset text
|
|
147
|
+
(the larger unit common to every tick: ``"Mar 2026"``)."""
|
|
148
|
+
if hi < lo:
|
|
149
|
+
lo, hi = hi, lo
|
|
150
|
+
span = max(hi - lo, 1e-6)
|
|
151
|
+
unit = "year"
|
|
152
|
+
ticks: list[float] = []
|
|
153
|
+
for u, s, approx in _STEPS:
|
|
154
|
+
if span / approx > max_ticks * 1.5:
|
|
155
|
+
continue
|
|
156
|
+
cand = _candidates(lo, hi, u, s, tz)
|
|
157
|
+
if 2 <= len(cand) <= max_ticks or (len(cand) <= max_ticks and u == "year"):
|
|
158
|
+
unit, ticks = u, cand
|
|
159
|
+
break
|
|
160
|
+
if not ticks:
|
|
161
|
+
ticks = [lo, hi]
|
|
162
|
+
unit = "second" if span < 86400 else "day"
|
|
163
|
+
dts = [from_seconds(t, tz) for t in ticks]
|
|
164
|
+
if isinstance(fmt, str) and "%" in fmt:
|
|
165
|
+
return [(t, d.strftime(fmt)) for t, d in zip(ticks, dts)], None
|
|
166
|
+
if callable(fmt):
|
|
167
|
+
return [(t, str(fmt(d))) for t, d in zip(ticks, dts)], None
|
|
168
|
+
return _concise(ticks, dts, unit)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _concise(ticks: list[float], dts: list[dt.datetime], unit: str) -> tuple[
|
|
172
|
+
list[tuple[float, str]], str | None]:
|
|
173
|
+
same_year = len({d.year for d in dts}) == 1
|
|
174
|
+
same_day = len({d.date() for d in dts}) == 1
|
|
175
|
+
if unit == "year":
|
|
176
|
+
return [(t, d.strftime("%Y")) for t, d in zip(ticks, dts)], None
|
|
177
|
+
if unit == "month":
|
|
178
|
+
labels = [d.strftime("%Y") if d.month == 1 and not same_year else d.strftime("%b")
|
|
179
|
+
for d in dts]
|
|
180
|
+
return list(zip(ticks, labels)), (dts[0].strftime("%Y") if same_year else None)
|
|
181
|
+
if unit == "day":
|
|
182
|
+
labels = [d.strftime("%b %d").replace(" 0", " ") for d in dts]
|
|
183
|
+
return list(zip(ticks, labels)), (dts[0].strftime("%Y") if same_year else None)
|
|
184
|
+
if unit in ("hour", "minute"):
|
|
185
|
+
labels = [d.strftime("%H:%M") if (d.hour, d.minute) != (0, 0) or same_day
|
|
186
|
+
else d.strftime("%b %d").replace(" 0", " ") for d in dts]
|
|
187
|
+
off = dts[0].strftime("%b %d, %Y").replace(" 0", " ") if same_day else (
|
|
188
|
+
dts[0].strftime("%Y") if same_year else None)
|
|
189
|
+
return list(zip(ticks, labels)), off
|
|
190
|
+
if unit == "second":
|
|
191
|
+
labels = [d.strftime("%H:%M:%S") for d in dts]
|
|
192
|
+
off = dts[0].strftime("%b %d, %Y").replace(" 0", " ") if same_day else None
|
|
193
|
+
return list(zip(ticks, labels)), off
|
|
194
|
+
digits = 6
|
|
195
|
+
while digits > 0 and all(d.microsecond % (10 ** (7 - digits)) == 0 for d in dts):
|
|
196
|
+
digits -= 1
|
|
197
|
+
labels = [d.strftime("%S") + ("." + f"{d.microsecond:06d}"[:digits] if digits else "")
|
|
198
|
+
for d in dts]
|
|
199
|
+
return list(zip(ticks, labels)), dts[0].strftime("%b %d, %Y %H:%M").replace(" 0", " ", 1)
|
plot_io/values.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Values given to ``color``, ``size`` and ``alpha``: colors, numbers to map, or computed
|
|
2
|
+
values (``"density"``).
|
|
3
|
+
|
|
4
|
+
The rules: a color string or a tuple of 3 or 4 numbers is one color; a list of colors is
|
|
5
|
+
one color per point; a list, array or other buffer of numbers is one value per point,
|
|
6
|
+
mapped through the series' ``color_map`` (or, for ``size``, its ``size_map``);
|
|
7
|
+
``"density"`` or a :class:`~plot_io.Density` is computed from the series' points.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import array
|
|
11
|
+
import math
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from . import _backend
|
|
15
|
+
|
|
16
|
+
__all__ = ["Values", "paint", "category_paint", "size_value", "is_values", "is_computed",
|
|
17
|
+
"check_color_map", "check_size_map"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Values:
|
|
21
|
+
"""Numbers to map through a color or size map: an ``array('d')`` (one per point), or a
|
|
22
|
+
dict ``{category: number}``."""
|
|
23
|
+
|
|
24
|
+
__slots__ = ("data",)
|
|
25
|
+
|
|
26
|
+
def __init__(self, data: Any) -> None:
|
|
27
|
+
self.data = data
|
|
28
|
+
|
|
29
|
+
def __len__(self) -> int:
|
|
30
|
+
return len(self.data)
|
|
31
|
+
|
|
32
|
+
def __repr__(self) -> str:
|
|
33
|
+
return f"Values({len(self.data)})"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_values(v: Any) -> bool:
|
|
37
|
+
return isinstance(v, Values)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def is_computed(v: Any) -> bool:
|
|
41
|
+
from .density import Density
|
|
42
|
+
return isinstance(v, Density)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _number(x: Any) -> bool:
|
|
46
|
+
return isinstance(x, (int, float)) and not isinstance(x, bool)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _is_color_tuple(v: Any) -> bool:
|
|
50
|
+
return isinstance(v, tuple) and len(v) in (3, 4) and all(_number(c) for c in v)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _buffer(v: Any) -> Any:
|
|
54
|
+
if isinstance(v, (str, bytes, bytearray, list, tuple, dict)):
|
|
55
|
+
return None
|
|
56
|
+
try:
|
|
57
|
+
return memoryview(v)
|
|
58
|
+
except TypeError:
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _rgba_rows(v: Any) -> Any:
|
|
63
|
+
"""An ``(n, 3)`` or ``(n, 4)`` numeric buffer as ``(n, 4)`` doubles (alpha 1 added)."""
|
|
64
|
+
from pdf_io import arrays
|
|
65
|
+
n, k, _, flat = arrays.grid(v)
|
|
66
|
+
if any(x > 1.0 for x in flat[:4096]): # 0–255 integers
|
|
67
|
+
flat = array.array("d", (x / 255.0 for x in flat))
|
|
68
|
+
if k == 3:
|
|
69
|
+
out = array.array("d", [1.0]) * (n * 4)
|
|
70
|
+
for i in range(n):
|
|
71
|
+
out[4 * i:4 * i + 3] = flat[3 * i:3 * i + 3]
|
|
72
|
+
flat = out
|
|
73
|
+
mv: Any = memoryview(flat).cast("B")
|
|
74
|
+
return mv.cast("d", (n, 4))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _color_map_error(name: str) -> ValueError:
|
|
78
|
+
return ValueError(f"a ColorMap goes in color_map (alias cmap), and the values in {name}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def paint(v: Any, name: str = "color") -> Any:
|
|
82
|
+
"""A color, colors per point, values per point (``Values``), or ``"density"``."""
|
|
83
|
+
from .density import Density
|
|
84
|
+
from .maps import ColorMap
|
|
85
|
+
from .settings import color
|
|
86
|
+
|
|
87
|
+
if isinstance(v, ColorMap):
|
|
88
|
+
raise _color_map_error(name)
|
|
89
|
+
if isinstance(v, Density):
|
|
90
|
+
return v
|
|
91
|
+
if isinstance(v, str):
|
|
92
|
+
if v.strip().lower() == "density":
|
|
93
|
+
return Density()
|
|
94
|
+
return color(v)
|
|
95
|
+
if _is_color_tuple(v):
|
|
96
|
+
return color(v)
|
|
97
|
+
if isinstance(v, dict):
|
|
98
|
+
raise ValueError(f"a dict of {name}s applies to categorical series (box, bar, violin)")
|
|
99
|
+
mv = _buffer(v)
|
|
100
|
+
if mv is not None:
|
|
101
|
+
if mv.ndim == 2 and mv.shape[1] in (3, 4):
|
|
102
|
+
return _rgba_rows(v)
|
|
103
|
+
if mv.ndim == 1:
|
|
104
|
+
return Values(_backend.doubles(v))
|
|
105
|
+
raise ValueError(f"{name}: expected a 1-D buffer of values or an (n, 3|4) buffer of "
|
|
106
|
+
f"colors, got shape {tuple(mv.shape)}")
|
|
107
|
+
if hasattr(v, "to_numpy") and not isinstance(v, (list, tuple)):
|
|
108
|
+
return paint(v.to_numpy(), name)
|
|
109
|
+
if isinstance(v, (list, tuple)):
|
|
110
|
+
items = list(v)
|
|
111
|
+
if items and all(_number(x) or x is None for x in items):
|
|
112
|
+
if isinstance(v, tuple):
|
|
113
|
+
raise ValueError(f"{name}: a tuple of numbers is one color (3 or 4 numbers "
|
|
114
|
+
f"in [0, 1], or 0–255); use a list for one value per point")
|
|
115
|
+
return Values(_backend.doubles(items))
|
|
116
|
+
return [color(c) if c is not None else None for c in items]
|
|
117
|
+
return color(v)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def category_paint(v: Any, name: str = "color") -> Any:
|
|
121
|
+
"""``paint`` for categorical series: also a dict ``{category: color or number}``."""
|
|
122
|
+
if isinstance(v, dict):
|
|
123
|
+
if v and all(_number(x) or x is None for x in v.values()):
|
|
124
|
+
return Values({k: (math.nan if x is None else float(x)) for k, x in v.items()})
|
|
125
|
+
from .settings import color
|
|
126
|
+
return {k: (color(c) if c is not None else None) for k, c in v.items()}
|
|
127
|
+
return paint(v, name)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def size_value(v: Any) -> Any:
|
|
131
|
+
"""A size (pt), sizes per point, or ``"density"`` (mapped through ``size_map``)."""
|
|
132
|
+
from .density import Density
|
|
133
|
+
from .maps import SizeMap
|
|
134
|
+
from .settings import number
|
|
135
|
+
|
|
136
|
+
if isinstance(v, SizeMap):
|
|
137
|
+
raise ValueError("a SizeMap goes in size_map (alias smap), and the values in size")
|
|
138
|
+
if isinstance(v, Density):
|
|
139
|
+
return v
|
|
140
|
+
if isinstance(v, str) and v.strip().lower() == "density":
|
|
141
|
+
return Density()
|
|
142
|
+
if _number(v):
|
|
143
|
+
return number(lo=0)(v)
|
|
144
|
+
try:
|
|
145
|
+
return _backend.doubles(v.to_numpy() if hasattr(v, "to_numpy") else v)
|
|
146
|
+
except (TypeError, ValueError):
|
|
147
|
+
raise ValueError(f"expected a size, sizes or \"density\", got {v!r}") from None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def check_color_map(v: Any) -> Any:
|
|
151
|
+
from .maps import check_cmap
|
|
152
|
+
return check_cmap(v)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def check_size_map(v: Any) -> Any:
|
|
156
|
+
from .maps import SizeMap
|
|
157
|
+
if isinstance(v, SizeMap):
|
|
158
|
+
return v
|
|
159
|
+
if isinstance(v, (tuple, list)) and len(v) == 2:
|
|
160
|
+
return SizeMap(tuple(v))
|
|
161
|
+
raise ValueError(f"expected a SizeMap or a pair of sizes, got {v!r}")
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: plot_io
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Plots for PDF, PNG and JPEG, drawn with pdf_io
|
|
5
|
+
Author-email: Arthur Gouhier <ajgouhier@gmail.com>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: pdf-io>=0.4
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: flake8; extra == 'dev'
|
|
12
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# plot_io
|
|
16
|
+
|
|
17
|
+
Plots for PDF, PNG and JPEG in Python, drawn with [pdf_io](https://github.com/ajgouhier/pdf_io).
|
|
18
|
+
No other dependency.
|
|
19
|
+
|
|
20
|
+

|
|
21
|
+
|
|
22
|
+
* **Familiar:** a figure of plots; `scatter`, `line`, `fill`, `box`, `heatmap`, `bar`,
|
|
23
|
+
`hist`, `kde` (1-D and 2-D), `ecdf`, `violin`, `strip`, `swarm`, `area`, `errorbar`, `stem`,
|
|
24
|
+
`regression`, `hist2d`, `hexbin`, `contour`, `pie`; reference lines, spans, text and
|
|
25
|
+
annotations; `color`, `marker`, `alpha`, `limits`, `scale`, `legend`, sizes in inches.
|
|
26
|
+
* **One way to set things:** keywords at creation or `set(...)` on any object; figure
|
|
27
|
+
settings (font, palette, line width…) are inherited by plots and series.
|
|
28
|
+
* **Layouts:** an explicit grid of ids with spans, or automatic rows and columns; plots
|
|
29
|
+
sized individually; axis lines (or plot borders) aligned across the figure; margins from
|
|
30
|
+
the real text metrics; shared axes; secondary axes; facets of a table.
|
|
31
|
+
* **Data:** columns of any table by name (`data=`: dicts, pandas, polars, numpy), one
|
|
32
|
+
series per group (`group=`), numbers given as colors or sizes, point density (scipy's
|
|
33
|
+
`gaussian_kde`, in C) for colors, sizes, opacity and drawing order.
|
|
34
|
+
* **Axes:** linear, log, log2, log10, sqrt, exp, symlog, logit, time and category scales;
|
|
35
|
+
limits such as `"0/max"` or `"q:0.01/q:0.99"`; ticks with shared decimals and ×10ᵏ
|
|
36
|
+
factors, calendar ticks for dates.
|
|
37
|
+
* **Colors:** a colorblind-safe palette, color maps interpolated in Oklab, colorbars;
|
|
38
|
+
marker sizes by area, equal for every shape.
|
|
39
|
+
* **Styles:** `style="matplotlib"`, `"seaborn"`, `"ggplot2"`, `"altair"`, `"matlab"`,
|
|
40
|
+
`"chartjs"`, `"plotly"`, `"bokeh"`, `"observable"`, `"excel"`, `"fivethirtyeight"`,
|
|
41
|
+
`"economist"`, `"tufte"` and variants give the look of those libraries; styles are
|
|
42
|
+
presets of settings, and your own build on them.
|
|
43
|
+
* **Large data:** one million points mapped and drawn in C; `simplify="auto"` drops what
|
|
44
|
+
cannot be seen, `rasterize=` turns a series into an image inside the PDF.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
pip install /path/to/pdf_io # pdf_io 0.4 or later
|
|
50
|
+
pip install .
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Python 3.11+.
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import plot_io as pl
|
|
59
|
+
|
|
60
|
+
s = pl.Scatter(x, y, name="data")
|
|
61
|
+
s.plot.x_axis(label="Time (s)")
|
|
62
|
+
s.plot.save("scatter.pdf")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
fig = pl.Figure(grid=[[1, 1, 2], [3, 4, 2]])
|
|
67
|
+
p = fig.plot(1, title="Signal")
|
|
68
|
+
p.line(t, v, name="raw")
|
|
69
|
+
p.fill(t, upper, lower, alpha=0.3)
|
|
70
|
+
p.y_axis(scale="log10", label="Power")
|
|
71
|
+
fig.plot(2).box({"a": va, "b": vb})
|
|
72
|
+
h = fig.plot(3).heatmap(matrix, x=columns, y=rows, cmap="magma", colorbar=True)
|
|
73
|
+
fig.plot(4).scatter(x, y, c=z, cmap="viridis")
|
|
74
|
+
fig.save("figure.pdf")
|
|
75
|
+
fig.save("figure.png", dpi=200)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
fig = pl.Figure(style="ggplot2") # or a dict: {"base": "seaborn", "font_size": 9}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
p = pl.Plot()
|
|
84
|
+
p.scatter("umap_x", "umap_y", data=genes, c="density", order="density") # columns of a table
|
|
85
|
+
p.scatter("x", "y", data=df, group="cluster", marker="auto") # one series per group
|
|
86
|
+
fig = pl.facet(df, col="condition", row="replicate") # small multiples
|
|
87
|
+
fig.scatter("x", "y")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Documentation
|
|
91
|
+
|
|
92
|
+
* [API.md](API.md): the complete reference, with every setting.
|
|
93
|
+
* [EXAMPLES.md](EXAMPLES.md): worked examples.
|
|
94
|
+
|
|
95
|
+
## Development
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
python -m unittest discover -s tests -t tests
|
|
99
|
+
flake8 --select F plot_io tests && mypy
|
|
100
|
+
python tools/bench.py --check # timings against the targets
|
|
101
|
+
python tools/settings_doc.py # regenerate the settings tables of API.md
|
|
102
|
+
```
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
plot_io/__init__.py,sha256=wOuiSG1_9RZK75scfMcOHX_2v14zIklkzX-eupVznTI,2479
|
|
2
|
+
plot_io/_backend.py,sha256=99mKkfuoMq2HyRJurrtOko8mTjHb6xo2uF1rKhBS2xM,4489
|
|
3
|
+
plot_io/axis.py,sha256=jtr90_r9aRzYx-Y038EhXWH4u0ZCdUUch_KCUWcWDFM,27710
|
|
4
|
+
plot_io/colorbar.py,sha256=RjZ8nzRpa__w9M_9k9vmwgcavxDg0byCLq74TyUedTM,14647
|
|
5
|
+
plot_io/decorations.py,sha256=Xru8c6IrBEhjgVVySTEOy1n4ZXYYYFjRWfCpvDs-xUE,19167
|
|
6
|
+
plot_io/defaults.py,sha256=XIkStLlHH3KkZHtKetlN1sV-CwaY-6euGgCAiWvIXiU,21240
|
|
7
|
+
plot_io/density.py,sha256=HdMHGO7CWKjgh3dVkSrV1uPT3RMPNwEuXkKqhYyzkmA,4243
|
|
8
|
+
plot_io/enums.py,sha256=C7ToKwzFnZZvE1KgmcElowhyoeLxaFPC8pAiZ_MatiE,1275
|
|
9
|
+
plot_io/facets.py,sha256=iQuERUPWMtKrBWqVAh5j7VIQjH_DRjuEnUaX_9zcXS0,8300
|
|
10
|
+
plot_io/figure.py,sha256=_UhTqj2cKwUAV2VXPF8R-k-YVptu0nIq0c5y4feq9SI,15572
|
|
11
|
+
plot_io/labels.py,sha256=UJZKS5PTBqhV9puNIprfqm60AWfKEYHxf4dcVuwVn9Q,2377
|
|
12
|
+
plot_io/legend.py,sha256=l6ixJue42hLWKtH6Ox-vNxHV5Lm386AvrvnD_ZdgHiE,20542
|
|
13
|
+
plot_io/limits.py,sha256=b6YIO29FuSPZD8-13ZukOgcaeJWeCy4AcVKm0uNyJLA,3279
|
|
14
|
+
plot_io/markers.py,sha256=E44DlmGE1vhasJ58LPFnOaub3FVUJa15GM7snDQR0mQ,3815
|
|
15
|
+
plot_io/numbers.py,sha256=Ys7W7fZoExDv0ayDqEFV60-1tWzdaUni14nfuliqQq0,11413
|
|
16
|
+
plot_io/occupancy.py,sha256=XpsaOROBBQvdnX_Tv5A8gMogqKaOBgOJKotJLB6kVH4,8956
|
|
17
|
+
plot_io/palettes.py,sha256=xJ2NQlPEehHMFm1MFNb4pcFc-XPzZpLFwBpOjhkML_4,6343
|
|
18
|
+
plot_io/plot.py,sha256=TCdeeXfAnT6NlV6lrK4gyo47rZVMtQcNArXzMZO38as,19292
|
|
19
|
+
plot_io/render.py,sha256=x8dEp7ELV-1X7IUoU0QKl9r3iEF-3gutd-1GQrNJ14o,47363
|
|
20
|
+
plot_io/scales.py,sha256=H99IMvIPU2lmt77xrGNBBNZUASpTMzmjzC9jUFu-6IQ,3397
|
|
21
|
+
plot_io/settings.py,sha256=Q23_aodNc67OpYuT0sOYcWrs7hIv2by5yeEFRHr7IzI,9900
|
|
22
|
+
plot_io/shapes.py,sha256=ACXqgVcqpaP5uLLAaYFsmVJWQ1nMuT6X1pT2F6LWlAQ,2436
|
|
23
|
+
plot_io/sharing.py,sha256=f7Xbv4jTU2B_d4-KYcj53DKJP2133jR4Pdk_vXcUFCE,6047
|
|
24
|
+
plot_io/slots.py,sha256=XYHXNFNM5SXYV5EWQlyjkKRBmceEIZTVINZYbC2KyBY,2040
|
|
25
|
+
plot_io/stacking.py,sha256=4L6hpXOghruxQbgy_wbb3vSMEA469KZ5iaHD7fH4uJo,2268
|
|
26
|
+
plot_io/stats.py,sha256=p-GFhywMirU2Ltk_zNgEvQsvqSYk2CIoakNGHEfCWzo,6095
|
|
27
|
+
plot_io/style.py,sha256=7i3w8SkP3PmbLSlrRnEN3hV9M78abNxZeke6TffmFao,2171
|
|
28
|
+
plot_io/tables.py,sha256=VzZfvrJF_73fEFub-ge-dAArNI-lNRbhbjkFnCj-OzM,5108
|
|
29
|
+
plot_io/text.py,sha256=9i3xPwZ7xtMWSJFBXfxGamJM-SkRdSgpkXWEgcDIg3k,3717
|
|
30
|
+
plot_io/ticks.py,sha256=Cv7NpG7T7Ug8gqZqIXM5DC0aau2BLv9syScf2RFFbO0,11874
|
|
31
|
+
plot_io/times.py,sha256=RJ6eQxh29zYayzY9TVEVzbwb6UVOHSDGIWrPqLSBqtY,8241
|
|
32
|
+
plot_io/values.py,sha256=BymMVwdjZtFIue7_JEZx7ubBSb0Sr2QfoVvOvH1oSB0,5521
|
|
33
|
+
plot_io/maps/__init__.py,sha256=qTsi345pplO1Krmu7kV8Set5iojW1Tb8ABJhOOPBzj0,15614
|
|
34
|
+
plot_io/maps/data.py,sha256=BFdSPNo027iZhothJ4uvN75Utqant8Zs3RlRoJHeZBM,26175
|
|
35
|
+
plot_io/series/__init__.py,sha256=-_23tbCFO0BRu70zb25FJElxT7Dk5TP-ELcybNyrooU,20798
|
|
36
|
+
plot_io/series/area.py,sha256=jFyDKDkaeE55LF0skI2drv25j7AFc5Ij_UTJingFkxE,7106
|
|
37
|
+
plot_io/series/bar.py,sha256=KCnwPo0Y1NLdeSL49xG54AZQ2-QmO-BOpFaswuAX7_I,12513
|
|
38
|
+
plot_io/series/box.py,sha256=EUOKH-_ZJJkd005fD2gHsN1D_I7EmmgffnUAMQCIYd4,17736
|
|
39
|
+
plot_io/series/catdata.py,sha256=m404Cn12Rw8TheDA7A2w55h_ruj1giT7SX_UJj5ze5I,965
|
|
40
|
+
plot_io/series/catpoints.py,sha256=tOWUK8uGenluV1JjkJn4ipK4aGtvlV7oQWdJFucHefE,7366
|
|
41
|
+
plot_io/series/contour.py,sha256=mMHxHn5RbUm4854VTwatm40RLljKNnQRgaQQoiG56q4,12655
|
|
42
|
+
plot_io/series/curves.py,sha256=f0-Rm0IQljMsxn2xRn1GpxNAKKQJP_KFtTuYWa-uNM4,4414
|
|
43
|
+
plot_io/series/ecdf.py,sha256=H9vXFRU6dPjrxq8tZMXBN89JmAvGAxy20V2029u-zWw,5908
|
|
44
|
+
plot_io/series/errorbar.py,sha256=Pa2yaYhFRzwp9HwjMVzI7a5EY_tKjxo_2pvBu_9-WDg,4594
|
|
45
|
+
plot_io/series/errors.py,sha256=oMWRUgR4aGyERYfVgs2sq1IP9tPeswcLlUeMpqOxr9E,3897
|
|
46
|
+
plot_io/series/fill.py,sha256=f9EoTt3lEOiJZ-M27elbWpkioo6tnEam3dUe1vnoNtE,6930
|
|
47
|
+
plot_io/series/heatmap.py,sha256=WLCX4zM70RXxnNZtGrXHgc1-kNlkz1R91mwDXmVW7VQ,12663
|
|
48
|
+
plot_io/series/hexbin.py,sha256=hhTN9QrQBhN5Ga86t00MCiM1wrfPsX61b8GzGyr1Dm0,9773
|
|
49
|
+
plot_io/series/hist.py,sha256=VjjoA-Z90IdkBAf5Wb06Yk2AsOo5qPDnsvyRwF5UPYM,16583
|
|
50
|
+
plot_io/series/hist2d.py,sha256=5XCQtQc1Hid2f5VU4C_nlCt7LJH-VJkSeK2WW5cEMVM,5776
|
|
51
|
+
plot_io/series/kde.py,sha256=DdYxbluGgSS4dDWQwTcqu6RKlYKPvq_CLknFYNvbo48,9286
|
|
52
|
+
plot_io/series/kde2d.py,sha256=4ci6DCfgyIDY1lc-i8hoTGsxpIeV2HtCTyR58utXV5Q,6839
|
|
53
|
+
plot_io/series/line.py,sha256=mIjyXOQ26wSIqysFJFoTzD4H3851eGgK-4ZNjgEbszc,6461
|
|
54
|
+
plot_io/series/pie.py,sha256=EuL_KNjIviu4lvaByisCEJ5oZaSixKjIaf1ldW7F0eQ,13353
|
|
55
|
+
plot_io/series/points.py,sha256=G-1BuOvaa6Z6Sz0iqjtZhskjjgzRoeZnhMfM0qUmwd8,15329
|
|
56
|
+
plot_io/series/regression.py,sha256=xkcOF7Ekw0cx8nJJNYlV7Wz0zzUa9BvzhJrKTLYqNxw,10403
|
|
57
|
+
plot_io/series/scatter.py,sha256=KnZgxlawihVygL9lWzqb2lF20BpZdrZu0gUzSQSeK8g,3728
|
|
58
|
+
plot_io/series/stem.py,sha256=adtzcpl1-4jkGv3kNBAVw6axhadnSEOHDoUMA7sLb1Q,4724
|
|
59
|
+
plot_io/series/violin.py,sha256=MGY5q7j3Uo1pO10ah8mDSmMq19SvcmGCbPPj-25jL_k,14369
|
|
60
|
+
plot_io/styles/__init__.py,sha256=br2OOqhrUsL5CGni-Kl-1i8JkqMlwVOTUg6hhy3PfRQ,8730
|
|
61
|
+
plot_io/styles/library.py,sha256=WseTAPqeP0tfCWVingbrnrPNfMDOT7l2KdldxtAd08Q,24112
|
|
62
|
+
plot_io-0.1.0.dist-info/METADATA,sha256=njY8VYO1SIvo1xPyqE3b_hyPAPy5-cUrHVNkMHvR-VQ,3912
|
|
63
|
+
plot_io-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
64
|
+
plot_io-0.1.0.dist-info/licenses/LICENSE,sha256=KD-5bsauyA0Kh68YcImL7q5yE6OCTZwHnhvybthBbNg,1501
|
|
65
|
+
plot_io-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Arthur Gouhier
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|