masster 0.3.9__py3-none-any.whl → 0.3.11__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.

Potentially problematic release.


This version of masster might be problematic. Click here for more details.

masster/sample/lib.py CHANGED
@@ -421,14 +421,14 @@ def save_lib_mgf(
421
421
  # trim spectrum 2 Da lower and 10 Da higher than precursor m/z
422
422
  spec = spec.mz_trim(mz_min=row["mz"] - 2.0, mz_max=row["mz"] + 10.0)
423
423
 
424
- filename: str = os.path.basename(self.file_path)
424
+ file_basename: str = os.path.basename(self.file_path)
425
425
  mslevel = 1 if spec.ms_level is None else spec.ms_level
426
426
  activation = None
427
427
  energy = None
428
428
  kineticenergy = None
429
429
  if mslevel > 1:
430
- if "CID" in filename.upper() or "ZTS" in filename.upper():
431
- if "EAD" in filename.upper():
430
+ if "CID" in file_basename.upper() or "ZTS" in file_basename.upper():
431
+ if "EAD" in file_basename.upper():
432
432
  activation = "CID-EAD"
433
433
  # search ([0-9]*KE) in filename.upper() using regex
434
434
  match = re.search(r"(\d+)KE", str(filename.upper()))
@@ -440,14 +440,14 @@ def save_lib_mgf(
440
440
  kineticenergy = int(match.group(1))
441
441
  else:
442
442
  activation = "CID"
443
- elif "EAD" in filename.upper():
443
+ elif "EAD" in file_basename.upper():
444
444
  activation = "EAD"
445
445
  # search ([0-9]*KE) in filename.upper() using regex
446
- match = re.search(r"(\d+)KE", filename.upper())
446
+ match = re.search(r"(\d+)KE", file_basename.upper())
447
447
  if match:
448
448
  kineticenergy = int(match.group(1))
449
449
  else:
450
- match = re.search(r"(\d+)EV", filename.upper())
450
+ match = re.search(r"(\d+)EV", file_basename.upper())
451
451
  if match:
452
452
  kineticenergy = int(match.group(1))
453
453
  energy = spec.energy if hasattr(spec, "energy") else None
@@ -515,14 +515,14 @@ def save_lib_mgf(
515
515
  kineticenergy = int(match.group(1))
516
516
  else:
517
517
  activation = "CID"
518
- elif "EAD" in filename.upper():
518
+ elif "EAD" in file_basename.upper():
519
519
  activation = "EAD"
520
- # search ([0-9]*KE) in filename.upper() using regex
521
- match = re.search(r"(\d+)KE", filename.upper())
520
+ # search ([0-9]*KE) in file_basename.upper() using regex
521
+ match = re.search(r"(\d+)KE", file_basename.upper())
522
522
  if match:
523
523
  kineticenergy = int(match.group(1))
524
524
  else:
525
- match = re.search(r"(\d+)EV", filename.upper())
525
+ match = re.search(r"(\d+)EV", file_basename.upper())
526
526
  if match:
527
527
  kineticenergy = int(match.group(1))
528
528
  energy = spec.energy if hasattr(spec, "energy") else None
@@ -541,7 +541,7 @@ def save_lib_mgf(
541
541
  "ACTIVATION": activation,
542
542
  "COLLISIONENERGY": energy,
543
543
  "KINETICENERGY": kineticenergy,
544
- "FILENAME": filename,
544
+ "FILENAME": file_basename,
545
545
  "SCANS": ms1_scan_uid,
546
546
  "FID": row["feature_uid"],
547
547
  "MSLEVEL": 1 if spec.ms_level is None else spec.ms_level,
masster/sample/load.py CHANGED
@@ -85,9 +85,9 @@ def load(
85
85
  filename = self.file_path
86
86
  filename = os.path.abspath(filename)
87
87
  if not os.path.exists(filename):
88
- raise FileNotFoundError(f"Filename not valid. Provide a valid file path.")
88
+ raise FileNotFoundError("Filename not valid. Provide a valid file path.")
89
89
  self.ondisk = ondisk
90
-
90
+
91
91
  # check if file is mzML
92
92
  if filename.lower().endswith(".mzml"):
93
93
  self._load_mzML(filename)
@@ -97,7 +97,7 @@ def load(
97
97
  self._load_raw(filename)
98
98
  elif filename.lower().endswith(".sample5"):
99
99
  self._load_sample5(filename)
100
- #elif filename.lower().endswith(".h5"):
100
+ # elif filename.lower().endswith(".h5"):
101
101
  # self._load_h5(filename)
102
102
  else:
103
103
  raise ValueError("File must be .mzML, .wiff, *.raw, or .sample5")
@@ -137,7 +137,7 @@ def _load_mzML(
137
137
  # check if filename exists
138
138
  if filename is None:
139
139
  raise ValueError("Filename must be provided.")
140
-
140
+
141
141
  filename = os.path.abspath(filename)
142
142
  # check if it exists
143
143
  if not os.path.exists(filename):
@@ -310,7 +310,7 @@ def _load_raw(
310
310
  - Initiates further analysis by invoking analyze_dda().
311
311
  """
312
312
  from alpharaw.thermo import ThermoRawData
313
-
313
+
314
314
  if not filename:
315
315
  raise ValueError("Filename must be provided.")
316
316
 
@@ -318,7 +318,7 @@ def _load_raw(
318
318
  # check if it exists
319
319
  if not os.path.exists(filename):
320
320
  raise FileNotFoundError(f"File {filename} not found.")
321
-
321
+
322
322
  raw_data = ThermoRawData(centroided=False)
323
323
  raw_data.keep_k_peaks_per_spec = self.parameters.max_points_per_spectrum
324
324
  # check thatupdat filename ends with .raw
@@ -470,10 +470,12 @@ def _load_wiff(
470
470
  try:
471
471
  # Use masster's own implementation first
472
472
  from masster.sample.sciex import SciexWiffData as MassterSciexWiffData
473
+
473
474
  SciexWiffDataClass = MassterSciexWiffData
474
475
  except ImportError:
475
476
  # Fallback to alpharaw if masster implementation fails
476
477
  from alpharaw.sciex import SciexWiffData as AlpharawSciexWiffData
478
+
477
479
  SciexWiffDataClass = AlpharawSciexWiffData
478
480
 
479
481
  if not filename:
@@ -483,7 +485,7 @@ def _load_wiff(
483
485
  # check if it exists
484
486
  if not os.path.exists(filename):
485
487
  raise FileNotFoundError(f"File {filename} not found.")
486
-
488
+
487
489
  raw_data = SciexWiffDataClass(centroided=False)
488
490
  raw_data.keep_k_peaks_per_spec = self.parameters.max_points_per_spectrum
489
491
 
@@ -911,7 +913,7 @@ def index_file(self):
911
913
  oms.MzMLFile().load(self.file_source, omsexp)
912
914
  self.file_obj = omsexp
913
915
  elif os.path.exists(self.file_source) and self.file_source.lower().endswith(".sample5"):
914
- # this is an old save, try to see if
916
+ # this is an old save, try to see if
915
917
  if os.path.exists(self.file_source.replace(".sample5", ".wiff")):
916
918
  self.set_source(self.file_source.replace(".sample5", ".wiff"))
917
919
  elif os.path.exists(self.file_source.replace(".sample5", ".raw")):
@@ -919,7 +921,9 @@ def index_file(self):
919
921
  elif os.path.exists(self.file_source.replace(".sample5", ".mzml")):
920
922
  self.set_source(self.file_source.replace(".sample5", ".mzml"))
921
923
  else:
922
- raise FileNotFoundError(f"File {self.file_source} not found. Did the path change? Consider running source().")
924
+ raise FileNotFoundError(
925
+ f"File {self.file_source} not found. Did the path change? Consider running source()."
926
+ )
923
927
  self.index_file()
924
928
  else:
925
929
  raise FileNotFoundError(f"File {self.file_source} not found. Did the path change? Consider running source().")
masster/sample/plot.py CHANGED
@@ -66,49 +66,51 @@ hv.extension("bokeh")
66
66
  def _is_notebook_environment():
67
67
  """
68
68
  Detect if code is running in a notebook environment (Jupyter, JupyterLab, or Marimo).
69
-
69
+
70
70
  Returns:
71
71
  bool: True if running in a notebook, False otherwise
72
72
  """
73
73
  try:
74
74
  # Check for Jupyter/JupyterLab
75
75
  from IPython import get_ipython
76
+
76
77
  if get_ipython() is not None:
77
78
  # Check if we're in a notebook context
78
79
  shell = get_ipython().__class__.__name__
79
- if shell in ['ZMQInteractiveShell', 'Shell']: # Jupyter notebook/lab
80
+ if shell in ["ZMQInteractiveShell", "Shell"]: # Jupyter notebook/lab
80
81
  return True
81
-
82
+
82
83
  # Check for Marimo
83
84
  import sys
84
- if 'marimo' in sys.modules:
85
+
86
+ if "marimo" in sys.modules:
85
87
  return True
86
-
88
+
87
89
  # Additional check for notebook environments
88
- if hasattr(__builtins__, '__IPYTHON__') or hasattr(__builtins__, '_ih'):
90
+ if hasattr(__builtins__, "__IPYTHON__") or hasattr(__builtins__, "_ih"):
89
91
  return True
90
-
92
+
91
93
  except ImportError:
92
94
  pass
93
-
95
+
94
96
  return False
95
97
 
96
98
 
97
99
  def _display_plot(plot_object, layout=None):
98
100
  """
99
101
  Display a plot object in the appropriate way based on the environment.
100
-
102
+
101
103
  Args:
102
104
  plot_object: The plot object to display (holoviews overlay, etc.)
103
105
  layout: Optional panel layout object
104
-
106
+
105
107
  Returns:
106
108
  The layout object if in notebook environment, None otherwise
107
109
  """
108
110
  if _is_notebook_environment():
109
111
  # Display inline in notebook
110
112
  try:
111
- # For Jupyter notebooks, just return the plot object -
113
+ # For Jupyter notebooks, just return the plot object -
112
114
  # holoviews will handle the display automatically
113
115
  return plot_object
114
116
  except Exception:
@@ -395,17 +397,18 @@ def plot_2d(
395
397
  # Configure marker and size behavior based on size parameter
396
398
  use_dynamic_sizing = size.lower() in ["dyn", "dynamic"]
397
399
  use_slider_sizing = size.lower() == "slider"
398
-
400
+
399
401
  def dynamic_sizing_hook(plot, element):
400
402
  """Hook to convert size-based markers to radius-based for dynamic behavior"""
401
403
  try:
402
- if use_dynamic_sizing and hasattr(plot, 'state') and hasattr(plot.state, 'renderers'):
404
+ if use_dynamic_sizing and hasattr(plot, "state") and hasattr(plot.state, "renderers"):
403
405
  from bokeh.models import Circle
406
+
404
407
  for renderer in plot.state.renderers:
405
- if hasattr(renderer, 'glyph'):
408
+ if hasattr(renderer, "glyph"):
406
409
  glyph = renderer.glyph
407
410
  # Check if it's a circle/scatter glyph that we can convert
408
- if hasattr(glyph, 'size') and marker_type == "circle":
411
+ if hasattr(glyph, "size") and marker_type == "circle":
409
412
  # Create a new Circle glyph with radius instead of size
410
413
  new_glyph = Circle(
411
414
  x=glyph.x,
@@ -420,7 +423,7 @@ def plot_2d(
420
423
  except Exception:
421
424
  # Silently fail and use regular sizing if hook doesn't work
422
425
  pass
423
-
426
+
424
427
  if use_dynamic_sizing:
425
428
  # Dynamic sizing: use coordinate-based sizing that scales with zoom
426
429
  marker_type = "circle"
@@ -446,7 +449,7 @@ def plot_2d(
446
449
  size_2 = markersize
447
450
  base_radius = None # Not used in static mode
448
451
  hooks = []
449
-
452
+
450
453
  color_1 = "forestgreen"
451
454
  color_2 = "darkorange"
452
455
  if filename is not None:
@@ -519,6 +522,16 @@ def plot_2d(
519
522
  # find features with ms2_scans not None and iso==0
520
523
  features_df = feats[feats["ms2_scans"].notnull()]
521
524
  # Create feature points with proper sizing method
525
+ feature_hover_1 = HoverTool(
526
+ tooltips=[
527
+ ("rt", "@rt"),
528
+ ("m/z", "@mz{0.0000}"),
529
+ ("feature_uid", "@feature_uid"),
530
+ ("inty", "@inty"),
531
+ ("quality", "@quality"),
532
+ ("rt_delta", "@rt_delta"),
533
+ ],
534
+ )
522
535
  feature_points_1 = hv.Points(
523
536
  features_df,
524
537
  kdims=["rt", "mz"],
@@ -536,11 +549,21 @@ def plot_2d(
536
549
  color=color_1,
537
550
  marker=marker_type,
538
551
  size=size_1,
539
- tools=["hover"],
552
+ tools=[feature_hover_1],
540
553
  hooks=hooks,
541
554
  )
542
555
  # find features without MS2 data
543
556
  features_df = feats[feats["ms2_scans"].isnull()]
557
+ feature_hover_2 = HoverTool(
558
+ tooltips=[
559
+ ("rt", "@rt"),
560
+ ("m/z", "@mz{0.0000}"),
561
+ ("feature_uid", "@feature_uid"),
562
+ ("inty", "@inty"),
563
+ ("quality", "@quality"),
564
+ ("rt_delta", "@rt_delta"),
565
+ ],
566
+ )
544
567
  feature_points_2 = hv.Points(
545
568
  features_df,
546
569
  kdims=["rt", "mz"],
@@ -557,7 +580,7 @@ def plot_2d(
557
580
  color="red",
558
581
  marker=marker_type,
559
582
  size=size_2,
560
- tools=["hover"],
583
+ tools=[feature_hover_2],
561
584
  hooks=hooks,
562
585
  )
563
586
 
@@ -567,6 +590,18 @@ def plot_2d(
567
590
  # Convert to pandas for plotting compatibility
568
591
  if hasattr(features_df, "to_pandas"):
569
592
  features_df = features_df.to_pandas()
593
+ feature_hover_iso = HoverTool(
594
+ tooltips=[
595
+ ("rt", "@rt"),
596
+ ("m/z", "@mz{0.0000}"),
597
+ ("feature_uid", "@feature_uid"),
598
+ ("inty", "@inty"),
599
+ ("quality", "@quality"),
600
+ ("rt_delta", "@rt_delta"),
601
+ ("iso", "@iso"),
602
+ ("iso_of", "@iso_of"),
603
+ ],
604
+ )
570
605
  feature_points_iso = hv.Points(
571
606
  features_df,
572
607
  kdims=["rt", "mz"],
@@ -585,7 +620,7 @@ def plot_2d(
585
620
  color="violet",
586
621
  marker=marker_type,
587
622
  size=size_1,
588
- tools=["hover"],
623
+ tools=[feature_hover_iso],
589
624
  hooks=hooks,
590
625
  )
591
626
  if show_ms2:
@@ -597,6 +632,15 @@ def plot_2d(
597
632
  if len(ms2_orphan) > 0:
598
633
  # pandalize
599
634
  ms2 = ms2_orphan.to_pandas()
635
+ ms2_hover_3 = HoverTool(
636
+ tooltips=[
637
+ ("rt", "@rt"),
638
+ ("prec_mz", "@prec_mz{0.0000}"),
639
+ ("index", "@index"),
640
+ ("inty_tot", "@inty_tot"),
641
+ ("bl", "@bl"),
642
+ ],
643
+ )
600
644
  feature_points_3 = hv.Points(
601
645
  ms2,
602
646
  kdims=["rt", "prec_mz"],
@@ -606,7 +650,7 @@ def plot_2d(
606
650
  color=color_2,
607
651
  marker="x",
608
652
  size=size_2,
609
- tools=["hover"],
653
+ tools=[ms2_hover_3],
610
654
  )
611
655
 
612
656
  ms2_linked = self.scans_df.filter(pl.col("ms_level") == 2).filter(
@@ -615,6 +659,15 @@ def plot_2d(
615
659
  if len(ms2_linked) > 0:
616
660
  # pandalize
617
661
  ms2 = ms2_linked.to_pandas()
662
+ ms2_hover_4 = HoverTool(
663
+ tooltips=[
664
+ ("rt", "@rt"),
665
+ ("prec_mz", "@prec_mz{0.0000}"),
666
+ ("index", "@index"),
667
+ ("inty_tot", "@inty_tot"),
668
+ ("bl", "@bl"),
669
+ ],
670
+ )
618
671
  feature_points_4 = hv.Points(
619
672
  ms2,
620
673
  kdims=["rt", "prec_mz"],
@@ -624,7 +677,7 @@ def plot_2d(
624
677
  color=color_1,
625
678
  marker="x",
626
679
  size=size_2,
627
- tools=["hover"],
680
+ tools=[ms2_hover_4],
628
681
  )
629
682
 
630
683
  overlay = raster
@@ -648,17 +701,17 @@ def plot_2d(
648
701
  # For slider functionality, we need to work with the feature points directly
649
702
  # and not nest DynamicMaps. We'll create the slider using param and panel.
650
703
  import param
651
- import panel as pn
652
-
704
+ import panel as on
705
+
653
706
  class MarkerSizeController(param.Parameterized):
654
707
  size_slider = param.Number(default=markersize, bounds=(1, 20), step=0.5)
655
-
708
+
656
709
  controller = MarkerSizeController()
657
-
710
+
658
711
  # Create a function that generates just the feature overlays with different sizes
659
712
  def create_feature_overlay(size_val):
660
713
  feature_overlay = None
661
-
714
+
662
715
  if feature_points_4 is not None:
663
716
  updated_points_4 = feature_points_4.opts(size=size_val)
664
717
  feature_overlay = updated_points_4 if feature_overlay is None else feature_overlay * updated_points_4
@@ -673,22 +726,24 @@ def plot_2d(
673
726
  feature_overlay = updated_points_2 if feature_overlay is None else feature_overlay * updated_points_2
674
727
  if feature_points_iso is not None:
675
728
  updated_points_iso = feature_points_iso.opts(size=size_val)
676
- feature_overlay = updated_points_iso if feature_overlay is None else feature_overlay * updated_points_iso
677
-
729
+ feature_overlay = (
730
+ updated_points_iso if feature_overlay is None else feature_overlay * updated_points_iso
731
+ )
732
+
678
733
  # Combine with the static raster background
679
734
  if feature_overlay is not None:
680
735
  combined_overlay = raster * feature_overlay
681
736
  else:
682
737
  combined_overlay = raster
683
-
738
+
684
739
  if title is not None:
685
740
  combined_overlay = combined_overlay.opts(title=title)
686
-
741
+
687
742
  return combined_overlay
688
-
743
+
689
744
  # Create a horizontal control widget on top of the plot
690
745
  # Create the slider widget with explicit visibility
691
- size_slider = pn.widgets.FloatSlider(
746
+ size_slider = on.widgets.FloatSlider(
692
747
  name="Marker Size",
693
748
  start=1.0,
694
749
  end=20.0,
@@ -697,19 +752,19 @@ def plot_2d(
697
752
  width=300,
698
753
  height=40,
699
754
  margin=(5, 5),
700
- show_value=True
755
+ show_value=True,
701
756
  )
702
-
757
+
703
758
  # Create the slider widget row with clear styling
704
- slider_widget = pn.Row(
705
- pn.pane.HTML("<b>Marker Size Control:</b>", width=150, height=40, margin=(5, 10)),
759
+ slider_widget = on.Row(
760
+ on.pane.HTML("<b>Marker Size Control:</b>", width=150, height=40, margin=(5, 10)),
706
761
  size_slider,
707
762
  height=60,
708
- margin=10
763
+ margin=10,
709
764
  )
710
-
765
+
711
766
  # Create slider widget
712
- size_slider = pn.widgets.FloatSlider(
767
+ size_slider = on.widgets.FloatSlider(
713
768
  name="Marker Size",
714
769
  start=1.0,
715
770
  end=20.0,
@@ -718,18 +773,18 @@ def plot_2d(
718
773
  width=300,
719
774
  height=40,
720
775
  margin=(5, 5),
721
- show_value=True
776
+ show_value=True,
722
777
  )
723
-
724
- slider_widget = pn.Row(
725
- pn.pane.HTML("<b>Marker Size:</b>", width=100, height=40, margin=(5, 10)),
778
+
779
+ slider_widget = on.Row(
780
+ on.pane.HTML("<b>Marker Size:</b>", width=100, height=40, margin=(5, 10)),
726
781
  size_slider,
727
782
  height=60,
728
- margin=10
783
+ margin=10,
729
784
  )
730
-
785
+
731
786
  # Simple reactive plot - slider mode doesn't use dynamic rasterization
732
- @pn.depends(size_slider.param.value)
787
+ @on.depends(size_slider.param.value)
733
788
  def reactive_plot(size_val):
734
789
  overlay = create_feature_overlay(float(size_val))
735
790
  # Apply static rasterization for slider mode
@@ -739,19 +794,19 @@ def plot_2d(
739
794
  aggregator=ds.count(),
740
795
  width=raster_max_px,
741
796
  height=raster_max_px,
742
- dynamic=False # Static raster for slider mode
797
+ dynamic=False, # Static raster for slider mode
743
798
  ).opts(
744
- cnorm='eq_hist',
745
- tools=['hover'],
799
+ cnorm="eq_hist",
800
+ tools=["hover"],
746
801
  width=width,
747
- height=height
802
+ height=height,
748
803
  )
749
804
  else:
750
805
  return overlay
751
-
806
+
752
807
  # Create layout
753
- layout = pn.Column(slider_widget, reactive_plot, sizing_mode='stretch_width')
754
-
808
+ layout = on.Column(slider_widget, reactive_plot, sizing_mode="stretch_width")
809
+
755
810
  return layout
756
811
  else:
757
812
  # Create a panel layout without slider
@@ -1041,6 +1096,20 @@ def plot_2d_oracle(
1041
1096
  feat_df = feats.copy()
1042
1097
  feat_df = feat_df[feat_df["id_level"] == 2]
1043
1098
 
1099
+ oracle_hover_1 = HoverTool(
1100
+ tooltips=[
1101
+ ("rt", "@rt"),
1102
+ ("m/z", "@mz{0.0000}"),
1103
+ ("feature_uid", "@feature_uid"),
1104
+ ("id_level", "@id_level"),
1105
+ ("id_class", "@id_class"),
1106
+ ("id_label", "@id_label"),
1107
+ ("id_ion", "@id_ion"),
1108
+ ("id_evidence", "@id_evidence"),
1109
+ ("score", "@score"),
1110
+ ("score2", "@score2"),
1111
+ ],
1112
+ )
1044
1113
  feature_points_1 = hv.Points(
1045
1114
  feat_df,
1046
1115
  kdims=["rt", "mz"],
@@ -1062,7 +1131,7 @@ def plot_2d_oracle(
1062
1131
  marker="circle",
1063
1132
  size=markersize,
1064
1133
  fill_alpha=1.0,
1065
- tools=["hover"],
1134
+ tools=[oracle_hover_1],
1066
1135
  )
1067
1136
 
1068
1137
  # feature_points_2 are all features that have ms2_scans not null and id_level ==1
@@ -1070,6 +1139,17 @@ def plot_2d_oracle(
1070
1139
  feat_df = feats.copy()
1071
1140
  feat_df = feat_df[(feat_df["ms2_scans"].notnull()) & (feat_df["id_level"] == 1)]
1072
1141
  if len(feat_df) > 0:
1142
+ oracle_hover_2 = HoverTool(
1143
+ tooltips=[
1144
+ ("rt", "@rt"),
1145
+ ("m/z", "@mz{0.0000}"),
1146
+ ("feature_uid", "@feature_uid"),
1147
+ ("id_level", "@id_level"),
1148
+ ("id_label", "@id_label"),
1149
+ ("id_ion", "@id_ion"),
1150
+ ("id_class", "@id_class"),
1151
+ ],
1152
+ )
1073
1153
  feature_points_2 = hv.Points(
1074
1154
  feat_df,
1075
1155
  kdims=["rt", "mz"],
@@ -1088,7 +1168,7 @@ def plot_2d_oracle(
1088
1168
  marker="circle",
1089
1169
  size=markersize,
1090
1170
  fill_alpha=0.0,
1091
- tools=["hover"],
1171
+ tools=[oracle_hover_2],
1092
1172
  )
1093
1173
 
1094
1174
  # feature_points_3 are all features that have ms2_scans null and id_level ==1
@@ -1096,6 +1176,17 @@ def plot_2d_oracle(
1096
1176
  feat_df = feats.copy()
1097
1177
  feat_df = feat_df[(feat_df["ms2_scans"].isnull()) & (feat_df["id_level"] == 1)]
1098
1178
  if len(feat_df) > 0:
1179
+ oracle_hover_3 = HoverTool(
1180
+ tooltips=[
1181
+ ("rt", "@rt"),
1182
+ ("m/z", "@mz{0.0000}"),
1183
+ ("feature_uid", "@feature_uid"),
1184
+ ("id_level", "@id_level"),
1185
+ ("id_label", "@id_label"),
1186
+ ("id_ion", "@id_ion"),
1187
+ ("id_class", "@id_class"),
1188
+ ],
1189
+ )
1099
1190
  feature_points_3 = hv.Points(
1100
1191
  feat_df,
1101
1192
  kdims=["rt", "mz"],
@@ -1114,7 +1205,7 @@ def plot_2d_oracle(
1114
1205
  marker="diamond",
1115
1206
  size=markersize,
1116
1207
  fill_alpha=0.0,
1117
- tools=["hover"],
1208
+ tools=[oracle_hover_3],
1118
1209
  )
1119
1210
 
1120
1211
  # feature_points_4 are all features that have ms2_scans null and id_level ==0
@@ -1122,6 +1213,14 @@ def plot_2d_oracle(
1122
1213
  feat_df = feats.copy()
1123
1214
  feat_df = feat_df[(feat_df["ms2_scans"].notnull()) & (feat_df["id_level"] < 1)]
1124
1215
  if len(feat_df) > 0:
1216
+ oracle_hover_4 = HoverTool(
1217
+ tooltips=[
1218
+ ("rt", "@rt"),
1219
+ ("m/z", "@mz{0.0000}"),
1220
+ ("feature_uid", "@feature_uid"),
1221
+ ("inty", "@inty"),
1222
+ ],
1223
+ )
1125
1224
  feature_points_4 = hv.Points(
1126
1225
  feat_df,
1127
1226
  kdims=["rt", "mz"],
@@ -1132,14 +1231,22 @@ def plot_2d_oracle(
1132
1231
  marker="circle",
1133
1232
  size=markersize,
1134
1233
  fill_alpha=0.0,
1135
- tools=["hover"],
1234
+ tools=[oracle_hover_4],
1136
1235
  )
1137
1236
 
1138
- # feature_points_4 are all features that have ms2_scans null and id_level ==0
1237
+ # feature_points_5 are all features that have ms2_scans null and id_level ==0
1139
1238
  feature_points_5 = None
1140
1239
  feat_df = feats.copy()
1141
1240
  feat_df = feat_df[(feat_df["ms2_scans"].isnull()) & (feat_df["id_level"] < 1)]
1142
1241
  if len(feat_df) > 0:
1242
+ oracle_hover_5 = HoverTool(
1243
+ tooltips=[
1244
+ ("rt", "@rt"),
1245
+ ("m/z", "@mz{0.0000}"),
1246
+ ("feature_uid", "@feature_uid"),
1247
+ ("inty", "@inty"),
1248
+ ],
1249
+ )
1143
1250
  feature_points_5 = hv.Points(
1144
1251
  feat_df,
1145
1252
  kdims=["rt", "mz"],
@@ -1150,7 +1257,7 @@ def plot_2d_oracle(
1150
1257
  marker="diamond",
1151
1258
  fill_alpha=0.0,
1152
1259
  size=markersize,
1153
- tools=["hover"],
1260
+ tools=[oracle_hover_5],
1154
1261
  )
1155
1262
 
1156
1263
  overlay = raster