From 038aa24c314fe1e6ca7f7aed665aac28b0173607 Mon Sep 17 00:00:00 2001 From: Bo Date: Wed, 17 Dec 2025 13:36:31 -0800 Subject: [PATCH 1/3] Update spectrum_display_panel.rs A few updates to the ms2 plot panel: (1) added fragment labels; (2) updated color setting. --- .../src/ui/panels/spectrum_display_panel.rs | 66 ++++++++++++------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/rust/timsquery_viewer/src/ui/panels/spectrum_display_panel.rs b/rust/timsquery_viewer/src/ui/panels/spectrum_display_panel.rs index 2d852e1..acd7d21 100644 --- a/rust/timsquery_viewer/src/ui/panels/spectrum_display_panel.rs +++ b/rust/timsquery_viewer/src/ui/panels/spectrum_display_panel.rs @@ -1,8 +1,13 @@ -use eframe::egui; +use eframe::egui::{ + self, + Color32, +}; use egui_plot::{ - Bar, - BarChart, + Line, Plot, + PlotPoint, + PlotPoints, + Text, }; use crate::ui::{ @@ -17,6 +22,16 @@ impl SpectrumPanel { pub fn new() -> Self { Self } + + /// Get color based on fragment label prefix + fn get_fragment_color(label: &str) -> Color32 { + match label.chars().next() { + Some('b') | Some('B') => Color32::from_rgb(100, 149, 237), // Blue (Cornflower) + Some('y') | Some('Y') => Color32::from_rgb(220, 20, 60), // Red (Crimson) + Some('p') | Some('P') => Color32::from_rgb(255, 200, 0), // Yellow + _ => Color32::from_rgb(50, 205, 50), // Green (Lime) + } + } } impl Panel for SpectrumPanel { @@ -30,31 +45,34 @@ impl Panel for SpectrumPanel { .show_axes([true, true]) .allow_zoom(true) .allow_drag(true) + .x_axis_label("m/z") + .y_axis_label("Intensity") + .include_y(0.0) .show(ui, |plot_ui| { - let bounds = plot_ui.plot_bounds(); - let min_x = bounds.min()[0]; - let max_x = bounds.max()[0]; - let x_range = (max_x - min_x).max(0.1); - let screen_width = plot_ui.response().rect.width().max(1.0); - let px_per_mz = screen_width as f64 / x_range; + // Calculate label offset based on max intensity + let max_intensity = spec.intensities.iter().cloned().fold(0.0f32, f32::max); + let label_offset = (max_intensity * 0.03) as f64; // 3% of max intensity - // Dynamic bar width: at least 3 pixels wide on screen, - // but never thinner than 0.5 Th (to preserve isotope resolution when zoomed in) - let bar_width = (3.0 / px_per_mz).max(0.5); + // Draw each peak as a vertical line from 0 to intensity + for (idx, (&mz, &intensity)) in + spec.mz_values.iter().zip(&spec.intensities).enumerate() + { + let label_str = &spec.fragment_labels[idx]; + let color = Self::get_fragment_color(label_str); - let bars: Vec = spec - .mz_values - .iter() - .zip(&spec.intensities) - .enumerate() - .map(|(idx, (&mz, &intensity))| { - Bar::new(mz, intensity as f64) - .width(bar_width) - .name(&spec.fragment_labels[idx]) - }) - .collect(); + let points = PlotPoints::new(vec![[mz, 0.0], [mz, intensity as f64]]); + let line = Line::new(label_str, points).color(color); + plot_ui.line(line); - plot_ui.bar_chart(BarChart::new("MS2 Spectrum", bars)); + // Add label above the peak with offset + let label = Text::new( + label_str, + PlotPoint::new(mz, intensity as f64 + label_offset), + label_str, + ) + .color(color); + plot_ui.text(label); + } }); } else { ui.centered_and_justified(|ui| { From e5d32a16cd9f363f27df4312ccd06e364886be65 Mon Sep 17 00:00:00 2001 From: Bo Date: Wed, 17 Dec 2025 16:31:40 -0800 Subject: [PATCH 2/3] Added mirror plot --- .../src/serde/chromatogram_output.rs | 6 ++ rust/timsquery/src/serde/diann_io.rs | 39 +++++++++ rust/timsquery_viewer/src/app.rs | 20 ++++- .../src/chromatogram_processor.rs | 10 +++ .../src/domain/file_service.rs | 35 +++++++- rust/timsquery_viewer/src/file_loader.rs | 7 ++ rust/timsquery_viewer/src/plot_renderer.rs | 4 + .../src/ui/panels/spectrum_display_panel.rs | 84 +++++++++++++++---- 8 files changed, 188 insertions(+), 17 deletions(-) diff --git a/rust/timsquery/src/serde/chromatogram_output.rs b/rust/timsquery/src/serde/chromatogram_output.rs index ff27f16..274e5b5 100644 --- a/rust/timsquery/src/serde/chromatogram_output.rs +++ b/rust/timsquery/src/serde/chromatogram_output.rs @@ -20,6 +20,11 @@ pub struct ChromatogramOutput { pub fragment_intensities: Vec>, pub fragment_labels: Vec, pub retention_time_results_seconds: Vec, + /// Library (predicted) fragment intensities aligned with `fragment_labels`. + /// Used for mirror plot visualization to compare observed vs predicted spectra. + /// Populated from library sidecar when available. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub library_fragment_intensities: Option>, } impl ChromatogramOutput { @@ -121,6 +126,7 @@ impl ChromatogramOutput { .iter() .map(|&x| x as f32 / 1000.0) .collect(), + library_fragment_intensities: None, }) } } diff --git a/rust/timsquery/src/serde/diann_io.rs b/rust/timsquery/src/serde/diann_io.rs index 20d1628..5b13610 100644 --- a/rust/timsquery/src/serde/diann_io.rs +++ b/rust/timsquery/src/serde/diann_io.rs @@ -4,6 +4,7 @@ use crate::ion::{ IonParsingError, }; use serde::Deserialize; +use serde::Serialize; use std::path::Path; use tinyvec::tiny_vec; use tracing::{ @@ -211,6 +212,44 @@ pub fn read_library_file>( } info!("Parsed {} elution groups", elution_groups.len()); + // Write a small sidecar JSON with fragment label + relative intensity per elution group + #[derive(Serialize)] + struct SidecarEntry { + id: u64, + labels: Vec, + intensities: Vec, + } + + let sidecar_path = file.as_ref().with_extension("library_extras.json"); + let mut sidecar = Vec::with_capacity(elution_groups.len()); + for (eg, extras) in &elution_groups { + let labels: Vec = extras + .relative_intensities + .iter() + .map(|(ia, _)| format!("{}", ia)) + .collect(); + let intensities: Vec = extras + .relative_intensities + .iter() + .map(|(_, v)| *v) + .collect(); + sidecar.push(SidecarEntry { + id: eg.id(), + labels, + intensities, + }); + } + + if let Ok(json) = serde_json::to_string_pretty(&sidecar) { + if let Err(e) = std::fs::write(&sidecar_path, json) { + warn!("Failed to write DIA-NN sidecar {}: {:?}", sidecar_path.display(), e); + } else { + info!("Wrote DIA-NN extras sidecar to {}", sidecar_path.display()); + } + } else { + warn!("Failed to serialize DIA-NN extras sidecar"); + } + Ok(elution_groups) } diff --git a/rust/timsquery_viewer/src/app.rs b/rust/timsquery_viewer/src/app.rs index 74f1299..c941873 100644 --- a/rust/timsquery_viewer/src/app.rs +++ b/rust/timsquery_viewer/src/app.rs @@ -416,7 +416,25 @@ impl ViewerApp { &self.data.tolerance, &self.data.smoothing, ) { - Ok(chrom) => { + Ok(mut chrom) => { + // If we have library extras sidecar, attach library fragment intensities + if let Some(extras_map) = &elution_groups.extras { + if let Some(entries) = extras_map.get(&chrom.id) { + let lib_ints: Vec = chrom + .fragment_labels + .iter() + .map(|lbl| { + entries + .iter() + .find(|(l, _)| l == lbl) + .map(|(_, v)| *v) + .unwrap_or(0.0) + }) + .collect(); + chrom.library_fragment_intensities = Some(lib_ints); + } + } + let chrom_lines = ChromatogramLines::from_chromatogram(&chrom); self.computed.chromatogram_x_bounds = Some(chrom_lines.rt_seconds_range); diff --git a/rust/timsquery_viewer/src/chromatogram_processor.rs b/rust/timsquery_viewer/src/chromatogram_processor.rs index bfb8250..402381b 100644 --- a/rust/timsquery_viewer/src/chromatogram_processor.rs +++ b/rust/timsquery_viewer/src/chromatogram_processor.rs @@ -102,6 +102,7 @@ pub fn extract_ms2_spectrum_from_chromatogram( let mut mz_values = Vec::new(); let mut intensities = Vec::new(); let mut labels = Vec::new(); + let mut lib_intensities: Vec = Vec::new(); for (frag_idx, (&mz, intensity_vec)) in chromatogram .fragment_mzs @@ -119,6 +120,10 @@ pub fn extract_ms2_spectrum_from_chromatogram( .cloned() .unwrap_or_else(|| format!("Fragment {}", frag_idx + 1)), ); + // library_fragment_intensities (if present) are aligned with fragment_labels + if let Some(lib) = &chromatogram.library_fragment_intensities { + lib_intensities.push(*lib.get(frag_idx).unwrap_or(&0.0)); + } } } @@ -133,6 +138,11 @@ pub fn extract_ms2_spectrum_from_chromatogram( intensities, rt_seconds: actual_rt, fragment_labels: labels, + library_fragment_intensities: if lib_intensities.is_empty() { + None + } else { + Some(lib_intensities) + }, }) } diff --git a/rust/timsquery_viewer/src/domain/file_service.rs b/rust/timsquery_viewer/src/domain/file_service.rs index d49e144..269f36c 100644 --- a/rust/timsquery_viewer/src/domain/file_service.rs +++ b/rust/timsquery_viewer/src/domain/file_service.rs @@ -10,6 +10,7 @@ use timsquery::models::tolerance::Tolerance; use timsquery::serde::load_index_caching; use timsrust::MSLevel; use tracing::info; +use std::collections::HashMap; use crate::error::ViewerError; use crate::file_loader::ElutionGroupData; @@ -32,7 +33,39 @@ impl FileService { res.len(), path.display() ); - Ok(ElutionGroupData { inner: res }) + + // Attempt to load library extras sidecar if present. Sidecar shape is Vec<{id, labels, intensities}> + #[derive(serde::Deserialize)] + struct SidecarEntry { + id: u64, + labels: Vec, + intensities: Vec, + } + + let sidecar_path = path.with_extension("library_extras.json"); + let extras_map: Option>> = match std::fs::read_to_string(&sidecar_path) { + Ok(s) => match serde_json::from_str::>(&s) { + Ok(entries) => { + let mut map = HashMap::new(); + for e in entries.into_iter() { + let pairs = e + .labels + .into_iter() + .zip(e.intensities.into_iter()) + .collect::>(); + map.insert(e.id, pairs); + } + Some(map) + } + Err(err) => { + tracing::warn!("Failed to parse sidecar {}: {:?}", sidecar_path.display(), err); + None + } + }, + Err(_) => None, + }; + + Ok(ElutionGroupData { inner: res, extras: extras_map }) } /// Load and index raw timsTOF data diff --git a/rust/timsquery_viewer/src/file_loader.rs b/rust/timsquery_viewer/src/file_loader.rs index c3ae6b0..ddd591c 100644 --- a/rust/timsquery_viewer/src/file_loader.rs +++ b/rust/timsquery_viewer/src/file_loader.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use timscentroid::IndexedTimstofPeaks; use timsquery::models::tolerance::Tolerance; use timsquery::serde::ElutionGroupCollection; +use std::collections::HashMap; use crate::domain::FileService; use crate::error::ViewerError; @@ -73,9 +74,15 @@ impl FileLoader { } } +/// Wrapper around elution group collection with optional library metadata #[derive(Debug)] pub struct ElutionGroupData { + /// The parsed elution groups pub inner: ElutionGroupCollection, + /// Library fragment intensities from library sidecar file. + /// Maps elution group ID → list of (fragment_label, relative_intensity). + /// Used for mirror plot visualization comparing observed vs predicted spectra. + pub extras: Option>>, } impl ElutionGroupData { diff --git a/rust/timsquery_viewer/src/plot_renderer.rs b/rust/timsquery_viewer/src/plot_renderer.rs index 30812f5..a815fc0 100644 --- a/rust/timsquery_viewer/src/plot_renderer.rs +++ b/rust/timsquery_viewer/src/plot_renderer.rs @@ -152,6 +152,10 @@ pub struct MS2Spectrum { pub intensities: Vec, pub rt_seconds: f64, pub fragment_labels: Vec, + /// Library (predicted) fragment intensities aligned with `fragment_labels`. + /// When present, the spectrum panel renders a mirror plot with observed peaks + /// above the x-axis and library peaks below (both max-normalized). + pub library_fragment_intensities: Option>, } /// Renders a chromatogram plot using egui_plot with custom zoom/pan controls diff --git a/rust/timsquery_viewer/src/ui/panels/spectrum_display_panel.rs b/rust/timsquery_viewer/src/ui/panels/spectrum_display_panel.rs index acd7d21..1e7d31e 100644 --- a/rust/timsquery_viewer/src/ui/panels/spectrum_display_panel.rs +++ b/rust/timsquery_viewer/src/ui/panels/spectrum_display_panel.rs @@ -49,29 +49,83 @@ impl Panel for SpectrumPanel { .y_axis_label("Intensity") .include_y(0.0) .show(ui, |plot_ui| { - // Calculate label offset based on max intensity - let max_intensity = spec.intensities.iter().cloned().fold(0.0f32, f32::max); - let label_offset = (max_intensity * 0.03) as f64; // 3% of max intensity + // Check if we have library intensities for mirror mode + let has_library = spec.library_fragment_intensities.is_some(); - // Draw each peak as a vertical line from 0 to intensity + // Max-normalize observed intensities when in mirror mode + let observed_max = spec.intensities.iter().cloned().fold(0.0f32, f32::max); + let normalized_observed: Vec = if has_library && observed_max > 0.0 { + spec.intensities + .iter() + .map(|&v| (v / observed_max) as f64) + .collect() + } else { + spec.intensities.iter().map(|&v| v as f64).collect() + }; + + // Calculate label offset (3% of 1.0 for normalized, or 3% of max for raw) + let obs_max_for_offset = normalized_observed.iter().cloned().fold(0.0f64, f64::max); + let label_offset = obs_max_for_offset * 0.03; + + // Draw library mirror (if present) as negative intensities + // Normalize only if max library intensity > 1, otherwise use raw values + if let Some(lib) = &spec.library_fragment_intensities { + let lib_max = lib.iter().cloned().fold(0.0f32, f32::max); + let should_normalize_lib = lib_max > 1.0; + let lib_display_max = if should_normalize_lib { 1.0 } else { lib_max as f64 }; + let lib_label_offset = lib_display_max * 0.03; + + for (idx, (&mz, &lib_int)) in spec.mz_values.iter().zip(lib).enumerate() { + // Normalize library intensity only if max > 1 + let display_lib = if should_normalize_lib && lib_max > 0.0 { + (lib_int / lib_max) as f64 + } else { + lib_int as f64 + }; + + // Use same fragment-type color as observed spectrum + let label_str = &spec.fragment_labels[idx]; + let color = Self::get_fragment_color(label_str); + let points = PlotPoints::new(vec![[mz, 0.0], [mz, -display_lib]]); + let line = Line::new(format!("lib_{}", idx), points) + .stroke(egui::Stroke::new(2.0, color)); + plot_ui.line(line); + + // Label below mirrored peak for all fragments with intensity > 0 + if display_lib > 0.0 { + let label = Text::new( + label_str, + PlotPoint::new(mz, -display_lib - lib_label_offset), + label_str, + ) + .color(color); + plot_ui.text(label); + } + } + } + + // Draw observed (normalized in mirror mode) peaks on top for (idx, (&mz, &intensity)) in - spec.mz_values.iter().zip(&spec.intensities).enumerate() + spec.mz_values.iter().zip(&normalized_observed).enumerate() { let label_str = &spec.fragment_labels[idx]; let color = Self::get_fragment_color(label_str); - let points = PlotPoints::new(vec![[mz, 0.0], [mz, intensity as f64]]); - let line = Line::new(label_str, points).color(color); + let points = PlotPoints::new(vec![[mz, 0.0], [mz, intensity]]); + let line = Line::new(label_str, points) + .stroke(egui::Stroke::new(2.0, color)); plot_ui.line(line); - // Add label above the peak with offset - let label = Text::new( - label_str, - PlotPoint::new(mz, intensity as f64 + label_offset), - label_str, - ) - .color(color); - plot_ui.text(label); + // Label above peak for all fragments with intensity > 0 + if intensity > 0.0 { + let label = Text::new( + label_str, + PlotPoint::new(mz, intensity + label_offset), + label_str, + ) + .color(color); + plot_ui.text(label); + } } }); } else { From 9a6551d9180e78f88987c7d528e39e294562546d Mon Sep 17 00:00:00 2001 From: Bo Date: Wed, 17 Dec 2025 17:29:42 -0800 Subject: [PATCH 3/3] Added peptide and charge to the precursor table. --- rust/timsquery/src/serde/diann_io.rs | 4 ++ rust/timsquery_viewer/src/app.rs | 4 +- .../src/domain/file_service.rs | 15 ++++-- rust/timsquery_viewer/src/file_loader.rs | 38 ++++++++++++--- .../src/ui/components/precursor_table.rs | 48 +++++++++++++++++-- .../src/ui/panels/precursor_table_panel.rs | 4 +- 6 files changed, 98 insertions(+), 15 deletions(-) diff --git a/rust/timsquery/src/serde/diann_io.rs b/rust/timsquery/src/serde/diann_io.rs index 5b13610..2a3e6b3 100644 --- a/rust/timsquery/src/serde/diann_io.rs +++ b/rust/timsquery/src/serde/diann_io.rs @@ -216,6 +216,8 @@ pub fn read_library_file>( #[derive(Serialize)] struct SidecarEntry { id: u64, + modified_peptide: String, + precursor_charge: u8, labels: Vec, intensities: Vec, } @@ -235,6 +237,8 @@ pub fn read_library_file>( .collect(); sidecar.push(SidecarEntry { id: eg.id(), + modified_peptide: extras.modified_peptide.clone(), + precursor_charge: eg.precursor_charge(), labels, intensities, }); diff --git a/rust/timsquery_viewer/src/app.rs b/rust/timsquery_viewer/src/app.rs index c941873..f5f357e 100644 --- a/rust/timsquery_viewer/src/app.rs +++ b/rust/timsquery_viewer/src/app.rs @@ -419,12 +419,12 @@ impl ViewerApp { Ok(mut chrom) => { // If we have library extras sidecar, attach library fragment intensities if let Some(extras_map) = &elution_groups.extras { - if let Some(entries) = extras_map.get(&chrom.id) { + if let Some(extras) = extras_map.get(&chrom.id) { let lib_ints: Vec = chrom .fragment_labels .iter() .map(|lbl| { - entries + extras.fragment_intensities .iter() .find(|(l, _)| l == lbl) .map(|(_, v)| *v) diff --git a/rust/timsquery_viewer/src/domain/file_service.rs b/rust/timsquery_viewer/src/domain/file_service.rs index 269f36c..2b9eeef 100644 --- a/rust/timsquery_viewer/src/domain/file_service.rs +++ b/rust/timsquery_viewer/src/domain/file_service.rs @@ -34,16 +34,21 @@ impl FileService { path.display() ); - // Attempt to load library extras sidecar if present. Sidecar shape is Vec<{id, labels, intensities}> + // Attempt to load library extras sidecar if present. Sidecar shape is Vec<{id, modified_peptide, precursor_charge, labels, intensities}> #[derive(serde::Deserialize)] struct SidecarEntry { id: u64, + #[serde(default)] + modified_peptide: String, + #[serde(default)] + precursor_charge: u8, labels: Vec, intensities: Vec, } + use crate::file_loader::LibraryExtras; let sidecar_path = path.with_extension("library_extras.json"); - let extras_map: Option>> = match std::fs::read_to_string(&sidecar_path) { + let extras_map: Option> = match std::fs::read_to_string(&sidecar_path) { Ok(s) => match serde_json::from_str::>(&s) { Ok(entries) => { let mut map = HashMap::new(); @@ -53,7 +58,11 @@ impl FileService { .into_iter() .zip(e.intensities.into_iter()) .collect::>(); - map.insert(e.id, pairs); + map.insert(e.id, LibraryExtras { + modified_peptide: e.modified_peptide, + precursor_charge: e.precursor_charge, + fragment_intensities: pairs, + }); } Some(map) } diff --git a/rust/timsquery_viewer/src/file_loader.rs b/rust/timsquery_viewer/src/file_loader.rs index ddd591c..edbeb8c 100644 --- a/rust/timsquery_viewer/src/file_loader.rs +++ b/rust/timsquery_viewer/src/file_loader.rs @@ -74,15 +74,23 @@ impl FileLoader { } } +/// Library extras metadata from sidecar file +#[derive(Debug, Clone)] +pub struct LibraryExtras { + pub modified_peptide: String, + pub precursor_charge: u8, + pub fragment_intensities: Vec<(String, f32)>, +} + /// Wrapper around elution group collection with optional library metadata #[derive(Debug)] pub struct ElutionGroupData { /// The parsed elution groups pub inner: ElutionGroupCollection, - /// Library fragment intensities from library sidecar file. - /// Maps elution group ID → list of (fragment_label, relative_intensity). - /// Used for mirror plot visualization comparing observed vs predicted spectra. - pub extras: Option>>, + /// Library extras from sidecar file. + /// Maps elution group ID → LibraryExtras (peptide, charge, fragment intensities). + /// Used for mirror plot visualization and precursor table display. + pub extras: Option>, } impl ElutionGroupData { @@ -95,21 +103,39 @@ impl ElutionGroupData { self.len() == 0 } - /// Returns indices of all elution groups matching the ID filter. + /// Returns indices of all elution groups matching the filter. /// /// If filter is an empty string, returns ALL indices (no filtering applied). /// This allows seamless toggling between filtered and unfiltered views. + /// Matches against ID and peptide sequence (if extras are available). pub fn matching_indices_for_id_filter(&self, filter: &str) -> Vec { if filter.is_empty() { return (0..self.len()).collect(); } + let filter_lower = filter.to_lowercase(); + let extras_ref = self.extras.as_ref(); + macro_rules! get_ids { ($self:expr) => { $self .iter() .enumerate() - .filter(|(_, eg)| eg.id().to_string().contains(filter)) + .filter(|(_, eg)| { + // Match by ID + if eg.id().to_string().contains(filter) { + return true; + } + // Match by peptide sequence (case-insensitive) + if let Some(extras_map) = extras_ref { + if let Some(ext) = extras_map.get(&eg.id()) { + if ext.modified_peptide.to_lowercase().contains(&filter_lower) { + return true; + } + } + } + false + }) .map(|(idx, _)| idx) .collect() }; diff --git a/rust/timsquery_viewer/src/ui/components/precursor_table.rs b/rust/timsquery_viewer/src/ui/components/precursor_table.rs index 311b678..8c9a86a 100644 --- a/rust/timsquery_viewer/src/ui/components/precursor_table.rs +++ b/rust/timsquery_viewer/src/ui/components/precursor_table.rs @@ -1,27 +1,43 @@ use eframe::egui; +use std::collections::HashMap; use timsquery::KeyLike; use timsquery::models::elution_group::TimsElutionGroup; +use crate::file_loader::LibraryExtras; pub fn render_precursor_table_filtered( ui: &mut egui::Ui, filtered_eg_idxs: &[usize], reference_eg_slice: &[TimsElutionGroup], selected_index: &mut Option, + extras: Option<&HashMap>, ) { use egui_extras::{ Column, TableBuilder, }; - TableBuilder::new(ui) + // Check if extras available to decide whether to show peptide/charge columns + let has_extras = extras.is_some(); + + let mut builder = TableBuilder::new(ui) .striped(true) .resizable(true) .cell_layout(egui::Layout::left_to_right(egui::Align::Center)) .column(Column::auto().at_least(60.0)) // ID .column(Column::auto().at_least(80.0)) // RT - .column(Column::auto().at_least(80.0)) // Mobility + .column(Column::auto().at_least(80.0)); // Mobility + + if has_extras { + builder = builder + .column(Column::auto().at_least(200.0)) // Peptide + .column(Column::auto().at_least(50.0)); // Charge + } + + builder = builder .column(Column::auto().at_least(120.0)) // Precursor m/z - .column(Column::auto().at_least(100.0)) // Fragment count + .column(Column::auto().at_least(100.0)); // Fragment count + + builder .header(20.0, |mut header| { header.col(|ui| { ui.strong("ID"); @@ -32,6 +48,14 @@ pub fn render_precursor_table_filtered( header.col(|ui| { ui.strong("Mobility"); }); + if has_extras { + header.col(|ui| { + ui.strong("Peptide"); + }); + header.col(|ui| { + ui.strong("Z"); + }); + } header.col(|ui| { ui.strong("Precursor m/z"); }); @@ -66,6 +90,24 @@ pub fn render_precursor_table_filtered( ui.label(text); }); + if let Some(extras_map) = extras { + let eg_extras = extras_map.get(&eg.id()); + row.col(|ui| { + if let Some(ext) = eg_extras { + ui.label(&ext.modified_peptide); + } else { + ui.label("-"); + } + }); + row.col(|ui| { + if let Some(ext) = eg_extras { + ui.label(format!("+{}", ext.precursor_charge)); + } else { + ui.label("-"); + } + }); + } + row.col(|ui| { let lims = eg.get_precursor_mz_limits(); let display_text = format!("{:.4} - {:.4}", lims.0, lims.1); diff --git a/rust/timsquery_viewer/src/ui/panels/precursor_table_panel.rs b/rust/timsquery_viewer/src/ui/panels/precursor_table_panel.rs index a692d16..002d990 100644 --- a/rust/timsquery_viewer/src/ui/panels/precursor_table_panel.rs +++ b/rust/timsquery_viewer/src/ui/panels/precursor_table_panel.rs @@ -27,7 +27,7 @@ impl TablePanel { fn render_filter_ui(&self, ui: &mut egui::Ui, ctx: &mut PanelContext) { ui.horizontal(|ui| { - ui.label("Filter by ID:"); + ui.label("Filter:"); ui.text_edit_singleline(&mut ctx.ui.table_filter); if ui.button("Clear").clicked() { ctx.ui.table_filter.clear(); @@ -51,6 +51,7 @@ impl TablePanel { commands: &mut crate::ui::CommandSink, ) { let old_selection = *selected_index; + let extras_ref = elution_groups.extras.as_ref(); egui::ScrollArea::vertical() .auto_shrink([false; 2]) @@ -62,6 +63,7 @@ impl TablePanel { filtered_indices, $egs, selected_index, + extras_ref, ) }; }