modusa 0.3.52__py3-none-any.whl → 0.3.53__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.
modusa/__init__.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from modusa.utils import excp, config
2
2
 
3
3
  #=====Giving access to plot functions to plot multiple signals.=====
4
- from modusa.tools import fig1d, fig2d, plot_dist, fig
4
+ from modusa.tools import plot_dist, fig
5
5
  #=====
6
6
 
7
7
  from modusa.tools import play, convert, record
modusa/tools/__init__.py CHANGED
@@ -7,7 +7,5 @@ from .audio_loader import load
7
7
  from .ann_loader import load_ann
8
8
  from .audio_recorder import record
9
9
 
10
- from .plotter import Figure1D as fig1d
11
- from .plotter import Figure2D as fig2d
12
10
  from .plotter import Fig as fig
13
11
  from .plotter import plot_dist
modusa/tools/plotter.py CHANGED
@@ -36,887 +36,15 @@ def _load_devanagari_font():
36
36
  _load_devanagari_font()
37
37
  #==============
38
38
 
39
- #--------------------------
40
- # Figuure 1D
41
- #--------------------------
42
- class Figure1D:
43
- """
44
- A utility class that provides easy-to-use API
45
- for plotting 1D signals along with clean
46
- representations of annotations, events.
47
-
48
-
49
-
50
- Parameters
51
- ----------
52
- n_aux_subplots: int
53
- - Total number of auxiliary subplots
54
- - These include annotations and events subplots.
55
- - Default: 0
56
- title: str
57
- - Title of the figure.
58
- - Default: Title
59
- xlim: tuple[number, number]
60
- - xlim for the figure.
61
- - All subplots will be automatically adjusted.
62
- - Default: None
63
- ylim: tuple[number, number]
64
- - ylim for the signal.
65
- - Default: None
66
- """
67
-
68
- def __init__(self, n_aux_subplots=0, xlim=None, ylim=None, sharex=None):
69
- self._n_aux_subplots: int = n_aux_subplots
70
- self._active_subplot_idx: int = 1 # Any addition will happen on this subplot (0 is reserved for reference axis)
71
- if sharex is not None:
72
- xlim = sharex._xlim
73
- self._xlim = xlim # Many add functions depend on this, so we fix it while instantiating the class
74
- self._ylim = ylim
75
- self._subplots, self._fig = self._generate_subplots() # Will contain all the subplots (list, fig)
76
-
77
- def _get_active_subplot(self):
78
- """
79
- Get the active subplot where you can add
80
- either annotations or events.
81
- """
82
- active_subplot = self._subplots[self._active_subplot_idx]
83
- self._active_subplot_idx += 1
84
-
85
- return active_subplot
86
-
87
- def _generate_subplots(self):
88
- """
89
- Generate subplots based on the configuration.
90
- """
91
-
92
- n_aux_subplots = self._n_aux_subplots
93
-
94
- # Fixed heights per subplot type
95
- ref_height = 0.0
96
- aux_height = 0.4
97
- signal_height = 2.0
98
-
99
- # Total number of subplots
100
- n_subplots = 1 + n_aux_subplots + 1
101
-
102
- # Compute total fig height
103
- fig_height = ref_height + n_aux_subplots * aux_height + signal_height
104
-
105
- # Define height ratios
106
- height_ratios = [ref_height] + [aux_height] * n_aux_subplots + [signal_height]
107
-
108
- # Create figure and grid
109
- fig = plt.figure(figsize=(16, fig_height))
110
- gs = gridspec.GridSpec(n_subplots, 1, height_ratios=height_ratios)
111
-
112
- # Add subplots
113
- subplots_list = []
114
-
115
- ref_subplot = fig.add_subplot(gs[0, 0])
116
- ref_subplot.axis("off")
117
- subplots_list.append(ref_subplot)
118
-
119
- for i in range(1, n_subplots):
120
- subplots_list.append(fig.add_subplot(gs[i, 0], sharex=ref_subplot))
121
-
122
- for i in range(n_subplots - 1):
123
- subplots_list[i].tick_params(left=False, bottom=False, labelleft=False, labelbottom=False)
124
-
125
-
126
- # Set xlim
127
- if self._xlim is not None: # xlim should be applied on reference subplot, rest all sharex
128
- ref_subplot.set_xlim(self._xlim)
129
-
130
- # Set ylim
131
- if self._ylim is not None: # ylim should be applied on the signal subplot
132
- subplots_list[-1].set_ylim(self._ylim)
133
-
134
- fig.subplots_adjust(hspace=0.01, wspace=0.05)
135
-
136
- return subplots_list, fig
137
-
138
-
139
- def add_events(self, events, c="k", ls="-", lw=1.5, label="Event Label"):
140
- """
141
- Add events to the figure.
142
-
143
- Parameters
144
- ----------
145
- events: np.ndarray
146
- - All the event marker values.
147
- c: str
148
- - Color of the event marker.
149
- - Default: "k"
150
- ls: str
151
- - Line style.
152
- - Default: "-"
153
- lw: float
154
- - Linewidth.
155
- - Default: 1.5
156
- label: str
157
- - Label for the event type.
158
- - This will appear in the legend.
159
- - Default: "Event label"
160
-
161
- Returns
162
- -------
163
- None
164
- """
165
- event_subplot = self._get_active_subplot()
166
- xlim = self._xlim
167
-
168
- for i, event in enumerate(events):
169
- if xlim is not None:
170
- if xlim[0] <= event <= xlim[1]:
171
- if i == 0: # Label should be set only once for all the events
172
- event_subplot.axvline(x=event, color=c, linestyle=ls, linewidth=lw, label=label)
173
- else:
174
- event_subplot.axvline(x=event, color=c, linestyle=ls, linewidth=lw)
175
- else:
176
- if i == 0: # Label should be set only once for all the events
177
- event_subplot.axvline(x=event, color=c, linestyle=ls, linewidth=lw, label=label)
178
- else:
179
- event_subplot.axvline(x=event, color=c, linestyle=ls, linewidth=lw)
180
-
181
- def add_annotation(self, ann):
182
- """
183
- Add annotation to the figure.
184
-
185
- Parameters
186
- ----------
187
- ann : list[tuple[Number, Number, str]] | None
188
- - A list of annotation spans. Each tuple should be (start, end, label).
189
- - Default: None (no annotations).
190
-
191
- Returns
192
- -------
193
- None
194
- """
195
-
196
- ann_subplot = self._get_active_subplot()
197
- xlim = self._xlim
198
-
199
- for i, (start, end, tag) in enumerate(ann):
200
-
201
- # We make sure that we only plot annotation that are within the x range of the current view
202
- if xlim is not None:
203
- if start >= xlim[1] or end <= xlim[0]:
204
- continue
205
-
206
- # Clip boundaries to xlim
207
- start = max(start, xlim[0])
208
- end = min(end, xlim[1])
209
-
210
- box_colors = ["gray", "lightgray"] # Alternates color between two
211
- box_color = box_colors[i % 2]
212
-
213
- width = end - start
214
- rect = Rectangle((start, 0), width, 1, facecolor=box_color, edgecolor="black", alpha=0.7)
215
- ann_subplot.add_patch(rect)
216
-
217
- text_obj = ann_subplot.text(
218
- (start + end) / 2, 0.5, tag,
219
- ha='center', va='center',
220
- fontsize=10, color="black", fontweight='bold', zorder=10, clip_on=True
221
- )
222
-
223
- text_obj.set_clip_path(rect)
224
- else:
225
- box_colors = ["gray", "lightgray"] # Alternates color between two
226
- box_color = box_colors[i % 2]
227
-
228
- width = end - start
229
- rect = Rectangle((start, 0), width, 1, facecolor=box_color, edgecolor="black", alpha=0.7)
230
- ann_subplot.add_patch(rect)
231
-
232
- text_obj = ann_subplot.text(
233
- (start + end) / 2, 0.5, tag,
234
- ha='center', va='center',
235
- fontsize=10, color="black", fontweight='bold', zorder=10, clip_on=True
236
- )
237
-
238
- text_obj.set_clip_path(rect)
239
-
240
- def add_signal(self, y, x=None, c=None, ls="-", lw=1.5, m=None, ms=3, label="Signal"):
241
- """
242
- Add signal to the figure.
243
-
244
- Parameters
245
- ----------
246
- y: np.ndarray
247
- - Signal y values.
248
- x: np.ndarray | None
249
- - Signal x values.
250
- - Default: None (indices will be used)
251
- c: str
252
- - Color of the line.
253
- - Default: None
254
- ls: str
255
- - Linestyle
256
- - Default: "-"
257
- lw: Number
258
- - Linewidth
259
- - Default: 1
260
- m: str
261
- - Marker
262
- - Default: None
263
- ms: number
264
- - Markersize
265
- - Default: 5
266
- label: str
267
- - Label for the plot.
268
- - Legend will use this.
269
- - Default: "Signal"
270
-
271
- Returns
272
- -------
273
- None
274
- """
275
- if x is None:
276
- x = np.arange(y.size)
277
- signal_subplot = self._subplots[-1]
278
- signal_subplot.plot(x, y, color=c, linestyle=ls, linewidth=lw, marker=m, markersize=ms, label=label)
279
-
280
- def add_legend(self, ypos=1.3):
281
- """
282
- Add legend to the figure.
283
-
284
- Parameters
285
- ----------
286
- ypos: float
287
- - y position from the top.
288
- - > 1 to push it higher, < 1 to push it lower
289
- - Default: 1.3
290
-
291
- Returns
292
- -------
293
- None
294
- """
295
- subplots: list = self._subplots
296
- fig = self._fig
297
-
298
- all_handles, all_labels = [], []
299
-
300
- for subplot in subplots:
301
- handles, labels = subplot.get_legend_handles_labels()
302
- all_handles.extend(handles)
303
- all_labels.extend(labels)
304
-
305
- # remove duplicates if needed
306
- fig.legend(all_handles, all_labels, loc='upper right', bbox_to_anchor=(0.9, ypos), ncol=2, frameon=True, bbox_transform=fig.transFigure)
307
-
308
- def add_meta_info(self, title=None, ylabel=None, xlabel=None, ts=13, ls=11):
309
- """
310
- Add meta info to the figure.
311
-
312
- Parameters
313
- ----------
314
- title: str
315
- - Title of the figure.
316
- - Default: None
317
- ylabel: str
318
- - y label of the signal.
319
- - It will only appear in the signal subplot.
320
- - Default: None
321
- xlabel: str
322
- - x label of the signal.
323
- - It will only appear in the signal subplot.
324
- - Default: None
325
- ts: Number
326
- - Title size
327
- - Default: 10
328
- ls: Number
329
- - Label size.
330
- - Default: 10
331
- Returns
332
- -------
333
- None
334
- """
335
- subplots: list = self._subplots
336
- fig = self._fig
337
-
338
- ref_subplot = subplots[0]
339
- signal_subplot = subplots[-1]
340
-
341
- if title is not None:
342
- ref_subplot.set_title(title, pad=10, size=ts)
343
- if ylabel is not None:
344
- signal_subplot.set_ylabel(ylabel, size=ls)
345
- if xlabel is not None:
346
- signal_subplot.set_xlabel(xlabel, size=ls)
347
-
348
-
349
- def save(self, path="./figure.png"):
350
- """
351
- Save the figure.
352
-
353
- Parameters
354
- ----------
355
- path: str
356
- - Path to the output file.
357
-
358
- Returns
359
- -------
360
- None
361
- """
362
- fig = self._fig
363
- fig.savefig(path, bbox_inches="tight")
364
-
365
-
366
- #--------------------------
367
- # Figuure 2D
368
- #--------------------------
369
- class Figure2D:
370
- """
371
- A utility class that provides easy-to-use API
372
- for plotting 2D signals along with clean
373
- representations of annotations, events.
374
-
375
-
376
-
377
- Parameters
378
- ----------
379
- n_aux_subplots: int
380
- - Total number of auxiliary subplots
381
- - These include annotations and events subplots.
382
- - Default: 0
383
- title: str
384
- - Title of the figure.
385
- - Default: Title
386
- xlim: tuple[number, number]
387
- - xlim for the figure.
388
- - All subplots will be automatically adjusted.
389
- - Default: None
390
- ylim: tuple[number, number]
391
- - ylim for the signal.
392
- - Default: None
393
- """
394
-
395
- def __init__(self, n_aux_subplots=0, xlim=None, ylim=None, sharex=None):
396
- self._n_aux_subplots: int = n_aux_subplots
397
- self._active_subplot_idx: int = 1 # Any addition will happen on this subplot (0 is reserved for reference axis)
398
- if sharex is not None:
399
- xlim = sharex._xlim
400
- self._xlim = xlim # Many add functions depend on this, so we fix it while instantiating the class
401
- self._ylim = ylim
402
- self._subplots, self._fig = self._generate_subplots() # Will contain all the subplots (list, fig)
403
- self._im = None # Useful while creating colorbar for the image
404
-
405
- def _get_active_subplot(self):
406
- """
407
- Get the active subplot where you can add
408
- either annotations or events.
409
- """
410
- active_subplot = self._subplots[self._active_subplot_idx]
411
- self._active_subplot_idx += 1
412
-
413
- return active_subplot
414
-
415
- def _calculate_extent(self, x, y):
416
- # Handle spacing safely
417
- if len(x) > 1:
418
- dx = x[1] - x[0]
419
- else:
420
- dx = 1 # Default spacing for single value
421
- if len(y) > 1:
422
- dy = y[1] - y[0]
423
- else:
424
- dy = 1 # Default spacing for single value
425
-
426
- return [x[0] - dx / 2, x[-1] + dx / 2, y[0] - dy / 2, y[-1] + dy / 2]
427
-
428
- def _add_colorbar(self, im, label=None, width="20%", height="35%"):
429
- from mpl_toolkits.axes_grid1.inset_locator import inset_axes
430
-
431
- ref_subplot = self._subplots[0]
432
-
433
- # Assume ref_subplot is your reference axes, im is the image
434
- cax = inset_axes(
435
- ref_subplot,
436
- width=width, # width of colorbar
437
- height=height, # height of colorbar
438
- loc='right',
439
- bbox_to_anchor=(0, 1, 1, 1), # move 0.9 right, 1.2 up from the subplot
440
- bbox_transform=ref_subplot.transAxes, # important: use subplot coords
441
- borderpad=0
442
- )
443
-
444
- cbar = plt.colorbar(im, cax=cax, orientation='horizontal')
445
- cbar.ax.xaxis.set_ticks_position('top')
446
- cbar.set_label(label, labelpad=5)
447
-
448
-
449
-
450
- def _generate_subplots(self):
451
- """
452
- Generate subplots based on the configuration.
453
- """
454
-
455
- n_aux_subplots = self._n_aux_subplots
456
-
457
- # Fixed heights per subplot type
458
- ref_height = 0.4
459
- aux_height = 0.4
460
- signal_height = 4.0
461
-
462
- # Total number of subplots
463
- n_subplots = 1 + n_aux_subplots + 1
464
-
465
- # Compute total fig height
466
- fig_height = ref_height + n_aux_subplots * aux_height + signal_height
467
-
468
- # Define height ratios
469
- height_ratios = [ref_height] + [aux_height] * n_aux_subplots + [signal_height]
470
-
471
- # Create figure and grid
472
- fig = plt.figure(figsize=(16, fig_height))
473
- gs = gridspec.GridSpec(n_subplots, 1, height_ratios=height_ratios)
474
-
475
- # Add subplots
476
- subplots_list = []
477
- ref_subplot = fig.add_subplot(gs[0, 0])
478
- ref_subplot.axis("off")
479
- subplots_list.append(ref_subplot)
480
-
481
-
482
- for i in range(1, n_subplots):
483
- subplots_list.append(fig.add_subplot(gs[i, 0], sharex=ref_subplot))
484
-
485
- for i in range(n_subplots - 1):
486
- subplots_list[i].tick_params(left=False, bottom=False, labelleft=False, labelbottom=False)
487
-
488
- # Set xlim
489
- if self._xlim is not None: # xlim should be applied on reference subplot, rest all sharex
490
- ref_subplot.set_xlim(self._xlim)
491
-
492
- # Set ylim
493
- if self._ylim is not None: # ylim should be applied on the signal subplot
494
- subplots_list[-1].set_ylim(self._ylim)
495
-
496
- fig.subplots_adjust(hspace=0.01, wspace=0.05)
497
-
498
- return subplots_list, fig
499
-
500
-
501
- def add_events(self, events, c="k", ls="-", lw=1.5, label="Event Label"):
502
- """
503
- Add events to the figure.
504
-
505
- Parameters
506
- ----------
507
- events: np.ndarray
508
- - All the event marker values.
509
- c: str
510
- - Color of the event marker.
511
- - Default: "k"
512
- ls: str
513
- - Line style.
514
- - Default: "-"
515
- lw: float
516
- - Linewidth.
517
- - Default: 1.5
518
- label: str
519
- - Label for the event type.
520
- - This will appear in the legend.
521
- - Default: "Event label"
522
-
523
- Returns
524
- -------
525
- None
526
- """
527
- event_subplot = self._get_active_subplot()
528
- xlim = self._xlim
529
-
530
- for i, event in enumerate(events):
531
- if xlim is not None:
532
- if xlim[0] <= event <= xlim[1]:
533
- if i == 0: # Label should be set only once for all the events
534
- event_subplot.axvline(x=event, color=c, linestyle=ls, linewidth=lw, label=label)
535
- else:
536
- event_subplot.axvline(x=event, color=c, linestyle=ls, linewidth=lw)
537
- else:
538
- if i == 0: # Label should be set only once for all the events
539
- event_subplot.axvline(x=event, color=c, linestyle=ls, linewidth=lw, label=label)
540
- else:
541
- event_subplot.axvline(x=event, color=c, linestyle=ls, linewidth=lw)
542
-
543
- def add_annotation(self, ann):
544
- """
545
- Add annotation to the figure.
546
-
547
- Parameters
548
- ----------
549
- ann : list[tuple[Number, Number, str]] | None
550
- - A list of annotation spans. Each tuple should be (start, end, label).
551
- - Default: None (no annotations).
552
-
553
- Returns
554
- -------
555
- None
556
- """
557
- ann_subplot = self._get_active_subplot()
558
- xlim = self._xlim
559
-
560
- for i, (start, end, tag) in enumerate(ann):
561
-
562
- # We make sure that we only plot annotation that are within the x range of the current view
563
- if xlim is not None:
564
- if start >= xlim[1] or end <= xlim[0]:
565
- continue
566
-
567
- # Clip boundaries to xlim
568
- start = max(start, xlim[0])
569
- end = min(end, xlim[1])
570
-
571
- box_colors = ["gray", "lightgray"] # Alternates color between two
572
- box_color = box_colors[i % 2]
573
-
574
- width = end - start
575
- rect = Rectangle((start, 0), width, 1, facecolor=box_color, edgecolor="black", alpha=0.7)
576
- ann_subplot.add_patch(rect)
577
-
578
- text_obj = ann_subplot.text(
579
- (start + end) / 2, 0.5, tag,
580
- ha='center', va='center',
581
- fontsize=10, color="black", fontweight='bold', zorder=10, clip_on=True
582
- )
583
-
584
- text_obj.set_clip_path(rect)
585
- else:
586
- box_colors = ["gray", "lightgray"] # Alternates color between two
587
- box_color = box_colors[i % 2]
588
-
589
- width = end - start
590
- rect = Rectangle((start, 0), width, 1, facecolor=box_color, edgecolor="black", alpha=0.7)
591
- ann_subplot.add_patch(rect)
592
-
593
- text_obj = ann_subplot.text(
594
- (start + end) / 2, 0.5, tag,
595
- ha='center', va='center',
596
- fontsize=10, color="black", fontweight='bold', zorder=10, clip_on=True
597
- )
598
-
599
- text_obj.set_clip_path(rect)
600
-
601
- def add_matrix(self, M, y=None, x=None, c="gray_r", o="lower", label="Matrix"):
602
- """
603
- Add matrix to the figure.
604
-
605
- Parameters
606
- ----------
607
- M: np.ndarray
608
- - Matrix (2D) array
609
- y: np.ndarray | None
610
- - y axis values.
611
- x: np.ndarray | None (indices will be used)
612
- - x axis values.
613
- - Default: None (indices will be used)
614
- c: str
615
- - cmap for the matrix.
616
- - Default: None
617
- o: str
618
- - origin
619
- - Default: "lower"
620
- label: str
621
- - Label for the plot.
622
- - Legend will use this.
623
- - Default: "Signal"
624
-
625
- Returns
626
- -------
627
- None
628
- """
629
- if x is None: x = np.arange(M.shape[1])
630
- if y is None: y = np.arange(M.shape[0])
631
-
632
- matrix_subplot = self._subplots[-1]
633
- extent = self._calculate_extent(x, y)
634
- im = matrix_subplot.imshow(M, aspect="auto", origin=o, cmap=c, extent=extent)
635
-
636
- self._add_colorbar(im=im, label=label)
637
-
638
- def add_signal(self, y, x=None, c=None, ls="-", lw=1.5, m="o", ms=3, label="Signal"):
639
- """
640
- Add signal on the matrix.
641
-
642
- Parameters
643
- ----------
644
- y: np.ndarray
645
- - Signal y values.
646
- x: np.ndarray | None
647
- - Signal x values.
648
- - Default: None (indices will be used)
649
- c: str
650
- - Color of the line.
651
- - Default: None
652
- ls: str
653
- - Linestyle
654
- - Default: "-"
655
- lw: Number
656
- - Linewidth
657
- - Default: 1
658
- m: str
659
- - Marker
660
- - Default: None
661
- ms: number
662
- - Markersize
663
- - Default: 5
664
- label: str
665
- - Label for the plot.
666
- - Legend will use this.
667
- - Default: "Signal"
668
-
669
- Returns
670
- -------
671
- None
672
- """
673
- if x is None:
674
- x = np.arange(y.size)
675
- matrix_subplot = self._subplots[-1]
676
- matrix_subplot.plot(x, y, color=c, linestyle=ls, linewidth=lw, marker=m, markersize=ms, label=label)
677
-
678
-
679
-
680
- def add_legend(self, ypos=1.1):
681
- """
682
- Add legend to the figure.
683
-
684
- Parameters
685
- ----------
686
- ypos: float
687
- - y position from the top.
688
- - > 1 to push it higher, < 1 to push it lower
689
- - Default: 1.3
690
-
691
- Returns
692
- -------
693
- None
694
- """
695
- subplots: list = self._subplots
696
- fig = self._fig
697
-
698
- all_handles, all_labels = [], []
699
-
700
- for subplot in subplots:
701
- handles, labels = subplot.get_legend_handles_labels()
702
- all_handles.extend(handles)
703
- all_labels.extend(labels)
704
-
705
- # remove duplicates if needed
706
- fig.legend(all_handles, all_labels, loc='upper right', bbox_to_anchor=(0.9, ypos), ncol=2, frameon=True, bbox_transform=fig.transFigure)
707
-
708
- def add_meta_info(self, title=None, ylabel=None, xlabel=None, ts=13, ls=11):
709
- """
710
- Add meta info to the figure.
711
-
712
- Parameters
713
- ----------
714
- title: str
715
- - Title of the figure.
716
- - Default: None
717
- ylabel: str
718
- - y label of the signal.
719
- - It will only appear in the signal subplot.
720
- - Default: None
721
- xlabel: str
722
- - x label of the signal.
723
- - It will only appear in the signal subplot.
724
- - Default: None
725
- ts: Number
726
- - Title size
727
- - Default: 10
728
- ls: Number
729
- - Label size.
730
- - Default: 10
731
-
732
- Returns
733
- -------
734
- None
735
- """
736
- subplots: list = self._subplots
737
- fig = self._fig
738
-
739
- ref_subplot = subplots[0]
740
- signal_subplot = subplots[-1]
741
-
742
- if title is not None:
743
- ref_subplot.set_title(title, pad=10, size=ts)
744
- if ylabel is not None:
745
- signal_subplot.set_ylabel(ylabel, size=ls)
746
- if xlabel is not None:
747
- signal_subplot.set_xlabel(xlabel, size=ls)
748
-
749
- def save(self, path="./figure.png"):
750
- """
751
- Save the figure.
752
-
753
- Parameters
754
- ----------
755
- path: str
756
- - Path to the output file.
757
-
758
- Returns
759
- -------
760
- None
761
- """
762
- fig = self._fig
763
- fig.savefig(path, bbox_inches="tight")
764
-
765
-
766
-
767
- #======== Plot distribution ===========
768
- def plot_dist(*args, ann=None, xlim=None, ylim=None, ylabel=None, xlabel=None, title=None, legend=None, show_hist=True, npoints=200, bins=30):
769
- """
770
- Plot distribution.
771
-
772
- .. code-block:: python
773
-
774
- import modusa as ms
775
- import numpy as np
776
- np.random.seed(42)
777
- data = np.random.normal(loc=1, scale=1, size=1000)
778
- ms.plot_dist(data, data+5, data-10, ann=[(0, 1, "A")], legend=("D1", "D2", "D3"), ylim=(0, 1), xlabel="X", ylabel="Counts", title="Distribution")
779
-
780
- Parameters
781
- ----------
782
- *args: ndarray
783
- - Data arrays for which distribution needs to be plotted.
784
- - Arrays will be flattened.
785
- ann : list[tuple[Number, Number, str] | None
786
- - A list of annotations to mark specific points. Each tuple should be of the form (start, end, label).
787
- - Default: None => No annotation.
788
- events : list[Number] | None
789
- - A list of x-values where vertical lines (event markers) will be drawn.
790
- - Default: None
791
- xlim : tuple[Number, Number] | None
792
- - Limits for the x-axis as (xmin, xmax).
793
- - Default: None
794
- ylim : tuple[Number, Number] | None
795
- - Limits for the y-axis as (ymin, ymax).
796
- - Default: None
797
- xlabel : str | None
798
- - Label for the x-axis.
799
- - - Default: None
800
- ylabel : str | None
801
- - Label for the y-axis.
802
- - Default: None
803
- title : str | None
804
- - Title of the plot.
805
- - Default: None
806
- legend : list[str] | None
807
- - List of legend labels corresponding to each signal if plotting multiple distributions.
808
- - Default: None
809
- show_hist: bool
810
- - Want to show histogram as well.
811
- npoints: int
812
- - Number of points for which gaussian needs to be computed between min and max.
813
- - Higher value means more points are evaluated with the fitted gaussian, thereby higher resolution.
814
- bins: int
815
- - The number of bins for histogram.
816
- - This is used only to plot the histogram.
817
-
818
- Returns
819
- -------
820
- plt.Figure
821
- - Matplotlib figure.
822
- """
823
- from scipy.stats import gaussian_kde
824
-
825
- if isinstance(legend, str):
826
- legend = (legend, )
827
-
828
- if legend is not None:
829
- if len(legend) < len(args):
830
- raise ValueError(f"Legend should be provided for each signal.")
831
-
832
- # Create figure
833
- fig = plt.figure(figsize=(16, 4))
834
- gs = gridspec.GridSpec(2, 1, height_ratios=[0.1, 1])
835
-
836
- colors = plt.get_cmap('tab10').colors
837
-
838
- dist_ax = fig.add_subplot(gs[1, 0])
839
- annotation_ax = fig.add_subplot(gs[0, 0], sharex=dist_ax)
840
-
841
- # Set limits
842
- if xlim is not None:
843
- dist_ax.set_xlim(xlim)
844
-
845
- if ylim is not None:
846
- dist_ax.set_ylim(ylim)
847
-
848
- # Add plot
849
- for i, data in enumerate(args):
850
- # Fit gaussian to the data
851
- kde = gaussian_kde(data)
852
-
853
- # Create points to evaluate KDE
854
- x = np.linspace(np.min(data), np.max(data), npoints)
855
- y = kde(x)
856
-
857
- if legend is not None:
858
- dist_ax.plot(x, y, color=colors[i], label=legend[i])
859
- if show_hist is True:
860
- dist_ax.hist(data, bins=bins, density=True, alpha=0.3, facecolor=colors[i], edgecolor='black', label=legend[i])
861
- else:
862
- dist_ax.plot(x, y, color=colors[i])
863
- if show_hist is True:
864
- dist_ax.hist(data, bins=bins, density=True, alpha=0.3, facecolor=colors[i], edgecolor='black')
865
-
866
- # Add annotations
867
- if ann is not None:
868
- annotation_ax.set_ylim(0, 1) # For consistent layout
869
- # Determine visible x-range
870
- x_view_min = xlim[0] if xlim is not None else np.min(x)
871
- x_view_max = xlim[1] if xlim is not None else np.max(x)
872
- for i, (start, end, tag) in enumerate(ann):
873
- # We make sure that we only plot annotation that are within the x range of the current view
874
- if start >= x_view_max or end <= x_view_min:
875
- continue
876
-
877
- # Clip boundaries to xlim
878
- start = max(start, x_view_min)
879
- end = min(end, x_view_max)
880
-
881
- color = colors[i % len(colors)]
882
- width = end - start
883
- rect = Rectangle((start, 0), width, 1, color=color, alpha=0.7)
884
- annotation_ax.add_patch(rect)
885
-
886
- text_obj = annotation_ax.text((start + end) / 2, 0.5, tag, ha='center', va='center', fontsize=10, color='white', fontweight='bold', zorder=10, clip_on=True)
887
- text_obj.set_clip_path(rect)
888
-
889
- # Add legend
890
- if legend is not None:
891
- handles, labels = dist_ax.get_legend_handles_labels()
892
- fig.legend(handles, labels, loc='upper right', bbox_to_anchor=(0.9, 1.1), ncol=len(legend), frameon=True)
893
-
894
- # Set title, labels
895
- if title is not None:
896
- annotation_ax.set_title(title, pad=10, size=11)
897
- if xlabel is not None:
898
- dist_ax.set_xlabel(xlabel)
899
- if ylabel is not None:
900
- dist_ax.set_ylabel(ylabel)
901
-
902
- # Remove the boundaries and ticks from annotation axis
903
- if ann is not None:
904
- annotation_ax.tick_params(left=False, bottom=False, labelleft=False, labelbottom=False)
905
- else:
906
- annotation_ax.axis("off")
907
-
908
- fig.subplots_adjust(hspace=0.01, wspace=0.05)
909
- plt.close()
910
- return fig
911
-
912
-
913
39
  class Fig:
