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/plot.py ADDED
@@ -0,0 +1,435 @@
1
+ """Plots: one area of a figure, with axes, series, title, legend and colorbars."""
2
+
3
+ from typing import Any
4
+
5
+ from .axis import Axis
6
+ from .settings import (Setting, Settings, boolean, canonical_kwargs, color, either, literal,
7
+ number, one_of, route, table, text)
8
+ from .numbers import number_settings
9
+ from .maps import check_cmap
10
+ from .style import check_palette
11
+
12
+ __all__ = ["Plot", "Title", "Legend"]
13
+
14
+ _SPINES = ("left", "bottom", "right", "top")
15
+ _POSITIONS = ("auto", "top right", "top left", "bottom right", "bottom left", "top", "bottom",
16
+ "outside right", "outside top", "outside bottom", "none")
17
+
18
+
19
+ def _spines(v: Any) -> frozenset[str]:
20
+ if isinstance(v, str):
21
+ s = v.strip().lower()
22
+ if s == "all":
23
+ return frozenset(_SPINES)
24
+ if s == "none":
25
+ return frozenset()
26
+ parts = s.replace(",", " ").split()
27
+ else:
28
+ parts = [str(p).lower() for p in v]
29
+ bad = [p for p in parts if p not in _SPINES]
30
+ if bad:
31
+ raise ValueError(f"unknown spine {bad[0]!r} (use left, bottom, right, top, all, none)")
32
+ return frozenset(parts)
33
+
34
+
35
+ def _position(v: Any) -> str:
36
+ s = str(getattr(v, "value", v)).strip().lower().replace("_", " ").replace("-", " ")
37
+ s = " ".join(s.split())
38
+ if s not in _POSITIONS:
39
+ raise ValueError(f"expected one of {', '.join(map(repr, _POSITIONS))}, got {v!r}")
40
+ return s
41
+
42
+
43
+ def _plot(v: Any) -> Any:
44
+ if not isinstance(v, Plot):
45
+ raise ValueError(f"expected a Plot, got {v!r}")
46
+ return v
47
+
48
+
49
+ _INHERITED = dict(
50
+ font_size=Setting(number(positive=True), inherited=True, doc="Base font size (pt).", length=True),
51
+ font_family=Setting(str, inherited=True, doc="Font family."),
52
+ text_color=Setting(color, inherited=True, doc="Text color."),
53
+ line_width=Setting(number(lo=0), inherited=True, doc="Default line width (pt).", length=True),
54
+ palette=Setting(check_palette, inherited=True, doc="Colors of series without one."),
55
+ alpha=Setting(number(0, 1), inherited=True, doc="Default opacity of series."),
56
+ rasterize=Setting(either(boolean, number(positive=True)), inherited=True,
57
+ doc="`True` or a dpi: series drawn as images inside the PDF."),
58
+ simplify=Setting(either(literal("auto"), boolean, number(positive=True)), inherited=True,
59
+ doc="pdf_io's `simplify` for every series: `\"auto\"`, `True`, a dpi or "
60
+ "`False`."),
61
+ color_map=Setting(check_cmap, inherited=True,
62
+ doc="Default color map (alias `cmap`): of heatmaps, contours and "
63
+ "numbers given as colors."),
64
+ data=Setting(lambda v: v, inherited=True,
65
+ doc="A table (dict, pandas or polars data frame, numpy structured array): "
66
+ "strings in the data arguments of series name its columns."),
67
+ **number_settings(),
68
+ )
69
+
70
+
71
+ class Title(Settings):
72
+ """The title of a plot (``plot.title``)."""
73
+
74
+ SETTINGS = table(
75
+ text=Setting(text, doc="Title text: a string or rich content."),
76
+ font_size=Setting(number(positive=True),
77
+ doc="Font size (pt); default 1.2 × the base font size.", length=True),
78
+ font_weight=Setting(one_of("normal", "bold"), doc="`normal` or `bold`."),
79
+ color=Setting(color, doc="Text color."),
80
+ align=Setting(one_of("center", "left", "right"),
81
+ doc="Alignment over the axes."),
82
+ pad=Setting(number(lo=0), doc="Gap above the axes (pt).", length=True),
83
+ over=Setting(one_of("axes", "plot"),
84
+ doc="Width the title is aligned within: the axes, or the axes and the "
85
+ "y axis labels."),
86
+ font_family=Setting(str, inherited=True, doc="Font family."),
87
+ )
88
+
89
+ def __init__(self, plot: "Plot") -> None:
90
+ self._plot = plot
91
+ super().__init__()
92
+
93
+ def _parent(self) -> Settings:
94
+ return self._plot
95
+
96
+ def __call__(self, text: Any = None, **settings: Any) -> "Title":
97
+ if text is not None:
98
+ settings["text"] = text
99
+ return self.set(**settings)
100
+
101
+ def __repr__(self) -> str:
102
+ return f"Title({self.get('text')!r})"
103
+
104
+
105
+ class Legend(Settings):
106
+ """The legend of a plot (``plot.legend``): an entry per named series."""
107
+
108
+ SETTINGS = table(
109
+ position=Setting(_position,
110
+ doc="`auto`, `top right`, `top left`, `bottom right`, `bottom left`, "
111
+ "`top`, `bottom`, `outside right`, `outside top`, `outside bottom` "
112
+ "or `none`."),
113
+ columns=Setting(number(lo=1, integer=True), doc="Number of columns."),
114
+ frame=Setting(boolean, doc="Draw a frame and background."),
115
+ title=Setting(text,
116
+ doc="Legend title; default the `group` column name."),
117
+ font_size=Setting(number(positive=True),
118
+ doc="Font size (pt).", length=True),
119
+ color=Setting(color, doc="Text color."),
120
+ background=Setting(color, doc="Background of a framed legend."),
121
+ frame_color=Setting(color, doc="Frame color."),
122
+ pad=Setting(number(lo=0), doc="Gap to the axes (pt).", length=True),
123
+ frame_width=Setting(number(lo=0), doc="Frame line width (pt).", length=True),
124
+ frame_radius=Setting(number(lo=0), doc="Corner radius of the frame (pt).",
125
+ length=True),
126
+ frame_alpha=Setting(number(0, 1), doc="Opacity of the frame and background."),
127
+ glyph_alpha=Setting(number(0, 1),
128
+ doc="Fill opacity of `box` and `circle` symbols, outlined in the "
129
+ "opaque color; default: the series' own fill."),
130
+ glyph=Setting(one_of("series", "box", "circle", "line"),
131
+ doc="Entry symbol: as the series draws itself, or a filled `box`, a "
132
+ "`circle` or a `line` in the series color."),
133
+ orientation=Setting(one_of("auto", "vertical", "horizontal"),
134
+ doc="`horizontal`: entries side by side; `auto`: horizontal at "
135
+ "`top`, `bottom`, `outside top` and `outside bottom`."),
136
+ glyph_width=Setting(number(positive=True),
137
+ doc="Width of an entry's symbol, in font sizes."),
138
+ row_height=Setting(number(positive=True), doc="Height of an entry, in font sizes."),
139
+ font_family=Setting(str, inherited=True, doc="Font family."),
140
+ )
141
+
142
+ def __init__(self, plot: "Plot") -> None:
143
+ self._plot = plot
144
+ super().__init__()
145
+
146
+ def _parent(self) -> Settings:
147
+ return self._plot
148
+
149
+ def __call__(self, position: Any = None, **settings: Any) -> "Legend":
150
+ if position is not None:
151
+ settings["position"] = position
152
+ return self.set(**settings)
153
+
154
+ def __repr__(self) -> str:
155
+ return f"Legend({self.get('position')!r})"
156
+
157
+
158
+ class Plot(Settings):
159
+ """One plot: axes, series and decorations.
160
+
161
+ ``pl.Plot(**settings)`` creates a one-plot figure (``plot.figure``);
162
+ ``figure.plot(…)`` adds a plot to an existing figure.
163
+ """
164
+
165
+ SETTINGS = table(
166
+ spines=Setting(_spines,
167
+ doc="Axis lines drawn: any of `left`, `bottom`, `right`, `top`, or "
168
+ "`all`, `none`."),
169
+ aspect=Setting(number(positive=True),
170
+ doc="Data aspect ratio: 1 makes one unit as long on both axes."),
171
+ background=Setting(color, doc="Background color of the axes area."),
172
+ border=Setting(color, doc="Color of a frame around the axes area (drawn under the "
173
+ "axis lines); default none."),
174
+ border_width=Setting(number(lo=0), doc="Width of the frame (pt).", length=True),
175
+ clip=Setting(boolean, doc="Clip series to the axes area."),
176
+ axes=Setting(boolean, doc="Draw axes; `False` hides both axes, drops padding "
177
+ "and fixes the aspect to 1 (set by pie charts)."),
178
+ share_x=Setting(_plot, doc="Another plot of the figure whose x axis this plot "
179
+ "shares (groups chain); overrides the figure's `share_x`."),
180
+ share_y=Setting(_plot, doc="Another plot whose y axis this plot shares."),
181
+ share=Setting(_plot, doc="Another plot whose x and y axes this plot shares."),
182
+ **_INHERITED,
183
+ )
184
+ #: Keywords accepted besides settings (``title="…"`` sets the title text).
185
+ EXTRA = ("title",)
186
+
187
+ def __init__(self, figure: Any = None, id: Any = None, _twin: Any = None,
188
+ **settings: Any) -> None:
189
+ from .figure import Figure
190
+
191
+ own, fig_kw = route(settings, [Plot, Figure], extra={0: Plot.EXTRA})
192
+ title = own.pop("title", None)
193
+ if figure is None:
194
+ figure = Figure(**fig_kw)
195
+ elif fig_kw:
196
+ figure.set(**fig_kw)
197
+ self.figure = figure
198
+ self.x_axis = Axis(self, "x")
199
+ self.y_axis = Axis(self, "y")
200
+ self.title = Title(self)
201
+ self.legend = Legend(self)
202
+ self.series: list[Any] = []
203
+ self.decorations: list[Any] = []
204
+ self.colorbars: list[Any] = []
205
+ self.twins: list[Plot] = []
206
+ #: ``(main plot, "y" or "x")`` for a twin: the axis it has of its own.
207
+ self.twin_of: tuple[Plot, str] | None = _twin
208
+ super().__init__(**own)
209
+ if title is not None:
210
+ self.title.set(text=title)
211
+ self.id = figure._attach(self, id) if _twin is None else None
212
+
213
+ def _parent(self) -> Settings:
214
+ return self.twin_of[0] if self.twin_of is not None else self.figure
215
+
216
+ def twin_y(self, **settings: Any) -> "Plot":
217
+ """A plot on the same axes with its own y axis (on the right) and the x axis of
218
+ this plot: add series to it and set its ``y_axis``."""
219
+ t = Plot(figure=self.figure, _twin=(self, "y"), **settings)
220
+ t.x_axis.set(visible=False)
221
+ if not t.y_axis.is_set("side"):
222
+ t.y_axis.set(side="right")
223
+ if not t.is_set("spines"):
224
+ t.set(spines="left") # drawn on the right with the axis
225
+ self.twins.append(t)
226
+ return t
227
+
228
+ def twin_x(self, **settings: Any) -> "Plot":
229
+ """A plot on the same axes with its own x axis (on top) and the y axis of this
230
+ plot."""
231
+ t = Plot(figure=self.figure, _twin=(self, "x"), **settings)
232
+ t.y_axis.set(visible=False)
233
+ if not t.x_axis.is_set("side"):
234
+ t.x_axis.set(side="top")
235
+ if not t.is_set("spines"):
236
+ t.set(spines="bottom")
237
+ self.twins.append(t)
238
+ return t
239
+
240
+ def set(self, **settings: Any) -> "Plot":
241
+ settings = canonical_kwargs(settings, self.SETTINGS, "Plot")
242
+ fig_share = {k: settings.pop(k) for k in ("share_x", "share_y", "share")
243
+ if k in settings and settings[k] is not None
244
+ and not isinstance(settings[k], Plot)}
245
+ if fig_share:
246
+ self.figure.set(**fig_share)
247
+ title = settings.pop("title", None)
248
+ if title is not None:
249
+ self.title.set(text=title)
250
+ super().set(**settings)
251
+ return self
252
+
253
+ def __repr__(self) -> str:
254
+ return f"Plot({self.id!r})"
255
+
256
+ def own_share(self, role: str) -> Any:
257
+ """The plot this plot's ``role`` axis is paired with, or ``None``."""
258
+ return self.get(f"share_{role}") or self.get("share")
259
+
260
+ def _add_series(self, s: Any) -> None:
261
+ self.series.append(s)
262
+
263
+ # -- series ----------------------------------------------------------------------
264
+
265
+ def scatter(self, x: Any, y: Any, **settings: Any) -> Any:
266
+ """Markers at points ``(x, y)``: see :class:`~plot_io.Scatter`."""
267
+ from .series.scatter import Scatter
268
+ return Scatter(x, y, plot=self, **settings)
269
+
270
+ def line(self, x: Any, y: Any = None, **settings: Any) -> Any:
271
+ """A line through points: see :class:`~plot_io.Line`."""
272
+ from .series.line import Line
273
+ return Line(x, y, plot=self, **settings)
274
+
275
+ def fill(self, x: Any, y1: Any, y2: Any = 0.0, **settings: Any) -> Any:
276
+ """The area between two curves: see :class:`~plot_io.Fill`."""
277
+ from .series.fill import Fill
278
+ return Fill(x, y1, y2, plot=self, **settings)
279
+
280
+ def box(self, values: Any, **settings: Any) -> Any:
281
+ """Box plots: see :class:`~plot_io.Box`."""
282
+ from .series.box import Box
283
+ return Box(values, plot=self, **settings)
284
+
285
+ def heatmap(self, values: Any, **settings: Any) -> Any:
286
+ """A grid of values as colored cells: see :class:`~plot_io.Heatmap`."""
287
+ from .series.heatmap import Heatmap
288
+ return Heatmap(values, plot=self, **settings)
289
+
290
+ def bar(self, x: Any, height: Any = None, **settings: Any) -> Any:
291
+ """Bars: see :class:`~plot_io.Bar`."""
292
+ from .series.bar import Bar
293
+ return Bar(x, height, plot=self, **settings)
294
+
295
+ def hist(self, values: Any, **settings: Any) -> Any:
296
+ """A histogram: see :class:`~plot_io.Hist`."""
297
+ from .series.hist import Hist
298
+ return Hist(values, plot=self, **settings)
299
+
300
+ def kde(self, values: Any, y: Any = None, **settings: Any) -> Any:
301
+ """A kernel density estimate: see :class:`~plot_io.Kde`; with ``y``, 2-D
302
+ density contours (:class:`~plot_io.Kde2d`)."""
303
+ if y is not None:
304
+ from .series.kde2d import Kde2d
305
+ return Kde2d(values, y, plot=self, **settings)
306
+ from .series.kde import Kde
307
+ return Kde(values, plot=self, **settings)
308
+
309
+ def ecdf(self, values: Any, **settings: Any) -> Any:
310
+ """An empirical cumulative distribution: see :class:`~plot_io.Ecdf`."""
311
+ from .series.ecdf import Ecdf
312
+ return Ecdf(values, plot=self, **settings)
313
+
314
+ def violin(self, values: Any, **settings: Any) -> Any:
315
+ """Violin plots: see :class:`~plot_io.Violin`."""
316
+ from .series.violin import Violin
317
+ return Violin(values, plot=self, **settings)
318
+
319
+ def strip(self, values: Any, **settings: Any) -> Any:
320
+ """Jittered points per category: see :class:`~plot_io.Strip`."""
321
+ from .series.catpoints import Strip
322
+ return Strip(values, plot=self, **settings)
323
+
324
+ def swarm(self, values: Any, **settings: Any) -> Any:
325
+ """Non-overlapping points per category: see :class:`~plot_io.Swarm`."""
326
+ from .series.catpoints import Swarm
327
+ return Swarm(values, plot=self, **settings)
328
+
329
+ def area(self, x: Any, ys: Any, **settings: Any) -> Any:
330
+ """Stacked areas: see :class:`~plot_io.Area`."""
331
+ from .series.area import Area
332
+ return Area(x, ys, plot=self, **settings)
333
+
334
+ def errorbar(self, x: Any, y: Any, **settings: Any) -> Any:
335
+ """Error bars: see :class:`~plot_io.Errorbar`."""
336
+ from .series.errorbar import Errorbar
337
+ return Errorbar(x, y, plot=self, **settings)
338
+
339
+ def stem(self, x: Any, y: Any = None, **settings: Any) -> Any:
340
+ """Stems with markers: see :class:`~plot_io.Stem`."""
341
+ from .series.stem import Stem
342
+ return Stem(x, y, plot=self, **settings)
343
+
344
+ def regression(self, x: Any, y: Any, **settings: Any) -> Any:
345
+ """A fitted line with a confidence band: see :class:`~plot_io.Regression`."""
346
+ from .series.regression import Regression
347
+ return Regression(x, y, plot=self, **settings)
348
+
349
+ def hist2d(self, x: Any, y: Any, **settings: Any) -> Any:
350
+ """A 2-D histogram: see :class:`~plot_io.Hist2d`."""
351
+ from .series.hist2d import Hist2d
352
+ return Hist2d(x, y, plot=self, **settings)
353
+
354
+ def hexbin(self, x: Any, y: Any, **settings: Any) -> Any:
355
+ """A hexagonal 2-D histogram: see :class:`~plot_io.Hexbin`."""
356
+ from .series.hexbin import Hexbin
357
+ return Hexbin(x, y, plot=self, **settings)
358
+
359
+ def contour(self, values: Any, **settings: Any) -> Any:
360
+ """Contour lines and bands of a grid: see :class:`~plot_io.Contour`."""
361
+ from .series.contour import Contour
362
+ return Contour(values, plot=self, **settings)
363
+
364
+ def pie(self, values: Any, **settings: Any) -> Any:
365
+ """A pie or donut chart: see :class:`~plot_io.Pie`."""
366
+ from .series.pie import Pie
367
+ return Pie(values, plot=self, **settings)
368
+
369
+ # -- decorations -----------------------------------------------------------------
370
+
371
+ def hline(self, y: Any, **settings: Any) -> Any:
372
+ """A horizontal reference line at ``y``."""
373
+ from .decorations import HLine
374
+ return HLine(self, y, **settings)
375
+
376
+ def vline(self, x: Any, **settings: Any) -> Any:
377
+ """A vertical reference line at ``x``."""
378
+ from .decorations import VLine
379
+ return VLine(self, x, **settings)
380
+
381
+ def axline(self, point: Any, slope: Any = None, point2: Any = None,
382
+ **settings: Any) -> Any:
383
+ """An infinite line through ``point`` with ``slope``, or through ``point2``."""
384
+ from .decorations import AxLine
385
+ return AxLine(self, point, slope, point2, **settings)
386
+
387
+ def hspan(self, y0: Any, y1: Any, **settings: Any) -> Any:
388
+ """A horizontal band from ``y0`` to ``y1``."""
389
+ from .decorations import HSpan
390
+ return HSpan(self, y0, y1, **settings)
391
+
392
+ def vspan(self, x0: Any, x1: Any, **settings: Any) -> Any:
393
+ """A vertical band from ``x0`` to ``x1``."""
394
+ from .decorations import VSpan
395
+ return VSpan(self, x0, x1, **settings)
396
+
397
+ def text(self, x: Any, y: Any, content: Any, **settings: Any) -> Any:
398
+ """Text at ``(x, y)``."""
399
+ from .decorations import Text
400
+ return Text(self, x, y, content, **settings)
401
+
402
+ def annotate(self, content: Any, xy: Any, xytext: Any = None, **settings: Any) -> Any:
403
+ """Text at ``xytext`` with an arrow to ``xy``."""
404
+ from .decorations import Annotate
405
+ return Annotate(self, content, xy, xytext, **settings)
406
+
407
+ def colorbar(self, source: Any = None, **settings: Any) -> Any:
408
+ """A colorbar attached to this plot (``source``: a mapped series or a
409
+ ``ColorMap``; default: the last mapped series)."""
410
+ from .colorbar import Colorbar
411
+ cb = Colorbar(source, plot=self, **settings)
412
+ self.colorbars.append(cb)
413
+ return cb
414
+
415
+ # -- output ----------------------------------------------------------------------
416
+
417
+ def draw(self, canvas: Any, x: float = 0.0, y: float = 0.0) -> tuple[float, float]:
418
+ """Draw the plot alone on a pdf_io canvas with its top-left corner at ``(x, y)``
419
+ (page units); returns its size in points."""
420
+ from . import render
421
+ return render.draw(self.figure, canvas, x, y, only=self)
422
+
423
+ def save(self, path: Any, **options: Any) -> None:
424
+ """Save the plot alone: PDF, PNG, JPEG or JPEG XL by the extension (see
425
+ :meth:`Figure.save`)."""
426
+ from . import render
427
+ render.save(self.figure, path, only=self, **options)
428
+
429
+ def to_bytes(self, format: str = "pdf", **options: Any) -> bytes:
430
+ """The plot alone as ``"pdf"``, ``"png"``, ``"jpeg"`` or ``"jxl"`` bytes."""
431
+ from . import render
432
+ return render.to_bytes(self.figure, format, only=self, **options)
433
+
434
+ def _repr_png_(self) -> bytes:
435
+ return self.to_bytes("png")