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.
Files changed (54) hide show
  1. ggplot2_py/__init__.py +852 -0
  2. ggplot2_py/_compat.py +475 -0
  3. ggplot2_py/_plugins.py +129 -0
  4. ggplot2_py/_utils.py +544 -0
  5. ggplot2_py/aes.py +586 -0
  6. ggplot2_py/annotation.py +540 -0
  7. ggplot2_py/coord.py +2108 -0
  8. ggplot2_py/coords/__init__.py +49 -0
  9. ggplot2_py/datasets.py +265 -0
  10. ggplot2_py/draw_key.py +454 -0
  11. ggplot2_py/facet.py +1456 -0
  12. ggplot2_py/fortify.py +95 -0
  13. ggplot2_py/geom.py +4516 -0
  14. ggplot2_py/geoms/__init__.py +12 -0
  15. ggplot2_py/ggproto.py +279 -0
  16. ggplot2_py/guide.py +2925 -0
  17. ggplot2_py/guide_axis.py +615 -0
  18. ggplot2_py/guide_colourbar.py +657 -0
  19. ggplot2_py/guide_legend.py +1061 -0
  20. ggplot2_py/guides/__init__.py +8 -0
  21. ggplot2_py/labeller.py +296 -0
  22. ggplot2_py/labels.py +309 -0
  23. ggplot2_py/layer.py +954 -0
  24. ggplot2_py/layout.py +754 -0
  25. ggplot2_py/limits.py +314 -0
  26. ggplot2_py/plot.py +1401 -0
  27. ggplot2_py/plot_render.py +866 -0
  28. ggplot2_py/position.py +1269 -0
  29. ggplot2_py/protocols.py +171 -0
  30. ggplot2_py/py.typed +0 -0
  31. ggplot2_py/qplot.py +233 -0
  32. ggplot2_py/resources/diamonds.csv +53941 -0
  33. ggplot2_py/resources/economics.csv +575 -0
  34. ggplot2_py/resources/economics_long.csv +2871 -0
  35. ggplot2_py/resources/faithfuld.csv +5626 -0
  36. ggplot2_py/resources/luv_colours.csv +658 -0
  37. ggplot2_py/resources/midwest.csv +438 -0
  38. ggplot2_py/resources/mpg.csv +235 -0
  39. ggplot2_py/resources/msleep.csv +84 -0
  40. ggplot2_py/resources/presidential.csv +13 -0
  41. ggplot2_py/resources/seals.csv +1156 -0
  42. ggplot2_py/resources/txhousing.csv +8603 -0
  43. ggplot2_py/save.py +316 -0
  44. ggplot2_py/scale.py +2727 -0
  45. ggplot2_py/scales/__init__.py +4252 -0
  46. ggplot2_py/stat.py +6071 -0
  47. ggplot2_py/stats/__init__.py +9 -0
  48. ggplot2_py/theme.py +490 -0
  49. ggplot2_py/theme_defaults.py +1350 -0
  50. ggplot2_py/theme_elements.py +2052 -0
  51. ggplot2_python-4.0.2.9000.dist-info/METADATA +179 -0
  52. ggplot2_python-4.0.2.9000.dist-info/RECORD +54 -0
  53. ggplot2_python-4.0.2.9000.dist-info/WHEEL +4 -0
  54. ggplot2_python-4.0.2.9000.dist-info/licenses/LICENSE +3 -0
