plotnine 0.15.0.dev3__py3-none-any.whl → 0.15.2__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 +2 -0
- plotnine/_mpl/layout_manager/_engine.py +1 -1
- plotnine/_mpl/layout_manager/_layout_items.py +126 -41
- plotnine/_mpl/layout_manager/_layout_tree.py +712 -314
- plotnine/_mpl/layout_manager/_spaces.py +305 -101
- plotnine/_mpl/patches.py +70 -34
- plotnine/_mpl/text.py +144 -63
- plotnine/_mpl/utils.py +1 -1
- plotnine/_utils/__init__.py +50 -107
- plotnine/_utils/context.py +78 -2
- plotnine/_utils/ipython.py +35 -51
- plotnine/_utils/quarto.py +26 -0
- plotnine/_utils/yippie.py +115 -0
- plotnine/composition/__init__.py +11 -0
- plotnine/composition/_beside.py +55 -0
- plotnine/composition/_compose.py +471 -0
- plotnine/composition/_plot_spacer.py +60 -0
- plotnine/composition/_stack.py +55 -0
- plotnine/coords/coord.py +3 -3
- plotnine/data/__init__.py +31 -0
- plotnine/data/anscombe-quartet.csv +45 -0
- plotnine/doctools.py +4 -4
- plotnine/facets/facet.py +4 -4
- plotnine/facets/strips.py +17 -28
- plotnine/geoms/annotate.py +13 -13
- plotnine/geoms/annotation_logticks.py +7 -8
- plotnine/geoms/annotation_stripes.py +6 -6
- plotnine/geoms/geom.py +60 -27
- plotnine/geoms/geom_abline.py +3 -2
- plotnine/geoms/geom_area.py +2 -2
- plotnine/geoms/geom_bar.py +1 -0
- plotnine/geoms/geom_bin_2d.py +6 -2
- plotnine/geoms/geom_blank.py +0 -3
- plotnine/geoms/geom_boxplot.py +8 -4
- plotnine/geoms/geom_col.py +2 -2
- plotnine/geoms/geom_count.py +6 -2
- plotnine/geoms/geom_crossbar.py +3 -3
- plotnine/geoms/geom_density_2d.py +6 -2
- plotnine/geoms/geom_dotplot.py +2 -2
- plotnine/geoms/geom_errorbar.py +2 -2
- plotnine/geoms/geom_errorbarh.py +2 -2
- plotnine/geoms/geom_histogram.py +1 -1
- plotnine/geoms/geom_hline.py +3 -2
- plotnine/geoms/geom_linerange.py +2 -2
- plotnine/geoms/geom_map.py +5 -5
- plotnine/geoms/geom_path.py +11 -12
- plotnine/geoms/geom_point.py +4 -5
- plotnine/geoms/geom_pointdensity.py +4 -0
- plotnine/geoms/geom_pointrange.py +3 -5
- plotnine/geoms/geom_polygon.py +2 -3
- plotnine/geoms/geom_qq.py +4 -0
- plotnine/geoms/geom_qq_line.py +4 -0
- plotnine/geoms/geom_quantile.py +4 -0
- plotnine/geoms/geom_raster.py +4 -5
- plotnine/geoms/geom_rect.py +3 -4
- plotnine/geoms/geom_ribbon.py +7 -7
- plotnine/geoms/geom_rug.py +1 -1
- plotnine/geoms/geom_segment.py +2 -2
- plotnine/geoms/geom_sina.py +3 -3
- plotnine/geoms/geom_smooth.py +7 -3
- plotnine/geoms/geom_step.py +2 -2
- plotnine/geoms/geom_text.py +2 -3
- plotnine/geoms/geom_violin.py +8 -5
- plotnine/geoms/geom_vline.py +3 -2
- plotnine/ggplot.py +64 -85
- plotnine/guides/guide.py +7 -10
- plotnine/guides/guide_colorbar.py +3 -3
- plotnine/guides/guide_legend.py +3 -3
- plotnine/guides/guides.py +6 -6
- plotnine/helpers.py +49 -0
- plotnine/iapi.py +28 -5
- plotnine/labels.py +3 -3
- plotnine/layer.py +36 -19
- plotnine/mapping/_atomic.py +178 -0
- plotnine/mapping/_env.py +13 -2
- plotnine/mapping/_eval_environment.py +1 -1
- plotnine/mapping/aes.py +85 -49
- plotnine/scales/__init__.py +2 -0
- plotnine/scales/limits.py +7 -7
- plotnine/scales/scale.py +3 -3
- plotnine/scales/scale_color.py +82 -18
- plotnine/scales/scale_continuous.py +6 -4
- plotnine/scales/scale_datetime.py +28 -14
- plotnine/scales/scale_discrete.py +1 -1
- plotnine/scales/scale_identity.py +21 -2
- plotnine/scales/scale_manual.py +8 -2
- plotnine/scales/scale_xy.py +2 -2
- plotnine/stats/binning.py +4 -1
- plotnine/stats/smoothers.py +23 -36
- plotnine/stats/stat.py +20 -32
- plotnine/stats/stat_bin.py +6 -5
- plotnine/stats/stat_bin_2d.py +11 -9
- plotnine/stats/stat_bindot.py +13 -16
- plotnine/stats/stat_boxplot.py +6 -6
- plotnine/stats/stat_count.py +6 -9
- plotnine/stats/stat_density.py +7 -10
- plotnine/stats/stat_density_2d.py +12 -8
- plotnine/stats/stat_ecdf.py +7 -6
- plotnine/stats/stat_ellipse.py +9 -6
- plotnine/stats/stat_function.py +10 -8
- plotnine/stats/stat_hull.py +6 -3
- plotnine/stats/stat_identity.py +5 -2
- plotnine/stats/stat_pointdensity.py +5 -7
- plotnine/stats/stat_qq.py +46 -20
- plotnine/stats/stat_qq_line.py +16 -11
- plotnine/stats/stat_quantile.py +15 -9
- plotnine/stats/stat_sina.py +13 -15
- plotnine/stats/stat_smooth.py +8 -10
- plotnine/stats/stat_sum.py +5 -2
- plotnine/stats/stat_summary.py +7 -10
- plotnine/stats/stat_summary_bin.py +11 -14
- plotnine/stats/stat_unique.py +5 -2
- plotnine/stats/stat_ydensity.py +8 -11
- plotnine/themes/elements/__init__.py +2 -1
- plotnine/themes/elements/element_line.py +17 -9
- plotnine/themes/elements/margin.py +64 -1
- plotnine/themes/theme.py +9 -1
- 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 +6 -5
- plotnine/themes/theme_light.py +1 -1
- plotnine/themes/theme_matplotlib.py +5 -5
- plotnine/themes/theme_seaborn.py +7 -4
- plotnine/themes/theme_void.py +9 -8
- plotnine/themes/theme_xkcd.py +0 -1
- plotnine/themes/themeable.py +109 -31
- plotnine/typing.py +17 -6
- plotnine/watermark.py +3 -3
- {plotnine-0.15.0.dev3.dist-info → plotnine-0.15.2.dist-info}/METADATA +13 -6
- plotnine-0.15.2.dist-info/RECORD +221 -0
- {plotnine-0.15.0.dev3.dist-info → plotnine-0.15.2.dist-info}/WHEEL +1 -1
- plotnine/plot_composition/__init__.py +0 -10
- plotnine/plot_composition/_compose.py +0 -436
- plotnine/plot_composition/_spacer.py +0 -32
- plotnine-0.15.0.dev3.dist-info/RECORD +0 -215
- /plotnine/{plot_composition → composition}/_plotspec.py +0 -0
- {plotnine-0.15.0.dev3.dist-info → plotnine-0.15.2.dist-info}/licenses/LICENSE +0 -0
- {plotnine-0.15.0.dev3.dist-info → plotnine-0.15.2.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,9 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from functools import lru_cache
|
|
3
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
12
|
from .utils import bbox_in_axes_space, rel_position
|
|
9
13
|
|
|
@@ -11,7 +15,6 @@ if TYPE_CHECKING:
|
|
|
11
15
|
from matplotlib.backend_bases import RendererBase
|
|
12
16
|
|
|
13
17
|
from plotnine.iapi import strip_draw_info
|
|
14
|
-
from plotnine.typing import HorizontalJustification, VerticalJustification
|
|
15
18
|
|
|
16
19
|
|
|
17
20
|
class StripText(Text):
|
|
@@ -28,86 +31,164 @@ class StripText(Text):
|
|
|
28
31
|
"transform": info.ax.transAxes,
|
|
29
32
|
"clip_on": False,
|
|
30
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",
|
|
31
42
|
}
|
|
32
43
|
|
|
33
|
-
super().__init__(
|
|
34
|
-
info.x,
|
|
35
|
-
info.y,
|
|
36
|
-
info.label,
|
|
37
|
-
**kwargs,
|
|
38
|
-
)
|
|
44
|
+
super().__init__(0, 0, info.label, **kwargs)
|
|
39
45
|
self.draw_info = info
|
|
40
46
|
self.patch = StripTextPatch(self)
|
|
41
47
|
|
|
42
|
-
# TODO:
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
# TODO: This should really be part of the unit conversions in the
|
|
49
|
+
# margin class.
|
|
50
|
+
@lru_cache(2)
|
|
51
|
+
def _line_height(self, renderer) -> float:
|
|
52
|
+
"""
|
|
53
|
+
The line height in display space of the text on the canvas
|
|
54
|
+
"""
|
|
55
|
+
# Text string, (width, height), x, y
|
|
56
|
+
parts: list[tuple[str, tuple[float, float], float, float]]
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
# matplotlib.Text._get_layout is a private API and we cannot
|
|
60
|
+
# tell how using it may fail in the future.
|
|
61
|
+
_, parts, _ = self._get_layout(renderer) # pyright: ignore[reportAttributeAccessIssue]
|
|
62
|
+
except Exception:
|
|
63
|
+
from warnings import warn
|
|
64
|
+
|
|
65
|
+
from plotnine.exceptions import PlotnineWarning
|
|
66
|
+
|
|
67
|
+
# The canvas height is nearly always bigger than the stated
|
|
68
|
+
# fontsize. 1.36 is a good multiplication factor obtained by
|
|
69
|
+
# some rough exploration
|
|
70
|
+
f = 1.36
|
|
71
|
+
size = self.get_fontsize()
|
|
72
|
+
height = round(size * f) if isinstance(size, int) else 14
|
|
73
|
+
warn(
|
|
74
|
+
f"Could not calculate line height for {self.get_text()}. "
|
|
75
|
+
"Using an estimate, please let us know about this at "
|
|
76
|
+
"https://github.com/has2k1/plotnine/issues",
|
|
77
|
+
PlotnineWarning,
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
# The the text has multiple lines, we use the maximum height
|
|
81
|
+
# of anyone single line.
|
|
82
|
+
height = max([p[1][1] for p in parts])
|
|
83
|
+
|
|
84
|
+
return height
|
|
85
|
+
|
|
86
|
+
def _set_position(self, renderer):
|
|
87
|
+
"""
|
|
88
|
+
Set the postion of the text within the strip_background
|
|
89
|
+
"""
|
|
90
|
+
# We have to two premises that depend on each other:
|
|
91
|
+
#
|
|
92
|
+
# 1. The breadth of the strip_background grows to accomodate
|
|
93
|
+
# the strip_text.
|
|
94
|
+
# 2. The strip_text is justified within the strip_background.
|
|
95
|
+
#
|
|
96
|
+
# From these we note that the strip_background does not need the
|
|
97
|
+
# position of the strip_text, but it needs its size. Therefore
|
|
98
|
+
# we implement StripTextPatch.get_window_extent can use
|
|
99
|
+
# StripText.get_window_extent, peeking only at the size.
|
|
100
|
+
#
|
|
101
|
+
# And we implement StripText._set_position_* to use
|
|
102
|
+
# StripTextPatch.get_window_extent and make the calculations in
|
|
103
|
+
# both methods independent.
|
|
104
|
+
if self.draw_info.position == "top":
|
|
105
|
+
self._set_position_top(renderer)
|
|
106
|
+
else: # "right"
|
|
107
|
+
self._set_position_right(renderer)
|
|
108
|
+
|
|
109
|
+
def _set_position_top(self, renderer):
|
|
47
110
|
"""
|
|
48
|
-
|
|
111
|
+
Set position of the text within the top strip_background
|
|
49
112
|
"""
|
|
50
113
|
info = self.draw_info
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
114
|
+
ha, va, ax, m = info.ha, info.va, info.ax, info.margin
|
|
115
|
+
|
|
116
|
+
rel_x, rel_y = ha_as_float(ha), va_as_float(va)
|
|
117
|
+
patch_bbox = bbox_in_axes_space(self.patch, ax, renderer)
|
|
118
|
+
text_bbox = bbox_in_axes_space(self, ax, renderer)
|
|
119
|
+
|
|
120
|
+
# line_height and margins in axes space
|
|
121
|
+
line_height = self._line_height(renderer) / ax.bbox.height
|
|
122
|
+
|
|
123
|
+
x = (
|
|
124
|
+
# Justify horizontally within the strip_background
|
|
125
|
+
rel_position(
|
|
126
|
+
rel_x,
|
|
127
|
+
text_bbox.width + (line_height * (m.l + m.r)),
|
|
128
|
+
patch_bbox.x0,
|
|
129
|
+
patch_bbox.x1,
|
|
130
|
+
)
|
|
131
|
+
+ (m.l * line_height)
|
|
132
|
+
+ text_bbox.width / 2
|
|
133
|
+
)
|
|
134
|
+
# Setting the y position based on the bounding box is wrong
|
|
135
|
+
y = (
|
|
136
|
+
rel_position(
|
|
137
|
+
rel_y,
|
|
138
|
+
text_bbox.height,
|
|
139
|
+
patch_bbox.y0 + m.b * line_height,
|
|
140
|
+
patch_bbox.y1 - m.t * line_height,
|
|
141
|
+
)
|
|
142
|
+
+ text_bbox.height / 2
|
|
143
|
+
)
|
|
65
144
|
self.set_position((x, y))
|
|
66
145
|
|
|
67
|
-
def
|
|
146
|
+
def _set_position_right(self, renderer):
|
|
68
147
|
"""
|
|
69
|
-
|
|
148
|
+
Set position of the text within the bottom strip_background
|
|
70
149
|
"""
|
|
71
|
-
# Note that the strip text & background and horizontal but
|
|
72
|
-
# rotated to appear vertical. So we really are still justifying
|
|
73
|
-
# horizontally.
|
|
74
150
|
info = self.draw_info
|
|
75
|
-
|
|
76
|
-
"bottom": 0.0,
|
|
77
|
-
"center": 0.5,
|
|
78
|
-
"top": 1.0,
|
|
79
|
-
}
|
|
80
|
-
rel = lookup.get(info.va, 0.5) if isinstance(info.va, str) else info.va
|
|
81
|
-
patch_bbox = bbox_in_axes_space(self.patch, info.ax, renderer)
|
|
82
|
-
text_bbox = bbox_in_axes_space(self, info.ax, renderer)
|
|
83
|
-
l, b, w, h = info.x, info.y, patch_bbox.width, info.box_height
|
|
84
|
-
l = l + patch_bbox.width * info.strip_align
|
|
85
|
-
x = l + w / 2
|
|
86
|
-
y = rel_position(rel, text_bbox.height, patch_bbox.y0, patch_bbox.y1)
|
|
87
|
-
self.set_horizontalalignment("right") # 90CW right means bottom
|
|
88
|
-
self.patch.set_bounds(l, b, w, h)
|
|
89
|
-
self.set_position((x, y))
|
|
151
|
+
ha, va, ax, m = info.ha, info.va, info.ax, info.margin
|
|
90
152
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
153
|
+
# bboxes in axes space
|
|
154
|
+
patch_bbox = bbox_in_axes_space(self.patch, ax, renderer)
|
|
155
|
+
text_bbox = bbox_in_axes_space(self, ax, renderer)
|
|
94
156
|
|
|
95
|
-
#
|
|
96
|
-
self.
|
|
157
|
+
# line_height in axes space
|
|
158
|
+
line_height = self._line_height(renderer) / ax.bbox.width
|
|
97
159
|
|
|
98
|
-
|
|
99
|
-
if self.draw_info.position == "top":
|
|
100
|
-
self._justify_horizontally(renderer)
|
|
101
|
-
else: # "right"
|
|
102
|
-
self._justify_vertically(renderer)
|
|
160
|
+
rel_x, rel_y = ha_as_float(ha), va_as_float(va)
|
|
103
161
|
|
|
104
|
-
|
|
105
|
-
|
|
162
|
+
x = (
|
|
163
|
+
rel_position(
|
|
164
|
+
rel_x,
|
|
165
|
+
text_bbox.width,
|
|
166
|
+
patch_bbox.x0 + m.l * line_height,
|
|
167
|
+
patch_bbox.x1 - m.r * line_height,
|
|
168
|
+
)
|
|
169
|
+
+ text_bbox.width / 2
|
|
170
|
+
)
|
|
171
|
+
y = (
|
|
172
|
+
# Justify vertically within the strip_background
|
|
173
|
+
rel_position(
|
|
174
|
+
rel_y,
|
|
175
|
+
text_bbox.height + ((m.b + m.t) * line_height),
|
|
176
|
+
patch_bbox.y0,
|
|
177
|
+
patch_bbox.y1,
|
|
178
|
+
)
|
|
179
|
+
+ (m.b * line_height)
|
|
180
|
+
+ text_bbox.height / 2
|
|
181
|
+
)
|
|
182
|
+
self.set_position((x, y))
|
|
106
183
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
184
|
+
@artist.allow_rasterization
|
|
185
|
+
def draw(self, renderer: RendererBase):
|
|
186
|
+
"""
|
|
187
|
+
Draw text along with the patch
|
|
188
|
+
"""
|
|
189
|
+
if not self.get_visible():
|
|
190
|
+
return
|
|
110
191
|
|
|
111
|
-
|
|
192
|
+
self._set_position(renderer)
|
|
112
193
|
self.patch.draw(renderer)
|
|
113
194
|
return super().draw(renderer)
|
plotnine/_mpl/utils.py
CHANGED
plotnine/_utils/__init__.py
CHANGED
|
@@ -12,9 +12,10 @@ from collections.abc import Iterable, Sequence
|
|
|
12
12
|
from contextlib import suppress
|
|
13
13
|
from copy import deepcopy
|
|
14
14
|
from dataclasses import field
|
|
15
|
-
from typing import TYPE_CHECKING
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
16
|
from warnings import warn
|
|
17
17
|
|
|
18
|
+
import mizani._colors.utils as color_utils
|
|
18
19
|
import numpy as np
|
|
19
20
|
import pandas as pd
|
|
20
21
|
from pandas.core.groupby import DataFrameGroupBy
|
|
@@ -26,17 +27,17 @@ if TYPE_CHECKING:
|
|
|
26
27
|
from typing import Any, Callable, Literal, TypeVar
|
|
27
28
|
|
|
28
29
|
import numpy.typing as npt
|
|
29
|
-
from matplotlib.typing import ColorType
|
|
30
30
|
from typing_extensions import TypeGuard
|
|
31
31
|
|
|
32
32
|
from plotnine.typing import (
|
|
33
33
|
AnyArrayLike,
|
|
34
|
-
AnySeries,
|
|
35
34
|
DataLike,
|
|
36
35
|
FloatArray,
|
|
37
36
|
FloatArrayLike,
|
|
37
|
+
HorizontalJustification,
|
|
38
38
|
IntArray,
|
|
39
|
-
|
|
39
|
+
Side,
|
|
40
|
+
VerticalJustification,
|
|
40
41
|
)
|
|
41
42
|
|
|
42
43
|
T = TypeVar("T")
|
|
@@ -58,6 +59,8 @@ BOX_LOCATIONS: dict[str, tuple[float, float]] = {
|
|
|
58
59
|
"centre": (0.5, 0.5),
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
to_rgba = color_utils.to_rgba
|
|
63
|
+
|
|
61
64
|
|
|
62
65
|
def is_scalar(val):
|
|
63
66
|
"""
|
|
@@ -344,6 +347,8 @@ def _id_var(x: AnyArrayLike, drop: bool = False) -> list[int]:
|
|
|
344
347
|
# NaNs are -1, we give them the highest code
|
|
345
348
|
nan_code = -1
|
|
346
349
|
new_nan_code = np.max(x.cat.codes) + 1
|
|
350
|
+
# TODO: We are assuming that x is of type Sequence[int|nan]
|
|
351
|
+
# is that accurate.
|
|
347
352
|
lst = [val if val != nan_code else new_nan_code for val in x]
|
|
348
353
|
else:
|
|
349
354
|
lst = list(x.cat.codes + 1)
|
|
@@ -511,7 +516,7 @@ def remove_missing(
|
|
|
511
516
|
if finite:
|
|
512
517
|
lst = [np.inf, -np.inf]
|
|
513
518
|
to_replace = {v: lst for v in vars}
|
|
514
|
-
data.replace(to_replace, np.nan, inplace=True)
|
|
519
|
+
data.replace(to_replace, np.nan, inplace=True)
|
|
515
520
|
txt = "non-finite"
|
|
516
521
|
else:
|
|
517
522
|
txt = "missing"
|
|
@@ -526,105 +531,6 @@ def remove_missing(
|
|
|
526
531
|
return data
|
|
527
532
|
|
|
528
533
|
|
|
529
|
-
@overload
|
|
530
|
-
def to_rgba(colors: ColorType, alpha: float) -> ColorType: ...
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
@overload
|
|
534
|
-
def to_rgba(
|
|
535
|
-
colors: Sequence[ColorType], alpha: float
|
|
536
|
-
) -> Sequence[ColorType] | ColorType: ...
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
@overload
|
|
540
|
-
def to_rgba(
|
|
541
|
-
colors: AnySeries, alpha: AnySeries
|
|
542
|
-
) -> Sequence[ColorType] | ColorType: ...
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
def to_rgba(
|
|
546
|
-
colors: Sequence[ColorType] | AnySeries | ColorType,
|
|
547
|
-
alpha: float | Sequence[float] | AnySeries,
|
|
548
|
-
) -> Sequence[ColorType] | ColorType:
|
|
549
|
-
"""
|
|
550
|
-
Convert hex colors to rgba values.
|
|
551
|
-
|
|
552
|
-
Parameters
|
|
553
|
-
----------
|
|
554
|
-
colors : iterable | str
|
|
555
|
-
colors to convert
|
|
556
|
-
alphas : iterable | float
|
|
557
|
-
alpha values
|
|
558
|
-
|
|
559
|
-
Returns
|
|
560
|
-
-------
|
|
561
|
-
out : ndarray | tuple
|
|
562
|
-
rgba color(s)
|
|
563
|
-
|
|
564
|
-
Notes
|
|
565
|
-
-----
|
|
566
|
-
Matplotlib plotting functions only accept scalar
|
|
567
|
-
alpha values. Hence no two objects with different
|
|
568
|
-
alpha values may be plotted in one call. This would
|
|
569
|
-
make plots with continuous alpha values innefficient.
|
|
570
|
-
However :), the colors can be rgba hex values or
|
|
571
|
-
list-likes and the alpha dimension will be respected.
|
|
572
|
-
"""
|
|
573
|
-
|
|
574
|
-
def is_iterable(var):
|
|
575
|
-
return np.iterable(var) and not isinstance(var, str)
|
|
576
|
-
|
|
577
|
-
def has_alpha(c):
|
|
578
|
-
return (isinstance(c, tuple) and len(c) == 4) or (
|
|
579
|
-
isinstance(c, str) and len(c) == 9 and c[0] == "#"
|
|
580
|
-
)
|
|
581
|
-
|
|
582
|
-
def no_color(c):
|
|
583
|
-
return c is None or c.lower() in ("none", "")
|
|
584
|
-
|
|
585
|
-
def to_rgba_hex(c: ColorType, a: float) -> str:
|
|
586
|
-
"""
|
|
587
|
-
Convert rgb color to rgba hex value
|
|
588
|
-
|
|
589
|
-
If color c has an alpha channel, then alpha value
|
|
590
|
-
a is ignored
|
|
591
|
-
"""
|
|
592
|
-
from matplotlib.colors import colorConverter, to_hex
|
|
593
|
-
|
|
594
|
-
if c in ("None", "none"):
|
|
595
|
-
return c
|
|
596
|
-
|
|
597
|
-
_has_alpha = has_alpha(c)
|
|
598
|
-
c = to_hex(c, keep_alpha=_has_alpha)
|
|
599
|
-
|
|
600
|
-
if not _has_alpha:
|
|
601
|
-
arr = colorConverter.to_rgba(c, a)
|
|
602
|
-
return to_hex(arr, keep_alpha=True)
|
|
603
|
-
|
|
604
|
-
return c
|
|
605
|
-
|
|
606
|
-
if is_iterable(colors):
|
|
607
|
-
colors = cast("Sequence[ColorType]", colors)
|
|
608
|
-
|
|
609
|
-
if all(no_color(c) for c in colors):
|
|
610
|
-
return "none"
|
|
611
|
-
|
|
612
|
-
if isinstance(alpha, (Sequence, pd.Series)):
|
|
613
|
-
return [to_rgba_hex(c, a) for c, a in zip(colors, alpha)]
|
|
614
|
-
else:
|
|
615
|
-
return [to_rgba_hex(c, alpha) for c in colors]
|
|
616
|
-
else:
|
|
617
|
-
colors = cast("ColorType", colors)
|
|
618
|
-
|
|
619
|
-
if no_color(colors):
|
|
620
|
-
return colors
|
|
621
|
-
|
|
622
|
-
if isinstance(alpha, (Sequence, pd.Series)):
|
|
623
|
-
return [to_rgba_hex(colors, a) for a in alpha]
|
|
624
|
-
else:
|
|
625
|
-
return to_rgba_hex(colors, alpha)
|
|
626
|
-
|
|
627
|
-
|
|
628
534
|
def groupby_apply(
|
|
629
535
|
df: pd.DataFrame,
|
|
630
536
|
cols: str | list[str],
|
|
@@ -1227,11 +1133,11 @@ def default_field(default: T) -> T:
|
|
|
1227
1133
|
return field(default_factory=lambda: deepcopy(default))
|
|
1228
1134
|
|
|
1229
1135
|
|
|
1230
|
-
def get_opposite_side(s:
|
|
1136
|
+
def get_opposite_side(s: Side) -> Side:
|
|
1231
1137
|
"""
|
|
1232
1138
|
Return the opposite side
|
|
1233
1139
|
"""
|
|
1234
|
-
lookup: dict[
|
|
1140
|
+
lookup: dict[Side, Side] = {
|
|
1235
1141
|
"right": "left",
|
|
1236
1142
|
"left": "right",
|
|
1237
1143
|
"top": "bottom",
|
|
@@ -1241,7 +1147,7 @@ def get_opposite_side(s: SidePosition) -> SidePosition:
|
|
|
1241
1147
|
|
|
1242
1148
|
|
|
1243
1149
|
def ensure_xy_location(
|
|
1244
|
-
loc:
|
|
1150
|
+
loc: Side | Literal["center"] | float | tuple[float, float],
|
|
1245
1151
|
) -> tuple[float, float]:
|
|
1246
1152
|
"""
|
|
1247
1153
|
Convert input into (x, y) location
|
|
@@ -1264,3 +1170,40 @@ def ensure_xy_location(
|
|
|
1264
1170
|
if isinstance(h, (int, float)) and isinstance(v, (int, float)):
|
|
1265
1171
|
return (h, v)
|
|
1266
1172
|
raise ValueError(f"Cannot make a location from '{loc}'")
|
|
1173
|
+
|
|
1174
|
+
|
|
1175
|
+
def ha_as_float(ha: HorizontalJustification | float) -> float:
|
|
1176
|
+
"""
|
|
1177
|
+
Return horizontal alignment as a float
|
|
1178
|
+
"""
|
|
1179
|
+
lookup = {"left": 0.0, "center": 0.5, "right": 1.0}
|
|
1180
|
+
return lookup[ha] if isinstance(ha, str) else ha
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
def va_as_float(va: VerticalJustification | float) -> float:
|
|
1184
|
+
"""
|
|
1185
|
+
Return vertical alignment as a float
|
|
1186
|
+
"""
|
|
1187
|
+
lookup = {
|
|
1188
|
+
"top": 1.0,
|
|
1189
|
+
"center": 0.5,
|
|
1190
|
+
"bottom": 0.0,
|
|
1191
|
+
# baseline and center_baseline are valid for texts but we do
|
|
1192
|
+
# not handle them accurately
|
|
1193
|
+
"baseline": 0.5,
|
|
1194
|
+
"center_baseline": 0.5,
|
|
1195
|
+
}
|
|
1196
|
+
return lookup[va] if isinstance(va, str) else va
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
def has_alpha_channel(c: str | tuple) -> bool:
|
|
1200
|
+
"""
|
|
1201
|
+
Return True if c a color with an alpha value
|
|
1202
|
+
|
|
1203
|
+
Either a 9 character hex string e.g. #AABBCC88 or
|
|
1204
|
+
an RGBA tuple e.g. (.6, .7, .8, .5)
|
|
1205
|
+
"""
|
|
1206
|
+
if isinstance(c, str):
|
|
1207
|
+
return c.startswith("#") and len(c) == 9
|
|
1208
|
+
else:
|
|
1209
|
+
return color_utils.is_color_tuple(c) and len(c) == 4
|