914
40
  """
915
-
41
+ A utility class that provides easy-to-use API for
42
+ plotting 1D/2D signals along with clean representations
43
+ of annotations, events.
916
44
  """
917
45
 
918
46
  def __init__(self, arrangement="asm", xlim=None):
919
-
47
+
920
48
  self._xlim = xlim
921
49
  self._curr_row_idx = 1 # Starting from 1 because row 0 is reserved for reference subplot
922
50
  self._curr_color_idx = 0 # So that we have different color across all the subplots to avoid legend confusion
@@ -924,7 +52,7 @@ class Fig:
924
52
  # Subplot setup
925
53
  self._fig, self._axs = self._generate_subplots(arrangement) # This will fill in the all the above variables
926
54
 
927
-
55
+
928
56
  def _get_curr_row(self):
929
57
  """
930
58
  Get the active row where you can add
@@ -957,7 +85,7 @@ class Fig:
957
85
 
958
86
  return [x[0] - dx / 2, x[-1] + dx / 2, y[0] - dy / 2, y[-1] + dy / 2]
959
87
 
960
-
88
+
961
89
  def _generate_subplots(self, arrangement):
962
90
  """
963
91
  Generate subplots based on the configuration.
@@ -983,7 +111,7 @@ class Fig:
983
111
  # Calculate height ratios list based on the arrangement
