universal-render 0.1.0__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.
- universal_render/__init__.py +277 -0
- universal_render/backend.py +375 -0
- universal_render/compat.py +128 -0
- universal_render/diagnostics.py +249 -0
- universal_render/fonts.py +516 -0
- universal_render/highlevel.py +378 -0
- universal_render/layout.py +967 -0
- universal_render/mpl_support.py +1144 -0
- universal_render/renderer.py +806 -0
- universal_render/scripts.py +311 -0
- universal_render-0.1.0.dist-info/METADATA +189 -0
- universal_render-0.1.0.dist-info/RECORD +15 -0
- universal_render-0.1.0.dist-info/WHEEL +5 -0
- universal_render-0.1.0.dist-info/licenses/LICENSE +21 -0
- universal_render-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1144 @@
|
|
|
1
|
+
# universal_render/mpl_support.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Optional, Sequence, Tuple
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from matplotlib.lines import Line2D
|
|
8
|
+
from matplotlib.offsetbox import (
|
|
9
|
+
AnnotationBbox,
|
|
10
|
+
AnchoredOffsetbox,
|
|
11
|
+
DrawingArea,
|
|
12
|
+
HPacker,
|
|
13
|
+
OffsetImage,
|
|
14
|
+
VPacker,
|
|
15
|
+
)
|
|
16
|
+
from matplotlib.patches import Patch, Rectangle
|
|
17
|
+
|
|
18
|
+
from .layout import get_layout_manager
|
|
19
|
+
from .renderer import render_paragraph_qimage, render_text_qimage
|
|
20
|
+
from .scripts import to_native_numerals
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from PySide6.QtGui import QImage
|
|
25
|
+
except Exception: # pragma: no cover
|
|
26
|
+
QImage = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _qimage_to_rgba_array(qimg: QImage) -> np.ndarray:
|
|
30
|
+
"""
|
|
31
|
+
Convert a QImage to an RGBA numpy array in [0, 1].
|
|
32
|
+
"""
|
|
33
|
+
qimg = qimg.convertToFormat(QImage.Format.Format_RGBA8888)
|
|
34
|
+
w, h = qimg.width(), qimg.height()
|
|
35
|
+
|
|
36
|
+
ptr = qimg.bits()
|
|
37
|
+
buf = ptr.tobytes()
|
|
38
|
+
arr = np.frombuffer(buf, np.uint8).reshape((h, w, 4))
|
|
39
|
+
return arr / 255.0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _resolve_font_size(fontsize, font_size, default):
|
|
43
|
+
"""
|
|
44
|
+
Allow both Matplotlib-style `fontsize` and internal `font_size`.
|
|
45
|
+
Priority: explicit font_size > fontsize > default.
|
|
46
|
+
"""
|
|
47
|
+
if font_size is not None:
|
|
48
|
+
return font_size
|
|
49
|
+
if fontsize is not None:
|
|
50
|
+
return fontsize
|
|
51
|
+
return default
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _resolve_xycoords(ax, coord: str):
|
|
55
|
+
"""
|
|
56
|
+
Map a user-facing coord string to Matplotlib coordinates.
|
|
57
|
+
"""
|
|
58
|
+
coord = (coord or "data").lower()
|
|
59
|
+
if coord == "axes":
|
|
60
|
+
return ax.transAxes
|
|
61
|
+
if coord == "figure":
|
|
62
|
+
return ax.figure.transFigure
|
|
63
|
+
return ax.transData
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _alignment_to_box_alignment(
|
|
67
|
+
ha: str = "center",
|
|
68
|
+
va: str = "center",
|
|
69
|
+
) -> Tuple[float, float]:
|
|
70
|
+
ha_map = {
|
|
71
|
+
"center": 0.5,
|
|
72
|
+
"left": 0.0,
|
|
73
|
+
"right": 1.0,
|
|
74
|
+
}
|
|
75
|
+
va_map = {
|
|
76
|
+
"center": 0.5,
|
|
77
|
+
"middle": 0.5,
|
|
78
|
+
"bottom": 0.0,
|
|
79
|
+
"baseline": 0.0,
|
|
80
|
+
"top": 1.0,
|
|
81
|
+
}
|
|
82
|
+
return (
|
|
83
|
+
ha_map.get((ha or "center").lower(), 0.5),
|
|
84
|
+
va_map.get((va or "center").lower(), 0.5),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _default_zoom_for_fontsize(font_size: float) -> float:
|
|
89
|
+
# Rendered image height already scales with font_size, so zoom must
|
|
90
|
+
# stay constant: scaling it by font_size again squares the effect
|
|
91
|
+
# and makes small labels (ticks, legends) unreadably tiny.
|
|
92
|
+
return 0.40
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _build_offset_image(
|
|
96
|
+
text: str,
|
|
97
|
+
font_size: int,
|
|
98
|
+
font_family: Optional[str] = None,
|
|
99
|
+
font_path: Optional[str] = None,
|
|
100
|
+
color: str = "black",
|
|
101
|
+
bg: str = "transparent",
|
|
102
|
+
padding: int = 10,
|
|
103
|
+
scale: float = 1.0,
|
|
104
|
+
zoom: Optional[float] = None,
|
|
105
|
+
rotate_90: bool = False,
|
|
106
|
+
weight=None,
|
|
107
|
+
italic: bool = False,
|
|
108
|
+
alpha: float = 1.0,
|
|
109
|
+
rotation: float = 0.0,
|
|
110
|
+
):
|
|
111
|
+
qimg = render_text_qimage(
|
|
112
|
+
text=text,
|
|
113
|
+
font_family=font_family,
|
|
114
|
+
font_path=font_path,
|
|
115
|
+
font_size=font_size,
|
|
116
|
+
color=color,
|
|
117
|
+
bg=bg,
|
|
118
|
+
padding=padding,
|
|
119
|
+
scale=scale,
|
|
120
|
+
weight=weight,
|
|
121
|
+
italic=italic,
|
|
122
|
+
alpha=alpha,
|
|
123
|
+
rotation=rotation,
|
|
124
|
+
)
|
|
125
|
+
img = _qimage_to_rgba_array(qimg)
|
|
126
|
+
|
|
127
|
+
if rotate_90:
|
|
128
|
+
img = np.rot90(img, k=1)
|
|
129
|
+
|
|
130
|
+
if zoom is None:
|
|
131
|
+
zoom = _default_zoom_for_fontsize(font_size)
|
|
132
|
+
|
|
133
|
+
oi = OffsetImage(img, zoom=zoom)
|
|
134
|
+
return qimg, img, oi
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _build_paragraph_offset_image(
|
|
138
|
+
text: str,
|
|
139
|
+
width: int,
|
|
140
|
+
height: Optional[int] = None,
|
|
141
|
+
font_size: int = 18,
|
|
142
|
+
font_family: Optional[str] = None,
|
|
143
|
+
font_path: Optional[str] = None,
|
|
144
|
+
color: str = "black",
|
|
145
|
+
bg: str = "transparent",
|
|
146
|
+
margin: int = 12,
|
|
147
|
+
scale: float = 1.0,
|
|
148
|
+
zoom: Optional[float] = None,
|
|
149
|
+
):
|
|
150
|
+
qimg = render_paragraph_qimage(
|
|
151
|
+
text=text,
|
|
152
|
+
width=width,
|
|
153
|
+
height=height,
|
|
154
|
+
font_family=font_family,
|
|
155
|
+
font_path=font_path,
|
|
156
|
+
font_size=font_size,
|
|
157
|
+
color=color,
|
|
158
|
+
bg=bg,
|
|
159
|
+
margin=margin,
|
|
160
|
+
scale=scale,
|
|
161
|
+
)
|
|
162
|
+
img = _qimage_to_rgba_array(qimg)
|
|
163
|
+
|
|
164
|
+
if zoom is None:
|
|
165
|
+
zoom = _default_zoom_for_fontsize(font_size)
|
|
166
|
+
|
|
167
|
+
oi = OffsetImage(img, zoom=zoom)
|
|
168
|
+
return qimg, img, oi
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ---------------------------------------------------------------------
|
|
172
|
+
# Renderer-based legend helpers
|
|
173
|
+
# ---------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
def _normalize_legend_loc(loc):
|
|
176
|
+
"""
|
|
177
|
+
Convert matplotlib legend loc strings/ints to AnchoredOffsetbox loc values.
|
|
178
|
+
"""
|
|
179
|
+
mapping = {
|
|
180
|
+
"best": "upper right",
|
|
181
|
+
"upper right": "upper right",
|
|
182
|
+
"upper left": "upper left",
|
|
183
|
+
"lower left": "lower left",
|
|
184
|
+
"lower right": "lower right",
|
|
185
|
+
"right": "center right",
|
|
186
|
+
"center left": "center left",
|
|
187
|
+
"center right": "center right",
|
|
188
|
+
"lower center": "lower center",
|
|
189
|
+
"upper center": "upper center",
|
|
190
|
+
"center": "center",
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if isinstance(loc, str):
|
|
194
|
+
return mapping.get(loc.lower(), "upper right")
|
|
195
|
+
|
|
196
|
+
int_map = {
|
|
197
|
+
0: "upper right",
|
|
198
|
+
1: "upper right",
|
|
199
|
+
2: "upper left",
|
|
200
|
+
3: "lower left",
|
|
201
|
+
4: "lower right",
|
|
202
|
+
5: "right",
|
|
203
|
+
6: "center left",
|
|
204
|
+
7: "center right",
|
|
205
|
+
8: "lower center",
|
|
206
|
+
9: "upper center",
|
|
207
|
+
10: "center",
|
|
208
|
+
}
|
|
209
|
+
return int_map.get(loc, "upper right")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _build_legend_text_box(
|
|
213
|
+
text: str,
|
|
214
|
+
font_size: int,
|
|
215
|
+
font_family: Optional[str],
|
|
216
|
+
font_path: Optional[str],
|
|
217
|
+
color: str,
|
|
218
|
+
bg: str,
|
|
219
|
+
padding: int,
|
|
220
|
+
scale: float,
|
|
221
|
+
zoom: Optional[float],
|
|
222
|
+
):
|
|
223
|
+
_, _, oi = _build_offset_image(
|
|
224
|
+
text=text,
|
|
225
|
+
font_size=font_size,
|
|
226
|
+
font_family=font_family,
|
|
227
|
+
font_path=font_path,
|
|
228
|
+
color=color,
|
|
229
|
+
bg=bg,
|
|
230
|
+
padding=padding,
|
|
231
|
+
scale=scale,
|
|
232
|
+
zoom=zoom,
|
|
233
|
+
rotate_90=False,
|
|
234
|
+
)
|
|
235
|
+
return oi
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _build_line_handle_box(handle, width=34, height=18):
|
|
239
|
+
"""
|
|
240
|
+
Create a simple legend handle sample for Line2D.
|
|
241
|
+
"""
|
|
242
|
+
da = DrawingArea(width, height, 0, 0)
|
|
243
|
+
|
|
244
|
+
color = getattr(handle, "get_color", lambda: "black")()
|
|
245
|
+
linewidth = getattr(handle, "get_linewidth", lambda: 2.0)()
|
|
246
|
+
linestyle = getattr(handle, "get_linestyle", lambda: "-")()
|
|
247
|
+
marker = getattr(handle, "get_marker", lambda: None)()
|
|
248
|
+
markersize = getattr(handle, "get_markersize", lambda: 6.0)()
|
|
249
|
+
markerfacecolor = getattr(handle, "get_markerfacecolor", lambda: color)()
|
|
250
|
+
markeredgecolor = getattr(handle, "get_markeredgecolor", lambda: color)()
|
|
251
|
+
|
|
252
|
+
line = Line2D(
|
|
253
|
+
[2, width - 2],
|
|
254
|
+
[height / 2.0, height / 2.0],
|
|
255
|
+
color=color,
|
|
256
|
+
linewidth=linewidth,
|
|
257
|
+
linestyle=linestyle,
|
|
258
|
+
)
|
|
259
|
+
da.add_artist(line)
|
|
260
|
+
|
|
261
|
+
if marker not in (None, "", "None", " "):
|
|
262
|
+
mark = Line2D(
|
|
263
|
+
[width / 2.0],
|
|
264
|
+
[height / 2.0],
|
|
265
|
+
color=color,
|
|
266
|
+
marker=marker,
|
|
267
|
+
markersize=markersize,
|
|
268
|
+
linestyle="None",
|
|
269
|
+
markerfacecolor=markerfacecolor,
|
|
270
|
+
markeredgecolor=markeredgecolor,
|
|
271
|
+
)
|
|
272
|
+
da.add_artist(mark)
|
|
273
|
+
|
|
274
|
+
return da
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _build_patch_handle_box(handle, width=18, height=12):
|
|
278
|
+
"""
|
|
279
|
+
Create a simple legend handle sample for Patch-like artists.
|
|
280
|
+
"""
|
|
281
|
+
da = DrawingArea(width, height, 0, 0)
|
|
282
|
+
|
|
283
|
+
facecolor = getattr(handle, "get_facecolor", lambda: (0.7, 0.7, 0.7, 1.0))()
|
|
284
|
+
edgecolor = getattr(handle, "get_edgecolor", lambda: (0.0, 0.0, 0.0, 1.0))()
|
|
285
|
+
linewidth = getattr(handle, "get_linewidth", lambda: 1.0)()
|
|
286
|
+
|
|
287
|
+
if isinstance(facecolor, (list, tuple)) and len(facecolor) and isinstance(facecolor[0], (list, tuple, np.ndarray)):
|
|
288
|
+
facecolor = facecolor[0]
|
|
289
|
+
if isinstance(edgecolor, (list, tuple)) and len(edgecolor) and isinstance(edgecolor[0], (list, tuple, np.ndarray)):
|
|
290
|
+
edgecolor = edgecolor[0]
|
|
291
|
+
|
|
292
|
+
rect = Rectangle(
|
|
293
|
+
(1, 1),
|
|
294
|
+
width - 2,
|
|
295
|
+
height - 2,
|
|
296
|
+
facecolor=facecolor,
|
|
297
|
+
edgecolor=edgecolor,
|
|
298
|
+
linewidth=linewidth,
|
|
299
|
+
)
|
|
300
|
+
da.add_artist(rect)
|
|
301
|
+
return da
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _build_generic_handle_box(width=18, height=12):
|
|
305
|
+
da = DrawingArea(width, height, 0, 0)
|
|
306
|
+
rect = Rectangle(
|
|
307
|
+
(1, 1),
|
|
308
|
+
width - 2,
|
|
309
|
+
height - 2,
|
|
310
|
+
facecolor=(0.75, 0.75, 0.75, 1.0),
|
|
311
|
+
edgecolor=(0.0, 0.0, 0.0, 1.0),
|
|
312
|
+
linewidth=1.0,
|
|
313
|
+
)
|
|
314
|
+
da.add_artist(rect)
|
|
315
|
+
return da
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _build_legend_handle_box(handle):
|
|
319
|
+
if isinstance(handle, Line2D):
|
|
320
|
+
return _build_line_handle_box(handle)
|
|
321
|
+
if isinstance(handle, Patch):
|
|
322
|
+
return _build_patch_handle_box(handle)
|
|
323
|
+
return _build_generic_handle_box()
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def set_multilingual_legend(
|
|
327
|
+
ax,
|
|
328
|
+
labels,
|
|
329
|
+
handles=None,
|
|
330
|
+
loc="upper right",
|
|
331
|
+
frameon=True,
|
|
332
|
+
fontsize=None,
|
|
333
|
+
font_size=None,
|
|
334
|
+
font_family=None,
|
|
335
|
+
font_path=None,
|
|
336
|
+
color="black",
|
|
337
|
+
bg="transparent",
|
|
338
|
+
padding=8,
|
|
339
|
+
scale=1.0,
|
|
340
|
+
zoom=None,
|
|
341
|
+
title=None,
|
|
342
|
+
title_fontsize=None,
|
|
343
|
+
title_font_size=None,
|
|
344
|
+
title_color=None,
|
|
345
|
+
title_zoom=None,
|
|
346
|
+
borderpad=0.5,
|
|
347
|
+
labelspacing=0.35,
|
|
348
|
+
handletextpad=0.6,
|
|
349
|
+
borderaxespad=0.5,
|
|
350
|
+
facecolor="white",
|
|
351
|
+
edgecolor="0.8",
|
|
352
|
+
framealpha=0.95,
|
|
353
|
+
zorder=8,
|
|
354
|
+
):
|
|
355
|
+
"""
|
|
356
|
+
Renderer-based multilingual legend.
|
|
357
|
+
|
|
358
|
+
This version does not rely on native Matplotlib legend text rendering.
|
|
359
|
+
multilingual labels (and optional title) are rendered through render_text_qimage()
|
|
360
|
+
and packed into an AnchoredOffsetbox.
|
|
361
|
+
|
|
362
|
+
Parameters
|
|
363
|
+
----------
|
|
364
|
+
labels:
|
|
365
|
+
multilingual legend label strings.
|
|
366
|
+
handles:
|
|
367
|
+
Optional artist handles. If None, uses ax.get_legend_handles_labels().
|
|
368
|
+
loc:
|
|
369
|
+
Legend location, similar to matplotlib legend loc.
|
|
370
|
+
"""
|
|
371
|
+
if handles is None:
|
|
372
|
+
handles, _ = ax.get_legend_handles_labels()
|
|
373
|
+
|
|
374
|
+
handles = list(handles)
|
|
375
|
+
labels = list(labels)
|
|
376
|
+
|
|
377
|
+
if len(handles) != len(labels):
|
|
378
|
+
raise ValueError("handles and labels must have the same length")
|
|
379
|
+
|
|
380
|
+
# Remove any native legend already present
|
|
381
|
+
if getattr(ax, "legend_", None) is not None:
|
|
382
|
+
try:
|
|
383
|
+
ax.legend_.remove()
|
|
384
|
+
except Exception:
|
|
385
|
+
pass
|
|
386
|
+
ax.legend_ = None
|
|
387
|
+
|
|
388
|
+
fs = _resolve_font_size(fontsize, font_size, default=16)
|
|
389
|
+
title_fs = _resolve_font_size(title_fontsize, title_font_size, default=max(fs, 16))
|
|
390
|
+
title_color = color if title_color is None else title_color
|
|
391
|
+
|
|
392
|
+
row_boxes = []
|
|
393
|
+
sep_px = max(2, int(round(8 * handletextpad)))
|
|
394
|
+
|
|
395
|
+
for handle, label in zip(handles, labels):
|
|
396
|
+
handle_box = _build_legend_handle_box(handle)
|
|
397
|
+
text_box = _build_legend_text_box(
|
|
398
|
+
text=label,
|
|
399
|
+
font_size=fs,
|
|
400
|
+
font_family=font_family,
|
|
401
|
+
font_path=font_path,
|
|
402
|
+
color=color,
|
|
403
|
+
bg=bg,
|
|
404
|
+
padding=padding,
|
|
405
|
+
scale=scale,
|
|
406
|
+
zoom=zoom,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
row = HPacker(
|
|
410
|
+
children=[handle_box, text_box],
|
|
411
|
+
align="center",
|
|
412
|
+
pad=0,
|
|
413
|
+
sep=sep_px,
|
|
414
|
+
)
|
|
415
|
+
row_boxes.append(row)
|
|
416
|
+
|
|
417
|
+
children = []
|
|
418
|
+
|
|
419
|
+
if title:
|
|
420
|
+
title_box = _build_legend_text_box(
|
|
421
|
+
text=title,
|
|
422
|
+
font_size=title_fs,
|
|
423
|
+
font_family=font_family,
|
|
424
|
+
font_path=font_path,
|
|
425
|
+
color=title_color,
|
|
426
|
+
bg=bg,
|
|
427
|
+
padding=padding,
|
|
428
|
+
scale=scale,
|
|
429
|
+
zoom=title_zoom if title_zoom is not None else zoom,
|
|
430
|
+
)
|
|
431
|
+
children.append(title_box)
|
|
432
|
+
|
|
433
|
+
children.extend(row_boxes)
|
|
434
|
+
|
|
435
|
+
row_sep = max(2, int(round(10 * labelspacing)))
|
|
436
|
+
legend_box = VPacker(
|
|
437
|
+
children=children,
|
|
438
|
+
align="left",
|
|
439
|
+
pad=0,
|
|
440
|
+
sep=row_sep,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
anchored = AnchoredOffsetbox(
|
|
444
|
+
loc=_normalize_legend_loc(loc),
|
|
445
|
+
child=legend_box,
|
|
446
|
+
frameon=frameon,
|
|
447
|
+
pad=borderpad,
|
|
448
|
+
borderpad=borderaxespad,
|
|
449
|
+
bbox_to_anchor=None,
|
|
450
|
+
bbox_transform=ax.transAxes,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
anchored.set_zorder(zorder)
|
|
454
|
+
|
|
455
|
+
if frameon and getattr(anchored, "patch", None) is not None:
|
|
456
|
+
try:
|
|
457
|
+
anchored.patch.set_facecolor(facecolor)
|
|
458
|
+
anchored.patch.set_edgecolor(edgecolor)
|
|
459
|
+
anchored.patch.set_alpha(framealpha)
|
|
460
|
+
except Exception:
|
|
461
|
+
pass
|
|
462
|
+
|
|
463
|
+
ax.add_artist(anchored)
|
|
464
|
+
return anchored
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def set_multilingual_numeric_ticks(
|
|
468
|
+
ax,
|
|
469
|
+
script: str = "Bengali",
|
|
470
|
+
axis: str = "x",
|
|
471
|
+
positions=None,
|
|
472
|
+
values=None,
|
|
473
|
+
fontsize=None,
|
|
474
|
+
font_size=None,
|
|
475
|
+
font_family=None,
|
|
476
|
+
font_path=None,
|
|
477
|
+
color="black",
|
|
478
|
+
bg="transparent",
|
|
479
|
+
padding=8,
|
|
480
|
+
scale=1.0,
|
|
481
|
+
zoom=None,
|
|
482
|
+
extra_pad_axes: float = 0.02,
|
|
483
|
+
ha=None,
|
|
484
|
+
va=None,
|
|
485
|
+
zorder=5,
|
|
486
|
+
hide_native: bool = True,
|
|
487
|
+
collision_avoidance: bool = True,
|
|
488
|
+
formatter=None,
|
|
489
|
+
):
|
|
490
|
+
"""
|
|
491
|
+
Render numeric tick labels using the native digits of ``script``
|
|
492
|
+
(e.g. "Bengali" -> ২০২৬, "Devanagari" -> २०२६, "Arabic" -> ٢٠٢٦).
|
|
493
|
+
Scripts without native digits keep ASCII digits.
|
|
494
|
+
"""
|
|
495
|
+
axis = (axis or "x").lower()
|
|
496
|
+
if axis not in ("x", "y"):
|
|
497
|
+
raise ValueError("axis must be 'x' or 'y'")
|
|
498
|
+
|
|
499
|
+
if positions is None:
|
|
500
|
+
positions = ax.get_xticks() if axis == "x" else ax.get_yticks()
|
|
501
|
+
|
|
502
|
+
positions = list(positions)
|
|
503
|
+
|
|
504
|
+
if values is None:
|
|
505
|
+
values = positions
|
|
506
|
+
else:
|
|
507
|
+
values = list(values)
|
|
508
|
+
|
|
509
|
+
if len(positions) != len(values):
|
|
510
|
+
raise ValueError("positions and values must have the same length")
|
|
511
|
+
|
|
512
|
+
labels = []
|
|
513
|
+
for v in values:
|
|
514
|
+
s = formatter(v) if formatter is not None else str(v)
|
|
515
|
+
labels.append(to_native_numerals(s, script))
|
|
516
|
+
|
|
517
|
+
if axis == "x":
|
|
518
|
+
return set_multilingual_xticks(
|
|
519
|
+
ax=ax,
|
|
520
|
+
positions=positions,
|
|
521
|
+
labels=labels,
|
|
522
|
+
fontsize=fontsize,
|
|
523
|
+
font_size=font_size,
|
|
524
|
+
font_family=font_family,
|
|
525
|
+
font_path=font_path,
|
|
526
|
+
color=color,
|
|
527
|
+
bg=bg,
|
|
528
|
+
padding=padding,
|
|
529
|
+
scale=scale,
|
|
530
|
+
zoom=zoom,
|
|
531
|
+
extra_pad_axes=extra_pad_axes,
|
|
532
|
+
ha="center" if ha is None else ha,
|
|
533
|
+
va="top" if va is None else va,
|
|
534
|
+
zorder=zorder,
|
|
535
|
+
hide_native=hide_native,
|
|
536
|
+
collision_avoidance=collision_avoidance,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
return set_multilingual_yticks(
|
|
540
|
+
ax=ax,
|
|
541
|
+
positions=positions,
|
|
542
|
+
labels=labels,
|
|
543
|
+
fontsize=fontsize,
|
|
544
|
+
font_size=font_size,
|
|
545
|
+
font_family=font_family,
|
|
546
|
+
font_path=font_path,
|
|
547
|
+
color=color,
|
|
548
|
+
bg=bg,
|
|
549
|
+
padding=padding,
|
|
550
|
+
scale=scale,
|
|
551
|
+
zoom=zoom,
|
|
552
|
+
extra_pad_axes=extra_pad_axes,
|
|
553
|
+
ha="right" if ha is None else ha,
|
|
554
|
+
va="center" if va is None else va,
|
|
555
|
+
zorder=zorder,
|
|
556
|
+
hide_native=hide_native,
|
|
557
|
+
collision_avoidance=collision_avoidance,
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
# ---------------------------------------------------------------------
|
|
562
|
+
# Core managed labels via layout manager
|
|
563
|
+
# ---------------------------------------------------------------------
|
|
564
|
+
|
|
565
|
+
def set_multilingual_title(
|
|
566
|
+
ax,
|
|
567
|
+
text,
|
|
568
|
+
fontsize=None,
|
|
569
|
+
font_size=None,
|
|
570
|
+
font_family=None,
|
|
571
|
+
font_path=None,
|
|
572
|
+
color="black",
|
|
573
|
+
padding=10,
|
|
574
|
+
scale=1.0,
|
|
575
|
+
zoom=0.40,
|
|
576
|
+
extra_pad_px=10.0,
|
|
577
|
+
zorder=5,
|
|
578
|
+
loc="center",
|
|
579
|
+
weight=None,
|
|
580
|
+
italic=False,
|
|
581
|
+
alpha=1.0,
|
|
582
|
+
):
|
|
583
|
+
"""
|
|
584
|
+
Set a multilingual title above the axes using the layout manager.
|
|
585
|
+
|
|
586
|
+
``loc`` matches Matplotlib's set_title(loc=): "left", "center",
|
|
587
|
+
or "right". ``weight``/``italic``/``alpha`` match Matplotlib text
|
|
588
|
+
styling ("bold", True, 0..1).
|
|
589
|
+
"""
|
|
590
|
+
manager = get_layout_manager(ax.figure)
|
|
591
|
+
return manager.add_title(
|
|
592
|
+
ax=ax,
|
|
593
|
+
text=text,
|
|
594
|
+
fontsize=fontsize,
|
|
595
|
+
font_size=font_size,
|
|
596
|
+
font_family=font_family,
|
|
597
|
+
font_path=font_path,
|
|
598
|
+
color=color,
|
|
599
|
+
padding=padding,
|
|
600
|
+
scale=scale,
|
|
601
|
+
zoom=zoom,
|
|
602
|
+
extra_pad_px=extra_pad_px,
|
|
603
|
+
zorder=zorder,
|
|
604
|
+
loc=loc,
|
|
605
|
+
weight=weight,
|
|
606
|
+
italic=italic,
|
|
607
|
+
alpha=alpha,
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def set_multilingual_xlabel(
|
|
612
|
+
ax,
|
|
613
|
+
text,
|
|
614
|
+
fontsize=None,
|
|
615
|
+
font_size=None,
|
|
616
|
+
font_family=None,
|
|
617
|
+
font_path=None,
|
|
618
|
+
color="black",
|
|
619
|
+
padding=10,
|
|
620
|
+
scale=1.0,
|
|
621
|
+
zoom=0.40,
|
|
622
|
+
extra_pad_px=8.0,
|
|
623
|
+
zorder=5,
|
|
624
|
+
weight=None,
|
|
625
|
+
italic=False,
|
|
626
|
+
alpha=1.0,
|
|
627
|
+
):
|
|
628
|
+
"""
|
|
629
|
+
Set a multilingual x-axis label using the layout manager.
|
|
630
|
+
"""
|
|
631
|
+
manager = get_layout_manager(ax.figure)
|
|
632
|
+
return manager.add_xlabel(
|
|
633
|
+
ax=ax,
|
|
634
|
+
text=text,
|
|
635
|
+
fontsize=fontsize,
|
|
636
|
+
font_size=font_size,
|
|
637
|
+
font_family=font_family,
|
|
638
|
+
font_path=font_path,
|
|
639
|
+
color=color,
|
|
640
|
+
padding=padding,
|
|
641
|
+
scale=scale,
|
|
642
|
+
zoom=zoom,
|
|
643
|
+
extra_pad_px=extra_pad_px,
|
|
644
|
+
zorder=zorder,
|
|
645
|
+
weight=weight,
|
|
646
|
+
italic=italic,
|
|
647
|
+
alpha=alpha,
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def set_multilingual_ylabel(
|
|
652
|
+
ax,
|
|
653
|
+
text,
|
|
654
|
+
fontsize=None,
|
|
655
|
+
font_size=None,
|
|
656
|
+
font_family=None,
|
|
657
|
+
font_path=None,
|
|
658
|
+
color="black",
|
|
659
|
+
padding=10,
|
|
660
|
+
scale=1.0,
|
|
661
|
+
zoom=0.40,
|
|
662
|
+
extra_pad_px=8.0,
|
|
663
|
+
zorder=5,
|
|
664
|
+
weight=None,
|
|
665
|
+
italic=False,
|
|
666
|
+
alpha=1.0,
|
|
667
|
+
):
|
|
668
|
+
"""
|
|
669
|
+
Set a multilingual y-axis label using the layout manager.
|
|
670
|
+
"""
|
|
671
|
+
manager = get_layout_manager(ax.figure)
|
|
672
|
+
return manager.add_ylabel(
|
|
673
|
+
ax=ax,
|
|
674
|
+
text=text,
|
|
675
|
+
fontsize=fontsize,
|
|
676
|
+
font_size=font_size,
|
|
677
|
+
font_family=font_family,
|
|
678
|
+
font_path=font_path,
|
|
679
|
+
color=color,
|
|
680
|
+
padding=padding,
|
|
681
|
+
scale=scale,
|
|
682
|
+
zoom=zoom,
|
|
683
|
+
extra_pad_px=extra_pad_px,
|
|
684
|
+
zorder=zorder,
|
|
685
|
+
weight=weight,
|
|
686
|
+
italic=italic,
|
|
687
|
+
alpha=alpha,
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def set_multilingual_suptitle(
|
|
692
|
+
fig,
|
|
693
|
+
text,
|
|
694
|
+
fontsize=None,
|
|
695
|
+
font_size=None,
|
|
696
|
+
font_family=None,
|
|
697
|
+
font_path=None,
|
|
698
|
+
color="black",
|
|
699
|
+
padding=10,
|
|
700
|
+
scale=1.0,
|
|
701
|
+
zoom=0.40,
|
|
702
|
+
extra_pad_px=18.0,
|
|
703
|
+
zorder=6,
|
|
704
|
+
weight=None,
|
|
705
|
+
italic=False,
|
|
706
|
+
alpha=1.0,
|
|
707
|
+
):
|
|
708
|
+
"""
|
|
709
|
+
Set a multilingual suptitle for the whole figure, stacked above any
|
|
710
|
+
per-axes titles.
|
|
711
|
+
"""
|
|
712
|
+
manager = get_layout_manager(fig)
|
|
713
|
+
return manager.add_suptitle(
|
|
714
|
+
fig=fig,
|
|
715
|
+
text=text,
|
|
716
|
+
fontsize=fontsize,
|
|
717
|
+
font_size=font_size,
|
|
718
|
+
font_family=font_family,
|
|
719
|
+
font_path=font_path,
|
|
720
|
+
color=color,
|
|
721
|
+
padding=padding,
|
|
722
|
+
scale=scale,
|
|
723
|
+
zoom=zoom,
|
|
724
|
+
extra_pad_px=extra_pad_px,
|
|
725
|
+
zorder=zorder,
|
|
726
|
+
weight=weight,
|
|
727
|
+
italic=italic,
|
|
728
|
+
alpha=alpha,
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
# ---------------------------------------------------------------------
|
|
733
|
+
# General multilingual text / annotation helpers
|
|
734
|
+
# ---------------------------------------------------------------------
|
|
735
|
+
|
|
736
|
+
def multilingual_text(
|
|
737
|
+
ax,
|
|
738
|
+
x,
|
|
739
|
+
y,
|
|
740
|
+
text,
|
|
741
|
+
fontsize=None,
|
|
742
|
+
font_size=None,
|
|
743
|
+
font_family=None,
|
|
744
|
+
font_path=None,
|
|
745
|
+
color="black",
|
|
746
|
+
bg="transparent",
|
|
747
|
+
padding=10,
|
|
748
|
+
scale=1.0,
|
|
749
|
+
coord="data",
|
|
750
|
+
ha="center",
|
|
751
|
+
va="center",
|
|
752
|
+
zoom=None,
|
|
753
|
+
zorder=5,
|
|
754
|
+
weight=None,
|
|
755
|
+
italic=False,
|
|
756
|
+
alpha=1.0,
|
|
757
|
+
rotation=0.0,
|
|
758
|
+
):
|
|
759
|
+
"""
|
|
760
|
+
General multilingual text helper, similar to ax.text() but using Qt
|
|
761
|
+
rendering. Supports Matplotlib-style ``weight`` ("bold"), ``italic``,
|
|
762
|
+
``alpha`` (0..1), ``rotation`` (CCW degrees, any angle), and
|
|
763
|
+
multiline strings containing ``\\n``.
|
|
764
|
+
"""
|
|
765
|
+
fs = _resolve_font_size(fontsize, font_size, default=18)
|
|
766
|
+
|
|
767
|
+
_, _, oi = _build_offset_image(
|
|
768
|
+
text=text,
|
|
769
|
+
font_size=fs,
|
|
770
|
+
font_family=font_family,
|
|
771
|
+
font_path=font_path,
|
|
772
|
+
color=color,
|
|
773
|
+
bg=bg,
|
|
774
|
+
padding=padding,
|
|
775
|
+
scale=scale,
|
|
776
|
+
zoom=zoom,
|
|
777
|
+
rotate_90=False,
|
|
778
|
+
weight=weight,
|
|
779
|
+
italic=italic,
|
|
780
|
+
alpha=alpha,
|
|
781
|
+
rotation=rotation,
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
box_alignment = _alignment_to_box_alignment(ha=ha, va=va)
|
|
785
|
+
xycoords = _resolve_xycoords(ax, coord)
|
|
786
|
+
|
|
787
|
+
ab = AnnotationBbox(
|
|
788
|
+
oi,
|
|
789
|
+
(x, y),
|
|
790
|
+
xycoords=xycoords,
|
|
791
|
+
frameon=False,
|
|
792
|
+
box_alignment=box_alignment,
|
|
793
|
+
zorder=zorder,
|
|
794
|
+
annotation_clip=False,
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
if (coord or "data").lower() == "figure":
|
|
798
|
+
ax.figure.add_artist(ab)
|
|
799
|
+
else:
|
|
800
|
+
ax.add_artist(ab)
|
|
801
|
+
|
|
802
|
+
return ab
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def text(ax, *args, **kwargs):
|
|
806
|
+
"""
|
|
807
|
+
Alias for multilingual_text().
|
|
808
|
+
"""
|
|
809
|
+
return multilingual_text(ax, *args, **kwargs)
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
def annotate_multilingual(
|
|
813
|
+
ax,
|
|
814
|
+
text,
|
|
815
|
+
xy,
|
|
816
|
+
xytext=None,
|
|
817
|
+
fontsize=None,
|
|
818
|
+
font_size=None,
|
|
819
|
+
font_family=None,
|
|
820
|
+
font_path=None,
|
|
821
|
+
color="black",
|
|
822
|
+
bg="transparent",
|
|
823
|
+
padding=10,
|
|
824
|
+
scale=1.0,
|
|
825
|
+
coord="data",
|
|
826
|
+
textcoord=None,
|
|
827
|
+
ha="center",
|
|
828
|
+
va="center",
|
|
829
|
+
zoom=None,
|
|
830
|
+
zorder=5,
|
|
831
|
+
arrowprops=None,
|
|
832
|
+
):
|
|
833
|
+
"""
|
|
834
|
+
Add multilingual annotation text, with an optional Matplotlib arrow.
|
|
835
|
+
"""
|
|
836
|
+
if xytext is None:
|
|
837
|
+
xytext = xy
|
|
838
|
+
if textcoord is None:
|
|
839
|
+
textcoord = coord
|
|
840
|
+
|
|
841
|
+
arrow_artist = None
|
|
842
|
+
if arrowprops is not None:
|
|
843
|
+
arrow_artist = ax.annotate(
|
|
844
|
+
"",
|
|
845
|
+
xy=xy,
|
|
846
|
+
xytext=xytext,
|
|
847
|
+
xycoords=_resolve_xycoords(ax, coord),
|
|
848
|
+
textcoords=_resolve_xycoords(ax, textcoord),
|
|
849
|
+
arrowprops=arrowprops,
|
|
850
|
+
annotation_clip=False,
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
text_artist = multilingual_text(
|
|
854
|
+
ax=ax,
|
|
855
|
+
x=xytext[0],
|
|
856
|
+
y=xytext[1],
|
|
857
|
+
text=text,
|
|
858
|
+
fontsize=fontsize,
|
|
859
|
+
font_size=font_size,
|
|
860
|
+
font_family=font_family,
|
|
861
|
+
font_path=font_path,
|
|
862
|
+
color=color,
|
|
863
|
+
bg=bg,
|
|
864
|
+
padding=padding,
|
|
865
|
+
scale=scale,
|
|
866
|
+
coord=textcoord,
|
|
867
|
+
ha=ha,
|
|
868
|
+
va=va,
|
|
869
|
+
zoom=zoom,
|
|
870
|
+
zorder=zorder,
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
return {
|
|
874
|
+
"text_artist": text_artist,
|
|
875
|
+
"arrow_artist": arrow_artist,
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
# ---------------------------------------------------------------------
|
|
880
|
+
# Heatmap / cell helpers
|
|
881
|
+
# ---------------------------------------------------------------------
|
|
882
|
+
|
|
883
|
+
def add_multilingual_cell_text(
|
|
884
|
+
ax,
|
|
885
|
+
row,
|
|
886
|
+
col,
|
|
887
|
+
text,
|
|
888
|
+
rows,
|
|
889
|
+
cols,
|
|
890
|
+
fontsize=None,
|
|
891
|
+
font_size=None,
|
|
892
|
+
font_family=None,
|
|
893
|
+
font_path=None,
|
|
894
|
+
color="black",
|
|
895
|
+
bg="transparent",
|
|
896
|
+
padding=10,
|
|
897
|
+
scale=1.0,
|
|
898
|
+
origin="upper",
|
|
899
|
+
zoom=None,
|
|
900
|
+
zorder=6,
|
|
901
|
+
):
|
|
902
|
+
"""
|
|
903
|
+
Draw multilingual text at the center of a heatmap cell using axes coordinates.
|
|
904
|
+
"""
|
|
905
|
+
fs = _resolve_font_size(fontsize, font_size, default=22)
|
|
906
|
+
|
|
907
|
+
x_axes = (col + 0.5) / cols
|
|
908
|
+
if origin == "upper":
|
|
909
|
+
y_axes = 1.0 - (row + 0.5) / rows
|
|
910
|
+
else:
|
|
911
|
+
y_axes = (row + 0.5) / rows
|
|
912
|
+
|
|
913
|
+
return multilingual_text(
|
|
914
|
+
ax=ax,
|
|
915
|
+
x=x_axes,
|
|
916
|
+
y=y_axes,
|
|
917
|
+
text=text,
|
|
918
|
+
coord="axes",
|
|
919
|
+
ha="center",
|
|
920
|
+
va="center",
|
|
921
|
+
fontsize=fs,
|
|
922
|
+
font_family=font_family,
|
|
923
|
+
font_path=font_path,
|
|
924
|
+
color=color,
|
|
925
|
+
bg=bg,
|
|
926
|
+
padding=padding,
|
|
927
|
+
scale=scale,
|
|
928
|
+
zoom=zoom,
|
|
929
|
+
zorder=zorder,
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def multilingual_paragraph(
|
|
934
|
+
ax,
|
|
935
|
+
x,
|
|
936
|
+
y,
|
|
937
|
+
text,
|
|
938
|
+
width=400,
|
|
939
|
+
height=None,
|
|
940
|
+
fontsize=None,
|
|
941
|
+
font_size=None,
|
|
942
|
+
font_family=None,
|
|
943
|
+
font_path=None,
|
|
944
|
+
color="black",
|
|
945
|
+
bg="transparent",
|
|
946
|
+
margin=12,
|
|
947
|
+
scale=1.0,
|
|
948
|
+
coord="axes",
|
|
949
|
+
ha="left",
|
|
950
|
+
va="top",
|
|
951
|
+
zoom=None,
|
|
952
|
+
zorder=5,
|
|
953
|
+
):
|
|
954
|
+
"""
|
|
955
|
+
Place wrapped multilingual paragraph text as an image artist.
|
|
956
|
+
Note: this is a free/manual artist, not layout-managed.
|
|
957
|
+
"""
|
|
958
|
+
fs = _resolve_font_size(fontsize, font_size, default=18)
|
|
959
|
+
|
|
960
|
+
_, _, oi = _build_paragraph_offset_image(
|
|
961
|
+
text=text,
|
|
962
|
+
width=width,
|
|
963
|
+
height=height,
|
|
964
|
+
font_size=fs,
|
|
965
|
+
font_family=font_family,
|
|
966
|
+
font_path=font_path,
|
|
967
|
+
color=color,
|
|
968
|
+
bg=bg,
|
|
969
|
+
margin=margin,
|
|
970
|
+
scale=scale,
|
|
971
|
+
zoom=zoom,
|
|
972
|
+
)
|
|
973
|
+
|
|
974
|
+
box_alignment = _alignment_to_box_alignment(ha=ha, va=va)
|
|
975
|
+
xycoords = _resolve_xycoords(ax, coord)
|
|
976
|
+
|
|
977
|
+
ab = AnnotationBbox(
|
|
978
|
+
oi,
|
|
979
|
+
(x, y),
|
|
980
|
+
xycoords=xycoords,
|
|
981
|
+
frameon=False,
|
|
982
|
+
box_alignment=box_alignment,
|
|
983
|
+
zorder=zorder,
|
|
984
|
+
annotation_clip=False,
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
if (coord or "data").lower() == "figure":
|
|
988
|
+
ax.figure.add_artist(ab)
|
|
989
|
+
else:
|
|
990
|
+
ax.add_artist(ab)
|
|
991
|
+
|
|
992
|
+
return ab
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
# ---------------------------------------------------------------------
|
|
996
|
+
# Tick helpers via layout manager
|
|
997
|
+
# ---------------------------------------------------------------------
|
|
998
|
+
|
|
999
|
+
def set_multilingual_xticks(
|
|
1000
|
+
ax,
|
|
1001
|
+
positions: Sequence[float],
|
|
1002
|
+
labels: Sequence[str],
|
|
1003
|
+
fontsize=None,
|
|
1004
|
+
font_size=None,
|
|
1005
|
+
font_family=None,
|
|
1006
|
+
font_path=None,
|
|
1007
|
+
color="black",
|
|
1008
|
+
bg="transparent",
|
|
1009
|
+
padding=8,
|
|
1010
|
+
scale=1.0,
|
|
1011
|
+
zoom=None,
|
|
1012
|
+
extra_pad_axes: float = 0.02,
|
|
1013
|
+
ha="center",
|
|
1014
|
+
va="top",
|
|
1015
|
+
zorder=5,
|
|
1016
|
+
hide_native: bool = True,
|
|
1017
|
+
collision_avoidance: bool = True,
|
|
1018
|
+
rotation: float = 0.0,
|
|
1019
|
+
):
|
|
1020
|
+
"""
|
|
1021
|
+
Replace x tick labels with script-aware rendered image labels using the layout manager.
|
|
1022
|
+
``rotation`` rotates each label counter-clockwise (e.g. 45 for
|
|
1023
|
+
slanted long labels, matching Matplotlib's tick rotation).
|
|
1024
|
+
"""
|
|
1025
|
+
if len(positions) != len(labels):
|
|
1026
|
+
raise ValueError("positions and labels must have the same length")
|
|
1027
|
+
|
|
1028
|
+
manager = get_layout_manager(ax.figure)
|
|
1029
|
+
return manager.add_xticks(
|
|
1030
|
+
ax=ax,
|
|
1031
|
+
positions=list(positions),
|
|
1032
|
+
labels=list(labels),
|
|
1033
|
+
fontsize=fontsize,
|
|
1034
|
+
font_size=font_size,
|
|
1035
|
+
font_family=font_family,
|
|
1036
|
+
font_path=font_path,
|
|
1037
|
+
color=color,
|
|
1038
|
+
bg=bg,
|
|
1039
|
+
padding=padding,
|
|
1040
|
+
scale=scale,
|
|
1041
|
+
zoom=zoom,
|
|
1042
|
+
extra_pad_axes=extra_pad_axes,
|
|
1043
|
+
ha=ha,
|
|
1044
|
+
va=va,
|
|
1045
|
+
zorder=zorder,
|
|
1046
|
+
hide_native=hide_native,
|
|
1047
|
+
collision_avoidance=collision_avoidance,
|
|
1048
|
+
rotation=rotation,
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def set_multilingual_yticks(
|
|
1053
|
+
ax,
|
|
1054
|
+
positions: Sequence[float],
|
|
1055
|
+
labels: Sequence[str],
|
|
1056
|
+
fontsize=None,
|
|
1057
|
+
font_size=None,
|
|
1058
|
+
font_family=None,
|
|
1059
|
+
font_path=None,
|
|
1060
|
+
color="black",
|
|
1061
|
+
bg="transparent",
|
|
1062
|
+
padding=8,
|
|
1063
|
+
scale=1.0,
|
|
1064
|
+
zoom=None,
|
|
1065
|
+
extra_pad_axes: float = 0.02,
|
|
1066
|
+
ha="right",
|
|
1067
|
+
va="center",
|
|
1068
|
+
zorder=5,
|
|
1069
|
+
hide_native: bool = True,
|
|
1070
|
+
collision_avoidance: bool = True,
|
|
1071
|
+
rotation: float = 0.0,
|
|
1072
|
+
):
|
|
1073
|
+
"""
|
|
1074
|
+
Replace y tick labels with script-aware rendered image labels using the layout manager.
|
|
1075
|
+
``rotation`` rotates each label counter-clockwise (degrees).
|
|
1076
|
+
"""
|
|
1077
|
+
if len(positions) != len(labels):
|
|
1078
|
+
raise ValueError("positions and labels must have the same length")
|
|
1079
|
+
|
|
1080
|
+
manager = get_layout_manager(ax.figure)
|
|
1081
|
+
return manager.add_yticks(
|
|
1082
|
+
ax=ax,
|
|
1083
|
+
positions=list(positions),
|
|
1084
|
+
labels=list(labels),
|
|
1085
|
+
fontsize=fontsize,
|
|
1086
|
+
font_size=font_size,
|
|
1087
|
+
font_family=font_family,
|
|
1088
|
+
font_path=font_path,
|
|
1089
|
+
color=color,
|
|
1090
|
+
bg=bg,
|
|
1091
|
+
padding=padding,
|
|
1092
|
+
scale=scale,
|
|
1093
|
+
zoom=zoom,
|
|
1094
|
+
extra_pad_axes=extra_pad_axes,
|
|
1095
|
+
ha=ha,
|
|
1096
|
+
va=va,
|
|
1097
|
+
zorder=zorder,
|
|
1098
|
+
hide_native=hide_native,
|
|
1099
|
+
collision_avoidance=collision_avoidance,
|
|
1100
|
+
rotation=rotation,
|
|
1101
|
+
)
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
# ---------------------------------------------------------------------
|
|
1105
|
+
# Layout helper
|
|
1106
|
+
# ---------------------------------------------------------------------
|
|
1107
|
+
|
|
1108
|
+
def apply_multilingual_layout(
|
|
1109
|
+
fig,
|
|
1110
|
+
left=0.18,
|
|
1111
|
+
right=0.88,
|
|
1112
|
+
bottom=0.22,
|
|
1113
|
+
top=0.84,
|
|
1114
|
+
auto=False,
|
|
1115
|
+
min_left=0.12,
|
|
1116
|
+
min_right=0.90,
|
|
1117
|
+
min_bottom=0.12,
|
|
1118
|
+
min_top=0.88,
|
|
1119
|
+
):
|
|
1120
|
+
"""
|
|
1121
|
+
Adjust figure margins to leave enough space for multilingual titles,
|
|
1122
|
+
axis labels, and managed multilingual tick labels.
|
|
1123
|
+
"""
|
|
1124
|
+
manager = get_layout_manager(fig)
|
|
1125
|
+
|
|
1126
|
+
if auto:
|
|
1127
|
+
manager.update_layout()
|
|
1128
|
+
manager.auto_adjust_margins(
|
|
1129
|
+
min_left=min_left,
|
|
1130
|
+
min_right=min_right,
|
|
1131
|
+
min_bottom=min_bottom,
|
|
1132
|
+
min_top=min_top,
|
|
1133
|
+
)
|
|
1134
|
+
manager.update_layout()
|
|
1135
|
+
return manager
|
|
1136
|
+
|
|
1137
|
+
fig.subplots_adjust(
|
|
1138
|
+
left=left,
|
|
1139
|
+
right=right,
|
|
1140
|
+
bottom=bottom,
|
|
1141
|
+
top=top,
|
|
1142
|
+
)
|
|
1143
|
+
manager.update_layout()
|
|
1144
|
+
return manager
|