gsplot 0.0.1__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.
gsplot/style/label.py ADDED
@@ -0,0 +1,866 @@
1
+ from functools import wraps
2
+ from typing import Any, Callable, Literal, TypeVar, cast
3
+
4
+ import matplotlib.pyplot as plt
5
+ import matplotlib.ticker as plticker
6
+ import numpy as np
7
+ from matplotlib.axes import Axes
8
+ from matplotlib.backends.backend_agg import FigureCanvasAgg
9
+ from matplotlib.figure import Figure
10
+ from matplotlib.transforms import Bbox
11
+ from numpy.typing import ArrayLike, NDArray
12
+ from rich.console import Console
13
+ from rich.panel import Panel
14
+ from rich.text import Text
15
+
16
+ from ..base.base import CreateClassParams, ParamsGetter, bind_passed_params
17
+ from ..figure.axes_base import (AxesRangeSingleton, AxisRangeController,
18
+ AxisRangeManager)
19
+ from .ticks import MinorTicksAxes
20
+
21
+ __all__ = ["label", "label_add_index"]
22
+
23
+
24
+ console = Console()
25
+
26
+
27
+ F = TypeVar("F", bound=Callable[..., Any])
28
+
29
+
30
+ # !TODO: When label is removed from script with index, and the script is called to repl, it will throw warning.
31
+ # fix this behavior.
32
+ class FuncOrderManager:
33
+ def __init__(
34
+ self,
35
+ ) -> None:
36
+ self.last_called: str | None = None
37
+ self.rules: dict = {}
38
+
39
+ def add_rule(self, func_a: str, func_b: str, warning_message: str) -> None:
40
+ self.rules[(func_b, func_a)] = warning_message
41
+
42
+ def track(self, func_name: str) -> None:
43
+ if self.last_called is not None:
44
+ if (self.last_called, func_name) in self.rules:
45
+ warning_message = self.rules[(self.last_called, func_name)]
46
+ warning_text = Text.from_markup(warning_message, justify="center")
47
+ console.print(
48
+ Panel(
49
+ warning_text,
50
+ title="[bold yellow]Warning",
51
+ style="bold yellow",
52
+ )
53
+ )
54
+
55
+ self.reset()
56
+ else:
57
+ self.last_called = func_name
58
+
59
+ def reset(self) -> None:
60
+ self.last_called = None
61
+
62
+
63
+ order_manager = FuncOrderManager()
64
+ order_manager.add_rule(
65
+ "label",
66
+ "label_add_index",
67
+ "[bold green]label_add_index[bold yellow] should be called after [bold red]label",
68
+ )
69
+
70
+
71
+ def track_order(func: F) -> F:
72
+
73
+ @wraps(func)
74
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
75
+ func_name = func.__name__
76
+ order_manager.track(func_name)
77
+ return func(*args, **kwargs)
78
+
79
+ return cast(F, wrapper)
80
+
81
+
82
+ class LabelAddIndex:
83
+ """
84
+ A class to add index labels to axes in a Matplotlib figure.
85
+
86
+ This class allows labeling axes with indices in customizable positions, glyphs,
87
+ and styles.
88
+
89
+ Parameters
90
+ --------------------
91
+ loc : {'in', 'out', 'corner'}, default='out'
92
+ Location of the label relative to the axes:
93
+ - 'in': Inside the axes.
94
+ - 'out': Outside the axes.
95
+ - 'corner': Top-left corner of the axes.
96
+ x_offset : float, default=0
97
+ Horizontal offset for the label position.
98
+ y_offset : float, default=0
99
+ Vertical offset for the label position.
100
+ ha : str, default='center'
101
+ Horizontal alignment of the label.
102
+ va : str, default='top'
103
+ Vertical alignment of the label.
104
+ fontsize : str or float, default='large'
105
+ Font size of the label.
106
+ glyph : {'alphabet', 'roman', 'number', 'hiragana'}, default='alphabet'
107
+ Style of the label:
108
+ - 'alphabet': Letters (a, b, c, ...).
109
+ - 'roman': Roman numerals (i, ii, iii, ...).
110
+ - 'number': Numbers (1, 2, 3, ...).
111
+ - 'hiragana': Japanese Hiragana (あ, い, う, ...).
112
+ capitalize : bool, default=False
113
+ If True, capitalize the label (e.g., A, B, C instead of a, b, c).
114
+ *args : Any
115
+ Additional arguments for `matplotlib.text.Text`.
116
+ **kwargs : Any
117
+ Additional keyword arguments for `matplotlib.text.Text`.
118
+
119
+ Attributes
120
+ --------------------
121
+ fig : matplotlib.figure.Figure
122
+ The current figure object.
123
+ _axes : list[matplotlib.axes.Axes]
124
+ List of axes in the current figure.
125
+ renderer : matplotlib.backends.backend_agg.RendererAgg
126
+ Renderer for the figure.
127
+ fig_width : float
128
+ Width of the figure in pixels.
129
+ fig_height : float
130
+ Height of the figure in pixels.
131
+ canvas_width : int
132
+ Width of the canvas in pixels.
133
+ canvas_height : int
134
+ Height of the canvas in pixels.
135
+ normalization_factors : numpy.ndarray
136
+ Factors for normalizing coordinates.
137
+
138
+ Methods
139
+ --------------------
140
+ add_index() -> None
141
+ Adds index labels to the axes.
142
+ """
143
+
144
+ def __init__(
145
+ self,
146
+ loc: Literal["in", "out", "corner"] = "out",
147
+ x_offset: float = 0,
148
+ y_offset: float = 0,
149
+ ha: str = "center",
150
+ va: str = "top",
151
+ fontsize: str | float = "large",
152
+ glyph: Literal["alphabet", "roman", "number", "hiragana"] = "alphabet",
153
+ capitalize: bool = False,
154
+ *args: Any,
155
+ **kwargs: Any,
156
+ ) -> None:
157
+ self.loc = loc
158
+ self.x_offset = x_offset
159
+ self.y_offset = y_offset
160
+ self.ha = ha
161
+ self.va = va
162
+ self.fontsize = fontsize
163
+ self.glyph = glyph
164
+ self.capitalize = capitalize
165
+
166
+ self.args = args
167
+ self.kwargs = kwargs
168
+
169
+ self.fig: Figure = plt.gcf()
170
+ self._axes: list[Axes] = plt.gcf().axes
171
+
172
+ self.renderer = cast(FigureCanvasAgg, self.fig.canvas).get_renderer()
173
+
174
+ self.fig_width, self.fig_height = (
175
+ self.fig.bbox.bounds[2],
176
+ self.fig.bbox.bounds[3],
177
+ )
178
+ self.canvas_width, self.canvas_height = self.fig.canvas.get_width_height()
179
+
180
+ # To convert from device coordinates to figure coordinates
181
+ self.normalization_factors = np.array(
182
+ [self.fig_width, self.fig_height, self.fig_width, self.fig_height]
183
+ )
184
+
185
+ def _get_render_position(self, axis: Axes) -> tuple[float, float] | None:
186
+ """
187
+ Calculates the position for the index label based on the location.
188
+
189
+ Parameters
190
+ --------------------
191
+ axis : matplotlib.axes.Axes
192
+ The axis for which to calculate the label position.
193
+
194
+ Returns
195
+ --------------------
196
+ tuple[float, float] or None
197
+ The (x, y) coordinates of the label in figure-relative coordinates,
198
+ or None if the bounding box is unavailable.
199
+
200
+ Raises
201
+ --------------------
202
+ ValueError
203
+ If the specified location (`loc`) is invalid.
204
+ """
205
+ bbox: Bbox | None = None
206
+ PADDING_X: float = 0
207
+ PADDING_Y: float = 0
208
+
209
+ if self.loc == "out":
210
+ # Coordinate: device coordinates
211
+ bbox = axis.get_tightbbox(self.renderer)
212
+ PADDING_X, PADDING_Y = 0, -5
213
+ elif self.loc == "in":
214
+ # Coordinate: device coordinates
215
+ bbox = axis.get_window_extent(self.renderer)
216
+ PADDING_X, PADDING_Y = 30, -30
217
+ elif self.loc == "corner":
218
+ bbox = axis.get_window_extent(self.renderer)
219
+ else:
220
+ raise ValueError(
221
+ f"Invalid position: {self.loc}, must be 'in', 'out', or 'corner'"
222
+ )
223
+
224
+ # Ensure that padding does not depend on the figure size
225
+ PADDING_X = PADDING_X / self.canvas_width
226
+ PADDING_Y = PADDING_Y / self.canvas_height
227
+
228
+ if bbox is None:
229
+ print(f"No bounding box available for the axis. axis: {axis}")
230
+ return None
231
+
232
+ # Calculate the axis bounds in figure coordinates
233
+ axis_bounds_on_fig = np.array(bbox.bounds) / self.normalization_factors
234
+
235
+ x0 = axis_bounds_on_fig[0]
236
+ y0 = axis_bounds_on_fig[1]
237
+ width = axis_bounds_on_fig[2]
238
+ height = axis_bounds_on_fig[3]
239
+
240
+ x = x0 + self.x_offset + PADDING_X
241
+ y = y0 + height + self.y_offset + PADDING_Y
242
+ return x, y
243
+
244
+ @staticmethod
245
+ def int_to_roman(n: int) -> str:
246
+ """
247
+ Converts an integer to its Roman numeral representation.
248
+
249
+ Parameters
250
+ --------------------
251
+ n : int
252
+ The integer to convert.
253
+
254
+ Returns
255
+ --------------------
256
+ str
257
+ The Roman numeral representation.
258
+
259
+ Examples
260
+ --------------------
261
+ >>> LabelAddIndex.int_to_roman(3)
262
+ 'iii'
263
+ """
264
+ roman_numerals = {
265
+ 1: "i",
266
+ 2: "ii",
267
+ 3: "iii",
268
+ 4: "iv",
269
+ 5: "v",
270
+ 6: "vi",
271
+ 7: "vii",
272
+ 8: "viii",
273
+ 9: "ix",
274
+ 10: "x",
275
+ 11: "xi",
276
+ 12: "xii",
277
+ 13: "xiii",
278
+ 14: "xiv",
279
+ 15: "xv",
280
+ 16: "xvi",
281
+ 17: "xvii",
282
+ 18: "xviii",
283
+ }
284
+ return roman_numerals.get(n, "")
285
+
286
+ def get_index_glyph(self, n: int) -> str:
287
+ """
288
+ Retrieves the glyph representation of the index.
289
+
290
+ Parameters
291
+ --------------------
292
+ n : int
293
+ The index of the current axis (0-based).
294
+
295
+ Returns
296
+ --------------------
297
+ str
298
+ The glyph for the index.
299
+
300
+ Raises
301
+ --------------------
302
+ ValueError
303
+ If the specified glyph style (`glyph`) is invalid.
304
+
305
+ Examples
306
+ --------------------
307
+ >>> LabelAddIndex(glyph='alphabet').get_index_glyph(0)
308
+ 'a'
309
+ >>> LabelAddIndex(glyph='number').get_index_glyph(2)
310
+ '3'
311
+ """
312
+ index_glyph: str = ""
313
+ if self.glyph == "alphabet":
314
+ index_glyph = "abcdefghijklmnopqrstuvwxyz"[n]
315
+ elif self.glyph == "roman":
316
+ index_glyph = self.int_to_roman(n + 1)
317
+ elif self.glyph == "number":
318
+ index_glyph = str(n + 1)
319
+ elif self.glyph == "hiragana":
320
+ index_glyph = "あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよん"[
321
+ n
322
+ ]
323
+ else:
324
+ raise ValueError(
325
+ f"Invalid glyph: {self.glyph}, must be 'alphabet', 'roman', 'number', or 'hiragana'"
326
+ )
327
+ if self.capitalize:
328
+ index_glyph = index_glyph.upper()
329
+ return index_glyph
330
+
331
+ def add_index(self) -> None:
332
+ """
333
+ Adds index labels to all axes in the current figure.
334
+
335
+ The labels are positioned according to the specified location (`loc`),
336
+ offsets, and alignment.
337
+
338
+ Returns
339
+ --------------------
340
+ None
341
+ """
342
+ for i, axis in enumerate(self._axes):
343
+ position = self._get_render_position(axis)
344
+ if position is None:
345
+ continue
346
+ x, y = position
347
+
348
+ glyph = self.get_index_glyph(i)
349
+ self.fig.text(
350
+ x,
351
+ y,
352
+ f"($\\,${glyph}$\\,$)",
353
+ ha=self.ha,
354
+ va=self.va,
355
+ fontsize=self.fontsize,
356
+ transform=self.fig.transFigure,
357
+ *self.args,
358
+ **self.kwargs,
359
+ )
360
+
361
+
362
+ @bind_passed_params()
363
+ @track_order
364
+ def label_add_index(
365
+ loc: Literal["in", "out", "corner"] = "out",
366
+ x_offset: float = 0,
367
+ y_offset: float = 0,
368
+ ha: str = "center",
369
+ va: str = "center",
370
+ fontsize: float | str = "large",
371
+ glyph: Literal["alphabet", "roman", "number", "hiragana"] = "alphabet",
372
+ capitalize: bool = False,
373
+ *args: Any,
374
+ **kwargs: Any,
375
+ ) -> None:
376
+ """
377
+ Adds index labels to axes in a Matplotlib figure.
378
+
379
+ This function is a wrapper for the `LabelAddIndex` class.
380
+
381
+ Parameters
382
+ --------------------
383
+ loc : {'in', 'out', 'corner'}, default='out'
384
+ Location of the label relative to the axes.
385
+ x_offset : float, default=0
386
+ Horizontal offset for the label position.
387
+ y_offset : float, default=0
388
+ Vertical offset for the label position.
389
+ ha : str, default='center'
390
+ Horizontal alignment of the label.
391
+ va : str, default='center'
392
+ Vertical alignment of the label.
393
+ fontsize : str or float, default='large'
394
+ Font size of the label.
395
+ glyph : {'alphabet', 'roman', 'number', 'hiragana'}, default='alphabet'
396
+ Style of the label.
397
+ capitalize : bool, default=False
398
+ If True, capitalize the label.
399
+ *args : Any
400
+ Additional arguments for `matplotlib.text.Text`.
401
+ **kwargs : Any
402
+ Additional keyword arguments for `matplotlib.text.Text`.
403
+
404
+ Notes
405
+ --------------------
406
+ This function utilizes the `ParamsGetter` to retrieve bound parameters
407
+ and the `CreateClassParams` class to handle the merging of default,
408
+ configuration, and passed parameters.
409
+
410
+ Returns
411
+ --------------------
412
+ None
413
+
414
+ Warnings
415
+ --------------------
416
+ This function should be called after the :func:`gsplot.label <gsplot.style.label.label>` function
417
+
418
+ Examples
419
+ --------------------
420
+ >>> import gsplot as gs
421
+ >>> gs.label_add_index(loc='out', glyph='roman', fontsize=12)
422
+ >>> gs.label_add_index(loc='corner', capitalize=True)
423
+ """
424
+ passed_params: dict[str, Any] = ParamsGetter("passed_params").get_bound_params()
425
+ class_params = CreateClassParams(passed_params).get_class_params()
426
+
427
+ _label_add_index: LabelAddIndex = LabelAddIndex(
428
+ class_params["loc"],
429
+ class_params["x_offset"],
430
+ class_params["y_offset"],
431
+ class_params["ha"],
432
+ class_params["va"],
433
+ class_params["fontsize"],
434
+ class_params["glyph"],
435
+ class_params["capitalize"],
436
+ *class_params["args"],
437
+ **class_params["kwargs"],
438
+ )
439
+ _label_add_index.add_index()
440
+
441
+
442
+ class Label:
443
+ """
444
+ A class to configure labels, limits, and ticks for Matplotlib axes.
445
+
446
+ This class facilitates the customization of axis labels, limits, tick marks,
447
+ and layouts for multiple axes in a Matplotlib figure.
448
+
449
+ Parameters
450
+ --------------------
451
+ lab_lims : list[Any]
452
+ A list specifying labels and limits for each axis in the figure. Each entry
453
+ should be a tuple of the form `(x_label, y_label, x_limits, y_limits)`.
454
+ x_pad : int, default=2
455
+ Horizontal padding for tight layout.
456
+ y_pad : int, default=2
457
+ Vertical padding for tight layout.
458
+ minor_ticks_axes : bool, default=True
459
+ Whether to add minor ticks to all axes.
460
+ tight_layout : bool, default=True
461
+ Whether to apply `tight_layout` to the figure.
462
+ *args : Any
463
+ Additional arguments for `plt.tight_layout`.
464
+ **kwargs : Any
465
+ Additional keyword arguments for `plt.tight_layout`.
466
+
467
+ Attributes
468
+ --------------------
469
+ lab_lims : list[Any]
470
+ The labels and limits configuration for the axes.
471
+ x_pad : int
472
+ Horizontal padding for tight layout.
473
+ y_pad : int
474
+ Vertical padding for tight layout.
475
+ minor_ticks_axes : bool
476
+ Whether minor ticks are enabled for axes.
477
+ tight_layout : bool
478
+ Whether `tight_layout` is applied.
479
+ _axes : list[Axes]
480
+ List of axes in the current figure.
481
+
482
+ Methods
483
+ --------------------
484
+ set_xticks(axis, base=None, num_minor=None) -> None
485
+ Configures the major and minor ticks for the x-axis.
486
+ set_yticks(axis, base=None, num_minor=None) -> None
487
+ Configures the major and minor ticks for the y-axis.
488
+ remove_xlabels(axis) -> None
489
+ Removes the x-axis labels for a given axis.
490
+ remove_ylabels(axis) -> None
491
+ Removes the y-axis labels for a given axis.
492
+ add_minor_ticks_axes() -> None
493
+ Adds minor ticks to all axes in the figure.
494
+ configure_axis_labels(axis, x_lab, y_lab) -> None
495
+ Configures the labels for a given axis.
496
+ configure_axis_limits(axis, lims, final_axes_range=None, index=None) -> None
497
+ Configures the limits and scales for a given axis.
498
+ set_labels() -> None
499
+ Applies labels, limits, and scales to all axes based on the configuration.
500
+ apply_tight_layout() -> None
501
+ Applies `tight_layout` to the figure.
502
+ label() -> None
503
+ Adds labels, limits, and layouts to the figure's axes.
504
+ """
505
+
506
+ def __init__(
507
+ self,
508
+ lab_lims: list[Any],
509
+ x_pad: int = 2,
510
+ y_pad: int = 2,
511
+ minor_ticks_axes: bool = True,
512
+ tight_layout: bool = True,
513
+ *args: Any,
514
+ **kwargs: Any,
515
+ ) -> None:
516
+
517
+ self.lab_lims: list[Any] = lab_lims
518
+ self.x_pad: int = x_pad
519
+ self.y_pad: int = y_pad
520
+ self.minor_ticks_axes: bool = minor_ticks_axes
521
+ self.tight_layout: bool = tight_layout
522
+ self.args: Any = args
523
+ self.kwargs: Any = kwargs
524
+
525
+ self._axes: list[Axes] = plt.gcf().axes
526
+
527
+ def set_xticks(
528
+ self, axis: Axes, base: float | None = None, num_minor: int | None = None
529
+ ) -> None:
530
+ """
531
+ Configures the major and minor ticks for the x-axis.
532
+
533
+ Parameters
534
+ --------------------
535
+ axis : matplotlib.axes.Axes
536
+ The axis for which to configure ticks.
537
+ base : float, optional
538
+ Interval for the major ticks.
539
+ num_minor : int, optional
540
+ Number of minor ticks between consecutive major ticks.
541
+
542
+ Returns
543
+ --------------------
544
+ None
545
+ """
546
+
547
+ if base is not None:
548
+ axis.xaxis.set_major_locator(plticker.MultipleLocator(base=base))
549
+ if num_minor is not None:
550
+ axis.xaxis.set_minor_locator(plticker.AutoMinorLocator(num_minor))
551
+
552
+ def set_yticks(
553
+ self, axis: Axes, base: float | None = None, num_minor: int | None = None
554
+ ) -> None:
555
+ """
556
+ Configures the major and minor ticks for the y-axis.
557
+
558
+ Parameters
559
+ --------------------
560
+ axis : matplotlib.axes.Axes
561
+ The axis for which to configure ticks.
562
+ base : float, optional
563
+ Interval for the major ticks.
564
+ num_minor : int, optional
565
+ Number of minor ticks between consecutive major ticks.
566
+
567
+ Returns
568
+ --------------------
569
+ None
570
+ """
571
+
572
+ if base is not None:
573
+ axis.yaxis.set_major_locator(plticker.MultipleLocator(base=base))
574
+ if num_minor is not None:
575
+ axis.yaxis.set_minor_locator(plticker.AutoMinorLocator(num_minor))
576
+
577
+ def remove_xlabels(self, axis: Axes) -> None:
578
+ """
579
+ Removes the x-axis label for a given axis.
580
+
581
+ Parameters
582
+ --------------------
583
+ axis : matplotlib.axes.Axes
584
+ The axis for which to remove the x-axis label.
585
+
586
+ Returns
587
+ --------------------
588
+ None
589
+ """
590
+
591
+ axis.set_xlabel("")
592
+ axis.tick_params(labelbottom=False)
593
+
594
+ def remove_ylabels(self, axis: Axes) -> None:
595
+ """
596
+ Removes the y-axis label for a given axis.
597
+
598
+ Parameters
599
+ --------------------
600
+ axis : matplotlib.axes.Axes
601
+ The axis for which to remove the y-axis label.
602
+
603
+ Returns
604
+ --------------------
605
+ None
606
+ """
607
+
608
+ axis.set_ylabel("")
609
+ axis.tick_params(labelleft=False)
610
+
611
+ def add_minor_ticks_axes(self) -> None:
612
+ """
613
+ Adds minor ticks to all axes in the figure.
614
+
615
+ Returns
616
+ --------------------
617
+ None
618
+ """
619
+
620
+ if self.minor_ticks_axes:
621
+ MinorTicksAxes().set_minor_ticks_axes()
622
+
623
+ def configure_axis_labels(self, axis, x_lab, y_lab):
624
+ """
625
+ Configures the labels for a given axis.
626
+
627
+ Parameters
628
+ --------------------
629
+ axis : matplotlib.axes.Axes
630
+ The axis to configure labels for.
631
+ x_lab : str, optional
632
+ The label for the x-axis. If `None`, the x-axis label is removed.
633
+ y_lab : str, optional
634
+ The label for the y-axis. If `None`, the y-axis label is removed.
635
+
636
+ Returns
637
+ --------------------
638
+ None
639
+ """
640
+ if x_lab:
641
+ axis.set_xlabel(x_lab)
642
+ else:
643
+ self.remove_xlabels(axis)
644
+
645
+ if y_lab:
646
+ axis.set_ylabel(y_lab)
647
+ else:
648
+ self.remove_ylabels(axis)
649
+
650
+ def configure_axis_limits(self, axis, lims, final_axes_range=None, index=None):
651
+ """
652
+ Configures the limits and scales for a given axis.
653
+
654
+ Parameters
655
+ --------------------
656
+ axis : matplotlib.axes.Axes
657
+ The axis to configure limits for.
658
+ lims : list, optional
659
+ The limits for the axis in the form `[x_lims, y_lims]`.
660
+ final_axes_range : list, optional
661
+ The final axes ranges for all axes.
662
+ index : int, optional
663
+ The index of the current axis.
664
+
665
+ Returns
666
+ --------------------
667
+ None
668
+ """
669
+ if lims:
670
+ x_lims, y_lims = lims
671
+
672
+ # Set axis limits
673
+ for lim, val in zip(
674
+ ["xmin", "xmax", "ymin", "ymax"],
675
+ [x_lims[0], x_lims[1], y_lims[0], y_lims[1]],
676
+ ):
677
+ if val != "":
678
+ axis.axis(**{lim: val})
679
+
680
+ # Configure x-axis scale or ticks
681
+ if len(x_lims) > 2:
682
+ if isinstance(x_lims[2], str):
683
+ axis.set_xscale(x_lims[2])
684
+ else:
685
+ self.set_xticks(axis, num_minor=x_lims[2])
686
+ if len(x_lims) > 3:
687
+ self.set_xticks(axis, base=x_lims[3])
688
+
689
+ # Configure y-axis scale or ticks
690
+ if len(y_lims) > 2:
691
+ if isinstance(y_lims[2], str):
692
+ axis.set_yscale(y_lims[2])
693
+ else:
694
+ self.set_yticks(axis, num_minor=y_lims[2])
695
+ if len(y_lims) > 3:
696
+ self.set_yticks(axis, base=y_lims[3])
697
+ elif final_axes_range and index is not None:
698
+ final_axis_range = final_axes_range[index]
699
+ axis.axis(
700
+ xmin=final_axis_range[0][0],
701
+ xmax=final_axis_range[0][1],
702
+ ymin=final_axis_range[1][0],
703
+ ymax=final_axis_range[1][1],
704
+ )
705
+
706
+ def set_labels(self):
707
+
708
+ final_axes_range = self._get_final_axes_range()
709
+
710
+ for i, (x_lab, y_lab, *lims) in enumerate(self.lab_lims):
711
+ axis = self._axes[i]
712
+
713
+ # Configure axis labels
714
+ self.configure_axis_labels(axis, x_lab, y_lab)
715
+
716
+ # Configure axis limits and scales
717
+ self.configure_axis_limits(axis, lims, final_axes_range, i)
718
+
719
+ self._get_final_axes_range()
720
+
721
+ def _calculate_padding_range(self, range: NDArray[Any]) -> NDArray[Any]:
722
+
723
+ PADDING_FACTOR: float = 0.05
724
+ span: float = range[1] - range[0]
725
+ return np.array(
726
+ range + np.array([-PADDING_FACTOR, PADDING_FACTOR]) * span, dtype=np.float64
727
+ )
728
+
729
+ def _get_wider_range(
730
+ self, range1: NDArray[Any], range2: NDArray[Any]
731
+ ) -> NDArray[Any]:
732
+
733
+ new_range = np.array([min(range1[0], range2[0]), max(range1[1], range2[1])])
734
+ return new_range
735
+
736
+ def _get_axes_ranges_current(self) -> list[list[NDArray[Any]]]:
737
+
738
+ axes_ranges_current = []
739
+ for axis_index in range(len(self._axes)):
740
+ xrange = AxisRangeController(axis_index).get_axis_xrange()
741
+ yrange = AxisRangeController(axis_index).get_axis_yrange()
742
+ axes_ranges_current.append([xrange, yrange])
743
+ return axes_ranges_current
744
+
745
+ def _get_final_axes_range(self) -> list[list[NDArray[Any]]]:
746
+
747
+ axes_ranges_singleton = AxesRangeSingleton().axes_ranges
748
+ axes_ranges_current = self._get_axes_ranges_current()
749
+
750
+ final_axes_ranges = []
751
+ for axis_index, (xrange, yrange) in enumerate(axes_ranges_current):
752
+ xrange_singleton = axes_ranges_singleton[axis_index][0]
753
+ yrange_singleton = axes_ranges_singleton[axis_index][1]
754
+
755
+ is_init_axis = AxisRangeManager(axis_index).is_init_axis()
756
+
757
+ if is_init_axis and xrange_singleton is not None:
758
+ new_xrange = xrange_singleton
759
+ elif not is_init_axis and xrange_singleton is not None:
760
+ new_xrange = self._get_wider_range(xrange, xrange_singleton)
761
+ else:
762
+ new_xrange = xrange
763
+
764
+ if is_init_axis and yrange_singleton is not None:
765
+ new_yrange = yrange_singleton
766
+ elif not is_init_axis and yrange_singleton is not None:
767
+ new_yrange = self._get_wider_range(yrange, yrange_singleton)
768
+ else:
769
+ new_yrange = yrange
770
+
771
+ new_xrange = self._calculate_padding_range(new_xrange)
772
+ new_yrange = self._calculate_padding_range(new_yrange)
773
+
774
+ final_axes_ranges.append([new_xrange, new_yrange])
775
+ return final_axes_ranges
776
+
777
+ #! Xpad and Ypad will change the size of the axis
778
+ def apply_tight_layout(self) -> None:
779
+
780
+ if self.tight_layout:
781
+ try:
782
+ plt.tight_layout(
783
+ w_pad=self.x_pad, h_pad=self.y_pad, *self.args, **self.kwargs
784
+ )
785
+ except Exception:
786
+ plt.tight_layout(w_pad=self.x_pad, h_pad=self.y_pad)
787
+
788
+ def label(self) -> None:
789
+
790
+ self.add_minor_ticks_axes()
791
+ self.set_labels()
792
+ self.apply_tight_layout()
793
+
794
+
795
+ @bind_passed_params()
796
+ @track_order
797
+ def label(
798
+ lab_lims: list[Any],
799
+ x_pad: int = 2,
800
+ y_pad: int = 2,
801
+ minor_ticks_axes: bool = True,
802
+ tight_layout: bool = True,
803
+ *args: Any,
804
+ **kwargs: Any,
805
+ ) -> None:
806
+ """
807
+ Configures labels, limits, ticks, and layouts for Matplotlib axes.
808
+
809
+ This function is a wrapper for the `Label` class.
810
+
811
+ Parameters
812
+ --------------------
813
+ lab_lims : list[Any]
814
+ A list specifying labels and limits for each axis in the figure. Each entry
815
+ should be a tuple of the form `(x_label, y_label, x_limits, y_limits)`.
816
+ x_pad : int, default=2
817
+ Horizontal padding for tight layout.
818
+ y_pad : int, default=2
819
+ Vertical padding for tight layout.
820
+ minor_ticks_axes : bool, default=True
821
+ Whether to add minor ticks to all axes.
822
+ tight_layout : bool, default=True
823
+ Whether to apply `tight_layout` to the figure.
824
+ *args : Any
825
+ Additional arguments for `plt.tight_layout`.
826
+ **kwargs : Any
827
+ Additional keyword arguments for `plt.tight_layout`.
828
+
829
+ Notes
830
+ --------------------
831
+ This function utilizes the `ParamsGetter` to retrieve bound parameters
832
+ and the `CreateClassParams` class to handle the merging of default,
833
+ configuration, and passed parameters.
834
+
835
+ Returns
836
+ --------------------
837
+ None
838
+
839
+ Warnings
840
+ --------------------
841
+ This function should be called before the :func:`gsplot.label_add_index <gsplot.style.label.label_add_index>` function
842
+
843
+ Examples
844
+ --------------------
845
+ >>> import gsplot as gs
846
+ >>> gs.label(
847
+ >>> lab_lims=[("X Label", "Y Label", [1, 10, "log"], [1, 20, 2])],
848
+ >>> x_pad=5,
849
+ >>> y_pad=5,
850
+ >>> )
851
+ """
852
+
853
+ passed_params: dict[str, Any] = ParamsGetter("passed_params").get_bound_params()
854
+ class_params = CreateClassParams(passed_params).get_class_params()
855
+
856
+ _label = Label(
857
+ class_params["lab_lims"],
858
+ class_params["x_pad"],
859
+ class_params["y_pad"],
860
+ class_params["minor_ticks_axes"],
861
+ class_params["tight_layout"],
862
+ *class_params["args"],
863
+ **class_params["kwargs"],
864
+ )
865
+
866
+ _label.label()