984
112
  for char in arrangement:
985
113
  height_ratios = [height[char] for char in arrangement]
986
-
114
+
987
115
  # Calculate total fig height
988
116
  fig_height = height["r"] + (n_aux_sp * height["a"]) + (n_signal_sp * height["s"]) + (n_matrix_sp * height["m"])
989
117
 
@@ -1005,17 +133,17 @@ class Fig:
1005
133
  elif char == "m":
1006
134
  axs[i, 0].grid(True, linestyle=':', linewidth=0.7, color='gray', alpha=0.7)
1007
135
  axs[i, 0].tick_params(bottom=False, labelbottom=False)
1008
-
136
+
1009
137
  axs[i, 0].sharex(axs[0, 0])
1010
-
138
+
1011
139
  axs[-1, 0].tick_params(bottom=True, labelbottom=True)
1012
140
 
1013
141
  # xlim should be applied on reference subplot, rest all subplots will automatically adjust
1014
142
  if xlim is not None:
1015
143
  axs[0, 0].set_xlim(xlim)
1016
-
144
+
1017
145
  fig.subplots_adjust(hspace=0.2, wspace=0.05)
1018
-
146
+
1019
147
  return fig, axs
1020
148
 
1021
149
  def add_signal(self, y, x=None, c=None, ls=None, lw=None, m=None, ms=3, label=None, ylabel=None, ylim=None, ax=None):
