plot-io 0.12.0__py3-none-win_amd64.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 (130) hide show
  1. plot_io/__init__.py +73 -0
  2. plot_io/_methods.py +1691 -0
  3. plot_io/_native/__init__.py +199 -0
  4. plot_io/_native/__main__.py +197 -0
  5. plot_io/_native/_api.py +457 -0
  6. plot_io/_native/bin/windows-x86_64.dll +0 -0
  7. plot_io/_native/kernels.py +335 -0
  8. plot_io/_native/manifest.json +49 -0
  9. plot_io/axis.py +1347 -0
  10. plot_io/boxes.py +1156 -0
  11. plot_io/canvas.py +389 -0
  12. plot_io/color/__init__.py +3 -0
  13. plot_io/color/data.py +1646 -0
  14. plot_io/color/maps.py +922 -0
  15. plot_io/color/values.py +295 -0
  16. plot_io/color/variants.py +1246 -0
  17. plot_io/colorbar.py +1426 -0
  18. plot_io/columns.py +614 -0
  19. plot_io/decorations.py +920 -0
  20. plot_io/defaults.py +809 -0
  21. plot_io/density.py +113 -0
  22. plot_io/ext.py +68 -0
  23. plot_io/facets.py +240 -0
  24. plot_io/figure.py +788 -0
  25. plot_io/formatters.py +97 -0
  26. plot_io/genome/__init__.py +17 -0
  27. plot_io/genome/adapters.py +356 -0
  28. plot_io/genome/combine.py +123 -0
  29. plot_io/genome/contacts.py +508 -0
  30. plot_io/genome/intervals.py +750 -0
  31. plot_io/genome/lanes.py +191 -0
  32. plot_io/genome/layout.py +184 -0
  33. plot_io/genome/links.py +364 -0
  34. plot_io/genome/pack.py +27 -0
  35. plot_io/genome/readers.py +145 -0
  36. plot_io/genome/regions.py +228 -0
  37. plot_io/genome/scale.py +349 -0
  38. plot_io/genome/series.py +344 -0
  39. plot_io/genome/signal.py +621 -0
  40. plot_io/genome/sites.py +782 -0
  41. plot_io/genome/sources.py +459 -0
  42. plot_io/genome/testing.py +161 -0
  43. plot_io/geometry.py +289 -0
  44. plot_io/groups.py +308 -0
  45. plot_io/guides.py +503 -0
  46. plot_io/helpers/__init__.py +17 -0
  47. plot_io/helpers/clustermap.py +202 -0
  48. plot_io/helpers/dendrogram.py +172 -0
  49. plot_io/helpers/grids.py +304 -0
  50. plot_io/helpers/qq.py +334 -0
  51. plot_io/helpers/raincloud.py +249 -0
  52. plot_io/helpers/tree.py +214 -0
  53. plot_io/helpers/volcano.py +505 -0
  54. plot_io/hints.py +125 -0
  55. plot_io/kernels.py +740 -0
  56. plot_io/labels.py +566 -0
  57. plot_io/layers/__init__.py +100 -0
  58. plot_io/layers/base.py +630 -0
  59. plot_io/layers/context.py +87 -0
  60. plot_io/layers/core.py +446 -0
  61. plot_io/layers/geoms.py +1362 -0
  62. plot_io/layers/grammar.py +586 -0
  63. plot_io/layers/paint.py +614 -0
  64. plot_io/layers/pipeline.py +170 -0
  65. plot_io/layers/positions.py +431 -0
  66. plot_io/layers/stats.py +1088 -0
  67. plot_io/layout.py +1381 -0
  68. plot_io/legend.py +605 -0
  69. plot_io/levels.py +509 -0
  70. plot_io/limits.py +124 -0
  71. plot_io/lines.py +42 -0
  72. plot_io/mapped.py +522 -0
  73. plot_io/markers.py +110 -0
  74. plot_io/markup.py +128 -0
  75. plot_io/numbers.py +354 -0
  76. plot_io/occupancy.py +295 -0
  77. plot_io/plot.py +597 -0
  78. plot_io/presets/__init__.py +51 -0
  79. plot_io/presets/_util.py +153 -0
  80. plot_io/presets/area.py +145 -0
  81. plot_io/presets/bar.py +261 -0
  82. plot_io/presets/box.py +400 -0
  83. plot_io/presets/catpoints.py +177 -0
  84. plot_io/presets/contour.py +353 -0
  85. plot_io/presets/curves.py +30 -0
  86. plot_io/presets/dots.py +355 -0
  87. plot_io/presets/ecdf.py +97 -0
  88. plot_io/presets/errorbar.py +106 -0
  89. plot_io/presets/errors.py +296 -0
  90. plot_io/presets/fill.py +74 -0
  91. plot_io/presets/heatmap.py +322 -0
  92. plot_io/presets/hexbin.py +198 -0
  93. plot_io/presets/hist.py +391 -0
  94. plot_io/presets/hist2d.py +87 -0
  95. plot_io/presets/kde.py +162 -0
  96. plot_io/presets/kde2d.py +147 -0
  97. plot_io/presets/line.py +143 -0
  98. plot_io/presets/pie.py +502 -0
  99. plot_io/presets/regression.py +115 -0
  100. plot_io/presets/scatter.py +194 -0
  101. plot_io/presets/stem.py +100 -0
  102. plot_io/presets/violin.py +323 -0
  103. plot_io/py.typed +0 -0
  104. plot_io/registry.py +82 -0
  105. plot_io/render.py +480 -0
  106. plot_io/rows.py +491 -0
  107. plot_io/scales.py +103 -0
  108. plot_io/seams.py +138 -0
  109. plot_io/settings.py +807 -0
  110. plot_io/shapes.py +66 -0
  111. plot_io/sharing.py +232 -0
  112. plot_io/sizemaps.py +97 -0
  113. plot_io/spec.py +240 -0
  114. plot_io/states.py +216 -0
  115. plot_io/styles/__init__.py +307 -0
  116. plot_io/styles/library.py +397 -0
  117. plot_io/tables.py +138 -0
  118. plot_io/text.py +412 -0
  119. plot_io/ticks.py +400 -0
  120. plot_io/times.py +287 -0
  121. plot_io/titles.py +225 -0
  122. plot_io/transform.py +114 -0
  123. plot_io/types.py +13604 -0
  124. plot_io/units.py +768 -0
  125. plot_io/validators.py +1029 -0
  126. plot_io/values.py +190 -0
  127. plot_io-0.12.0.dist-info/METADATA +215 -0
  128. plot_io-0.12.0.dist-info/RECORD +130 -0
  129. plot_io-0.12.0.dist-info/WHEEL +4 -0
  130. plot_io-0.12.0.dist-info/licenses/LICENSE +28 -0
