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/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
+ ![box plots](examples/box_plots.png)
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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.