mpl-richtext 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.
- mpl_richtext/__init__.py +12 -0
- mpl_richtext/core.py +460 -0
- mpl_richtext/version.py +1 -0
- mpl_richtext-0.1.0.dist-info/METADATA +153 -0
- mpl_richtext-0.1.0.dist-info/RECORD +8 -0
- mpl_richtext-0.1.0.dist-info/WHEEL +5 -0
- mpl_richtext-0.1.0.dist-info/licenses/LICENSE +21 -0
- mpl_richtext-0.1.0.dist-info/top_level.txt +1 -0
mpl_richtext/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
mpl-richtext: Rich text rendering for Matplotlib
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .core import richtext
|
|
6
|
+
from .version import __version__
|
|
7
|
+
|
|
8
|
+
__all__ = ['richtext', '__version__']
|
|
9
|
+
|
|
10
|
+
__author__ = 'Rabin Katel'
|
|
11
|
+
__email__ = 'kattelrabinraja13@gmail.com'
|
|
12
|
+
__license__ = 'MIT'
|
mpl_richtext/core.py
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
from matplotlib.axes import Axes
|
|
3
|
+
from matplotlib.text import Text
|
|
4
|
+
from matplotlib.lines import Line2D
|
|
5
|
+
from typing import List, Optional, Tuple, Union, Dict, Any
|
|
6
|
+
|
|
7
|
+
def richtext(
|
|
8
|
+
x: float,
|
|
9
|
+
y: float,
|
|
10
|
+
strings: List[str],
|
|
11
|
+
colors: Optional[Union[str, List[Any], Dict[Any, Any]]] = None,
|
|
12
|
+
ax: Optional[Axes] = None,
|
|
13
|
+
**kwargs
|
|
14
|
+
) -> List[Text]:
|
|
15
|
+
"""
|
|
16
|
+
Display text with different colors and properties for each string, supporting word wrapping and alignment.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
x, y : float
|
|
21
|
+
Starting position of the text block.
|
|
22
|
+
strings : List[str]
|
|
23
|
+
List of text strings to display.
|
|
24
|
+
colors : Union[str, List[str], Dict[int, str]]
|
|
25
|
+
Color for the text. Can be:
|
|
26
|
+
- A single string: Applied to all strings.
|
|
27
|
+
- A list of colors: Corresponding to each string (dynamic extension supported).
|
|
28
|
+
- A dictionary: Mapping indices to colors (e.g., {1: 'red'}). Unspecified indices use default/black.
|
|
29
|
+
ax : matplotlib.axes.Axes, optional
|
|
30
|
+
The axes to draw on. If None, uses the current axes.
|
|
31
|
+
**kwargs : dict
|
|
32
|
+
Additional arguments passed to `ax.text`.
|
|
33
|
+
|
|
34
|
+
Global arguments (applied to the whole block):
|
|
35
|
+
- box_width (float): Maximum width of the text block for wrapping. If None, no wrapping occurs.
|
|
36
|
+
- linespacing (float): Line spacing multiplier (default: 1.5).
|
|
37
|
+
- ha (str): Horizontal alignment ('left', 'center', 'right').
|
|
38
|
+
- va (str): Vertical alignment ('top', 'center', 'bottom').
|
|
39
|
+
- zorder (int): Z-order for the text.
|
|
40
|
+
- transform: Transform for the text.
|
|
41
|
+
|
|
42
|
+
Per-segment arguments:
|
|
43
|
+
- Any other text property (e.g., fontsize, fontweight, fontproperties) can be:
|
|
44
|
+
- A single value: Applied to all strings.
|
|
45
|
+
- A list of values: Corresponding to each string (dynamic extension supported).
|
|
46
|
+
- A dictionary: Mapping indices to values (e.g., {1: 20}).
|
|
47
|
+
- underline (bool): If True, draws a line below the text.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
List[matplotlib.text.Text]
|
|
52
|
+
A list of the created Text objects.
|
|
53
|
+
|
|
54
|
+
Raises
|
|
55
|
+
------
|
|
56
|
+
ValueError
|
|
57
|
+
If inputs are invalid.
|
|
58
|
+
"""
|
|
59
|
+
if ax is None:
|
|
60
|
+
ax = plt.gca()
|
|
61
|
+
|
|
62
|
+
# Extract global special kwargs
|
|
63
|
+
box_width: Optional[float] = kwargs.pop('box_width', None)
|
|
64
|
+
linespacing: float = kwargs.pop('linespacing', 1.5)
|
|
65
|
+
if 'spacing' in kwargs:
|
|
66
|
+
linespacing = kwargs.pop('spacing')
|
|
67
|
+
|
|
68
|
+
ha = kwargs.pop('ha', kwargs.pop('horizontalalignment', 'left'))
|
|
69
|
+
va = kwargs.pop('va', kwargs.pop('verticalalignment', 'center'))
|
|
70
|
+
transform = kwargs.pop('transform', ax.transData)
|
|
71
|
+
zorder = kwargs.pop('zorder', 1)
|
|
72
|
+
|
|
73
|
+
# Normalize properties for each string segment
|
|
74
|
+
segment_properties = _normalize_properties(strings, colors, **kwargs)
|
|
75
|
+
|
|
76
|
+
# Get renderer for measuring text
|
|
77
|
+
fig = ax.get_figure()
|
|
78
|
+
if fig == None:
|
|
79
|
+
raise ValueError("The axes must be associated with a figure.")
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
renderer = fig.canvas.get_renderer()
|
|
83
|
+
except Exception:
|
|
84
|
+
fig.canvas.draw()
|
|
85
|
+
renderer = fig.canvas.get_renderer()
|
|
86
|
+
|
|
87
|
+
# Logic separation: Wrapping vs Non-Wrapping
|
|
88
|
+
if box_width is not None:
|
|
89
|
+
# 1. Tokenize into words with properties
|
|
90
|
+
words = _tokenize_strings(strings, segment_properties)
|
|
91
|
+
# 2. Build lines with wrapping
|
|
92
|
+
lines = _build_lines_wrapped(words, ax, renderer, box_width)
|
|
93
|
+
else:
|
|
94
|
+
# 1. Treat strings as segments
|
|
95
|
+
# 2. Build a single line
|
|
96
|
+
lines = [_build_line_seamless(strings, segment_properties, ax, renderer)]
|
|
97
|
+
|
|
98
|
+
# 3. Draw lines
|
|
99
|
+
text_objects = _draw_lines(
|
|
100
|
+
lines, x, y, ax, renderer,
|
|
101
|
+
linespacing=linespacing, ha=ha, va=va,
|
|
102
|
+
transform=transform, zorder=zorder
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return text_objects
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _normalize_properties(
|
|
109
|
+
strings: List[str],
|
|
110
|
+
colors: Union[str, List[Any], Dict[Any, Any]],
|
|
111
|
+
**kwargs
|
|
112
|
+
) -> List[Dict[str, Any]]:
|
|
113
|
+
"""
|
|
114
|
+
Normalize colors and kwargs into a list of property dictionaries, one for each string.
|
|
115
|
+
Supports:
|
|
116
|
+
- Scalar values (applied globally).
|
|
117
|
+
- Lists of values (dynamic extension).
|
|
118
|
+
- Dictionaries with int/tuple keys (targeted overrides).
|
|
119
|
+
- Lists of pairs [(indices, value)] (targeted overrides).
|
|
120
|
+
- Plural arguments (e.g., fontsizes) overriding singular ones (e.g., fontsize).
|
|
121
|
+
"""
|
|
122
|
+
n = len(strings)
|
|
123
|
+
props_list = []
|
|
124
|
+
|
|
125
|
+
# Helper to extend list to length n
|
|
126
|
+
def extend_list(lst: List[Any], target_len: int) -> List[Any]:
|
|
127
|
+
if not lst:
|
|
128
|
+
return [None] * target_len
|
|
129
|
+
if len(lst) >= target_len:
|
|
130
|
+
return lst[:target_len]
|
|
131
|
+
last_val = lst[-1]
|
|
132
|
+
extension = [last_val] * (target_len - len(lst))
|
|
133
|
+
return lst + extension
|
|
134
|
+
|
|
135
|
+
# Helper to parse mapping from dict or list-of-pairs
|
|
136
|
+
def parse_mapping(val: Any) -> Optional[Dict[int, Any]]:
|
|
137
|
+
flat_d = {}
|
|
138
|
+
|
|
139
|
+
if isinstance(val, dict):
|
|
140
|
+
for k, v in val.items():
|
|
141
|
+
if isinstance(k, tuple):
|
|
142
|
+
for idx in k:
|
|
143
|
+
flat_d[idx] = v
|
|
144
|
+
else:
|
|
145
|
+
flat_d[k] = v
|
|
146
|
+
return flat_d
|
|
147
|
+
|
|
148
|
+
if isinstance(val, list):
|
|
149
|
+
if not val:
|
|
150
|
+
return None
|
|
151
|
+
first = val[0]
|
|
152
|
+
if isinstance(first, (list, tuple)) and len(first) == 2:
|
|
153
|
+
k, v = first
|
|
154
|
+
if isinstance(k, int) or (isinstance(k, (list, tuple)) and all(isinstance(x, int) for x in k)):
|
|
155
|
+
for item in val:
|
|
156
|
+
if not (isinstance(item, (list, tuple)) and len(item) == 2):
|
|
157
|
+
return None
|
|
158
|
+
k, v = item
|
|
159
|
+
if isinstance(k, (list, tuple)):
|
|
160
|
+
for idx in k:
|
|
161
|
+
flat_d[idx] = v
|
|
162
|
+
else:
|
|
163
|
+
flat_d[k] = v
|
|
164
|
+
return flat_d
|
|
165
|
+
return None
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
# Handle colors
|
|
169
|
+
color_mapping = parse_mapping(colors)
|
|
170
|
+
color_list = [None] * n
|
|
171
|
+
|
|
172
|
+
if color_mapping is not None:
|
|
173
|
+
pass
|
|
174
|
+
elif isinstance(colors, str):
|
|
175
|
+
color_list = [colors] * n
|
|
176
|
+
elif isinstance(colors, list):
|
|
177
|
+
color_list = extend_list(colors, n)
|
|
178
|
+
else:
|
|
179
|
+
if colors is not None:
|
|
180
|
+
raise ValueError("colors must be a string, a list, or a mapping.")
|
|
181
|
+
|
|
182
|
+
# Handle other kwargs
|
|
183
|
+
list_kwargs = {}
|
|
184
|
+
mapping_kwargs = {}
|
|
185
|
+
scalar_kwargs = {}
|
|
186
|
+
|
|
187
|
+
# Map plural keys to singular keys
|
|
188
|
+
plural_map = {
|
|
189
|
+
'fontsizes': 'fontsize',
|
|
190
|
+
'fontweights': 'fontweight',
|
|
191
|
+
'fontfamilies': 'fontfamily',
|
|
192
|
+
'fontstyles': 'fontstyle',
|
|
193
|
+
'alphas': 'alpha',
|
|
194
|
+
'backgroundcolors': 'backgroundcolor',
|
|
195
|
+
'colors': 'color', # Special case if passed in kwargs
|
|
196
|
+
'underlines': 'underline'
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
# Separate kwargs into types
|
|
200
|
+
for k, v in kwargs.items():
|
|
201
|
+
# Check for plural override first
|
|
202
|
+
if k in plural_map:
|
|
203
|
+
singular_k = plural_map[k]
|
|
204
|
+
mapping = parse_mapping(v)
|
|
205
|
+
if mapping is not None:
|
|
206
|
+
if singular_k not in mapping_kwargs:
|
|
207
|
+
mapping_kwargs[singular_k] = {}
|
|
208
|
+
mapping_kwargs[singular_k].update(mapping)
|
|
209
|
+
elif isinstance(v, list):
|
|
210
|
+
# If plural is a list, treat as list_kwargs for the singular key
|
|
211
|
+
list_kwargs[singular_k] = extend_list(v, n)
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
mapping = parse_mapping(v)
|
|
215
|
+
if mapping is not None:
|
|
216
|
+
mapping_kwargs[k] = mapping
|
|
217
|
+
elif isinstance(v, list):
|
|
218
|
+
list_kwargs[k] = extend_list(v, n)
|
|
219
|
+
else:
|
|
220
|
+
scalar_kwargs[k] = v
|
|
221
|
+
|
|
222
|
+
for i in range(n):
|
|
223
|
+
# 1. Start with scalar (global) properties
|
|
224
|
+
props = scalar_kwargs.copy()
|
|
225
|
+
|
|
226
|
+
# 2. Apply list-based properties (if any)
|
|
227
|
+
for k, v_list in list_kwargs.items():
|
|
228
|
+
props[k] = v_list[i]
|
|
229
|
+
|
|
230
|
+
# 3. Apply color (specific overrides global)
|
|
231
|
+
if color_list[i] is not None:
|
|
232
|
+
props['color'] = color_list[i]
|
|
233
|
+
|
|
234
|
+
if color_mapping and i in color_mapping:
|
|
235
|
+
props['color'] = color_mapping[i]
|
|
236
|
+
|
|
237
|
+
# 4. Apply mapping-based properties (specific overrides global & list)
|
|
238
|
+
for k, v_map in mapping_kwargs.items():
|
|
239
|
+
if i in v_map:
|
|
240
|
+
props[k] = v_map[i]
|
|
241
|
+
|
|
242
|
+
props_list.append(props)
|
|
243
|
+
|
|
244
|
+
return props_list
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _tokenize_strings(
|
|
248
|
+
strings: List[str],
|
|
249
|
+
properties: List[Dict[str, Any]]
|
|
250
|
+
) -> List[Tuple[str, Dict[str, Any]]]:
|
|
251
|
+
"""
|
|
252
|
+
Split strings into words while preserving spaces and associating properties.
|
|
253
|
+
Used ONLY when wrapping is enabled.
|
|
254
|
+
"""
|
|
255
|
+
words: List[Tuple[str, Dict[str, Any]]] = []
|
|
256
|
+
for string, props in zip(strings, properties):
|
|
257
|
+
parts = string.split(' ')
|
|
258
|
+
for i, part in enumerate(parts):
|
|
259
|
+
if i < len(parts) - 1:
|
|
260
|
+
words.append((part + ' ', props))
|
|
261
|
+
else:
|
|
262
|
+
if part:
|
|
263
|
+
words.append((part, props))
|
|
264
|
+
elif not part and i > 0:
|
|
265
|
+
pass
|
|
266
|
+
return words
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _get_text_width(text: str, ax: Axes, renderer, **text_kwargs) -> float:
|
|
270
|
+
"""Measure the width of a text string."""
|
|
271
|
+
# Remove custom properties that ax.text doesn't understand
|
|
272
|
+
kwargs = text_kwargs.copy()
|
|
273
|
+
kwargs.pop('underline', None)
|
|
274
|
+
|
|
275
|
+
t = ax.text(0, 0, text, **kwargs)
|
|
276
|
+
bbox = t.get_window_extent(renderer=renderer)
|
|
277
|
+
bbox_data = bbox.transformed(ax.transData.inverted())
|
|
278
|
+
w = bbox_data.width
|
|
279
|
+
t.remove()
|
|
280
|
+
return w
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _get_text_height(text: str, ax: Axes, renderer, **text_kwargs) -> float:
|
|
284
|
+
"""Measure the height of a text string."""
|
|
285
|
+
# Remove custom properties that ax.text doesn't understand
|
|
286
|
+
kwargs = text_kwargs.copy()
|
|
287
|
+
kwargs.pop('underline', None)
|
|
288
|
+
|
|
289
|
+
# Use a representative character for height if text is empty or space
|
|
290
|
+
# But we need the height of THIS specific font configuration.
|
|
291
|
+
measure_text = text if text.strip() else "Hg"
|
|
292
|
+
t = ax.text(0, 0, measure_text, **kwargs)
|
|
293
|
+
bbox = t.get_window_extent(renderer=renderer)
|
|
294
|
+
bbox_data = bbox.transformed(ax.transData.inverted())
|
|
295
|
+
h = bbox_data.height
|
|
296
|
+
t.remove()
|
|
297
|
+
return h
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _build_lines_wrapped(
|
|
301
|
+
words: List[Tuple[str, Dict[str, Any]]],
|
|
302
|
+
ax: Axes,
|
|
303
|
+
renderer,
|
|
304
|
+
box_width: float
|
|
305
|
+
) -> List[List[Tuple[str, Dict[str, Any], float]]]:
|
|
306
|
+
"""
|
|
307
|
+
Group words into lines based on box_width.
|
|
308
|
+
"""
|
|
309
|
+
lines: List[List[Tuple[str, Dict[str, Any], float]]] = []
|
|
310
|
+
current_line: List[Tuple[str, Dict[str, Any], float]] = []
|
|
311
|
+
current_line_width = 0.0
|
|
312
|
+
|
|
313
|
+
for word, props in words:
|
|
314
|
+
w = _get_text_width(word, ax, renderer, **props)
|
|
315
|
+
|
|
316
|
+
if current_line_width + w > box_width and current_line:
|
|
317
|
+
# Wrap to new line
|
|
318
|
+
lines.append(current_line)
|
|
319
|
+
current_line = [(word, props, w)]
|
|
320
|
+
current_line_width = w
|
|
321
|
+
else:
|
|
322
|
+
current_line.append((word, props, w))
|
|
323
|
+
current_line_width += w
|
|
324
|
+
|
|
325
|
+
if current_line:
|
|
326
|
+
lines.append(current_line)
|
|
327
|
+
|
|
328
|
+
return lines
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _build_line_seamless(
|
|
332
|
+
strings: List[str],
|
|
333
|
+
properties: List[Dict[str, Any]],
|
|
334
|
+
ax: Axes,
|
|
335
|
+
renderer
|
|
336
|
+
) -> List[Tuple[str, Dict[str, Any], float]]:
|
|
337
|
+
"""
|
|
338
|
+
Build a single line from strings without splitting by spaces.
|
|
339
|
+
"""
|
|
340
|
+
line: List[Tuple[str, Dict[str, Any], float]] = []
|
|
341
|
+
for string, props in zip(strings, properties):
|
|
342
|
+
w = _get_text_width(string, ax, renderer, **props)
|
|
343
|
+
line.append((string, props, w))
|
|
344
|
+
return line
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _draw_lines(
|
|
348
|
+
lines: List[List[Tuple[str, Dict[str, Any], float]]],
|
|
349
|
+
x: float,
|
|
350
|
+
y: float,
|
|
351
|
+
ax: Axes,
|
|
352
|
+
renderer,
|
|
353
|
+
linespacing: float,
|
|
354
|
+
ha: str,
|
|
355
|
+
va: str,
|
|
356
|
+
transform,
|
|
357
|
+
zorder: int
|
|
358
|
+
) -> List[Text]:
|
|
359
|
+
"""
|
|
360
|
+
Draw the lines of text onto the axes.
|
|
361
|
+
"""
|
|
362
|
+
text_objects: List[Text] = []
|
|
363
|
+
|
|
364
|
+
# Calculate height for each line
|
|
365
|
+
line_heights = []
|
|
366
|
+
for line in lines:
|
|
367
|
+
# Find max height in this line
|
|
368
|
+
max_h = 0.0
|
|
369
|
+
for word, props, _ in line:
|
|
370
|
+
h = _get_text_height(word, ax, renderer, **props)
|
|
371
|
+
if h > max_h:
|
|
372
|
+
max_h = h
|
|
373
|
+
line_heights.append(max_h * linespacing)
|
|
374
|
+
|
|
375
|
+
total_block_height = sum(line_heights)
|
|
376
|
+
|
|
377
|
+
# Calculate top Y position based on vertical alignment
|
|
378
|
+
if va == 'center':
|
|
379
|
+
top_y = y + (total_block_height / 2)
|
|
380
|
+
elif va == 'top':
|
|
381
|
+
top_y = y
|
|
382
|
+
elif va == 'bottom':
|
|
383
|
+
top_y = y + total_block_height
|
|
384
|
+
else:
|
|
385
|
+
top_y = y + (total_block_height / 2)
|
|
386
|
+
|
|
387
|
+
current_y = top_y
|
|
388
|
+
|
|
389
|
+
for i, line in enumerate(lines):
|
|
390
|
+
line_height = line_heights[i]
|
|
391
|
+
|
|
392
|
+
# Position line center
|
|
393
|
+
line_center_y = current_y - (line_height / 2)
|
|
394
|
+
|
|
395
|
+
# Calculate line width for horizontal alignment
|
|
396
|
+
line_width = sum(item[2] for item in line)
|
|
397
|
+
|
|
398
|
+
if ha == 'center':
|
|
399
|
+
line_start_x = x - (line_width / 2)
|
|
400
|
+
elif ha == 'right':
|
|
401
|
+
line_start_x = x - line_width
|
|
402
|
+
else: # left
|
|
403
|
+
line_start_x = x
|
|
404
|
+
|
|
405
|
+
current_x = line_start_x
|
|
406
|
+
|
|
407
|
+
for word, props, w in line:
|
|
408
|
+
text_kwargs = props.copy()
|
|
409
|
+
|
|
410
|
+
# Extract underline property
|
|
411
|
+
underline = text_kwargs.pop('underline', False)
|
|
412
|
+
|
|
413
|
+
text_kwargs.update({
|
|
414
|
+
'va': 'center',
|
|
415
|
+
'ha': 'left',
|
|
416
|
+
'transform': transform,
|
|
417
|
+
'zorder': zorder
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
t = ax.text(current_x, line_center_y, word, **text_kwargs)
|
|
421
|
+
text_objects.append(t)
|
|
422
|
+
|
|
423
|
+
# Draw underline if requested
|
|
424
|
+
if underline:
|
|
425
|
+
# Get the bounding box of the text we just drew
|
|
426
|
+
# We can use the width w we already calculated
|
|
427
|
+
# But for Y position, we want it slightly below the text
|
|
428
|
+
|
|
429
|
+
# A simple approximation is to draw it at the bottom of the line band?
|
|
430
|
+
# Or relative to the text baseline?
|
|
431
|
+
# Since we used va='center', the text is centered at line_center_y.
|
|
432
|
+
# The height is roughly line_height / linespacing.
|
|
433
|
+
# Let's put the underline at line_center_y - (h/2) - padding?
|
|
434
|
+
|
|
435
|
+
# Let's use the bbox of the text object to find the bottom
|
|
436
|
+
bbox = t.get_window_extent(renderer=renderer)
|
|
437
|
+
bbox_data = bbox.transformed(ax.transData.inverted())
|
|
438
|
+
|
|
439
|
+
# y0 is the bottom of the text bbox
|
|
440
|
+
y_bottom = bbox_data.y0
|
|
441
|
+
|
|
442
|
+
# Draw line from current_x to current_x + w
|
|
443
|
+
# Use the same color as the text
|
|
444
|
+
color = t.get_color()
|
|
445
|
+
|
|
446
|
+
line = Line2D(
|
|
447
|
+
[current_x, current_x + w],
|
|
448
|
+
[y_bottom, y_bottom],
|
|
449
|
+
color=color,
|
|
450
|
+
linewidth=1, # Maybe configurable?
|
|
451
|
+
transform=transform,
|
|
452
|
+
zorder=zorder
|
|
453
|
+
)
|
|
454
|
+
ax.add_line(line)
|
|
455
|
+
|
|
456
|
+
current_x += w
|
|
457
|
+
|
|
458
|
+
current_y -= line_height
|
|
459
|
+
|
|
460
|
+
return text_objects
|
mpl_richtext/version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.1.0'
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mpl-richtext
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Rich text rendering for Matplotlib with multi-color and multi-style support
|
|
5
|
+
Home-page: https://github.com/ra8in/mpl-richtext
|
|
6
|
+
Author: Rabin Katel
|
|
7
|
+
Author-email: Rabin Katel <kattelrabinraja13@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/ra8in/mpl-richtext
|
|
10
|
+
Project-URL: Documentation, https://github.com/ra8in/mpl-richtext#readme
|
|
11
|
+
Project-URL: Repository, https://github.com/ra8in/mpl-richtext
|
|
12
|
+
Project-URL: Bug Tracker, https://github.com/ra8in/mpl-richtext/issues
|
|
13
|
+
Keywords: matplotlib,text,color,rich-text,multi-color,visualization,plotting
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Science/Research
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Operating System :: OS Independent
|
|
26
|
+
Requires-Python: >=3.8
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: matplotlib>=3.5.0
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
33
|
+
Requires-Dist: black>=22.0; extra == "dev"
|
|
34
|
+
Requires-Dist: flake8>=5.0; extra == "dev"
|
|
35
|
+
Requires-Dist: mypy>=0.990; extra == "dev"
|
|
36
|
+
Dynamic: author
|
|
37
|
+
Dynamic: home-page
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
Dynamic: requires-python
|
|
40
|
+
|
|
41
|
+
# mpl-richtext
|
|
42
|
+
|
|
43
|
+
**Rich text rendering for Matplotlib** - Create beautiful multi-color, multi-style text in a single line.
|
|
44
|
+
|
|
45
|
+
[](https://pypi.org/project/mpl-richtext/)
|
|
46
|
+
[](https://opensource.org/licenses/MIT)
|
|
47
|
+
[](https://www.python.org/downloads/)
|
|
48
|
+
|
|
49
|
+
## Why mpl-richtext?
|
|
50
|
+
|
|
51
|
+
Standard Matplotlib only supports single-color text. To create multi-colored text, you need to manually position each piece and calculate spacing - tedious and error-prone!
|
|
52
|
+
|
|
53
|
+
**mpl-richtext** solves this by letting you specify colors and styles for each text segment in one simple function call.
|
|
54
|
+
|
|
55
|
+

|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install mpl-richtext
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import matplotlib.pyplot as plt
|
|
67
|
+
from mpl_richtext import richtext
|
|
68
|
+
|
|
69
|
+
fig, ax = plt.subplots()
|
|
70
|
+
|
|
71
|
+
# Create multi-colored text in one line!
|
|
72
|
+
richtext(0.5, 0.5,
|
|
73
|
+
strings=["hello", ", ", "world"],
|
|
74
|
+
colors=["red", "blue", "green"],
|
|
75
|
+
ax=ax, fontsize=20, transform=ax.transAxes)
|
|
76
|
+
|
|
77
|
+
plt.show()
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
## Features
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
✨ **Multi-color text** - Different colors for each word or character
|
|
87
|
+
🎨 **Multi-style text** - Mix font sizes, weights, families, and styles (italic/oblique)
|
|
88
|
+
📦 **Flexible input** - Lists, dicts, or tuples for colors and properties
|
|
89
|
+
📏 **Auto word-wrapping** - Specify `box_width` for automatic text wrapping
|
|
90
|
+
🎯 **Full alignment** - Left, center, right horizontal and vertical alignment
|
|
91
|
+
✨ **Decorations** - Support for **underlines** and **background colors**
|
|
92
|
+
🔄 **Transformations** - Support for text **rotation**
|
|
93
|
+
👻 **Transparency** - Support for **alpha** values per segment
|
|
94
|
+
⚡ **Easy to use** - Simple API, works with any Matplotlib axes
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
## API Reference
|
|
99
|
+
|
|
100
|
+
### `richtext(x, y, strings, colors=None, ax=None, **kwargs)`
|
|
101
|
+
|
|
102
|
+
**Parameters:**
|
|
103
|
+
|
|
104
|
+
- **x, y** : `float`
|
|
105
|
+
Starting position of the text
|
|
106
|
+
|
|
107
|
+
- **strings** : `list of str`
|
|
108
|
+
List of text segments, e.g., `["hello", ", ", "world"]`
|
|
109
|
+
|
|
110
|
+
- **colors** : `str`, `list`, or `dict`, optional
|
|
111
|
+
Colors for each segment. Can be:
|
|
112
|
+
- Single string: `"red"` (applies to all)
|
|
113
|
+
- List: `["red", "blue", "green"]` (one per segment)
|
|
114
|
+
- Dict: `{0: "red", 2: "green"}` (specific indices)
|
|
115
|
+
- Tuple keys: `{(0, 2): "red"}` (multiple indices same color)
|
|
116
|
+
|
|
117
|
+
- **ax** : `matplotlib.axes.Axes`, optional
|
|
118
|
+
Axes to draw on. If `None`, uses current axes.
|
|
119
|
+
|
|
120
|
+
- **kwargs** : Additional properties
|
|
121
|
+
- **Global:** `box_width`, `linespacing`, `ha`, `va`, `transform`, `zorder`
|
|
122
|
+
- **Per-segment:** `fontsize`/`fontsizes`, `fontweight`/`fontweights`, `fontfamily`/`fontfamilies`, etc.
|
|
123
|
+
- Any property can be:
|
|
124
|
+
- Single value (applies to all)
|
|
125
|
+
- List (one per segment, auto-extends)
|
|
126
|
+
- Dict (specific indices)
|
|
127
|
+
|
|
128
|
+
**Returns:**
|
|
129
|
+
- `list of Text` - List of created matplotlib Text objects
|
|
130
|
+
|
|
131
|
+
## Contributing
|
|
132
|
+
|
|
133
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
134
|
+
|
|
135
|
+
1. Fork the repository
|
|
136
|
+
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
|
|
137
|
+
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
|
138
|
+
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
|
139
|
+
5. Open a Pull Request
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
144
|
+
|
|
145
|
+
## Links
|
|
146
|
+
|
|
147
|
+
- **PyPI:** https://pypi.org/project/mpl-richtext/
|
|
148
|
+
- **GitHub:** https://github.com/ra8in/mpl-richtext
|
|
149
|
+
- **Issues:** https://github.com/ra8in/mpl-richtext/issues
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
Made with ❤️ for the Matplotlib community
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
mpl_richtext/__init__.py,sha256=430UvdXDNTa9xCoQ48OT_lzHrFCW_xCuVaTr1apEVq4,246
|
|
2
|
+
mpl_richtext/core.py,sha256=OZ1NCJQOHpIsdcM6c-7NKXdZxeTKBGoAPWkPkhpTfr4,15504
|
|
3
|
+
mpl_richtext/version.py,sha256=L6zbQIZKsAP-Knhm6fBcQFPoVdIDuejxze60qX23jiw,21
|
|
4
|
+
mpl_richtext-0.1.0.dist-info/licenses/LICENSE,sha256=3ldFhzXg_DuFYRbdYJaN661E5PAzUpVjWxFCxVm7kG8,1067
|
|
5
|
+
mpl_richtext-0.1.0.dist-info/METADATA,sha256=4NdhCgy6coUWZo6JOex4dZQgABBY17ReCKnwwtWkLC8,5311
|
|
6
|
+
mpl_richtext-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
+
mpl_richtext-0.1.0.dist-info/top_level.txt,sha256=3K_jSX3xxD2Dhaqm4jtejlO0HI_5tl7F7AMq4lGk6xw,13
|
|
8
|
+
mpl_richtext-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Rabin Katel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mpl_richtext
|