plot_io/plot.py ADDED
@@ -0,0 +1,597 @@
1
+ """Plots: one area of a figure, with axes, series, title, legend and colorbars."""
2
+
3
+ from typing import TYPE_CHECKING, Any, Self, Unpack
4
+
5
+ from .canvas import Canvas
6
+ from ._methods import FigureLegendMixin, PlotMixin, TitleMixin
7
+ from . import groups
8
+ from .axis import Axis
9
+ from .color.values import VARIANT_DOC, check_variant, color
10
+ from .settings import (DECLINE, custom_vars, Setting, Settings, boolean, canonical_kwargs,
11
+ dpi_or_bool, either, literal, number, one_of, route, simplify_value,
12
+ table, text)
13
+ from .guides import anchor as _anchor, placement, set_place, where_of
14
+ from .titles import ALIGNS, CREATED, SIDES, TitleBase, align_of
15
+ from .validators import length, typed
16
+ from .color.maps import kind_settings
17
+
18
+ if TYPE_CHECKING:
19
+ from .colorbar import Colorbar
20
+ from .decorations import AxLine, Annotate, Bracket, HLine, HSpan, Text, VLine, VSpan
21
+ from .types import (AnnotateSettings, AxLineSettings, BracketSettings, ColorbarSettings,
22
+ HLineSettings, HSpanSettings, LegendSettings, NewPlotSettings,
23
+ PlotSettings, TextSettings, VLineSettings, VSpanSettings, WriteOptions)
24
+
25
+ __all__ = ["Plot", "Title", "Legend", "FigureLegend"]
26
+
27
+ _SPINES = ("left", "bottom", "right", "top")
28
+ @typed("str | Iterable[str]")
29
+ def _spines(v: Any) -> frozenset[str]:
30
+ if isinstance(v, str):
31
+ s = v.strip().lower()
32
+ if s == "all":
33
+ return frozenset(_SPINES)
34
+ if s == "none":
35
+ return frozenset()
36
+ parts = s.replace(",", " ").split()
37
+ else:
38
+ parts = [str(p).lower() for p in v]
39
+ bad = [p for p in parts if p not in _SPINES]
40
+ if bad:
41
+ raise ValueError(f"unknown spine {bad[0]!r} (use left, bottom, right, top, all, none)")
42
+ return frozenset(parts)
43
+
44
+
45
+ @typed("Plot")
46
+ def _plot(v: Any) -> Any:
47
+ if not isinstance(v, Plot):
48
+ raise ValueError(f"expected a Plot, got {v!r}")
49
+ return v
50
+
51
+
52
+ _INHERITED = dict(
53
+ **groups.Font.embed(),
54
+ text_color=Setting(color, inherited=True, doc="Text color."),
55
+ line_width=Setting(length(lo=0), inherited=True, doc="Default line width."),
56
+ **kind_settings(),
57
+ color_variant=Setting(check_variant, inherited=True, doc=VARIANT_DOC),
58
+ plain_color=Setting(color, inherited=True,
59
+ doc="Color of a series that needs no hue (no levels, one color, no "
60
+ "`name`); unset: the plot's next color."),
61
+ opacity=Setting(number(0, 1), inherited=True, doc="Default opacity of series."),
62
+ rasterize=Setting(dpi_or_bool, inherited=True,
63
+ doc="`True` or a dpi: series drawn as images inside the PDF."),
64
+ simplify=Setting(simplify_value, inherited=True,
65
+ doc="vec_io's `simplify` for every series: `\"auto\"`, `True`, a dpi, "
66
+ "`False`, or a `Simplify` (or a dict of its fields)."),
67
+ data=Setting(lambda v: v, inherited=True,
68
+ doc="A table (dict, pandas or polars data frame, numpy structured array): "
69
+ "strings in the data arguments of series name its columns."),
70
+ **groups.NumberFormat.embed(),
71
+ )
72
+
73
+
74
+ def _legend_title(legend: "Legend") -> Any:
75
+ """``Legend.title="auto"``: the name of the `group` column of the plot's series."""
76
+ return getattr(legend._plot, "_legend_title", None)
77
+
78
+
79
+ class Title(TitleMixin, TitleBase):
80
+ """A title of a plot: its main title (``plot.title``) or another one
81
+ (``plot.add_title``), in a band of the plot's box on its ``side``.
82
+
83
+ The titles of one side stack outward from the axes, outermost of the side's bands: the
84
+ main title nearest, then the others in creation order; ``order`` places one elsewhere,
85
+ lower nearer (``order=-1``: a subtitle between the main title and the axes). Each takes
86
+ a band of its ``pad``, its text and, with a ``rule``, ``rule_width + rule_gap``. A line
87
+ break starts a line; the last line is nearest the axes on top, left and right. Left
88
+ titles read upward, right titles downward (``rotation="auto"``). ``align``: ``start``,
89
+ ``center``, ``end``, or an end by name (``left``, ``right`` on top and bottom; ``top``,
90
+ ``bottom`` on the left and right, where ``start`` is where the text begins).
91
+ """
92
+
93
+ def __init__(self, plot: "Plot", **settings: Any) -> None:
94
+ self._plot = plot
95
+ super().__init__(**settings)
96
+
97
+ def _parent(self) -> Settings:
98
+ return self._plot
99
+
100
+
101
+ class Legend(Settings):
102
+ """The legend of a plot (``plot.legend``): an entry per named series.
103
+
104
+ Entries are drawn as the series draws itself (marker, line, swatch, box), or as
105
+ ``glyph`` (``"box"``, ``"circle"``, ``"line"``) in its color; ``name=""`` gives none. A
106
+ plot and its twins share one legend. ``inside`` (an anchor, ``"auto"`` or ``"best"``)
107
+ places it inside the axes, ``side`` outside; setting one unsets the other, and a
108
+ ``side`` from the style yields to an ``inside`` set on the legend. On the top and
109
+ bottom entries are side by side unless ``orientation`` or ``columns`` is set; without
110
+ ``columns`` they wrap within the axes' length (inside: less ``pad``). An inside legend
111
+ larger than its axes goes to the right side, with a warning. Outside, a legend wraps
112
+ within the axes' length along its side, or in a figure of given size within the
113
+ plot's box when longer (passing the axes' ends); ``align`` places it along its side
114
+ (``auto``: centred on top and bottom, else at the axes' start).
115
+
116
+ ``inside="auto"`` marks what the series of the plot and its twins draw on a 64 × 64 grid
117
+ over the axes (markers with their size, curves along their length, the areas of bars,
118
+ boxes, violins and fills, the whole axes for heatmaps and contours; data sampled beyond
119
+ 50,000 points), with the text and lines of decorations and inside colorbars. The legend
120
+ takes the first of ``top_right``, ``top_left``, ``bottom_left``, ``bottom_right``,
121
+ ``top``, ``bottom``, ``center_right``, ``center_left`` that covers no marked cell, else
122
+ the free place nearest a corner, else the candidate covering no marker and the fewest
123
+ marked cells (at most 2 %), else the right side. ``inside="best"`` (matplotlib's
124
+ ``loc="best"``) chooses among nine places, any covering no marker (the fewest marked
125
+ cells first), else goes outside on the right like ``"auto"``.
126
+ """
127
+
128
+ SETTINGS = table(
129
+ inside=Setting(_anchor,
130
+ doc="Place inside the axes: `auto` (the least covered place, "
131
+ "never over a marker; else outside right), `best` (the "
132
+ "least covered place, never outside), `top_right`, `top_left`, "
133
+ "`bottom_right`, `bottom_left`, `top`, `bottom`, `center_right` "
134
+ "or `center_left`; setting it unsets `side`."),
135
+ side=Setting(one_of(*SIDES),
136
+ doc="Outside the axes on a side of the plot: `top`, `bottom`, `left` "
137
+ "or `right`; setting it unsets `inside`."),
138
+ align=Setting(one_of("auto", *ALIGNS),
139
+ doc="Alignment along its side, outside the axes: `start` (left, or "
140
+ "top), `center`, `end` (right, or bottom), or an end by name; "
141
+ "`auto`: centred when horizontal on top or bottom, else `start`."),
142
+ visible=Setting(boolean, doc="Draw the legend."),
143
+ columns=Setting(number(lo=1, hi=10_000, integer=True), doc="Number of columns."),
144
+ frame=Setting(boolean, doc="Draw a frame and background."),
145
+ title=Setting(text, auto=_legend_title,
146
+ doc="Legend title; `\"auto\"`: the `group` column name, if any."),
147
+ **groups.TextStyle.embed(only=("font_size", "color")),
148
+ background=Setting(color, doc="Background of a framed legend."),
149
+ **groups.Stroke.embed("frame_", only=("color", "line_width"),
150
+ docs={"color": "Frame color.",
151
+ "line_width": "Frame line width."}),
152
+ pad=Setting(length(lo=0, percent="axes"), doc="Gap to the axes."),
153
+ frame_radius=Setting(length(lo=0), doc="Corner radius of the frame."),
154
+ frame_opacity=Setting(number(0, 1), doc="Opacity of the frame and background."),
155
+ glyph_opacity=Setting(number(0, 1),
156
+ doc="Fill opacity of `box` and `circle` symbols, outlined in the "
157
+ "opaque color; default: the series' own fill."),
158
+ glyph=Setting(one_of("series", "box", "circle", "line"),
159
+ doc="Entry symbol: as the series draws itself, or a filled `box`, a "
160
+ "`circle` or a `line` in the series color."),
161
+ orientation=Setting(one_of("auto", "vertical", "horizontal"),
162
+ doc="`horizontal`: entries side by side; `auto`: horizontal on "
163
+ "the top and bottom sides and at `inside` `top` and "
164
+ "`bottom`."),
165
+ glyph_length=Setting(length(positive=True, percent="em"),
166
+ doc="Width of an entry's symbol."),
167
+ row_height=Setting(length(positive=True, percent="em"), doc="Height of an entry."),
168
+ glyph_gap=Setting(length(lo=0, percent="em"), doc="Gap between a symbol and its text."),
169
+ column_gap=Setting(length(lo=0, percent="em"), doc="Gap between columns."),
170
+ title_gap=Setting(length(lo=0, percent="em"), doc="Gap below the title."),
171
+ inner_pad=Setting(either(literal("auto"), length(lo=0, percent="em")),
172
+ auto=lambda g: (0.5 if g.resolve("frame") else 0.15)
173
+ * g.resolve("font_size"),
174
+ doc="Space inside the box; `\"auto\"`: 0.5em with a frame, 0.15em "
175
+ "without."),
176
+ **groups.Font.embed(only=groups.Font.FACE),
177
+ )
178
+
179
+ def __init__(self, plot: "Plot") -> None:
180
+ self._plot = plot
181
+ super().__init__()
182
+
183
+ def _parent(self) -> Settings:
184
+ return self._plot
185
+
186
+ def pads(self) -> tuple[float, float]:
187
+ """``pad`` across and down (px), while drawn: `%` of the axes width and height."""
188
+ return (self.resolve("pad", of=self._box("axes_width", "pad")),
189
+ self.resolve("pad", of=self._box("axes_height", "pad")))
190
+
191
+ def set(self, **settings: "Unpack[LegendSettings]") -> Self: # type: ignore[override]
192
+ super().set(**settings)
193
+ set_place(self, dict(settings))
194
+ return self
195
+
196
+ def placement(self) -> tuple[str, Any]:
197
+ """``("inside", anchor)`` or ``("side", side)``: where the legend goes."""
198
+ return placement(self, side_first=True)
199
+
200
+ def is_placed(self) -> bool:
201
+ """Whether its place (``inside``, ``side`` or ``visible``) is set on it."""
202
+ return any(self.is_set(k) for k in ("inside", "side", "visible"))
203
+
204
+ def __repr__(self) -> str:
205
+ return f"Legend({self.placement()!r})"
206
+
207
+
208
+ def _figure_legend_title(legend: "FigureLegend") -> Any:
209
+ """``FigureLegend.title="auto"``: its plots' legend titles (each the name of the
210
+ `group` column of its series unless set) when they are all the same (otherwise each
211
+ heads its own entries)."""
212
+ plots = legend.plots if legend.plots is not None else legend._figure.plots
213
+ titles = [p.legend.resolve("title") for p in plots]
214
+ return titles[0] if titles and all(t == titles[0] for t in titles) else None
215
+
216
+
217
+ class FigureLegend(FigureLegendMixin, Legend):
218
+ """A legend of the figure (``figure.add_legend``, or made by ``share_legend``): the
219
+ entries of a set of plots, in the lanes beside them as figure titles are.
220
+
221
+ Its plots' cells must fill the grid rectangle they span. Entries come in plot order (row
222
+ by row), then series order; entries with the same label and the same symbol are one,
223
+ the same label with another symbol keeps both. Its plots' legends lose those entries
224
+ (a plot left without entries shows none), unless the plot's legend has ``inside``,
225
+ ``side`` or ``visible`` set. A size key whose plots the legend all covers goes into it.
226
+ ``side`` and ``align`` place it within the axes of its plots, in the lanes of
227
+ :class:`FigureTitle` (a legend nearer the plots than a title over the same plots; one
228
+ without entries takes no lane). ``share_legend`` (figure setting) makes one per group
229
+ of plots, as ``share_color`` groups them (a group with a plot an ``add_legend`` legend
230
+ covers gets none). ``plot.save()`` leaves figure legends out.
231
+ """
232
+
233
+ SETTINGS = table(**{
234
+ **{k: s for k, s in Legend.SETTINGS.items()
235
+ if k not in ("inside", "side", "visible", "orientation", "pad", "title")},
236
+ "side": Setting(one_of(*SIDES), doc="`top`, `bottom`, `left` or `right` of its plots."),
237
+ "align": Setting(one_of("auto", *ALIGNS),
238
+ doc="Alignment along the side: `start` (left, or top), `center`, "
239
+ "`end` (right, or bottom), or an end by name: `left`, `right` "
240
+ "on top and bottom, `top`, `bottom` on the left and right; "
241
+ "`auto`: `center`."),
242
+ "order": Setting(number(), doc="Place in its side's lanes, as figure titles' "
243
+ "`order`: lower is nearer the plots."),
244
+ "pad": Setting(length(lo=0), doc="Gap between the legend and the plots."),
245
+ "title": Setting(text, auto=_figure_legend_title,
246
+ doc="Legend title; `\"auto\"`: its plots' legend title when they "
247
+ "share one, else each heads its own entries."),
248
+ })
249
+
250
+ def __init__(self, figure: Any, plots: Any = None, **settings: Any) -> None:
251
+ self._figure = figure
252
+ #: the plots it covers (``None``: every plot)
253
+ self.plots: list[Any] | None = plots
254
+ self._made = next(CREATED)
255
+ Settings.__init__(self, **settings)
256
+
257
+ def _parent(self) -> Settings:
258
+ return self._figure
259
+
260
+ def __repr__(self) -> str:
261
+ return f"FigureLegend({self.get('side')!r})"
262
+
263
+ def along(self) -> str:
264
+ """``align`` as ``start``, ``center`` or ``end`` (``auto``: ``center``)."""
265
+ a = self.resolve("align")
266
+ return "center" if a == "auto" else align_of(a)
267
+
268
+
269
+ def _data_ratio(plot: "Plot") -> Any:
270
+ """1 when a series asks for equal units on both axes (a square Hi-C matrix)."""
271
+ if any(getattr(s, "equal_aspect", lambda: False)() for s in plot.series):
272
+ return 1.0
273
+ return DECLINE
274
+
275
+
276
+ class Plot(PlotMixin, Settings):
277
+ """One plot: axes, series and decorations.
278
+
279
+ ``plt.Plot(**settings)`` creates a one-plot figure (``plot.figure``);
280
+ ``figure.plot(…)`` adds a plot to an existing figure.
281
+
282
+ At creation and in ``set``: ``title=`` sets the title text, ``x_axis={…}`` and
283
+ ``y_axis={…}`` the axes' settings, ``legend=`` (a place, a dict of legend settings or
284
+ ``False``) calls :meth:`add_legend` and ``colorbar=`` (``True``, a place or a dict)
285
+ :meth:`add_colorbar`. A place is a side (``"right"``: ``side=``), an anchor inside the
286
+ axes (``"top_left"``, ``"auto"``: ``inside=``) or ``"none"``. ``plot.title``,
287
+ ``plot.x_axis`` and ``plot.y_axis`` are objects: calling one sets its settings and
288
+ returns it. Series are drawn in ``z`` order (default: call order) inside the axes clip.
289
+
290
+ ``twin_y()`` (``twin_x()``) returns a plot on the same axes rectangle sharing the x (y)
291
+ axis, limits from the series of both, with its own y axis on the right (x axis on top).
292
+ Its axis adds margin on its side; its series continue the plot's colors and appear in the
293
+ plot's legend (``twin.legend is plot.legend``); background, title, frame and grid are
294
+ the plot's (a twin draws a grid only when its axis asks). ``save`` includes the twins.
295
+ """
296
+
297
+ SETTINGS = table(
298
+ spines=Setting(_spines,
299
+ doc="Axis lines drawn: any of `left`, `bottom`, `right`, `top`, or "
300
+ "`all`, `none`."),
301
+ data_ratio=Setting(number(positive=True),
302
+ doc="Data aspect ratio: 1 makes one unit as long on both axes; "
303
+ "unset: 1 for a square Hi-C matrix (outside a tracks "
304
+ "figure)."),
305
+ background=Setting(color, doc="Background color of the axes area."),
306
+ **groups.Stroke.embed("border_", only=("color", "line_width"),
307
+ docs={"color": "Color of a frame around the axes area (drawn "
308
+ "under the axis lines); default none.",
309
+ "line_width": "Width of the frame."}),
310
+ clip=Setting(boolean, doc="Clip series to the axes area."),
311
+ axes=Setting(boolean, doc="Draw axes; `False` hides both axes, drops padding "
312
+ "and fixes the aspect to 1 (set by pie charts)."),
313
+ share_x=Setting(_plot, doc="Another plot of the figure whose x axis this plot "
314
+ "shares (groups chain); overrides the figure's `share_x`."),
315
+ share_y=Setting(_plot, doc="Another plot whose y axis this plot shares."),
316
+ vars=Setting(custom_vars, expr=False,
317
+ doc="Custom variables, read by `var(--name)` in any setting here and "
318
+ "below: `{\"accent\": \"#c00\"}`."),
319
+ share=Setting(_plot, doc="Another plot whose x and y axes this plot shares."),
320
+ space_above=Setting(length(lo=0, percent="em"),
321
+ doc="Space above the plot, beyond the figure's `spacing`."),
322
+ **_INHERITED,
323
+ )
324
+ RULES = {"data_ratio": _data_ratio}
325
+ #: Keywords accepted besides settings: ``title="…"`` sets the title text,
326
+ #: ``x_axis={…}`` and ``y_axis={…}`` the axes' settings, ``legend=`` and ``colorbar=``
327
+ #: call :meth:`add_legend` and :meth:`add_colorbar`.
328
+ EXTRA = ("title", "x_axis", "y_axis", "legend", "colorbar")
329
+ #: Of those, the ones a series call making the plot passes on to it (``colorbar=`` is
330
+ #: a series setting there).
331
+ CALL_EXTRA = ("title", "x_axis", "y_axis", "legend")
332
+
333
+ def __init__(self, figure: Any = None, id: Any = None, _twin: Any = None,
334
+ **settings: Unpack["NewPlotSettings"]) -> None:
335
+ from .figure import Figure
336
+
337
+ own, fig_kw = route(dict(settings), [Plot, Figure], extra={0: Plot.EXTRA})
338
+ title = own.pop("title", None)
339
+ axes: dict[str, Any] = {k: own.pop(k) for k in ("x_axis", "y_axis") if k in own}
340
+ guides: dict[str, Any] = {k: own.pop(k) for k in ("legend", "colorbar") if k in own}
341
+ if figure is None:
342
+ figure = Figure(**fig_kw)
343
+ elif fig_kw:
344
+ from .layers.base import upward_message
345
+ raise ValueError(upward_message("Plot", [("figure", "figure", fig_kw)]))
346
+ self.figure = figure
347
+ self.x_axis = Axis(self, "x")
348
+ self.y_axis = Axis(self, "y")
349
+ self.title = Title(self)
350
+ self._titles: list[Title] = []
351
+ #: the legend: a twin's is its main plot's (one legend per axes rectangle)
352
+ self.legend = _twin[0].legend if _twin is not None else Legend(self)
353
+ self.series: list[Any] = []
354
+ self.decorations: list[Any] = []
355
+ self.colorbars: list[Any] = []
356
+ self.twins: list[Plot] = []
357
+ #: ``(main plot, "y" or "x")`` for a twin: the axis it has of its own.
358
+ self.twin_of: tuple[Plot, str] | None = _twin
359
+ super().__init__(**own)
360
+ if title is not None:
361
+ self.title.set(text=title)
362
+ self._set_axes(axes)
363
+ self.id = figure._attach(self, id) if _twin is None else None
364
+ self._set_guides(guides)
365
+
366
+ def _parent(self) -> Settings:
367
+ return self.twin_of[0] if self.twin_of is not None else self.figure
368
+
369
+ def _children(self) -> list[Settings]:
370
+ legend = [] if self.twin_of is not None else [self.legend]
371
+ return [self.title, *self._titles, *legend, self.x_axis, self.y_axis, *self.series,
372
+ *self.decorations, *self.colorbars, *self.twins]
373
+
374
+ @property
375
+ def titles(self) -> list[Title]:
376
+ """The plot's titles: its main title first, then those of :meth:`add_title`."""
377
+ return [self.title, *self._titles]
378
+
379
+ def add_title(self, text: Any, side: Any = None, **settings: Any) -> Title:
380
+ """Another title of the plot, on ``side`` (default ``"top"``), stacked outward of
381
+ the titles before it there (``order`` places it elsewhere); returns it."""
382
+ if side is not None:
383
+ settings["side"] = side
384
+ t = Title(self, text=text, **settings)
385
+ self._titles.append(t)
386
+ return t
387
+
388
+ def twin_y(self, **settings: Unpack["PlotSettings"]) -> "Plot":
389
+ """A plot on the same axes with its own y axis (on the right) and the x axis of
390
+ this plot: add series to it and set its ``y_axis``."""
391
+ return self._twin("y", dict(settings))
392
+
393
+ def twin_x(self, **settings: Unpack["PlotSettings"]) -> "Plot":
394
+ """A plot on the same axes with its own x axis (on top) and the y axis of this
395
+ plot."""
396
+ return self._twin("x", dict(settings))
397
+
398
+ def _twin(self, role: str, settings: dict[str, Any]) -> "Plot":
399
+ """A twin with its own ``role`` axis on the far side (its line drawn there with
400
+ the axis) and the other axis hidden."""
401
+ t = Plot(figure=self.figure, _twin=(self, role), **settings)
402
+ own, other = (t.y_axis, t.x_axis) if role == "y" else (t.x_axis, t.y_axis)
403
+ other.set(visible=False)
404
+ if not own.is_set("side"):
405
+ own.set(side="right" if role == "y" else "top")
406
+ if not t.is_set("spines"):
407
+ t.set(spines="left" if role == "y" else "bottom")
408
+ self.twins.append(t)
409
+ return t
410
+
411
+ def get(self, name: str) -> Any:
412
+ """As :meth:`Settings.get`; ``"title"``: the title's text."""
413
+ if name == "title":
414
+ return self.title.get("text")
415
+ return super().get(name)
416
+
417
+ def set(self, **given: "Unpack[PlotSettings]") -> Self: # type: ignore[override]
418
+ settings = canonical_kwargs(dict(given), self.SETTINGS, "Plot")
419
+ fig_share = {k: settings.pop(k) for k in ("share_x", "share_y", "share")
420
+ if k in settings and settings[k] is not None
421
+ and not isinstance(settings[k], Plot)}
422
+ if fig_share:
423
+ self.figure.set(**fig_share)
424
+ title = settings.pop("title", None)
425
+ if title is not None:
426
+ self.title.set(text=title)
427
+ self._set_axes({k: settings.pop(k) for k in ("x_axis", "y_axis") if k in settings})
428
+ guides: dict[str, Any] = {k: settings.pop(k) for k in ("legend", "colorbar")
429
+ if k in settings}
430
+ super().set(**settings)
431
+ self._set_guides(guides)
432
+ return self
433
+
434
+ def _set_axes(self, axes: dict[str, Any]) -> None:
435
+ """``x_axis={…}``, ``y_axis={…}``: settings of the plot's axes."""
436
+ for k, v in axes.items():
437
+ if not isinstance(v, dict):
438
+ raise ValueError(f"{k}= takes a dict of axis settings, got {v!r}")
439
+ getattr(self, k).set(**v)
440
+
441
+ def _set_guides(self, guides: dict[str, Any]) -> None:
442
+ """``legend=``: a place (a side, a place inside the axes, ``"none"``), a dict of
443
+ legend settings or ``False`` (no legend); ``colorbar=``: ``True``, a place or a
444
+ dict of colorbar settings (one more colorbar each time)."""
445
+ legend = guides.get("legend")
446
+ if legend is not None:
447
+ if legend is False:
448
+ self.add_legend(visible=False)
449
+ elif isinstance(legend, dict):
450
+ self.add_legend(**legend)
451
+ elif isinstance(legend, str):
452
+ self.add_legend(legend)
453
+ else:
454
+ raise ValueError(f"legend= takes a place, a dict of legend settings or "
455
+ f"False, got {legend!r}")
456
+ bar = guides.get("colorbar")
457
+ if bar is not None and bar is not False:
458
+ if bar is True:
459
+ self.add_colorbar()
460
+ elif isinstance(bar, dict):
461
+ self.add_colorbar(**bar)
462
+ elif isinstance(bar, str) and "visible" not in where_of(bar):
463
+ self.add_colorbar(None, bar)
464
+ else:
465
+ raise ValueError(f"colorbar= takes True, a place or a dict of colorbar "
466
+ f"settings, got {bar!r}")
467
+
468
+ def __repr__(self) -> str:
469
+ from .layout import arrange
470
+ main = self.twin_of[0] if self.twin_of is not None else self
471
+ cell = next(((a.r0, a.c0) for a in arrange(self.figure)[0] if a.obj is main), None)
472
+ where = f"row {cell[0]}, col {cell[1]}, " if cell is not None else ""
473
+ return f"Plot({where}{len(self.series)} series)"
474
+
475
+ def own_share(self, role: str) -> Any:
476
+ """The plot this plot's ``role`` axis is paired with, or ``None``."""
477
+ return self.resolve(f"share_{role}") or self.resolve("share")
478
+
479
+ def _add_series(self, s: Any) -> None:
480
+ self.series.append(s)
481
+ from .spec import current
482
+ r = current()
483
+ if r is not None: # added during a render: its calls change
484
+ r.plots.clear()
485
+ r.calls.clear()
486
+
487
+ # Plot types are methods added by ``registry.layers.add``
488
+ # (``p.scatter(…)``, ``p.hist(…)``, ``p.bigwig(…)``…).
489
+
490
+ # -- decorations -----------------------------------------------------------------
491
+
492
+ def hline(self, y: Any, **settings: Unpack["HLineSettings"]) -> "HLine":
493
+ """A horizontal reference line at ``y``."""
494
+ from .decorations import HLine
495
+ return HLine(self, y, **settings)
496
+
497
+ def vline(self, x: Any, **settings: Unpack["VLineSettings"]) -> "VLine":
498
+ """A vertical reference line at ``x``."""
499
+ from .decorations import VLine
500
+ return VLine(self, x, **settings)
501
+
502
+ def axline(self, point: Any, slope: Any = None, point2: Any = None,
503
+ **settings: Unpack["AxLineSettings"]) -> "AxLine":
504
+ """An infinite line through ``point`` with ``slope``, or through ``point2``."""
505
+ from .decorations import AxLine
506
+ return AxLine(self, point, slope, point2, **settings)
507
+
508
+ def hspan(self, y0: Any, y1: Any, **settings: Unpack["HSpanSettings"]) -> "HSpan":
509
+ """A horizontal band from ``y0`` to ``y1``."""
510
+ from .decorations import HSpan
511
+ return HSpan(self, y0, y1, **settings)
512
+
513
+ def vspan(self, x0: Any, x1: Any, **settings: Unpack["VSpanSettings"]) -> "VSpan":
514
+ """A vertical band from ``x0`` to ``x1``."""
515
+ from .decorations import VSpan
516
+ return VSpan(self, x0, x1, **settings)
517
+
518
+ def text(self, x: Any, y: Any, content: Any,
519
+ **settings: Unpack["TextSettings"]) -> "Text":
520
+ """Text at ``(x, y)``."""
521
+ from .decorations import Text
522
+ return Text(self, x, y, content, **settings)
523
+
524
+ def annotate(self, content: Any, xy: Any, xytext: Any = None,
525
+ **settings: Unpack["AnnotateSettings"]) -> "Annotate":
526
+ """Text at ``xytext`` with an arrow to ``xy``."""
527
+ from .decorations import Annotate
528
+ return Annotate(self, content, xy, xytext, **settings)
529
+
530
+ def bracket(self, a: Any, b: Any, text: Any = None,
531
+ **settings: Unpack["BracketSettings"]) -> "Bracket":
532
+ """A bracket from ``a`` to ``b`` (categories or positions) with ``text``, or ``p=``
533
+ written as stars or a p-value."""
534
+ from .decorations import Bracket
535
+ return Bracket(self, a, b, text, **settings)
536
+
537
+ def brackets(self, pairs: Any, p: Any = None, text: Any = None,
538
+ **settings: Unpack["BracketSettings"]) -> list["Bracket"]:
539
+ """A bracket per ``(a, b)`` of ``pairs``, with the p-value (``p``) or text
540
+ (``text``) of the same index; the same settings."""
541
+ from .decorations import Bracket
542
+ pairs = list(pairs)
543
+ for name, v in (("p", p), ("text", text)):
544
+ if v is not None and len(v) != len(pairs):
545
+ raise ValueError(f"brackets: {len(v)} {name} values for {len(pairs)} pairs")
546
+ return [Bracket(self, a, b, None if text is None else text[i],
547
+ p=None if p is None else p[i], **settings)
548
+ for i, (a, b) in enumerate(pairs)]
549
+
550
+ def add_legend(self, where: Any = None, /, **settings: Unpack["LegendSettings"]) -> Legend:
551
+ """Set the plot's legend (``plot.legend``, shared with its twins) and return it:
552
+ ``where`` is a side (``"right"``: ``side=``), a place inside the axes
553
+ (``"top_left"``: ``inside=``) or ``"none"``; then any legend settings."""
554
+ kw: dict[str, Any] = {**where_of(where), **settings} if where is not None \
555
+ else dict(settings)
556
+ return self.legend.set(**kw)
557
+
558
+ def add_colorbar(self, source: Any = None, where: Any = None,
559
+ **settings: Unpack["ColorbarSettings"]) -> "Colorbar":
560
+ """A colorbar attached to this plot: the guide of the color (or size) axis of
561
+ ``source`` (a mapped series or an axis; default: the plot's last mapped series);
562
+ ``where``: a side (``side=``, default ``"bottom"``) or a place inside the axes
563
+ (``inside=``); returns it."""
564
+ from .colorbar import Colorbar
565
+ kw: dict[str, Any] = {**where_of(where), **settings} if where is not None \
566
+ else dict(settings)
567
+ cb = Colorbar(source, plot=self, **kw)
568
+ self.colorbars.append(cb)
569
+ return cb
570
+
571
+ # -- output ----------------------------------------------------------------------
572
+
573
+ def draw(self, canvas: Canvas, x: float = 0.0, y: float = 0.0) -> tuple[float, float]:
574
+ """Draw the plot alone on a canvas (a vec_io page, a
575
+ :class:`~plot_io.canvas.Recording`) with its top-left corner at ``(x, y)``
576
+ (page units); returns its size in px."""
577
+ from . import render
578
+ return render.draw(self.figure, canvas, x, y, only=self)
579
+
580
+ def save(self, file: Any, *, region: Any = None, format: str | None = None,
581
+ **options: Unpack["WriteOptions"]) -> None:
582
+ """Save the plot alone (see :meth:`Figure.save`)."""
583
+ from . import render
584
+ with self.figure._region(region):
585
+ render.save(self.figure, file, only=self, format=format, **options)
586
+
587
+ def write(self, format: str, *, region: Any = None,
588
+ **options: Unpack["WriteOptions"]) -> bytes:
589
+ """The plot alone as ``format`` bytes (see :meth:`Figure.write`)."""
590
+ from . import render
591
+ with self.figure._region(region):
592
+ return render.write(self.figure, format, only=self, **options)
593
+
594
+ def _repr_mimebundle_(self, include: Any = None, exclude: Any = None) -> dict[str, str]:
595
+ """The plot alone as SVG for notebooks (``image/svg+xml`` only)."""
596
+ from . import render
597
+ return render.repr_svg(self.figure, self, include, exclude)
@@ -0,0 +1,51 @@
1
+ """The plot types: each a :class:`~plot_io.layers.Series` declared by a ``SPEC`` (its data,
2
+ layers and what the axes need, built by :mod:`plot_io.layers.grammar`) or with a
3
+ ``layers()`` of its own, registered as a ``Plot`` method (``registry.layers.add``)."""
4
+
5
+ from ..layers import add_layer
6
+ from ..levels import adopt
7
+ from ..styles import register_class
8
+ from .area import Area
9
+ from .bar import Bar
10
+ from .box import Box
11
+ from .catpoints import Strip, Swarm
12
+ from .contour import Contour
13
+ from .dots import Dots, group_summary
14
+ from .ecdf import Ecdf
15
+ from .errorbar import Errorbar
16
+ from .errors import Errors
17
+ from .fill import Fill
18
+ from .heatmap import Heatmap
19
+ from .hexbin import Hexbin
20
+ from .hist import Hist
21
+ from .hist2d import Hist2d
22
+ from .kde import Kde
23
+ from .kde2d import Kde2d
24
+ from .line import Line
25
+ from .pie import Pie
26
+ from .regression import Regression
27
+ from .scatter import Scatter
28
+ from .stem import Stem
29
+ from .violin import Violin
30
+
31
+ __all__ = ["Area", "Bar", "Box", "Strip", "Swarm", "Contour", "Dots", "group_summary", "Ecdf", "Errorbar",
32
+ "Errors", "Fill", "Heatmap", "Hexbin", "Hist", "Hist2d", "Kde", "Kde2d", "Line",
33
+ "Pie", "Regression", "Scatter", "Stem", "Violin", "TYPES"]
34
+
35
+ #: The plot types by ``Plot`` method name.
36
+ TYPES = {"scatter": Scatter, "line": Line, "fill": Fill, "box": Box, "heatmap": Heatmap,
37
+ "bar": Bar, "hist": Hist, "kde": Kde, "ecdf": Ecdf, "violin": Violin,
38
+ "strip": Strip, "swarm": Swarm, "area": Area, "errorbar": Errorbar, "stem": Stem,
39
+ "regression": Regression, "hist2d": Hist2d, "hexbin": Hexbin, "contour": Contour,
40
+ "pie": Pie, "dots": Dots}
41
+
42
+ for _split in (Line, Fill, Area, Hist, Kde, Ecdf, Regression, Bar, Box, Violin, Strip, Swarm,
43
+ Errorbar, Stem):
44
+ adopt(_split, "split")
45
+ adopt(Scatter, "items")
46
+ Heatmap.LEVELS = Contour.LEVELS = Hexbin.LEVELS = "grid"
47
+ Pie.LEVELS = "refuse"
48
+
49
+ for _name, _cls in TYPES.items():
50
+ add_layer(_name, _cls)
51
+ register_class(Kde2d) # p.kde(x, y): a style class of its own