@@ -1068,7 +196,7 @@ class Fig:
1068
196
  if x is None: x = np.arange(y.size)
1069
197
 
1070
198
  if c is None: c = self._get_new_color()
1071
-
199
+
1072
200
  curr_row[0].plot(x, y, color=c, linestyle=ls, linewidth=lw, marker=m, markersize=ms, label=label)
1073
201
 
1074
202
  if ylabel is not None: curr_row[0].set_ylabel(ylabel)
@@ -1117,7 +245,7 @@ class Fig:
1117
245
  """
1118
246
  if x is None: x = np.arange(M.shape[1])
1119
247
  if y is None: y = np.arange(M.shape[0])
1120
-
248
+
1121
249
  curr_row = self._get_curr_row() if ax is None else self._axs[ax]
1122
250
 
1123
251
  extent = self._calculate_extent(x, y)
@@ -1131,8 +259,8 @@ class Fig:
1131
259
  cbar = plt.colorbar(im, cax=curr_row[1])
1132
260
  if label is not None:
1133
261
  cbar.set_label(label, labelpad=5)
1134
-
1135
-
262
+
263
+
1136
264
  def add_events(self, events, c=None, ls=None, lw=None, label=None, ax=None):
1137
265
  """
1138
266
  Add events to the figure.
@@ -1164,9 +292,9 @@ class Fig:
1164
292
  """
1165
293
 
1166
294
  curr_row = self._get_curr_row() if ax is None else self._axs[ax]
1167
-
295
+
1168
296
  if c is None: c = self._get_new_color()
1169
-
297
+
1170
298
  xlim = self._xlim
1171
299
 
1172
300
  for i, event in enumerate(events):
@@ -1203,7 +331,7 @@ class Fig:
1203
331
  None
1204
332
  """
