ggh4x-python 0.3.1.9000__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 (64) hide show
  1. ggh4x/__init__.py +140 -0
  2. ggh4x/_aimed_text_grob.py +432 -0
  3. ggh4x/_borrowed_ggplot2.py +273 -0
  4. ggh4x/_cli.py +84 -0
  5. ggh4x/_datasets.py +106 -0
  6. ggh4x/_download.py +111 -0
  7. ggh4x/_facet_helpers.py +313 -0
  8. ggh4x/_facet_utils.py +649 -0
  9. ggh4x/_gap_grobs.py +606 -0
  10. ggh4x/_registry.py +10 -0
  11. ggh4x/_rlang.py +93 -0
  12. ggh4x/_utils.py +150 -0
  13. ggh4x/_vctrs.py +233 -0
  14. ggh4x/conveniences.py +601 -0
  15. ggh4x/coord_axes_inside.py +380 -0
  16. ggh4x/element_part_rect.py +545 -0
  17. ggh4x/facet_grid2.py +1018 -0
  18. ggh4x/facet_manual.py +901 -0
  19. ggh4x/facet_nested.py +776 -0
  20. ggh4x/facet_nested_wrap.py +193 -0
  21. ggh4x/facet_wrap2.py +896 -0
  22. ggh4x/geom_box.py +536 -0
  23. ggh4x/geom_outline_point.py +444 -0
  24. ggh4x/geom_pointpath.py +259 -0
  25. ggh4x/geom_polygonraster.py +252 -0
  26. ggh4x/geom_rectrug.py +489 -0
  27. ggh4x/geom_text_aimed.py +279 -0
  28. ggh4x/guide_stringlegend.py +354 -0
  29. ggh4x/help_secondary.py +549 -0
  30. ggh4x/multiscale/__init__.py +51 -0
  31. ggh4x/multiscale/_multiscale_add.py +207 -0
  32. ggh4x/multiscale/scale_listed.py +167 -0
  33. ggh4x/multiscale/scale_manual.py +478 -0
  34. ggh4x/multiscale/scale_multi.py +393 -0
  35. ggh4x/panel_scales/__init__.py +58 -0
  36. ggh4x/panel_scales/at_panel.py +115 -0
  37. ggh4x/panel_scales/facetted_pos_scales.py +647 -0
  38. ggh4x/panel_scales/force_panelsize.py +411 -0
  39. ggh4x/panel_scales/scale_facet.py +222 -0
  40. ggh4x/position_disjoint_ranges.py +229 -0
  41. ggh4x/position_lineartrans.py +242 -0
  42. ggh4x/py.typed +0 -0
  43. ggh4x/resources/faithful.csv +273 -0
  44. ggh4x/resources/iris.csv +151 -0
  45. ggh4x/resources/mtcars.csv +33 -0
  46. ggh4x/resources/pressure.csv +20 -0
  47. ggh4x/resources/volcano.csv +87 -0
  48. ggh4x/save.py +255 -0
  49. ggh4x/stat_difference.py +388 -0
  50. ggh4x/stat_funxy.py +436 -0
  51. ggh4x/stat_rle.py +290 -0
  52. ggh4x/stat_rollingkernel.py +369 -0
  53. ggh4x/stat_theodensity.py +681 -0
  54. ggh4x/strip_nested.py +448 -0
  55. ggh4x/strip_split.py +687 -0
  56. ggh4x/strip_tag.py +636 -0
  57. ggh4x/strip_themed.py +232 -0
  58. ggh4x/strip_vanilla.py +1464 -0
  59. ggh4x/themes.py +31 -0
  60. ggh4x/themes_ggh4x.py +67 -0
  61. ggh4x_python-0.3.1.9000.dist-info/METADATA +40 -0
  62. ggh4x_python-0.3.1.9000.dist-info/RECORD +64 -0
  63. ggh4x_python-0.3.1.9000.dist-info/WHEEL +4 -0
  64. ggh4x_python-0.3.1.9000.dist-info/licenses/LICENSE +3 -0
