ggh4x-python 0.3.1.9000__tar.gz
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.
- ggh4x_python-0.3.1.9000/.gitignore +35 -0
- ggh4x_python-0.3.1.9000/LICENSE +3 -0
- ggh4x_python-0.3.1.9000/PKG-INFO +40 -0
- ggh4x_python-0.3.1.9000/README.md +11 -0
- ggh4x_python-0.3.1.9000/ggh4x/__init__.py +140 -0
- ggh4x_python-0.3.1.9000/ggh4x/_aimed_text_grob.py +432 -0
- ggh4x_python-0.3.1.9000/ggh4x/_borrowed_ggplot2.py +273 -0
- ggh4x_python-0.3.1.9000/ggh4x/_cli.py +84 -0
- ggh4x_python-0.3.1.9000/ggh4x/_datasets.py +106 -0
- ggh4x_python-0.3.1.9000/ggh4x/_download.py +111 -0
- ggh4x_python-0.3.1.9000/ggh4x/_facet_helpers.py +313 -0
- ggh4x_python-0.3.1.9000/ggh4x/_facet_utils.py +649 -0
- ggh4x_python-0.3.1.9000/ggh4x/_gap_grobs.py +606 -0
- ggh4x_python-0.3.1.9000/ggh4x/_registry.py +10 -0
- ggh4x_python-0.3.1.9000/ggh4x/_rlang.py +93 -0
- ggh4x_python-0.3.1.9000/ggh4x/_utils.py +150 -0
- ggh4x_python-0.3.1.9000/ggh4x/_vctrs.py +233 -0
- ggh4x_python-0.3.1.9000/ggh4x/conveniences.py +601 -0
- ggh4x_python-0.3.1.9000/ggh4x/coord_axes_inside.py +380 -0
- ggh4x_python-0.3.1.9000/ggh4x/element_part_rect.py +545 -0
- ggh4x_python-0.3.1.9000/ggh4x/facet_grid2.py +1018 -0
- ggh4x_python-0.3.1.9000/ggh4x/facet_manual.py +901 -0
- ggh4x_python-0.3.1.9000/ggh4x/facet_nested.py +776 -0
- ggh4x_python-0.3.1.9000/ggh4x/facet_nested_wrap.py +193 -0
- ggh4x_python-0.3.1.9000/ggh4x/facet_wrap2.py +896 -0
- ggh4x_python-0.3.1.9000/ggh4x/geom_box.py +536 -0
- ggh4x_python-0.3.1.9000/ggh4x/geom_outline_point.py +444 -0
- ggh4x_python-0.3.1.9000/ggh4x/geom_pointpath.py +259 -0
- ggh4x_python-0.3.1.9000/ggh4x/geom_polygonraster.py +252 -0
- ggh4x_python-0.3.1.9000/ggh4x/geom_rectrug.py +489 -0
- ggh4x_python-0.3.1.9000/ggh4x/geom_text_aimed.py +279 -0
- ggh4x_python-0.3.1.9000/ggh4x/guide_stringlegend.py +354 -0
- ggh4x_python-0.3.1.9000/ggh4x/help_secondary.py +549 -0
- ggh4x_python-0.3.1.9000/ggh4x/multiscale/__init__.py +51 -0
- ggh4x_python-0.3.1.9000/ggh4x/multiscale/_multiscale_add.py +207 -0
- ggh4x_python-0.3.1.9000/ggh4x/multiscale/scale_listed.py +167 -0
- ggh4x_python-0.3.1.9000/ggh4x/multiscale/scale_manual.py +478 -0
- ggh4x_python-0.3.1.9000/ggh4x/multiscale/scale_multi.py +393 -0
- ggh4x_python-0.3.1.9000/ggh4x/panel_scales/__init__.py +58 -0
- ggh4x_python-0.3.1.9000/ggh4x/panel_scales/at_panel.py +115 -0
- ggh4x_python-0.3.1.9000/ggh4x/panel_scales/facetted_pos_scales.py +647 -0
- ggh4x_python-0.3.1.9000/ggh4x/panel_scales/force_panelsize.py +411 -0
- ggh4x_python-0.3.1.9000/ggh4x/panel_scales/scale_facet.py +222 -0
- ggh4x_python-0.3.1.9000/ggh4x/position_disjoint_ranges.py +229 -0
- ggh4x_python-0.3.1.9000/ggh4x/position_lineartrans.py +242 -0
- ggh4x_python-0.3.1.9000/ggh4x/py.typed +0 -0
- ggh4x_python-0.3.1.9000/ggh4x/resources/faithful.csv +273 -0
- ggh4x_python-0.3.1.9000/ggh4x/resources/iris.csv +151 -0
- ggh4x_python-0.3.1.9000/ggh4x/resources/mtcars.csv +33 -0
- ggh4x_python-0.3.1.9000/ggh4x/resources/pressure.csv +20 -0
- ggh4x_python-0.3.1.9000/ggh4x/resources/volcano.csv +87 -0
- ggh4x_python-0.3.1.9000/ggh4x/save.py +255 -0
- ggh4x_python-0.3.1.9000/ggh4x/stat_difference.py +388 -0
- ggh4x_python-0.3.1.9000/ggh4x/stat_funxy.py +436 -0
- ggh4x_python-0.3.1.9000/ggh4x/stat_rle.py +290 -0
- ggh4x_python-0.3.1.9000/ggh4x/stat_rollingkernel.py +369 -0
- ggh4x_python-0.3.1.9000/ggh4x/stat_theodensity.py +681 -0
- ggh4x_python-0.3.1.9000/ggh4x/strip_nested.py +448 -0
- ggh4x_python-0.3.1.9000/ggh4x/strip_split.py +687 -0
- ggh4x_python-0.3.1.9000/ggh4x/strip_tag.py +636 -0
- ggh4x_python-0.3.1.9000/ggh4x/strip_themed.py +232 -0
- ggh4x_python-0.3.1.9000/ggh4x/strip_vanilla.py +1464 -0
- ggh4x_python-0.3.1.9000/ggh4x/themes.py +31 -0
- ggh4x_python-0.3.1.9000/ggh4x/themes_ggh4x.py +67 -0
- ggh4x_python-0.3.1.9000/pyproject.toml +39 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
.eggs/
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv/
|
|
11
|
+
.tox/
|
|
12
|
+
|
|
13
|
+
# Compiled extensions
|
|
14
|
+
*.so
|
|
15
|
+
*.pyd
|
|
16
|
+
*.dylib
|
|
17
|
+
|
|
18
|
+
# IDE
|
|
19
|
+
.idea/
|
|
20
|
+
.vscode/
|
|
21
|
+
*.swp
|
|
22
|
+
|
|
23
|
+
# OS
|
|
24
|
+
.DS_Store
|
|
25
|
+
Thumbs.db
|
|
26
|
+
.coverage
|
|
27
|
+
|
|
28
|
+
# Docs build
|
|
29
|
+
site/
|
|
30
|
+
.cache/
|
|
31
|
+
.ipynb_checkpoints/
|
|
32
|
+
validation/
|
|
33
|
+
|
|
34
|
+
# Unrelated R export script (kept on disk, not tracked)
|
|
35
|
+
scripts/
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ggh4x-python
|
|
3
|
+
Version: 0.3.1.9000
|
|
4
|
+
Summary: Python port of the R ggh4x package
|
|
5
|
+
Project-URL: Homepage, https://github.com/Bio-Babel/ggh4x-python
|
|
6
|
+
Project-URL: Issues, https://github.com/Bio-Babel/ggh4x-python/issues
|
|
7
|
+
Author-email: Jeffery Liu <jeffliu.lucky@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Requires-Dist: ggplot2-python>=4.0.2
|
|
12
|
+
Requires-Dist: gtable-python>=0.3.6
|
|
13
|
+
Requires-Dist: numpy>=1.24
|
|
14
|
+
Requires-Dist: pandas>=2.0
|
|
15
|
+
Requires-Dist: patchwork-python>=1.3.2
|
|
16
|
+
Requires-Dist: rgrid-python>=4.5.3
|
|
17
|
+
Requires-Dist: scales-python>=1.4.0
|
|
18
|
+
Requires-Dist: scipy>=1.10
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
22
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
23
|
+
Provides-Extra: docs
|
|
24
|
+
Requires-Dist: mkdocs; extra == 'docs'
|
|
25
|
+
Requires-Dist: mkdocs-jupyter; extra == 'docs'
|
|
26
|
+
Requires-Dist: mkdocs-material; extra == 'docs'
|
|
27
|
+
Requires-Dist: mkdocstrings[python]; extra == 'docs'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# ggh4x-python
|
|
31
|
+
|
|
32
|
+
[](https://pypi.org/project/ggh4x-python/)
|
|
33
|
+
|
|
34
|
+
Python version of the R **ggh4x** package.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install ggh4x-python
|
|
40
|
+
```
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""ggh4x-python — Python port of the R ggh4x package.
|
|
2
|
+
|
|
3
|
+
A ggplot2-extension toolkit ported onto ``ggplot2_py``: extended facets
|
|
4
|
+
(``facet_grid2`` / ``facet_wrap2`` / ``facet_nested`` / ``facet_manual``) with
|
|
5
|
+
pluggable strips, per-panel scales and forced panel sizes, plus extra stats,
|
|
6
|
+
geoms, multi-scales, positions/coords, and the ``guide_stringlegend`` legend.
|
|
7
|
+
|
|
8
|
+
Importing this package wires every subsystem's registration side-effects
|
|
9
|
+
(theme elements, the ``+``-add handlers for ``MultiScale`` / ``ForcedSize`` /
|
|
10
|
+
``FacettedPosScales`` / ``ScaleFacet``, and the ``element_grob`` dispatch for
|
|
11
|
+
``ElementPartRect``).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
__version__ = "0.3.1.9000"
|
|
17
|
+
__r_commit__ = "63c91b7"
|
|
18
|
+
|
|
19
|
+
# --- Stats -------------------------------------------------------------------
|
|
20
|
+
from .stat_theodensity import StatTheoDensity, stat_theodensity
|
|
21
|
+
from .stat_difference import StatDifference, stat_difference
|
|
22
|
+
from .stat_rle import StatRle, stat_rle
|
|
23
|
+
from .stat_rollingkernel import StatRollingkernel, stat_rollingkernel
|
|
24
|
+
from .stat_funxy import StatFunxy, stat_centroid, stat_funxy, stat_midpoint
|
|
25
|
+
|
|
26
|
+
# --- Geoms -------------------------------------------------------------------
|
|
27
|
+
from .geom_outline_point import GeomOutlinePoint, geom_outline_point
|
|
28
|
+
from .geom_box import GeomBox, geom_box
|
|
29
|
+
from .geom_pointpath import GeomPointPath, GeomPointpath, geom_pointpath
|
|
30
|
+
from .geom_text_aimed import GeomTextAimed, geom_text_aimed
|
|
31
|
+
from .geom_polygonraster import GeomPolygonRaster, geom_polygonraster
|
|
32
|
+
from .geom_rectrug import (
|
|
33
|
+
GeomRectMargin,
|
|
34
|
+
GeomTileMargin,
|
|
35
|
+
geom_rectmargin,
|
|
36
|
+
geom_tilemargin,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# --- Helpers / utils / elements ----------------------------------------------
|
|
40
|
+
from .conveniences import (
|
|
41
|
+
center_limits,
|
|
42
|
+
distribute_args,
|
|
43
|
+
elem_list_rect,
|
|
44
|
+
elem_list_text,
|
|
45
|
+
sep_discrete,
|
|
46
|
+
weave_factors,
|
|
47
|
+
)
|
|
48
|
+
from .save import save_plot
|
|
49
|
+
from .help_secondary import help_secondary
|
|
50
|
+
from .element_part_rect import ElementPartRect, element_part_rect
|
|
51
|
+
from .themes_ggh4x import ggh4x_theme_elements
|
|
52
|
+
|
|
53
|
+
# --- Strips ------------------------------------------------------------------
|
|
54
|
+
from .strip_vanilla import Strip, resolve_strip, strip_vanilla
|
|
55
|
+
from .strip_themed import StripThemed, strip_themed
|
|
56
|
+
from .strip_nested import StripNested, strip_nested
|
|
57
|
+
from .strip_split import StripSplit, strip_split
|
|
58
|
+
from .strip_tag import StripTag, strip_tag
|
|
59
|
+
|
|
60
|
+
# --- Facets ------------------------------------------------------------------
|
|
61
|
+
from .facet_grid2 import FacetGrid2, facet_grid2
|
|
62
|
+
from .facet_wrap2 import FacetWrap2, facet_wrap2
|
|
63
|
+
from .facet_nested import FacetNested, facet_nested
|
|
64
|
+
from .facet_nested_wrap import FacetNestedWrap, facet_nested_wrap
|
|
65
|
+
from .facet_manual import FacetManual, facet_manual
|
|
66
|
+
|
|
67
|
+
# --- Panel sizing / per-panel scales -----------------------------------------
|
|
68
|
+
from .panel_scales import (
|
|
69
|
+
at_panel,
|
|
70
|
+
facetted_pos_scales,
|
|
71
|
+
force_panelsizes,
|
|
72
|
+
scale_x_facet,
|
|
73
|
+
scale_y_facet,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# --- Coord / Position --------------------------------------------------------
|
|
77
|
+
from .coord_axes_inside import CoordAxesInside, coord_axes_inside
|
|
78
|
+
from .position_lineartrans import PositionLinearTrans, position_lineartrans
|
|
79
|
+
from .position_disjoint_ranges import (
|
|
80
|
+
PositionDisjointRanges,
|
|
81
|
+
position_disjoint_ranges,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# --- Multi / listed / manual scales ------------------------------------------
|
|
85
|
+
from .multiscale import (
|
|
86
|
+
scale_color_multi,
|
|
87
|
+
scale_colour_multi,
|
|
88
|
+
scale_fill_multi,
|
|
89
|
+
scale_listed,
|
|
90
|
+
scale_x_manual,
|
|
91
|
+
scale_y_manual,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# --- Live guide --------------------------------------------------------------
|
|
95
|
+
from .guide_stringlegend import GuideStringlegend, guide_stringlegend
|
|
96
|
+
|
|
97
|
+
__all__ = [
|
|
98
|
+
"__version__",
|
|
99
|
+
# stats
|
|
100
|
+
"stat_theodensity", "StatTheoDensity",
|
|
101
|
+
"stat_difference", "StatDifference",
|
|
102
|
+
"stat_rle", "StatRle",
|
|
103
|
+
"stat_rollingkernel", "StatRollingkernel",
|
|
104
|
+
"stat_funxy", "StatFunxy", "stat_centroid", "stat_midpoint",
|
|
105
|
+
# geoms
|
|
106
|
+
"geom_outline_point", "GeomOutlinePoint",
|
|
107
|
+
"geom_box", "GeomBox",
|
|
108
|
+
"geom_pointpath", "GeomPointPath", "GeomPointpath",
|
|
109
|
+
"geom_text_aimed", "GeomTextAimed",
|
|
110
|
+
"geom_polygonraster", "GeomPolygonRaster",
|
|
111
|
+
"geom_rectmargin", "geom_tilemargin", "GeomRectMargin", "GeomTileMargin",
|
|
112
|
+
# helpers / elements
|
|
113
|
+
"distribute_args", "elem_list_text", "elem_list_rect", "weave_factors",
|
|
114
|
+
"center_limits", "sep_discrete", "save_plot", "help_secondary",
|
|
115
|
+
"element_part_rect", "ElementPartRect", "ggh4x_theme_elements",
|
|
116
|
+
# strips
|
|
117
|
+
"Strip", "strip_vanilla", "resolve_strip",
|
|
118
|
+
"StripThemed", "strip_themed",
|
|
119
|
+
"StripNested", "strip_nested",
|
|
120
|
+
"StripSplit", "strip_split",
|
|
121
|
+
"StripTag", "strip_tag",
|
|
122
|
+
# facets
|
|
123
|
+
"facet_grid2", "FacetGrid2",
|
|
124
|
+
"facet_wrap2", "FacetWrap2",
|
|
125
|
+
"facet_nested", "FacetNested",
|
|
126
|
+
"facet_nested_wrap", "FacetNestedWrap",
|
|
127
|
+
"facet_manual", "FacetManual",
|
|
128
|
+
# panel sizing / per-panel scales
|
|
129
|
+
"force_panelsizes", "facetted_pos_scales",
|
|
130
|
+
"scale_x_facet", "scale_y_facet", "at_panel",
|
|
131
|
+
# coord / position
|
|
132
|
+
"coord_axes_inside", "CoordAxesInside",
|
|
133
|
+
"position_lineartrans", "PositionLinearTrans",
|
|
134
|
+
"position_disjoint_ranges", "PositionDisjointRanges",
|
|
135
|
+
# multi / listed / manual scales
|
|
136
|
+
"scale_colour_multi", "scale_color_multi", "scale_fill_multi",
|
|
137
|
+
"scale_listed", "scale_x_manual", "scale_y_manual",
|
|
138
|
+
# guide
|
|
139
|
+
"guide_stringlegend", "GuideStringlegend",
|
|
140
|
+
]
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"""Aimed-text grob (port of the grid layer of ggh4x ``geom_text_aimed.R``).
|
|
2
|
+
|
|
3
|
+
This module ports ``aimed_textGrob()`` and its ``makeContent.aimed_text`` S3
|
|
4
|
+
method, plus the ggplot2-internal ``compute_just`` helper that
|
|
5
|
+
``GeomTextAimed`` relies on.
|
|
6
|
+
|
|
7
|
+
R source: ``ggh4x/R/geom_text_aimed.R`` (``aimed_textGrob``,
|
|
8
|
+
``makeContent.aimed_text``) and ggplot2-internal ``compute_just`` / ``just_dir``.
|
|
9
|
+
|
|
10
|
+
Notes
|
|
11
|
+
-----
|
|
12
|
+
* R's ``makeContent.aimed_text`` *reclasses* the grob to a ``"text"`` grob in
|
|
13
|
+
place and returns it. :mod:`grid_py`'s ``text`` renderer reads a scalar
|
|
14
|
+
``rot``/``hjust``/``vjust`` per call, so per-row angle/justification cannot
|
|
15
|
+
travel on a single text grob. :class:`AimedTextGrob` therefore subclasses
|
|
16
|
+
:class:`grid_py.GTree`: its :meth:`make_content` performs the absolute-unit
|
|
17
|
+
angle computation (after the panel viewport is active) and rebuilds its
|
|
18
|
+
children as one scalar :func:`grid_py.text_grob` per label. This is
|
|
19
|
+
behaviourally identical to R while staying renderable.
|
|
20
|
+
* Angles are computed in millimetres (``convert_x``/``convert_y``) so they are
|
|
21
|
+
invariant under resizing, matching R's design intent.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import Any, List, Optional, Sequence, Union
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
|
|
30
|
+
from grid_py import (
|
|
31
|
+
GList,
|
|
32
|
+
GTree,
|
|
33
|
+
Gpar,
|
|
34
|
+
Unit,
|
|
35
|
+
convert_x,
|
|
36
|
+
convert_y,
|
|
37
|
+
is_unit,
|
|
38
|
+
text_grob,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"compute_just",
|
|
43
|
+
"just_dir",
|
|
44
|
+
"aimed_text_grob",
|
|
45
|
+
"AimedTextGrob",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# compute_just (ggplot2 internal)
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
def just_dir(x: np.ndarray, tol: float = 0.001) -> np.ndarray:
|
|
53
|
+
"""Map relative positions to justification directions ``1``/``2``/``3``.
|
|
54
|
+
|
|
55
|
+
Port of ggplot2-internal ``just_dir``. ``1`` (left/bottom) for values
|
|
56
|
+
below ``0.5 - tol``, ``3`` (right/top) above ``0.5 + tol``, else ``2``
|
|
57
|
+
(centre/middle).
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
x : numpy.ndarray
|
|
62
|
+
Relative positions (typically in ``[0, 1]``).
|
|
63
|
+
tol : float, default ``0.001``
|
|
64
|
+
Half-width of the central dead zone.
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
numpy.ndarray
|
|
69
|
+
Integer direction codes (1-based, matching R).
|
|
70
|
+
"""
|
|
71
|
+
x = np.asarray(x, dtype="float64")
|
|
72
|
+
out = np.full(x.shape, 2, dtype=int)
|
|
73
|
+
out[x < 0.5 - tol] = 1
|
|
74
|
+
out[x > 0.5 + tol] = 3
|
|
75
|
+
return out
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def compute_just(
|
|
79
|
+
just: Union[str, Sequence[str], np.ndarray, Any],
|
|
80
|
+
a: Any = 0.5,
|
|
81
|
+
b: Any = None,
|
|
82
|
+
angle: Any = 0,
|
|
83
|
+
) -> Any:
|
|
84
|
+
"""Resolve character justifications to numeric ``hjust``/``vjust``.
|
|
85
|
+
|
|
86
|
+
Port of ggplot2-internal ``compute_just(just, a, b, angle)``. Plain
|
|
87
|
+
keywords (``"left"``/``"right"``/``"center"``/``"bottom"``/``"middle"``/
|
|
88
|
+
``"top"``) map to ``0``/``1``/``0.5``; ``"inward"``/``"outward"`` resolve
|
|
89
|
+
relative to ``a`` (and ``b`` after rotation), honouring ``angle``.
|
|
90
|
+
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
just : str or sequence of str or array
|
|
94
|
+
The justification specification. Non-character input is returned
|
|
95
|
+
unchanged (matching R's early return).
|
|
96
|
+
a : array-like, default ``0.5``
|
|
97
|
+
Primary position values (e.g. data ``x`` for ``hjust``).
|
|
98
|
+
b : array-like, optional
|
|
99
|
+
Secondary position values; defaults to *a* (used when rotation swaps
|
|
100
|
+
the relevant axis).
|
|
101
|
+
angle : array-like, default ``0``
|
|
102
|
+
Text rotation in degrees.
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
numpy.ndarray
|
|
107
|
+
Numeric justification values.
|
|
108
|
+
"""
|
|
109
|
+
# R: if (!is.character(just)) return(just)
|
|
110
|
+
arr = np.atleast_1d(np.asarray(just, dtype=object))
|
|
111
|
+
if not all(isinstance(v, str) for v in arr):
|
|
112
|
+
return just
|
|
113
|
+
|
|
114
|
+
just_chr = np.array([str(v) for v in arr], dtype=object)
|
|
115
|
+
a = np.atleast_1d(np.asarray(a, dtype="float64"))
|
|
116
|
+
if b is None:
|
|
117
|
+
b = a
|
|
118
|
+
b = np.atleast_1d(np.asarray(b, dtype="float64"))
|
|
119
|
+
|
|
120
|
+
has_io = np.array(["outward" in v or "inward" in v for v in just_chr])
|
|
121
|
+
if np.any(has_io):
|
|
122
|
+
ang = np.atleast_1d(np.asarray(angle, dtype="float64")) % 360
|
|
123
|
+
ang = np.where(ang > 180, ang - 360, ang)
|
|
124
|
+
ang = np.where(ang < -180, ang + 360, ang)
|
|
125
|
+
# Broadcast angle to the length of just.
|
|
126
|
+
ang = np.broadcast_to(ang, just_chr.shape).astype("float64").copy()
|
|
127
|
+
|
|
128
|
+
rotated_forward = has_io & (ang > 45) & (ang < 135)
|
|
129
|
+
rotated_backwards = has_io & (ang < -45) & (ang > -135)
|
|
130
|
+
# ab <- ifelse(rotated_forward | rotated_backwards, b, a)
|
|
131
|
+
a_b = np.broadcast_to(a, just_chr.shape).astype("float64").copy()
|
|
132
|
+
b_b = np.broadcast_to(b, just_chr.shape).astype("float64").copy()
|
|
133
|
+
ab = np.where(rotated_forward | rotated_backwards, b_b, a_b)
|
|
134
|
+
|
|
135
|
+
just_swap = rotated_backwards | (np.abs(ang) > 135)
|
|
136
|
+
|
|
137
|
+
inward = ((just_chr == "inward") & ~just_swap) | (
|
|
138
|
+
(just_chr == "outward") & just_swap
|
|
139
|
+
)
|
|
140
|
+
if np.any(inward):
|
|
141
|
+
dir_codes = just_dir(ab[inward])
|
|
142
|
+
mapping = np.array(["left", "middle", "right"], dtype=object)
|
|
143
|
+
just_chr[inward] = mapping[dir_codes - 1]
|
|
144
|
+
|
|
145
|
+
outward = ((just_chr == "outward") & ~just_swap) | (
|
|
146
|
+
(just_chr == "inward") & just_swap
|
|
147
|
+
)
|
|
148
|
+
if np.any(outward):
|
|
149
|
+
dir_codes = just_dir(ab[outward])
|
|
150
|
+
mapping = np.array(["right", "middle", "left"], dtype=object)
|
|
151
|
+
just_chr[outward] = mapping[dir_codes - 1]
|
|
152
|
+
|
|
153
|
+
lookup = {
|
|
154
|
+
"left": 0.0,
|
|
155
|
+
"center": 0.5,
|
|
156
|
+
"centre": 0.5,
|
|
157
|
+
"right": 1.0,
|
|
158
|
+
"bottom": 0.0,
|
|
159
|
+
"middle": 0.5,
|
|
160
|
+
"top": 1.0,
|
|
161
|
+
}
|
|
162
|
+
out = np.array([lookup.get(str(v), 0.5) for v in just_chr], dtype="float64")
|
|
163
|
+
return out
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ---------------------------------------------------------------------------
|
|
167
|
+
# aimed_text_grob constructor
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
def aimed_text_grob(
|
|
170
|
+
label: Any,
|
|
171
|
+
x: Any = 0.5,
|
|
172
|
+
y: Any = 0.5,
|
|
173
|
+
x0: Any = 0.0,
|
|
174
|
+
y0: Any = 0.0,
|
|
175
|
+
just: Any = "centre",
|
|
176
|
+
hjust: Any = None,
|
|
177
|
+
vjust: Any = None,
|
|
178
|
+
rot: Any = 0,
|
|
179
|
+
check_overlap: bool = False,
|
|
180
|
+
default_units: str = "npc",
|
|
181
|
+
name: Optional[str] = None,
|
|
182
|
+
gp: Optional[Gpar] = None,
|
|
183
|
+
vp: Optional[Any] = None,
|
|
184
|
+
flip_upsidedown: bool = True,
|
|
185
|
+
) -> "AimedTextGrob":
|
|
186
|
+
"""Construct an :class:`AimedTextGrob`.
|
|
187
|
+
|
|
188
|
+
Port of R ``aimed_textGrob()`` (``geom_text_aimed.R:161-183``). Bare
|
|
189
|
+
numeric coordinates are wrapped in *default_units*.
|
|
190
|
+
|
|
191
|
+
Parameters
|
|
192
|
+
----------
|
|
193
|
+
label : array-like
|
|
194
|
+
Text labels.
|
|
195
|
+
x, y : array-like or grid unit
|
|
196
|
+
Text anchor positions.
|
|
197
|
+
x0, y0 : array-like or grid unit
|
|
198
|
+
The aim-target positions.
|
|
199
|
+
just : str or sequence of str, default ``"centre"``
|
|
200
|
+
Default justification.
|
|
201
|
+
hjust, vjust : array-like, optional
|
|
202
|
+
Horizontal / vertical justification.
|
|
203
|
+
rot : array-like, default ``0``
|
|
204
|
+
Base rotation (the aim angle is added on top at draw time).
|
|
205
|
+
check_overlap : bool, default ``False``
|
|
206
|
+
Whether overlapping labels are suppressed.
|
|
207
|
+
default_units : str, default ``"npc"``
|
|
208
|
+
Units for bare numeric coordinates.
|
|
209
|
+
name : str, optional
|
|
210
|
+
Grob name.
|
|
211
|
+
gp : grid_py.Gpar, optional
|
|
212
|
+
Graphical parameters.
|
|
213
|
+
vp : Any, optional
|
|
214
|
+
Viewport.
|
|
215
|
+
flip_upsidedown : bool, default ``True``
|
|
216
|
+
If ``True``, labels rotated into ``(90, 270)`` are flipped 180 degrees
|
|
217
|
+
for readability (with mirrored ``hjust``).
|
|
218
|
+
|
|
219
|
+
Returns
|
|
220
|
+
-------
|
|
221
|
+
AimedTextGrob
|
|
222
|
+
The custom grob.
|
|
223
|
+
"""
|
|
224
|
+
x = x if is_unit(x) else Unit(np.atleast_1d(np.asarray(x, dtype="float64")), default_units)
|
|
225
|
+
y = y if is_unit(y) else Unit(np.atleast_1d(np.asarray(y, dtype="float64")), default_units)
|
|
226
|
+
x0 = x0 if is_unit(x0) else Unit(np.atleast_1d(np.asarray(x0, dtype="float64")), default_units)
|
|
227
|
+
y0 = y0 if is_unit(y0) else Unit(np.atleast_1d(np.asarray(y0, dtype="float64")), default_units)
|
|
228
|
+
|
|
229
|
+
return AimedTextGrob(
|
|
230
|
+
label=label,
|
|
231
|
+
x=x,
|
|
232
|
+
y=y,
|
|
233
|
+
x0=x0,
|
|
234
|
+
y0=y0,
|
|
235
|
+
just=just,
|
|
236
|
+
hjust=hjust,
|
|
237
|
+
vjust=vjust,
|
|
238
|
+
rot=rot,
|
|
239
|
+
check_overlap=check_overlap,
|
|
240
|
+
flip_upsidedown=flip_upsidedown,
|
|
241
|
+
name=name,
|
|
242
|
+
gp=gp,
|
|
243
|
+
vp=vp,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# ---------------------------------------------------------------------------
|
|
248
|
+
# AimedTextGrob
|
|
249
|
+
# ---------------------------------------------------------------------------
|
|
250
|
+
class AimedTextGrob(GTree):
|
|
251
|
+
"""Text grob that rotates each label towards a per-label aim point.
|
|
252
|
+
|
|
253
|
+
Subclass of :class:`grid_py.GTree`. Stored ``x``/``y``/``x0``/``y0`` are
|
|
254
|
+
grid units; :meth:`make_content` converts them to millimetres against the
|
|
255
|
+
live panel viewport, computes ``ang = atan2(y1 - y0, x1 - x0)``, adds it to
|
|
256
|
+
the base ``rot`` and rebuilds the children as one scalar
|
|
257
|
+
:func:`grid_py.text_grob` per label.
|
|
258
|
+
|
|
259
|
+
Port of the grob carrying ``cl = "aimed_text"`` and its S3 method
|
|
260
|
+
``makeContent.aimed_text`` (``geom_text_aimed.R:188-214``).
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
def __init__(
|
|
264
|
+
self,
|
|
265
|
+
label: Any,
|
|
266
|
+
x: Unit,
|
|
267
|
+
y: Unit,
|
|
268
|
+
x0: Unit,
|
|
269
|
+
y0: Unit,
|
|
270
|
+
just: Any = "centre",
|
|
271
|
+
hjust: Any = None,
|
|
272
|
+
vjust: Any = None,
|
|
273
|
+
rot: Any = 0,
|
|
274
|
+
check_overlap: bool = False,
|
|
275
|
+
flip_upsidedown: bool = True,
|
|
276
|
+
name: Optional[str] = None,
|
|
277
|
+
gp: Optional[Gpar] = None,
|
|
278
|
+
vp: Optional[Any] = None,
|
|
279
|
+
) -> None:
|
|
280
|
+
super().__init__(
|
|
281
|
+
children=None,
|
|
282
|
+
name=name,
|
|
283
|
+
gp=gp,
|
|
284
|
+
vp=vp,
|
|
285
|
+
_grid_class="aimed_text",
|
|
286
|
+
label=label,
|
|
287
|
+
x=x,
|
|
288
|
+
y=y,
|
|
289
|
+
x0=x0,
|
|
290
|
+
y0=y0,
|
|
291
|
+
just=just,
|
|
292
|
+
hjust=hjust,
|
|
293
|
+
vjust=vjust,
|
|
294
|
+
rot=rot,
|
|
295
|
+
check_overlap=check_overlap,
|
|
296
|
+
flip_upsidedown=flip_upsidedown,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
def make_content(self) -> "AimedTextGrob":
|
|
300
|
+
"""Compute aim angles in mm and rebuild the per-label text children.
|
|
301
|
+
|
|
302
|
+
Returns
|
|
303
|
+
-------
|
|
304
|
+
AimedTextGrob
|
|
305
|
+
``self``, with its children replaced by the rotated text grobs.
|
|
306
|
+
"""
|
|
307
|
+
x1 = np.atleast_1d(np.asarray(convert_x(self.x, "mm", valueOnly=True), dtype="float64"))
|
|
308
|
+
y1 = np.atleast_1d(np.asarray(convert_y(self.y, "mm", valueOnly=True), dtype="float64"))
|
|
309
|
+
x0 = np.atleast_1d(np.asarray(convert_x(self.x0, "mm", valueOnly=True), dtype="float64"))
|
|
310
|
+
y0 = np.atleast_1d(np.asarray(convert_y(self.y0, "mm", valueOnly=True), dtype="float64"))
|
|
311
|
+
|
|
312
|
+
rot, hjust, vjust = compute_aimed_angles(
|
|
313
|
+
x1, y1, x0, y0, self.rot, self.hjust, self.vjust, self.flip_upsidedown
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
labels = self.label
|
|
317
|
+
if isinstance(labels, str):
|
|
318
|
+
labels = [labels]
|
|
319
|
+
labels = list(labels)
|
|
320
|
+
n = len(labels)
|
|
321
|
+
|
|
322
|
+
# Native anchor positions for the text grobs (drawing happens in the
|
|
323
|
+
# panel vp, so the original native/npc x/y are reused directly).
|
|
324
|
+
xs = np.atleast_1d(np.asarray(convert_x(self.x, "native", valueOnly=True), dtype="float64"))
|
|
325
|
+
ys = np.atleast_1d(np.asarray(convert_y(self.y, "native", valueOnly=True), dtype="float64"))
|
|
326
|
+
|
|
327
|
+
gp = self.gp
|
|
328
|
+
children: List[Any] = []
|
|
329
|
+
for i in range(n):
|
|
330
|
+
gp_i = _subset_gpar(gp, i, n)
|
|
331
|
+
children.append(
|
|
332
|
+
text_grob(
|
|
333
|
+
label=str(labels[i]),
|
|
334
|
+
x=Unit(float(xs[i % len(xs)]), "native"),
|
|
335
|
+
y=Unit(float(ys[i % len(ys)]), "native"),
|
|
336
|
+
hjust=float(hjust[i % len(hjust)]),
|
|
337
|
+
vjust=float(vjust[i % len(vjust)]),
|
|
338
|
+
rot=float(rot[i % len(rot)]),
|
|
339
|
+
check_overlap=bool(self.check_overlap),
|
|
340
|
+
gp=gp_i,
|
|
341
|
+
name=f"aimed_text.{i}",
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
self.set_children(GList(*children))
|
|
346
|
+
return self
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def compute_aimed_angles(
|
|
350
|
+
x1: np.ndarray,
|
|
351
|
+
y1: np.ndarray,
|
|
352
|
+
x0: np.ndarray,
|
|
353
|
+
y0: np.ndarray,
|
|
354
|
+
rot: Any,
|
|
355
|
+
hjust: Any,
|
|
356
|
+
vjust: Any,
|
|
357
|
+
flip_upsidedown: bool,
|
|
358
|
+
) -> tuple:
|
|
359
|
+
"""Compute rotated text angles and (possibly mirrored) justifications.
|
|
360
|
+
|
|
361
|
+
Pure-math core of :meth:`AimedTextGrob.make_content`, split out for
|
|
362
|
+
verification. Port of ``makeContent.aimed_text``
|
|
363
|
+
(``geom_text_aimed.R:195-204``).
|
|
364
|
+
|
|
365
|
+
Parameters
|
|
366
|
+
----------
|
|
367
|
+
x1, y1 : numpy.ndarray
|
|
368
|
+
Text anchor positions (mm).
|
|
369
|
+
x0, y0 : numpy.ndarray
|
|
370
|
+
Aim-target positions (mm).
|
|
371
|
+
rot : array-like
|
|
372
|
+
Base rotation (degrees).
|
|
373
|
+
hjust, vjust : array-like
|
|
374
|
+
Justification values.
|
|
375
|
+
flip_upsidedown : bool
|
|
376
|
+
Whether to flip labels rotated into ``(90, 270)``.
|
|
377
|
+
|
|
378
|
+
Returns
|
|
379
|
+
-------
|
|
380
|
+
tuple of numpy.ndarray
|
|
381
|
+
``(rot, hjust, vjust)`` after the aim rotation (and optional flip).
|
|
382
|
+
"""
|
|
383
|
+
rot = np.atleast_1d(np.asarray(rot, dtype="float64"))
|
|
384
|
+
hjust = np.atleast_1d(np.asarray(hjust, dtype="float64"))
|
|
385
|
+
vjust = np.atleast_1d(np.asarray(vjust, dtype="float64"))
|
|
386
|
+
|
|
387
|
+
ang = np.arctan2(y1 - y0, x1 - x0)
|
|
388
|
+
rot = (rot + ang * (180.0 / np.pi)) % 360.0
|
|
389
|
+
|
|
390
|
+
if flip_upsidedown:
|
|
391
|
+
upsidedown = (rot > 90) & (rot < 270)
|
|
392
|
+
rot = np.where(upsidedown, rot + 180, rot) % 360.0
|
|
393
|
+
# hjust may be a scalar; broadcast before mirroring.
|
|
394
|
+
hjust = np.broadcast_to(hjust, upsidedown.shape).astype("float64").copy()
|
|
395
|
+
hjust = np.where(upsidedown, 1.0 - hjust, hjust)
|
|
396
|
+
|
|
397
|
+
return rot, hjust, vjust
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _subset_gpar(gp: Optional[Gpar], i: int, n: int) -> Optional[Gpar]:
|
|
401
|
+
"""Return the ``i``-th element of every length>1 :class:`grid_py.Gpar` field.
|
|
402
|
+
|
|
403
|
+
Parameters
|
|
404
|
+
----------
|
|
405
|
+
gp : grid_py.Gpar or None
|
|
406
|
+
Graphical parameters that may carry per-label vectors.
|
|
407
|
+
i : int
|
|
408
|
+
Row index.
|
|
409
|
+
n : int
|
|
410
|
+
Total number of labels.
|
|
411
|
+
|
|
412
|
+
Returns
|
|
413
|
+
-------
|
|
414
|
+
grid_py.Gpar or None
|
|
415
|
+
A per-label :class:`grid_py.Gpar`, or ``None`` if *gp* is ``None``.
|
|
416
|
+
"""
|
|
417
|
+
if gp is None:
|
|
418
|
+
return None
|
|
419
|
+
from ._gap_grobs import _gpar_to_dict
|
|
420
|
+
|
|
421
|
+
params = _gpar_to_dict(gp)
|
|
422
|
+
out = {}
|
|
423
|
+
for key, value in params.items():
|
|
424
|
+
if value is None:
|
|
425
|
+
out[key] = value
|
|
426
|
+
continue
|
|
427
|
+
arr = np.asarray(value)
|
|
428
|
+
if arr.ndim >= 1 and arr.shape[0] == n and n > 1:
|
|
429
|
+
out[key] = arr[i]
|
|
430
|
+
else:
|
|
431
|
+
out[key] = value
|
|
432
|
+
return Gpar(**out)
|