1205
333
  curr_row = self._get_curr_row() if ax is None else self._axs[ax]
1206
-
334
+
1207
335
  xlim = self._xlim
1208
336
 
1209
337
  for i, (start, end, tag) in enumerate(ann):
@@ -1250,7 +378,7 @@ class Fig:
1250
378
  curr_row[0].set_ylabel(label, rotation=0, ha="center", va="center")
1251
379
  curr_row[0].yaxis.set_label_position("right")
1252
380
  curr_row[0].yaxis.set_label_coords(1.05, 0.75)
1253
-
381
+
1254
382
  def add_legend(self, ypos=1.0):
1255
383
  """
1256
384
  Add legend to the figure.
@@ -1297,8 +425,8 @@ class Fig:
1297
425
 
1298
426
  if title is not None:
1299
427
  ref_ax.set_title(title, pad=10, size=s)
1300
-
1301
-
428
+
429
+
1302
430
  def add_xlabel(self, xlabel=None, s=None):
1303
431
  """
1304
432
  Add shared x-label to the figure.
@@ -1316,7 +444,7 @@ class Fig:
1316
444
  ref_ax = axs[-1, 0] # X-label is added to the last subplot
1317
445
  if xlabel is not None:
1318
446
  ref_ax.set_xlabel(xlabel, size=s)