ggh4x/strip_nested.py ADDED
@@ -0,0 +1,448 @@
1
+ """Nested strips for ggh4x facets (port of ggh4x ``strip_nested.R``).
2
+
3
+ This module ports :class:`StripNested` and the :func:`strip_nested`
4
+ constructor. Nested strips merge adjacent strips on the same layer that share a
5
+ label, so an outer faceting variable spans the panels of its inner variables.
6
+ It is the default strip for ``facet_nested()`` / ``facet_nested_wrap()``.
7
+
8
+ ``StripNested`` extends :class:`ggh4x.strip_themed.StripThemed`. It overrides
9
+ two methods:
10
+
11
+ * :meth:`StripNested.assemble_strip` -- run-length-encoding (RLE) merge of
12
+ adjacent equal labels per layer. A single-layer strip (one faceting variable)
13
+ has nothing to merge and is delegated to the base
14
+ :meth:`Strip.assemble_strip` via :func:`ggplot2_py.ggproto.ggproto_parent`.
15
+ * :meth:`StripNested.finish_strip` -- builds *one* multi-cell gtable per
16
+ panel-group (the merged strip) when the redefined layout carries a ``layer``
17
+ column; otherwise (the monolayer fast path produced a plain layout) delegates
18
+ to the self-less :func:`Strip.finish_strip`.
19
+
20
+ R source: ``ggh4x/R/strip_nested.R``.
21
+
22
+ Notes
23
+ -----
24
+ * **Column-major / ROW-COL ordering is load-bearing.** The labels and layout
25
+ are re-ordered by ``order(ROW, COL)`` (x strips) or ``order(COL, ROW)`` (y
26
+ strips); the RLE separator variable (``ROW`` for x, ``COL`` for y) is pasted
27
+ onto each label to block merges across rows/columns. When ``bleed`` is
28
+ ``False`` each layer's pasted key is further pasted with all *preceding*
29
+ layers so a lower-layer strip cannot merge across a higher-layer boundary.
30
+ * **Run encoding.** ``rle(x)$lengths`` per layer -> ``ends = cumsum(lengths)``,
31
+ ``starts = ends - lengths + 1`` (1-based, per-layer concatenated). The merged
32
+ cell takes the label at its run-start row and its layer column
33
+ (``labels[cbind(starts, index)]``).
34
+ * **Method-binding asymmetry.** ``assemble_strip`` / ``finish_strip`` here take
35
+ ``self`` (instance methods). The monolayer delegate uses
36
+ ``ggproto_parent(Strip, self).assemble_strip(...)`` (parent dispatch on a
37
+ *self-bearing* function), while the no-``layer``-column delegate calls the
38
+ *self-less* :func:`Strip.finish_strip` directly (a plain function). These two
39
+ idioms are intentionally different and must not be unified.
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ from typing import Any, Dict, List, Sequence
45
+
46
+ import numpy as np
47
+ import pandas as pd
48
+
49
+ from ggplot2_py.ggproto import ggproto, ggproto_parent
50
+ from grid_py import unit_c, unit_rep
51
+ from gtable_py import Gtable, gtable_add_grob
52
+
53
+ from ggh4x._rlang import arg_match0
54
+ from ggh4x.strip_themed import StripThemed
55
+ from ggh4x.strip_vanilla import (
56
+ Strip,
57
+ _is_zero_grob,
58
+ validate_element_list,
59
+ )
60
+
61
+ __all__ = ["StripNested", "strip_nested"]
62
+
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # Run-length helper
66
+ # ---------------------------------------------------------------------------
67
+ def _rle_lengths(x: Sequence[Any]) -> List[int]:
68
+ """Port of R ``rle(x)$lengths`` -- run lengths of consecutive equal values.
69
+
70
+ Parameters
71
+ ----------
72
+ x : sequence
73
+ Values to run-length encode.
74
+
75
+ Returns
76
+ -------
77
+ list of int
78
+ The length of each maximal run of consecutive equal elements.
79
+ """
80
+ lengths: List[int] = []
81
+ prev = object()
82
+ for v in x:
83
+ if v == prev and lengths:
84
+ lengths[-1] += 1
85
+ else:
86
+ lengths.append(1)
87
+ prev = v
88
+ return lengths
89
+
90
+
91
+ def _recycle(seq: Sequence[Any], length_out: int) -> List[Any]:
92
+ """Port of R ``rep(seq, length.out=n)`` for a plain list.
93
+
94
+ Parameters
95
+ ----------
96
+ seq : sequence
97
+ Source values.
98
+ length_out : int
99
+ Target length.
100
+
101
+ Returns
102
+ -------
103
+ list
104
+ *seq* cyclically recycled (or truncated) to *length_out*.
105
+ """
106
+ n = len(seq)
107
+ if length_out <= 0 or n == 0:
108
+ return []
109
+ return [seq[i % n] for i in range(length_out)]
110
+
111
+
112
+ def _reverse_unit(u: Any) -> Any:
113
+ """Reverse a unit vector (R ``rev(unit_vector)``).
114
+
115
+ Parameters
116
+ ----------
117
+ u : grid_py.Unit
118
+ Source unit vector.
119
+
120
+ Returns
121
+ -------
122
+ grid_py.Unit
123
+ The unit vector with its elements in reverse order.
124
+ """
125
+ n = len(u)
126
+ if n <= 1:
127
+ return u
128
+ return unit_c(*[u[i] for i in range(n - 1, -1, -1)])
129
+
130
+
131
+ class StripNested(StripThemed):
132
+ """Strip that merges adjacent same-label strips into spanning strips.
133
+
134
+ Subclass of :class:`ggh4x.strip_themed.StripThemed`. See the module
135
+ docstring for the RLE-merge algorithm.
136
+
137
+ Attributes
138
+ ----------
139
+ params : dict
140
+ Adds ``bleed`` (bool) to the base ``clip`` / ``size`` params.
141
+ """
142
+
143
+ _class_name = "StripNested"
144
+
145
+ params: Dict[str, Any] = {"bleed": False}
146
+
147
+ def assemble_strip(
148
+ self,
149
+ labels: np.ndarray,
150
+ position: str,
151
+ elements: Dict[str, Any],
152
+ params: Dict[str, Any],
153
+ layout: pd.DataFrame,
154
+ ) -> pd.DataFrame:
155
+ """RLE-merge adjacent equal labels per layer, then draw and finish.
156
+
157
+ Port of R ``StripNested$assemble_strip`` (``strip_nested.R:110-168``).
158
+
159
+ Parameters
160
+ ----------
161
+ labels : numpy.ndarray
162
+ 2-D object array of label strings (rows = panels, cols = layers).
163
+ position : str
164
+ Strip side (``"top"`` / ``"bottom"`` / ``"left"`` / ``"right"``).
165
+ elements : dict
166
+ Resolved element bundle from :meth:`setup_elements`.
167
+ params : dict
168
+ Strip params (``size``, ``clip``, ``bleed``).
169
+ layout : pandas.DataFrame
170
+ The sliced layout for this side (carries ``PANEL`` / ``ROW`` /
171
+ ``COL``).
172
+
173
+ Returns
174
+ -------
175
+ pandas.DataFrame
176
+ Placement frame with a ``layer`` column and per-group merged
177
+ gtables (from :meth:`finish_strip`).
178
+ """
179
+ nlayers = labels.shape[1]
180
+ # Monolayer fast path: nothing to merge -> base assembly.
181
+ if nlayers == 1:
182
+ return ggproto_parent(Strip, self).assemble_strip(
183
+ labels, position, elements, params, layout
184
+ )
185
+
186
+ aes = "x" if position in ("top", "bottom") else "y"
187
+ bleed = self.params["bleed"]
188
+
189
+ # Right strip reverses label columns (inside-out) -- note the base
190
+ # build_strip already reversed; R reverses again here so it operates on
191
+ # outermost-first columns.
192
+ if position == "right":
193
+ labels = labels[:, ::-1]
194
+
195
+ if aes == "x":
196
+ sepvar = "ROW"
197
+ order_keys = ["ROW", "COL"]
198
+ else:
199
+ sepvar = "COL"
200
+ order_keys = ["COL", "ROW"]
201
+
202
+ # R order(a, b) -> stable lexicographic order.
203
+ order = np.lexsort(
204
+ tuple(np.asarray(layout[k].to_numpy()) for k in reversed(order_keys))
205
+ )
206
+ labels = labels[order, :]
207
+ layout = layout.iloc[order].reset_index(drop=True)
208
+
209
+ sep = [str(v) for v in layout[sepvar].tolist()]
210
+
211
+ nrow = labels.shape[0]
212
+ # Per-layer pasted key columns (R: tmp[] <- paste0(col, layout[[sepvar]])).
213
+ tmp_cols: List[List[str]] = []
214
+ for j in range(nlayers):
215
+ tmp_cols.append([str(labels[i, j]) + sep[i] for i in range(nrow)])
216
+
217
+ if not bleed:
218
+ # Paste each layer (from the 2nd) with all preceding pasted layers
219
+ # to prevent lower-layer bleeding across higher-layer boundaries.
220
+ cumulative = list(tmp_cols[0])
221
+ for j in range(1, nlayers):
222
+ cumulative = [cumulative[i] + tmp_cols[j][i] for i in range(nrow)]
223
+ tmp_cols[j] = list(cumulative)
224
+
225
+ # Run lengths per layer.
226
+ lens = [_rle_lengths(col) for col in tmp_cols]
227
+ flat_lens: List[int] = [length for col_lens in lens for length in col_lens]
228
+
229
+ ends: List[int] = []
230
+ for col_lens in lens:
231
+ acc = 0
232
+ for length in col_lens:
233
+ acc += length
234
+ ends.append(acc)
235
+ starts = [e - flat_lens[k] + 1 for k, e in enumerate(ends)]
236
+
237
+ panel = [int(p) for p in layout["PANEL"].tolist()]
238
+ # layer id per run, repeated by the number of runs in each layer.
239
+ layer = [j + 1 for j, col_lens in enumerate(lens) for _ in col_lens]
240
+
241
+ new_layout = pd.DataFrame(
242
+ {
243
+ "t": [panel[s - 1] for s in starts],
244
+ "b": [panel[e - 1] for e in ends],
245
+ "l": [panel[s - 1] for s in starts],
246
+ "r": [panel[e - 1] for e in ends],
247
+ "layer": layer,
248
+ }
249
+ )
250
+ index = list(new_layout["layer"])
251
+ # labels[cbind(starts, index)] -- matrix index (1-based) -> the run-start
252
+ # row at its layer column.
253
+ merged = [labels[starts[k] - 1, index[k] - 1] for k in range(len(starts))]
254
+
255
+ elems = self.init_strip(elements, position, index)
256
+ strips = self.draw_labels(merged, elems, position, index, params["size"])
257
+
258
+ width = _recycle_unit_attr(strips.width, nlayers)
259
+ height = _recycle_unit_attr(strips.height, nlayers)
260
+
261
+ return self.finish_strip(
262
+ list(strips),
263
+ width,
264
+ height,
265
+ position,
266
+ new_layout,
267
+ (new_layout.shape[0], nlayers),
268
+ params["clip"],
269
+ )
270
+
271
+ def finish_strip(
272
+ self,
273
+ strip: Sequence[Any],
274
+ width: Any,
275
+ height: Any,
276
+ position: str,
277
+ layout: pd.DataFrame,
278
+ dim: Any,
279
+ clip: str = "inherit",
280
+ ) -> pd.DataFrame:
281
+ """Build one multi-cell gtable per merged group (or delegate).
282
+
283
+ Port of R ``StripNested$finish_strip`` (``strip_nested.R:170-200``).
284
+ When the *layout* lacks a ``layer`` column (the monolayer fast path
285
+ produced a plain placement frame) this delegates to the *self-less*
286
+ :func:`Strip.finish_strip`. Otherwise each merged strip is placed into a
287
+ bare :class:`gtable_py.Gtable` at its layer index (``t=index, l=1`` for
288
+ horizontal; ``t=1, l=index`` for vertical); for ``"bottom"`` / ``"right"``
289
+ the index and the width/height vectors are reversed so the outermost
290
+ layer sits furthest from the panel.
291
+
292
+ Parameters
293
+ ----------
294
+ strip : sequence
295
+ The merged label grobs (one per run).
296
+ width, height : grid_py.Unit
297
+ Per-layer width / height unit vectors (length ``nlayers``).
298
+ position : str
299
+ Strip side.
300
+ layout : pandas.DataFrame
301
+ The redefined layout (carries ``layer`` when merged).
302
+ dim : tuple of int
303
+ ``(nrow, nlayers)``.
304
+ clip : str, default ``"inherit"``
305
+ Clip setting.
306
+
307
+ Returns
308
+ -------
309
+ pandas.DataFrame
310
+ The *layout* with an attached object ``grobs`` column.
311
+ """
312
+ # No 'layer' column -> delegate to the self-less base finish_strip.
313
+ if "layer" not in layout.columns:
314
+ return Strip.finish_strip(
315
+ strip, width, height, position, layout, dim, clip
316
+ )
317
+
318
+ strip = list(strip)
319
+ empty_strips = len(strip) == 0 or all(_is_zero_grob(g) for g in strip)
320
+
321
+ out_grobs: List[Any] = strip
322
+ if not empty_strips:
323
+ index = [int(v) for v in layout["layer"]]
324
+ ncols = int(dim[1])
325
+ if position in ("bottom", "right"):
326
+ index = [ncols - i + 1 for i in index]
327
+ width = _reverse_unit(width)
328
+ height = _reverse_unit(height)
329
+ out_grobs = []
330
+ if position in ("top", "bottom"):
331
+ # gt = gtable(widths = width[1], heights = height)
332
+ for g, i in zip(strip, index):
333
+ gt = Gtable(widths=_unit_slice(width, 0), heights=height)
334
+ gt = gtable_add_grob(gt, g, t=i, l=1, clip=clip)
335
+ out_grobs.append(gt)
336
+ else:
337
+ for g, i in zip(strip, index):
338
+ gt = Gtable(widths=width, heights=_unit_slice(height, 0))
339
+ gt = gtable_add_grob(gt, g, t=1, l=i, clip=clip)
340
+ out_grobs.append(gt)
341
+
342
+ result = layout.copy()
343
+ result["grobs"] = pd.Series(out_grobs, index=result.index, dtype=object)
344
+ return result
345
+
346
+
347
+ # ---------------------------------------------------------------------------
348
+ # Unit helpers
349
+ # ---------------------------------------------------------------------------
350
+ def _recycle_unit_attr(u: Any, length_out: int) -> Any:
351
+ """Recycle the ``width``/``height`` unit attribute to ``nlayers``.
352
+
353
+ Port of R ``rep(attr(strips, 'width'), length.out = nlayers)``.
354
+
355
+ Parameters
356
+ ----------
357
+ u : grid_py.Unit or None
358
+ The unit vector attribute from ``draw_labels``.
359
+ length_out : int
360
+ Target length (number of layers).
361
+
362
+ Returns
363
+ -------
364
+ grid_py.Unit or None
365
+ The recycled unit vector (``None`` passes through).
366
+ """
367
+ if u is None:
368
+ return None
369
+ return unit_rep(u, length_out=length_out)
370
+
371
+
372
+ def _unit_slice(u: Any, i: int) -> Any:
373
+ """Return the single-element unit at position *i* (R ``u[i+1]``).
374
+
375
+ Parameters
376
+ ----------
377
+ u : grid_py.Unit
378
+ Source unit vector.
379
+ i : int
380
+ 0-based index.
381
+
382
+ Returns
383
+ -------
384
+ grid_py.Unit
385
+ The unit at *i* (or *u* itself when it is already scalar).
386
+ """
387
+ n = len(u)
388
+ if n <= 1:
389
+ return u
390
+ return u[i % n]
391
+
392
+
393
+ # R's ``StripNested`` ggproto instance used as the parent of every clone.
394
+ _STRIP_NESTED_SINGLETON: "StripNested" = StripNested()
395
+
396
+
397
+ def strip_nested(
398
+ clip: str = "inherit",
399
+ size: str = "constant",
400
+ bleed: bool = False,
401
+ text_x: Any = None,
402
+ text_y: Any = None,
403
+ background_x: Any = None,
404
+ background_y: Any = None,
405
+ by_layer_x: bool = False,
406
+ by_layer_y: bool = False,
407
+ ) -> StripNested:
408
+ """Create a nested (label-merging) strip.
409
+
410
+ Port of R ``strip_nested()`` (``strip_nested.R:66-97``).
411
+
412
+ Parameters
413
+ ----------
414
+ clip : str, default ``"inherit"``
415
+ Whether labels are clipped to background boxes.
416
+ size : str, default ``"constant"``
417
+ Whether strip margins across layers are ``"constant"`` or ``"variable"``.
418
+ bleed : bool, default ``False``
419
+ Whether lower-layer strips may merge across higher-layer boundaries.
420
+ text_x, text_y, background_x, background_y : list or element or None
421
+ Per-strip themed elements (see :func:`ggh4x.strip_themed.strip_themed`).
422
+ by_layer_x, by_layer_y : bool, default ``False``
423
+ Map elements to layers (``True``) or strips (``False``).
424
+
425
+ Returns
426
+ -------
427
+ StripNested
428
+ A ``StripNested`` ggproto instance usable in ggh4x facets.
429
+ """
430
+ params = {
431
+ "clip": arg_match0(clip, ["on", "off", "inherit"], arg_name="clip"),
432
+ "size": arg_match0(size, ["constant", "variable"], arg_name="size"),
433
+ "bleed": bool(bleed),
434
+ }
435
+ given_elements = {
436
+ "text_x": validate_element_list(text_x, "element_text"),
437
+ "text_y": validate_element_list(text_y, "element_text"),
438
+ "background_x": validate_element_list(background_x, "element_rect"),
439
+ "background_y": validate_element_list(background_y, "element_rect"),
440
+ "by_layer_x": bool(by_layer_x),
441
+ "by_layer_y": bool(by_layer_y),
442
+ }
443
+ return ggproto(
444
+ None,
445
+ _STRIP_NESTED_SINGLETON,
446
+ params=params,
447
+ given_elements=given_elements,
448
+ )