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
|
@@ -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
|