1319
-
447
+
1320
448
  def add_xticks():
1321
449
  raise NotImplementedError("Please raise a github issue `https://github.com/meluron-toolbox/modusa/issues`")
1322
450
 
@@ -1335,6 +463,155 @@ class Fig:
1335
463
  """
1336
464
  fig = self._fig
1337
465
  fig.savefig(path, bbox_inches="tight")
466
+
467
+
468
+
469
+ #======== Plot distribution ===========
470
+ def plot_dist(*args, ann=None, xlim=None, ylim=None, ylabel=None, xlabel=None, title=None, legend=None, show_hist=True, npoints=200, bins=30):
471
+ """
472
+ Plot distribution.
473
+
474
+ .. code-block:: python
475
+
476
+ import modusa as ms
477
+ import numpy as np
478
+ np.random.seed(42)
479
+ data = np.random.normal(loc=1, scale=1, size=1000)
480
+ ms.plot_dist(data, data+5, data-10, ann=[(0, 1, "A")], legend=("D1", "D2", "D3"), ylim=(0, 1), xlabel="X", ylabel="Counts", title="Distribution")
481
+
482
+ Parameters
483
+ ----------
484
+ *args: ndarray
485
+ - Data arrays for which distribution needs to be plotted.
486
+ - Arrays will be flattened.
487
+ ann : list[tuple[Number, Number, str] | None
488
+ - A list of annotations to mark specific points. Each tuple should be of the form (start, end, label).
489
+ - Default: None => No annotation.
490
+ events : list[Number] | None
491
+ - A list of x-values where vertical lines (event markers) will be drawn.
492
+ - Default: None
493
+ xlim : tuple[Number, Number] | None
494
+ - Limits for the x-axis as (xmin, xmax).
495
+ - Default: None
496
+ ylim : tuple[Number, Number] | None
497
+ - Limits for the y-axis as (ymin, ymax).
498
+ - Default: None
499
+ xlabel : str | None
500
+ - Label for the x-axis.
501
+ - - Default: None
502
+ ylabel : str | None
503
+ - Label for the y-axis.
504
+ - Default: None
505
+ title : str | None
506
+ - Title of the plot.
507
+ - Default: None
508
+ legend : list[str] | None
509
+ - List of legend labels corresponding to each signal if plotting multiple distributions.
510
+ - Default: None
511
+ show_hist: bool
512
+ - Want to show histogram as well.
513
+ npoints: int
514
+ - Number of points for which gaussian needs to be computed between min and max.
515
+ - Higher value means more points are evaluated with the fitted gaussian, thereby higher resolution.
516
+ bins: int
517
+ - The number of bins for histogram.
518
+ - This is used only to plot the histogram.
519
+
520
+ Returns
521
+ -------
522
+ plt.Figure
523
+ - Matplotlib figure.
524
+ """
525
+ from scipy.stats import gaussian_kde
526
+
527
+ if isinstance(legend, str):
528
+ legend = (legend, )
529
+
530
+ if legend is not None:
531
+ if len(legend) < len(args):
532
+ raise ValueError(f"Legend should be provided for each signal.")
533
+
534
+ # Create figure
535
+ fig = plt.figure(figsize=(16, 4))
536
+ gs = gridspec.GridSpec(2, 1, height_ratios=[0.1, 1])
537
+
538
+ colors = plt.get_cmap('tab10').colors
539
+
540
+ dist_ax = fig.add_subplot(gs[1, 0])
541
+ annotation_ax = fig.add_subplot(gs[0, 0], sharex=dist_ax)
542
+
543
+ # Set limits
544
+ if xlim is not None:
545
+ dist_ax.set_xlim(xlim)
546
+
547
+ if ylim is not None:
548
+ dist_ax.set_ylim(ylim)
549
+
550
+ # Add plot
551
+ for i, data in enumerate(args):
552
+ # Fit gaussian to the data
553
+ kde = gaussian_kde(data)
554
+
555
+ # Create points to evaluate KDE
556
+ x = np.linspace(np.min(data), np.max(data), npoints)
557
+ y = kde(x)
558
+
559
+ if legend is not None:
560
+ dist_ax.plot(x, y, color=colors[i], label=legend[i])
561
+ if show_hist is True:
562
+ dist_ax.hist(data, bins=bins, density=True, alpha=0.3, facecolor=colors[i], edgecolor='black', label=legend[i])
563
+ else:
564
+ dist_ax.plot(x, y, color=colors[i])
565
+ if show_hist is True:
566
+ dist_ax.hist(data, bins=bins, density=True, alpha=0.3, facecolor=colors[i], edgecolor='black')
567
+
568
+ # Add annotations
569
+ if ann is not None:
570
+ annotation_ax.set_ylim(0, 1) # For consistent layout
571
+ # Determine visible x-range
572
+ x_view_min = xlim[0] if xlim is not None else np.min(x)
573
+ x_view_max = xlim[1] if xlim is not None else np.max(x)
574
+ for i, (start, end, tag) in enumerate(ann):
575
+ # We make sure that we only plot annotation that are within the x range of the current view
576
+ if start >= x_view_max or end <= x_view_min:
577
+ continue
578
+
579
+ # Clip boundaries to xlim
580
+ start = max(start, x_view_min)
581
+ end = min(end, x_view_max)
582
+
583
+ color = colors[i % len(colors)]
584
+ width = end - start
585
+ rect = Rectangle((start, 0), width, 1, color=color, alpha=0.7)
586
+ annotation_ax.add_patch(rect)
587
+
588
+ text_obj = annotation_ax.text((start + end) / 2, 0.5, tag, ha='center', va='center', fontsize=10, color='white', fontweight='bold', zorder=10, clip_on=True)
589
+ text_obj.set_clip_path(rect)
590
+
591
+ # Add legend
592
+ if legend is not None:
593
+ handles, labels = dist_ax.get_legend_handles_labels()
594
+ fig.legend(handles, labels, loc='upper right', bbox_to_anchor=(0.9, 1.1), ncol=len(legend), frameon=True)
595
+
596
+ # Set title, labels
597
+ if title is not None:
598
+ annotation_ax.set_title(title, pad=10, size=11)
599
+ if xlabel is not None:
600
+ dist_ax.set_xlabel(xlabel)
601
+ if ylabel is not None:
602
+ dist_ax.set_ylabel(ylabel)
603
+
604
+ # Remove the boundaries and ticks from annotation axis
605
+ if ann is not None:
606
+ annotation_ax.tick_params(left=False, bottom=False, labelleft=False, labelbottom=False)
607
+ else:
608
+ annotation_ax.axis("off")
609
+
610
+ fig.subplots_adjust(hspace=0.01, wspace=0.05)
611
+ plt.close()
612
+ return fig
613
+
614
+
1338
615
 
1339
616
 
1340
617
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modusa
3
- Version: 0.3.52
3
+ Version: 0.3.53
4
4
  Summary: A modular signal analysis python library.
5
5
  Author-Email: Ankit Anand <ankit0.anand0@gmail.com>
6
6
  License: MIT
@@ -1,9 +1,9 @@
1
- modusa-0.3.52.dist-info/METADATA,sha256=8HFh6KCZ9E82bUa9vvfoW3gmGauYsciPJcgmW8L1jCg,1436
2
- modusa-0.3.52.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
3
- modusa-0.3.52.dist-info/entry_points.txt,sha256=fmKpleVXj6CdaBVL14WoEy6xx7JQCs85jvzwTi3lePM,73
4
- modusa-0.3.52.dist-info/licenses/LICENSE.md,sha256=JTaXAjx5awk76VArKCx5dUW8vmLEWsL_ZlR7-umaHbA,1078
1
+ modusa-0.3.53.dist-info/METADATA,sha256=hiJs3x9W5XEj9aBYaje0gwt5imC6tnc54MKBd221MFo,1436
2
+ modusa-0.3.53.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
3
+ modusa-0.3.53.dist-info/entry_points.txt,sha256=fmKpleVXj6CdaBVL14WoEy6xx7JQCs85jvzwTi3lePM,73
4
+ modusa-0.3.53.dist-info/licenses/LICENSE.md,sha256=JTaXAjx5awk76VArKCx5dUW8vmLEWsL_ZlR7-umaHbA,1078
5
5
  modusa/.DS_Store,sha256=_gm6qJREwfMi8dE7n5S89_RG46u5t3xHyD-smNhtNoM,6148
6
- modusa/__init__.py,sha256=_Fk3GxsI0GT-uhsZaHgs6p5YcIkqPGb3s78_dzFy1lE,291
6
+ modusa/__init__.py,sha256=RMIKAZZ27w1oh2UFMJvZQtQ-SluIQsac_3mSK_LaM30,277
7
7
  modusa/config.py,sha256=bTqK4t00FZqERVITrxW_q284aDDJAa9aMSfFknfR-oU,280
8
8
  modusa/decorators.py,sha256=8zeNX_wE37O6Vp0ysR4-WCZaEL8mq8dyCF_I5DHOzks,5905
9
9
  modusa/devtools/generate_docs_source.py,sha256=UDflHsk-Yh9-3YJTVBzKL32y8hcxiRgAlFEBTMiDqwM,3301
@@ -41,7 +41,7 @@ modusa/models/t_ax.py,sha256=ZUhvZPUW1TkdZYuUd6Ucm-vsv0JqtZ9yEe3ab67Ma6w,8022
41
41
  modusa/models/tds.py,sha256=FAGfibjyyE_lkEuQp-vSCuqQnopOjmy_IXqUjRlg9kc,11677
42
42
  modusa/plugins/__init__.py,sha256=r1Bf5mnrVKRIwxboutY1iGzDy4EPQhqpk1kSW7iJj_Q,54
43
43
  modusa/plugins/base.py,sha256=Bh_1Bja7fOymFsCgwhXDbV6ys3D8muNrPwrfDrG_G_A,2382
44
- modusa/tools/__init__.py,sha256=s3Yfr4FbfZCXwfyy4uPHDfZ4lqf8XZqgH5KlKUVwQpM,373
44
+ modusa/tools/__init__.py,sha256=TfIlnbSRVEk8C9YY6H5y0qiaKttFV_X3m6nI2qPYUH0,295
45
45
  modusa/tools/_plotter_old.py,sha256=KGow7mihA2H1WNq7s5bpivhCgGo2qVIeDaO6iabpsrg,19294
46
46
  modusa/tools/ann_loader.py,sha256=RePlwD4qG6gGrD4mOJ3RDR9q_gUscCY90_R9lgFU9lM,780
47
47
  modusa/tools/audio_converter.py,sha256=415qBoPm2sBIuBSI7m1XBKm0AbmVmPydIPPr-uO8D3c,1778
@@ -50,7 +50,7 @@ modusa/tools/audio_player.py,sha256=GP04TWW4jBwQBjANkfR_cJtEy7cIhvbu8RTwnf9hD6E,
50
50
  modusa/tools/audio_recorder.py,sha256=d2fVt0Sd2tlBdb2WlUs60K4N23zuxM3KUpQqX0ifPi8,2769
51
51
  modusa/tools/base.py,sha256=C0ESJ0mIfjjRlAkRbSetNtMoOfS6IrHBjexRp3l_Mh4,1293
52
52
  modusa/tools/math_ops.py,sha256=ZZ7U4DgqT7cOeE7_Lzi_Qq-48WYfwR9_osbZwTmE9eg,8690
53
- modusa/tools/plotter.py,sha256=a9C8yh7RoX0XyubflYflKupN5EllvCwl7ZYYL6ENwWM,35040
53
+ modusa/tools/plotter.py,sha256=sMJ0PfiRwfw_dUVTLtH1Oed4lOiwR06foAlDljhh3QA,16911
54
54
  modusa/tools/youtube_downloader.py,sha256=hB_X8-7nOHXOlxg6vv3wyhBLoAsWyomrULP6_uCQL7s,1698
55
55
  modusa/utils/.DS_Store,sha256=nLXMwF7QJNuglLI_Gk74F7vl5Dyus2Wd74Mgowijmdo,6148
56
56
  modusa/utils/__init__.py,sha256=1oLL20yLB1GL9IbFiZD8OReDqiCpFr-yetIR6x1cNkI,23
@@ -59,4 +59,4 @@ modusa/utils/excp.py,sha256=L9vhaGjKpv9viJYdmC9n5ndmk2GVbUBuFyZyhAQZmWY,906
59
59
  modusa/utils/logger.py,sha256=K0rsnObeNKCxlNeSnVnJeRhgfmob6riB2uyU7h3dDmA,571
60
60
  modusa/utils/np_func_cat.py,sha256=TyIFgRc6bARRMDnZxlVURO5Z0I-GWhxRONYyIv-Vwxs,1007
61
61
  modusa/utils/plot.py,sha256=s_vNdxvKfwxEngvJPgrF1PcmxZNnNaaXPViHWjyjJ-c,5335
62
- modusa-0.3.52.dist-info/RECORD,,
62
+ modusa-0.3.53.dist-info/RECORD,,