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,378 @@
|
|
|
1
|
+
# universal_render/highlevel.py
|
|
2
|
+
"""
|
|
3
|
+
High-level convenience API.
|
|
4
|
+
|
|
5
|
+
Users think in languages ("hindi", "bangla"), not Unicode script names,
|
|
6
|
+
and usually want to label a whole plot in one call rather than six.
|
|
7
|
+
This module provides:
|
|
8
|
+
|
|
9
|
+
- language_to_script("hindi") -> "Devanagari"
|
|
10
|
+
- set_language_font("bangla", family) -> pin a font by language name
|
|
11
|
+
- localize_axes(ax, title=..., ...) -> label an entire axes in one call
|
|
12
|
+
- multilingual_heatmap(ax, data, labels=...) -> annotated heatmap in one call
|
|
13
|
+
- multilingual_bar_labels(ax, labels) -> value/category labels on bar charts
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any, Dict, List, Optional, Sequence, Union
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
from .fonts import find_font_for_script, set_script_font
|
|
22
|
+
from .scripts import detect_script, to_native_numerals
|
|
23
|
+
from .mpl_support import (
|
|
24
|
+
add_multilingual_cell_text,
|
|
25
|
+
apply_multilingual_layout,
|
|
26
|
+
multilingual_text,
|
|
27
|
+
set_multilingual_legend,
|
|
28
|
+
set_multilingual_suptitle,
|
|
29
|
+
set_multilingual_title,
|
|
30
|
+
set_multilingual_xlabel,
|
|
31
|
+
set_multilingual_xticks,
|
|
32
|
+
set_multilingual_ylabel,
|
|
33
|
+
set_multilingual_yticks,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------
|
|
38
|
+
# Language names -> script
|
|
39
|
+
# ---------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
LANGUAGE_TO_SCRIPT: Dict[str, str] = {
|
|
42
|
+
# Indic
|
|
43
|
+
"bengali": "Bengali", "bangla": "Bengali", "assamese": "Bengali",
|
|
44
|
+
"hindi": "Devanagari", "marathi": "Devanagari", "nepali": "Devanagari",
|
|
45
|
+
"sanskrit": "Devanagari", "konkani": "Devanagari",
|
|
46
|
+
"tamil": "Tamil",
|
|
47
|
+
"telugu": "Telugu",
|
|
48
|
+
"kannada": "Kannada",
|
|
49
|
+
"malayalam": "Malayalam",
|
|
50
|
+
"gujarati": "Gujarati",
|
|
51
|
+
"punjabi": "Gurmukhi", "panjabi": "Gurmukhi",
|
|
52
|
+
"odia": "Odia", "oriya": "Odia",
|
|
53
|
+
"sinhala": "Sinhala", "sinhalese": "Sinhala",
|
|
54
|
+
# RTL
|
|
55
|
+
"arabic": "Arabic", "urdu": "Arabic", "persian": "Arabic", "farsi": "Arabic",
|
|
56
|
+
"pashto": "Arabic", "kurdish": "Arabic",
|
|
57
|
+
"hebrew": "Hebrew", "yiddish": "Hebrew",
|
|
58
|
+
# Southeast Asian
|
|
59
|
+
"thai": "Thai",
|
|
60
|
+
"lao": "Lao",
|
|
61
|
+
"khmer": "Khmer", "cambodian": "Khmer",
|
|
62
|
+
"burmese": "Myanmar", "myanmar": "Myanmar",
|
|
63
|
+
"tibetan": "Tibetan", "dzongkha": "Tibetan",
|
|
64
|
+
# East Asian
|
|
65
|
+
"chinese": "Han", "mandarin": "Han", "cantonese": "Han",
|
|
66
|
+
"japanese": "Hiragana",
|
|
67
|
+
"korean": "Hangul",
|
|
68
|
+
# European / other
|
|
69
|
+
"english": "Latin", "french": "Latin", "german": "Latin",
|
|
70
|
+
"spanish": "Latin", "portuguese": "Latin", "italian": "Latin",
|
|
71
|
+
"vietnamese": "Latin", "turkish": "Latin", "indonesian": "Latin",
|
|
72
|
+
"russian": "Cyrillic", "ukrainian": "Cyrillic", "bulgarian": "Cyrillic",
|
|
73
|
+
"serbian": "Cyrillic", "kazakh": "Cyrillic",
|
|
74
|
+
"greek": "Greek",
|
|
75
|
+
"georgian": "Georgian",
|
|
76
|
+
"armenian": "Armenian",
|
|
77
|
+
"amharic": "Ethiopic", "tigrinya": "Ethiopic",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def supported_languages() -> List[str]:
|
|
82
|
+
"""Language names accepted by language_to_script()."""
|
|
83
|
+
return sorted(LANGUAGE_TO_SCRIPT)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def language_to_script(language: str) -> str:
|
|
87
|
+
"""
|
|
88
|
+
Map a language name to its Unicode script.
|
|
89
|
+
|
|
90
|
+
>>> language_to_script("hindi")
|
|
91
|
+
'Devanagari'
|
|
92
|
+
>>> language_to_script("urdu")
|
|
93
|
+
'Arabic'
|
|
94
|
+
"""
|
|
95
|
+
key = str(language).strip().lower()
|
|
96
|
+
script = LANGUAGE_TO_SCRIPT.get(key)
|
|
97
|
+
if script is None:
|
|
98
|
+
raise KeyError(
|
|
99
|
+
f"Unknown language: {language!r}. "
|
|
100
|
+
f"Known languages: {', '.join(supported_languages())}"
|
|
101
|
+
)
|
|
102
|
+
return script
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def set_language_font(language: str, family: str) -> None:
|
|
106
|
+
"""Pin a font family by language name, e.g. ("bangla", "Kalpurush")."""
|
|
107
|
+
set_script_font(language_to_script(language), family)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def font_for_language(language: str, sample_text: Optional[str] = None) -> str:
|
|
111
|
+
"""Best installed font family for a language name."""
|
|
112
|
+
return find_font_for_script(language_to_script(language), sample_text=sample_text)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def localized_numerals(value, language: str) -> str:
|
|
116
|
+
"""to_native_numerals() addressed by language name."""
|
|
117
|
+
return to_native_numerals(value, language_to_script(language))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------------
|
|
121
|
+
# One-call axes localization
|
|
122
|
+
# ---------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
def localize_axes(
|
|
125
|
+
ax,
|
|
126
|
+
title: Optional[str] = None,
|
|
127
|
+
xlabel: Optional[str] = None,
|
|
128
|
+
ylabel: Optional[str] = None,
|
|
129
|
+
suptitle: Optional[str] = None,
|
|
130
|
+
xticklabels: Optional[Sequence[str]] = None,
|
|
131
|
+
xtick_positions: Optional[Sequence[float]] = None,
|
|
132
|
+
yticklabels: Optional[Sequence[str]] = None,
|
|
133
|
+
ytick_positions: Optional[Sequence[float]] = None,
|
|
134
|
+
legend_labels: Optional[Sequence[str]] = None,
|
|
135
|
+
legend_loc: str = "upper right",
|
|
136
|
+
title_size: int = 32,
|
|
137
|
+
label_size: int = 24,
|
|
138
|
+
tick_size: int = 16,
|
|
139
|
+
legend_size: int = 16,
|
|
140
|
+
color: str = "black",
|
|
141
|
+
font_family: Optional[str] = None,
|
|
142
|
+
auto_layout: bool = True,
|
|
143
|
+
) -> Dict[str, Any]:
|
|
144
|
+
"""
|
|
145
|
+
Label an entire axes with script-aware text in a single call.
|
|
146
|
+
|
|
147
|
+
Every argument is optional; only what you pass gets applied. Each
|
|
148
|
+
string picks its own font from its dominant script, so the title can
|
|
149
|
+
be Bengali while the xlabel is Arabic.
|
|
150
|
+
|
|
151
|
+
ur.localize_axes(
|
|
152
|
+
ax,
|
|
153
|
+
title="বিক্রয় প্রতিবেদন",
|
|
154
|
+
xlabel="महीना",
|
|
155
|
+
ylabel="மதிப்பு",
|
|
156
|
+
xticklabels=["এক", "दो", "மூன்று"],
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
Returns a dict of the created artists, keyed by role.
|
|
160
|
+
"""
|
|
161
|
+
artists: Dict[str, Any] = {}
|
|
162
|
+
|
|
163
|
+
if title is not None:
|
|
164
|
+
artists["title"] = set_multilingual_title(
|
|
165
|
+
ax, title, font_size=title_size, color=color, font_family=font_family,
|
|
166
|
+
)
|
|
167
|
+
if xlabel is not None:
|
|
168
|
+
artists["xlabel"] = set_multilingual_xlabel(
|
|
169
|
+
ax, xlabel, font_size=label_size, color=color, font_family=font_family,
|
|
170
|
+
)
|
|
171
|
+
if ylabel is not None:
|
|
172
|
+
artists["ylabel"] = set_multilingual_ylabel(
|
|
173
|
+
ax, ylabel, font_size=label_size, color=color, font_family=font_family,
|
|
174
|
+
)
|
|
175
|
+
if suptitle is not None:
|
|
176
|
+
artists["suptitle"] = set_multilingual_suptitle(
|
|
177
|
+
ax.figure, suptitle, font_size=title_size + 2, color=color,
|
|
178
|
+
font_family=font_family,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if xticklabels is not None:
|
|
182
|
+
positions = (
|
|
183
|
+
list(xtick_positions) if xtick_positions is not None
|
|
184
|
+
else list(ax.get_xticks())[: len(xticklabels)]
|
|
185
|
+
)
|
|
186
|
+
if len(positions) != len(xticklabels):
|
|
187
|
+
positions = list(range(1, len(xticklabels) + 1))
|
|
188
|
+
artists["xticks"] = set_multilingual_xticks(
|
|
189
|
+
ax, positions, list(xticklabels),
|
|
190
|
+
font_size=tick_size, color=color, font_family=font_family,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if yticklabels is not None:
|
|
194
|
+
positions = (
|
|
195
|
+
list(ytick_positions) if ytick_positions is not None
|
|
196
|
+
else list(ax.get_yticks())[: len(yticklabels)]
|
|
197
|
+
)
|
|
198
|
+
if len(positions) != len(yticklabels):
|
|
199
|
+
positions = list(range(1, len(yticklabels) + 1))
|
|
200
|
+
artists["yticks"] = set_multilingual_yticks(
|
|
201
|
+
ax, positions, list(yticklabels),
|
|
202
|
+
font_size=tick_size, color=color, font_family=font_family,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if legend_labels is not None:
|
|
206
|
+
artists["legend"] = set_multilingual_legend(
|
|
207
|
+
ax, list(legend_labels), loc=legend_loc,
|
|
208
|
+
font_size=legend_size, color=color, font_family=font_family,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
if auto_layout:
|
|
212
|
+
apply_multilingual_layout(ax.figure, auto=True)
|
|
213
|
+
|
|
214
|
+
return artists
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ---------------------------------------------------------------------
|
|
218
|
+
# One-call heatmap
|
|
219
|
+
# ---------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
def multilingual_heatmap(
|
|
222
|
+
ax,
|
|
223
|
+
data,
|
|
224
|
+
labels=None,
|
|
225
|
+
row_labels: Optional[Sequence[str]] = None,
|
|
226
|
+
col_labels: Optional[Sequence[str]] = None,
|
|
227
|
+
title: Optional[str] = None,
|
|
228
|
+
cmap: str = "viridis",
|
|
229
|
+
cell_font_size: int = 24,
|
|
230
|
+
tick_font_size: int = 16,
|
|
231
|
+
title_font_size: int = 30,
|
|
232
|
+
cell_color: str = "auto",
|
|
233
|
+
colorbar: bool = True,
|
|
234
|
+
value_format: Optional[str] = None,
|
|
235
|
+
value_script: Optional[str] = None,
|
|
236
|
+
imshow_kwargs: Optional[Dict[str, Any]] = None,
|
|
237
|
+
):
|
|
238
|
+
"""
|
|
239
|
+
Draw a fully script-aware annotated heatmap in one call.
|
|
240
|
+
|
|
241
|
+
Parameters
|
|
242
|
+
----------
|
|
243
|
+
data:
|
|
244
|
+
2D array of cell values.
|
|
245
|
+
labels:
|
|
246
|
+
Optional 2D array of cell label strings (any script). When None
|
|
247
|
+
and ``value_format`` is given, numeric values are formatted (and
|
|
248
|
+
converted to ``value_script`` digits if provided).
|
|
249
|
+
row_labels / col_labels:
|
|
250
|
+
Script-aware tick labels for the axes.
|
|
251
|
+
cell_color:
|
|
252
|
+
"auto" flips each cell's text between black/white based on the
|
|
253
|
+
cell's luminance; any other value is used as-is.
|
|
254
|
+
|
|
255
|
+
Returns (im, artists_dict).
|
|
256
|
+
"""
|
|
257
|
+
import matplotlib.pyplot as plt # local import; backend already chosen
|
|
258
|
+
|
|
259
|
+
data = np.asarray(data, dtype=float)
|
|
260
|
+
rows, cols = data.shape
|
|
261
|
+
|
|
262
|
+
kwargs = dict(imshow_kwargs or {})
|
|
263
|
+
kwargs.setdefault("cmap", cmap)
|
|
264
|
+
im = ax.imshow(data, **kwargs)
|
|
265
|
+
|
|
266
|
+
# Build labels from values when not supplied
|
|
267
|
+
if labels is None and value_format is not None:
|
|
268
|
+
labels = np.empty(data.shape, dtype=object)
|
|
269
|
+
for i in range(rows):
|
|
270
|
+
for j in range(cols):
|
|
271
|
+
s = format(data[i, j], value_format)
|
|
272
|
+
if value_script:
|
|
273
|
+
s = to_native_numerals(s, value_script)
|
|
274
|
+
labels[i, j] = s
|
|
275
|
+
|
|
276
|
+
artists: Dict[str, Any] = {"cells": []}
|
|
277
|
+
|
|
278
|
+
if labels is not None:
|
|
279
|
+
labels = np.asarray(labels, dtype=object)
|
|
280
|
+
if labels.shape != data.shape:
|
|
281
|
+
raise ValueError(
|
|
282
|
+
f"labels shape {labels.shape} != data shape {data.shape}"
|
|
283
|
+
)
|
|
284
|
+
cmap_obj = im.get_cmap()
|
|
285
|
+
vmin, vmax = im.get_clim()
|
|
286
|
+
span = (vmax - vmin) or 1.0
|
|
287
|
+
for i in range(rows):
|
|
288
|
+
for j in range(cols):
|
|
289
|
+
if cell_color == "auto":
|
|
290
|
+
r, g, b, _ = cmap_obj((data[i, j] - vmin) / span)
|
|
291
|
+
luminance = 0.299 * r + 0.587 * g + 0.114 * b
|
|
292
|
+
color = "black" if luminance > 0.55 else "white"
|
|
293
|
+
else:
|
|
294
|
+
color = cell_color
|
|
295
|
+
artists["cells"].append(add_multilingual_cell_text(
|
|
296
|
+
ax, i, j, str(labels[i, j]),
|
|
297
|
+
rows=rows, cols=cols,
|
|
298
|
+
font_size=cell_font_size, color=color,
|
|
299
|
+
))
|
|
300
|
+
|
|
301
|
+
ax.set_xticks(range(cols))
|
|
302
|
+
ax.set_yticks(range(rows))
|
|
303
|
+
ax.set_xticklabels([])
|
|
304
|
+
ax.set_yticklabels([])
|
|
305
|
+
|
|
306
|
+
if col_labels is not None:
|
|
307
|
+
artists["xticks"] = set_multilingual_xticks(
|
|
308
|
+
ax, list(range(cols)), list(col_labels), font_size=tick_font_size,
|
|
309
|
+
)
|
|
310
|
+
if row_labels is not None:
|
|
311
|
+
artists["yticks"] = set_multilingual_yticks(
|
|
312
|
+
ax, list(range(rows)), list(row_labels), font_size=tick_font_size,
|
|
313
|
+
)
|
|
314
|
+
if title is not None:
|
|
315
|
+
artists["title"] = set_multilingual_title(
|
|
316
|
+
ax, title, font_size=title_font_size,
|
|
317
|
+
)
|
|
318
|
+
if colorbar:
|
|
319
|
+
artists["colorbar"] = ax.figure.colorbar(im, ax=ax)
|
|
320
|
+
|
|
321
|
+
return im, artists
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# ---------------------------------------------------------------------
|
|
325
|
+
# Bar-chart labels
|
|
326
|
+
# ---------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
def multilingual_bar_labels(
|
|
329
|
+
ax,
|
|
330
|
+
labels: Sequence[str],
|
|
331
|
+
bars=None,
|
|
332
|
+
orientation: str = "vertical",
|
|
333
|
+
font_size: int = 16,
|
|
334
|
+
color: str = "black",
|
|
335
|
+
offset_frac: float = 0.02,
|
|
336
|
+
):
|
|
337
|
+
"""
|
|
338
|
+
Put a script-aware label at the end of each bar.
|
|
339
|
+
|
|
340
|
+
``bars`` defaults to the first BarContainer on the axes (the result
|
|
341
|
+
of ax.bar()/ax.barh()). ``orientation`` is "vertical" for ax.bar()
|
|
342
|
+
or "horizontal" for ax.barh().
|
|
343
|
+
"""
|
|
344
|
+
if bars is None:
|
|
345
|
+
containers = getattr(ax, "containers", [])
|
|
346
|
+
if not containers:
|
|
347
|
+
raise ValueError("No bar containers found on axes; pass bars=")
|
|
348
|
+
bars = containers[0]
|
|
349
|
+
|
|
350
|
+
patches = list(bars)
|
|
351
|
+
labels = list(labels)
|
|
352
|
+
if len(patches) != len(labels):
|
|
353
|
+
raise ValueError("labels and bars must have the same length")
|
|
354
|
+
|
|
355
|
+
vertical = str(orientation).lower().startswith("v")
|
|
356
|
+
if vertical:
|
|
357
|
+
span = ax.get_ylim()
|
|
358
|
+
pad = (span[1] - span[0]) * offset_frac
|
|
359
|
+
else:
|
|
360
|
+
span = ax.get_xlim()
|
|
361
|
+
pad = (span[1] - span[0]) * offset_frac
|
|
362
|
+
|
|
363
|
+
artists = []
|
|
364
|
+
for patch, label in zip(patches, labels):
|
|
365
|
+
if vertical:
|
|
366
|
+
x = patch.get_x() + patch.get_width() / 2.0
|
|
367
|
+
y = patch.get_y() + patch.get_height() + pad
|
|
368
|
+
ha, va = "center", "bottom"
|
|
369
|
+
else:
|
|
370
|
+
x = patch.get_x() + patch.get_width() + pad
|
|
371
|
+
y = patch.get_y() + patch.get_height() / 2.0
|
|
372
|
+
ha, va = "left", "center"
|
|
373
|
+
artists.append(multilingual_text(
|
|
374
|
+
ax, x, y, str(label),
|
|
375
|
+
coord="data", ha=ha, va=va,
|
|
376
|
+
font_size=font_size, color=color,
|
|
377
|
+
))
|
|
378
|
+
return artists
|