use crate::analysis::analysis_modal;
use crate::render::context::RenderContext;
use crate::widgets::{analysis, data_quality};
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use ratatui::widgets::{Clear, Paragraph, Widget};
pub fn render(
area: Rect,
buf: &mut ratatui::buffer::Buffer,
app: &mut crate::App,
ctx: &RenderContext,
) {
render_body(area, buf, app, ctx);
if let Some(picker) = &app.analysis_modal.quality.picker {
render_plan_picker(
picker,
crate::widgets::data_quality::main_pane(area),
buf,
ctx,
);
}
if let Some(form) = &app.analysis_modal.sample_form {
let quality =
app.analysis_modal.selected_tool == Some(analysis_modal::AnalysisTool::DataQuality);
let target = match (form.inline, quality) {
(false, _) => area,
(true, true) => crate::widgets::data_quality::main_pane(area),
(true, false) => crate::widgets::analysis::main_pane(area),
};
let focused =
!form.inline || app.analysis_modal.focus == analysis_modal::AnalysisFocus::Main;
if !form.inline {
crate::app::pointer::record(target, crate::app::pointer::Hit::Modal);
}
crate::widgets::sample_form::render(form, focused, target, buf, ctx);
}
}
fn render_plan_picker(
picker: &analysis_modal::PlanPicker,
area: Rect,
buf: &mut ratatui::buffer::Buffer,
ctx: &RenderContext,
) {
let title = picker.title.as_str();
let items = picker.state.filtered();
let widest = items
.iter()
.map(|(_, item)| crate::glyphs::display_width(item))
.max()
.unwrap_or(0) as u16;
let width = (widest + 8).clamp(28, area.width.saturating_sub(2).max(28));
let height = (items.len() as u16 + 3)
.min(area.height.saturating_sub(2))
.max(5);
let popup = Rect {
x: area.x + area.width.saturating_sub(width) / 2,
y: area.y + area.height.saturating_sub(height) / 3,
width: width.min(area.width),
height,
};
Clear.render(popup, buf);
let inner = crate::widgets::ui::Surface::new(title).render(popup, buf, ctx);
let filter = if picker.state.filter.is_empty() {
Line::from(Span::styled(
"type to narrow",
Style::default().fg(ctx.dimmed),
))
} else {
Line::from(Span::raw(picker.state.filter.clone()))
};
Paragraph::new(filter).render(Rect { height: 1, ..inner }, buf);
crate::widgets::ui::Picker::from_state(&picker.state, true).render(
Rect {
y: inner.y + 1,
height: inner.height.saturating_sub(1),
..inner
},
buf,
ctx,
);
}
fn render_body(
area: Rect,
buf: &mut ratatui::buffer::Buffer,
app: &mut crate::App,
ctx: &RenderContext,
) {
if let Some(ref progress) = app.analysis_modal.computing {
Clear.render(area, buf);
let g = crate::glyphs::get();
let spinner = g.spinner[app.throbber_frame as usize % g.spinner.len()];
let quality =
app.analysis_modal.selected_tool == Some(analysis_modal::AnalysisTool::DataQuality);
let source = if quality {
let rows = progress
.read
.as_ref()
.and_then(crate::analysis::sampling::ReadWatch::rows_seen)
.map(|rows| {
format!(
" {} {} rows so far",
g.middot,
crate::numfmt::group_chrome(rows)
)
})
.unwrap_or_default();
match progress.reads_source {
Some(true) => format!(" Reading the source{rows}"),
Some(false) => " No source read: rows already read".to_string(),
None => progress
.reuse
.as_ref()
.map(|reuse| format!(" {reuse}"))
.unwrap_or_default(),
}
} else {
String::new()
};
let lines = vec![
Line::from(""),
Line::from(vec![
Span::styled(format!(" {spinner} "), Style::default().fg(ctx.accent)),
Span::styled(
progress.phase.clone(),
Style::default().fg(ctx.text_primary),
),
Span::styled(
format!(" {}", crate::numfmt::clock(progress.started.elapsed())),
Style::default().fg(ctx.dimmed),
),
]),
Line::from(Span::styled(
if quality
&& app.analysis_modal.quality.plan.compute
== crate::analysis::data_quality::QualityCompute::Metadata
{
" Reads file metadata only, no values".to_string()
} else if let Some(sampled) =
app.data_table_state.as_ref().and_then(|s| s.sampled())
{
format!(" Reads the view's {}", sampled.label())
} else {
format!(" Reads {}", app.analysis_modal.sample.summary())
},
Style::default().fg(ctx.dimmed),
)),
Line::from(Span::styled(source, Style::default().fg(ctx.dimmed))),
];
Paragraph::new(lines).render(area, buf);
} else if let Some(state) = &app.data_table_state {
if app.analysis_modal.selected_tool == Some(analysis_modal::AnalysisTool::DataQuality) {
let candidates = app.quality_time_candidates();
let note = app.analysis_modal.quality.setup_note.clone();
let plan = &app.analysis_modal.quality.plan;
let unchanged = app.analysis_modal.quality.results.is_some()
&& app.analysis_modal.quality.last_plan.as_ref() == Some(plan);
let relabel_only = !unchanged
&& app.analysis_modal.quality.results.is_some()
&& app
.analysis_modal
.quality
.last_plan
.as_ref()
.is_some_and(|last| last.same_measurement(plan));
let setup = data_quality::SetupView {
time_candidates: &candidates,
reuses_sample: app.quality_kept_serves(plan),
reads_blocks: app.quality_reads_blocks(plan),
segment_count: app.quality_segment_count(plan),
released: app.quality_released(plan),
cached: app.quality_cached(plan),
unchanged,
relabel_only,
edited: app.analysis_modal.setup_edited(),
note: note.as_deref(),
cancelling: app.cancelled_run_shown(),
kept: app.quality_kept_rows(),
copy: app.quality_copy_plan(plan),
copy_released: app.quality_copy_released(),
};
let rows_kept = app.quality_rows_kept().is_some();
let modal = &mut app.analysis_modal;
let config = data_quality::DataQualityWidgetConfig {
checks_expanded: modal.quality.checks_expanded,
state,
plan: if modal.quality.page.is_setup() {
&modal.quality.plan
} else {
modal
.quality
.last_plan
.as_ref()
.unwrap_or(&modal.quality.plan)
},
measured: modal
.quality
.last_plan
.as_ref()
.unwrap_or(&modal.quality.plan),
results: modal.quality.results.as_ref(),
from_cache: modal.quality.from_cache,
metric: modal.quality.metric,
segment_index: modal.quality.segment_index,
interval_index: modal.quality.interval_index,
trend_line: modal.quality.trend_line,
expected_form: modal.quality.expected_form.as_ref(),
segments_by_change: modal.quality.segments_by_change,
page: modal.quality.page,
setup,
plan_field: modal.quality.plan_field,
show_access: modal.quality.show_access,
observation_detail: modal.quality.observation_detail,
findings: &modal.quality.findings,
rows_kept,
evidence_read: modal.quality.evidence_read.as_ref(),
focus: modal.focus,
theme: &app.theme,
ctx,
intent_form: modal.quality.intent_form.as_ref(),
export_form: modal.quality.export.as_ref(),
};
Clear.render(area, buf);
data_quality::render(
config,
&mut modal.quality.table_state,
&mut modal.sidebar_state,
&mut modal.quality.detail_scroll,
area,
buf,
);
return;
}
Clear.render(area, buf);
let results_for_widget = app.analysis_modal.current_results().cloned();
let config = analysis::AnalysisWidgetConfig {
results: results_for_widget.as_ref(),
view: app.analysis_modal.view,
selected_tool: app.analysis_modal.selected_tool,
selected_correlation: app.analysis_modal.selected_correlation,
correlation_method: app.analysis_modal.correlation_method,
focus: app.analysis_modal.focus,
selected_theoretical_distribution: app.analysis_modal.selected_theoretical_distribution,
histogram_scale: app.analysis_modal.histogram_scale,
theme: &app.theme,
table_cell_padding: app.display.table_cell_padding,
number_format: &ctx.number_format,
sample: &app.analysis_modal.sample,
ctx,
};
let mut unused = analysis_modal::ColumnScroll::default();
let column_scroll = match app.analysis_modal.selected_tool {
Some(analysis_modal::AnalysisTool::Describe) => {
&mut app.analysis_modal.describe_columns
}
Some(analysis_modal::AnalysisTool::DistributionAnalysis) => {
&mut app.analysis_modal.distribution_columns
}
Some(analysis_modal::AnalysisTool::CorrelationMatrix) => {
&mut app.analysis_modal.correlation_columns
}
_ => &mut unused,
};
let widget = analysis::AnalysisWidget::new(
config,
&mut app.analysis_modal.table_state,
&mut app.analysis_modal.distribution_table_state,
&mut app.analysis_modal.correlation_table_state,
&mut app.analysis_modal.sidebar_state,
&mut app.analysis_modal.distribution_selector_state,
column_scroll,
);
widget.render(area, buf);
} else {
Clear.render(area, buf);
Paragraph::new("No data available for analysis")
.centered()
.style(ratatui::style::Style::default().fg(ctx.warning))
.render(area, buf);
}
}