ggplot2-python 4.0.2.9000__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ggplot2_py/__init__.py +852 -0
- ggplot2_py/_compat.py +475 -0
- ggplot2_py/_plugins.py +129 -0
- ggplot2_py/_utils.py +544 -0
- ggplot2_py/aes.py +586 -0
- ggplot2_py/annotation.py +540 -0
- ggplot2_py/coord.py +2108 -0
- ggplot2_py/coords/__init__.py +49 -0
- ggplot2_py/datasets.py +265 -0
- ggplot2_py/draw_key.py +454 -0
- ggplot2_py/facet.py +1456 -0
- ggplot2_py/fortify.py +95 -0
- ggplot2_py/geom.py +4516 -0
- ggplot2_py/geoms/__init__.py +12 -0
- ggplot2_py/ggproto.py +279 -0
- ggplot2_py/guide.py +2925 -0
- ggplot2_py/guide_axis.py +615 -0
- ggplot2_py/guide_colourbar.py +657 -0
- ggplot2_py/guide_legend.py +1061 -0
- ggplot2_py/guides/__init__.py +8 -0
- ggplot2_py/labeller.py +296 -0
- ggplot2_py/labels.py +309 -0
- ggplot2_py/layer.py +954 -0
- ggplot2_py/layout.py +754 -0
- ggplot2_py/limits.py +314 -0
- ggplot2_py/plot.py +1401 -0
- ggplot2_py/plot_render.py +866 -0
- ggplot2_py/position.py +1269 -0
- ggplot2_py/protocols.py +171 -0
- ggplot2_py/py.typed +0 -0
- ggplot2_py/qplot.py +233 -0
- ggplot2_py/resources/diamonds.csv +53941 -0
- ggplot2_py/resources/economics.csv +575 -0
- ggplot2_py/resources/economics_long.csv +2871 -0
- ggplot2_py/resources/faithfuld.csv +5626 -0
- ggplot2_py/resources/luv_colours.csv +658 -0
- ggplot2_py/resources/midwest.csv +438 -0
- ggplot2_py/resources/mpg.csv +235 -0
- ggplot2_py/resources/msleep.csv +84 -0
- ggplot2_py/resources/presidential.csv +13 -0
- ggplot2_py/resources/seals.csv +1156 -0
- ggplot2_py/resources/txhousing.csv +8603 -0
- ggplot2_py/save.py +316 -0
- ggplot2_py/scale.py +2727 -0
- ggplot2_py/scales/__init__.py +4252 -0
- ggplot2_py/stat.py +6071 -0
- ggplot2_py/stats/__init__.py +9 -0
- ggplot2_py/theme.py +490 -0
- ggplot2_py/theme_defaults.py +1350 -0
- ggplot2_py/theme_elements.py +2052 -0
- ggplot2_python-4.0.2.9000.dist-info/METADATA +179 -0
- ggplot2_python-4.0.2.9000.dist-info/RECORD +54 -0
- ggplot2_python-4.0.2.9000.dist-info/WHEEL +4 -0
- ggplot2_python-4.0.2.9000.dist-info/licenses/LICENSE +3 -0
ggplot2_py/layout.py
ADDED
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Layout: coordinate system + faceting + panel-scale management.
|
|
3
|
+
|
|
4
|
+
The :class:`Layout` class is the internal engine that connects facets,
|
|
5
|
+
coordinates, and per-panel scales during the build and render phases of
|
|
6
|
+
a ggplot.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import pandas as pd
|
|
15
|
+
|
|
16
|
+
from ggplot2_py._compat import Waiver, is_waiver, waiver, cli_abort
|
|
17
|
+
from ggplot2_py.ggproto import GGProto, ggproto
|
|
18
|
+
from ggplot2_py._utils import data_frame
|
|
19
|
+
|
|
20
|
+
__all__ = ["Layout"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Helper
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
def _scale_apply(
|
|
28
|
+
data: pd.DataFrame,
|
|
29
|
+
vars_: List[str],
|
|
30
|
+
method: str,
|
|
31
|
+
scale_id: pd.Series,
|
|
32
|
+
scales: List[Any],
|
|
33
|
+
) -> Dict[str, Any]:
|
|
34
|
+
"""Apply a scale method to columns split by panel scale index.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
data : DataFrame
|
|
39
|
+
Layer data.
|
|
40
|
+
vars_ : list of str
|
|
41
|
+
Aesthetic column names to process.
|
|
42
|
+
method : str
|
|
43
|
+
Scale method name (e.g. ``"map"``).
|
|
44
|
+
scale_id : array-like
|
|
45
|
+
Per-row scale index (from ``layout.SCALE_X[match_id]``).
|
|
46
|
+
scales : list of Scale
|
|
47
|
+
The panel scales list.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
dict
|
|
52
|
+
Column-name -> mapped-values mapping.
|
|
53
|
+
"""
|
|
54
|
+
if len(vars_) == 0 or data.shape[0] == 0:
|
|
55
|
+
return {}
|
|
56
|
+
|
|
57
|
+
result: Dict[str, Any] = {}
|
|
58
|
+
for var in vars_:
|
|
59
|
+
pieces: List[Any] = []
|
|
60
|
+
indices: List[np.ndarray] = []
|
|
61
|
+
for i, sc in enumerate(scales):
|
|
62
|
+
mask = scale_id == (i + 1) # scale indices are 1-based
|
|
63
|
+
idx = np.where(mask)[0]
|
|
64
|
+
if len(idx) == 0:
|
|
65
|
+
continue
|
|
66
|
+
chunk = data[var].iloc[idx]
|
|
67
|
+
mapped = getattr(sc, method)(chunk)
|
|
68
|
+
pieces.append(mapped)
|
|
69
|
+
indices.append(idx)
|
|
70
|
+
if pieces:
|
|
71
|
+
# Reconstruct in original order
|
|
72
|
+
out = pd.Series(np.nan, index=data.index, dtype=object)
|
|
73
|
+
for idx_arr, piece in zip(indices, pieces):
|
|
74
|
+
if isinstance(piece, pd.Series):
|
|
75
|
+
out.iloc[idx_arr] = piece.values
|
|
76
|
+
elif isinstance(piece, np.ndarray):
|
|
77
|
+
out.iloc[idx_arr] = piece
|
|
78
|
+
elif isinstance(piece, (list, tuple)):
|
|
79
|
+
out.iloc[idx_arr] = piece
|
|
80
|
+
else:
|
|
81
|
+
out.iloc[idx_arr] = piece
|
|
82
|
+
# Try to convert to numeric if possible
|
|
83
|
+
try:
|
|
84
|
+
result[var] = pd.to_numeric(out, errors="raise")
|
|
85
|
+
except (ValueError, TypeError):
|
|
86
|
+
result[var] = out
|
|
87
|
+
else:
|
|
88
|
+
result[var] = data[var].copy()
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
# Layout class
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
class Layout(GGProto):
|
|
97
|
+
"""Panel layout manager.
|
|
98
|
+
|
|
99
|
+
The Layout manages panel creation and scale management during the
|
|
100
|
+
build (``ggplot_build``) and render (``ggplot_gtable``) phases.
|
|
101
|
+
|
|
102
|
+
Attributes
|
|
103
|
+
----------
|
|
104
|
+
coord : Coord
|
|
105
|
+
The coordinate system.
|
|
106
|
+
coord_params : dict
|
|
107
|
+
Parameters populated by ``Coord.setup_params()``.
|
|
108
|
+
facet : Facet
|
|
109
|
+
The faceting specification.
|
|
110
|
+
facet_params : dict
|
|
111
|
+
Parameters populated by ``Facet.setup_params()``.
|
|
112
|
+
layout : DataFrame
|
|
113
|
+
One row per panel with columns ``PANEL``, ``ROW``, ``COL``,
|
|
114
|
+
``SCALE_X``, ``SCALE_Y``, and possibly faceting variables.
|
|
115
|
+
panel_scales_x : list of Scale
|
|
116
|
+
Per-panel x scales (indexed by ``SCALE_X``).
|
|
117
|
+
panel_scales_y : list of Scale
|
|
118
|
+
Per-panel y scales (indexed by ``SCALE_Y``).
|
|
119
|
+
panel_params : list of dict
|
|
120
|
+
Per-panel coordinate parameters.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
_class_name = "Layout"
|
|
124
|
+
|
|
125
|
+
coord: Any = None
|
|
126
|
+
coord_params: Dict[str, Any] = {}
|
|
127
|
+
facet: Any = None
|
|
128
|
+
facet_params: Dict[str, Any] = {}
|
|
129
|
+
layout: Optional[pd.DataFrame] = None
|
|
130
|
+
panel_scales_x: Optional[List[Any]] = None
|
|
131
|
+
panel_scales_y: Optional[List[Any]] = None
|
|
132
|
+
panel_params: Optional[List[Dict[str, Any]]] = None
|
|
133
|
+
|
|
134
|
+
# ------------------------------------------------------------------
|
|
135
|
+
# ggplot_build phase
|
|
136
|
+
# ------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
def setup(
|
|
139
|
+
self,
|
|
140
|
+
data: List[pd.DataFrame],
|
|
141
|
+
plot_data: pd.DataFrame = None,
|
|
142
|
+
plot_env: Any = None,
|
|
143
|
+
) -> List[pd.DataFrame]:
|
|
144
|
+
"""Initialise facet layout and assign panels to data.
|
|
145
|
+
|
|
146
|
+
Parameters
|
|
147
|
+
----------
|
|
148
|
+
data : list of DataFrame
|
|
149
|
+
Layer data (one DataFrame per layer).
|
|
150
|
+
plot_data : DataFrame, optional
|
|
151
|
+
The plot-level default data.
|
|
152
|
+
plot_env : object, optional
|
|
153
|
+
The plot environment (unused in Python).
|
|
154
|
+
|
|
155
|
+
Returns
|
|
156
|
+
-------
|
|
157
|
+
list of DataFrame
|
|
158
|
+
Layer data with ``PANEL`` column assigned.
|
|
159
|
+
"""
|
|
160
|
+
if plot_data is None:
|
|
161
|
+
plot_data = pd.DataFrame()
|
|
162
|
+
|
|
163
|
+
all_data = [plot_data] + list(data)
|
|
164
|
+
|
|
165
|
+
# Setup facet
|
|
166
|
+
if hasattr(self.facet, "setup_params"):
|
|
167
|
+
self.facet_params = self.facet.setup_params(
|
|
168
|
+
all_data, getattr(self.facet, "params", {})
|
|
169
|
+
)
|
|
170
|
+
else:
|
|
171
|
+
self.facet_params = getattr(self.facet, "params", {})
|
|
172
|
+
|
|
173
|
+
if plot_env is not None:
|
|
174
|
+
self.facet_params["plot_env"] = plot_env
|
|
175
|
+
|
|
176
|
+
if hasattr(self.facet, "setup_data"):
|
|
177
|
+
all_data = self.facet.setup_data(all_data, self.facet_params)
|
|
178
|
+
|
|
179
|
+
# Setup coord
|
|
180
|
+
if hasattr(self.coord, "setup_params"):
|
|
181
|
+
self.coord_params = self.coord.setup_params(all_data)
|
|
182
|
+
else:
|
|
183
|
+
self.coord_params = {}
|
|
184
|
+
|
|
185
|
+
if hasattr(self.coord, "setup_data"):
|
|
186
|
+
all_data = self.coord.setup_data(all_data, self.coord_params)
|
|
187
|
+
|
|
188
|
+
# Generate panel layout
|
|
189
|
+
if hasattr(self.facet, "compute_layout"):
|
|
190
|
+
self.layout = self.facet.compute_layout(all_data, self.facet_params)
|
|
191
|
+
else:
|
|
192
|
+
self.layout = pd.DataFrame({
|
|
193
|
+
"PANEL": pd.Categorical([1]),
|
|
194
|
+
"ROW": [1],
|
|
195
|
+
"COL": [1],
|
|
196
|
+
"SCALE_X": [1],
|
|
197
|
+
"SCALE_Y": [1],
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
if hasattr(self.coord, "setup_layout"):
|
|
201
|
+
self.layout = self.coord.setup_layout(self.layout, self.coord_params)
|
|
202
|
+
|
|
203
|
+
# Add COORD column if not present (used for deduplicating panel_params)
|
|
204
|
+
if "COORD" not in self.layout.columns:
|
|
205
|
+
# Default: unique combination of SCALE_X and SCALE_Y
|
|
206
|
+
self.layout["COORD"] = (
|
|
207
|
+
self.layout["SCALE_X"].astype(str) + "_" +
|
|
208
|
+
self.layout["SCALE_Y"].astype(str)
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Map data to panels
|
|
212
|
+
result = []
|
|
213
|
+
for layer_data in all_data[1:]: # skip plot_data (index 0)
|
|
214
|
+
if hasattr(self.facet, "map_data"):
|
|
215
|
+
mapped = self.facet.map_data(
|
|
216
|
+
layer_data,
|
|
217
|
+
layout=self.layout,
|
|
218
|
+
params=self.facet_params,
|
|
219
|
+
)
|
|
220
|
+
result.append(mapped)
|
|
221
|
+
else:
|
|
222
|
+
# Default: assign all rows to panel 1
|
|
223
|
+
ld = layer_data.copy()
|
|
224
|
+
if "PANEL" not in ld.columns:
|
|
225
|
+
ld["PANEL"] = pd.Categorical(
|
|
226
|
+
[1] * len(ld),
|
|
227
|
+
categories=self.layout["PANEL"].cat.categories
|
|
228
|
+
if hasattr(self.layout["PANEL"], "cat") else [1],
|
|
229
|
+
)
|
|
230
|
+
result.append(ld)
|
|
231
|
+
return result
|
|
232
|
+
|
|
233
|
+
def train_position(
|
|
234
|
+
self,
|
|
235
|
+
data: List[pd.DataFrame],
|
|
236
|
+
x_scale: Any,
|
|
237
|
+
y_scale: Any,
|
|
238
|
+
) -> None:
|
|
239
|
+
"""Train position scales for each panel.
|
|
240
|
+
|
|
241
|
+
Parameters
|
|
242
|
+
----------
|
|
243
|
+
data : list of DataFrame
|
|
244
|
+
Layer data.
|
|
245
|
+
x_scale, y_scale : Scale
|
|
246
|
+
Prototype position scales.
|
|
247
|
+
"""
|
|
248
|
+
layout = self.layout
|
|
249
|
+
|
|
250
|
+
# Initialise scales if needed
|
|
251
|
+
if self.panel_scales_x is None and x_scale is not None:
|
|
252
|
+
if hasattr(self.facet, "init_scales"):
|
|
253
|
+
res = self.facet.init_scales(
|
|
254
|
+
layout, x_scale=x_scale, params=self.facet_params
|
|
255
|
+
)
|
|
256
|
+
self.panel_scales_x = res.get("x", [x_scale.clone()])
|
|
257
|
+
else:
|
|
258
|
+
n_x = int(layout["SCALE_X"].max()) if len(layout) > 0 else 1
|
|
259
|
+
self.panel_scales_x = [x_scale.clone() for _ in range(n_x)]
|
|
260
|
+
|
|
261
|
+
if self.panel_scales_y is None and y_scale is not None:
|
|
262
|
+
if hasattr(self.facet, "init_scales"):
|
|
263
|
+
res = self.facet.init_scales(
|
|
264
|
+
layout, y_scale=y_scale, params=self.facet_params
|
|
265
|
+
)
|
|
266
|
+
self.panel_scales_y = res.get("y", [y_scale.clone()])
|
|
267
|
+
else:
|
|
268
|
+
n_y = int(layout["SCALE_Y"].max()) if len(layout) > 0 else 1
|
|
269
|
+
self.panel_scales_y = [y_scale.clone() for _ in range(n_y)]
|
|
270
|
+
|
|
271
|
+
# Train scales
|
|
272
|
+
if hasattr(self.facet, "train_scales"):
|
|
273
|
+
self.facet.train_scales(
|
|
274
|
+
self.panel_scales_x,
|
|
275
|
+
self.panel_scales_y,
|
|
276
|
+
layout,
|
|
277
|
+
data,
|
|
278
|
+
self.facet_params,
|
|
279
|
+
)
|
|
280
|
+
else:
|
|
281
|
+
# Default training: train each scale on matching panel data
|
|
282
|
+
for layer_data in data:
|
|
283
|
+
if layer_data is None or layer_data.empty:
|
|
284
|
+
continue
|
|
285
|
+
if "PANEL" not in layer_data.columns:
|
|
286
|
+
continue
|
|
287
|
+
for _, row in layout.iterrows():
|
|
288
|
+
panel_id = row["PANEL"]
|
|
289
|
+
sx_idx = int(row["SCALE_X"]) - 1
|
|
290
|
+
sy_idx = int(row["SCALE_Y"]) - 1
|
|
291
|
+
mask = layer_data["PANEL"] == panel_id
|
|
292
|
+
panel_data = layer_data.loc[mask]
|
|
293
|
+
if panel_data.empty:
|
|
294
|
+
continue
|
|
295
|
+
if self.panel_scales_x and sx_idx < len(self.panel_scales_x):
|
|
296
|
+
self.panel_scales_x[sx_idx].train_df(panel_data)
|
|
297
|
+
if self.panel_scales_y and sy_idx < len(self.panel_scales_y):
|
|
298
|
+
self.panel_scales_y[sy_idx].train_df(panel_data)
|
|
299
|
+
|
|
300
|
+
def map_position(self, data: List[pd.DataFrame]) -> List[pd.DataFrame]:
|
|
301
|
+
"""Map position aesthetics through trained panel scales.
|
|
302
|
+
|
|
303
|
+
Parameters
|
|
304
|
+
----------
|
|
305
|
+
data : list of DataFrame
|
|
306
|
+
Layer data.
|
|
307
|
+
|
|
308
|
+
Returns
|
|
309
|
+
-------
|
|
310
|
+
list of DataFrame
|
|
311
|
+
Data with mapped position columns.
|
|
312
|
+
"""
|
|
313
|
+
layout = self.layout
|
|
314
|
+
result = []
|
|
315
|
+
|
|
316
|
+
for layer_data in data:
|
|
317
|
+
if layer_data is None or layer_data.empty:
|
|
318
|
+
result.append(layer_data)
|
|
319
|
+
continue
|
|
320
|
+
|
|
321
|
+
ld = layer_data.copy()
|
|
322
|
+
|
|
323
|
+
if "PANEL" not in ld.columns:
|
|
324
|
+
result.append(ld)
|
|
325
|
+
continue
|
|
326
|
+
|
|
327
|
+
# Match panels
|
|
328
|
+
panel_vals = ld["PANEL"].values
|
|
329
|
+
# Build match index: for each row, which layout row?
|
|
330
|
+
layout_panels = layout["PANEL"].values
|
|
331
|
+
match_id = np.searchsorted(
|
|
332
|
+
np.sort(layout_panels),
|
|
333
|
+
panel_vals,
|
|
334
|
+
)
|
|
335
|
+
# Safer: use a mapping
|
|
336
|
+
panel_to_idx = {p: i for i, p in enumerate(layout_panels)}
|
|
337
|
+
match_idx = np.array([
|
|
338
|
+
panel_to_idx.get(p, 0) for p in panel_vals
|
|
339
|
+
])
|
|
340
|
+
|
|
341
|
+
# Map x variables
|
|
342
|
+
if self.panel_scales_x and len(self.panel_scales_x) > 0:
|
|
343
|
+
x_aes = getattr(self.panel_scales_x[0], "aesthetics", ["x"])
|
|
344
|
+
x_vars = [v for v in x_aes if v in ld.columns]
|
|
345
|
+
if x_vars:
|
|
346
|
+
scale_x_ids = layout["SCALE_X"].values[match_idx]
|
|
347
|
+
mapped = _scale_apply(
|
|
348
|
+
ld, x_vars, "map", pd.Series(scale_x_ids),
|
|
349
|
+
self.panel_scales_x,
|
|
350
|
+
)
|
|
351
|
+
for k, v in mapped.items():
|
|
352
|
+
ld[k] = v
|
|
353
|
+
|
|
354
|
+
# Map y variables
|
|
355
|
+
if self.panel_scales_y and len(self.panel_scales_y) > 0:
|
|
356
|
+
y_aes = getattr(self.panel_scales_y[0], "aesthetics", ["y"])
|
|
357
|
+
y_vars = [v for v in y_aes if v in ld.columns]
|
|
358
|
+
if y_vars:
|
|
359
|
+
scale_y_ids = layout["SCALE_Y"].values[match_idx]
|
|
360
|
+
mapped = _scale_apply(
|
|
361
|
+
ld, y_vars, "map", pd.Series(scale_y_ids),
|
|
362
|
+
self.panel_scales_y,
|
|
363
|
+
)
|
|
364
|
+
for k, v in mapped.items():
|
|
365
|
+
ld[k] = v
|
|
366
|
+
|
|
367
|
+
result.append(ld)
|
|
368
|
+
return result
|
|
369
|
+
|
|
370
|
+
def reset_scales(self) -> None:
|
|
371
|
+
"""Reset scale ranges (called between stat computation and re-training).
|
|
372
|
+
|
|
373
|
+
If the facet's ``shrink`` attribute is ``False``, this is a no-op.
|
|
374
|
+
"""
|
|
375
|
+
if not getattr(self.facet, "shrink", True):
|
|
376
|
+
return
|
|
377
|
+
if self.panel_scales_x:
|
|
378
|
+
for s in self.panel_scales_x:
|
|
379
|
+
if hasattr(s, "reset"):
|
|
380
|
+
s.reset()
|
|
381
|
+
if self.panel_scales_y:
|
|
382
|
+
for s in self.panel_scales_y:
|
|
383
|
+
if hasattr(s, "reset"):
|
|
384
|
+
s.reset()
|
|
385
|
+
|
|
386
|
+
def setup_panel_params(self) -> None:
|
|
387
|
+
"""Compute per-panel coordinate parameters.
|
|
388
|
+
|
|
389
|
+
Calls ``Coord.setup_panel_params()`` for each unique x/y scale
|
|
390
|
+
combination.
|
|
391
|
+
"""
|
|
392
|
+
if hasattr(self.coord, "modify_scales"):
|
|
393
|
+
self.coord.modify_scales(self.panel_scales_x, self.panel_scales_y)
|
|
394
|
+
|
|
395
|
+
layout = self.layout
|
|
396
|
+
n_panels = len(layout)
|
|
397
|
+
params_list: List[Dict[str, Any]] = []
|
|
398
|
+
|
|
399
|
+
# Deduplicate by COORD column if available
|
|
400
|
+
if "COORD" in layout.columns:
|
|
401
|
+
unique_coords = layout["COORD"].unique()
|
|
402
|
+
coord_to_params: Dict[Any, Dict[str, Any]] = {}
|
|
403
|
+
for uc in unique_coords:
|
|
404
|
+
row = layout.loc[layout["COORD"] == uc].iloc[0]
|
|
405
|
+
sx_idx = int(row["SCALE_X"]) - 1
|
|
406
|
+
sy_idx = int(row["SCALE_Y"]) - 1
|
|
407
|
+
sx = self.panel_scales_x[sx_idx] if self.panel_scales_x else None
|
|
408
|
+
sy = self.panel_scales_y[sy_idx] if self.panel_scales_y else None
|
|
409
|
+
if hasattr(self.coord, "setup_panel_params"):
|
|
410
|
+
pp = self.coord.setup_panel_params(
|
|
411
|
+
sx, sy, params=self.coord_params,
|
|
412
|
+
)
|
|
413
|
+
else:
|
|
414
|
+
pp = {}
|
|
415
|
+
coord_to_params[uc] = pp
|
|
416
|
+
|
|
417
|
+
# Expand to all panels
|
|
418
|
+
for _, row in layout.iterrows():
|
|
419
|
+
params_list.append(coord_to_params[row["COORD"]])
|
|
420
|
+
else:
|
|
421
|
+
for _, row in layout.iterrows():
|
|
422
|
+
sx_idx = int(row["SCALE_X"]) - 1
|
|
423
|
+
sy_idx = int(row["SCALE_Y"]) - 1
|
|
424
|
+
sx = self.panel_scales_x[sx_idx] if self.panel_scales_x else None
|
|
425
|
+
sy = self.panel_scales_y[sy_idx] if self.panel_scales_y else None
|
|
426
|
+
if hasattr(self.coord, "setup_panel_params"):
|
|
427
|
+
pp = self.coord.setup_panel_params(
|
|
428
|
+
sx, sy, params=self.coord_params,
|
|
429
|
+
)
|
|
430
|
+
else:
|
|
431
|
+
pp = {}
|
|
432
|
+
params_list.append(pp)
|
|
433
|
+
|
|
434
|
+
# Let facet modify panel_params
|
|
435
|
+
if hasattr(self.facet, "setup_panel_params"):
|
|
436
|
+
params_list = self.facet.setup_panel_params(params_list, self.coord)
|
|
437
|
+
|
|
438
|
+
self.panel_params = params_list
|
|
439
|
+
|
|
440
|
+
def setup_panel_guides(self, guides: Any, layers: List[Any]) -> None:
|
|
441
|
+
"""Set up and train position guides (axes) per panel.
|
|
442
|
+
|
|
443
|
+
Parameters
|
|
444
|
+
----------
|
|
445
|
+
guides : Guides
|
|
446
|
+
The plot's guides specification.
|
|
447
|
+
layers : list
|
|
448
|
+
Plot layers.
|
|
449
|
+
"""
|
|
450
|
+
if self.panel_params is None:
|
|
451
|
+
return
|
|
452
|
+
|
|
453
|
+
# Setup guides
|
|
454
|
+
if hasattr(self.coord, "setup_panel_guides"):
|
|
455
|
+
self.panel_params = [
|
|
456
|
+
self.coord.setup_panel_guides(
|
|
457
|
+
pp, guides, self.coord_params,
|
|
458
|
+
)
|
|
459
|
+
for pp in self.panel_params
|
|
460
|
+
]
|
|
461
|
+
|
|
462
|
+
# Train guides
|
|
463
|
+
if hasattr(self.coord, "train_panel_guides"):
|
|
464
|
+
self.panel_params = [
|
|
465
|
+
self.coord.train_panel_guides(
|
|
466
|
+
pp, layers, self.coord_params,
|
|
467
|
+
)
|
|
468
|
+
for pp in self.panel_params
|
|
469
|
+
]
|
|
470
|
+
|
|
471
|
+
def finish_data(self, data: List[pd.DataFrame]) -> List[pd.DataFrame]:
|
|
472
|
+
"""Apply facet's ``finish_data()`` hook.
|
|
473
|
+
|
|
474
|
+
Parameters
|
|
475
|
+
----------
|
|
476
|
+
data : list of DataFrame
|
|
477
|
+
Layer data.
|
|
478
|
+
|
|
479
|
+
Returns
|
|
480
|
+
-------
|
|
481
|
+
list of DataFrame
|
|
482
|
+
"""
|
|
483
|
+
if hasattr(self.facet, "finish_data"):
|
|
484
|
+
return [
|
|
485
|
+
self.facet.finish_data(
|
|
486
|
+
d,
|
|
487
|
+
layout=self.layout,
|
|
488
|
+
x_scales=self.panel_scales_x,
|
|
489
|
+
y_scales=self.panel_scales_y,
|
|
490
|
+
params=self.facet_params,
|
|
491
|
+
)
|
|
492
|
+
for d in data
|
|
493
|
+
]
|
|
494
|
+
return data
|
|
495
|
+
|
|
496
|
+
# ------------------------------------------------------------------
|
|
497
|
+
# ggplot_gtable phase (render)
|
|
498
|
+
# ------------------------------------------------------------------
|
|
499
|
+
|
|
500
|
+
def render(
|
|
501
|
+
self,
|
|
502
|
+
panels: List[Any],
|
|
503
|
+
data: List[pd.DataFrame],
|
|
504
|
+
theme: Any,
|
|
505
|
+
labels: Dict[str, Any],
|
|
506
|
+
) -> Any:
|
|
507
|
+
"""Render panels, axes, and strips into a gtable.
|
|
508
|
+
|
|
509
|
+
Parameters
|
|
510
|
+
----------
|
|
511
|
+
panels : list
|
|
512
|
+
Geom grobs per layer (list of lists).
|
|
513
|
+
data : list of DataFrame
|
|
514
|
+
Layer data.
|
|
515
|
+
theme : Theme
|
|
516
|
+
Complete theme.
|
|
517
|
+
labels : dict
|
|
518
|
+
Plot labels.
|
|
519
|
+
|
|
520
|
+
Returns
|
|
521
|
+
-------
|
|
522
|
+
gtable
|
|
523
|
+
The assembled plot table.
|
|
524
|
+
"""
|
|
525
|
+
# Draw panel content
|
|
526
|
+
if hasattr(self.facet, "draw_panel_content"):
|
|
527
|
+
panels = self.facet.draw_panel_content(
|
|
528
|
+
panels,
|
|
529
|
+
self.layout,
|
|
530
|
+
self.panel_scales_x,
|
|
531
|
+
self.panel_scales_y,
|
|
532
|
+
self.panel_params,
|
|
533
|
+
self.coord,
|
|
534
|
+
data,
|
|
535
|
+
theme,
|
|
536
|
+
self.facet_params,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
# Draw panels into gtable
|
|
540
|
+
if hasattr(self.facet, "draw_panels"):
|
|
541
|
+
plot_table = self.facet.draw_panels(
|
|
542
|
+
panels,
|
|
543
|
+
self.layout,
|
|
544
|
+
self.panel_scales_x,
|
|
545
|
+
self.panel_scales_y,
|
|
546
|
+
self.panel_params,
|
|
547
|
+
self.coord,
|
|
548
|
+
data,
|
|
549
|
+
theme,
|
|
550
|
+
self.facet_params,
|
|
551
|
+
)
|
|
552
|
+
else:
|
|
553
|
+
# Minimal fallback
|
|
554
|
+
from gtable_py import Gtable
|
|
555
|
+
plot_table = Gtable()
|
|
556
|
+
|
|
557
|
+
# Set panel sizes
|
|
558
|
+
if hasattr(self.facet, "set_panel_size"):
|
|
559
|
+
plot_table = self.facet.set_panel_size(plot_table, theme)
|
|
560
|
+
|
|
561
|
+
# Resolve axis labels
|
|
562
|
+
resolved_labels = {}
|
|
563
|
+
if self.panel_scales_x and len(self.panel_scales_x) > 0:
|
|
564
|
+
resolved_labels["x"] = self.resolve_label(
|
|
565
|
+
self.panel_scales_x[0], labels,
|
|
566
|
+
)
|
|
567
|
+
if self.panel_scales_y and len(self.panel_scales_y) > 0:
|
|
568
|
+
resolved_labels["y"] = self.resolve_label(
|
|
569
|
+
self.panel_scales_y[0], labels,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
# Let coord modify labels
|
|
573
|
+
if hasattr(self.coord, "labels") and self.panel_params:
|
|
574
|
+
resolved_labels = self.coord.labels(
|
|
575
|
+
resolved_labels,
|
|
576
|
+
self.panel_params[0] if self.panel_params else {},
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
# Render label grobs
|
|
580
|
+
label_grobs = self.render_labels(resolved_labels, theme)
|
|
581
|
+
|
|
582
|
+
# Draw axis title labels via facet
|
|
583
|
+
if hasattr(self.facet, "draw_labels"):
|
|
584
|
+
plot_table = self.facet.draw_labels(
|
|
585
|
+
plot_table,
|
|
586
|
+
self.layout,
|
|
587
|
+
self.panel_scales_x,
|
|
588
|
+
self.panel_scales_y,
|
|
589
|
+
self.panel_params,
|
|
590
|
+
self.coord,
|
|
591
|
+
data,
|
|
592
|
+
theme,
|
|
593
|
+
label_grobs,
|
|
594
|
+
self.facet_params,
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
return plot_table
|
|
598
|
+
|
|
599
|
+
def resolve_label(
|
|
600
|
+
self,
|
|
601
|
+
scale: Any,
|
|
602
|
+
labels: Dict[str, Any],
|
|
603
|
+
) -> Dict[str, Any]:
|
|
604
|
+
"""Resolve axis titles from guides, scales, or plot labels.
|
|
605
|
+
|
|
606
|
+
Parameters
|
|
607
|
+
----------
|
|
608
|
+
scale : Scale
|
|
609
|
+
The position scale.
|
|
610
|
+
labels : dict
|
|
611
|
+
Plot labels dictionary.
|
|
612
|
+
|
|
613
|
+
Returns
|
|
614
|
+
-------
|
|
615
|
+
dict
|
|
616
|
+
``{"primary": ..., "secondary": ...}`` title dict.
|
|
617
|
+
"""
|
|
618
|
+
aes = scale.aesthetics[0] if scale.aesthetics else "x"
|
|
619
|
+
|
|
620
|
+
# From scale name
|
|
621
|
+
prim_scale = getattr(scale, "name", None)
|
|
622
|
+
seco_scale = getattr(scale, "sec_name", None)
|
|
623
|
+
if callable(seco_scale):
|
|
624
|
+
seco_scale = seco_scale()
|
|
625
|
+
|
|
626
|
+
# From plot labels
|
|
627
|
+
prim_label = labels.get(aes)
|
|
628
|
+
seco_label = labels.get(f"sec.{aes}")
|
|
629
|
+
|
|
630
|
+
# From scale's make_title
|
|
631
|
+
if hasattr(scale, "make_title"):
|
|
632
|
+
primary = scale.make_title(
|
|
633
|
+
prim_scale if not is_waiver(prim_scale) and prim_scale is not None
|
|
634
|
+
else prim_label
|
|
635
|
+
)
|
|
636
|
+
else:
|
|
637
|
+
primary = prim_scale if prim_scale is not None else prim_label
|
|
638
|
+
|
|
639
|
+
secondary = seco_scale if seco_scale is not None else seco_label
|
|
640
|
+
|
|
641
|
+
return {"primary": primary, "secondary": secondary}
|
|
642
|
+
|
|
643
|
+
def render_labels(
|
|
644
|
+
self,
|
|
645
|
+
labels: Dict[str, Any],
|
|
646
|
+
theme: Any,
|
|
647
|
+
) -> Dict[str, Any]:
|
|
648
|
+
"""Render axis title grobs.
|
|
649
|
+
|
|
650
|
+
Mirrors R's ``Layout$render_labels``: produces text grobs for
|
|
651
|
+
x-axis and y-axis titles. Falls back to a simple ``text_grob``
|
|
652
|
+
when theme ``element_render`` is unavailable.
|
|
653
|
+
|
|
654
|
+
Parameters
|
|
655
|
+
----------
|
|
656
|
+
labels : dict
|
|
657
|
+
Resolved labels keyed by ``"x"`` / ``"y"``, each
|
|
658
|
+
``{"primary": ..., "secondary": ...}``.
|
|
659
|
+
theme : Theme
|
|
660
|
+
Complete theme.
|
|
661
|
+
|
|
662
|
+
Returns
|
|
663
|
+
-------
|
|
664
|
+
dict
|
|
665
|
+
``{"x": [primary_grob, secondary_grob], "y": [...]}``
|
|
666
|
+
"""
|
|
667
|
+
from grid_py import null_grob, text_grob, Gpar
|
|
668
|
+
|
|
669
|
+
result: Dict[str, Any] = {}
|
|
670
|
+
for axis, label_pair in labels.items():
|
|
671
|
+
grobs = []
|
|
672
|
+
if not isinstance(label_pair, dict):
|
|
673
|
+
result[axis] = [null_grob(), null_grob()]
|
|
674
|
+
continue
|
|
675
|
+
for i, key in enumerate(["primary", "secondary"]):
|
|
676
|
+
val = label_pair.get(key)
|
|
677
|
+
if val is None or is_waiver(val):
|
|
678
|
+
grobs.append(null_grob())
|
|
679
|
+
else:
|
|
680
|
+
# R: element_render(theme, "axis.title.x.bottom", label=...,
|
|
681
|
+
# margin_x = label == "y", margin_y = label == "x")
|
|
682
|
+
from ggplot2_py.theme_elements import element_render as _el_render
|
|
683
|
+
pos = ".bottom" if axis == "x" else ".left"
|
|
684
|
+
if i == 1:
|
|
685
|
+
pos = ".top" if axis == "x" else ".right"
|
|
686
|
+
g = _el_render(
|
|
687
|
+
theme, f"axis.title.{axis}{pos}",
|
|
688
|
+
label=str(val),
|
|
689
|
+
margin_x=(axis == "y"),
|
|
690
|
+
margin_y=(axis == "x"),
|
|
691
|
+
)
|
|
692
|
+
grobs.append(g)
|
|
693
|
+
result[axis] = grobs
|
|
694
|
+
return result
|
|
695
|
+
|
|
696
|
+
# ------------------------------------------------------------------
|
|
697
|
+
# Utilities
|
|
698
|
+
# ------------------------------------------------------------------
|
|
699
|
+
|
|
700
|
+
def get_scales(self, i: int) -> Dict[str, Any]:
|
|
701
|
+
"""Get scales for panel *i*.
|
|
702
|
+
|
|
703
|
+
Parameters
|
|
704
|
+
----------
|
|
705
|
+
i : int
|
|
706
|
+
Panel index (1-based, matching ``PANEL`` column values).
|
|
707
|
+
|
|
708
|
+
Returns
|
|
709
|
+
-------
|
|
710
|
+
dict
|
|
711
|
+
``{"x": Scale, "y": Scale}`` for the requested panel.
|
|
712
|
+
"""
|
|
713
|
+
row = self.layout.loc[self.layout["PANEL"] == i]
|
|
714
|
+
if row.empty:
|
|
715
|
+
return {"x": None, "y": None}
|
|
716
|
+
row = row.iloc[0]
|
|
717
|
+
sx_idx = int(row["SCALE_X"]) - 1
|
|
718
|
+
sy_idx = int(row["SCALE_Y"]) - 1
|
|
719
|
+
return {
|
|
720
|
+
"x": self.panel_scales_x[sx_idx] if self.panel_scales_x and sx_idx < len(self.panel_scales_x) else None,
|
|
721
|
+
"y": self.panel_scales_y[sy_idx] if self.panel_scales_y and sy_idx < len(self.panel_scales_y) else None,
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
# ---------------------------------------------------------------------------
|
|
726
|
+
# Factory
|
|
727
|
+
# ---------------------------------------------------------------------------
|
|
728
|
+
|
|
729
|
+
def create_layout(
|
|
730
|
+
facet: Any,
|
|
731
|
+
coord: Any,
|
|
732
|
+
layout_cls: Any = None,
|
|
733
|
+
) -> Layout:
|
|
734
|
+
"""Create a :class:`Layout` instance for a plot.
|
|
735
|
+
|
|
736
|
+
Parameters
|
|
737
|
+
----------
|
|
738
|
+
facet : Facet
|
|
739
|
+
Faceting specification.
|
|
740
|
+
coord : Coord
|
|
741
|
+
Coordinate system.
|
|
742
|
+
layout_cls : type, optional
|
|
743
|
+
Layout subclass to use (defaults to :class:`Layout`).
|
|
744
|
+
|
|
745
|
+
Returns
|
|
746
|
+
-------
|
|
747
|
+
Layout
|
|
748
|
+
A configured layout instance.
|
|
749
|
+
"""
|
|
750
|
+
cls = layout_cls or Layout
|
|
751
|
+
obj = cls()
|
|
752
|
+
obj.facet = facet
|
|
753
|
+
obj.coord = coord
|
|
754
|
+
return obj
|