Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion crates/app/src/ui/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ pub fn describe(app: &PlotxApp, id: CommandId) -> CommandDescriptor {
),
CommandId::StackData => requires(
app.stackable_selection().is_some(),
"Select at least two compatible datasets before stacking them.",
"Select at least two compatible datasets. Trace collections such as electrophysiology require compatible axes and units.",
),
CommandId::ExtractMassSpectrum => requires(
dataset().is_some_and(|dataset| {
Expand Down Expand Up @@ -668,6 +668,7 @@ pub fn describe(app: &PlotxApp, id: CommandId) -> CommandDescriptor {
&& app.session.ui.processing_template_dialog.is_none()
&& app.session.ui.spectrum_arithmetic_dialog.is_none()
&& app.session.ui.align_spectra_dialog.is_none()
&& app.session.ui.trace_composer.is_none()
&& !app.session.ui.interaction.is_active());
// Activation requirements must not trap an already-active tool after the
// dataset context changes: its command remains available for deactivation.
Expand Down
3 changes: 3 additions & 0 deletions crates/app/src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ mod switcher;
#[cfg(not(target_os = "macos"))]
mod title_bar;
pub(crate) mod tools;
mod trace_composer;
mod windows;

use data_sheet::*;
Expand Down Expand Up @@ -90,6 +91,7 @@ pub fn render(
|| app.session.ui.processing_template_dialog.is_some()
|| app.session.ui.spectrum_arithmetic_dialog.is_some()
|| app.session.ui.align_spectra_dialog.is_some()
|| app.session.ui.trace_composer.is_some()
|| app.session.ui.command_palette.is_some()
|| app.session.ui.save_project_options
|| app.session.ui.project_transition.is_some()
Expand Down Expand Up @@ -170,6 +172,7 @@ pub fn render(
processing_templates::processing_template_window(app, &ctx);
arithmetic::spectrum_arithmetic_window(app, &ctx);
align::align_spectra_window(app, &ctx);
trace_composer::trace_composer_window(app, &ctx);
batch_workflow.show(app, &ctx);

handle_file_drop(app, &ctx);
Expand Down
6 changes: 4 additions & 2 deletions crates/app/src/ui/primary_sidebar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -502,9 +502,11 @@ fn render_dataset_node(
ui.separator();
if ui
.add_enabled(can_stack, egui::Button::new("Stack selected data"))
.on_hover_text("Build a new page stacking the selected datasets")
.on_hover_text(
"Build a new page from compatible datasets. Trace collections such as electrophysiology require compatible axes and units.",
)
.on_disabled_hover_text(
"Select 2 or more datasets of the same type (1D NMR, data tables, or 2D NMR).",
"Select at least two compatible datasets. Trace collections such as electrophysiology require compatible axes and units.",
)
.clicked()
{
Expand Down
125 changes: 125 additions & 0 deletions crates/app/src/ui/trace_composer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
use plotx_core::state::PlotxApp;

pub(crate) fn trace_composer_window(app: &mut PlotxApp, ctx: &egui::Context) {
let Some(mut state) = app.session.ui.trace_composer.take() else {
return;
};
let mut create = false;
let mut cancel = false;
let available = ctx.content_rect().size() - egui::vec2(32.0, 48.0);
let size = egui::vec2(760.0, 540.0).min(available).max(egui::vec2(
300.0_f32.min(available.x),
260.0_f32.min(available.y),
));
let modal = super::modal(ctx, "trace_composer_modal", super::ModalKind::Dialog).show(ctx, |ui| {
ui.set_min_size(size);
ui.set_max_size(size);
ui.heading("Compose trace stack");
ui.separator();
ui.label("Choose the traces to include. You can edit their appearance after creating the plot.");
ui.add_space(4.0);

ui.horizontal_wrapped(|ui| {
ui.label("Search");
ui.add(
egui::TextEdit::singleline(&mut state.query)
.hint_text("Dataset, trace, or parameter")
.desired_width(ui.available_width().min(280.0)),
);
if ui.button("Select all").clicked() {
state.set_all(true);
}
if ui.button("Clear all").clicked() {
state.set_all(false);
}
});
let normalized_query = state.normalized_query();
let visible_count = state.visible_count(&normalized_query);
let filtered = !normalized_query.is_empty() && visible_count < state.items.len();
if filtered {
ui.horizontal(|ui| {
if ui.button("Select filtered").clicked() {
state.set_filtered(&normalized_query, true);
}
if ui.button("Clear filtered").clicked() {
state.set_filtered(&normalized_query, false);
}
});
}

ui.add_space(4.0);
let table_width = ui.available_width();
let trace_width = (table_width * 0.48).max(120.0);
let parameter_width = (table_width - trace_width - 16.0).max(80.0);
egui::ScrollArea::vertical()
.id_salt("trace_composer_items")
.max_height((size.y - 190.0).max(100.0))
.show(ui, |ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
egui::Grid::new("trace_composer_grid")
.num_columns(2)
.striped(true)
.show(ui, |ui| {
ui.strong("Trace");
ui.strong("Parameters");
ui.end_row();
for item in state
.items
.iter_mut()
.filter(|item| item.matches_normalized_query(&normalized_query))
{
let trace_name = format!("{} — {}", item.dataset_name, item.label);
ui.add_sized(
[trace_width, 20.0],
egui::Checkbox::new(&mut item.selected, &trace_name),
)
.on_hover_text(format!("Include {trace_name}"));
let parameters = item
.parameters
.iter()
.map(|(name, value)| format!("{name}: {value}"))
.collect::<Vec<_>>()
.join("; ");
ui.add_sized(
[parameter_width, 20.0],
egui::Label::new(&parameters).truncate(),
)
.on_hover_text(parameters);
ui.end_row();
}
});
});

ui.add_space(4.0);
ui.label(format!(
"{} of {} traces selected; {} shown",
state.selected_count(),
state.items.len(),
visible_count
));
ui.separator();
ui.horizontal(|ui| {
if ui
.add_enabled(
state.selected_count() > 0,
egui::Button::new("Create stack"),
)
.clicked()
{
create = true;
}
if ui.button("Cancel").clicked() {
cancel = true;
}
});
});

if create {
app.session.ui.trace_composer = Some(state);
app.create_trace_composer_stack();
} else if cancel || modal.should_close() {
app.cancel_trace_composer();
} else {
app.session.ui.trace_composer = Some(state);
}
}
1 change: 1 addition & 0 deletions crates/core/src/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ mod table_fit;
mod table_native;
mod table_numeric;
mod tile_drop;
mod trace_composer;
mod trace_provider;
#[cfg(test)]
#[path = "trace_provider_tests.rs"]
Expand Down
70 changes: 40 additions & 30 deletions crates/core/src/state/series_binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,47 +40,57 @@ impl SeriesBinding {
}

pub(crate) fn from_field_all(dataset: &Dataset, field: FieldId) -> Vec<Self> {
let Some(descriptor) = dataset.field_descriptor(field) else {
return Vec::new();
};
let make = |item, index: usize| {
let mut encoding = default_encoding(
&descriptor.capabilities,
&descriptor.metadata,
RequestedChart::Auto,
&PresentationProfile::default(),
&|| field_peak_magnitude(dataset, field),
);
if let plotx_figure::SeriesEncoding::Line(line) = &mut encoding {
line.color = plotx_figure::ColorSource::Explicit(
OVERLAY_PALETTE[index % OVERLAY_PALETTE.len()],
);
}
Self {
id: SeriesId::default(),
source: SeriesSource {
resource: dataset.resource_id(),
field,
item,
},
visible: true,
label: None,
encoding,
}
};
dataset.trace_collection(field).map_or_else(
|| vec![make(None, 0)],
|| {
Self::from_field_item(dataset, field, None, 0)
.into_iter()
.collect()
},
|collection| {
collection
.items
.iter()
.enumerate()
.map(|(index, item)| make(Some(item.id), index))
.filter_map(|(index, item)| {
Self::from_field_item(dataset, field, Some(item.id), index)
})
.collect()
},
)
}

pub(crate) fn from_field_item(
dataset: &Dataset,
field: FieldId,
item: Option<plotx_data::TraceItemId>,
palette_index: usize,
) -> Option<Self> {
let descriptor = dataset.field_descriptor(field)?;
let mut encoding = default_encoding(
&descriptor.capabilities,
&descriptor.metadata,
RequestedChart::Auto,
&PresentationProfile::default(),
&|| field_peak_magnitude(dataset, field),
);
if let plotx_figure::SeriesEncoding::Line(line) = &mut encoding {
line.color = plotx_figure::ColorSource::Explicit(
OVERLAY_PALETTE[palette_index % OVERLAY_PALETTE.len()],
);
}
Some(Self {
id: SeriesId::default(),
source: SeriesSource {
resource: dataset.resource_id(),
field,
item,
},
visible: true,
label: None,
encoding,
})
}

pub fn with_source(source: SeriesSource) -> Self {
Self {
id: SeriesId::default(),
Expand Down
Loading
Loading