plotnine 0.14.5__py3-none-any.whl → 0.15.0a2__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.
- plotnine/__init__.py +31 -37
- plotnine/_mpl/gridspec.py +265 -0
- plotnine/_mpl/layout_manager/__init__.py +6 -0
- plotnine/_mpl/layout_manager/_engine.py +87 -0
- plotnine/_mpl/layout_manager/_layout_items.py +957 -0
- plotnine/_mpl/layout_manager/_layout_tree.py +905 -0
- plotnine/_mpl/layout_manager/_spaces.py +1154 -0
- plotnine/_mpl/patches.py +70 -34
- plotnine/_mpl/text.py +159 -37
- plotnine/_mpl/utils.py +78 -10
- plotnine/_utils/__init__.py +35 -9
- plotnine/_utils/dev.py +45 -27
- plotnine/_utils/yippie.py +115 -0
- plotnine/animation.py +1 -1
- plotnine/coords/coord.py +3 -3
- plotnine/coords/coord_trans.py +1 -1
- plotnine/data/__init__.py +43 -8
- plotnine/data/anscombe-quartet.csv +45 -0
- plotnine/doctools.py +2 -2
- plotnine/facets/facet.py +34 -43
- plotnine/facets/facet_grid.py +14 -6
- plotnine/facets/facet_wrap.py +3 -5
- plotnine/facets/strips.py +20 -33
- plotnine/geoms/annotate.py +3 -3
- plotnine/geoms/annotation_logticks.py +2 -0
- plotnine/geoms/annotation_stripes.py +2 -0
- plotnine/geoms/geom.py +3 -3
- plotnine/geoms/geom_bar.py +10 -2
- plotnine/geoms/geom_col.py +6 -0
- plotnine/geoms/geom_crossbar.py +2 -3
- plotnine/geoms/geom_path.py +2 -2
- plotnine/geoms/geom_violin.py +24 -7
- plotnine/ggplot.py +95 -66
- plotnine/guides/guide.py +19 -20
- plotnine/guides/guide_colorbar.py +6 -6
- plotnine/guides/guide_legend.py +15 -16
- plotnine/guides/guides.py +8 -8
- plotnine/helpers.py +49 -0
- plotnine/iapi.py +33 -7
- plotnine/labels.py +8 -3
- plotnine/layer.py +4 -4
- plotnine/mapping/_env.py +2 -2
- plotnine/mapping/_eval_environment.py +85 -0
- plotnine/mapping/aes.py +14 -30
- plotnine/mapping/evaluation.py +7 -65
- plotnine/options.py +14 -7
- plotnine/plot_composition/__init__.py +10 -0
- plotnine/plot_composition/_compose.py +462 -0
- plotnine/plot_composition/_plotspec.py +50 -0
- plotnine/plot_composition/_spacer.py +32 -0
- plotnine/positions/position_dodge.py +1 -1
- plotnine/positions/position_dodge2.py +1 -1
- plotnine/positions/position_stack.py +1 -2
- plotnine/qplot.py +1 -2
- plotnine/scales/__init__.py +0 -6
- plotnine/scales/limits.py +7 -7
- plotnine/scales/scale.py +4 -4
- plotnine/scales/scale_continuous.py +2 -1
- plotnine/scales/scale_identity.py +10 -2
- plotnine/scales/scale_manual.py +6 -2
- plotnine/stats/binning.py +5 -2
- plotnine/stats/smoothers.py +3 -5
- plotnine/stats/stat.py +3 -3
- plotnine/stats/stat_bindot.py +1 -3
- plotnine/stats/stat_density.py +2 -2
- plotnine/stats/stat_qq_line.py +1 -1
- plotnine/stats/stat_sina.py +34 -1
- plotnine/themes/elements/__init__.py +3 -0
- plotnine/themes/elements/element_text.py +35 -24
- plotnine/themes/elements/margin.py +137 -61
- plotnine/themes/targets.py +3 -1
- plotnine/themes/theme.py +21 -7
- plotnine/themes/theme_538.py +0 -1
- plotnine/themes/theme_bw.py +0 -1
- plotnine/themes/theme_dark.py +0 -1
- plotnine/themes/theme_gray.py +32 -34
- plotnine/themes/theme_light.py +1 -1
- plotnine/themes/theme_matplotlib.py +28 -31
- plotnine/themes/theme_seaborn.py +36 -36
- plotnine/themes/theme_void.py +25 -27
- plotnine/themes/theme_xkcd.py +0 -1
- plotnine/themes/themeable.py +369 -169
- plotnine/typing.py +3 -3
- plotnine/watermark.py +3 -3
- {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info}/METADATA +8 -5
- {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info}/RECORD +89 -78
- {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info}/WHEEL +1 -1
- plotnine/_mpl/_plot_side_space.py +0 -888
- plotnine/_mpl/_plotnine_tight_layout.py +0 -293
- plotnine/_mpl/layout_engine.py +0 -110
- {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info/licenses}/LICENSE +0 -0
- {plotnine-0.14.5.dist-info → plotnine-0.15.0a2.dist-info}/top_level.txt +0 -0
plotnine/_mpl/patches.py
CHANGED
|
@@ -4,12 +4,11 @@ from typing import TYPE_CHECKING
|
|
|
4
4
|
|
|
5
5
|
from matplotlib import artist
|
|
6
6
|
from matplotlib.patches import FancyBboxPatch, Rectangle
|
|
7
|
-
from matplotlib.
|
|
8
|
-
from matplotlib.transforms import Affine2D
|
|
7
|
+
from matplotlib.transforms import Bbox
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
from matplotlib.backend_bases import RendererBase
|
|
9
|
+
from plotnine._mpl.utils import rel_position
|
|
12
10
|
|
|
11
|
+
if TYPE_CHECKING:
|
|
13
12
|
from plotnine.typing import StripPosition
|
|
14
13
|
|
|
15
14
|
from .text import StripText
|
|
@@ -26,51 +25,88 @@ class StripTextPatch(FancyBboxPatch):
|
|
|
26
25
|
Strip text background box
|
|
27
26
|
"""
|
|
28
27
|
|
|
29
|
-
# The text artists that is wrapped by this box
|
|
30
28
|
text: StripText
|
|
29
|
+
"""
|
|
30
|
+
The text artists that is wrapped by this box
|
|
31
|
+
"""
|
|
32
|
+
|
|
31
33
|
position: StripPosition
|
|
32
|
-
|
|
34
|
+
"""
|
|
35
|
+
The position of the strip_text associated with this patch
|
|
36
|
+
"""
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
expand: float = 1
|
|
39
|
+
"""
|
|
40
|
+
Factor by which to expand the thickness of this patch.
|
|
36
41
|
|
|
42
|
+
This value is used by the layout manager to increase the breadth
|
|
43
|
+
of the narrower strip_backgrounds.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, text: StripText):
|
|
37
47
|
super().__init__(
|
|
38
|
-
|
|
48
|
+
# The position, width and height are determine in
|
|
49
|
+
# .get_window_extent.
|
|
50
|
+
(0, 0),
|
|
51
|
+
width=1,
|
|
52
|
+
height=1,
|
|
53
|
+
boxstyle="square, pad=0",
|
|
54
|
+
clip_on=False,
|
|
55
|
+
zorder=2.2,
|
|
39
56
|
)
|
|
40
57
|
|
|
41
58
|
self.text = text
|
|
42
59
|
self.position = text.draw_info.position
|
|
43
60
|
|
|
44
|
-
def
|
|
61
|
+
def get_window_extent(self, renderer=None):
|
|
45
62
|
"""
|
|
46
|
-
|
|
63
|
+
Location & dimensions of the box in display coordinates
|
|
47
64
|
"""
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
self.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
info = self.text.draw_info
|
|
66
|
+
m = info.margin
|
|
67
|
+
|
|
68
|
+
# bboxes in display space
|
|
69
|
+
text_bbox = self.text.get_window_extent(renderer)
|
|
70
|
+
ax_bbox = info.ax.bbox.frozen()
|
|
71
|
+
|
|
72
|
+
# line height in display space
|
|
73
|
+
line_height = self.text._line_height(renderer)
|
|
74
|
+
|
|
75
|
+
# Convert the bottom left coordinates of the patch from
|
|
76
|
+
# transAxes to display space. We are not justifying the patch
|
|
77
|
+
# within the axes so we use 0 for the lengths, this gives us
|
|
78
|
+
# a patch that starts at the edge of the axes and not one that
|
|
79
|
+
# ends at the edge
|
|
80
|
+
x0 = rel_position(info.bg_x, 0, ax_bbox.x0, ax_bbox.x1)
|
|
81
|
+
y0 = rel_position(info.bg_y, 0, ax_bbox.y0, ax_bbox.y1)
|
|
82
|
+
|
|
83
|
+
# info.bg_width and info.bg_height are in axes space
|
|
84
|
+
# so they are a scaling factor
|
|
85
|
+
if info.position == "top":
|
|
86
|
+
width = ax_bbox.width * info.bg_width
|
|
87
|
+
height = text_bbox.height + ((m.b + m.t) * line_height)
|
|
88
|
+
height *= self.expand
|
|
89
|
+
y0 += height * info.strip_align
|
|
90
|
+
else:
|
|
91
|
+
height = ax_bbox.height * info.bg_height
|
|
92
|
+
width = text_bbox.width + ((m.l + m.r) * line_height)
|
|
93
|
+
width *= self.expand
|
|
94
|
+
x0 += width * info.strip_align
|
|
95
|
+
|
|
96
|
+
return Bbox.from_bounds(x0, y0, width, height)
|
|
66
97
|
|
|
67
|
-
|
|
98
|
+
@artist.allow_rasterization
|
|
99
|
+
def draw(self, renderer):
|
|
68
100
|
"""
|
|
69
|
-
|
|
101
|
+
Draw patch
|
|
70
102
|
"""
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
103
|
+
# The geometry of the patch is determined by its rectangular bounds,
|
|
104
|
+
# this is also its "window_extent". As the extent value is in
|
|
105
|
+
# display units, we don't need a transform.
|
|
106
|
+
bbox = self.get_window_extent(renderer)
|
|
107
|
+
self.set_bounds(bbox.bounds)
|
|
108
|
+
self.set_transform(None)
|
|
109
|
+
return super().draw(renderer)
|
|
74
110
|
|
|
75
111
|
|
|
76
112
|
class InsideStrokedRectangle(Rectangle):
|
plotnine/_mpl/text.py
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
4
5
|
|
|
6
|
+
from matplotlib import artist
|
|
5
7
|
from matplotlib.text import Text
|
|
6
8
|
|
|
9
|
+
from plotnine._utils import ha_as_float, va_as_float
|
|
10
|
+
|
|
7
11
|
from .patches import StripTextPatch
|
|
8
|
-
from .utils import bbox_in_axes_space
|
|
12
|
+
from .utils import bbox_in_axes_space, rel_position
|
|
9
13
|
|
|
10
|
-
if
|
|
14
|
+
if TYPE_CHECKING:
|
|
11
15
|
from matplotlib.backend_bases import RendererBase
|
|
12
16
|
|
|
13
17
|
from plotnine.iapi import strip_draw_info
|
|
@@ -23,56 +27,174 @@ class StripText(Text):
|
|
|
23
27
|
|
|
24
28
|
def __init__(self, info: strip_draw_info):
|
|
25
29
|
kwargs = {
|
|
26
|
-
"ha": info.ha,
|
|
27
|
-
"va": info.va,
|
|
28
30
|
"rotation": info.rotation,
|
|
29
31
|
"transform": info.ax.transAxes,
|
|
30
32
|
"clip_on": False,
|
|
31
33
|
"zorder": 3.3,
|
|
34
|
+
# Since the text can be rotated, it is simpler to anchor it at
|
|
35
|
+
# the center, align it then do the rotation. Vertically,
|
|
36
|
+
# center_baseline places the text in the visual center, but
|
|
37
|
+
# only if it is one line. For multiline text, we are better
|
|
38
|
+
# off with plain center.
|
|
39
|
+
"ha": "center",
|
|
40
|
+
"va": "center_baseline" if info.is_oneline else "center",
|
|
41
|
+
"rotation_mode": "anchor",
|
|
32
42
|
}
|
|
33
43
|
|
|
34
|
-
super().__init__(
|
|
35
|
-
info.x,
|
|
36
|
-
info.y,
|
|
37
|
-
info.label,
|
|
38
|
-
**kwargs,
|
|
39
|
-
)
|
|
44
|
+
super().__init__(0, 0, info.label, **kwargs)
|
|
40
45
|
self.draw_info = info
|
|
41
46
|
self.patch = StripTextPatch(self)
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
# self.set_horizontalalignment("center")
|
|
49
|
+
# self.set_verticalalignment(
|
|
50
|
+
# "center_baseline" if info.is_oneline else "center"
|
|
51
|
+
# )
|
|
52
|
+
# self.set_rotation_mode("anchor")
|
|
53
|
+
|
|
54
|
+
# TODO: This should really be part of the unit conversions in the
|
|
55
|
+
# margin class.
|
|
56
|
+
@lru_cache(2)
|
|
57
|
+
def _line_height(self, renderer) -> float:
|
|
58
|
+
"""
|
|
59
|
+
The line height in display space of the text on the canvas
|
|
60
|
+
"""
|
|
61
|
+
# Text string, (width, height), x, y
|
|
62
|
+
parts: list[tuple[str, tuple[float, float], float, float]]
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
# matplotlib.Text._get_layout is a private API and we cannot
|
|
66
|
+
# tell how using it may fail in the future.
|
|
67
|
+
_, parts, _ = self._get_layout(renderer) # pyright: ignore[reportAttributeAccessIssue]
|
|
68
|
+
except Exception:
|
|
69
|
+
from warnings import warn
|
|
70
|
+
|
|
71
|
+
from plotnine.exceptions import PlotnineWarning
|
|
72
|
+
|
|
73
|
+
# The canvas height is nearly always bigger than the stated
|
|
74
|
+
# fontsize. 1.36 is a good multiplication factor obtained by
|
|
75
|
+
# some rough exploration
|
|
76
|
+
f = 1.36
|
|
77
|
+
size = self.get_fontsize()
|
|
78
|
+
height = round(size * f) if isinstance(size, int) else 14
|
|
79
|
+
warn(
|
|
80
|
+
f"Could not calculate line height for {self.get_text()}. "
|
|
81
|
+
"Using an estimate, please let us know about this at "
|
|
82
|
+
"https://github.com/has2k1/plotnine/issues",
|
|
83
|
+
PlotnineWarning,
|
|
84
|
+
)
|
|
85
|
+
else:
|
|
86
|
+
# The the text has multiple lines, we use the maximum height
|
|
87
|
+
# of anyone single line.
|
|
88
|
+
height = max([p[1][1] for p in parts])
|
|
89
|
+
|
|
90
|
+
return height
|
|
91
|
+
|
|
92
|
+
def _set_position(self, renderer):
|
|
93
|
+
"""
|
|
94
|
+
Set the postion of the text within the strip_background
|
|
95
|
+
"""
|
|
96
|
+
# We have to two premises that depend on each other:
|
|
97
|
+
#
|
|
98
|
+
# 1. The breadth of the strip_background grows to accomodate
|
|
99
|
+
# the strip_text.
|
|
100
|
+
# 2. The strip_text is justified within the strip_background.
|
|
101
|
+
#
|
|
102
|
+
# From these we note that the strip_background does not need the
|
|
103
|
+
# position of the strip_text, but it needs its size. Therefore
|
|
104
|
+
# we implement StripTextPatch.get_window_extent can use
|
|
105
|
+
# StripText.get_window_extent, peeking only at the size.
|
|
106
|
+
#
|
|
107
|
+
# And we implement StripText._set_position_* to use
|
|
108
|
+
# StripTextPatch.get_window_extent and make the calculations in
|
|
109
|
+
# both methods independent.
|
|
110
|
+
if self.draw_info.position == "top":
|
|
111
|
+
self._set_position_top(renderer)
|
|
112
|
+
else: # "right"
|
|
113
|
+
self._set_position_right(renderer)
|
|
114
|
+
|
|
115
|
+
def _set_position_top(self, renderer):
|
|
116
|
+
"""
|
|
117
|
+
Set position of the text within the top strip_background
|
|
118
|
+
"""
|
|
119
|
+
info = self.draw_info
|
|
120
|
+
ha, va, ax, m = info.ha, info.va, info.ax, info.margin
|
|
121
|
+
|
|
122
|
+
rel_x, rel_y = ha_as_float(ha), va_as_float(va)
|
|
123
|
+
patch_bbox = bbox_in_axes_space(self.patch, ax, renderer)
|
|
124
|
+
text_bbox = bbox_in_axes_space(self, ax, renderer)
|
|
125
|
+
|
|
126
|
+
# line_height and margins in axes space
|
|
127
|
+
line_height = self._line_height(renderer) / ax.bbox.height
|
|
128
|
+
|
|
129
|
+
x = (
|
|
130
|
+
# Justify horizontally within the strip_background
|
|
131
|
+
rel_position(
|
|
132
|
+
rel_x,
|
|
133
|
+
text_bbox.width + (line_height * (m.l + m.r)),
|
|
134
|
+
patch_bbox.x0,
|
|
135
|
+
patch_bbox.x1,
|
|
136
|
+
)
|
|
137
|
+
+ (m.l * line_height)
|
|
138
|
+
+ text_bbox.width / 2
|
|
139
|
+
)
|
|
140
|
+
# Setting the y position based on the bounding box is wrong
|
|
141
|
+
y = (
|
|
142
|
+
rel_position(
|
|
143
|
+
rel_y,
|
|
144
|
+
text_bbox.height,
|
|
145
|
+
patch_bbox.y0 + m.b * line_height,
|
|
146
|
+
patch_bbox.y1 - m.t * line_height,
|
|
147
|
+
)
|
|
148
|
+
+ text_bbox.height / 2
|
|
149
|
+
)
|
|
150
|
+
self.set_position((x, y))
|
|
46
151
|
|
|
152
|
+
def _set_position_right(self, renderer):
|
|
153
|
+
"""
|
|
154
|
+
Set position of the text within the bottom strip_background
|
|
155
|
+
"""
|
|
47
156
|
info = self.draw_info
|
|
48
|
-
|
|
49
|
-
self.patch.update_position_size(renderer)
|
|
157
|
+
ha, va, ax, m = info.ha, info.va, info.ax, info.margin
|
|
50
158
|
|
|
51
|
-
#
|
|
52
|
-
patch_bbox = bbox_in_axes_space(self.patch,
|
|
159
|
+
# bboxes in axes space
|
|
160
|
+
patch_bbox = bbox_in_axes_space(self.patch, ax, renderer)
|
|
161
|
+
text_bbox = bbox_in_axes_space(self, ax, renderer)
|
|
53
162
|
|
|
54
|
-
#
|
|
55
|
-
|
|
56
|
-
l, b, w, h = info.x, info.y, info.box_width, patch_bbox.height
|
|
57
|
-
b = b + patch_bbox.height * info.strip_align
|
|
58
|
-
else: # "right"
|
|
59
|
-
l, b, w, h = info.x, info.y, patch_bbox.width, info.box_height
|
|
60
|
-
l = l + patch_bbox.width * info.strip_align
|
|
163
|
+
# line_height in axes space
|
|
164
|
+
line_height = self._line_height(renderer) / ax.bbox.width
|
|
61
165
|
|
|
62
|
-
|
|
63
|
-
self.patch.set_transform(info.ax.transAxes)
|
|
64
|
-
self.patch.set_mutation_scale(0)
|
|
166
|
+
rel_x, rel_y = ha_as_float(ha), va_as_float(va)
|
|
65
167
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
168
|
+
x = (
|
|
169
|
+
rel_position(
|
|
170
|
+
rel_x,
|
|
171
|
+
text_bbox.width,
|
|
172
|
+
patch_bbox.x0 + m.l * line_height,
|
|
173
|
+
patch_bbox.x1 - m.r * line_height,
|
|
174
|
+
)
|
|
175
|
+
+ text_bbox.width / 2
|
|
176
|
+
)
|
|
177
|
+
y = (
|
|
178
|
+
# Justify vertically within the strip_background
|
|
179
|
+
rel_position(
|
|
180
|
+
rel_y,
|
|
181
|
+
text_bbox.height + ((m.b + m.t) * line_height),
|
|
182
|
+
patch_bbox.y0,
|
|
183
|
+
patch_bbox.y1,
|
|
184
|
+
)
|
|
185
|
+
+ (m.b * line_height)
|
|
186
|
+
+ text_bbox.height / 2
|
|
187
|
+
)
|
|
188
|
+
self.set_position((x, y))
|
|
69
189
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
190
|
+
@artist.allow_rasterization
|
|
191
|
+
def draw(self, renderer: RendererBase):
|
|
192
|
+
"""
|
|
193
|
+
Draw text along with the patch
|
|
194
|
+
"""
|
|
195
|
+
if not self.get_visible():
|
|
196
|
+
return
|
|
75
197
|
|
|
76
|
-
|
|
198
|
+
self._set_position(renderer)
|
|
77
199
|
self.patch.draw(renderer)
|
|
78
200
|
return super().draw(renderer)
|
plotnine/_mpl/utils.py
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
from matplotlib.transforms import Affine2D, Bbox
|
|
6
6
|
|
|
7
7
|
from .transforms import ZEROS_BBOX
|
|
8
8
|
|
|
9
|
-
if
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
10
|
from matplotlib.artist import Artist
|
|
11
11
|
from matplotlib.axes import Axes
|
|
12
12
|
from matplotlib.backend_bases import RendererBase
|
|
13
13
|
from matplotlib.figure import Figure
|
|
14
|
+
from matplotlib.gridspec import SubplotSpec
|
|
14
15
|
from matplotlib.transforms import Transform
|
|
15
16
|
|
|
17
|
+
from .gridspec import p9GridSpec
|
|
18
|
+
|
|
16
19
|
|
|
17
20
|
def bbox_in_figure_space(
|
|
18
21
|
artist: Artist, fig: Figure, renderer: RendererBase
|
|
@@ -51,28 +54,93 @@ def pts_in_figure_space(fig: Figure, pts: float) -> float:
|
|
|
51
54
|
return fig.transFigure.inverted().transform([0, pts])[1]
|
|
52
55
|
|
|
53
56
|
|
|
54
|
-
def get_transPanels(fig: Figure) -> Transform:
|
|
57
|
+
def get_transPanels(fig: Figure, gs: p9GridSpec) -> Transform:
|
|
55
58
|
"""
|
|
56
59
|
Coordinate system of the Panels (facets) area
|
|
57
60
|
|
|
58
61
|
(0, 0) is the bottom-left of the bottom-left panel and
|
|
59
62
|
(1, 1) is the top right of the top-right panel.
|
|
60
63
|
|
|
61
|
-
The
|
|
62
|
-
i.e.
|
|
64
|
+
The gridspec parameters must be set before calling this function.
|
|
65
|
+
i.e. gs.update have been called.
|
|
63
66
|
"""
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
params = fig.subplotpars
|
|
67
|
+
# The position of the panels area in figure coordinates
|
|
68
|
+
params = gs.get_subplot_params(fig)
|
|
67
69
|
|
|
68
70
|
# Figure width & height in display coordinates
|
|
69
71
|
W, H = fig.bbox.width, fig.bbox.height
|
|
70
72
|
|
|
71
73
|
# 1. The panels occupy space that is smaller than the figure
|
|
72
74
|
# 2. That space is contained within the figure
|
|
73
|
-
# We create a transform that
|
|
74
|
-
# (but order matters), and use to transform transFigure
|
|
75
|
+
# We create a transform that represents these separable aspects
|
|
76
|
+
# (but order matters), and use it to transform transFigure
|
|
75
77
|
sx, sy = params.right - params.left, params.top - params.bottom
|
|
76
78
|
dx, dy = params.left * W, params.bottom * H
|
|
77
79
|
transFiguretoPanels = Affine2D().scale(sx, sy).translate(dx, dy)
|
|
78
80
|
return fig.transFigure + transFiguretoPanels
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def rel_position(rel: float, length: float, low: float, high: float) -> float:
|
|
84
|
+
"""
|
|
85
|
+
Relatively position an object of a given length between two position
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
rel:
|
|
90
|
+
Relative position of the object between the limits.
|
|
91
|
+
length:
|
|
92
|
+
Length of the object
|
|
93
|
+
low:
|
|
94
|
+
Lower limit position
|
|
95
|
+
high:
|
|
96
|
+
Upper limit position
|
|
97
|
+
"""
|
|
98
|
+
return low * (1 - rel) + (high - length) * rel
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_subplotspecs(axs: list[Axes]) -> list[SubplotSpec]:
|
|
102
|
+
"""
|
|
103
|
+
Return the SubplotSpecs of the given axes
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
axs:
|
|
108
|
+
List of axes
|
|
109
|
+
|
|
110
|
+
Notes
|
|
111
|
+
-----
|
|
112
|
+
This functions returns the innermost subplotspec and it expects
|
|
113
|
+
every axes object to have one.
|
|
114
|
+
"""
|
|
115
|
+
subplotspecs: list[SubplotSpec] = []
|
|
116
|
+
for ax in axs:
|
|
117
|
+
if not (subplotspec := ax.get_subplotspec()):
|
|
118
|
+
raise ValueError("Axes has no suplotspec")
|
|
119
|
+
subplotspecs.append(subplotspec)
|
|
120
|
+
return subplotspecs
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def draw_gridspec(gs: p9GridSpec, color="black", **kwargs):
|
|
124
|
+
"""
|
|
125
|
+
A debug function to draw a rectangle around the gridspec
|
|
126
|
+
"""
|
|
127
|
+
draw_bbox(gs.bbox_relative, gs.figure, color, **kwargs)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def draw_bbox(bbox, figure, color="black", **kwargs):
|
|
131
|
+
"""
|
|
132
|
+
A debug function to draw a rectangle around a bounding bbox
|
|
133
|
+
"""
|
|
134
|
+
from matplotlib.patches import Rectangle
|
|
135
|
+
|
|
136
|
+
figure.add_artist(
|
|
137
|
+
Rectangle(
|
|
138
|
+
xy=bbox.p0,
|
|
139
|
+
width=bbox.width,
|
|
140
|
+
height=bbox.height,
|
|
141
|
+
edgecolor=color,
|
|
142
|
+
fill="facecolor" in kwargs,
|
|
143
|
+
clip_on=False,
|
|
144
|
+
**kwargs,
|
|
145
|
+
)
|
|
146
|
+
)
|
plotnine/_utils/__init__.py
CHANGED
|
@@ -35,8 +35,10 @@ if TYPE_CHECKING:
|
|
|
35
35
|
DataLike,
|
|
36
36
|
FloatArray,
|
|
37
37
|
FloatArrayLike,
|
|
38
|
+
HorizontalJustification,
|
|
38
39
|
IntArray,
|
|
39
|
-
|
|
40
|
+
Side,
|
|
41
|
+
VerticalJustification,
|
|
40
42
|
)
|
|
41
43
|
|
|
42
44
|
T = TypeVar("T")
|
|
@@ -299,7 +301,7 @@ def ninteraction(df: pd.DataFrame, drop: bool = False) -> list[int]:
|
|
|
299
301
|
return _id_var(df[df.columns[0]], drop)
|
|
300
302
|
|
|
301
303
|
# Calculate individual ids
|
|
302
|
-
ids = df.apply(_id_var, axis=0)
|
|
304
|
+
ids = df.apply(_id_var, axis=0, drop=drop)
|
|
303
305
|
ids = ids.reindex(columns=list(reversed(ids.columns)))
|
|
304
306
|
|
|
305
307
|
# Calculate dimensions
|
|
@@ -310,8 +312,8 @@ def ninteraction(df: pd.DataFrame, drop: bool = False) -> list[int]:
|
|
|
310
312
|
|
|
311
313
|
combs = np.array(np.hstack([1, np.cumprod(ndistinct[:-1])]))
|
|
312
314
|
mat = np.array(ids)
|
|
313
|
-
|
|
314
|
-
res = np.array(
|
|
315
|
+
_res = (mat - 1) @ combs.T + 1
|
|
316
|
+
res: list[int] = np.array(_res).flatten().tolist()
|
|
315
317
|
|
|
316
318
|
if drop:
|
|
317
319
|
return _id_var(res, drop)
|
|
@@ -511,7 +513,7 @@ def remove_missing(
|
|
|
511
513
|
if finite:
|
|
512
514
|
lst = [np.inf, -np.inf]
|
|
513
515
|
to_replace = {v: lst for v in vars}
|
|
514
|
-
data.replace(to_replace, np.nan, inplace=True)
|
|
516
|
+
data.replace(to_replace, np.nan, inplace=True) # pyright: ignore[reportArgumentType,reportCallIssue]
|
|
515
517
|
txt = "non-finite"
|
|
516
518
|
else:
|
|
517
519
|
txt = "missing"
|
|
@@ -604,7 +606,7 @@ def to_rgba(
|
|
|
604
606
|
return c
|
|
605
607
|
|
|
606
608
|
if is_iterable(colors):
|
|
607
|
-
colors = cast(Sequence[
|
|
609
|
+
colors = cast("Sequence[ColorType]", colors)
|
|
608
610
|
|
|
609
611
|
if all(no_color(c) for c in colors):
|
|
610
612
|
return "none"
|
|
@@ -1227,11 +1229,11 @@ def default_field(default: T) -> T:
|
|
|
1227
1229
|
return field(default_factory=lambda: deepcopy(default))
|
|
1228
1230
|
|
|
1229
1231
|
|
|
1230
|
-
def get_opposite_side(s:
|
|
1232
|
+
def get_opposite_side(s: Side) -> Side:
|
|
1231
1233
|
"""
|
|
1232
1234
|
Return the opposite side
|
|
1233
1235
|
"""
|
|
1234
|
-
lookup: dict[
|
|
1236
|
+
lookup: dict[Side, Side] = {
|
|
1235
1237
|
"right": "left",
|
|
1236
1238
|
"left": "right",
|
|
1237
1239
|
"top": "bottom",
|
|
@@ -1241,7 +1243,7 @@ def get_opposite_side(s: SidePosition) -> SidePosition:
|
|
|
1241
1243
|
|
|
1242
1244
|
|
|
1243
1245
|
def ensure_xy_location(
|
|
1244
|
-
loc:
|
|
1246
|
+
loc: Side | Literal["center"] | float | tuple[float, float],
|
|
1245
1247
|
) -> tuple[float, float]:
|
|
1246
1248
|
"""
|
|
1247
1249
|
Convert input into (x, y) location
|
|
@@ -1264,3 +1266,27 @@ def ensure_xy_location(
|
|
|
1264
1266
|
if isinstance(h, (int, float)) and isinstance(v, (int, float)):
|
|
1265
1267
|
return (h, v)
|
|
1266
1268
|
raise ValueError(f"Cannot make a location from '{loc}'")
|
|
1269
|
+
|
|
1270
|
+
|
|
1271
|
+
def ha_as_float(ha: HorizontalJustification | float) -> float:
|
|
1272
|
+
"""
|
|
1273
|
+
Return horizontal alignment as a float
|
|
1274
|
+
"""
|
|
1275
|
+
lookup = {"left": 0.0, "center": 0.5, "right": 1.0}
|
|
1276
|
+
return lookup[ha] if isinstance(ha, str) else ha
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
def va_as_float(va: VerticalJustification | float) -> float:
|
|
1280
|
+
"""
|
|
1281
|
+
Return vertical alignment as a float
|
|
1282
|
+
"""
|
|
1283
|
+
lookup = {
|
|
1284
|
+
"top": 1.0,
|
|
1285
|
+
"center": 0.5,
|
|
1286
|
+
"bottom": 0.0,
|
|
1287
|
+
# baseline and center_baseline are valid for texts but we do
|
|
1288
|
+
# not handle them accurately
|
|
1289
|
+
"baseline": 0.5,
|
|
1290
|
+
"center_baseline": 0.5,
|
|
1291
|
+
}
|
|
1292
|
+
return lookup[va] if isinstance(va, str) else va
|
plotnine/_utils/dev.py
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Optional
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
def get_plotnine_all(use_clipboard=True) -> Optional[str]:
|
|
4
|
+
def get_plotnine_all() -> str:
|
|
7
5
|
"""
|
|
8
6
|
Generate package level * (star) imports for plotnine
|
|
9
|
-
|
|
10
|
-
The contents of __all__ in plotnine/__init__.py
|
|
11
7
|
"""
|
|
12
8
|
from importlib import import_module
|
|
13
9
|
|
|
@@ -28,32 +24,54 @@ def get_plotnine_all(use_clipboard=True) -> Optional[str]:
|
|
|
28
24
|
"watermark",
|
|
29
25
|
)
|
|
30
26
|
|
|
31
|
-
def get_all_from_module(name
|
|
27
|
+
def get_all_from_module(name):
|
|
32
28
|
"""
|
|
33
29
|
Module level imports
|
|
34
30
|
"""
|
|
35
31
|
qname = f"plotnine.{name}"
|
|
36
32
|
m = import_module(qname)
|
|
37
|
-
fmt = ('"{}",' if quote else "{},").format
|
|
38
|
-
return "\n ".join(fmt(x) for x in sorted(m.__all__))
|
|
39
33
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
34
|
+
return sorted(m.__all__)
|
|
35
|
+
|
|
36
|
+
imports = []
|
|
37
|
+
all_funcs = []
|
|
38
|
+
|
|
39
|
+
for name in modules:
|
|
40
|
+
funcs = get_all_from_module(name)
|
|
41
|
+
import_funcs = "\n ".join(f"{x}," for x in funcs)
|
|
42
|
+
imports.append(f"from .{name} import (\n {import_funcs}\n)")
|
|
43
|
+
all_funcs.extend(funcs)
|
|
44
|
+
|
|
45
|
+
all_funcs = [f' "{x}",' for x in sorted(all_funcs)]
|
|
46
|
+
|
|
47
|
+
_imports = "\n".join(imports)
|
|
48
|
+
_all = "__all__ = (\n" + "\n".join(all_funcs) + "\n)"
|
|
49
|
+
|
|
50
|
+
return f"{_imports}\n\n{_all}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_init_py() -> str:
|
|
54
|
+
"""
|
|
55
|
+
Generate plotnine/__init__.py
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
preamble: str = """# Do not edit this file by hand.
|
|
59
|
+
#
|
|
60
|
+
# Generate it using:
|
|
61
|
+
#
|
|
62
|
+
# $ python -c 'from plotnine._utils import dev; print(dev.get_init_py())'
|
|
63
|
+
|
|
64
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
__version__ = version("plotnine")
|
|
68
|
+
except PackageNotFoundError:
|
|
69
|
+
# package is not installed
|
|
70
|
+
pass
|
|
71
|
+
finally:
|
|
72
|
+
del version
|
|
73
|
+
del PackageNotFoundError
|
|
74
|
+
|
|
75
|
+
"""
|
|
56
76
|
|
|
57
|
-
|
|
58
|
-
else:
|
|
59
|
-
return content
|
|
77
|
+
return preamble + get_plotnine_all()
|