@@ -0,0 +1,657 @@
1
+ """
2
+ Colourbar guide building functions — faithful port of R's GuideColourbar.
3
+
4
+ Produces a continuous colour gradient bar as an independent
5
+ :class:`~gtable_py.Gtable`, using ``raster_grob`` for the colour strip
6
+ and text/segment grobs for tick labels and tick marks.
7
+
8
+ R references
9
+ ------------
10
+ * ``ggplot2/R/guide-colorbar.R`` — GuideColourbar class
11
+ * ``ggplot2/R/guide-legend.R`` — inherited assemble_drawing
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import math
17
+ from typing import Any, Dict, List, Optional, Tuple
18
+
19
+ import numpy as np
20
+
21
+ from grid_py import (
22
+ Gpar,
23
+ Unit,
24
+ Viewport,
25
+ null_grob,
26
+ raster_grob,
27
+ rect_grob,
28
+ segments_grob,
29
+ text_grob,
30
+ unit_c,
31
+ )
32
+ from grid_py._grob import GList, GTree, grob_tree
33
+
34
+ from gtable_py import (
35
+ Gtable,
36
+ gtable_add_cols,
37
+ gtable_add_grob,
38
+ gtable_add_padding,
39
+ gtable_add_rows,
40
+ )
41
+
42
+ __all__ = [
43
+ "extract_colourbar_decor",
44
+ "extract_coloursteps_decor",
45
+ "build_colourbar_decor",
46
+ "build_coloursteps_decor",
47
+ "build_colourbar_labels",
48
+ "build_colourbar_ticks",
49
+ "assemble_colourbar",
50
+ ]
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # Constants (R defaults from GuideColourbar / theme)
54
+ # ---------------------------------------------------------------------------
55
+
56
+ _DEFAULT_NBIN: int = 300
57
+ _DEFAULT_BAR_WIDTH_CM: float = 0.5 # legend.key.width
58
+ _DEFAULT_BAR_HEIGHT_CM: float = 3.0 # legend.key.height * 5 (R multiplies)
59
+ _DEFAULT_LABEL_SIZE: float = 6.0
60
+ _DEFAULT_TITLE_SIZE: float = 7.0
61
+ _DEFAULT_PADDING_CM: float = 0.15
62
+ _DEFAULT_TICK_LENGTH_CM: float = 0.1
63
+
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # extract_colourbar_decor
67
+ # ---------------------------------------------------------------------------
68
+
69
+ def extract_colourbar_decor(
70
+ scale: Any,
71
+ nbin: int = _DEFAULT_NBIN,
72
+ alpha: Optional[float] = None,
73
+ reverse: bool = False,
74
+ ) -> Dict[str, Any]:
75
+ """Generate a dense colour sequence across the scale's limits.
76
+
77
+ Mirrors ``GuideColourbar$extract_decor`` (guide-colorbar.R:244-260).
78
+
79
+ Parameters
80
+ ----------
81
+ scale : Scale
82
+ A trained continuous colour/fill scale.
83
+ nbin : int
84
+ Number of colour bins.
85
+ alpha : float or None
86
+ Optional alpha override.
87
+ reverse : bool
88
+ Reverse the colour order.
89
+
90
+ Returns
91
+ -------
92
+ dict
93
+ ``{"colour": list[str], "value": ndarray}``
94
+ """
95
+ limits = scale.get_limits()
96
+ bar_values = np.linspace(limits[0], limits[1], nbin)
97
+ if len(bar_values) == 0:
98
+ bar_values = np.unique(limits)
99
+
100
+ # Map values through the scale to get colours
101
+ colours = scale.map(bar_values)
102
+ if isinstance(colours, np.ndarray):
103
+ colours = colours.tolist()
104
+
105
+ # Apply alpha if specified
106
+ if alpha is not None and not (isinstance(alpha, float) and np.isnan(alpha)):
107
+ try:
108
+ from scales import alpha as _scales_alpha
109
+ colours = [_scales_alpha(c, alpha) for c in colours]
110
+ except Exception:
111
+ pass
112
+
113
+ if reverse:
114
+ colours = list(reversed(colours))
115
+ bar_values = bar_values[::-1]
116
+
117
+ return {"colour": colours, "value": bar_values}
118
+
119
+
120
+ # ---------------------------------------------------------------------------
121
+ # extract_coloursteps_decor
122
+ # ---------------------------------------------------------------------------
123
+
124
+ def extract_coloursteps_decor(
125
+ scale: Any,
126
+ breaks: List[Any],
127
+ alpha: Optional[float] = None,
128
+ reverse: bool = False,
129
+ even_steps: bool = True,
130
+ ) -> Dict[str, Any]:
131
+ """Generate discrete colour bins from a binned scale's breaks.
132
+
133
+ Mirrors ``GuideColoursteps$extract_decor`` (guide-colorsteps.R:137-159).
134
+
135
+ Instead of a dense colour sequence, produces one colour per bin
136
+ between consecutive breaks.
137
+
138
+ Parameters
139
+ ----------
140
+ scale : Scale
141
+ A trained binned colour/fill scale.
142
+ breaks : list
143
+ Scale breaks (bin boundaries).
144
+ alpha : float or None
145
+ Optional alpha override.
146
+ reverse : bool
147
+ Reverse the colour order.
148
+ even_steps : bool
149
+ If ``True``, bins have equal visual width.
150
+
151
+ Returns
152
+ -------
153
+ dict
154
+ ``{"colour": list[str], "min": list[float], "max": list[float]}``
155
+ """
156
+ limits = scale.get_limits()
157
+ # Combine limits and breaks into sorted unique boundary set
158
+ boundaries = sorted(set(list(limits) + [float(b) for b in breaks
159
+ if b is not None and not
160
+ (isinstance(b, float) and np.isnan(b))]))
161
+ n = len(boundaries)
162
+ if n < 2:
163
+ return {"colour": [], "min": [], "max": []}
164
+
165
+ # Bin midpoints: map each mid value to get the bin colour
166
+ bin_mids = [(boundaries[i] + boundaries[i + 1]) / 2.0 for i in range(n - 1)]
167
+ colours = scale.map(np.array(bin_mids))
168
+ if isinstance(colours, np.ndarray):
169
+ colours = colours.tolist()
170
+
171
+ # Apply alpha
172
+ if alpha is not None and not (isinstance(alpha, float) and np.isnan(alpha)):
173
+ try:
174
+ from scales import alpha as _scales_alpha
175
+ colours = [_scales_alpha(c, alpha) for c in colours]
176
+ except Exception:
177
+ pass
178
+
179
+ # Even steps: use integer indices instead of actual break values
180
+ if even_steps:
181
+ mins = list(range(n - 1))
182
+ maxs = list(range(1, n))
183
+ else:
184
+ mins = boundaries[:-1]
185
+ maxs = boundaries[1:]
186
+
187
+ if reverse:
188
+ colours = list(reversed(colours))
189
+ mins = list(reversed(mins))
190
+ maxs = list(reversed(maxs))
191
+
192
+ return {"colour": colours, "min": mins, "max": maxs}
193
+
194
+
195
+ def build_coloursteps_decor(
196
+ decor: Dict[str, Any],
197
+ direction: str = "vertical",
198
+ ) -> Dict[str, Any]:
199
+ """Build stepped rectangle grobs for coloursteps guide.
200
+
201
+ Mirrors ``GuideColoursteps$build_decor`` (guide-colorsteps.R:208-229).
202
+
203
+ Parameters
204
+ ----------
205
+ decor : dict
206
+ From :func:`extract_coloursteps_decor`.
207
+ direction : str
208
+ ``"vertical"`` or ``"horizontal"``.
209
+
210
+ Returns
211
+ -------
212
+ dict
213
+ ``{"bar": grob, "frame": grob}``
214
+ """
215
+ colours = decor["colour"]
216
+ mins = decor["min"]
217
+ maxs = decor["max"]
218
+ n = len(colours)
219
+
220
+ if n == 0:
221
+ return {"bar": null_grob(), "frame": null_grob()}
222
+
223
+ # Normalise positions to [0, 1]
224
+ all_vals = mins + maxs
225
+ lo = min(all_vals)
226
+ hi = max(all_vals)
227
+ rng = hi - lo if hi != lo else 1.0
228
+
229
+ norm_mins = [(v - lo) / rng for v in mins]
230
+ norm_maxs = [(v - lo) / rng for v in maxs]
231
+ sizes = [mx - mn for mn, mx in zip(norm_mins, norm_maxs)]
232
+
233
+ if direction == "vertical":
234
+ bar = rect_grob(
235
+ x=[0.0] * n, y=norm_mins,
236
+ width=[1.0] * n, height=sizes,
237
+ just=("left", "bottom"),
238
+ default_units="npc",
239
+ gp=Gpar(col=None, fill=colours),
240
+ name="coloursteps.bar",
241
+ )
242
+ else:
243
+ bar = rect_grob(
244
+ x=norm_mins, y=[0.0] * n,
245
+ width=sizes, height=[1.0] * n,
246
+ just=("left", "bottom"),
247
+ default_units="npc",
248
+ gp=Gpar(col=None, fill=colours),
249
+ name="coloursteps.bar",
250
+ )
251
+
252
+ frame = rect_grob(
253
+ gp=Gpar(col="grey50", fill=None, lwd=0.5),
254
+ name="coloursteps.frame",
255
+ )
256
+
257
+ return {"bar": bar, "frame": frame}
258
+
259
+
260
+ # ---------------------------------------------------------------------------
261
+ # build_colourbar_decor
262
+ # ---------------------------------------------------------------------------
263
+
264
+ def build_colourbar_decor(
265
+ decor: Dict[str, Any],
266
+ direction: str = "vertical",
267
+ display: str = "raster",
268
+ ) -> Dict[str, Any]:
269
+ """Build the colour bar grob.
270
+
271
+ Mirrors ``GuideColourbar$build_decor`` (guide-colorbar.R:360-413).
272
+
273
+ Supports two display modes:
274
+ - ``"raster"``: a single ``raster_grob`` with interpolated colours
275
+ (default, matches R's default)
276
+ - ``"rectangles"``: individual ``rect_grob`` for each colour bin
277
+
278
+ The ``"gradient"`` mode (using ``linearGradient``) is deferred pending
279
+ grid_py gradient support.
280
+
281
+ Parameters
282
+ ----------
283
+ decor : dict
284
+ From :func:`extract_colourbar_decor`.
285
+ direction : str
286
+ ``"vertical"`` or ``"horizontal"``.
287
+ display : str
288
+ ``"raster"`` or ``"rectangles"``.
289
+
290
+ Returns
291
+ -------
292
+ dict
293
+ ``{"bar": grob, "frame": grob}``
294
+ """
295
+ colours = decor["colour"]
296
+ n = len(colours)
297
+
298
+ if display == "raster":
299
+ # Build a raster image from the colour array
300
+ # R: rasterGrob(image, width=1, height=1, default.units="npc",
301
+ # interpolate=TRUE)
302
+ if direction == "horizontal":
303
+ # 1-row image, colours left to right
304
+ image = np.array([colours], dtype=object) # shape (1, n)
305
+ else:
306
+ # n-row image (reversed for bottom-to-top), 1 column
307
+ image = np.array([[c] for c in reversed(colours)], dtype=object)
308
+
309
+ bar = raster_grob(
310
+ image=image,
311
+ x=0.5, y=0.5,
312
+ width=1, height=1,
313
+ default_units="npc",
314
+ interpolate=True,
315
+ gp=Gpar(col=None),
316
+ name="colourbar.bar",
317
+ )
318
+
319
+ elif display == "rectangles":
320
+ # Individual rectangles for each colour bin
321
+ # R: rectGrob(x, y, width, height, vjust=0, hjust=0, ...)
322
+ if direction == "horizontal":
323
+ w = 1.0 / n
324
+ xs = [(i * w) for i in range(n)]
325
+ bar = rect_grob(
326
+ x=xs, y=0,
327
+ width=w, height=1,
328
+ just=("left", "bottom"),
329
+ default_units="npc",
330
+ gp=Gpar(col=None, fill=colours),
331
+ name="colourbar.bar",
332
+ )
333
+ else:
334
+ h = 1.0 / n
335
+ ys = [(i * h) for i in range(n)]
336
+ bar = rect_grob(
337
+ x=0, y=ys,
338
+ width=1, height=h,
339
+ just=("left", "bottom"),
340
+ default_units="npc",
341
+ gp=Gpar(col=None, fill=colours),
342
+ name="colourbar.bar",
343
+ )
344
+ else:
345
+ # Fallback to raster
346
+ return build_colourbar_decor(decor, direction, display="raster")
347
+
348
+ # Frame around the bar
349
+ frame = rect_grob(
350
+ gp=Gpar(col="grey50", fill=None, lwd=0.5),
351
+ name="colourbar.frame",
352
+ )
353
+
354
+ return {"bar": bar, "frame": frame}
355
+
356
+
357
+ # ---------------------------------------------------------------------------
358
+ # build_colourbar_labels
359
+ # ---------------------------------------------------------------------------
360
+
361
+ def build_colourbar_labels(
362
+ breaks: List[Any],
363
+ break_labels: List[str],
364
+ limits: Tuple[float, float],
365
+ direction: str = "vertical",
366
+ label_size: float = _DEFAULT_LABEL_SIZE,
367
+ label_colour: str = "grey20",
368
+ ) -> List[Any]:
369
+ """Build tick labels positioned at break NPC positions along the bar.
370
+
371
+ Mirrors ``GuideColourbar$build_labels`` (guide-colorbar.R:327-341).
372
+
373
+ Parameters
374
+ ----------
375
+ breaks : list
376
+ Numeric break values.
377
+ break_labels : list of str
378
+ Formatted break labels.
379
+ limits : tuple of float
380
+ Scale limits (min, max).
381
+ direction : str
382
+ ``"vertical"`` or ``"horizontal"``.
383
+ label_size : float
384
+ Font size in points.
385
+ label_colour : str
386
+ Font colour.
387
+
388
+ Returns
389
+ -------
390
+ list of grob
391
+ One text grob per break.
392
+ """
393
+ lo, hi = limits
394
+ rng = hi - lo
395
+ if rng == 0:
396
+ rng = 1.0
397
+
398
+ # R (guide-colorbar.R: build_labels filters breaks to the visible
399
+ # bar via GuideColourbar$extract_key / limit clip). A break at
400
+ # e.g. 0 when limits=[1,14] lies at NPC = -0.077 and would render
401
+ # OUTSIDE the bar extent — both visually (overflowing the legend
402
+ # frame) and semantically (labelling a colour that isn't in the
403
+ # bar). Skip those breaks, matching R.
404
+ _EPS = 1e-9
405
+ grobs = []
406
+ for i, (brk, lab) in enumerate(zip(breaks, break_labels)):
407
+ try:
408
+ brk_val = float(brk)
409
+ except (TypeError, ValueError):
410
+ continue
411
+ npc_pos = (brk_val - lo) / rng
412
+ if npc_pos < -_EPS or npc_pos > 1 + _EPS:
413
+ continue
414
+
415
+ if direction == "vertical":
416
+ grobs.append(text_grob(
417
+ label=str(lab),
418
+ x=0.0, y=npc_pos,
419
+ just=("left", "centre"),
420
+ gp=Gpar(fontsize=label_size, col=label_colour),
421
+ name=f"colourbar.label.{i}",
422
+ ))
423
+ else:
424
+ grobs.append(text_grob(
425
+ label=str(lab),
426
+ x=npc_pos, y=0.0,
427
+ just=("centre", "top"),
428
+ gp=Gpar(fontsize=label_size, col=label_colour),
429
+ name=f"colourbar.label.{i}",
430
+ ))
431
+ return grobs
432
+
433
+
434
+ # ---------------------------------------------------------------------------
435
+ # build_colourbar_ticks
436
+ # ---------------------------------------------------------------------------
437
+
438
+ def build_colourbar_ticks(
439
+ breaks: List[Any],
440
+ limits: Tuple[float, float],
441
+ direction: str = "vertical",
442
+ draw_lim: Tuple[bool, bool] = (True, True),
443
+ tick_length_npc: float = 0.2,
444
+ tick_colour: str = "white",
445
+ tick_linewidth_mm: float = 0.5 / (72.27 / 25.4),
446
+ ) -> Any:
447
+ """Build tick marks at break positions.
448
+
449
+ Mirrors ``GuideColourbar$build_ticks`` + ``Guide$build_ticks``
450
+ (guide-colorbar.R:343-358, guide-.R:698-741). In R each tick
451
+ starts at the bar edge and extends **inward** by
452
+ ``length = rel(0.2) * legend.key.size`` — for a vertical bar
453
+ whose width matches the key size this is 0.2 npc of the bar's
454
+ parallel dimension. The default colour is **white** (from
455
+ ``default_ticks = element_line(colour="white", linewidth=0.5/.pt)``)
456
+ so the ticks are visible on any dark/light bar colour.
457
+
458
+ Parameters
459
+ ----------
460
+ breaks : list
461
+ Numeric break values.
462
+ limits : tuple of float
463
+ Scale limits.
464
+ direction : str
465
+ ``"vertical"`` or ``"horizontal"``.
466
+ draw_lim : tuple of bool
467
+ Whether to draw ticks at lower/upper limits.
468
+ tick_length_npc : float
469
+ Tick length as a fraction of the bar's orthogonal dimension
470
+ (0.2 = R default ``legend.ticks.length = rel(0.2)``).
471
+ tick_colour : str
472
+ Tick line colour (default ``"white"`` per R ``default_ticks``).
473
+ tick_linewidth : float
474
+ Tick line width in grid lwd units.
475
+ """
476
+ lo, hi = limits
477
+ rng = hi - lo
478
+ if rng == 0:
479
+ rng = 1.0
480
+
481
+ # Filter breaks to the visible bar range (R guide-colorbar.R:
482
+ # extract_key discards breaks outside [lo, hi]).
483
+ _EPS = 1e-9
484
+ positions = []
485
+ for brk in breaks:
486
+ try:
487
+ npc_pos = (float(brk) - lo) / rng
488
+ except (TypeError, ValueError):
489
+ continue
490
+ if npc_pos < -_EPS or npc_pos > 1 + _EPS:
491
+ continue
492
+ positions.append(npc_pos)
493
+
494
+ if not draw_lim[0] and positions:
495
+ positions = positions[1:]
496
+ if not draw_lim[1] and positions:
497
+ positions = positions[:-1]
498
+
499
+ if not positions:
500
+ return null_grob()
501
+
502
+ n = len(positions)
503
+ # R (utilities-grid.R:32-33 gg_par):
504
+ # args$lwd <- args$lwd * .pt
505
+ # element_line linewidth is in mm; grid lwd is in points. Multiply
506
+ # by .pt = 72.27/25.4 to convert. default_ticks linewidth=0.5/.pt
507
+ # mm → lwd = 0.5 pt (matches R's visible tick stroke).
508
+ _PT = 72.27 / 25.4
509
+ gp = Gpar(col=tick_colour, lwd=tick_linewidth_mm * _PT)
510
+
511
+ if direction == "vertical":
512
+ # Ticks on both sides, extending INWARD (R guide-.R:728-732):
513
+ # right side (position=1): dir = -length; segment = [1, 1-length]
514
+ # left side (position=0): dir = +length; segment = [0, 0+length]
515
+ x0_r = [1.0] * n
516
+ x1_r = [1.0 - tick_length_npc] * n
517
+ x0_l = [0.0] * n
518
+ x1_l = [0.0 + tick_length_npc] * n
519
+ return grob_tree(
520
+ segments_grob(x0=x0_l, y0=positions, x1=x1_l, y1=positions,
521
+ gp=gp, name="ticks.left"),
522
+ segments_grob(x0=x0_r, y0=positions, x1=x1_r, y1=positions,
523
+ gp=gp, name="ticks.right"),
524
+ name="colourbar.ticks",
525
+ )
526
+ else:
527
+ # Horizontal: ticks top & bottom, extending inward
528
+ y0_t = [1.0] * n
529
+ y1_t = [1.0 - tick_length_npc] * n
530
+ y0_b = [0.0] * n
531
+ y1_b = [0.0 + tick_length_npc] * n
532
+ return grob_tree(
533
+ segments_grob(x0=positions, y0=y0_b, x1=positions, y1=y1_b,
534
+ gp=gp, name="ticks.bottom"),
535
+ segments_grob(x0=positions, y0=y0_t, x1=positions, y1=y1_t,
536
+ gp=gp, name="ticks.top"),
537
+ name="colourbar.ticks",
538
+ )
539
+
540
+
541
+ # ---------------------------------------------------------------------------
542
+ # assemble_colourbar
543
+ # ---------------------------------------------------------------------------
544
+
545
+ def assemble_colourbar(
546
+ bar_grob: Any,
547
+ frame_grob: Any,
548
+ ticks_grob: Any,
549
+ label_grobs: List[Any],
550
+ title_grob: Any,
551
+ direction: str = "vertical",
552
+ bar_width_cm: float = _DEFAULT_BAR_WIDTH_CM,
553
+ bar_height_cm: float = _DEFAULT_BAR_HEIGHT_CM,
554
+ label_width_cm: float = 0.8,
555
+ padding_cm: float = _DEFAULT_PADDING_CM,
556
+ bg_colour: Optional[str] = "white",
557
+ ) -> Gtable:
558
+ """Assemble all colourbar parts into a Gtable.
559
+
560
+ Mirrors ``GuideColourbar`` using the inherited
561
+ ``GuideLegend$assemble_drawing`` pattern (guide-legend.R:533-591).
562
+
563
+ The layout for a vertical colourbar:
564
+ ```
565
+ [bar_col] [gap] [labels_col]
566
+ ```
567
+ The bar occupies a single tall cell; labels are stacked in the
568
+ adjacent column.
569
+
570
+ Parameters
571
+ ----------
572
+ bar_grob, frame_grob, ticks_grob : grob
573
+ Colour bar components.
574
+ label_grobs : list of grob
575
+ Tick label grobs.
576
+ title_grob : grob
577
+ Title grob.
578
+ direction : str
579
+ ``"vertical"`` or ``"horizontal"``.
580
+ bar_width_cm, bar_height_cm : float
581
+ Bar dimensions.
582
+ label_width_cm : float
583
+ Width for label column.
584
+ padding_cm : float
585
+ Padding.
586
+ bg_colour : str or None
587
+ Background fill.
588
+
589
+ Returns
590
+ -------
591
+ Gtable
592
+ Self-contained colourbar gtable.
593
+ """
594
+ gap_cm = 0.1
595
+
596
+ if direction == "vertical":
597
+ # Layout: [bar] [gap] [labels] — 1 row, 3 columns
598
+ widths = Unit([bar_width_cm, gap_cm, label_width_cm], "cm")
599
+ heights = Unit([bar_height_cm], "cm")
600
+ gt = Gtable(widths=widths, heights=heights, name="colourbar")
601
+
602
+ # Bar + frame + ticks in cell (1, 1)
603
+ bar_tree = GTree(
604
+ children=GList(bar_grob, frame_grob, ticks_grob),
605
+ name="colourbar.bar.tree",
606
+ )
607
+ gt = gtable_add_grob(gt, bar_tree, t=1, l=1, clip="off", name="bar")
608
+
609
+ # Labels in cell (1, 3)
610
+ if label_grobs:
611
+ label_tree = GTree(
612
+ children=GList(*label_grobs),
613
+ name="colourbar.labels",
614
+ )
615
+ gt = gtable_add_grob(gt, label_tree, t=1, l=3, clip="off", name="labels")
616
+
617
+ else:
618
+ # Horizontal: [labels] above [gap] above [bar]
619
+ # — 3 rows, 1 column (bar at bottom, labels on top)
620
+ widths = Unit([bar_height_cm], "cm") # bar length is "height" param
621
+ heights = Unit([label_width_cm, gap_cm, bar_width_cm], "cm")
622
+ gt = Gtable(widths=widths, heights=heights, name="colourbar")
623
+
624
+ bar_tree = GTree(
625
+ children=GList(bar_grob, frame_grob, ticks_grob),
626
+ name="colourbar.bar.tree",
627
+ )
628
+ gt = gtable_add_grob(gt, bar_tree, t=3, l=1, clip="off", name="bar")
629
+
630
+ if label_grobs:
631
+ label_tree = GTree(
632
+ children=GList(*label_grobs),
633
+ name="colourbar.labels",
634
+ )
635
+ gt = gtable_add_grob(gt, label_tree, t=1, l=1, clip="off", name="labels")
636
+
637
+ # Add title
638
+ from ggplot2_py.guide_legend import add_legend_title
639
+ gt = add_legend_title(gt, title_grob, position="top")
640
+
641
+ # Add padding
642
+ pad = Unit([padding_cm] * 4, "cm")
643
+ gt = gtable_add_padding(gt, pad)
644
+
645
+ # Add background
646
+ if bg_colour is not None:
647
+ bg = rect_grob(
648
+ gp=Gpar(fill=bg_colour, col="grey85", lwd=0.5),
649
+ name="colourbar.background",
650
+ )
651
+ gt = gtable_add_grob(
652
+ gt, bg,
653
+ t=1, l=1, b=gt.nrow, r=gt.ncol,
654
+ z=-math.inf, clip="off", name="background",
655
+ )
656
+
657
+ return gt