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
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from .._utils.ipython import (
|
|
10
|
+
get_display_function,
|
|
11
|
+
get_ipython,
|
|
12
|
+
)
|
|
13
|
+
from ..options import get_option
|
|
14
|
+
from ._plotspec import plotspec
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Generator, Iterator, Self
|
|
19
|
+
|
|
20
|
+
from matplotlib.figure import Figure
|
|
21
|
+
|
|
22
|
+
from plotnine._mpl.gridspec import p9GridSpec
|
|
23
|
+
from plotnine._utils.ipython import FigureFormat
|
|
24
|
+
from plotnine.ggplot import PlotAddable, ggplot
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Compose:
|
|
28
|
+
"""
|
|
29
|
+
Arrange two or more plots
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
operands:
|
|
34
|
+
The objects to be put together (composed).
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, operands: list[ggplot | Compose]):
|
|
38
|
+
self.operands = operands
|
|
39
|
+
|
|
40
|
+
# These are created in the _create_figure method
|
|
41
|
+
self.figure: Figure
|
|
42
|
+
self.plotspecs: list[plotspec]
|
|
43
|
+
self.gridspec: p9GridSpec
|
|
44
|
+
|
|
45
|
+
@abc.abstractmethod
|
|
46
|
+
def __or__(self, rhs: ggplot | Compose) -> Compose:
|
|
47
|
+
"""
|
|
48
|
+
Add rhs as a column
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
@abc.abstractmethod
|
|
52
|
+
def __truediv__(self, rhs: ggplot | Compose) -> Compose:
|
|
53
|
+
"""
|
|
54
|
+
Add rhs as a row
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __add__(self, rhs: ggplot | Compose | PlotAddable) -> Compose:
|
|
58
|
+
"""
|
|
59
|
+
Add rhs to the composition
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
rhs:
|
|
64
|
+
What to add to the composition
|
|
65
|
+
"""
|
|
66
|
+
from plotnine import ggplot
|
|
67
|
+
|
|
68
|
+
if not isinstance(rhs, (ggplot, Compose)):
|
|
69
|
+
cmp = deepcopy(self)
|
|
70
|
+
cmp.last_plot = cmp.last_plot + rhs
|
|
71
|
+
return cmp
|
|
72
|
+
return self.__class__([*self, rhs])
|
|
73
|
+
|
|
74
|
+
def __sub__(self, rhs: ggplot | Compose) -> Compose:
|
|
75
|
+
"""
|
|
76
|
+
Add the rhs besides the composition
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
rhs:
|
|
81
|
+
What to place besides the composition
|
|
82
|
+
"""
|
|
83
|
+
return self.__class__([self, rhs])
|
|
84
|
+
|
|
85
|
+
def __and__(self, rhs: PlotAddable) -> Compose:
|
|
86
|
+
"""
|
|
87
|
+
Add rhs to all plots in the composition
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
rhs:
|
|
92
|
+
What to add.
|
|
93
|
+
"""
|
|
94
|
+
self = deepcopy(self)
|
|
95
|
+
|
|
96
|
+
def add_other(op: Compose):
|
|
97
|
+
for item in op:
|
|
98
|
+
if isinstance(item, Compose):
|
|
99
|
+
add_other(item)
|
|
100
|
+
else:
|
|
101
|
+
item += rhs
|
|
102
|
+
|
|
103
|
+
add_other(self)
|
|
104
|
+
return self
|
|
105
|
+
|
|
106
|
+
def __mul__(self, rhs: PlotAddable) -> Compose:
|
|
107
|
+
"""
|
|
108
|
+
Add rhs to the outermost nesting level of the composition
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
rhs:
|
|
113
|
+
What to add.
|
|
114
|
+
"""
|
|
115
|
+
from plotnine import ggplot
|
|
116
|
+
|
|
117
|
+
self = deepcopy(self)
|
|
118
|
+
|
|
119
|
+
for item in self:
|
|
120
|
+
if isinstance(item, ggplot):
|
|
121
|
+
item += rhs
|
|
122
|
+
return self
|
|
123
|
+
|
|
124
|
+
def __len__(self) -> int:
|
|
125
|
+
"""
|
|
126
|
+
Number of operand
|
|
127
|
+
"""
|
|
128
|
+
return len(self.operands)
|
|
129
|
+
|
|
130
|
+
def __iter__(self) -> Iterator[ggplot | Compose]:
|
|
131
|
+
"""
|
|
132
|
+
Return an iterable of all the operands
|
|
133
|
+
"""
|
|
134
|
+
return iter(self.operands)
|
|
135
|
+
|
|
136
|
+
def _ipython_display_(self):
|
|
137
|
+
"""
|
|
138
|
+
Display plot in the output of the cell
|
|
139
|
+
"""
|
|
140
|
+
return self._display()
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def nrow(self) -> int:
|
|
144
|
+
"""
|
|
145
|
+
Number of rows in the composition
|
|
146
|
+
"""
|
|
147
|
+
return 0
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def ncol(self) -> int:
|
|
151
|
+
"""
|
|
152
|
+
Number of cols in the composition
|
|
153
|
+
"""
|
|
154
|
+
return 0
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def last_plot(self) -> ggplot:
|
|
158
|
+
"""
|
|
159
|
+
Last plot added to the composition
|
|
160
|
+
"""
|
|
161
|
+
from plotnine import ggplot
|
|
162
|
+
|
|
163
|
+
last_operand = self.operands[-1]
|
|
164
|
+
if isinstance(last_operand, ggplot):
|
|
165
|
+
return last_operand
|
|
166
|
+
else:
|
|
167
|
+
return last_operand.last_plot
|
|
168
|
+
|
|
169
|
+
@last_plot.setter
|
|
170
|
+
def last_plot(self, plot: ggplot):
|
|
171
|
+
"""
|
|
172
|
+
Replace the last plot in the composition
|
|
173
|
+
"""
|
|
174
|
+
from plotnine import ggplot
|
|
175
|
+
|
|
176
|
+
last_operand = self.operands[-1]
|
|
177
|
+
if isinstance(last_operand, ggplot):
|
|
178
|
+
self.operands[-1] = plot
|
|
179
|
+
else:
|
|
180
|
+
last_operand.last_plot = plot
|
|
181
|
+
|
|
182
|
+
def __deepcopy__(self, memo):
|
|
183
|
+
"""
|
|
184
|
+
Deep copy without copying the figure
|
|
185
|
+
"""
|
|
186
|
+
cls = self.__class__
|
|
187
|
+
result = cls.__new__(cls)
|
|
188
|
+
memo[id(self)] = result
|
|
189
|
+
old = self.__dict__
|
|
190
|
+
new = result.__dict__
|
|
191
|
+
|
|
192
|
+
shallow = {"figure", "gridsspec", "__copy"}
|
|
193
|
+
for key, item in old.items():
|
|
194
|
+
if key in shallow:
|
|
195
|
+
new[key] = item
|
|
196
|
+
memo[id(new[key])] = new[key]
|
|
197
|
+
else:
|
|
198
|
+
new[key] = deepcopy(item, memo)
|
|
199
|
+
|
|
200
|
+
old["__copy"] = result
|
|
201
|
+
|
|
202
|
+
return result
|
|
203
|
+
|
|
204
|
+
def _to_retina(self):
|
|
205
|
+
from plotnine import ggplot
|
|
206
|
+
|
|
207
|
+
for item in self:
|
|
208
|
+
if isinstance(item, ggplot):
|
|
209
|
+
item.theme = item.theme.to_retina()
|
|
210
|
+
else:
|
|
211
|
+
item._to_retina()
|
|
212
|
+
|
|
213
|
+
def _create_gridspec(self, figure, nest_into):
|
|
214
|
+
"""
|
|
215
|
+
Create the gridspec for this composition
|
|
216
|
+
"""
|
|
217
|
+
from plotnine._mpl.gridspec import p9GridSpec
|
|
218
|
+
|
|
219
|
+
self.gridspec = p9GridSpec(
|
|
220
|
+
self.nrow, self.ncol, figure, nest_into=nest_into
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def _create_figure(self):
|
|
224
|
+
import matplotlib.pyplot as plt
|
|
225
|
+
|
|
226
|
+
from plotnine import ggplot
|
|
227
|
+
from plotnine._mpl.gridspec import p9GridSpec
|
|
228
|
+
|
|
229
|
+
def _make_plotspecs(
|
|
230
|
+
cmp: Compose, parent_gridspec: p9GridSpec | None
|
|
231
|
+
) -> Generator[plotspec]:
|
|
232
|
+
"""
|
|
233
|
+
Return the plot specification for each subplot in the composition
|
|
234
|
+
"""
|
|
235
|
+
# This gridspec contains a composition group e.g.
|
|
236
|
+
# (p2 | p3) of p1 | (p2 | p3)
|
|
237
|
+
ss_or_none = parent_gridspec[0] if parent_gridspec else None
|
|
238
|
+
cmp._create_gridspec(self.figure, ss_or_none)
|
|
239
|
+
|
|
240
|
+
# Each subplot in the composition will contain one of:
|
|
241
|
+
# 1. A plot
|
|
242
|
+
# 2. A plot composition
|
|
243
|
+
# 3. Nothing
|
|
244
|
+
# Iterating over the gridspec yields the SubplotSpecs for each
|
|
245
|
+
# "subplot" in the grid. The SubplotSpec is the handle that
|
|
246
|
+
# allows us to set it up for a plot or to nest another gridspec
|
|
247
|
+
# in it.
|
|
248
|
+
for item, subplot_spec in zip(cmp, cmp.gridspec): # pyright: ignore[reportArgumentType]
|
|
249
|
+
if isinstance(item, ggplot):
|
|
250
|
+
yield plotspec(
|
|
251
|
+
item,
|
|
252
|
+
self.figure,
|
|
253
|
+
cmp.gridspec,
|
|
254
|
+
subplot_spec,
|
|
255
|
+
p9GridSpec(1, 1, self.figure, nest_into=subplot_spec),
|
|
256
|
+
)
|
|
257
|
+
elif item:
|
|
258
|
+
yield from _make_plotspecs(
|
|
259
|
+
item,
|
|
260
|
+
p9GridSpec(1, 1, self.figure, nest_into=subplot_spec),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
self.figure = plt.figure()
|
|
264
|
+
self.plotspecs = list(_make_plotspecs(self, None))
|
|
265
|
+
|
|
266
|
+
def _display(self):
|
|
267
|
+
"""
|
|
268
|
+
Display plot in the cells output
|
|
269
|
+
|
|
270
|
+
This function is called for its side-effects.
|
|
271
|
+
|
|
272
|
+
It draws the plot to an io buffer then uses ipython display
|
|
273
|
+
methods to show the result.
|
|
274
|
+
"""
|
|
275
|
+
ip = get_ipython()
|
|
276
|
+
format: FigureFormat = get_option(
|
|
277
|
+
"figure_format"
|
|
278
|
+
) or ip.config.InlineBackend.get("figure_format", "retina")
|
|
279
|
+
|
|
280
|
+
if format == "retina":
|
|
281
|
+
self = deepcopy(self)
|
|
282
|
+
self._to_retina()
|
|
283
|
+
|
|
284
|
+
buf = BytesIO()
|
|
285
|
+
self.save(buf, "png" if format == "retina" else format)
|
|
286
|
+
figure_size_px = self.last_plot.theme._figure_size_px
|
|
287
|
+
display_func = get_display_function(format, figure_size_px)
|
|
288
|
+
display_func(buf.getvalue())
|
|
289
|
+
|
|
290
|
+
def draw(self, *, show: bool = False) -> Figure:
|
|
291
|
+
"""
|
|
292
|
+
Render the composed plots
|
|
293
|
+
|
|
294
|
+
Parameters
|
|
295
|
+
----------
|
|
296
|
+
show :
|
|
297
|
+
Whether to show the plot.
|
|
298
|
+
|
|
299
|
+
Returns
|
|
300
|
+
-------
|
|
301
|
+
:
|
|
302
|
+
Matplotlib figure
|
|
303
|
+
"""
|
|
304
|
+
from .._mpl.layout_manager import PlotnineCompositionLayoutEngine
|
|
305
|
+
|
|
306
|
+
with plot_composition_context(self, show):
|
|
307
|
+
self._create_figure()
|
|
308
|
+
figure = self.figure
|
|
309
|
+
|
|
310
|
+
for ps in self.plotspecs:
|
|
311
|
+
ps.plot.draw()
|
|
312
|
+
|
|
313
|
+
self.figure.set_layout_engine(
|
|
314
|
+
PlotnineCompositionLayoutEngine(self)
|
|
315
|
+
)
|
|
316
|
+
return figure
|
|
317
|
+
|
|
318
|
+
def save(self, filename: str | Path | BytesIO, format: str | None = None):
|
|
319
|
+
"""
|
|
320
|
+
Save a Compose object as an image file
|
|
321
|
+
|
|
322
|
+
Parameters
|
|
323
|
+
----------
|
|
324
|
+
filename :
|
|
325
|
+
File name to write the plot to. If not specified, a name
|
|
326
|
+
format :
|
|
327
|
+
Image format to use, automatically extract from
|
|
328
|
+
file name extension.
|
|
329
|
+
"""
|
|
330
|
+
figure = self.draw()
|
|
331
|
+
figure.savefig(filename, format=format)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@dataclass
|
|
335
|
+
class plot_composition_context:
|
|
336
|
+
cmp: Compose
|
|
337
|
+
show: bool
|
|
338
|
+
|
|
339
|
+
def __post_init__(self):
|
|
340
|
+
import matplotlib as mpl
|
|
341
|
+
|
|
342
|
+
# The dpi is needed when the figure is created, either as
|
|
343
|
+
# a parameter to plt.figure() or an rcParam.
|
|
344
|
+
# https://github.com/matplotlib/matplotlib/issues/24644
|
|
345
|
+
# When drawing the Composition, the dpi themeable is infective
|
|
346
|
+
# because it sets the rcParam after this figure is created.
|
|
347
|
+
rcParams = {"figure.dpi": self.cmp.last_plot.theme.getp("dpi")}
|
|
348
|
+
self._rc_context = mpl.rc_context(rcParams)
|
|
349
|
+
|
|
350
|
+
def __enter__(self) -> Self:
|
|
351
|
+
"""
|
|
352
|
+
Enclose in matplolib & pandas environments
|
|
353
|
+
"""
|
|
354
|
+
self._rc_context.__enter__()
|
|
355
|
+
return self
|
|
356
|
+
|
|
357
|
+
def __exit__(self, exc_type, exc_value, exc_traceback):
|
|
358
|
+
import matplotlib.pyplot as plt
|
|
359
|
+
|
|
360
|
+
if exc_type is None:
|
|
361
|
+
if self.show:
|
|
362
|
+
plt.show()
|
|
363
|
+
else:
|
|
364
|
+
plt.close(self.cmp.figure)
|
|
365
|
+
else:
|
|
366
|
+
# There is an exception, close any figure
|
|
367
|
+
if hasattr(self.cmp, "figure"):
|
|
368
|
+
plt.close(self.cmp.figure)
|
|
369
|
+
|
|
370
|
+
self._rc_context.__exit__(exc_type, exc_value, exc_traceback)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class OR(Compose):
|
|
374
|
+
"""
|
|
375
|
+
Compose by adding a column
|
|
376
|
+
"""
|
|
377
|
+
|
|
378
|
+
@property
|
|
379
|
+
def nrow(self) -> int:
|
|
380
|
+
return 1
|
|
381
|
+
|
|
382
|
+
@property
|
|
383
|
+
def ncol(self) -> int:
|
|
384
|
+
return len(self)
|
|
385
|
+
|
|
386
|
+
def __or__(self, rhs: ggplot | Compose) -> Compose:
|
|
387
|
+
"""
|
|
388
|
+
Add rhs as a column
|
|
389
|
+
"""
|
|
390
|
+
# This is adjacent or i.e. (OR | rhs) so we collapse the
|
|
391
|
+
# operands into a single operation
|
|
392
|
+
return OR([*self, rhs])
|
|
393
|
+
|
|
394
|
+
def __truediv__(self, rhs: ggplot | Compose) -> Compose:
|
|
395
|
+
"""
|
|
396
|
+
Add rhs as a row
|
|
397
|
+
"""
|
|
398
|
+
return DIV([self, rhs])
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class DIV(Compose):
|
|
402
|
+
"""
|
|
403
|
+
Compose by adding a row
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
@property
|
|
407
|
+
def nrow(self) -> int:
|
|
408
|
+
return len(self)
|
|
409
|
+
|
|
410
|
+
@property
|
|
411
|
+
def ncol(self) -> int:
|
|
412
|
+
return 1
|
|
413
|
+
|
|
414
|
+
def __truediv__(self, rhs: ggplot | Compose) -> Compose:
|
|
415
|
+
"""
|
|
416
|
+
Add rhs as a row
|
|
417
|
+
"""
|
|
418
|
+
# This is an adjacent div i.e. (DIV | rhs) so we collapse the
|
|
419
|
+
# operands into a single operation
|
|
420
|
+
return DIV([*self, rhs])
|
|
421
|
+
|
|
422
|
+
def __or__(self, rhs: ggplot | Compose) -> Compose:
|
|
423
|
+
"""
|
|
424
|
+
Add rhs as a column
|
|
425
|
+
"""
|
|
426
|
+
return OR([self, rhs])
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class ADD(Compose):
|
|
430
|
+
"""
|
|
431
|
+
Compose by adding
|
|
432
|
+
"""
|
|
433
|
+
|
|
434
|
+
@property
|
|
435
|
+
def nrow(self) -> int:
|
|
436
|
+
from plotnine.facets.facet_wrap import wrap_dims
|
|
437
|
+
|
|
438
|
+
return wrap_dims(len(self))[0]
|
|
439
|
+
|
|
440
|
+
@property
|
|
441
|
+
def ncol(self) -> int:
|
|
442
|
+
from plotnine.facets.facet_wrap import wrap_dims
|
|
443
|
+
|
|
444
|
+
return wrap_dims(len(self))[1]
|
|
445
|
+
|
|
446
|
+
def __or__(self, rhs: ggplot | Compose) -> Compose:
|
|
447
|
+
"""
|
|
448
|
+
Add rhs as a column
|
|
449
|
+
"""
|
|
450
|
+
return OR([self, rhs])
|
|
451
|
+
|
|
452
|
+
def __truediv__(self, rhs: ggplot | Compose) -> Compose:
|
|
453
|
+
"""
|
|
454
|
+
Add rhs as a row
|
|
455
|
+
"""
|
|
456
|
+
return DIV([self, rhs])
|
|
457
|
+
|
|
458
|
+
def __sub__(self, rhs: ggplot | Compose) -> Compose:
|
|
459
|
+
"""
|
|
460
|
+
Add rhs as a column
|
|
461
|
+
"""
|
|
462
|
+
return OR([self, rhs])
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from matplotlib.figure import Figure
|
|
8
|
+
from matplotlib.gridspec import SubplotSpec
|
|
9
|
+
|
|
10
|
+
from plotnine._mpl.gridspec import p9GridSpec
|
|
11
|
+
from plotnine.ggplot import ggplot
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class plotspec:
|
|
16
|
+
"""
|
|
17
|
+
Plot Specification
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
plot: ggplot
|
|
21
|
+
"""
|
|
22
|
+
Plot
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
figure: Figure
|
|
26
|
+
"""
|
|
27
|
+
Figure in which the draw the plot
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
composition_gridspec: p9GridSpec
|
|
31
|
+
"""
|
|
32
|
+
The gridspec of the innermost composition group that contains the plot
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
subplotspec: SubplotSpec
|
|
36
|
+
"""
|
|
37
|
+
The subplotspec that contains the plot
|
|
38
|
+
|
|
39
|
+
This is the subplot within the composition gridspec and it will
|
|
40
|
+
contain the plot's gridspec.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
plot_gridspec: p9GridSpec
|
|
44
|
+
"""
|
|
45
|
+
The gridspec in which the plot is drawn
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __post_init__(self):
|
|
49
|
+
self.plot.figure = self.figure
|
|
50
|
+
self.plot._gridspec = self.plot_gridspec
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
|
|
5
|
+
from plotnine import element_rect, ggplot, theme, theme_void
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class spacer(ggplot):
|
|
9
|
+
"""
|
|
10
|
+
An empty plot
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self):
|
|
14
|
+
super().__init__()
|
|
15
|
+
self.theme = theme_void()
|
|
16
|
+
|
|
17
|
+
def __add__(self, rhs) -> spacer: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
18
|
+
"""
|
|
19
|
+
Add to spacer
|
|
20
|
+
|
|
21
|
+
All added objects are no ops except the plot_background,
|
|
22
|
+
i.e.:
|
|
23
|
+
|
|
24
|
+
theme(plot_background=element_rect(fill="red"))
|
|
25
|
+
"""
|
|
26
|
+
self = deepcopy(self)
|
|
27
|
+
if isinstance(rhs, theme):
|
|
28
|
+
fill = rhs.getp(("plot_background", "facecolor"))
|
|
29
|
+
self.theme += theme(
|
|
30
|
+
plot_background=element_rect(fill=fill),
|
|
31
|
+
)
|
|
32
|
+
return self
|
|
@@ -61,7 +61,7 @@ class position_dodge(position):
|
|
|
61
61
|
and ("xmax" not in data)
|
|
62
62
|
and (self.params["width"] is None)
|
|
63
63
|
):
|
|
64
|
-
msg = "Width not defined.
|
|
64
|
+
msg = "Width not defined. Set with `position_dodge(width = ?)`"
|
|
65
65
|
raise PlotnineError(msg)
|
|
66
66
|
|
|
67
67
|
params = copy(self.params)
|
|
@@ -64,7 +64,7 @@ class position_dodge2(position_dodge):
|
|
|
64
64
|
and ("xmax" not in data)
|
|
65
65
|
and (self.params["width"] is None)
|
|
66
66
|
):
|
|
67
|
-
msg = "Width not defined.
|
|
67
|
+
msg = "Width not defined. Set with `position_dodge2(width = ?)`"
|
|
68
68
|
raise PlotnineError(msg)
|
|
69
69
|
|
|
70
70
|
params = copy(self.params)
|
|
@@ -39,8 +39,7 @@ class position_stack(position):
|
|
|
39
39
|
if "ymax" in data:
|
|
40
40
|
if any((data["ymin"] != 0) & (data["ymax"] != 0)):
|
|
41
41
|
warn(
|
|
42
|
-
"Stacking not well defined when not "
|
|
43
|
-
"anchored on the axis.",
|
|
42
|
+
"Stacking not well defined when not anchored on the axis.",
|
|
44
43
|
PlotnineWarning,
|
|
45
44
|
)
|
|
46
45
|
var = "ymax"
|
plotnine/qplot.py
CHANGED
plotnine/scales/__init__.py
CHANGED
|
@@ -79,7 +79,6 @@ from .scale_identity import (
|
|
|
79
79
|
# linetype
|
|
80
80
|
from .scale_linetype import (
|
|
81
81
|
scale_linetype,
|
|
82
|
-
scale_linetype_continuous,
|
|
83
82
|
scale_linetype_discrete,
|
|
84
83
|
)
|
|
85
84
|
|
|
@@ -97,7 +96,6 @@ from .scale_manual import (
|
|
|
97
96
|
# shape
|
|
98
97
|
from .scale_shape import (
|
|
99
98
|
scale_shape,
|
|
100
|
-
scale_shape_continuous,
|
|
101
99
|
scale_shape_discrete,
|
|
102
100
|
)
|
|
103
101
|
|
|
@@ -116,7 +114,6 @@ from .scale_size import (
|
|
|
116
114
|
from .scale_stroke import (
|
|
117
115
|
scale_stroke,
|
|
118
116
|
scale_stroke_continuous,
|
|
119
|
-
scale_stroke_discrete,
|
|
120
117
|
)
|
|
121
118
|
|
|
122
119
|
# xy position and transforms
|
|
@@ -198,11 +195,9 @@ __all__ = (
|
|
|
198
195
|
# linetype
|
|
199
196
|
"scale_linetype",
|
|
200
197
|
"scale_linetype_discrete",
|
|
201
|
-
"scale_linetype_continuous",
|
|
202
198
|
# shape
|
|
203
199
|
"scale_shape",
|
|
204
200
|
"scale_shape_discrete",
|
|
205
|
-
"scale_shape_continuous",
|
|
206
201
|
# size
|
|
207
202
|
"scale_size",
|
|
208
203
|
"scale_size_area",
|
|
@@ -214,7 +209,6 @@ __all__ = (
|
|
|
214
209
|
# stroke
|
|
215
210
|
"scale_stroke",
|
|
216
211
|
"scale_stroke_continuous",
|
|
217
|
-
"scale_stroke_discrete",
|
|
218
212
|
# identity
|
|
219
213
|
"scale_alpha_identity",
|
|
220
214
|
"scale_color_identity",
|
plotnine/scales/limits.py
CHANGED
|
@@ -78,10 +78,10 @@ class _lim:
|
|
|
78
78
|
self.aesthetic, series, limits=self.limits, trans=self.trans
|
|
79
79
|
)
|
|
80
80
|
|
|
81
|
-
def __radd__(self,
|
|
82
|
-
scale = self.get_scale(
|
|
83
|
-
|
|
84
|
-
return
|
|
81
|
+
def __radd__(self, other):
|
|
82
|
+
scale = self.get_scale(other)
|
|
83
|
+
other.scales.append(scale)
|
|
84
|
+
return other
|
|
85
85
|
|
|
86
86
|
|
|
87
87
|
class xlim(_lim):
|
|
@@ -194,7 +194,7 @@ class lims:
|
|
|
194
194
|
def __init__(self, **kwargs):
|
|
195
195
|
self._kwargs = kwargs
|
|
196
196
|
|
|
197
|
-
def __radd__(self,
|
|
197
|
+
def __radd__(self, other):
|
|
198
198
|
"""
|
|
199
199
|
Add limits to ggplot object
|
|
200
200
|
"""
|
|
@@ -206,9 +206,9 @@ class lims:
|
|
|
206
206
|
msg = "Cannot change limits for '{}'"
|
|
207
207
|
raise PlotnineError(msg) from e
|
|
208
208
|
|
|
209
|
-
|
|
209
|
+
other += klass(value)
|
|
210
210
|
|
|
211
|
-
return
|
|
211
|
+
return other
|
|
212
212
|
|
|
213
213
|
|
|
214
214
|
def expand_limits(**kwargs):
|
plotnine/scales/scale.py
CHANGED
|
@@ -148,12 +148,12 @@ class scale(
|
|
|
148
148
|
self.aesthetics if self.aesthetics else self._aesthetics
|
|
149
149
|
)
|
|
150
150
|
|
|
151
|
-
def __radd__(self,
|
|
151
|
+
def __radd__(self, other):
|
|
152
152
|
"""
|
|
153
153
|
Add this scale to ggplot object
|
|
154
154
|
"""
|
|
155
|
-
|
|
156
|
-
return
|
|
155
|
+
other.scales.append(copy(self))
|
|
156
|
+
return other
|
|
157
157
|
|
|
158
158
|
def map(self, x, limits=None):
|
|
159
159
|
"""
|
|
@@ -243,7 +243,7 @@ class scale(
|
|
|
243
243
|
if not (exp := self.expand):
|
|
244
244
|
m1, m2 = mult if isinstance(mult, (tuple, list)) else (mult, mult)
|
|
245
245
|
a1, a2 = cast(
|
|
246
|
-
tuple[float, float],
|
|
246
|
+
"tuple[float, float]",
|
|
247
247
|
(add if isinstance(add, (tuple, list)) else (add, add)),
|
|
248
248
|
)
|
|
249
249
|
exp = (m1, a1, m2, a2)
|
|
@@ -520,11 +520,12 @@ class scale_continuous(
|
|
|
520
520
|
# When user sets breaks and labels of equal size,
|
|
521
521
|
# but the limits exclude some of the breaks.
|
|
522
522
|
# We remove the corresponding labels
|
|
523
|
-
from collections.abc import Sized
|
|
523
|
+
from collections.abc import Iterable, Sized
|
|
524
524
|
|
|
525
525
|
labels = self.labels
|
|
526
526
|
if (
|
|
527
527
|
len(labels) != len(breaks)
|
|
528
|
+
and isinstance(self.breaks, Iterable)
|
|
528
529
|
and isinstance(self.breaks, Sized)
|
|
529
530
|
and len(labels) == len(self.breaks)
|
|
530
531
|
):
|